diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 0000000000..9ae47b9375
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,2 @@
+ARG BASEIMAGE=mcr.microsoft.com/devcontainers/typescript-node:22@sha256:9791f4aa527774bc370c6bd2f6705ce5a686f1e6f204badd8dfaacce28c631ae
+FROM ${BASEIMAGE}
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000000..b297f9a2d8
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,20 @@
+{
+	"name": "Immich devcontainers",
+	"build": {
+		"dockerfile": "Dockerfile",
+		"args": {
+			"BASEIMAGE": "mcr.microsoft.com/devcontainers/typescript-node:22"
+		}
+	},
+	"customizations": {
+		"vscode": {
+			"extensions": [
+				"svelte.svelte-vscode"
+			]
+		}
+	},
+	"forwardPorts": [],
+	"postCreateCommand": "make install-all",
+	"remoteUser": "node"
+}
+
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index 2c7d170839..0000000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-version: 2
-updates:
-  # Maintain dependencies for GitHub Actions
-  - package-ecosystem: "github-actions"
-    directory: "/"
-    schedule:
-      interval: "daily"
diff --git a/.github/release.yml b/.github/release.yml
index 1d9764194c..c549ead475 100644
--- a/.github/release.yml
+++ b/.github/release.yml
@@ -4,6 +4,10 @@ changelog:
       labels:
         - changelog:breaking-change
 
+    - title: 🫥 Deprecated Changes
+      labels:
+        - changelog:deprecated
+
     - title: 🔒 Security
       labels:
         - changelog:security
diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml
index 5292075cce..e3b2d68435 100644
--- a/.github/workflows/cli.yml
+++ b/.github/workflows/cli.yml
@@ -59,7 +59,7 @@ jobs:
         uses: docker/setup-qemu-action@v3.2.0
 
       - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v3.6.1
+        uses: docker/setup-buildx-action@v3.8.0
 
       - name: Login to GitHub Container Registry
         uses: docker/login-action@v3
@@ -88,7 +88,7 @@ jobs:
             type=raw,value=latest,enable=${{ github.event_name == 'release' }}
 
       - name: Build and push image
-        uses: docker/build-push-action@v6.7.0
+        uses: docker/build-push-action@v6.10.0
         with:
           file: cli/Dockerfile
           platforms: linux/amd64,linux/arm64
diff --git a/.github/workflows/docker-cleanup.yml b/.github/workflows/docker-cleanup.yml
index bd0ec91d14..29b518e0a5 100644
--- a/.github/workflows/docker-cleanup.yml
+++ b/.github/workflows/docker-cleanup.yml
@@ -22,7 +22,7 @@ concurrency:
 jobs:
   cleanup-images:
     name: Cleanup Stale Images Tags for ${{ matrix.primary-name }}
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-24.04
     strategy:
       fail-fast: false
       matrix:
@@ -35,7 +35,7 @@ jobs:
     steps:
       - name: Clean temporary images
         if: "${{ env.TOKEN != '' }}"
-        uses: stumpylog/image-cleaner-action/ephemeral@v0.8.0
+        uses: stumpylog/image-cleaner-action/ephemeral@v0.9.0
         with:
           token: "${{ env.TOKEN }}"
           owner: "immich-app"
@@ -48,7 +48,7 @@ jobs:
 
   cleanup-untagged-images:
     name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-24.04
     needs:
       - cleanup-images
     strategy:
@@ -64,7 +64,7 @@ jobs:
     steps:
       - name: Clean untagged images
         if: "${{ env.TOKEN != '' }}"
-        uses: stumpylog/image-cleaner-action/untagged@v0.8.0
+        uses: stumpylog/image-cleaner-action/untagged@v0.9.0
         with:
           token: "${{ env.TOKEN }}"
           owner: "immich-app"
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index bf393bbcf6..2fac92c4e8 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -33,6 +33,7 @@ jobs:
               - 'server/**'
               - 'openapi/**'
               - 'web/**'
+              - 'i18n/**'
             machine-learning:
               - 'machine-learning/**'
 
@@ -124,7 +125,7 @@ jobs:
         uses: docker/setup-qemu-action@v3.2.0
 
       - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v3.6.1
+        uses: docker/setup-buildx-action@v3.8.0
 
       - name: Login to Docker Hub
         # Only push to Docker Hub when making a release
@@ -173,7 +174,7 @@ jobs:
           fi
 
       - name: Build and push image
-        uses: docker/build-push-action@v6.7.0
+        uses: docker/build-push-action@v6.10.0
         with:
           context: ${{ env.context }}
           file: ${{ env.file }}
@@ -215,7 +216,7 @@ jobs:
         uses: docker/setup-qemu-action@v3.2.0
 
       - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v3.6.1
+        uses: docker/setup-buildx-action@v3.8.0
 
       - name: Login to Docker Hub
         # Only push to Docker Hub when making a release
@@ -264,7 +265,7 @@ jobs:
           fi
 
       - name: Build and push image
-        uses: docker/build-push-action@v6.7.0
+        uses: docker/build-push-action@v6.10.0
         with:
           context: ${{ env.context }}
           file: ${{ env.file }}
diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml
index 387d8e0424..efb84d510e 100644
--- a/.github/workflows/docs-build.yml
+++ b/.github/workflows/docs-build.yml
@@ -27,7 +27,7 @@ jobs:
               - 'docs/**'
       - name: Check if we should force jobs to run
         id: should_force
-        run: echo "should_force=${{ github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
+        run: echo "should_force=${{ github.event_name == 'release' || github.ref_name == 'main' }}" >> "$GITHUB_OUTPUT"
 
   build:
     name: Docs Build
diff --git a/.github/workflows/docs-destroy.yml b/.github/workflows/docs-destroy.yml
index 8070056924..f9e69b135a 100644
--- a/.github/workflows/docs-destroy.yml
+++ b/.github/workflows/docs-destroy.yml
@@ -23,7 +23,7 @@ jobs:
           tg_version: "0.58.12"
           tofu_version: "1.7.1"
           tg_dir: "deployment/modules/cloudflare/docs"
-          tg_command: "destroy"
+          tg_command: "destroy -refresh=false"
 
       - name: Comment
         uses: actions-cool/maintain-one-comment@v3
diff --git a/.github/workflows/fix-format.yml b/.github/workflows/fix-format.yml
new file mode 100644
index 0000000000..0c630c9e4b
--- /dev/null
+++ b/.github/workflows/fix-format.yml
@@ -0,0 +1,52 @@
+name: Fix formatting
+
+on:
+  pull_request:
+    types: [labeled]
+
+jobs:
+  fix-formatting:
+    runs-on: ubuntu-latest
+    if: ${{ github.event.label.name == 'fix:formatting' }}
+    permissions:
+      pull-requests: write
+    steps:
+      - name: Generate a token
+        id: generate-token
+        uses: actions/create-github-app-token@v1
+        with:
+          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
+
+      - name: 'Checkout'
+        uses: actions/checkout@v4
+        with:
+          ref: ${{ github.event.pull_request.head.ref }}
+          token: ${{ steps.generate-token.outputs.token }}
+
+      - name: Setup Node
+        uses: actions/setup-node@v4
+        with:
+          node-version-file: './server/.nvmrc'
+
+      - name: Fix formatting
+        run: make install-all && make format-all
+
+      - name: Commit and push
+        uses: EndBug/add-and-commit@v9
+        with:
+          default_author: github_actions
+          message: 'chore: fix formatting'
+
+      - name: Remove label
+        uses: actions/github-script@v7
+        if: always()
+        with:
+          script: |
+            github.rest.issues.removeLabel({
+              issue_number: context.payload.pull_request.number,
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              name: 'fix:formatting'
+            })
+
diff --git a/.github/workflows/pr-label-validation.yml b/.github/workflows/pr-label-validation.yml
index 1557b3d15c..0abbc01afd 100644
--- a/.github/workflows/pr-label-validation.yml
+++ b/.github/workflows/pr-label-validation.yml
@@ -9,7 +9,7 @@ jobs:
     runs-on: ubuntu-latest
     permissions:
       issues: write
-      pull-requests: read
+      pull-requests: write
     steps:
       - name: Require PR to have a changelog label
         uses: mheap/github-action-required-labels@v5
@@ -19,3 +19,4 @@ jobs:
           use_regex: true
           labels: "changelog:.*"
           add_comment: true
+          message: "Label error. Requires {{errorString}} {{count}} of: {{ provided }}. Found: {{ applied }}. A maintainer will add the required label."
diff --git a/.github/workflows/pr-require-conventional-commit.yml b/.github/workflows/pr-require-conventional-commit.yml
index 4899031249..d4bd44ec43 100644
--- a/.github/workflows/pr-require-conventional-commit.yml
+++ b/.github/workflows/pr-require-conventional-commit.yml
@@ -9,7 +9,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: PR Conventional Commit Validation
-        uses:  ytanikin/PRConventionalCommits@1.2.0
+        uses:  ytanikin/PRConventionalCommits@1.3.0
         with:
           task_types: '["feat","fix","docs","test","ci","refactor","perf","chore","revert"]'
           add_label: 'false'
diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml
index 94567c1cd5..196f8faf59 100644
--- a/.github/workflows/static_analysis.yml
+++ b/.github/workflows/static_analysis.yml
@@ -56,6 +56,10 @@ jobs:
         run: dart format lib/ --set-exit-if-changed
         working-directory: ./mobile
 
+      - name: Run dart custom_lint
+        run: dart run custom_lint
+        working-directory: ./mobile
+
       # Enable after riverpod generator migration is completed
       # - name: Run dart custom lint
       #   run: dart run custom_lint
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 24e3e08623..52e0ba7b07 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -30,6 +30,7 @@ jobs:
           filters: |
             web:
               - 'web/**'
+              - 'i18n/**'
               - 'open-api/typescript-sdk/**'
             server:
               - 'server/**'
@@ -80,7 +81,7 @@ jobs:
         run: npm run check
         if: ${{ !cancelled() }}
 
-      - name: Run unit tests & coverage
+      - name: Run small tests & coverage
         run: npm run test:cov
         if: ${{ !cancelled() }}
 
@@ -243,6 +244,26 @@ jobs:
         run: npm run check
         if: ${{ !cancelled() }}
 
+  medium-tests-server:
+    name: Medium Tests (Server)
+    needs: pre-job
+    if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
+    runs-on: mich
+
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+        with:
+          submodules: 'recursive'
+
+      - name: Production build
+        if: ${{ !cancelled() }}
+        run: docker compose -f e2e/docker-compose.yml build
+
+      - name: Run medium tests
+        if: ${{ !cancelled() }}
+        run: make test-medium
+
   e2e-tests-server-cli:
     name: End-to-End Tests (Server & CLI)
     needs: pre-job
diff --git a/.gitignore b/.gitignore
index 537e048be2..e0544ad8d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,5 @@ mobile/openapi/.openapi-generator/FILES
 open-api/typescript-sdk/build
 mobile/android/fastlane/report.xml
 mobile/ios/fastlane/report.xml
+
+vite.config.js.timestamp-*
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 7a88b7f3e1..ed3da9f667 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -5,8 +5,8 @@
       "type": "node",
       "request": "attach",
       "restart": true,
-      "port": 9230,
-      "name": "Immich Server",
+      "port": 9231,
+      "name": "Immich API Server",
       "remoteRoot": "/usr/src/app",
       "localRoot": "${workspaceFolder}/server"
     },
@@ -14,8 +14,8 @@
       "type": "node",
       "request": "attach",
       "restart": true,
-      "port": 9231,
-      "name": "Immich Microservices",
+      "port": 9230,
+      "name": "Immich Workers",
       "remoteRoot": "/usr/src/app",
       "localRoot": "${workspaceFolder}/server"
     }
diff --git a/.vscode/settings.json b/.vscode/settings.json
index a8661326a0..49dbf3944c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -41,4 +41,4 @@
   "explorer.fileNesting.patterns": {
     "*.ts": "${capture}.spec.ts,${capture}.mock.ts"
   }
-}
+}
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 349a5c5e92..0899d82d24 100644
--- a/Makefile
+++ b/Makefile
@@ -39,7 +39,7 @@ attach-server:
 renovate:
   LOG_LEVEL=debug npx renovate --platform=local --repository-cache=reset
 
-MODULES = e2e server web cli sdk
+MODULES = e2e server web cli sdk docs
 
 audit-%:
 	npm --prefix $(subst sdk,open-api/typescript-sdk,$*) audit fix
@@ -48,11 +48,9 @@ install-%:
 build-cli: build-sdk
 build-web: build-sdk
 build-%: install-%
-	npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run | grep 'build' >/dev/null \
-		&& npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run build || true
+	npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run build
 format-%:
-	npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run | grep 'format:fix' >/dev/null \
-		&& npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run format:fix || true
+	npm --prefix $* run format:fix
 lint-%:
 	npm --prefix $* run lint:fix
 check-%:
@@ -66,15 +64,27 @@ test-e2e:
 	docker compose -f ./e2e/docker-compose.yml build
 	npm --prefix e2e run test
 	npm --prefix e2e run test:web
+test-medium:
+	docker run \
+    --rm \
+    -v ./server/src:/usr/src/app/src \
+    -v ./server/test:/usr/src/app/test \
+    -v ./server/vitest.config.medium.mjs:/usr/src/app/vitest.config.medium.mjs \
+    -v ./server/tsconfig.json:/usr/src/app/tsconfig.json \
+    -e NODE_ENV=development \
+    immich-server:latest \
+    -c "npm ci && npm run test:medium -- --run"
+test-medium-dev:
+	docker exec -it immich_server /bin/sh -c "npm run test:medium"
 
-build-all: $(foreach M,$(MODULES),build-$M) ;
+build-all: $(foreach M,$(filter-out e2e,$(MODULES)),build-$M) ;
 install-all: $(foreach M,$(MODULES),install-$M) ;
-check-all: $(foreach M,$(MODULES),check-$M) ;
-lint-all: $(foreach M,$(MODULES),lint-$M) ;
-format-all: $(foreach M,$(MODULES),format-$M) ;
+check-all: $(foreach M,$(filter-out sdk cli docs,$(MODULES)),check-$M) ;
+lint-all: $(foreach M,$(filter-out sdk docs,$(MODULES)),lint-$M) ;
+format-all: $(foreach M,$(filter-out sdk,$(MODULES)),format-$M) ;
 audit-all:  $(foreach M,$(MODULES),audit-$M) ;
 hygiene-all: lint-all format-all check-all sql audit-all;
-test-all: $(foreach M,$(MODULES),test-$M) ;
+test-all: $(foreach M,$(filter-out sdk docs,$(MODULES)),test-$M) ;
 
 clean:
 	find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
diff --git a/README.md b/README.md
index 44c38e6d14..0c7b1252ab 100644
--- a/README.md
+++ b/README.md
@@ -17,23 +17,24 @@
 <img src="design/immich-screenshots.png" title="Main Screenshot">
 </a>
 <br/>
+
 <p align="center">
-
-<a href="readme_i18n/README_ca_ES.md">Català</a>
-<a href="readme_i18n/README_es_ES.md">Español</a>
-<a href="readme_i18n/README_fr_FR.md">Français</a>
-<a href="readme_i18n/README_it_IT.md">Italiano</a>
-<a href="readme_i18n/README_ja_JP.md">日本語</a>
-<a href="readme_i18n/README_ko_KR.md">한국어</a>
-<a href="readme_i18n/README_de_DE.md">Deutsch</a>
-<a href="readme_i18n/README_nl_NL.md">Nederlands</a>
-<a href="readme_i18n/README_tr_TR.md">Türkçe</a>
-<a href="readme_i18n/README_zh_CN.md">中文</a>
-<a href="readme_i18n/README_ru_RU.md">Русский</a>
-<a href="readme_i18n/README_pt_BR.md">Português Brasileiro</a>
-<a href="readme_i18n/README_sv_SE.md">Svenska</a>
-<a href="readme_i18n/README_ar_JO.md">العربية</a>
-
+  <a href="readme_i18n/README_ca_ES.md">Català</a>
+  <a href="readme_i18n/README_es_ES.md">Español</a>
+  <a href="readme_i18n/README_fr_FR.md">Français</a>
+  <a href="readme_i18n/README_it_IT.md">Italiano</a>
+  <a href="readme_i18n/README_ja_JP.md">日本語</a>
+  <a href="readme_i18n/README_ko_KR.md">한국어</a>
+  <a href="readme_i18n/README_de_DE.md">Deutsch</a>
+  <a href="readme_i18n/README_nl_NL.md">Nederlands</a>
+  <a href="readme_i18n/README_tr_TR.md">Türkçe</a>
+  <a href="readme_i18n/README_zh_CN.md">中文</a>
+  <a href="readme_i18n/README_ru_RU.md">Русский</a>
+  <a href="readme_i18n/README_pt_BR.md">Português Brasileiro</a>
+  <a href="readme_i18n/README_sv_SE.md">Svenska</a>
+  <a href="readme_i18n/README_ar_JO.md">العربية</a>
+  <a href="readme_i18n/README_vi_VN.md">Tiếng Việt</a>
+  <a href="readme_i18n/README_th_TH.md">ภาษาไทย</a>
 </p>
 
 ## Disclaimer
@@ -101,6 +102,8 @@ For the mobile app, you can use `https://demo.immich.app/api` for the `Server En
 | Offline support                              | Yes    | No  |
 | Read-only gallery                            | Yes    | Yes |
 | Stacked Photos                               | Yes    | Yes |
+| Tags                                         | No     | Yes |
+| Folder View                                  | No     | Yes |
 
 ## Translations
 
diff --git a/cli/.nvmrc b/cli/.nvmrc
index 3516580bbb..1d9b7831ba 100644
--- a/cli/.nvmrc
+++ b/cli/.nvmrc
@@ -1 +1 @@
-20.17.0
+22.12.0
diff --git a/cli/Dockerfile b/cli/Dockerfile
index b08aba9d3c..31dd8576e2 100644
--- a/cli/Dockerfile
+++ b/cli/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:20.17.0-alpine3.20@sha256:2d07db07a2df6830718ae2a47db6fedce6745f5bcd174c398f2acdda90a11c03 AS core
+FROM node:22.12.0-alpine3.20@sha256:96cc8323e25c8cc6ddcb8b965e135cfd57846e8003ec0d7bcec16c5fd5f6d39f AS core
 
 WORKDIR /usr/src/open-api/typescript-sdk
 COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
diff --git a/cli/package-lock.json b/cli/package-lock.json
index 3c18729552..af339110d0 100644
--- a/cli/package-lock.json
+++ b/cli/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "@immich/cli",
-  "version": "2.2.19",
+  "version": "2.2.37",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@immich/cli",
-      "version": "2.2.19",
+      "version": "2.2.37",
       "license": "GNU Affero General Public License version 3",
       "dependencies": {
         "fast-glob": "^3.3.2",
@@ -24,17 +24,17 @@
         "@types/cli-progress": "^3.11.0",
         "@types/lodash-es": "^4.17.12",
         "@types/mock-fs": "^4.13.1",
-        "@types/node": "^20.16.5",
-        "@typescript-eslint/eslint-plugin": "^8.0.0",
-        "@typescript-eslint/parser": "^8.0.0",
+        "@types/node": "^22.10.2",
+        "@typescript-eslint/eslint-plugin": "^8.15.0",
+        "@typescript-eslint/parser": "^8.15.0",
         "@vitest/coverage-v8": "^2.0.5",
         "byte-size": "^9.0.0",
         "cli-progress": "^3.12.0",
         "commander": "^12.0.0",
-        "eslint": "^9.0.0",
+        "eslint": "^9.14.0",
         "eslint-config-prettier": "^9.1.0",
         "eslint-plugin-prettier": "^5.1.3",
-        "eslint-plugin-unicorn": "^55.0.0",
+        "eslint-plugin-unicorn": "^56.0.1",
         "globals": "^15.9.0",
         "mock-fs": "^5.2.0",
         "prettier": "^3.2.5",
@@ -43,7 +43,7 @@
         "vite": "^5.0.12",
         "vite-tsconfig-paths": "^5.0.0",
         "vitest": "^2.0.5",
-        "vitest-fetch-mock": "^0.3.0",
+        "vitest-fetch-mock": "^0.4.0",
         "yaml": "^2.3.1"
       },
       "engines": {
@@ -52,14 +52,14 @@
     },
     "../open-api/typescript-sdk": {
       "name": "@immich/sdk",
-      "version": "1.115.0",
+      "version": "1.123.0",
       "dev": true,
       "license": "GNU Affero General Public License version 3",
       "dependencies": {
         "@oazapfts/runtime": "^1.0.2"
       },
       "devDependencies": {
-        "@types/node": "^20.16.5",
+        "@types/node": "^22.10.2",
         "typescript": "^5.3.3"
       }
     },
@@ -173,19 +173,21 @@
       }
     },
     "node_modules/@babel/helper-string-parser": {
-      "version": "7.24.8",
-      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
-      "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
+      "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-validator-identifier": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
-      "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+      "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
       }
@@ -276,12 +278,13 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.25.3",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
-      "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
+      "version": "7.26.2",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz",
+      "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/types": "^7.25.2"
+        "@babel/types": "^7.26.0"
       },
       "bin": {
         "parser": "bin/babel-parser.js"
@@ -291,14 +294,14 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.25.2",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
-      "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
+      "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/helper-string-parser": "^7.24.8",
-        "@babel/helper-validator-identifier": "^7.24.7",
-        "to-fast-properties": "^2.0.0"
+        "@babel/helper-string-parser": "^7.25.9",
+        "@babel/helper-validator-identifier": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -717,9 +720,9 @@
       }
     },
     "node_modules/@eslint-community/regexpp": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz",
-      "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==",
+      "version": "4.12.1",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+      "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -765,10 +768,20 @@
         "node": "*"
       }
     },
+    "node_modules/@eslint/core": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz",
+      "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
     "node_modules/@eslint/eslintrc": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
-      "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
+      "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -794,6 +807,7 @@
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
       "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "balanced-match": "^1.0.0",
         "concat-map": "0.0.1"
@@ -817,6 +831,7 @@
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
       "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "brace-expansion": "^1.1.7"
       },
@@ -825,9 +840,9 @@
       }
     },
     "node_modules/@eslint/js": {
-      "version": "9.9.1",
-      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz",
-      "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==",
+      "version": "9.15.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz",
+      "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -844,6 +859,57 @@
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       }
     },
+    "node_modules/@eslint/plugin-kit": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz",
+      "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "levn": "^0.4.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@humanfs/core": {
+      "version": "0.19.1",
+      "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+      "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanfs/node": {
+      "version": "0.16.6",
+      "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
+      "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@humanfs/core": "^0.19.1",
+        "@humanwhocodes/retry": "^0.3.0"
+      },
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+      "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
     "node_modules/@humanwhocodes/module-importer": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -858,9 +924,9 @@
       }
     },
     "node_modules/@humanwhocodes/retry": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz",
-      "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==",
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz",
+      "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==",
       "dev": true,
       "license": "Apache-2.0",
       "engines": {
@@ -1299,6 +1365,13 @@
       "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
       "dev": true
     },
+    "node_modules/@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/lodash": {
       "version": "4.17.0",
       "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
@@ -1324,13 +1397,13 @@
       }
     },
     "node_modules/@types/node": {
-      "version": "20.16.5",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz",
-      "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==",
+      "version": "22.10.2",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
+      "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "undici-types": "~6.19.2"
+        "undici-types": "~6.20.0"
       }
     },
     "node_modules/@types/normalize-package-data": {
@@ -1340,17 +1413,17 @@
       "dev": true
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz",
-      "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz",
+      "integrity": "sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/regexpp": "^4.10.0",
-        "@typescript-eslint/scope-manager": "8.3.0",
-        "@typescript-eslint/type-utils": "8.3.0",
-        "@typescript-eslint/utils": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0",
+        "@typescript-eslint/scope-manager": "8.15.0",
+        "@typescript-eslint/type-utils": "8.15.0",
+        "@typescript-eslint/utils": "8.15.0",
+        "@typescript-eslint/visitor-keys": "8.15.0",
         "graphemer": "^1.4.0",
         "ignore": "^5.3.1",
         "natural-compare": "^1.4.0",
@@ -1374,16 +1447,16 @@
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz",
-      "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz",
+      "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
-        "@typescript-eslint/scope-manager": "8.3.0",
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/typescript-estree": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0",
+        "@typescript-eslint/scope-manager": "8.15.0",
+        "@typescript-eslint/types": "8.15.0",
+        "@typescript-eslint/typescript-estree": "8.15.0",
+        "@typescript-eslint/visitor-keys": "8.15.0",
         "debug": "^4.3.4"
       },
       "engines": {
@@ -1403,14 +1476,14 @@
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz",
-      "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz",
+      "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0"
+        "@typescript-eslint/types": "8.15.0",
+        "@typescript-eslint/visitor-keys": "8.15.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1421,14 +1494,14 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz",
-      "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz",
+      "integrity": "sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "8.3.0",
-        "@typescript-eslint/utils": "8.3.0",
+        "@typescript-eslint/typescript-estree": "8.15.0",
+        "@typescript-eslint/utils": "8.15.0",
         "debug": "^4.3.4",
         "ts-api-utils": "^1.3.0"
       },
@@ -1439,6 +1512,9 @@
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
       },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0"
+      },
       "peerDependenciesMeta": {
         "typescript": {
           "optional": true
@@ -1446,9 +1522,9 @@
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz",
-      "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz",
+      "integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -1460,14 +1536,14 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz",
-      "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz",
+      "integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0",
+        "@typescript-eslint/types": "8.15.0",
+        "@typescript-eslint/visitor-keys": "8.15.0",
         "debug": "^4.3.4",
         "fast-glob": "^3.3.2",
         "is-glob": "^4.0.3",
@@ -1489,16 +1565,16 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz",
-      "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.15.0.tgz",
+      "integrity": "sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
-        "@typescript-eslint/scope-manager": "8.3.0",
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/typescript-estree": "8.3.0"
+        "@typescript-eslint/scope-manager": "8.15.0",
+        "@typescript-eslint/types": "8.15.0",
+        "@typescript-eslint/typescript-estree": "8.15.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1509,17 +1585,22 @@
       },
       "peerDependencies": {
         "eslint": "^8.57.0 || ^9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz",
-      "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz",
+      "integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.3.0",
-        "eslint-visitor-keys": "^3.4.3"
+        "@typescript-eslint/types": "8.15.0",
+        "eslint-visitor-keys": "^4.2.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1529,22 +1610,36 @@
         "url": "https://opencollective.com/typescript-eslint"
       }
     },
-    "node_modules/@vitest/coverage-v8": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz",
-      "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==",
+    "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
       "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@vitest/coverage-v8": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.5.tgz",
+      "integrity": "sha512-/RoopB7XGW7UEkUndRXF87A9CwkoZAJW01pj8/3pgmDVsjMH2IKy6H1A38po9tmUlwhSyYs0az82rbKd9Yaynw==",
+      "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@ampproject/remapping": "^2.3.0",
         "@bcoe/v8-coverage": "^0.2.3",
-        "debug": "^4.3.5",
+        "debug": "^4.3.7",
         "istanbul-lib-coverage": "^3.2.2",
         "istanbul-lib-report": "^3.0.1",
         "istanbul-lib-source-maps": "^5.0.6",
         "istanbul-reports": "^3.1.7",
-        "magic-string": "^0.30.10",
-        "magicast": "^0.3.4",
-        "std-env": "^3.7.0",
+        "magic-string": "^0.30.12",
+        "magicast": "^0.3.5",
+        "std-env": "^3.8.0",
         "test-exclude": "^7.0.1",
         "tinyrainbow": "^1.2.0"
       },
@@ -1552,29 +1647,64 @@
         "url": "https://opencollective.com/vitest"
       },
       "peerDependencies": {
-        "vitest": "2.0.5"
+        "@vitest/browser": "2.1.5",
+        "vitest": "2.1.5"
+      },
+      "peerDependenciesMeta": {
+        "@vitest/browser": {
+          "optional": true
+        }
       }
     },
     "node_modules/@vitest/expect": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz",
-      "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.5.tgz",
+      "integrity": "sha512-nZSBTW1XIdpZvEJyoP/Sy8fUg0b8od7ZpGDkTUcfJ7wz/VoZAFzFfLyxVxGFhUjJzhYqSbIpfMtl/+k/dpWa3Q==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@vitest/spy": "2.0.5",
-        "@vitest/utils": "2.0.5",
-        "chai": "^5.1.1",
+        "@vitest/spy": "2.1.5",
+        "@vitest/utils": "2.1.5",
+        "chai": "^5.1.2",
         "tinyrainbow": "^1.2.0"
       },
       "funding": {
         "url": "https://opencollective.com/vitest"
       }
     },
-    "node_modules/@vitest/pretty-format": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz",
-      "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==",
+    "node_modules/@vitest/mocker": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.5.tgz",
+      "integrity": "sha512-XYW6l3UuBmitWqSUXTNXcVBUCRytDogBsWuNXQijc00dtnU/9OqpXWp4OJroVrad/gLIomAq9aW8yWDBtMthhQ==",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/spy": "2.1.5",
+        "estree-walker": "^3.0.3",
+        "magic-string": "^0.30.12"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "msw": "^2.4.9",
+        "vite": "^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "msw": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vitest/pretty-format": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.5.tgz",
+      "integrity": "sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw==",
+      "dev": true,
+      "license": "MIT",
       "dependencies": {
         "tinyrainbow": "^1.2.0"
       },
@@ -1583,12 +1713,13 @@
       }
     },
     "node_modules/@vitest/runner": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz",
-      "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.5.tgz",
+      "integrity": "sha512-pKHKy3uaUdh7X6p1pxOkgkVAFW7r2I818vHDthYLvUyjRfkKOU6P45PztOch4DZarWQne+VOaIMwA/erSSpB9g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@vitest/utils": "2.0.5",
+        "@vitest/utils": "2.1.5",
         "pathe": "^1.1.2"
       },
       "funding": {
@@ -1596,13 +1727,14 @@
       }
     },
     "node_modules/@vitest/snapshot": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz",
-      "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.5.tgz",
+      "integrity": "sha512-zmYw47mhfdfnYbuhkQvkkzYroXUumrwWDGlMjpdUr4jBd3HZiV2w7CQHj+z7AAS4VOtWxI4Zt4bWt4/sKcoIjg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@vitest/pretty-format": "2.0.5",
-        "magic-string": "^0.30.10",
+        "@vitest/pretty-format": "2.1.5",
+        "magic-string": "^0.30.12",
         "pathe": "^1.1.2"
       },
       "funding": {
@@ -1610,26 +1742,27 @@
       }
     },
     "node_modules/@vitest/spy": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz",
-      "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.5.tgz",
+      "integrity": "sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "tinyspy": "^3.0.0"
+        "tinyspy": "^3.0.2"
       },
       "funding": {
         "url": "https://opencollective.com/vitest"
       }
     },
     "node_modules/@vitest/utils": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz",
-      "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.5.tgz",
+      "integrity": "sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@vitest/pretty-format": "2.0.5",
-        "estree-walker": "^3.0.3",
-        "loupe": "^3.1.1",
+        "@vitest/pretty-format": "2.1.5",
+        "loupe": "^3.1.2",
         "tinyrainbow": "^1.2.0"
       },
       "funding": {
@@ -1637,9 +1770,9 @@
       }
     },
     "node_modules/acorn": {
-      "version": "8.12.1",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
-      "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
+      "version": "8.14.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
+      "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
       "dev": true,
       "license": "MIT",
       "bin": {
@@ -1710,6 +1843,7 @@
       "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
       "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=12"
       }
@@ -1742,9 +1876,9 @@
       }
     },
     "node_modules/browserslist": {
-      "version": "4.23.0",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
-      "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
+      "version": "4.24.3",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
+      "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
       "dev": true,
       "funding": [
         {
@@ -1760,11 +1894,12 @@
           "url": "https://github.com/sponsors/ai"
         }
       ],
+      "license": "MIT",
       "dependencies": {
-        "caniuse-lite": "^1.0.30001587",
-        "electron-to-chromium": "^1.4.668",
-        "node-releases": "^2.0.14",
-        "update-browserslist-db": "^1.0.13"
+        "caniuse-lite": "^1.0.30001688",
+        "electron-to-chromium": "^1.5.73",
+        "node-releases": "^2.0.19",
+        "update-browserslist-db": "^1.1.1"
       },
       "bin": {
         "browserslist": "cli.js"
@@ -1800,6 +1935,7 @@
       "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
       "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
@@ -1814,9 +1950,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001597",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz",
-      "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==",
+      "version": "1.0.30001689",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz",
+      "integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==",
       "dev": true,
       "funding": [
         {
@@ -1831,13 +1967,15 @@
           "type": "github",
           "url": "https://github.com/sponsors/ai"
         }
-      ]
+      ],
+      "license": "CC-BY-4.0"
     },
     "node_modules/chai": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
-      "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz",
+      "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "assertion-error": "^2.0.1",
         "check-error": "^2.1.1",
@@ -1870,6 +2008,7 @@
       "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
       "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">= 16"
       }
@@ -1954,35 +2093,29 @@
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/core-js-compat": {
-      "version": "3.37.1",
-      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
-      "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
+      "version": "3.39.0",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz",
+      "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "browserslist": "^4.23.0"
+        "browserslist": "^4.24.2"
       },
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/core-js"
       }
     },
-    "node_modules/cross-fetch": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
-      "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
-      "dev": true,
-      "dependencies": {
-        "node-fetch": "^2.6.12"
-      }
-    },
     "node_modules/cross-spawn": {
-      "version": "7.0.3",
-      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
-      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "path-key": "^3.1.0",
         "shebang-command": "^2.0.0",
@@ -1993,12 +2126,13 @@
       }
     },
     "node_modules/debug": {
-      "version": "4.3.6",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
-      "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "ms": "2.1.2"
+        "ms": "^2.1.3"
       },
       "engines": {
         "node": ">=6.0"
@@ -2014,6 +2148,7 @@
       "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
       "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
@@ -2031,10 +2166,11 @@
       "dev": true
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.705",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.705.tgz",
-      "integrity": "sha512-LKqhpwJCLhYId2VVwEzFXWrqQI5n5zBppz1W9ehhTlfYU8CUUW6kClbN8LHF/v7flMgRdETS772nqywJ+ckVAw==",
-      "dev": true
+      "version": "1.5.74",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz",
+      "integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==",
+      "dev": true,
+      "license": "ISC"
     },
     "node_modules/emoji-regex": {
       "version": "8.0.0",
@@ -2051,6 +2187,13 @@
         "is-arrayish": "^0.2.1"
       }
     },
+    "node_modules/es-module-lexer": {
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
+      "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/esbuild": {
       "version": "0.21.5",
       "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
@@ -2091,10 +2234,11 @@
       }
     },
     "node_modules/escalade": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
-      "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
@@ -2112,28 +2256,32 @@
       }
     },
     "node_modules/eslint": {
-      "version": "9.9.1",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz",
-      "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==",
+      "version": "9.14.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz",
+      "integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
-        "@eslint-community/regexpp": "^4.11.0",
+        "@eslint-community/regexpp": "^4.12.1",
         "@eslint/config-array": "^0.18.0",
+        "@eslint/core": "^0.7.0",
         "@eslint/eslintrc": "^3.1.0",
-        "@eslint/js": "9.9.1",
+        "@eslint/js": "9.14.0",
+        "@eslint/plugin-kit": "^0.2.0",
+        "@humanfs/node": "^0.16.6",
         "@humanwhocodes/module-importer": "^1.0.1",
-        "@humanwhocodes/retry": "^0.3.0",
-        "@nodelib/fs.walk": "^1.2.8",
+        "@humanwhocodes/retry": "^0.4.0",
+        "@types/estree": "^1.0.6",
+        "@types/json-schema": "^7.0.15",
         "ajv": "^6.12.4",
         "chalk": "^4.0.0",
         "cross-spawn": "^7.0.2",
         "debug": "^4.3.2",
         "escape-string-regexp": "^4.0.0",
-        "eslint-scope": "^8.0.2",
-        "eslint-visitor-keys": "^4.0.0",
-        "espree": "^10.1.0",
+        "eslint-scope": "^8.2.0",
+        "eslint-visitor-keys": "^4.2.0",
+        "espree": "^10.3.0",
         "esquery": "^1.5.0",
         "esutils": "^2.0.2",
         "fast-deep-equal": "^3.1.3",
@@ -2143,14 +2291,11 @@
         "ignore": "^5.2.0",
         "imurmurhash": "^0.1.4",
         "is-glob": "^4.0.0",
-        "is-path-inside": "^3.0.3",
         "json-stable-stringify-without-jsonify": "^1.0.1",
-        "levn": "^0.4.1",
         "lodash.merge": "^4.6.2",
         "minimatch": "^3.1.2",
         "natural-compare": "^1.4.0",
         "optionator": "^0.9.3",
-        "strip-ansi": "^6.0.1",
         "text-table": "^0.2.0"
       },
       "bin": {
@@ -2215,19 +2360,19 @@
       }
     },
     "node_modules/eslint-plugin-unicorn": {
-      "version": "55.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz",
-      "integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==",
+      "version": "56.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.1.tgz",
+      "integrity": "sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@babel/helper-validator-identifier": "^7.24.5",
+        "@babel/helper-validator-identifier": "^7.24.7",
         "@eslint-community/eslint-utils": "^4.4.0",
         "ci-info": "^4.0.0",
         "clean-regexp": "^1.0.0",
-        "core-js-compat": "^3.37.0",
-        "esquery": "^1.5.0",
-        "globals": "^15.7.0",
+        "core-js-compat": "^3.38.1",
+        "esquery": "^1.6.0",
+        "globals": "^15.9.0",
         "indent-string": "^4.0.0",
         "is-builtin-module": "^3.2.1",
         "jsesc": "^3.0.2",
@@ -2235,7 +2380,7 @@
         "read-pkg-up": "^7.0.1",
         "regexp-tree": "^0.1.27",
         "regjsparser": "^0.10.0",
-        "semver": "^7.6.1",
+        "semver": "^7.6.3",
         "strip-indent": "^3.0.0"
       },
       "engines": {
@@ -2249,9 +2394,9 @@
       }
     },
     "node_modules/eslint-scope": {
-      "version": "8.0.2",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz",
-      "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==",
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
+      "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
@@ -2277,6 +2422,23 @@
         "url": "https://opencollective.com/eslint"
       }
     },
+    "node_modules/eslint/node_modules/@eslint/js": {
+      "version": "9.14.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz",
+      "integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/eslint/node_modules/@types/estree": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+      "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/eslint/node_modules/brace-expansion": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -2289,9 +2451,9 @@
       }
     },
     "node_modules/eslint/node_modules/eslint-visitor-keys": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
-      "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
       "dev": true,
       "license": "Apache-2.0",
       "engines": {
@@ -2315,15 +2477,15 @@
       }
     },
     "node_modules/espree": {
-      "version": "10.1.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz",
-      "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==",
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
+      "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
-        "acorn": "^8.12.0",
+        "acorn": "^8.14.0",
         "acorn-jsx": "^5.3.2",
-        "eslint-visitor-keys": "^4.0.0"
+        "eslint-visitor-keys": "^4.2.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2333,9 +2495,9 @@
       }
     },
     "node_modules/espree/node_modules/eslint-visitor-keys": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
-      "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
       "dev": true,
       "license": "Apache-2.0",
       "engines": {
@@ -2346,10 +2508,11 @@
       }
     },
     "node_modules/esquery": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
-      "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+      "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "estraverse": "^5.1.0"
       },
@@ -2362,6 +2525,7 @@
       "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
       "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
       "dev": true,
+      "license": "BSD-2-Clause",
       "dependencies": {
         "estraverse": "^5.2.0"
       },
@@ -2383,6 +2547,7 @@
       "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
       "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/estree": "^1.0.0"
       }
@@ -2396,27 +2561,14 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/execa": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
-      "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
+    "node_modules/expect-type": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz",
+      "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==",
       "dev": true,
-      "dependencies": {
-        "cross-spawn": "^7.0.3",
-        "get-stream": "^8.0.1",
-        "human-signals": "^5.0.0",
-        "is-stream": "^3.0.0",
-        "merge-stream": "^2.0.0",
-        "npm-run-path": "^5.1.0",
-        "onetime": "^6.0.0",
-        "signal-exit": "^4.1.0",
-        "strip-final-newline": "^3.0.0"
-      },
+      "license": "Apache-2.0",
       "engines": {
-        "node": ">=16.17"
-      },
-      "funding": {
-        "url": "https://github.com/sindresorhus/execa?sponsor=1"
+        "node": ">=12.0.0"
       }
     },
     "node_modules/fast-deep-equal": {
@@ -2435,6 +2587,7 @@
       "version": "3.3.2",
       "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
       "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+      "license": "MIT",
       "dependencies": {
         "@nodelib/fs.stat": "^2.0.2",
         "@nodelib/fs.walk": "^1.2.3",
@@ -2577,27 +2730,6 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/get-func-name": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
-      "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
-      "dev": true,
-      "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/get-stream": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
-      "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
-      "dev": true,
-      "engines": {
-        "node": ">=16"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/glob": {
       "version": "10.4.5",
       "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@@ -2631,9 +2763,9 @@
       }
     },
     "node_modules/globals": {
-      "version": "15.9.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz",
-      "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==",
+      "version": "15.12.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz",
+      "integrity": "sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -2688,15 +2820,6 @@
       "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
       "dev": true
     },
-    "node_modules/human-signals": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
-      "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=16.17.0"
-      }
-    },
     "node_modules/ignore": {
       "version": "5.3.1",
       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
@@ -2809,27 +2932,6 @@
         "node": ">=0.12.0"
       }
     },
-    "node_modules/is-path-inside": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
-      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/is-stream": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
-      "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
-      "dev": true,
-      "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/isexe": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -3012,13 +3114,11 @@
       "dev": true
     },
     "node_modules/loupe": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz",
-      "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz",
+      "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==",
       "dev": true,
-      "dependencies": {
-        "get-func-name": "^2.0.1"
-      }
+      "license": "MIT"
     },
     "node_modules/lru-cache": {
       "version": "10.4.3",
@@ -3027,22 +3127,24 @@
       "dev": true
     },
     "node_modules/magic-string": {
-      "version": "0.30.11",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
-      "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
+      "version": "0.30.12",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz",
+      "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.5.0"
       }
     },
     "node_modules/magicast": {
-      "version": "0.3.4",
-      "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz",
-      "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==",
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
+      "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/parser": "^7.24.4",
-        "@babel/types": "^7.24.0",
+        "@babel/parser": "^7.25.4",
+        "@babel/types": "^7.25.4",
         "source-map-js": "^1.2.0"
       }
     },
@@ -3061,12 +3163,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/merge-stream": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
-      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
-      "dev": true
-    },
     "node_modules/merge2": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -3087,18 +3183,6 @@
         "node": ">=8.6"
       }
     },
-    "node_modules/mimic-fn": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
-      "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/min-indent": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@@ -3134,19 +3218,21 @@
       }
     },
     "node_modules/mock-fs": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.2.0.tgz",
-      "integrity": "sha512-2dF2R6YMSZbpip1V1WHKGLNjr/k48uQClqMVb5H3MOvwc9qhYis3/IWbj02qIg/Y8MDXKFF4c5v0rxx2o6xTZw==",
+      "version": "5.4.1",
+      "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.4.1.tgz",
+      "integrity": "sha512-sz/Q8K1gXXXHR+qr0GZg2ysxCRr323kuN10O7CtQjraJsFDJ4SJ+0I5MzALz7aRp9lHk8Cc/YdsT95h9Ka1aFw==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=12.0.0"
       }
     },
     "node_modules/ms": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-      "dev": true
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/nanoid": {
       "version": "3.3.7",
@@ -3172,31 +3258,12 @@
       "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
       "dev": true
     },
-    "node_modules/node-fetch": {
-      "version": "2.7.0",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
-      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
-      "dev": true,
-      "dependencies": {
-        "whatwg-url": "^5.0.0"
-      },
-      "engines": {
-        "node": "4.x || >=6.0.0"
-      },
-      "peerDependencies": {
-        "encoding": "^0.1.0"
-      },
-      "peerDependenciesMeta": {
-        "encoding": {
-          "optional": true
-        }
-      }
-    },
     "node_modules/node-releases": {
-      "version": "2.0.14",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
-      "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
-      "dev": true
+      "version": "2.0.19",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+      "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/normalize-package-data": {
       "version": "2.5.0",
@@ -3219,48 +3286,6 @@
         "semver": "bin/semver"
       }
     },
-    "node_modules/npm-run-path": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
-      "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
-      "dev": true,
-      "dependencies": {
-        "path-key": "^4.0.0"
-      },
-      "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/npm-run-path/node_modules/path-key": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
-      "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/onetime": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
-      "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
-      "dev": true,
-      "dependencies": {
-        "mimic-fn": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/optionator": {
       "version": "0.9.3",
       "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@@ -3397,21 +3422,23 @@
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
       "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/pathval": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
       "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">= 14.16"
       }
     },
     "node_modules/picocolors": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
-      "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
+      "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
       "dev": true,
       "license": "ISC"
     },
@@ -3436,9 +3463,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.4.41",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
-      "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
+      "version": "8.4.47",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
+      "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
       "dev": true,
       "funding": [
         {
@@ -3457,8 +3484,8 @@
       "license": "MIT",
       "dependencies": {
         "nanoid": "^3.3.7",
-        "picocolors": "^1.0.1",
-        "source-map-js": "^1.2.0"
+        "picocolors": "^1.1.0",
+        "source-map-js": "^1.2.1"
       },
       "engines": {
         "node": "^10 || ^12 || >=14"
@@ -3502,21 +3529,17 @@
       }
     },
     "node_modules/prettier-plugin-organize-imports": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
-      "integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz",
+      "integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==",
       "dev": true,
       "license": "MIT",
       "peerDependencies": {
-        "@vue/language-plugin-pug": "^2.0.24",
         "prettier": ">=2.0",
         "typescript": ">=2.9",
-        "vue-tsc": "^2.0.24"
+        "vue-tsc": "^2.1.0"
       },
       "peerDependenciesMeta": {
-        "@vue/language-plugin-pug": {
-          "optional": true
-        },
         "vue-tsc": {
           "optional": true
         }
@@ -3776,10 +3799,11 @@
       }
     },
     "node_modules/semver": {
-      "version": "7.6.2",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
-      "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
+      "version": "7.6.3",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+      "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
       "dev": true,
+      "license": "ISC",
       "bin": {
         "semver": "bin/semver.js"
       },
@@ -3827,10 +3851,11 @@
       }
     },
     "node_modules/source-map-js": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
-      "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -3874,10 +3899,11 @@
       "dev": true
     },
     "node_modules/std-env": {
-      "version": "3.7.0",
-      "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
-      "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==",
-      "dev": true
+      "version": "3.8.0",
+      "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz",
+      "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/string-width": {
       "version": "4.2.3",
@@ -3933,18 +3959,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/strip-final-newline": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
-      "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/strip-indent": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
@@ -4028,19 +4042,29 @@
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
       "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/tinybench": {
-      "version": "2.8.0",
-      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz",
-      "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==",
-      "dev": true
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+      "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tinyexec": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz",
+      "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/tinypool": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz",
-      "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz",
+      "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": "^18.0.0 || >=20.0.0"
       }
@@ -4055,23 +4079,15 @@
       }
     },
     "node_modules/tinyspy": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz",
-      "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==",
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
+      "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=14.0.0"
       }
     },
-    "node_modules/to-fast-properties": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
-      "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/to-regex-range": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -4083,12 +4099,6 @@
         "node": ">=8.0"
       }
     },
-    "node_modules/tr46": {
-      "version": "0.0.3",
-      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
-      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
-      "dev": true
-    },
     "node_modules/ts-api-utils": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
@@ -4140,9 +4150,9 @@
       }
     },
     "node_modules/typescript": {
-      "version": "5.5.4",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
-      "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
+      "version": "5.6.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+      "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
       "dev": true,
       "license": "Apache-2.0",
       "bin": {
@@ -4154,16 +4164,16 @@
       }
     },
     "node_modules/undici-types": {
-      "version": "6.19.8",
-      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
-      "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
+      "version": "6.20.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
+      "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
       "dev": true,
       "license": "MIT"
     },
     "node_modules/update-browserslist-db": {
-      "version": "1.0.13",
-      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
-      "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
+      "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
       "dev": true,
       "funding": [
         {
@@ -4179,9 +4189,10 @@
           "url": "https://github.com/sponsors/ai"
         }
       ],
+      "license": "MIT",
       "dependencies": {
-        "escalade": "^3.1.1",
-        "picocolors": "^1.0.0"
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.0"
       },
       "bin": {
         "update-browserslist-db": "cli.js"
@@ -4210,14 +4221,14 @@
       }
     },
     "node_modules/vite": {
-      "version": "5.4.2",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz",
-      "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==",
+      "version": "5.4.11",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
+      "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "esbuild": "^0.21.3",
-        "postcss": "^8.4.41",
+        "postcss": "^8.4.43",
         "rollup": "^4.20.0"
       },
       "bin": {
@@ -4270,15 +4281,16 @@
       }
     },
     "node_modules/vite-node": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz",
-      "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.5.tgz",
+      "integrity": "sha512-rd0QIgx74q4S1Rd56XIiL2cYEdyWn13cunYBIuqh9mpmQr7gGS0IxXoP8R6OaZtNQQLyXSWbd4rXKYUbhFpK5w==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "cac": "^6.7.14",
-        "debug": "^4.3.5",
+        "debug": "^4.3.7",
+        "es-module-lexer": "^1.5.4",
         "pathe": "^1.1.2",
-        "tinyrainbow": "^1.2.0",
         "vite": "^5.0.0"
       },
       "bin": {
@@ -4292,9 +4304,9 @@
       }
     },
     "node_modules/vite-tsconfig-paths": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.0.1.tgz",
-      "integrity": "sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==",
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.2.tgz",
+      "integrity": "sha512-gEIbKfJzSEv0yR3XS2QEocKetONoWkbROj6hGx0FHM18qKUojhvcokQsxQx5nMkelZq2n37zbSGCJn+FSODSjA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -4312,29 +4324,31 @@
       }
     },
     "node_modules/vitest": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz",
-      "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.5.tgz",
+      "integrity": "sha512-P4ljsdpuzRTPI/kbND2sDZ4VmieerR2c9szEZpjc+98Z9ebvnXmM5+0tHEKqYZumXqlvnmfWsjeFOjXVriDG7A==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@ampproject/remapping": "^2.3.0",
-        "@vitest/expect": "2.0.5",
-        "@vitest/pretty-format": "^2.0.5",
-        "@vitest/runner": "2.0.5",
-        "@vitest/snapshot": "2.0.5",
-        "@vitest/spy": "2.0.5",
-        "@vitest/utils": "2.0.5",
-        "chai": "^5.1.1",
-        "debug": "^4.3.5",
-        "execa": "^8.0.1",
-        "magic-string": "^0.30.10",
+        "@vitest/expect": "2.1.5",
+        "@vitest/mocker": "2.1.5",
+        "@vitest/pretty-format": "^2.1.5",
+        "@vitest/runner": "2.1.5",
+        "@vitest/snapshot": "2.1.5",
+        "@vitest/spy": "2.1.5",
+        "@vitest/utils": "2.1.5",
+        "chai": "^5.1.2",
+        "debug": "^4.3.7",
+        "expect-type": "^1.1.0",
+        "magic-string": "^0.30.12",
         "pathe": "^1.1.2",
-        "std-env": "^3.7.0",
-        "tinybench": "^2.8.0",
-        "tinypool": "^1.0.0",
+        "std-env": "^3.8.0",
+        "tinybench": "^2.9.0",
+        "tinyexec": "^0.3.1",
+        "tinypool": "^1.0.1",
         "tinyrainbow": "^1.2.0",
         "vite": "^5.0.0",
-        "vite-node": "2.0.5",
+        "vite-node": "2.1.5",
         "why-is-node-running": "^2.3.0"
       },
       "bin": {
@@ -4349,8 +4363,8 @@
       "peerDependencies": {
         "@edge-runtime/vm": "*",
         "@types/node": "^18.0.0 || >=20.0.0",
-        "@vitest/browser": "2.0.5",
-        "@vitest/ui": "2.0.5",
+        "@vitest/browser": "2.1.5",
+        "@vitest/ui": "2.1.5",
         "happy-dom": "*",
         "jsdom": "*"
       },
@@ -4376,36 +4390,18 @@
       }
     },
     "node_modules/vitest-fetch-mock": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/vitest-fetch-mock/-/vitest-fetch-mock-0.3.0.tgz",
-      "integrity": "sha512-g6upWcL8/32fXL43/5f4VHcocuwQIi9Fj5othcK9gPO8XqSEGtnIZdenr2IaipDr61ReRFt+vaOEgo8jiUUX5w==",
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/vitest-fetch-mock/-/vitest-fetch-mock-0.4.2.tgz",
+      "integrity": "sha512-MuN/TCAvvUs9sLMdOPKqdXEUOD0E5cNW/LN7Tro3KkrLBsvUaH7iQWcznNUU4ml+GqX6ZbNguDmFQ2tliKqhCg==",
       "dev": true,
-      "dependencies": {
-        "cross-fetch": "^4.0.0"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">=14.14.0"
+        "node": ">=18.0.0"
       },
       "peerDependencies": {
         "vitest": ">=2.0.0"
       }
     },
-    "node_modules/webidl-conversions": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
-      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
-      "dev": true
-    },
-    "node_modules/whatwg-url": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
-      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
-      "dev": true,
-      "dependencies": {
-        "tr46": "~0.0.3",
-        "webidl-conversions": "^3.0.0"
-      }
-    },
     "node_modules/which": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -4535,9 +4531,9 @@
       }
     },
     "node_modules/yaml": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
-      "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
+      "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==",
       "dev": true,
       "license": "ISC",
       "bin": {
diff --git a/cli/package.json b/cli/package.json
index 287974e49b..bdfaa0f528 100644
--- a/cli/package.json
+++ b/cli/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@immich/cli",
-  "version": "2.2.19",
+  "version": "2.2.37",
   "description": "Command Line Interface (CLI) for Immich",
   "type": "module",
   "exports": "./dist/index.js",
@@ -20,17 +20,17 @@
     "@types/cli-progress": "^3.11.0",
     "@types/lodash-es": "^4.17.12",
     "@types/mock-fs": "^4.13.1",
-    "@types/node": "^20.16.5",
-    "@typescript-eslint/eslint-plugin": "^8.0.0",
-    "@typescript-eslint/parser": "^8.0.0",
+    "@types/node": "^22.10.2",
+    "@typescript-eslint/eslint-plugin": "^8.15.0",
+    "@typescript-eslint/parser": "^8.15.0",
     "@vitest/coverage-v8": "^2.0.5",
     "byte-size": "^9.0.0",
     "cli-progress": "^3.12.0",
     "commander": "^12.0.0",
-    "eslint": "^9.0.0",
+    "eslint": "^9.14.0",
     "eslint-config-prettier": "^9.1.0",
     "eslint-plugin-prettier": "^5.1.3",
-    "eslint-plugin-unicorn": "^55.0.0",
+    "eslint-plugin-unicorn": "^56.0.1",
     "globals": "^15.9.0",
     "mock-fs": "^5.2.0",
     "prettier": "^3.2.5",
@@ -39,7 +39,7 @@
     "vite": "^5.0.12",
     "vite-tsconfig-paths": "^5.0.0",
     "vitest": "^2.0.5",
-    "vitest-fetch-mock": "^0.3.0",
+    "vitest-fetch-mock": "^0.4.0",
     "yaml": "^2.3.1"
   },
   "scripts": {
@@ -67,6 +67,6 @@
     "lodash-es": "^4.17.21"
   },
   "volta": {
-    "node": "20.17.0"
+    "node": "22.12.0"
   }
 }
diff --git a/cli/src/commands/asset.ts b/cli/src/commands/asset.ts
index 9c1a503cda..4cf6742f24 100644
--- a/cli/src/commands/asset.ts
+++ b/cli/src/commands/asset.ts
@@ -1,5 +1,6 @@
 import {
   Action,
+  AssetBulkUploadCheckItem,
   AssetBulkUploadCheckResult,
   AssetMediaResponseDto,
   AssetMediaStatus,
@@ -11,7 +12,7 @@ import {
   getSupportedMediaTypes,
 } from '@immich/sdk';
 import byteSize from 'byte-size';
-import { Presets, SingleBar } from 'cli-progress';
+import { MultiBar, Presets, SingleBar } from 'cli-progress';
 import { chunk } from 'lodash-es';
 import { Stats, createReadStream } from 'node:fs';
 import { stat, unlink } from 'node:fs/promises';
@@ -90,23 +91,23 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas
     return { newFiles: files, duplicates: [] };
   }
 
-  const progressBar = new SingleBar(
-    { format: 'Checking files | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' },
+  const multiBar = new MultiBar(
+    { format: '{message} | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' },
     Presets.shades_classic,
   );
 
-  progressBar.start(files.length, 0);
+  const hashProgressBar = multiBar.create(files.length, 0, { message: 'Hashing files          ' });
+  const checkProgressBar = multiBar.create(files.length, 0, { message: 'Checking for duplicates' });
 
   const newFiles: string[] = [];
   const duplicates: Asset[] = [];
 
-  const queue = new Queue<string[], AssetBulkUploadCheckResults>(
-    async (filepaths: string[]) => {
-      const dto = await Promise.all(
-        filepaths.map(async (filepath) => ({ id: filepath, checksum: await sha1(filepath) })),
-      );
-      const response = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: dto } });
+  const checkBulkUploadQueue = new Queue<AssetBulkUploadCheckItem[], void>(
+    async (assets: AssetBulkUploadCheckItem[]) => {
+      const response = await checkBulkUpload({ assetBulkUploadCheckDto: { assets } });
+
       const results = response.results as AssetBulkUploadCheckResults;
+
       for (const { id: filepath, assetId, action } of results) {
         if (action === Action.Accept) {
           newFiles.push(filepath);
@@ -115,19 +116,46 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas
           duplicates.push({ id: assetId as string, filepath });
         }
       }
-      progressBar.increment(filepaths.length);
+
+      checkProgressBar.increment(assets.length);
+    },
+    { concurrency, retry: 3 },
+  );
+
+  const results: { id: string; checksum: string }[] = [];
+  let checkBulkUploadRequests: AssetBulkUploadCheckItem[] = [];
+
+  const queue = new Queue<string, AssetBulkUploadCheckItem[]>(
+    async (filepath: string): Promise<AssetBulkUploadCheckItem[]> => {
+      const dto = { id: filepath, checksum: await sha1(filepath) };
+
+      results.push(dto);
+      checkBulkUploadRequests.push(dto);
+      if (checkBulkUploadRequests.length === 5000) {
+        const batch = checkBulkUploadRequests;
+        checkBulkUploadRequests = [];
+        void checkBulkUploadQueue.push(batch);
+      }
+
+      hashProgressBar.increment();
       return results;
     },
     { concurrency, retry: 3 },
   );
 
-  for (const items of chunk(files, concurrency)) {
-    await queue.push(items);
+  for (const item of files) {
+    void queue.push(item);
   }
 
   await queue.drained();
 
-  progressBar.stop();
+  if (checkBulkUploadRequests.length > 0) {
+    void checkBulkUploadQueue.push(checkBulkUploadRequests);
+  }
+
+  await checkBulkUploadQueue.drained();
+
+  multiBar.stop();
 
   console.log(`Found ${newFiles.length} new files and ${duplicates.length} duplicate${s(duplicates.length)}`);
 
@@ -201,8 +229,8 @@ export const uploadFiles = async (files: string[], { dryRun, concurrency }: Uplo
     { concurrency, retry: 3 },
   );
 
-  for (const filepath of files) {
-    await queue.push(filepath);
+  for (const item of files) {
+    void queue.push(item);
   }
 
   await queue.drained();
diff --git a/cli/src/queue.ts b/cli/src/queue.ts
index c700028a15..0b6d628146 100644
--- a/cli/src/queue.ts
+++ b/cli/src/queue.ts
@@ -72,8 +72,8 @@ export class Queue<T, R> {
    * @returns Promise<void> - The returned Promise will be resolved when all tasks in the queue have been processed by a worker.
    * This promise could be ignored as it will not lead to a `unhandledRejection`.
    */
-  async drained(): Promise<void> {
-    await this.queue.drain();
+  drained(): Promise<void> {
+    return this.queue.drained();
   }
 
   /**
diff --git a/cli/src/utils.spec.ts b/cli/src/utils.spec.ts
index 0094b329b8..3e7e55fcb6 100644
--- a/cli/src/utils.spec.ts
+++ b/cli/src/utils.spec.ts
@@ -115,17 +115,7 @@ const tests: Test[] = [
       '/albums/image3.jpg': true,
     },
   },
-  {
-    test: 'should support globbing paths',
-    options: {
-      pathsToCrawl: ['/photos*'],
-    },
-    files: {
-      '/photos1/image1.jpg': true,
-      '/photos2/image2.jpg': true,
-      '/images/image3.jpg': false,
-    },
-  },
+
   {
     test: 'should crawl a single path without trailing slash',
     options: {
diff --git a/cli/src/utils.ts b/cli/src/utils.ts
index 67948e0bd2..7bbbb5615b 100644
--- a/cli/src/utils.ts
+++ b/cli/src/utils.ts
@@ -141,25 +141,21 @@ export const crawl = async (options: CrawlOptions): Promise<string[]> => {
     }
   }
 
-  let searchPattern: string;
-  if (patterns.length === 1) {
-    searchPattern = patterns[0];
-  } else if (patterns.length === 0) {
+  if (patterns.length === 0) {
     return crawledFiles;
-  } else {
-    searchPattern = '{' + patterns.join(',') + '}';
   }
 
-  if (recursive) {
-    searchPattern = searchPattern + '/**/';
-  }
+  const searchPatterns = patterns.map((pattern) => {
+    let escapedPattern = pattern;
+    if (recursive) {
+      escapedPattern = escapedPattern + '/**';
+    }
+    return `${escapedPattern}/*.{${extensions.join(',')}}`;
+  });
 
-  searchPattern = `${searchPattern}/*.{${extensions.join(',')}}`;
-
-  const globbedFiles = await glob(searchPattern, {
+  const globbedFiles = await glob(searchPatterns, {
     absolute: true,
     caseSensitiveMatch: false,
-    onlyFiles: true,
     dot: includeHidden,
     ignore: [`**/${exclusionPattern}`],
   });
diff --git a/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl
index afa00e6067..00222921f1 100644
--- a/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl
+++ b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl
@@ -2,37 +2,37 @@
 # Manual edits may be lost in future updates.
 
 provider "registry.opentofu.org/cloudflare/cloudflare" {
-  version     = "4.41.0"
-  constraints = "4.41.0"
+  version     = "4.48.0"
+  constraints = "4.48.0"
   hashes = [
-    "h1:0mc+YrjQrcctGrGYDmzlcqcgSv9MYB74rvMaZylIKC8=",
-    "h1:0zUx4vk4jOORQqn6xHBF7dO6N6bielFHdJ0mgF4Obn8=",
-    "h1:AsIZW3uLFNOZO7kL/K7/Y/S0IYxUV9Hz85NNk/3TTsA=",
-    "h1:FSgYM4+LHMbX/a4Y1kx7FPPWmXqS3/MQYzvjMJHHHWM=",
-    "h1:Tx6Nh3BWP1x9L3KK/Eyi+ET0T26g3+jf1jyiuqpNIis=",
-    "h1:VRI9wu8P43xxfpeTndRwsisLnqncfnmEYMOEH5zH4pQ=",
-    "h1:YxQqmiES/Yanq/VfGqBEqg+VIO7FGhO88aKoWFHyGIg=",
-    "h1:ZWHiaesjgDLKWlfdNj0oKyj/DWdxcfsO6NINu39zfpY=",
-    "h1:a2aCgDDBz3ccrr8YstIMl7VFnKo1xZAp+rOv59PPJ7U=",
-    "h1:aRyv8tB6wBAF9lKsLEdiHyCqnK5LfZq0FqMXCcUB4UU=",
-    "h1:lXpuO7zv2uD2GzPE1ARxznreRAh+QHTc2lAJ7iOoFgY=",
-    "h1:sA1xq0QNQ4fH8SHXouYNq50xirVD18SamKQwPsBQrrY=",
-    "h1:v7sHvKq7oqMYPn47ULHFyIQsKD9o+6Xg/uHbxQUixEw=",
-    "h1:wo/x4atWyXuWGlfR6h5nH0YwBAmBwTRY27HtWP8ycLo=",
-    "zh:339d26e06dc6fb299ea8aad9476a60fd65bb1d40631ae8eeb81cddf2dd2bebc8",
-    "zh:3dec2ad96ac2c283fd34ce65781b55c4edbb4d5c5cb53da8e31537176c0ed562",
-    "zh:5f63a5f8080319a2fff09d4d49944829fa708723436520787cfb60725ced80cf",
-    "zh:67162c28ccea71cb8141ed15c0637e35621354ebe14878e0b75a8f160fc5505d",
-    "zh:6ac1e07f5347b6395aca690ed22101bb25e957d25f986f760ff673a7adfd5ef6",
-    "zh:70282a723c7b52fcabde2baad41c864ed3a8d69f0c4d27a6b6933cac434cffc6",
+    "h1:0IKUOR32xEI1suS5QCOjfxjQ2mRd058btXk8hVnaOJ4=",
+    "h1:3YG6vu/bFPcYOeLdSUZhiAWiWKaFlOAR34z2o8cbE9k=",
+    "h1:FvGy06/i9AMtVkSIUnCrXNv5xF6jqBqMH8oPVLyeeAg=",
+    "h1:GXH7nIF0ocMqebbA41+fSGIYfM+VAM/PvTe7fJr8UrQ=",
+    "h1:H0ll0ph4404vFE868W3qJ3zhOyy4jbXrOMtdkViEZsU=",
+    "h1:SX42e3k73IcFcrQlZ2e/Veqt2tvCMy6fwlo5yNUktCE=",
+    "h1:Uu/gjBc99GefdPdSrlBwU75DWU0ZcwGcrd3ZFyTeL0s=",
+    "h1:VZw0uN41PWRmNlhg7Ze0Eh7cdoklX1oZbfNAXNYnU1I=",
+    "h1:cMdV7ql6PsFa4qtb0EoZSctvTaTqV7yplBSDwcLRCLc=",
+    "h1:ePGvSurmlqOCkD761vkhRmz7bsK36/EnIvx2Xy8TdXo=",
+    "h1:fOYufF+1bzw2N3aHLpkLB6E8VbZ4ysXDODYQOlwhwd4=",
+    "h1:qe8RbnWq0T4xhqjn9QcbO6YW5YDx47P+eJ0NUMIfwCc=",
+    "h1:tRD2av6PafHDP/b9jDQsG5/aX+lHeKxpbIEHYYLBVUc=",
+    "h1:zyl6Gvx/CFpwYW8pFFDesfO8Lxv+a6CopyAsIMhp54s=",
+    "zh:04c0a49c2b23140b2f21cfd0d52f9798d70d3bdae3831613e156aabe519bbc6c",
+    "zh:185f21b4834ba63e8df1f84aa34639d8a7e126429a4007bb5f9ad82f2602a997",
+    "zh:234724f52cb4c0c3f7313d3b2697caef26d921d134f26ae14801e7afac522f7b",
+    "zh:38a56fcd1b3e40706af995611c977816543b53f1e55fe2720944aae2b6828fcb",
+    "zh:419938f5430fc78eff933470aefbf94a460a478f867cf7761a3dea177b4eb153",
+    "zh:4b46d92bfde1deab7de7ba1a6bbf4ba7c711e4fd925341ddf09d4cc28dae03d8",
+    "zh:537acd4a31c752f1bae305ba7190f60b71ad1a459f22d464f3f914336c9e919f",
+    "zh:5ff36b005aad07697dd0b30d4f0c35dbcdc30dc52b41722552060792fa87ce04",
+    "zh:635c5ee419daea098060f794d9d7d999275301181e49562c4e4c08f043076937",
+    "zh:859277c330d61f91abe9e799389467ca11b77131bf34bedbef52f8da68b2bb49",
     "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
-    "zh:924cd23abc326c6b3914e2cd9c94c7832c2552e1e9ae258fb9fd9aedaa5f7ce7",
-    "zh:a4b75e4c239879296259e7d54f1befbc7fdc16da2d62d1294e9f73add4cae61e",
-    "zh:a6ceb08feb63b00c7141783b31e45a154c76fd8cdebbdf371074805f0053572d",
-    "zh:afae1843f9ba85f2f6d94108c65cf43a457e83531a632d44d863e935160cb2ba",
-    "zh:bd6628ce60c778960a5755f7010b7e2cc5c6ff0341a21c175341b28058ec843d",
-    "zh:cd30866a1ff99d72b5fa1699db582fa4f25562e6ab21dcc6870324f3056108e0",
-    "zh:df5924cca691a8220aaaebb5cb55c3d6c32ff0a881f198695eff28155eb12b54",
-    "zh:e78d0696c941aba58df1cb36b8a0d25cd5f3963f01d9338fdbda74db58afdd49",
+    "zh:927dfdb8d9aef37ead03fceaa29e87ba076a3dd24e19b6cefdbb0efe9987ff8c",
+    "zh:bbf2226f07f6b1e721877328e69ded4b64f9c196634d2e2429e3cfabbe41e532",
+    "zh:daeed873d6f38604232b46ee4a5830c85d195b967f8dbcafe2fcffa98daf9c5f",
+    "zh:f8f2fc4646c1ba44085612fa7f4dbb7cbcead43b4e661f2b98ddfb4f68afc758",
   ]
 }
diff --git a/deployment/modules/cloudflare/docs-release/config.tf b/deployment/modules/cloudflare/docs-release/config.tf
index 18d8ff1eb4..c5397ea410 100644
--- a/deployment/modules/cloudflare/docs-release/config.tf
+++ b/deployment/modules/cloudflare/docs-release/config.tf
@@ -5,7 +5,7 @@ terraform {
   required_providers {
     cloudflare = {
       source = "cloudflare/cloudflare"
-      version = "4.41.0"
+      version = "4.48.0"
     }
   }
 }
diff --git a/deployment/modules/cloudflare/docs/.terraform.lock.hcl b/deployment/modules/cloudflare/docs/.terraform.lock.hcl
index afa00e6067..00222921f1 100644
--- a/deployment/modules/cloudflare/docs/.terraform.lock.hcl
+++ b/deployment/modules/cloudflare/docs/.terraform.lock.hcl
@@ -2,37 +2,37 @@
 # Manual edits may be lost in future updates.
 
 provider "registry.opentofu.org/cloudflare/cloudflare" {
-  version     = "4.41.0"
-  constraints = "4.41.0"
+  version     = "4.48.0"
+  constraints = "4.48.0"
   hashes = [
-    "h1:0mc+YrjQrcctGrGYDmzlcqcgSv9MYB74rvMaZylIKC8=",
-    "h1:0zUx4vk4jOORQqn6xHBF7dO6N6bielFHdJ0mgF4Obn8=",
-    "h1:AsIZW3uLFNOZO7kL/K7/Y/S0IYxUV9Hz85NNk/3TTsA=",
-    "h1:FSgYM4+LHMbX/a4Y1kx7FPPWmXqS3/MQYzvjMJHHHWM=",
-    "h1:Tx6Nh3BWP1x9L3KK/Eyi+ET0T26g3+jf1jyiuqpNIis=",
-    "h1:VRI9wu8P43xxfpeTndRwsisLnqncfnmEYMOEH5zH4pQ=",
-    "h1:YxQqmiES/Yanq/VfGqBEqg+VIO7FGhO88aKoWFHyGIg=",
-    "h1:ZWHiaesjgDLKWlfdNj0oKyj/DWdxcfsO6NINu39zfpY=",
-    "h1:a2aCgDDBz3ccrr8YstIMl7VFnKo1xZAp+rOv59PPJ7U=",
-    "h1:aRyv8tB6wBAF9lKsLEdiHyCqnK5LfZq0FqMXCcUB4UU=",
-    "h1:lXpuO7zv2uD2GzPE1ARxznreRAh+QHTc2lAJ7iOoFgY=",
-    "h1:sA1xq0QNQ4fH8SHXouYNq50xirVD18SamKQwPsBQrrY=",
-    "h1:v7sHvKq7oqMYPn47ULHFyIQsKD9o+6Xg/uHbxQUixEw=",
-    "h1:wo/x4atWyXuWGlfR6h5nH0YwBAmBwTRY27HtWP8ycLo=",
-    "zh:339d26e06dc6fb299ea8aad9476a60fd65bb1d40631ae8eeb81cddf2dd2bebc8",
-    "zh:3dec2ad96ac2c283fd34ce65781b55c4edbb4d5c5cb53da8e31537176c0ed562",
-    "zh:5f63a5f8080319a2fff09d4d49944829fa708723436520787cfb60725ced80cf",
-    "zh:67162c28ccea71cb8141ed15c0637e35621354ebe14878e0b75a8f160fc5505d",
-    "zh:6ac1e07f5347b6395aca690ed22101bb25e957d25f986f760ff673a7adfd5ef6",
-    "zh:70282a723c7b52fcabde2baad41c864ed3a8d69f0c4d27a6b6933cac434cffc6",
+    "h1:0IKUOR32xEI1suS5QCOjfxjQ2mRd058btXk8hVnaOJ4=",
+    "h1:3YG6vu/bFPcYOeLdSUZhiAWiWKaFlOAR34z2o8cbE9k=",
+    "h1:FvGy06/i9AMtVkSIUnCrXNv5xF6jqBqMH8oPVLyeeAg=",
+    "h1:GXH7nIF0ocMqebbA41+fSGIYfM+VAM/PvTe7fJr8UrQ=",
+    "h1:H0ll0ph4404vFE868W3qJ3zhOyy4jbXrOMtdkViEZsU=",
+    "h1:SX42e3k73IcFcrQlZ2e/Veqt2tvCMy6fwlo5yNUktCE=",
+    "h1:Uu/gjBc99GefdPdSrlBwU75DWU0ZcwGcrd3ZFyTeL0s=",
+    "h1:VZw0uN41PWRmNlhg7Ze0Eh7cdoklX1oZbfNAXNYnU1I=",
+    "h1:cMdV7ql6PsFa4qtb0EoZSctvTaTqV7yplBSDwcLRCLc=",
+    "h1:ePGvSurmlqOCkD761vkhRmz7bsK36/EnIvx2Xy8TdXo=",
+    "h1:fOYufF+1bzw2N3aHLpkLB6E8VbZ4ysXDODYQOlwhwd4=",
+    "h1:qe8RbnWq0T4xhqjn9QcbO6YW5YDx47P+eJ0NUMIfwCc=",
+    "h1:tRD2av6PafHDP/b9jDQsG5/aX+lHeKxpbIEHYYLBVUc=",
+    "h1:zyl6Gvx/CFpwYW8pFFDesfO8Lxv+a6CopyAsIMhp54s=",
+    "zh:04c0a49c2b23140b2f21cfd0d52f9798d70d3bdae3831613e156aabe519bbc6c",
+    "zh:185f21b4834ba63e8df1f84aa34639d8a7e126429a4007bb5f9ad82f2602a997",
+    "zh:234724f52cb4c0c3f7313d3b2697caef26d921d134f26ae14801e7afac522f7b",
+    "zh:38a56fcd1b3e40706af995611c977816543b53f1e55fe2720944aae2b6828fcb",
+    "zh:419938f5430fc78eff933470aefbf94a460a478f867cf7761a3dea177b4eb153",
+    "zh:4b46d92bfde1deab7de7ba1a6bbf4ba7c711e4fd925341ddf09d4cc28dae03d8",
+    "zh:537acd4a31c752f1bae305ba7190f60b71ad1a459f22d464f3f914336c9e919f",
+    "zh:5ff36b005aad07697dd0b30d4f0c35dbcdc30dc52b41722552060792fa87ce04",
+    "zh:635c5ee419daea098060f794d9d7d999275301181e49562c4e4c08f043076937",
+    "zh:859277c330d61f91abe9e799389467ca11b77131bf34bedbef52f8da68b2bb49",
     "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
-    "zh:924cd23abc326c6b3914e2cd9c94c7832c2552e1e9ae258fb9fd9aedaa5f7ce7",
-    "zh:a4b75e4c239879296259e7d54f1befbc7fdc16da2d62d1294e9f73add4cae61e",
-    "zh:a6ceb08feb63b00c7141783b31e45a154c76fd8cdebbdf371074805f0053572d",
-    "zh:afae1843f9ba85f2f6d94108c65cf43a457e83531a632d44d863e935160cb2ba",
-    "zh:bd6628ce60c778960a5755f7010b7e2cc5c6ff0341a21c175341b28058ec843d",
-    "zh:cd30866a1ff99d72b5fa1699db582fa4f25562e6ab21dcc6870324f3056108e0",
-    "zh:df5924cca691a8220aaaebb5cb55c3d6c32ff0a881f198695eff28155eb12b54",
-    "zh:e78d0696c941aba58df1cb36b8a0d25cd5f3963f01d9338fdbda74db58afdd49",
+    "zh:927dfdb8d9aef37ead03fceaa29e87ba076a3dd24e19b6cefdbb0efe9987ff8c",
+    "zh:bbf2226f07f6b1e721877328e69ded4b64f9c196634d2e2429e3cfabbe41e532",
+    "zh:daeed873d6f38604232b46ee4a5830c85d195b967f8dbcafe2fcffa98daf9c5f",
+    "zh:f8f2fc4646c1ba44085612fa7f4dbb7cbcead43b4e661f2b98ddfb4f68afc758",
   ]
 }
diff --git a/deployment/modules/cloudflare/docs/config.tf b/deployment/modules/cloudflare/docs/config.tf
index 18d8ff1eb4..c5397ea410 100644
--- a/deployment/modules/cloudflare/docs/config.tf
+++ b/deployment/modules/cloudflare/docs/config.tf
@@ -5,7 +5,7 @@ terraform {
   required_providers {
     cloudflare = {
       source = "cloudflare/cloudflare"
-      version = "4.41.0"
+      version = "4.48.0"
     }
   }
 }
diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml
index f42bcc0ab0..5da5bd3f91 100644
--- a/docker/docker-compose.dev.yml
+++ b/docker/docker-compose.dev.yml
@@ -36,13 +36,18 @@ services:
       IMMICH_BUILD_URL: https://github.com/immich-app/immich/actions/runs/9654404849
       IMMICH_BUILD_IMAGE: development
       IMMICH_BUILD_IMAGE_URL: https://github.com/immich-app/immich/pkgs/container/immich-server
+      IMMICH_THIRD_PARTY_SOURCE_URL: https://github.com/immich-app/immich/
+      IMMICH_THIRD_PARTY_BUG_FEATURE_URL: https://github.com/immich-app/immich/issues
+      IMMICH_THIRD_PARTY_DOCUMENTATION_URL: https://immich.app/docs
+      IMMICH_THIRD_PARTY_SUPPORT_URL: https://immich.app/docs/third-party
     ulimits:
       nofile:
         soft: 1048576
         hard: 1048576
     ports:
-      - 3001:3001
       - 9230:9230
+      - 9231:9231
+      - 2283:2283
     depends_on:
       - redis
       - database
@@ -52,16 +57,19 @@ services:
   immich-web:
     container_name: immich_web
     image: immich-web-dev:latest
+    # Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919
+    # user: 0:0
     build:
       context: ../web
     command: ['/usr/src/app/bin/immich-web']
     env_file:
       - .env
     ports:
-      - 2283:3000
+      - 3000:3000
       - 24678:24678
     volumes:
       - ../web:/usr/src/app
+      - ../i18n:/usr/src/i18n
       - ../open-api/:/usr/src/open-api/
       - /usr/src/app/node_modules
     ulimits:
@@ -98,7 +106,7 @@ services:
 
   redis:
     container_name: immich_redis
-    image: redis:6.2-alpine@sha256:2d1463258f2764328496376f5d965f20c6a67f66ea2b06dc42af351f75248792
+    image: redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8
     healthcheck:
       test: redis-cli ping || exit 1
 
@@ -117,28 +125,25 @@ services:
     ports:
       - 5432:5432
     healthcheck:
-      test: pg_isready --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
+      test: >-
+        pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1;
+        Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align
+        --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')";
+        echo "checksum failure count is $$Chksum";
+        [ "$$Chksum" = '0' ] || exit 1
       interval: 5m
       start_interval: 30s
       start_period: 5m
-    command:
-      [
-        'postgres',
-        '-c',
-        'shared_preload_libraries=vectors.so',
-        '-c',
-        'search_path="$$user", public, vectors',
-        '-c',
-        'logging_collector=on',
-        '-c',
-        'max_wal_size=2GB',
-        '-c',
-        'shared_buffers=512MB',
-        '-c',
-        'wal_compression=on',
-      ]
+    command: >-
+      postgres
+      -c shared_preload_libraries=vectors.so
+      -c 'search_path="$$user", public, vectors'
+      -c logging_collector=on
+      -c max_wal_size=2GB
+      -c shared_buffers=512MB
+      -c wal_compression=on
 
-  # set IMMICH_METRICS=true in .env to enable metrics
+  # set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics
   # immich-prometheus:
   #   container_name: immich_prometheus
   #   ports:
diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml
index 05e35ac8c1..8521390079 100644
--- a/docker/docker-compose.prod.yml
+++ b/docker/docker-compose.prod.yml
@@ -16,7 +16,7 @@ services:
     env_file:
       - .env
     ports:
-      - 2283:3001
+      - 2283:2283
     depends_on:
       - redis
       - database
@@ -47,7 +47,7 @@ services:
 
   redis:
     container_name: immich_redis
-    image: redis:6.2-alpine@sha256:2d1463258f2764328496376f5d965f20c6a67f66ea2b06dc42af351f75248792
+    image: redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8
     healthcheck:
       test: redis-cli ping || exit 1
     restart: always
@@ -67,19 +67,31 @@ services:
     ports:
       - 5432:5432
     healthcheck:
-      test: pg_isready --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
+      test: >-
+        pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1;
+        Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align
+        --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')";
+        echo "checksum failure count is $$Chksum";
+        [ "$$Chksum" = '0' ] || exit 1
       interval: 5m
       start_interval: 30s
       start_period: 5m
-    command: ["postgres", "-c", "shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
+    command: >-
+      postgres
+      -c shared_preload_libraries=vectors.so
+      -c 'search_path="$$user", public, vectors'
+      -c logging_collector=on
+      -c max_wal_size=2GB
+      -c shared_buffers=512MB
+      -c wal_compression=on
     restart: always
 
-  # set IMMICH_METRICS=true in .env to enable metrics
+  # set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics
   immich-prometheus:
     container_name: immich_prometheus
     ports:
       - 9090:9090
-    image: prom/prometheus@sha256:f6639335d34a77d9d9db382b92eeb7fc00934be8eae81dbc03b31cfe90411a94
+    image: prom/prometheus@sha256:565ee86501224ebbb98fc10b332fa54440b100469924003359edf49cbce374bd
     volumes:
       - ./prometheus.yml:/etc/prometheus/prometheus.yml
       - prometheus-data:/prometheus
@@ -91,7 +103,7 @@ services:
     command: ['./run.sh', '-disable-reporting']
     ports:
       - 3000:3000
-    image: grafana/grafana:11.2.0-ubuntu@sha256:8e2c13739563c3da9d45de96c6bcb63ba617cac8c571c060112c7fc8ad6914e9
+    image: grafana/grafana:11.4.0-ubuntu@sha256:afccec22ba0e4815cca1d2bf3836e414322390dc78d77f1851976ffa8d61051c
     volumes:
       - grafana-data:/var/lib/grafana
 
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index eec723dc08..4b8453ce58 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -22,7 +22,7 @@ services:
     env_file:
       - .env
     ports:
-      - 2283:3001
+      - '2283:2283'
     depends_on:
       - redis
       - database
@@ -48,7 +48,7 @@ services:
 
   redis:
     container_name: immich_redis
-    image: docker.io/redis:6.2-alpine@sha256:2d1463258f2764328496376f5d965f20c6a67f66ea2b06dc42af351f75248792
+    image: docker.io/redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8
     healthcheck:
       test: redis-cli ping || exit 1
     restart: always
@@ -65,11 +65,23 @@ services:
       # Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file
       - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
     healthcheck:
-      test: pg_isready --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
+      test: >-
+        pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1;
+        Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align
+        --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')";
+        echo "checksum failure count is $$Chksum";
+        [ "$$Chksum" = '0' ] || exit 1
       interval: 5m
       start_interval: 30s
       start_period: 5m
-    command: ["postgres", "-c", "shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
+    command: >-
+      postgres
+      -c shared_preload_libraries=vectors.so
+      -c 'search_path="$$user", public, vectors'
+      -c logging_collector=on
+      -c max_wal_size=2GB
+      -c shared_buffers=512MB
+      -c wal_compression=on
     restart: always
 
 volumes:
diff --git a/docker/hwaccel.transcoding.yml b/docker/hwaccel.transcoding.yml
index bd4e2a46b8..33fb7b3c06 100644
--- a/docker/hwaccel.transcoding.yml
+++ b/docker/hwaccel.transcoding.yml
@@ -51,5 +51,4 @@ services:
     volumes:
       - /usr/lib/wsl:/usr/lib/wsl
     environment:
-      - LD_LIBRARY_PATH=/usr/lib/wsl/lib
       - LIBVA_DRIVER_NAME=d3d12
diff --git a/docs/.nvmrc b/docs/.nvmrc
index 3516580bbb..1d9b7831ba 100644
--- a/docs/.nvmrc
+++ b/docs/.nvmrc
@@ -1 +1 @@
-20.17.0
+22.12.0
diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx
index b1a24e1788..006c515558 100644
--- a/docs/docs/FAQ.mdx
+++ b/docs/docs/FAQ.mdx
@@ -69,7 +69,8 @@ However, Immich will delete original files that have been trashed when the trash
 
 ### Why do my file names appear as a random string in the file manager?
 
-When Storage Template is off (default) Immich saves the file names in a random string (also known as random UUIDs) to prevent duplicate file names. To retrieve the original file names, you must enable the Storage Template and then run the STORAGE TEMPLATE MIGRATION job.
+When Storage Template is off (default) Immich saves the file names in a random string (also known as random UUIDs) to prevent duplicate file names.
+To retrieve the original file names, you must enable the Storage Template and then run the STORAGE TEMPLATE MIGRATION job.
 It is recommended to read about [Storage Template](https://immich.app/docs/administration/storage-template) before activation.
 
 ### Can I add my existing photo library?
@@ -82,11 +83,20 @@ Template changes will only apply to _new_ assets. To retroactively apply the tem
 
 ### Why are only photos and not videos being uploaded to Immich?
 
-This often happens when using a reverse proxy (such as Nginx or Cloudflare tunnel) in front of Immich. Make sure to set your reverse proxy to allow large `POST` requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Also, check the disk space of your reverse proxy. In some cases, proxies cache requests to disk before passing them on, and if disk space runs out, the request fails.
+This often happens when using a reverse proxy in front of Immich.
+Make sure to [set your reverse proxy](/docs/administration/reverse-proxy/) to allow large requests.
+Also, check the disk space of your reverse proxy.
+In some cases, proxies cache requests to disk before passing them on, and if disk space runs out, the request fails.
+
+If you are using Cloudflare Tunnel, please know that they set a maxiumum filesize of 100 MB that cannot be changed.
+At times, files larger than this may work, potentially up to 1 GB. However, the official limit is 100 MB.
+If you are having issues, we recommend switching to a different network deployment.
 
 ### Why are some photos stored in the file system with the wrong date?
 
-There are a few different scenarios that can lead to this situation. The solution is to rerun the storage migration job. The job is only automatically run once per asset after upload. If metadata extraction originally failed, the jobs were cleared/canceled, etc., the job may not have run automatically the first time.
+There are a few different scenarios that can lead to this situation. The solution is to rerun the storage migration job.
+The job is only automatically run once per asset after upload. If metadata extraction originally failed, the jobs were cleared/canceled, etc.,
+the job may not have run automatically the first time.
 
 ### How can I hide photos from the timeline?
 
@@ -116,7 +126,8 @@ Also, there are additional jobs for person (face) thumbnails.
 
 ### Why do files from WhatsApp not appear with the correct date?
 
-Files sent on WhatsApp are saved without metadata on the file. Therefore, Immich has no way of knowing the original date of the file when files are uploaded from WhatsApp, not the order of arrival on the device. [See #3527](https://github.com/immich-app/immich/issues/3527).
+Files sent on WhatsApp are saved without metadata on the file. Therefore, Immich has no way of knowing the original date of the file when files are uploaded from WhatsApp,
+not the order of arrival on the device. [See #9116](https://github.com/immich-app/immich/discussions/9116).
 
 ### What happens if an asset exists in more than one account?
 
@@ -187,7 +198,7 @@ However, when the trash is emptied, the files will re-appear in the main timelin
 
 ### How does smart search work?
 
-Immich uses CLIP models. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip).
+Immich uses CLIP models. An ML model converts each image to an "embedding", which is essentially a string of numbers that semantically encodes what is in the image. The same is done for the text that you enter when you do a search, and that text embedding is then compared with those of the images to find similar ones. As such, there are no "tags", "labels", or "descriptions" generated that you can look at. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip).
 
 ### How does facial recognition work?
 
@@ -308,7 +319,7 @@ Do not exaggerate with the job concurrency because you're probably thoroughly ov
 
 ### My server shows Server Status Offline | Version Unknown. What can I do?
 
-You need to enable WebSockets on your reverse proxy.
+You need to [enable WebSockets](/docs/administration/reverse-proxy/) on your reverse proxy.
 
 ---
 
@@ -333,9 +344,13 @@ You may need to add mount points or docker volumes for the following internal co
 - `immich-machine-learning:/.cache`
 - `redis:/data`
 
-The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION`.
+The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION` and `/cache` for machine-learning.
 
-For a further hardened system, you can add the following block to every container except for `immich_postgres`.
+:::note Docker Compose Volumes
+The Docker Compose top level volume element does not support non-root access, all of the above volumes must be local volume mounts.
+:::
+
+For a further hardened system, you can add the following block to every container.
 
 <details>
 <summary>docker-compose.yml</summary>
@@ -384,22 +399,21 @@ If the error says the worker is exiting, then this is normal. This is a feature
 
 There are a few reasons why this can happen.
 
-If the error mentions SIGKILL or error code 137, it most likely means the service is running out of memory. Consider either increasing the server's RAM or moving the service to a server with more RAM.
+If the error mentions SIGKILL or error code 137, it most likely means the service is running out of memory.
+Consider either increasing the server's RAM or moving the service to a server with more RAM.
 
-If it mentions SIGILL (note the lack of a K) or error code 132, it most likely means your server's CPU is incompatible. This is unlikely to occur on version 1.92.0 or later. Consider upgrading if your version of Immich is below that.
-
-If your version of Immich is below 1.92.0 and the crash occurs after logs about tracing or exporting a model, consider either upgrading or disabling the Tag Objects job.
+If it mentions SIGILL (note the lack of a K) or error code 132, it most likely means your server's CPU is incompatible with Immich.
 
 ## Database
 
 ### Why am I getting database ownership errors?
 
 If you get database errors such as `FATAL:  data directory "/var/lib/postgresql/data" has wrong ownership` upon database startup, this is likely due to an issue with your filesystem.
-NTFS and ex/FAT/32 filesystems are not supported. See [here](/docs/install/environment-variables#supported-filesystems) for more details.
+NTFS and ex/FAT/32 filesystems are not supported. See [here](/docs/install/requirements#special-requirements-for-windows-users) for more details.
 
 ### How can I verify the integrity of my database?
 
-If you installed Immich using v1.104.0 or later, you likely have database checksums enabled by default. You can check this by running the following command.
+Database checksums are enabled by default for new installations since v1.104.0. You can check if they are enabled by running the following command.
 A result of `on` means that checksums are enabled.
 
 <details>
@@ -415,7 +429,7 @@ docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME> --
 
 </details>
 
-If checksums are enabled, you can check the status of the database with the following command. A normal result is all zeroes.
+If checksums are enabled, you can check the status of the database with the following command. A normal result is all `0`s.
 
 <details>
 <summary>Check for database corruption</summary>
diff --git a/docs/docs/administration/backup-and-restore.md b/docs/docs/administration/backup-and-restore.md
index 860b1e1ce7..1f8d489728 100644
--- a/docs/docs/administration/backup-and-restore.md
+++ b/docs/docs/administration/backup-and-restore.md
@@ -15,12 +15,21 @@ Immich saves [file paths in the database](https://github.com/immich-app/immich/d
 Refer to the official [postgres documentation](https://www.postgresql.org/docs/current/backup.html) for details about backing up and restoring a postgres database.
 :::
 
-The recommended way to backup and restore the Immich database is to use the `pg_dumpall` command. When restoring, you need to delete the `DB_DATA_LOCATION` folder (if it exists) to reset the database.
-
 :::caution
 It is not recommended to directly backup the `DB_DATA_LOCATION` folder. Doing so while the database is running can lead to a corrupted backup that cannot be restored.
 :::
 
+### Automatic Database Backups
+
+Immich will automatically create database backups by default. The backups are stored in `UPLOAD_LOCATION/backups`.  
+You can adjust the schedule and amount of kept backups in the [admin settings](http://my.immich.app/admin/system-settings?isOpen=backup).  
+By default, Immich will keep the last 14 backups and create a new backup every day at 2:00 AM.
+
+#### Restoring
+
+We hope to make restoring simpler in future versions, for now you can find the backups in the `UPLOAD_LOCATION/backups` folder on your host.  
+Then please follow the steps in the following section for restoring the database.
+
 ### Manual Backup and Restore
 
 <Tabs>
@@ -34,84 +43,49 @@ docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgre
 docker compose down -v  # CAUTION! Deletes all Immich data to start from scratch
 ## Uncomment the next line and replace DB_DATA_LOCATION with your Postgres path to permanently reset the Postgres database
 # rm -rf DB_DATA_LOCATION # CAUTION! Deletes all Immich data to start from scratch
-docker compose pull     # Update to latest version of Immich (if desired)
-docker compose create   # Create Docker containers for Immich apps without running them
+docker compose pull             # Update to latest version of Immich (if desired)
+docker compose create           # Create Docker containers for Immich apps without running them
 docker start immich_postgres    # Start Postgres server
-sleep 10    # Wait for Postgres server to start up
+sleep 10                        # Wait for Postgres server to start up
+# Check the database user if you deviated from the default
 gunzip < "/path/to/backup/dump.sql.gz" \
 | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \
-| docker exec -i immich_postgres psql --username=postgres    # Restore Backup
-docker compose up -d    # Start remainder of Immich apps
+| docker exec -i immich_postgres psql --username=postgres  # Restore Backup
+docker compose up -d            # Start remainder of Immich apps
 ```
 
 </TabItem>
   <TabItem value="Windows system (PowerShell)" label="Windows system (PowerShell)">
 
 ```powershell title='Backup'
-docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres | Set-Content -Encoding utf8 "C:\path\to\backup\dump.sql"
+[System.IO.File]::WriteAllLines("C:\absolute\path\to\backup\dump.sql", (docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres))
 ```
 
 ```powershell title='Restore'
 docker compose down -v  # CAUTION! Deletes all Immich data to start from scratch
 ## Uncomment the next line and replace DB_DATA_LOCATION with your Postgres path to permanently reset the Postgres database
 # Remove-Item -Recurse -Force DB_DATA_LOCATION # CAUTION! Deletes all Immich data to start from scratch
-docker compose pull     # Update to latest version of Immich (if desired)
-docker compose create   # Create Docker containers for Immich apps without running them
+## You should mount the backup (as a volume, example: - 'C:\path\to\backup\dump.sql':/dump.sql) into the immich_postgres container using the docker-compose.yml
+docker compose pull             # Update to latest version of Immich (if desired)
+docker compose create           # Create Docker containers for Immich apps without running them
 docker start immich_postgres    # Start Postgres server
-sleep 10    # Wait for Postgres server to start up
-gc "C:\path\to\backup\dump.sql" | docker exec -i immich_postgres psql --username=postgres   # Restore Backup
-docker compose up -d    # Start remainder of Immich apps
+sleep 10                        # Wait for Postgres server to start up
+docker exec -it immich_postgres bash    # Enter the Docker shell and run the following command
+# Check the database user if you deviated from the default
+cat "/dump.sql" \
+| sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \
+| psql --username=postgres      # Restore Backup
+exit                            # Exit the Docker shell
+docker compose up -d            # Start remainder of Immich apps
 ```
 
 </TabItem>
 </Tabs>
 
-Note that for the database restore to proceed properly, it requires a completely fresh install (i.e. the Immich server has never run since creating the Docker containers). If the Immich app has run, Postgres conflicts may be encountered upon database restoration (relation already exists, violated foreign key constraints, multiple primary keys, etc.).
+Note that for the database restore to proceed properly, it requires a completely fresh install (i.e. the Immich server has never run since creating the Docker containers). If the Immich app has run, Postgres conflicts may be encountered upon database restoration (relation already exists, violated foreign key constraints, multiple primary keys, etc.), in which case you need to delete the `DB_DATA_LOCATION` folder to reset the database.
 
 :::tip
-Some deployment methods make it difficult to start the database without also starting the server or microservices. In these cases, you may set the environmental variable `DB_SKIP_MIGRATIONS=true` before starting the services. This will prevent the server from running migrations that interfere with the restore process. Note that both the server and microservices must have this variable set to prevent the migrations from running. Be sure to remove this variable and restart the services after the database is restored.
-:::
-
-### Automatic Database Backups
-
-The database dumps can also be automated (using [this image](https://github.com/prodrigestivill/docker-postgres-backup-local)) by editing the docker compose file to match the following:
-
-```yaml
-services:
-  ...
-  backup:
-    container_name: immich_db_dumper
-    image: prodrigestivill/postgres-backup-local:14
-    restart: always
-    env_file:
-      - .env
-    environment:
-      POSTGRES_HOST: database
-      POSTGRES_CLUSTER: 'TRUE'
-      POSTGRES_USER: ${DB_USERNAME}
-      POSTGRES_PASSWORD: ${DB_PASSWORD}
-      POSTGRES_DB: ${DB_DATABASE_NAME}
-      SCHEDULE: "@daily"
-      POSTGRES_EXTRA_OPTS: '--clean --if-exists'
-      BACKUP_DIR: /db_dumps
-    volumes:
-      - ./db_dumps:/db_dumps
-    depends_on:
-      - database
-```
-
-Then you can restore with the same command but pointed at the latest dump.
-
-```bash title='Automated Restore'
-gunzip < db_dumps/last/immich-latest.sql.gz \
-| sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \
-| docker exec -i immich_postgres psql --username=postgres
-```
-
-:::note
-If you see the error `ERROR:  type "earth" does not exist`, or you have problems with Reverse Geocoding after a restore, add the following `sed` fragment to your restore command.
-
-Example: `gunzip < "/path/to/backup/dump.sql.gz" | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | docker exec -i immich_postgres psql --username=postgres`
+Some deployment methods make it difficult to start the database without also starting the server. In these cases, you may set the environment variable `DB_SKIP_MIGRATIONS=true` before starting the services. This will prevent the server from running migrations that interfere with the restore process. Be sure to remove this variable and restart the services after the database is restored.
 :::
 
 ## Filesystem
@@ -197,7 +171,7 @@ When you turn off the storage template engine, it will leave the assets in `UPLO
   - Stored in `UPLOAD_LOCATION/profile/<userID>`.
 - **Thumbs Images:**
   - Preview images (blurred, small, large) for each asset and thumbnails for recognized faces.
-  - Stored in `UPLOCAD_LOCATION/thumbs/<userID>`.
+  - Stored in `UPLOAD_LOCATION/thumbs/<userID>`.
 - **Encoded Assets:**
   - Videos that have been re-encoded from the original for wider compatibility. The original is not removed.
   - Stored in `UPLOAD_LOCATION/encoded-video/<userID>`.
diff --git a/docs/docs/administration/email-notification.mdx b/docs/docs/administration/email-notification.mdx
index 4a2a0b5a83..2f244f3352 100644
--- a/docs/docs/administration/email-notification.mdx
+++ b/docs/docs/administration/email-notification.mdx
@@ -8,16 +8,20 @@ Immich supports the option to send notifications via Email for the following eve
 
 ## SMTP settings
 
-You can access the settings panel from the web at `Administration -> Settings -> Notification settings`
+You can access the settings panel from the web at `Administration -> Settings -> Notification settings`.
 
-Under Email, enter the following details to connect with SMTP servers.
+Under Email, enter the required details to connect with an SMTP server.
 
-You can use the following [guide](/docs/guides/smtp-gmail) to use Gmail's SMTP server.
-
-<img src={require('./img/email-settings.png').default} width="80%" title="SMTP settings" />
+You can use [this guide](/docs/guides/smtp-gmail) to use Gmail's SMTP server.
 
 ## User's notifications settings
 
 Users can manage their email notification settings from their account settings page on the web. They can choose to turn email notifications on or off for the following events:
 
 <img src={require('./img/user-notifications-settings.png').default} width="80%" title="User notification settings" />
+
+## Notification templates
+
+You can override the default notification text with custom templates in HTML format. You can use tags to show dynamic tags in your templates.
+
+<img src={require('./img/user-notifications-templates.png').default} width="80%" title="User notification templates" />
diff --git a/docs/docs/administration/img/email-settings.png b/docs/docs/administration/img/email-settings.png
deleted file mode 100644
index a0d7135426..0000000000
Binary files a/docs/docs/administration/img/email-settings.png and /dev/null differ
diff --git a/docs/docs/administration/img/user-notifications-templates.png b/docs/docs/administration/img/user-notifications-templates.png
new file mode 100644
index 0000000000..150d39b7a6
Binary files /dev/null and b/docs/docs/administration/img/user-notifications-templates.png differ
diff --git a/docs/docs/administration/jobs-workers.md b/docs/docs/administration/jobs-workers.md
index fb5ca7c059..fde39a2e3a 100644
--- a/docs/docs/administration/jobs-workers.md
+++ b/docs/docs/administration/jobs-workers.md
@@ -22,7 +22,7 @@ Copy the entire `immich-server` block as a new service and make the following ch
 -   container_name: immich_server
 ...
 -   ports:
--     - 2283:3001
+-     - 2283:2283
 + immich-microservices:
 +   container_name: immich_microservices
 ```
diff --git a/docs/docs/administration/oauth.md b/docs/docs/administration/oauth.md
index 12cd7502a5..2dc6990944 100644
--- a/docs/docs/administration/oauth.md
+++ b/docs/docs/administration/oauth.md
@@ -11,7 +11,7 @@ Unable to set `app.immich:///oauth-callback` as a valid redirect URI? See [Mobil
 Immich supports 3rd party authentication via [OpenID Connect][oidc] (OIDC), an identity layer built on top of OAuth2. OIDC is supported by most identity providers, including:
 
 - [Authentik](https://goauthentik.io/integrations/sources/oauth/#openid-connect)
-- [Authelia](https://www.authelia.com/configuration/identity-providers/openid-connect/clients/)
+- [Authelia](https://www.authelia.com/integration/openid-connect/immich/)
 - [Okta](https://www.okta.com/openid-connect/)
 - [Google](https://developers.google.com/identity/openid-connect/openid-connect)
 
diff --git a/docs/docs/administration/postgres-standalone.md b/docs/docs/administration/postgres-standalone.md
index b5028c788e..798555975f 100644
--- a/docs/docs/administration/postgres-standalone.md
+++ b/docs/docs/administration/postgres-standalone.md
@@ -13,9 +13,9 @@ Running with a pre-existing Postgres server can unlock powerful administrative f
 You must install pgvecto.rs into your instance of Postgres using their [instructions][vectors-install]. After installation, add `shared_preload_libraries = 'vectors.so'` to your `postgresql.conf`. If you already have some `shared_preload_libraries` set, you can separate each extension with a comma. For example, `shared_preload_libraries = 'pg_stat_statements, vectors.so'`.
 
 :::note
-Immich is known to work with Postgres versions 14, 15, and 16. Earlier versions are unsupported.
+Immich is known to work with Postgres versions 14, 15, and 16. Earlier versions are unsupported. Postgres 17 is nominally compatible, but pgvecto.rs does not have prebuilt images or packages for it as of writing.
 
-Make sure the installed version of pgvecto.rs is compatible with your version of Immich. For example, if your Immich version uses the dedicated database image `tensorchord/pgvecto-rs:pg14-v0.2.1`, you must install pgvecto.rs `>= 0.2.1, < 0.3.0`.
+Make sure the installed version of pgvecto.rs is compatible with your version of Immich. The current accepted range for pgvecto.rs is `>= 0.2.0, < 0.4.0`.
 :::
 
 ## Specifying the connection URL
diff --git a/docs/docs/administration/repair-page.md b/docs/docs/administration/repair-page.md
index f230c6d582..4246c7e39c 100644
--- a/docs/docs/administration/repair-page.md
+++ b/docs/docs/administration/repair-page.md
@@ -1,5 +1,9 @@
 # Repair Page
 
+:::warning
+This feature is currently disabled and will be reworked in the near future.
+:::
+
 The repair page is designed to give information to the system administrator about files that are not tracked, or offline paths.
 
 ## Natural State
diff --git a/docs/docs/administration/reverse-proxy.md b/docs/docs/administration/reverse-proxy.md
index 1d2488f119..25762ad7f1 100644
--- a/docs/docs/administration/reverse-proxy.md
+++ b/docs/docs/administration/reverse-proxy.md
@@ -40,6 +40,26 @@ server {
 }
 ```
 
+#### Compatibility with Let's Encrypt
+
+In the event that your nginx configuration includes a section for Let's Encrypt, it's likely that you have a segment similar to the following:
+
+```nginx
+location ~ /.well-known {
+    ...
+}
+```
+
+This particular `location` directive can inadvertently prevent mobile clients from reaching the `/.well-known/immich` path, which is crucial for discovery. Usual error message for this case is: "Your app major version is not compatible with the server". To remedy this, you should introduce an additional location block specifically for this path, ensuring that requests are correctly proxied to the Immich server:
+
+```nginx
+location = /.well-known/immich {
+    proxy_pass http://<backend_url>:2283;
+}
+```
+
+By doing so, you'll maintain the functionality of Let's Encrypt while allowing mobile clients to access the necessary Immich path without obstruction.
+
 ### Caddy example config
 
 As an alternative to nginx, you can also use [Caddy](https://caddyserver.com/) as a reverse proxy (with automatic HTTPS configuration). Below is an example config.
@@ -64,3 +84,43 @@ Below is an example config for Apache2 site configuration.
    ProxyPreserveHost On
 </VirtualHost>
 ```
+
+### Traefik Proxy example config
+
+The example below is for Traefik version 3.
+
+The most important is to increase the `respondingTimeouts` of the entrypoint used by immich. In this example of entrypoint `websecure` for port `443`. Per default it's set to 60s which leeds to videos stop uploading after 1 minute (Error Code 499). With this config it will fail after 10 minutes which is in most cases enough. Increase it if needed.
+
+`traefik.yaml`
+
+```yaml
+[...]
+entryPoints:
+  websecure:
+    address: :443
+    # this section needs to be added
+    transport:
+      respondingTimeouts:
+        readTimeout: 600s
+        idleTimeout: 600s
+        writeTimeout: 600s
+```
+
+The second part is in the `docker-compose.yml` file where immich is in. Add the Traefik specific labels like in the example.
+
+`docker-compose.yml`
+
+```yaml
+services:
+  immich-server:
+    [...]
+    labels:
+      traefik.enable: true
+      # increase readingTimeouts for the entrypoint used here
+      traefik.http.routers.immich.entrypoints: websecure
+      traefik.http.routers.immich.rule: Host(`immich.your-domain.com`)
+      traefik.http.services.immich.loadbalancer.server.port: 2283
+```
+
+Keep in mind, that Traefik needs to communicate with the network where immich is in, usually done
+by adding the Traefik network to the `immich-server`.
diff --git a/docs/docs/administration/server-stats.md b/docs/docs/administration/server-stats.md
index b77037e4ce..eb5f72a41d 100644
--- a/docs/docs/administration/server-stats.md
+++ b/docs/docs/administration/server-stats.md
@@ -7,7 +7,7 @@ If a storage quota has been defined for the user, the usage number will be displ
 :::
 
 :::info External library
-External library is not included in the storage quota.
+External libraries are not included in the storage quota.
 :::
 
 <img src={require('./img/server-stats.png').default} title="server statistic" />
diff --git a/docs/docs/administration/system-integrity.md b/docs/docs/administration/system-integrity.md
new file mode 100644
index 0000000000..2b373134a9
--- /dev/null
+++ b/docs/docs/administration/system-integrity.md
@@ -0,0 +1,49 @@
+# System Integrity
+
+## Folder checks
+
+:::info
+The folders considered for these checks include: `upload/`, `library/`, `thumbs/`, `encoded-video/`, `profile/`, `backups/`
+:::
+
+When Immich starts, it performs a series of checks in order to validate that it can read and write files to the volume mounts used by the storage system. If it cannot perform all the required operations, it will fail to start. The checks include:
+
+- Creating an initial hidden file (`.immich`) in each folder
+- Reading a hidden file (`.immich`) in each folder
+- Overwriting a hidden file (`.immich`) in each folder
+
+The checks are designed to catch the following situations:
+
+- Incorrect permissions (cannot read/write files)
+- Missing volume mount (`.immich` files should exist, but are missing)
+
+### Common issues
+
+:::note
+`.immich` files serve as markers and help keep track of volume mounts being used by Immich. Except for the situations listed below, they should never be manually created or deleted.
+:::
+
+#### Missing `.immich` files
+
+```
+Verifying system mount folder checks (enabled=true)
+...
+ENOENT: no such file or directory, open 'upload/encoded-video/.immich'
+```
+
+The above error messages show that the server has previously (successfully) written `.immich` files to each folder, but now does not detect them. This could be because any of the following:
+
+- Permission error - unable to read the file, but it exists
+- File does not exist - volume mount has changed and should be corrected
+- File does not exist - user manually deleted it and should be manually re-created (`touch .immich`)
+- File does not exist - user restored from a backup, but did not restore each folder (user should restore all folders or manually create `.immich` in any missing folders)
+
+### Ignoring the checks
+
+:::warning
+The checks are designed to catch common problems that we have seen users have in the past, and often indicate there's something wrong that you should solve. If you know what you're doing and you want to disable them you can set the following environment variable:
+:::
+
+```
+IMMICH_IGNORE_MOUNT_CHECK_ERRORS=true
+```
diff --git a/docs/docs/administration/system-settings.md b/docs/docs/administration/system-settings.md
index 9f35ed1010..d6c219a168 100644
--- a/docs/docs/administration/system-settings.md
+++ b/docs/docs/administration/system-settings.md
@@ -157,6 +157,10 @@ Immich supports [Reverse Geocoding](/docs/features/reverse-geocoding) using data
 
 SMTP server setup, for user creation notifications, new albums, etc. More information can be found [here](/docs/administration/email-notification)
 
+## Notification Templates
+
+Override the default notifications text with notification templates. More information can be found [here](/docs/administration/email-notification)
+
 ## Server Settings
 
 ### External Domain
@@ -205,4 +209,68 @@ When this option is enabled the `immich-server` will periodically make requests
 
 ## Video Transcoding Settings
 
-The system administrator can define parameters according to which video files will be converted to different formats (depending on the settings). The settings can be changed in depth, to learn more about the terminology used here, refer to FFmpeg documentation for [H.264](https://trac.ffmpeg.org/wiki/Encode/H.264) codec, [HEVC](https://trac.ffmpeg.org/wiki/Encode/H.265) codec and [VP9](https://trac.ffmpeg.org/wiki/Encode/VP9) codec.
+The system administrator can configure which video files will be converted to different formats. The settings can be changed in depth, to learn more about the terminology used here, refer to FFmpeg documentation for [H.264](https://trac.ffmpeg.org/wiki/Encode/H.264) codec, [HEVC](https://trac.ffmpeg.org/wiki/Encode/H.265) codec and [VP9](https://trac.ffmpeg.org/wiki/Encode/VP9) codec.
+
+Which streams of a video file will be transcoded is determined by the [Transcode Policy](#ffmpeg.transcode). Streams that are transcoded use the following settings (config file name in brackets). Streams that are not transcoded are untouched and preserve their original settings.
+
+### Accepted containers (`ffmpeg.acceptedContainers`) {#ffmpeg.acceptedContainers}
+
+If the video asset's container format is not in this list, it will be remuxed to MP4 even if no streams need to be transcoded.
+
+The default set of accepted container formats is `mov`, `ogg` and `webm`.
+
+### Preset (`ffmpeg.preset`) {#ffmpeg.preset}
+
+The amount of "compute effort" to put into transcoding. These use [the preset names from h264](https://trac.ffmpeg.org/wiki/Encode/H.264#Preset) and will be converted to appropriate values for encoders that configure effort in different ways.
+
+The default value is `ultrafast`.
+
+### Audio codec (`ffmpeg.targetAudioCodec`) {#ffmpeg.targetAudioCodec}
+
+Which audio codec to use when the audio stream is being transcoded. Can be one of `mp3`, `aac`, `libopus`.
+
+The default value is `aac`.
+
+### Video Codec (`ffmpeg.targetVideoCodec`) {#ffmpeg.targetVideoCodec}
+
+Which video codec to use when the video stream is being transcoded. Can be one of `h264`, `hevc`, `vp9` or `av1`.
+
+The default value is `h264`.
+
+### Target resolution (`ffmpeg.targetResolution`) {#ffmpeg.targetResolution}
+
+When transcoding a video stream, downscale the largest dimension to this value while preserving aspect ratio. Videos are never upscaled.
+
+The default value is `720`.
+
+### Transcode policy (`ffmpeg.transcode`) {#ffmpeg.transcode}
+
+The transcoding policy configures which streams of a video asset will be transcoded. The transcoding decision is made independently for video streams and audio streams. This means that if a video stream needs to be transcoded, but an audio stream does not, then the video stream will be transcoded while the audio stream will be copied. If the transcoding policy does not require any stream to be transcoded and does not require the video to be remuxed, then no separate video file will be created.
+
+The default policy is `required`.
+
+#### All videos (`all`) {#ffmpeg.transcode-all}
+
+Videos are always transcoded. This ensures consistency during video playback.
+
+#### Don't transcode any videos (`disabled`) {#ffmpeg.transcode-disabled}
+
+Videos are never transcoded. This saves space and resources on the server, but may prevent playback on devices that don't support the source format (especially web browsers) or result in high bandwidth usage when playing high-bitrate files.
+
+#### Only videos not in an accepted format (`required`) {#ffmpeg.transcode-required}
+
+Video streams are transcoded when any of the following conditions are met:
+
+- The video is HDR.
+- The video is not in the yuv420p pixel format.
+- The video codec is not in `acceptedVideoCodecs`.
+
+Audio is transcoded if the audio codec is not in `acceptedAudioCodecs`.
+
+#### Videos higher than max bitrate or not in an accepted format (`bitrate`) {#ffmpeg.transcode-bitrate}
+
+In addition to the conditions in `required`, video streams are also transcoded if their bitrate is over `maxBitrate`.
+
+#### Videos higher than target resolution or not in an accepted format (`optimal`) {#ffmpeg.transcode-optimal}
+
+In addition to the conditions in `required`, video streams are also transcoded if the horizontal **and** vertical dimensions are higher than [`targetResolution`](#ffmpeg.targetResolution).
diff --git a/docs/docs/developer/architecture.mdx b/docs/docs/developer/architecture.mdx
index cf004a1119..7b5debef4c 100644
--- a/docs/docs/developer/architecture.mdx
+++ b/docs/docs/developer/architecture.mdx
@@ -3,6 +3,7 @@ sidebar_position: 1
 ---
 
 import AppArchitecture from './img/app-architecture.png';
+import MobileArchitecture from './img/immich_mobile_architecture.svg';
 
 # Architecture
 
@@ -28,7 +29,14 @@ All three clients use [OpenAPI](./open-api.md) to auto-generate rest clients for
 
 ### Mobile App
 
-The mobile app is written in [Flutter](https://flutter.dev/). It uses [Isar Database](https://isar.dev/) for a local database and [Riverpod](https://riverpod.dev/) for state management.
+The mobile app is written in [Dart](https://dart.dev/) using [Flutter](https://flutter.dev/). Below is an architecture overview:
+
+<MobileArchitecture className="p-4 dark:bg-immich-dark-primary my-4" />
+
+The diagrams shows the target architecture, the current state of the code-base is not always following the architecture yet. New code and contributions should follow this architecture.
+Currently, it uses [Isar Database](https://isar.dev/) for a local database and [Riverpod](https://riverpod.dev/) for state management (providers).
+Entities and Models are the two types of data classes used. While entities are stored in the on-device database, models are ephemeral and only kept in memory.
+The Repositories should be the only place where other data classes are used internally (such as OpenAPI DTOs). However, their interfaces must not use foreign data classes!
 
 ### Web Client
 
diff --git a/docs/docs/developer/directories.md b/docs/docs/developer/directories.md
index 3ec483294a..409353e2c4 100644
--- a/docs/docs/developer/directories.md
+++ b/docs/docs/developer/directories.md
@@ -15,7 +15,7 @@ Our [GitHub Repository](https://github.com/immich-app/immich) is a [monorepo](ht
 | `design/`           | Screenshots and logos for the README                                 |
 | `docs/`             | Source code for the [https://immich.app](https://immich.app) website |
 | `machine-learning/` | Source code for the `immich-machine-learning` docker image           |
-| `misc/release/`     | Scripts for version pumps and draft releases                         |
+| `misc/release/`     | Scripts for version bumps and draft releases                         |
 | `mobile/`           | Source code for the mobile app, both Android and iOS                 |
 | `server/`           | Source code for the `immich-server` docker image                     |
 | `web/`              | Source code for the `web`                                            |
diff --git a/docs/docs/developer/img/immich_mobile_architecture.drawio b/docs/docs/developer/img/immich_mobile_architecture.drawio
new file mode 100644
index 0000000000..548cda0938
--- /dev/null
+++ b/docs/docs/developer/img/immich_mobile_architecture.drawio
@@ -0,0 +1,104 @@
+<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36" version="24.7.16">
+  <diagram name="Page-1" id="Bp2gX--FtC4sSMWxsLrs">
+    <mxGraphModel dx="1728" dy="954" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" pageScale="1" pageWidth="850" pageHeight="1100" background="none" math="0" shadow="0">
+      <root>
+        <mxCell id="0" />
+        <mxCell id="1" parent="0" />
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-1" value="" style="verticalLabelPosition=bottom;verticalAlign=top;html=1;shape=mxgraph.basic.polygon;polyCoords=[[0.25,0],[0.75,0],[1,0.25],[1,0.75],[0.75,1],[0.25,1],[0,0.75],[0,0.25]];polyline=0;strokeWidth=4;rounded=1;fillColor=#4251B0;" vertex="1" parent="1">
+          <mxGeometry x="280" y="217.5" width="465" height="465" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-2" value="&lt;b&gt;&lt;font style=&quot;font-size: 22px;&quot;&gt;Mobile App&lt;/font&gt;&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;rounded=1;fontColor=#ffffff;" vertex="1" parent="1">
+          <mxGeometry x="442.5" y="225" width="140" height="40" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-25" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-4" target="zHhczcy2-Jv_nqmJUiNH-5">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-4" value="Services" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFB400;" vertex="1" parent="1">
+          <mxGeometry x="530" y="420" width="80" height="60" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-26" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-5" target="zHhczcy2-Jv_nqmJUiNH-12">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-5" value="Repositories" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1E83F7;" vertex="1" parent="1">
+          <mxGeometry x="650" y="420" width="80" height="60" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-24" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-6" target="zHhczcy2-Jv_nqmJUiNH-4">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-6" value="Providers" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ED79B5;" vertex="1" parent="1">
+          <mxGeometry x="410" y="420" width="80" height="60" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-29" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-7" target="zHhczcy2-Jv_nqmJUiNH-8">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-30" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-7" target="zHhczcy2-Jv_nqmJUiNH-6">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-7" value="Pages" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FA2921;" vertex="1" parent="1">
+          <mxGeometry x="290" y="480" width="80" height="60" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-31" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-8" target="zHhczcy2-Jv_nqmJUiNH-6">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-8" value="Widgets" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FA2921;" vertex="1" parent="1">
+          <mxGeometry x="290" y="360" width="80" height="60" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-11" value="User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;rounded=1;fillColor=#4251B0;" vertex="1" parent="1">
+          <mxGeometry x="180" y="368.5" width="81.5" height="163" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-12" value="platform&lt;div&gt;system&lt;/div&gt;" style="rhombus;whiteSpace=wrap;html=1;rounded=1;fillColor=#ED79B5;" vertex="1" parent="1">
+          <mxGeometry x="800" y="410" width="80" height="80" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-13" value="on-device&lt;div&gt;database&lt;/div&gt;" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;rounded=1;fillColor=#FA2921;" vertex="1" parent="1">
+          <mxGeometry x="810" y="310" width="60" height="80" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-14" value="server" style="ellipse;shape=cloud;whiteSpace=wrap;html=1;rounded=1;fillColor=#FFB400;" vertex="1" parent="1">
+          <mxGeometry x="780" y="500" width="120" height="80" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-16" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0.07;entryY=0.4;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-5" target="zHhczcy2-Jv_nqmJUiNH-14">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-39" value="OpenAPI" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];rounded=1;labelBackgroundColor=#1E83F7;" vertex="1" connectable="0" parent="zHhczcy2-Jv_nqmJUiNH-16">
+          <mxGeometry x="0.0697" y="1" relative="1" as="geometry">
+            <mxPoint x="8" y="10" as="offset" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-23" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-6" target="zHhczcy2-Jv_nqmJUiNH-6">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-27" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=0;entryY=1;entryDx=0;entryDy=-15;entryPerimeter=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-5" target="zHhczcy2-Jv_nqmJUiNH-13">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-34" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;dashed=1;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-3">
+          <mxGeometry relative="1" as="geometry">
+            <mxPoint x="810" y="360" as="targetPoint" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-36" value="" style="endArrow=none;dashed=1;html=1;rounded=1;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-9">
+          <mxGeometry width="50" height="50" relative="1" as="geometry">
+            <mxPoint x="512.08" y="665" as="sourcePoint" />
+            <mxPoint x="512.08" y="265" as="targetPoint" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-37" value="UI part" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontStyle=1;fontSize=14;fontColor=#FFFFFF;" vertex="1" parent="1">
+          <mxGeometry x="387.5" y="640" width="70" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-38" value="non-UI part" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontStyle=1;fontSize=14;fontColor=#FFFFFF;" vertex="1" parent="1">
+          <mxGeometry x="550" y="640" width="90" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-41" value="" style="endArrow=none;dashed=1;html=1;rounded=1;" edge="1" parent="1" target="zHhczcy2-Jv_nqmJUiNH-9">
+          <mxGeometry width="50" height="50" relative="1" as="geometry">
+            <mxPoint x="512.08" y="665" as="sourcePoint" />
+            <mxPoint x="512.08" y="265" as="targetPoint" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-9" value="Models" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#18C249;" vertex="1" parent="1">
+          <mxGeometry x="470" y="510" width="80" height="60" as="geometry" />
+        </mxCell>
+        <mxCell id="zHhczcy2-Jv_nqmJUiNH-3" value="Entities" style="rounded=1;whiteSpace=wrap;html=1;gradientColor=none;fillColor=#18C249;" vertex="1" parent="1">
+          <mxGeometry x="472.5" y="330" width="80" height="60" as="geometry" />
+        </mxCell>
+      </root>
+    </mxGraphModel>
+  </diagram>
+</mxfile>
diff --git a/docs/docs/developer/img/immich_mobile_architecture.svg b/docs/docs/developer/img/immich_mobile_architecture.svg
new file mode 100644
index 0000000000..71f28235bf
--- /dev/null
+++ b/docs/docs/developer/img/immich_mobile_architecture.svg
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="721px" height="469px" viewBox="-0.5 -0.5 721 469"><defs/><g><g data-cell-id="0"><g data-cell-id="1"><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-1"><g><path d="M 216.25 1.5 L 448.75 1.5 L 565 117.75 L 565 350.25 L 448.75 466.5 L 216.25 466.5 L 100 350.25 L 100 117.75 Z" fill="#4251b0" stroke="rgb(0, 0, 0)" stroke-width="4" stroke-miterlimit="10" pointer-events="all"/></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-2"><g><rect x="262.5" y="9" width="140" height="40" rx="6" ry="6" fill="none" stroke="none" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 29px; margin-left: 333px;"><div data-drawio-colors="color: #ffffff; " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(255, 255, 255); line-height: 1.2; pointer-events: all; white-space: nowrap;"><b><font style="font-size: 22px;">Mobile App</font></b></div></div></div></foreignObject><text x="333" y="33" fill="#ffffff" font-family="&quot;Helvetica&quot;" font-size="12px" text-anchor="middle">Mobile App</text></switch></g></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-25"><g><path d="M 430 234 L 463.63 234" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 468.88 234 L 461.88 237.5 L 463.63 234 L 461.88 230.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-4"><g><rect x="350" y="204" width="80" height="60" rx="9" ry="9" fill="#ffb400" stroke="rgb(0, 0, 0)" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 234px; margin-left: 351px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Services</div></div></div></foreignObject><text x="390" y="238" fill="rgb(0, 0, 0)" font-family="&quot;Helvetica&quot;" font-size="12px" text-anchor="middle">Services</text></switch></g></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-26"><g><path d="M 550 234 L 613.63 234" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 618.88 234 L 611.88 237.5 L 613.63 234 L 611.88 230.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-5"><g><rect x="470" y="204" width="80" height="60" rx="9" ry="9" fill="#1e83f7" stroke="rgb(0, 0, 0)" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 234px; margin-left: 471px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Repositories</div></div></div></foreignObject><text x="510" y="238" fill="rgb(0, 0, 0)" font-family="&quot;Helvetica&quot;" font-size="12px" text-anchor="middle">Repositories</text></switch></g></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-24"><g><path d="M 310 234 L 343.63 234" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 348.88 234 L 341.88 237.5 L 343.63 234 L 341.88 230.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-6"><g><rect x="230" y="204" width="80" height="60" rx="9" ry="9" fill="#ed79b5" stroke="rgb(0, 0, 0)" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 234px; margin-left: 231px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Providers</div></div></div></foreignObject><text x="270" y="238" fill="rgb(0, 0, 0)" font-family="&quot;Helvetica&quot;" font-size="12px" text-anchor="middle">Providers</text></switch></g></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-29"><g><path d="M 150 264 L 150 210.37" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 150 205.12 L 153.5 212.12 L 150 210.37 L 146.5 212.12 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-30"><g><path d="M 190 294 L 225.77 253.76" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 229.26 249.84 L 227.22 257.39 L 225.77 253.76 L 221.99 252.74 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-7"><g><rect x="110" y="264" width="80" height="60" rx="9" ry="9" fill="#fa2921" stroke="rgb(0, 0, 0)" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 294px; margin-left: 111px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Pages</div></div></div></foreignObject><text x="150" y="298" fill="rgb(0, 0, 0)" font-family="&quot;Helvetica&quot;" font-size="12px" text-anchor="middle">Pages</text></switch></g></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-31"><g><path d="M 190 174 L 225.77 214.24" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 229.26 218.16 L 221.99 215.26 L 225.77 214.24 L 227.22 210.61 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-8"><g><rect x="110" y="144" width="80" height="60" rx="9" ry="9" fill="#fa2921" stroke="rgb(0, 0, 0)" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 174px; margin-left: 111px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Widgets</div></div></div></foreignObject><text x="150" y="178" fill="rgb(0, 0, 0)" font-family="&quot;Helvetica&quot;" font-size="12px" text-anchor="middle">Widgets</text></switch></g></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-11"><g><ellipse cx="40.75" cy="172.88" rx="20.375" ry="20.375" fill="#4251b0" stroke="rgb(0, 0, 0)" pointer-events="all"/><path d="M 40.75 193.25 L 40.75 261.17 M 40.75 206.83 L 0 206.83 M 40.75 206.83 L 81.5 206.83 M 40.75 261.17 L 0 315.5 M 40.75 261.17 L 81.5 315.5" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 323px; margin-left: 41px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: nowrap;">User</div></div></div></foreignObject><text x="41" y="335" fill="rgb(0, 0, 0)" font-family="&quot;Helvetica&quot;" font-size="12px" text-anchor="middle">User</text></switch></g></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-12"><g><path d="M 640 214 L 652.93 201.07 Q 660 194 667.07 201.07 L 692.93 226.93 Q 700 234 692.93 241.07 L 667.07 266.93 Q 660 274 652.93 266.93 L 627.07 241.07 Q 620 234 627.07 226.93 Z" fill="#ed79b5" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 234px; margin-left: 621px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">platform<div>system</div></div></div></div></foreignObject><text x="660" y="238" fill="rgb(0, 0, 0)" font-family="&quot;Helvetica&quot;" font-size="12px" text-anchor="middle">platform...</text></switch></g></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-13"><g><path d="M 630 109 C 630 100.72 643.43 94 660 94 C 667.96 94 675.59 95.58 681.21 98.39 C 686.84 101.21 690 105.02 690 109 L 690 159 C 690 167.28 676.57 174 660 174 C 643.43 174 630 167.28 630 159 Z" fill="#fa2921" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><path d="M 690 109 C 690 117.28 676.57 124 660 124 C 643.43 124 630 117.28 630 109" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 147px; margin-left: 631px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">on-device<div>database</div></div></div></div></foreignObject><text x="660" y="151" fill="rgb(0, 0, 0)" font-family="&quot;Helvetica&quot;" font-size="12px" text-anchor="middle">on-device...</text></switch></g></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-14"><g><path d="M 630 304 C 606 304 600 324 619.2 328 C 600 336.8 621.6 356 637.2 348 C 648 364 684 364 696 348 C 720 348 720 332 705 324 C 720 308 696 292 675 300 C 660 288 636 288 630 304 Z" fill="#ffb400" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 324px; margin-left: 601px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">server</div></div></div></foreignObject><text x="660" y="328" fill="rgb(0, 0, 0)" font-family="&quot;Helvetica&quot;" font-size="12px" text-anchor="middle">server</text></switch></g></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-16"><g><path d="M 550 249 L 604.22 311.2" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 607.67 315.16 L 600.43 312.18 L 604.22 311.2 L 605.7 307.58 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-39"><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 295px; margin-left: 590px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: #1E83F7; " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(30, 131, 247); white-space: nowrap;">OpenAPI</div></div></div></foreignObject><text x="590" y="298" fill="rgb(0, 0, 0)" font-family="&quot;Helvetica&quot;" font-size="11px" text-anchor="middle">OpenAPI</text></switch></g></g></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-23"><g/></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-27"><g><path d="M 550 219 L 624.91 162.82" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 629.11 159.67 L 625.61 166.67 L 624.91 162.82 L 621.41 161.07 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-34"><g><path d="M 372.5 144 L 623.63 144" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" stroke-dasharray="3 3" pointer-events="stroke"/><path d="M 628.88 144 L 621.88 147.5 L 623.63 144 L 621.88 140.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-36"><g><path d="M 330.23 294 L 332.08 49" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" stroke-dasharray="3 3" pointer-events="stroke"/></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-37"><g><rect x="207.5" y="424" width="70" height="30" fill="none" stroke="none" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 439px; margin-left: 243px;"><div data-drawio-colors="color: #FFFFFF; " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 14px; font-family: Helvetica; color: rgb(255, 255, 255); line-height: 1.2; pointer-events: all; font-weight: bold; white-space: nowrap;">UI part</div></div></div></foreignObject><text x="243" y="443" fill="#FFFFFF" font-family="&quot;Helvetica&quot;" font-size="14px" text-anchor="middle" font-weight="bold">UI part</text></switch></g></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-38"><g><rect x="370" y="424" width="90" height="30" fill="none" stroke="none" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 439px; margin-left: 415px;"><div data-drawio-colors="color: #FFFFFF; " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 14px; font-family: Helvetica; color: rgb(255, 255, 255); line-height: 1.2; pointer-events: all; font-weight: bold; white-space: nowrap;">non-UI part</div></div></div></foreignObject><text x="415" y="443" fill="#FFFFFF" font-family="&quot;Helvetica&quot;" font-size="14px" text-anchor="middle" font-weight="bold">non-UI part</text></switch></g></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-41"><g><path d="M 332.08 449 L 330.5 354" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" stroke-dasharray="3 3" pointer-events="stroke"/></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-9"><g><rect x="290" y="294" width="80" height="60" rx="9" ry="9" fill="#18c249" stroke="rgb(0, 0, 0)" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 324px; margin-left: 291px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Models</div></div></div></foreignObject><text x="330" y="328" fill="rgb(0, 0, 0)" font-family="&quot;Helvetica&quot;" font-size="12px" text-anchor="middle">Models</text></switch></g></g></g><g data-cell-id="zHhczcy2-Jv_nqmJUiNH-3"><g><rect x="292.5" y="114" width="80" height="60" rx="9" ry="9" fill="#18c249" stroke="rgb(0, 0, 0)" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 144px; margin-left: 294px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Entities</div></div></div></foreignObject><text x="333" y="148" fill="rgb(0, 0, 0)" font-family="&quot;Helvetica&quot;" font-size="12px" text-anchor="middle">Entities</text></switch></g></g></g></g></g></g></svg>
\ No newline at end of file
diff --git a/docs/docs/developer/pr-checklist.md b/docs/docs/developer/pr-checklist.md
index d2e7fbee40..58581e669a 100644
--- a/docs/docs/developer/pr-checklist.md
+++ b/docs/docs/developer/pr-checklist.md
@@ -1,5 +1,9 @@
 # PR Checklist
 
+A minimal devcontainer is supplied with this repository. All commands can be executed directly inside this container to avoid tedious installation of the environment.
+:::warning
+The provided devcontainer isn't complete at the moment. At least all dockerized steps in the Makefile won't work (`make dev`, ....). Feel free to contribute!
+:::
 When contributing code through a pull request, please check the following:
 
 ## Web Checks
@@ -7,6 +11,7 @@ When contributing code through a pull request, please check the following:
 - [ ] `npm run lint` (linting via ESLint)
 - [ ] `npm run format` (formatting via Prettier)
 - [ ] `npm run check:svelte` (Type checking via SvelteKit)
+- [ ] `npm run check:typescript` (check typescript)
 - [ ] `npm test` (unit tests)
 
 ## Documentation
diff --git a/docs/docs/developer/setup.md b/docs/docs/developer/setup.md
index 32e79849ef..9dbaf157b5 100644
--- a/docs/docs/developer/setup.md
+++ b/docs/docs/developer/setup.md
@@ -39,13 +39,16 @@ All the services are packaged to run as with single Docker Compose command.
 make dev # required Makefile installed on the system.
 ```
 
-5. Access the dev instance in your browser at http://localhost:2283, or connect via the mobile app.
+5. Access the dev instance in your browser at http://localhost:3000, or connect via the mobile app.
 
 All the services will be started with hot-reloading enabled for a quick feedback loop.
 
-You can access the web from `http://your-machine-ip:2283` or `http://localhost:2283` and access the server from the mobile app at `http://your-machine-ip:2283/api`
+You can access the web from `http://your-machine-ip:3000` or `http://localhost:3000` and access the server from the mobile app at `http://your-machine-ip:3000/api`
 
-**Note:** the "web" development container runs with uid 1000. If that uid does not have read/write permissions on the mounted volumes, you may encounter errors
+**Notes:**
+
+- The "web" development container runs with uid 1000. If that uid does not have read/write permissions on the mounted volumes, you may encounter errors
+- In case of rootless docker setup, you need to use root within the container, otherwise you will encounter read/write permission related errors, see comments in `docker/docker-compose.dev.yml`.
 
 #### Connect web to a remote backend
 
@@ -76,7 +79,7 @@ Setting these in the IDE give a better developer experience, auto-formatting cod
 
 ### Dart Code Metrics
 
-The mobile app uses DCM (Dart Code Metrics) for linting and metrics calculation. Please refer to the [Getting Started](https://dcm.dev/docs/getting-started/#installation) page for more information on setting up DCM
+The mobile app uses DCM (Dart Code Metrics) for linting and metrics calculation. Please refer to the [Getting Started](https://dcm.dev/docs/) page for more information on setting up DCM
 
 Note: Activating the license is not required.
 
diff --git a/docs/docs/features/hardware-transcoding.md b/docs/docs/features/hardware-transcoding.md
index deba45cacc..a561bafa80 100644
--- a/docs/docs/features/hardware-transcoding.md
+++ b/docs/docs/features/hardware-transcoding.md
@@ -1,7 +1,7 @@
 # Hardware Transcoding [Experimental]
 
 This feature allows you to use a GPU to accelerate transcoding and reduce CPU load.
-Note that hardware transcoding is much less efficient for file sizes.
+Note that hardware transcoding produces significantly larger videos than software transcoding with similar settings, typically with lower quality. Using slow presets and preferring more efficient codecs can narrow this gap.
 As this is a new feature, it is still experimental and may not work on all systems.
 
 :::info
@@ -23,7 +23,7 @@ You do not need to redo any transcoding jobs after enabling hardware acceleratio
 - Raspberry Pi is currently not supported.
 - Two-pass mode is only supported for NVENC. Other APIs will ignore this setting.
 - By default, only encoding is currently hardware accelerated. This means the CPU is still used for software decoding and tone-mapping.
-  - NVENC and RKMPP can be fully accelerated by enabling hardware decoding in the video transcoding settings.
+  - You can benefit from end-to-end acceleration by enabling hardware decoding in the video transcoding settings.
 - Hardware dependent
   - Codec support varies, but H.264 and HEVC are usually supported.
     - Notably, NVIDIA and AMD GPUs do not support VP9 encoding.
@@ -49,7 +49,7 @@ For RKMPP to work:
 
 - You must have a supported Rockchip ARM SoC.
 - Only RK3588 supports hardware tonemapping, other SoCs use slower software tonemapping while still using hardware encoding.
-- Tonemapping requires `/usr/lib/aarch64-linux-gnu/libmali.so.1` to be present on your host system. Install [`libmali-valhall-g610-g6p0-gbm`][libmali-rockchip] and modify the [`hwaccel.transcoding.yml`][hw-file] file:
+- Tonemapping requires `/usr/lib/aarch64-linux-gnu/libmali.so.1` to be present on your host system. Install the [`libmali`][libmali-rockchip] release that corresponds to your Mali GPU (`libmali-valhall-g610-g13p0-gbm` on RK3588) and modify the [`hwaccel.transcoding.yml`][hw-file] file:
   - under `rkmpp` uncomment the 3 lines required for OpenCL tonemapping by removing the `#` symbol at the beginning of each line
   - `- /dev/mali0:/dev/mali0`
   - `- /etc/OpenCL:/etc/OpenCL:ro`
@@ -62,11 +62,14 @@ For RKMPP to work:
 1. If you do not already have it, download the latest [`hwaccel.transcoding.yml`][hw-file] file and ensure it's in the same folder as the `docker-compose.yml`.
 2. In the `docker-compose.yml` under `immich-server`, uncomment the `extends` section and change `cpu` to the appropriate backend.
 
-- For VAAPI on WSL2, be sure to use `vaapi-wsl` rather than `vaapi`
+   Note: For VAAPI on WSL2, be sure to use `vaapi-wsl` rather than `vaapi`
 
 3. Redeploy the `immich-server` container with these updated settings.
 4. In the Admin page under `Video transcoding settings`, change the hardware acceleration setting to the appropriate option and save.
-5. (Optional) If using a compatible backend, you may enable hardware decoding for optimal performance.
+
+   Note: For Jasper Lake and Elkhart Lake CPUs, you will need to set the `Hardware Acceleration` -> `Constant quality mode` to `CQP`
+
+5. (Optional) Enable hardware decoding for optimal performance.
 
 #### Single Compose File
 
@@ -89,16 +92,7 @@ immich-server:
   devices:
     - /dev/dri:/dev/dri
   volumes:
-    - ${UPLOAD_LOCATION}:/usr/src/app/upload
-    - /etc/localtime:/etc/localtime:ro
-  env_file:
-    - .env
-  ports:
-    - 2283:3001
-  depends_on:
-    - redis
-    - database
-  restart: always
+  ...
 ```
 
 Once this is done, you can continue to step 3 of "Basic Setup".
diff --git a/docs/docs/features/img/folder-view.png b/docs/docs/features/img/folder-view.png
new file mode 100644
index 0000000000..8193b10ed9
Binary files /dev/null and b/docs/docs/features/img/folder-view.png differ
diff --git a/docs/docs/features/img/mobile-upload-open-photo.png b/docs/docs/features/img/mobile-upload-open-photo.png
new file mode 100644
index 0000000000..4e51826fd7
Binary files /dev/null and b/docs/docs/features/img/mobile-upload-open-photo.png differ
diff --git a/docs/docs/features/img/mobile-upload-selected-photos.png b/docs/docs/features/img/mobile-upload-selected-photos.png
new file mode 100644
index 0000000000..61360842c3
Binary files /dev/null and b/docs/docs/features/img/mobile-upload-selected-photos.png differ
diff --git a/docs/docs/features/libraries.md b/docs/docs/features/libraries.md
index cdea1a11a5..1d6028935f 100644
--- a/docs/docs/features/libraries.md
+++ b/docs/docs/features/libraries.md
@@ -1,18 +1,14 @@
-# Libraries
+# External Libraries
 
-## Overview
+External libraries track assets stored in the filesystem outside of Immich. When the external library is scanned, Immich will load videos and photos from disk and create the corresponding assets. These assets will then be shown in the main timeline, and they will look and behave like any other asset, including viewing on the map, adding to albums, etc. Later, if a file is modified outside of Immich, you need to scan the library for the changes to show up.
 
-Immich supports the creation of libraries which is a top-level asset container. Currently, there are two types of libraries: traditional upload libraries that can sync with a mobile device, and external libraries, that keeps up to date with files on disk. Libraries are different from albums in that an asset can belong to multiple albums but only one library, and deleting a library deletes all assets contained within. As of August 2023, this is a new feature and libraries have a lot of potential for future development beyond what is documented here. This document attempts to describe the current state of libraries.
+If an external asset is deleted from disk, Immich will move it to trash on rescan. To restore the asset, you need to restore the original file. After 30 days the file will be removed from trash, and any changes to metadata within Immich will be lost.
 
-## External Libraries
+:::caution
 
-External libraries tracks assets stored outside of Immich, i.e. in the file system. When the external library is scanned, Immich will read the metadata from the file and create an asset in the library for each image or video file. These items will then be shown in the main timeline, and they will look and behave like any other asset, including viewing on the map, adding to albums, etc.
+If you add metadata to an external asset in any way (i.e. add it to an album or edit the description), that metadata is only stored inside Immich and will not be persisted to the external asset file. If you move an asset to another location within the library all such metadata will be lost upon rescan. This is because the asset is considered a new asset after the move. This is a known issue and will be fixed in a future release.
 
-If a file is modified outside of Immich, the changes will not be reflected in immich until the library is scanned again. There are different ways to scan a library depending on the use case:
-
-- Scan Library Files: This is the default scan method and also the quickest. It will scan all files in the library and add new files to the library. It will notice if any files are missing (see below) but not check existing assets
-- Scan All Library Files: Same as above, but will check each existing asset to see if the modification time has changed. If it has, the asset will be updated. Since it has to check each asset, this is slower than Scan Library Files.
-- Force Scan All Library Files: Same as above, but will read each asset from disk no matter the modification time. This is useful in some cases where an asset has been modified externally but the modification time has not changed. This is the slowest way to scan because it reads each asset from disk.
+:::
 
 :::caution
 
@@ -20,22 +16,6 @@ Due to aggressive caching it can take some time for a refreshed asset to appear
 
 :::
 
-In external libraries, the file path is used for duplicate detection. This means that if a file is moved to a different location, it will be added as a new asset. If the file is moved back to its original location, it will be added as a new asset. In contrast to upload libraries, two identical files can be uploaded if they are in different locations. This is a deliberate design choice to make Immich reflect the file system as closely as possible. Remember that duplication detection is only done within the same library, so if you have multiple external libraries, the same file can be added to multiple libraries.
-
-:::caution
-
-If you add assets from an external library to an album and then move the asset to another location within the library, the asset will be removed from the album upon rescan. This is because the asset is considered a new asset after the move. This is a known issue and will be fixed in a future release.
-
-:::
-
-### Deleted External Assets
-
-Note: Either a manual or scheduled library scan must have been performed to identify offline assets before this process will work.
-
-In all above scan methods, Immich will check if any files are missing. This can happen if files are deleted, or if they are on a storage location that is currently unavailable, like a network drive that is not mounted, or a USB drive that has been unplugged. In order to prevent accidental deletion of assets, Immich will not immediately delete an asset from the library if the file is missing. Instead, the asset will be internally marked as offline and will still be visible in the main timeline. If the file is moved back to its original location and the library is scanned again, the asset will be restored.
-
-Finally, files can be deleted from Immich via the `Remove Offline Files` job. This job can be found by the three dots menu for the associated external storage that was configured under Administration > Libraries (the same location described at [create external libraries](#create-external-libraries)). When this job is run, any assets marked as offline will then be removed from Immich. Run this job whenever files have been deleted from the file system and you want to remove them from Immich.
-
 ### Import Paths
 
 External libraries use import paths to determine which files to scan. Each library can have multiple import paths so that files from different locations can be added to the same library. Import paths are scanned recursively, and if a file is in multiple import paths, it will only be added once. Each import file must be a readable directory that exists on the filesystem; the import path dialog will alert you of any paths that are not accessible.
@@ -66,9 +46,13 @@ Some basic examples:
 - `**/Raw/**` will exclude all files in any directory named `Raw`
 - `**/*.{tif,jpg}` will exclude all files with the extension `.tif` or `.jpg`
 
+Special characters such as @ should be escaped, for instance:
+
+- `**/\@eadir/**` will exclude all files in any directory named `@eadir`
+
 ### Automatic watching (EXPERIMENTAL)
 
-This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan. Deleted assets are, as always, marked as offline and can be removed with the "Remove offline files" button.
+This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan.
 
 If your photos are on a network drive, automatic file watching likely won't work. In that case, you will have to rely on a periodic library refresh to pull in your changes.
 
@@ -84,7 +68,7 @@ In rare cases, the library watcher can hang, preventing Immich from starting up.
 
 ### Nightly job
 
-There is an automatic job that's run once a day and refreshes all modified files in all libraries as well as cleans up any libraries stuck in deletion.
+There is an automatic scan job that is scheduled to run once a day. This job also cleans up any libraries stuck in deletion.
 
 ## Usage
 
@@ -120,7 +104,7 @@ This will disallow the images from being deleted in the web UI, or adding metada
 _Remember to run `docker compose up -d` to register the changes. Make sure you can see the mounted path in the container._
 :::
 
-### Create External Libraries
+### Create A New Library
 
 These actions must be performed by the Immich administrator.
 
@@ -144,7 +128,7 @@ Next, we'll add an exclusion pattern to filter out raw files.
 - Enter `**/Raw/**` and click save.
 - Click save
 - Click the drop-down menu on the newly created library
-- Click on Scan Library Files
+- Click on Scan
 
 The christmas trip library will now be scanned in the background. In the meantime, let's add the videos and old photos to another library.
 
@@ -161,10 +145,26 @@ If you get an error here, please rename the other external library to something
 - Click on Add Path
 - Enter `/mnt/media/videos` then click Add
 - Click Save
-- Click on Scan Library Files
+- Click on Scan
 
 Within seconds, the assets from the old-pics and videos folders should show up in the main timeline.
 
+### Folder view
+
+:::info
+This feature also exists for assets uploaded other than through external libraries.
+:::tip
+You can use the storage template migration feature for the best experience with uploaded assets in this view.
+:::
+
+You can browse your photos and videos by folder like in a file explorer.
+
+Enable this feature from the Users Settings > Features > Folders.
+
+The UI is currently only available for the web; mobile will come in a subsequent release.
+
+<img src={require('./img/folder-view.png').default} width="75%" title='Folder-view' />
+
 ### Set Custom Scan Interval
 
 :::note
diff --git a/docs/docs/features/ml-hardware-acceleration.md b/docs/docs/features/ml-hardware-acceleration.md
index 9f2d33cc35..ca1cb8edb1 100644
--- a/docs/docs/features/ml-hardware-acceleration.md
+++ b/docs/docs/features/ml-hardware-acceleration.md
@@ -53,6 +53,12 @@ You do not need to redo any machine learning jobs after enabling hardware accele
 3. Still in `immich-machine-learning`, add one of -[armnn, cuda, openvino] to the `image` section's tag at the end of the line.
 4. Redeploy the `immich-machine-learning` container with these updated settings.
 
+### Confirming Device Usage
+
+You can confirm the device is being recognized and used by checking its utilization. There are many tools to display this, such as `nvtop` for NVIDIA or Intel and `intel_gpu_top` for Intel.
+
+You can also check the logs of the `immich-machine-learning` container. When a Smart Search or Face Detection job begins, or when you search with text in Immich, you should either see a log for `Available ORT providers` containing the relevant provider (e.g. `CUDAExecutionProvider` in the case of CUDA), or a `Loaded ANN model` log entry without errors in the case of ARM NN.
+
 #### Single Compose File
 
 Some platforms, including Unraid and Portainer, do not support multiple Compose files as of writing. As an alternative, you can "inline" the relevant contents of the [`hwaccel.ml.yml`][hw-file] file into the `immich-machine-learning` service directly.
@@ -95,9 +101,22 @@ immich-machine-learning:
 
 Once this is done, you can redeploy the `immich-machine-learning` container.
 
-:::info
-You can confirm the device is being recognized and used by checking its utilization (via `nvtop` for CUDA, `intel_gpu_top` for OpenVINO, etc.). You can also enable debug logging by setting `IMMICH_LOG_LEVEL=debug` in the `.env` file and restarting the `immich-machine-learning` container. When a Smart Search or Face Detection job begins, you should see a log for `Available ORT providers` containing the relevant provider. In the case of ARM NN, the absence of a `Could not load ANN shared libraries` log entry means it loaded successfully.
-:::
+#### Multi-GPU
+
+If you want to utilize multiple NVIDIA or Intel GPUs, you can set the `MACHINE_LEARNING_DEVICE_IDS` environmental variable to a comma-separated list of device IDs and set `MACHINE_LEARNING_WORKERS` to the number of listed devices. You can run a command such as `nvidia-smi -L` or `glxinfo -B` to see the currently available devices and their corresponding IDs.
+
+For example, if you have devices 0 and 1, set the values as follows:
+
+```
+MACHINE_LEARNING_DEVICE_IDS=0,1
+MACHINE_LEARNING_WORKERS=2
+```
+
+In this example, the machine learning service will spawn two workers, one of which will allocate models to device 0 and the other to device 1. Different requests will be processed by one worker or the other.
+
+This approach can be used to simply specify a particular device as well. For example, setting `MACHINE_LEARNING_DEVICE_IDS=1` will ensure device 1 is always used instead of device 0.
+
+Note that you should increase job concurrencies to increase overall utilization and more effectively distribute work across multiple GPUs. Additionally, each GPU must be able to load all models. It is not possible to distribute a single model to multiple GPUs that individually have insufficient VRAM, or to delegate a specific model to one GPU.
 
 [hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml
 [nvct]: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
diff --git a/docs/docs/features/mobile-app.mdx b/docs/docs/features/mobile-app.mdx
index 2019f8cbcf..3b7f2bb9e2 100644
--- a/docs/docs/features/mobile-app.mdx
+++ b/docs/docs/features/mobile-app.mdx
@@ -1,6 +1,9 @@
+import Icon from '@mdi/react';
+import { mdiCloudOffOutline, mdiCloudCheckOutline } from '@mdi/js';
 import MobileAppDownload from '/docs/partials/_mobile-app-download.md';
 import MobileAppLogin from '/docs/partials/_mobile-app-login.md';
 import MobileAppBackup from '/docs/partials/_mobile-app-backup.md';
+import { cloudDonePath, cloudOffPath } from '@site/src/components/svg-paths';
 
 # Mobile App
 
@@ -27,3 +30,63 @@ The beta release channel allows users to test upcoming changes before they are o
 :::info
 You can enable automatic backup on supported devices. For more information see [Automatic Backup](/docs/features/automatic-backup.md).
 :::
+
+## Sync only selected photos
+
+If you have a large number of photos on the device, and you would prefer not to backup all the photos, then it might be prudent to only backup selected photos from device to the Immich server.
+
+First, you need to enable the Storage Indicator in your app's settings. Navigate to **<ins>Settings -> Photo Grid</ins>** and enable **"Show Storage indicator on asset tiles"**; this makes it easy to distinguish local-only assets and synced assets.
+:::note
+This will enable a small cloud icon on the bottom right corner of the asset tile, indicating that the asset is synced to the server:
+
+1. <Icon path={mdiCloudOffOutline} size={1} /> - Local-only asset; not synced to the server
+2. <Icon path={mdiCloudCheckOutline} size={1} /> - Asset is synced to the server :::
+
+Now make sure that the local album is selected in the backup screen (steps 1-2 above). You can find these albums listed in **<ins>Library -> On this device</ins>**. To selectively upload photos from these albums, simply select the local-only photos and tap on "Upload" button in the dynamic bottom menu.
+
+<img
+  src={require('./img/mobile-upload-open-photo.png').default}
+  width="50%"
+  title="Upload button on local asset preview"
+/>
+<img
+  src={require('./img/mobile-upload-selected-photos.png').default}
+  width="40%"
+  title="Upload button after photos selection"
+/>
+
+## Album Sync
+
+You can sync or mirror an album from your phone to the Immich server on your account. For example, if you select Recents, Camera and Videos album for backup, the corresponding album with the same name will be created on the server. Once the assets from those albums are uploaded, they will be put into the target albums automatically.
+
+### Album Synchronization Highlights
+
+- **One-Way Sync:** Synchronization is one-way, from the device to the server.
+
+- **Name Matching:** If an album on the server has the same name as the album on the device, images from the device will be merged with the existing images in the server album.
+
+- **Shared Albums:** If the matching album on the server is shared, the new photos merged into the album will also be shared.
+
+- **Album Structure:** When an album is created for the first time, its structure is based on the initial state. Future updates made on the phone (such as deleting or repositioning photos) will not be reflected in Immich.
+
+- **User-Specific Sync:** Album synchronization is unique to each server user and does not sync between different users or partners.
+
+- **Mobile-Only Feature:** Album synchronization is currently only available on mobile. For similar options on a computer, refer to [Libraries](/docs/features/libraries) for further details.
+
+### Synchronizing albums from the past
+
+Albums can be synchronized to the server even if they did not exist on the server before. In order to apply this setting you have to:  
+Enter the cloud on the top right -> cog wheel on the top right -> select the sync option under Sync albums.
+
+:::info Sync albums delete/move photos
+If you delete/move photos in the local album on your device, it will not be reflected in the album on the server **even if** you click Sync albums
+It will only reflect files you add.
+:::
+
+If the same asset is in more than one album it will only sync to the first album it's in, after that it won't sync again even if the user clicks sync albums manually.
+To overcome this limitation, the files must be removed from the blacklist by
+App settings -> Advanced -> Duplicate Assets -> Clear
+
+:::info
+Cleaning duplicate assets from the list will cause all the previously uploaded duplicate files to be re-uploaded, the files will not actually be uploaded and will be rejected on the server side (due to duplication) but will be synchronized to the album and at the end will be added to the black list again at the end of the synchronization.
+:::
diff --git a/docs/docs/features/monitoring.md b/docs/docs/features/monitoring.md
index 9de3feb7f6..184394abd0 100644
--- a/docs/docs/features/monitoring.md
+++ b/docs/docs/features/monitoring.md
@@ -25,10 +25,10 @@ The metrics in immich are grouped into API (endpoint calls and response times),
 
 ### Configuration
 
-Immich will not expose an endpoint for metrics by default. To enable this endpoint, you can add the `IMMICH_METRICS=true` environmental variable to your `.env` file. Note that only the server and microservices containers currently use this variable.
+Immich will not expose an endpoint for metrics by default. To enable this endpoint, you can add the `IMMICH_TELEMETRY_INCLUDE=all` environmental variable to your `.env` file. Note that only the server container currently use this variable.
 
 :::tip
-`IMMICH_METRICS` enables all metrics, but there are also [environmental variables](/docs/install/environment-variables.md#prometheus) to toggle specific metric groups. If you'd like to only expose certain kinds of metrics, you can set only those environmental variables to `true`. Explicitly setting the environmental variable for a metric group overrides `IMMICH_METRICS` for that group. For example, setting `IMMICH_METRICS=true` and `IMMICH_API_METRICS=false` will enable all metrics except API metrics.
+`IMMICH_TELEMETRY_INCLUDE=all` enables all metrics. For a more granular configuration you can enumerate the telemetry metrics that should be included as a comma separated list (e.g. `IMMICH_TELEMETRY_INCLUDE=repo,api`). Alternatively, you can also exclude specific metrics with `IMMICH_TELEMETRY_EXCLUDE`. For more information refer to the [environment section](/docs/install/environment-variables.md#prometheus).
 :::
 
 The next step is to configure a new or existing Prometheus instance to scrape this endpoint. The following steps assume that you do not have an existing Prometheus instance, but the steps will be similar either way.
diff --git a/docs/docs/guides/custom-locations.md b/docs/docs/guides/custom-locations.md
index b364cccf83..514008611d 100644
--- a/docs/docs/guides/custom-locations.md
+++ b/docs/docs/guides/custom-locations.md
@@ -1,15 +1,15 @@
 # Files Custom Locations
 
-This guide explains storing generated and raw files with docker's volume mount in different locations.
+This guide explains how to store generated and raw files with docker's volume mount in different locations.
 
 :::caution Backup
 It is important to remember to update the backup settings after following the guide to back up the new backup paths if using automatic backup tools, especially `profile/`.
 :::
 
-In our `.env` file, we will define variables that will help us in the future when we want to move to a more advanced server in the future
+In our `.env` file, we will define variables that will help us in the future when we want to move to a more advanced server
 
 ```diff title=".env"
-# You can find documentation for all the supported env variables [here](/docs/install/environment-variables)
+# You can find documentation for all the supported environment variables [here](/docs/install/environment-variables)
 
 # Custom location where your uploaded, thumbnails, and transcoded video files are stored
 - UPLOAD_LOCATION=./library
@@ -17,10 +17,11 @@ In our `.env` file, we will define variables that will help us in the future whe
 + THUMB_LOCATION=/custom/path/immich/thumbs
 + ENCODED_VIDEO_LOCATION=/custom/path/immich/encoded-video
 + PROFILE_LOCATION=/custom/path/immich/profile
++ BACKUP_LOCATION=/custom/path/immich/backups
 ...
 ```
 
-After defining the locations for these files, we will edit the `docker-compose.yml` file accordingly and add the new variables to the `immich-server` container.
+After defining the locations of these files, we will edit the `docker-compose.yml` file accordingly and add the new variables to the `immich-server` container.
 
 ```diff title="docker-compose.yml"
 services:
@@ -30,6 +31,7 @@ services:
 +     - ${THUMB_LOCATION}:/usr/src/app/upload/thumbs
 +     - ${ENCODED_VIDEO_LOCATION}:/usr/src/app/upload/encoded-video
 +     - ${PROFILE_LOCATION}:/usr/src/app/upload/profile
++     - ${BACKUP_LOCATION}:/usr/src/app/upload/backups
       - /etc/localtime:/etc/localtime:ro
 ```
 
@@ -41,12 +43,11 @@ docker compose up -d
 
 :::note
 Because of the underlying properties of docker bind mounts, it is not recommended to mount the `upload/` and `library/` folders as separate bind mounts if they are on the same device.
-For this reason, we mount the HDD or network storage to `/usr/src/app/upload` and then mount the folders we want quick access to below this folder.
+For this reason, we mount the HDD or the network storage (NAS) to `/usr/src/app/upload` and then mount the folders we want to access under that folder.
 
-The `thumbs/` folder contains both the small thumbnails shown in the timeline, and the larger previews shown when clicking into an image. These cannot be split up.
+The `thumbs/` folder contains both the small thumbnails displayed in the timeline and the larger previews shown when clicking into an image. These cannot be separated.
 
-The storage metrics of the Immich server will track the storage available at `UPLOAD_LOCATION`,
-so the administrator should setup some kind of monitoring to make sure the SSD does not run out of space. The `profile/` folder is much smaller, typically less than 1 MB.
+The storage metrics of the Immich server will track available storage at `UPLOAD_LOCATION`, so the administrator must set up some sort of monitoring to ensure the storage does not run out of space. The `profile/` folder is much smaller, usually less than 1 MB.
 :::
 
 Thanks to [Jrasm91](https://github.com/immich-app/immich/discussions/2110#discussioncomment-5477767) for writing the guide.
diff --git a/docs/docs/guides/database-queries.md b/docs/docs/guides/database-queries.md
index 2b4f27cfce..0e58d84f90 100644
--- a/docs/docs/guides/database-queries.md
+++ b/docs/docs/guides/database-queries.md
@@ -98,6 +98,10 @@ SELECT * FROM "move_history";
 SELECT * FROM "users";
 ```
 
+```sql title="Get owner info from asset ID"
+SELECT "users".* FROM "users" JOIN "assets" ON "users"."id" = "assets"."ownerId" WHERE "assets"."id" = 'fa310b01-2f26-4b7a-9042-d578226e021f';
+```
+
 ## System Config
 
 ```sql title="Custom settings"
diff --git a/docs/docs/guides/remote-access.md b/docs/docs/guides/remote-access.md
index 1ea068c3a0..6f401dfc5a 100644
--- a/docs/docs/guides/remote-access.md
+++ b/docs/docs/guides/remote-access.md
@@ -27,7 +27,7 @@ You may use a VPN service to open an encrypted connection to your Immich instanc
 
 If you are unable to open a port on your router for Wireguard or OpenVPN to your server, [Tailscale](https://tailscale.com/) is a good option. Tailscale mediates a peer-to-peer wireguard tunnel between your server and remote device, even if one or both of them are behind a [NAT firewall](https://en.wikipedia.org/wiki/Network_address_translation).
 
-:::tip Video toturial
+:::tip Video tutorial
 You can learn how to set up Tailscale together with Immich with the [tutorial video](https://www.youtube.com/watch?v=Vt4PDUXB_fg) they created.
 :::
 
diff --git a/docs/docs/guides/remote-machine-learning.md b/docs/docs/guides/remote-machine-learning.md
index 4dbb72a408..1abf7d4e54 100644
--- a/docs/docs/guides/remote-machine-learning.md
+++ b/docs/docs/guides/remote-machine-learning.md
@@ -1,18 +1,20 @@
 # Remote Machine Learning
 
-To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-immich-slow-on-low-memory-systems-like-the-raspberry-pi) like the Raspberry Pi, you may also host Immich's machine-learning container on a more powerful system (e.g. your laptop or desktop computer):
-
-- Set the URL in Machine Learning Settings on the Admin Settings page to point to the designated ML system, e.g. `http://workstation:3003`.
-- Copy the following `docker-compose.yml` to your ML system.
-  - If using [hardware acceleration](/docs/features/ml-hardware-acceleration), the [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml) file also needs to be added
-- Start the container by running `docker compose up -d`.
+To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-immich-slow-on-low-memory-systems-like-the-raspberry-pi) like the Raspberry Pi, you may also host Immich's machine learning container on a more powerful system, such as your laptop or desktop computer. The server container will send requests containing the image preview to the remote machine learning container for processing. The machine learning container does not persist this data or associate it with a particular user.
 
 :::info
-Smart Search and Face Detection will use this feature, but Facial Recognition is handled in the server.
+Smart Search and Face Detection will use this feature, but Facial Recognition will not. This is because Facial Recognition uses the _outputs_ of these models that have already been saved to the database. As such, its processing is between the server container and the database.
 :::
 
 :::danger
-When using remote machine learning, the thumbnails are sent to the remote machine learning container. Use this option carefully when running this on a public computer or a paid processing cloud.
+Image previews are sent to the remote machine learning container. Use this option carefully when running this on a public computer or a paid processing cloud. Additionally, as an internal service, the machine learning container has no security measures whatsoever. Please be mindful of where it's deployed and who can access it.
+:::
+
+1. Ensure the remote server has Docker installed
+2. Copy the following `docker-compose.yml` to the remote server
+
+:::info
+If using hardware acceleration, the [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml) file also needs to be added and the `docker-compose.yml` needs to be configured as described in the [hardware acceleration documentation](/docs/features/ml-hardware-acceleration)
 :::
 
 ```yaml
@@ -37,8 +39,26 @@ volumes:
   model-cache:
 ```
 
-Please note that version mismatches between both hosts may cause instabilities and bugs, so make sure to always perform updates together.
+3. Start the remote machine learning container by running `docker compose up -d`
 
-:::caution
-As an internal service, the machine learning container has no security measures whatsoever. Please be mindful of where it's deployed and who can access it.
+:::info
+Version mismatches between both hosts may cause bugs and instability, so remember to update this container as well when updating the local Immich instance.
+:::
+
+4. Navigate to the [Machine Learning Settings](https://my.immich.app/admin/system-settings?isOpen=machine-learning)
+5. Click _Add URL_
+6. Fill the new field with the URL to the remote machine learning container, e.g. `http://ip:port`
+
+## Forcing remote processing
+
+Adding a new URL to the settings is recommended over replacing the existing URL. This is because it will allow machine learning tasks to be processed successfully when the remote server is down by falling back to the local machine learning container. If you do not want machine learning tasks to be processed locally when the remote server is not available, you can instead replace the existing URL and only provide the remote container's URL. If doing this, you can remove the `immich-machine-learning` section of the local `docker-compose.yml` file to save resources, as this service will never be used.
+
+Do note that this will mean that Smart Search and Face Detection jobs will fail to be processed when the remote instance is not available. This in turn means that tasks dependent on these features—Duplicate Detection and Facial Recognition—will not run for affected assets. If this occurs, you must manually click the _Missing_ button next to Smart Search and Face Detection in the [Job Status](http://my.immich.app/admin/jobs-status) page for the jobs to be retried.
+
+## Load balancing
+
+While several URLs can be provided in the settings, they are tried sequentially; there is no attempt to distribute load across multiple containers. It is recommended to use a dedicated load balancer for such use-cases and specify it as the only URL. Among other things, it may enable the use of different APIs on the same server by running multiple containers with different configurations. For example, one might run an OpenVINO container in addition to a CUDA container, or run a standard release container to maximize both CPU and GPU utilization.
+
+:::tip
+The machine learning container can be shared among several Immich instances regardless of the models a particular instance uses. However, using different models will lead to higher peak memory usage.
 :::
diff --git a/docs/docs/guides/template-backup-script.md b/docs/docs/guides/template-backup-script.md
index 03c1a7a02b..dd0b94ebb1 100644
--- a/docs/docs/guides/template-backup-script.md
+++ b/docs/docs/guides/template-backup-script.md
@@ -6,10 +6,19 @@ This script assumes you have a second hard drive connected to your server for on
 
 The database is saved to your Immich upload folder in the `database-backup` subdirectory. The database is then backed up and versioned with your assets by Borg. This ensures that the database backup is in sync with your assets in every snapshot.
 
+:::info
+This script makes backups of your database along with your photo/video library. This is redundant with the [automatic database backup tool](https://immich.app/docs/administration/backup-and-restore#automatic-database-backups) built into Immich. Using this script to backup your database has two advantages over the built-in backup tool:
+
+- This script uses storage more efficiently by versioning your backups instead of making multiple copies.
+- The database backups are performed at the same time as the library backup, ensuring that the backups of your database and the library are always in sync.
+
+If you are using this script, it is therefore safe to turn off the built-in automatic database backups from your admin panel to save storage space.
+:::
+
 ### Prerequisites
 
 - Borg needs to be installed on your server as well as the remote machine. You can find instructions to install Borg [here](https://borgbackup.readthedocs.io/en/latest/installation.html).
-- (Optional) To run this sript as a non-root user, you should [add your username to the docker group](https://docs.docker.com/engine/install/linux-postinstall/).
+- (Optional) To run this script as a non-root user, you should [add your username to the docker group](https://docs.docker.com/engine/install/linux-postinstall/).
 - To run this script non-interactively, set up [passwordless ssh](https://www.redhat.com/sysadmin/passwordless-ssh) to your remote machine from your server. If you skipped the previous step, make sure this step is done from your root account.
 
 To initialize the borg repository, run the following commands once.
diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md
index abbba8c6b3..f5d2680658 100644
--- a/docs/docs/install/config-file.md
+++ b/docs/docs/install/config-file.md
@@ -19,20 +19,28 @@ The default configuration looks like this:
     "targetVideoCodec": "h264",
     "acceptedVideoCodecs": ["h264"],
     "targetAudioCodec": "aac",
-    "acceptedAudioCodecs": ["aac", "mp3", "libopus"],
+    "acceptedAudioCodecs": ["aac", "mp3", "libopus", "pcm_s16le"],
+    "acceptedContainers": ["mov", "ogg", "webm"],
     "targetResolution": "720",
     "maxBitrate": "0",
     "bframes": -1,
     "refs": 0,
     "gopSize": 0,
-    "npl": 0,
     "temporalAQ": false,
     "cqMode": "auto",
     "twoPass": false,
     "preferredHwDevice": "auto",
     "transcode": "required",
     "tonemap": "hable",
-    "accel": "disabled"
+    "accel": "disabled",
+    "accelDecode": false
+  },
+  "backup": {
+    "database": {
+      "enabled": true,
+      "cronExpression": "0 02 * * *",
+      "keepLastAmount": 14
+    }
   },
   "job": {
     "backgroundTask": {
@@ -60,10 +68,13 @@ The default configuration looks like this:
       "concurrency": 5
     },
     "thumbnailGeneration": {
-      "concurrency": 5
+      "concurrency": 3
     },
     "videoConversion": {
       "concurrency": 1
+    },
+    "notifications": {
+      "concurrency": 5
     }
   },
   "logging": {
@@ -72,46 +83,52 @@ The default configuration looks like this:
   },
   "machineLearning": {
     "enabled": true,
-    "url": "http://immich-machine-learning:3003",
+    "urls": ["http://immich-machine-learning:3003"],
     "clip": {
       "enabled": true,
       "modelName": "ViT-B-32__openai"
     },
     "duplicateDetection": {
-      "enabled": false,
-      "maxDistance": 0.03
+      "enabled": true,
+      "maxDistance": 0.01
     },
     "facialRecognition": {
       "enabled": true,
       "modelName": "buffalo_l",
       "minScore": 0.7,
-      "maxDistance": 0.6,
+      "maxDistance": 0.5,
       "minFaces": 3
     }
   },
   "map": {
     "enabled": true,
-    "lightStyle": "",
-    "darkStyle": ""
+    "lightStyle": "https://tiles.immich.cloud/v1/style/light.json",
+    "darkStyle": "https://tiles.immich.cloud/v1/style/dark.json"
   },
   "reverseGeocoding": {
     "enabled": true
   },
+  "metadata": {
+    "faces": {
+      "import": false
+    }
+  },
   "oauth": {
-    "enabled": false,
-    "issuerUrl": "",
+    "autoLaunch": false,
+    "autoRegister": true,
+    "buttonText": "Login with OAuth",
     "clientId": "",
     "clientSecret": "",
+    "defaultStorageQuota": 0,
+    "enabled": false,
+    "issuerUrl": "",
+    "mobileOverrideEnabled": false,
+    "mobileRedirectUri": "",
     "scope": "openid email profile",
     "signingAlgorithm": "RS256",
+    "profileSigningAlgorithm": "none",
     "storageLabelClaim": "preferred_username",
-    "storageQuotaClaim": "immich_quota",
-    "defaultStorageQuota": 0,
-    "buttonText": "Login with OAuth",
-    "autoRegister": true,
-    "autoLaunch": false,
-    "mobileOverrideEnabled": false,
-    "mobileRedirectUri": ""
+    "storageQuotaClaim": "immich_quota"
   },
   "passwordLogin": {
     "enabled": true
@@ -122,11 +139,16 @@ The default configuration looks like this:
     "template": "{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}"
   },
   "image": {
-    "thumbnailFormat": "webp",
-    "thumbnailSize": 250,
-    "previewFormat": "jpeg",
-    "previewSize": 1440,
-    "quality": 80,
+    "thumbnail": {
+      "format": "webp",
+      "size": 250,
+      "quality": 80
+    },
+    "preview": {
+      "format": "jpeg",
+      "size": 1440,
+      "quality": 80
+    },
     "colorspace": "p3",
     "extractEmbedded": false
   },
@@ -140,23 +162,35 @@ The default configuration looks like this:
   "theme": {
     "customCss": ""
   },
-  "user": {
-    "deleteDelay": 7
-  },
   "library": {
     "scan": {
       "enabled": true,
       "cronExpression": "0 0 * * *"
     },
     "watch": {
-      "enabled": false,
-      "usePolling": false,
-      "interval": 10000
+      "enabled": false
     }
   },
   "server": {
     "externalDomain": "",
     "loginPageMessage": ""
+  },
+  "notifications": {
+    "smtp": {
+      "enabled": false,
+      "from": "",
+      "replyTo": "",
+      "transport": {
+        "ignoreCert": false,
+        "host": "",
+        "port": 587,
+        "username": "",
+        "password": ""
+      }
+    }
+  },
+  "user": {
+    "deleteDelay": 7
   }
 }
 ```
diff --git a/docs/docs/install/docker-compose.mdx b/docs/docs/install/docker-compose.mdx
index 9ef63523a0..e3d5dd6864 100644
--- a/docs/docs/install/docker-compose.mdx
+++ b/docs/docs/install/docker-compose.mdx
@@ -7,10 +7,9 @@ import ExampleEnv from '!!raw-loader!../../../docker/example.env';
 
 # Docker Compose [Recommended]
 
-Docker Compose is the recommended method to run Immich in production. Below are the steps to deploy Immich with Docker Compose.  
-Immich requires Docker Compose version 2.x.
+Docker Compose is the recommended method to run Immich in production. Below are the steps to deploy Immich with Docker Compose.
 
-### Step 1 - Download the required files
+## Step 1 - Download the required files
 
 Create a directory of your choice (e.g. `./immich-app`) to hold the `docker-compose.yml` and `.env` files.
 
@@ -19,7 +18,7 @@ mkdir ./immich-app
 cd ./immich-app
 ```
 
-Download [`docker-compose.yml`][compose-file] and [`example.env`][env-file], either by running the following commands:
+Download [`docker-compose.yml`][compose-file] and [`example.env`][env-file] by running the following commands:
 
 ```bash title="Get docker-compose.yml file"
 wget -O docker-compose.yml https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
@@ -29,6 +28,11 @@ wget -O docker-compose.yml https://github.com/immich-app/immich/releases/latest/
 wget -O .env https://github.com/immich-app/immich/releases/latest/download/example.env
 ```
 
+You can alternatively download these two files from your browser and move them to the directory that you created, in which case ensure that you rename `example.env` to `.env`.
+
+:::info Optional Features
+If you intend to use hardware acceleration for transcoding or machine learning (ML), you can download now the config files you'll need, in the same way:
+
 ```bash title="(Optional) Get hwaccel.transcoding.yml file"
 wget -O hwaccel.transcoding.yml https://github.com/immich-app/immich/releases/latest/download/hwaccel.transcoding.yml
 ```
@@ -37,15 +41,9 @@ wget -O hwaccel.transcoding.yml https://github.com/immich-app/immich/releases/la
 wget -O hwaccel.ml.yml https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml
 ```
 
-or by downloading from your browser and moving the files to the directory that you created.
-
-Note: If you downloaded the files from your browser, also ensure that you rename `example.env` to `.env`.
-
-:::info
-Optionally, you can enable hardware acceleration for machine learning and transcoding. See the [Hardware Transcoding](/docs/features/hardware-transcoding.md) and [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md) guides for info on how to set these up.
 :::
 
-### Step 2 - Populate the .env file with custom values
+## Step 2 - Populate the .env file with custom values
 
 <details>
   <summary>
@@ -54,30 +52,41 @@ Optionally, you can enable hardware acceleration for machine learning and transc
   <CodeBlock language="bash">{ExampleEnv}</CodeBlock>
 </details>
 
-- Populate custom database information if necessary.
-- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets.
+- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets. It should be a new directory on the server with enough free space.
 - Consider changing `DB_PASSWORD` to a custom value. Postgres is not publically exposed, so this password is only used for local authentication.
-  To avoid issues with Docker parsing this value, it is best to use only the characters `A-Za-z0-9`.
+  To avoid issues with Docker parsing this value, it is best to use only the characters `A-Za-z0-9`. `pwgen` is a handy utility for this.
+- Set your timezone by uncommenting the `TZ=` line.
+- Populate custom database information if necessary.
 
-### Step 3 - Start the containers
+:::info Optional Features
+You can edit `docker-compose.yml` to add external libraries or enable hardware acceleration now by following [their guides](#setting-up-optional-features).
+:::
 
-From the directory you created in Step 1, (which should now contain your customized `docker-compose.yml` and `.env` files) run `docker compose up -d`.
+## Step 3 - Start the containers
+
+From the directory you created in Step 1 (which should now contain your customized `docker-compose.yml` and `.env` files), run this command:
 
 ```bash title="Start the containers using docker compose command"
 docker compose up -d
 ```
 
-:::info Docker version
-If you get an error `unknown shorthand flag: 'd' in -d`, you are probably running the wrong Docker version. (This happens, for example, with the docker.io package in Ubuntu 22.04.3 LTS.) You can correct the problem by `apt remove`ing Ubuntu's docker.io package and installing docker and docker-compose via [Docker's official repository][docker-repo].
+This starts immich as a background service (per the `-d` flag), ensuring it restarts after system reboots or crashes (per the `restart` fields in `docker-compose.yml`).
 
-Note that the correct command really is `docker compose`, not `docker-compose`. If you try the latter on vanilla Ubuntu 22.04 it will fail in a different way:
+:::info Docker version
+If you get an error `unknown shorthand flag: 'd' in -d`, you are probably running the wrong Docker version. (This happens, for example, with the docker.io package in Ubuntu 22.04.3 LTS.) You can correct the problem by following the complete [Docker Engine install](https://docs.docker.com/engine/install/) procedure for your distribution, crucially the "Uninstall old versions" and "Install using the apt/rpm repository" sections. These replace the distro's Docker packages with Docker's official ones.
+
+Note that the correct command really is `docker compose`, not `docker-compose`. If you try the latter on vanilla Ubuntu 22.04, it will fail in a different way:
 
 ```
 The Compose file './docker-compose.yml' is invalid because:
 'name' does not match any of the regexes: '^x-'
 ```
 
-See the previous paragraph about installing from the official docker repository.
+See the previous paragraph about installing from the official Docker repository.
+:::
+
+:::info Health check start interval
+If you get an error `can't set healthcheck.start_interval as feature require Docker Engine v25 or later`, it helps to comment out the line for `start_interval` in the `database` section of the `docker-compose.yml` file.
 :::
 
 :::tip
@@ -88,7 +97,17 @@ For more information on how to use the application, please refer to the [Post In
 Downloading container images might require you to authenticate to the GitHub Container Registry ([steps here][container-auth]).
 :::
 
-### Step 4 - Upgrading
+## Next Steps
+
+### Setting Up Optional Features
+
+You can set up the following now:
+
+- [External Libraries](/docs/features/libraries.md)
+- [Hardware Transcoding](/docs/features/hardware-transcoding.md)
+- [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md)
+
+### Upgrading
 
 :::danger Breaking Changes
 It is important to follow breaking updates to avoid problems. You can see versions that had breaking changes [here][breaking].
@@ -96,12 +115,18 @@ It is important to follow breaking updates to avoid problems. You can see versio
 
 If `IMMICH_VERSION` is set, it will need to be updated to the latest or desired version.
 
-When a new version of Immich is [released][releases], the application can be upgraded with the following commands, run in the directory with the `docker-compose.yml` file:
+When a new version of Immich is [released][releases], the application can be upgraded and restarted with the following commands, run in the directory with the `docker-compose.yml` file:
 
-```bash title="Upgrade Immich"
+```bash title="Upgrade and restart Immich"
 docker compose pull && docker compose up -d
 ```
 
+To clean up disk space, the old version's obsolete container images can be deleted with the following command:
+
+```bash title="Delete all obsolete container images"
+docker image prune
+```
+
 :::caution Automatic Updates
 Immich is currently under heavy development, which means you can expect [breaking changes][breaking] and bugs. Therefore, we recommend reading the release notes prior to updating and to take special care when using automated tools like [Watchtower][watchtower].
 :::
@@ -112,4 +137,3 @@ Immich is currently under heavy development, which means you can expect [breakin
 [breaking]: https://github.com/immich-app/immich/discussions?discussions_q=label%3Achangelog%3Abreaking-change+sort%3Adate_created
 [container-auth]: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry
 [releases]: https://github.com/immich-app/immich/releases
-[docker-repo]: https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository
diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md
index a0cf71e044..1f34b5c6d0 100644
--- a/docs/docs/install/environment-variables.md
+++ b/docs/docs/install/environment-variables.md
@@ -27,23 +27,14 @@ If this should not work, try running `docker compose up -d --force-recreate`.
 These environment variables are used by the `docker-compose.yml` file and do **NOT** affect the containers directly.
 :::
 
-### Supported filesystems
-
-The Immich Postgres database (`DB_DATA_LOCATION`) must be located on a filesystem that supports user/group
-ownership and permissions (EXT2/3/4, ZFS, APFS, BTRFS, XFS, etc.). It will not work on any filesystem formatted in NTFS or ex/FAT/32.
-It will not work in WSL (Windows Subsystem for Linux) when using a mounted host directory (commonly under `/mnt`).
-If this is an issue, you can change the bind mount to a Docker volume instead.
-
-Regardless of filesystem, it is not recommended to use a network share for your database location due to performance and possible data loss issues.
-
 ## General
 
 | Variable                            | Description                                                                               |           Default            | Containers               | Workers            |
 | :---------------------------------- | :---------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
-| `TZ`                                | Timezone                                                                                  |                              | server                   | microservices      |
+| `TZ`                                | Timezone                                                                                  |        <sup>\*1</sup>        | server                   | microservices      |
 | `IMMICH_ENV`                        | Environment (production, development)                                                     |         `production`         | server, machine learning | api, microservices |
 | `IMMICH_LOG_LEVEL`                  | Log Level (verbose, debug, log, warn, error)                                              |            `log`             | server, machine learning | api, microservices |
-| `IMMICH_MEDIA_LOCATION`             | Media Location inside the container ⚠️**You probably shouldn't set this**<sup>\*1</sup>⚠️ |   `./upload`<sup>\*2</sup>   | server                   | api, microservices |
+| `IMMICH_MEDIA_LOCATION`             | Media Location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ |   `./upload`<sup>\*3</sup>   | server                   | api, microservices |
 | `IMMICH_CONFIG_FILE`                | Path to config file                                                                       |                              | server                   | api, microservices |
 | `NO_COLOR`                          | Set to `true` to disable color-coded log output                                           |           `false`            | server, machine learning |                    |
 | `CPU_CORES`                         | Amount of cores available to the immich server                                            | auto-detected cpu core count | server                   |                    |
@@ -51,17 +42,15 @@ Regardless of filesystem, it is not recommended to use a network share for your
 | `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics                                                                 |            `8082`            | server                   | microservices      |
 | `IMMICH_PROCESS_INVALID_IMAGES`     | When `true`, generate thumbnails for invalid images                                       |                              | server                   | microservices      |
 | `IMMICH_TRUSTED_PROXIES`            | List of comma separated IPs set as trusted proxies                                        |                              | server                   | api                |
+| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS`  | See [System Integrity](/docs/administration/system-integrity)                             |                              | server                   | api, microservices |
 
-\*1: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead.
-
-\*2: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`.
-It only need to be set if the Immich deployment method is changing.
-
-:::tip
-`TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
-
+\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
 `TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
-:::
+
+\*2: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead.
+
+\*3: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`.
+It only need to be set if the Immich deployment method is changing.
 
 ## Workers
 
@@ -79,7 +68,7 @@ Information on the current workers can be found [here](/docs/administration/jobs
 | Variable      | Description    |                  Default                   |
 | :------------ | :------------- | :----------------------------------------: |
 | `IMMICH_HOST` | Listening host |                 `0.0.0.0`                  |
-| `IMMICH_PORT` | Listening port | `3001` (server), `3003` (machine learning) |
+| `IMMICH_PORT` | Listening port | `2283` (server), `3003` (machine learning) |
 
 ## Database
 
@@ -159,22 +148,24 @@ Redis (Sentinel) URL example JSON before encoding:
 
 ## Machine Learning
 
-| Variable                                                  | Description                                                                                         |                Default                | Containers       |
-| :-------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | :-----------------------------------: | :--------------- |
-| `MACHINE_LEARNING_MODEL_TTL`                              | Inactivity time (s) before a model is unloaded (disabled if \<= 0)                                  |                 `300`                 | machine learning |
-| `MACHINE_LEARNING_MODEL_TTL_POLL_S`                       | Interval (s) between checks for the model TTL (disabled if \<= 0)                                   |                 `10`                  | machine learning |
-| `MACHINE_LEARNING_CACHE_FOLDER`                           | Directory where models are downloaded                                                               |               `/cache`                | machine learning |
-| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup>          | Thread count of the request thread pool (disabled if \<= 0)                                         |          number of CPU cores          | machine learning |
-| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS`                 | Number of parallel model operations                                                                 |                  `1`                  | machine learning |
-| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS`                 | Number of threads for each model operation                                                          |                  `2`                  | machine learning |
-| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup>                  | Number of worker processes to spawn                                                                 |                  `1`                  | machine learning |
-| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`<sup>\*3</sup> | HTTP Keep-alive time in seconds                                                                     |                  `2`                  | machine learning |
-| `MACHINE_LEARNING_WORKER_TIMEOUT`                         | Maximum time (s) of unresponsiveness before a worker is killed                                      | `120` (`300` if using OpenVINO image) | machine learning |
-| `MACHINE_LEARNING_PRELOAD__CLIP`                          | Name of a CLIP model to be preloaded and kept in cache                                              |                                       | machine learning |
-| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION`            | Name of a facial recognition model to be preloaded and kept in cache                                |                                       | machine learning |
-| `MACHINE_LEARNING_ANN`                                    | Enable ARM-NN hardware acceleration if supported                                                    |                `True`                 | machine learning |
-| `MACHINE_LEARNING_ANN_FP16_TURBO`                         | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) |                `False`                | machine learning |
-| `MACHINE_LEARNING_ANN_TUNING_LEVEL`                       | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive)                                        |                  `2`                  | machine learning |
+| Variable                                                  | Description                                                                                         |             Default             | Containers       |
+| :-------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | :-----------------------------: | :--------------- |
+| `MACHINE_LEARNING_MODEL_TTL`                              | Inactivity time (s) before a model is unloaded (disabled if \<= 0)                                  |              `300`              | machine learning |
+| `MACHINE_LEARNING_MODEL_TTL_POLL_S`                       | Interval (s) between checks for the model TTL (disabled if \<= 0)                                   |              `10`               | machine learning |
+| `MACHINE_LEARNING_CACHE_FOLDER`                           | Directory where models are downloaded                                                               |            `/cache`             | machine learning |
+| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup>          | Thread count of the request thread pool (disabled if \<= 0)                                         |       number of CPU cores       | machine learning |
+| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS`                 | Number of parallel model operations                                                                 |               `1`               | machine learning |
+| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS`                 | Number of threads for each model operation                                                          |               `2`               | machine learning |
+| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup>                  | Number of worker processes to spawn                                                                 |               `1`               | machine learning |
+| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`<sup>\*3</sup> | HTTP Keep-alive time in seconds                                                                     |               `2`               | machine learning |
+| `MACHINE_LEARNING_WORKER_TIMEOUT`                         | Maximum time (s) of unresponsiveness before a worker is killed                                      | `120` (`300` if using OpenVINO) | machine learning |
+| `MACHINE_LEARNING_PRELOAD__CLIP`                          | Name of a CLIP model to be preloaded and kept in cache                                              |                                 | machine learning |
+| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION`            | Name of a facial recognition model to be preloaded and kept in cache                                |                                 | machine learning |
+| `MACHINE_LEARNING_ANN`                                    | Enable ARM-NN hardware acceleration if supported                                                    |             `True`              | machine learning |
+| `MACHINE_LEARNING_ANN_FP16_TURBO`                         | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) |             `False`             | machine learning |
+| `MACHINE_LEARNING_ANN_TUNING_LEVEL`                       | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive)                                        |               `2`               | machine learning |
+| `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup>               | Device IDs to use in multi-GPU environments                                                         |               `0`               | machine learning |
+| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION`     | Set the maximum number of faces that will be processed at once by the facial recognition model      |  None (`1` if using OpenVINO)   | machine learning |
 
 \*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.
 
@@ -182,6 +173,8 @@ Redis (Sentinel) URL example JSON before encoding:
 
 \*3: For scenarios like HPA in K8S. https://github.com/immich-app/immich/discussions/12064
 
+\*4: Using multiple GPUs requires `MACHINE_LEARNING_WORKERS` to be set greater than 1. A single device is assigned to each worker in round-robin priority.
+
 :::info
 
 Other machine learning parameters can be tuned from the admin UI.
@@ -190,15 +183,10 @@ Other machine learning parameters can be tuned from the admin UI.
 
 ## Prometheus
 
-| Variable                       | Description                                                                                   | Default | Containers | Workers            |
-| :----------------------------- | :-------------------------------------------------------------------------------------------- | :-----: | :--------- | :----------------- |
-| `IMMICH_METRICS`<sup>\*1</sup> | Toggle all metrics (one of [`true`, `false`])                                                 |         | server     | api, microservices |
-| `IMMICH_API_METRICS`           | Toggle metrics for endpoints and response times (one of [`true`, `false`])                    |         | server     | api, microservices |
-| `IMMICH_HOST_METRICS`          | Toggle metrics for CPU and memory utilization for host and process (one of [`true`, `false`]) |         | server     | api, microservices |
-| `IMMICH_IO_METRICS`            | Toggle metrics for database queries, image processing, etc. (one of [`true`, `false`])        |         | server     | api, microservices |
-| `IMMICH_JOB_METRICS`           | Toggle metrics for jobs and queues (one of [`true`, `false`])                                 |         | server     | api, microservices |
-
-\*1: Overridden for a metric group when its corresponding environmental variable is set.
+| Variable                   | Description                                                                                                           | Default | Containers | Workers            |
+| :------------------------- | :-------------------------------------------------------------------------------------------------------------------- | :-----: | :--------- | :----------------- |
+| `IMMICH_TELEMETRY_INCLUDE` | Collect these telemetries. List of `host`, `api`, `io`, `repo`, `job`. Note: You can also specify `all` to enable all |         | server     | api, microservices |
+| `IMMICH_TELEMETRY_EXCLUDE` | Do not collect these telemetries. List of `host`, `api`, `io`, `repo`, `job`                                          |         | server     | api, microservices |
 
 ## Docker Secrets
 
diff --git a/docs/docs/install/img/truenas01.png b/docs/docs/install/img/truenas01.png
index 81b0430a75..e648ab3734 100644
Binary files a/docs/docs/install/img/truenas01.png and b/docs/docs/install/img/truenas01.png differ
diff --git a/docs/docs/install/img/truenas02.png b/docs/docs/install/img/truenas02.png
index ae7d41e624..66f0dec7fa 100644
Binary files a/docs/docs/install/img/truenas02.png and b/docs/docs/install/img/truenas02.png differ
diff --git a/docs/docs/install/img/truenas03.png b/docs/docs/install/img/truenas03.png
index 90ff25b7ac..d9970f5aeb 100644
Binary files a/docs/docs/install/img/truenas03.png and b/docs/docs/install/img/truenas03.png differ
diff --git a/docs/docs/install/img/truenas04.png b/docs/docs/install/img/truenas04.png
index 281d02350a..45fa87e5e5 100644
Binary files a/docs/docs/install/img/truenas04.png and b/docs/docs/install/img/truenas04.png differ
diff --git a/docs/docs/install/img/truenas05.png b/docs/docs/install/img/truenas05.png
index 919b008030..0f9d6a835a 100644
Binary files a/docs/docs/install/img/truenas05.png and b/docs/docs/install/img/truenas05.png differ
diff --git a/docs/docs/install/img/truenas06.png b/docs/docs/install/img/truenas06.png
index 26cf06738a..3daf250e36 100644
Binary files a/docs/docs/install/img/truenas06.png and b/docs/docs/install/img/truenas06.png differ
diff --git a/docs/docs/install/img/truenas07.png b/docs/docs/install/img/truenas07.png
index 17943e5c81..946c1401ac 100644
Binary files a/docs/docs/install/img/truenas07.png and b/docs/docs/install/img/truenas07.png differ
diff --git a/docs/docs/install/img/truenas08.png b/docs/docs/install/img/truenas08.png
index 4c5a90be6b..4ace8b49ca 100644
Binary files a/docs/docs/install/img/truenas08.png and b/docs/docs/install/img/truenas08.png differ
diff --git a/docs/docs/install/img/truenas09.png b/docs/docs/install/img/truenas09.png
index 647c7295b4..41830fe9e6 100644
Binary files a/docs/docs/install/img/truenas09.png and b/docs/docs/install/img/truenas09.png differ
diff --git a/docs/docs/install/img/truenas10.png b/docs/docs/install/img/truenas10.png
new file mode 100644
index 0000000000..730685c309
Binary files /dev/null and b/docs/docs/install/img/truenas10.png differ
diff --git a/docs/docs/install/img/truenas11.png b/docs/docs/install/img/truenas11.png
new file mode 100644
index 0000000000..88c166aed3
Binary files /dev/null and b/docs/docs/install/img/truenas11.png differ
diff --git a/docs/docs/install/img/truenas12.png b/docs/docs/install/img/truenas12.png
new file mode 100644
index 0000000000..a107a85f24
Binary files /dev/null and b/docs/docs/install/img/truenas12.png differ
diff --git a/docs/docs/install/requirements.md b/docs/docs/install/requirements.md
index 88d85c7bee..ffb89c5c13 100644
--- a/docs/docs/install/requirements.md
+++ b/docs/docs/install/requirements.md
@@ -8,22 +8,61 @@ Hardware and software requirements for Immich:
 
 ## Software
 
-- [Docker](https://docs.docker.com/get-docker/)
-- [Docker Compose](https://docs.docker.com/compose/install/)
+Immich requires [**Docker**](https://docs.docker.com/get-started/get-docker/) with the **Docker Compose plugin**:
+
+- **Docker Engine**: This CLI variant is suitable for Linux servers (or Windows via WSL2).
+- **Docker Desktop**: This GUI variant is suitable for Linux desktop (or Windows or macOS).
+
+The Compose plugin will be installed by both Docker Engine and Desktop by following the linked installation guides; it can also be [separately installed](https://docs.docker.com/compose/install/).
 
 :::note
-Immich requires the command `docker compose` - the similarly named `docker-compose` is [deprecated](https://docs.docker.com/compose/migrate/) and is no longer compatible with Immich.
+Immich requires the command `docker compose`; the similarly named `docker-compose` is [deprecated](https://docs.docker.com/compose/migrate/) and is no longer supported by Immich.
 :::
 
 ## Hardware
 
 - **OS**: Recommended Linux operating system (Ubuntu, Debian, etc).
-  - Windows is supported with [Docker Desktop on Windows](https://docs.docker.com/desktop/install/windows-install/) or [WSL 2](https://docs.docker.com/desktop/wsl/).
-  - macOS is supported with [Docker Desktop on Mac](https://docs.docker.com/desktop/install/mac-install/).
+  - Non-Linux OSes tend to provide a poor Docker experience and are strongly discouraged.
+    Our ability to assist with setup or troubleshooting on non-Linux OSes will be severely reduced.
+    If you still want to try to use a non-Linux OS, you can set it up as follows:
+    - Windows: [Docker Desktop on Windows](https://docs.docker.com/desktop/install/windows-install/) or [WSL 2](https://docs.docker.com/desktop/wsl/).
+    - macOS: [Docker Desktop on Mac](https://docs.docker.com/desktop/install/mac-install/).
 - **RAM**: Minimum 4GB, recommended 6GB.
 - **CPU**: Minimum 2 cores, recommended 4 cores.
 - **Storage**: Recommended Unix-compatible filesystem (EXT4, ZFS, APFS, etc.) with support for user/group ownership and permissions.
-  - This can present an issue for Windows users. See [here](/docs/install/environment-variables#supported-filesystems)
-    for more details and alternatives.
   - The generation of thumbnails and transcoded video can increase the size of the photo library by 10-20% on average.
-  - Network shares are supported for the storage of image and video assets only.
+
+:::tip
+Good performance and a stable connection to the Postgres database is critical to a smooth Immich experience.
+The Postgres database files are typically between 1-3 GB in size.
+For this reason, the Postgres database (`DB_DATA_LOCATION`) should ideally use local SSD storage, and never a network share of any kind.
+Additionally, if Docker resource limits are used, the Postgres database requires at least 2GB of RAM.
+Windows users may run into issues with non-Unix-compatible filesystems, see below for more details.
+:::
+
+### Special requirements for Windows users
+
+<details>
+<summary>Database storage on Windows systems</summary>
+
+The Immich Postgres database (`DB_DATA_LOCATION`) must be located on a filesystem that supports user/group
+ownership and permissions (EXT2/3/4, ZFS, APFS, BTRFS, XFS, etc.). It will not work on any filesystem formatted in NTFS or ex/FAT/32.
+It will not work in WSL (Windows Subsystem for Linux) when using a mounted host directory (commonly under `/mnt`).
+If this is an issue, you can change the bind mount to a Docker volume instead as follows:
+
+Make the following change to `.env`:
+
+```diff
+- DB_DATA_LOCATION=./postgres
++ DB_DATA_LOCATION=pgdata
+```
+
+Add the following line to the bottom of `docker-compose.yml`:
+
+```diff
+volumes:
+  model-cache:
++ pgdata:
+```
+
+</details>
diff --git a/docs/docs/install/truenas.md b/docs/docs/install/truenas.md
index 271cd52cab..f35e9aa37a 100644
--- a/docs/docs/install/truenas.md
+++ b/docs/docs/install/truenas.md
@@ -7,7 +7,9 @@ sidebar_position: 80
 :::note
 This is a community contribution and not officially supported by the Immich team, but included here for convenience.
 
-**Please report issues to the corresponding [Github Repository](https://github.com/truenas/charts/tree/master/community/immich).**
+Community support can be found in the dedicated channel on the [Discord Server](https://discord.immich.app/).
+
+**Please report app issues to the corresponding [Github Repository](https://github.com/truenas/charts/tree/master/community/immich).**
 :::
 
 Immich can easily be installed on TrueNAS SCALE via the **Community** train application.
@@ -20,16 +22,26 @@ TrueNAS SCALE makes installing and updating Immich easy, but you must use the Im
 The Immich app in TrueNAS SCALE installs, completes the initial configuration, then starts the Immich web portal.
 When updates become available, SCALE alerts and provides easy updates.
 
-Before installing the Immich app in SCALE, review the [Environment Variables](/docs/install/environment-variables.md) documentation to see if you want to configure any during installation.
-You can configure environment variables at any time after deploying the application.
+Before installing the Immich app in SCALE, review the [Environment Variables](#environment-variables) documentation to see if you want to configure any during installation.
+You may also configure environment variables at any time after deploying the application.
 
-You can allow SCALE to create the datasets Immich requires automatically during app installation.
-Or before beginning app installation, [create the datasets](https://www.truenas.com/docs/scale/scaletutorials/storage/datasets/datasetsscale/) to use in the **Storage Configuration** section during installation.
-Immich requires seven datasets: **library**, **pgBackup**, **pgData**, **profile**, **thumbs**, **uploads**, and **video**.
-You can organize these as one parent with seven child datasets, for example `mnt/tank/immich/library`, `mnt/tank/immich/pgBackup`, and so on.
+### Setting up Storage Datasets
+
+Before beginning app installation, [create the datasets](https://www.truenas.com/docs/scale/scaletutorials/storage/datasets/datasetsscale/) to use in the **Storage Configuration** section during installation.
+Immich requires seven datasets: `library`, `upload`, `thumbs`, `profile`, `video`, `backups`, and `pgData`.
+You can organize these as one parent with seven child datasets, for example `/mnt/tank/immich/library`, `/mnt/tank/immich/upload`, and so on.
+
+<img
+src={require('./img/truenas12.png').default}
+width="30%"
+alt="Immich App Widget"
+className="border rounded-xl"
+/>
 
 :::info Permissions
 The **pgData** dataset must be owned by the user `netdata` (UID 999) for postgres to start. The other datasets must be owned by the user `root` (UID 0) or a group that includes the user `root` (UID 0) for immich to have the necessary permissions.
+
+If the **library** dataset uses ACL it must have [ACL mode](https://www.truenas.com/docs/core/coretutorials/storage/pools/permissions/#access-control-lists) set to `Passthrough` if you plan on using a [storage template](/docs/administration/storage-template.mdx) and the dataset is configured for network sharing (its ACL type is set to `SMB/NFSv4`). When the template is applied and files need to be moved from **upload** to **library**, immich performs `chmod` internally and needs to be allowed to execute the command. [More info.](https://github.com/immich-app/immich/pull/13017)
 :::
 
 ## Installing the Immich Application
@@ -45,6 +57,8 @@ className="border rounded-xl"
 
 Click on the widget to open the **Immich** application details screen.
 
+<br/><br/>
+
 <img
 src={require('./img/truenas02.png').default}
 width="100%"
@@ -54,9 +68,13 @@ className="border rounded-xl"
 
 Click **Install** to open the Immich application configuration screen.
 
+<br/><br/>
+
 Application configuration settings are presented in several sections, each explained below.
 To find specific fields click in the **Search Input Fields** search field, scroll down to a particular section or click on the section heading on the navigation area in the upper-right corner.
 
+### Application Name and Version
+
 <img
 src={require('./img/truenas03.png').default}
 width="100%"
@@ -64,21 +82,123 @@ alt="Install Immich Screen"
 className="border rounded-xl"
 />
 
-Accept the default values in **Application Name** and **Version**.
+Accept the default value or enter a name in **Application Name** field.
+In most cases use the default name, but if adding a second deployment of the application you must change this name.
+
+Accept the default version number in **Version**.
+When a new version becomes available, the application has an update badge.
+The **Installed Applications** screen shows the option to update applications.
+
+### Immich Configuration
+
+<img
+src={require('./img/truenas05.png').default}
+width="40%"
+alt="Configuration Settings"
+className="border rounded-xl"
+/>
 
 Accept the default value in **Timezone** or change to match your local timezone.
 **Timezone** is only used by the Immich `exiftool` microservice if it cannot be determined from the image metadata.
 
-Accept the default port in **Web Port**.
+Untick **Enable Machine Learning** if you will not use face recognition, image search, and smart duplicate detection.
+
+Accept the default option or select the **Machine Learning Image Type** for your hardware based on the [Hardware-Accelerated Machine Learning Supported Backends](/docs/features/ml-hardware-acceleration.md#supported-backends).
+
+Immich's default is `postgres` but you should consider setting the **Database Password** to a custom value using only the characters `A-Za-z0-9`.
+
+The **Redis Password** should be set to a custom value using only the characters `A-Za-z0-9`.
+
+Accept the **Log Level** default of **Log**.
+
+Leave **Hugging Face Endpoint** blank. (This is for downloading ML models from a different source.)
+
+Leave **Additional Environment Variables** blank or see [Environment Variables](#environment-variables) to set before installing.
+
+### Network Configuration
+
+<img
+src={require('./img/truenas06.png').default}
+width="40%"
+alt="Networking Settings"
+className="border rounded-xl"
+/>
+
+Accept the default port `30041` in **WebUI Port** or enter a custom port number.
+:::info Allowed Port Numbers
+Only numbers within the range 9000-65535 may be used on SCALE versions below TrueNAS Scale 24.10 Electric Eel.
+
+Regardless of version, to avoid port conflicts, don't use [ports on this list](https://www.truenas.com/docs/references/defaultports/).
+:::
+
+### Storage Configuration
 
 Immich requires seven storage datasets.
-You can allow SCALE to create them for you, or use the dataset(s) created in [First Steps](#first-steps).
-Select the storage options you want to use for **Immich Uploads Storage**, **Immich Library Storage**, **Immich Thumbs Storage**, **Immich Profile Storage**, **Immich Video Storage**, **Immich Postgres Data Storage**, **Immich Postgres Backup Storage**.
-Select **ixVolume (dataset created automatically by the system)** in **Type** to let SCALE create the dataset or select **Host Path** to use the existing datasets created on the system.
 
-Accept the defaults in Resources or change the CPU and memory limits to suit your use case.
+<img
+src={require('./img/truenas07.png').default}
+width="20%"
+alt="Configure Storage ixVolumes"
+className="border rounded-xl"
+/>
 
-Click **Install**.
+:::note Default Setting (Not recommended)
+The default setting for datasets is **ixVolume (dataset created automatically by the system)** but this results in your data being harder to access manually and can result in data loss if you delete the immich app. (Not recommended)
+:::
+
+For each Storage option select **Host Path (Path that already exists on the system)** and then select the matching dataset [created before installing the app](#setting-up-storage-datasets): **Immich Library Storage**: `library`, **Immich Uploads Storage**: `upload`, **Immich Thumbs Storage**: `thumbs`, **Immich Profile Storage**: `profile`, **Immich Video Storage**: `video`, **Immich Backups Storage**: `backups`, **Postgres Data Storage**: `pgData`.
+
+<img
+src={require('./img/truenas08.png').default}
+width="40%"
+alt="Configure Storage Host Paths"
+className="border rounded-xl"
+/>
+The image above has example values.
+
+<br/>
+
+### Additional Storage [(External Libraries)](/docs/features/libraries)
+
+<img
+src={require('./img/truenas10.png').default}
+width="40%"
+alt="Configure Storage Host Paths"
+className="border rounded-xl"
+/>
+
+You may configure [External Libraries](/docs/features/libraries) by mounting them using **Additional Storage**.
+The **Mount Path** is the loaction you will need to copy and paste into the External Library settings within Immich.
+The **Host Path** is the location on the TrueNAS SCALE server where your external library is located.
+
+<!-- A section for Labels would go here but I don't know what they do. -->
+
+### Resources Configuration
+
+<img
+src={require('./img/truenas09.png').default}
+width="40%"
+alt="Resource Limits"
+className="border rounded-xl"
+/>
+
+Accept the default **CPU** limit of `2` threads or specify the number of threads (CPUs with Multi-/Hyper-threading have 2 threads per core).
+
+Accept the default **Memory** limit of `4096` MB or specify the number of MB of RAM. If you're using Machine Learning you should probably set this above 8000 MB.
+
+:::info Older SCALE Versions
+Before TrueNAS SCALE version 24.10 Electric Eel:
+
+The **CPU** value was specified in a different format with a default of `4000m` which is 4 threads.
+
+The **Memory** value was specified in a different format with a default of `8Gi` which is 8 GiB of RAM. The value was specified in bytes or a number with a measurement suffix. Examples: `129M`, `123Mi`, `1000000000`
+:::
+
+Enable **GPU Configuration** options if you have a GPU that you will use for [Hardware Transcoding](/docs/features/hardware-transcoding) and/or [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md). More info: [GPU Passtrough Docs for TrueNAS Apps](https://www.truenas.com/docs/truenasapps/#gpu-passthrough)
+
+### Install
+
+Finally, click **Install**.
 The system opens the **Installed Applications** screen with the Immich app in the **Deploying** state.
 When the installation completes it changes to **Running**.
 
@@ -95,102 +215,41 @@ Click **Web Portal** on the **Application Info** widget to open the Immich web i
 For more information on how to use the application once installed, please refer to the [Post Install](/docs/install/post-install.mdx) guide.
 :::
 
-## Editing Environment Variables
+## Edit App Settings
 
-Go to the **Installed Applications** screen and select Immich from the list of installed applications.
-Click **Edit** on the **Application Info** widget to open the **Edit Immich** screen.
-The settings on the edit screen are the same as on the install screen.
-You cannot edit **Storage Configuration** paths after the initial app install.
+- Go to the **Installed Applications** screen and select Immich from the list of installed applications.
+- Click **Edit** on the **Application Info** widget to open the **Edit Immich** screen.
+- Change any settings you would like to change.
+  - The settings on the edit screen are the same as on the install screen.
+- Click **Update** at the very bottom of the page to save changes.
+  - TrueNAS automatically updates, recreates, and redeploys the Immich container with the updated settings.
 
-Click **Update** to save changes.
-TrueNAS automatically updates, recreates, and redeploys the Immich container with the updated environment variables.
+## Environment Variables
+
+You can set [Environment Variables](/docs/install/environment-variables) by clicking **Add** on the **Additional Environment Variables** option and filling in the **Name** and **Value**.
+
+<img
+src={require('./img/truenas11.png').default}
+width="40%"
+alt="Environment Variables"
+className="border rounded-xl"
+/>
+
+:::info
+Some Environment Variables are not available for the TrueNAS SCALE app. This is mainly because they can be configured through GUI options in the [Edit Immich screen](#edit-app-settings).
+
+Some examples are: `IMMICH_VERSION`, `UPLOAD_LOCATION`, `DB_DATA_LOCATION`, `TZ`, `IMMICH_LOG_LEVEL`, `DB_PASSWORD`, `REDIS_PASSWORD`.
+:::
 
 ## Updating the App
 
 When updates become available, SCALE alerts and provides easy updates.
-To update the app to the latest version, click **Update** on the **Application Info** widget from the **Installed Applications** screen.
+To update the app to the latest version:
 
-Update opens an update window for the application that includes two selectable options, Images (to be updated) and Changelog. Click on the down arrow to see the options available for each.
-
-Click **Upgrade** to begin the process and open a counter dialog that shows the upgrade progress. When complete, the update badge and buttons disappear and the application Update state on the Installed screen changes from Update Available to Up to date.
-
-## Understanding Immich Settings in TrueNAS SCALE
-
-Accept the default value or enter a name in **Application Name** field.
-In most cases use the default name, but if adding a second deployment of the application you must change this name.
-
-Accept the default version number in **Version**.
-When a new version becomes available, the application has an update badge.
-The **Installed Applications** screen shows the option to update applications.
-
-### Immich Configuration Settings
-
-You can accept the defaults in the **Immich Configuration** settings, or enter the settings you want to use.
-
-<img
-src={require('./img/truenas05.png').default}
-width="100%"
-alt="Configuration Settings"
-className="border rounded-xl"
-/>
-
-Accept the default setting in **Timezone** or change to match your local timezone.
-**Timezone** is only used by the Immich `exiftool` microservice if it cannot be determined from the image metadata.
-
-You can enter a **Public Login Message** to display on the login page, or leave it blank.
-
-### Networking Settings
-
-Accept the default port numbers in **Web Port**.
-The SCALE Immich app listens on port **30041**.
-
-Refer to the TrueNAS [default port list](https://www.truenas.com/docs/references/defaultports/) for a list of assigned port numbers.
-To change the port numbers, enter a number within the range 9000-65535.
-
-<img
-src={require('./img/truenas06.png').default}
-width="100%"
-alt="Networking Settings"
-className="border rounded-xl"
-/>
-
-### Storage Settings
-
-You can install Immich using the default setting **ixVolume (dataset created automatically by the system)** or use the host path option with datasets [created before installing the app](#first-steps).
-
-<img
-src={require('./img/truenas07.png').default}
-width="100%"
-alt="Configure Storage ixVolumes"
-className="border rounded-xl"
-/>
-
-Select **Host Path (Path that already exists on the system)** to browse to and select the datasets.
-
-<img
-src={require('./img/truenas08.png').default}
-width="100%"
-alt="Configure Storage Host Paths"
-className="border rounded-xl"
-/>
-
-### Resource Configuration Settings
-
-Accept the default values in **Resources Configuration** or enter new CPU and memory values
-By default, this application is limited to use no more than 4 CPU cores and 8 Gigabytes available memory. The application might use considerably less system resources.
-
-<img
-src={require('./img/truenas09.png').default}
-width="100%"
-alt="Resource Limits"
-className="border rounded-xl"
-/>
-
-To customize the CPU and memory allocated to the container Immich uses, enter new CPU values as a plain integer value followed by the suffix m (milli).
-Default is 4000m.
-
-Accept the default value 8Gi allocated memory or enter a new limit in bytes.
-Enter a plain integer followed by the measurement suffix, for example 129M or 123Mi.
-
-Systems with compatible GPU(s) display devices in **GPU Configuration**.
-See [Managing GPUs](https://www.truenas.com/docs/scale/scaletutorials/systemsettings/advanced/managegpuscale/) for more information about allocating isolated GPU devices in TrueNAS SCALE.
+- Go to the **Installed Applications** screen and select Immich from the list of installed applications.
+- Click **Update** on the **Application Info** widget from the **Installed Applications** screen.
+- This opens an update window with some options
+  - You may select an Image update too.
+  - You may view the Changelog.
+- Click **Upgrade** to begin the process and open a counter dialog that shows the upgrade progress.
+  - When complete, the update badge and buttons disappear and the application Update state on the Installed screen changes from Update Available to Up to date.
diff --git a/docs/docs/install/unraid.md b/docs/docs/install/unraid.md
index b17ed28295..356f81c9e8 100644
--- a/docs/docs/install/unraid.md
+++ b/docs/docs/install/unraid.md
@@ -77,6 +77,7 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
 7.  Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:
 
     - `UPLOAD_LOCATION`: Create a folder in your Images Unraid share and place the **absolute** location here > For example my _"images"_ share has a folder within it called _"immich"_. If I browse to this directory in the terminal and type `pwd` the output is `/mnt/user/images/immich`. This is the exact value I need to enter as my `UPLOAD_LOCATION`
+    - `DB_DATA_LOCATION`: Change this to use an Unraid share (preferably a cache pool, e.g. `/mnt/user/appdata`). If left at default it will try to use Unraid's `/boot/config/plugins/compose.manager/projects/[stack_name]/postgres` folder which it doesn't have permissions to, resulting in this container continuously restarting.
 
       <img
       src={require('./img/unraid05.webp').default}
diff --git a/docs/docs/overview/quick-start.mdx b/docs/docs/overview/quick-start.mdx
index e352757a0f..9c7ca8bd08 100644
--- a/docs/docs/overview/quick-start.mdx
+++ b/docs/docs/overview/quick-start.mdx
@@ -14,13 +14,7 @@ Check the [requirements page](/docs/install/requirements) to get started.
 
 ## Install and Launch via Docker Compose
 
-Follow the [Docker Compose (Recommended)](/docs/install/docker-compose) instructions
-to install the server.
-
-- Where random passwords are required, `pwgen` is a handy utility.
-- `UPLOAD_LOCATION` should be set to some new directory on the server
-  with enough free space.
-- You may ignore "Step 4 - Upgrading".
+Follow the [Docker Compose (Recommended)](/docs/install/docker-compose) instructions to install the server.
 
 ## Try the Web UI
 
@@ -56,6 +50,7 @@ import MobileAppBackup from '/docs/partials/_mobile-app-backup.md';
 
 The backup time differs depending on how many photos are on your mobile device. Large uploads may
 take quite a while.
+To quickly get going, you can selectively upload few photos first, by following this [guide](/docs/features/mobile-app#sync-only-selected-photos).
 
 You can select the **Jobs** tab to see Immich processing your photos.
 
diff --git a/docs/docs/overview/support-the-project.md b/docs/docs/overview/support-the-project.md
index 7060cef3e1..a439893a7e 100644
--- a/docs/docs/overview/support-the-project.md
+++ b/docs/docs/overview/support-the-project.md
@@ -16,5 +16,9 @@ Support the project by localizing on [Weblate](https://hosted.weblate.org/projec
 
 If you are a programmer or developer, take a look at Immich's [technology stack](/docs/developer/architecture.mdx) and consider fixing bugs or building new features. The team and I are always looking for new contributors. For information about how to contribute as a developer, see the [Developer](/docs/developer/architecture.mdx) section.
 
+## Purchase Immich
+
+You can also [purchase Immich](https://buy.immich.app), for either one user or your entire server. Building Immich takes a lot of time and effort, and we have full-time engineers working on it to make it as good as we possibly can, so any support is greatly appreciated. Don't worry, all features will be free, forever! Nothing will ever be put behind any paywalls.
+
 [github-issue]: https://github.com/immich-app/immich/issues/new/choose
 [github-langs]: https://github.com/immich-app/immich/tree/main/mobile/assets/i18n
diff --git a/docs/docs/partials/_mobile-app-backup.md b/docs/docs/partials/_mobile-app-backup.md
index 9929d0e36e..059f594754 100644
--- a/docs/docs/partials/_mobile-app-backup.md
+++ b/docs/docs/partials/_mobile-app-backup.md
@@ -1,9 +1,9 @@
-Navigate to the backup screen by clicking on the cloud icon in the top right corner of the screen.
+1. Navigate to the backup screen by clicking on the cloud icon in the top right corner of the screen.
 
 <img src={require('./img/backup-header.png').default} width='50%' title='Backup button' />
 
-You can select which album(s) you want to back up to the Immich server from the backup screen.
+2. You can select which album(s) you want to back up to the Immich server from the backup screen.
 
 <img src={require('./img/album-selection.png').default} width='50%' title='Backup button' />
 
-Scroll down to the bottom and press "**Start Backup**" to start the backup process.
+3. Scroll down to the bottom and press "**Start Backup**" to start the backup process. This will upload all the assets in the selected albums.
diff --git a/docs/docs/partials/_storage-template.md b/docs/docs/partials/_storage-template.md
index b6dcd5ad77..0c668d0a3e 100644
--- a/docs/docs/partials/_storage-template.md
+++ b/docs/docs/partials/_storage-template.md
@@ -31,5 +31,5 @@ Immich also provides a mechanism to migrate between templates so that if the tem
 If you want to store assets in album folders, but you also have assets that do not belong to any album, you can use `{{#if album}}`, `{{else}}` and `{{/if}}` to create a conditional statement. For example, the following template will store assets in album folders if they belong to an album, and in a folder named "Other/Month" if they do not belong to an album:
 
 ```
-{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}
+{{y}}/{{#if album}}{{album}}{{else}}Other{{/if}}/{{MM}}/{{filename}}
 ```
diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js
index a94a54b60c..16d654b46b 100644
--- a/docs/docusaurus.config.js
+++ b/docs/docusaurus.config.js
@@ -72,14 +72,9 @@ const config = {
   themeConfig:
     /** @type {import('@docusaurus/preset-classic').ThemeConfig} */
     ({
-      colorMode: {
-        defaultMode: 'dark',
-      },
       announcementBar: {
         id: 'site_announcement_immich',
         content: `⚠️ The project is under <strong>very active</strong> development. Expect bugs and changes. Do not use it as <strong>the only way</strong> to store your photos and videos!`,
-        backgroundColor: '#593f00',
-        textColor: '#ffefc9',
         isCloseable: false,
       },
       docs: {
@@ -201,7 +196,7 @@ const config = {
         darkTheme: prism.themes.dracula,
         additionalLanguages: ['sql', 'diff', 'bash', 'powershell', 'nginx'],
       },
-      image: 'overview/img/feature-panel.png',
+      image: 'img/feature-panel.png',
     }),
 };
 
diff --git a/docs/package-lock.json b/docs/package-lock.json
index 05417ce127..ca80d15fd0 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -8,8 +8,8 @@
       "name": "documentation",
       "version": "0.0.0",
       "dependencies": {
-        "@docusaurus/core": "^3.2.1",
-        "@docusaurus/preset-classic": "^3.2.1",
+        "@docusaurus/core": "~3.5.2",
+        "@docusaurus/preset-classic": "~3.5.2",
         "@mdi/js": "^7.3.67",
         "@mdi/react": "^1.6.1",
         "@mdx-js/react": "^3.0.0",
@@ -27,7 +27,7 @@
         "url": "^0.11.0"
       },
       "devDependencies": {
-        "@docusaurus/module-type-aliases": "^3.1.0",
+        "@docusaurus/module-type-aliases": "~3.5.2",
         "@tsconfig/docusaurus": "^2.0.2",
         "prettier": "^3.2.4",
         "typescript": "^5.1.6"
@@ -3006,9 +3006,10 @@
       }
     },
     "node_modules/@mdx-js/react": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.0.1.tgz",
-      "integrity": "sha512-9ZrPIU4MGf6et1m1ov3zKf+q9+deetI51zprKB1D/z3NOb+rUxxtEl3mCjW5wTGh6VhRdwPueh1oRzi6ezkA8A==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz",
+      "integrity": "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==",
+      "license": "MIT",
       "dependencies": {
         "@types/mdx": "^2.0.0"
       },
@@ -6068,9 +6069,10 @@
       }
     },
     "node_modules/docusaurus-lunr-search": {
-      "version": "3.4.0",
-      "resolved": "https://registry.npmjs.org/docusaurus-lunr-search/-/docusaurus-lunr-search-3.4.0.tgz",
-      "integrity": "sha512-GfllnNXCLgTSPH9TAKWmbn8VMfwpdOAZ1xl3T2GgX8Pm26qSDLfrrdVwjguaLfMJfzciFL97RKrAJlgrFM48yw==",
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/docusaurus-lunr-search/-/docusaurus-lunr-search-3.5.0.tgz",
+      "integrity": "sha512-k3zN4jYMi/prWInJILGKOxE+BVcgYinwj9+gcECsYm52tS+4ZKzXQzbPnVJAEXmvKOfFMcDFvS3MSmm6cEaxIQ==",
+      "license": "MIT",
       "dependencies": {
         "autocomplete.js": "^0.37.0",
         "clsx": "^1.2.1",
@@ -6097,14 +6099,16 @@
       }
     },
     "node_modules/docusaurus-lunr-search/node_modules/@types/unist": {
-      "version": "2.0.10",
-      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
-      "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="
+      "version": "2.0.11",
+      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+      "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+      "license": "MIT"
     },
     "node_modules/docusaurus-lunr-search/node_modules/bail": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz",
       "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==",
+      "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/wooorm"
@@ -6114,6 +6118,7 @@
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
       "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
@@ -6122,6 +6127,7 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
       "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
@@ -6130,6 +6136,7 @@
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz",
       "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==",
+      "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/wooorm"
@@ -6139,6 +6146,7 @@
       "version": "9.2.2",
       "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz",
       "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==",
+      "license": "MIT",
       "dependencies": {
         "bail": "^1.0.0",
         "extend": "^3.0.0",
@@ -6156,6 +6164,7 @@
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz",
       "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==",
+      "license": "MIT",
       "dependencies": {
         "@types/unist": "^2.0.2"
       },
@@ -6168,6 +6177,7 @@
       "version": "4.2.1",
       "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz",
       "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==",
+      "license": "MIT",
       "dependencies": {
         "@types/unist": "^2.0.0",
         "is-buffer": "^2.0.0",
@@ -6183,6 +6193,7 @@
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz",
       "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==",
+      "license": "MIT",
       "dependencies": {
         "@types/unist": "^2.0.0",
         "unist-util-stringify-position": "^2.0.0"
@@ -12705,9 +12716,9 @@
       }
     },
     "node_modules/picocolors": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
-      "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
+      "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
       "license": "ISC"
     },
     "node_modules/picomatch": {
@@ -12819,9 +12830,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.4.40",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz",
-      "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==",
+      "version": "8.4.47",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
+      "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
       "funding": [
         {
           "type": "opencollective",
@@ -12839,8 +12850,8 @@
       "license": "MIT",
       "dependencies": {
         "nanoid": "^3.3.7",
-        "picocolors": "^1.0.1",
-        "source-map-js": "^1.2.0"
+        "picocolors": "^1.1.0",
+        "source-map-js": "^1.2.1"
       },
       "engines": {
         "node": "^10 || ^12 || >=14"
@@ -15660,9 +15671,10 @@
       }
     },
     "node_modules/source-map-js": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
-      "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -16081,9 +16093,9 @@
       }
     },
     "node_modules/tailwindcss": {
-      "version": "3.4.10",
-      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
-      "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==",
+      "version": "3.4.14",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz",
+      "integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==",
       "license": "MIT",
       "dependencies": {
         "@alloc/quick-lru": "^5.2.0",
@@ -16443,9 +16455,9 @@
       }
     },
     "node_modules/typescript": {
-      "version": "5.5.4",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
-      "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
+      "version": "5.6.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+      "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
       "license": "Apache-2.0",
       "bin": {
         "tsc": "bin/tsc",
diff --git a/docs/package.json b/docs/package.json
index cdcdf53446..e739cd68c7 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -16,8 +16,8 @@
     "write-heading-ids": "docusaurus write-heading-ids"
   },
   "dependencies": {
-    "@docusaurus/core": "^3.2.1",
-    "@docusaurus/preset-classic": "^3.2.1",
+    "@docusaurus/core": "~3.5.2",
+    "@docusaurus/preset-classic": "~3.5.2",
     "@mdi/js": "^7.3.67",
     "@mdi/react": "^1.6.1",
     "@mdx-js/react": "^3.0.0",
@@ -35,8 +35,7 @@
     "url": "^0.11.0"
   },
   "devDependencies": {
-    "@docusaurus/module-type-aliases": "^3.1.0",
-    "@tsconfig/docusaurus": "^2.0.2",
+    "@docusaurus/module-type-aliases": "~3.5.2",
     "prettier": "^3.2.4",
     "typescript": "^5.1.6"
   },
@@ -56,6 +55,6 @@
     "node": ">=20"
   },
   "volta": {
-    "node": "20.17.0"
+    "node": "22.12.0"
   }
 }
diff --git a/docs/src/components/community-guides.tsx b/docs/src/components/community-guides.tsx
index 6982853fad..17fe562317 100644
--- a/docs/src/components/community-guides.tsx
+++ b/docs/src/components/community-guides.tsx
@@ -35,37 +35,42 @@ const guides: CommunityGuidesProps[] = [
   },
   {
     title: 'Google Photos import + albums',
-    description: 'Import your Google Photos files into Immich and add your albums',
+    description: 'Import your Google Photos files into Immich and add your albums.',
     url: 'https://github.com/immich-app/immich/discussions/1340',
   },
   {
     title: 'Access Immich with custom domain',
-    description: 'Access your local Immich installation over the internet using your own domain',
+    description: 'Access your local Immich installation over the internet using your own domain.',
     url: 'https://github.com/ppr88/immich-guides/blob/main/open-immich-custom-domain.md',
   },
   {
     title: 'Nginx caching map server',
-    description: 'Increase privacy by using nginx as a caching proxy in front of a map tile server',
+    description: 'Increase privacy by using nginx as a caching proxy in front of a map tile server.',
     url: 'https://github.com/pcouy/pcouy.github.io/blob/main/_posts/2024-08-30-proxying-a-map-tile-server-for-increased-privacy.md',
   },
+  {
+    title: 'fail2ban setup instructions',
+    description: 'How to configure an existing fail2ban installation to block incorrect login attempts.',
+    url: 'https://github.com/immich-app/immich/discussions/3243#discussioncomment-6681948',
+  },
 ];
 
 function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element {
   return (
-    <section className="flex flex-col gap-4 justify-between dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl p-4">
+    <section className="flex flex-col gap-4 justify-between dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl px-4 py-6">
       <div className="flex flex-col gap-2">
-        <p className="m-0 items-start flex gap-2">
+        <p className="m-0 items-start flex gap-2 text-2xl font-bold text-immich-primary dark:text-immich-dark-primary">
           <span>{title}</span>
         </p>
 
         <p className="m-0 text-sm text-gray-600 dark:text-gray-300">{description}</p>
-        <p className="m-0 text-sm text-gray-600 dark:text-gray-300">
+        <p className="m-0 text-sm text-gray-600 dark:text-gray-300 my-4">
           <a href={url}>{url}</a>
         </p>
       </div>
       <div className="flex">
         <Link
-          className="px-4 py-2 bg-immich-primary/10 dark:bg-gray-300  rounded-full hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
+          className="px-4 py-2 bg-immich-primary/10 dark:bg-gray-300 rounded-xl text-sm hover:no-underline text-immich-primary dark:text-immich-dark-bg font-semibold"
           to={url}
         >
           View Guide
diff --git a/docs/src/components/community-projects.tsx b/docs/src/components/community-projects.tsx
index d8273c67c2..2dbab979f2 100644
--- a/docs/src/components/community-projects.tsx
+++ b/docs/src/components/community-projects.tsx
@@ -83,27 +83,38 @@ const projects: CommunityProjectProps[] = [
     description: 'Power tools for organizing your immich library.',
     url: 'https://github.com/varun-raj/immich-power-tools',
   },
+  {
+    title: 'Immich Public Proxy',
+    description:
+      'Share your Immich photos and albums in a safe way without exposing your Immich instance to the public.',
+    url: 'https://github.com/alangrainger/immich-public-proxy',
+  },
+  {
+    title: 'Immich Kodi',
+    description: 'Unofficial Kodi plugin for Immich.',
+    url: 'https://github.com/vladd11/immich-kodi',
+  },
 ];
 
 function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element {
   return (
-    <section className="flex flex-col gap-4 justify-between dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl p-4">
+    <section className="flex flex-col gap-4 justify-between dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl px-4 py-6">
       <div className="flex flex-col gap-2">
-        <p className="m-0 items-start flex gap-2">
+        <p className="m-0 items-start flex gap-2 text-2xl font-bold text-immich-primary dark:text-immich-dark-primary">
           <span>{title}</span>
         </p>
 
         <p className="m-0 text-sm text-gray-600 dark:text-gray-300">{description}</p>
-        <p className="m-0 text-sm text-gray-600 dark:text-gray-300">
+        <p className="m-0 text-sm text-gray-600 dark:text-gray-300 my-4">
           <a href={url}>{url}</a>
         </p>
       </div>
       <div className="flex">
         <Link
-          className="px-4 py-2 bg-immich-primary/10 dark:bg-gray-300  rounded-full hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
+          className="px-4 py-2 bg-immich-primary/10 dark:bg-gray-300 rounded-xl text-sm hover:no-underline text-immich-primary dark:text-immich-dark-bg font-semibold"
           to={url}
         >
-          View Project
+          View Link
         </Link>
       </div>
     </section>
diff --git a/docs/src/components/svg-paths.ts b/docs/src/components/svg-paths.ts
new file mode 100644
index 0000000000..112ed1d70f
--- /dev/null
+++ b/docs/src/components/svg-paths.ts
@@ -0,0 +1,2 @@
+export const discordPath =
+  'M 9.1367188 3.8691406 C 9.1217187 3.8691406 9.1067969 3.8700938 9.0917969 3.8710938 C 8.9647969 3.8810937 5.9534375 4.1403594 4.0234375 5.6933594 C 3.0154375 6.6253594 1 12.073203 1 16.783203 C 1 16.866203 1.0215 16.946531 1.0625 17.019531 C 2.4535 19.462531 6.2473281 20.102859 7.1113281 20.130859 L 7.1269531 20.130859 C 7.2799531 20.130859 7.4236719 20.057594 7.5136719 19.933594 L 8.3886719 18.732422 C 6.0296719 18.122422 4.8248594 17.086391 4.7558594 17.025391 C 4.5578594 16.850391 4.5378906 16.549563 4.7128906 16.351562 C 4.8068906 16.244563 4.9383125 16.189453 5.0703125 16.189453 C 5.1823125 16.189453 5.2957188 16.228594 5.3867188 16.308594 C 5.4157187 16.334594 7.6340469 18.216797 11.998047 18.216797 C 16.370047 18.216797 18.589328 16.325641 18.611328 16.306641 C 18.702328 16.227641 18.815734 16.189453 18.927734 16.189453 C 19.059734 16.189453 19.190156 16.243562 19.285156 16.351562 C 19.459156 16.549563 19.441141 16.851391 19.244141 17.025391 C 19.174141 17.087391 17.968375 18.120469 15.609375 18.730469 L 16.484375 19.933594 C 16.574375 20.057594 16.718094 20.130859 16.871094 20.130859 L 16.886719 20.130859 C 17.751719 20.103859 21.5465 19.463531 22.9375 17.019531 C 22.9785 16.947531 23 16.866203 23 16.783203 C 23 12.073203 20.984172 6.624875 19.951172 5.671875 C 18.047172 4.140875 15.036203 3.8820937 14.908203 3.8710938 C 14.895203 3.8700938 14.880188 3.8691406 14.867188 3.8691406 C 14.681188 3.8691406 14.510594 3.9793906 14.433594 4.1503906 C 14.427594 4.1623906 14.362062 4.3138281 14.289062 4.5488281 C 15.548063 4.7608281 17.094141 5.1895937 18.494141 6.0585938 C 18.718141 6.1975938 18.787437 6.4917969 18.648438 6.7167969 C 18.558438 6.8627969 18.402188 6.9433594 18.242188 6.9433594 C 18.156188 6.9433594 18.069234 6.9200937 17.990234 6.8710938 C 15.584234 5.3800938 12.578 5.3046875 12 5.3046875 C 11.422 5.3046875 8.4157187 5.3810469 6.0117188 6.8730469 C 5.9327188 6.9210469 5.8457656 6.9433594 5.7597656 6.9433594 C 5.5997656 6.9433594 5.4425625 6.86475 5.3515625 6.71875 C 5.2115625 6.49375 5.2818594 6.1985938 5.5058594 6.0585938 C 6.9058594 5.1905937 8.4528906 4.7627812 9.7128906 4.5507812 C 9.6388906 4.3147813 9.5714062 4.1643437 9.5664062 4.1523438 C 9.4894063 3.9813438 9.3217188 3.8691406 9.1367188 3.8691406 z M 12 7.3046875 C 12.296 7.3046875 14.950594 7.3403125 16.933594 8.5703125 C 17.326594 8.8143125 17.777234 8.9453125 18.240234 8.9453125 C 18.633234 8.9453125 19.010656 8.8555 19.347656 8.6875 C 19.964656 10.2405 20.690828 12.686219 20.923828 15.199219 C 20.883828 15.143219 20.840922 15.089109 20.794922 15.037109 C 20.324922 14.498109 19.644687 14.191406 18.929688 14.191406 C 18.332687 14.191406 17.754078 14.405437 17.330078 14.773438 C 17.257078 14.832437 15.505 16.21875 12 16.21875 C 8.496 16.21875 6.7450313 14.834687 6.7070312 14.804688 C 6.2540312 14.407687 5.6742656 14.189453 5.0722656 14.189453 C 4.3612656 14.189453 3.6838438 14.494391 3.2148438 15.025391 C 3.1658438 15.080391 3.1201719 15.138266 3.0761719 15.197266 C 3.3091719 12.686266 4.0344375 10.235594 4.6484375 8.6835938 C 4.9864375 8.8525938 5.3657656 8.9433594 5.7597656 8.9433594 C 6.2217656 8.9433594 6.6724531 8.8143125 7.0644531 8.5703125 C 9.0494531 7.3393125 11.704 7.3046875 12 7.3046875 z M 8.890625 10.044922 C 7.966625 10.044922 7.2167969 10.901031 7.2167969 11.957031 C 7.2167969 13.013031 7.965625 13.869141 8.890625 13.869141 C 9.815625 13.869141 10.564453 13.013031 10.564453 11.957031 C 10.564453 10.900031 9.815625 10.044922 8.890625 10.044922 z M 15.109375 10.044922 C 14.185375 10.044922 13.435547 10.901031 13.435547 11.957031 C 13.435547 13.013031 14.184375 13.869141 15.109375 13.869141 C 16.034375 13.869141 16.783203 13.013031 16.783203 11.957031 C 16.783203 10.900031 16.033375 10.044922 15.109375 10.044922 z';
diff --git a/docs/src/components/timeline.tsx b/docs/src/components/timeline.tsx
index 374d2d88fa..32b15edb59 100644
--- a/docs/src/components/timeline.tsx
+++ b/docs/src/components/timeline.tsx
@@ -49,7 +49,7 @@ export function Timeline({ items }: Props): JSX.Element {
               <div className="flex flex-col flex-grow justify-between gap-2">
                 <div className="flex gap-2 items-center">
                   {cardIcon === 'immich' ? (
-                    <img src="img/immich-logo.svg" height="30" className="rounded-none" />
+                    <img src="/img/immich-logo.svg" height="30" className="rounded-none" />
                   ) : (
                     <Icon path={cardIcon} size={1} color={item.iconColor} />
                   )}
diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css
index 5ee7bf7393..f693ce701b 100644
--- a/docs/src/css/custom.css
+++ b/docs/src/css/custom.css
@@ -7,11 +7,12 @@
 @tailwind components;
 @tailwind utilities;
 
-@import url('https://fonts.googleapis.com/css2?family=Overpass:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
 
 html,
 button {
-  font-family: 'Overpass', sans-serif;
+  font-family: 'Be Vietnam Pro', sans-serif;
+  font-optical-sizing: auto;
 }
 
 img {
@@ -27,7 +28,6 @@ img {
   --ifm-color-primary-light: #4250af;
   --ifm-color-primary-lighter: #4250af;
   --ifm-color-primary-lightest: #4250af;
-  --ifm-code-font-size: 95%;
   --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
 }
 
@@ -40,10 +40,28 @@ img {
   --ifm-color-primary-light: #d5e4fc;
   --ifm-color-primary-lighter: #e9f1fe;
   --ifm-color-primary-lightest: #ffffff;
-  --ifm-background-color: #000000;
   --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
+  --ifm-background-color: #000000;
 }
 
 div[class^='announcementBar_'] {
   min-height: 2rem;
+  background-color: #2b3336;
+  color: white;
+}
+
+.menu__link {
+  padding: 10px;
+  padding-left: 16px;
+  border-radius: 10px;
+  font-size: 15px;
+}
+
+.menu__list-item-collapsible {
+  border-radius: 10px;
+  font-size: 15px;
+}
+
+code {
+  font-weight: 600;
 }
diff --git a/docs/src/pages/cursed-knowledge.tsx b/docs/src/pages/cursed-knowledge.tsx
index 55bb3d4cee..1e5c724d16 100644
--- a/docs/src/pages/cursed-knowledge.tsx
+++ b/docs/src/pages/cursed-knowledge.tsx
@@ -6,6 +6,7 @@ import {
   mdiLeadPencil,
   mdiLockOff,
   mdiLockOutline,
+  mdiMicrosoftWindows,
   mdiSecurity,
   mdiSpeedometerSlow,
   mdiTrashCan,
@@ -21,6 +22,18 @@ const withLanguage = (date: Date) => (language: string) => date.toLocaleDateStri
 type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date };
 
 const items: Item[] = [
+  {
+    icon: mdiMicrosoftWindows,
+    iconColor: '#357EC7',
+    title: 'Hidden files in Windows are cursed',
+    description:
+      'Hidden files in Windows cannot be opened with the "w" flag. That, combined with SMB option "hide dot files" leads to a lot of confusion.',
+    link: {
+      url: 'https://github.com/immich-app/immich/pull/12812',
+      text: '#12812',
+    },
+    date: new Date(2024, 8, 20),
+  },
   {
     icon: mdiWrap,
     iconColor: 'gray',
diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx
index a375efb8a8..a5dbc7aa98 100644
--- a/docs/src/pages/index.tsx
+++ b/docs/src/pages/index.tsx
@@ -2,46 +2,82 @@ import React from 'react';
 import Link from '@docusaurus/Link';
 import Layout from '@theme/Layout';
 import { useColorMode } from '@docusaurus/theme-common';
+import { discordPath } from '@site/src/components/svg-paths';
+import Icon from '@mdi/react';
 function HomepageHeader() {
   const { isDarkTheme } = useColorMode();
 
   return (
     <header>
-      <section className="text-center m-6 p-12 border border-red-400 rounded-[50px] bg-slate-200 dark:bg-immich-dark-gray">
+      <div className="top-[calc(12%)]  md:top-[calc(30%)] h-screen w-full absolute -z-10">
+        <img src={'img/immich-logo.svg'} className="h-[110%] w-[110%] mb-2 antialiased -z-10" alt="Immich logo" />
+        <div className="w-full h-[120vh] absolute left-0 top-0 backdrop-blur-3xl bg-immich-bg/40 dark:bg-transparent"></div>
+      </div>
+      <section className="text-center pt-12 sm:pt-24 bg-immich-bg/50 dark:bg-immich-dark-bg/80">
         <img
-          src={isDarkTheme ? 'img/immich-logo-stacked-dark.svg' : 'img/immich-logo-stacked-light.svg'}
-          className="md:h-60 h-44 mb-2 antialiased rounded-none"
+          src={isDarkTheme ? 'img/logomark-dark.svg' : 'img/logomark-light.svg'}
+          className="h-[115px] w-[115px] mb-2 antialiased rounded-none"
           alt="Immich logo"
         />
-        <div className="sm:text-2xl text-lg md:text-4xl mb-12 sm:leading-tight">
-          <p className="mb-1 font-medium text-immich-primary dark:text-immich-dark-primary">
-            Self-hosted photo and <span className="block"></span>
-            video management solution<span className="block"></span>
+        <div className="mt-8">
+          <p className="text-3xl md:text-5xl sm:leading-tight mb-1 font-extrabold text-black/90 dark:text-white px-4">
+            Self-hosted{' '}
+            <span className="text-immich-primary dark:text-immich-dark-primary">
+              photo and <span className="block"></span>
+              video management{' '}
+            </span>
+            solution<span className="block"></span>
+          </p>
+
+          <p className="max-w-1/4 m-auto mt-4 px-4">
+            Easily back up, organize, and manage your photos on your own server. Immich helps you
+            <span className="sm:block"></span> browse, search and organize your photos and videos with ease, without
+            sacrificing your privacy.
           </p>
         </div>
-        <div className="flex flex-col sm:flex-row place-items-center place-content-center mt-9 mb-16 gap-4 ">
+
+        <div className="flex flex-col sm:flex-row place-items-center place-content-center mt-9 gap-4 ">
           <Link
-            className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary dark:bg-immich-dark-primary rounded-full no-underline hover:no-underline text-white hover:text-gray-50 dark:text-immich-dark-bg font-bold uppercase"
+            className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary dark:bg-immich-dark-primary rounded-xl no-underline hover:no-underline text-white hover:text-gray-50 dark:text-immich-dark-bg font-bold uppercase"
             to="docs/overview/introduction"
           >
             Get started
           </Link>
 
           <Link
-            className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary/10 dark:bg-gray-300  rounded-full hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
+            className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary/10 dark:bg-gray-300  rounded-xl hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
             to="https://demo.immich.app/"
           >
-            Demo portal
-          </Link>
-
-          <Link
-            className="flex place-items-center place-content-center py-3 px-8 border bg-immich-dark-primary dark:bg-immich-primary  rounded-full hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
-            to="https://discord.immich.app"
-          >
-            Discord
+            Demo
           </Link>
         </div>
-        <img src="/img/immich-screenshots.webp" alt="screenshots" width={'70%'} />
+
+        <div className="my-12 flex gap-1 font-medium place-items-center place-content-center text-immich-primary dark:text-immich-dark-primary">
+          <Icon path={discordPath} size={1} />
+          <Link to="https://discord.immich.app/">Join our Discord</Link>
+        </div>
+        <img
+          src={isDarkTheme ? '/img/screenshot-dark.webp' : '/img/screenshot-light.webp'}
+          alt="screenshots"
+          className="w-[95%] lg:w-[85%] xl:w-[70%] 2xl:w-[60%] "
+        />
+
+        <div className="mx-[25%] m-auto my-14 md:my-28">
+          <hr className="border bg-gray-500 dark:bg-gray-400" />
+        </div>
+
+        <img
+          src={isDarkTheme ? 'img/logomark-dark.svg' : 'img/logomark-light.svg'}
+          className="h-[115px] w-[115px] mb-2 antialiased rounded-none"
+          alt="Immich logo"
+        />
+
+        <div>
+          <p className="font-bold text-2xl md:text-5xl ">Download mobile app</p>
+          <p className="text-lg">
+            Download Immich app and start backing up your photos and videos securely to your own server
+          </p>
+        </div>
         <div className="flex flex-col sm:flex-row place-items-center place-content-center mt-4 gap-1">
           <div className="h-24">
             <a href="https://play.google.com/store/apps/details?id=app.alextran.immich">
@@ -54,6 +90,13 @@ function HomepageHeader() {
             </a>
           </div>
         </div>
+
+        <img
+          src={isDarkTheme ? '/img/app-qr-code-dark.svg' : '/img/app-qr-code-light.svg'}
+          alt="app qr code"
+          width={'150px'}
+          className="shadow-lg p-3 my-8 dark:bg-immich-dark-bg "
+        />
       </section>
     </header>
   );
@@ -61,13 +104,9 @@ function HomepageHeader() {
 
 export default function Home(): JSX.Element {
   return (
-    <Layout
-      title="Home"
-      description="immich Self-hosted photo and video backup solution directly from your mobile phone "
-      noFooter={true}
-    >
+    <Layout title="Home" description="Self-hosted photo and video management solution" noFooter={true}>
       <HomepageHeader />
-      <div className="flex flex-col place-items-center place-content-center">
+      <div className="flex flex-col place-items-center text-center place-content-center dark:bg-immich-dark-bg py-8">
         <p>This project is available under GNU AGPL v3 license.</p>
         <p className="text-xs">Privacy should not be a luxury</p>
       </div>
diff --git a/docs/src/pages/roadmap.tsx b/docs/src/pages/roadmap.tsx
index b7c3c8af20..7de51f7513 100644
--- a/docs/src/pages/roadmap.tsx
+++ b/docs/src/pages/roadmap.tsx
@@ -70,14 +70,19 @@ import {
   mdiThemeLightDark,
   mdiTrashCanOutline,
   mdiVectorCombine,
+  mdiFolderSync,
+  mdiFaceRecognition,
   mdiVideo,
   mdiWeb,
+  mdiDatabaseOutline,
 } from '@mdi/js';
 import Layout from '@theme/Layout';
 import React from 'react';
 import { Item, Timeline } from '../components/timeline';
 
 const releases = {
+  'v1.120.0': new Date(2024, 10, 6),
+  'v1.114.0': new Date(2024, 8, 6),
   'v1.113.0': new Date(2024, 7, 30),
   'v1.112.0': new Date(2024, 7, 14),
   'v1.111.0': new Date(2024, 6, 26),
@@ -148,6 +153,9 @@ const weirdTags = {
   'v1.2.0': 'v0.2-dev ',
 };
 
+const title = 'Roadmap';
+const description = 'A list of future plans and goals, as well as past achievements and milestones.';
+
 const withLanguage = (date: Date) => (language: string) => date.toLocaleDateString(language);
 
 type Base = { icon: string; iconColor?: React.CSSProperties['color']; title: string; description: string };
@@ -172,6 +180,38 @@ const withRelease = ({
 };
 
 const roadmap: Item[] = [
+  {
+    done: false,
+    icon: mdiFlash,
+    iconColor: 'gold',
+    title: 'Workflows',
+    description: 'Automate tasks with workflows',
+    getDateLabel: () => 'Planned for 2025',
+  },
+  {
+    done: false,
+    icon: mdiTableKey,
+    iconColor: 'gray',
+    title: 'Fine grained access controls',
+    description: 'Granular access controls for users and api keys',
+    getDateLabel: () => 'Planned for 2025',
+  },
+  {
+    done: false,
+    icon: mdiImageEdit,
+    iconColor: 'rebeccapurple',
+    title: 'Basic editor',
+    description: 'Basic photo editing capabilities',
+    getDateLabel: () => 'Planned for 2025',
+  },
+  {
+    done: false,
+    icon: mdiRocketLaunch,
+    iconColor: 'indianred',
+    title: 'Stable release',
+    description: 'Immich goes stable',
+    getDateLabel: () => 'Planned for early 2025',
+  },
   {
     done: false,
     icon: mdiLockOutline,
@@ -180,14 +220,6 @@ const roadmap: Item[] = [
     description: 'Private assets with extra protections',
     getDateLabel: () => 'Planned for 2024',
   },
-  {
-    done: false,
-    icon: mdiRocketLaunch,
-    iconColor: 'indianred',
-    title: 'Stable release',
-    description: 'Immich goes stable',
-    getDateLabel: () => 'Planned for 2024',
-  },
   {
     done: false,
     icon: mdiCloudUploadOutline,
@@ -196,30 +228,6 @@ const roadmap: Item[] = [
     description: 'Rework background backups to be more reliable',
     getDateLabel: () => 'Planned for 2024',
   },
-  {
-    done: false,
-    icon: mdiImageEdit,
-    iconColor: 'rebeccapurple',
-    title: 'Basic editor',
-    description: 'Basic photo editing capabilities',
-    getDateLabel: () => 'Planned for 2024',
-  },
-  {
-    done: false,
-    icon: mdiFlash,
-    iconColor: 'gold',
-    title: 'Workflows',
-    description: 'Automate tasks with workflows',
-    getDateLabel: () => 'Planned for 2024',
-  },
-  {
-    done: false,
-    icon: mdiTableKey,
-    iconColor: 'gray',
-    title: 'Fine grained access controls',
-    description: 'Granular access controls for users and api keys',
-    getDateLabel: () => 'Planned for 2024',
-  },
   {
     done: false,
     icon: mdiCameraBurst,
@@ -231,6 +239,26 @@ const roadmap: Item[] = [
 ];
 
 const milestones: Item[] = [
+  withRelease({
+    icon: mdiDatabaseOutline,
+    iconColor: 'brown',
+    title: 'Automatic database backups',
+    description: 'Database backups are now integrated into the Immich server',
+    release: 'v1.120.0',
+  }),
+  {
+    icon: mdiStar,
+    iconColor: 'gold',
+    title: '50,000 Stars',
+    description: 'Reached 50K Stars on GitHub!',
+    getDateLabel: withLanguage(new Date(2024, 10, 1)),
+  },
+  withRelease({
+    icon: mdiFaceRecognition,
+    title: 'Metadata Face Import',
+    description: 'Read face metadata in Digikam format during import',
+    release: 'v1.114.0',
+  }),
   withRelease({
     icon: mdiTagMultiple,
     iconColor: 'orange',
@@ -238,11 +266,18 @@ const milestones: Item[] = [
     description: 'Tag your photos and videos',
     release: 'v1.113.0',
   }),
+  withRelease({
+    icon: mdiFolderSync,
+    iconColor: 'green',
+    title: 'Album sync (mobile)',
+    description: 'Sync or mirror an album from your phone to the Immich server',
+    release: 'v1.113.0',
+  }),
   withRelease({
     icon: mdiFolderMultiple,
     iconColor: 'brown',
     title: 'Folders',
-    description: 'View your photos and videos in folders',
+    description: 'Browse your photos and videos in their folder structure',
     release: 'v1.113.0',
   }),
   withRelease({
@@ -837,14 +872,12 @@ const milestones: Item[] = [
 
 export default function MilestonePage(): JSX.Element {
   return (
-    <Layout title="Milestones" description="History of Immich">
+    <Layout title={title} description={description}>
       <section className="my-8">
         <h1 className="md:text-6xl text-center mb-10 text-immich-primary dark:text-immich-dark-primary px-2">
-          Roadmap
+          {title}
         </h1>
-        <p className="text-center text-xl px-2">
-          A list of future plans and goals, as well as past achievements and milestones.
-        </p>
+        <p className="text-center text-xl px-2">{description}</p>
         <div className="flex justify-around mt-8 w-full max-w-full">
           <Timeline items={[...roadmap, ...milestones]} />
         </div>
diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json
index 18f3b0e40f..562098f76f 100644
--- a/docs/static/archived-versions.json
+++ b/docs/static/archived-versions.json
@@ -1,4 +1,76 @@
 [
+  {
+    "label": "v1.123.0",
+    "url": "https://v1.123.0.archive.immich.app"
+  },
+  {
+    "label": "v1.122.3",
+    "url": "https://v1.122.3.archive.immich.app"
+  },
+  {
+    "label": "v1.122.2",
+    "url": "https://v1.122.2.archive.immich.app"
+  },
+  {
+    "label": "v1.122.1",
+    "url": "https://v1.122.1.archive.immich.app"
+  },
+  {
+    "label": "v1.122.0",
+    "url": "https://v1.122.0.archive.immich.app"
+  },
+  {
+    "label": "v1.121.0",
+    "url": "https://v1.121.0.archive.immich.app"
+  },
+  {
+    "label": "v1.120.2",
+    "url": "https://v1.120.2.archive.immich.app"
+  },
+  {
+    "label": "v1.120.1",
+    "url": "https://v1.120.1.archive.immich.app"
+  },
+  {
+    "label": "v1.120.0",
+    "url": "https://v1.120.0.archive.immich.app"
+  },
+  {
+    "label": "v1.119.1",
+    "url": "https://v1.119.1.archive.immich.app"
+  },
+  {
+    "label": "v1.119.0",
+    "url": "https://v1.119.0.archive.immich.app"
+  },
+  {
+    "label": "v1.118.2",
+    "url": "https://v1.118.2.archive.immich.app"
+  },
+  {
+    "label": "v1.118.1",
+    "url": "https://v1.118.1.archive.immich.app"
+  },
+  {
+    "label": "v1.118.0",
+    "url": "https://v1.118.0.archive.immich.app"
+  },
+  {
+    "label": "v1.117.0",
+    "url": "https://v1.117.0.archive.immich.app"
+  },
+  {
+    "label": "v1.116.2",
+    "url": "https://v1.116.2.archive.immich.app"
+  },
+  {
+    "label": "v1.116.1",
+    "url": "https://v1.116.1.archive.immich.app"
+  },
+  {
+    "label": "v1.116.0",
+    "url": "https://v1.116.0.archive.immich.app"
+  },
   {
     "label": "v1.115.0",
     "url": "https://v1.115.0.archive.immich.app"
diff --git a/docs/static/img/app-qr-code-dark.svg b/docs/static/img/app-qr-code-dark.svg
new file mode 100644
index 0000000000..c2d593ea2a
--- /dev/null
+++ b/docs/static/img/app-qr-code-dark.svg
@@ -0,0 +1,378 @@
+<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_1160_310)">
+<path d="M500 0H0V500H500V0Z" fill="#070915"/>
+<mask id="mask0_1160_310" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="18" y="18" width="464" height="464">
+<path d="M18 162H34V146H26C23.8783 146 21.8434 146.843 20.3431 148.343C18.8428 149.843 18 151.878 18 154" fill="white"/>
+<path d="M34 178V162H18L18 170C18 172.122 18.8429 174.157 20.3431 175.657C21.8434 177.157 23.8783 178 26 178" fill="white"/>
+<path d="M18 242H34V234C34 231.878 33.1572 229.843 31.6569 228.343C30.1566 226.843 28.1217 226 26 226C23.8783 226 21.8434 226.843 20.3431 228.343C18.8428 229.843 18 231.878 18 234" fill="white"/>
+<path d="M34 242H18V258H34V242Z" fill="white"/>
+<path d="M34 258H18V266C18 268.122 18.8428 270.157 20.3431 271.657C21.8434 273.157 23.8783 274 26 274C28.1217 274 30.1566 273.157 31.6569 271.657C33.1572 270.157 34 268.122 34 266" fill="white"/>
+<path d="M34 322V306H26C23.8783 306 21.8434 306.843 20.3431 308.343C18.8429 309.843 18 311.878 18 314C18 316.122 18.8429 318.157 20.3431 319.657C21.8434 321.157 23.8783 322 26 322" fill="white"/>
+<path d="M50 146H34V162H50V146Z" fill="white"/>
+<path d="M50 162H34V178H42C44.1217 178 46.1566 177.157 47.6569 175.657C49.1572 174.157 50 172.122 50 170" fill="white"/>
+<path d="M34 290H50V282C50 279.878 49.1572 277.843 47.6569 276.343C46.1566 274.843 44.1217 274 42 274C39.8783 274 37.8434 274.843 36.3431 276.343C34.8428 277.843 34 279.878 34 282" fill="white"/>
+<path d="M50 290H34V306H50V290Z" fill="white"/>
+<path d="M50 306H34V322H50V306Z" fill="white"/>
+<path d="M66 146H50V162H66V146Z" fill="white"/>
+<path d="M58 258C62.4183 258 66 254.418 66 250C66 245.582 62.4183 242 58 242C53.5817 242 50 245.582 50 250C50 254.418 53.5817 258 58 258Z" fill="white"/>
+<path d="M50 306V322H66V314C66 311.878 65.1571 309.843 63.6569 308.343C62.1566 306.843 60.1217 306 58 306" fill="white"/>
+<path d="M66 322H50V330C50 332.122 50.8428 334.157 52.3431 335.657C53.8434 337.157 55.8783 338 58 338C60.1217 338 62.1566 337.157 63.6569 335.657C65.1572 334.157 66 332.122 66 330" fill="white"/>
+<path d="M82 146H66V162H82V146Z" fill="white"/>
+<path d="M66 210H82V202C82 199.878 81.1572 197.843 79.6569 196.343C78.1566 194.843 76.1217 194 74 194C71.8783 194 69.8434 194.843 68.3431 196.343C66.8428 197.843 66 199.878 66 202" fill="white"/>
+<path d="M82 210H66V218C66 220.122 66.8428 222.157 68.3431 223.657C69.8434 225.157 71.8783 226 74 226C76.1217 226 78.1566 225.157 79.6569 223.657C81.1572 222.157 82 220.122 82 218" fill="white"/>
+<path d="M74 354C78.4183 354 82 350.418 82 346C82 341.582 78.4183 338 74 338C69.5817 338 66 341.582 66 346C66 350.418 69.5817 354 74 354Z" fill="white"/>
+<path d="M82 146V162H98V154C98 151.878 97.1571 149.843 95.6569 148.343C94.1566 146.843 92.1217 146 90 146" fill="white"/>
+<path d="M98 162H82V178H98V162Z" fill="white"/>
+<path d="M98 194V178H82V186C82 188.122 82.8429 190.157 84.3431 191.657C85.8434 193.157 87.8783 194 90 194" fill="white"/>
+<path d="M82 242H98V226H90C87.8783 226 85.8434 226.843 84.3431 228.343C82.8428 229.843 82 231.878 82 234" fill="white"/>
+<path d="M98 258V242H82V250C82 252.122 82.8429 254.157 84.3431 255.657C85.8434 257.157 87.8783 258 90 258" fill="white"/>
+<path d="M98 290V274H90C87.8783 274 85.8434 274.843 84.3431 276.343C82.8429 277.843 82 279.878 82 282C82 284.122 82.8429 286.157 84.3431 287.657C85.8434 289.157 87.8783 290 90 290" fill="white"/>
+<path d="M82 322H98V314C98 311.878 97.1572 309.843 95.6569 308.343C94.1566 306.843 92.1217 306 90 306C87.8783 306 85.8434 306.843 84.3431 308.343C82.8428 309.843 82 311.878 82 314" fill="white"/>
+<path d="M98 338V322H82V330C82 332.122 82.8429 334.157 84.3431 335.657C85.8434 337.157 87.8783 338 90 338" fill="white"/>
+<path d="M98 162V178H114V170C114 167.878 113.157 165.843 111.657 164.343C110.157 162.843 108.122 162 106 162" fill="white"/>
+<path d="M114 178H98V194H114V178Z" fill="white"/>
+<path d="M114 194H98V210H114V194Z" fill="white"/>
+<path d="M114 210H98V226H114V210Z" fill="white"/>
+<path d="M114 226H98V242H114V226Z" fill="white"/>
+<path d="M114 242H98V258H114V242Z" fill="white"/>
+<path d="M114 258H98V274H114V258Z" fill="white"/>
+<path d="M114 274H98V290H114V274Z" fill="white"/>
+<path d="M114 290H98V298C98 300.122 98.8428 302.157 100.343 303.657C101.843 305.157 103.878 306 106 306C108.122 306 110.157 305.157 111.657 303.657C113.157 302.157 114 300.122 114 298" fill="white"/>
+<path d="M98 322V338H114V330C114 327.878 113.157 325.843 111.657 324.343C110.157 322.843 108.122 322 106 322" fill="white"/>
+<path d="M114 354V338H98V346C98 348.122 98.8429 350.157 100.343 351.657C101.843 353.157 103.878 354 106 354" fill="white"/>
+<path d="M130 162V146H122C119.878 146 117.843 146.843 116.343 148.343C114.843 149.843 114 151.878 114 154C114 156.122 114.843 158.157 116.343 159.657C117.843 161.157 119.878 162 122 162" fill="white"/>
+<path d="M114 178V194H122C124.122 194 126.157 193.157 127.657 191.657C129.157 190.157 130 188.122 130 186C130 183.878 129.157 181.843 127.657 180.343C126.157 178.843 124.122 178 122 178" fill="white"/>
+<path d="M114 210V226H122C124.122 226 126.157 225.157 127.657 223.657C129.157 222.157 130 220.122 130 218C130 215.878 129.157 213.843 127.657 212.343C126.157 210.843 124.122 210 122 210" fill="white"/>
+<path d="M130 242H114V258H130V242Z" fill="white"/>
+<path d="M114 274V290H122C124.122 290 126.157 289.157 127.657 287.657C129.157 286.157 130 284.122 130 282C130 279.878 129.157 277.843 127.657 276.343C126.157 274.843 124.122 274 122 274" fill="white"/>
+<path d="M130 322V306H122C119.878 306 117.843 306.843 116.343 308.343C114.843 309.843 114 311.878 114 314C114 316.122 114.843 318.157 116.343 319.657C117.843 321.157 119.878 322 122 322" fill="white"/>
+<path d="M130 338H114V354H130V338Z" fill="white"/>
+<path d="M130 146V162H146V154C146 151.878 145.157 149.843 143.657 148.343C142.157 146.843 140.122 146 138 146" fill="white"/>
+<path d="M146 162H130V170C130 172.122 130.843 174.157 132.343 175.657C133.843 177.157 135.878 178 138 178C140.122 178 142.157 177.157 143.657 175.657C145.157 174.157 146 172.122 146 170" fill="white"/>
+<path d="M138 210C142.418 210 146 206.418 146 202C146 197.582 142.418 194 138 194C133.582 194 130 197.582 130 202C130 206.418 133.582 210 138 210Z" fill="white"/>
+<path d="M130 242V258H138C140.122 258 142.157 257.157 143.657 255.657C145.157 254.157 146 252.122 146 250C146 247.878 145.157 245.843 143.657 244.343C142.157 242.843 140.122 242 138 242" fill="white"/>
+<path d="M130 306H146V298C146 295.878 145.157 293.843 143.657 292.343C142.157 290.843 140.122 290 138 290C135.878 290 133.843 290.843 132.343 292.343C130.843 293.843 130 295.878 130 298" fill="white"/>
+<path d="M146 306H130V322H146V306Z" fill="white"/>
+<path d="M146 322H130V338H146V322Z" fill="white"/>
+<path d="M146 338H130V354H138C140.122 354 142.157 353.157 143.657 351.657C145.157 350.157 146 348.122 146 346" fill="white"/>
+<path d="M146 50H162V42C162 39.8783 161.157 37.8434 159.657 36.3431C158.157 34.8428 156.122 34 154 34C151.878 34 149.843 34.8428 148.343 36.3431C146.843 37.8434 146 39.8783 146 42" fill="white"/>
+<path d="M162 66V50H146V58C146 60.1217 146.843 62.1566 148.343 63.6569C149.843 65.1571 151.878 66 154 66" fill="white"/>
+<path d="M162 98V82H154C151.878 82 149.843 82.8429 148.343 84.3431C146.843 85.8434 146 87.8783 146 90C146 92.1217 146.843 94.1566 148.343 95.6569C149.843 97.1571 151.878 98 154 98" fill="white"/>
+<path d="M146 130H162V122C162 119.878 161.157 117.843 159.657 116.343C158.157 114.843 156.122 114 154 114C151.878 114 149.843 114.843 148.343 116.343C146.843 117.843 146 119.878 146 122" fill="white"/>
+<path d="M162 146V130H146V138C146 140.122 146.843 142.157 148.343 143.657C149.843 145.157 151.878 146 154 146" fill="white"/>
+<path d="M162 194V178H154C151.878 178 149.843 178.843 148.343 180.343C146.843 181.843 146 183.878 146 186C146 188.122 146.843 190.157 148.343 191.657C149.843 193.157 151.878 194 154 194" fill="white"/>
+<path d="M154 226C158.418 226 162 222.418 162 218C162 213.582 158.418 210 154 210C149.582 210 146 213.582 146 218C146 222.418 149.582 226 154 226Z" fill="white"/>
+<path d="M146 274H162V266C162 263.878 161.157 261.843 159.657 260.343C158.157 258.843 156.122 258 154 258C151.878 258 149.843 258.843 148.343 260.343C146.843 261.843 146 263.878 146 266" fill="white"/>
+<path d="M162 274H146V282C146 284.122 146.843 286.157 148.343 287.657C149.843 289.157 151.878 290 154 290C156.122 290 158.157 289.157 159.657 287.657C161.157 286.157 162 284.122 162 282" fill="white"/>
+<path d="M162 306H146V322H162V306Z" fill="white"/>
+<path d="M154 386C158.418 386 162 382.418 162 378C162 373.582 158.418 370 154 370C149.582 370 146 373.582 146 378C146 382.418 149.582 386 154 386Z" fill="white"/>
+<path d="M146 418H162V410C162 407.878 161.157 405.843 159.657 404.343C158.157 402.843 156.122 402 154 402C151.878 402 149.843 402.843 148.343 404.343C146.843 405.843 146 407.878 146 410" fill="white"/>
+<path d="M162 418H146V434H162V418Z" fill="white"/>
+<path d="M162 434H146V450H162V434Z" fill="white"/>
+<path d="M162 450H146V466H162V450Z" fill="white"/>
+<path d="M162 482V466H146V474C146 476.122 146.843 478.157 148.343 479.657C149.843 481.157 151.878 482 154 482" fill="white"/>
+<path d="M178 50H162V66H178V50Z" fill="white"/>
+<path d="M178 66H162V82H178V66Z" fill="white"/>
+<path d="M178 82H162V98H178V82Z" fill="white"/>
+<path d="M178 98H162V106C162 108.122 162.843 110.157 164.343 111.657C165.843 113.157 167.878 114 170 114C172.122 114 174.157 113.157 175.657 111.657C177.157 110.157 178 108.122 178 106" fill="white"/>
+<path d="M178 130H162V146H178V130Z" fill="white"/>
+<path d="M178 146H162V162H178V146Z" fill="white"/>
+<path d="M178 162H162V178H178V162Z" fill="white"/>
+<path d="M178 178H162V194H178V178Z" fill="white"/>
+<path d="M178 194H162V202C162 204.122 162.843 206.157 164.343 207.657C165.843 209.157 167.878 210 170 210C172.122 210 174.157 209.157 175.657 207.657C177.157 206.157 178 204.122 178 202" fill="white"/>
+<path d="M178 242V226H170C167.878 226 165.843 226.843 164.343 228.343C162.843 229.843 162 231.878 162 234C162 236.122 162.843 238.157 164.343 239.657C165.843 241.157 167.878 242 170 242" fill="white"/>
+<path d="M162 306V322H178V314C178 311.878 177.157 309.843 175.657 308.343C174.157 306.843 172.122 306 170 306" fill="white"/>
+<path d="M178 322H162V330C162 332.122 162.843 334.157 164.343 335.657C165.843 337.157 167.878 338 170 338C172.122 338 174.157 337.157 175.657 335.657C177.157 334.157 178 332.122 178 330" fill="white"/>
+<path d="M170 370C174.418 370 178 366.418 178 362C178 357.582 174.418 354 170 354C165.582 354 162 357.582 162 362C162 366.418 165.582 370 170 370Z" fill="white"/>
+<path d="M170 402C174.418 402 178 398.418 178 394C178 389.582 174.418 386 170 386C165.582 386 162 389.582 162 394C162 398.418 165.582 402 170 402Z" fill="white"/>
+<path d="M162 418V434H178V426C178 423.878 177.157 421.843 175.657 420.343C174.157 418.843 172.122 418 170 418" fill="white"/>
+<path d="M178 434H162V450H178V434Z" fill="white"/>
+<path d="M178 466H162V482H178V466Z" fill="white"/>
+<path d="M178 50V66H186C188.122 66 190.157 65.1571 191.657 63.6569C193.157 62.1566 194 60.1217 194 58C194 55.8783 193.157 53.8434 191.657 52.3431C190.157 50.8429 188.122 50 186 50" fill="white"/>
+<path d="M194 82H178V98H194V82Z" fill="white"/>
+<path d="M178 130H194V122C194 119.878 193.157 117.843 191.657 116.343C190.157 114.843 188.122 114 186 114C183.878 114 181.843 114.843 180.343 116.343C178.843 117.843 178 119.878 178 122" fill="white"/>
+<path d="M194 130H178V146H194V130Z" fill="white"/>
+<path d="M194 178H178V194H194V178Z" fill="white"/>
+<path d="M194 226H178V242H194V226Z" fill="white"/>
+<path d="M194 242H178V258H194V242Z" fill="white"/>
+<path d="M194 258H178V274H194V258Z" fill="white"/>
+<path d="M194 274H178V290H194V274Z" fill="white"/>
+<path d="M194 306V290H178V298C178 300.122 178.843 302.157 180.343 303.657C181.843 305.157 183.878 306 186 306" fill="white"/>
+<path d="M186 418C190.418 418 194 414.418 194 410C194 405.582 190.418 402 186 402C181.582 402 178 405.582 178 410C178 414.418 181.582 418 186 418Z" fill="white"/>
+<path d="M178 434V450H194V442C194 439.878 193.157 437.843 191.657 436.343C190.157 434.843 188.122 434 186 434" fill="white"/>
+<path d="M194 450H178V466H194V450Z" fill="white"/>
+<path d="M194 466H178V482H186C188.122 482 190.157 481.157 191.657 479.657C193.157 478.157 194 476.122 194 474" fill="white"/>
+<path d="M202 50C206.418 50 210 46.4183 210 42C210 37.5817 206.418 34 202 34C197.582 34 194 37.5817 194 42C194 46.4183 197.582 50 202 50Z" fill="white"/>
+<path d="M210 82H194V98H210V82Z" fill="white"/>
+<path d="M210 130H194V146H210V130Z" fill="white"/>
+<path d="M210 146H194V154C194 156.122 194.843 158.157 196.343 159.657C197.843 161.157 199.878 162 202 162C204.122 162 206.157 161.157 207.657 159.657C209.157 158.157 210 156.122 210 154" fill="white"/>
+<path d="M194 178V194H202C204.122 194 206.157 193.157 207.657 191.657C209.157 190.157 210 188.122 210 186C210 183.878 209.157 181.843 207.657 180.343C206.157 178.843 204.122 178 202 178" fill="white"/>
+<path d="M194 226V242H210V234C210 231.878 209.157 229.843 207.657 228.343C206.157 226.843 204.122 226 202 226" fill="white"/>
+<path d="M210 242H194V258H202C204.122 258 206.157 257.157 207.657 255.657C209.157 254.157 210 252.122 210 250" fill="white"/>
+<path d="M194 274V290H210V282C210 279.878 209.157 277.843 207.657 276.343C206.157 274.843 204.122 274 202 274" fill="white"/>
+<path d="M210 290H194V306H210V290Z" fill="white"/>
+<path d="M202 338C206.418 338 210 334.418 210 330C210 325.582 206.418 322 202 322C197.582 322 194 325.582 194 330C194 334.418 197.582 338 202 338Z" fill="white"/>
+<path d="M194 370H210V362C210 359.878 209.157 357.843 207.657 356.343C206.157 354.843 204.122 354 202 354C199.878 354 197.843 354.843 196.343 356.343C194.843 357.843 194 359.878 194 362" fill="white"/>
+<path d="M210 370H194V386H210V370Z" fill="white"/>
+<path d="M210 402V386H194V394C194 396.122 194.843 398.157 196.343 399.657C197.843 401.157 199.878 402 202 402" fill="white"/>
+<path d="M202 434C206.418 434 210 430.418 210 426C210 421.582 206.418 418 202 418C197.582 418 194 421.582 194 426C194 430.418 197.582 434 202 434Z" fill="white"/>
+<path d="M194 450V466H202C204.122 466 206.157 465.157 207.657 463.657C209.157 462.157 210 460.122 210 458C210 455.878 209.157 453.843 207.657 452.343C206.157 450.843 204.122 450 202 450" fill="white"/>
+<path d="M218 66C222.418 66 226 62.4183 226 58C226 53.5817 222.418 50 218 50C213.582 50 210 53.5817 210 58C210 62.4183 213.582 66 218 66Z" fill="white"/>
+<path d="M210 82V98H218C220.122 98 222.157 97.1571 223.657 95.6569C225.157 94.1566 226 92.1217 226 90C226 87.8783 225.157 85.8434 223.657 84.3431C222.157 82.8429 220.122 82 218 82" fill="white"/>
+<path d="M210 130H226V122C226 119.878 225.157 117.843 223.657 116.343C222.157 114.843 220.122 114 218 114C215.878 114 213.843 114.843 212.343 116.343C210.843 117.843 210 119.878 210 122" fill="white"/>
+<path d="M226 130H210V146H218C220.122 146 222.157 145.157 223.657 143.657C225.157 142.157 226 140.122 226 138" fill="white"/>
+<path d="M226 178V162H218C215.878 162 213.843 162.843 212.343 164.343C210.843 165.843 210 167.878 210 170C210 172.122 210.843 174.157 212.343 175.657C213.843 177.157 215.878 178 218 178" fill="white"/>
+<path d="M218 226C222.418 226 226 222.418 226 218C226 213.582 222.418 210 218 210C213.582 210 210 213.582 210 218C210 222.418 213.582 226 218 226Z" fill="white"/>
+<path d="M210 290V306H218C220.122 306 222.157 305.157 223.657 303.657C225.157 302.157 226 300.122 226 298C226 295.878 225.157 293.843 223.657 292.343C222.157 290.843 220.122 290 218 290" fill="white"/>
+<path d="M226 354V338H218C215.878 338 213.843 338.843 212.343 340.343C210.843 341.843 210 343.878 210 346C210 348.122 210.843 350.157 212.343 351.657C213.843 353.157 215.878 354 218 354" fill="white"/>
+<path d="M226 370H210V386H226V370Z" fill="white"/>
+<path d="M226 386H210V402H226V386Z" fill="white"/>
+<path d="M234 50C238.418 50 242 46.4183 242 42C242 37.5817 238.418 34 234 34C229.582 34 226 37.5817 226 42C226 46.4183 229.582 50 234 50Z" fill="white"/>
+<path d="M242 82V66H234C231.878 66 229.843 66.8429 228.343 68.3431C226.843 69.8434 226 71.8783 226 74C226 76.1217 226.843 78.1566 228.343 79.6569C229.843 81.1571 231.878 82 234 82" fill="white"/>
+<path d="M242 162H226V178H242V162Z" fill="white"/>
+<path d="M234 274C238.418 274 242 270.418 242 266C242 261.582 238.418 258 234 258C229.582 258 226 261.582 226 266C226 270.418 229.582 274 234 274Z" fill="white"/>
+<path d="M226 338V354H242V346C242 343.878 241.157 341.843 239.657 340.343C238.157 338.843 236.122 338 234 338" fill="white"/>
+<path d="M242 354H226V370H242V354Z" fill="white"/>
+<path d="M242 370H226V386H242V370Z" fill="white"/>
+<path d="M242 386H226V402H242V386Z" fill="white"/>
+<path d="M226 466H242V450H234C231.878 450 229.843 450.843 228.343 452.343C226.843 453.843 226 455.878 226 458" fill="white"/>
+<path d="M242 482V466H226V474C226 476.122 226.843 478.157 228.343 479.657C229.843 481.157 231.878 482 234 482" fill="white"/>
+<path d="M242 66H258V50H250C247.878 50 245.843 50.8428 244.343 52.3431C242.843 53.8434 242 55.8783 242 58" fill="white"/>
+<path d="M258 66H242V82H258V66Z" fill="white"/>
+<path d="M258 82H242V98H258V82Z" fill="white"/>
+<path d="M258 98H242V114H258V98Z" fill="white"/>
+<path d="M258 114H242V122C242 124.122 242.843 126.157 244.343 127.657C245.843 129.157 247.878 130 250 130C252.122 130 254.157 129.157 255.657 127.657C257.157 126.157 258 124.122 258 122" fill="white"/>
+<path d="M242 162V178H258V170C258 167.878 257.157 165.843 255.657 164.343C254.157 162.843 252.122 162 250 162" fill="white"/>
+<path d="M258 178H242V194H258V178Z" fill="white"/>
+<path d="M258 194H242V210H258V194Z" fill="white"/>
+<path d="M258 210H242V226H258V210Z" fill="white"/>
+<path d="M258 226H242V242H258V226Z" fill="white"/>
+<path d="M258 242H242V250C242 252.122 242.843 254.157 244.343 255.657C245.843 257.157 247.878 258 250 258C252.122 258 254.157 257.157 255.657 255.657C257.157 254.157 258 252.122 258 250" fill="white"/>
+<path d="M250 306C254.418 306 258 302.418 258 298C258 293.582 254.418 290 250 290C245.582 290 242 293.582 242 298C242 302.418 245.582 306 250 306Z" fill="white"/>
+<path d="M258 354H242V370H258V354Z" fill="white"/>
+<path d="M258 386H242V402H258V386Z" fill="white"/>
+<path d="M242 450H258V442C258 439.878 257.157 437.843 255.657 436.343C254.157 434.843 252.122 434 250 434C247.878 434 245.843 434.843 244.343 436.343C242.843 437.843 242 439.878 242 442" fill="white"/>
+<path d="M258 450H242V466H258V450Z" fill="white"/>
+<path d="M258 466H242V482H258V466Z" fill="white"/>
+<path d="M258 34H274V18H266C263.878 18 261.843 18.8428 260.343 20.3431C258.843 21.8434 258 23.8783 258 26" fill="white"/>
+<path d="M274 34H258V50H274V34Z" fill="white"/>
+<path d="M274 50H258V66H274V50Z" fill="white"/>
+<path d="M274 66H258V82H266C268.122 82 270.157 81.1572 271.657 79.6569C273.157 78.1566 274 76.1217 274 74" fill="white"/>
+<path d="M274 98H258V114H274V98Z" fill="white"/>
+<path d="M266 146C270.418 146 274 142.418 274 138C274 133.582 270.418 130 266 130C261.582 130 258 133.582 258 138C258 142.418 261.582 146 266 146Z" fill="white"/>
+<path d="M274 194H258V210H274V194Z" fill="white"/>
+<path d="M274 210H258V226H274V210Z" fill="white"/>
+<path d="M258 354H274V338H266C263.878 338 261.843 338.843 260.343 340.343C258.843 341.843 258 343.878 258 346" fill="white"/>
+<path d="M274 354H258V370H274V354Z" fill="white"/>
+<path d="M274 370H258V386H274V370Z" fill="white"/>
+<path d="M274 386H258V402H274V386Z" fill="white"/>
+<path d="M274 466H258V482H274V466Z" fill="white"/>
+<path d="M274 18V34H290V26C290 23.8783 289.157 21.8434 287.657 20.3431C286.157 18.8429 284.122 18 282 18" fill="white"/>
+<path d="M290 34H274V50H290V34Z" fill="white"/>
+<path d="M290 50H274V66H282C284.122 66 286.157 65.1572 287.657 63.6569C289.157 62.1566 290 60.1217 290 58" fill="white"/>
+<path d="M290 98H274V114H290V98Z" fill="white"/>
+<path d="M290 114H274V122C274 124.122 274.843 126.157 276.343 127.657C277.843 129.157 279.878 130 282 130C284.122 130 286.157 129.157 287.657 127.657C289.157 126.157 290 124.122 290 122" fill="white"/>
+<path d="M274 162H290V154C290 151.878 289.157 149.843 287.657 148.343C286.157 146.843 284.122 146 282 146C279.878 146 277.843 146.843 276.343 148.343C274.843 149.843 274 151.878 274 154" fill="white"/>
+<path d="M290 178V162H274V170C274 172.122 274.843 174.157 276.343 175.657C277.843 177.157 279.878 178 282 178" fill="white"/>
+<path d="M274 194V210H290V202C290 199.878 289.157 197.843 287.657 196.343C286.157 194.843 284.122 194 282 194" fill="white"/>
+<path d="M290 210H274V226H290V210Z" fill="white"/>
+<path d="M282 258C286.418 258 290 254.418 290 250C290 245.582 286.418 242 282 242C277.582 242 274 245.582 274 250C274 254.418 277.582 258 282 258Z" fill="white"/>
+<path d="M290 338H274V354H290V338Z" fill="white"/>
+<path d="M290 354H274V370H290V354Z" fill="white"/>
+<path d="M274 386V402H282C284.122 402 286.157 401.157 287.657 399.657C289.157 398.157 290 396.122 290 394C290 391.878 289.157 389.843 287.657 388.343C286.157 386.843 284.122 386 282 386" fill="white"/>
+<path d="M282 434C286.418 434 290 430.418 290 426C290 421.582 286.418 418 282 418C277.582 418 274 421.582 274 426C274 430.418 277.582 434 282 434Z" fill="white"/>
+<path d="M290 466H274V482H290V466Z" fill="white"/>
+<path d="M290 98H306V82H298C295.878 82 293.843 82.8428 292.343 84.3431C290.843 85.8434 290 87.8783 290 90" fill="white"/>
+<path d="M306 98H290V114H306V98Z" fill="white"/>
+<path d="M298 146C302.418 146 306 142.418 306 138C306 133.582 302.418 130 298 130C293.582 130 290 133.582 290 138C290 142.418 293.582 146 298 146Z" fill="white"/>
+<path d="M306 162H290V178H306V162Z" fill="white"/>
+<path d="M306 210H290V226H306V210Z" fill="white"/>
+<path d="M298 274C302.418 274 306 270.418 306 266C306 261.582 302.418 258 298 258C293.582 258 290 261.582 290 266C290 270.418 293.582 274 298 274Z" fill="white"/>
+<path d="M290 338H306V322H298C295.878 322 293.843 322.843 292.343 324.343C290.843 325.843 290 327.878 290 330" fill="white"/>
+<path d="M306 338H290V354H306V338Z" fill="white"/>
+<path d="M306 354H290V370H306V354Z" fill="white"/>
+<path d="M290 450H306V442C306 439.878 305.157 437.843 303.657 436.343C302.157 434.843 300.122 434 298 434C295.878 434 293.843 434.843 292.343 436.343C290.843 437.843 290 439.878 290 442" fill="white"/>
+<path d="M306 450H290V466H306V450Z" fill="white"/>
+<path d="M306 466H290V482H306V466Z" fill="white"/>
+<path d="M314 34C318.418 34 322 30.4183 322 26C322 21.5817 318.418 18 314 18C309.582 18 306 21.5817 306 26C306 30.4183 309.582 34 314 34Z" fill="white"/>
+<path d="M306 82V98H322V90C322 87.8783 321.157 85.8434 319.657 84.3431C318.157 82.8429 316.122 82 314 82" fill="white"/>
+<path d="M322 98H306V114H322V98Z" fill="white"/>
+<path d="M322 114H306V122C306 124.122 306.843 126.157 308.343 127.657C309.843 129.157 311.878 130 314 130C316.122 130 318.157 129.157 319.657 127.657C321.157 126.157 322 124.122 322 122" fill="white"/>
+<path d="M306 162V178H314C316.122 178 318.157 177.157 319.657 175.657C321.157 174.157 322 172.122 322 170C322 167.878 321.157 165.843 319.657 164.343C318.157 162.843 316.122 162 314 162" fill="white"/>
+<path d="M306 210V226H322V218C322 215.878 321.157 213.843 319.657 212.343C318.157 210.843 316.122 210 314 210" fill="white"/>
+<path d="M322 226H306V234C306 236.122 306.843 238.157 308.343 239.657C309.843 241.157 311.878 242 314 242C316.122 242 318.157 241.157 319.657 239.657C321.157 238.157 322 236.122 322 234" fill="white"/>
+<path d="M306 322H322V306H314C311.878 306 309.843 306.843 308.343 308.343C306.843 309.843 306 311.878 306 314" fill="white"/>
+<path d="M322 322H306V338H322V322Z" fill="white"/>
+<path d="M322 338H306V354H322V338Z" fill="white"/>
+<path d="M322 354H306V370H314C316.122 370 318.157 369.157 319.657 367.657C321.157 366.157 322 364.122 322 362" fill="white"/>
+<path d="M314 418C318.418 418 322 414.418 322 410C322 405.582 318.418 402 314 402C309.582 402 306 405.582 306 410C306 414.418 309.582 418 314 418Z" fill="white"/>
+<path d="M322 450H306V466H322V450Z" fill="white"/>
+<path d="M322 466H306V482H322V466Z" fill="white"/>
+<path d="M322 50H338V42C338 39.8783 337.157 37.8434 335.657 36.3431C334.157 34.8428 332.122 34 330 34C327.878 34 325.843 34.8428 324.343 36.3431C322.843 37.8434 322 39.8783 322 42" fill="white"/>
+<path d="M338 50H322V58C322 60.1217 322.843 62.1566 324.343 63.6569C325.843 65.1572 327.878 66 330 66C332.122 66 334.157 65.1572 335.657 63.6569C337.157 62.1566 338 60.1217 338 58" fill="white"/>
+<path d="M338 98H322V114H338V98Z" fill="white"/>
+<path d="M330 146C334.418 146 338 142.418 338 138C338 133.582 334.418 130 330 130C325.582 130 322 133.582 322 138C322 142.418 325.582 146 330 146Z" fill="white"/>
+<path d="M330 210C334.418 210 338 206.418 338 202C338 197.582 334.418 194 330 194C325.582 194 322 197.582 322 202C322 206.418 325.582 210 330 210Z" fill="white"/>
+<path d="M330 274C334.418 274 338 270.418 338 266C338 261.582 334.418 258 330 258C325.582 258 322 261.582 322 266C322 270.418 325.582 274 330 274Z" fill="white"/>
+<path d="M322 306H338V290H330C327.878 290 325.843 290.843 324.343 292.343C322.843 293.843 322 295.878 322 298" fill="white"/>
+<path d="M338 306H322V322H330C332.122 322 334.157 321.157 335.657 319.657C337.157 318.157 338 316.122 338 314" fill="white"/>
+<path d="M338 338H322V354H338V338Z" fill="white"/>
+<path d="M322 386H338V370H330C327.878 370 325.843 370.843 324.343 372.343C322.843 373.843 322 375.878 322 378" fill="white"/>
+<path d="M338 402V386H322V394C322 396.122 322.843 398.157 324.343 399.657C325.843 401.157 327.878 402 330 402" fill="white"/>
+<path d="M338 450H322V466H338V450Z" fill="white"/>
+<path d="M338 466H322V482H338V466Z" fill="white"/>
+<path d="M346 34C350.418 34 354 30.4183 354 26C354 21.5817 350.418 18 346 18C341.582 18 338 21.5817 338 26C338 30.4183 341.582 34 346 34Z" fill="white"/>
+<path d="M338 82H354V74C354 71.8783 353.157 69.8434 351.657 68.3431C350.157 66.8428 348.122 66 346 66C343.878 66 341.843 66.8428 340.343 68.3431C338.843 69.8434 338 71.8783 338 74" fill="white"/>
+<path d="M354 82H338V98H354V82Z" fill="white"/>
+<path d="M354 98H338V114H354V98Z" fill="white"/>
+<path d="M354 114H338V122C338 124.122 338.843 126.157 340.343 127.657C341.843 129.157 343.878 130 346 130C348.122 130 350.157 129.157 351.657 127.657C353.157 126.157 354 124.122 354 122" fill="white"/>
+<path d="M354 162V146H346C343.878 146 341.843 146.843 340.343 148.343C338.843 149.843 338 151.878 338 154C338 156.122 338.843 158.157 340.343 159.657C341.843 161.157 343.878 162 346 162" fill="white"/>
+<path d="M346 226C350.418 226 354 222.418 354 218C354 213.582 350.418 210 346 210C341.582 210 338 213.582 338 218C338 222.418 341.582 226 346 226Z" fill="white"/>
+<path d="M338 290H354V274H346C343.878 274 341.843 274.843 340.343 276.343C338.843 277.843 338 279.878 338 282" fill="white"/>
+<path d="M354 290H338V306H354V290Z" fill="white"/>
+<path d="M338 338H354V322H346C343.878 322 341.843 322.843 340.343 324.343C338.843 325.843 338 327.878 338 330" fill="white"/>
+<path d="M354 338H338V354H354V338Z" fill="white"/>
+<path d="M354 354H338V370H354V354Z" fill="white"/>
+<path d="M354 370H338V386H354V370Z" fill="white"/>
+<path d="M354 386H338V402H354V386Z" fill="white"/>
+<path d="M354 418V402H338V410C338 412.122 338.843 414.157 340.343 415.657C341.843 417.157 343.878 418 346 418" fill="white"/>
+<path d="M354 450H338V466H354V450Z" fill="white"/>
+<path d="M354 466H338V482H354V466Z" fill="white"/>
+<path d="M370 146H354V162H370V146Z" fill="white"/>
+<path d="M370 162H354V178H370V162Z" fill="white"/>
+<path d="M370 178H354V194H370V178Z" fill="white"/>
+<path d="M370 194H354V202C354 204.122 354.843 206.157 356.343 207.657C357.843 209.157 359.878 210 362 210C364.122 210 366.157 209.157 367.657 207.657C369.157 206.157 370 204.122 370 202" fill="white"/>
+<path d="M354 242H370V226H362C359.878 226 357.843 226.843 356.343 228.343C354.843 229.843 354 231.878 354 234" fill="white"/>
+<path d="M370 242H354V250C354 252.122 354.843 254.157 356.343 255.657C357.843 257.157 359.878 258 362 258C364.122 258 366.157 257.157 367.657 255.657C369.157 254.157 370 252.122 370 250" fill="white"/>
+<path d="M354 274V290H370V282C370 279.878 369.157 277.843 367.657 276.343C366.157 274.843 364.122 274 362 274" fill="white"/>
+<path d="M370 290H354V306H362C364.122 306 366.157 305.157 367.657 303.657C369.157 302.157 370 300.122 370 298" fill="white"/>
+<path d="M370 322H354V338H370V322Z" fill="white"/>
+<path d="M370 338H354V354H370V338Z" fill="white"/>
+<path d="M370 402H354V418H370V402Z" fill="white"/>
+<path d="M370 450H354V466H370V450Z" fill="white"/>
+<path d="M370 466H354V482H370V466Z" fill="white"/>
+<path d="M370 146V162H378C380.122 162 382.157 161.157 383.657 159.657C385.157 158.157 386 156.122 386 154C386 151.878 385.157 149.843 383.657 148.343C382.157 146.843 380.122 146 378 146" fill="white"/>
+<path d="M370 226H386V210H378C375.878 210 373.843 210.843 372.343 212.343C370.843 213.843 370 215.878 370 218" fill="white"/>
+<path d="M386 226H370V242H386V226Z" fill="white"/>
+<path d="M370 322H386V306H378C375.878 306 373.843 306.843 372.343 308.343C370.843 309.843 370 311.878 370 314" fill="white"/>
+<path d="M386 322H370V338H386V322Z" fill="white"/>
+<path d="M386 338H370V354H386V338Z" fill="white"/>
+<path d="M378 386C382.418 386 386 382.418 386 378C386 373.582 382.418 370 378 370C373.582 370 370 373.582 370 378C370 382.418 373.582 386 378 386Z" fill="white"/>
+<path d="M386 402H370V418H386V402Z" fill="white"/>
+<path d="M370 450H386V442C386 439.878 385.157 437.843 383.657 436.343C382.157 434.843 380.122 434 378 434C375.878 434 373.843 434.843 372.343 436.343C370.843 437.843 370 439.878 370 442" fill="white"/>
+<path d="M386 450H370V466H386V450Z" fill="white"/>
+<path d="M386 466H370V482H378C380.122 482 382.157 481.157 383.657 479.657C385.157 478.157 386 476.122 386 474" fill="white"/>
+<path d="M394 194C398.418 194 402 190.418 402 186C402 181.582 398.418 178 394 178C389.582 178 386 181.582 386 186C386 190.418 389.582 194 394 194Z" fill="white"/>
+<path d="M386 210V226H402V218C402 215.878 401.157 213.843 399.657 212.343C398.157 210.843 396.122 210 394 210" fill="white"/>
+<path d="M402 226H386V242H402V226Z" fill="white"/>
+<path d="M402 242H386V258H402V242Z" fill="white"/>
+<path d="M402 258H386V274H402V258Z" fill="white"/>
+<path d="M402 274H386V290H402V274Z" fill="white"/>
+<path d="M402 290H386V306H402V290Z" fill="white"/>
+<path d="M402 306H386V322H402V306Z" fill="white"/>
+<path d="M402 338H386V354H402V338Z" fill="white"/>
+<path d="M402 402H386V418H402V402Z" fill="white"/>
+<path d="M410 162C414.418 162 418 158.418 418 154C418 149.582 414.418 146 410 146C405.582 146 402 149.582 402 154C402 158.418 405.582 162 410 162Z" fill="white"/>
+<path d="M402 226V242H410C412.122 242 414.157 241.157 415.657 239.657C417.157 238.157 418 236.122 418 234C418 231.878 417.157 229.843 415.657 228.343C414.157 226.843 412.122 226 410 226" fill="white"/>
+<path d="M418 258H402V274H418V258Z" fill="white"/>
+<path d="M418 274H402V290H418V274Z" fill="white"/>
+<path d="M418 306H402V322H418V306Z" fill="white"/>
+<path d="M418 338H402V354H418V338Z" fill="white"/>
+<path d="M418 354H402V370H418V354Z" fill="white"/>
+<path d="M418 370H402V386H418V370Z" fill="white"/>
+<path d="M418 386H402V402H418V386Z" fill="white"/>
+<path d="M418 402H402V418H418V402Z" fill="white"/>
+<path d="M418 434V418H402V426C402 428.122 402.843 430.157 404.343 431.657C405.843 433.157 407.878 434 410 434" fill="white"/>
+<path d="M418 178H434V162H426C423.878 162 421.843 162.843 420.343 164.343C418.843 165.843 418 167.878 418 170" fill="white"/>
+<path d="M434 178H418V186C418 188.122 418.843 190.157 420.343 191.657C421.843 193.157 423.878 194 426 194C428.122 194 430.157 193.157 431.657 191.657C433.157 190.157 434 188.122 434 186" fill="white"/>
+<path d="M426 226C430.418 226 434 222.418 434 218C434 213.582 430.418 210 426 210C421.582 210 418 213.582 418 218C418 222.418 421.582 226 426 226Z" fill="white"/>
+<path d="M418 258H434V242H426C423.878 242 421.843 242.843 420.343 244.343C418.843 245.843 418 247.878 418 250" fill="white"/>
+<path d="M434 258H418V274H434V258Z" fill="white"/>
+<path d="M434 274H418V290H434V274Z" fill="white"/>
+<path d="M418 306V322H426C428.122 322 430.157 321.157 431.657 319.657C433.157 318.157 434 316.122 434 314C434 311.878 433.157 309.843 431.657 308.343C430.157 306.843 428.122 306 426 306" fill="white"/>
+<path d="M418 338V354H434V346C434 343.878 433.157 341.843 431.657 340.343C430.157 338.843 428.122 338 426 338" fill="white"/>
+<path d="M434 354H418V370H426C428.122 370 430.157 369.157 431.657 367.657C433.157 366.157 434 364.122 434 362" fill="white"/>
+<path d="M434 402H418V418H434V402Z" fill="white"/>
+<path d="M434 418H418V434H434V418Z" fill="white"/>
+<path d="M434 162H450V146H442C439.878 146 437.843 146.843 436.343 148.343C434.843 149.843 434 151.878 434 154" fill="white"/>
+<path d="M450 162H434V178H450V162Z" fill="white"/>
+<path d="M450 210V194H442C439.878 194 437.843 194.843 436.343 196.343C434.843 197.843 434 199.878 434 202C434 204.122 434.843 206.157 436.343 207.657C437.843 209.157 439.878 210 442 210" fill="white"/>
+<path d="M434 242H450V234C450 231.878 449.157 229.843 447.657 228.343C446.157 226.843 444.122 226 442 226C439.878 226 437.843 226.843 436.343 228.343C434.843 229.843 434 231.878 434 234" fill="white"/>
+<path d="M450 242H434V258H450V242Z" fill="white"/>
+<path d="M450 258H434V274H450V258Z" fill="white"/>
+<path d="M450 274H434V290H450V274Z" fill="white"/>
+<path d="M442 338C446.418 338 450 334.418 450 330C450 325.582 446.418 322 442 322C437.582 322 434 325.582 434 330C434 334.418 437.582 338 442 338Z" fill="white"/>
+<path d="M434 402H450V386H442C439.878 386 437.843 386.843 436.343 388.343C434.843 389.843 434 391.878 434 394" fill="white"/>
+<path d="M450 402H434V418H450V402Z" fill="white"/>
+<path d="M450 418H434V434H450V418Z" fill="white"/>
+<path d="M450 434H434V442C434 444.122 434.843 446.157 436.343 447.657C437.843 449.157 439.878 450 442 450C444.122 450 446.157 449.157 447.657 447.657C449.157 446.157 450 444.122 450 442" fill="white"/>
+<path d="M442 482C446.418 482 450 478.418 450 474C450 469.582 446.418 466 442 466C437.582 466 434 469.582 434 474C434 478.418 437.582 482 442 482Z" fill="white"/>
+<path d="M450 146V162H466V154C466 151.878 465.157 149.843 463.657 148.343C462.157 146.843 460.122 146 458 146" fill="white"/>
+<path d="M466 162H450V178H466V162Z" fill="white"/>
+<path d="M466 178H450V194H466V178Z" fill="white"/>
+<path d="M466 194H450V210H466V194Z" fill="white"/>
+<path d="M466 226V210H450V218C450 220.122 450.843 222.157 452.343 223.657C453.843 225.157 455.878 226 458 226" fill="white"/>
+<path d="M466 274H450V290H466V274Z" fill="white"/>
+<path d="M466 290H450V306H466V290Z" fill="white"/>
+<path d="M466 306H450V314C450 316.122 450.843 318.157 452.343 319.657C453.843 321.157 455.878 322 458 322C460.122 322 462.157 321.157 463.657 319.657C465.157 318.157 466 316.122 466 314" fill="white"/>
+<path d="M466 370V354H458C455.878 354 453.843 354.843 452.343 356.343C450.843 357.843 450 359.878 450 362C450 364.122 450.843 366.157 452.343 367.657C453.843 369.157 455.878 370 458 370" fill="white"/>
+<path d="M450 386V402H458C460.122 402 462.157 401.157 463.657 399.657C465.157 398.157 466 396.122 466 394C466 391.878 465.157 389.843 463.657 388.343C462.157 386.843 460.122 386 458 386" fill="white"/>
+<path d="M466 466V450H458C455.878 450 453.843 450.843 452.343 452.343C450.843 453.843 450 455.878 450 458C450 460.122 450.843 462.157 452.343 463.657C453.843 465.157 455.878 466 458 466" fill="white"/>
+<path d="M466 210V226H474C476.122 226 478.157 225.157 479.657 223.657C481.157 222.157 482 220.122 482 218C482 215.878 481.157 213.843 479.657 212.343C478.157 210.843 476.122 210 474 210" fill="white"/>
+<path d="M466 274H482V266C482 263.878 481.157 261.843 479.657 260.343C478.157 258.843 476.122 258 474 258C471.878 258 469.843 258.843 468.343 260.343C466.843 261.843 466 263.878 466 266" fill="white"/>
+<path d="M482 274H466V290H482V274Z" fill="white"/>
+<path d="M482 290H466V306H474C476.122 306 478.157 305.157 479.657 303.657C481.157 302.157 482 300.122 482 298" fill="white"/>
+<path d="M466 354H482V346C482 343.878 481.157 341.843 479.657 340.343C478.157 338.843 476.122 338 474 338C471.878 338 469.843 338.843 468.343 340.343C466.843 341.843 466 343.878 466 346" fill="white"/>
+<path d="M482 354H466V370H482V354Z" fill="white"/>
+<path d="M482 370H466V378C466 380.122 466.843 382.157 468.343 383.657C469.843 385.157 471.878 386 474 386C476.122 386 478.157 385.157 479.657 383.657C481.157 382.157 482 380.122 482 378" fill="white"/>
+<path d="M466 418H482V410C482 407.878 481.157 405.843 479.657 404.343C478.157 402.843 476.122 402 474 402C471.878 402 469.843 402.843 468.343 404.343C466.843 405.843 466 407.878 466 410" fill="white"/>
+<path d="M482 418H466V426C466 428.122 466.843 430.157 468.343 431.657C469.843 433.157 471.878 434 474 434C476.122 434 478.157 433.157 479.657 431.657C481.157 430.157 482 428.122 482 426" fill="white"/>
+<path d="M466 450V466H482V458C482 455.878 481.157 453.843 479.657 452.343C478.157 450.843 476.122 450 474 450" fill="white"/>
+<path d="M482 466H466V474C466 476.122 466.843 478.157 468.343 479.657C469.843 481.157 471.878 482 474 482C476.122 482 478.157 481.157 479.657 479.657C481.157 478.157 482 476.122 482 474" fill="white"/>
+</mask>
+<g mask="url(#mask0_1160_310)">
+<path d="M500 0H0V500H500V0Z" fill="#E3E7FF"/>
+</g>
+<mask id="mask1_1160_310" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="18" y="18" width="112" height="112">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M18 58V90C18 100.609 22.2143 110.783 29.7157 118.284C33.4301 121.999 37.8396 124.945 42.6927 126.955C47.5457 128.965 52.7471 130 58 130H90C100.609 130 110.783 125.786 118.284 118.284C125.786 110.783 130 100.609 130 90V58C130 52.7471 128.965 47.5457 126.955 42.6927C124.945 37.8396 121.999 33.4301 118.284 29.7157C114.57 26.0014 110.16 23.055 105.307 21.0448C100.454 19.0346 95.2529 18 90 18H58C52.7471 18 47.5457 19.0346 42.6927 21.0448C37.8396 23.055 33.4301 26.0014 29.7157 29.7157C22.2143 37.2172 18 47.3913 18 58ZM58 34H90C96.3652 34 102.47 36.5286 106.971 41.0294C111.471 45.5303 114 51.6348 114 58V90C114 96.3652 111.471 102.47 106.971 106.971C102.47 111.471 96.3652 114 90 114H58C51.6348 114 45.5303 111.471 41.0294 106.971C36.5286 102.47 34 96.3652 34 90V58C34 51.6348 36.5286 45.5303 41.0294 41.0294C45.5303 36.5286 51.6348 34 58 34Z" fill="white"/>
+</mask>
+<g mask="url(#mask1_1160_310)">
+<path d="M130 18H18V130H130V18Z" fill="#E3E7FF"/>
+</g>
+<mask id="mask2_1160_310" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="50" y="50" width="48" height="48">
+<path d="M74 98C87.2548 98 98 87.2548 98 74C98 60.7452 87.2548 50 74 50C60.7452 50 50 60.7452 50 74C50 87.2548 60.7452 98 74 98Z" fill="white"/>
+</mask>
+<g mask="url(#mask2_1160_310)">
+<path d="M98 50H50V98H98V50Z" fill="#E3E7FF"/>
+</g>
+<mask id="mask3_1160_310" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="370" y="18" width="112" height="112">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M442 18H410C399.391 18 389.217 22.2143 381.716 29.7157C374.214 37.2172 370 47.3913 370 58V90C370 100.609 374.214 110.783 381.716 118.284C389.217 125.786 399.391 130 410 130H442C447.253 130 452.454 128.965 457.307 126.955C462.16 124.945 466.57 121.999 470.284 118.284C473.999 114.57 476.945 110.16 478.955 105.307C480.965 100.454 482 95.2529 482 90V58C482 47.3913 477.786 37.2172 470.284 29.7157C462.783 22.2143 452.609 18 442 18ZM466 58V90C466 96.3652 463.471 102.47 458.971 106.971C454.47 111.471 448.365 114 442 114H410C403.635 114 397.53 111.471 393.029 106.971C388.529 102.47 386 96.3652 386 90V58C386 51.6348 388.529 45.5303 393.029 41.0294C397.53 36.5286 403.635 34 410 34H442C448.365 34 454.47 36.5286 458.971 41.0294C463.471 45.5303 466 51.6348 466 58Z" fill="white"/>
+</mask>
+<g mask="url(#mask3_1160_310)">
+<path d="M482 18H370V130H482V18Z" fill="#E3E7FF"/>
+</g>
+<mask id="mask4_1160_310" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="402" y="50" width="48" height="48">
+<path d="M402 74C402 87.2548 412.745 98 426 98C439.255 98 450 87.2548 450 74C450 60.7452 439.255 50 426 50C412.745 50 402 60.7452 402 74Z" fill="white"/>
+</mask>
+<g mask="url(#mask4_1160_310)">
+<path d="M450 50H402V98H450V50Z" fill="#E3E7FF"/>
+</g>
+<mask id="mask5_1160_310" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="18" y="370" width="112" height="112">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M58 482H90C100.609 482 110.783 477.786 118.284 470.284C121.999 466.57 124.945 462.16 126.955 457.307C128.965 452.454 130 447.253 130 442V410C130 399.391 125.786 389.217 118.284 381.716C110.783 374.214 100.609 370 90 370H58C47.3913 370 37.2172 374.214 29.7157 381.716C22.2143 389.217 18 399.391 18 410V442C18 447.253 19.0346 452.454 21.0448 457.307C23.055 462.16 26.0014 466.57 29.7157 470.284C37.2172 477.786 47.3913 482 58 482ZM34 442V410C34 403.635 36.5286 397.53 41.0294 393.029C45.5303 388.529 51.6348 386 58 386H90C96.3652 386 102.47 388.529 106.971 393.029C111.471 397.53 114 403.635 114 410V442C114 448.365 111.471 454.47 106.971 458.971C102.47 463.471 96.3652 466 90 466H58C51.6348 466 45.5303 463.471 41.0294 458.971C36.5286 454.47 34 448.365 34 442Z" fill="white"/>
+</mask>
+<g mask="url(#mask5_1160_310)">
+<path d="M130 370H18V482H130V370Z" fill="#E3E7FF"/>
+</g>
+<mask id="mask6_1160_310" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="50" y="402" width="48" height="48">
+<path d="M98 426C98 412.745 87.2548 402 74 402C60.7452 402 50 412.745 50 426C50 439.255 60.7452 450 74 450C87.2548 450 98 439.255 98 426Z" fill="white"/>
+</mask>
+<g mask="url(#mask6_1160_310)">
+<path d="M98 402H50V450H98V402Z" fill="#E3E7FF"/>
+</g>
+</g>
+<defs>
+<clipPath id="clip0_1160_310">
+<rect width="500" height="500" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/docs/static/img/app-qr-code-light.svg b/docs/static/img/app-qr-code-light.svg
new file mode 100644
index 0000000000..d5d225201e
--- /dev/null
+++ b/docs/static/img/app-qr-code-light.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500"><defs><clipPath id="clip-path-dot-color"><path d="M 18 146v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,26,154)"/><path d="M 18 162v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(180,26,170)"/><path d="M 18 226v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,26,234)"/><rect x="18" y="242" width="16" height="16" transform="rotate(0,26,250)"/><path d="M 18 258v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,26,266)"/><path d="M 18 306v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(180,26,314)"/><rect x="34" y="146" width="16" height="16" transform="rotate(0,42,154)"/><path d="M 34 162v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(90,42,170)"/><path d="M 34 274v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,42,282)"/><rect x="34" y="290" width="16" height="16" transform="rotate(0,42,298)"/><rect x="34" y="306" width="16" height="16" transform="rotate(0,42,314)"/><rect x="50" y="146" width="16" height="16" transform="rotate(0,58,154)"/><circle cx="58" cy="250" r="8" transform="rotate(0,58,250)"/><path d="M 50 306v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,58,314)"/><path d="M 50 322v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,58,330)"/><rect x="66" y="146" width="16" height="16" transform="rotate(0,74,154)"/><path d="M 66 194v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,74,202)"/><path d="M 66 210v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,74,218)"/><circle cx="74" cy="346" r="8" transform="rotate(0,74,346)"/><path d="M 82 146v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,90,154)"/><rect x="82" y="162" width="16" height="16" transform="rotate(0,90,170)"/><path d="M 82 178v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(180,90,186)"/><path d="M 82 226v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,90,234)"/><path d="M 82 242v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(180,90,250)"/><path d="M 82 274v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(180,90,282)"/><path d="M 82 306v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,90,314)"/><path d="M 82 322v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(180,90,330)"/><path d="M 98 162v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,106,170)"/><rect x="98" y="178" width="16" height="16" transform="rotate(0,106,186)"/><rect x="98" y="194" width="16" height="16" transform="rotate(0,106,202)"/><rect x="98" y="210" width="16" height="16" transform="rotate(0,106,218)"/><rect x="98" y="226" width="16" height="16" transform="rotate(0,106,234)"/><rect x="98" y="242" width="16" height="16" transform="rotate(0,106,250)"/><rect x="98" y="258" width="16" height="16" transform="rotate(0,106,266)"/><rect x="98" y="274" width="16" height="16" transform="rotate(0,106,282)"/><path d="M 98 290v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,106,298)"/><path d="M 98 322v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,106,330)"/><path d="M 98 338v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(180,106,346)"/><path d="M 114 146v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(180,122,154)"/><path d="M 114 178v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(0,122,186)"/><path d="M 114 210v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(0,122,218)"/><rect x="114" y="242" width="16" height="16" transform="rotate(0,122,250)"/><path d="M 114 274v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(0,122,282)"/><path d="M 114 306v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(180,122,314)"/><rect x="114" y="338" width="16" height="16" transform="rotate(0,122,346)"/><path d="M 130 146v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,138,154)"/><path d="M 130 162v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,138,170)"/><circle cx="138" cy="202" r="8" transform="rotate(0,138,202)"/><path d="M 130 242v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(0,138,250)"/><path d="M 130 290v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,138,298)"/><rect x="130" y="306" width="16" height="16" transform="rotate(0,138,314)"/><rect x="130" y="322" width="16" height="16" transform="rotate(0,138,330)"/><path d="M 130 338v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(90,138,346)"/><path d="M 146 34v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,154,42)"/><path d="M 146 50v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(180,154,58)"/><path d="M 146 82v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(180,154,90)"/><path d="M 146 114v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,154,122)"/><path d="M 146 130v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(180,154,138)"/><path d="M 146 178v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(180,154,186)"/><circle cx="154" cy="218" r="8" transform="rotate(0,154,218)"/><path d="M 146 258v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,154,266)"/><path d="M 146 274v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,154,282)"/><rect x="146" y="306" width="16" height="16" transform="rotate(0,154,314)"/><circle cx="154" cy="378" r="8" transform="rotate(0,154,378)"/><path d="M 146 402v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,154,410)"/><rect x="146" y="418" width="16" height="16" transform="rotate(0,154,426)"/><rect x="146" y="434" width="16" height="16" transform="rotate(0,154,442)"/><rect x="146" y="450" width="16" height="16" transform="rotate(0,154,458)"/><path d="M 146 466v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(180,154,474)"/><rect x="162" y="50" width="16" height="16" transform="rotate(0,170,58)"/><rect x="162" y="66" width="16" height="16" transform="rotate(0,170,74)"/><rect x="162" y="82" width="16" height="16" transform="rotate(0,170,90)"/><path d="M 162 98v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,170,106)"/><rect x="162" y="130" width="16" height="16" transform="rotate(0,170,138)"/><rect x="162" y="146" width="16" height="16" transform="rotate(0,170,154)"/><rect x="162" y="162" width="16" height="16" transform="rotate(0,170,170)"/><rect x="162" y="178" width="16" height="16" transform="rotate(0,170,186)"/><path d="M 162 194v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,170,202)"/><path d="M 162 226v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(180,170,234)"/><path d="M 162 306v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,170,314)"/><path d="M 162 322v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,170,330)"/><circle cx="170" cy="362" r="8" transform="rotate(0,170,362)"/><circle cx="170" cy="394" r="8" transform="rotate(0,170,394)"/><path d="M 162 418v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,170,426)"/><rect x="162" y="434" width="16" height="16" transform="rotate(0,170,442)"/><rect x="162" y="466" width="16" height="16" transform="rotate(0,170,474)"/><path d="M 178 50v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(0,186,58)"/><rect x="178" y="82" width="16" height="16" transform="rotate(0,186,90)"/><path d="M 178 114v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,186,122)"/><rect x="178" y="130" width="16" height="16" transform="rotate(0,186,138)"/><rect x="178" y="178" width="16" height="16" transform="rotate(0,186,186)"/><rect x="178" y="226" width="16" height="16" transform="rotate(0,186,234)"/><rect x="178" y="242" width="16" height="16" transform="rotate(0,186,250)"/><rect x="178" y="258" width="16" height="16" transform="rotate(0,186,266)"/><rect x="178" y="274" width="16" height="16" transform="rotate(0,186,282)"/><path d="M 178 290v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(180,186,298)"/><circle cx="186" cy="410" r="8" transform="rotate(0,186,410)"/><path d="M 178 434v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,186,442)"/><rect x="178" y="450" width="16" height="16" transform="rotate(0,186,458)"/><path d="M 178 466v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(90,186,474)"/><circle cx="202" cy="42" r="8" transform="rotate(0,202,42)"/><rect x="194" y="82" width="16" height="16" transform="rotate(0,202,90)"/><rect x="194" y="130" width="16" height="16" transform="rotate(0,202,138)"/><path d="M 194 146v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,202,154)"/><path d="M 194 178v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(0,202,186)"/><path d="M 194 226v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,202,234)"/><path d="M 194 242v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(90,202,250)"/><path d="M 194 274v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,202,282)"/><rect x="194" y="290" width="16" height="16" transform="rotate(0,202,298)"/><circle cx="202" cy="330" r="8" transform="rotate(0,202,330)"/><path d="M 194 354v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,202,362)"/><rect x="194" y="370" width="16" height="16" transform="rotate(0,202,378)"/><path d="M 194 386v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(180,202,394)"/><circle cx="202" cy="426" r="8" transform="rotate(0,202,426)"/><path d="M 194 450v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(0,202,458)"/><circle cx="218" cy="58" r="8" transform="rotate(0,218,58)"/><path d="M 210 82v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(0,218,90)"/><path d="M 210 114v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,218,122)"/><path d="M 210 130v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(90,218,138)"/><path d="M 210 162v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(180,218,170)"/><circle cx="218" cy="218" r="8" transform="rotate(0,218,218)"/><path d="M 210 290v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(0,218,298)"/><path d="M 210 338v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(180,218,346)"/><rect x="210" y="370" width="16" height="16" transform="rotate(0,218,378)"/><rect x="210" y="386" width="16" height="16" transform="rotate(0,218,394)"/><circle cx="234" cy="42" r="8" transform="rotate(0,234,42)"/><path d="M 226 66v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(180,234,74)"/><rect x="226" y="162" width="16" height="16" transform="rotate(0,234,170)"/><circle cx="234" cy="266" r="8" transform="rotate(0,234,266)"/><path d="M 226 338v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,234,346)"/><rect x="226" y="354" width="16" height="16" transform="rotate(0,234,362)"/><rect x="226" y="370" width="16" height="16" transform="rotate(0,234,378)"/><rect x="226" y="386" width="16" height="16" transform="rotate(0,234,394)"/><path d="M 226 450v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,234,458)"/><path d="M 226 466v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(180,234,474)"/><path d="M 242 50v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,250,58)"/><rect x="242" y="66" width="16" height="16" transform="rotate(0,250,74)"/><rect x="242" y="82" width="16" height="16" transform="rotate(0,250,90)"/><rect x="242" y="98" width="16" height="16" transform="rotate(0,250,106)"/><path d="M 242 114v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,250,122)"/><path d="M 242 162v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,250,170)"/><rect x="242" y="178" width="16" height="16" transform="rotate(0,250,186)"/><rect x="242" y="194" width="16" height="16" transform="rotate(0,250,202)"/><rect x="242" y="210" width="16" height="16" transform="rotate(0,250,218)"/><rect x="242" y="226" width="16" height="16" transform="rotate(0,250,234)"/><path d="M 242 242v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,250,250)"/><circle cx="250" cy="298" r="8" transform="rotate(0,250,298)"/><rect x="242" y="354" width="16" height="16" transform="rotate(0,250,362)"/><rect x="242" y="386" width="16" height="16" transform="rotate(0,250,394)"/><path d="M 242 434v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,250,442)"/><rect x="242" y="450" width="16" height="16" transform="rotate(0,250,458)"/><rect x="242" y="466" width="16" height="16" transform="rotate(0,250,474)"/><path d="M 258 18v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,266,26)"/><rect x="258" y="34" width="16" height="16" transform="rotate(0,266,42)"/><rect x="258" y="50" width="16" height="16" transform="rotate(0,266,58)"/><path d="M 258 66v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(90,266,74)"/><rect x="258" y="98" width="16" height="16" transform="rotate(0,266,106)"/><circle cx="266" cy="138" r="8" transform="rotate(0,266,138)"/><rect x="258" y="194" width="16" height="16" transform="rotate(0,266,202)"/><rect x="258" y="210" width="16" height="16" transform="rotate(0,266,218)"/><path d="M 258 338v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,266,346)"/><rect x="258" y="354" width="16" height="16" transform="rotate(0,266,362)"/><rect x="258" y="370" width="16" height="16" transform="rotate(0,266,378)"/><rect x="258" y="386" width="16" height="16" transform="rotate(0,266,394)"/><rect x="258" y="466" width="16" height="16" transform="rotate(0,266,474)"/><path d="M 274 18v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,282,26)"/><rect x="274" y="34" width="16" height="16" transform="rotate(0,282,42)"/><path d="M 274 50v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(90,282,58)"/><rect x="274" y="98" width="16" height="16" transform="rotate(0,282,106)"/><path d="M 274 114v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,282,122)"/><path d="M 274 146v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,282,154)"/><path d="M 274 162v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(180,282,170)"/><path d="M 274 194v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,282,202)"/><rect x="274" y="210" width="16" height="16" transform="rotate(0,282,218)"/><circle cx="282" cy="250" r="8" transform="rotate(0,282,250)"/><rect x="274" y="338" width="16" height="16" transform="rotate(0,282,346)"/><rect x="274" y="354" width="16" height="16" transform="rotate(0,282,362)"/><path d="M 274 386v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(0,282,394)"/><circle cx="282" cy="426" r="8" transform="rotate(0,282,426)"/><rect x="274" y="466" width="16" height="16" transform="rotate(0,282,474)"/><path d="M 290 82v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,298,90)"/><rect x="290" y="98" width="16" height="16" transform="rotate(0,298,106)"/><circle cx="298" cy="138" r="8" transform="rotate(0,298,138)"/><rect x="290" y="162" width="16" height="16" transform="rotate(0,298,170)"/><rect x="290" y="210" width="16" height="16" transform="rotate(0,298,218)"/><circle cx="298" cy="266" r="8" transform="rotate(0,298,266)"/><path d="M 290 322v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,298,330)"/><rect x="290" y="338" width="16" height="16" transform="rotate(0,298,346)"/><rect x="290" y="354" width="16" height="16" transform="rotate(0,298,362)"/><path d="M 290 434v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,298,442)"/><rect x="290" y="450" width="16" height="16" transform="rotate(0,298,458)"/><rect x="290" y="466" width="16" height="16" transform="rotate(0,298,474)"/><circle cx="314" cy="26" r="8" transform="rotate(0,314,26)"/><path d="M 306 82v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,314,90)"/><rect x="306" y="98" width="16" height="16" transform="rotate(0,314,106)"/><path d="M 306 114v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,314,122)"/><path d="M 306 162v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(0,314,170)"/><path d="M 306 210v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,314,218)"/><path d="M 306 226v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,314,234)"/><path d="M 306 306v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,314,314)"/><rect x="306" y="322" width="16" height="16" transform="rotate(0,314,330)"/><rect x="306" y="338" width="16" height="16" transform="rotate(0,314,346)"/><path d="M 306 354v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(90,314,362)"/><circle cx="314" cy="410" r="8" transform="rotate(0,314,410)"/><rect x="306" y="450" width="16" height="16" transform="rotate(0,314,458)"/><rect x="306" y="466" width="16" height="16" transform="rotate(0,314,474)"/><path d="M 322 34v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,330,42)"/><path d="M 322 50v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,330,58)"/><rect x="322" y="98" width="16" height="16" transform="rotate(0,330,106)"/><circle cx="330" cy="138" r="8" transform="rotate(0,330,138)"/><circle cx="330" cy="202" r="8" transform="rotate(0,330,202)"/><circle cx="330" cy="266" r="8" transform="rotate(0,330,266)"/><path d="M 322 290v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,330,298)"/><path d="M 322 306v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(90,330,314)"/><rect x="322" y="338" width="16" height="16" transform="rotate(0,330,346)"/><path d="M 322 370v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,330,378)"/><path d="M 322 386v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(180,330,394)"/><rect x="322" y="450" width="16" height="16" transform="rotate(0,330,458)"/><rect x="322" y="466" width="16" height="16" transform="rotate(0,330,474)"/><circle cx="346" cy="26" r="8" transform="rotate(0,346,26)"/><path d="M 338 66v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,346,74)"/><rect x="338" y="82" width="16" height="16" transform="rotate(0,346,90)"/><rect x="338" y="98" width="16" height="16" transform="rotate(0,346,106)"/><path d="M 338 114v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,346,122)"/><path d="M 338 146v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(180,346,154)"/><circle cx="346" cy="218" r="8" transform="rotate(0,346,218)"/><path d="M 338 274v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,346,282)"/><rect x="338" y="290" width="16" height="16" transform="rotate(0,346,298)"/><path d="M 338 322v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,346,330)"/><rect x="338" y="338" width="16" height="16" transform="rotate(0,346,346)"/><rect x="338" y="354" width="16" height="16" transform="rotate(0,346,362)"/><rect x="338" y="370" width="16" height="16" transform="rotate(0,346,378)"/><rect x="338" y="386" width="16" height="16" transform="rotate(0,346,394)"/><path d="M 338 402v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(180,346,410)"/><rect x="338" y="450" width="16" height="16" transform="rotate(0,346,458)"/><rect x="338" y="466" width="16" height="16" transform="rotate(0,346,474)"/><rect x="354" y="146" width="16" height="16" transform="rotate(0,362,154)"/><rect x="354" y="162" width="16" height="16" transform="rotate(0,362,170)"/><rect x="354" y="178" width="16" height="16" transform="rotate(0,362,186)"/><path d="M 354 194v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,362,202)"/><path d="M 354 226v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,362,234)"/><path d="M 354 242v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,362,250)"/><path d="M 354 274v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,362,282)"/><path d="M 354 290v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(90,362,298)"/><rect x="354" y="322" width="16" height="16" transform="rotate(0,362,330)"/><rect x="354" y="338" width="16" height="16" transform="rotate(0,362,346)"/><rect x="354" y="402" width="16" height="16" transform="rotate(0,362,410)"/><rect x="354" y="450" width="16" height="16" transform="rotate(0,362,458)"/><rect x="354" y="466" width="16" height="16" transform="rotate(0,362,474)"/><path d="M 370 146v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(0,378,154)"/><path d="M 370 210v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,378,218)"/><rect x="370" y="226" width="16" height="16" transform="rotate(0,378,234)"/><path d="M 370 306v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,378,314)"/><rect x="370" y="322" width="16" height="16" transform="rotate(0,378,330)"/><rect x="370" y="338" width="16" height="16" transform="rotate(0,378,346)"/><circle cx="378" cy="378" r="8" transform="rotate(0,378,378)"/><rect x="370" y="402" width="16" height="16" transform="rotate(0,378,410)"/><path d="M 370 434v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,378,442)"/><rect x="370" y="450" width="16" height="16" transform="rotate(0,378,458)"/><path d="M 370 466v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(90,378,474)"/><circle cx="394" cy="186" r="8" transform="rotate(0,394,186)"/><path d="M 386 210v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,394,218)"/><rect x="386" y="226" width="16" height="16" transform="rotate(0,394,234)"/><rect x="386" y="242" width="16" height="16" transform="rotate(0,394,250)"/><rect x="386" y="258" width="16" height="16" transform="rotate(0,394,266)"/><rect x="386" y="274" width="16" height="16" transform="rotate(0,394,282)"/><rect x="386" y="290" width="16" height="16" transform="rotate(0,394,298)"/><rect x="386" y="306" width="16" height="16" transform="rotate(0,394,314)"/><rect x="386" y="338" width="16" height="16" transform="rotate(0,394,346)"/><rect x="386" y="402" width="16" height="16" transform="rotate(0,394,410)"/><circle cx="410" cy="154" r="8" transform="rotate(0,410,154)"/><path d="M 402 226v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(0,410,234)"/><rect x="402" y="258" width="16" height="16" transform="rotate(0,410,266)"/><rect x="402" y="274" width="16" height="16" transform="rotate(0,410,282)"/><rect x="402" y="306" width="16" height="16" transform="rotate(0,410,314)"/><rect x="402" y="338" width="16" height="16" transform="rotate(0,410,346)"/><rect x="402" y="354" width="16" height="16" transform="rotate(0,410,362)"/><rect x="402" y="370" width="16" height="16" transform="rotate(0,410,378)"/><rect x="402" y="386" width="16" height="16" transform="rotate(0,410,394)"/><rect x="402" y="402" width="16" height="16" transform="rotate(0,410,410)"/><path d="M 402 418v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(180,410,426)"/><path d="M 418 162v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,426,170)"/><path d="M 418 178v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,426,186)"/><circle cx="426" cy="218" r="8" transform="rotate(0,426,218)"/><path d="M 418 242v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,426,250)"/><rect x="418" y="258" width="16" height="16" transform="rotate(0,426,266)"/><rect x="418" y="274" width="16" height="16" transform="rotate(0,426,282)"/><path d="M 418 306v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(0,426,314)"/><path d="M 418 338v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,426,346)"/><path d="M 418 354v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(90,426,362)"/><rect x="418" y="402" width="16" height="16" transform="rotate(0,426,410)"/><rect x="418" y="418" width="16" height="16" transform="rotate(0,426,426)"/><path d="M 434 146v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,442,154)"/><rect x="434" y="162" width="16" height="16" transform="rotate(0,442,170)"/><path d="M 434 194v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(180,442,202)"/><path d="M 434 226v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,442,234)"/><rect x="434" y="242" width="16" height="16" transform="rotate(0,442,250)"/><rect x="434" y="258" width="16" height="16" transform="rotate(0,442,266)"/><rect x="434" y="274" width="16" height="16" transform="rotate(0,442,282)"/><circle cx="442" cy="330" r="8" transform="rotate(0,442,330)"/><path d="M 434 386v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(-90,442,394)"/><rect x="434" y="402" width="16" height="16" transform="rotate(0,442,410)"/><rect x="434" y="418" width="16" height="16" transform="rotate(0,442,426)"/><path d="M 434 434v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,442,442)"/><circle cx="442" cy="474" r="8" transform="rotate(0,442,474)"/><path d="M 450 146v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,458,154)"/><rect x="450" y="162" width="16" height="16" transform="rotate(0,458,170)"/><rect x="450" y="178" width="16" height="16" transform="rotate(0,458,186)"/><rect x="450" y="194" width="16" height="16" transform="rotate(0,458,202)"/><path d="M 450 210v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(180,458,218)"/><rect x="450" y="274" width="16" height="16" transform="rotate(0,458,282)"/><rect x="450" y="290" width="16" height="16" transform="rotate(0,458,298)"/><path d="M 450 306v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,458,314)"/><path d="M 450 354v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(180,458,362)"/><path d="M 450 386v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(0,458,394)"/><path d="M 450 450v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(180,458,458)"/><path d="M 466 210v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(0,474,218)"/><path d="M 466 258v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,474,266)"/><rect x="466" y="274" width="16" height="16" transform="rotate(0,474,282)"/><path d="M 466 290v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(90,474,298)"/><path d="M 466 338v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,474,346)"/><rect x="466" y="354" width="16" height="16" transform="rotate(0,474,362)"/><path d="M 466 370v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,474,378)"/><path d="M 466 402v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(-90,474,410)"/><path d="M 466 418v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,474,426)"/><path d="M 466 450v 16h 16v -8a 8 8, 0, 0, 0, -8 -8" transform="rotate(0,474,458)"/><path d="M 466 466v 16h 8a 8 8, 0, 0, 0, 0 -16" transform="rotate(90,474,474)"/></clipPath><clipPath id="clip-path-corners-square-color-0-0"><path clip-rule="evenodd" d="M 18 58v 32a 40 40, 0, 0, 0, 40 40h 32a 40 40, 0, 0, 0, 40 -40v -32a 40 40, 0, 0, 0, -40 -40h -32a 40 40, 0, 0, 0, -40 40M 58 34h 32a 24 24, 0, 0, 1, 24 24v 32a 24 24, 0, 0, 1, -24 24h -32a 24 24, 0, 0, 1, -24 -24v -32a 24 24, 0, 0, 1, 24 -24" transform="rotate(0,74,74)"/></clipPath><clipPath id="clip-path-corners-dot-color-0-0"><circle cx="74" cy="74" r="24" transform="rotate(0,74,74)"/></clipPath><clipPath id="clip-path-corners-square-color-1-0"><path clip-rule="evenodd" d="M 370 58v 32a 40 40, 0, 0, 0, 40 40h 32a 40 40, 0, 0, 0, 40 -40v -32a 40 40, 0, 0, 0, -40 -40h -32a 40 40, 0, 0, 0, -40 40M 410 34h 32a 24 24, 0, 0, 1, 24 24v 32a 24 24, 0, 0, 1, -24 24h -32a 24 24, 0, 0, 1, -24 -24v -32a 24 24, 0, 0, 1, 24 -24" transform="rotate(90,426,74)"/></clipPath><clipPath id="clip-path-corners-dot-color-1-0"><circle cx="426" cy="74" r="24" transform="rotate(90,426,74)"/></clipPath><clipPath id="clip-path-corners-square-color-0-1"><path clip-rule="evenodd" d="M 18 410v 32a 40 40, 0, 0, 0, 40 40h 32a 40 40, 0, 0, 0, 40 -40v -32a 40 40, 0, 0, 0, -40 -40h -32a 40 40, 0, 0, 0, -40 40M 58 386h 32a 24 24, 0, 0, 1, 24 24v 32a 24 24, 0, 0, 1, -24 24h -32a 24 24, 0, 0, 1, -24 -24v -32a 24 24, 0, 0, 1, 24 -24" transform="rotate(-90,74,426)"/></clipPath><clipPath id="clip-path-corners-dot-color-0-1"><circle cx="74" cy="426" r="24" transform="rotate(-90,74,426)"/></clipPath></defs><rect x="0" y="0" height="500" width="500" clip-path="url('#clip-path-background-color')" fill="#fff"/><rect x="0" y="0" height="500" width="500" clip-path="url('#clip-path-dot-color')" fill="#000000"/><rect x="18" y="18" height="112" width="112" clip-path="url('#clip-path-corners-square-color-0-0')" fill="#000000"/><rect x="50" y="50" height="48" width="48" clip-path="url('#clip-path-corners-dot-color-0-0')" fill="#000000"/><rect x="370" y="18" height="112" width="112" clip-path="url('#clip-path-corners-square-color-1-0')" fill="#000000"/><rect x="402" y="50" height="48" width="48" clip-path="url('#clip-path-corners-dot-color-1-0')" fill="#000000"/><rect x="18" y="370" height="112" width="112" clip-path="url('#clip-path-corners-square-color-0-1')" fill="#000000"/><rect x="50" y="402" height="48" width="48" clip-path="url('#clip-path-corners-dot-color-0-1')" fill="#000000"/></svg>
\ No newline at end of file
diff --git a/docs/static/img/feature-panel.png b/docs/static/img/feature-panel.png
new file mode 100644
index 0000000000..8c39fe0d40
Binary files /dev/null and b/docs/static/img/feature-panel.png differ
diff --git a/docs/static/img/immich-screenshots.png b/docs/static/img/immich-screenshots.png
deleted file mode 100644
index 6123279f2d..0000000000
Binary files a/docs/static/img/immich-screenshots.png and /dev/null differ
diff --git a/docs/static/img/immich-screenshots.webp b/docs/static/img/immich-screenshots.webp
deleted file mode 100644
index 62cc036797..0000000000
Binary files a/docs/static/img/immich-screenshots.webp and /dev/null differ
diff --git a/docs/static/img/logomark-dark.svg b/docs/static/img/logomark-dark.svg
new file mode 100644
index 0000000000..51f92109d4
--- /dev/null
+++ b/docs/static/img/logomark-dark.svg
@@ -0,0 +1,9 @@
+<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="0.5" y="0.5" width="95" height="95" rx="47.5" fill="#070915"/>
+<rect x="0.5" y="0.5" width="95" height="95" rx="47.5" stroke="#222326"/>
+<path d="M45.4161 33.5746C50.122 37.7636 53.9145 42.2527 56.3552 46.4834C60.5471 38.9453 63.3483 29.988 63.3836 24.283C63.3836 24.2426 63.3836 24.2059 63.3836 24.1716C63.3836 15.7298 55.0082 12.4445 47.7934 12.4445C40.5786 12.4445 32.2032 15.7298 32.2032 24.1716C32.2032 24.2867 32.2032 24.441 32.2032 24.6271C36.2247 26.4247 40.9915 29.6366 45.4161 33.5746Z" fill="#E93832"/>
+<path d="M19.7453 56.5954C22.6865 53.3052 27.1988 49.7394 32.2908 46.7247C37.708 43.5189 43.1264 41.2793 47.8822 40.2543C42.0473 33.9163 34.4404 28.4697 29.0536 26.6733C29.0159 26.6611 28.9806 26.6501 28.9489 26.639C20.9632 24.0308 15.2671 31.024 13.0384 37.9229C10.8096 44.8218 11.3285 53.8464 19.3142 56.4546C19.4226 56.4901 19.5687 56.5379 19.7453 56.5954Z" fill="#ED79B5"/>
+<path d="M82.6335 37.8066C80.4047 30.9077 74.7086 23.9145 66.7229 26.5227C66.6133 26.5582 66.4672 26.606 66.2918 26.6635C65.8351 31.0632 64.2701 36.6151 61.9123 42.063C59.4046 47.8573 56.3295 52.8717 53.0814 56.5122C61.5067 58.1922 70.8455 58.1016 76.2529 56.3726C76.2907 56.3603 76.326 56.3481 76.3577 56.3383C84.3434 53.7289 84.8622 44.7042 82.6335 37.8066Z" fill="#E8AB17"/>
+<path d="M40.6736 63.34C39.3157 57.1697 38.8712 51.2958 39.3705 46.432C31.5723 50.0529 24.0701 55.644 20.7051 60.2396C20.682 60.2714 20.6601 60.302 20.6406 60.329C15.7057 67.1593 20.5602 74.7672 26.3975 79.0297C32.2337 83.2934 40.9306 85.5857 45.8667 78.7554C45.9349 78.6623 46.0251 78.5374 46.1334 78.3868C43.9303 74.5578 41.95 69.1405 40.6736 63.34Z" fill="#2383F2"/>
+<path d="M74.8999 59.888C70.5971 60.8113 64.862 61.0305 58.9796 60.4587C52.7233 59.8513 47.0309 58.4603 42.5832 56.479C43.5977 65.0542 46.5693 73.9564 49.8759 78.5936C49.8991 78.6255 49.921 78.6561 49.9405 78.683C54.8754 85.5133 63.5723 83.2211 69.4096 78.9573C75.2458 74.6936 80.1015 67.0857 75.1666 60.2566C75.0984 60.1636 75.0083 60.0387 74.8999 59.888Z" fill="#1FBB4C"/>
+</svg>
diff --git a/docs/static/img/logomark-light.svg b/docs/static/img/logomark-light.svg
new file mode 100644
index 0000000000..497fbdcf14
--- /dev/null
+++ b/docs/static/img/logomark-light.svg
@@ -0,0 +1,9 @@
+<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="0.417391" y="0.417391" width="95.1652" height="95.1652" rx="47.5826" fill="white"/>
+<rect x="0.417391" y="0.417391" width="95.1652" height="95.1652" rx="47.5826" stroke="#E9EAEC" stroke-width="0.834783"/>
+<path d="M45.416 33.5745C50.1219 37.7635 53.9144 42.2526 56.3551 46.4832C60.5471 38.9452 63.3482 29.9879 63.3835 24.2829C63.3835 24.2425 63.3835 24.2057 63.3835 24.1715C63.3835 15.7297 55.0081 12.4443 47.7933 12.4443C40.5786 12.4443 32.2031 15.7297 32.2031 24.1715C32.2031 24.2866 32.2031 24.4409 32.2031 24.627C36.2246 26.4246 40.9914 29.6364 45.416 33.5745Z" fill="#FA2921"/>
+<path d="M19.7443 56.5951C22.6855 53.3048 27.1978 49.739 32.2898 46.7243C37.707 43.5185 43.1254 41.2789 47.8812 40.254C42.0463 33.9159 34.4394 28.4693 29.0527 26.673C29.0149 26.6607 28.9796 26.6497 28.9479 26.6387C20.9622 24.0305 15.2661 31.0236 13.0374 37.9225C10.8087 44.8214 11.3275 53.846 19.3132 56.4542C19.4216 56.4897 19.5677 56.5375 19.7443 56.5951Z" fill="#ED79B5"/>
+<path d="M82.6341 37.8063C80.4054 30.9074 74.7093 23.9143 66.7236 26.5225C66.614 26.558 66.4679 26.6057 66.2925 26.6633C65.8358 31.0629 64.2708 36.6149 61.9129 42.0627C59.4053 47.8571 56.3301 52.8715 53.082 56.5119C61.5074 58.1919 70.8462 58.1013 76.2536 56.3723C76.2914 56.3601 76.3267 56.3478 76.3583 56.338C84.344 53.7286 84.8629 44.704 82.6341 37.8063Z" fill="#FFB400"/>
+<path d="M40.6729 63.3397C39.315 57.1694 38.8704 51.2954 39.3698 46.4316C31.5716 50.0525 24.0694 55.6436 20.7044 60.2392C20.6812 60.271 20.6593 60.3017 20.6398 60.3286C15.7049 67.1589 20.5595 74.7668 26.3968 79.0293C32.2329 83.293 40.9299 85.5853 45.866 78.755C45.9342 78.6619 46.0243 78.537 46.1327 78.3864C43.9295 74.5574 41.9493 69.1402 40.6729 63.3397Z" fill="#1E83F7"/>
+<path d="M74.9007 59.8885C70.5979 60.8118 64.8628 61.031 58.9804 60.4591C52.7241 59.8518 47.0317 58.4607 42.584 56.4795C43.5985 65.0547 46.5701 73.9569 49.8767 78.5941C49.8998 78.626 49.9218 78.6566 49.9413 78.6835C54.8761 85.5138 63.5731 83.2215 69.4104 78.9578C75.2466 74.6941 80.1023 67.0862 75.1674 60.2571C75.0992 60.164 75.0091 60.0391 74.9007 59.8885Z" fill="#18C249"/>
+</svg>
diff --git a/docs/static/img/screenshot-dark.webp b/docs/static/img/screenshot-dark.webp
new file mode 100644
index 0000000000..a6a7d0e1d6
Binary files /dev/null and b/docs/static/img/screenshot-dark.webp differ
diff --git a/docs/static/img/screenshot-light.webp b/docs/static/img/screenshot-light.webp
new file mode 100644
index 0000000000..0d88697f47
Binary files /dev/null and b/docs/static/img/screenshot-light.webp differ
diff --git a/docs/tailwind.config.js b/docs/tailwind.config.js
index 1ef26facbb..98f69bcd59 100644
--- a/docs/tailwind.config.js
+++ b/docs/tailwind.config.js
@@ -11,13 +11,13 @@ module.exports = {
       colors: {
         // Light Theme
         'immich-primary': '#4250af',
-        'immich-bg': 'white',
+        'immich-bg': '#f9f8fb',
         'immich-fg': 'black',
         'immich-gray': '#F6F6F4',
 
         // Dark Theme
         'immich-dark-primary': '#adcbfa',
-        'immich-dark-bg': 'black',
+        'immich-dark-bg': '#070a14',
         'immich-dark-fg': '#e5e7eb',
         'immich-dark-gray': '#212121',
       },
diff --git a/e2e/.nvmrc b/e2e/.nvmrc
index 3516580bbb..1d9b7831ba 100644
--- a/e2e/.nvmrc
+++ b/e2e/.nvmrc
@@ -1 +1 @@
-20.17.0
+22.12.0
diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml
index dbb95f176d..d9117b1b4a 100644
--- a/e2e/docker-compose.yml
+++ b/e2e/docker-compose.yml
@@ -19,10 +19,11 @@ services:
       - DB_PASSWORD=postgres
       - DB_DATABASE_NAME=immich
       - IMMICH_MACHINE_LEARNING_ENABLED=false
-      - IMMICH_METRICS=true
+      - IMMICH_TELEMETRY_INCLUDE=all
       - IMMICH_ENV=testing
+      - IMMICH_PORT=2285
+      - IMMICH_IGNORE_MOUNT_CHECK_ERRORS=true
     volumes:
-      - upload:/usr/src/app/upload
       - ./test-assets:/test-assets
     extra_hosts:
       - 'auth-server:host-gateway'
@@ -30,10 +31,10 @@ services:
       - redis
       - database
     ports:
-      - 2285:3001
+      - 2285:2285
 
   redis:
-    image: redis:6.2-alpine@sha256:2d1463258f2764328496376f5d965f20c6a67f66ea2b06dc42af351f75248792
+    image: redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8
 
   database:
     image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
@@ -44,7 +45,3 @@ services:
       POSTGRES_DB: immich
     ports:
       - 5435:5432
-
-volumes:
-  model-cache:
-  upload:
diff --git a/e2e/package-lock.json b/e2e/package-lock.json
index 8347bb12c6..5af61ee6f9 100644
--- a/e2e/package-lock.json
+++ b/e2e/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "immich-e2e",
-  "version": "1.115.0",
+  "version": "1.123.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "immich-e2e",
-      "version": "1.115.0",
+      "version": "1.123.0",
       "license": "GNU Affero General Public License version 3",
       "devDependencies": {
         "@eslint/eslintrc": "^3.1.0",
@@ -15,19 +15,19 @@
         "@immich/sdk": "file:../open-api/typescript-sdk",
         "@playwright/test": "^1.44.1",
         "@types/luxon": "^3.4.2",
-        "@types/node": "^20.16.5",
+        "@types/node": "^22.10.2",
         "@types/oidc-provider": "^8.5.1",
         "@types/pg": "^8.11.0",
         "@types/pngjs": "^6.0.4",
         "@types/supertest": "^6.0.2",
-        "@typescript-eslint/eslint-plugin": "^8.0.0",
-        "@typescript-eslint/parser": "^8.0.0",
+        "@typescript-eslint/eslint-plugin": "^8.15.0",
+        "@typescript-eslint/parser": "^8.15.0",
         "@vitest/coverage-v8": "^2.0.5",
-        "eslint": "^9.0.0",
+        "eslint": "^9.14.0",
         "eslint-config-prettier": "^9.1.0",
         "eslint-plugin-prettier": "^5.1.3",
-        "eslint-plugin-unicorn": "^55.0.0",
-        "exiftool-vendored": "^28.0.0",
+        "eslint-plugin-unicorn": "^56.0.1",
+        "exiftool-vendored": "^28.3.1",
         "globals": "^15.9.0",
         "jose": "^5.6.3",
         "luxon": "^3.4.4",
@@ -45,7 +45,7 @@
     },
     "../cli": {
       "name": "@immich/cli",
-      "version": "2.2.19",
+      "version": "2.2.37",
       "dev": true,
       "license": "GNU Affero General Public License version 3",
       "dependencies": {
@@ -64,17 +64,17 @@
         "@types/cli-progress": "^3.11.0",
         "@types/lodash-es": "^4.17.12",
         "@types/mock-fs": "^4.13.1",
-        "@types/node": "^20.16.5",
-        "@typescript-eslint/eslint-plugin": "^8.0.0",
-        "@typescript-eslint/parser": "^8.0.0",
+        "@types/node": "^22.10.2",
+        "@typescript-eslint/eslint-plugin": "^8.15.0",
+        "@typescript-eslint/parser": "^8.15.0",
         "@vitest/coverage-v8": "^2.0.5",
         "byte-size": "^9.0.0",
         "cli-progress": "^3.12.0",
         "commander": "^12.0.0",
-        "eslint": "^9.0.0",
+        "eslint": "^9.14.0",
         "eslint-config-prettier": "^9.1.0",
         "eslint-plugin-prettier": "^5.1.3",
-        "eslint-plugin-unicorn": "^55.0.0",
+        "eslint-plugin-unicorn": "^56.0.1",
         "globals": "^15.9.0",
         "mock-fs": "^5.2.0",
         "prettier": "^3.2.5",
@@ -83,7 +83,7 @@
         "vite": "^5.0.12",
         "vite-tsconfig-paths": "^5.0.0",
         "vitest": "^2.0.5",
-        "vitest-fetch-mock": "^0.3.0",
+        "vitest-fetch-mock": "^0.4.0",
         "yaml": "^2.3.1"
       },
       "engines": {
@@ -92,14 +92,14 @@
     },
     "../open-api/typescript-sdk": {
       "name": "@immich/sdk",
-      "version": "1.115.0",
+      "version": "1.123.0",
       "dev": true,
       "license": "GNU Affero General Public License version 3",
       "dependencies": {
         "@oazapfts/runtime": "^1.0.2"
       },
       "devDependencies": {
-        "@types/node": "^20.16.5",
+        "@types/node": "^22.10.2",
         "typescript": "^5.3.3"
       }
     },
@@ -210,19 +210,21 @@
       }
     },
     "node_modules/@babel/helper-string-parser": {
-      "version": "7.24.8",
-      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
-      "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
+      "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-validator-identifier": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
-      "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+      "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
       }
@@ -319,12 +321,13 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.25.3",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
-      "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
+      "version": "7.26.2",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz",
+      "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/types": "^7.25.2"
+        "@babel/types": "^7.26.0"
       },
       "bin": {
         "parser": "bin/babel-parser.js"
@@ -334,14 +337,14 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.25.2",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
-      "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
+      "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/helper-string-parser": "^7.24.8",
-        "@babel/helper-validator-identifier": "^7.24.7",
-        "to-fast-properties": "^2.0.0"
+        "@babel/helper-string-parser": "^7.25.9",
+        "@babel/helper-validator-identifier": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -361,6 +364,7 @@
         "ppc64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "aix"
@@ -377,6 +381,7 @@
         "arm"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "android"
@@ -393,6 +398,7 @@
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "android"
@@ -409,6 +415,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "android"
@@ -425,6 +432,7 @@
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
@@ -441,6 +449,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
@@ -457,6 +466,7 @@
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "freebsd"
@@ -473,6 +483,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "freebsd"
@@ -489,6 +500,7 @@
         "arm"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -505,6 +517,7 @@
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -521,6 +534,7 @@
         "ia32"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -537,6 +551,7 @@
         "loong64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -553,6 +568,7 @@
         "mips64el"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -569,6 +585,7 @@
         "ppc64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -585,6 +602,7 @@
         "riscv64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -601,6 +619,7 @@
         "s390x"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -617,6 +636,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -633,6 +653,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "netbsd"
@@ -649,6 +670,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "openbsd"
@@ -665,6 +687,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "sunos"
@@ -681,6 +704,7 @@
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
@@ -697,6 +721,7 @@
         "ia32"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
@@ -713,6 +738,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
@@ -737,9 +763,9 @@
       }
     },
     "node_modules/@eslint-community/regexpp": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz",
-      "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==",
+      "version": "4.12.1",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+      "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -761,10 +787,20 @@
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       }
     },
+    "node_modules/@eslint/core": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz",
+      "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
     "node_modules/@eslint/eslintrc": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
-      "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
+      "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -799,9 +835,9 @@
       }
     },
     "node_modules/@eslint/js": {
-      "version": "9.9.1",
-      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz",
-      "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==",
+      "version": "9.15.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz",
+      "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -818,6 +854,57 @@
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       }
     },
+    "node_modules/@eslint/plugin-kit": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz",
+      "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "levn": "^0.4.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@humanfs/core": {
+      "version": "0.19.1",
+      "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+      "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanfs/node": {
+      "version": "0.16.6",
+      "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
+      "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@humanfs/core": "^0.19.1",
+        "@humanwhocodes/retry": "^0.3.0"
+      },
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+      "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
     "node_modules/@humanwhocodes/module-importer": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -832,9 +919,9 @@
       }
     },
     "node_modules/@humanwhocodes/retry": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz",
-      "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==",
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz",
+      "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==",
       "dev": true,
       "license": "Apache-2.0",
       "engines": {
@@ -990,19 +1077,18 @@
       }
     },
     "node_modules/@koa/router": {
-      "version": "12.0.1",
-      "resolved": "https://registry.npmjs.org/@koa/router/-/router-12.0.1.tgz",
-      "integrity": "sha512-ribfPYfHb+Uw3b27Eiw6NPqjhIhTpVFzEWLwyc/1Xp+DCdwRRyIlAUODX+9bPARF6aQtUu1+/PHzdNvRzcs/+Q==",
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/@koa/router/-/router-13.1.0.tgz",
+      "integrity": "sha512-mNVu1nvkpSd8Q8gMebGbCkDWJ51ODetrFvLKYusej+V0ByD4btqHYnPIzTBLXnQMVUlm/oxVwqmWBY3zQfZilw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "debug": "^4.3.4",
         "http-errors": "^2.0.0",
         "koa-compose": "^4.1.0",
-        "methods": "^1.1.2",
-        "path-to-regexp": "^6.2.1"
+        "path-to-regexp": "^6.3.0"
       },
       "engines": {
-        "node": ">= 12"
+        "node": ">= 18"
       }
     },
     "node_modules/@mapbox/node-pre-gyp": {
@@ -1054,6 +1140,7 @@
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
       "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@nodelib/fs.stat": "2.0.5",
         "run-parallel": "^1.1.9"
@@ -1067,6 +1154,7 @@
       "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
       "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">= 8"
       }
@@ -1076,6 +1164,7 @@
       "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
       "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@nodelib/fs.scandir": "2.1.5",
         "fastq": "^1.6.0"
@@ -1085,10 +1174,11 @@
       }
     },
     "node_modules/@photostructure/tz-lookup": {
-      "version": "10.0.0",
-      "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-10.0.0.tgz",
-      "integrity": "sha512-8ZAjoj/irCuvUlyEinQ/HB6A8hP3bD1dgTOZvfl1b9nAwqniutFDHOQRcGM6Crea68bOwPj010f0Z4KkmuLHEA==",
-      "dev": true
+      "version": "11.0.0",
+      "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-11.0.0.tgz",
+      "integrity": "sha512-QMV5/dWtY/MdVPXZs/EApqzyhnqDq1keYEqpS+Xj2uidyaqw2Nk/fWcsszdruIXjdqp1VoWNzsgrO6bUHU1mFw==",
+      "dev": true,
+      "license": "CC0-1.0"
     },
     "node_modules/@pkgjs/parseargs": {
       "version": "0.11.0",
@@ -1113,13 +1203,13 @@
       }
     },
     "node_modules/@playwright/test": {
-      "version": "1.46.1",
-      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.1.tgz",
-      "integrity": "sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==",
+      "version": "1.48.2",
+      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz",
+      "integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==",
       "dev": true,
       "license": "Apache-2.0",
       "dependencies": {
-        "playwright": "1.46.1"
+        "playwright": "1.48.2"
       },
       "bin": {
         "playwright": "cli.js"
@@ -1129,208 +1219,252 @@
       }
     },
     "node_modules/@rollup/rollup-android-arm-eabi": {
-      "version": "4.19.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.1.tgz",
-      "integrity": "sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==",
+      "version": "4.27.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.27.3.tgz",
+      "integrity": "sha512-EzxVSkIvCFxUd4Mgm4xR9YXrcp976qVaHnqom/Tgm+vU79k4vV4eYTjmRvGfeoW8m9LVcsAy/lGjcgVegKEhLQ==",
       "cpu": [
         "arm"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "android"
       ]
     },
     "node_modules/@rollup/rollup-android-arm64": {
-      "version": "4.19.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.1.tgz",
-      "integrity": "sha512-thFUbkHteM20BGShD6P08aungq4irbIZKUNbG70LN8RkO7YztcGPiKTTGZS7Kw+x5h8hOXs0i4OaHwFxlpQN6A==",
+      "version": "4.27.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.27.3.tgz",
+      "integrity": "sha512-LJc5pDf1wjlt9o/Giaw9Ofl+k/vLUaYsE2zeQGH85giX2F+wn/Cg8b3c5CDP3qmVmeO5NzwVUzQQxwZvC2eQKw==",
       "cpu": [
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "android"
       ]
     },
     "node_modules/@rollup/rollup-darwin-arm64": {
-      "version": "4.19.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.1.tgz",
-      "integrity": "sha512-8o6eqeFZzVLia2hKPUZk4jdE3zW7LCcZr+MD18tXkgBBid3lssGVAYuox8x6YHoEPDdDa9ixTaStcmx88lio5Q==",
+      "version": "4.27.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.27.3.tgz",
+      "integrity": "sha512-OuRysZ1Mt7wpWJ+aYKblVbJWtVn3Cy52h8nLuNSzTqSesYw1EuN6wKp5NW/4eSre3mp12gqFRXOKTcN3AI3LqA==",
       "cpu": [
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
       ]
     },
     "node_modules/@rollup/rollup-darwin-x64": {
-      "version": "4.19.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.1.tgz",
-      "integrity": "sha512-4T42heKsnbjkn7ovYiAdDVRRWZLU9Kmhdt6HafZxFcUdpjlBlxj4wDrt1yFWLk7G4+E+8p2C9tcmSu0KA6auGA==",
+      "version": "4.27.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.27.3.tgz",
+      "integrity": "sha512-xW//zjJMlJs2sOrCmXdB4d0uiilZsOdlGQIC/jjmMWT47lkLLoB1nsNhPUcnoqyi5YR6I4h+FjBpILxbEy8JRg==",
       "cpu": [
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
       ]
     },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.27.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.27.3.tgz",
+      "integrity": "sha512-58E0tIcwZ+12nK1WiLzHOD8I0d0kdrY/+o7yFVPRHuVGY3twBwzwDdTIBGRxLmyjciMYl1B/U515GJy+yn46qw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.27.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.27.3.tgz",
+      "integrity": "sha512-78fohrpcVwTLxg1ZzBMlwEimoAJmY6B+5TsyAZ3Vok7YabRBUvjYTsRXPTjGEvv/mfgVBepbW28OlMEz4w8wGA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
     "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-      "version": "4.19.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.1.tgz",
-      "integrity": "sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==",
+      "version": "4.27.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.27.3.tgz",
+      "integrity": "sha512-h2Ay79YFXyQi+QZKo3ISZDyKaVD7uUvukEHTOft7kh00WF9mxAaxZsNs3o/eukbeKuH35jBvQqrT61fzKfAB/Q==",
       "cpu": [
         "arm"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-musleabihf": {
-      "version": "4.19.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.1.tgz",
-      "integrity": "sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==",
+      "version": "4.27.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.27.3.tgz",
+      "integrity": "sha512-Sv2GWmrJfRY57urktVLQ0VKZjNZGogVtASAgosDZ1aUB+ykPxSi3X1nWORL5Jk0sTIIwQiPH7iE3BMi9zGWfkg==",
       "cpu": [
         "arm"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-gnu": {
-      "version": "4.19.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.1.tgz",
-      "integrity": "sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==",
+      "version": "4.27.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.27.3.tgz",
+      "integrity": "sha512-FPoJBLsPW2bDNWjSrwNuTPUt30VnfM8GPGRoLCYKZpPx0xiIEdFip3dH6CqgoT0RnoGXptaNziM0WlKgBc+OWQ==",
       "cpu": [
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-musl": {
-      "version": "4.19.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.1.tgz",
-      "integrity": "sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==",
+      "version": "4.27.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.27.3.tgz",
+      "integrity": "sha512-TKxiOvBorYq4sUpA0JT+Fkh+l+G9DScnG5Dqx7wiiqVMiRSkzTclP35pE6eQQYjP4Gc8yEkJGea6rz4qyWhp3g==",
       "cpu": [
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
-      "version": "4.19.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.1.tgz",
-      "integrity": "sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==",
+      "version": "4.27.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.27.3.tgz",
+      "integrity": "sha512-v2M/mPvVUKVOKITa0oCFksnQQ/TqGrT+yD0184/cWHIu0LoIuYHwox0Pm3ccXEz8cEQDLk6FPKd1CCm+PlsISw==",
       "cpu": [
         "ppc64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@rollup/rollup-linux-riscv64-gnu": {
-      "version": "4.19.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.1.tgz",
-      "integrity": "sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==",
+      "version": "4.27.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.27.3.tgz",
+      "integrity": "sha512-LdrI4Yocb1a/tFVkzmOE5WyYRgEBOyEhWYJe4gsDWDiwnjYKjNs7PS6SGlTDB7maOHF4kxevsuNBl2iOcj3b4A==",
       "cpu": [
         "riscv64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@rollup/rollup-linux-s390x-gnu": {
-      "version": "4.19.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.1.tgz",
-      "integrity": "sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==",
+      "version": "4.27.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.27.3.tgz",
+      "integrity": "sha512-d4wVu6SXij/jyiwPvI6C4KxdGzuZOvJ6y9VfrcleHTwo68fl8vZC5ZYHsCVPUi4tndCfMlFniWgwonQ5CUpQcA==",
       "cpu": [
         "s390x"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-gnu": {
-      "version": "4.19.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.1.tgz",
-      "integrity": "sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==",
+      "version": "4.27.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.3.tgz",
+      "integrity": "sha512-/6bn6pp1fsCGEY5n3yajmzZQAh+mW4QPItbiWxs69zskBzJuheb3tNynEjL+mKOsUSFK11X4LYF2BwwXnzWleA==",
       "cpu": [
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-musl": {
-      "version": "4.19.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.1.tgz",
-      "integrity": "sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==",
+      "version": "4.27.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.27.3.tgz",
+      "integrity": "sha512-nBXOfJds8OzUT1qUreT/en3eyOXd2EH5b0wr2bVB5999qHdGKkzGzIyKYaKj02lXk6wpN71ltLIaQpu58YFBoQ==",
       "cpu": [
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@rollup/rollup-win32-arm64-msvc": {
-      "version": "4.19.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.1.tgz",
-      "integrity": "sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==",
+      "version": "4.27.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.27.3.tgz",
+      "integrity": "sha512-ogfbEVQgIZOz5WPWXF2HVb6En+kWzScuxJo/WdQTqEgeyGkaa2ui5sQav9Zkr7bnNCLK48uxmmK0TySm22eiuw==",
       "cpu": [
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
       ]
     },
     "node_modules/@rollup/rollup-win32-ia32-msvc": {
-      "version": "4.19.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.1.tgz",
-      "integrity": "sha512-LdxxcqRVSXi6k6JUrTah1rHuaupoeuiv38du8Mt4r4IPer3kwlTo+RuvfE8KzZ/tL6BhaPlzJ3835i6CxrFIRQ==",
+      "version": "4.27.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.27.3.tgz",
+      "integrity": "sha512-ecE36ZBMLINqiTtSNQ1vzWc5pXLQHlf/oqGp/bSbi7iedcjcNb6QbCBNG73Euyy2C+l/fn8qKWEwxr+0SSfs3w==",
       "cpu": [
         "ia32"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
       ]
     },
     "node_modules/@rollup/rollup-win32-x64-msvc": {
-      "version": "4.19.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.1.tgz",
-      "integrity": "sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==",
+      "version": "4.27.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.27.3.tgz",
+      "integrity": "sha512-vliZLrDmYKyaUoMzEbMTg2JkerfBjn03KmAw9CykO0Zzkzoyd7o3iZNam/TpyWNjNT+Cz2iO3P9Smv2wgrR+Eg==",
       "cpu": [
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
@@ -1419,10 +1553,11 @@
       }
     },
     "node_modules/@types/estree": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
-      "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
-      "dev": true
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+      "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@types/express": {
       "version": "4.17.21",
@@ -1466,6 +1601,13 @@
       "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
       "dev": true
     },
+    "node_modules/@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/keygrip": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz",
@@ -1516,13 +1658,13 @@
       "dev": true
     },
     "node_modules/@types/node": {
-      "version": "20.16.5",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz",
-      "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==",
+      "version": "22.10.2",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
+      "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "undici-types": "~6.19.2"
+        "undici-types": "~6.20.0"
       }
     },
     "node_modules/@types/normalize-package-data": {
@@ -1543,9 +1685,9 @@
       }
     },
     "node_modules/@types/pg": {
-      "version": "8.11.8",
-      "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.8.tgz",
-      "integrity": "sha512-IqpCf8/569txXN/HoP5i1LjXfKZWL76Yr2R77xgeIICUbAYHeoaEZFhYHo2uDftecLWrTJUq63JvQu8q3lnDyA==",
+      "version": "8.11.10",
+      "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz",
+      "integrity": "sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -1680,17 +1822,17 @@
       }
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz",
-      "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz",
+      "integrity": "sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/regexpp": "^4.10.0",
-        "@typescript-eslint/scope-manager": "8.3.0",
-        "@typescript-eslint/type-utils": "8.3.0",
-        "@typescript-eslint/utils": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0",
+        "@typescript-eslint/scope-manager": "8.15.0",
+        "@typescript-eslint/type-utils": "8.15.0",
+        "@typescript-eslint/utils": "8.15.0",
+        "@typescript-eslint/visitor-keys": "8.15.0",
         "graphemer": "^1.4.0",
         "ignore": "^5.3.1",
         "natural-compare": "^1.4.0",
@@ -1714,16 +1856,16 @@
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz",
-      "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz",
+      "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
-        "@typescript-eslint/scope-manager": "8.3.0",
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/typescript-estree": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0",
+        "@typescript-eslint/scope-manager": "8.15.0",
+        "@typescript-eslint/types": "8.15.0",
+        "@typescript-eslint/typescript-estree": "8.15.0",
+        "@typescript-eslint/visitor-keys": "8.15.0",
         "debug": "^4.3.4"
       },
       "engines": {
@@ -1743,14 +1885,14 @@
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz",
-      "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz",
+      "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0"
+        "@typescript-eslint/types": "8.15.0",
+        "@typescript-eslint/visitor-keys": "8.15.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1761,14 +1903,14 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz",
-      "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz",
+      "integrity": "sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "8.3.0",
-        "@typescript-eslint/utils": "8.3.0",
+        "@typescript-eslint/typescript-estree": "8.15.0",
+        "@typescript-eslint/utils": "8.15.0",
         "debug": "^4.3.4",
         "ts-api-utils": "^1.3.0"
       },
@@ -1779,6 +1921,9 @@
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
       },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0"
+      },
       "peerDependenciesMeta": {
         "typescript": {
           "optional": true
@@ -1786,9 +1931,9 @@
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz",
-      "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz",
+      "integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -1800,14 +1945,14 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz",
-      "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz",
+      "integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0",
+        "@typescript-eslint/types": "8.15.0",
+        "@typescript-eslint/visitor-keys": "8.15.0",
         "debug": "^4.3.4",
         "fast-glob": "^3.3.2",
         "is-glob": "^4.0.3",
@@ -1855,16 +2000,16 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz",
-      "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.15.0.tgz",
+      "integrity": "sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
-        "@typescript-eslint/scope-manager": "8.3.0",
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/typescript-estree": "8.3.0"
+        "@typescript-eslint/scope-manager": "8.15.0",
+        "@typescript-eslint/types": "8.15.0",
+        "@typescript-eslint/typescript-estree": "8.15.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1875,17 +2020,22 @@
       },
       "peerDependencies": {
         "eslint": "^8.57.0 || ^9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz",
-      "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz",
+      "integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.3.0",
-        "eslint-visitor-keys": "^3.4.3"
+        "@typescript-eslint/types": "8.15.0",
+        "eslint-visitor-keys": "^4.2.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1895,22 +2045,36 @@
         "url": "https://opencollective.com/typescript-eslint"
       }
     },
-    "node_modules/@vitest/coverage-v8": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz",
-      "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==",
+    "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
       "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@vitest/coverage-v8": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.5.tgz",
+      "integrity": "sha512-/RoopB7XGW7UEkUndRXF87A9CwkoZAJW01pj8/3pgmDVsjMH2IKy6H1A38po9tmUlwhSyYs0az82rbKd9Yaynw==",
+      "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@ampproject/remapping": "^2.3.0",
         "@bcoe/v8-coverage": "^0.2.3",
-        "debug": "^4.3.5",
+        "debug": "^4.3.7",
         "istanbul-lib-coverage": "^3.2.2",
         "istanbul-lib-report": "^3.0.1",
         "istanbul-lib-source-maps": "^5.0.6",
         "istanbul-reports": "^3.1.7",
-        "magic-string": "^0.30.10",
-        "magicast": "^0.3.4",
-        "std-env": "^3.7.0",
+        "magic-string": "^0.30.12",
+        "magicast": "^0.3.5",
+        "std-env": "^3.8.0",
         "test-exclude": "^7.0.1",
         "tinyrainbow": "^1.2.0"
       },
@@ -1918,29 +2082,64 @@
         "url": "https://opencollective.com/vitest"
       },
       "peerDependencies": {
-        "vitest": "2.0.5"
+        "@vitest/browser": "2.1.5",
+        "vitest": "2.1.5"
+      },
+      "peerDependenciesMeta": {
+        "@vitest/browser": {
+          "optional": true
+        }
       }
     },
     "node_modules/@vitest/expect": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz",
-      "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.5.tgz",
+      "integrity": "sha512-nZSBTW1XIdpZvEJyoP/Sy8fUg0b8od7ZpGDkTUcfJ7wz/VoZAFzFfLyxVxGFhUjJzhYqSbIpfMtl/+k/dpWa3Q==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@vitest/spy": "2.0.5",
-        "@vitest/utils": "2.0.5",
-        "chai": "^5.1.1",
+        "@vitest/spy": "2.1.5",
+        "@vitest/utils": "2.1.5",
+        "chai": "^5.1.2",
         "tinyrainbow": "^1.2.0"
       },
       "funding": {
         "url": "https://opencollective.com/vitest"
       }
     },
-    "node_modules/@vitest/pretty-format": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz",
-      "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==",
+    "node_modules/@vitest/mocker": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.5.tgz",
+      "integrity": "sha512-XYW6l3UuBmitWqSUXTNXcVBUCRytDogBsWuNXQijc00dtnU/9OqpXWp4OJroVrad/gLIomAq9aW8yWDBtMthhQ==",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/spy": "2.1.5",
+        "estree-walker": "^3.0.3",
+        "magic-string": "^0.30.12"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "msw": "^2.4.9",
+        "vite": "^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "msw": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vitest/pretty-format": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.5.tgz",
+      "integrity": "sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw==",
+      "dev": true,
+      "license": "MIT",
       "dependencies": {
         "tinyrainbow": "^1.2.0"
       },
@@ -1949,12 +2148,13 @@
       }
     },
     "node_modules/@vitest/runner": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz",
-      "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.5.tgz",
+      "integrity": "sha512-pKHKy3uaUdh7X6p1pxOkgkVAFW7r2I818vHDthYLvUyjRfkKOU6P45PztOch4DZarWQne+VOaIMwA/erSSpB9g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@vitest/utils": "2.0.5",
+        "@vitest/utils": "2.1.5",
         "pathe": "^1.1.2"
       },
       "funding": {
@@ -1962,13 +2162,14 @@
       }
     },
     "node_modules/@vitest/snapshot": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz",
-      "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.5.tgz",
+      "integrity": "sha512-zmYw47mhfdfnYbuhkQvkkzYroXUumrwWDGlMjpdUr4jBd3HZiV2w7CQHj+z7AAS4VOtWxI4Zt4bWt4/sKcoIjg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@vitest/pretty-format": "2.0.5",
-        "magic-string": "^0.30.10",
+        "@vitest/pretty-format": "2.1.5",
+        "magic-string": "^0.30.12",
         "pathe": "^1.1.2"
       },
       "funding": {
@@ -1976,26 +2177,27 @@
       }
     },
     "node_modules/@vitest/spy": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz",
-      "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.5.tgz",
+      "integrity": "sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "tinyspy": "^3.0.0"
+        "tinyspy": "^3.0.2"
       },
       "funding": {
         "url": "https://opencollective.com/vitest"
       }
     },
     "node_modules/@vitest/utils": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz",
-      "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.5.tgz",
+      "integrity": "sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@vitest/pretty-format": "2.0.5",
-        "estree-walker": "^3.0.3",
-        "loupe": "^3.1.1",
+        "@vitest/pretty-format": "2.1.5",
+        "loupe": "^3.1.2",
         "tinyrainbow": "^1.2.0"
       },
       "funding": {
@@ -2022,9 +2224,9 @@
       }
     },
     "node_modules/acorn": {
-      "version": "8.12.1",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
-      "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
+      "version": "8.14.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
+      "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
       "dev": true,
       "license": "MIT",
       "bin": {
@@ -2129,6 +2331,7 @@
       "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
       "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=12"
       }
@@ -2178,9 +2381,9 @@
       }
     },
     "node_modules/browserslist": {
-      "version": "4.23.0",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
-      "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
+      "version": "4.24.3",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
+      "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
       "dev": true,
       "funding": [
         {
@@ -2196,11 +2399,12 @@
           "url": "https://github.com/sponsors/ai"
         }
       ],
+      "license": "MIT",
       "dependencies": {
-        "caniuse-lite": "^1.0.30001587",
-        "electron-to-chromium": "^1.4.668",
-        "node-releases": "^2.0.14",
-        "update-browserslist-db": "^1.0.13"
+        "caniuse-lite": "^1.0.30001688",
+        "electron-to-chromium": "^1.5.73",
+        "node-releases": "^2.0.19",
+        "update-browserslist-db": "^1.1.1"
       },
       "bin": {
         "browserslist": "cli.js"
@@ -2235,6 +2439,7 @@
       "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
       "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
@@ -2320,9 +2525,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001591",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz",
-      "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==",
+      "version": "1.0.30001689",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz",
+      "integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==",
       "dev": true,
       "funding": [
         {
@@ -2337,13 +2542,15 @@
           "type": "github",
           "url": "https://github.com/sponsors/ai"
         }
-      ]
+      ],
+      "license": "CC-BY-4.0"
     },
     "node_modules/chai": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
-      "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz",
+      "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "assertion-error": "^2.0.1",
         "check-error": "^2.1.1",
@@ -2391,6 +2598,7 @@
       "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
       "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">= 16"
       }
@@ -2551,12 +2759,13 @@
       }
     },
     "node_modules/core-js-compat": {
-      "version": "3.37.1",
-      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
-      "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
+      "version": "3.39.0",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz",
+      "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "browserslist": "^4.23.0"
+        "browserslist": "^4.24.2"
       },
       "funding": {
         "type": "opencollective",
@@ -2564,10 +2773,11 @@
       }
     },
     "node_modules/cross-spawn": {
-      "version": "7.0.3",
-      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
-      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "path-key": "^3.1.0",
         "shebang-command": "^2.0.0",
@@ -2578,12 +2788,13 @@
       }
     },
     "node_modules/debug": {
-      "version": "4.3.5",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
-      "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "ms": "2.1.2"
+        "ms": "^2.1.3"
       },
       "engines": {
         "node": ">=6.0"
@@ -2626,6 +2837,7 @@
       "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
       "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
@@ -2734,10 +2946,11 @@
       "dev": true
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.687",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.687.tgz",
-      "integrity": "sha512-Ic85cOuXSP6h7KM0AIJ2hpJ98Bo4hyTUjc4yjMbkvD+8yTxEhfK9+8exT2KKYsSjnCn2tGsKVSZwE7ZgTORQCw==",
-      "dev": true
+      "version": "1.5.74",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz",
+      "integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==",
+      "dev": true,
+      "license": "ISC"
     },
     "node_modules/emoji-regex": {
       "version": "8.0.0",
@@ -2755,16 +2968,17 @@
       }
     },
     "node_modules/engine.io-client": {
-      "version": "6.5.4",
-      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz",
-      "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==",
+      "version": "6.6.1",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz",
+      "integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@socket.io/component-emitter": "~3.1.0",
         "debug": "~4.3.1",
         "engine.io-parser": "~5.2.1",
         "ws": "~8.17.1",
-        "xmlhttprequest-ssl": "~2.0.0"
+        "xmlhttprequest-ssl": "~2.1.1"
       }
     },
     "node_modules/engine.io-parser": {
@@ -2806,12 +3020,20 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/es-module-lexer": {
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
+      "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/esbuild": {
       "version": "0.21.5",
       "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
       "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
       "dev": true,
       "hasInstallScript": true,
+      "license": "MIT",
       "bin": {
         "esbuild": "bin/esbuild"
       },
@@ -2845,10 +3067,11 @@
       }
     },
     "node_modules/escalade": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
-      "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
@@ -2872,28 +3095,32 @@
       }
     },
     "node_modules/eslint": {
-      "version": "9.9.1",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz",
-      "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==",
+      "version": "9.14.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz",
+      "integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
-        "@eslint-community/regexpp": "^4.11.0",
+        "@eslint-community/regexpp": "^4.12.1",
         "@eslint/config-array": "^0.18.0",
+        "@eslint/core": "^0.7.0",
         "@eslint/eslintrc": "^3.1.0",
-        "@eslint/js": "9.9.1",
+        "@eslint/js": "9.14.0",
+        "@eslint/plugin-kit": "^0.2.0",
+        "@humanfs/node": "^0.16.6",
         "@humanwhocodes/module-importer": "^1.0.1",
-        "@humanwhocodes/retry": "^0.3.0",
-        "@nodelib/fs.walk": "^1.2.8",
+        "@humanwhocodes/retry": "^0.4.0",
+        "@types/estree": "^1.0.6",
+        "@types/json-schema": "^7.0.15",
         "ajv": "^6.12.4",
         "chalk": "^4.0.0",
         "cross-spawn": "^7.0.2",
         "debug": "^4.3.2",
         "escape-string-regexp": "^4.0.0",
-        "eslint-scope": "^8.0.2",
-        "eslint-visitor-keys": "^4.0.0",
-        "espree": "^10.1.0",
+        "eslint-scope": "^8.2.0",
+        "eslint-visitor-keys": "^4.2.0",
+        "espree": "^10.3.0",
         "esquery": "^1.5.0",
         "esutils": "^2.0.2",
         "fast-deep-equal": "^3.1.3",
@@ -2903,14 +3130,11 @@
         "ignore": "^5.2.0",
         "imurmurhash": "^0.1.4",
         "is-glob": "^4.0.0",
-        "is-path-inside": "^3.0.3",
         "json-stable-stringify-without-jsonify": "^1.0.1",
-        "levn": "^0.4.1",
         "lodash.merge": "^4.6.2",
         "minimatch": "^3.1.2",
         "natural-compare": "^1.4.0",
         "optionator": "^0.9.3",
-        "strip-ansi": "^6.0.1",
         "text-table": "^0.2.0"
       },
       "bin": {
@@ -2975,19 +3199,19 @@
       }
     },
     "node_modules/eslint-plugin-unicorn": {
-      "version": "55.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz",
-      "integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==",
+      "version": "56.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.1.tgz",
+      "integrity": "sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@babel/helper-validator-identifier": "^7.24.5",
+        "@babel/helper-validator-identifier": "^7.24.7",
         "@eslint-community/eslint-utils": "^4.4.0",
         "ci-info": "^4.0.0",
         "clean-regexp": "^1.0.0",
-        "core-js-compat": "^3.37.0",
-        "esquery": "^1.5.0",
-        "globals": "^15.7.0",
+        "core-js-compat": "^3.38.1",
+        "esquery": "^1.6.0",
+        "globals": "^15.9.0",
         "indent-string": "^4.0.0",
         "is-builtin-module": "^3.2.1",
         "jsesc": "^3.0.2",
@@ -2995,7 +3219,7 @@
         "read-pkg-up": "^7.0.1",
         "regexp-tree": "^0.1.27",
         "regjsparser": "^0.10.0",
-        "semver": "^7.6.1",
+        "semver": "^7.6.3",
         "strip-indent": "^3.0.0"
       },
       "engines": {
@@ -3009,9 +3233,9 @@
       }
     },
     "node_modules/eslint-scope": {
-      "version": "8.0.2",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz",
-      "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==",
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
+      "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
@@ -3037,10 +3261,20 @@
         "url": "https://opencollective.com/eslint"
       }
     },
+    "node_modules/eslint/node_modules/@eslint/js": {
+      "version": "9.14.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz",
+      "integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
     "node_modules/eslint/node_modules/eslint-visitor-keys": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
-      "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
       "dev": true,
       "license": "Apache-2.0",
       "engines": {
@@ -3051,15 +3285,15 @@
       }
     },
     "node_modules/espree": {
-      "version": "10.1.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz",
-      "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==",
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
+      "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
-        "acorn": "^8.12.0",
+        "acorn": "^8.14.0",
         "acorn-jsx": "^5.3.2",
-        "eslint-visitor-keys": "^4.0.0"
+        "eslint-visitor-keys": "^4.2.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3069,9 +3303,9 @@
       }
     },
     "node_modules/espree/node_modules/eslint-visitor-keys": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
-      "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
       "dev": true,
       "license": "Apache-2.0",
       "engines": {
@@ -3082,10 +3316,11 @@
       }
     },
     "node_modules/esquery": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
-      "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+      "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "estraverse": "^5.1.0"
       },
@@ -3098,6 +3333,7 @@
       "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
       "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
       "dev": true,
+      "license": "BSD-2-Clause",
       "dependencies": {
         "estraverse": "^5.2.0"
       },
@@ -3119,6 +3355,7 @@
       "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
       "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/estree": "^1.0.0"
       }
@@ -3133,10 +3370,11 @@
       }
     },
     "node_modules/eta": {
-      "version": "3.4.0",
-      "resolved": "https://registry.npmjs.org/eta/-/eta-3.4.0.tgz",
-      "integrity": "sha512-tCsc7WXTjrTx4ZjYLplcqrI3o4mYJ+Z6YspeuGL8tbt/hHoMchwBwtKfwM09svEY86iRapY93vUqQttcNuIO5Q==",
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/eta/-/eta-3.5.0.tgz",
+      "integrity": "sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6.0.0"
       },
@@ -3144,51 +3382,28 @@
         "url": "https://github.com/eta-dev/eta?sponsor=1"
       }
     },
-    "node_modules/execa": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
-      "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
-      "dev": true,
-      "dependencies": {
-        "cross-spawn": "^7.0.3",
-        "get-stream": "^8.0.1",
-        "human-signals": "^5.0.0",
-        "is-stream": "^3.0.0",
-        "merge-stream": "^2.0.0",
-        "npm-run-path": "^5.1.0",
-        "onetime": "^6.0.0",
-        "signal-exit": "^4.1.0",
-        "strip-final-newline": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=16.17"
-      },
-      "funding": {
-        "url": "https://github.com/sindresorhus/execa?sponsor=1"
-      }
-    },
     "node_modules/exiftool-vendored": {
-      "version": "28.2.1",
-      "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.1.tgz",
-      "integrity": "sha512-D3YsKErr3BbjKeJzUVsv6CVZ+SQNgAJKPFWVLXu0CBtr24FNuE3CZBXWKWysGu0MjzeDCNwQrQI5+bXUFeiYVA==",
+      "version": "28.8.0",
+      "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.8.0.tgz",
+      "integrity": "sha512-R7tirJLr9fWuH9JS/KFFLB+O7jNGKuPXGxREc6YybYangEudGb+X8ERsYXk9AifMiAWh/2agNfbgkbcQcF+MxA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@photostructure/tz-lookup": "^10.0.0",
+        "@photostructure/tz-lookup": "^11.0.0",
         "@types/luxon": "^3.4.2",
         "batch-cluster": "^13.0.0",
         "he": "^1.2.0",
         "luxon": "^3.5.0"
       },
       "optionalDependencies": {
-        "exiftool-vendored.exe": "12.91.0",
-        "exiftool-vendored.pl": "12.91.0"
+        "exiftool-vendored.exe": "13.0.0",
+        "exiftool-vendored.pl": "13.0.1"
       }
     },
     "node_modules/exiftool-vendored.exe": {
-      "version": "12.91.0",
-      "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.91.0.tgz",
-      "integrity": "sha512-nxcoGBaJL/D+Wb0jVe8qwyV8QZpRcCzU0aCKhG0S1XNGWGjJJJ4QV851aobcfDwI4NluFOdqkjTSf32pVijvHg==",
+      "version": "13.0.0",
+      "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-13.0.0.tgz",
+      "integrity": "sha512-4zAMuFGgxZkOoyQIzZMHv1HlvgyJK3AkNqjAgm8A8V0UmOZO7yv3pH49cDV1OduzFJqgs6yQ6eG4OGydhKtxlg==",
       "dev": true,
       "license": "MIT",
       "optional": true,
@@ -3197,9 +3412,9 @@
       ]
     },
     "node_modules/exiftool-vendored.pl": {
-      "version": "12.91.0",
-      "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.91.0.tgz",
-      "integrity": "sha512-GZMy9+Jiv8/C7R4uYe1kWtXsAaJdgVezTwYa+wDeoqvReHiX2t5uzkCrzWdjo4LGl5mPQkyKhN7/uPLYk5Ak6w==",
+      "version": "13.0.1",
+      "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-13.0.1.tgz",
+      "integrity": "sha512-+BRRzjselpWudKR0ltAW5SUt9T82D+gzQN8DdOQUgnSVWWp7oLCeTGBRptbQz+436Ihn/mPzmo/xnf0cv/Qw1A==",
       "dev": true,
       "license": "MIT",
       "optional": true,
@@ -3207,6 +3422,16 @@
         "!win32"
       ]
     },
+    "node_modules/expect-type": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz",
+      "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "node_modules/fast-deep-equal": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -3272,6 +3497,7 @@
       "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
       "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "reusify": "^1.0.4"
       }
@@ -3480,15 +3706,6 @@
       "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
       "dev": true
     },
-    "node_modules/get-func-name": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
-      "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
-      "dev": true,
-      "engines": {
-        "node": "*"
-      }
-    },
     "node_modules/get-intrinsic": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
@@ -3508,18 +3725,6 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/get-stream": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
-      "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
-      "dev": true,
-      "engines": {
-        "node": ">=16"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/glob": {
       "version": "7.2.3",
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -3553,9 +3758,9 @@
       }
     },
     "node_modules/globals": {
-      "version": "15.9.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz",
-      "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==",
+      "version": "15.12.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz",
+      "integrity": "sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -3835,22 +4040,14 @@
         "node": ">= 6"
       }
     },
-    "node_modules/human-signals": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
-      "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=16.17.0"
-      }
-    },
     "node_modules/iconv-lite": {
-      "version": "0.4.24",
-      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
-      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "safer-buffer": ">= 2.1.2 < 3"
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
       },
       "engines": {
         "node": ">=0.10.0"
@@ -4003,27 +4200,6 @@
         "node": ">=0.12.0"
       }
     },
-    "node_modules/is-path-inside": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
-      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/is-stream": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
-      "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
-      "dev": true,
-      "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/isexe": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -4096,9 +4272,9 @@
       }
     },
     "node_modules/jose": {
-      "version": "5.8.0",
-      "resolved": "https://registry.npmjs.org/jose/-/jose-5.8.0.tgz",
-      "integrity": "sha512-E7CqYpL/t7MMnfGnK/eg416OsFCVUrU/Y3Vwe7QjKhu/BkS1Ms455+2xsqZQVN57/U2MHMBvEb5SrmAZWAIntA==",
+      "version": "5.9.6",
+      "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz",
+      "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==",
       "dev": true,
       "license": "MIT",
       "funding": {
@@ -4302,13 +4478,11 @@
       "dev": true
     },
     "node_modules/loupe": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz",
-      "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz",
+      "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==",
       "dev": true,
-      "dependencies": {
-        "get-func-name": "^2.0.1"
-      }
+      "license": "MIT"
     },
     "node_modules/lowercase-keys": {
       "version": "3.0.0",
@@ -4339,22 +4513,24 @@
       }
     },
     "node_modules/magic-string": {
-      "version": "0.30.11",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
-      "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
+      "version": "0.30.12",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz",
+      "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.5.0"
       }
     },
     "node_modules/magicast": {
-      "version": "0.3.4",
-      "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz",
-      "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==",
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
+      "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/parser": "^7.24.4",
-        "@babel/types": "^7.24.0",
+        "@babel/parser": "^7.25.4",
+        "@babel/types": "^7.25.4",
         "source-map-js": "^1.2.0"
       }
     },
@@ -4382,12 +4558,6 @@
         "node": ">= 0.6"
       }
     },
-    "node_modules/merge-stream": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
-      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
-      "dev": true
-    },
     "node_modules/merge2": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -4454,18 +4624,6 @@
         "node": ">= 0.6"
       }
     },
-    "node_modules/mimic-fn": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
-      "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/mimic-response": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
@@ -4546,15 +4704,16 @@
       }
     },
     "node_modules/ms": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-      "dev": true
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/nanoid": {
-      "version": "3.3.7",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
-      "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+      "version": "5.0.8",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.8.tgz",
+      "integrity": "sha512-TcJPw+9RV9dibz1hHUzlLVy8N4X9TnwirAjrU08Juo6BNKggzVfP2ZJ/3ZUSq15Xl5i85i+Z89XBO90pB2PghQ==",
       "dev": true,
       "funding": [
         {
@@ -4562,11 +4721,12 @@
           "url": "https://github.com/sponsors/ai"
         }
       ],
+      "license": "MIT",
       "bin": {
-        "nanoid": "bin/nanoid.cjs"
+        "nanoid": "bin/nanoid.js"
       },
       "engines": {
-        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+        "node": "^18 || >=20"
       }
     },
     "node_modules/natural-compare": {
@@ -4611,10 +4771,11 @@
       }
     },
     "node_modules/node-releases": {
-      "version": "2.0.14",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
-      "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
-      "dev": true
+      "version": "2.0.19",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+      "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/nopt": {
       "version": "5.0.0",
@@ -4664,33 +4825,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/npm-run-path": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
-      "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
-      "dev": true,
-      "dependencies": {
-        "path-key": "^4.0.0"
-      },
-      "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/npm-run-path/node_modules/path-key": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
-      "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/npmlog": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
@@ -4737,47 +4871,30 @@
       "dev": true
     },
     "node_modules/oidc-provider": {
-      "version": "8.5.1",
-      "resolved": "https://registry.npmjs.org/oidc-provider/-/oidc-provider-8.5.1.tgz",
-      "integrity": "sha512-Bm3EyxN68/KS76IlciJ3+4pnVtfdRWL+NghWpIF0XQbiRT1gzc6Qf/cyFmpL9yieko/jXYZ/uLHUv77jD00qww==",
+      "version": "8.5.3",
+      "resolved": "https://registry.npmjs.org/oidc-provider/-/oidc-provider-8.5.3.tgz",
+      "integrity": "sha512-eSHoHiPxdJ7Ar+hgaogeBVdGgjtIPp6GXFuzuTYdQ4rhhWC6jFapa0GQ49zk0M6I6OnqTYU3dU8j/QDaLu5BaA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@koa/cors": "^5.0.0",
-        "@koa/router": "^12.0.1",
-        "debug": "^4.3.5",
-        "eta": "^3.4.0",
+        "@koa/router": "^13.1.0",
+        "debug": "^4.3.7",
+        "eta": "^3.5.0",
         "got": "^13.0.0",
-        "jose": "^5.6.2",
+        "jose": "^5.9.6",
         "jsesc": "^3.0.2",
         "koa": "^2.15.3",
-        "nanoid": "^5.0.7",
+        "nanoid": "^5.0.8",
         "object-hash": "^3.0.0",
         "oidc-token-hash": "^5.0.3",
         "quick-lru": "^7.0.0",
-        "raw-body": "^2.5.2"
+        "raw-body": "^3.0.0"
       },
       "funding": {
         "url": "https://github.com/sponsors/panva"
       }
     },
-    "node_modules/oidc-provider/node_modules/nanoid": {
-      "version": "5.0.7",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz",
-      "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/ai"
-        }
-      ],
-      "bin": {
-        "nanoid": "bin/nanoid.js"
-      },
-      "engines": {
-        "node": "^18 || >=20"
-      }
-    },
     "node_modules/oidc-token-hash": {
       "version": "5.0.3",
       "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",
@@ -4808,21 +4925,6 @@
         "wrappy": "1"
       }
     },
-    "node_modules/onetime": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
-      "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
-      "dev": true,
-      "dependencies": {
-        "mimic-fn": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/only": {
       "version": "0.0.2",
       "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz",
@@ -5001,36 +5103,38 @@
       }
     },
     "node_modules/path-to-regexp": {
-      "version": "6.2.2",
-      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz",
-      "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==",
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
+      "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
       "dev": true
     },
     "node_modules/pathe": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
       "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/pathval": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
       "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">= 14.16"
       }
     },
     "node_modules/pg": {
-      "version": "8.12.0",
-      "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz",
-      "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==",
+      "version": "8.13.1",
+      "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz",
+      "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "pg-connection-string": "^2.6.4",
-        "pg-pool": "^3.6.2",
-        "pg-protocol": "^1.6.1",
+        "pg-connection-string": "^2.7.0",
+        "pg-pool": "^3.7.0",
+        "pg-protocol": "^1.7.0",
         "pg-types": "^2.1.0",
         "pgpass": "1.x"
       },
@@ -5057,10 +5161,11 @@
       "optional": true
     },
     "node_modules/pg-connection-string": {
-      "version": "2.6.4",
-      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz",
-      "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==",
-      "dev": true
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz",
+      "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/pg-int8": {
       "version": "1.0.1",
@@ -5081,19 +5186,21 @@
       }
     },
     "node_modules/pg-pool": {
-      "version": "3.6.2",
-      "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz",
-      "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==",
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz",
+      "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==",
       "dev": true,
+      "license": "MIT",
       "peerDependencies": {
         "pg": ">=8.0"
       }
     },
     "node_modules/pg-protocol": {
-      "version": "1.6.1",
-      "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz",
-      "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==",
-      "dev": true
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz",
+      "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/pg-types": {
       "version": "2.2.0",
@@ -5121,10 +5228,11 @@
       }
     },
     "node_modules/picocolors": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
-      "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
-      "dev": true
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
+      "license": "ISC"
     },
     "node_modules/picomatch": {
       "version": "2.3.1",
@@ -5140,13 +5248,13 @@
       }
     },
     "node_modules/playwright": {
-      "version": "1.46.1",
-      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz",
-      "integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==",
+      "version": "1.48.2",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz",
+      "integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==",
       "dev": true,
       "license": "Apache-2.0",
       "dependencies": {
-        "playwright-core": "1.46.1"
+        "playwright-core": "1.48.2"
       },
       "bin": {
         "playwright": "cli.js"
@@ -5159,9 +5267,9 @@
       }
     },
     "node_modules/playwright-core": {
-      "version": "1.46.1",
-      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz",
-      "integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==",
+      "version": "1.48.2",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz",
+      "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==",
       "dev": true,
       "license": "Apache-2.0",
       "bin": {
@@ -5190,9 +5298,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.4.40",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz",
-      "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==",
+      "version": "8.4.49",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+      "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
       "dev": true,
       "funding": [
         {
@@ -5208,15 +5316,35 @@
           "url": "https://github.com/sponsors/ai"
         }
       ],
+      "license": "MIT",
       "dependencies": {
         "nanoid": "^3.3.7",
-        "picocolors": "^1.0.1",
-        "source-map-js": "^1.2.0"
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
       },
       "engines": {
         "node": "^10 || ^12 || >=14"
       }
     },
+    "node_modules/postcss/node_modules/nanoid": {
+      "version": "3.3.7",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+      "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
     "node_modules/postgres-array": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
@@ -5300,21 +5428,17 @@
       }
     },
     "node_modules/prettier-plugin-organize-imports": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
-      "integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz",
+      "integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==",
       "dev": true,
       "license": "MIT",
       "peerDependencies": {
-        "@vue/language-plugin-pug": "^2.0.24",
         "prettier": ">=2.0",
         "typescript": ">=2.9",
-        "vue-tsc": "^2.0.24"
+        "vue-tsc": "^2.1.0"
       },
       "peerDependenciesMeta": {
-        "@vue/language-plugin-pug": {
-          "optional": true
-        },
         "vue-tsc": {
           "optional": true
         }
@@ -5362,7 +5486,8 @@
           "type": "consulting",
           "url": "https://feross.org/support"
         }
-      ]
+      ],
+      "license": "MIT"
     },
     "node_modules/quick-lru": {
       "version": "7.0.0",
@@ -5377,14 +5502,15 @@
       }
     },
     "node_modules/raw-body": {
-      "version": "2.5.2",
-      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
-      "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
+      "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "bytes": "3.1.2",
         "http-errors": "2.0.0",
-        "iconv-lite": "0.4.24",
+        "iconv-lite": "0.6.3",
         "unpipe": "1.0.0"
       },
       "engines": {
@@ -5589,6 +5715,7 @@
       "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
       "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "iojs": ">=1.0.0",
         "node": ">=0.10.0"
@@ -5610,12 +5737,13 @@
       }
     },
     "node_modules/rollup": {
-      "version": "4.19.1",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.1.tgz",
-      "integrity": "sha512-K5vziVlg7hTpYfFBI+91zHBEMo6jafYXpkMlqZjg7/zhIG9iHqazBf4xz9AVdjS9BruRn280ROqLI7G3OFRIlw==",
+      "version": "4.27.3",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.27.3.tgz",
+      "integrity": "sha512-SLsCOnlmGt9VoZ9Ek8yBK8tAdmPHeppkw+Xa7yDlCEhDTvwYei03JlWo1fdc7YTfLZ4tD8riJCUyAgTbszk1fQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@types/estree": "1.0.5"
+        "@types/estree": "1.0.6"
       },
       "bin": {
         "rollup": "dist/bin/rollup"
@@ -5625,22 +5753,24 @@
         "npm": ">=8.0.0"
       },
       "optionalDependencies": {
-        "@rollup/rollup-android-arm-eabi": "4.19.1",
-        "@rollup/rollup-android-arm64": "4.19.1",
-        "@rollup/rollup-darwin-arm64": "4.19.1",
-        "@rollup/rollup-darwin-x64": "4.19.1",
-        "@rollup/rollup-linux-arm-gnueabihf": "4.19.1",
-        "@rollup/rollup-linux-arm-musleabihf": "4.19.1",
-        "@rollup/rollup-linux-arm64-gnu": "4.19.1",
-        "@rollup/rollup-linux-arm64-musl": "4.19.1",
-        "@rollup/rollup-linux-powerpc64le-gnu": "4.19.1",
-        "@rollup/rollup-linux-riscv64-gnu": "4.19.1",
-        "@rollup/rollup-linux-s390x-gnu": "4.19.1",
-        "@rollup/rollup-linux-x64-gnu": "4.19.1",
-        "@rollup/rollup-linux-x64-musl": "4.19.1",
-        "@rollup/rollup-win32-arm64-msvc": "4.19.1",
-        "@rollup/rollup-win32-ia32-msvc": "4.19.1",
-        "@rollup/rollup-win32-x64-msvc": "4.19.1",
+        "@rollup/rollup-android-arm-eabi": "4.27.3",
+        "@rollup/rollup-android-arm64": "4.27.3",
+        "@rollup/rollup-darwin-arm64": "4.27.3",
+        "@rollup/rollup-darwin-x64": "4.27.3",
+        "@rollup/rollup-freebsd-arm64": "4.27.3",
+        "@rollup/rollup-freebsd-x64": "4.27.3",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.27.3",
+        "@rollup/rollup-linux-arm-musleabihf": "4.27.3",
+        "@rollup/rollup-linux-arm64-gnu": "4.27.3",
+        "@rollup/rollup-linux-arm64-musl": "4.27.3",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.27.3",
+        "@rollup/rollup-linux-riscv64-gnu": "4.27.3",
+        "@rollup/rollup-linux-s390x-gnu": "4.27.3",
+        "@rollup/rollup-linux-x64-gnu": "4.27.3",
+        "@rollup/rollup-linux-x64-musl": "4.27.3",
+        "@rollup/rollup-win32-arm64-msvc": "4.27.3",
+        "@rollup/rollup-win32-ia32-msvc": "4.27.3",
+        "@rollup/rollup-win32-x64-msvc": "4.27.3",
         "fsevents": "~2.3.2"
       }
     },
@@ -5663,6 +5793,7 @@
           "url": "https://feross.org/support"
         }
       ],
+      "license": "MIT",
       "dependencies": {
         "queue-microtask": "^1.2.2"
       }
@@ -5691,13 +5822,15 @@
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/semver": {
-      "version": "7.6.2",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
-      "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
+      "version": "7.6.3",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+      "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
       "dev": true,
+      "license": "ISC",
       "bin": {
         "semver": "bin/semver.js"
       },
@@ -5792,14 +5925,15 @@
       }
     },
     "node_modules/socket.io-client": {
-      "version": "4.7.5",
-      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz",
-      "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==",
+      "version": "4.8.1",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
+      "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@socket.io/component-emitter": "~3.1.0",
         "debug": "~4.3.2",
-        "engine.io-client": "~6.5.2",
+        "engine.io-client": "~6.6.1",
         "socket.io-parser": "~4.2.4"
       },
       "engines": {
@@ -5820,10 +5954,11 @@
       }
     },
     "node_modules/source-map-js": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
-      "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -5885,10 +6020,11 @@
       }
     },
     "node_modules/std-env": {
-      "version": "3.7.0",
-      "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
-      "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==",
-      "dev": true
+      "version": "3.8.0",
+      "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz",
+      "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/string_decoder": {
       "version": "1.3.0",
@@ -5953,18 +6089,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/strip-final-newline": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
-      "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/strip-indent": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
@@ -6152,19 +6276,29 @@
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
       "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/tinybench": {
-      "version": "2.8.0",
-      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz",
-      "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==",
-      "dev": true
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+      "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tinyexec": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz",
+      "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/tinypool": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz",
-      "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz",
+      "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": "^18.0.0 || >=20.0.0"
       }
@@ -6179,23 +6313,15 @@
       }
     },
     "node_modules/tinyspy": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz",
-      "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==",
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
+      "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=14.0.0"
       }
     },
-    "node_modules/to-fast-properties": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
-      "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/to-regex-range": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -6277,9 +6403,9 @@
       }
     },
     "node_modules/typescript": {
-      "version": "5.5.4",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
-      "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
+      "version": "5.6.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+      "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
       "dev": true,
       "license": "Apache-2.0",
       "bin": {
@@ -6291,9 +6417,9 @@
       }
     },
     "node_modules/undici-types": {
-      "version": "6.19.8",
-      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
-      "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
+      "version": "6.20.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
+      "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
       "dev": true,
       "license": "MIT"
     },
@@ -6307,9 +6433,9 @@
       }
     },
     "node_modules/update-browserslist-db": {
-      "version": "1.0.13",
-      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
-      "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
+      "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
       "dev": true,
       "funding": [
         {
@@ -6325,9 +6451,10 @@
           "url": "https://github.com/sponsors/ai"
         }
       ],
+      "license": "MIT",
       "dependencies": {
-        "escalade": "^3.1.1",
-        "picocolors": "^1.0.0"
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.0"
       },
       "bin": {
         "update-browserslist-db": "cli.js"
@@ -6385,14 +6512,15 @@
       }
     },
     "node_modules/vite": {
-      "version": "5.3.5",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz",
-      "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==",
+      "version": "5.4.11",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
+      "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "esbuild": "^0.21.3",
-        "postcss": "^8.4.39",
-        "rollup": "^4.13.0"
+        "postcss": "^8.4.43",
+        "rollup": "^4.20.0"
       },
       "bin": {
         "vite": "bin/vite.js"
@@ -6411,6 +6539,7 @@
         "less": "*",
         "lightningcss": "^1.21.0",
         "sass": "*",
+        "sass-embedded": "*",
         "stylus": "*",
         "sugarss": "*",
         "terser": "^5.4.0"
@@ -6428,6 +6557,9 @@
         "sass": {
           "optional": true
         },
+        "sass-embedded": {
+          "optional": true
+        },
         "stylus": {
           "optional": true
         },
@@ -6440,15 +6572,16 @@
       }
     },
     "node_modules/vite-node": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz",
-      "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.5.tgz",
+      "integrity": "sha512-rd0QIgx74q4S1Rd56XIiL2cYEdyWn13cunYBIuqh9mpmQr7gGS0IxXoP8R6OaZtNQQLyXSWbd4rXKYUbhFpK5w==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "cac": "^6.7.14",
-        "debug": "^4.3.5",
+        "debug": "^4.3.7",
+        "es-module-lexer": "^1.5.4",
         "pathe": "^1.1.2",
-        "tinyrainbow": "^1.2.0",
         "vite": "^5.0.0"
       },
       "bin": {
@@ -6467,6 +6600,7 @@
       "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
       "dev": true,
       "hasInstallScript": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
@@ -6476,29 +6610,31 @@
       }
     },
     "node_modules/vitest": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz",
-      "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.5.tgz",
+      "integrity": "sha512-P4ljsdpuzRTPI/kbND2sDZ4VmieerR2c9szEZpjc+98Z9ebvnXmM5+0tHEKqYZumXqlvnmfWsjeFOjXVriDG7A==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@ampproject/remapping": "^2.3.0",
-        "@vitest/expect": "2.0.5",
-        "@vitest/pretty-format": "^2.0.5",
-        "@vitest/runner": "2.0.5",
-        "@vitest/snapshot": "2.0.5",
-        "@vitest/spy": "2.0.5",
-        "@vitest/utils": "2.0.5",
-        "chai": "^5.1.1",
-        "debug": "^4.3.5",
-        "execa": "^8.0.1",
-        "magic-string": "^0.30.10",
+        "@vitest/expect": "2.1.5",
+        "@vitest/mocker": "2.1.5",
+        "@vitest/pretty-format": "^2.1.5",
+        "@vitest/runner": "2.1.5",
+        "@vitest/snapshot": "2.1.5",
+        "@vitest/spy": "2.1.5",
+        "@vitest/utils": "2.1.5",
+        "chai": "^5.1.2",
+        "debug": "^4.3.7",
+        "expect-type": "^1.1.0",
+        "magic-string": "^0.30.12",
         "pathe": "^1.1.2",
-        "std-env": "^3.7.0",
-        "tinybench": "^2.8.0",
-        "tinypool": "^1.0.0",
+        "std-env": "^3.8.0",
+        "tinybench": "^2.9.0",
+        "tinyexec": "^0.3.1",
+        "tinypool": "^1.0.1",
         "tinyrainbow": "^1.2.0",
         "vite": "^5.0.0",
-        "vite-node": "2.0.5",
+        "vite-node": "2.1.5",
         "why-is-node-running": "^2.3.0"
       },
       "bin": {
@@ -6513,8 +6649,8 @@
       "peerDependencies": {
         "@edge-runtime/vm": "*",
         "@types/node": "^18.0.0 || >=20.0.0",
-        "@vitest/browser": "2.0.5",
-        "@vitest/ui": "2.0.5",
+        "@vitest/browser": "2.1.5",
+        "@vitest/ui": "2.1.5",
         "happy-dom": "*",
         "jsdom": "*"
       },
@@ -6723,9 +6859,9 @@
       }
     },
     "node_modules/xmlhttprequest-ssl": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
-      "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz",
+      "integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==",
       "dev": true,
       "engines": {
         "node": ">=0.4.0"
diff --git a/e2e/package.json b/e2e/package.json
index 6a6f2d9b76..d500d63bf0 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -1,6 +1,6 @@
 {
   "name": "immich-e2e",
-  "version": "1.115.0",
+  "version": "1.123.0",
   "description": "",
   "main": "index.js",
   "type": "module",
@@ -25,19 +25,19 @@
     "@immich/sdk": "file:../open-api/typescript-sdk",
     "@playwright/test": "^1.44.1",
     "@types/luxon": "^3.4.2",
-    "@types/node": "^20.16.5",
+    "@types/node": "^22.10.2",
     "@types/oidc-provider": "^8.5.1",
     "@types/pg": "^8.11.0",
     "@types/pngjs": "^6.0.4",
     "@types/supertest": "^6.0.2",
-    "@typescript-eslint/eslint-plugin": "^8.0.0",
-    "@typescript-eslint/parser": "^8.0.0",
+    "@typescript-eslint/eslint-plugin": "^8.15.0",
+    "@typescript-eslint/parser": "^8.15.0",
     "@vitest/coverage-v8": "^2.0.5",
-    "eslint": "^9.0.0",
+    "eslint": "^9.14.0",
     "eslint-config-prettier": "^9.1.0",
     "eslint-plugin-prettier": "^5.1.3",
-    "eslint-plugin-unicorn": "^55.0.0",
-    "exiftool-vendored": "^28.0.0",
+    "eslint-plugin-unicorn": "^56.0.1",
+    "exiftool-vendored": "^28.3.1",
     "globals": "^15.9.0",
     "jose": "^5.6.3",
     "luxon": "^3.4.4",
@@ -53,6 +53,6 @@
     "vitest": "^2.0.5"
   },
   "volta": {
-    "node": "20.17.0"
+    "node": "22.12.0"
   }
 }
diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts
index 55032bd364..2576a2c5c9 100644
--- a/e2e/playwright.config.ts
+++ b/e2e/playwright.config.ts
@@ -53,8 +53,10 @@ export default defineConfig({
 
   /* Run your local dev server before starting the tests */
   webServer: {
-    command: 'docker compose up --build -V --remove-orphans',
+    command: 'docker compose up --build --renew-anon-volumes --force-recreate --remove-orphans',
     url: 'http://127.0.0.1:2285',
+    stdout: 'pipe',
+    stderr: 'pipe',
     reuseExistingServer: true,
   },
 });
diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts
index 9e925c4021..5c101a0793 100644
--- a/e2e/src/api/specs/album.e2e-spec.ts
+++ b/e2e/src/api/specs/album.e2e-spec.ts
@@ -141,6 +141,7 @@ describe('/albums', () => {
       expect(body).toEqual({
         ...user1Albums[0],
         assets: [expect.objectContaining({ isFavorite: false })],
+        lastModifiedAssetTimestamp: expect.any(String),
       });
     });
 
@@ -297,6 +298,7 @@ describe('/albums', () => {
       expect(body).toEqual({
         ...user1Albums[0],
         assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
+        lastModifiedAssetTimestamp: expect.any(String),
       });
     });
 
@@ -327,6 +329,7 @@ describe('/albums', () => {
       expect(body).toEqual({
         ...user1Albums[0],
         assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
+        lastModifiedAssetTimestamp: expect.any(String),
       });
     });
 
@@ -340,6 +343,7 @@ describe('/albums', () => {
         ...user1Albums[0],
         assets: [],
         assetCount: 1,
+        lastModifiedAssetTimestamp: expect.any(String),
       });
     });
   });
diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts
index e0281085cf..a0c429a82e 100644
--- a/e2e/src/api/specs/asset.e2e-spec.ts
+++ b/e2e/src/api/specs/asset.e2e-spec.ts
@@ -76,7 +76,6 @@ describe('/asset', () => {
   let user2Assets: AssetMediaResponseDto[];
   let locationAsset: AssetMediaResponseDto;
   let ratingAsset: AssetMediaResponseDto;
-  let facesAsset: AssetMediaResponseDto;
 
   const setupTests = async () => {
     await utils.resetDatabase();
@@ -236,7 +235,7 @@ describe('/asset', () => {
       await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
 
       // asset faces
-      facesAsset = await utils.createAsset(admin.accessToken, {
+      const facesAsset = await utils.createAsset(admin.accessToken, {
         assetData: {
           filename: 'portrait.jpg',
           bytes: await readFile(facesAssetFilepath),
@@ -1061,7 +1060,7 @@ describe('/asset', () => {
         expected: {
           type: AssetTypeEnum.Image,
           originalFileName: 'philadelphia.nef',
-          fileCreatedAt: '2016-09-22T22:10:29.060Z',
+          fileCreatedAt: '2016-09-22T21:10:29.060Z',
           exifInfo: {
             make: 'NIKON CORPORATION',
             model: 'NIKON D700',
@@ -1070,11 +1069,11 @@ describe('/asset', () => {
             focalLength: 85,
             iso: 200,
             fileSizeInByte: 15_856_335,
-            dateTimeOriginal: '2016-09-22T22:10:29.060Z',
+            dateTimeOriginal: '2016-09-22T21:10:29.060Z',
             latitude: null,
             longitude: null,
             orientation: '1',
-            timeZone: 'UTC-5',
+            timeZone: 'UTC-4',
           },
         },
       },
@@ -1149,6 +1148,78 @@ describe('/asset', () => {
           },
         },
       },
+      {
+        input: 'formats/raw/Canon/PowerShot_G12.CR2',
+        expected: {
+          type: AssetTypeEnum.Image,
+          originalFileName: 'PowerShot_G12.CR2',
+          fileCreatedAt: '2015-12-27T09:55:40.000Z',
+          exifInfo: {
+            make: 'Canon',
+            model: 'Canon PowerShot G12',
+            exifImageHeight: 2736,
+            exifImageWidth: 3648,
+            exposureTime: '1/1000',
+            fNumber: 4,
+            focalLength: 18.098,
+            iso: 80,
+            lensModel: null,
+            fileSizeInByte: 11_113_617,
+            dateTimeOriginal: '2015-12-27T09:55:40.000Z',
+            latitude: null,
+            longitude: null,
+            orientation: '1',
+          },
+        },
+      },
+      {
+        input: 'formats/raw/Fujifilm/X100V_compressed.RAF',
+        expected: {
+          type: AssetTypeEnum.Image,
+          originalFileName: 'X100V_compressed.RAF',
+          fileCreatedAt: '2024-10-12T21:01:01.000Z',
+          exifInfo: {
+            make: 'FUJIFILM',
+            model: 'X100V',
+            exifImageHeight: 4160,
+            exifImageWidth: 6240,
+            exposureTime: '1/4000',
+            fNumber: 16,
+            focalLength: 23,
+            iso: 160,
+            lensModel: null,
+            fileSizeInByte: 13_551_312,
+            dateTimeOriginal: '2024-10-12T21:01:01.000Z',
+            latitude: null,
+            longitude: null,
+            orientation: '6',
+          },
+        },
+      },
+      {
+        input: 'formats/raw/Ricoh/GR3/Ricoh_GR3-450.DNG',
+        expected: {
+          type: AssetTypeEnum.Image,
+          originalFileName: 'Ricoh_GR3-450.DNG',
+          fileCreatedAt: '2024-06-08T13:48:39.000Z',
+          exifInfo: {
+            dateTimeOriginal: '2024-06-08T13:48:39.000Z',
+            exifImageHeight: 4064,
+            exifImageWidth: 6112,
+            exposureTime: '1/400',
+            fNumber: 5,
+            fileSizeInByte: 31_175_472,
+            focalLength: 18.3,
+            iso: 100,
+            latitude: 36.613_24,
+            lensModel: 'GR LENS 18.3mm F2.8',
+            longitude: -121.897_85,
+            make: 'RICOH IMAGING COMPANY, LTD.',
+            model: 'RICOH GR III',
+            orientation: '1',
+          },
+        },
+      },
     ];
 
     it(`should upload and generate a thumbnail for different file types`, async () => {
diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts
index 8d98e86630..23cdf092cf 100644
--- a/e2e/src/api/specs/library.e2e-spec.ts
+++ b/e2e/src/api/specs/library.e2e-spec.ts
@@ -1,12 +1,5 @@
-import {
-  LibraryResponseDto,
-  LoginResponseDto,
-  ScanLibraryDto,
-  getAllLibraries,
-  removeOfflineFiles,
-  scanLibrary,
-} from '@immich/sdk';
-import { cpSync, existsSync } from 'node:fs';
+import { LibraryResponseDto, LoginResponseDto, getAllLibraries, scanLibrary } from '@immich/sdk';
+import { cpSync, existsSync, rmSync, unlinkSync } from 'node:fs';
 import { Socket } from 'socket.io-client';
 import { userDto, uuidDto } from 'src/fixtures';
 import { errorDto } from 'src/responses';
@@ -15,8 +8,7 @@ import request from 'supertest';
 import { utimes } from 'utimes';
 import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
 
-const scan = async (accessToken: string, id: string, dto: ScanLibraryDto = {}) =>
-  scanLibrary({ id, scanLibraryDto: dto }, { headers: asBearerAuth(accessToken) });
+const scan = async (accessToken: string, id: string) => scanLibrary({ id }, { headers: asBearerAuth(accessToken) });
 
 describe('/libraries', () => {
   let admin: LoginResponseDto;
@@ -293,16 +285,21 @@ describe('/libraries', () => {
       expect(body).toEqual(errorDto.unauthorized);
     });
 
-    it('should scan external library', async () => {
+    it('should import new asset when scanning external library', async () => {
       const library = await utils.createLibrary(admin.accessToken, {
         ownerId: admin.userId,
         importPaths: [`${testAssetDirInternal}/temp/directoryA`],
       });
 
-      await scan(admin.accessToken, library.id);
-      await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 1 });
+      const { status } = await request(app)
+        .post(`/libraries/${library.id}/scan`)
+        .set('Authorization', `Bearer ${admin.accessToken}`)
+        .send();
+      expect(status).toBe(204);
 
-      const { assets } = await utils.metadataSearch(admin.accessToken, {
+      await utils.waitForQueueFinish(admin.accessToken, 'library');
+
+      const { assets } = await utils.searchAssets(admin.accessToken, {
         originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`,
       });
       expect(assets.count).toBe(1);
@@ -315,10 +312,15 @@ describe('/libraries', () => {
         exclusionPatterns: ['**/directoryA'],
       });
 
-      await scan(admin.accessToken, library.id);
-      await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 1 });
+      const { status } = await request(app)
+        .post(`/libraries/${library.id}/scan`)
+        .set('Authorization', `Bearer ${admin.accessToken}`)
+        .send();
+      expect(status).toBe(204);
 
-      const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
+      await utils.waitForQueueFinish(admin.accessToken, 'library');
+
+      const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
 
       expect(assets.count).toBe(1);
       expect(assets.items[0].originalPath.includes('directoryB'));
@@ -330,105 +332,304 @@ describe('/libraries', () => {
         importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`],
       });
 
-      await scan(admin.accessToken, library.id);
-      await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 });
+      const { status } = await request(app)
+        .post(`/libraries/${library.id}/scan`)
+        .set('Authorization', `Bearer ${admin.accessToken}`)
+        .send();
+      expect(status).toBe(204);
 
-      const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
+      await utils.waitForQueueFinish(admin.accessToken, 'library');
+
+      const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
 
       expect(assets.count).toBe(2);
       expect(assets.items.find((asset) => asset.originalPath.includes('directoryA'))).toBeDefined();
       expect(assets.items.find((asset) => asset.originalPath.includes('directoryB'))).toBeDefined();
     });
 
-    it('should pick up new files', async () => {
+    it('should scan multiple import paths with commas', async () => {
+      // https://github.com/immich-app/immich/issues/10699
       const library = await utils.createLibrary(admin.accessToken, {
         ownerId: admin.userId,
-        importPaths: [`${testAssetDirInternal}/temp`],
+        importPaths: [`${testAssetDirInternal}/temp/folder, a`, `${testAssetDirInternal}/temp/folder, b`],
       });
 
-      await scan(admin.accessToken, library.id);
-      await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 });
+      utils.createImageFile(`${testAssetDir}/temp/folder, a/assetA.png`);
+      utils.createImageFile(`${testAssetDir}/temp/folder, b/assetB.png`);
 
-      const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
+      const { status } = await request(app)
+        .post(`/libraries/${library.id}/scan`)
+        .set('Authorization', `Bearer ${admin.accessToken}`)
+        .send();
+      expect(status).toBe(204);
+
+      await utils.waitForQueueFinish(admin.accessToken, 'library');
+
+      const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
 
       expect(assets.count).toBe(2);
+      expect(assets.items.find((asset) => asset.originalPath.includes('folder, a'))).toBeDefined();
+      expect(assets.items.find((asset) => asset.originalPath.includes('folder, b'))).toBeDefined();
 
-      utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
-
-      await scan(admin.accessToken, library.id);
-      await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 3 });
-
-      const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
-
-      expect(newAssets.count).toBe(3);
-      utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
+      utils.removeImageFile(`${testAssetDir}/temp/folder, a/assetA.png`);
+      utils.removeImageFile(`${testAssetDir}/temp/folder, b/assetB.png`);
     });
 
-    it('should offline a file missing from disk', async () => {
-      utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
+    it('should scan multiple import paths with braces', async () => {
+      // https://github.com/immich-app/immich/issues/10699
       const library = await utils.createLibrary(admin.accessToken, {
         ownerId: admin.userId,
-        importPaths: [`${testAssetDirInternal}/temp`],
+        importPaths: [`${testAssetDirInternal}/temp/folder{ a`, `${testAssetDirInternal}/temp/folder} b`],
       });
 
-      await scan(admin.accessToken, library.id);
+      utils.createImageFile(`${testAssetDir}/temp/folder{ a/assetA.png`);
+      utils.createImageFile(`${testAssetDir}/temp/folder} b/assetB.png`);
+
+      const { status } = await request(app)
+        .post(`/libraries/${library.id}/scan`)
+        .set('Authorization', `Bearer ${admin.accessToken}`)
+        .send();
+      expect(status).toBe(204);
+
       await utils.waitForQueueFinish(admin.accessToken, 'library');
 
-      const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
-      expect(assets.count).toBe(3);
+      const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
 
-      utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
+      expect(assets.count).toBe(2);
+      expect(assets.items.find((asset) => asset.originalPath.includes('folder{ a'))).toBeDefined();
+      expect(assets.items.find((asset) => asset.originalPath.includes('folder} b'))).toBeDefined();
+
+      utils.removeImageFile(`${testAssetDir}/temp/folder{ a/assetA.png`);
+      utils.removeImageFile(`${testAssetDir}/temp/folder} b/assetB.png`);
+    });
+
+    const annoyingChars = [
+      "'",
+      '"',
+      '`',
+      '*',
+      '{',
+      '}',
+      ',',
+      '(',
+      ')',
+      '[',
+      ']',
+      '?',
+      '!',
+      '@',
+      '#',
+      '$',
+      '%',
+      '^',
+      '&',
+      '=',
+      '+',
+      '~',
+      '|',
+      '<',
+      '>',
+      ';',
+      ':',
+      '/', // We never got backslashes to work
+    ];
+
+    it.each(annoyingChars)('should scan multiple import paths with %s', async (char) => {
+      const library = await utils.createLibrary(admin.accessToken, {
+        ownerId: admin.userId,
+        importPaths: [`${testAssetDirInternal}/temp/folder${char}1`, `${testAssetDirInternal}/temp/folder${char}2`],
+      });
+
+      utils.createImageFile(`${testAssetDir}/temp/folder${char}1/asset1.png`);
+      utils.createImageFile(`${testAssetDir}/temp/folder${char}2/asset2.png`);
+
+      const { status } = await request(app)
+        .post(`/libraries/${library.id}/scan`)
+        .set('Authorization', `Bearer ${admin.accessToken}`)
+        .send();
+      expect(status).toBe(204);
 
-      await scan(admin.accessToken, library.id);
       await utils.waitForQueueFinish(admin.accessToken, 'library');
 
-      const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
-      expect(newAssets.count).toBe(3);
+      const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
 
-      expect(newAssets.items).toEqual(
+      expect(assets.items).toEqual(
         expect.arrayContaining([
-          expect.objectContaining({
-            isOffline: true,
-            originalFileName: 'assetC.png',
-          }),
+          expect.objectContaining({ originalPath: expect.stringContaining(`folder${char}1/asset1.png`) }),
+          expect.objectContaining({ originalPath: expect.stringContaining(`folder${char}2/asset2.png`) }),
         ]),
       );
+
+      utils.removeImageFile(`${testAssetDir}/temp/folder${char}1/asset1.png`);
+      utils.removeImageFile(`${testAssetDir}/temp/folder${char}2/asset2.png`);
     });
 
-    it('should offline a file outside of import paths', async () => {
+    it('should reimport a modified file', async () => {
       const library = await utils.createLibrary(admin.accessToken, {
         ownerId: admin.userId,
-        importPaths: [`${testAssetDirInternal}/temp`],
+        importPaths: [`${testAssetDirInternal}/temp/reimport`],
+      });
+
+      utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
+      await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
+
+      await scan(admin.accessToken, library.id);
+      await utils.waitForQueueFinish(admin.accessToken, 'library');
+      await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
+      await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+      cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
+      await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_001);
+
+      const { status } = await request(app)
+        .post(`/libraries/${library.id}/scan`)
+        .set('Authorization', `Bearer ${admin.accessToken}`)
+        .send();
+      expect(status).toBe(204);
+
+      await utils.waitForQueueFinish(admin.accessToken, 'library');
+      await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
+      await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+      const { assets } = await utils.searchAssets(admin.accessToken, {
+        libraryId: library.id,
+      });
+
+      expect(assets.count).toEqual(1);
+
+      const asset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
+
+      expect(asset).toEqual(
+        expect.objectContaining({
+          originalFileName: 'asset.jpg',
+          exifInfo: expect.objectContaining({
+            model: 'NIKON D750',
+          }),
+        }),
+      );
+
+      utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
+    });
+
+    it('should not reimport unmodified files', async () => {
+      const library = await utils.createLibrary(admin.accessToken, {
+        ownerId: admin.userId,
+        importPaths: [`${testAssetDirInternal}/temp/reimport`],
+      });
+
+      utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
+      await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
+
+      await scan(admin.accessToken, library.id);
+      await utils.waitForQueueFinish(admin.accessToken, 'library');
+
+      cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
+      await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
+
+      const { status } = await request(app)
+        .post(`/libraries/${library.id}/scan`)
+        .set('Authorization', `Bearer ${admin.accessToken}`)
+        .send();
+      expect(status).toBe(204);
+
+      await utils.waitForQueueFinish(admin.accessToken, 'library');
+      await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
+      await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+      const { assets } = await utils.searchAssets(admin.accessToken, {
+        libraryId: library.id,
+      });
+
+      expect(assets.count).toEqual(1);
+
+      const asset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
+
+      expect(asset).toEqual(
+        expect.objectContaining({
+          originalFileName: 'asset.jpg',
+          exifInfo: expect.not.objectContaining({
+            model: 'NIKON D750',
+          }),
+        }),
+      );
+
+      utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
+    });
+
+    it('should set an asset offline if its file is missing', async () => {
+      const library = await utils.createLibrary(admin.accessToken, {
+        ownerId: admin.userId,
+        importPaths: [`${testAssetDirInternal}/temp/offline`],
+      });
+
+      utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
+
+      await scan(admin.accessToken, library.id);
+      await utils.waitForQueueFinish(admin.accessToken, 'library');
+
+      const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
+      expect(assets.count).toBe(1);
+
+      utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
+
+      const { status } = await request(app)
+        .post(`/libraries/${library.id}/scan`)
+        .set('Authorization', `Bearer ${admin.accessToken}`)
+        .send();
+      expect(status).toBe(204);
+
+      await utils.waitForQueueFinish(admin.accessToken, 'library');
+
+      const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
+      expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
+      expect(trashedAsset.isOffline).toEqual(true);
+
+      const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
+      expect(newAssets.items).toEqual([]);
+    });
+
+    it('should set an asset offline its file is not in any import path', async () => {
+      utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
+
+      const library = await utils.createLibrary(admin.accessToken, {
+        ownerId: admin.userId,
+        importPaths: [`${testAssetDirInternal}/temp/offline`],
       });
 
       await scan(admin.accessToken, library.id);
       await utils.waitForQueueFinish(admin.accessToken, 'library');
 
+      const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
+      expect(assets.count).toBe(1);
+
+      utils.createDirectory(`${testAssetDir}/temp/another-path/`);
+
       await request(app)
         .put(`/libraries/${library.id}`)
         .set('Authorization', `Bearer ${admin.accessToken}`)
-        .send({ importPaths: [`${testAssetDirInternal}/temp/directoryA`] });
+        .send({ importPaths: [`${testAssetDirInternal}/temp/another-path/`] });
+
+      const { status } = await request(app)
+        .post(`/libraries/${library.id}/scan`)
+        .set('Authorization', `Bearer ${admin.accessToken}`)
+        .send();
+      expect(status).toBe(204);
 
-      await scan(admin.accessToken, library.id);
       await utils.waitForQueueFinish(admin.accessToken, 'library');
 
-      const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
+      const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
+      expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
+      expect(trashedAsset.isOffline).toBe(true);
 
-      expect(assets.items).toEqual(
-        expect.arrayContaining([
-          expect.objectContaining({
-            isOffline: false,
-            originalFileName: 'assetA.png',
-          }),
-          expect.objectContaining({
-            isOffline: true,
-            originalFileName: 'assetB.png',
-          }),
-        ]),
-      );
+      const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
+
+      expect(newAssets.items).toEqual([]);
+
+      utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
+      utils.removeDirectory(`${testAssetDir}/temp/another-path/`);
     });
 
-    it('should offline a file covered by an exclusion pattern', async () => {
+    it('should set an asset offline if its file is covered by an exclusion pattern', async () => {
       const library = await utils.createLibrary(admin.accessToken, {
         ownerId: admin.userId,
         importPaths: [`${testAssetDirInternal}/temp`],
@@ -437,6 +638,12 @@ describe('/libraries', () => {
       await scan(admin.accessToken, library.id);
       await utils.waitForQueueFinish(admin.accessToken, 'library');
 
+      const { assets } = await utils.searchAssets(admin.accessToken, {
+        libraryId: library.id,
+        originalFileName: 'assetB.png',
+      });
+      expect(assets.count).toBe(1);
+
       await request(app)
         .put(`/libraries/${library.id}`)
         .set('Authorization', `Bearer ${admin.accessToken}`)
@@ -445,72 +652,21 @@ describe('/libraries', () => {
       await scan(admin.accessToken, library.id);
       await utils.waitForQueueFinish(admin.accessToken, 'library');
 
-      const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
+      const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
+      expect(trashedAsset.isTrashed).toBe(true);
+      expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/directoryB/assetB.png`);
+      expect(trashedAsset.isOffline).toBe(true);
 
-      expect(assets.count).toBe(2);
+      const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
 
-      expect(assets.items).toEqual(
-        expect.arrayContaining([
-          expect.objectContaining({
-            isOffline: false,
-            originalFileName: 'assetA.png',
-          }),
-          expect.objectContaining({
-            isOffline: true,
-            originalFileName: 'assetB.png',
-          }),
-        ]),
-      );
+      expect(newAssets.items).toEqual([
+        expect.objectContaining({
+          originalFileName: 'assetA.png',
+        }),
+      ]);
     });
 
-    it('should not try to delete offline files', async () => {
-      utils.createImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
-
-      const library = await utils.createLibrary(admin.accessToken, {
-        ownerId: admin.userId,
-        importPaths: [`${testAssetDirInternal}/temp/offline1`],
-      });
-
-      await scan(admin.accessToken, library.id);
-      await utils.waitForQueueFinish(admin.accessToken, 'library');
-
-      const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
-      expect(initialAssets).toEqual({
-        count: 1,
-        total: 1,
-        facets: [],
-        items: [expect.objectContaining({ originalFileName: 'assetA.png' })],
-        nextPage: null,
-      });
-
-      utils.removeImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
-
-      await scan(admin.accessToken, library.id);
-      await utils.waitForQueueFinish(admin.accessToken, 'library');
-
-      const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, {
-        libraryId: library.id,
-        isOffline: true,
-      });
-      expect(offlineAssets).toEqual({
-        count: 1,
-        total: 1,
-        facets: [],
-        items: [expect.objectContaining({ originalFileName: 'assetA.png' })],
-        nextPage: null,
-      });
-
-      utils.createImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
-      await removeOfflineFiles({ id: library.id }, { headers: asBearerAuth(admin.accessToken) });
-      await utils.waitForQueueFinish(admin.accessToken, 'library');
-      await utils.waitForWebsocketEvent({ event: 'assetDelete', total: 1 });
-
-      expect(existsSync(`${testAssetDir}/temp/offline1/assetA.png`)).toBe(true);
-
-      utils.removeImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
-    });
-
-    it('should scan new files', async () => {
+    it('should not trash an online asset', async () => {
       const library = await utils.createLibrary(admin.accessToken, {
         ownerId: admin.userId,
         importPaths: [`${testAssetDirInternal}/temp`],
@@ -519,230 +675,313 @@ describe('/libraries', () => {
       await scan(admin.accessToken, library.id);
       await utils.waitForQueueFinish(admin.accessToken, 'library');
 
-      utils.createImageFile(`${testAssetDir}/temp/directoryC/assetC.png`);
-
-      await scan(admin.accessToken, library.id);
-      await utils.waitForQueueFinish(admin.accessToken, 'library');
-
-      const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
-
-      expect(assets.count).toBe(3);
-      expect(assets.items).toEqual(
-        expect.arrayContaining([
-          expect.objectContaining({
-            originalFileName: 'assetC.png',
-          }),
-        ]),
-      );
-
-      utils.removeImageFile(`${testAssetDir}/temp/directoryC/assetC.png`);
-    });
-
-    describe('with refreshModifiedFiles=true', () => {
-      it('should reimport modified files', async () => {
-        const library = await utils.createLibrary(admin.accessToken, {
-          ownerId: admin.userId,
-          importPaths: [`${testAssetDirInternal}/temp`],
-        });
-
-        utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
-        await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
-
-        await scan(admin.accessToken, library.id);
-        await utils.waitForQueueFinish(admin.accessToken, 'library');
-
-        cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
-        await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_001);
-
-        await scan(admin.accessToken, library.id, { refreshModifiedFiles: true });
-        await utils.waitForQueueFinish(admin.accessToken, 'library');
-        await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
-        utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
-
-        const { assets } = await utils.metadataSearch(admin.accessToken, {
-          libraryId: library.id,
-          model: 'NIKON D750',
-        });
-        expect(assets.count).toBe(1);
-      });
-
-      it('should not reimport unmodified files', async () => {
-        const library = await utils.createLibrary(admin.accessToken, {
-          ownerId: admin.userId,
-          importPaths: [`${testAssetDirInternal}/temp`],
-        });
-
-        utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
-        await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
-
-        await scan(admin.accessToken, library.id);
-        await utils.waitForQueueFinish(admin.accessToken, 'library');
-
-        cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
-        await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
-
-        await scan(admin.accessToken, library.id, { refreshModifiedFiles: true });
-        await utils.waitForQueueFinish(admin.accessToken, 'library');
-        await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
-        utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
-
-        const { assets } = await utils.metadataSearch(admin.accessToken, {
-          libraryId: library.id,
-          model: 'NIKON D750',
-        });
-        expect(assets.count).toBe(0);
-      });
-    });
-
-    describe('with refreshAllFiles=true', () => {
-      it('should reimport all files', async () => {
-        const library = await utils.createLibrary(admin.accessToken, {
-          ownerId: admin.userId,
-          importPaths: [`${testAssetDirInternal}/temp`],
-        });
-
-        utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
-        await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
-
-        await scan(admin.accessToken, library.id);
-        await utils.waitForQueueFinish(admin.accessToken, 'library');
-
-        cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
-        await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
-
-        await scan(admin.accessToken, library.id, { refreshAllFiles: true });
-        await utils.waitForQueueFinish(admin.accessToken, 'library');
-        await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
-        utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
-
-        const { assets } = await utils.metadataSearch(admin.accessToken, {
-          libraryId: library.id,
-          model: 'NIKON D750',
-        });
-        expect(assets.count).toBe(1);
-      });
-    });
-  });
-
-  describe('POST /libraries/:id/removeOffline', () => {
-    it('should require authentication', async () => {
-      const { status, body } = await request(app).post(`/libraries/${uuidDto.notFound}/removeOffline`).send({});
-
-      expect(status).toBe(401);
-      expect(body).toEqual(errorDto.unauthorized);
-    });
-
-    it('should remove offline files', async () => {
-      const library = await utils.createLibrary(admin.accessToken, {
-        ownerId: admin.userId,
-        importPaths: [`${testAssetDirInternal}/temp/offline`],
-      });
-
-      utils.createImageFile(`${testAssetDir}/temp/offline/online.png`);
-      utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
-
-      await scan(admin.accessToken, library.id);
-      await utils.waitForQueueFinish(admin.accessToken, 'library');
-
-      const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, {
-        libraryId: library.id,
-      });
-      expect(initialAssets.count).toBe(2);
-
-      utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
-
-      await scan(admin.accessToken, library.id);
-      await utils.waitForQueueFinish(admin.accessToken, 'library');
-
-      const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, {
-        libraryId: library.id,
-        isOffline: true,
-      });
-      expect(offlineAssets.count).toBe(1);
-
-      const { status } = await request(app)
-        .post(`/libraries/${library.id}/removeOffline`)
-        .set('Authorization', `Bearer ${admin.accessToken}`)
-        .send();
-      expect(status).toBe(204);
-      await utils.waitForQueueFinish(admin.accessToken, 'library');
-      await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask');
-
-      const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
-
-      expect(assets.count).toBe(1);
-
-      utils.removeImageFile(`${testAssetDir}/temp/offline/online.png`);
-    });
-
-    it('should remove offline files from trash', async () => {
-      const library = await utils.createLibrary(admin.accessToken, {
-        ownerId: admin.userId,
-        importPaths: [`${testAssetDirInternal}/temp/offline`],
-      });
-
-      utils.createImageFile(`${testAssetDir}/temp/offline/online.png`);
-      utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
-
-      await scan(admin.accessToken, library.id);
-      await utils.waitForQueueFinish(admin.accessToken, 'library');
-
-      const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, {
-        libraryId: library.id,
-      });
-
-      expect(initialAssets.count).toBe(2);
-      utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
-
-      await scan(admin.accessToken, library.id);
-      await utils.waitForQueueFinish(admin.accessToken, 'library');
-
-      const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, {
-        libraryId: library.id,
-        isOffline: true,
-      });
-      expect(offlineAssets.count).toBe(1);
-
-      const { status } = await request(app)
-        .post(`/libraries/${library.id}/removeOffline`)
-        .set('Authorization', `Bearer ${admin.accessToken}`)
-        .send();
-      expect(status).toBe(204);
-      await utils.waitForQueueFinish(admin.accessToken, 'library');
-      await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask');
-
-      const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
-
-      expect(assets.count).toBe(1);
-      expect(assets.items[0].isOffline).toBe(false);
-      expect(assets.items[0].originalPath).toEqual(`${testAssetDirInternal}/temp/offline/online.png`);
-
-      utils.removeImageFile(`${testAssetDir}/temp/offline/online.png`);
-    });
-
-    it('should not remove online files', async () => {
-      const library = await utils.createLibrary(admin.accessToken, {
-        ownerId: admin.userId,
-        importPaths: [`${testAssetDirInternal}/temp`],
-      });
-
-      await scan(admin.accessToken, library.id);
-      await utils.waitForQueueFinish(admin.accessToken, 'library');
-
-      const { assets: assetsBefore } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
+      const { assets: assetsBefore } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
       expect(assetsBefore.count).toBeGreaterThan(1);
 
       const { status } = await request(app)
-        .post(`/libraries/${library.id}/removeOffline`)
+        .post(`/libraries/${library.id}/scan`)
         .set('Authorization', `Bearer ${admin.accessToken}`)
         .send();
       expect(status).toBe(204);
+
       await utils.waitForQueueFinish(admin.accessToken, 'library');
 
-      const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
+      const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
 
       expect(assets).toEqual(assetsBefore);
     });
+
+    describe('xmp metadata', async () => {
+      it('should import metadata from file.xmp', async () => {
+        const library = await utils.createLibrary(admin.accessToken, {
+          ownerId: admin.userId,
+          importPaths: [`${testAssetDirInternal}/temp/xmp`],
+        });
+
+        cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
+        cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
+
+        await scan(admin.accessToken, library.id);
+
+        await utils.waitForQueueFinish(admin.accessToken, 'library');
+        await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
+        await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+        const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
+
+        expect(newAssets.items).toEqual([
+          expect.objectContaining({
+            originalFileName: 'glarus.nef',
+            fileCreatedAt: '2000-09-27T12:35:33.000Z',
+          }),
+        ]);
+
+        rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
+      });
+
+      it('should import metadata from file.ext.xmp', async () => {
+        const library = await utils.createLibrary(admin.accessToken, {
+          ownerId: admin.userId,
+          importPaths: [`${testAssetDirInternal}/temp/xmp`],
+        });
+
+        cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
+        cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
+
+        await scan(admin.accessToken, library.id);
+        await utils.waitForQueueFinish(admin.accessToken, 'library');
+        await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
+        await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+        const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
+
+        expect(newAssets.items).toEqual([
+          expect.objectContaining({
+            originalFileName: 'glarus.nef',
+            fileCreatedAt: '2000-09-27T12:35:33.000Z',
+          }),
+        ]);
+
+        rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
+      });
+
+      it('should import metadata in file.ext.xmp before file.xmp if both exist', async () => {
+        const library = await utils.createLibrary(admin.accessToken, {
+          ownerId: admin.userId,
+          importPaths: [`${testAssetDirInternal}/temp/xmp`],
+        });
+
+        cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
+        cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
+        cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
+
+        await scan(admin.accessToken, library.id);
+        await utils.waitForQueueFinish(admin.accessToken, 'library');
+        await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
+        await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+        const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
+
+        expect(newAssets.items).toEqual([
+          expect.objectContaining({
+            originalFileName: 'glarus.nef',
+            fileCreatedAt: '2000-09-27T12:35:33.000Z',
+          }),
+        ]);
+
+        rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
+      });
+
+      it('should switch from using file.xmp to file.ext.xmp when asset refreshes', async () => {
+        const library = await utils.createLibrary(admin.accessToken, {
+          ownerId: admin.userId,
+          importPaths: [`${testAssetDirInternal}/temp/xmp`],
+        });
+
+        cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
+        cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
+        await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
+
+        await scan(admin.accessToken, library.id);
+        await utils.waitForQueueFinish(admin.accessToken, 'library');
+        await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
+        await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+        cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
+        unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
+        await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
+
+        await scan(admin.accessToken, library.id);
+        await utils.waitForQueueFinish(admin.accessToken, 'library');
+        await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
+        await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+        const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
+
+        expect(newAssets.items).toEqual([
+          expect.objectContaining({
+            originalFileName: 'glarus.nef',
+            fileCreatedAt: '2010-09-27T12:35:33.000Z',
+          }),
+        ]);
+
+        rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
+      });
+
+      it('should switch from using file metadata to file.xmp metadata when asset refreshes', async () => {
+        const library = await utils.createLibrary(admin.accessToken, {
+          ownerId: admin.userId,
+          importPaths: [`${testAssetDirInternal}/temp/xmp`],
+        });
+
+        cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
+        await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
+
+        await scan(admin.accessToken, library.id);
+        await utils.waitForQueueFinish(admin.accessToken, 'library');
+        await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
+        await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+        cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
+        await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
+
+        await scan(admin.accessToken, library.id);
+        await utils.waitForQueueFinish(admin.accessToken, 'library');
+        await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
+        await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+        const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
+
+        expect(newAssets.items).toEqual([
+          expect.objectContaining({
+            originalFileName: 'glarus.nef',
+            fileCreatedAt: '2000-09-27T12:35:33.000Z',
+          }),
+        ]);
+
+        rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
+      });
+
+      it('should switch from using file metadata to file.xmp metadata when asset refreshes', async () => {
+        const library = await utils.createLibrary(admin.accessToken, {
+          ownerId: admin.userId,
+          importPaths: [`${testAssetDirInternal}/temp/xmp`],
+        });
+
+        cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
+        await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
+
+        await scan(admin.accessToken, library.id);
+        await utils.waitForQueueFinish(admin.accessToken, 'library');
+        await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
+        await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+        cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
+        await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
+
+        await scan(admin.accessToken, library.id);
+        await utils.waitForQueueFinish(admin.accessToken, 'library');
+        await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
+        await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+        const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
+
+        expect(newAssets.items).toEqual([
+          expect.objectContaining({
+            originalFileName: 'glarus.nef',
+            fileCreatedAt: '2000-09-27T12:35:33.000Z',
+          }),
+        ]);
+
+        rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
+      });
+
+      it('should switch from using file.ext.xmp to file.xmp when asset refreshes', async () => {
+        const library = await utils.createLibrary(admin.accessToken, {
+          ownerId: admin.userId,
+          importPaths: [`${testAssetDirInternal}/temp/xmp`],
+        });
+
+        cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
+        cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
+        await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
+
+        await scan(admin.accessToken, library.id);
+        await utils.waitForQueueFinish(admin.accessToken, 'library');
+        await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
+        await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+        cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
+        unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
+        await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
+
+        await scan(admin.accessToken, library.id);
+        await utils.waitForQueueFinish(admin.accessToken, 'library');
+        await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
+        await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+        const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
+
+        expect(newAssets.items).toEqual([
+          expect.objectContaining({
+            originalFileName: 'glarus.nef',
+            fileCreatedAt: '2010-09-27T12:35:33.000Z',
+          }),
+        ]);
+
+        rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
+      });
+
+      it('should switch from using file.ext.xmp to file metadata', async () => {
+        const library = await utils.createLibrary(admin.accessToken, {
+          ownerId: admin.userId,
+          importPaths: [`${testAssetDirInternal}/temp/xmp`],
+        });
+
+        cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
+        cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
+        await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
+
+        await scan(admin.accessToken, library.id);
+        await utils.waitForQueueFinish(admin.accessToken, 'library');
+        await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
+        await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+        unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
+        await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
+
+        await scan(admin.accessToken, library.id);
+        await utils.waitForQueueFinish(admin.accessToken, 'library');
+        await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
+        await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+        const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
+
+        expect(newAssets.items).toEqual([
+          expect.objectContaining({
+            originalFileName: 'glarus.nef',
+            fileCreatedAt: '2010-07-20T17:27:12.000Z',
+          }),
+        ]);
+
+        rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
+      });
+
+      it('should switch from using file.xmp to file metadata', async () => {
+        const library = await utils.createLibrary(admin.accessToken, {
+          ownerId: admin.userId,
+          importPaths: [`${testAssetDirInternal}/temp/xmp`],
+        });
+
+        cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
+        cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
+        await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
+
+        await scan(admin.accessToken, library.id);
+        await utils.waitForQueueFinish(admin.accessToken, 'library');
+        await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
+        await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+        unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
+        await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
+
+        await scan(admin.accessToken, library.id);
+        await utils.waitForQueueFinish(admin.accessToken, 'library');
+        await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
+        await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+        const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
+
+        expect(newAssets.items).toEqual([
+          expect.objectContaining({
+            originalFileName: 'glarus.nef',
+            fileCreatedAt: '2010-07-20T17:27:12.000Z',
+          }),
+        ]);
+
+        rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
+      });
+    });
   });
 
   describe('POST /libraries/:id/validate', () => {
@@ -775,6 +1014,29 @@ describe('/libraries', () => {
       });
     });
 
+    it("should fail if path isn't absolute", async () => {
+      const pathToTest = `relative/path`;
+
+      const cwd = process.cwd();
+      // Create directory in cwd
+      utils.createDirectory(`${cwd}/${pathToTest}`);
+
+      const response = await utils.validateLibrary(admin.accessToken, library.id, {
+        importPaths: [pathToTest],
+      });
+
+      utils.removeDirectory(`${cwd}/${pathToTest}`);
+
+      expect(response.importPaths?.length).toEqual(1);
+      const pathResponse = response?.importPaths?.at(0);
+
+      expect(pathResponse).toEqual({
+        importPath: pathToTest,
+        isValid: false,
+        message: expect.stringMatching('Import path must be absolute, try /usr/src/app/relative/path'),
+      });
+    });
+
     it('should fail if path is a file', async () => {
       const pathToTest = `${testAssetDirInternal}/albums/nature/el_torcal_rocks.jpg`;
 
@@ -828,7 +1090,7 @@ describe('/libraries', () => {
       });
 
       await scan(admin.accessToken, library.id);
-      await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 });
+      await utils.waitForQueueFinish(admin.accessToken, 'library');
 
       const { status, body } = await request(app)
         .delete(`/libraries/${library.id}`)
diff --git a/e2e/src/api/specs/map.e2e-spec.ts b/e2e/src/api/specs/map.e2e-spec.ts
index 343a7c91d0..da5f779cff 100644
--- a/e2e/src/api/specs/map.e2e-spec.ts
+++ b/e2e/src/api/specs/map.e2e-spec.ts
@@ -1,8 +1,7 @@
-import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
+import { LoginResponseDto } from '@immich/sdk';
 import { readFile } from 'node:fs/promises';
 import { basename, join } from 'node:path';
 import { Socket } from 'socket.io-client';
-import { createUserDto } from 'src/fixtures';
 import { errorDto } from 'src/responses';
 import { app, testAssetDir, utils } from 'src/utils';
 import request from 'supertest';
@@ -11,18 +10,13 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
 describe('/map', () => {
   let websocket: Socket;
   let admin: LoginResponseDto;
-  let nonAdmin: LoginResponseDto;
-  let asset: AssetMediaResponseDto;
 
   beforeAll(async () => {
     await utils.resetDatabase();
     admin = await utils.adminSetup({ onboarding: false });
-    nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
 
     websocket = await utils.connectWebsocket(admin.accessToken);
 
-    asset = await utils.createAsset(admin.accessToken);
-
     const files = ['formats/heic/IMG_2682.heic', 'metadata/gps-position/thompson-springs.jpg'];
     utils.resetEvents();
     const uploadFile = async (input: string) => {
@@ -103,63 +97,6 @@ describe('/map', () => {
     });
   });
 
-  describe('GET /map/style.json', () => {
-    it('should require authentication', async () => {
-      const { status, body } = await request(app).get('/map/style.json');
-      expect(status).toBe(401);
-      expect(body).toEqual(errorDto.unauthorized);
-    });
-
-    it('should allow shared link access', async () => {
-      const sharedLink = await utils.createSharedLink(admin.accessToken, {
-        type: SharedLinkType.Individual,
-        assetIds: [asset.id],
-      });
-      const { status, body } = await request(app).get(`/map/style.json?key=${sharedLink.key}`).query({ theme: 'dark' });
-
-      expect(status).toBe(200);
-      expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
-    });
-
-    it('should throw an error if a theme is not light or dark', async () => {
-      for (const theme of ['dark1', true, 123, '', null, undefined]) {
-        const { status, body } = await request(app)
-          .get('/map/style.json')
-          .query({ theme })
-          .set('Authorization', `Bearer ${admin.accessToken}`);
-        expect(status).toBe(400);
-        expect(body).toEqual(errorDto.badRequest(['theme must be one of the following values: light, dark']));
-      }
-    });
-
-    it('should return the light style.json', async () => {
-      const { status, body } = await request(app)
-        .get('/map/style.json')
-        .query({ theme: 'light' })
-        .set('Authorization', `Bearer ${admin.accessToken}`);
-      expect(status).toBe(200);
-      expect(body).toEqual(expect.objectContaining({ id: 'immich-map-light' }));
-    });
-
-    it('should return the dark style.json', async () => {
-      const { status, body } = await request(app)
-        .get('/map/style.json')
-        .query({ theme: 'dark' })
-        .set('Authorization', `Bearer ${admin.accessToken}`);
-      expect(status).toBe(200);
-      expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
-    });
-
-    it('should not require admin authentication', async () => {
-      const { status, body } = await request(app)
-        .get('/map/style.json')
-        .query({ theme: 'dark' })
-        .set('Authorization', `Bearer ${nonAdmin.accessToken}`);
-      expect(status).toBe(200);
-      expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
-    });
-  });
-
   describe('GET /map/reverse-geocode', () => {
     it('should require authentication', async () => {
       const { status, body } = await request(app).get('/map/reverse-geocode');
diff --git a/e2e/src/api/specs/oauth.e2e-spec.ts b/e2e/src/api/specs/oauth.e2e-spec.ts
index a37a9528c9..42989a118f 100644
--- a/e2e/src/api/specs/oauth.e2e-spec.ts
+++ b/e2e/src/api/specs/oauth.e2e-spec.ts
@@ -17,6 +17,8 @@ const authServer = {
   external: 'http://127.0.0.1:3000',
 };
 
+const mobileOverrideRedirectUri = 'https://photos.immich.app/oauth/mobile-redirect';
+
 const redirect = async (url: string, cookies?: string[]) => {
   const { headers } = await request(url)
     .get('/')
@@ -24,8 +26,8 @@ const redirect = async (url: string, cookies?: string[]) => {
   return { cookies: (headers['set-cookie'] as unknown as string[]) || [], location: headers.location };
 };
 
-const loginWithOAuth = async (sub: OAuthUser | string) => {
-  const { url } = await startOAuth({ oAuthConfigDto: { redirectUri: `${baseUrl}/auth/login` } });
+const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) => {
+  const { url } = await startOAuth({ oAuthConfigDto: { redirectUri: redirectUri ?? `${baseUrl}/auth/login` } });
 
   // login
   const response1 = await redirect(url.replace(authServer.internal, authServer.external));
@@ -255,4 +257,50 @@ describe(`/oauth`, () => {
       });
     });
   });
+
+  describe('mobile redirect override', () => {
+    beforeAll(async () => {
+      await setupOAuth(admin.accessToken, {
+        enabled: true,
+        clientId: OAuthClient.DEFAULT,
+        clientSecret: OAuthClient.DEFAULT,
+        buttonText: 'Login with Immich',
+        storageLabelClaim: 'immich_username',
+        mobileOverrideEnabled: true,
+        mobileRedirectUri: mobileOverrideRedirectUri,
+      });
+    });
+
+    it('should return the mobile redirect uri', async () => {
+      const { status, body } = await request(app)
+        .post('/oauth/authorize')
+        .send({ redirectUri: 'app.immich:///oauth-callback' });
+      expect(status).toBe(201);
+      expect(body).toEqual({ url: expect.stringContaining(`${authServer.internal}/auth?`) });
+
+      const params = new URL(body.url).searchParams;
+      expect(params.get('client_id')).toBe('client-default');
+      expect(params.get('response_type')).toBe('code');
+      expect(params.get('redirect_uri')).toBe(mobileOverrideRedirectUri);
+      expect(params.get('state')).toBeDefined();
+    });
+
+    it('should auto register the user by default', async () => {
+      const url = await loginWithOAuth('oauth-mobile-override', 'app.immich:///oauth-callback');
+      expect(url).toEqual(expect.stringContaining(mobileOverrideRedirectUri));
+
+      // simulate redirecting back to mobile app
+      const redirectUri = url.replace(mobileOverrideRedirectUri, 'app.immich:///oauth-callback');
+
+      const { status, body } = await request(app).post('/oauth/callback').send({ url: redirectUri });
+      expect(status).toBe(201);
+      expect(body).toMatchObject({
+        accessToken: expect.any(String),
+        isAdmin: false,
+        name: 'OAuth User',
+        userEmail: 'oauth-mobile-override@immich.app',
+        userId: expect.any(String),
+      });
+    });
+  });
 });
diff --git a/e2e/src/api/specs/search.e2e-spec.ts b/e2e/src/api/specs/search.e2e-spec.ts
index beeaf1cc01..11bb37be18 100644
--- a/e2e/src/api/specs/search.e2e-spec.ts
+++ b/e2e/src/api/specs/search.e2e-spec.ts
@@ -98,6 +98,7 @@ describe('/search', () => {
       { latitude: 31.634_16, longitude: -7.999_94 }, // marrakesh
       { latitude: 38.523_735_4, longitude: -78.488_619_4 }, // tanners ridge
       { latitude: 59.938_63, longitude: 30.314_13 }, // st. petersburg
+      { latitude: 0, longitude: 0 }, // null island
     ];
 
     const updates = coordinates.map((dto, i) =>
@@ -181,7 +182,7 @@ describe('/search', () => {
         dto: { size: -1.5 },
         expected: ['size must not be less than 1', 'size must be an integer number'],
       },
-      ...['isArchived', 'isFavorite', 'isEncoded', 'isMotion', 'isOffline', 'isVisible'].map((value) => ({
+      ...['isArchived', 'isFavorite', 'isEncoded', 'isOffline', 'isMotion', 'isVisible'].map((value) => ({
         should: `should reject ${value} not a boolean`,
         dto: { [value]: 'immich' },
         expected: [`${value} must be a boolean value`],
@@ -473,10 +474,7 @@ describe('/search', () => {
         .get('/search/explore')
         .set('Authorization', `Bearer ${admin.accessToken}`);
       expect(status).toBe(200);
-      expect(body).toEqual([
-        { fieldName: 'exifInfo.city', items: [] },
-        { fieldName: 'smartInfo.tags', items: [] },
-      ]);
+      expect(body).toEqual([{ fieldName: 'exifInfo.city', items: [] }]);
     });
   });
 
@@ -535,7 +533,7 @@ describe('/search', () => {
       expect(body).toEqual(errorDto.unauthorized);
     });
 
-    it('should get suggestions for country', async () => {
+    it('should get suggestions for country (including null)', async () => {
       const { status, body } = await request(app)
         .get('/search/suggestions?type=country&includeNull=true')
         .set('Authorization', `Bearer ${admin.accessToken}`);
@@ -558,7 +556,29 @@ describe('/search', () => {
       expect(status).toBe(200);
     });
 
-    it('should get suggestions for state', async () => {
+    it('should get suggestions for country', async () => {
+      const { status, body } = await request(app)
+        .get('/search/suggestions?type=country')
+        .set('Authorization', `Bearer ${admin.accessToken}`);
+      expect(body).toEqual([
+        'Cuba',
+        'France',
+        'Georgia',
+        'Germany',
+        'Ghana',
+        'Japan',
+        'Morocco',
+        "People's Republic of China",
+        'Russian Federation',
+        'Singapore',
+        'Spain',
+        'Switzerland',
+        'United States of America',
+      ]);
+      expect(status).toBe(200);
+    });
+
+    it('should get suggestions for state (including null)', async () => {
       const { status, body } = await request(app)
         .get('/search/suggestions?type=state&includeNull=true')
         .set('Authorization', `Bearer ${admin.accessToken}`);
@@ -582,7 +602,30 @@ describe('/search', () => {
       expect(status).toBe(200);
     });
 
-    it('should get suggestions for city', async () => {
+    it('should get suggestions for state', async () => {
+      const { status, body } = await request(app)
+        .get('/search/suggestions?type=state')
+        .set('Authorization', `Bearer ${admin.accessToken}`);
+      expect(body).toEqual([
+        'Andalusia',
+        'Berlin',
+        'Glarus',
+        'Greater Accra',
+        'Havana',
+        'Île-de-France',
+        'Marrakesh-Safi',
+        'Mississippi',
+        'New York',
+        'Shanghai',
+        'St.-Petersburg',
+        'Tbilisi',
+        'Tokyo',
+        'Virginia',
+      ]);
+      expect(status).toBe(200);
+    });
+
+    it('should get suggestions for city (including null)', async () => {
       const { status, body } = await request(app)
         .get('/search/suggestions?type=city&includeNull=true')
         .set('Authorization', `Bearer ${admin.accessToken}`);
@@ -607,7 +650,31 @@ describe('/search', () => {
       expect(status).toBe(200);
     });
 
-    it('should get suggestions for camera make', async () => {
+    it('should get suggestions for city', async () => {
+      const { status, body } = await request(app)
+        .get('/search/suggestions?type=city')
+        .set('Authorization', `Bearer ${admin.accessToken}`);
+      expect(body).toEqual([
+        'Accra',
+        'Berlin',
+        'Glarus',
+        'Havana',
+        'Marrakesh',
+        'Montalbán de Córdoba',
+        'New York City',
+        'Novena',
+        'Paris',
+        'Philadelphia',
+        'Saint Petersburg',
+        'Shanghai',
+        'Stanley',
+        'Tbilisi',
+        'Tokyo',
+      ]);
+      expect(status).toBe(200);
+    });
+
+    it('should get suggestions for camera make (including null)', async () => {
       const { status, body } = await request(app)
         .get('/search/suggestions?type=camera-make&includeNull=true')
         .set('Authorization', `Bearer ${admin.accessToken}`);
@@ -624,7 +691,23 @@ describe('/search', () => {
       expect(status).toBe(200);
     });
 
-    it('should get suggestions for camera model', async () => {
+    it('should get suggestions for camera make', async () => {
+      const { status, body } = await request(app)
+        .get('/search/suggestions?type=camera-make')
+        .set('Authorization', `Bearer ${admin.accessToken}`);
+      expect(body).toEqual([
+        'Apple',
+        'Canon',
+        'FUJIFILM',
+        'NIKON CORPORATION',
+        'PENTAX Corporation',
+        'samsung',
+        'SONY',
+      ]);
+      expect(status).toBe(200);
+    });
+
+    it('should get suggestions for camera model (including null)', async () => {
       const { status, body } = await request(app)
         .get('/search/suggestions?type=camera-model&includeNull=true')
         .set('Authorization', `Bearer ${admin.accessToken}`);
@@ -645,5 +728,26 @@ describe('/search', () => {
       ]);
       expect(status).toBe(200);
     });
+
+    it('should get suggestions for camera model', async () => {
+      const { status, body } = await request(app)
+        .get('/search/suggestions?type=camera-model')
+        .set('Authorization', `Bearer ${admin.accessToken}`);
+      expect(body).toEqual([
+        'Canon EOS 7D',
+        'Canon EOS R5',
+        'DSLR-A550',
+        'FinePix S3Pro',
+        'iPhone 7',
+        'NIKON D700',
+        'NIKON D750',
+        'NIKON D80',
+        'PENTAX K10D',
+        'SM-F711N',
+        'SM-S906U',
+        'SM-T970',
+      ]);
+      expect(status).toBe(200);
+    });
   });
 });
diff --git a/e2e/src/api/specs/server-info.e2e-spec.ts b/e2e/src/api/specs/server-info.e2e-spec.ts
deleted file mode 100644
index 571d98cda7..0000000000
--- a/e2e/src/api/specs/server-info.e2e-spec.ts
+++ /dev/null
@@ -1,202 +0,0 @@
-import { LoginResponseDto } from '@immich/sdk';
-import { createUserDto } from 'src/fixtures';
-import { errorDto } from 'src/responses';
-import { app, utils } from 'src/utils';
-import request from 'supertest';
-import { beforeAll, describe, expect, it } from 'vitest';
-
-describe('/server-info', () => {
-  let admin: LoginResponseDto;
-  let nonAdmin: LoginResponseDto;
-
-  beforeAll(async () => {
-    await utils.resetDatabase();
-    admin = await utils.adminSetup({ onboarding: false });
-    nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
-  });
-
-  describe('GET /server-info/about', () => {
-    it('should require authentication', async () => {
-      const { status, body } = await request(app).get('/server-info/about');
-      expect(status).toBe(401);
-      expect(body).toEqual(errorDto.unauthorized);
-    });
-
-    it('should return about information', async () => {
-      const { status, body } = await request(app)
-        .get('/server-info/about')
-        .set('Authorization', `Bearer ${admin.accessToken}`);
-      expect(status).toBe(200);
-      expect(body).toEqual({
-        version: expect.any(String),
-        versionUrl: expect.any(String),
-        repository: 'immich-app/immich',
-        repositoryUrl: 'https://github.com/immich-app/immich',
-        build: '1234567890',
-        buildUrl: 'https://github.com/immich-app/immich/actions/runs/1234567890',
-        buildImage: 'e2e',
-        buildImageUrl: 'https://github.com/immich-app/immich/pkgs/container/immich-server',
-        sourceRef: 'e2e',
-        sourceCommit: 'e2eeeeeeeeeeeeeeeeee',
-        sourceUrl: 'https://github.com/immich-app/immich/commit/e2eeeeeeeeeeeeeeeeee',
-        nodejs: expect.any(String),
-        ffmpeg: expect.any(String),
-        imagemagick: expect.any(String),
-        libvips: expect.any(String),
-        exiftool: expect.any(String),
-        licensed: false,
-      });
-    });
-  });
-
-  describe('GET /server-info/storage', () => {
-    it('should require authentication', async () => {
-      const { status, body } = await request(app).get('/server-info/storage');
-      expect(status).toBe(401);
-      expect(body).toEqual(errorDto.unauthorized);
-    });
-
-    it('should return the disk information', async () => {
-      const { status, body } = await request(app)
-        .get('/server-info/storage')
-        .set('Authorization', `Bearer ${admin.accessToken}`);
-      expect(status).toBe(200);
-      expect(body).toEqual({
-        diskAvailable: expect.any(String),
-        diskAvailableRaw: expect.any(Number),
-        diskSize: expect.any(String),
-        diskSizeRaw: expect.any(Number),
-        diskUsagePercentage: expect.any(Number),
-        diskUse: expect.any(String),
-        diskUseRaw: expect.any(Number),
-      });
-    });
-  });
-
-  describe('GET /server-info/ping', () => {
-    it('should respond with pong', async () => {
-      const { status, body } = await request(app).get('/server-info/ping');
-      expect(status).toBe(200);
-      expect(body).toEqual({ res: 'pong' });
-    });
-  });
-
-  describe('GET /server-info/version', () => {
-    it('should respond with the server version', async () => {
-      const { status, body } = await request(app).get('/server-info/version');
-      expect(status).toBe(200);
-      expect(body).toEqual({
-        major: expect.any(Number),
-        minor: expect.any(Number),
-        patch: expect.any(Number),
-      });
-    });
-  });
-
-  describe('GET /server-info/features', () => {
-    it('should respond with the server features', async () => {
-      const { status, body } = await request(app).get('/server-info/features');
-      expect(status).toBe(200);
-      expect(body).toEqual({
-        smartSearch: false,
-        configFile: false,
-        duplicateDetection: false,
-        facialRecognition: false,
-        importFaces: false,
-        map: true,
-        reverseGeocoding: true,
-        oauth: false,
-        oauthAutoLaunch: false,
-        passwordLogin: true,
-        search: true,
-        sidecar: true,
-        trash: true,
-        email: false,
-      });
-    });
-  });
-
-  describe('GET /server-info/config', () => {
-    it('should respond with the server configuration', async () => {
-      const { status, body } = await request(app).get('/server-info/config');
-      expect(status).toBe(200);
-      expect(body).toEqual({
-        loginPageMessage: '',
-        oauthButtonText: 'Login with OAuth',
-        trashDays: 30,
-        userDeleteDelay: 7,
-        isInitialized: true,
-        externalDomain: '',
-        isOnboarded: false,
-      });
-    });
-  });
-
-  describe('GET /server-info/statistics', () => {
-    it('should require authentication', async () => {
-      const { status, body } = await request(app).get('/server-info/statistics');
-      expect(status).toBe(401);
-      expect(body).toEqual(errorDto.unauthorized);
-    });
-
-    it('should only work for admins', async () => {
-      const { status, body } = await request(app)
-        .get('/server-info/statistics')
-        .set('Authorization', `Bearer ${nonAdmin.accessToken}`);
-      expect(status).toBe(403);
-      expect(body).toEqual(errorDto.forbidden);
-    });
-
-    it('should return the server stats', async () => {
-      const { status, body } = await request(app)
-        .get('/server-info/statistics')
-        .set('Authorization', `Bearer ${admin.accessToken}`);
-      expect(status).toBe(200);
-      expect(body).toEqual({
-        photos: 0,
-        usage: 0,
-        usageByUser: [
-          {
-            quotaSizeInBytes: null,
-            photos: 0,
-            usage: 0,
-            userName: 'Immich Admin',
-            userId: admin.userId,
-            videos: 0,
-          },
-          {
-            quotaSizeInBytes: null,
-            photos: 0,
-            usage: 0,
-            userName: 'User 1',
-            userId: nonAdmin.userId,
-            videos: 0,
-          },
-        ],
-        videos: 0,
-      });
-    });
-  });
-
-  describe('GET /server-info/media-types', () => {
-    it('should return accepted media types', async () => {
-      const { status, body } = await request(app).get('/server-info/media-types');
-      expect(status).toBe(200);
-      expect(body).toEqual({
-        sidecar: ['.xmp'],
-        image: expect.any(Array),
-        video: expect.any(Array),
-      });
-    });
-  });
-
-  describe('GET /server-info/theme', () => {
-    it('should respond with the server theme', async () => {
-      const { status, body } = await request(app).get('/server-info/theme');
-      expect(status).toBe(200);
-      expect(body).toEqual({
-        customCss: '',
-      });
-    });
-  });
-});
diff --git a/e2e/src/api/specs/server.e2e-spec.ts b/e2e/src/api/specs/server.e2e-spec.ts
index b19e6d85c4..c89280f579 100644
--- a/e2e/src/api/specs/server.e2e-spec.ts
+++ b/e2e/src/api/specs/server.e2e-spec.ts
@@ -133,7 +133,10 @@ describe('/server', () => {
         userDeleteDelay: 7,
         isInitialized: true,
         externalDomain: '',
+        publicUsers: true,
         isOnboarded: false,
+        mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
+        mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
       });
     });
   });
@@ -161,11 +164,15 @@ describe('/server', () => {
       expect(body).toEqual({
         photos: 0,
         usage: 0,
+        usagePhotos: 0,
+        usageVideos: 0,
         usageByUser: [
           {
             quotaSizeInBytes: null,
             photos: 0,
             usage: 0,
+            usagePhotos: 0,
+            usageVideos: 0,
             userName: 'Immich Admin',
             userId: admin.userId,
             videos: 0,
@@ -174,6 +181,8 @@ describe('/server', () => {
             quotaSizeInBytes: null,
             photos: 0,
             usage: 0,
+            usagePhotos: 0,
+            usageVideos: 0,
             userName: 'User 1',
             userId: nonAdmin.userId,
             videos: 0,
diff --git a/e2e/src/api/specs/trash.e2e-spec.ts b/e2e/src/api/specs/trash.e2e-spec.ts
index 0c28a72825..f86f38ab61 100644
--- a/e2e/src/api/specs/trash.e2e-spec.ts
+++ b/e2e/src/api/specs/trash.e2e-spec.ts
@@ -1,10 +1,13 @@
-import { LoginResponseDto, getAssetInfo, getAssetStatistics } from '@immich/sdk';
+import { LoginResponseDto, getAssetInfo, getAssetStatistics, scanLibrary } from '@immich/sdk';
+import { existsSync } from 'node:fs';
 import { Socket } from 'socket.io-client';
 import { errorDto } from 'src/responses';
-import { app, asBearerAuth, utils } from 'src/utils';
+import { app, asBearerAuth, testAssetDir, testAssetDirInternal, utils } from 'src/utils';
 import request from 'supertest';
 import { afterAll, beforeAll, describe, expect, it } from 'vitest';
 
+const scan = async (accessToken: string, id: string) => scanLibrary({ id }, { headers: asBearerAuth(accessToken) });
+
 describe('/trash', () => {
   let admin: LoginResponseDto;
   let ws: Socket;
@@ -34,13 +37,18 @@ describe('/trash', () => {
       const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
       expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
 
-      const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
-      expect(status).toBe(204);
+      const { status, body } = await request(app)
+        .post('/trash/empty')
+        .set('Authorization', `Bearer ${admin.accessToken}`);
+      expect(status).toBe(200);
+      expect(body).toEqual({ count: 1 });
 
       await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
 
       const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) });
       expect(after.total).toBe(0);
+
+      expect(existsSync(before.originalPath)).toBe(false);
     });
 
     it('should empty the trash with archived assets', async () => {
@@ -51,13 +59,56 @@ describe('/trash', () => {
       const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
       expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true, isArchived: true }));
 
-      const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
-      expect(status).toBe(204);
+      const { status, body } = await request(app)
+        .post('/trash/empty')
+        .set('Authorization', `Bearer ${admin.accessToken}`);
+      expect(status).toBe(200);
+      expect(body).toEqual({ count: 1 });
 
       await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
 
       const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) });
       expect(after.total).toBe(0);
+
+      expect(existsSync(before.originalPath)).toBe(false);
+    });
+
+    it('should not delete offline-trashed assets from disk', async () => {
+      const library = await utils.createLibrary(admin.accessToken, {
+        ownerId: admin.userId,
+        importPaths: [`${testAssetDirInternal}/temp/offline`],
+      });
+
+      utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
+
+      await scan(admin.accessToken, library.id);
+      await utils.waitForQueueFinish(admin.accessToken, 'library');
+
+      const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
+      expect(assets.items.length).toBe(1);
+      const asset = assets.items[0];
+
+      utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
+
+      await scan(admin.accessToken, library.id);
+      await utils.waitForQueueFinish(admin.accessToken, 'library');
+
+      const assetBefore = await utils.getAssetInfo(admin.accessToken, asset.id);
+      expect(assetBefore).toMatchObject({ isTrashed: true, isOffline: true });
+
+      utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
+
+      const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
+      expect(status).toBe(200);
+
+      await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask');
+
+      const assetAfter = await utils.getAssetInfo(admin.accessToken, asset.id);
+      expect(assetAfter).toMatchObject({ isTrashed: true, isOffline: true });
+
+      expect(existsSync(`${testAssetDir}/temp/offline/offline.png`)).toBe(true);
+
+      utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
     });
   });
 
@@ -76,12 +127,46 @@ describe('/trash', () => {
       const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
       expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
 
-      const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`);
-      expect(status).toBe(204);
+      const { status, body } = await request(app)
+        .post('/trash/restore')
+        .set('Authorization', `Bearer ${admin.accessToken}`);
+      expect(status).toBe(200);
+      expect(body).toEqual({ count: 1 });
 
       const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
       expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: false }));
     });
+
+    it('should not restore offline-trashed assets', async () => {
+      const library = await utils.createLibrary(admin.accessToken, {
+        ownerId: admin.userId,
+        importPaths: [`${testAssetDirInternal}/temp/offline`],
+      });
+
+      utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
+
+      await scan(admin.accessToken, library.id);
+      await utils.waitForQueueFinish(admin.accessToken, 'library');
+
+      const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
+      expect(assets.count).toBe(1);
+      const assetId = assets.items[0].id;
+
+      utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
+
+      await scan(admin.accessToken, library.id);
+
+      await utils.waitForQueueFinish(admin.accessToken, 'library');
+
+      const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
+      expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true }));
+
+      const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`);
+      expect(status).toBe(200);
+
+      const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
+      expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true }));
+    });
   });
 
   describe('POST /trash/restore/assets', () => {
@@ -99,14 +184,48 @@ describe('/trash', () => {
       const before = await utils.getAssetInfo(admin.accessToken, assetId);
       expect(before.isTrashed).toBe(true);
 
-      const { status } = await request(app)
+      const { status, body } = await request(app)
         .post('/trash/restore/assets')
         .set('Authorization', `Bearer ${admin.accessToken}`)
         .send({ ids: [assetId] });
-      expect(status).toBe(204);
+      expect(status).toBe(200);
+      expect(body).toEqual({ count: 1 });
 
       const after = await utils.getAssetInfo(admin.accessToken, assetId);
       expect(after.isTrashed).toBe(false);
     });
+
+    it('should not restore an offline-trashed asset', async () => {
+      const library = await utils.createLibrary(admin.accessToken, {
+        ownerId: admin.userId,
+        importPaths: [`${testAssetDirInternal}/temp/offline`],
+      });
+
+      utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
+
+      await scan(admin.accessToken, library.id);
+      await utils.waitForQueueFinish(admin.accessToken, 'library');
+
+      const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
+      expect(assets.count).toBe(1);
+      const assetId = assets.items[0].id;
+
+      utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
+
+      await scan(admin.accessToken, library.id);
+      await utils.waitForQueueFinish(admin.accessToken, 'library');
+
+      const before = await utils.getAssetInfo(admin.accessToken, assetId);
+      expect(before.isTrashed).toBe(true);
+
+      const { status } = await request(app)
+        .post('/trash/restore/assets')
+        .set('Authorization', `Bearer ${admin.accessToken}`)
+        .send({ ids: [assetId] });
+      expect(status).toBe(200);
+
+      const after = await utils.getAssetInfo(admin.accessToken, assetId);
+      expect(after.isTrashed).toBe(true);
+    });
   });
 });
diff --git a/e2e/src/cli/specs/upload.e2e-spec.ts b/e2e/src/cli/specs/upload.e2e-spec.ts
index db2b6c5341..301c6d3bf0 100644
--- a/e2e/src/cli/specs/upload.e2e-spec.ts
+++ b/e2e/src/cli/specs/upload.e2e-spec.ts
@@ -1,9 +1,90 @@
 import { LoginResponseDto, getAllAlbums, getAssetStatistics } from '@immich/sdk';
-import { readFileSync } from 'node:fs';
+import { cpSync, readFileSync } from 'node:fs';
 import { mkdir, readdir, rm, symlink } from 'node:fs/promises';
-import { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils';
+import { asKeyAuth, immichCli, specialCharStrings, testAssetDir, utils } from 'src/utils';
 import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
 
+interface Test {
+  test: string;
+  paths: string[];
+  files: Record<string, boolean>;
+}
+
+const tests: Test[] = [
+  {
+    test: 'should support globbing with *',
+    paths: [`/photos*`],
+    files: {
+      '/photos1/image1.jpg': true,
+      '/photos2/image2.jpg': true,
+      '/images/image3.jpg': false,
+    },
+  },
+  {
+    test: 'should support paths with an asterisk',
+    paths: [`/photos\*/image1.jpg`],
+    files: {
+      '/photos*/image1.jpg': true,
+      '/photos*/image2.jpg': false,
+      '/images/image3.jpg': false,
+    },
+  },
+  {
+    test: 'should support paths with a space',
+    paths: [`/my photos/image1.jpg`],
+    files: {
+      '/my photos/image1.jpg': true,
+      '/my photos/image2.jpg': false,
+      '/images/image3.jpg': false,
+    },
+  },
+  {
+    test: 'should support paths with a single quote',
+    paths: [`/photos\'/image1.jpg`],
+    files: {
+      "/photos'/image1.jpg": true,
+      "/photos'/image2.jpg": false,
+      '/images/image3.jpg': false,
+    },
+  },
+  {
+    test: 'should support paths with a double quote',
+    paths: [`/photos\"/image1.jpg`],
+    files: {
+      '/photos"/image1.jpg': true,
+      '/photos"/image2.jpg': false,
+      '/images/image3.jpg': false,
+    },
+  },
+  {
+    test: 'should support paths with a comma',
+    paths: [`/photos, eh/image1.jpg`],
+    files: {
+      '/photos, eh/image1.jpg': true,
+      '/photos, eh/image2.jpg': false,
+      '/images/image3.jpg': false,
+    },
+  },
+  {
+    test: 'should support paths with an opening brace',
+    paths: [`/photos\{/image1.jpg`],
+    files: {
+      '/photos{/image1.jpg': true,
+      '/photos{/image2.jpg': false,
+      '/images/image3.jpg': false,
+    },
+  },
+  {
+    test: 'should support paths with a closing brace',
+    paths: [`/photos\}/image1.jpg`],
+    files: {
+      '/photos}/image1.jpg': true,
+      '/photos}/image2.jpg': false,
+      '/images/image3.jpg': false,
+    },
+  },
+];
+
 describe(`immich upload`, () => {
   let admin: LoginResponseDto;
   let key: string;
@@ -22,7 +103,7 @@ describe(`immich upload`, () => {
   describe(`immich upload /path/to/file.jpg`, () => {
     it('should upload a single file', async () => {
       const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
-      expect(stderr).toBe('');
+      expect(stderr).toContain('{message}');
       expect(stdout.split('\n')).toEqual(
         expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 new asset')]),
       );
@@ -32,9 +113,63 @@ describe(`immich upload`, () => {
       expect(assets.total).toBe(1);
     });
 
+    describe(`should accept special cases`, () => {
+      for (const { test, paths, files } of tests) {
+        it(test, async () => {
+          const baseDir = `/tmp/upload/`;
+
+          const testPaths = Object.keys(files).map((filePath) => `${baseDir}/${filePath}`);
+          testPaths.map((filePath) => utils.createImageFile(filePath));
+
+          const commandLine = paths.map((argument) => `${baseDir}/${argument}`);
+
+          const expectedCount = Object.entries(files).filter((entry) => entry[1]).length;
+
+          const { stderr, stdout, exitCode } = await immichCli(['upload', ...commandLine]);
+          expect(stderr).toContain('{message}');
+          expect(stdout.split('\n')).toEqual(
+            expect.arrayContaining([expect.stringContaining(`Successfully uploaded ${expectedCount} new asset`)]),
+          );
+          expect(exitCode).toBe(0);
+
+          const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
+          expect(assets.total).toBe(expectedCount);
+
+          testPaths.map((filePath) => utils.removeImageFile(filePath));
+        });
+      }
+    });
+
+    it.each(specialCharStrings)(`should upload a multiple files from paths containing %s`, async (testString) => {
+      // https://github.com/immich-app/immich/issues/12078
+
+      // NOTE: this test must contain more than one path since a related bug is only triggered with multiple paths
+
+      const testPaths = [
+        `${testAssetDir}/temp/dir1${testString}name/asset.jpg`,
+        `${testAssetDir}/temp/dir2${testString}name/asset.jpg`,
+      ];
+
+      cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, testPaths[0]);
+      cpSync(`${testAssetDir}/albums/nature/silver_fir.jpg`, testPaths[1]);
+
+      const { stderr, stdout, exitCode } = await immichCli(['upload', ...testPaths]);
+      expect(stderr).toContain('{message}');
+      expect(stdout.split('\n')).toEqual(
+        expect.arrayContaining([expect.stringContaining('Successfully uploaded 2 new assets')]),
+      );
+      expect(exitCode).toBe(0);
+
+      utils.removeImageFile(testPaths[0]);
+      utils.removeImageFile(testPaths[1]);
+
+      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
+      expect(assets.total).toBe(2);
+    });
+
     it('should skip a duplicate file', async () => {
       const first = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
-      expect(first.stderr).toBe('');
+      expect(first.stderr).toContain('{message}');
       expect(first.stdout.split('\n')).toEqual(
         expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 new asset')]),
       );
@@ -44,7 +179,7 @@ describe(`immich upload`, () => {
       expect(assets.total).toBe(1);
 
       const second = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
-      expect(second.stderr).toBe('');
+      expect(second.stderr).toContain('{message}');
       expect(second.stdout.split('\n')).toEqual(
         expect.arrayContaining([
           expect.stringContaining('Found 0 new files and 1 duplicate'),
@@ -70,7 +205,7 @@ describe(`immich upload`, () => {
         `${testAssetDir}/albums/nature/silver_fir.jpg`,
         '--dry-run',
       ]);
-      expect(stderr).toBe('');
+      expect(stderr).toContain('{message}');
       expect(stdout.split('\n')).toEqual(
         expect.arrayContaining([expect.stringContaining('Would have uploaded 1 asset')]),
       );
@@ -82,7 +217,7 @@ describe(`immich upload`, () => {
 
     it('dry run should handle duplicates', async () => {
       const first = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
-      expect(first.stderr).toBe('');
+      expect(first.stderr).toContain('{message}');
       expect(first.stdout.split('\n')).toEqual(
         expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 new asset')]),
       );
@@ -92,7 +227,7 @@ describe(`immich upload`, () => {
       expect(assets.total).toBe(1);
 
       const second = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--dry-run']);
-      expect(second.stderr).toBe('');
+      expect(second.stderr).toContain('{message}');
       expect(second.stdout.split('\n')).toEqual(
         expect.arrayContaining([
           expect.stringContaining('Found 8 new files and 1 duplicate'),
@@ -106,7 +241,7 @@ describe(`immich upload`, () => {
   describe('immich upload --recursive', () => {
     it('should upload a folder recursively', async () => {
       const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']);
-      expect(stderr).toBe('');
+      expect(stderr).toContain('{message}');
       expect(stdout.split('\n')).toEqual(
         expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 new assets')]),
       );
@@ -132,7 +267,7 @@ describe(`immich upload`, () => {
           expect.stringContaining('Successfully updated 9 assets'),
         ]),
       );
-      expect(stderr).toBe('');
+      expect(stderr).toContain('{message}');
       expect(exitCode).toBe(0);
 
       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
@@ -148,7 +283,7 @@ describe(`immich upload`, () => {
       expect(response1.stdout.split('\n')).toEqual(
         expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 new assets')]),
       );
-      expect(response1.stderr).toBe('');
+      expect(response1.stderr).toContain('{message}');
       expect(response1.exitCode).toBe(0);
 
       const assets1 = await getAssetStatistics({}, { headers: asKeyAuth(key) });
@@ -164,7 +299,7 @@ describe(`immich upload`, () => {
           expect.stringContaining('Successfully updated 9 assets'),
         ]),
       );
-      expect(response2.stderr).toBe('');
+      expect(response2.stderr).toContain('{message}');
       expect(response2.exitCode).toBe(0);
 
       const assets2 = await getAssetStatistics({}, { headers: asKeyAuth(key) });
@@ -190,7 +325,7 @@ describe(`immich upload`, () => {
           expect.stringContaining('Would have updated albums of 9 assets'),
         ]),
       );
-      expect(stderr).toBe('');
+      expect(stderr).toContain('{message}');
       expect(exitCode).toBe(0);
 
       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
@@ -216,7 +351,7 @@ describe(`immich upload`, () => {
           expect.stringContaining('Successfully updated 9 assets'),
         ]),
       );
-      expect(stderr).toBe('');
+      expect(stderr).toContain('{message}');
       expect(exitCode).toBe(0);
 
       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
@@ -242,7 +377,7 @@ describe(`immich upload`, () => {
           expect.stringContaining('Would have updated albums of 9 assets'),
         ]),
       );
-      expect(stderr).toBe('');
+      expect(stderr).toContain('{message}');
       expect(exitCode).toBe(0);
 
       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
@@ -273,7 +408,7 @@ describe(`immich upload`, () => {
           expect.stringContaining('Deleting assets that have been uploaded'),
         ]),
       );
-      expect(stderr).toBe('');
+      expect(stderr).toContain('{message}');
       expect(exitCode).toBe(0);
 
       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
@@ -299,7 +434,7 @@ describe(`immich upload`, () => {
           expect.stringContaining('Would have deleted 9 local assets'),
         ]),
       );
-      expect(stderr).toBe('');
+      expect(stderr).toContain('{message}');
       expect(exitCode).toBe(0);
 
       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
@@ -358,7 +493,7 @@ describe(`immich upload`, () => {
         '2',
       ]);
 
-      expect(stderr).toBe('');
+      expect(stderr).toContain('{message}');
       expect(stdout.split('\n')).toEqual(
         expect.arrayContaining([
           'Found 9 new files and 0 duplicates',
@@ -399,7 +534,7 @@ describe(`immich upload`, () => {
         'silver_fir.jpg',
       ]);
 
-      expect(stderr).toBe('');
+      expect(stderr).toContain('{message}');
       expect(stdout.split('\n')).toEqual(
         expect.arrayContaining([
           'Found 8 new files and 0 duplicates',
@@ -420,7 +555,7 @@ describe(`immich upload`, () => {
         '!(*_*_*).jpg',
       ]);
 
-      expect(stderr).toBe('');
+      expect(stderr).toContain('{message}');
       expect(stdout.split('\n')).toEqual(
         expect.arrayContaining([
           'Found 1 new files and 0 duplicates',
@@ -442,7 +577,7 @@ describe(`immich upload`, () => {
         '--dry-run',
       ]);
 
-      expect(stderr).toBe('');
+      expect(stderr).toContain('{message}');
       expect(stdout.split('\n')).toEqual(
         expect.arrayContaining([
           'Found 8 new files and 0 duplicates',
diff --git a/e2e/src/immich-admin/specs/immich-admin.e2e-spec.ts b/e2e/src/immich-admin/specs/immich-admin.e2e-spec.ts
index d025b7a338..cf0558883a 100644
--- a/e2e/src/immich-admin/specs/immich-admin.e2e-spec.ts
+++ b/e2e/src/immich-admin/specs/immich-admin.e2e-spec.ts
@@ -9,9 +9,11 @@ describe(`immich-admin`, () => {
 
   describe('list-users', () => {
     it('should list the admin user', async () => {
-      const { stdout, stderr, exitCode } = await immichAdmin(['list-users']).promise;
+      const { stdout, exitCode } = await immichAdmin(['list-users']).promise;
       expect(exitCode).toBe(0);
-      expect(stderr).toBe('');
+
+      // TODO: Vitest needs upgrade to Node 22.x to fix the failed check
+      // expect(stderr).toBe('');
       expect(stdout).toContain("email: 'admin@immich.cloud'");
       expect(stdout).toContain("name: 'Immich Admin'");
     });
@@ -29,9 +31,10 @@ describe(`immich-admin`, () => {
         }
       });
 
-      const { stderr, stdout, exitCode } = await promise;
+      const { stdout, exitCode } = await promise;
       expect(exitCode).toBe(0);
-      expect(stderr).toBe('');
+      // TODO: Vitest needs upgrade to Node 22.x to fix the failed check
+      // expect(stderr).toBe('');
       expect(stdout).toContain('The admin password has been updated to:');
     });
   });
diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts
index 6ca2225180..0148f2e1e9 100644
--- a/e2e/src/responses.ts
+++ b/e2e/src/responses.ts
@@ -94,6 +94,7 @@ export const signupResponseDto = {
     quotaSizeInBytes: null,
     status: 'active',
     license: null,
+    profileChangedAt: expect.any(String),
   },
 };
 
diff --git a/e2e/src/setup/auth-server.ts b/e2e/src/setup/auth-server.ts
index 3dd63fc403..cde50813dd 100644
--- a/e2e/src/setup/auth-server.ts
+++ b/e2e/src/setup/auth-server.ts
@@ -50,6 +50,7 @@ const getClaims = (sub: string) => claims.find((user) => user.sub === sub) || wi
 const setup = async () => {
   const { privateKey, publicKey } = await generateKeyPair('RS256');
 
+  const redirectUris = ['http://127.0.0.1:2285/auth/login', 'https://photos.immich.app/oauth/mobile-redirect'];
   const port = 3000;
   const host = '0.0.0.0';
   const oidc = new Provider(`http://${host}:${port}`, {
@@ -86,14 +87,14 @@ const setup = async () => {
       {
         client_id: OAuthClient.DEFAULT,
         client_secret: OAuthClient.DEFAULT,
-        redirect_uris: ['http://127.0.0.1:2285/auth/login'],
+        redirect_uris: redirectUris,
         grant_types: ['authorization_code'],
         response_types: ['code'],
       },
       {
         client_id: OAuthClient.RS256_TOKENS,
         client_secret: OAuthClient.RS256_TOKENS,
-        redirect_uris: ['http://127.0.0.1:2285/auth/login'],
+        redirect_uris: redirectUris,
         grant_types: ['authorization_code'],
         id_token_signed_response_alg: 'RS256',
         jwks: { keys: [await exportJWK(publicKey)] },
@@ -101,7 +102,7 @@ const setup = async () => {
       {
         client_id: OAuthClient.RS256_PROFILE,
         client_secret: OAuthClient.RS256_PROFILE,
-        redirect_uris: ['http://127.0.0.1:2285/auth/login'],
+        redirect_uris: redirectUris,
         grant_types: ['authorization_code'],
         userinfo_signed_response_alg: 'RS256',
         jwks: { keys: [await exportJWK(publicKey)] },
diff --git a/e2e/src/setup/docker-compose.ts b/e2e/src/setup/docker-compose.ts
index 3ae87417a2..49a702e776 100644
--- a/e2e/src/setup/docker-compose.ts
+++ b/e2e/src/setup/docker-compose.ts
@@ -12,7 +12,8 @@ const setup = async () => {
 
   const timeout = setTimeout(() => _reject(new Error('Timeout starting e2e environment')), 60_000);
 
-  const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' });
+  const command = 'compose up --build --renew-anon-volumes --force-recreate --remove-orphans';
+  const child = spawn('docker', command.split(' '), { stdio: 'pipe' });
 
   child.stdout.on('data', (data) => {
     const input = data.toString();
diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts
index c67e569697..14225ff063 100644
--- a/e2e/src/utils.ts
+++ b/e2e/src/utils.ts
@@ -11,6 +11,7 @@ import {
   PersonCreateDto,
   SharedLinkCreateDto,
   UserAdminCreateDto,
+  UserPreferencesUpdateDto,
   ValidateLibraryDto,
   checkExistingAssets,
   createAlbum,
@@ -19,19 +20,23 @@ import {
   createPartner,
   createPerson,
   createSharedLink,
+  createStack,
   createUserAdmin,
   deleteAssets,
   getAllJobsStatus,
   getAssetInfo,
   getConfigDefaults,
   login,
-  searchMetadata,
+  searchAssets,
   setBaseUrl,
   signUpAdmin,
+  tagAssets,
   updateAdminOnboarding,
   updateAlbumUser,
   updateAssets,
   updateConfig,
+  updateMyPreferences,
+  upsertTags,
   validate,
 } from '@immich/sdk';
 import { BrowserContext } from '@playwright/test';
@@ -68,6 +73,7 @@ export const immichCli = (args: string[]) =>
   executeCommand('node', ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]).promise;
 export const immichAdmin = (args: string[]) =>
   executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
+export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
 
 const executeCommand = (command: string, args: string[]) => {
   let _resolve: (value: CommandResponse) => void;
@@ -156,8 +162,7 @@ export const utils = {
 
       for (const table of tables) {
         if (table === 'system_metadata') {
-          // prevent reverse geocoder from being re-initialized
-          sql.push(`DELETE FROM "system_metadata" where "key" != 'reverse-geocoding-state';`);
+          sql.push(`DELETE FROM "system_metadata" where "key" NOT IN ('reverse-geocoding-state', 'system-flags');`);
         } else {
           sql.push(`DELETE FROM ${table} CASCADE;`);
         }
@@ -373,6 +378,12 @@ export const utils = {
     writeFileSync(path, makeRandomImage());
   },
 
+  createDirectory: (path: string) => {
+    if (!existsSync(path)) {
+      mkdirSync(path, { recursive: true });
+    }
+  },
+
   removeImageFile: (path: string) => {
     if (!existsSync(path)) {
       return;
@@ -381,13 +392,21 @@ export const utils = {
     rmSync(path);
   },
 
+  removeDirectory: (path: string) => {
+    if (!existsSync(path)) {
+      return;
+    }
+
+    rmSync(path, { recursive: true });
+  },
+
   getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
 
   checkExistingAssets: (accessToken: string, checkExistingAssetsDto: CheckExistingAssetsDto) =>
     checkExistingAssets({ checkExistingAssetsDto }, { headers: asBearerAuth(accessToken) }),
 
-  metadataSearch: async (accessToken: string, dto: MetadataSearchDto) => {
-    return searchMetadata({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) });
+  searchAssets: async (accessToken: string, dto: MetadataSearchDto) => {
+    return searchAssets({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) });
   },
 
   archiveAssets: (accessToken: string, ids: string[]) =>
@@ -430,6 +449,18 @@ export const utils = {
 
   createPartner: (accessToken: string, id: string) => createPartner({ id }, { headers: asBearerAuth(accessToken) }),
 
+  updateMyPreferences: (accessToken: string, userPreferencesUpdateDto: UserPreferencesUpdateDto) =>
+    updateMyPreferences({ userPreferencesUpdateDto }, { headers: asBearerAuth(accessToken) }),
+
+  createStack: (accessToken: string, assetIds: string[]) =>
+    createStack({ stackCreateDto: { assetIds } }, { headers: asBearerAuth(accessToken) }),
+
+  upsertTags: (accessToken: string, tags: string[]) =>
+    upsertTags({ tagUpsertDto: { tags } }, { headers: asBearerAuth(accessToken) }),
+
+  tagAssets: (accessToken: string, tagId: string, assetIds: string[]) =>
+    tagAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }, { headers: asBearerAuth(accessToken) }),
+
   setAuthCookies: async (context: BrowserContext, accessToken: string, domain = '127.0.0.1') =>
     await context.addCookies([
       {
diff --git a/e2e/src/web/specs/asset-viewer/stack.e2e-spec.ts b/e2e/src/web/specs/asset-viewer/stack.e2e-spec.ts
new file mode 100644
index 0000000000..cb40f82c0a
--- /dev/null
+++ b/e2e/src/web/specs/asset-viewer/stack.e2e-spec.ts
@@ -0,0 +1,66 @@
+import { AssetMediaResponseDto, LoginResponseDto } from '@immich/sdk';
+import { expect, Page, test } from '@playwright/test';
+import { utils } from 'src/utils';
+
+async function ensureDetailPanelVisible(page: Page) {
+  await page.waitForSelector('#immich-asset-viewer');
+
+  const isVisible = await page.locator('#detail-panel').isVisible();
+  if (!isVisible) {
+    await page.keyboard.press('i');
+    await page.waitForSelector('#detail-panel');
+  }
+}
+
+test.describe('Asset Viewer stack', () => {
+  let admin: LoginResponseDto;
+  let assetOne: AssetMediaResponseDto;
+  let assetTwo: AssetMediaResponseDto;
+
+  test.beforeAll(async () => {
+    utils.initSdk();
+    await utils.resetDatabase();
+    admin = await utils.adminSetup();
+    await utils.updateMyPreferences(admin.accessToken, { tags: { enabled: true } });
+
+    assetOne = await utils.createAsset(admin.accessToken);
+    assetTwo = await utils.createAsset(admin.accessToken);
+    await utils.createStack(admin.accessToken, [assetOne.id, assetTwo.id]);
+
+    const tags = await utils.upsertTags(admin.accessToken, ['test/1', 'test/2']);
+    const tagOne = tags.find((tag) => tag.value === 'test/1')!;
+    const tagTwo = tags.find((tag) => tag.value === 'test/2')!;
+    await utils.tagAssets(admin.accessToken, tagOne.id, [assetOne.id]);
+    await utils.tagAssets(admin.accessToken, tagTwo.id, [assetTwo.id]);
+  });
+
+  test('stack slideshow is visible', async ({ page, context }) => {
+    await utils.setAuthCookies(context, admin.accessToken);
+    await page.goto(`/photos/${assetOne.id}`);
+
+    const stackAssets = page.locator('#stack-slideshow [data-asset]');
+    await expect(stackAssets.first()).toBeVisible();
+    await expect(stackAssets.nth(1)).toBeVisible();
+  });
+
+  test('tags of primary asset are visible', async ({ page, context }) => {
+    await utils.setAuthCookies(context, admin.accessToken);
+    await page.goto(`/photos/${assetOne.id}`);
+    await ensureDetailPanelVisible(page);
+
+    const tags = page.getByTestId('detail-panel-tags').getByRole('link');
+    await expect(tags.first()).toHaveText('test/1');
+  });
+
+  test('tags of second asset are visible', async ({ page, context }) => {
+    await utils.setAuthCookies(context, admin.accessToken);
+    await page.goto(`/photos/${assetOne.id}`);
+    await ensureDetailPanelVisible(page);
+
+    const stackAssets = page.locator('#stack-slideshow [data-asset]');
+    await stackAssets.nth(1).click();
+
+    const tags = page.getByTestId('detail-panel-tags').getByRole('link');
+    await expect(tags.first()).toHaveText('test/2');
+  });
+});
diff --git a/e2e/test-assets b/e2e/test-assets
index 3e057d2f58..9e3b964b08 160000
--- a/e2e/test-assets
+++ b/e2e/test-assets
@@ -1 +1 @@
-Subproject commit 3e057d2f58750acdf7ff281a3938e34a86cfef4d
+Subproject commit 9e3b964b080dca6f035b29b86e66454ae8aeda78
diff --git a/i18n/af.json b/i18n/af.json
new file mode 100644
index 0000000000..ede1a745eb
--- /dev/null
+++ b/i18n/af.json
@@ -0,0 +1,57 @@
+{
+  "about": "Verfris",
+  "account": "Rekening",
+  "account_settings": "Rekeninginstellings",
+  "acknowledge": "Erken",
+  "action": "Aksie",
+  "actions": "Aksies",
+  "active": "Aktief",
+  "activity": "Aktiwiteite",
+  "activity_changed": "Aktiwiteit is {enabled, select, true {aangeskakel} other {afgeskakel}}",
+  "add": "Voegby",
+  "add_a_description": "Voeg 'n beskrywing by",
+  "add_a_location": "Voeg 'n ligging by",
+  "add_a_name": "Voeg 'n naam by",
+  "add_a_title": "Voeg 'n titel by",
+  "add_exclusion_pattern": "Voeg uitsgluitingspatrone by",
+  "add_import_path": "Voeg invoerpad by",
+  "add_location": "Voeg ligging by",
+  "add_more_users": "Voeg meer gebruikers by",
+  "add_partner": "Voeg vennoot by",
+  "add_path": "Voeg pad by",
+  "add_photos": "Voeg foto's by",
+  "add_to": "Voeg na...",
+  "add_to_album": "Voeg na album",
+  "add_to_shared_album": "Voeg na gedeelde album",
+  "added_to_archive": "By argief gevoeg",
+  "added_to_favorites": "By gunstelinge gevoeg",
+  "added_to_favorites_count": "Het {count, number} by gunstelinge gevoeg",
+  "admin": {
+    "add_exclusion_pattern_description": "Voeg uitsluitingspatrone by. Globbing met *, ** en ? word ondersteun. Om alle lêers in enige lêergids genaamd \"Raw\" te ignoreer, gebruik \"**/Raw/**\". Om alle lêers wat op \".tif\" eindig, te ignoreer, gebruik \"**/*.tif\". Om 'n absolute pad te ignoreer, gebruik \"/path/to/ignore/**\".",
+    "asset_offline_description": "Hierdie eksterne biblioteekbate word nie meer op skyf gevind nie en is na die asblik geskuif. As die lêer binne die biblioteek geskuif is, gaan jou tydlyn na vir die nuwe ooreenstemmende bate. Om hierdie bate te herstel, maak asseblief seker dat die lêerpad hieronder deur Immich verkry kan word en skandeer die biblioteek.",
+    "authentication_settings": "Verifikasie instellings",
+    "authentication_settings_description": "Bestuur wagwoord, OAuth en ander verifikasie instellings",
+    "authentication_settings_disable_all": "Is jy seker jy wil alle aanmeldmetodes deaktiveer? Aanmelding sal heeltemal gedeaktiveer word.",
+    "authentication_settings_reenable": "Om te heraktiveer, gebruik 'n <link>Server Command</link>.",
+    "background_task_job": "Agtergrondtake",
+    "backup_database": "Rugsteun databasis",
+    "backup_database_enable_description": "Aktiveer databasisrugsteun",
+    "backup_keep_last_amount": "Aantal vorige rugsteune om te hou",
+    "backup_settings": "Rugsteun instellings",
+    "backup_settings_description": "Bestuur databasis rugsteun instellings",
+    "check_all": "Kies Alles",
+    "cleared_jobs": "Poste gevee vir: {job}",
+    "config_set_by_file": "Config word tans deur 'n konfigurasielêer gestel",
+    "confirm_delete_library": "Is jy seker jy wil {library}-biblioteek uitvee?",
+    "confirm_delete_library_assets": "Is jy seker jy wil hierdie biblioteek uitvee? Dit sal {count, plural, one {# bevatte base} other {# bevatte bates}} uit Immich uitvee en kan nie ongedaan gemaak word nie. Lêers sal op skyf bly.",
+    "confirm_email_below": "Om te bevestig, tik \"{email}\" hieronder",
+    "confirm_reprocess_all_faces": "Is jy seker jy wil alle gesigte herverwerk? Dit sal ook genoemde mense skoonmaak.",
+    "confirm_user_password_reset": "Is jy seker jy wil {user} se wagwoord terugstel?",
+    "create_job": "Skep werk",
+    "cron_expression": "Cron uitdrukking",
+    "cron_expression_description": "Stel die skanderingsinterval in met die cron-formaat. Vir meer inligting verwys asseblief na bv. <link>Crontab Guru</link>",
+    "cron_expression_presets": "Cron uitdrukking voorafinstellings",
+    "disable_login": "Deaktiveer aanmelding",
+    "duplicate_detection_job_description": "Begin masjienleer op bates om soortgelyke beelde op te spoor. Maak staat op Smart Search"
+  }
+}
diff --git a/web/src/lib/i18n/ar.json b/i18n/ar.json
similarity index 91%
rename from web/src/lib/i18n/ar.json
rename to i18n/ar.json
index 1d90f9e625..7e1805ca34 100644
--- a/web/src/lib/i18n/ar.json
+++ b/i18n/ar.json
@@ -1,5 +1,5 @@
 {
-  "about": "حول",
+  "about": "تحديث",
   "account": "الحساب",
   "account_settings": "إعدادات الحساب",
   "acknowledge": "أُدرك ذلك",
@@ -28,11 +28,17 @@
   "added_to_favorites_count": "تم إضافة {count, number} إلى المفضلات",
   "admin": {
     "add_exclusion_pattern_description": "إضافة أنماط الاستبعاد. يدعم التمويه باستخدام *، **، و؟. لتجاهل جميع الملفات في أي دليل يسمى \"Raw\"، استخدم \"**/Raw/**\". لتجاهل جميع الملفات التي تنتهي بـ \".tif\"، استخدم \"**/*.tif\". لتجاهل مسار مطلق، استخدم \"/path/to/ignore/**\".",
+    "asset_offline_description": "لم يعد هذا الأصل الخاص بالمكتبة الخارجية موجودًا على القرص وتم نقله إلى سلة المهملات. إذا تم نقل الملف داخل المكتبة، فتحقق من الجدول الزمني الخاص بك لمعرفة الأصل الجديد المقابل. لاستعادة هذا الأصل، يرجى التأكد من إمكانية الوصول إلى مسار الملف أدناه بواسطة Immich ومن ثم قم بمسح المكتبة.",
     "authentication_settings": "إعدادات المصادقة",
     "authentication_settings_description": "إدارة كلمة المرور وOAuth وإعدادات المصادقة الأُخرى",
     "authentication_settings_disable_all": "هل أنت متأكد أنك تريد تعطيل جميع وسائل تسجيل الدخول؟ سيتم تعطيل تسجيل الدخول بالكامل.",
     "authentication_settings_reenable": "لإعادة التفعيل، استخدم <link>أمر الخادم</link>.",
     "background_task_job": "المهام الخلفية",
+    "backup_database": "قاعدة البيانات الاحتياطية",
+    "backup_database_enable_description": "تمكين النسخ الاحتياطي لقاعدة البيانات",
+    "backup_keep_last_amount": "مقدار النسخ الاحتياطية السابقة للاحتفاظ بها",
+    "backup_settings": "إعدادات النسخ الاحتياطي",
+    "backup_settings_description": "إدارة إعدادات النسخ الاحتياطي لقاعدة البيانات",
     "check_all": "اختر الكل",
     "cleared_jobs": "تم إخلاء مهام: {job}",
     "config_set_by_file": "الإعدادات حاليًا معينة عن طريق ملف الاعدادات",
@@ -41,35 +47,40 @@
     "confirm_email_below": "للتأكيد، اكتب \"{email}\" بالأسفل",
     "confirm_reprocess_all_faces": "هل أنت متأكد أنك تريد إعادة معالجة جميع الوجوه؟ سيخلي هذا كل الأشخاص الذين سَميتَهم.",
     "confirm_user_password_reset": "هل أنت متأكد أنك تريد إعادة تعيين كلمة مرور {user}؟",
-    "crontab_guru": "",
+    "create_job": "إنشاء وظيفة",
+    "cron_expression": "تعبير Cron",
+    "cron_expression_description": "اضبط الفاصل الزمني للفحص باستخدام تنسيق cron. لمزيد من المعلومات يُرجى الرجوع إلى <link>Crontab Guru</link> على سبيل المثال",
+    "cron_expression_presets": "الإعدادات المسبقة لتعبير Cron",
     "disable_login": "تعطيل تسجيل الدخول",
-    "disabled": "",
     "duplicate_detection_job_description": "بدء التعلم الآلي على المحتوى للعثور على الصور المتشابهة. يعتمد على البحث الذكي",
     "exclusion_pattern_description": "تتيح لك أنماط الاستبعاد تجاهل الملفات والمجلدات عند فحص مكتبتك. يعد هذا مفيدًا إذا كان لديك مجلدات تحتوي على ملفات لا تريد استيرادها، مثل ملفات RAW.",
     "external_library_created_at": "مكتبة خارجية (أُنشئت في {date})",
     "external_library_management": "إدارة المكتبة الخارجية",
     "face_detection": "إ‏كتشاف الوجوه",
-    "face_detection_description": "اكتشف الوجوه في المحتويات باستخدام التعلم الآلي. بالنسبة للفيديوهات، سيتم فقط استخدام الصورة المصغرة. خيار \"الكل\" يعيد معالجة كل المحتويات. خيار \"مفقود\" يضع في قائمة الإنتظار المحتويات التي لم تعالج بعد. سيتم وضع الوجوه المكتشفة في قائمة إنتظار التعرف على الوجه بعد اكتمال اكتشاف الوجه، مما يجمعها بأشخاص موجودين أو جدد.",
-    "facial_recognition_job_description": "تجميع الوجوه المكتشفة كأشخاص. يتم تنفيذ هذه الخطوة بعد اكتمال اكتشاف الوجه. خيار \"الكل\" يعيد تجميع جميع الوجوه. خيار \"المفقود\" يضع في قائمة الانتظار الوجوه التي لم يتم تعيين شخص لها.",
+    "face_detection_description": "اكتشف الوجوه في الأصول باستخدام التعلم الآلي. بالنسبة لمقاطع الفيديو، يتم اعتبار الصورة المصغرة فقط. \"تحديث\" (إعادة) معالجة جميع الأصول. \"إعادة تعيين\" تمسح أيضًا جميع بيانات الوجوه الحالية. \"مفقود\" يضع الأصول التي لم تتم معالجتها بعد في قائمة الانتظار. سيتم وضع الوجوه المكتشفة في قائمة الانتظار للتعرف على الوجه بعد اكتمال اكتشاف الوجه، وتجميعها في أشخاص موجودين أو جدد.",
+    "facial_recognition_job_description": "تجميع الوجوه المكتشفة كأشخاص. يتم تنفيذ هذه الخطوة بعد اكتمال اكتشاف الوجه. خيار \"إعادة التعيين\" يعيد تجميع جميع الوجوه. خيار \"المفقود\" يضع في قائمة الانتظار الوجوه التي لم يتم تعيين شخص لها.",
     "failed_job_command": "فشل الأمر {command} للمهمة: {job}",
     "force_delete_user_warning": "تحذير: سيؤدي ذلك إلى إزالة المستخدم وجميع محتوياته على الفور. لا يمكن التراجع عن هذا الإجراء ولا يمكن استرداد الملفات.",
     "forcing_refresh_library_files": "إجبار التحديث لجميع ملفات المكتبة",
+    "image_format": "التنسيق",
     "image_format_description": "يُنتج WebP ملفات أصغر حجمًا من ملفات JPEG، ولكنه أبطأ في عملية الترميز.",
     "image_prefer_embedded_preview": "تفضيل المعاينة المدمجة",
     "image_prefer_embedded_preview_setting_description": "استخدم المعاينات المضمنة في صور RAW كمدخل لمعالجة الصور عندما تكون متاحة. يؤدي لإنتاج ألوان أكثر دقة لبعض الصور، لكن جودة المعاينة تعتمد على الكاميرا وقد تحتوي الصورة على شوائب ضغطٍ أكثر.",
     "image_prefer_wide_gamut": "تفضيل نطاق الألوان الواسع",
     "image_prefer_wide_gamut_setting_description": "استخدم Display P3 للصور المصغرة. يحافظ هذا على حيوية الصور ذات مساحات الألوان الواسعة بشكل أفضل، ولكن قد تظهر الصور بشكل مختلف على الأجهزة القديمة ذات إصدار متصفح قديم. يتم الاحتفاظ بصور sRGB بتنسيق sRGB لتجنب تغيرات اللون.",
-    "image_preview_format": "تنسيق المعاينة",
-    "image_preview_resolution": "معاينة الدقّة",
-    "image_preview_resolution_description": "يُستخدم عند عرض صورة واحدة وللتعلم الآلي. ستحافظ الدقاتُ العالية على المزيد من التفاصيل ولكنها ستستغرق وقتًا أطول للترميز، ولها أحجام ملفات أكبر، ويمكن أن تقلل من استجابة التطبيق.",
+    "image_preview_description": "صورة متوسطة الحجم مع بيانات وصفية مجردة، تُستخدم عند عرض أصل واحد وللتعلم الآلي",
+    "image_preview_quality_description": "جودة المعاينة من 1 إلى 100. كلما كانت القيمة أعلى كان ذلك أفضل، ولكنها تنتج ملفات أكبر وقد تقلل من استجابة التطبيق. قد يؤثر ضبط قيمة منخفضة على جودة التعلم الآلي.",
+    "image_preview_title": "إعدادات المعاينة",
     "image_quality": "الجودة",
-    "image_quality_description": "جودة الصورة من 1-100. الأعلى هو الأفضل من حيث الجودة ولكنه ينتج ملفات أكبر، ويؤثر هذا الخيار على صور المعاينة والصور المصغرة.",
+    "image_resolution": "الدقة",
+    "image_resolution_description": "يمكن للدقة العالية الحفاظ على مزيد من التفاصيل ولكنها تستغرق وقتًا أطول للترميز، وتحتوي على أحجام ملفات أكبر ويمكن أن تقلل من استجابة التطبيق.",
     "image_settings": "إعدادات الصور",
     "image_settings_description": "إدارة جودة ودقة الصور التي تم إنشاؤها",
-    "image_thumbnail_format": "تنسيق الصور المصغّرة",
-    "image_thumbnail_resolution": "دقة الصور المصغّرة",
-    "image_thumbnail_resolution_description": "يُستخدم عند عرض مجموعات من الصور (المخطط الزمني الرئيسي، عرض الألبوم، وما إلى ذلك). ستحافظ الدقاتُ العالية على المزيد من التفاصيل ولكنها ستستغرق وقتًا أطول للترميز، ولها أحجام ملفات أكبر، ويمكن أن تقلل من استجابة التطبيق.",
+    "image_thumbnail_description": "صورة مصغرة صغيرة مع بيانات وصفية مجردة، تُستخدم عند عرض مجموعات من الصور مثل الجدول الزمني الرئيسي",
+    "image_thumbnail_quality_description": "تتراوح جودة الصورة المصغرة من 1 إلى 100. كلما كانت الجودة أعلى كان ذلك أفضل، ولكنها تنتج ملفات أكبر وقد تقلل من استجابة التطبيق.",
+    "image_thumbnail_title": "إعدادات الصورة المصغرة",
     "job_concurrency": "تزامن {job}",
+    "job_created": "تم إنشاء الوظيفة",
     "job_not_concurrency_safe": "هذه الوظيفة غير آمنة للتشغيل المتزامن.",
     "job_settings": "إعدادات الوظائف",
     "job_settings_description": "إدارة تزامن الوظائف",
@@ -77,9 +88,6 @@
     "jobs_delayed": "{jobCount, plural, other {# مؤجلة}}",
     "jobs_failed": "{jobCount, plural, other {# فشلت}}",
     "library_created": "تم إنشاء المكتبة: {library}",
-    "library_cron_expression": "تعبير Cron",
-    "library_cron_expression_description": "\"اضبط فواصلَ زمنِ الفحص باستخدام صيغة cron. للمزيد من المعلومات، يرجى الرجوع إلى <link>Crontab Guru</link>\"",
-    "library_cron_expression_presets": "إعدادات مسبقة لتعبير Cron",
     "library_deleted": "تم حذف المكتبة",
     "library_import_path_description": "حدد مجلدًا للاستيراد. سيتم فحص هذا المجلد، بما في ذلك المجلدات الفرعية، بحثًا عن الصور ومقاطع الفيديو.",
     "library_scanning": "الفحص الدوري",
@@ -139,7 +147,11 @@
     "map_settings_description": "إدارة إعدادات الخريطة",
     "map_style_description": "عنوان URL لسمة الخريطة style.json",
     "metadata_extraction_job": "استخراج البيانات الوصفية",
-    "metadata_extraction_job_description": "استخراج معلومات البيانات الوصفية من كل أصل، مثل إحداثيات الموقع والدقة",
+    "metadata_extraction_job_description": "استخراج معلومات البيانات الوصفية من كل أصل، مثل إحداثيات الموقع, الوجوه والدقة",
+    "metadata_faces_import_setting": "تمكين استيراد الوجه",
+    "metadata_faces_import_setting_description": "استيراد الوجوه من بيانات EXIF للصور وملفات Sidecar",
+    "metadata_settings": "إعدادات البيانات الوصفية",
+    "metadata_settings_description": "إدارة إعدادات البيانات الوصفية",
     "migration_job": "ترحيل",
     "migration_job_description": "ترحيل الصور المصغرة للمحتويات والوجوه إلى أحدث هيكل مجلدات",
     "no_paths_added": "لم يتم إضافة أي مسارات",
@@ -148,7 +160,7 @@
     "note_cannot_be_changed_later": "ملاحظة: لا يمكن تغيير هذا لاحقًا!",
     "note_unlimited_quota": "ملاحظة: أدخل 0 للحصول على حصة غير محدودة",
     "notification_email_from_address": "عنوان المرسل",
-    "notification_email_from_address_description": "عنوان البريد الإلكتروني للمرسل، على سبيل المثال: \"Immich Photo Server noreply@immich.app\"",
+    "notification_email_from_address_description": "عنوان البريد الإلكتروني للمرسل، على سبيل المثال: \"Immich Photo Server noreply@example.com\"",
     "notification_email_host_description": "مضيف خادم البريد الإلكتروني (مثلًا: smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "تجاهل أخطاء الشهادة",
     "notification_email_ignore_certificate_errors_description": "تجاهل أخطاء التحقق من صحة شهادة TLS (غير مستحسن)",
@@ -194,22 +206,24 @@
     "password_settings": "تسجيل الدخول بكلمة المرور",
     "password_settings_description": "إدارة تسجيل الدخول بكلمة المرور",
     "paths_validated_successfully": "تم التحقق من صحة كافة المسارات بنجاح",
+    "person_cleanup_job": "تنظيف الشخص",
     "quota_size_gib": "حجم الحصة (جيجابايت)",
     "refreshing_all_libraries": "تحديث كافة المكتبات",
     "registration": "تسجيل المدير",
     "registration_description": "بما أنك أول مستخدم في النظام، سيتم تعيينك كمسؤول وستكون مسؤولًا عن المهام الإدارية، وسيتم إنشاء مستخدمين إضافيين بواسطتك.",
-    "removing_offline_files": "إزالة الملفات غير المتصلة",
     "repair_all": "إصلاح الكل",
     "repair_matched_items": "تمت مطابقة {count, plural, one {# عنصر} other {# عناصر}}",
     "repaired_items": "تم إصلاح {count, plural, one {# عنصر} other {# عناصر}}",
     "require_password_change_on_login": "الطلب من المستخدم تغيير كلمة المرور عند تسجيل الدخول الأول",
     "reset_settings_to_default": "إعادة ضبط الإعدادات إلى الوضع الافتراضي",
     "reset_settings_to_recent_saved": "إعادة ضبط الإعدادات إلى الإعدادات المحفوظة مؤخرًا",
-    "scanning_library_for_changed_files": "فحص المكتبة لاكتشاف الملفات التي تم تغييرها",
-    "scanning_library_for_new_files": "فحص المكتبة للبحث عن ملفات جديدة",
+    "scanning_library": "مسح المكتبة",
+    "search_jobs": "البحث عن وظائف...",
     "send_welcome_email": "إرسال بريد ترحيبي",
     "server_external_domain_settings": "إسم النطاق الخارجي",
     "server_external_domain_settings_description": "إسم النطاق لروابط المشاركة العامة، بما في ذلك http(s)://",
+    "server_public_users": "المستخدمون العامون",
+    "server_public_users_description": "يتم إدراج جميع المستخدمين (الاسم والبريد الإلكتروني) عند إضافة مستخدم إلى الألبومات المشتركة. عند تعطيل هذه الميزة، ستكون قائمة المستخدمين متاحة فقط لمستخدمي الإدارة.",
     "server_settings": "إعدادات الخادم",
     "server_settings_description": "إدارة إعدادات الخادم",
     "server_welcome_message": "الرسالة الترحيبية",
@@ -234,6 +248,7 @@
     "storage_template_settings_description": "إدارة هيكل المجلد واسم الملف للأصول المرفوعة",
     "storage_template_user_label": "<code>{label}</code> هو تسمية التخزين الخاصة بالمستخدم",
     "system_settings": "إعدادات النظام",
+    "tag_cleanup_job": "تنظيف العلامة",
     "theme_custom_css_settings": "CSS مخصص",
     "theme_custom_css_settings_description": "أوراق الأنماط المتتالية تسمح بتخصيص تصميم Immich.",
     "theme_settings": "إعدادات السمة",
@@ -241,7 +256,6 @@
     "these_files_matched_by_checksum": "تتم مطابقة هذه الملفات من خلال المجاميع الاختبارية الخاصة بهم",
     "thumbnail_generation_job": "إنشاء الصور المصغرة",
     "thumbnail_generation_job_description": "إنشاء صور مصغرة كبيرة وصغيرة وغير واضحة لكل أصل، بالإضافة إلى صور مصغرة لكل شخص",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "واجهة برمجة التطبيقات للتسريع",
     "transcoding_acceleration_api_description": "الواجهة البرمجية التي ستتفاعل مع جهازك لتسريع التحويل. هذا الإعداد هو \"أفضل محاولة\": سيعود إلى التحويل البرمجي في حالة الفشل. قد لا يعمل VP9 اعتمادًا على عتادك.",
     "transcoding_acceleration_nvenc": "NVENC (يتطلب GPU من NVIDIA)",
@@ -293,8 +307,6 @@
     "transcoding_threads_description": "تؤدي القيم الأعلى إلى تشفير أسرع، ولكنها تترك مساحة أقل للخادم لمعالجة المهام الأخرى أثناء النشاط. يجب ألا تزيد هذه القيمة عن عدد مراكز وحدة المعالجة المركزية. يزيد من الإستغلال إذا تم ضبطه على 0.",
     "transcoding_tone_mapping": "رسم الخرائط النغمية",
     "transcoding_tone_mapping_description": "تحاول الحفاظ على مظهر مقاطع الفيديو HDR عند تحويلها إلى SDR. يقدم كل خوارزمية تنازلات مختلفة بين اللون والتفاصيل والسطوع. Hable تحافظ على التفاصيل، Mobius تحافظ على الألوان، و Reinhard تحافظ على السطوع.",
-    "transcoding_tone_mapping_npl": "تحويل الصور من نطاق الإضاءة العالية",
-    "transcoding_tone_mapping_npl_description": "سيتم ضبط الألوان لتبدو طبيعية على شاشة بهذه السطوع. على عكس المتوقع، تزيد القيم الأقل من سطوع الفيديو والعكس بسبب تعويضها لسطوع الشاشة. قيمة 0 تضبط هذه القيمة تلقائيًا.",
     "transcoding_transcode_policy": "سياسة الترميز",
     "transcoding_transcode_policy_description": "سياسة تحديد متى يجب ترميز الفيديو. سيتم دائمًا ترميز مقاطع الفيديو HDR (ما لم يتم تعطيل الترميز).",
     "transcoding_two_pass_encoding": "الترميز بمرورين",
@@ -308,6 +320,7 @@
     "trash_settings_description": "إدارة إعدادات سلة المهملات",
     "untracked_files": "الملفات التي لم يتم تعقبها",
     "untracked_files_description": "لا يتم تعقب هذه الملفات بواسطة التطبيق. يمكن أن تكون نتيجة لعمليات نقل فاشلة، أو عمليات تحميل متقطعة، أو يتم تركها في الخلف بسبب خطأ ما",
+    "user_cleanup_job": "تنظيف المستخدم",
     "user_delete_delay": "سيتم جدولة حساب <b>{user}</b> ومحتوياته للحذف النهائي في غضون {delay, plural, one {# يوم} other {# أيام}}.",
     "user_delete_delay_settings": "فترة التأخير قبل الحذف",
     "user_delete_delay_settings_description": "عدد الأيام بعد الإزالة لحذف حساب المستخدم ومحتوياته بشكل دائم. تقوم وظيفة حذف المستخدم بالتشغيل في منتصف الليل للتحقق من المستخدمين الجاهزين للحذف. سيتم تقييم التغييرات على هذا الإعداد في التنفيذ القادم.",
@@ -374,7 +387,6 @@
   "archive_or_unarchive_photo": "أرشفة الصورة أو إلغاء أرشفتها",
   "archive_size": "حجم الأرشيف",
   "archive_size_description": "تكوين حجم الأرشيف للتنزيلات (بالجيجابايت)",
-  "archived": "",
   "archived_count": "{count, plural, other {الأرشيف #}}",
   "are_these_the_same_person": "هل هؤلاء هم نفس الشخص؟",
   "are_you_sure_to_do_this": "هل انت متأكد من أنك تريد أن تفعل هذا؟",
@@ -384,9 +396,10 @@
   "asset_filename_is_offline": "الأصل {filename} غير متصل",
   "asset_has_unassigned_faces": "يحتوي الأصل على وجوه غير مخصصة",
   "asset_hashing": "التجزئة...",
-  "asset_offline": "المحتوى دون اتصال",
-  "asset_offline_description": "هذا الأصل غير متصل. لا يستطيع Immic الوصول إلى موقع الملف الخاص به. يرجى التأكد من توفر الأصل ثم إعادة فحص المكتبة.",
+  "asset_offline": "المحتوى غير اتصال",
+  "asset_offline_description": "لم يعد هذا الأصل الخارجي موجودًا على القرص. يرجى الاتصال بمسؤول Immich للحصول على المساعدة.",
   "asset_skipped": "تم تخطيه",
+  "asset_skipped_in_trash": "في سلة المهملات",
   "asset_uploaded": "تم الرفع",
   "asset_uploading": "جارٍ الرفع...",
   "assets": "المحتويات",
@@ -397,7 +410,7 @@
   "assets_moved_to_trash_count": "تم نقل {count, plural, one {# محتوى} other {# محتويات}} إلى سلة المهملات",
   "assets_permanently_deleted_count": "تم حذف {count, plural, one {# هذا المحتوى} other {# هذه المحتويات}} بشكل دائم",
   "assets_removed_count": "تمت إزالة {count, plural, one {# محتوى} other {# محتويات}}",
-  "assets_restore_confirmation": "هل أنت متأكد أنك تريد استعادة كافة المحتويات المحذوفة؟ لا يمكنك التراجع عن هذا الإجراء!",
+  "assets_restore_confirmation": "هل أنت متأكد من أنك تريد استعادة جميع الأصول المحذوفة؟ لا يمكنك التراجع عن هذا الإجراء! لاحظ أنه لا يمكن استعادة أي أصول غير متصلة بهذه الطريقة.",
   "assets_restored_count": "تمت استعادة {count, plural, one {# محتوى} other {# محتويات}}",
   "assets_trashed_count": "تم إرسال {count, plural, one {# محتوى} other {# محتويات}} إلى سلة المهملات",
   "assets_were_part_of_album_count": "{count, plural, one {هذا المحتوى} other {هذه المحتويات}} في الألبوم بالفعل",
@@ -408,6 +421,7 @@
   "birthdate_saved": "تم حفظ تاريخ الميلاد بنجاح",
   "birthdate_set_description": "يتم استخدام تاريخ الميلاد لحساب عمر هذا الشخص وقت التقاط الصورة.",
   "blurred_background": "خلفية مشوشة",
+  "bugs_and_feature_requests": "الأخطاء وطلبات الميزات",
   "build": "يبني",
   "build_image": "بناء الصورة",
   "bulk_delete_duplicates_confirmation": "هل أنت متأكد من أنك تريد حذف {count, plural, one {# محتوى مكرر} other {# محتويات مكررة}} بالجملة؟ سيحتفظ هذا بأكبر محتوى من كل مجموعة ويحذف جميع النسخ المكررة الأخرى بشكل دائم. لا يمكنك التراجع عن هذا الإجراء!",
@@ -422,10 +436,6 @@
   "cannot_merge_people": "لا يمكن دمج الأشخاص",
   "cannot_undo_this_action": "لا يمكنك التراجع عن هذا الإجراء!",
   "cannot_update_the_description": "لا يمكن تحديث الوصف",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
   "change_date": "غيّر التاريخ",
   "change_expiration_time": "تغيير وقت انتهاء الصلاحية",
   "change_location": "غيّر الموقع",
@@ -457,6 +467,7 @@
   "confirm": "تأكيد",
   "confirm_admin_password": "تأكيد كلمة مرور المسؤول",
   "confirm_delete_shared_link": "هل أنت متأكد أنك تريد حذف هذا الرابط المشترك؟",
+  "confirm_keep_this_delete_others": "سيتم حذف جميع الأصول الأخرى في المجموعة باستثناء هذا الأصل. هل أنت متأكد من أنك تريد المتابعة؟",
   "confirm_password": "تأكيد كلمة المرور",
   "contain": "محتواة",
   "context": "السياق",
@@ -506,16 +517,19 @@
   "delete_key": "حذف المفتاح",
   "delete_library": "حذف المكتبة",
   "delete_link": "حذف الرابط",
+  "delete_others": "حذف الأخرى",
   "delete_shared_link": "حذف الرابط المشترك",
   "delete_tag": "حذف العلامة",
   "delete_tag_confirmation_prompt": "هل أنت متأكد أنك تريد حذف العلامة {tagName}؟",
   "delete_user": "حذف المستخدم",
   "deleted_shared_link": "تم حذف الرابط المشارك",
+  "deletes_missing_assets": "حذف الأصول المفقودة من القرص",
   "description": "وصف",
   "details": "تفاصيل",
   "direction": "الإتجاه",
   "disabled": "معطل",
   "disallow_edits": "منع التعديلات",
+  "discord": "Discord",
   "discover": "اكتشف",
   "dismiss_all_errors": "تجاهل كافة الأخطاء",
   "dismiss_error": "تجاهل الخطأ",
@@ -524,6 +538,7 @@
   "display_original_photos": "عرض الصور الأصلية",
   "display_original_photos_setting_description": "فضل عرض الصورة الأصلية عند عرض المحتويات بدلاً من الصور المصغرة عندما يكون المحتوى الأصلي متوافقًا مع الويب. قد يؤدي ذلك إلى بطءٍ في سرعات عرض الصور.",
   "do_not_show_again": "لا تُظهر هذه الرسالة مرة آخرى",
+  "documentation": "الوثائق",
   "done": "تم",
   "download": "تنزيل",
   "download_include_embedded_motion_videos": "مقاطع الفيديو المدمجة",
@@ -536,13 +551,6 @@
   "duplicates": "التكرارات",
   "duplicates_description": "قم بحل كل مجموعة من خلال الإشارة إلى التكرارات، إن وجدت",
   "duration": "المدة",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
   "edit": "تعديل",
   "edit_album": "تعديل الألبوم",
   "edit_avatar": "تعديل الصورة الشخصية",
@@ -567,8 +575,6 @@
   "editor_crop_tool_h2_aspect_ratios": "نسب العرض إلى الارتفاع",
   "editor_crop_tool_h2_rotation": "التدوير",
   "email": "البريد الإلكتروني",
-  "empty": "",
-  "empty_album": "",
   "empty_trash": "أفرغ سلة المهملات",
   "empty_trash_confirmation": "هل أنت متأكد أنك تريد إفراغ سلة المهملات؟ سيؤدي هذا إلى إزالة جميع المحتويات الموجودة في سلة المهملات بشكل نهائي من Immich.\nلا يمكنك التراجع عن هذا الإجراء!",
   "enable": "تفعيل",
@@ -602,6 +608,7 @@
     "failed_to_create_shared_link": "فشل إنشاء رابط مشترك",
     "failed_to_edit_shared_link": "فشل تعديل الرابط المشترك",
     "failed_to_get_people": "فشل في الحصول على الناس",
+    "failed_to_keep_this_delete_others": "فشل في الاحتفاظ بهذا الأصل وحذف الأصول الأخرى",
     "failed_to_load_asset": "فشل تحميل المحتوى",
     "failed_to_load_assets": "فشل تحميل المحتويات",
     "failed_to_load_people": "فشل تحميل الأشخاص",
@@ -629,8 +636,6 @@
     "unable_to_change_location": "غير قادر على تغيير الموقع",
     "unable_to_change_password": "غير قادر على تغيير كلمة المرور",
     "unable_to_change_visibility": "غير قادر على تغيير الظهور لـ {count, plural, one {# شخص} other {# أشخاص}}",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_complete_oauth_login": "غير قادر على إكمال تسجيل الدخول عبر OAuth",
     "unable_to_connect": "غير قادر على الإتصال",
     "unable_to_connect_to_server": "غير قادر على الإتصال بالسيرفر",
@@ -655,6 +660,7 @@
     "unable_to_get_comments_number": "غير قادر على الحصول على عدد التعليقات",
     "unable_to_get_shared_link": "فشل الحصول على الرابط المشترك",
     "unable_to_hide_person": "غير قادر على إخفاء الشخص",
+    "unable_to_link_motion_video": "غير قادر على ربط فيديو الحركة",
     "unable_to_link_oauth_account": "غير قادر على ربط حساب OAuth",
     "unable_to_load_album": "غير قادر على تحميل الألبوم",
     "unable_to_load_asset_activity": "غير قادر على تحميل نشاط المحتويات",
@@ -670,12 +676,10 @@
     "unable_to_remove_album_users": "تعذر إزالة المستخدمين من الألبوم",
     "unable_to_remove_api_key": "تعذر إزالة مفتاح API",
     "unable_to_remove_assets_from_shared_link": "غير قادر على إزالة المحتويات من الرابط المشترك",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_deleted_assets": "غير قادر على إزالة الملفات غير المتصلة",
     "unable_to_remove_library": "غير قادر على إزالة المكتبة",
-    "unable_to_remove_offline_files": "غير قادر على إزالة الملفات غير المتصلة",
     "unable_to_remove_partner": "غير قادر على إزالة الشريك",
     "unable_to_remove_reaction": "غير قادر على إزالة رد الفعل",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "غير قادر على إصلاح العناصر",
     "unable_to_reset_password": "غير قادر على إعادة تعيين كلمة المرور",
     "unable_to_resolve_duplicate": "غير قادر على حل التكرارات",
@@ -695,6 +699,7 @@
     "unable_to_submit_job": "غير قادر على تقديم الوظيفة",
     "unable_to_trash_asset": "غير قادر على نقل المحتويات إلى سلة المهملات",
     "unable_to_unlink_account": "غير قادر على إلغاء ربط الحساب",
+    "unable_to_unlink_motion_video": "غير قادر على إلغاء ربط فيديو الحركة",
     "unable_to_update_album_cover": "غير قادر على تحديث غلاف الألبوم",
     "unable_to_update_album_info": "غير قادر على تحديث معلومات الألبوم",
     "unable_to_update_library": "غير قادر على تحديث المكتبة",
@@ -704,10 +709,6 @@
     "unable_to_update_user": "غير قادر على تحديث المستخدم",
     "unable_to_upload_file": "تعذر رفع الملف"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exif": "Exif (صيغة ملف صوري قابل للتبادل)",
   "exit_slideshow": "خروج من العرض التقديمي",
   "expand_all": "توسيع الكل",
@@ -722,33 +723,27 @@
   "external": "خارجي",
   "external_libraries": "المكتبات الخارجية",
   "face_unassigned": "غير معين",
-  "failed_to_get_people": "",
   "favorite": "مفضل",
   "favorite_or_unfavorite_photo": "تفضيل أو إلغاء تفضيل الصورة",
   "favorites": "المفضلة",
-  "feature": "",
   "feature_photo_updated": "تم تحديث الصورة المميزة",
-  "featurecollection": "",
   "features": "الميزات",
   "features_setting_description": "إدارة ميزات التطبيق",
   "file_name": "إسم الملف",
   "file_name_or_extension": "اسم الملف أو امتداده",
   "filename": "اسم الملف",
-  "files": "",
   "filetype": "نوع الملف",
   "filter_people": "تصفية الاشخاص",
   "find_them_fast": "يمكنك العثور عليها بسرعة بالاسم من خلال البحث",
   "fix_incorrect_match": "إصلاح المطابقة غير الصحيحة",
   "folders": "المجلدات",
   "folders_feature_description": "تصفح عرض المجلد للصور ومقاطع الفيديو الموجودة على نظام الملفات",
-  "force_re-scan_library_files": "فرض إعادة فحص جميع ملفات المكتبة",
   "forward": "إلى الأمام",
   "general": "عام",
   "get_help": "الحصول على المساعدة",
   "getting_started": "البدء",
   "go_back": "الرجوع للخلف",
   "go_to_search": "اذهب إلى البحث",
-  "go_to_share_page": "انتقل إلى صفحة المشاركة",
   "group_albums_by": "تجميع الألبومات حسب...",
   "group_no": "بدون تجميع",
   "group_owner": "تجميع حسب المالك",
@@ -774,10 +769,6 @@
   "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} مع {person1} و{person2} في {date}",
   "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} مع {person1}، {person2}، و{person3} في {date}",
   "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}, {country} with {person1}, {person2}, مع {additionalCount, number} آخرين في {date}",
-  "image_alt_text_people": "{count, plural, =1 {مع {person1}} =2 {مع {person1} و {person2}} =3 {مع {person1} و {person2} و {person3}} other {مع {person1} و {person2} و {others, number} آخرين}}",
-  "image_alt_text_place": "في {city}, {country}",
-  "image_taken": "{isVideo, select, true {تم التقاط الفيديو} other {تم التقاط الصورة}}",
-  "img": "",
   "immich_logo": "شعار immich",
   "immich_web_interface": "واجهة ويب immich",
   "import_from_json": "استيراد من JSON",
@@ -798,10 +789,11 @@
   "invite_people": "دعوة الأشخاص",
   "invite_to_album": "دعوة إلى الألبوم",
   "items_count": "{count, plural, one {# عنصر} other {# عناصر}}",
-  "job_settings_description": "",
   "jobs": "الوظائف",
   "keep": "احتفظ",
   "keep_all": "احتفظ بالكل",
+  "keep_this_delete_others": "احتفظ بهذا، واحذف الآخرين",
+  "kept_this_deleted_others": "تم الاحتفاظ بهذا الأصل وحذف {count, plural, one {# asset} other {# assets}}",
   "keyboard_shortcuts": "اختصارات لوحة المفاتيح",
   "language": "اللغة",
   "language_setting_description": "اختر لغتك المفضلة",
@@ -813,33 +805,9 @@
   "level": "المستوى",
   "library": "مكتبة",
   "library_options": "خيارات المكتبة",
-  "license_account_info": "حسابك مرخص",
-  "license_activated_subtitle": "شكرا بدعمك لـ Immich وبرمجيات المصدر المفتوح",
-  "license_activated_title": "رخصتك نُشطت بنجاح",
-  "license_button_activate": "تنشيط",
-  "license_button_buy": "شراء",
-  "license_button_buy_license": "اشتر رخصة",
-  "license_button_select": "إختر",
-  "license_failed_activation": "فشل في تفعيل الترخيص. يرجى التحقق من بريدك الإلكتروني للحصول على مفتاح الترخيص الصحيح!",
-  "license_individual_description_1": "رخصة واحدة لكل مستخدم على أي خادم",
-  "license_individual_title": "رخصة فردية",
-  "license_info_licensed": "مُرَخص",
-  "license_info_unlicensed": "غير مُرَخص",
-  "license_input_suggestion": "لديك رخصة؟ أدخِل الرمز بالأسفل",
-  "license_license_subtitle": "اشتر رخصةً لدعم Immich",
-  "license_license_title": "الرخصة",
-  "license_lifetime_description": "رخصة مدى الحياة",
-  "license_per_server": "لكل خادم",
-  "license_per_user": "لكل مستحدم",
-  "license_server_description_1": "رخصة واحدة لكل خادم",
-  "license_server_description_2": "رخصة لكل المستخدمين على الخادم",
-  "license_server_title": "رخصة خادم",
-  "license_trial_info_1": "أنت تستخدم نسخةً غير مرخصة ل Immich",
-  "license_trial_info_2": "لقد استخدمتَ Immich تقريبا لمدة",
-  "license_trial_info_3": "{accountAge, plural, one {# يوم} other {# أيام}}",
-  "license_trial_info_4": "يُرجى التفكير في شراء رخصة لدعم التطوير المستمر للخدمة",
   "light": "المضيئ",
   "like_deleted": "تم حذف الإعجاب",
+  "link_motion_video": "رابط فيديو الحركة",
   "link_options": "خيارات الرابط",
   "link_to_oauth": "الربط مع OAuth",
   "linked_oauth_account": "حساب مرتبط بـ OAuth",
@@ -858,6 +826,7 @@
   "look": "الشكل",
   "loop_videos": "تكرار مقاطع الفيديو",
   "loop_videos_description": "فَعْل لتكرار مقطع فيديو تلقائيًا في عارض التفاصيل.",
+  "main_branch_warning": "أنت تستخدم إصداراً تطويرياً؛ ونحن نوصي بشدة باستخدام إصدار النشر!",
   "make": "صنع",
   "manage_shared_links": "إدارة الروابط المشتركة",
   "manage_sharing_with_partners": "إدارة المشاركة مع الشركاء",
@@ -927,6 +896,7 @@
   "notifications": "إشعارات",
   "notifications_setting_description": "إدارة الإشعارات",
   "oauth": "OAuth",
+  "official_immich_resources": "الموارد الرسمية لشركة Immich",
   "offline": "غير متصل",
   "offline_paths": "مسارات غير متصلة",
   "offline_paths_description": "قد تكون هذه النتائج بسبب الحذف اليدوي للملفات التي لا تشكل جزءًا من مكتبة خارجية.",
@@ -939,7 +909,6 @@
   "onboarding_welcome_user": "مرحبا، {user}",
   "online": "متصل",
   "only_favorites": "المفضلة فقط",
-  "only_refreshes_modified_files": "تحديث الملفات المعدلة فقط",
   "open_in_map_view": "فتح في عرض الخريطة",
   "open_in_openstreetmap": "فتح في OpenStreetMap",
   "open_the_search_filters": "افتح مرشحات البحث",
@@ -977,7 +946,6 @@
   "people_edits_count": "تم تعديل {count, plural, one {# شخص } other {# أشخاص }}",
   "people_feature_description": "تصفح الصور ومقاطع الفيديو المجمعة حسب الأشخاص",
   "people_sidebar_description": "عرض رابط للأشخاص في الشريط الجانبي",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "تحذير الحذف الدائم",
   "permanent_deletion_warning_setting_description": "إظهار تحذير عند حذف المحتويات نهائيًا",
   "permanently_delete": "حذف بشكل دائم",
@@ -999,7 +967,6 @@
   "play_memories": "تشغيل الذكريات",
   "play_motion_photo": "تشغيل الصور المتحركة",
   "play_or_pause_video": "تشغيل الفيديو أو إيقافه مؤقتًا",
-  "point": "",
   "port": "المنفذ",
   "preset": "الإعداد المسبق",
   "preview": "معاينة",
@@ -1044,12 +1011,10 @@
   "purchase_server_description_2": "حالة الداعم",
   "purchase_server_title": "الخادم",
   "purchase_settings_server_activated": "يتم إدارة مفتاح منتج الخادم من قبل مدير النظام",
-  "range": "",
   "rating": "تقييم نجمي",
   "rating_clear": "مسح التقييم",
   "rating_count": "{count, plural, one {# نجمة} other {# نجوم}}",
   "rating_description": "‫‌اعرض تقييم EXIF في لوحة المعلومات",
-  "raw": "",
   "reaction_options": "خيارات رد الفعل",
   "read_changelog": "قراءة سجل التغيير",
   "reassign": "إعادة التعيين",
@@ -1060,11 +1025,13 @@
   "recent_searches": "عمليات البحث الأخيرة",
   "refresh": "تحديث",
   "refresh_encoded_videos": "تحديث مقاطع الفيديو المشفرة",
+  "refresh_faces": "تحديث الوجوه",
   "refresh_metadata": "تحديث البيانات الوصفية",
   "refresh_thumbnails": "تحديث الصور المصغرة",
   "refreshed": "تم التحديث",
-  "refreshes_every_file": "تحديث جميع الملفات",
+  "refreshes_every_file": "إعادة قراءة كافة الملفات الموجودة والجديدة",
   "refreshing_encoded_video": "جارٍ تحديث الفيديو المرمز",
+  "refreshing_faces": "جاري تحديث الوجوه",
   "refreshing_metadata": "جارٍ تحديث البيانات الوصفية",
   "regenerating_thumbnails": "جارٍ تجديد الصور المصغرة",
   "remove": "إزالة",
@@ -1072,10 +1039,10 @@
   "remove_assets_shared_link_confirmation": "هل أنت متأكد أنك تريد إزالة {count, plural, one {# المحتوى} other {# المحتويات}} من رابط المشاركة هذا؟",
   "remove_assets_title": "هل تريد إزالة المحتويات؟",
   "remove_custom_date_range": "إزالة النطاق الزمني المخصص",
+  "remove_deleted_assets": "إزالة الملفات الغير متصلة",
   "remove_from_album": "إزالة من الألبوم",
   "remove_from_favorites": "إزالة من المفضلة",
   "remove_from_shared_link": "إزالة من الرابط المشترك",
-  "remove_offline_files": "إزالة الملفات الغير متصلة",
   "remove_user": "إزالة المستخدم",
   "removed_api_key": "تم إزالة مفتاح API: {name}",
   "removed_from_archive": "تمت إزالتها من الأرشيف",
@@ -1092,7 +1059,6 @@
   "reset": "إعادة ضبط",
   "reset_password": "إعادة تعيين كلمة المرور",
   "reset_people_visibility": "إعادة ضبط ظهور الأشخاص",
-  "reset_settings_to_default": "",
   "reset_to_default": "إعادة التعيين إلى الافتراضي",
   "resolve_duplicates": "معالجة النسخ المكررة",
   "resolved_all_duplicates": "تم حل جميع التكرارات",
@@ -1112,8 +1078,7 @@
   "saved_settings": "تم حفظ الإعدادات",
   "say_something": "قل شيئًا",
   "scan_all_libraries": "فحص كل المكتبات",
-  "scan_all_library_files": "إعادة فحص كافة ملفات المكتبة",
-  "scan_new_library_files": "فحص ملفات المكتبة الجديدة",
+  "scan_library": "مسح",
   "scan_settings": "إعدادات الفحص",
   "scanning_for_album": "جارٍ الفحص عن ألبوم...",
   "search": "بحث",
@@ -1128,8 +1093,10 @@
   "search_for_existing_person": "البحث عن شخص موجود",
   "search_no_people": "لا يوجد أشخاص",
   "search_no_people_named": "لا يوجد أشخاص بالاسم \"{name}\"",
+  "search_options": "خيارات البحث",
   "search_people": "البحث عن الأشخاص",
   "search_places": "البحث عن الأماكن",
+  "search_settings": "إعدادات البحث",
   "search_state": "البحث حسب الولاية...",
   "search_tags": "البحث عن العلامات...",
   "search_timezone": "البحث حسب المنطقة الزمنية...",
@@ -1154,7 +1121,6 @@
   "selected_count": "{count, plural, other {# محددة }}",
   "send_message": "أرسل رسالة",
   "send_welcome_email": "أرسل بريدًا إلكترونيًا ترحيبيًا",
-  "server": "الخادم",
   "server_offline": "الخادم غير متصل",
   "server_online": "الخادم متصل",
   "server_stats": "إحصائيات الخادم",
@@ -1173,6 +1139,7 @@
   "shared_by_user": "تمت المشاركة بواسطة {user}",
   "shared_by_you": "تمت مشاركته من قِبلك",
   "shared_from_partner": "صور من {partner}",
+  "shared_link_options": "خيارات الرابط المشترك",
   "shared_links": "روابط مشتركة",
   "shared_photos_and_videos_count": "{assetCount, plural, other {# الصور ومقاطع الفيديو المُشارَكة.}}",
   "shared_with_partner": "تمت المشاركة مع {partner}",
@@ -1196,13 +1163,18 @@
   "show_person_options": "إظهار خيارات الشخص",
   "show_progress_bar": "إظهار شريط التقدم",
   "show_search_options": "إظهار خيارات البحث",
+  "show_slideshow_transition": "إظهار انتقال عرض الشرائح",
   "show_supporter_badge": "شارة المؤيد",
   "show_supporter_badge_description": "إظهار شارة المؤيد",
   "shuffle": "خلط",
+  "sidebar": "الشريط الجانبي",
+  "sidebar_display_description": "عرض رابط للعرض في الشريط الجانبي",
   "sign_out": "خروج",
   "sign_up": "تسجيل",
   "size": "الحجم",
   "skip_to_content": "تخطي إلى المحتوى",
+  "skip_to_folders": "تخطي إلى المجلدات",
+  "skip_to_tags": "تخطي إلى العلامات",
   "slideshow": "عرض الشرائح",
   "slideshow_settings": "إعدادات عرض الشرائح",
   "sort_albums_by": "رتب الألبومات حسب...",
@@ -1233,23 +1205,37 @@
   "submit": "إرسال",
   "suggestions": "اقتراحات",
   "sunrise_on_the_beach": "شروق الشمس على الشاطئ",
+  "support": "الدعم",
+  "support_and_feedback": "الدعم والتعليقات",
+  "support_third_party_description": "تم حزم تثبيت immich الخاص بك بواسطة جهة خارجية. قد تكون المشكلات التي تواجهها ناجمة عن هذه الحزمة، لذا يرجى طرح المشكلات معهم في المقام الأول باستخدام الروابط أدناه.",
   "swap_merge_direction": "تبديل اتجاه الدمج",
   "sync": "مزامنة",
+  "tag": "العلامة",
+  "tag_assets": "أصول العلامة",
+  "tag_created": "تم إنشاء العلامة: {tag}",
+  "tag_feature_description": "تصفح الصور ومقاطع الفيديو المجمعة حسب مواضيع العلامات المنطقية",
+  "tag_not_found_question": "لا يمكن العثور على علامة؟ <link>قم بإنشاء علامة جديدة.</link>",
+  "tag_updated": "تم تحديث العلامة: {tag}",
+  "tagged_assets": "تم وضع علامة {count, plural, one {# asset} other {# assets}}",
+  "tags": "العلامات",
   "template": "النموذج",
   "theme": "مظهر",
   "theme_selection": "اختيار السمة",
   "theme_selection_description": "قم بتعيين السمة تلقائيًا على اللون الفاتح أو الداكن بناءً على تفضيلات نظام المتصفح الخاص بك",
   "they_will_be_merged_together": "سيتم دمجهم معًا",
+  "third_party_resources": "موارد الطرف الثالث",
   "time_based_memories": "ذكريات استنادًا للوقت",
+  "timeline": "الخط الزمني",
   "timezone": "المنطقة الزمنية",
   "to_archive": "أرشفة",
   "to_change_password": "تغيير كلمة المرور",
   "to_favorite": "تفضيل",
   "to_login": "تسجيل الدخول",
+  "to_parent": "انتقل إلى الوالد",
   "to_trash": "حذف",
   "toggle_settings": "الإعدادات",
-  "toggle_theme": "تبديل السمة",
-  "toggle_visibility": "تبديل الرؤية",
+  "toggle_theme": "تبديل المظهر الداكن",
+  "total": "الإجمالي",
   "total_usage": "الاستخدام الإجمالي",
   "trash": "المهملات",
   "trash_all": "نقل الكل إلى سلة المهملات",
@@ -1259,14 +1245,13 @@
   "trashed_items_will_be_permanently_deleted_after": "سيتم حذفُ العناصر المحذوفة نِهائيًا بعد {days, plural, one {# يوم} other {# أيام }}.",
   "type": "النوع",
   "unarchive": "أخرج من الأرشيف",
-  "unarchived": "",
   "unarchived_count": "{count, plural, other {غير مؤرشفة #}}",
   "unfavorite": "أزل التفضيل",
   "unhide_person": "أظهر الشخص",
   "unknown": "غير معروف",
-  "unknown_album": "",
   "unknown_year": "سنة غير معروفة",
   "unlimited": "غير محدود",
+  "unlink_motion_video": "إلغاء ربط فيديو الحركة",
   "unlink_oauth": "إلغاء ربط OAuth",
   "unlinked_oauth_account": "تم إلغاء ربط حساب OAuth",
   "unnamed_album": "ألبوم بلا إسم",
@@ -1295,13 +1280,13 @@
   "use_custom_date_range": "استخدم النطاق الزمني المخصص بدلاً من ذلك",
   "user": "مستخدم",
   "user_id": "معرف المستخدم",
-  "user_license_settings": "رخصة",
-  "user_license_settings_description": "ادر رخصتك",
   "user_liked": "قام {user} بالإعجاب {type, select, photo {بهذه الصورة} video {بهذا الفيديو} asset {بهذا المحتوى} other {بها}}",
   "user_purchase_settings": "الشراء",
   "user_purchase_settings_description": "إدارة عملية الشراء الخاصة بك",
   "user_role_set": "قم بتعيين {user} كـ {role}",
   "user_usage_detail": "تفاصيل استخدام المستخدم",
+  "user_usage_stats": "إحصائيات استخدام الحساب",
+  "user_usage_stats_description": "عرض إحصائيات استخدام الحساب",
   "username": "اسم المستخدم",
   "users": "المستخدمين",
   "utilities": "أدوات",
@@ -1309,7 +1294,9 @@
   "variables": "المتغيرات",
   "version": "الإصدار",
   "version_announcement_closing": "صديقك، أليكس",
-  "version_announcement_message": "مرحباً يا صديقي، هنالك نسخة جديدة من التطبيق. خذ وقتك لزيارة <link>ملاحظات الإصدار</link> والتأكد من أن ملف <code>docker-compose.yml</code> وإعداد <code>.env</code> مُحدّثين لتجنب أي إعدادات خاطئة، خاصةً إذا كنت تستخدم WatchTower أو أي آلية تقوم بتحديث التطبيق تلقائياً.",
+  "version_announcement_message": "مرحبًا! يتوفر إصدار جديد من Immich. يُرجى تخصيص بعض الوقت لقراءة <link>ملاحظات الإصدار</link> للتأكد من تحديث إعداداتك لمنع أي أخطاء في التكوين، خاصة إذا كنت تستخدم WatchTower أو أي آلية تتولى تحديث مثيل Immich الخاص بك تلقائيًا.",
+  "version_history": "تاريخ الإصدار",
+  "version_history_item": "تم تثبيت {version} في {date}",
   "video": "فيديو",
   "video_hover_setting": "تشغيل الصورة المصغرة للفيديو عند التمرير",
   "video_hover_setting_description": "تشغيل الصورة المصغرة للفيديو عند تحريك الماوس فوق العنصر. حتى عند التعطيل، يمكن بدء التشغيل عن طريق التمرير فوق رمز التشغيل.",
@@ -1319,11 +1306,12 @@
   "view_album": "عرض الألبوم",
   "view_all": "عرض الكل",
   "view_all_users": "عرض كافة المستخدمين",
+  "view_in_timeline": "عرض في الجدول الزمني",
   "view_links": "عرض الروابط",
+  "view_name": "عرض",
   "view_next_asset": "عرض المحتوى التالي",
   "view_previous_asset": "عرض المحتوى السابق",
   "view_stack": "عرض التكديس",
-  "viewer": "",
   "visibility_changed": "الرؤية تغيرت لـ {count, plural, one {شخص واحد} other {# عدة أشخاص}}",
   "waiting": "في الانتظار",
   "warning": "تحذير",
diff --git a/i18n/az.json b/i18n/az.json
new file mode 100644
index 0000000000..7848462414
--- /dev/null
+++ b/i18n/az.json
@@ -0,0 +1,92 @@
+{
+  "about": "Yenilə",
+  "account": "Hesab",
+  "account_settings": "Hesab parametrləri",
+  "acknowledge": "Təsdiq et",
+  "action": "Əməliyyat",
+  "actions": "Əməliyyatlar",
+  "active": "Aktiv",
+  "activity": "Fəaliyyət",
+  "add": "Əlavə et",
+  "add_a_description": "Təsviri əlavə et",
+  "add_a_location": "Məkan əlavə et",
+  "add_a_name": "Ad əlavə et",
+  "add_a_title": "Başlıq əlavə et",
+  "add_exclusion_pattern": "İstisna nümunəsi əlavə et",
+  "add_import_path": "Import yolunu əlavə et",
+  "add_location": "Məkanı əlavə et",
+  "add_more_users": "Daha çox istifadəçi əlavə et",
+  "add_partner": "Partnyor əlavə et",
+  "add_path": "Yol əlavə et",
+  "add_photos": "Şəkilləri əlavə et",
+  "add_to": "... əlavə et",
+  "add_to_album": "Albom əlavə et",
+  "add_to_shared_album": "Paylaşılan alboma əlavə et",
+  "added_to_archive": "Arxivə əlavə edildi",
+  "added_to_favorites": "Sevimlilələrə əlavə edildi",
+  "added_to_favorites_count": "{count, number} şəkil sevimlilələrə əlavə edildi",
+  "admin": {
+    "authentication_settings": "Səlahiyyətləndirmə parametrləri",
+    "authentication_settings_description": "Şifrə, OAuth və digər səlahiyyətləndirmə parametrləri",
+    "authentication_settings_disable_all": "Bütün giriş etmə metodlarını söndürmək istədiyinizdən əminsinizmi? Giriş etmə funksiyası tamamilə söndürüləcəkdir.",
+    "authentication_settings_reenable": "Yenidən aktiv etmək üçün <link> Server Əmri</link> -ni istifadə edin.",
+    "background_task_job": "Arxa plan tapşırıqları",
+    "backup_database_enable_description": "Verilənlər bazasının ehtiyat nüsxələrini aktiv et",
+    "backup_settings": "Ehtiyat Nüsxə Parametrləri",
+    "backup_settings_description": "Verilənlər bazasının ehtiyat nüsxə parametrlərini idarə et",
+    "check_all": "Hamısını yoxla",
+    "config_set_by_file": "Konfiqurasiya hal-hazırda konfiqurasiya faylı ilə təyin olunub",
+    "confirm_delete_library": "{library} kitabxanasını silmək istədiyinizdən əminmisiniz?",
+    "confirm_email_below": "Təsdiqləmək üçün aşağıya {email} yazın",
+    "confirm_user_password_reset": "{user} adlı istifadəçinin şifrəsini sıfırlamaq istədiyinizdən əminmisiniz?",
+    "disable_login": "Giriş etməni söndür",
+    "duplicate_detection_job_description": "Bənzər şəkilləri tapmaq üçün maşın öyrənməsini işə salın. Bu prosses Smart Search funksiyasına əsaslanır",
+    "external_library_created_at": "Xarici kitabxana ({date} (tarixində yaradıldı)",
+    "external_library_management": "Xarici kitabxana idarəetməsi",
+    "face_detection": "Üz tanıma",
+    "force_delete_user_warning": "XƏBƏRDARLIQ: Bu əməliyyat istifadəçi və bütün məlumatları siləcəkdir. Bu prossesi və silinən faylları geri qaytarmaq olmaz.",
+    "forcing_refresh_library_files": "Bütün kitabxana fayllarını məcburi yeniləmə",
+    "image_format_description": "WebP, JPEG faylına görə daha kiçik həcmə sahibdir, lakin onu kodlaşdırmaq daha çox vaxt alır.",
+    "image_preview_title": "Önizləmə parametrləri",
+    "image_quality": "Keyfiyyət",
+    "image_resolution": "Çözümlülük",
+    "image_resolution_description": "Yüksək çözümlülükdə daha çox detallar vardır, lakin onları kodlaşdırmaq da daha çox vaxt alır, daha böyük həcmə sahib olurlar və tətbiqin işləmə sürətini yavaşladır.",
+    "image_settings": "Şəklin parametrləri",
+    "image_settings_description": "Hazırlanan şəkillərin keyfiyyətini və çözümlülüyünü idarə et",
+    "image_thumbnail_title": "Önizləmə parametrləri",
+    "job_concurrency": "{job}paralellik",
+    "job_created": "Tapşırıq yaradıldı",
+    "job_not_concurrency_safe": "Bu tapşırıq parallel fəaliyyət üçün uyğun deyil",
+    "job_settings": "Tapşırıq parametrləri",
+    "job_settings_description": "Parallel şəkildə fəaliyyət göstərən tapşırıqları idarə et",
+    "job_status": "Tapşırıq statusu",
+    "jobs_delayed": "{jobCount, plural, other {# gecikməli}}",
+    "jobs_failed": "{jobCount, plural, other {# uğursuz}}",
+    "library_created": "{library} kitabxanası yaradıldı",
+    "library_deleted": "Kitabxana silindi",
+    "library_import_path_description": "İdxal olunacaq qovluöu seçin. Bu qovluq, alt qovluqlar daxil olmaqla şəkil və videolar üçün skan ediləcəkdir.",
+    "library_scanning": "Periodik skan",
+    "library_scanning_description": "Periodik kitabxana skanını confiqurasiya et",
+    "library_scanning_enable_description": "Periodik kitabxana skanını aktivləşdir",
+    "library_settings": "Xarici kitabxana",
+    "library_settings_description": "Xarici kitabxana parametrlərini idarə et",
+    "library_tasks_description": "Kitabxana tapşırıqlarını yerinə yetir",
+    "library_watching_enable_description": "Fayl dəyişiklikləri üçün xarici kitabxanalara baxış keçirin",
+    "library_watching_settings": "Kitabxana nəzarəti (EKSPERİMENTAL)",
+    "library_watching_settings_description": "Dəyişdirilən faylları avtomatik olaraq yoxla",
+    "logging_enable_description": "Jurnalı aktivləşdir",
+    "logging_level_description": "Aktiv edildikdə hansı jurnal səviyyəsi istifadə olunur.",
+    "logging_settings": "",
+    "machine_learning_clip_model": "CLIP modeli",
+    "machine_learning_clip_model_description": "<link>Burada</link>qeyd olunan CLIP modelinin adı. Modeli dəyişdirdikdən sonra bütün şəkillər üçün 'Ağıllı Axtarış' funksiyasını yenidən işə salmalısınız.",
+    "machine_learning_duplicate_detection": "Dublikat Aşkarlama",
+    "machine_learning_duplicate_detection_enabled": "Dublikat aşkarlamanı aktiv etmək",
+    "machine_learning_duplicate_detection_enabled_description": "Əgər deaktiv edilibsə, birə-bir eyni fayllar yenədə silinəcək.",
+    "machine_learning_duplicate_detection_setting_description": "Bir-birinin dublikatı olan faylları tapmaq üçün CLIP-dən istifadə edin",
+    "machine_learning_enabled": "Maşın öyrənməsini aktiv edin",
+    "machine_learning_enabled_description": "Əgər deaktiv edilərsə, aşağıdakı parametrlərdən asılı olmayaq, bütün Maşın Öyrənmə funksiyaları deaktiv ediləcək.",
+    "machine_learning_facial_recognition": "Üz Tanıma",
+    "machine_learning_facial_recognition_description": "Şəkillərdəki üzləri aşkarla, tanı və qruplaşdır",
+    "machine_learning_facial_recognition_model": "Üz tanıma modeli"
+  }
+}
diff --git a/i18n/be.json b/i18n/be.json
new file mode 100644
index 0000000000..8377ec5383
--- /dev/null
+++ b/i18n/be.json
@@ -0,0 +1,115 @@
+{
+  "about": "Аб",
+  "account": "Уліковы запіс",
+  "account_settings": "Налады ўліковага запісу",
+  "acknowledge": "Пацвердзіць",
+  "action": "Дзеянне",
+  "actions": "Дзеянні",
+  "active": "Актыўны",
+  "activity": "Актыўнасць",
+  "activity_changed": "Актыўнасць {enabled, select, true {уключана} other {адключана}}",
+  "add": "Дадаць",
+  "add_a_description": "Дадаць апісанне",
+  "add_a_location": "Дадаць месца",
+  "add_a_name": "Дадаць імя",
+  "add_a_title": "Дадаць загаловак",
+  "add_exclusion_pattern": "Дадаць шаблон выключэння",
+  "add_import_path": "Дадаць шлях імпарту",
+  "add_location": "Дадайце месца",
+  "add_more_users": "Дадаць больш карыстальнікаў",
+  "add_partner": "Дадаць партнёра",
+  "add_path": "Дадаць шлях",
+  "add_photos": "Дадаць фота",
+  "add_to": "Дадаць у...",
+  "add_to_album": "Дадаць у альбом",
+  "add_to_shared_album": "Дадаць у агульны альбом",
+  "add_url": "Дадаць URL",
+  "added_to_archive": "Дададзена ў архіў",
+  "added_to_favorites": "Дададзена ў абраныя",
+  "added_to_favorites_count": "Дададзена {count, number} да абранага",
+  "admin": {
+    "add_exclusion_pattern_description": "Дадайце шаблоны выключэнняў. Падтрымліваецца выкарыстанне сімвалаў * , ** і ?. Каб ігнараваць усе файлы ў любой дырэкторыі з назвай \"Raw\", выкарыстоўвайце \"**/Raw/**\". Каб ігнараваць усе файлы, якія заканчваюцца на \".tif\", выкарыстоўвайце \"**/.tif\". Каб ігнараваць абсолютны шлях, выкарыстоўвайце \"/path/to/ignore/**\".",
+    "asset_offline_description": "Гэты знешні бібліятэчны актыў больш не знойдзены на дыску і быў перамешчаны ў сметніцу. Калі файл быў перамешчаны ў межах бібліятэкі, праверце вашу хроніку для новага адпаведнага актыва. Каб аднавіць гэты актыў, пераканайцеся, што шлях да файла ніжэй даступны для Immich і адскануйце бібліятэку.",
+    "authentication_settings": "Налады праверкі сапраўднасці",
+    "authentication_settings_description": "Кіраванне паролямі, OAuth, і іншыя налады праверкі сапраўднасці",
+    "authentication_settings_disable_all": "Вы ўпэўнены, што жадаеце адключыць усе спосабы логіну? Логін будзе цалкам адключаны.",
+    "authentication_settings_reenable": "Каб зноў уключыць, выкарыстайце <link>Каманду сервера</link>.",
+    "background_task_job": "Фонавыя заданні",
+    "backup_database": "Рэзервовая копія базы даных",
+    "backup_database_enable_description": "Уключыць рэзерваванне базы даных",
+    "backup_keep_last_amount": "Колькасць папярэдніх рэзервовых копій для захавання",
+    "backup_settings": "Налады рэзервовага капіявання",
+    "backup_settings_description": "Кіраванне наладкамі рэзервовага капіявання базы даных",
+    "check_all": "Праверыць усе",
+    "cleared_jobs": "Ачышчаны заданні для: {job}",
+    "config_set_by_file": "Канфігурацыя ў зараз усталявана праз файл канфігурацыі",
+    "confirm_delete_library": "Вы ўпэўнены што жадаеце выдаліць {library} бібліятэку?",
+    "confirm_delete_library_assets": "Вы ўпэўнены, што хочаце выдаліць гэтую бібліятэку? Гэта прывядзе да выдалення {count, plural, one {# актыву} other {усіх # актываў}}, якія змяшчаюцца ў Immich, і гэта дзеянне немагчыма будзе адмяніць. Файлы застануцца на дыску.",
+    "confirm_email_below": "Каб пацвердзіць, увядзіце \"{email}\" ніжэй",
+    "confirm_reprocess_all_faces": "Вы ўпэўнены, што хочаце пераапрацаваць усе твары? Гэта таксама прывядзе да выдалення імя людзей.",
+    "confirm_user_password_reset": "Вы ўпэўнены ў тым, што жадаеце скінуць пароль {user}?",
+    "create_job": "Стварыць заданне",
+    "cron_expression": "Выраз Cron",
+    "cron_expression_description": "Усталюйце інтэрвал сканавання, выкарыстоўваючы фармат cron. Для атрымання дадатковай інфармацыі, калі ласка, звярніцеся, напрыклад, да <link>Crontab Guru</link>",
+    "cron_expression_presets": "Прадустановкі выразаў Cron",
+    "disable_login": "Адключыць уваход",
+    "duplicate_detection_job_description": "Запусціць машыннае навучанне на актывах для выяўлення падобных выяў. Залежыць ад Smart Search",
+    "exclusion_pattern_description": "Шаблоны выключэння дазваляюць ігнараваць файлы і папкі пры сканаванні вашай бібліятэкі. Гэта карысна, калі ў вас ёсць папкі, якія змяшчаюць файлы, якія вы не хочаце імпартаваць, напрыклад, файлы RAW.",
+    "external_library_created_at": "Знешняя бібліятэка (створана {date})",
+    "external_library_management": "Кіраванне знешняй бібліятэкай",
+    "face_detection": "Выяўленне твараў",
+    "force_delete_user_warning": "ПАПЯРЭДЖАННЕ: Гэта дзеянне неадкладна выдаліць карыстальніка і ўсе аб'екты. Гэта дзеянне не можа быць адроблена і файлы немагчыма будзе аднавіць.",
+    "image_format": "Фармат",
+    "image_preview_title": "Налады папярэдняга прагляду",
+    "image_quality": "Якасць",
+    "image_resolution": "Раздзяляльнасць",
+    "image_settings": "Налады відарыса",
+    "image_settings_description": "Кіруйце якасцю і раздзяляльнасцю сгенерыраваных відарысаў"
+  },
+  "timeline": "Хроніка",
+  "total": "Усяго",
+  "user": "Карыстальнік",
+  "user_id": "ID карыстальніка",
+  "user_purchase_settings": "Купля",
+  "user_purchase_settings_description": "Кіруйце пакупкамі",
+  "user_role_set": "Прызначыць {user} як {role}",
+  "user_usage_detail": "Падрабязнасці выкарыстання карыстальнікам",
+  "user_usage_stats": "Статыстыка карыстання ўліковага запісу",
+  "user_usage_stats_description": "Прагледзець статыстыку карыстання ўліковага запісу",
+  "username": "Імя карыстальніка",
+  "users": "Карыстальнікі",
+  "utilities": "Утыліты",
+  "validate": "Праверыць",
+  "variables": "Пераменныя",
+  "version": "Версія",
+  "version_announcement_closing": "Твой сябар, Алекс",
+  "version_announcement_message": "Вітаем! Даступная новая версія Immich. Калі ласка, знайдзіце час, каб прачытаць <link>нататкі да выпуску</link>, каб пераканацца, што ваша налада актуальная і пазбегнуць магчымых памылак канфігурацыі, асабліва калі вы карыстаецеся WatchTower або іншымі механізмамі, якія аўтаматычна абнаўляюць вашу інстанцыю Immich.",
+  "version_history": "Гісторыя версій",
+  "version_history_item": "Усталявана версія {version} на {date}",
+  "video": "Відэа",
+  "video_hover_setting": "Прайграванне мініяцюры відэа пры навядзенні курсора",
+  "video_hover_setting_description": "Прайграванне мініяцюры відэа пры навядзенні курсора на элемент. Нават калі функцыя адключана, прайграванне можна пачаць, навёўшы курсор на значок прайгравання.",
+  "videos": "Відэа",
+  "videos_count": "{count, plural, one {# відэа} астатнія {# відэа}}",
+  "view": "Прагляд",
+  "view_album": "Праглядзець альбом",
+  "view_all": "Праглядзець усё",
+  "view_all_users": "Праглядзець усех карыстальнікаў",
+  "view_in_timeline": "Паглядзець хроніку",
+  "view_links": "Праглядзець спасылкі",
+  "view_name": "Прагледзець",
+  "view_next_asset": "Паказаць наступны аб'ект",
+  "view_previous_asset": "Праглядзець папярэдні аб'ект",
+  "view_stack": "Прагляд стэка",
+  "visibility_changed": "Відзімасць змянілася для {count, plural, one {# чалавек(-аў)} астатніх {# чалавек}}",
+  "waiting": "Чакаюць",
+  "warning": "Папярэджанне",
+  "week": "Тыдзень",
+  "welcome": "Вітаем",
+  "welcome_to_immich": "Вітаем у Immich",
+  "year": "Год",
+  "years_ago": "{years, plural, one {# год} other {# гадоў}} таму",
+  "yes": "Так",
+  "you_dont_have_any_shared_links": "У вас няма абагуленых спасылак",
+  "zoom_image": "Павялічыць відарыс"
+}
diff --git a/web/src/lib/i18n/bg.json b/i18n/bg.json
similarity index 81%
rename from web/src/lib/i18n/bg.json
rename to i18n/bg.json
index 492a888f62..b0ab195289 100644
--- a/web/src/lib/i18n/bg.json
+++ b/i18n/bg.json
@@ -12,27 +12,34 @@
   "add_a_description": "Добави описание",
   "add_a_location": "Добави местоположение",
   "add_a_name": "Добави име",
-  "add_a_title": "Добави заглавие",
+  "add_a_title": "Добавете заглавие",
   "add_exclusion_pattern": "Добави модел за изключване",
   "add_import_path": "Добави път за импортиране",
-  "add_location": "Добави местоположение",
-  "add_more_users": "Добави още потребители",
-  "add_partner": "Добави партньор",
+  "add_location": "Добавете местоположение",
+  "add_more_users": "Добавете още потребители",
+  "add_partner": "Добавете партньор",
   "add_path": "Добави път",
-  "add_photos": "Добави снимки",
+  "add_photos": "Добавете снимки",
   "add_to": "Добави към...",
   "add_to_album": "Добави към албум",
   "add_to_shared_album": "Добави към споделен албум",
-  "added_to_archive": "Добавено в архива",
-  "added_to_favorites": "Добавено към любими",
+  "add_url": "Добави URL",
+  "added_to_archive": "Добавено към архива",
+  "added_to_favorites": "Добавени към любимите ви",
   "added_to_favorites_count": "Добавени {count, number} към любими",
   "admin": {
     "add_exclusion_pattern_description": "Добави модели за изключване. Поддържа се \"globbing\" с помощта на *, ** и ?. За да игнорирате всички файлове в директория с име \"Raw\", използвайте \"**/Raw/**\". За да игнорирате всички файлове, завършващи на \".tif\", използвайте \"**/*.tif\". За да игнорирате абсолютен път, използвайте \"/path/to/ignore/**\".",
+    "asset_offline_description": "Този външен библиотечен елемент не може да бъде открит на диска и е преместен в кошчето за боклук. Ако файлът е преместен в библиотеката, проверете вашата история за нов съответстващ елемент. За да възстановите елемента, моля проверете дали файловият път отдолу може да бъде достъпен от Immich и сканирайте библиотеката.",
     "authentication_settings": "Настройки за удостоверяване",
     "authentication_settings_description": "Управление на парола, OAuth и други настройки за удостоверяване",
     "authentication_settings_disable_all": "Сигурни ли сте, че искате да деактивирате всички методи за вписване? Вписването ще бъде напълно деактивирано.",
     "authentication_settings_reenable": "За да реактивирате, изполвайте <link>Server Command</link>.",
     "background_task_job": "Процеси на заден фон",
+    "backup_database": "Резервна База данни",
+    "backup_database_enable_description": "Разрешаване на резервни копия на базата данни",
+    "backup_keep_last_amount": "Брой резервни копия за запазване",
+    "backup_settings": "Настройка на резервни копия",
+    "backup_settings_description": "Управление на настройките за резервно копие на базата данни",
     "check_all": "Провери всичко",
     "cleared_jobs": "Изчистени задачи от тип: {job}",
     "config_set_by_file": "Конфигурацията е зададена от файл",
@@ -41,6 +48,10 @@
     "confirm_email_below": "За потвърждение, моля въведете \"{email}\" отдолу",
     "confirm_reprocess_all_faces": "Сигурни ли сте, че искате да се обработят лицата отново? Това ще изчисти всички именувани хора.",
     "confirm_user_password_reset": "Сигурни ли сте, че искате да нулирате паролата на {user}?",
+    "create_job": "Създайте задача",
+    "cron_expression": "Cron израз",
+    "cron_expression_description": "Настрой интервала на сканиране използвайки cron формата. За повече информация <link>Crontab Guru</link>",
+    "cron_expression_presets": "Примерни Cron изрази",
     "disable_login": "Изключете вписването",
     "duplicate_detection_job_description": "Стартиране машинно обучение на ресурси, за откриване на подобни изображения. Разчита на Интелигентно Търсене",
     "exclusion_pattern_description": "Модели за изключване позволяват да игнорирате файлове и папки, когато сканирате вашата библиотека. Това е потребно, ако имате папки, които съдържат файлове, които не искате да импортирате. Примерно - RAW файлове.",
@@ -52,22 +63,25 @@
     "failed_job_command": "Командата {command} е неуспешна за задача: {job}",
     "force_delete_user_warning": "ВНИМАНИЕ: Това веднага ще изтрие потребителя и всичките му ресурси. Действието е необратимо и файловете не могат да бъдат възстановени.",
     "forcing_refresh_library_files": "Принуждаване обновяване на всички файлове в библиотеката",
+    "image_format": "Формат",
     "image_format_description": "WebP създава по-малки файлове от JPEG, но ги кодира по-бавно.",
     "image_prefer_embedded_preview": "Предпочитане на вградените прегледи",
     "image_prefer_embedded_preview_setting_description": "Използване на вградените прегледи в RAW снимките като вход за обработка на изображенията, когато има такива. Това може да доведе до по-точни цветове за някои изображения, но качеството на прегледите зависи от камерата и изображението може да има повече компресионни артефакти.",
     "image_prefer_wide_gamut": "Предпочитане на широка гама",
     "image_prefer_wide_gamut_setting_description": "Използване на Display P3 за миниатюри. Това запазва по-добре жизнеността на изображенията с широки цветови пространства, но изображенията може да изглеждат по различен начин на стари устройства със стара версия на браузъра. sRGB изображенията се запазват като sRGB, за да се избегнат цветови промени.",
-    "image_preview_format": "Формат на прегледите",
-    "image_preview_resolution": "Резолюция на прегледите",
-    "image_preview_resolution_description": "Използва се при разглеждане на единична снимка и за машинно обучение. По-високите резолюции могат да запазят повече детайли, но отнемат повече време за кодиране, имат по-големи размери на файловете и могат да намалят отзивчивостта на приложението.",
+    "image_preview_description": "Среден размер на изображението с премахнати метаданни, използвано при преглед на един актив и за машинно обучение",
+    "image_preview_quality_description": "Качество на предварителния преглед от 1 до 100. По-високата стойност е по-добра, но води до по-големи файлове и може да намали бързодействието на приложението. Задаването на ниска стойност може да повлияе на качеството на машинното обучение.",
+    "image_preview_title": "Настойки на прегледа",
     "image_quality": "Качество",
-    "image_quality_description": "Качество на изображението от 1-100. По-голяма стойност води до по-добро качество, но създава по-големи файлове. Тази настройка засяга изображенията от тип преглед и миниатюра.",
+    "image_resolution": "Резолюция",
+    "image_resolution_description": "По-високите резолюции могат да запазят повече детайли, но изискват повече време за кодиране, имат по-големи размери на файловете и могат да намалят бързодействието на приложението.",
     "image_settings": "Настройки за изображенията",
     "image_settings_description": "Управляване качеството и резолюцията на създадените изображения",
-    "image_thumbnail_format": "Формат на миниатюрните изображения",
-    "image_thumbnail_resolution": "Резолюция на миниатюрните изображения",
-    "image_thumbnail_resolution_description": "Използва се при разглеждане на групи от снимки (основна времева линия, изглед на албум и др.). По-високите резолюции могат да запазят повече детайли, но отнемат повече време за кодиране, имат по-големи размери на файловете и могат да намалят отзивчивостта на приложението.",
+    "image_thumbnail_description": "Малка миниатюра с премахнати метаданни, използвана при преглед на групи снимки, като основния екран",
+    "image_thumbnail_quality_description": "Качество на миниатюрата от 1 до 100. По-високата стойност е по-добра, но води до по-големи файлове и може да намали бързодействието на приложението.",
+    "image_thumbnail_title": "Настройки на миниатюрите",
     "job_concurrency": "Паралелност на {job}",
+    "job_created": "Задачата е създадена",
     "job_not_concurrency_safe": "Тази задача не е безопасна за паралелно изпълнение.",
     "job_settings": "Настройки за задачите",
     "job_settings_description": "Управление на паралелността на задачите",
@@ -75,9 +89,6 @@
     "jobs_delayed": "{jobCount, plural, other {# delayed}}",
     "jobs_failed": "{jobCount, plural, other {# failed}}",
     "library_created": "Създадена библиотека: {library}",
-    "library_cron_expression": "Cron израз",
-    "library_cron_expression_description": "Задайте интервала за сканиране чрез cron интервал. За повече информация, вижте например <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Предварителни настройки на Cron израза",
     "library_deleted": "Библиотека е изтрита",
     "library_import_path_description": "Посочете папка за импортиране. Тази папка, включително подпапките, ще бъдат сканирани за изображения и видеоклипове.",
     "library_scanning": "Периодично сканиране",
@@ -120,7 +131,7 @@
     "machine_learning_smart_search_description": "Семантично търсене на изображения с помощта на CLIP вграждания",
     "machine_learning_smart_search_enabled": "Включване на Интелигентно Търсене",
     "machine_learning_smart_search_enabled_description": "Ако е деактивирано, изображенията няма да бъдат кодирани за Интелигентно Търсене.",
-    "machine_learning_url_description": "URL адрес на сървъра за машинно обучение",
+    "machine_learning_url_description": "URL на сървъра за машинно обучение. Ако са предоставени повече от един URL, всеки сървър ще бъде опитан един по един, докато един не отговори успешно, в реда от първия до последния",
     "manage_concurrency": "Управление на паралелност",
     "manage_log_settings": "Управление на настройките на записване",
     "map_dark_style": "Тъмен стил",
@@ -137,7 +148,7 @@
     "map_settings_description": "Управление на настройките на картата",
     "map_style_description": "URL адрес към файл \"style.json\" за задаване на стил на картата",
     "metadata_extraction_job": "Извличане на метаданни",
-    "metadata_extraction_job_description": "Извличане на метаданни от всеки ресурс, като GPS и резолюция",
+    "metadata_extraction_job_description": "Извличане на метаданни от всеки от ресурсите, като GPS локация, лица и резолюция на файловете",
     "metadata_faces_import_setting": "Включи импорт на лице",
     "metadata_faces_import_setting_description": "Импортирай лица от EXIF данни и помощни файлове",
     "metadata_settings": "Опции за метаданни",
@@ -150,7 +161,7 @@
     "note_cannot_be_changed_later": "ВНИМАНИЕ: Това не може да бъде променено по-късно!",
     "note_unlimited_quota": "Бележка: Въведете 0 за да нямате лимит на квотата",
     "notification_email_from_address": "От адрес",
-    "notification_email_from_address_description": "Електронна поща на изпращача, например: \"Immich Photo Server <noreply@immich.app>\"",
+    "notification_email_from_address_description": "Електронна поща на изпращача, например: \"Immich Photo Server <noreply@example.com>\"",
     "notification_email_host_description": "Хост на сървъра за електронна поща (например: smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Игорнорирайте сертификационни грешки",
     "notification_email_ignore_certificate_errors_description": "Игнорирай грешки свързани с валидация на TLS сертификат (не се препоръчва)",
@@ -176,7 +187,7 @@
     "oauth_issuer_url": "URL на издателя",
     "oauth_mobile_redirect_uri": "URI за мобилно пренасочване",
     "oauth_mobile_redirect_uri_override": "URI пренасочване за мобилни устройства",
-    "oauth_mobile_redirect_uri_override_description": "Разреши когато 'app.immich:/' е невалиден пренасочвар адрес/URI.",
+    "oauth_mobile_redirect_uri_override_description": "Разреши когато доставчика за OAuth удостоверяване не позволява за мобилни URI идентификатори, като '{callback}'",
     "oauth_profile_signing_algorithm": "Алгоритъм за създаване на профили",
     "oauth_profile_signing_algorithm_description": "Алгоритъм излпозлван за вписване на потребителски профил.",
     "oauth_scope": "Област/обхват на приложение",
@@ -196,22 +207,24 @@
     "password_settings": "Вписване с парола",
     "password_settings_description": "Управление на настройките за влизане с парола",
     "paths_validated_successfully": "Всички пътища са валидирани успешно",
+    "person_cleanup_job": "Почистване на лица",
     "quota_size_gib": "Размер на квотата (GiB)",
     "refreshing_all_libraries": "Опресняване на всички библиотеки",
     "registration": "Администраторска регистрация",
     "registration_description": "Тъй като сте първият потребител в системата, ще бъдете назначен като администратор и ще отговаряте за административните задачи, а допълнителните потребители ще бъдат създадени от вас.",
-    "removing_offline_files": "Премахване на офлайн файлове",
     "repair_all": "Поправяне на всичко",
     "repair_matched_items": "{count, plural, one {Съвпадащ елемент (#)} other {Съвпадащи елементи (#)}}",
     "repaired_items": "{count, plural, one {Поправен елемент (#)} other {Поправени елементи (#)}}",
     "require_password_change_on_login": "Изискване за промяна паролата при първо влизане",
     "reset_settings_to_default": "Възстановяване на настройките по подразбиране",
     "reset_settings_to_recent_saved": "Възстановяване на настройките до последните запазени настройки",
-    "scanning_library_for_changed_files": "Сканиране на библиотеката за променени файлове",
-    "scanning_library_for_new_files": "Сканиране на библиотеката за нови файлове",
+    "scanning_library": "Сканиране на библиотеката",
+    "search_jobs": "Търсене на задачи...",
     "send_welcome_email": "Изпращане на имейл за добре дошли",
     "server_external_domain_settings": "Външен домейн",
     "server_external_domain_settings_description": "Домейн за публични споделени връзки, включително http(s)://",
+    "server_public_users": "Публични потребители",
+    "server_public_users_description": "Всички потребители (име и имейл) са изброени при добавяне на потребител в споделени албуми. Когато е деактивирано, списъкът с потребители ще бъде достъпен само за администраторите.",
     "server_settings": "Настройки на сървъра",
     "server_settings_description": "Управление на настройките на сървъра",
     "server_welcome_message": "Поздравително съобщение",
@@ -230,12 +243,23 @@
     "storage_template_migration_info": "Промените в шаблоните ще се прилагат само за нови ресурси. За да приложите шаблона със задна дата към предварително качени активи, изпълнете <link>{job}</link>.",
     "storage_template_migration_job": "Задача за миграция на шаблона за съхранение",
     "storage_template_more_details": "За повече подробности относно тази функция се обърнете към шаблона <template-link>Storage Template</template-link> и неговите <implications-link> последствия </implications-link>",
-    "storage_template_onboarding_description": "Когато е активирана, тази функция ще организира автоматично файлове въз основа на дефиниран от потребителя шаблон. Поради проблеми със стабилността функцията е изключена по подразбиране. За повече информация, моля, вижте <link>документацията</link>.",
+    "storage_template_onboarding_description": "Когато е активирана, тази функция ще организира автоматично файлове въз основа на дефиниран от потребителя шаблон. Поради проблеми със стабилността, функцията е изключена по подразбиране. За повече информация, моля, вижте <link>документацията</link>.",
     "storage_template_path_length": "Ограничение на дължината на пътя: <b>{length, number}</b>/{limit, number}",
     "storage_template_settings": "Шаблон за съхранение",
     "storage_template_settings_description": "Управление на структурата на папките и името на файла за качване",
     "storage_template_user_label": "<code>{label}</code> е етикетът за съхранение на потребителя",
     "system_settings": "Системни настройки",
+    "tag_cleanup_job": "Почистване на тагове",
+    "template_email_available_tags": "Можете да използвате следните променливи в шаблона си: {tags}",
+    "template_email_if_empty": "Ако шаблонът е празен, ще се използва имейлът по подразбиране.",
+    "template_email_invite_album": "Шаблон за покана за албум",
+    "template_email_preview": "Преглед",
+    "template_email_settings": "Шаблони за имейли",
+    "template_email_settings_description": "Управление на шаблони за имейл известия",
+    "template_email_update_album": "Шаблон за актуализация на албум",
+    "template_email_welcome": "Шаблон за приветстващ имейл",
+    "template_settings": "Шаблони за известия",
+    "template_settings_description": "Управление на шаблони за известия.",
     "theme_custom_css_settings": "Персонализиран CSS",
     "theme_custom_css_settings_description": "Каскадните стилови таблици позволяват персонализиране на дизайна на Immich.",
     "theme_settings": "Настройки на темата",
@@ -244,7 +268,7 @@
     "thumbnail_generation_job": "Генериране на миниатюри",
     "thumbnail_generation_job_description": "Генерирайте големи, малки и замъглени миниатюри за всеки актив, както и миниатюри за всеки човек",
     "transcoding_acceleration_api": "API за ускоряване",
-    "transcoding_acceleration_api_description": "API, който ще взаимодейства с вашето устройство, за да ускори транскодирането. Тази настройка е „best effort“: тя ще се върне към софтуерно транскодиране при повреда. VP9 може или не може да работи в зависимост от вашия хардуер.",
+    "transcoding_acceleration_api_description": "API интерфейсът, който ще взаимодейства с вашето устройство, за да ускори транскодирането. Тази настройка е „възможно най-доброто“: тя ще се върне към софтуерно транскодиране при повреда. VP9 може и да не работи в зависимост от вашия хардуер.",
     "transcoding_acceleration_nvenc": "NVENC (необходим NVIDIA GPU)",
     "transcoding_acceleration_qsv": "Quick Sync (необходим 7th поколение Intel CPU или по-ново)",
     "transcoding_acceleration_rkmpp": "RKMPP (само на Rockchip SOCs)",
@@ -252,9 +276,9 @@
     "transcoding_accepted_audio_codecs": "Допустими аудио кодеци",
     "transcoding_accepted_audio_codecs_description": "Изберете кои аудио кодеци не са нужни за разкодиране. Използва се само за определени правила за разкодиране.",
     "transcoding_accepted_containers": "Приети контейнери",
-    "transcoding_accepted_containers_description": "Изберете кои формати на контейнери не трябва да се пренасочват към MP4. Използва се само за определени правила за разкодиране.",
+    "transcoding_accepted_containers_description": "Изберете кои формати на контейнери не е нужно да бъдат преобразувани в MP4 формат. Използва се само за определени правила за разкодиране.",
     "transcoding_accepted_video_codecs": "Приети видео кодеци",
-    "transcoding_accepted_video_codecs_description": "Изберете кои видео кодеци не трябва да се разкодиране. Използва се само за определени правила за разкодиране.",
+    "transcoding_accepted_video_codecs_description": "Изберете кои видео кодеци не трябват за разкодиране. Използва се само за определени правила за разкодиране.",
     "transcoding_advanced_options_description": "Опции, които повечето потребители не трябва да променят",
     "transcoding_audio_codec": "Аудио кодек",
     "transcoding_audio_codec_description": "Opus е опцията с най-високо качество, но има по-ниска съвместимост със стари устройства или софтуер.",
@@ -292,10 +316,8 @@
     "transcoding_temporal_aq_description": "Само за NVENC. Повишава качеството на сцени с висока детайлност и ниско ниво на движение. Може да не е съвместимо с по-стари устройства.",
     "transcoding_threads": "Нишки",
     "transcoding_threads_description": "По-високите стойности водят до по-бързо разкодиране, но оставят по-малко място за сървъра да обработва други задачи, докато е активен. Тази стойност не трябва да надвишава броя на процесорните ядра. Увеличава максимално използването, ако е зададено на 0.",
-    "transcoding_tone_mapping": "",
+    "transcoding_tone_mapping": "Tone-mapping",
     "transcoding_tone_mapping_description": "Опитва се да запази външния вид на HDR видеоклипове, когато се преобразува в SDR. Всеки алгоритъм прави различни компромиси за цвят, детайлност и яркост. Hable запазва детайлите, Mobius запазва цвета, а Reinhard запазва яркостта.",
-    "transcoding_tone_mapping_npl": "",
-    "transcoding_tone_mapping_npl_description": "Цветовете ще бъдат коригирани, за да изглеждат нормално за дисплей с тази яркост. Противоинтуитивно, по-ниските стойности увеличават яркостта на видеото и обратно, тъй като компенсират яркостта на дисплея. 0 задава тази стойност автоматично.",
     "transcoding_transcode_policy": "Правила за транскодиране",
     "transcoding_transcode_policy_description": "Правила за това кога видеоклипът трябва да бъде транскодиран. HDR видеоклиповете винаги ще бъдат транскодирани (освен ако транскодирането е деактивирано).",
     "transcoding_two_pass_encoding": "Кодиране с двойно минаване",
@@ -309,6 +331,7 @@
     "trash_settings_description": "Управление на настройките на кошчето",
     "untracked_files": "Непроследени файлове",
     "untracked_files_description": "Тези файлове не се проследяват от приложението. Те могат да бъдат резултат от неуспешни премествания, прекъснати качвания или оставени поради грешка",
+    "user_cleanup_job": "Почистване на потребители",
     "user_delete_delay": "<b>{user}</b> aкаунтът и файловете на потребителя ще бъдат планирани за постоянно изтриване след {delay, plural, one {# ден} other {# дни}}.",
     "user_delete_delay_settings": "Забавяне на изтриване",
     "user_delete_delay_settings_description": "Брой дни след окончателно изтриване акаунта на потребителя. Задачата за изтриване на потребител се изпълнява в полунощ, за да се провери за потребители, които са готови за изтриване. Промените на тази настройка ще влязат в сила при следващото изпълнение.",
@@ -333,6 +356,9 @@
   "admin_password": "Администраторска парола",
   "administration": "Администрация",
   "advanced": "Разширено",
+  "age_months": "Възраст {months, plural, one {# month} other {# months}}",
+  "age_year_months": "Възраст 1 година, {months, plural, one {# month} other {# months}}",
+  "age_years": "{years, plural, other {Age #}}",
   "album_added": "Албумът е добавен",
   "album_added_notification_setting_description": "Получавайте известие по имейл, когато бъдете добавени към споделен албум",
   "album_cover_updated": "Обложката на албума е актуализирана",
@@ -352,7 +378,7 @@
   "album_user_removed": "Премахнат {user}",
   "album_with_link_access": "Нека всеки с линк вижда снимки и хора в този албум.",
   "albums": "Албуми",
-  "albums_count": "",
+  "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albums}}",
   "all": "Всички",
   "all_albums": "Всички албуми",
   "all_people": "Всички хора",
@@ -361,24 +387,44 @@
   "allow_edits": "Позволяване на редакции",
   "allow_public_user_to_download": "Позволете на публичен потребител да може да изтегля",
   "allow_public_user_to_upload": "Позволете на публичния потребител да може да качва",
+  "anti_clockwise": "Обратно на часовниковата стрелка",
   "api_key": "API ключ",
   "api_key_description": "Тази стойност ще бъде показана само веднъж. Моля, не забравяйте да го копирате, преди да затворите прозореца.",
   "api_key_empty": "Името на вашия API ключ не трябва да е празно",
   "api_keys": "API ключове",
   "app_settings": "Настройки ма приложението",
-  "appears_in": "",
+  "appears_in": "Излиза в",
   "archive": "Архив",
   "archive_or_unarchive_photo": "Архивиране или деархивиране на снимка",
   "archive_size": "Размер на архива",
   "archive_size_description": "Конфигурирайте размера на архива за изтегляния (в GiB)",
-  "archived": "",
+  "archived_count": "{count, plural, other {Archived #}}",
   "are_these_the_same_person": "Това едно и също лице ли е?",
+  "are_you_sure_to_do_this": "Сигурни ли сте, че искате да направите това?",
+  "asset_added_to_album": "Добавено в албум",
+  "asset_adding_to_album": "Добавяне в албум...",
+  "asset_description_updated": "Описание на актива е обновено",
+  "asset_filename_is_offline": "Активът {filename} е офлайн",
+  "asset_has_unassigned_faces": "Активът има неразпределени лица",
+  "asset_hashing": "Хеширане...",
   "asset_offline": "Ресурсът е офлайн",
+  "asset_offline_description": "Този външен актив вече не се намира на диска. Моля, свържете се с администратора на Immich за помощ.",
   "asset_skipped": "Пропуснато",
+  "asset_skipped_in_trash": "В кошчето",
   "asset_uploaded": "Качено",
   "asset_uploading": "Качване...",
   "assets": "Ресурси",
-  "assets_moved_to_trash": "",
+  "assets_added_count": "Добавено {count, plural, one {# asset} other {# assets}}",
+  "assets_added_to_album_count": "Добавен(и) са {count, plural, one {# актив} other {# актива}} в албума",
+  "assets_added_to_name_count": "Добавен(и) са {count, plural, one {# актив} other {# актива}} към {hasName, select, true {<b>{name}</b>} other {нов албум}}",
+  "assets_count": "{count, plural, one {# актив} other {# актива}}",
+  "assets_moved_to_trash_count": "Преместен(и) са {count, plural, one {# актив} other {# актива}} в кошчето",
+  "assets_permanently_deleted_count": "Постоянно изтрит(и) са {count, plural, one {# актив} other {# актива}}",
+  "assets_removed_count": "Премахнат(и) са {count, plural, one {# актив} other {# актива}}",
+  "assets_restore_confirmation": "Сигурни ли сте, че искате да възстановите всички активи в кошчето? Не можете да отмените това действие! Имайте предвид, че активи, които са офлайн, не могат да бъдат възстановени по този начин.",
+  "assets_restored_count": "Възстановен(и) са {count, plural, one {# актив} other {# актива}}",
+  "assets_trashed_count": "Възстановен(и) са {count, plural, one {# файл} other {# файла}}",
+  "assets_were_part_of_album_count": "{count, plural, one {Файлът е} other {Файловете са}} вече част от албума",
   "authorized_devices": "Удостоверени устройства",
   "back": "Назад",
   "back_close_deselect": "Назад, затваряне или премахване на избора",
@@ -386,9 +432,12 @@
   "birthdate_saved": "Датата на раждане е запазена успешно",
   "birthdate_set_description": "Датата на раждане се използва за изчисляване на възрастта на този човек към момента на снимката.",
   "blurred_background": "Замъглен заден фон",
-  "bulk_delete_duplicates_confirmation": "",
-  "bulk_keep_duplicates_confirmation": "",
-  "bulk_trash_duplicates_confirmation": "",
+  "bugs_and_feature_requests": "Бъгове и заявки за функции",
+  "build": "Build",
+  "build_image": "Build Image",
+  "bulk_delete_duplicates_confirmation": "Сигурни ли сте, че искате да изтриете масово {count, plural, one {# дублиран файл} other {# дублирани файла}}? Това ще запази най-големия файл от всяка група и ще изтрие трайно всички други дубликати. Не можете да отмените това действие!",
+  "bulk_keep_duplicates_confirmation": "Сигурни ли сте, че искате да запазите {count, plural, one {# дублиран файл} other {# дублирани файла}}? Това ще потвърди всички групи дубликати, без да изтрива нищо.",
+  "bulk_trash_duplicates_confirmation": "Сигурни ли сте, че искате да преместите в кошчето масово {count, plural, one {# дублиран файл} other {# дублирани файла}}? Това ще запази най-големия файл от всяка група и ще премести в кошчето всички други дубликати.",
   "buy": "Купете Immich",
   "camera": "Камера",
   "camera_brand": "Марка на камерата",
@@ -398,10 +447,6 @@
   "cannot_merge_people": "Не може да обединява хора",
   "cannot_undo_this_action": "Не можете да отмените това действие!",
   "cannot_update_the_description": "Описанието не може да бъде актуализирано",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
   "change_date": "Промени датата",
   "change_expiration_time": "Променете времето на изтичане",
   "change_location": "Промени локацията",
@@ -420,9 +465,11 @@
   "clear_all_recent_searches": "Изчистете всички скорошни търсения",
   "clear_message": "Изчисти съобщението",
   "clear_value": "Изчисти стойността",
+  "clockwise": "По часовниковата стрелка",
   "close": "Затвори",
   "collapse": "Свиване",
   "collapse_all": "Свиване на всичко",
+  "color": "Цвят",
   "color_theme": "Цветова тема",
   "comment_deleted": "Коментарът е изтрит",
   "comment_options": "Опции за коментар",
@@ -431,8 +478,9 @@
   "confirm": "Потвърди",
   "confirm_admin_password": "Потвърждаване на паролата на администратора",
   "confirm_delete_shared_link": "Сигурни ли сте, че искате да изтриете тази споделена връзка?",
+  "confirm_keep_this_delete_others": "Всички останали файлове в стека ще бъдат изтрити, с изключение на този файл. Сигурни ли сте, че искате да продължите?",
   "confirm_password": "Потвърдете паролата",
-  "contain": "",
+  "contain": "В рамките на",
   "context": "Контекст",
   "continue": "Продължи",
   "copied_image_to_clipboard": "Изображението е копирано в клипборда.",
@@ -445,8 +493,8 @@
   "copy_password": "Копиране на парола",
   "copy_to_clipboard": "Копиране в клипборда",
   "country": "Държава",
-  "cover": "",
-  "covers": "",
+  "cover": "Cover",
+  "covers": "Обложка",
   "create": "Създай",
   "create_album": "Създай албум",
   "create_library": "Създай библиотека",
@@ -454,7 +502,10 @@
   "create_link_to_share": "Създаване на линк за споделяне",
   "create_link_to_share_description": "Позволете на всеки, който има линк, да види избраната(ите) снимка(и)",
   "create_new_person": "Създаване на ново лице",
+  "create_new_person_hint": "Присвойте избраните файлове на нов човек",
   "create_new_user": "Създаване на нов потребител",
+  "create_tag": "Създай таг",
+  "create_tag_description": "Създайте нов таг. За вложени тагове, моля, въведете пълния път на тага, включително наклонените черти.",
   "create_user": "Създай потребител",
   "created": "Създадено",
   "current_device": "Текущо устройство",
@@ -468,7 +519,7 @@
   "date_range": "Период от време",
   "day": "Ден",
   "deduplicate_all": "Дедупликиране на всички",
-  "default_locale": "",
+  "default_locale": "Локализация по подразбиране",
   "default_locale_description": "Форматиране на дати и числа в зависимост от местоположението на браузъра",
   "delete": "Изтрий",
   "delete_album": "Изтрий албум",
@@ -477,14 +528,19 @@
   "delete_key": "Изтрий ключ",
   "delete_library": "Изтрий библиотека",
   "delete_link": "Изтрий линк",
+  "delete_others": "Изтрий останалите",
   "delete_shared_link": "Изтриване на споделен линк",
+  "delete_tag": "Изтрий таг",
+  "delete_tag_confirmation_prompt": "Сигурни ли сте, че искате да изтриете таг {tagName}?",
   "delete_user": "Изтрий потребител",
   "deleted_shared_link": "Изтрит споделен линк",
+  "deletes_missing_assets": "Изтрива файлове, които липсват на диска",
   "description": "Описание",
   "details": "Детайли",
   "direction": "Посока",
   "disabled": "Изключено",
   "disallow_edits": "Забраняване на редакциите",
+  "discord": "Discord",
   "discover": "Открий",
   "dismiss_all_errors": "Отхвърляне на всички грешки",
   "dismiss_error": "Отхвърляне на грешка",
@@ -493,15 +549,18 @@
   "display_original_photos": "Показване на оригинални снимки",
   "display_original_photos_setting_description": "Показване на оригиналната снимка вместо миниатюри, когато оригиналният актив е съвместим с мрежата. Това може да доведе до по-бавни скорости на показване на снимки.",
   "do_not_show_again": "Не показвайте това съобщение отново",
+  "documentation": "Документация",
   "done": "Готово",
   "download": "Изтегли",
+  "download_include_embedded_motion_videos": "Вградени видеа",
+  "download_include_embedded_motion_videos_description": "Включете видеата, вградени в динамични снимки, като отделен файл",
   "download_settings": "Изтегли",
   "download_settings_description": "Управление на настройките, свързани с изтеглянето на файлове",
   "downloading": "Изтегляне",
   "downloading_asset_filename": "Изтегляне на файл {filename}",
   "drop_files_to_upload": "Пуснете файловете, за да ги качите",
   "duplicates": "Дубликати",
-  "duplicates_description": "",
+  "duplicates_description": "Изберете всяка група, като посочите кои, ако има такива, са дубликати",
   "duration": "Продължителност",
   "edit": "Редактиране",
   "edit_album": "Редактиране на албум",
@@ -517,10 +576,15 @@
   "edit_location": "Редактиране на местоположението",
   "edit_name": "Редактиране на име",
   "edit_people": "Редактиране на хора",
+  "edit_tag": "Редактирай таг",
   "edit_title": "Редактиране на заглавието",
   "edit_user": "Редактиране на потребител",
   "edited": "Редактирано",
-  "editor": "",
+  "editor": "Редактор",
+  "editor_close_without_save_prompt": "Промените няма да бъдат запазени",
+  "editor_close_without_save_title": "Затваряне на редактора?",
+  "editor_crop_tool_h2_aspect_ratios": "Съотношения на страните",
+  "editor_crop_tool_h2_rotation": "Завъртане",
   "email": "Имейл",
   "empty_trash": "Изпразване на кош",
   "empty_trash_confirmation": "Сигурни ли сте, че искате да изпразните кошчето? Това ще премахне всичко в кошчето за постоянно от Immich.\nНе можете да отмените това действие!",
@@ -534,7 +598,9 @@
     "cannot_navigate_next_asset": "Не можете да преминете към следващия файл",
     "cannot_navigate_previous_asset": "Не можете да преминете към предишния актив",
     "cant_apply_changes": "Не могат да се приложат промение",
+    "cant_change_activity": "Не може {enabled, select, true {да се деактивира} other {да се активира}} дейността",
     "cant_change_asset_favorite": "Не може да промени любими за файл",
+    "cant_change_metadata_assets_count": "Не може да се промени метаданните на {count, plural, one {# обект} other {# обекта}}",
     "cant_get_faces": "Не мога да намеря лица",
     "cant_get_number_of_comments": "Не може да получи броя на коментарите",
     "cant_search_people": "Не може да търси хора",
@@ -553,19 +619,28 @@
     "failed_to_create_shared_link": "Неуспешно създаване на споделена връзка",
     "failed_to_edit_shared_link": "Неуспешно редактиране на споделена връзка",
     "failed_to_get_people": "Неуспешно зареждане на хора",
+    "failed_to_keep_this_delete_others": "Неуспешно запазване на този обект и изтриване на останалите обекти",
     "failed_to_load_asset": "Неуспешно зареждане на файл",
     "failed_to_load_assets": "Неуспешно зареждане на файлове",
-    "import_path_already_exists": "",
+    "failed_to_load_people": "Неуспешно зареждане на хора",
+    "failed_to_remove_product_key": "Неуспешно премахване на продуктовия ключ",
+    "failed_to_stack_assets": "Неуспешно подреждане на обекти",
+    "failed_to_unstack_assets": "Неуспешно премахване на подредбата на обекти",
+    "import_path_already_exists": "Този път за импортиране вече съществува.",
     "incorrect_email_or_password": "Неправилен имейл или парола",
-    "paths_validation_failed": "",
+    "paths_validation_failed": "{paths, plural, one {# път} other {# пътища}} не преминаха валидация",
     "profile_picture_transparent_pixels": "Профилните снимки не могат да имат прозрачни пиксели. Моля, увеличете и/или преместете изображението.",
     "quota_higher_than_disk_size": "Зададена е квота, по-голяма от размера на диска",
-    "repair_unable_to_check_items": "",
-    "unable_to_add_album_users": "",
-    "unable_to_add_comment": "",
-    "unable_to_add_exclusion_pattern": "",
-    "unable_to_add_import_path": "",
-    "unable_to_add_partners": "",
+    "repair_unable_to_check_items": "Неуспешно проверяване на {count, select, one {обект} other {обекти}}",
+    "unable_to_add_album_users": "Неуспешно добавяне на потребители в албум",
+    "unable_to_add_assets_to_shared_link": "Неуспешно добавяне на обекти в споделен линк",
+    "unable_to_add_comment": "Неуспешно добавяне на коментар",
+    "unable_to_add_exclusion_pattern": "Неуспешно добавяне на шаблон за изключение",
+    "unable_to_add_import_path": "Неуспешно добавяне на път за импортиране",
+    "unable_to_add_partners": "Неуспешно добавяне на партньори",
+    "unable_to_add_remove_archive": "Неуспешно {archived, select, true {премахване на обект от} other {добавяне на обект в}} архива",
+    "unable_to_add_remove_favorites": "Неуспешно {favorite, select, true {добавяне на обект в} other {премахване на обект от}} любими",
+    "unable_to_archive_unarchive": "Неуспешно {archived, select, true {архивиране} other {разархивиране}}",
     "unable_to_change_album_user_role": "Не може да се промени ролята на потребителя на албума",
     "unable_to_change_date": "Не може да се промени датата",
     "unable_to_change_favorite": "Не може да промени фаворит за актив",
@@ -596,17 +671,18 @@
     "unable_to_get_comments_number": "Не може да получи брой коментари",
     "unable_to_get_shared_link": "Неуспешно създаване на споделена връзка",
     "unable_to_hide_person": "Не може да скрие човек",
-    "unable_to_link_oauth_account": "",
-    "unable_to_load_album": "",
-    "unable_to_load_asset_activity": "",
-    "unable_to_load_items": "",
-    "unable_to_load_liked_status": "",
+    "unable_to_link_motion_video": "Неуспешно свързване на видео с движение",
+    "unable_to_link_oauth_account": "Неуспешно свързване на OAuth акаунт",
+    "unable_to_load_album": "Неуспешно зареждане на албум",
+    "unable_to_load_asset_activity": "Неуспешно зареждане на активност на обект",
+    "unable_to_load_items": "Неуспешно зареждане на обекти",
+    "unable_to_load_liked_status": "Неуспешно зареждане на статус на харесване",
     "unable_to_play_video": "",
     "unable_to_refresh_user": "",
     "unable_to_remove_album_users": "",
     "unable_to_remove_api_key": "",
+    "unable_to_remove_deleted_assets": "",
     "unable_to_remove_library": "",
-    "unable_to_remove_offline_files": "",
     "unable_to_remove_partner": "",
     "unable_to_remove_reaction": "",
     "unable_to_repair_items": "",
@@ -645,7 +721,6 @@
   "external": "Външно",
   "external_libraries": "Външни библиотеки",
   "face_unassigned": "Незададено",
-  "failed_to_get_people": "",
   "favorite": "Любим",
   "favorite_or_unfavorite_photo": "",
   "favorites": "Любими",
@@ -657,14 +732,12 @@
   "filter_people": "Филтриране на хора",
   "find_them_fast": "Намерете ги бързо по име с търсене",
   "fix_incorrect_match": "Поправяне на неправилно съвпадение",
-  "force_re-scan_library_files": "Принудително повторно сканиране на всички библиотечни файлове",
   "forward": "Напред",
   "general": "Общи",
   "get_help": "Помощ",
   "getting_started": "",
   "go_back": "Връщане назад",
   "go_to_search": "Преминаване към търсене",
-  "go_to_share_page": "",
   "group_albums_by": "Групирай албум по...",
   "group_owner": "Групиране по собственик",
   "group_year": "Групиране по година",
@@ -680,7 +753,6 @@
   "hour": "Час",
   "image": "Изображение",
   "image_alt_text_date": "на {date}",
-  "image_alt_text_place": "в {city}, {country}",
   "immich_logo": "Immich лого",
   "immich_web_interface": "",
   "import_from_json": "Импортиране от JSON",
@@ -713,29 +785,6 @@
   "level": "Ниво",
   "library": "Библиотека",
   "library_options": "Опции на библиотеката",
-  "license_account_info": "Вашият акаунт е лицензиран",
-  "license_activated_title": "Вашият лиценз е активиран успешно",
-  "license_button_activate": "Активирай",
-  "license_button_buy": "Купи",
-  "license_button_buy_license": "Купи лиценз",
-  "license_button_select": "Избери",
-  "license_failed_activation": "Неуспешно активиране на лиценз. Моля, проверете имейла си за правилния лицензен ключ!",
-  "license_individual_description_1": "1 лиценз за потребител на всеки сървър",
-  "license_individual_title": "Индивидуален лиценз",
-  "license_info_licensed": "Лицензиран",
-  "license_info_unlicensed": "Не лицензиран",
-  "license_input_suggestion": "Имате лиценз? Въведете ключа по-долу",
-  "license_license_subtitle": "Купете лиценз, за да подкрепите Immich",
-  "license_license_title": "ЛИЦЕНЗ",
-  "license_lifetime_description": "Доживотен лиценз",
-  "license_per_server": "За сървър",
-  "license_per_user": "За потребител",
-  "license_server_description_1": "1 лиценз за сървър",
-  "license_server_description_2": "Лиценз за всички потребители на сървъра",
-  "license_server_title": "Лиценз за сървър",
-  "license_trial_info_1": "Работите с нелицензирана версия на Immich",
-  "license_trial_info_2": "Използвали сте Immich за приблизително",
-  "license_trial_info_4": "Моля, помислете за закупуване на лиценз, за да подкрепите по-нататъшното развитие на услугата",
   "light": "Светло",
   "link_options": "Опции на линк за споделяне",
   "link_to_oauth": "",
@@ -827,7 +876,6 @@
   "onboarding_welcome_user": "Добре дошъл, {user}",
   "online": "Онлайн",
   "only_favorites": "Само любими",
-  "only_refreshes_modified_files": "Опреснява само модифицирани файлове",
   "open_the_search_filters": "Отваряне на филтрите за търсене",
   "options": "Настройки",
   "or": "или",
@@ -865,7 +913,6 @@
   "permanent_deletion_warning_setting_description": "Показване на предупреждение при трайно изтриване на активи",
   "permanently_delete": "Трайно изтриване",
   "permanently_deleted_asset": "",
-  "permanently_deleted_assets": "",
   "person": "Човек",
   "photos": "Снимки",
   "photos_count": "",
@@ -895,10 +942,10 @@
   "refreshed": "Опреснено",
   "refreshes_every_file": "",
   "remove": "Премахни",
+  "remove_deleted_assets": "",
   "remove_from_album": "",
   "remove_from_favorites": "",
   "remove_from_shared_link": "",
-  "remove_offline_files": "",
   "removed_api_key": "",
   "rename": "Преименувай",
   "repair": "Поправи",
@@ -926,8 +973,6 @@
   "saved_settings": "",
   "say_something": "",
   "scan_all_libraries": "",
-  "scan_all_library_files": "",
-  "scan_new_library_files": "",
   "scan_settings": "",
   "scanning_for_album": "",
   "search": "Търсене",
@@ -938,19 +983,22 @@
   "search_city": "",
   "search_country": "",
   "search_for_existing_person": "",
-  "search_people": "",
-  "search_places": "",
+  "search_people": "Търсете на хора",
+  "search_places": "Търсене на места",
   "search_state": "",
-  "search_timezone": "",
-  "search_type": "",
-  "search_your_photos": "",
+  "search_tags": "Търсене на етикети...",
+  "search_timezone": "Търсене на часова зона...",
+  "search_type": "Тип на търсене",
+  "search_your_photos": "Търсете вашите снимки",
   "searching_locales": "",
   "second": "Секунда",
-  "select_album_cover": "",
-  "select_all": "",
-  "select_avatar_color": "",
-  "select_face": "",
+  "see_all_people": "Вижте всички хора",
+  "select_album_cover": "Изберете обложка на албум",
+  "select_all": "Изберете всички",
+  "select_avatar_color": "Изберете цвят на аватара",
+  "select_face": "Изберете лице",
   "select_featured_photo": "",
+  "select_from_computer": "Изберете от компютъра",
   "select_keep_all": "",
   "select_library_owner": "Изберете собственик на библиотека",
   "select_new_face": "Изберете ново лице",
@@ -959,7 +1007,6 @@
   "selected": "Избрано",
   "send_message": "Изпратете съобщение",
   "send_welcome_email": "Изпратете имейл за добре дошли",
-  "server": "Сървър",
   "server_offline": "Сървър офлайн",
   "server_online": "Сървър онлайн",
   "server_stats": "Статус на сървъра",
@@ -998,28 +1045,40 @@
   "show_metadata": "Покажи метаданни",
   "show_or_hide_info": "Покажи или скрий информацията",
   "show_password": "Покажи паролата",
-  "show_person_options": "",
-  "show_progress_bar": "",
-  "show_search_options": "",
+  "show_person_options": "Показване на опции за лица",
+  "show_progress_bar": "Показване на прогрес бара",
+  "show_search_options": "Показване на опциите за търсене",
+  "show_supporter_badge": "Значка поддръжник",
+  "show_supporter_badge_description": "Покажи значка поддръжник",
   "shuffle": "Разбъркване",
-  "sign_out": "",
-  "sign_up": "",
+  "sidebar": "Странична лента",
+  "sidebar_display_description": "Показване на връзка към изгледа в страничната лента",
+  "sign_out": "Отписване",
+  "sign_up": "Запиши се",
   "size": "Размер",
-  "skip_to_content": "",
+  "skip_to_content": "Премини към съдържанието",
+  "skip_to_folders": "Премини към папките",
+  "skip_to_tags": "Премини към етикетите",
   "slideshow": "Слайдшоу",
-  "slideshow_settings": "",
-  "sort_albums_by": "",
+  "slideshow_settings": "Настройки за слайдшоу",
+  "sort_albums_by": "Сортиране на албуми по...",
+  "sort_created": "Дата на създаване",
+  "sort_items": "Брой елементи",
+  "sort_modified": "Дата на промяна",
+  "sort_oldest": "Най-старата снимка",
+  "sort_recent": "Най-новата снимка",
   "sort_title": "Заглавие",
   "source": "Източник",
   "stack": "",
-  "stack_selected_photos": "",
+  "stack_duplicates": "Подреждане на дубликати",
+  "stack_selected_photos": "Подреждане на избрани снимки",
   "stacktrace": "",
   "start": "Старт",
-  "start_date": "",
+  "start_date": "Начална дата",
   "state": "",
   "status": "Статус",
   "stop_motion_photo": "",
-  "stop_photo_sharing": "Да спрете ли споделянето на вашите снимки?",
+  "stop_photo_sharing": "Да спра ли споделянето на вашите снимки?",
   "stop_photo_sharing_description": "{partner} вече няма достъп до вашите снимки.",
   "stop_sharing_photos_with_user": "Прекратете споделянето на снимки с този потребител",
   "storage": "Пространство на хранилището",
@@ -1030,6 +1089,12 @@
   "sunrise_on_the_beach": "Изгрев на плажа",
   "swap_merge_direction": "Размяна посоката на сливане",
   "sync": "Синхронизиране",
+  "tag": "Таг",
+  "tag_created": "Създаден етикет: {tag}",
+  "tag_feature_description": "Разглеждане на снимки и видеоклипове, групирани по теми с логически тагове",
+  "tag_not_found_question": "Не можете да намерите етикет? Създайте такъв <link>тук</link>",
+  "tag_updated": "Актуализиран етикет: {tag}",
+  "tags": "Етикет",
   "template": "Шаблон",
   "theme": "Тема",
   "theme_selection": "Избор на тема",
@@ -1044,16 +1109,14 @@
   "to_trash": "Кошче",
   "toggle_settings": "Превключване на настройките",
   "toggle_theme": "Превключване на тема",
-  "toggle_visibility": "",
   "total_usage": "Общо използвано",
   "trash": "кошче",
   "trash_all": "Изхвърли всички",
-  "trash_count": "",
+  "trash_count": "Кошче {count, number}",
   "trash_no_results_message": "Изтритите снимки и видеоклипове ще се показват тук.",
   "trashed_items_will_be_permanently_deleted_after": "Изхвърлените в кошчето елементи ще бъдат изтрити за постоянно след {days, plural, one {# day} other {# days}}.",
   "type": "Тип",
   "unarchive": "Разархивирай",
-  "unarchived": "",
   "unfavorite": "Премахване от любимите",
   "unhide_person": "",
   "unknown": "Неизвестно",
@@ -1062,6 +1125,7 @@
   "unlink_oauth": "",
   "unlinked_oauth_account": "",
   "unnamed_album": "Албум без име",
+  "unnamed_album_delete_confirmation": "Сигурни ли сте, че искате да изтриете този албум?",
   "unnamed_share": "Споделяне без име",
   "unsaved_change": "Незапазена промяна",
   "unselect_all": "Деселектирайте всички",
@@ -1085,7 +1149,9 @@
   "user_purchase_settings": "Покупка",
   "user_purchase_settings_description": "Управлявай покупката си",
   "user_role_set": "Задай {user} като {role}",
-  "user_usage_detail": "",
+  "user_usage_detail": "Подробности за използването на потребителя",
+  "user_usage_stats": "Статистика за използването на акаунта",
+  "user_usage_stats_description": "Преглед на статистиката за използването на акаунта",
   "username": "Потребителско име",
   "users": "Потребители",
   "utilities": "Инструменти",
@@ -1098,20 +1164,22 @@
   "video_hover_setting": "Възпроизвеждане на видеоклип при посочване с мишката",
   "video_hover_setting_description": "Възпроизвеждане на видеоклипа, когато мишката се движи над елемента. Дори когато е деактивирано, възпроизвеждането може да бъде стартирано чрез задържане на курсора на мишката върху иконата за възпроизвеждане.",
   "videos": "Видеоклипове",
-  "videos_count": "",
+  "videos_count": "{count, plural, one {# Видео} other {# Видеа}}",
   "view": "Преглед",
   "view_album": "Разгледай албума",
   "view_all": "Преглед на всички",
   "view_all_users": "Преглед на всички потребители",
+  "view_in_timeline": "Покажи във времева линия",
   "view_links": "Преглед на връзките",
   "view_next_asset": "Преглед на следващия файл",
   "view_previous_asset": "Преглед на предишния файл",
-  "viewer": "",
+  "view_stack": "Покажи в стек",
+  "visibility_changed": "Видимостта е променена за {count, plural, one {# person} other {# people}}",
   "waiting": "в изчакване",
   "warning": "Внимание",
   "week": "Седмица",
   "welcome": "Добре дошли",
-  "welcome_to_immich": "Добре дошли в immich",
+  "welcome_to_immich": "Добре дошли в Immich",
   "year": "Година",
   "yes": "Да",
   "you_dont_have_any_shared_links": "Нямате споделени връзки",
diff --git a/web/src/lib/i18n/bi.json b/i18n/bi.json
similarity index 95%
rename from web/src/lib/i18n/bi.json
rename to i18n/bi.json
index 7d70cb8434..dfcc614bea 100644
--- a/web/src/lib/i18n/bi.json
+++ b/i18n/bi.json
@@ -33,7 +33,6 @@
     "confirm_email_below": "",
     "confirm_reprocess_all_faces": "",
     "confirm_user_password_reset": "",
-    "crontab_guru": "",
     "disable_login": "",
     "duplicate_detection_job_description": "",
     "exclusion_pattern_description": "",
@@ -49,16 +48,9 @@
     "image_prefer_embedded_preview_setting_description": "",
     "image_prefer_wide_gamut": "",
     "image_prefer_wide_gamut_setting_description": "",
-    "image_preview_format": "",
-    "image_preview_resolution": "",
-    "image_preview_resolution_description": "",
     "image_quality": "",
-    "image_quality_description": "",
     "image_settings": "",
     "image_settings_description": "",
-    "image_thumbnail_format": "",
-    "image_thumbnail_resolution": "",
-    "image_thumbnail_resolution_description": "",
     "job_concurrency": "",
     "job_not_concurrency_safe": "",
     "job_settings": "",
@@ -67,8 +59,6 @@
     "jobs_delayed": "",
     "jobs_failed": "",
     "library_created": "",
-    "library_cron_expression": "",
-    "library_cron_expression_presets": "",
     "library_deleted": "",
     "library_import_path_description": "",
     "library_scanning": "",
@@ -172,15 +162,12 @@
     "paths_validated_successfully": "",
     "quota_size_gib": "",
     "refreshing_all_libraries": "",
-    "removing_offline_files": "",
     "repair_all": "",
     "repair_matched_items": "",
     "repaired_items": "",
     "require_password_change_on_login": "",
     "reset_settings_to_default": "",
     "reset_settings_to_recent_saved": "",
-    "scanning_library_for_changed_files": "",
-    "scanning_library_for_new_files": "",
     "send_welcome_email": "",
     "server_external_domain_settings": "",
     "server_external_domain_settings_description": "",
@@ -255,8 +242,6 @@
     "transcoding_threads_description": "",
     "transcoding_tone_mapping": "",
     "transcoding_tone_mapping_description": "",
-    "transcoding_tone_mapping_npl": "",
-    "transcoding_tone_mapping_npl_description": "",
     "transcoding_transcode_policy": "",
     "transcoding_transcode_policy_description": "",
     "transcoding_two_pass_encoding": "",
@@ -308,7 +293,6 @@
   "appears_in": "",
   "archive": "",
   "archive_or_unarchive_photo": "",
-  "archived": "",
   "asset_offline": "",
   "assets": "",
   "authorized_devices": "",
@@ -322,10 +306,6 @@
   "cancel_search": "",
   "cannot_merge_people": "",
   "cannot_update_the_description": "",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
   "change_date": "",
   "change_expiration_time": "",
   "change_location": "",
@@ -411,13 +391,6 @@
   "download": "",
   "downloading": "",
   "duration": "",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
   "edit_album": "",
   "edit_avatar": "",
   "edit_date": "",
@@ -436,7 +409,6 @@
   "edited": "",
   "editor": "",
   "email": "",
-  "empty_album": "",
   "empty_trash": "",
   "enable": "",
   "enabled": "",
@@ -485,8 +457,8 @@
     "unable_to_refresh_user": "",
     "unable_to_remove_album_users": "",
     "unable_to_remove_api_key": "",
+    "unable_to_remove_deleted_assets": "",
     "unable_to_remove_library": "",
-    "unable_to_remove_offline_files": "",
     "unable_to_remove_partner": "",
     "unable_to_remove_reaction": "",
     "unable_to_repair_items": "",
@@ -522,7 +494,6 @@
   "extension": "",
   "external": "",
   "external_libraries": "",
-  "failed_to_get_people": "",
   "favorite": "",
   "favorite_or_unfavorite_photo": "",
   "favorites": "",
@@ -534,14 +505,12 @@
   "filter_people": "",
   "find_them_fast": "",
   "fix_incorrect_match": "",
-  "force_re-scan_library_files": "",
   "forward": "",
   "general": "",
   "get_help": "",
   "getting_started": "",
   "go_back": "",
   "go_to_search": "",
-  "go_to_share_page": "",
   "group_albums_by": "",
   "has_quota": "",
   "hide_gallery": "",
@@ -656,7 +625,6 @@
   "oldest_first": "",
   "online": "",
   "only_favorites": "",
-  "only_refreshes_modified_files": "",
   "open_the_search_filters": "",
   "options": "",
   "organize_your_library": "",
@@ -718,10 +686,10 @@
   "refreshed": "",
   "refreshes_every_file": "",
   "remove": "",
+  "remove_deleted_assets": "",
   "remove_from_album": "",
   "remove_from_favorites": "",
   "remove_from_shared_link": "",
-  "remove_offline_files": "",
   "removed_api_key": "",
   "rename": "",
   "repair": "",
@@ -745,8 +713,6 @@
   "saved_settings": "",
   "say_something": "",
   "scan_all_libraries": "",
-  "scan_all_library_files": "",
-  "scan_new_library_files": "",
   "scan_settings": "",
   "search": "",
   "search_albums": "",
@@ -777,7 +743,6 @@
   "selected": "",
   "send_message": "",
   "send_welcome_email": "",
-  "server": "",
   "server_stats": "",
   "set": "",
   "set_as_album_cover": "",
@@ -847,7 +812,6 @@
   "to_favorite": "",
   "toggle_settings": "",
   "toggle_theme": "",
-  "toggle_visibility": "",
   "total_usage": "",
   "trash": "",
   "trash_all": "",
@@ -855,11 +819,9 @@
   "trashed_items_will_be_permanently_deleted_after": "",
   "type": "",
   "unarchive": "",
-  "unarchived": "",
   "unfavorite": "",
   "unhide_person": "",
   "unknown": "",
-  "unknown_album": "",
   "unknown_year": "",
   "unlimited": "",
   "unlink_oauth": "",
@@ -893,7 +855,6 @@
   "view_links": "",
   "view_next_asset": "",
   "view_previous_asset": "",
-  "viewer": "",
   "waiting": "",
   "week": "",
   "welcome_to_immich": "",
diff --git a/web/src/lib/i18n/af.json b/i18n/bn.json
similarity index 100%
rename from web/src/lib/i18n/af.json
rename to i18n/bn.json
diff --git a/web/src/lib/i18n/ca.json b/i18n/ca.json
similarity index 87%
rename from web/src/lib/i18n/ca.json
rename to i18n/ca.json
index a0fd6ff437..a38e9e4743 100644
--- a/web/src/lib/i18n/ca.json
+++ b/i18n/ca.json
@@ -1,38 +1,45 @@
 {
-  "about": "Quant a",
+  "about": "Sobre",
   "account": "Compte",
   "account_settings": "Configuració del compte",
-  "acknowledge": "Reconeix",
+  "acknowledge": "Confirmar",
   "action": "Acció",
   "actions": "Accions",
   "active": "Actiu",
   "activity": "Activitat",
   "activity_changed": "L'activitat està {enabled, select, true {activada} other {desactivada}}",
-  "add": "Afig",
+  "add": "Afegir",
   "add_a_description": "Afegiu una descripció",
   "add_a_location": "Afegiu una ubicació",
   "add_a_name": "Afegir un nom",
   "add_a_title": "Afegir un títol",
   "add_exclusion_pattern": "Afegir un patró d'exclusió",
-  "add_import_path": "Afegir un camí d'importació",
+  "add_import_path": "Afegir una ruta d'importació",
   "add_location": "Afegir la ubicació",
   "add_more_users": "Afegir més usuaris",
   "add_partner": "Afegir company/a",
-  "add_path": "Afegir un camí",
+  "add_path": "Afegir una ruta",
   "add_photos": "Afegir fotografies",
   "add_to": "Afegir a...",
   "add_to_album": "Afegir a un l'àlbum",
   "add_to_shared_album": "Afegir a un àlbum compartit",
+  "add_url": "Afegir URL",
   "added_to_archive": "Afegit als arxivats",
   "added_to_favorites": "Afegit als preferits",
   "added_to_favorites_count": "{count, number} afegits als preferits",
   "admin": {
-    "add_exclusion_pattern_description": "Afegeix patrons d'eclusió. És permès de l'ús de *, **, i ? (globbing). Per a ignorar els fitxers de qualsevol directori anomenat \"Raw\" introduïu \"**/Raw/**\". Per a ignorar els fitxers acabats en \".tif\" introduïu \"**/*.tif\". Per a ignorar un camí absolut, utilitzeu \"/camí/a/ignorar/**\".",
+    "add_exclusion_pattern_description": "Afegeix patrons d'exclusió. Es permet englobar fent ús de *, **, i ?. Per a ignorar els fitxers de qualsevol directori anomenat \"Raw\" introduïu \"**/Raw/**\". Per a ignorar els fitxers acabats en \".tif\" introduïu \"**/*.tif\". Per a ignorar una ruta absoluta, utilitzeu \"/ruta/a/ignorar/**\".",
+    "asset_offline_description": "Aquest recurs de la biblioteca externa ja no es troba al disc i s'ha mogut a la paperera. Si el fitxer s'ha mogut dins de la biblioteca, comproveu la vostra línia de temps per trobar el nou recurs corresponent. Per restaurar aquest recurs, assegureu-vos que Immich pugui accedir a la ruta del fitxer següent i escanegeu la biblioteca.",
     "authentication_settings": "Configuració de l'autenticació",
     "authentication_settings_description": "Gestiona la contrasenya, OAuth i altres configuracions de l'autenticació",
     "authentication_settings_disable_all": "Estàs segur que vols desactivar tots els mètodes d'inici de sessió? L'inici de sessió quedarà completament desactivat.",
     "authentication_settings_reenable": "Per a tornar a habilitar, empra una <link>Comanda de Servidor</link>.",
     "background_task_job": "Tasques en segon pla",
+    "backup_database": "Còpia de la base de dades",
+    "backup_database_enable_description": "Habilitar còpies de la base de dades",
+    "backup_keep_last_amount": "Quantitat de còpies de seguretat anteriors per conservar",
+    "backup_settings": "Ajustes de les còpies de seguretat",
+    "backup_settings_description": "Gestionar la configuració de la còpia de seguretat de la base de dades",
     "check_all": "Marca-ho tot",
     "cleared_jobs": "Tasques esborrades per a: {job}",
     "config_set_by_file": "La configuració està definida per un fitxer de configuració",
@@ -41,35 +48,40 @@
     "confirm_email_below": "Per a confirmar, escriviu \"{email}\" a sota",
     "confirm_reprocess_all_faces": "Esteu segur que voleu reprocessar totes les cares? Això també esborrarà la gent que heu anomenat.",
     "confirm_user_password_reset": "Esteu segur que voleu reinicialitzar la contrasenya de l'usuari {user}?",
-    "crontab_guru": "Crontab Guru",
+    "create_job": "Crear tasca",
+    "cron_expression": "Expressió Cron",
+    "cron_expression_description": "Estableix l'interval d'escaneig amb el format cron. Per obtenir més informació, consulteu, p.e <link>Crontab Guru</link>",
+    "cron_expression_presets": "Ajustos predefinits d'expressions Cron",
     "disable_login": "Deshabiliteu l'inici de sessió",
-    "disabled": "Deshabilitat",
     "duplicate_detection_job_description": "Executa l'aprenentatge automàtic en els elements per a detectar imatges semblants. Fa servir l'Smart Search",
     "exclusion_pattern_description": "Els patrons d'exclusió permeten ignorar fitxers i carpetes quan escanegeu una llibreria. Això és útil si teniu carpetes que contenen fitxer que no voleu importar, com els fitxers RAW.",
     "external_library_created_at": "Llibreria externa (creada el {date})",
     "external_library_management": "Gestió de llibreries externes",
     "face_detection": "Detecció de cares",
-    "face_detection_description": "Detecta les cares fent servir aprenentatge automàtic. Per a videos només és té en compte la miniatura. \"Tot\" reprocessa tots els elements. \"Pendent\" encua els elements que encar no han estat processats. Les cares detectades s'encuaran per al Reconeixement Facial després de completar la Detecció Facial, tot agrupant-les entre noves persones o les ja existents.",
-    "facial_recognition_job_description": "Agrupa les cares detectades per persona. Aquest pas s'executa després de completar la detecció de cares. \"Tot\" reagrupa totes les cares. \"Pendent\" encua les cares que no tenen cap persona assignada.",
+    "face_detection_description": "Detecta les cares fent servir aprenentatge automàtic. Per a videos només és té en compte la miniatura. \"Actualitzar\" reprocessa tots els elements. \"Resetejar\" esborra tota la informació de cares actuals. \"Pendent\" afegeix a la cua els elements que encara no han estat processats. Les cares detectades s'afegiran a la cua per al Reconeixement Facial després de completar la Detecció Facial, tot agrupant-les entre noves persones o les ja existents.",
+    "facial_recognition_job_description": "Agrupa les cares detectades per persona. Aquest pas s'executa després de completar la detecció de cares. \"Resetejar\" reagrupa totes les cares. \"Pendent\" afegeix a la cua les cares que no tenen cap persona assignada.",
     "failed_job_command": "La comanda {command} ha fallat per la tasca: {job}",
     "force_delete_user_warning": "COMPTE: Aquesta acció eliminara immediatament l'usuari i els seus elements. Aquesta acció és irreversible i els fitxers no es poden recuperar.",
     "forcing_refresh_library_files": "Força l'actualització de tots els fitxers de les biblioteques",
+    "image_format": "Format",
     "image_format_description": "WebP genera fitxers més petits que JPEG, però codifica més lentament.",
     "image_prefer_embedded_preview": "Prefereix vista prèvia incrustada",
     "image_prefer_embedded_preview_setting_description": "Empra vista prèvia incrustada en les fotografies RAW com a entrada per al processament d'imatge, quan sigui possible. Aquesta acció pot produir colors més acurats en algunes imatges, però la qualitat de la vista prèvia depèn de la càmera i la imatge pot tenir més artefactes de compressió.",
     "image_prefer_wide_gamut": "Prefereix àmplia gamma",
     "image_prefer_wide_gamut_setting_description": "Uitlitza Display P3 per a les miniatures. Això preserva més bé la vitalitat de les imatges amb espais de color àmplis, però les imatges es poden veure diferent en aparells antics amb una versió antiga del navegador. Les imatges sRGB romandran com a sRGB per a evitar canvis de color.",
-    "image_preview_format": "Format de previsualització",
-    "image_preview_resolution": "Resolució de previsualització",
-    "image_preview_resolution_description": "S'empra al visualitzar una única fotografia i per a l'Aprenentatge Automàtic. L'alta resolució por preservar més detalls però es triga més a codificar, té fitxers més pesats i pot reduir la resposta de l'aplicació.",
+    "image_preview_description": "Imatge de mida mitjana amb metadades eliminades, que s'utilitza quan es visualitza un sol recurs i per a l'aprenentatge automàtic",
+    "image_preview_quality_description": "Vista prèvia de la qualitat de l'1 al 100. Més alt és millor, però produeix fitxers més grans i pot reduir la capacitat de resposta de l'aplicació. Establir un valor baix pot afectar la qualitat de l'aprenentatge automàtic.",
+    "image_preview_title": "Paràmetres de previsualització",
     "image_quality": "Qualitat",
-    "image_quality_description": "Qualitat d'imatge de 1 a 100. Un valor més alt millora la qualitat però genera fitxers més pesats.",
+    "image_resolution": "Resolució",
+    "image_resolution_description": "Les resolucions més altes poden conservar més detalls però triguen més a codificar-se, tenen mides de fitxer més grans i poden reduir la capacitat de resposta de l'aplicació.",
     "image_settings": "Configuració d'imatges",
     "image_settings_description": "Gestiona la qualitat i resolució de les imatges generades",
-    "image_thumbnail_format": "Format de la miniatura",
-    "image_thumbnail_resolution": "Resolució de la miniatura",
-    "image_thumbnail_resolution_description": "S'empra per a veure grups de fotos (cronologia, vista d'àlbum, etc.). L'alta resolució pot preservar més detalls però triguen més en codificar-se, tenen fitxers més pesats i poden reduir la reactivitat de l'aplicació.",
+    "image_thumbnail_description": "Miniatura petita amb metadades eliminades, que s'utilitza quan es visualitzen grups de fotos com la línia de temps principal",
+    "image_thumbnail_quality_description": "Qualitat de miniatura d'1 a 100. Més alt és millor, però produeix fitxers més grans i pot reduir la capacitat de resposta de l'aplicació.",
+    "image_thumbnail_title": "Configuració de miniatures",
     "job_concurrency": "{job} concurrència",
+    "job_created": "Tasca creada",
     "job_not_concurrency_safe": "Aquesta tasca no és segura per a la conconcurrència.",
     "job_settings": "Configuració de les tasques",
     "job_settings_description": "Gestiona la concurrència de tasques",
@@ -77,9 +89,6 @@
     "jobs_delayed": "{jobCount, plural, other {# posposades}}",
     "jobs_failed": "{jobCount, plural, other {# fallides}}",
     "library_created": "Bilbioteca creada: {library}",
-    "library_cron_expression": "Expressió cron",
-    "library_cron_expression_description": "Estableix l'interval d'escaneig utilitzant el format cron. Per a més informació, consulta per exemple, <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Expressions cron predeterminades",
     "library_deleted": "Bilbioteca eliminada",
     "library_import_path_description": "Especifiqueu una carpeta a importar. Aquesta carpeta, incloses les seves subcarpetes, serà escanejada per cercar-hi imatges i vídeos.",
     "library_scanning": "Escaneig periòdic",
@@ -129,25 +138,30 @@
     "map_enable_description": "Habilita característiques del mapa",
     "map_gps_settings": "Configuració de mapa i GPS",
     "map_gps_settings_description": "Gestiona la configuració de mapa i GPS (Geocodificació inversa)",
+    "map_implications": "La funció mapa depèn del servei extern de tesel·les (tiles.immich.cloud)",
     "map_light_style": "Tema clar",
     "map_manage_reverse_geocoding_settings": "Gestiona els paràmetres de <link>geocodificació inversa</link>",
     "map_reverse_geocoding": "Geocodificació inversa",
     "map_reverse_geocoding_enable_description": "Habilita la geocodificació inversa",
     "map_reverse_geocoding_settings": "Configuració de Geocodificació Inversa",
-    "map_settings": "Configuració del mapa i GPS",
+    "map_settings": "Mapa",
     "map_settings_description": "Gestiona la configuració del mapa",
     "map_style_description": "URL a un tema del mapa style.json",
     "metadata_extraction_job": "Extreure metadades",
     "metadata_extraction_job_description": "Extreu la informació de metadades de cada element, com per exemple el GPS i la resolució",
+    "metadata_faces_import_setting": "Activar la importació de cares",
+    "metadata_faces_import_setting_description": "Importar cares des de les metadades EXIF de les imatges i arxius auxiliars",
+    "metadata_settings": "Configuració de les metadades",
+    "metadata_settings_description": "Administrar la configuració de les metadades",
     "migration_job": "Migració",
     "migration_job_description": "Migra les miniatures d'elements i cares cap a la nova estructura de carpetes",
-    "no_paths_added": "Cap camí afegit",
+    "no_paths_added": "No s'ha afegit cap ruta",
     "no_pattern_added": "Cap patró aplicat",
     "note_apply_storage_label_previous_assets": "Nota: Per aplicar l'etiquetatge d'emmagatzematge a elements pujats prèviament, executeu la",
     "note_cannot_be_changed_later": "NOTA: Això és irreversible!",
     "note_unlimited_quota": "Nota: Intruduïu 0 per a quota il·limitada",
     "notification_email_from_address": "Des de l'adreça",
-    "notification_email_from_address_description": "Adreça de correu electrònic del remitent, per exemple: \"Immich Photo Server <noreply@immich.app>\"",
+    "notification_email_from_address_description": "Adreça de correu electrònic del remitent, per exemple: \"Immich Photo Server <noreply@example.com>\"",
     "notification_email_host_description": "Amfitrió del servidor de correu electrònic (p.ex. smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Ignora els errors de certificat",
     "notification_email_ignore_certificate_errors_description": "Ignora els errors de validació de certificat TLS (no recomanat)",
@@ -173,7 +187,7 @@
     "oauth_issuer_url": "URL de l'emissor",
     "oauth_mobile_redirect_uri": "URI de redirecció mòbil",
     "oauth_mobile_redirect_uri_override": "Sobreescriu l'URI de redirecció mòbil",
-    "oauth_mobile_redirect_uri_override_description": "Habilita quan 'app.immich:/' és una URI de redirecció invàlida.",
+    "oauth_mobile_redirect_uri_override_description": "Habilita quan el proveïdor d'OAuth no permet una URI mòbil, com ara '{callback}'",
     "oauth_profile_signing_algorithm": "Algoritme de signatura del perfil",
     "oauth_profile_signing_algorithm_description": "Algoritme utilitzat per signar el perfil d’usuari.",
     "oauth_scope": "Abast",
@@ -192,20 +206,20 @@
     "password_enable_description": "Inicia sessió amb correu electrònic i contrasenya",
     "password_settings": "Inici de sessió amb contrasenya",
     "password_settings_description": "Gestiona la configuració de l'inici de sessió amb contrasenya",
-    "paths_validated_successfully": "Tots els camins han estat validats amb èxit",
+    "paths_validated_successfully": "Totes les rutes han estat validades amb èxit",
+    "person_cleanup_job": "Neteja de persona",
     "quota_size_gib": "Tamany de la quota (GiB)",
     "refreshing_all_libraries": "Actualitzant totes les biblioteques",
     "registration": "Registre d'administrador",
     "registration_description": "Com que ets el primer usuari del sistema, seràs designat com a administrador i seràs responsable de les tasques administratives. També seràs l'encarregat de crear usuaris addicionals.",
-    "removing_offline_files": "Eliminant fitxers fora de línia",
     "repair_all": "Reparar tot",
     "repair_matched_items": "Coincidència {count, plural, one {# element} other {# elements}}",
     "repaired_items": "Corregit {count, plural, one {# element} other {# elements}}",
     "require_password_change_on_login": "Requerir que l'usuari canviï la contrasenya en el primer inici de sessió",
     "reset_settings_to_default": "Restablir configuracions per defecte",
     "reset_settings_to_recent_saved": "Restablir la configuració guardada més recent",
-    "scanning_library_for_changed_files": "Escanejant llibreria per trobar fitxers modificats",
-    "scanning_library_for_new_files": "Escanejant llibreria per trobar fitxers nous",
+    "scanning_library": "Escanejant biblioteca",
+    "search_jobs": "Tasques de cerca...",
     "send_welcome_email": "Enviar correu electrònic de benvinguda",
     "server_external_domain_settings": "Domini extern",
     "server_external_domain_settings_description": "Domini per enllaços públics compartits, incloent http(s)://",
@@ -233,6 +247,7 @@
     "storage_template_settings_description": "Gestiona l'estructura de les carpetes i el nom del fitxers dels elements pujats",
     "storage_template_user_label": "<code>{label}</code> és l'etiqueta d'emmagatzematge de l'usuari",
     "system_settings": "Configuració del sistema",
+    "tag_cleanup_job": "Neteja d'etiqueta",
     "theme_custom_css_settings": "CSS personalitzat",
     "theme_custom_css_settings_description": "Els Fulls d'Estil en Cascada permeten personalitzar el disseny d'Immich.",
     "theme_settings": "Configuració del tema",
@@ -240,7 +255,6 @@
     "these_files_matched_by_checksum": "Aquests fitxers coincideixen amb els seus checksums",
     "thumbnail_generation_job": "Generar miniatures",
     "thumbnail_generation_job_description": "Genera miniatures grans, petites i borroses per a cada element, així com miniatures per a cada persona",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "API d'acceleració",
     "transcoding_acceleration_api_description": "L'API que interactuarà amb el vostre dispositiu per accelerar la transcodificació. Aquesta configuració és \"millor esforç\": tornarà a la transcodificació del programari en cas d'error. VP9 pot funcionar o no depenent del vostre maquinari.",
     "transcoding_acceleration_nvenc": "NVENC (requereix GPU d'NVIDIA)",
@@ -266,7 +280,7 @@
     "transcoding_hardware_acceleration": "Acceleració de maquinari",
     "transcoding_hardware_acceleration_description": "Experimental. Molt més ràpid, però tindrà una qualitat més baixa amb la mateixa taxa de bits",
     "transcoding_hardware_decoding": "Descodificació de maquinari",
-    "transcoding_hardware_decoding_setting_description": "S'aplica només a NVENC, QSV i RKMPP. Permet l'acceleració d'extrem a extrem en lloc d'accelerar només la codificació. És possible que no funcioni en tots els vídeos.",
+    "transcoding_hardware_decoding_setting_description": "Habilita l'acceleració d'extrem a extrem en lloc d'accelerar només la codificació. És possible que no funcioni en tots els vídeos.",
     "transcoding_hevc_codec": "Còdec HEVC",
     "transcoding_max_b_frames": "Nombre màxim de B-frames",
     "transcoding_max_b_frames_description": "Els valors més alts milloren l'eficiència de la compressió, però alenteixen la codificació. És possible que no sigui compatible amb l'acceleració de maquinari en dispositius antics. 0 desactiva els B-frames, mentre que -1 estableix aquest valor automàticament.",
@@ -278,7 +292,7 @@
     "transcoding_preferred_hardware_device": "Dispositiu de maquinari preferit",
     "transcoding_preferred_hardware_device_description": "S'aplica només a VAAPI i QSV. Estableix el node dri utilitzat per a la transcodificació de maquinari.",
     "transcoding_preset_preset": "Preestablert (-preset)",
-    "transcoding_preset_preset_description": "Velocitat de compressió. Els valors predefinits més lents produeixen fitxers més petits i augmenten la qualitat quan s'orienta a una taxa de bits determinada. VP9 ignora les velocitats superiors a \"més ràpides\".",
+    "transcoding_preset_preset_description": "Velocitat de compressió. Els valors predefinits més lents produeixen fitxers més petits i augmenten la qualitat quan s'orienta a una taxa de bits determinada. VP9 ignora les velocitats superiors a 'més ràpides'.",
     "transcoding_reference_frames": "Fotogrames de referència",
     "transcoding_reference_frames_description": "El nombre de fotogrames a fer referència en comprimir un fotograma determinat. Els valors més alts milloren l'eficiència de la compressió, però alenteixen la codificació. 0 estableix aquest valor automàticament.",
     "transcoding_required_description": "Només vídeos que no tenen un format acceptat",
@@ -292,8 +306,6 @@
     "transcoding_threads_description": "Els valors més alts condueixen a una codificació més ràpida, però deixen menys espai perquè el servidor processi altres tasques mentre està actiu. Aquest valor no hauria de ser superior al nombre de nuclis de CPU. Maximitza la utilització si s'estableix a 0.",
     "transcoding_tone_mapping": "Mapeig de to",
     "transcoding_tone_mapping_description": "Intenta preservar l'aspecte dels vídeos HDR quan es converteixen a SDR. Cada algorisme fa diferents compensacions pel color, el detall i la brillantor. Hable conserva els detalls, Mobius conserva el color i Reinhard conserva la brillantor.",
-    "transcoding_tone_mapping_npl": "NPL de mapatge de to",
-    "transcoding_tone_mapping_npl_description": "Els colors s'ajustaran perquè semblin normals per a exposicions amb aquesta brillantor. Contra intuïtivament, els valors més baixos augmenten la brillantor del vídeo i viceversa, ja que compensa la brillantor de la pantalla. 0 estableix aquest valor automàticament.",
     "transcoding_transcode_policy": "Política de transcodificació",
     "transcoding_transcode_policy_description": "Política sobre quan s'ha de transcodificar un vídeo. Els vídeos HDR sempre es transcodificaran (excepte si la transcodificació està desactivada).",
     "transcoding_two_pass_encoding": "Codificació de dues passades",
@@ -307,6 +319,7 @@
     "trash_settings_description": "Gestiona la configuració de la paperera",
     "untracked_files": "Fitxers sense seguiment",
     "untracked_files_description": "L'aplicació no fa un seguiment d'aquests fitxers. Poden ser el resultat de moviments fallits, càrregues interrompudes o deixades enrere a causa d'un error",
+    "user_cleanup_job": "Neteja d'usuari",
     "user_delete_delay": "El compte i els recursos de <b>{user}</b> es programaran per a la supressió permanent en {delay, plural, one {# dia} other {# dies}}.",
     "user_delete_delay_settings": "Retard de la supressió",
     "user_delete_delay_settings_description": "Nombre de dies després de la supressió per eliminar permanentment el compte i els elements d'un usuari. El treball de supressió d'usuaris s'executa a mitjanit per comprovar si hi ha usuaris preparats per eliminar. Els canvis en aquesta configuració s'avaluaran en la propera execució.",
@@ -320,7 +333,8 @@
     "user_settings": "Configuració d'usuaris",
     "user_settings_description": "Gestiona la configuració dels usuaris",
     "user_successfully_removed": "L'usuari {email} s'ha eliminat correctament.",
-    "version_check_enabled_description": "Activa sol·licituds periòdiques a GitHub per comprovar si hi ha versions noves",
+    "version_check_enabled_description": "Activa la comprovació de la versió",
+    "version_check_implications": "La funció de comprovació de versions depèn de comunicacions periòdiques amb github.com",
     "version_check_settings": "Comprovació de versió",
     "version_check_settings_description": "Activa/desactiva la notificació de nova versió",
     "video_conversion_job": "Transcodificació de vídeos",
@@ -336,7 +350,8 @@
   "album_added": "Àlbum afegit",
   "album_added_notification_setting_description": "Rep una notificació per correu quan siguis afegit a un àlbum compartit",
   "album_cover_updated": "Portada de l'àlbum actualitzada",
-  "album_delete_confirmation": "N'esteu segur que voleu suprimir l'àlbum {album}?\nSi aquest àlbum és compartit, altres usuaris no hi podran accedir més.",
+  "album_delete_confirmation": "Esteu segur que voleu suprimir l'àlbum {album}?",
+  "album_delete_confirmation_description": "Si aquest àlbum es comparteix, els altres usuaris ja no podran accedir-hi.",
   "album_info_updated": "Informació de l'àlbum actualitzada",
   "album_leave": "Sortir de l'àlbum?",
   "album_leave_confirmation": "N'esteu segur que voleu sortir de {album}?",
@@ -360,6 +375,7 @@
   "allow_edits": "Permet editar",
   "allow_public_user_to_download": "Permet que l'usuari públic pugui descarregar",
   "allow_public_user_to_upload": "Permet que l'usuari públic pugui carregar",
+  "anti_clockwise": "En sentit antihorari",
   "api_key": "Clau API",
   "api_key_description": "Aquest valor només es mostrarà una vegada. Assegureu-vos de copiar-lo abans de tancar la finestra.",
   "api_key_empty": "El nom de la clau de l'API no pot estar buit",
@@ -370,7 +386,6 @@
   "archive_or_unarchive_photo": "Arxivar o desarxivar fotografia",
   "archive_size": "Mida de l'arxiu",
   "archive_size_description": "Configureu la mida de l'arxiu de les descàrregues (en GiB)",
-  "archived": "Arxivat",
   "archived_count": "{count, plural, one {Arxivat #} other {Arxivats #}}",
   "are_these_the_same_person": "Són la mateixa persona?",
   "are_you_sure_to_do_this": "Esteu segurs que voleu fer-ho?",
@@ -381,8 +396,9 @@
   "asset_has_unassigned_faces": "L'element té cares no assignades",
   "asset_hashing": "Hashing...",
   "asset_offline": "Element fora de línia",
-  "asset_offline_description": "Aquest element està fora de línia. L'Immich no pot accedir a la seva ubicació. Si us plau, assegureu-vos que l'actiu està disponible i després torneu la llibreria.",
+  "asset_offline_description": "Aquest recurs extern ja no es troba al disc. Poseu-vos en contacte amb el vostre administrador d'Immich per obtenir ajuda.",
   "asset_skipped": "Saltat",
+  "asset_skipped_in_trash": "A la paperera",
   "asset_uploaded": "Carregat",
   "asset_uploading": "S'està carregant...",
   "assets": "Elements",
@@ -393,7 +409,7 @@
   "assets_moved_to_trash_count": "{count, plural, one {# recurs mogut} other {# recursos moguts}} a la paperera",
   "assets_permanently_deleted_count": "{count, plural, one {# recurs esborrat} other {# recursos esborrats}} permanentment",
   "assets_removed_count": "{count, plural, one {# element eliminat} other {# elements eliminats}}",
-  "assets_restore_confirmation": "Esteu segurs que voleu restaurar tots els teus actius? Aquesta acció no es pot desfer!",
+  "assets_restore_confirmation": "Esteu segurs que voleu restaurar tots els teus actius? Aquesta acció no es pot desfer! Tingueu en compte que els recursos fora de línia no es poden restaurar d'aquesta manera.",
   "assets_restored_count": "{count, plural, one {# element restaurat} other {# elements restaurats}}",
   "assets_trashed_count": "{count, plural, one {# element enviat} other {# elements enviats}} a la paperera",
   "assets_were_part_of_album_count": "{count, plural, one {L'element ja és} other {Els elements ja són}} part de l'àlbum",
@@ -404,6 +420,7 @@
   "birthdate_saved": "Data de naixement guardada amb èxit",
   "birthdate_set_description": "La data de naixement s'utilitza per calcular l'edat d'aquesta persona en el moment d'una foto.",
   "blurred_background": "Fons difuminat",
+  "bugs_and_feature_requests": "Errors i sol·licituds de funcions",
   "build": "Construeix",
   "build_image": "Construeix la imatge",
   "bulk_delete_duplicates_confirmation": "Esteu segur que voleu suprimir de manera massiva {count, plural, one {# recurs duplicat} other {# recursos duplicats}}? Això mantindrà el recurs més gran de cada grup i esborrarà permanentment tots els altres duplicats. No podeu desfer aquesta acció!",
@@ -418,10 +435,6 @@
   "cannot_merge_people": "No es pot fusionar gent",
   "cannot_undo_this_action": "Aquesta acció no es pot desfer!",
   "cannot_update_the_description": "No es pot actualitzar la descripció",
-  "cant_apply_changes": "No es poden aplicar els canvis",
-  "cant_get_faces": "No es poden obtenir les cares",
-  "cant_search_people": "No es pot buscar gent",
-  "cant_search_places": "No es poden cercar llocs",
   "change_date": "Canvia la data",
   "change_expiration_time": "Canvia la data d'expiració",
   "change_location": "Canvia la ubicació",
@@ -440,9 +453,11 @@
   "clear_all_recent_searches": "Esborra totes les cerques recents",
   "clear_message": "Neteja el missatge",
   "clear_value": "Neteja el valor",
+  "clockwise": "En sentit horari",
   "close": "Tanca",
   "collapse": "Tanca",
   "collapse_all": "Redueix-ho tot",
+  "color": "Color",
   "color_theme": "Tema de color",
   "comment_deleted": "Comentari esborrat",
   "comment_options": "Opcions de comentari",
@@ -451,6 +466,7 @@
   "confirm": "Confirmar",
   "confirm_admin_password": "Confirmeu la contrasenya d'administrador",
   "confirm_delete_shared_link": "Esteu segurs que voleu eliminar aquest enllaç compartit?",
+  "confirm_keep_this_delete_others": "Excepte aquest element, tots els altres de la pila se suprimiran. Esteu segur que voleu continuar?",
   "confirm_password": "Confirmació de contrasenya",
   "contain": "Contingut",
   "context": "Context",
@@ -476,6 +492,8 @@
   "create_new_person": "Crea una nova persona",
   "create_new_person_hint": "Assigna els elements seleccionats a una persona nova",
   "create_new_user": "Crea un usuari nou",
+  "create_tag": "Crear etiqueta",
+  "create_tag_description": "Crear una nova etiqueta. Per les etiquetes aniuades, escriu la ruta comperta de l'etiqueta, incloses les barres diagonals.",
   "create_user": "Crea un usuari",
   "created": "Creat",
   "current_device": "Dispositiu actual",
@@ -496,16 +514,21 @@
   "delete_api_key_prompt": "Esteu segurs que voleu eliminar aquesta clau API?",
   "delete_duplicates_confirmation": "Esteu segurs que voleu eliminar aquests duplicats permanentment?",
   "delete_key": "Suprimeix la clau",
-  "delete_library": "Suprimeix la llibreria",
+  "delete_library": "Suprimeix la Llibreria",
   "delete_link": "Esborra l'enllaç",
+  "delete_others": "Suprimeix altres",
   "delete_shared_link": "Odstranit sdílený odkaz",
+  "delete_tag": "Eliminar etiqueta",
+  "delete_tag_confirmation_prompt": "Estàs segur que vols eliminar l'etiqueta {tagName}?",
   "delete_user": "Suprimeix l'usuari",
   "deleted_shared_link": "Suprimeix l'enllaç compartit",
+  "deletes_missing_assets": "Elimina els actius que falten del disc",
   "description": "Descripció",
   "details": "Detalls",
   "direction": "Direcció",
   "disabled": "Desactivat",
   "disallow_edits": "No permetre les edicions",
+  "discord": "Discord",
   "discover": "Descobreix",
   "dismiss_all_errors": "Descarta tots els errors",
   "dismiss_error": "Descarta l'error",
@@ -514,8 +537,11 @@
   "display_original_photos": "Mostra les fotografies originals",
   "display_original_photos_setting_description": "Preferiu mostrar la foto original quan visualitzeu un recurs en lloc de miniatures quan el recurs original és compatible amb el web. Això pot provocar una velocitat de visualització de fotos més lenta.",
   "do_not_show_again": "No tornis a mostrar aquest missatge",
+  "documentation": "Documentació",
   "done": "Fet",
   "download": "Descarregar",
+  "download_include_embedded_motion_videos": "Vídeos incrustats",
+  "download_include_embedded_motion_videos_description": "Incloure vídeos incrustats en fotografies en moviment com un arxiu separat",
   "download_settings": "Descarregar",
   "download_settings_description": "Gestioneu la configuració relacionada amb la descàrrega de recursos",
   "downloading": "Baixant",
@@ -524,13 +550,6 @@
   "duplicates": "Duplicats",
   "duplicates_description": "Resol cada grup indicant quins, si n'hi ha, són duplicats",
   "duration": "Duració",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
   "edit": "Editar",
   "edit_album": "Edita l'àlbum",
   "edit_avatar": "Edita l'avatar",
@@ -538,20 +557,23 @@
   "edit_date_and_time": "Edita data i hora",
   "edit_exclusion_pattern": "Edita patró d'exclusió",
   "edit_faces": "Edita les cares",
-  "edit_import_path": "Edita el camí d'importació",
-  "edit_import_paths": "Edita camins d'importació",
+  "edit_import_path": "Edita la ruta d'importació",
+  "edit_import_paths": "Edita les rutes d'importació",
   "edit_key": "Edita clau",
   "edit_link": "Edita enllaç",
   "edit_location": "Edita ubicació",
   "edit_name": "Edita el nom",
   "edit_people": "Edita la gent",
+  "edit_tag": "Editar etiqueta",
   "edit_title": "Edita títol",
   "edit_user": "Edita l'usuari",
   "edited": "Editat",
   "editor": "Editor",
+  "editor_close_without_save_prompt": "No es desaran els canvis",
+  "editor_close_without_save_title": "Tancar l'editor?",
+  "editor_crop_tool_h2_aspect_ratios": "Relació d'aspecte",
+  "editor_crop_tool_h2_rotation": "Rotació",
   "email": "Correu electrònic",
-  "empty": "",
-  "empty_album": "",
   "empty_trash": "Buidar la paperera",
   "empty_trash_confirmation": "Esteu segur que voleu buidar la paperera? Això eliminarà tots els recursos a la paperera permanentment d'Immich.\nNo podeu desfer aquesta acció!",
   "enable": "Activar",
@@ -585,13 +607,14 @@
     "failed_to_create_shared_link": "No s'ha pogut crear l'enllaç compartit",
     "failed_to_edit_shared_link": "No s'ha pogut editar l'enllaç compartit",
     "failed_to_get_people": "No s'han pogut aconseguir persones",
+    "failed_to_keep_this_delete_others": "No s'ha pogut conservar aquest element i suprimir els altres",
     "failed_to_load_asset": "No s'ha pogut carregar l'element",
     "failed_to_load_assets": "No s'han pogut carregar els elements",
     "failed_to_load_people": "No s'han pogut carregar les persones",
     "failed_to_remove_product_key": "No s'ha pogut eliminar la clau del producte",
     "failed_to_stack_assets": "No s'han pogut apilar els elements",
     "failed_to_unstack_assets": "No s'han pogut desapilar els elements",
-    "import_path_already_exists": "Aquest camí d'importació ja existeix.",
+    "import_path_already_exists": "Aquesta ruta d'importació ja existeix.",
     "incorrect_email_or_password": "Correu electrònic o contrasenya incorrectes",
     "paths_validation_failed": "{paths, plural, one {# ruta} other {# rutes}} no ha pogut validar",
     "profile_picture_transparent_pixels": "Les fotos de perfil no poden tenir píxels transparents. Per favor, feu zoom in, mogueu la imatge o ambdues.",
@@ -601,7 +624,7 @@
     "unable_to_add_assets_to_shared_link": "No s'han pogut afegir els elements a l'enllaç compartit",
     "unable_to_add_comment": "No es pot afegir el comentari",
     "unable_to_add_exclusion_pattern": "No s'ha pogut afegir el patró d’exclusió",
-    "unable_to_add_import_path": "No s'ha pogut afegir el camí d'importació",
+    "unable_to_add_import_path": "No s'ha pogut afegir la ruta d'importació",
     "unable_to_add_partners": "No es poden afegir companys",
     "unable_to_add_remove_archive": "No s'ha pogut {archived, select, true {eliminar l'element de} other {afegir l'element a}} l'arxiu",
     "unable_to_add_remove_favorites": "No s'ha pogut {favorite, select, true {afegir l'element als} other {eliminar l'element dels}} preferits",
@@ -612,8 +635,6 @@
     "unable_to_change_location": "No es pot canviar la ubicació",
     "unable_to_change_password": "No es pot canviar la contrasenya",
     "unable_to_change_visibility": "No es pot canviar la visibilitat de {count, plural, one {# persona} other {# persones}}",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_complete_oauth_login": "No es pot completar l'inici de sessió OAuth",
     "unable_to_connect": "No pot connectar",
     "unable_to_connect_to_server": "No es pot connectar al servidor",
@@ -638,6 +659,7 @@
     "unable_to_get_comments_number": "No es pot obtenir el nombre de comentaris",
     "unable_to_get_shared_link": "No s'ha pogut obtenir l'enllaç compartit",
     "unable_to_hide_person": "No es pot amagar la persona",
+    "unable_to_link_motion_video": "No es pot enllaçar el vídeo en moviment",
     "unable_to_link_oauth_account": "No es pot enllaçar el compte OAuth",
     "unable_to_load_album": "No es pot carregar l'àlbum",
     "unable_to_load_asset_activity": "No es pot carregar l'activitat dels recursos",
@@ -653,12 +675,10 @@
     "unable_to_remove_album_users": "No es poden eliminar usuaris de l'àlbum",
     "unable_to_remove_api_key": "No es pot eliminar la clau de l'API",
     "unable_to_remove_assets_from_shared_link": "No es poden eliminar recursos de l'enllaç compartit",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_deleted_assets": "No es poden eliminar els fitxers fora de línia",
     "unable_to_remove_library": "No es pot eliminar la biblioteca",
-    "unable_to_remove_offline_files": "No es poden eliminar els fitxers fora de línia",
     "unable_to_remove_partner": "No es pot eliminar company/a",
     "unable_to_remove_reaction": "No es pot eliminar la reacció",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "No es poden reparar els elements",
     "unable_to_reset_password": "No es pot restablir la contrasenya",
     "unable_to_resolve_duplicate": "No es pot resoldre el duplicat",
@@ -678,6 +698,7 @@
     "unable_to_submit_job": "No es pot enviar la tasca",
     "unable_to_trash_asset": "No es pot eliminar el recurs a la paperera",
     "unable_to_unlink_account": "No es pot desenllaçar el compte",
+    "unable_to_unlink_motion_video": "No es pot desvincular el vídeo en moviment",
     "unable_to_update_album_cover": "No es pot actualitzar la portada de l'àlbum",
     "unable_to_update_album_info": "No es pot actualitzar la informació de l'àlbum",
     "unable_to_update_library": "No es pot actualitzar la biblioteca",
@@ -687,10 +708,6 @@
     "unable_to_update_user": "No es pot actualitzar l'usuari",
     "unable_to_upload_file": "No es pot carregar el fitxer"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exif": "Exif",
   "exit_slideshow": "Surt de la presentació de diapositives",
   "expand_all": "Ampliar-ho tot",
@@ -698,35 +715,34 @@
   "expired": "Caducat",
   "expires_date": "Caduca el {date}",
   "explore": "Explorar",
+  "explorer": "Explorador",
   "export": "Exporta",
   "export_as_json": "Exportar com a JSON",
   "extension": "Extensió",
   "external": "Extern",
   "external_libraries": "Llibreries externes",
   "face_unassigned": "Sense assignar",
-  "failed_to_get_people": "",
   "favorite": "Preferit",
   "favorite_or_unfavorite_photo": "Foto preferida o no preferida",
   "favorites": "Preferits",
-  "feature": "",
   "feature_photo_updated": "Foto destacada actualitzada",
-  "featurecollection": "",
+  "features": "Característiques",
+  "features_setting_description": "Administrar les funcions de l'aplicació",
   "file_name": "Nom de l'arxiu",
   "file_name_or_extension": "Nom de l'arxiu o extensió",
   "filename": "Nom del fitxer",
-  "files": "",
   "filetype": "Tipus d'arxiu",
   "filter_people": "Filtra persones",
   "find_them_fast": "Trobeu-los ràpidament pel nom amb la cerca",
   "fix_incorrect_match": "Corregiu la coincidència incorrecta",
-  "force_re-scan_library_files": "Força a tornar a escanejar tots els fitxers de la biblioteca",
+  "folders": "Carpetes",
+  "folders_feature_description": "Explorar la vista de carpetes per les fotos i vídeos del sistema d'arxius",
   "forward": "Endavant",
   "general": "General",
   "get_help": "Aconseguir ajuda",
   "getting_started": "Començant",
   "go_back": "Torna",
   "go_to_search": "Vés a cercar",
-  "go_to_share_page": "Vés a la pàgina de compartir",
   "group_albums_by": "Agrupa àlbums per...",
   "group_no": "Cap agrupació",
   "group_owner": "Agrupar per propietari",
@@ -752,7 +768,6 @@
   "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} pres/a a {city}, {country} amb {person1} i {person2} el {date}",
   "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} pres/a a {city}, {country} amb {person1}, {person2}, i {person3} el {date}",
   "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} pres/a a {city}, {country} amb {person1}, {person2}, i {additionalCount, number} altres el {date}",
-  "img": "",
   "immich_logo": "Logotip d'Immich",
   "immich_web_interface": "Interfície web Immich",
   "import_from_json": "Importar des de JSON",
@@ -773,10 +788,11 @@
   "invite_people": "Convida gent",
   "invite_to_album": "Convida a l'àlbum",
   "items_count": "{count, plural, one {# element} other {# elements}}",
-  "job_settings_description": "",
   "jobs": "Tasques",
   "keep": "Mantenir",
   "keep_all": "Mantenir-ho tot",
+  "keep_this_delete_others": "Conserveu-ho, suprimiu-ne els altres",
+  "kept_this_deleted_others": "S'ha conservat aquest element i s'han suprimit {count, plural, one {# asset} other {# assets}}",
   "keyboard_shortcuts": "Dreceres de teclat",
   "language": "Idioma",
   "language_setting_description": "Seleccioneu el vostre idioma",
@@ -788,21 +804,9 @@
   "level": "Nivell",
   "library": "Bibilioteca",
   "library_options": "Opcions de biblioteca",
-  "license_activated_title": "La vostra llicència ha estat activada amb èxit",
-  "license_button_activate": "Activar",
-  "license_button_buy": "Comprar",
-  "license_button_select": "Seleccionar",
-  "license_individual_title": "Llicència individual",
-  "license_info_unlicensed": "Sense llicència",
-  "license_license_title": "LLICÈNCIA",
-  "license_per_server": "Per servidor",
-  "license_per_user": "Per usuari",
-  "license_server_description_1": "1 llicència per servidor",
-  "license_server_title": "Llicència de servidor",
-  "license_trial_info_2": "Heu utilitzat l'Immich durant uns",
-  "license_trial_info_3": "{accountAge, plural, one {# dia} other {# dies}}",
   "light": "Llum",
   "like_deleted": "M'agrada suprimit",
+  "link_motion_video": "Enllaçar vídeo en moviment",
   "link_options": "Opcions d'enllaç",
   "link_to_oauth": "Enllaç a OAuth",
   "linked_oauth_account": "Compte OAuth enllaçat",
@@ -821,8 +825,9 @@
   "look": "Aspecte",
   "loop_videos": "Vídeos en bucle",
   "loop_videos_description": "Habilita la reproducció en bucle del vídeo en els detalls.",
+  "main_branch_warning": "Esteu usant una versió de desenvolupaent. Recomanem fer servir una versió publicada!",
   "make": "Fabricant",
-  "manage_shared_links": "Spravovat sdílené odkazy",
+  "manage_shared_links": "Administrar enllaços compartits",
   "manage_sharing_with_partners": "Gestiona la compartició amb els companys",
   "manage_the_app_settings": "Gestioneu la configuració de l'aplicació",
   "manage_your_account": "Gestiona el teu compte",
@@ -890,18 +895,20 @@
   "notifications": "Notificacions",
   "notifications_setting_description": "Gestiona les notificacions",
   "oauth": "OAuth",
+  "official_immich_resources": "Recursos oficials d'Immich",
   "offline": "Fora de línia",
   "offline_paths": "Rutes fora de línia",
   "offline_paths_description": "Aquests resultats poden ser deguts a la supressió manual de fitxers que no formen part d'una biblioteca externa.",
   "ok": "D'acord",
   "oldest_first": "El més vell primer",
-  "onboarding": "Onboarding",
+  "onboarding": "Incorporació",
+  "onboarding_privacy_description": "Les següents funcions (opcionals) depenen de serveis externs i poden desactivarse en qualsevol moment de dels ajustos.",
   "onboarding_theme_description": "Trieu un tema de color per a la vostra instància. Podeu canviar-ho més endavant a la vostra configuració.",
   "onboarding_welcome_description": "Configurem la vostra instància amb alguns paràmetres habituals.",
   "onboarding_welcome_user": "Benvingut, {user}",
   "online": "En línia",
   "only_favorites": "Només preferits",
-  "only_refreshes_modified_files": "Només actualitza els fitxers modificats",
+  "open_in_map_view": "Obrir a la vista del mapa",
   "open_in_openstreetmap": "Obre a OpenStreetMap",
   "open_the_search_filters": "Obriu els filtres de cerca",
   "options": "Opcions",
@@ -936,8 +943,8 @@
   "pending": "Pendent",
   "people": "Persones",
   "people_edits_count": "{count, plural, one {# persona editada} other {# persones editades}}",
+  "people_feature_description": "Explorar fotos i vídeos agrupades per persona",
   "people_sidebar_description": "Mostrar un enllaç a Persones a la barra lateral",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "Avís d'eliminació permanent",
   "permanent_deletion_warning_setting_description": "Mostrar un avís quan s'eliminin els elements permanentment",
   "permanently_delete": "Eliminar permanentment",
@@ -959,7 +966,6 @@
   "play_memories": "Reproduir records",
   "play_motion_photo": "Reproduir Fotos en Moviment",
   "play_or_pause_video": "Reproduir o posar en pausa el vídeo",
-  "point": "",
   "port": "Port",
   "preset": "Preestablert",
   "preview": "Previsualització",
@@ -967,6 +973,7 @@
   "previous_memory": "Memòria anterior",
   "previous_or_next_photo": "Foto anterior o següent",
   "primary": "Primària",
+  "privacy": "Privacitat",
   "profile_image_of_user": "Imatge de perfil de {user}",
   "profile_picture_set": "Imatge de perfil configurada.",
   "public_album": "Àlbum públic",
@@ -1003,8 +1010,10 @@
   "purchase_server_description_2": "Estat del contribuent",
   "purchase_server_title": "Servidor",
   "purchase_settings_server_activated": "La clau de producte del servidor la gestiona l'administrador",
-  "range": "",
-  "raw": "",
+  "rating": "Valoració",
+  "rating_clear": "Esborrar valoració",
+  "rating_count": "{count, plural, one {# estrella} other {# estrelles}}",
+  "rating_description": "Mostrar la valoració EXIF al panell d'informació",
   "reaction_options": "Opcions de reacció",
   "read_changelog": "Llegeix el registre de canvis",
   "reassign": "Reassignar",
@@ -1015,11 +1024,13 @@
   "recent_searches": "Cerques recents",
   "refresh": "Actualitzar",
   "refresh_encoded_videos": "Actualitza vídeos codificats",
+  "refresh_faces": "Actualitzar cares",
   "refresh_metadata": "Actualitzar les metadades",
   "refresh_thumbnails": "Actualitzar la miniatura",
   "refreshed": "Actualitzat",
-  "refreshes_every_file": "Actualitza tots els fitxers",
+  "refreshes_every_file": "Torna a llegir tots els fitxers existents i nous",
   "refreshing_encoded_video": "S'està actualitzant el vídeo codificat",
+  "refreshing_faces": "Refrescant cares",
   "refreshing_metadata": "Actualitzant les metadades",
   "regenerating_thumbnails": "Regenerant les miniatures",
   "remove": "Eliminar",
@@ -1027,15 +1038,16 @@
   "remove_assets_shared_link_confirmation": "Esteu segur que voleu eliminar {count, plural, one {# recurs} other {# recursos}} d'aquest enllaç compartit?",
   "remove_assets_title": "Eliminar els elements?",
   "remove_custom_date_range": "Elimina l'interval de dates personalitzat",
+  "remove_deleted_assets": "Suprimeix fitxers fora de línia",
   "remove_from_album": "Treu de l'àlbum",
   "remove_from_favorites": "Eliminar dels preferits",
   "remove_from_shared_link": "Eliminar de l'enllaç compartit",
-  "remove_offline_files": "Suprimeix fitxers fora de línia",
   "remove_user": "Eliminar l'usuari",
   "removed_api_key": "Eliminada la clau d'API: {name}",
   "removed_from_archive": "Eliminat de l'arxiu",
   "removed_from_favorites": "Eliminat dels preferits",
   "removed_from_favorites_count": "{count, plural, other {# eliminats}} dels preferits",
+  "removed_tagged_assets": "Etiqueta eliminada de {count, plural, one {# actiu} other {# actius}}",
   "rename": "Canviar nom",
   "repair": "Reparació",
   "repair_no_results_message": "Els fitxers sense seguiment i que falten es mostraran aquí",
@@ -1046,7 +1058,6 @@
   "reset": "Restablir",
   "reset_password": "Restablir contrasenya",
   "reset_people_visibility": "Restablir la visibilitat de les persones",
-  "reset_settings_to_default": "",
   "reset_to_default": "Restableix els valors predeterminats",
   "resolve_duplicates": "Resoldre duplicats",
   "resolved_all_duplicates": "Tots els duplicats resolts",
@@ -1066,15 +1077,14 @@
   "saved_settings": "Configuració guardada",
   "say_something": "Digues quelcom",
   "scan_all_libraries": "Escanejar totes les llibreries",
-  "scan_all_library_files": "Re-escanejar tots els fitxers de la llibreria",
-  "scan_new_library_files": "Escanejar nous fitxers de la llibreria",
+  "scan_library": "Escaneja",
   "scan_settings": "Configuració d'escaneig",
   "scanning_for_album": "S'està buscant l'àlbum...",
   "search": "Cerca",
   "search_albums": "Buscar àlbums",
   "search_by_context": "Buscar per context",
   "search_by_filename": "Cerca per nom de fitxer o extensió",
-  "search_by_filename_example": "i.e. IMG_1234.JPG or PNG",
+  "search_by_filename_example": "per exemple IMG_1234.JPG o PNG",
   "search_camera_make": "Buscar per fabricant de càmara...",
   "search_camera_model": "Buscar per model de càmera...",
   "search_city": "Buscar per ciutat...",
@@ -1082,9 +1092,12 @@
   "search_for_existing_person": "Busca una persona existent",
   "search_no_people": "Cap persona",
   "search_no_people_named": "Cap persona anomenada \"{name}\"",
+  "search_options": "Opcions de cerca",
   "search_people": "Buscar persones",
   "search_places": "Buscar llocs",
+  "search_settings": "Configuració de cerca",
   "search_state": "Buscar per regió...",
+  "search_tags": "Cercant etiquetes...",
   "search_timezone": "Buscar per fus horari...",
   "search_type": "Buscar per tipus",
   "search_your_photos": "Cerca les teves fotos",
@@ -1107,7 +1120,6 @@
   "selected_count": "{count, plural, one {# seleccionat} other {# seleccionats}}",
   "send_message": "Envia missatge",
   "send_welcome_email": "Envia correu de benvinguda",
-  "server": "Servidor",
   "server_offline": "Servidor fora de línia",
   "server_online": "Servidor en línia",
   "server_stats": "Estadístiques del servidor",
@@ -1126,6 +1138,7 @@
   "shared_by_user": "Compartit per {user}",
   "shared_by_you": "Compartit per tu",
   "shared_from_partner": "Fotos de {partner}",
+  "shared_link_options": "Opcions d'enllaços compartits",
   "shared_links": "Enllaços compartits",
   "shared_photos_and_videos_count": "{assetCount, plural, other {# fotos i vídeos compartits.}}",
   "shared_with_partner": "Compartit amb {partner}",
@@ -1134,6 +1147,7 @@
   "sharing_sidebar_description": "Mostra un enllaç a Compartit a la barra lateral",
   "shift_to_permanent_delete": "premeu ⇧ per suprimir el recurs permanentment",
   "show_album_options": "Mostra les opcions d'àlbum",
+  "show_albums": "Mostrar àlbums",
   "show_all_people": "Veure totes les persones",
   "show_and_hide_people": "Mostra i amaga persones",
   "show_file_location": "Mostra l'ubicació del fitxer",
@@ -1148,13 +1162,18 @@
   "show_person_options": "Mostra opcions de la persona",
   "show_progress_bar": "Mostra barra de progrés",
   "show_search_options": "Mostra opcions de cerca",
+  "show_slideshow_transition": "Mostra la transició de la presentació de diapositives",
   "show_supporter_badge": "Insígnia de contribuent",
   "show_supporter_badge_description": "Mostra una insígnia de contributor",
   "shuffle": "Mescla",
+  "sidebar": "Barra lateral",
+  "sidebar_display_description": "Mostra un enllaç a la vista a la barra lateral",
   "sign_out": "Tanca sessió",
   "sign_up": "Registrar-se",
   "size": "Mida",
   "skip_to_content": "Salta al contingut",
+  "skip_to_folders": "Anar a carpetes",
+  "skip_to_tags": "Anar a etiquetes",
   "slideshow": "Diapositives",
   "slideshow_settings": "Configuració de diapositives",
   "sort_albums_by": "Ordena àlbums per...",
@@ -1166,6 +1185,8 @@
   "sort_title": "Títol",
   "source": "Font",
   "stack": "Apila",
+  "stack_duplicates": "Aplicar duplicats",
+  "stack_select_one_photo": "Selecciona una imatge principal per la pila",
   "stack_selected_photos": "Apila les fotos seleccionades",
   "stacked_assets_count": "Apilats {count, plural, one {# element} other {# elements}}",
   "stacktrace": "Traça de pila",
@@ -1183,23 +1204,35 @@
   "submit": "Envia",
   "suggestions": "Suggeriments",
   "sunrise_on_the_beach": "Albada a la platja",
+  "support": "Suport",
+  "support_and_feedback": "Suport i comentaris",
+  "support_third_party_description": "La vostra instal·lació immich la va empaquetar un tercer. Els problemes que experimenteu poden ser causats per aquest paquet així que, si us plau, plantegeu els poblemes amb ells en primer lloc mitjançant els enllaços següents.",
   "swap_merge_direction": "Canvia la direcció d'unió",
   "sync": "Sincronitza",
+  "tag": "Etiqueta",
+  "tag_assets": "Etiquetar actius",
+  "tag_created": "Etiqueta creada: {tag}",
+  "tag_feature_description": "Exploreu fotos i vídeos agrupats per temes d'etiquetes lògiques",
+  "tag_not_found_question": "No trobeu una etiqueta? <link>Crear una nova etiqueta</link>",
+  "tag_updated": "Etiqueta actualizada: {tag}",
+  "tagged_assets": "{count, plural, one {#Etiquetat} other {#Etiquetats}} {count, plural, one {# actiu} other {# actius}}",
+  "tags": "Etiquetes",
   "template": "Plantilla",
   "theme": "Tema",
   "theme_selection": "Selecció de tema",
   "theme_selection_description": "Activa automàticament el tema fosc o clar en funció de les preferències del sistema del navegador",
   "they_will_be_merged_together": "Es combinaran",
+  "third_party_resources": "Recursos de tercers",
   "time_based_memories": "Records basats en el temps",
   "timezone": "Fus horari",
   "to_archive": "Arxivar",
   "to_change_password": "Canviar la contrasenya",
   "to_favorite": "Prefereix",
   "to_login": "Iniciar sessió",
+  "to_parent": "Anar als pares",
   "to_trash": "Paperera",
   "toggle_settings": "Canvia configuració",
-  "toggle_theme": "Canvia tema",
-  "toggle_visibility": "Canvia visibilitat",
+  "toggle_theme": "Alternar tema",
   "total_usage": "Ús total",
   "trash": "Paperera",
   "trash_all": "Envia-ho tot a la paperera",
@@ -1209,17 +1242,17 @@
   "trashed_items_will_be_permanently_deleted_after": "Els elements que s'enviïn a la paperera s'eliminaran permanentment després de {days, plural, one {# dia} other {# dies}}.",
   "type": "Tipus",
   "unarchive": "Desarxivar",
-  "unarchived": "Desarxivat",
   "unarchived_count": "{count, plural, other {# elements desarxivats}}",
   "unfavorite": "Reverteix preferit",
   "unhide_person": "Mostra persona",
   "unknown": "Desconegut",
-  "unknown_album": "Àlbum desconegut",
   "unknown_year": "Any desconegut",
   "unlimited": "Il·limitat",
+  "unlink_motion_video": "Desvincular vídeo en moviment",
   "unlink_oauth": "Desvincula OAuth",
   "unlinked_oauth_account": "Compte Oauth desvinculat",
   "unnamed_album": "Àlbum sense nom",
+  "unnamed_album_delete_confirmation": "Segur que voleu esborrar aquest àlbum?",
   "unnamed_share": "Compartit sense nom",
   "unsaved_change": "Canvi no desat",
   "unselect_all": "Deselecciona-ho tot",
@@ -1244,12 +1277,13 @@
   "use_custom_date_range": "Fes servir un rang de dates personalitzat",
   "user": "Usuari",
   "user_id": "ID d'usuari",
-  "user_license_settings": "Llicència",
   "user_liked": "A {user} li ha agradat {type, select, photo {aquesta foto} video {aquest vídeo} asset {aquest recurs} other {}}",
   "user_purchase_settings": "Compra",
   "user_purchase_settings_description": "Gestiona la teva compra",
   "user_role_set": "Establir {user} com a {role}",
   "user_usage_detail": "Detall d'ús d'usuari",
+  "user_usage_stats": "Estadístiques d'ús de del compte",
+  "user_usage_stats_description": "Veure les estadístiques d'ús del compte",
   "username": "Nom d'usuari",
   "users": "Usuaris",
   "utilities": "Utilitats",
@@ -1257,7 +1291,9 @@
   "variables": "Variables",
   "version": "Versió",
   "version_announcement_closing": "El teu amic Alex",
-  "version_announcement_message": "Hola amic, hi ha una nova versió de l'aplicació, si us plau, preneu-vos el temps per visitar les <link>release notes</link> i assegureu-vos que el vostre <code>docker-compose.yml</code> i <code>.env</code> estàn actualitzats per evitar qualsevol configuració incorrecta, especialment si utilitzeu WatchTower o qualsevol mecanisme que gestioni l'actualització automàtica de la vostra aplicació.",
+  "version_announcement_message": "Hola! Hi ha una nova versió d'Immich, si us plau, preneu-vos una estona per llegir les <link>notes de llançament</link> per assegurar que la teva configuració estigui actualitzada per evitar qualsevol error de configuració, especialment si utilitzeu WatchTower o qualsevol mecanisme que gestioni l'actualització automàtica de la vostra instància Immich.",
+  "version_history": "Historial de versions",
+  "version_history_item": "Instal·lat {version} el {date}",
   "video": "Vídeo",
   "video_hover_setting": "Reprodueix la miniatura en passar el ratolí",
   "video_hover_setting_description": "Reprodueix la miniatura quan el ratolí plana sobre l'element. Fins i tot quan estigui deshabilitat, la reproducció s'iniciarà planant sobre el botó de reproducció.",
@@ -1267,11 +1303,11 @@
   "view_album": "Veure l'àlbum",
   "view_all": "Veure tot",
   "view_all_users": "Mostra tot els usuaris",
+  "view_in_timeline": "Mostrar a la línia de temps",
   "view_links": "Mostra enllaços",
   "view_next_asset": "Mostra el següent element",
   "view_previous_asset": "Mostra l'element anterior",
   "view_stack": "Veure la pila",
-  "viewer": "Visualitzador",
   "visibility_changed": "La visibilitat ha canviat per {count, plural, one {# persona} other {# persones}}",
   "waiting": "Esperant",
   "warning": "Avís",
diff --git a/web/src/lib/i18n/cs.json b/i18n/cs.json
similarity index 89%
rename from web/src/lib/i18n/cs.json
rename to i18n/cs.json
index ec97fe01b2..a762f26b9a 100644
--- a/web/src/lib/i18n/cs.json
+++ b/i18n/cs.json
@@ -23,16 +23,23 @@
   "add_to": "Přidat do...",
   "add_to_album": "Přidat do alba",
   "add_to_shared_album": "Přidat do sdíleného alba",
+  "add_url": "Přidat URL",
   "added_to_archive": "Přidáno do archivu",
   "added_to_favorites": "Přidáno do oblíbených",
   "added_to_favorites_count": "Přidáno {count, number} do oblíbených",
   "admin": {
     "add_exclusion_pattern_description": "Přidání vzorů vyloučení. Podporováno je globování pomocí *, ** a ?. Chcete-li ignorovat všechny soubory v jakémkoli adresáři s názvem \"Raw\", použijte \"**/Raw/**\". Chcete-li ignorovat všechny soubory končící na \".tif\", použijte \"**/*.tif\". Chcete-li ignorovat absolutní cestu, použijte příkaz \"/path/to/ignore/**\".",
+    "asset_offline_description": "Tato položka externí knihovny se již na disku nenachází a byla přesunuta do koše. Pokud byl soubor přesunut v rámci knihovny, zkontrolujte časovou osu a vyhledejte nové odpovídající položku. Chcete-li tuto položku obnovit, ujistěte se, že je cesta k níže uvedenému souboru přístupná pomocí aplikace Immich a prohledejte knihovnu.",
     "authentication_settings": "Přihlašování",
     "authentication_settings_description": "Správa hesel, OAuth a dalších nastavení ověření",
     "authentication_settings_disable_all": "Opravdu chcete zakázat všechny metody přihlášení? Přihlašování bude úplně zakázáno.",
     "authentication_settings_reenable": "Pro opětovné povolení použijte příkaz <link>Příkaz serveru</link>.",
     "background_task_job": "Úkoly na pozadí",
+    "backup_database": "Zálohování databáze",
+    "backup_database_enable_description": "Povolit zálohování databáze",
+    "backup_keep_last_amount": "Počet předchozích záloh k uchování",
+    "backup_settings": "Nastavení zálohování",
+    "backup_settings_description": "Správa nastavení zálohování databáze",
     "check_all": "Vše zkontrolovat",
     "cleared_jobs": "Hotové úlohy pro: {job}",
     "config_set_by_file": "Konfigurace je aktuálně prováděna konfiguračním souborem",
@@ -41,35 +48,40 @@
     "confirm_email_below": "Pro potvrzení zadejte níže \"{email}\"",
     "confirm_reprocess_all_faces": "Opravdu chcete znovu zpracovat všechny obličeje? Tím se vymažou i pojmenované osoby.",
     "confirm_user_password_reset": "Opravdu chcete obnovit heslo uživatele {user}?",
-    "crontab_guru": "Crontab Guru",
+    "create_job": "Vytvořit úlohu",
+    "cron_expression": "Výraz cron",
+    "cron_expression_description": "Nastavte interval prohledávání pomocí cron formátu. Další informace naleznete např. v <link>Crontab Guru</link>",
+    "cron_expression_presets": "Předvolby výrazů cron",
     "disable_login": "Zakázat přihlášení",
-    "disabled": "Zakázáno",
     "duplicate_detection_job_description": "Spuštění strojového učení na položkách za účelem detekce podobných obrázků. Spoléhá na Chytré vyhledávání",
     "exclusion_pattern_description": "Vzory vyloučení umožňují při prohledávání knihovny ignorovat soubory a složky. To je užitečné, pokud máte složky obsahující soubory, které nechcete importovat, například RAW soubory.",
     "external_library_created_at": "Externí knihovna (vytvořena {date})",
     "external_library_management": "Správa externích knihoven",
     "face_detection": "Detekce obličejů",
-    "face_detection_description": "Detekce obličejů v obrázcích pomocí strojového učení. U videí se bere v úvahu pouze miniatura. \"Vše\" znovu zpracovává všechny položky. \"Chybějící\" zařadí do fronty položky, které ještě nebyly zpracovány. Zjištěné obličeje budou po dokončení funkce Rozpoznávání obličejů zařazeny do fronty a seskupeny do stávajících nebo nových osob.",
-    "facial_recognition_job_description": "Seskupí nalezené obličeje do osob. Tento krok se spustí po dokončení detekce obličejů. \"Vše\" znovu seskupí všechny obličeje. \"Chybějící\" zpracuje obličeje, které nemají přiřazenou osobu.",
+    "face_detection_description": "Detekce obličejů v obrázcích pomocí strojového učení. U videí se bere v úvahu pouze miniatura. „Obnovit“ znovu zpracuje všechny položky. „Resetovat“ navíc vymaže všechna aktuální data obličejů. „Chybějící“ zařadí do fronty položky, které ještě nebyly zpracovány. Zjištěné obličeje budou po dokončení funkce Rozpoznávání obličejů zařazeny do fronty a seskupeny do stávajících nebo nových osob.",
+    "facial_recognition_job_description": "Seskupí nalezené obličeje do osob. Tento krok se spustí po dokončení detekce obličejů. „Resetovat“ znovu seskupí všechny obličeje. „Chybějící“ zpracuje obličeje, které nemají přiřazenou osobu.",
     "failed_job_command": "Příkaz {command} se nezdařil pro úlohu: {job}",
     "force_delete_user_warning": "UPOZORNĚNÍ: Tímto okamžitě odstraníte uživatele a všechny jeho položky. Tento krok nelze vrátit zpět a soubory nelze obnovit.",
     "forcing_refresh_library_files": "Vynucení obnovy všech souborů knihovny",
+    "image_format": "Formát",
     "image_format_description": "WebP vytváří menší soubory než JPEG, ale je pomalejší při kódování.",
     "image_prefer_embedded_preview": "Preferovat vložený náhled",
     "image_prefer_embedded_preview_setting_description": "Použít vložené náhledy z RAW fotografií jako vstup pro zpracování snímků, pokud jsou k dispozici. U některých snímků tak lze dosáhnout přesnějších barev, ale kvalita náhledu závisí na fotoaparátu a snímek může obsahovat více kompresních artefaktů.",
     "image_prefer_wide_gamut": "Preferovat široký gamut",
     "image_prefer_wide_gamut_setting_description": "Použít Display P3 pro miniatury. To lépe zachovává živost obrázků s širokým barevným prostorem, ale obrázky se mohou na starých zařízeních se starou verzí prohlížeče zobrazovat jinak. sRGB obrázky jsou ponechány jako sRGB, aby se zabránilo posunům barev.",
-    "image_preview_format": "Formát náhledů",
-    "image_preview_resolution": "Rozlišení náhledů",
-    "image_preview_resolution_description": "Používá se při prohlížení jedné fotografie a pro strojové učení. Vyšší rozlišení mohou zachovat více detailů, ale jejich kódování trvá déle, mají větší velikost souboru a mohou snížit odezvu aplikace.",
+    "image_preview_description": "Středně velký obrázek se zbavenými metadaty, který se používá při prohlížení jedné položky a pro strojové učení",
+    "image_preview_quality_description": "Kvalita náhledu od 1 do 100. Vyšší je lepší, ale vytváří větší soubory a může snížit responzivitu aplikace. Nastavení nízké hodnoty může ovlivnit kvalitu strojového učení.",
+    "image_preview_title": "Náhledy",
     "image_quality": "Kvalita",
-    "image_quality_description": "Kvalita obrazu od 1 do 100. Vyšší kvalita je lepší, ale vytváří větší soubory, tato volba ovlivňuje náhled a miniatury obrázků.",
+    "image_resolution": "Rozlišení",
+    "image_resolution_description": "Vyšší rozlišení mohou zachovat více detailů, ale jejich kódování trvá déle, mají větší velikost souboru a mohou snížit odezvu aplikace.",
     "image_settings": "Obrázky",
     "image_settings_description": "Správa kvality a rozlišení generovaných obrázků",
-    "image_thumbnail_format": "Formát miniatur",
-    "image_thumbnail_resolution": "Rozlišení miniatur",
-    "image_thumbnail_resolution_description": "Používá se při prohlížení skupin fotografií (hlavní časová osa, zobrazení alba atd.). Vyšší rozlišení může zachovat více detailů, ale trvá déle, než se zakóduje, má větší velikost souboru a může snížit odezvu aplikace.",
-    "job_concurrency": "Souběžnost {job}",
+    "image_thumbnail_description": "Malá miniatura s odstraněnými metadaty, který se používá při prohlížení skupin fotografií, jako je hlavní časová osa",
+    "image_thumbnail_quality_description": "Kvalita miniatur od 1 do 100. Vyšší je lepší, ale vytváří větší soubory a může snížit odezvu aplikace.",
+    "image_thumbnail_title": "Miniatury",
+    "job_concurrency": "Souběžnost úlohy {job}",
+    "job_created": "Úloha vytvořena",
     "job_not_concurrency_safe": "Tato úloha není bezpečená pro souběh.",
     "job_settings": "Úlohy",
     "job_settings_description": "Správa souběžnosti úloh",
@@ -77,9 +89,6 @@
     "jobs_delayed": "{jobCount, plural, one {# zpožděný} few {# zpožděné} other {# zpožděných}}",
     "jobs_failed": "{jobCount, plural, one {# neúspěšný} few {# neúspěšné} other {# neúspěšných}}",
     "library_created": "Vytvořena knihovna: {library}",
-    "library_cron_expression": "Výraz pro Cron",
-    "library_cron_expression_description": "Nastavte interval prohledávání pomocí formátu cron. Další informace naleznete např. v <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Předvolby výrazu pro Cron",
     "library_deleted": "Knihovna smazána",
     "library_import_path_description": "Zadejte složku, kterou chcete importovat. Tato složka bude prohledána včetně podsložek a budou v ní hledány obrázky a videa.",
     "library_scanning": "Pravidelné prohledávání",
@@ -98,7 +107,7 @@
     "machine_learning_clip_model_description": "Název CLIP modelu je uvedený <link>zde</link>. Pamatujte, že při změně modelu je nutné znovu spustit úlohu 'Chytré vyhledávání' pro všechny obrázky.",
     "machine_learning_duplicate_detection": "Kontrola duplicit",
     "machine_learning_duplicate_detection_enabled": "Povolit kontrolu duplicit",
-    "machine_learning_duplicate_detection_enabled_description": "Pokud je tato funkce vypnuta, budou identické položky stále duplikovány.",
+    "machine_learning_duplicate_detection_enabled_description": "Pokud je tato funkce vypnuta, budou identické položky stále deduplikovány.",
     "machine_learning_duplicate_detection_setting_description": "Použít CLIP embeddings k nalezení pravděpodobných duplicit",
     "machine_learning_enabled": "Povolit strojové učení",
     "machine_learning_enabled_description": "Pokud je vypnuto, budou všechny funkce strojového učení vypnuty bez ohledu na níže uvedená nastavení.",
@@ -122,7 +131,7 @@
     "machine_learning_smart_search_description": "Sémantické vyhledávání obrázků pomocí CLIP embeddings",
     "machine_learning_smart_search_enabled": "Povolit chytré vyhledávání",
     "machine_learning_smart_search_enabled_description": "Pokud je vypnuto, obrázky nebudou kódovány pro inteligentní vyhledávání.",
-    "machine_learning_url_description": "URL serveru pro strojové učení",
+    "machine_learning_url_description": "URL serveru strojového učení. Pokud je zadáno více URL adres, budou jednotlivé servery zkoušeny postupně, dokud jeden z nich neodpoví úspěšně, a to v pořadí od prvního k poslednímu.",
     "manage_concurrency": "Správa souběžnosti",
     "manage_log_settings": "Správa nastavení protokolu",
     "map_dark_style": "Tmavý motiv",
@@ -148,11 +157,11 @@
     "migration_job_description": "Migrace miniatur snímků a obličejů do nejnovější struktury složek",
     "no_paths_added": "Nebyly přidány žádné cesty",
     "no_pattern_added": "Nebyl přidán žádný vzor",
-    "note_apply_storage_label_previous_assets": "Upozornění: Pro uplatnění Štítku úložiště na dříve nahrané položky, spusťte",
+    "note_apply_storage_label_previous_assets": "Upozornění: Pro uplatnění Štítku úložiště na dříve nahrané položky spusťte",
     "note_cannot_be_changed_later": "UPOZORNĚNÍ: Toto nelze později změnit!",
     "note_unlimited_quota": "Upozornění: Pro neomezenou kvótu zadejte 0",
     "notification_email_from_address": "Adresa Od",
-    "notification_email_from_address_description": "E-mailová adresa odesílatele, např.: \"Immich Photo Server <noreply@immich.app>\"",
+    "notification_email_from_address_description": "E-mailová adresa odesílatele, např.: „Immich Photo Server <noreply@example.com>“",
     "notification_email_host_description": "Adresa e-mailového serveru (např. smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Ignorovat chyby certifikátů",
     "notification_email_ignore_certificate_errors_description": "Ignorovat chyby ověření certifikátu TLS (nedoporučuje se)",
@@ -198,22 +207,24 @@
     "password_settings": "Přihlášení heslem",
     "password_settings_description": "Správa nastavení přihlašování pomocí hesla",
     "paths_validated_successfully": "Všechny cesty byly úspěšně ověřeny",
+    "person_cleanup_job": "Promazání osob",
     "quota_size_gib": "Velikost kvóty (GiB)",
     "refreshing_all_libraries": "Obnovení všech knihoven",
     "registration": "Registrace správce",
     "registration_description": "Vzhledem k tomu, že jste prvním uživatelem v systému, budete přiřazen jako správce a budete zodpovědný za úkoly správy a další uživatelé budou vytvořeni vámi.",
-    "removing_offline_files": "Odstranění offline souborů",
     "repair_all": "Opravit vše",
     "repair_matched_items": "Shoda {count, plural, one {# položky} other {# položek}}",
     "repaired_items": "{count, plural, one {Opravena # položka} few {Opraveny # položky} other {Opraveno # položek}}",
     "require_password_change_on_login": "Požadovat, aby si uživatel při prvním přihlášení změnil heslo",
     "reset_settings_to_default": "Obnovení výchozího nastavení",
     "reset_settings_to_recent_saved": "Obnovit poslední uložené nastavení",
-    "scanning_library_for_changed_files": "Hledání změněných souborů v knihovně",
-    "scanning_library_for_new_files": "Hledání nových souborů v knihovně",
+    "scanning_library": "Prohledat knihovnu",
+    "search_jobs": "Hledat úlohy...",
     "send_welcome_email": "Odeslat uvítací e-mail",
     "server_external_domain_settings": "Externí doména",
     "server_external_domain_settings_description": "Doména pro veřejně sdílené odkazy, včetně http(s)://",
+    "server_public_users": "Veřejní uživatelé",
+    "server_public_users_description": "Všichni uživatelé (jméno a e-mail) jsou uvedeni při přidávání uživatele do sdílených alb. Pokud je tato funkce vypnuta, bude seznam uživatelů dostupný pouze uživatelům z řad správců.",
     "server_settings": "Server",
     "server_settings_description": "Správa nastavení serveru",
     "server_welcome_message": "Uvítací zpráva",
@@ -238,14 +249,24 @@
     "storage_template_settings_description": "Správa struktury složek a názvů nahraných souborů",
     "storage_template_user_label": "<code>{label}</code> je štítek úložiště uživatele",
     "system_settings": "Systémová nastavení",
+    "tag_cleanup_job": "Promazání značek",
+    "template_email_available_tags": "V šabloně můžete použít následující proměnné: {tags}",
+    "template_email_if_empty": "Pokud je šablona prázdná, použije se výchozí e-mail.",
+    "template_email_invite_album": "Šablona pozvánky do alba",
+    "template_email_preview": "Náhled",
+    "template_email_settings": "Šablony e-mailů",
+    "template_email_settings_description": "Správa vlastních šablon e-mailových oznámení",
+    "template_email_update_album": "Šablona aktualizace alba",
+    "template_email_welcome": "Šablona uvítacího e-mailu",
+    "template_settings": "Šablony oznámení",
+    "template_settings_description": "Správa vlastních šablon oznámení.",
     "theme_custom_css_settings": "Vlastní CSS",
     "theme_custom_css_settings_description": "Kaskádové styly umožňují přizpůsobit design aplikace Immich.",
     "theme_settings": "Motivy",
     "theme_settings_description": "Správa přizpůsobení webového rozhraní Immich",
     "these_files_matched_by_checksum": "Tyto soubory jsou porovnávány podle jejich kontrolních součtů",
     "thumbnail_generation_job": "Generování miniatur",
-    "thumbnail_generation_job_description": "Generování velkých, malých a rozmazaných náhledů pro každý obrázek a náhledů pro každou osobu",
-    "transcode_policy_description": "Zásady, kdy má být video překódováno. Videa HDR budou překódována vždy (kromě případů, kdy je překódování zakázáno).",
+    "thumbnail_generation_job_description": "Generování velkých, malých a rozmazaných miniatur pro každý obrázek a miniatur pro každou osobu",
     "transcoding_acceleration_api": "API pro akceleraci",
     "transcoding_acceleration_api_description": "Rozhraní, které bude komunikovat se zařízením a urychlovat překódování. Toto nastavení je 'best effort': při selhání se vrátí k softwarovému překódování. VP9 může, ale nemusí fungovat v závislosti na vašem hardwaru.",
     "transcoding_acceleration_nvenc": "NVENC (vyžaduje NVIDIA GPU)",
@@ -271,7 +292,7 @@
     "transcoding_hardware_acceleration": "Hardwarová akcelerace",
     "transcoding_hardware_acceleration_description": "Experimentální; mnohem rychlejší, ale při stejném datovém toku bude mít nižší kvalitu",
     "transcoding_hardware_decoding": "Hardwarové dekódování",
-    "transcoding_hardware_decoding_setting_description": "Platí pouze pro NVENC, QSV a RKMPP. Povoluje kompletní akceleraci namísto akcelerace pouze kódování. Nemusí fungovat u všech videí.",
+    "transcoding_hardware_decoding_setting_description": "Povoluje kompletní akceleraci namísto akcelerace pouze kódování. Nemusí fungovat u všech videí.",
     "transcoding_hevc_codec": "Kodek HEVC",
     "transcoding_max_b_frames": "Maximální počet B-snímků",
     "transcoding_max_b_frames_description": "Vyšší hodnoty zvyšují účinnost komprese, ale zpomalují kódování. Nemusí být kompatibilní s hardwarovou akcelerací na starších zařízeních. Hodnota 0 zakáže B-snímky, zatímco -1 tuto hodnotu nastaví automaticky.",
@@ -297,8 +318,6 @@
     "transcoding_threads_description": "Vyšší hodnoty vedou k rychlejšímu kódování, ale ponechávají serveru méně prostoru pro zpracování jiných úloh. Tato hodnota by neměla být vyšší než počet jader procesoru. Maximalizuje využití, pokud je nastavena na 0.",
     "transcoding_tone_mapping": "Tone-mapping",
     "transcoding_tone_mapping_description": "Snaží se zachovat vzhled videí HDR při převodu na SDR. Každý algoritmus dělá různé kompromisy v oblasti barev, detailů a jasu. Hable zachovává detaily, Mobius zachovává barvy a Reinhard zachovává jas.",
-    "transcoding_tone_mapping_npl": "Tone-mapping NPL",
-    "transcoding_tone_mapping_npl_description": "Barvy budou upraveny tak, aby vypadaly normálně pro displej s tímto jasem. Nižší hodnoty naopak zvyšují jas videa a naopak, protože kompenzují jas displeje. Hodnota 0 nastavuje tuto hodnotu automaticky.",
     "transcoding_transcode_policy": "Zásady překódování",
     "transcoding_transcode_policy_description": "Zásady, kdy má být video překódováno. Videa HDR budou překódována vždy (kromě případů, kdy je překódování zakázáno).",
     "transcoding_two_pass_encoding": "Dvouprůchodové kódování",
@@ -312,6 +331,7 @@
     "trash_settings_description": "Správa nastavení koše",
     "untracked_files": "Neznámé soubory",
     "untracked_files_description": "Tyto soubory nejsou aplikaci známy. Mohou být výsledkem neúspěšných přesunů, přerušeného nahrávání nebo mohou zůstat pozadu kvůli chybě",
+    "user_cleanup_job": "Promazání uživatelů",
     "user_delete_delay": "Účet a položky uživatele <b>{user}</b> budou trvale smazány za {delay, plural, one {# den} few {# dny} other {# dní}}.",
     "user_delete_delay_settings": "Odložení odstranění",
     "user_delete_delay_settings_description": "Počet dní po odstranění, po kterých bude odstraněn účet a položky uživatele. Úloha odstraňování uživatelů se spouští o půlnoci a kontroluje uživatele, kteří jsou připraveni k odstranění. Změny tohoto nastavení se vyhodnotí při dalším spuštění.",
@@ -365,7 +385,7 @@
   "all_videos": "Všechna videa",
   "allow_dark_mode": "Povolit tmavý režim",
   "allow_edits": "Povolit úpravy",
-  "allow_public_user_to_download": "Povolit veřejnosti stahování",
+  "allow_public_user_to_download": "Povolit veřejnosti stahovat",
   "allow_public_user_to_upload": "Povolit veřejnosti nahrávat",
   "anti_clockwise": "Proti směru hodinových ručiček",
   "api_key": "API klíč",
@@ -378,7 +398,6 @@
   "archive_or_unarchive_photo": "Archivovat nebo odarchivovat fotku",
   "archive_size": "Velikost archivu",
   "archive_size_description": "Nastavte velikost archivu pro stahování (v GiB)",
-  "archived": "Archivováno",
   "archived_count": "{count, plural, other {Archivováno #}}",
   "are_these_the_same_person": "Jedná se o stejnou osobu?",
   "are_you_sure_to_do_this": "Opravdu to chcete udělat?",
@@ -389,7 +408,7 @@
   "asset_has_unassigned_faces": "Položka má nepřiřazené obličeje",
   "asset_hashing": "Hashování...",
   "asset_offline": "Offline položka",
-  "asset_offline_description": "Tato položka je offline. Immich nemá přístup k jejímu umístění. Zkontrolujte, zda je položka dostupná, a poté knihovnu znovu prohledejte.",
+  "asset_offline_description": "Toto externí položka se již na disku nenachází. Obraťte se na Immich správce a požádejte o pomoc.",
   "asset_skipped": "Přeskočeno",
   "asset_skipped_in_trash": "V koši",
   "asset_uploaded": "Nahráno",
@@ -399,11 +418,10 @@
   "assets_added_to_album_count": "Do alba {count, plural, one {byla přidána # položka} few {byly přidány # položky} other {bylo přidáno # položek}}",
   "assets_added_to_name_count": "{count, plural, one {Přidána # položka} few {Přidány # položky} other {Přidáno # položek}} do {hasName, select, true {alba <b>{name}</b>} other {nového alba}}",
   "assets_count": "{count, plural, one {# položka} few {# položky} other {# položek}}",
-  "assets_moved_to_trash": "{count, plural, one {# položka přesunuta} few {# položky přesunuty} other {# položek přesunuto}} do koše",
   "assets_moved_to_trash_count": "Do koše {count, plural, one {přesunuta # položka} few {přesunuty # položky} other {přesunuto # položek}}",
   "assets_permanently_deleted_count": "Trvale {count, plural, one {smazána # položka} few {smazány # položky} other {smazáno # položek}}",
   "assets_removed_count": "{count, plural, one {Odstraněna # položka} few {Odstraněny # položky} other {Odstraněno # položek}}",
-  "assets_restore_confirmation": "Opravdu chcete obnovit všechny vyhozené položky? Tuto akci nelze vrátit zpět!",
+  "assets_restore_confirmation": "Opravdu chcete obnovit všechny vyhozené položky? Tuto akci nelze vrátit zpět! Upozorňujeme, že tímto způsobem nelze obnovit žádné offline položky.",
   "assets_restored_count": "{count, plural, one {Obnovena # položka} few {Obnoveny # položky} other {Obnoveno # položek}}",
   "assets_trashed_count": "{count, plural, one {Vyhozena # položka} few {Vyhozeny # položky} other {Vyhozeno # položek}}",
   "assets_were_part_of_album_count": "{count, plural, one {Položka byla} other {Položky byly}} součástí alba",
@@ -414,6 +432,7 @@
   "birthdate_saved": "Datum narození úspěšně uloženo",
   "birthdate_set_description": "Datum narození se používá k výpočtu věku osoby v době pořízení fotografie.",
   "blurred_background": "Rozmazané pozadí",
+  "bugs_and_feature_requests": "Chyby a návrhy na funkce",
   "build": "Sestavení",
   "build_image": "Sestavení obrazu",
   "bulk_delete_duplicates_confirmation": "Opravdu chcete hromadně odstranit {count, plural, one {# duplicitní položku} few {# duplicitní položky} other {# duplicitních položek}}? Tím se zachová největší položka z každé skupiny a všechny ostatní duplicity se trvale odstraní. Tuto akci nelze vrátit zpět!",
@@ -428,10 +447,6 @@
   "cannot_merge_people": "Nelze sloučit osoby",
   "cannot_undo_this_action": "Tuto akci nelze vrátit zpět!",
   "cannot_update_the_description": "Nelze aktualizovat popis",
-  "cant_apply_changes": "Nelze uplatnit změny",
-  "cant_get_faces": "Nelze získat obličeje",
-  "cant_search_people": "Nelze vyhledávat lidi",
-  "cant_search_places": "Nelze vyhledávat místa",
   "change_date": "Změnit datum",
   "change_expiration_time": "Změna konce platnosti",
   "change_location": "Změna polohy",
@@ -463,6 +478,7 @@
   "confirm": "Potvrdit",
   "confirm_admin_password": "Potvrzení hesla správce",
   "confirm_delete_shared_link": "Opravdu chcete odstranit tento sdílený odkaz?",
+  "confirm_keep_this_delete_others": "Všechny ostatní položky v tomto uskupení mimo této budou odstraněny. Opravdu chcete pokračovat?",
   "confirm_password": "Potvrzení hesla",
   "contain": "Obsah",
   "context": "Kontext",
@@ -512,16 +528,19 @@
   "delete_key": "Smazat klíč",
   "delete_library": "Smazat knihovnu",
   "delete_link": "Smazat odkaz",
+  "delete_others": "Odstranit ostatní",
   "delete_shared_link": "Smazat sdílený odkaz",
   "delete_tag": "Smazat značku",
   "delete_tag_confirmation_prompt": "Opravdu chcete odstranit značku {tagName}?",
   "delete_user": "Odstranit uživatele",
   "deleted_shared_link": "Smazat sdílený odkaz",
+  "deletes_missing_assets": "Odstraní položky chybějící na disku",
   "description": "Popis",
   "details": "Podrobnosti",
   "direction": "Směr",
   "disabled": "Zakázáno",
   "disallow_edits": "Zakázat úpravy",
+  "discord": "Discord",
   "discover": "Objevit",
   "dismiss_all_errors": "Zrušit všechny chyby",
   "dismiss_error": "Zrušit chybu",
@@ -530,6 +549,7 @@
   "display_original_photos": "Zobrazit originální fotky",
   "display_original_photos_setting_description": "Preferovat zobrazení původních fotek při prohlížení položek namísto miniatur, pokud je originální položka kompatibilní s webem. To může mít za následek nižší rychlost zobrazení fotek.",
   "do_not_show_again": "Tuto zprávu již nezobrazovat",
+  "documentation": "Dokumentace",
   "done": "Hotovo",
   "download": "Stáhnout",
   "download_include_embedded_motion_videos": "Vložená videa",
@@ -542,13 +562,6 @@
   "duplicates": "Duplicity",
   "duplicates_description": "Vyřešte každou skupinu tak, že uvedete, které skupiny jsou duplicitní",
   "duration": "Doba trvání",
-  "durations": {
-    "days": "{days, plural, one {den} few {{days, number} dny} other {{days, number} dní}}",
-    "hours": "{hours, plural, one {hodina} few {{hours, number} hodiny} other {{hours, number} hodin}}",
-    "minutes": "{minutes, plural, one {minuta} few {{minutes, number} minuty} other {{minutes, number} minut}}",
-    "months": "{months, plural, one {měsíc} few {{months, number} měsíce} other {{months, number} měsíců}}",
-    "years": "{years, plural, one {rok} few {{years, number} roky} other {{years, number} let}}"
-  },
   "edit": "Upravit",
   "edit_album": "Upravit album",
   "edit_avatar": "Upravit avatar",
@@ -573,8 +586,6 @@
   "editor_crop_tool_h2_aspect_ratios": "Poměr stran",
   "editor_crop_tool_h2_rotation": "Otočení",
   "email": "E-mail",
-  "empty": "Prázdné",
-  "empty_album": "Prázdné album",
   "empty_trash": "Vyprázdnit koš",
   "empty_trash_confirmation": "Opravdu chcete vysypat koš? Tím se z Immiche trvale odstraní všechny položky v koši.\nTuto akci nelze vrátit zpět!",
   "enable": "Povolit",
@@ -608,6 +619,7 @@
     "failed_to_create_shared_link": "Nepodařilo se vytvořit sdílený odkaz",
     "failed_to_edit_shared_link": "Nepodařilo se upravit sdílený odkaz",
     "failed_to_get_people": "Nepodařilo se načíst lidi",
+    "failed_to_keep_this_delete_others": "Nepodařilo se zachovat tuto položku a odstranit ostatní položky",
     "failed_to_load_asset": "Nepodařilo se načíst položku",
     "failed_to_load_assets": "Nepodařilo se načíst položky",
     "failed_to_load_people": "Chyba načítání osob",
@@ -635,8 +647,6 @@
     "unable_to_change_location": "Nelze změnit polohu",
     "unable_to_change_password": "Nelze změnit heslo",
     "unable_to_change_visibility": "Nelze změnit viditelnost u {count, plural, one {# osoby} few {# osob} other {# lidí}}",
-    "unable_to_check_item": "Nelze zkontrolovat položku",
-    "unable_to_check_items": "Nelze zkontrolovat položky",
     "unable_to_complete_oauth_login": "Nelze dokončit OAuth přihlášení",
     "unable_to_connect": "Nelze se připojit",
     "unable_to_connect_to_server": "Nepodařilo se připojit k serveru",
@@ -661,6 +671,7 @@
     "unable_to_get_comments_number": "Nelze načíst počet komentářů",
     "unable_to_get_shared_link": "Nepodařilo se získat sdílený odkaz",
     "unable_to_hide_person": "Nelze skrýt osobu",
+    "unable_to_link_motion_video": "Nelze připojit pohyblivé video",
     "unable_to_link_oauth_account": "Nelze propojit OAuth účet",
     "unable_to_load_album": "Nelze načíst album",
     "unable_to_load_asset_activity": "Nelze načíst aktivitu položky",
@@ -676,12 +687,10 @@
     "unable_to_remove_album_users": "Nelze odebrat uživatele z alba",
     "unable_to_remove_api_key": "Nelze odstranit API klíč",
     "unable_to_remove_assets_from_shared_link": "Nelze odstranit položky ze sdíleného odkazu",
-    "unable_to_remove_comment": "Nelze odstranit komentář",
+    "unable_to_remove_deleted_assets": "Nelze odstranit offline soubory",
     "unable_to_remove_library": "Nelze odstranit knihovnu",
-    "unable_to_remove_offline_files": "Nelze odstranit offline soubory",
     "unable_to_remove_partner": "Nelze odebrat partnera",
     "unable_to_remove_reaction": "Nelze odstranit reakci",
-    "unable_to_remove_user": "Nelze odebrat uživatele",
     "unable_to_repair_items": "Nelze opravit položky",
     "unable_to_reset_password": "Nelze obnovit heslo",
     "unable_to_resolve_duplicate": "Nelze vyřešit duplicitu",
@@ -701,6 +710,7 @@
     "unable_to_submit_job": "Nelze odeslat úlohu",
     "unable_to_trash_asset": "Nelze vyhodit položku do koše",
     "unable_to_unlink_account": "Nelze zrušit propojení účtu",
+    "unable_to_unlink_motion_video": "Nelze odpojit pohyblivé video",
     "unable_to_update_album_cover": "Nelze aktualizovat obal alba",
     "unable_to_update_album_info": "Nelze aktualizovat informace o albu",
     "unable_to_update_library": "Nelze aktualizovat knihovnu",
@@ -710,10 +720,6 @@
     "unable_to_update_user": "Nelze aktualizovat uživatele",
     "unable_to_upload_file": "Nepodařilo se nahrát soubor"
   },
-  "every_day_at_onepm": "Každý den ve 13:00",
-  "every_night_at_midnight": "Každý den o půlnoci",
-  "every_night_at_twoam": "Každou noc ve 2:00",
-  "every_six_hours": "Každých 6 hodin",
   "exif": "Exif",
   "exit_slideshow": "Ukončit prezentaci",
   "expand_all": "Rozbalit vše",
@@ -728,33 +734,28 @@
   "external": "Externí",
   "external_libraries": "Externí knihovny",
   "face_unassigned": "Nepřiřazena",
-  "failed_to_get_people": "Nepodařilo se načíst lidi",
+  "failed_to_load_assets": "Nepodařilo se načíst položky",
   "favorite": "Oblíbit",
   "favorite_or_unfavorite_photo": "Oblíbit nebo zrušit oblíbení fotky",
   "favorites": "Oblíbené",
-  "feature": "Funkce",
   "feature_photo_updated": "Hlavní fotka aktualizována",
-  "featurecollection": "Kolekce Funkcí",
   "features": "Funkce",
   "features_setting_description": "Správa funkcí aplikace",
   "file_name": "Název souboru",
   "file_name_or_extension": "Název nebo přípona souboru",
   "filename": "Filename",
-  "files": "",
   "filetype": "Filetype",
   "filter_people": "Filtrovat lidi",
   "find_them_fast": "Najděte je rychle vyhledáním jejich jména",
   "fix_incorrect_match": "Opravit nesprávnou shodu",
   "folders": "Složky",
   "folders_feature_description": "Procházení zobrazení složek s fotografiemi a videi v souborovém systému",
-  "force_re-scan_library_files": "Vynucené prohledání všech souborů knihovny",
   "forward": "Dopředu",
   "general": "Obecné",
   "get_help": "Získat pomoc",
   "getting_started": "Začínáme",
   "go_back": "Přejít zpět",
   "go_to_search": "Přejít na vyhledávání",
-  "go_to_share_page": "Přejít na stránku sdílení",
   "group_albums_by": "Seskupit alba podle...",
   "group_no": "Neseskupovat",
   "group_owner": "Seskupit podle uživatele",
@@ -780,10 +781,6 @@
   "image_alt_text_date_place_2_people": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}} {date} v místě {city}, {country} uživateli {person1} a {person2}",
   "image_alt_text_date_place_3_people": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}} {date} v místě {city}, {country} uživateli {person1}, {person2} a {person3}",
   "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}} {date} v místě {city}, {country} uživateli {person1}, {person2} a {additionalCount, plural, one {dalším # uživatelem} other {dalšími # uživateli}}",
-  "image_alt_text_people": "{count, plural, =1 {a {person1}} =2 {s {person1} a {person2}} =3 {s {person1}, {person2}, a {person3}} other {s {person1}, {person2}, a {others, number} dalšími}}",
-  "image_alt_text_place": "v {city}, {country}",
-  "image_taken": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}}",
-  "img": "Img",
   "immich_logo": "Immich Logo",
   "immich_web_interface": "Webové rozhraní Immich",
   "import_from_json": "Import z JSONu",
@@ -804,10 +801,11 @@
   "invite_people": "Pozvat lidi",
   "invite_to_album": "Pozvat do alba",
   "items_count": "{count, plural, one {# položka} few {# položky} other {# položek}}",
-  "job_settings_description": "Správa souběhu úloh",
   "jobs": "Úlohy",
   "keep": "Ponechat",
   "keep_all": "Ponechat vše",
+  "keep_this_delete_others": "Ponechat tuto, odstranit ostatní",
+  "kept_this_deleted_others": "Ponechána tato položka a {count, plural, one {odstraněna # položka} few {odstraněny # položky} other {odstraněno # položek}}",
   "keyboard_shortcuts": "Klávesové zkratky",
   "language": "Jazyk",
   "language_setting_description": "Vyberte upřednostňovaný jazyk",
@@ -819,31 +817,6 @@
   "level": "Úroveň",
   "library": "Knihovna",
   "library_options": "Možnosti knihovny",
-  "license_account_info": "Váš účet je licencován",
-  "license_activated_subtitle": "Děkujeme vám za podporu aplikace Immich a open-source softwaru",
-  "license_activated_title": "Vaše licence byla úspěšně aktivována",
-  "license_button_activate": "Aktivovat",
-  "license_button_buy": "Koupit",
-  "license_button_buy_license": "Koupit licenci",
-  "license_button_select": "Vybrat",
-  "license_failed_activation": "Nepodařilo se aktivovat licenci. Zkontrolujte prosím svůj e-mail pro správný licenční klíč!",
-  "license_individual_description_1": "1 licence za uživatele na libovolném serveru",
-  "license_individual_title": "Individuální licence",
-  "license_info_licensed": "Licencováno",
-  "license_info_unlicensed": "Nelicencováno",
-  "license_input_suggestion": "Máte licenci? Zadejte klíč níže",
-  "license_license_subtitle": "Koupí licence podpoříte Immich",
-  "license_license_title": "LICENCE",
-  "license_lifetime_description": "Doživotní licence",
-  "license_per_server": "Za server",
-  "license_per_user": "Za uživatele",
-  "license_server_description_1": "1 licence za každý server",
-  "license_server_description_2": "Licence za všechny uživatele na serveru",
-  "license_server_title": "Serverová licence",
-  "license_trial_info_1": "Používáte nelicencovanou verzi aplikace Immich",
-  "license_trial_info_2": "Immich používáte přibližně",
-  "license_trial_info_3": "{accountAge, plural, one {# den} few {# dny} other {# dní}}",
-  "license_trial_info_4": "Zvažte prosím zakoupení licence na podporu dalšího rozvoje služby",
   "light": "Světlý",
   "like_deleted": "Lajk smazán",
   "link_motion_video": "Připojit pohyblivé video",
@@ -865,6 +838,7 @@
   "look": "Zobrazení",
   "loop_videos": "Videa ve smyčce",
   "loop_videos_description": "Povolit automatickou smyčku videa v prohlížeči.",
+  "main_branch_warning": "Používáte vývojovou verzi; důrazně doporučujeme používat verzi z vydání!",
   "make": "Výrobce",
   "manage_shared_links": "Spravovat sdílené odkazy",
   "manage_sharing_with_partners": "Správa sdílení s partnery",
@@ -934,6 +908,7 @@
   "notifications": "Oznámení",
   "notifications_setting_description": "Správa oznámení",
   "oauth": "OAuth",
+  "official_immich_resources": "Oficiální zdroje Immich",
   "offline": "Offline",
   "offline_paths": "Offline cesty",
   "offline_paths_description": "Tyto výsledky mohou být způsobeny ručním odstraněním souborů, které nejsou součástí externí knihovny.",
@@ -941,13 +916,11 @@
   "oldest_first": "Nejstarší první",
   "onboarding": "Zahájení",
   "onboarding_privacy_description": "Následující (volitelné) funkce jsou závislé na externích službách a lze je kdykoli zakázat v nastavení správy.",
-  "onboarding_storage_template_description": "Pokud je tato funkce povolena, automaticky uspořádá soubory na základě uživatelem definované šablony. Vzhledem k problémům se stabilitou byla tato funkce ve výchozím nastavení vypnuta. Další informace naleznete v [dokumentaci].",
   "onboarding_theme_description": "Zvolte si barevné téma pro svou instanci. Můžete to později změnit v nastavení.",
   "onboarding_welcome_description": "Nastavíme vaši instanci pomocí několika běžných nastavení.",
   "onboarding_welcome_user": "Vítej, {user}",
   "online": "Online",
   "only_favorites": "Pouze oblíbené",
-  "only_refreshes_modified_files": "Obnovuje pouze změněné soubory",
   "open_in_map_view": "Otevřít v zobrazení mapy",
   "open_in_openstreetmap": "Otevřít v OpenStreetMap",
   "open_the_search_filters": "Otevřít vyhledávací filtry",
@@ -964,7 +937,7 @@
   "partner_can_access": "{partner} má přístup",
   "partner_can_access_assets": "Všechny vaše fotky a videa kromě těch, které jsou v sekcích Archivováno a Smazáno",
   "partner_can_access_location": "Místo, kde byly vaše fotografie pořízeny",
-  "partner_sharing": "Sdílení partnerů",
+  "partner_sharing": "Sdílení mezi partnery",
   "partners": "Partneři",
   "password": "Heslo",
   "password_does_not_match": "Heslo se neshoduje",
@@ -985,14 +958,12 @@
   "people_edits_count": "Upraveno {count, plural, one {# osoba} few {# osoby} other {# lidí}}",
   "people_feature_description": "Procházení fotografií a videí seskupených podle osob",
   "people_sidebar_description": "Zobrazit sekci Lidé v postranním panelu",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "Upozornění na trvalé smazání",
   "permanent_deletion_warning_setting_description": "Zobrazit varování při trvalém odstranění položek",
   "permanently_delete": "Trvale odstranit",
-  "permanently_delete_assets_count": "Trvale vymazat {count, plural, one {položku} other {položky}}",
+  "permanently_delete_assets_count": "Trvale smazat {count, plural, one {položku} other {položky}}",
   "permanently_delete_assets_prompt": "Opravdu chcete trvale smazat {count, plural, one {tuto položku} few {tyto <b>#</b> položky} other {těchto <b>#</b> položek}}? Tím {count, plural, one {ji také odstraníte z jejích} other {je také odstraníte z jejich}} alb.",
   "permanently_deleted_asset": "Položka trvale odstraněna",
-  "permanently_deleted_assets": "Trvale {count, plural, one {odstraněna # položka} few {odstraněny # položky} other {odstraněno # položek}}",
   "permanently_deleted_assets_count": "{count, plural, one {Položka trvale vymazána} other {Položky trvale vymazány}}",
   "person": "Osoba",
   "person_hidden": "{name}{hidden, select, true { (skryto)} other {}}",
@@ -1008,7 +979,6 @@
   "play_memories": "Přehrát vzpomníky",
   "play_motion_photo": "Přehrát pohybovou fotografii",
   "play_or_pause_video": "Přehrát nebo pozastavit video",
-  "point": "Bod",
   "port": "Port",
   "preset": "Přednastavení",
   "preview": "Náhled",
@@ -1032,19 +1002,19 @@
   "purchase_button_reminder": "Připomenout za 30 dní",
   "purchase_button_remove_key": "Odstranit klíč",
   "purchase_button_select": "Vybrat",
-  "purchase_failed_activation": "Aktivace se nezdařila! Zkontrolujte prosím svůj e-mail pro správný produktový klíč!",
+  "purchase_failed_activation": "Aktivace se nezdařila! Zkontrolujte prosím svůj e-mail zda je zadaný produktový klíč bez chyb!",
   "purchase_individual_description_1": "Pro jednotlivce",
   "purchase_individual_description_2": "Stav podporovatele",
   "purchase_individual_title": "Individuální",
-  "purchase_input_suggestion": "Máte produktový klíč? Zadejte klíč níže",
-  "purchase_license_subtitle": "Koupit Immich na podporu dalšího rozvoje služby",
+  "purchase_input_suggestion": "Máte produktový klíč? Zadejte ho níže",
+  "purchase_license_subtitle": "Koupit Immich a podpořit další rozvoj služby",
   "purchase_lifetime_description": "Doživotní platnost",
-  "purchase_option_title": "MOŽNOSTI NÁKUPU",
+  "purchase_option_title": "MOŽNOSTI ZAKOUPENÍ",
   "purchase_panel_info_1": "Tvorba aplikace Immich vyžaduje spoustu času a úsilí, a proto na ní pracují vývojáři na plný úvazek, aby byla co nejlepší. Naším cílem je, aby se software s otevřeným zdrojovým kódem a etické obchodní postupy staly udržitelným zdrojem příjmů pro vývojáře a aby vznikl ekosystém respektující soukromí se skutečnými alternativami k ziskuchtivým službám.",
   "purchase_panel_info_2": "Protože jsme se zavázali, že nebudeme zavádět paywally, nezískáte tímto nákupem žádné další funkce v aplikaci Immich. Spoléháme na uživatele, jako jste vy, že podpoří neustálý vývoj aplikace.",
-  "purchase_panel_title": "Podpora projektu",
-  "purchase_per_server": "Na server",
-  "purchase_per_user": "Na uživatele",
+  "purchase_panel_title": "Podpořit projekt",
+  "purchase_per_server": "Za server",
+  "purchase_per_user": "Za uživatele",
   "purchase_remove_product_key": "Odstranění produktového klíče",
   "purchase_remove_product_key_prompt": "Opravdu chcete odebrat produktový klíč?",
   "purchase_remove_server_product_key": "Odstranění serverového produktového klíče",
@@ -1053,12 +1023,10 @@
   "purchase_server_description_2": "Stav podporovatele",
   "purchase_server_title": "Server",
   "purchase_settings_server_activated": "Produktový klíč serveru spravuje správce",
-  "range": "Rozsah",
   "rating": "Hodnocení hvězdičkami",
   "rating_clear": "Vyčistit hodnocení",
   "rating_count": "{count, plural, one {# hvězdička} few {# hvězdičky} other {# hvězdček}}",
   "rating_description": "Zobrazit EXIF hodnocení v informačním panelu",
-  "raw": "Raw",
   "reaction_options": "Možnosti reakce",
   "read_changelog": "Přečtěte si seznam změn",
   "reassign": "Přeřadit",
@@ -1066,25 +1034,29 @@
   "reassigned_assets_to_new_person": "{count, plural, one {Přeřazena # položka} few {Přeřazeny # položky} other {Přeřazeno # položek}} na novou osobu",
   "reassing_hint": "Přiřazení vybraných položek existující osobě",
   "recent": "Nedávné",
+  "recent-albums": "Nedávná alba",
   "recent_searches": "Nedávná vyhledávání",
   "refresh": "Obnovit",
   "refresh_encoded_videos": "Obnovit kódovaná videa",
+  "refresh_faces": "Obnovit obličeje",
   "refresh_metadata": "Obnovit metadata",
-  "refresh_thumbnails": "Obnovit náhledy",
+  "refresh_thumbnails": "Obnovit miniatury",
   "refreshed": "Obnoveno",
-  "refreshes_every_file": "Obnoví každý soubor",
+  "refreshes_every_file": "Znovu načte všechny stávající a nové soubory",
   "refreshing_encoded_video": "Obnovování kódovaného videa",
+  "refreshing_faces": "Obnovování obličejů",
   "refreshing_metadata": "Obnovování metadat",
-  "regenerating_thumbnails": "Regenerace náhledů",
+  "regenerating_thumbnails": "Regenerace miniatur",
   "remove": "Odstranit",
   "remove_assets_album_confirmation": "Opravdu chcete z alba odstranit {count, plural, one {# položku} few {# položky} other {# položek}}?",
   "remove_assets_shared_link_confirmation": "Opravdu chcete ze sdíleného odkazu odstranit {count, plural, one {# položku} few {# položky} other {# položek}}?",
   "remove_assets_title": "Odstranit položky?",
   "remove_custom_date_range": "Odstranit vlastní rozsah datumů",
+  "remove_deleted_assets": "Odstranit offline soubory",
   "remove_from_album": "Odstranit z alba",
   "remove_from_favorites": "Odstranit z oblíbených",
   "remove_from_shared_link": "Odstranit ze sdíleného odkazu",
-  "remove_offline_files": "Odstranit offline soubory",
+  "remove_url": "Odstranit URL",
   "remove_user": "Odebrat uživatele",
   "removed_api_key": "Odstraněn API klíč: {name}",
   "removed_from_archive": "Odstraněno z archivu",
@@ -1101,7 +1073,6 @@
   "reset": "Výchozí",
   "reset_password": "Obnovit heslo",
   "reset_people_visibility": "Obnovit viditelnost lidí",
-  "reset_settings_to_default": "Obnovit výchozí nastavení",
   "reset_to_default": "Obnovit výchozí nastavení",
   "resolve_duplicates": "Vyřešit duplicity",
   "resolved_all_duplicates": "Vyřešeny všechny duplicity",
@@ -1121,8 +1092,7 @@
   "saved_settings": "Nastavení uloženo",
   "say_something": "Řekněte něco",
   "scan_all_libraries": "Prohledat všechny knihovny",
-  "scan_all_library_files": "Prohledání všech souborů knihovny",
-  "scan_new_library_files": "Hledat nové soubory v knihovně",
+  "scan_library": "Prohledat",
   "scan_settings": "Nastavení prohledávání",
   "scanning_for_album": "Prohledávání alba...",
   "search": "Hledat",
@@ -1140,6 +1110,7 @@
   "search_options": "Možnosti vyhledávání",
   "search_people": "Vyhledat lidi",
   "search_places": "Vyhledat místa",
+  "search_settings": "Hledat nastavení",
   "search_state": "Vyhledat stát...",
   "search_tags": "Vyhledávat značky...",
   "search_timezone": "Vyhledat časové pásmo...",
@@ -1164,9 +1135,8 @@
   "selected_count": "{count, plural, one {# vybraný} few {# vybrané} other {# vybraných}}",
   "send_message": "Odeslat zprávu",
   "send_welcome_email": "Poslat uvítací e-mail",
-  "server": "Server",
-  "server_offline": "Server Offline",
-  "server_online": "Server Online",
+  "server_offline": "Server offline",
+  "server_online": "Server online",
   "server_stats": "Statistiky serveru",
   "server_version": "Verze serveru",
   "set": "Nastavit",
@@ -1179,8 +1149,8 @@
   "settings_saved": "Nastavení uloženo",
   "share": "Sdílet",
   "shared": "Sdílené",
-  "shared_by": "Sdílel",
-  "shared_by_user": "Sdíleno uživatelem {user}",
+  "shared_by": "Sdílel(a)",
+  "shared_by_user": "Sdílel(a) {user}",
   "shared_by_you": "Sdíleli jste",
   "shared_from_partner": "Fotky od {partner}",
   "shared_link_options": "Možnosti sdíleného odkazu",
@@ -1207,6 +1177,7 @@
   "show_person_options": "Zobrazit možnosti osoby",
   "show_progress_bar": "Zobrazit ukazatel průběhu",
   "show_search_options": "Zobrazit možnosti vyhledávání",
+  "show_slideshow_transition": "Zobrazit přechod prezentace",
   "show_supporter_badge": "Odznak podporovatele",
   "show_supporter_badge_description": "Zobrazit odznak podporovatele",
   "shuffle": "Náhodný výběr",
@@ -1248,13 +1219,16 @@
   "submit": "Odeslat",
   "suggestions": "Návrhy",
   "sunrise_on_the_beach": "Východ slunce na pláži",
+  "support": "Podpora",
+  "support_and_feedback": "Podpora a zpětná vazba",
+  "support_third_party_description": "Vaše Immich instalace byla připravena třetí stranou. Problémy, které se u vás vyskytly, mohou být způsobeny tímto balíčkem, proto se na ně obraťte v první řadě pomocí níže uvedených odkazů.",
   "swap_merge_direction": "Obrátit směr sloučení",
   "sync": "Synchronizovat",
   "tag": "Značka",
   "tag_assets": "Přiřadit značku",
   "tag_created": "Vytvořena značka: {tag}",
   "tag_feature_description": "Procházení fotografií a videí seskupených podle témat logických značek",
-  "tag_not_found_question": "Nemůžete najít značku? Vytvořte ji <link>zde</link>",
+  "tag_not_found_question": "Nemůžete najít značku? <link>Vytvořte novou.</link>",
   "tag_updated": "Aktualizována značka: {tag}",
   "tagged_assets": "Přiřazena značka {count, plural, one {# položce} other {# položkám}}",
   "tags": "Značky",
@@ -1263,18 +1237,19 @@
   "theme_selection": "Výběr motivu",
   "theme_selection_description": "Automatické nastavení světlého nebo tmavého motivu podle systémových preferencí prohlížeče",
   "they_will_be_merged_together": "Budou sloučeny dohromady",
+  "third_party_resources": "Zdroje třetích stran",
   "time_based_memories": "Časové vzpomínky",
+  "timeline": "Časová osa",
   "timezone": "Časové pásmo",
   "to_archive": "Archivovat",
   "to_change_password": "Změnit heslo",
   "to_favorite": "Oblíbit",
   "to_login": "Přihlásit",
   "to_parent": "Přejít k rodiči",
-  "to_root": "Přejít ke kořenu",
   "to_trash": "Vyhodit",
   "toggle_settings": "Přepnout nastavení",
   "toggle_theme": "Přepnout tmavý motiv",
-  "toggle_visibility": "Přepnout viditelnost",
+  "total": "Celkem",
   "total_usage": "Celkové využití",
   "trash": "Koš",
   "trash_all": "Vyhodit vše",
@@ -1284,19 +1259,18 @@
   "trashed_items_will_be_permanently_deleted_after": "Smazané položky budou trvale odstraněny po {days, plural, one {# dni} other {# dnech}}.",
   "type": "Typ",
   "unarchive": "Odarchivovat",
-  "unarchived": "Odarchivováno",
   "unarchived_count": "{count, plural, one {Odarchivována #} few {Odarchivovány #} other {Odarchivováno #}}",
   "unfavorite": "Zrušit oblíbení",
   "unhide_person": "Zrušit skrytí osoby",
   "unknown": "Neznámý",
-  "unknown_album": "Neznámé album",
   "unknown_year": "Neznámý rok",
   "unlimited": "Neomezeně",
+  "unlink_motion_video": "Odpojit pohyblivé video",
   "unlink_oauth": "Zrušit OAuth propojení",
   "unlinked_oauth_account": "OAuth účet odpojen",
   "unnamed_album": "Nepojmenované album",
   "unnamed_album_delete_confirmation": "Opravdu chcete toto album smazat?",
-  "unnamed_share": "Nejmenované sdílení",
+  "unnamed_share": "Nepojmenované sdílení",
   "unsaved_change": "Neuložená změna",
   "unselect_all": "Zrušit výběr všech",
   "unselect_all_duplicates": "Zrušit výběr všech duplicit",
@@ -1320,13 +1294,13 @@
   "use_custom_date_range": "Použít vlastní rozsah dat",
   "user": "Uživatel",
   "user_id": "ID uživatele",
-  "user_license_settings": "Licence",
-  "user_license_settings_description": "Správa licence",
   "user_liked": "Uživateli {user} se {type, select, photo {líbila tato fotka} video {líbilo toto video} asset {líbila tato položka} other {to líbilo}}",
   "user_purchase_settings": "Nákup",
   "user_purchase_settings_description": "Správa vašeho nákupu",
   "user_role_set": "Uživatel {user} nastaven jako {role}",
   "user_usage_detail": "Podrobnosti využití uživatelů",
+  "user_usage_stats": "Statistiky používání účtu",
+  "user_usage_stats_description": "Zobrazit statistiky používání účtu",
   "username": "Uživateleské jméno",
   "users": "Uživatelé",
   "utilities": "Nástroje",
@@ -1334,7 +1308,9 @@
   "variables": "Proměnné",
   "version": "Verze",
   "version_announcement_closing": "Váš přítel Alex",
-  "version_announcement_message": "Ahoj příteli, je tu nová verze aplikace, věnuj prosím čas přečtení <link>poznámek k vydání</link> a zajisti si, aby <code>docker-compose.yml</code> a nastavení <code>.env</code> bylo aktuální, a aby nedošlo k chybné konfiguraci, zejména pokud používáš WatchTower nebo jiný mechanismus, který se stará o automatickou aktualizaci aplikace.",
+  "version_announcement_message": "Ahoj! K dispozici je nová verze aplikace Immich. Věnujte prosím chvíli přečtení <link>poznámek k vydání</link> a ujistěte se, že je vaše nastavení aktuální, abyste předešli případným chybným konfiguracím, zejména pokud používáte WatchTower nebo jiný mechanismus, který se stará o automatickou aktualizaci instance aplikace Immich.",
+  "version_history": "Historie verzí",
+  "version_history_item": "Nainstalováno {version} dne {date}",
   "video": "Video",
   "video_hover_setting": "Přehrávat miniaturu videa po najetí myší",
   "video_hover_setting_description": "Přehrát miniaturu videa při najetí myší na položku. I když je přehrávání vypnuto, lze jej spustit najetím na ikonu přehrávání.",
@@ -1346,10 +1322,10 @@
   "view_all_users": "Zobrazit všechny uživatele",
   "view_in_timeline": "Zobrazit na časové ose",
   "view_links": "Zobrazit odkazy",
+  "view_name": "Zobrazit",
   "view_next_asset": "Zobrazit další položku",
   "view_previous_asset": "Zobrazit předchozí položku",
   "view_stack": "Zobrazit seskupení",
-  "viewer": "Prohlížeč",
   "visibility_changed": "Viditelnost změněna u {count, plural, one {# osoby} few {# osob} other {# lidí}}",
   "waiting": "Čekající",
   "warning": "Upozornění",
diff --git a/i18n/cv.json b/i18n/cv.json
new file mode 100644
index 0000000000..61dcb12b8d
--- /dev/null
+++ b/i18n/cv.json
@@ -0,0 +1,52 @@
+{
+  "about": "Ҫинчен",
+  "account": "Шута ҫырни",
+  "account_settings": "Шута ҫырни ӗнерленӳ",
+  "acknowledge": "Çирӗплет",
+  "action": "Ӗçлени",
+  "actions": "Ӗҫсем",
+  "active": "Хастар",
+  "activity": "Хастарлӑх",
+  "activity_changed": "Хастарлӑха {enabled, select, true {кӗртнӗ} other {сӳнтернӗ}}",
+  "add": "Хуш",
+  "add_a_description": "Ҫырса кӑтартни хуш",
+  "add_a_location": "Вырӑн хуш",
+  "add_a_name": "Ятне хуш",
+  "add_a_title": "Ят хуш",
+  "add_exclusion_pattern": "Кӑларса пӑрахмалли йӗрке хуш",
+  "add_import_path": "Импорт ҫулне хуш",
+  "add_location": "Вырӑн хуш",
+  "add_more_users": "Усӑҫсем ытларах хуш",
+  "add_partner": "Мӑшӑр хуш",
+  "add_path": "Ҫулне хуш",
+  "add_photos": "Сӑнӳкерчӗксем хуш",
+  "add_to": "Мӗн те пулин хуш...",
+  "add_to_album": "Альбома хуш",
+  "add_to_shared_album": "Пӗрлехи альбома хуш",
+  "add_url": "URL хушӑр",
+  "added_to_archive": "Архива хушнӑ",
+  "added_to_favorites": "Суйласа илнине хушнӑ",
+  "added_to_favorites_count": "Суйласа илнине {count, number} хушнӑ",
+  "admin": {
+    "asset_offline_description": "Библиотекӑн ҫак тулаш файлне дискра урӑх тупайман, карҫинккана куҫарнӑ. Енчен те файла вулавӑш ӑшне куҫарнӑ пулсан, тивӗҫлӗ ҫӗнӗ ресурс тупас тесен хӑвӑрӑн вӑхӑтлӑх шкалӑна тӗрӗслӗр. Ҫак файла ҫӗнӗрен чӗртес тесен файл патне каймалли ҫула Immich валли аяларах ҫитернине курса ӗненӗр, библиотекӑна сканерланине пурнӑҫлӑр.",
+    "authentication_settings_disable_all": "Эсир кӗмелли пур меслетсене те чарса лартасшӑн тесе шутлатӑр-и? Кӗмелли шӑтӑка пӗтӗмпех уҫаҫҫӗ.",
+    "background_task_job": "Курăнман ӗҫсем",
+    "check_all": "Пурне те тӗрӗслӗр",
+    "cleared_jobs": "Ӗҫсене тасатнӑ:{job}",
+    "confirm_email_below": "Ҫирӗплетес тесен, аяларах «{email}» кӗртӗр",
+    "confirm_reprocess_all_faces": "Пӗтӗм сӑнӗсене тепӗр хут палӑртас килет тесе шанатӑр-и? Ҫавӑн пекех ятсене пур ҫынран та хуратӗҫ.",
+    "create_job": "Ӗҫе ту",
+    "disable_login": "Кӗме чарӑр",
+    "duplicate_detection_job_description": "Пӗр пек ӳкерчӗксене тупма машинӑллӑ вӗренӗве ӗҫлеттерӗр. Ӑслӑ шыравпа усӑ кураҫҫӗ",
+    "face_detection": "Пит-куҫа тупасси",
+    "force_delete_user_warning": "ПУЛТАРУЛӐХ: Ку усӑ куракана тата мӗнпур ресурса ҫийӗнчех кӑларса пӑрахасси патне илсе ҫитерӗ. Кӑна пӑрахӑҫлама май ҫук, файлсене те юсаса пӗтереймеҫҫӗ.",
+    "image_format": "Тулашлăх",
+    "image_preview_description": "Вӑтам пысӑкӑш ӳкерчӗк, уйрӑм метаданнӑйсем, пӗр объекта пӑхнӑ чухне тата машинӑллӑ вӗренӳре усӑ кураҫҫӗ",
+    "image_preview_quality_description": "1-100 таран малтанхи пахалӑх. Ҫӳллӗреххи лайӑхрах, анчах та пысӑкрах файлсем туса кӑларать тата приложенисен хуравлӑхне чакарма пултарать. Пӗчӗк хак лартни машинӑллӑ вӗренӳ пахалӑхне витӗм кӳме пултарать.",
+    "image_preview_title": "Малтанлӑха пӑхмалли ӗнерлевсем",
+    "image_quality": "Пахалӑх",
+    "image_resolution": "Виҫе"
+  },
+  "user_usage_stats": "Шута ҫырни усӑ курмалли статистика",
+  "user_usage_stats_description": "Шута ҫырни усӑ курмалли статистикӑна пӑхасси"
+}
diff --git a/web/src/lib/i18n/da.json b/i18n/da.json
similarity index 87%
rename from web/src/lib/i18n/da.json
rename to i18n/da.json
index eb9d99d074..e455c4d567 100644
--- a/web/src/lib/i18n/da.json
+++ b/i18n/da.json
@@ -2,12 +2,12 @@
   "about": "Om",
   "account": "Konto",
   "account_settings": "Kontoindstillinger",
-  "acknowledge": "Anerkend",
+  "acknowledge": "Godkend",
   "action": "Handling",
   "actions": "Handlinger",
   "active": "Aktive",
   "activity": "Aktivitet",
-  "activity_changed": "Aktivitet er {enabled, select, true {aktiveret} other {deaktiveret}}",
+  "activity_changed": "Aktivitet er {aktiveret, valg, sand {aktiveret} andet {deaktiveret}}",
   "add": "Tilføj",
   "add_a_description": "Tilføj en beskrivelse",
   "add_a_location": "Tilføj en placering",
@@ -23,16 +23,23 @@
   "add_to": "Tilføj til...",
   "add_to_album": "Tilføj til album",
   "add_to_shared_album": "Tilføj til delt album",
+  "add_url": "Tilføj URL",
   "added_to_archive": "Tilføjet til arkiv",
   "added_to_favorites": "Tilføjet til favoritter",
   "added_to_favorites_count": "Tilføjet {count, number} til favoritter",
   "admin": {
     "add_exclusion_pattern_description": "Tilføj udelukkelsesmønstre. Globbing ved hjælp af *, ** og ? understøttes. For at ignorere alle filer i enhver mappe med navnet \"Raw\", brug \"**/Raw/**\". For at ignorere alle filer, der slutter på \".tif\", brug \"**/*.tif\". For at ignorere en absolut sti, brug \"/sti/til/ignoreret/**\".",
+    "asset_offline_description": "Denne eksterne biblioteksressource findes ikke længere på disken og er blevet flyttet til papirkurven. Hvis filen blev flyttet inde i biblioteket, skal du tjekke din tidslinje for den nye tilsvarende ressource. For at gendanne denne ressource skal du sikre, at filstien nedenfor kan tilgås af Immich og scanne biblioteket.",
     "authentication_settings": "Godkendelsesindstillinger",
     "authentication_settings_description": "Administrer adgangskode, OAuth og andre godkendelsesindstillinger",
     "authentication_settings_disable_all": "Er du sikker på at du vil deaktivere alle loginmuligheder? Login vil blive helt deaktiveret.",
     "authentication_settings_reenable": "Brug en <link>server-kommando</link> for at genaktivere.",
     "background_task_job": "Baggrundsopgaver",
+    "backup_database": "Backup Database",
+    "backup_database_enable_description": "Slå database-backup til",
+    "backup_keep_last_amount": "Mængde af tidligere backups, der skal gemmes",
+    "backup_settings": "Backup-indstillinger",
+    "backup_settings_description": "Administrer backupindstillinger for database",
     "check_all": "Tjek Alle",
     "cleared_jobs": "Ryddet jobs til: {job}",
     "config_set_by_file": "konfigurationen er i øjeblikket indstillet af en konfigurations fil",
@@ -41,9 +48,10 @@
     "confirm_email_below": "For at bekræfte, skriv \"{email}\" herunder",
     "confirm_reprocess_all_faces": "Er du sikker på, at du vil genbehandle alle ansigter? Dette vil også rydde navngivne personer.",
     "confirm_user_password_reset": "Er du sikker på, at du vil nulstille {user}s adgangskode?",
-    "crontab_guru": "Crontab Guru",
+    "create_job": "Opret job",
+    "cron_expression": "Cron formel",
+    "cron_expression_description": "Indstil skannings intervallet i cron format. For mere information se: <link>Crontab Guru</link>",
     "disable_login": "Deaktiver login",
-    "disabled": "",
     "duplicate_detection_job_description": "Kør maskinlæring på mediefiler for at opdage lignende billeder. Er afhængig af Smart Søgning",
     "exclusion_pattern_description": "Ekskluderingsmønstre lader dig ignorere filer og mapper, når du scanner dit bibliotek. Dette er nyttigt, hvis du har mapper, der indeholder filer, du ikke vil importere, såsom RAW-filer.",
     "external_library_created_at": "Eksternt bibliotek (oprettet {date})",
@@ -54,21 +62,20 @@
     "failed_job_command": "Kommando {command} mislykkedes for job: {job}",
     "force_delete_user_warning": "ADVARSEL: Dette vil øjeblikkeligt fjerne brugeren og alle Billeder/Videoer. Dette kan ikke fortrydes, og filerne kan ikke gendannes.",
     "forcing_refresh_library_files": "Tvinger genopfriskning af alle biblioteksfiler",
+    "image_format": "Format",
     "image_format_description": "WebP producerer mindre filer end JPEG, men er langsommere at komprimere.",
     "image_prefer_embedded_preview": "Foretræk indlejret forhåndsvisning",
     "image_prefer_embedded_preview_setting_description": "Brug indlejrede forhåndsvisninger i RAW fotos som input til billedbehandling, når det er tilgængeligt. Dette kan give mere nøjagtige farver for nogle billeder, men kvaliteten af forhåndsvisningen er kameraafhængig, og billedet kan have flere komprimeringsartefakter.",
     "image_prefer_wide_gamut": "Foretrækker bred farveskala",
     "image_prefer_wide_gamut_setting_description": "Brug Display P3 til miniaturebilleder. Dette bevarer billeder med brede farveskalaers dynamik bedre, men billeder kan komme til at se anderledes ud på gamle enheder med en gammel browserversion. sRGB-billeder bliver beholdt som sRGB for at undgå farveskift.",
-    "image_preview_format": "Forhåndsvisningsformat",
-    "image_preview_resolution": "Forhåndsvisnings opløsning",
-    "image_preview_resolution_description": "Bliver brugt når et enkelt billede betragtes og ved maskinlæring. Højere opløsninger kan bevare flere detaljer, men tager længere tid at indkode, har større filstørrelser, og kan gøre appoplevelsen sløvere.",
+    "image_preview_description": "Mellemstørrelse billede med fjernet metadata, der bruges, når du ser en enkelt mediefil og til machine learning",
+    "image_preview_quality_description": "Kvalitet af forhåndsvisning fra 1-100. Højere er bedre, men producerer større filer og kan reducere apprespons. Valg af en lav værdi kan påvirke kvaliteten af machine learning.",
+    "image_preview_title": "Indstillinger for forhåndsvisning",
     "image_quality": "Kvalitet",
-    "image_quality_description": "Billedkvalitet fra 1-100. Højere er bedre for kvaliteten, men producerer større filer. Denne indstilling påvirker forhåndsvisningen og miniaturebillederne.",
+    "image_resolution": "Opløsning",
     "image_settings": "Billedindstillinger",
     "image_settings_description": "Administrer kvaliteten og opløsningen af genererede billeder",
-    "image_thumbnail_format": "Miniatureformat",
-    "image_thumbnail_resolution": "Miniature opløsning",
-    "image_thumbnail_resolution_description": "Bruges ved visning af grupper af billeder (hovedtidslinje, albumvisning osv.). Højere opløsninger kan bevare flere detaljer, men det tager længere tid at kode, har større filstørrelser og kan reducere appens reaktionsevne.",
+    "image_thumbnail_title": "Thumbnail-indstillinger",
     "job_concurrency": "{job} samtidighed",
     "job_not_concurrency_safe": "Denne opgave er ikke sikker at køre samtidigt med andre.",
     "job_settings": "Jobindstillinger",
@@ -77,9 +84,6 @@
     "jobs_delayed": "{jobCount, plural, one {# forsinket} other {# forsinkede}}",
     "jobs_failed": "{jobCount, plural, one {# fejlet} other {# fejlede}}",
     "library_created": "Skabte bibliotek: {library}",
-    "library_cron_expression": "Cron-udtryk",
-    "library_cron_expression_description": "Sæt skannings interval ved at bruge cron formatet. For mere information se dokumentation her <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Cron-udtryksforudindstillinger",
     "library_deleted": "Bibliotek slettet",
     "library_import_path_description": "Angiv en mappe, der skal importeres. Denne mappe, inklusive undermapper, vil blive scannet for billeder og videoer.",
     "library_scanning": "Periodisk scanning",
@@ -140,6 +144,10 @@
     "map_style_description": "URL til en style.json for et korttema",
     "metadata_extraction_job": "Udtræk metadata",
     "metadata_extraction_job_description": "Udtræk metadataoplysninger fra hvert Billede/Video, såsom GPS og opløsning",
+    "metadata_faces_import_setting": "Aktivér for at importere ansigter",
+    "metadata_faces_import_setting_description": "Importerer ansigter fra billed EXIF-data og forbandt filer",
+    "metadata_settings": "Metadatainstillinger",
+    "metadata_settings_description": "Håndtér metadataindstillinger",
     "migration_job": "Migrering",
     "migration_job_description": "Migrér miniaturebilleder for aktiver og ansigter til den seneste mappestruktur",
     "no_paths_added": "Ingen stier tilføjet",
@@ -148,7 +156,7 @@
     "note_cannot_be_changed_later": "BEMÆRK: Dette kan ikke ændres senere!",
     "note_unlimited_quota": "Bemærk: Indsæt 0 for uendelig kvote",
     "notification_email_from_address": "Fra adressse",
-    "notification_email_from_address_description": "Afsenderemailadresse, for eksempel: \"Immich Billedserver <noreply@immich.app>\"",
+    "notification_email_from_address_description": "Afsenderemailadresse, for eksempel: \"Immich Billedserver <noreply@example.com>\"",
     "notification_email_host_description": "Host af emailserver (fx smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Ignorér certifikatfejl",
     "notification_email_ignore_certificate_errors_description": "Ignorér TLS-certifikatgodkendelsesfejl (ikke anbefalet)",
@@ -194,19 +202,18 @@
     "password_settings": "Adgangskodelogin",
     "password_settings_description": "Administrer indstillinger for adgangskodelogin",
     "paths_validated_successfully": "Alle stier valideret med succes",
+    "person_cleanup_job": "Person-oprydning",
     "quota_size_gib": "Kvotestørrelse (GiB)",
     "refreshing_all_libraries": "Opdaterer alle biblioteker",
     "registration": "Administratorregistrering",
     "registration_description": "Da du er den første bruger i systemet, får du tildelt rollen som administrator og ansvar for administration og oprettelsen af nye brugere.",
-    "removing_offline_files": "Fjerner offline-filer",
     "repair_all": "Reparér alle",
     "repair_matched_items": "Har parret {count, plural, one {# element} other {# elementer}}",
     "repaired_items": "Reparerede {count, plural, one {# element} other {# elementer}}",
     "require_password_change_on_login": "Kræv at brugeren skifter adgangskode ved første login",
     "reset_settings_to_default": "Nulstil indstillingerne til standard",
     "reset_settings_to_recent_saved": "Nulstil indstillinger til de senest gemte indstillinger",
-    "scanning_library_for_changed_files": "Skanner bibliotek efter ændrede filer",
-    "scanning_library_for_new_files": "Skanner bibliotek efter nye filer",
+    "scanning_library": "Scanner bibliotek",
     "send_welcome_email": "Send velkomst-email",
     "server_external_domain_settings": "Eksternt domæne",
     "server_external_domain_settings_description": "Domæne til offentligt delte links, inklusiv http(s)://",
@@ -241,7 +248,6 @@
     "these_files_matched_by_checksum": "Disse filer er blevet matchet med deres checksummer",
     "thumbnail_generation_job": "Generér miniaturebilleder",
     "thumbnail_generation_job_description": "Generér store, små og slørede miniaturebilleder for hver mediefil, såvel som miniaturebilleder for hver person",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "Accelerations-API",
     "transcoding_acceleration_api_description": "API'en som interagerer med din enhed for at accelerere transkodning. Denne er indstilling er \"i bedste fald\": Den vil falde tilbage til software-transkodning ved svigt. VP9 virker måske, måske ikke, afhængigt af dit hardware.",
     "transcoding_acceleration_nvenc": "NVENC (kræver NVIDIA GPU)",
@@ -293,8 +299,6 @@
     "transcoding_threads_description": "Højere værdier medfører hurtigere indkodning, men efterlader mindre plads til at serveren kan foretage andre opgaver når aktiv. Denne værdi bør ikke være større end antallet af CPU-kerner. Maksimerer udnyttelse hvis sat til 0.",
     "transcoding_tone_mapping": "Tone-kortlægning",
     "transcoding_tone_mapping_description": "Forsøger at bevare HDR-videoers udseende når konverteret til SDR. Hver algoritme har forskellige afvejninger af farve, detalje og lysstyrke. Hable bevarer farve og Reinhard bevarer lysstyrke.",
-    "transcoding_tone_mapping_npl": "Tone-kortlægning NPL",
-    "transcoding_tone_mapping_npl_description": "Farver vil blive justeret til at se normale ud for en skærm med denne lysstyrke. Ulogisk nok øger lavere værdier videoens lysstyrke og omvendt, siden det kompenserer for skærmens lysstyrke. 0 sætter debbe værdi automatisk.",
     "transcoding_transcode_policy": "Transkodningspolitik",
     "transcoding_transcode_policy_description": "Politik for hvornår en video skal transkodes. HDR videoer vil altid blive transkodet (bortset fra, hvis transkodning er slået fra).",
     "transcoding_two_pass_encoding": "To-omgangsindkodning",
@@ -308,6 +312,7 @@
     "trash_settings_description": "Administrér skraldeindstillinger",
     "untracked_files": "Utrackede filer",
     "untracked_files_description": "Applikationen holder ikke styr på disse filer. De kan være resultatet af mislykkede flytninger, afbrudte uploads eller være efterladt på grund af en fejl",
+    "user_cleanup_job": "Bruger-oprydning",
     "user_delete_delay": "<b>{user}</b>'s konto og mediefiler vil blive planlagt til permanent sletning om {delay, plural, one {# dag} other {# dage}}.",
     "user_delete_delay_settings": "Slet forsinkelse",
     "user_delete_delay_settings_description": "Antal dage efter fjernelse for permanent at slette en brugers konto og mediefiler. Opgaven for sletning af brugere kører ved midnat for at tjekke efter brugere, der er klar til sletning. Ændringer i denne indstilling vil blive evalueret ved næste udførelse.",
@@ -347,15 +352,26 @@
   "album_options": "Albumindstillinger",
   "album_remove_user": "Fjern bruger?",
   "album_remove_user_confirmation": "Er du sikker på at du vil fjerne {user}?",
+  "album_share_no_users": "Det ser ud til at du har delt denne album med alle brugere, eller du har ikke nogen brugere til at dele med.",
   "album_updated": "Album opdateret",
   "album_updated_setting_description": "Modtag en emailnotifikation når et delt album får nye mediefiler",
+  "album_user_left": "Forlod {album}",
+  "album_user_removed": "Fjernede {user}",
+  "album_with_link_access": "Lad alle med linket se billeder og personer i dette album.",
   "albums": "Albummer",
   "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albummer}}",
   "all": "Alt",
+  "all_albums": "Alle albummer",
   "all_people": "Alle personer",
+  "all_videos": "Alle videoer",
   "allow_dark_mode": "Tillad mørk tilstand",
   "allow_edits": "Tillad redigeringer",
+  "allow_public_user_to_download": "Tillad offentlige brugere til at hente",
+  "allow_public_user_to_upload": "Tillad offentlige brugere til at uploade",
+  "anti_clockwise": "Mod uret",
   "api_key": "API-nøgle",
+  "api_key_description": "Denne værdi vises kun én gang. Venligst kopiér den før du lukker vinduet.",
+  "api_key_empty": "Din API-nøgle-navn burde ikke være tom",
   "api_keys": "API-nøgler",
   "app_settings": "Appindstillinger",
   "appears_in": "Optræder i",
@@ -363,44 +379,63 @@
   "archive_or_unarchive_photo": "Arkivér eller dearkivér billede",
   "archive_size": "Arkiv størelse",
   "archive_size_description": "Konfigurer arkivstørrelsen for downloads (i GiB)",
-  "archived": "Arkiveret",
+  "are_these_the_same_person": "Er disse den samme person?",
+  "are_you_sure_to_do_this": "Er du sikker på, at du vil gøre det her?",
+  "asset_added_to_album": "Tilføjet til album",
+  "asset_adding_to_album": "Tilføjer til album...",
+  "asset_description_updated": "Mediefilsbeskrivelse er blevet opdateret",
+  "asset_filename_is_offline": "Mediefil {filename} er offline",
   "asset_offline": "Mediefil offline",
+  "asset_offline_description": "Denne eksterne mediefil kan ikke længere findes på drevet. Kontakt venligst din Immich-administrator for hjælp.",
+  "asset_skipped": "Sprunget over",
+  "asset_uploaded": "Uploaded",
+  "asset_uploading": "Uploader...",
   "assets": "elementer",
   "authorized_devices": "Tilladte enheder",
   "back": "Tilbage",
   "backward": "Baglæns",
   "blurred_background": "Sløret baggrund",
+  "bugs_and_feature_requests": "Fejl & forbedringsønsker",
+  "build": "Byg",
+  "build_image": "Byggefil",
+  "bulk_delete_duplicates_confirmation": "Er du sikker på, at du vil slette alle {count, plural, one {# duplicate asset} other {# duplicate assets}}? Dette vil beholde den største fil i hver gruppe og slette alle dubletter. Denne handling kan ikke fortrydes!",
+  "bulk_keep_duplicates_confirmation": "Er du sikker på, at du vil beholde {count, plural, one {# duplicate asset} other {# duplicate assets}}? Dette vil løse alle dubletgrupper uden at slette noget.",
+  "buy": "Køb Immich",
   "camera": "Kamera",
   "camera_brand": "Kameramærke",
   "camera_model": "Kameramodel",
-  "cancel": "Annuller",
+  "cancel": "Annullér",
   "cancel_search": "Annullér søgning",
   "cannot_merge_people": "Kan ikke sammenflette personer",
+  "cannot_undo_this_action": "Du kan ikke fortryde denne handling!",
   "cannot_update_the_description": "Kan ikke opdatere beskrivelsen",
-  "cant_apply_changes": "Kan ikke anvende ændringer",
-  "cant_get_faces": "Kan ikke hente ansigter",
-  "cant_search_people": "Kan ikke søge i personer",
-  "cant_search_places": "Kan ikke søge i steder",
   "change_date": "Ændr dato",
-  "change_expiration_time": "Ændrer udløbstidspunkt",
+  "change_expiration_time": "Ændr udløbstidspunkt",
   "change_location": "Ændr sted",
   "change_name": "Ændr navn",
-  "change_name_successfully": "Navn ændret med succes",
-  "change_password": "Skift Kodeord",
-  "change_your_password": "Skift din adgangskode",
-  "changed_visibility_successfully": "Ændrede synlighed med succes",
-  "check_all": "Tjek alle",
-  "check_logs": "Tjek logs",
-  "choose_matching_people_to_merge": "Vælg personer der matcher til sammenfletning",
+  "change_name_successfully": "Navn er ændret",
+  "change_password": "Skift kodeord",
+  "change_password_description": "Dette er enten første gang du tilmelder dig, eller en ændring af kodeordet blev bestilt. Indtast dit nye kodeord herunder.",
+  "change_your_password": "Skift dit kodeord",
+  "changed_visibility_successfully": "Synlighed blev ændret",
+  "check_all": "Markér alle",
+  "check_logs": "Tjek logfiler",
+  "choose_matching_people_to_merge": "Vælg matchende personer til sammenfletning",
   "city": "By",
   "clear": "Ryd",
   "clear_all": "Ryd alle",
+  "clear_all_recent_searches": "Ryd alle seneste søgninger",
   "clear_message": "Ryd bedsked",
   "clear_value": "Ryd værdi",
+  "clockwise": "Med uret",
   "close": "Luk",
-  "collapse_all": "Kollaps alle",
+  "collapse": "Klap sammen",
+  "collapse_all": "Klap alle sammen",
+  "color": "Farve",
   "color_theme": "Farvetema",
+  "comment_deleted": "Kommentar slettet",
   "comment_options": "Kommentarindstillinger",
+  "comments_and_likes": "Kommentarer og likes",
   "comments_are_disabled": "Kommentarer er slået fra",
   "confirm": "Bekræft",
   "confirm_admin_password": "Bekræft administratoradgangskode",
@@ -456,6 +491,7 @@
   "direction": "Retning",
   "disabled": "Deaktiveret",
   "disallow_edits": "Deaktivér redigeringer",
+  "discord": "Discord",
   "discover": "Opdag",
   "dismiss_all_errors": "Afvis alle fejl",
   "dismiss_error": "Afvis fejl",
@@ -463,6 +499,7 @@
   "display_order": "Display-rækkefølge",
   "display_original_photos": "Vis originale billeder",
   "display_original_photos_setting_description": "Foretræk at vise det originale billede frem for miniaturebilleder når den originale fil er web-kompatibelt. Dette kan gøre billedvisning langsommere.",
+  "do_not_show_again": "Vis ikke denne besked igen",
   "done": "Færdig",
   "download": "Hent",
   "download_settings": "Download",
@@ -470,13 +507,7 @@
   "downloading": "Downloader",
   "duplicates": "Duplikater",
   "duration": "Varighed",
-  "durations": {
-    "days": "{days, plural, one {dag} other {{days, number} dage}}",
-    "hours": "{hours, plural, one {time} other {{hours, number} timer}}",
-    "minutes": "{minutes, plural, one {minut} other {{minutes, number} minutter}}",
-    "months": "{months, plural, one {måned} other {{months, number} måneder}}",
-    "years": "{years, plural, one {år} other {{years, number} år}}"
-  },
+  "edit": "Rediger",
   "edit_album": "Redigér album",
   "edit_avatar": "Redigér avatar",
   "edit_date": "Redigér dato",
@@ -494,21 +525,40 @@
   "edit_user": "Redigér bruger",
   "edited": "Redigeret",
   "editor": "Redaktør",
+  "editor_close_without_save_prompt": "Ændringerne vil ikke blive gemt",
+  "editor_close_without_save_title": "Luk editor?",
+  "editor_crop_tool_h2_rotation": "Rotation",
   "email": "E-mail",
-  "empty": "",
-  "empty_album": "Tomt album",
   "empty_trash": "Tøm papirkurv",
   "enable": "Aktivér",
   "enabled": "Aktiveret",
   "end_date": "Slutdato",
   "error": "Fejl",
   "error_loading_image": "Fejl ved indlæsning af billede",
+  "error_title": "Fejl - Noget gik galt",
   "errors": {
+    "cannot_navigate_next_asset": "Kan ikke navigere til næste mediefil",
+    "cannot_navigate_previous_asset": "Kan ikke navigere til forrige mediefil",
     "cleared_jobs": "Ryddede opgaver for: {job}",
+    "error_adding_assets_to_album": "Fejl i tilføjelse af mediefiler til album",
+    "error_adding_users_to_album": "Fejl i tilføjelse af brugere til album",
+    "error_deleting_shared_user": "Fejl i sletning af delt bruger",
+    "error_downloading": "Fejl i download af {filename}",
+    "error_hiding_buy_button": "Fejl i skjulning af køb-knap",
+    "error_removing_assets_from_album": "Fejl i fjernelse af mediefiler fra album. Tjek konsol for flere detaljer",
     "exclusion_pattern_already_exists": "Denne udelukkelsesmønster findes allerede.",
     "failed_job_command": "Kommando {command} slog fejl for opgave: {job}",
+    "failed_to_create_album": "Oprettelse af album mislykkedes",
+    "failed_to_create_shared_link": "Oprettelse af delt link mislykkedes",
+    "failed_to_edit_shared_link": "Redigering af delt link mislykkedes",
+    "failed_to_load_asset": "Indlæsning af mediefil mislykkedes",
+    "failed_to_load_assets": "Indlæsning af mediefiler mislykkedes",
+    "failed_to_load_people": "Indlæsning af personer mislykkedes",
+    "failed_to_remove_product_key": "Fjernelse af produktnøgle mislykkedes",
     "import_path_already_exists": "Denne importsti findes allerede.",
+    "incorrect_email_or_password": "Forkert email eller kodeord",
     "paths_validation_failed": "{paths, plural, one {# sti} other {# stier}} slog fejl ved validering",
+    "profile_picture_transparent_pixels": "Profilbilleder kan ikke have gennemsigtige pixels. Zoom venligst ind og/eller flyt billedet.",
     "quota_higher_than_disk_size": "Du har sat en kvote der er større end disken",
     "repair_unable_to_check_items": "Kunne ikke tjekke {count, select, one {element} other {elementer}}",
     "unable_to_add_album_users": "Ikke i stand til at tilføje brugere til album",
@@ -520,8 +570,6 @@
     "unable_to_change_date": "Ikke i stand til at ændre dato",
     "unable_to_change_location": "Ikke i stand til at ændre sted",
     "unable_to_change_password": "Kunne ikke ændre adgangskode",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_copy_to_clipboard": "Kan ikke kopiere til udklipsholder, sørg for at du tilgår siden gennem https",
     "unable_to_create_admin_account": "",
     "unable_to_create_api_key": "Kunne ikke oprette ny API-nøgle",
@@ -529,6 +577,7 @@
     "unable_to_create_user": "Ikke i stand til at oprette bruger",
     "unable_to_delete_album": "Ikke i stand til at slette album",
     "unable_to_delete_asset": "Kan ikke slette mediefil",
+    "unable_to_delete_assets": "Fejl i sletning af mediefiler",
     "unable_to_delete_exclusion_pattern": "Kunne ikke slette udelukkelsesmønster",
     "unable_to_delete_import_path": "Kunne ikke slette importsti",
     "unable_to_delete_shared_link": "Kunne ikke slette delt link",
@@ -548,12 +597,10 @@
     "unable_to_refresh_user": "Ikke i stand til at genopfriske bruger",
     "unable_to_remove_album_users": "Ikke i stand til at fjerne brugere fra album",
     "unable_to_remove_api_key": "Kunne ikke fjerne API-nøgle",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_deleted_assets": "Kunne ikke fjerne offlinefiler",
     "unable_to_remove_library": "Ikke i stand til at fjerne bibliotek",
-    "unable_to_remove_offline_files": "Kunne ikke fjerne offlinefiler",
     "unable_to_remove_partner": "Ikke i stand til at fjerne partner",
     "unable_to_remove_reaction": "Ikke i stand til at reaktion",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "Ikke i stand til at reparere ting",
     "unable_to_reset_password": "Ikke i stand til at nulstille adgangskode",
     "unable_to_resolve_duplicate": "Kunne ikke opklare duplikat",
@@ -577,52 +624,51 @@
     "unable_to_update_timeline_display_status": "Kunne ikke opdate status for tidslinjevisning",
     "unable_to_update_user": "Ikke i stand til at opdatere bruger"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
+  "exif": "Exif",
   "exit_slideshow": "Forlad slideshow",
   "expand_all": "Udvid alle",
   "expire_after": "Udløb efter",
   "expired": "Udløbet",
+  "expires_date": "Udløber {date}",
   "explore": "Udforsk",
   "export": "Eksportér",
   "export_as_json": "Eksportér som JSON",
   "extension": "Udvidelse",
   "external": "Ekstern",
   "external_libraries": "Eksterne biblioteker",
-  "failed_to_get_people": "At hente personer slog fejl",
   "favorite": "Favorit",
   "favorite_or_unfavorite_photo": "Tilføj eller fjern fra yndlingsbilleder",
   "favorites": "Favoritter",
-  "feature": "",
   "feature_photo_updated": "Forsidebillede uploadet",
-  "featurecollection": "",
+  "features": "Funktioner",
+  "features_setting_description": "Administrer app-funktioner",
   "file_name": "Filnavn",
   "file_name_or_extension": "Filnavn eller filtype",
   "filename": "Filnavn",
-  "files": "",
   "filetype": "Filtype",
   "filter_people": "Filtrér personer",
   "find_them_fast": "Find dem hurtigt med søgning via navn",
   "fix_incorrect_match": "Fix forkert match",
-  "force_re-scan_library_files": "Tving genskanning af alle biblioteksfiler",
+  "folders": "Mapper",
   "forward": "Fremad",
   "general": "Generel",
   "get_help": "Få hjælp",
   "getting_started": "Kom godt i gang",
   "go_back": "Gå tilbage",
   "go_to_search": "Gå til søgning",
-  "go_to_share_page": "Gå til delingsside",
   "group_albums_by": "Gruppér albummer efter...",
+  "group_no": "Ingen gruppering",
   "has_quota": "Har kvote",
+  "hi_user": "Hej {name} ({email})",
+  "hide_all_people": "Skjul alle personer",
   "hide_gallery": "Gem galleri",
+  "hide_named_person": "Skjul person {name}",
   "hide_password": "Gem adgangskode",
   "hide_person": "Gem person",
+  "hide_unnamed_people": "Skjul unavngivne personer",
   "host": "Host",
   "hour": "Time",
   "image": "Billede",
-  "img": "",
   "immich_logo": "Immich logo",
   "immich_web_interface": "Immich webinterface",
   "import_from_json": "Importér fra JSON",
@@ -641,13 +687,14 @@
   },
   "invite_people": "Inviter personer",
   "invite_to_album": "Inviter til album",
-  "job_settings_description": "",
   "jobs": "Opgaver",
   "keep": "Behold",
+  "keep_all": "Behold alle",
   "keyboard_shortcuts": "Tastaturgenveje",
   "language": "Sprog",
   "language_setting_description": "Vælg dit foretrukne sprog",
   "last_seen": "Sidst set",
+  "latest_version": "Seneste version",
   "leave": "Forlad",
   "let_others_respond": "Lad andre svare",
   "level": "Niveau",
@@ -662,7 +709,12 @@
   "loading_search_results_failed": "At loade søgeresultater slog fejl",
   "log_out": "Log ud",
   "log_out_all_devices": "Log ud af alle enheder",
+  "logged_out_all_devices": "Logget ud af alle enheder",
+  "logged_out_device": "Logget ud af enhed",
+  "login": "Log ind",
   "login_has_been_disabled": "Login er blevet deaktiveret.",
+  "logout_all_device_confirmation": "Er du sikker på, at du vil logge ud af alle enheder?",
+  "logout_this_device_confirmation": "Er du sikker på, at du vil logge denne enhed ud?",
   "look": "Kig",
   "loop_videos": "Gentag videoer",
   "loop_videos_description": "Aktivér for at genafspille videoer automatisk i detaljeret visning.",
@@ -696,15 +748,19 @@
   "name": "Navn",
   "name_or_nickname": "Navn eller kælenavn",
   "never": "aldrig",
+  "new_album": "Nyt album",
   "new_api_key": "Ny API-nøgle",
   "new_password": "Ny adgangskode",
   "new_person": "Ny person",
   "new_user_created": "Ny bruger oprettet",
+  "new_version_available": "NY VERSION TILGÆNGELIG",
   "newest_first": "Nyeste først",
   "next": "Næste",
   "next_memory": "Næste minde",
   "no": "Nej",
   "no_albums_message": "Opret et album for at organisere dine billeder og videoer",
+  "no_albums_with_name_yet": "Det ser ud til, at du ikke har noget album med dette navn endnu.",
+  "no_albums_yet": "Det ser ud til, at du ikke har nogen album endnu.",
   "no_archived_assets_message": "Arkivér billeder og fotos for at gemme dem væk fra dit Billed-view",
   "no_assets_message": "KLIK FOR AT UPLOADE DIT FØRSTE BILLEDE",
   "no_duplicates_found": "Ingen duplikater fundet.",
@@ -715,6 +771,7 @@
   "no_name": "Intet navn",
   "no_places": "Ingen steder",
   "no_results": "Ingen resultater",
+  "no_results_description": "Prøv et synonym eller et mere generelt søgeord",
   "no_shared_albums_message": "Opret et album for at dele billeder og videoer med personer i dit netværk",
   "not_in_any_album": "Ikke i noget album",
   "note_apply_storage_label_to_previously_uploaded assets": "Bemærk: For at anvende Lagringsmærkat på tidligere uploadede medier, kør",
@@ -724,17 +781,23 @@
   "notifications": "Notifikationer",
   "notifications_setting_description": "Administrér notifikationer",
   "oauth": "OAuth",
+  "official_immich_resources": "Officielle Immich-ressourcer",
   "offline": "Offline",
   "offline_paths": "Offline-stier",
   "offline_paths_description": "Disse resultater kan være på grund af manuel sletning af filer, som ikke er en del af et eksternt bibliotek.",
   "ok": "Ok",
   "oldest_first": "Ældste først",
+  "onboarding_privacy_description": "Følgende (valgfrie) funktioner er afhængige af eksterne tjenester, og kan til enhver tid deaktiveres i administrationsindstillingerne.",
+  "onboarding_welcome_user": "Velkommen, {user}",
   "online": "Online",
   "only_favorites": "Kun favoritter",
-  "only_refreshes_modified_files": "Kun genopfrisk ændrede filer",
+  "open_in_map_view": "Åben i kortvisning",
+  "open_in_openstreetmap": "Åben i OpenStreetMap",
   "open_the_search_filters": "Åbn søgefiltre",
   "options": "Handlinger",
+  "or": "eller",
   "organize_your_library": "Organisér dit bibliotek",
+  "original": "original",
   "other": "Andet",
   "other_devices": "Andre enheder",
   "other_variables": "Andre variable",
@@ -762,11 +825,11 @@
   "pending": "Afventer",
   "people": "Personer",
   "people_sidebar_description": "Vis et link til Personer i sidepanelet",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "Advarsel om permanent sletning",
   "permanent_deletion_warning_setting_description": "Vis en advarsel, når medier slettes permanent",
   "permanently_delete": "Slet permanent",
   "permanently_deleted_asset": "Permanent slettet medie",
+  "person": "Person",
   "photos": "Billeder",
   "photos_count": "{count, plural, one {{count, number} Billede} other {{count, number} Billeder}}",
   "photos_from_previous_years": "Billeder fra tidligere år",
@@ -777,7 +840,6 @@
   "play_memories": "Afspil minder",
   "play_motion_photo": "Afspil bevægelsesbillede",
   "play_or_pause_video": "Afspil eller paus video",
-  "point": "",
   "port": "Port",
   "preset": "Forudindstilling",
   "preview": "Forhåndsvisning",
@@ -787,8 +849,6 @@
   "primary": "Primære",
   "profile_picture_set": "Profilbillede sat.",
   "public_share": "Offentlig deling",
-  "range": "",
-  "raw": "",
   "reaction_options": "Reaktionsindstillinger",
   "read_changelog": "Læs ændringslog",
   "recent": "For nylig",
@@ -797,10 +857,10 @@
   "refreshed": "Opdateret",
   "refreshes_every_file": "Opdaterer alle filer",
   "remove": "Fjern",
+  "remove_deleted_assets": "Fjern fra offlinefiler",
   "remove_from_album": "Fjern fra album",
   "remove_from_favorites": "Fjern fra favoritter",
   "remove_from_shared_link": "Fjern fra delt link",
-  "remove_offline_files": "Fjern fra offlinefiler",
   "removed_api_key": "Fjernede API-nøgle: {name}",
   "rename": "Omdøb",
   "repair": "Reparér",
@@ -811,7 +871,6 @@
   "reset": "Nulstil",
   "reset_password": "Nulstil adgangskode",
   "reset_people_visibility": "Nulstil personsynlighed",
-  "reset_settings_to_default": "",
   "restore": "Gendan",
   "restore_all": "Gendan alle",
   "restore_user": "Gendan bruger",
@@ -825,8 +884,6 @@
   "saved_settings": "Gemte indstillinger",
   "say_something": "Skriv noget",
   "scan_all_libraries": "Skan gennem alle biblioteker",
-  "scan_all_library_files": "Genskan alle biblioteksfiler",
-  "scan_new_library_files": "Skan nye biblioteksfiler",
   "scan_settings": "Skanningsindstillinger",
   "search": "Søg",
   "search_albums": "Søg i albummer",
@@ -857,7 +914,6 @@
   "selected": "Valgt",
   "send_message": "Send besked",
   "send_welcome_email": "Send velkomstemail",
-  "server": "Server",
   "server_stats": "Serverstatus",
   "set": "Sæt",
   "set_as_album_cover": "Sæt som albumcover",
@@ -928,7 +984,6 @@
   "to_favorite": "Gør til favorit",
   "toggle_settings": "Slå indstillinger til eller fra",
   "toggle_theme": "Slå mørkt tema til eller fra",
-  "toggle_visibility": "Slå synlighed til eller fra",
   "total_usage": "Samlet forbrug",
   "trash": "Papirkurv",
   "trash_all": "Smid alle ud",
@@ -936,11 +991,9 @@
   "trashed_items_will_be_permanently_deleted_after": "Mediefiler i skraldespanden vil blive slettet permanent efter {days, plural, one {# dag} other {# dage}}.",
   "type": "Type",
   "unarchive": "Afakivér",
-  "unarchived": "Uarkiveret",
   "unfavorite": "Fjern favorit",
   "unhide_person": "Hold op med at gemme person væk",
   "unknown": "Ukendt",
-  "unknown_album": "Ukendt album",
   "unknown_year": "Ukendt år",
   "unlimited": "Ubegrænset",
   "unlink_oauth": "Frakobl OAuth",
@@ -958,6 +1011,8 @@
   "user": "Bruger",
   "user_id": "Bruger-ID",
   "user_usage_detail": "Detaljer om brugers forbrug",
+  "user_usage_stats": "Konto anvendelsesstatistik",
+  "user_usage_stats_description": "Vis konto anvendelsesstatistik",
   "username": "Brugernavn",
   "users": "Brugere",
   "utilities": "Værktøjer",
@@ -974,7 +1029,6 @@
   "view_links": "Vis links",
   "view_next_asset": "Se næste medie",
   "view_previous_asset": "Se forrige medie",
-  "viewer": "Viewer",
   "waiting": "Venter",
   "week": "Uge",
   "welcome": "Velkommen",
diff --git a/web/src/lib/i18n/de.json b/i18n/de.json
similarity index 82%
rename from web/src/lib/i18n/de.json
rename to i18n/de.json
index 77b420db00..7d6bffba91 100644
--- a/web/src/lib/i18n/de.json
+++ b/i18n/de.json
@@ -11,7 +11,7 @@
   "add": "Hinzufügen",
   "add_a_description": "Beschreibung hinzufügen",
   "add_a_location": "Standort hinzufügen",
-  "add_a_name": "Namen hinzufügen",
+  "add_a_name": "Name hinzufügen",
   "add_a_title": "Titel hinzufügen",
   "add_exclusion_pattern": "Ausschlussmuster hinzufügen",
   "add_import_path": "Importpfad hinzufügen",
@@ -23,66 +23,75 @@
   "add_to": "Hinzufügen zu ...",
   "add_to_album": "Zu Album hinzufügen",
   "add_to_shared_album": "Zu geteiltem Album hinzufügen",
+  "add_url": "URL hinzufügen",
   "added_to_archive": "Zum Archiv hinzugefügt",
   "added_to_favorites": "Zu Favoriten hinzugefügt",
   "added_to_favorites_count": "{count, number} zu Favoriten hinzugefügt",
   "admin": {
     "add_exclusion_pattern_description": "Ausschlussmuster hinzufügen. Platzhalter, wie *, **, und ? werden unterstützt. Um alle Dateien in einem Verzeichnis namens „Raw\" zu ignorieren, „**/Raw/**“ verwenden. Um alle Dateien zu ignorieren, die auf „.tif“ enden, „**/*.tif“ verwenden. Um einen absoluten Pfad zu ignorieren, „/pfad/zum/ignorieren/**“ verwenden.",
+    "asset_offline_description": "Diese Datei einer externen Bibliothek befindet sich nicht mehr auf der Festplatte und wurde in den Papierkorb verschoben. Falls die Datei innerhalb der Bibliothek verschoben wurde, überprüfe deine Zeitleiste auf die neue entsprechende Datei. Um diese Datei wiederherzustellen, stelle bitte sicher, dass Immich auf den unten stehenden Dateipfad zugreifen kann und scanne die Bibliothek.",
     "authentication_settings": "Authentifizierungseinstellungen",
     "authentication_settings_description": "Passwort-, OAuth- und sonstigen Authentifizierungseinstellungen verwalten",
     "authentication_settings_disable_all": "Bist du sicher, dass du alle Anmeldemethoden deaktivieren willst? Die Anmeldung wird vollständig deaktiviert.",
     "authentication_settings_reenable": "Nutze einen <link>Server-Befehl</link> zur Reaktivierung.",
     "background_task_job": "Hintergrund-Aufgaben",
+    "backup_database": "Datenbank sichern",
+    "backup_database_enable_description": "Sicherung der Datenbank aktivieren",
+    "backup_keep_last_amount": "Anzahl der aufzubewahrenden früheren Sicherungen",
+    "backup_settings": "Datensicherungs-Einstellungen",
+    "backup_settings_description": "Datensicherungs-Einstellungen verwalten",
     "check_all": "Alle überprüfen",
     "cleared_jobs": "Folgende Aufgaben zurückgesetzt: {job}",
     "config_set_by_file": "Ist derzeit in einer Konfigurationsdatei festgelegt",
     "confirm_delete_library": "Bist du sicher, dass du die Bibliothek {library} löschen willst?",
-    "confirm_delete_library_assets": "Bist du sicher, dass du diese Bibliothek löschen willst? Dies löscht alle {count, plural, one {# enthaltenes Objekt} other {alle # enthaltenen Objekte}} aus Immich und kann nicht rückgängig gemacht werden. Die Dateien bleiben auf der Festplatte erhalten.",
-    "confirm_email_below": "Bestätige, indem du \"{email}\" unten eingibst",
+    "confirm_delete_library_assets": "Bist du sicher, dass du diese Bibliothek löschen willst? Dies löscht {count, plural, one {# enthaltenes Objekt} other {alle # enthaltenen Objekte}} aus Immich und kann nicht rückgängig gemacht werden. Die Dateien bleiben auf der Festplatte erhalten.",
+    "confirm_email_below": "Bestätige, indem du unten \"{email}\" eingibst",
     "confirm_reprocess_all_faces": "Bist du sicher, dass du alle Gesichter erneut verarbeiten möchtest? Dies löscht auch alle bereits benannten Personen.",
     "confirm_user_password_reset": "Bist du sicher, dass du das Passwort für {user} zurücksetzen möchtest?",
-    "crontab_guru": "Crontab Guru",
+    "create_job": "Aufgabe erstellen",
+    "cron_expression": "Cron-Ausdruck",
+    "cron_expression_description": "Stellen Sie das Scanintervall im Cron-Format ein. Weitere Informationen finden Sie beispielsweise unter <link>Crontab Guru</link>",
+    "cron_expression_presets": "Cron-Ausdruck-Vorlagen",
     "disable_login": "Login deaktvieren",
-    "disabled": "Deaktiviert",
-    "duplicate_detection_job_description": "Diese Aufgabe führt das maschinelle Lernen für jede Datei aus, um Duplikate zu finden. Diese Aufgabe beruht auf der Smart Search Technologie",
-    "exclusion_pattern_description": "Mit Ausschlussmustern können Dateien und Ordner beim Scannen Ihrer Bibliothek ignoriert werden. Dies ist nützlich, wenn Sie Ordner haben, die Dateien enthalten, die Sie nicht importieren möchten, wie z. B. RAW-Dateien.",
+    "duplicate_detection_job_description": "Diese Aufgabe führt das maschinelle Lernen für jede Datei aus, um Duplikate zu finden. Diese Aufgabe beruht auf der intelligenten Suche",
+    "exclusion_pattern_description": "Mit Ausschlussmustern können Dateien und Ordner beim Scannen Ihrer Bibliothek ignoriert werden. Dies ist nützlich, wenn du Ordner hast, die Dateien enthalten, die du nicht importieren möchtest, wie z. B. RAW-Dateien.",
     "external_library_created_at": "Externe Bibliothek (erstellt am {date})",
-    "external_library_management": "Externe Bibliotheksverwaltung",
+    "external_library_management": "Verwaltung externer Bibliotheken",
     "face_detection": "Gesichtserkennung",
-    "face_detection_description": "Diese Aufgabe erkennt Gesichter in Dateien mittels maschinellen Lernens. Bei Videos wird nur die Miniaturansicht verwendet. „Alle“ verarbeitet alle Dateien neu, während „Fehlende“ nur nicht verarbeitete Dateien in die Warteschlange stellt. Erkannte Gesichter werden zur Gruppierung in bestehende oder neue Personen in die Warteschlange gestellt.",
-    "facial_recognition_job_description": "Diese Aufgabe gruppiert erkannte Gesichter zu Personen nach der Gesichtserkennung. „Alle“ clustert alle Gesichter neu, während „Fehlende“ Gesichter ohne Zuordnung in die Warteschlange stellt.",
+    "face_detection_description": "Diese Aufgabe erkennt Gesichter in Dateien mittels maschinellen Lernens. Bei Videos wird nur die Miniaturansicht verwendet. „Aktualisieren“ verarbeitet alle Dateien neu. „Zurücksetzen“ setzt zusätzlich alle Gesichter zurück. „Fehlende“ stellt nur nicht verarbeitete Dateien in die Warteschlange. Erkannte Gesichter werden zur Gruppierung in bestehende oder neue Personen in die Warteschlange gestellt.",
+    "facial_recognition_job_description": "Diese Aufgabe gruppiert im Anschluss an die Gesichtserkennung die erkannten Gesichter zu Personen. „Zurücksetzen“ gruppiert alle Gesichter neu, während „Fehlende“ Gesichter ohne Zuordnung in die Warteschlange stellt.",
     "failed_job_command": "Befehl {command} ist für Aufgabe {job} fehlgeschlagen",
     "force_delete_user_warning": "WARNUNG: Diese Aktion löscht sofort den Benutzer und all seine Dateien. Dies kann nicht rückgängig gemacht werden und die Dateien können nicht wiederhergestellt werden.",
     "forcing_refresh_library_files": "Erneutes Laden aller Bibliotheksdateien erzwingen",
-    "image_format_description": "WebP erzeugt kleinere Dateien als JPEG, ist dafür aber etwas langsamer in der Verarbeitung.",
+    "image_format": "Format",
+    "image_format_description": "WebP erzeugt kleinere Dateien als JPEG, ist aber etwas langsamer in der Erstellung.",
     "image_prefer_embedded_preview": "Eingebettete Vorschau bevorzugen",
     "image_prefer_embedded_preview_setting_description": "Verwende eingebettete Vorschaubilder in RAW-Fotos als Grundlage für die Bildverarbeitung, sofern diese zur Verfügung stehen. Dies kann bei einigen Bildern genauere Farben erzeugen, allerdings ist die Qualität der Vorschau kameraabhängig und das Bild kann mehr Kompressionsartefakte aufweisen.",
     "image_prefer_wide_gamut": "Breites Spektrum bevorzugen",
     "image_prefer_wide_gamut_setting_description": "Verwendung von Display P3 (DCI-P3) für Miniaturansichten. Dadurch bleibt die Lebendigkeit von Bildern mit breiten Farbräumen besser erhalten, aber die Bilder können auf älteren Geräten mit einer älteren Browserversion etwas anders aussehen. sRGB-Bilder werden im sRGB-Format belassen, um Farbverschiebungen zu vermeiden.",
-    "image_preview_format": "Vorschauformat",
-    "image_preview_resolution": "Vorschau-Auflösung",
-    "image_preview_resolution_description": "Dies wird beim Anzeigen eines einzelnen Fotos und für das maschinelle Lernen verwendet. Höhere Auflösungen können mehr Details beibehalten, benötigen aber mehr Zeit für die Kodierung, haben größere Dateigrößen und können die Reaktionsfähigkeit der App beeinträchtigen.",
+    "image_preview_description": "Mittelgroßes Bild mit entfernten Metadaten, das bei der Betrachtung einer einzelnen Datei und für maschinelles Lernen verwendet wird",
+    "image_preview_quality_description": "Vorschauqualität von 1-100. Ein höherer Wert ist besser, erzeugt dadurch aber größere Dateien und kann die Reaktionsfähigkeit der App beeinträchtigen. Die Einstellung eines niedrigen Wertes kann dafür aber die Qualität des maschinellen Lernens beeinträchtigen.",
+    "image_preview_title": "Vorschaueinstellungen",
     "image_quality": "Qualität",
-    "image_quality_description": "Bildqualität von 1-100. Höher bedeutet bessere Qualität, erzeugt aber größere Dateien. Diese Option betrifft die Vorschaubilder und Miniaturansichten.",
+    "image_resolution": "Auflösung",
+    "image_resolution_description": "Höhere Auflösungen können mehr Details erhalten, benötigen aber mehr Zeit für die Kodierung, haben größere Dateigrößen und können die Reaktionsfähigkeit von Anwendungen beeinträchtigen.",
     "image_settings": "Bildeinstellungen",
     "image_settings_description": "Qualität und Auflösung von generierten Bildern verwalten",
-    "image_thumbnail_format": "Miniaturansichts-Format",
-    "image_thumbnail_resolution": "Miniaturansichts-Auflösung",
-    "image_thumbnail_resolution_description": "Dies wird bei der Anzeige von Bildergruppen („Zeitleiste“, „Albumansicht“ usw.) verwendet. Höhere Auflösungen können mehr Details beibehalten, benötigen aber mehr Zeit für die Kodierung, haben größere Dateigrößen und können die Reaktionsfähigkeit der App beeinträchtigen.",
-    "job_concurrency": "{job} - (Anzahl gleichzeitiger Prozesse)",
-    "job_not_concurrency_safe": "Dieser Job ist nicht parallelisierungssicher.",
-    "job_settings": "Job-Einstellungen",
-    "job_settings_description": "Gleichzeitige Job-Prozessen verwalten",
-    "job_status": "Job-Status",
+    "image_thumbnail_description": "Kleine Miniaturansicht mit entfernten Metadaten, die bei der Anzeige von Sammlungen von Fotos wie der Zeitleiste verwendet wird",
+    "image_thumbnail_quality_description": "Qualität der Miniaturansicht von 1-100. Höher ist besser, erzeugt aber größere Dateien und kann die Reaktionsfähigkeit der App beeinträchtigen.",
+    "image_thumbnail_title": "Miniaturansicht-Einstellungen",
+    "job_concurrency": "{job} (Anzahl gleichzeitiger Prozesse)",
+    "job_created": "Aufgabe erstellt",
+    "job_not_concurrency_safe": "Diese Aufgabe ist nicht parallelisierungssicher.",
+    "job_settings": "Aufgaben-Einstellungen",
+    "job_settings_description": "Gleichzeitige Aufgaben-Prozesse verwalten",
+    "job_status": "Aufgaben-Status",
     "jobs_delayed": "{jobCount, plural, other {# verzögert}}",
     "jobs_failed": "{jobCount, plural, other {# fehlgeschlagen}}",
     "library_created": "Bibliothek erstellt: {library}",
-    "library_cron_expression": "Cron-Ausdruck",
-    "library_cron_expression_description": "Legen Sie das Überprüfungsintervall mit Hilfe des cron-Formats fest. Für weitere Informationen siehe z.B. <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Cron-Expression Voreinstellungen",
     "library_deleted": "Bibliothek gelöscht",
     "library_import_path_description": "Gib einen Ordner für den Import an. Dieser Ordner, einschließlich der Unterordner, wird nach Bildern und Videos durchsucht.",
-    "library_scanning": "Periodisches scannen",
+    "library_scanning": "Periodisches Scannen",
     "library_scanning_description": "Regelmäßiges Durchsuchen der Bibliothek einstellen",
     "library_scanning_enable_description": "Regelmäßiges Scannen der Bibliothek aktivieren",
     "library_settings": "Externe Bibliothek",
@@ -92,12 +101,12 @@
     "library_watching_settings": "Bibliotheksüberwachung (EXPERIMENTELL)",
     "library_watching_settings_description": "Automatisch auf geänderte Dateien prüfen",
     "logging_enable_description": "Aktiviere Logging",
-    "logging_level_description": "Wenn aktiviert, welches Log Level genutzt wird.",
+    "logging_level_description": "Wenn aktiviert, welches Log-Level genutzt wird.",
     "logging_settings": "Protokollierung",
     "machine_learning_clip_model": "CLIP-Modell",
-    "machine_learning_clip_model_description": "Der Name eines CLIP-Modells, welches <link>\"hier\"</link> aufgeführt ist. Beachte, dass du den Job \"Intelligente Suche\" für alle Bilder erneut ausführen musst, wenn du das Modell wechselst.",
-    "machine_learning_duplicate_detection": "Duplikats-Erkennung",
-    "machine_learning_duplicate_detection_enabled": "Duplikat-Erkennung aktivieren",
+    "machine_learning_clip_model_description": "Der Name eines CLIP-Modells, welches <link>hier</link> aufgeführt ist. Beachte, dass du die Aufgabe \"Intelligente Suche\" für alle Bilder erneut ausführen musst, wenn du das Modell wechselst.",
+    "machine_learning_duplicate_detection": "Duplikaterkennung",
+    "machine_learning_duplicate_detection_enabled": "Duplikaterkennung aktivieren",
     "machine_learning_duplicate_detection_enabled_description": "Falls diese Option deaktiviert ist, werden exakt identische Dateien dennoch de-dupliziert.",
     "machine_learning_duplicate_detection_setting_description": "Verwendung von CLIP-Embeddings zum Erkennen möglicher Duplikate",
     "machine_learning_enabled": "Maschinelles Lernen aktivieren",
@@ -105,54 +114,54 @@
     "machine_learning_facial_recognition": "Gesichtsidentifikation",
     "machine_learning_facial_recognition_description": "Erkenne, identifiziere und gruppiere Gesichter in Bildern",
     "machine_learning_facial_recognition_model": "Gesichtserkennungs-Modell",
-    "machine_learning_facial_recognition_model_description": "Die Modelle sind in absteigender Reihenfolge ihrer Größe aufgeführt. Größere Modelle sind langsamer und verbrauchen mehr Speicher, liefern aber bessere Ergebnisse. Bitte beachte dabei, dass du den Gesichtserkennungsjob für alle Bilder neu starten musst, wenn du ein Modell änderst.",
+    "machine_learning_facial_recognition_model_description": "Die Modelle sind in absteigender Reihenfolge ihrer Größe aufgeführt. Größere Modelle sind langsamer und verbrauchen mehr Speicher, liefern aber bessere Ergebnisse. Bitte beachte dabei, dass du die Gesichtserkennungsaufgabe für alle Bilder neu starten musst, wenn du ein Modell änderst.",
     "machine_learning_facial_recognition_setting": "Gesichtserkennung aktivieren",
     "machine_learning_facial_recognition_setting_description": "Wenn diese Option deaktiviert ist, werden die Bilder nicht für die Gesichtserkennung kodiert und der Abschnitt „Personen“ auf der Seite „Erkunden“ wird nicht dargestellt.",
     "machine_learning_max_detection_distance": "Maximaler Erkennungsabstand",
-    "machine_learning_max_detection_distance_description": "Maximaler Unterschied zwischen zwei Bildern, um sie als Duplikate zu betrachten, im Bereich von 0,001-0,1. Bei höheren Werten werden mehr Duplikate erkannt, aber es kann zu falsch positiven Ergebnissen kommen.",
+    "machine_learning_max_detection_distance_description": "Maximaler Unterschied zwischen zwei Bildern, um sie als Duplikate zu betrachten, im Bereich von 0,001-0,1. Bei höheren Werten werden mehr Duplikate erkannt, aber es kann zu falsch-positiven Ergebnissen kommen.",
     "machine_learning_max_recognition_distance": "Maximaler Erkennungsabstand",
     "machine_learning_max_recognition_distance_description": "Maximaler Abstand zwischen zwei Gesichtern, die als dieselbe Person angesehen werden, von 0-2. Ein niedrigerer Wert kann verhindern, dass zwei Personen als dieselbe Person eingestuft werden, während ein höherer Wert verhindern kann, dass ein und dieselbe Person als zwei verschiedene Personen eingestuft wird. Bitte beachte dabei, dass es einfacher ist, zwei Personen zu verschmelzen, als eine Person in zwei zu teilen, also wähle nach Möglichkeit einen niedrigeren Schwellenwert.",
     "machine_learning_min_detection_score": "Minimale Erkennungsrate",
     "machine_learning_min_detection_score_description": "Minimale Konfidenzrate für die Erkennung eines Gesichts von 0-1. Bei niedrigeren Werten werden mehr Gesichter erkannt, aber es kann zu falsch-positiven Ergebnissen kommen.",
     "machine_learning_min_recognized_faces": "Mindestens erkannte Gesichter",
-    "machine_learning_min_recognized_faces_description": "Die Mindestanzahl von erkannten Gesichtern, damit eine Person erstellt werden kann. Eine Erhöhung dieses Wertes macht die Gesichtserkennung präziser, erhöht aber die Wahrscheinlichkeit, dass ein Gesicht nicht zu einer Person zugeordnet werden kann.",
+    "machine_learning_min_recognized_faces_description": "Die Mindestanzahl von erkannten Gesichtern, damit eine Person erstellt werden kann. Eine Erhöhung dieses Wertes macht die Gesichtserkennung präziser, erhöht aber die Wahrscheinlichkeit, dass ein Gesicht nicht zu einer Person zugeordnet wird.",
     "machine_learning_settings": "Einstellungen für maschinelles Lernen",
-    "machine_learning_settings_description": "Funktionen und Einstellungen für das maschinelle Lernen verwalten",
+    "machine_learning_settings_description": "Funktionen und Einstellungen des maschinellen Lernens verwalten",
     "machine_learning_smart_search": "Intelligente Suche",
-    "machine_learning_smart_search_description": "Semantische Bildsuche mit CLIP-Einbettungen",
+    "machine_learning_smart_search_description": "Semantische Bildsuche mittels CLIP-Einbettungen",
     "machine_learning_smart_search_enabled": "Intelligente Suche aktivieren",
     "machine_learning_smart_search_enabled_description": "Ist diese Option deaktiviert, werden die Bilder nicht für die intelligente Suche verwendet.",
-    "machine_learning_url_description": "Server-URL für maschinelles Lernen",
+    "machine_learning_url_description": "Die URL des Servers für maschinelles Lernen. Wenn mehr als eine URL angegeben wird, wird jeder Server einzeln ausprobiert, bis einer erfolgreich antwortet, und zwar in der Reihenfolge vom ersten bis zum letzten.",
     "manage_concurrency": "Gleichzeitige Ausführungen verwalten",
     "manage_log_settings": "Log-Einstellungen verwalten",
     "map_dark_style": "Dunkler Stil",
     "map_enable_description": "Kartenfunktionen aktivieren",
-    "map_gps_settings": "Karten & GPS Einstellungen",
-    "map_gps_settings_description": "Karten & GPS Einstellungen verwalten",
+    "map_gps_settings": "Karten- & GPS-Einstellungen",
+    "map_gps_settings_description": "Karten- & GPS-Einstellungen verwalten",
     "map_implications": "Die Kartenfunktion verwendet einen externen Tile-Service (tiles.immich.cloud)",
     "map_light_style": "Heller Stil",
-    "map_manage_reverse_geocoding_settings": "Einstellungen für die <link>Umgekehrte Geokodierung</link> verwalten",
+    "map_manage_reverse_geocoding_settings": "Einstellungen für die <link>umgekehrte Geokodierung</link> verwalten",
     "map_reverse_geocoding": "Umgekehrte Geokodierung",
     "map_reverse_geocoding_enable_description": "Umgekehrte Geokodierung aktivieren",
-    "map_reverse_geocoding_settings": "Einstellungen für Umgekehrte Geokodierung",
-    "map_settings": "Karten",
-    "map_settings_description": "Karten- und GPS Einstellungen verwalten",
+    "map_reverse_geocoding_settings": "Einstellungen für umgekehrte Geokodierung",
+    "map_settings": "Karte",
+    "map_settings_description": "Karten- und GPS-Einstellungen verwalten",
     "map_style_description": "URL zu einem style.json Karten-Theme",
     "metadata_extraction_job": "Metadaten extrahieren",
     "metadata_extraction_job_description": "Extrahieren von Metadaten, wie zum Beispiel GPS, Gesichtern und Auflösung aus jeder Datei",
     "metadata_faces_import_setting": "Import von Gesichtern aktivieren",
-    "metadata_faces_import_setting_description": "Gesichter aus EXIF Daten des Bildes und Sidecar Dateien importieren",
-    "metadata_settings": "Metadaten Einstellungen",
-    "metadata_settings_description": "Metadaten Einstellungen verwalten",
+    "metadata_faces_import_setting_description": "Gesichter aus EXIF-Daten des Bildes und Sidecar-Dateien importieren",
+    "metadata_settings": "Metadaten-Einstellungen",
+    "metadata_settings_description": "Metadaten-Einstellungen verwalten",
     "migration_job": "Migration",
     "migration_job_description": "Diese Aufgabe migriert Miniaturansichten für Dateien und Gesichter in die neueste Ordnerstruktur",
     "no_paths_added": "Keine Pfade hinzugefügt",
-    "no_pattern_added": "Kein Pattern hinzugefügt",
-    "note_apply_storage_label_previous_assets": "Hinweis: Um das Storage Label auf die vorher hochgeladenen Dateien anzuwenden, starte den",
+    "no_pattern_added": "Kein Ausschlussmuster hinzugefügt",
+    "note_apply_storage_label_previous_assets": "Hinweis: Um den Speicherpfad auf die vorher hochgeladenen Dateien anzuwenden, starte den",
     "note_cannot_be_changed_later": "HINWEIS: Dies kann später nicht mehr geändert werden!",
     "note_unlimited_quota": "Hinweis: 0 eingeben für unlimitiertes Kontingent",
-    "notification_email_from_address": "Von",
-    "notification_email_from_address_description": "E-Mail-Adresse des Senders, zum Beispiel: \"Immich Photo Server <noreply@immich.app>\"",
+    "notification_email_from_address": "Absenderadresse",
+    "notification_email_from_address_description": "E-Mail-Adresse des Senders, zum Beispiel: \"Immich Photo Server <noreply@example.com>\"",
     "notification_email_host_description": "Host des E-Mail-Servers (z.B. smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Ignoriere Zertifikats-Fehler",
     "notification_email_ignore_certificate_errors_description": "TLS-Zertifikatsvalidierungsfehler ignorieren (nicht empfohlen)",
@@ -166,13 +175,13 @@
     "notification_email_username_description": "Benutzername, der bei der Anmeldung am E-Mail-Server verwendet wird",
     "notification_enable_email_notifications": "E-Mail-Benachrichtigungen aktivieren",
     "notification_settings": "Benachrichtigungseinstellungen",
-    "notification_settings_description": "Eenachrichtigungseinstellungen (inkl. E-Mail) verwalten",
+    "notification_settings_description": "Benachrichtigungseinstellungen (inkl. E-Mail) verwalten",
     "oauth_auto_launch": "Auto-Start",
     "oauth_auto_launch_description": "Automatischer Start des OAuth-Anmeldevorgangs beim Aufrufen der Anmeldeseite",
     "oauth_auto_register": "Automatische Registrierung",
     "oauth_auto_register_description": "Automatische Registrierung neuer Benutzer nach der OAuth-Anmeldung",
-    "oauth_button_text": "Button Text",
-    "oauth_client_id": "Client ID",
+    "oauth_button_text": "Button-Text",
+    "oauth_client_id": "Client-ID",
     "oauth_client_secret": "Client-Geheimnis",
     "oauth_enable_description": "Anmeldung mit OAuth",
     "oauth_issuer_url": "Aussteller-URL",
@@ -180,7 +189,7 @@
     "oauth_mobile_redirect_uri_override": "Mobile Umleitungs-URI überschreiben",
     "oauth_mobile_redirect_uri_override_description": "Einschalten, wenn der OAuth-Provider keine mobile URI wie '{callback}' erlaubt",
     "oauth_profile_signing_algorithm": "Algorithmus zur Profilsignierung",
-    "oauth_profile_signing_algorithm_description": "Dieser Algorithmus wird für die für die Signatur des Benutzerprofils verwendet.",
+    "oauth_profile_signing_algorithm_description": "Dieser Algorithmus wird für die Signatur des Benutzerprofils verwendet.",
     "oauth_scope": "Umfang",
     "oauth_settings": "OAuth",
     "oauth_settings_description": "OAuth-Anmeldeeinstellungen verwalten",
@@ -191,29 +200,31 @@
     "oauth_storage_quota_claim": "Speicherkontingentangabe",
     "oauth_storage_quota_claim_description": "Setzen Sie das Speicherkontingent des Benutzers automatisch auf den angegebenen Wert.",
     "oauth_storage_quota_default": "Standard-Speicherplatzkontingent (GiB)",
-    "oauth_storage_quota_default_description": "Kontingent in GiB, welcher verwendet werden kann, wenn kein Anspruch erhoben wurde (Gib 0 für einen unbegrenzten Speicherkontingent ein).",
+    "oauth_storage_quota_default_description": "Kontingent in GiB, das verwendet werden soll, wenn keines übermittelt wird (gib 0 für ein unbegrenztes Kontingent ein).",
     "offline_paths": "Offline-Pfade",
     "offline_paths_description": "Die Ergebnisse könnten durch manuelles Löschen von Dateien, die nicht Teil einer externen Bibliothek sind, verursacht sein.",
     "password_enable_description": "Login mit E-Mail und Passwort",
-    "password_settings": "Passwort Login",
+    "password_settings": "Passwort-Login",
     "password_settings_description": "Passwort-Anmeldeeinstellungen verwalten",
     "paths_validated_successfully": "Alle Pfade wurden erfolgreich validiert",
+    "person_cleanup_job": "Personen aufräumen",
     "quota_size_gib": "Kontingent (GiB)",
     "refreshing_all_libraries": "Alle Bibliotheken aktualisieren",
     "registration": "Admin-Registrierung",
     "registration_description": "Da du der erste Benutzer im System bist, wirst du als Admin zugewiesen und bist für administrative Aufgaben zuständig. Weitere Benutzer werden von dir erstellt.",
-    "removing_offline_files": "Offline-Dateien entfernen",
     "repair_all": "Alle reparieren",
     "repair_matched_items": "{count, plural, one {# Eintrag} other {# Einträge}} gefunden",
     "repaired_items": "{count, plural, one {# Eintrag} other {# Einträge}} repariert",
     "require_password_change_on_login": "Benutzer muss das Passwort beim ersten Login ändern",
     "reset_settings_to_default": "Einstellungen auf Standard zurücksetzen",
     "reset_settings_to_recent_saved": "Einstellungen auf die zuletzt gespeicherten Einstellungen zurücksetzen",
-    "scanning_library_for_changed_files": "Untersuche Bibliothek auf geänderte Dateien",
-    "scanning_library_for_new_files": "Untersuche Bibliothek auf neue Dateien",
+    "scanning_library": "Bibliothek scannen",
+    "search_jobs": "Aufgaben suchen...",
     "send_welcome_email": "Begrüssungsmail senden",
     "server_external_domain_settings": "Externe Domain",
     "server_external_domain_settings_description": "Domäne für öffentlich freigegebene Links, einschließlich http(s)://",
+    "server_public_users": "Öffentliche Benutzer",
+    "server_public_users_description": "Beim Hinzufügen eines Benutzers zu freigegebenen Alben werden alle Benutzer (Name und E-Mail) aufgelistet. Wenn diese Option deaktiviert ist, steht die Benutzerliste nur Administratoren zur Verfügung.",
     "server_settings": "Servereinstellungen",
     "server_settings_description": "Servereinstellungen verwalten",
     "server_welcome_message": "Willkommensnachricht",
@@ -221,7 +232,7 @@
     "sidecar_job": "Filialdatei-Metadaten",
     "sidecar_job_description": "Durch diese Aufgabe werden Filialdatei-Metadaten im Dateisystem entdeckt oder synchronisiert",
     "slideshow_duration_description": "Dauer der Anzeige jedes Bildes in Sekunden",
-    "smart_search_job_description": "Diese Aufgabe wendet das maschinelles Lernen auf Dateien an, um die intelligente Suche zu ermöglichen",
+    "smart_search_job_description": "Diese Aufgabe wendet das maschinelle Lernen auf Dateien an, um die intelligente Suche zu ermöglichen",
     "storage_template_date_time_description": "Der Erstellungszeitstempel der Datei wird für die Datums- und Uhrzeitinformation verwendet",
     "storage_template_date_time_sample": "Beispielzeitpunkt {date}",
     "storage_template_enable_description": "Speichervorlagen-Engine aktivieren",
@@ -230,14 +241,25 @@
     "storage_template_migration": "Migration von Speichervorlagen",
     "storage_template_migration_description": "Diese Aufgabe wendet die aktuelle <link>{template}</link> auf zuvor hochgeladene Dateien an",
     "storage_template_migration_info": "Vorlagenänderungen gelten nur für neue Dateien. Um die Vorlage rückwirkend auf bereits hochgeladene Assets anzuwenden, führe den <link>{job}</link> aus.",
-    "storage_template_migration_job": "Speichervorlagenmigrations-Job",
-    "storage_template_more_details": "Weitere Details zu dieser Funktion finden Sie unter <template-link>Speichervorlage</template-link> und dessen <implications-link>Implikationen</implications-link>",
+    "storage_template_migration_job": "Speichervorlagenmigrations-Aufgabe",
+    "storage_template_more_details": "Weitere Details zu dieser Funktion findest du unter <template-link>Speichervorlage</template-link> und dessen <implications-link>Implikationen</implications-link>",
     "storage_template_onboarding_description": "Wenn aktiviert, sortiert diese Funktion Dateien automatisch basierend auf einer benutzerdefinierten Vorlage. Aufgrund von Stabilitätsproblemen ist die Funktion standardmäßig deaktiviert. Weitere Informationen findest du in der <link>Dokumentation</link>.",
-    "storage_template_path_length": "Ungefähres Pfad Längen Limit: <b>{length, number}</b>/{limit, number}",
+    "storage_template_path_length": "Ungefähres Pfadlängen-Limit: <b>{length, number}</b>/{limit, number}",
     "storage_template_settings": "Speichervorlage",
     "storage_template_settings_description": "Die Ordnerstruktur und den Dateinamen der hochgeladenen Datei verwalten",
-    "storage_template_user_label": "<code>{label}</code> is das Speicher-Label des Benutzers",
+    "storage_template_user_label": "<code>{label}</code> is die Speicherpfadbezeichnung des Benutzers",
     "system_settings": "Systemeinstellungen",
+    "tag_cleanup_job": "Tags aufräumen",
+    "template_email_available_tags": "In deiner Vorlage kannst du die folgenden Variablen verwenden: {tags}",
+    "template_email_if_empty": "Wenn die Vorlage leer ist, wird die Standard-E-Mail verwendet.",
+    "template_email_invite_album": "E-Mail-Vorlage: Einladung zu Album",
+    "template_email_preview": "Vorschau",
+    "template_email_settings": "E-Mail Vorlagen",
+    "template_email_settings_description": "Benutzerdefinierte E-Mail Benachrichtigungsvorlagen verwalten",
+    "template_email_update_album": "Album Vorlage aktualisieren",
+    "template_email_welcome": "Willkommen bei den E-Mail Vorlagen",
+    "template_settings": "Benachrichtigungsvorlagen",
+    "template_settings_description": "Benutzerdefinierte Vorlagen für Benachrichtigungen verwalten",
     "theme_custom_css_settings": "Benutzerdefiniertes CSS",
     "theme_custom_css_settings_description": "Mit Cascading Style Sheets (CSS) kann das Design von Immich angepasst werden.",
     "theme_settings": "Theme-Einstellungen",
@@ -245,7 +267,6 @@
     "these_files_matched_by_checksum": "Diese Dateien wurden anhand ihrer Prüfsummen abgeglichen",
     "thumbnail_generation_job": "Miniaturansichten generieren",
     "thumbnail_generation_job_description": "Diese Aufgabe erzeugt große, kleine und unscharfe Miniaturansichten für jede einzelne Datei, sowie Miniaturansichten für jede Person",
-    "transcode_policy_description": "Richtlinien, wann ein Video transkodiert werden soll. HDR-Videos werden immer transkodiert (außer wenn die Transkodierung deaktiviert ist).",
     "transcoding_acceleration_api": "Beschleunigungs-API",
     "transcoding_acceleration_api_description": "Die Schnittstelle welche mit dem Gerät interagiert, um die Transkodierung zu beschleunigen. Bei dieser Einstellung handelt es sich um die \"bestmögliche Lösung\": Bei einem Fehler wird auf die Software-Transkodierung zurückgegriffen. Abhängig von der verwendeten Hardware kann VP9 funktionieren oder auch nicht.",
     "transcoding_acceleration_nvenc": "NVENC (NVIDIA-GPU erforderlich)",
@@ -271,7 +292,7 @@
     "transcoding_hardware_acceleration": "Hardware-Beschleunigung",
     "transcoding_hardware_acceleration_description": "Experimentell; viel schneller, aber bei gleicher Bitrate mit geringerer Qualität",
     "transcoding_hardware_decoding": "Hardware-Dekodierung",
-    "transcoding_hardware_decoding_setting_description": "Nur gültig für NVENC, QSV und RKMPP. Ermöglicht eine Ende-zu-Ende-Beschleunigung, anstatt nur die Codierung zu beschleunigen. Dies funktioniert möglicherweise nicht bei allen Videos.",
+    "transcoding_hardware_decoding_setting_description": "Ermöglicht eine Ende-zu-Ende-Beschleunigung, anstatt nur die Codierung zu beschleunigen. Dies funktioniert möglicherweise nicht bei allen Videos.",
     "transcoding_hevc_codec": "HEVC-Codec",
     "transcoding_max_b_frames": "Maximale B-Frames",
     "transcoding_max_b_frames_description": "Höhere Werte verbessern die Komprimierungseffizienz, verlangsamen aber die Kodierung. Ist möglicherweise nicht mit der Hardware-Beschleunigung älterer Geräte kompatibel. 0 deaktiviert die B-Frames, während -1 diesen Wert automatisch setzt.",
@@ -297,8 +318,6 @@
     "transcoding_threads_description": "Höhere Werte führen zu einer schnelleren Codierung, lassen dem Server aber weniger Spielraum für die Verarbeitung anderer Aufgaben, solange dies aktiv ist. Dieser Wert sollte nicht höher sein als die Anzahl der CPU-Kerne. Nutzt die maximale Auslastung, wenn der Wert auf 0 gesetzt ist.",
     "transcoding_tone_mapping": "Farbton-Mapping",
     "transcoding_tone_mapping_description": "Versucht, das Aussehen von HDR-Videos bei der Konvertierung in SDR beizubehalten. Jeder Algorithmus geht unterschiedliche Kompromisse bei Farbe, Details und Helligkeit ein. Hable bewahrt Details, Mobius bewahrt die Farbe und Reinhard bewahrt die Helligkeit.",
-    "transcoding_tone_mapping_npl": "Farbton-Mapping NPL",
-    "transcoding_tone_mapping_npl_description": "Die Farben werden so angepasst, dass sie für einen Bildschirm mit entsprechender Helligkeit normal aussehen. Entgegen der Annahme, dass niedrigere Werte die Helligkeit des Videos erhöhen und umgekehrt, wird die Helligkeit des Bildschirms ausgeglichen. Mit 0 wird dieser Wert automatisch eingestellt.",
     "transcoding_transcode_policy": "Transcodierungsrichtlinie",
     "transcoding_transcode_policy_description": "Richtlinie, wann ein Video transkodiert werden soll. HDR-Videos werden immer transkodiert (außer wenn die Transkodierung deaktiviert ist).",
     "transcoding_two_pass_encoding": "Two-Pass Codierung",
@@ -311,7 +330,8 @@
     "trash_settings": "Papierkorb-Einstellungen",
     "trash_settings_description": "Papierkorb-Einstellungen verwalten",
     "untracked_files": "Unverfolgte Dateien",
-    "untracked_files_description": "Diese Dateien werden nicht von der Application getrackt. Sie können das Ergebnis fehlgeschlagener Verschiebungen, unterbrochener Uploads oder aufgrund eines Fehlers sein",
+    "untracked_files_description": "Diese Dateien werden nicht von der Anwendung getrackt. Sie können das Ergebnis fehlgeschlagener Verschiebungen, unterbrochener Uploads oder aufgrund eines Fehlers sein",
+    "user_cleanup_job": "Benutzer aufräumen",
     "user_delete_delay": "Das Konto und die Dateien von <b>{user}</b> werden in {delay, plural, one {einem Tag} other {# Tagen}} für eine permanente Löschung geplant.",
     "user_delete_delay_settings": "Verzögerung für das Löschen von Benutzern",
     "user_delete_delay_settings_description": "Gibt die Anzahl der Tage bis zur endgültigen Löschung eines Kontos und seiner Dateien an. Der Benutzerlöschauftrag wird täglich um Mitternacht ausgeführt, um zu überprüfen, ob Nutzer zur Löschung bereit sind. Änderungen an dieser Einstellung werden erst bei der nächsten Ausführung berücksichtigt.",
@@ -343,12 +363,12 @@
   "album_added_notification_setting_description": "Erhalte eine E-Mail-Benachrichtigung, wenn du zu einem freigegebenen Album hinzugefügt wurdest",
   "album_cover_updated": "Album-Cover aktualisiert",
   "album_delete_confirmation": "Bist du sicher, dass du das Album {album} löschen willst?",
-  "album_delete_confirmation_description": "Wenn dieses Album geteilt wurde, können andere Benutzer nicht mehr darauf zugreifen.",
+  "album_delete_confirmation_description": "Falls dieses Album geteilt wurde, können andere Benutzer nicht mehr darauf zugreifen.",
   "album_info_updated": "Album-Infos aktualisiert",
   "album_leave": "Album verlassen?",
   "album_leave_confirmation": "Bist du sicher, dass du das Album {album} verlassen willst?",
-  "album_name": "Album Name",
-  "album_options": "Album Optionen",
+  "album_name": "Albumname",
+  "album_options": "Albumoptionen",
   "album_remove_user": "Nutzer entfernen?",
   "album_remove_user_confirmation": "Bist du sicher, dass du {user} entfernen willst?",
   "album_share_no_users": "Es sieht so aus, als hättest du dieses Album mit allen Benutzern geteilt oder du hast keine Benutzer, mit denen du teilen kannst.",
@@ -356,7 +376,7 @@
   "album_updated_setting_description": "Erhalte eine E-Mail-Benachrichtigung, wenn ein freigegebenes Album neue Dateien enthält",
   "album_user_left": "{album} verlassen",
   "album_user_removed": "{user} entfernt",
-  "album_with_link_access": "Lass jeden mit dem Link Fotos und Personen in diesem Album sehen.",
+  "album_with_link_access": "Lass jeden mit dem Link die Fotos und Personen in diesem Album sehen.",
   "albums": "Alben",
   "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Alben}}",
   "all": "Alle",
@@ -378,8 +398,7 @@
   "archive_or_unarchive_photo": "Foto archivieren bzw. Archivierung aufheben",
   "archive_size": "Archivgröße",
   "archive_size_description": "Archivgröße für Downloads konfigurieren (in GiB)",
-  "archived": "Archiviert",
-  "archived_count": "{count, plural, other {# Archiviert}}",
+  "archived_count": "{count, plural, other {# archiviert}}",
   "are_these_the_same_person": "Ist das dieselbe Person?",
   "are_you_sure_to_do_this": "Bist du sicher, dass du das tun willst?",
   "asset_added_to_album": "Zum Album hinzugefügt",
@@ -389,7 +408,7 @@
   "asset_has_unassigned_faces": "Datei hat nicht zugewiesene Gesichter",
   "asset_hashing": "Berechnung des Hashwerts...",
   "asset_offline": "Datei offline",
-  "asset_offline_description": "Diese Datei ist nicht erreichbar. Immich kann nicht auf ihren Speicherort zugreifen. Bitte stelle sicher, dass die Datei verfügbar ist und scanne die Bibliothek erneut.",
+  "asset_offline_description": "Diese externe Datei ist nicht mehr auf dem Datenträger vorhanden. Bitte wende dich an deinen Immich-Administrator, um Hilfe zu erhalten.",
   "asset_skipped": "Übersprungen",
   "asset_skipped_in_trash": "Im Papierkorb",
   "asset_uploaded": "Hochgeladen",
@@ -397,28 +416,28 @@
   "assets": "Dateien",
   "assets_added_count": "{count, plural, one {# Datei} other {# Dateien}} hinzugefügt",
   "assets_added_to_album_count": "{count, plural, one {# Datei} other {# Dateien}} zum Album hinzugefügt",
-  "assets_added_to_name_count": "{count, plural, one {# Element} other {# Elemente}} zu {hasName, select, true {<b>{name}</b>} other {neuen Album}} hinzugefügt",
+  "assets_added_to_name_count": "{count, plural, one {# Element} other {# Elemente}} zu {hasName, select, true {<b>{name}</b>} other {neuem Album}} hinzugefügt",
   "assets_count": "{count, plural, one {# Datei} other {# Dateien}}",
-  "assets_moved_to_trash": "{count, plural, one {# Datei} other {# Dateien}} in den Papierkorb verschoben",
   "assets_moved_to_trash_count": "{count, plural, one {# Datei} other {# Dateien}} in den Papierkorb verschoben",
-  "assets_permanently_deleted_count": "{count, plural, one {# Datei} other {# Dateien}} dauerhaft gelöscht",
+  "assets_permanently_deleted_count": "{count, plural, one {# Datei} other {# Dateien}} endgültig gelöscht",
   "assets_removed_count": "{count, plural, one {# Datei} other {# Dateien}} entfernt",
-  "assets_restore_confirmation": "Bist du sicher, dass du alle Dateien aus dem Papierkorb wiederherstellen willst? Diese Aktion kann nicht rückgängig gemacht werden!",
+  "assets_restore_confirmation": "Bist du sicher, dass du alle Dateien aus dem Papierkorb wiederherstellen willst? Diese Aktion kann nicht rückgängig gemacht werden! Beachte, dass Offline-Dateien auf diese Weise nicht wiederhergestellt werden können.",
   "assets_restored_count": "{count, plural, one {# Datei} other {# Dateien}} wiederhergestellt",
   "assets_trashed_count": "{count, plural, one {# Datei} other {# Dateien}} in den Papierkorb verschoben",
   "assets_were_part_of_album_count": "{count, plural, one {# Datei ist} other {# Dateien sind}} bereits im Album vorhanden",
   "authorized_devices": "Verwendete Geräte",
   "back": "Zurück",
   "back_close_deselect": "Zurück, Schließen oder Abwählen",
-  "backward": "Zurück",
+  "backward": "Rückwärts",
   "birthdate_saved": "Geburtsdatum erfolgreich gespeichert",
   "birthdate_set_description": "Das Geburtsdatum wird verwendet, um das Alter dieser Person zum Zeitpunkt eines Fotos zu berechnen.",
   "blurred_background": "Unscharfer Hintergrund",
+  "bugs_and_feature_requests": "Fehler & Verbesserungsvorschläge",
   "build": "Build",
   "build_image": "Build Abbild",
-  "bulk_delete_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien}} gemeinsam löschen möchtest? Dabei wird die größte Datei jeder Gruppe behalten und alle anderen Duplikate dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden!",
+  "bulk_delete_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien gemeinsam}} löschen möchtest? Dabei wird die größte Datei jeder Gruppe behalten und alle anderen Duplikate endgültig gelöscht. Diese Aktion kann nicht rückgängig gemacht werden!",
   "bulk_keep_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien}} behalten möchtest? Dies wird alle Duplikat-Gruppen auflösen ohne etwas zu löschen.",
-  "bulk_trash_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien}} gemeinsam in den Papierkorb verschieben möchtest? Dies wird die größte Datei jeder Gruppe behalten und alle anderen Duplikate in den Papierkorb verschieben.",
+  "bulk_trash_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien gemeinsam}} in den Papierkorb verschieben möchtest? Dies wird die größte Datei jeder Gruppe behalten und alle anderen Duplikate in den Papierkorb verschieben.",
   "buy": "Immich erwerben",
   "camera": "Kamera",
   "camera_brand": "Kamera-Marke",
@@ -428,16 +447,12 @@
   "cannot_merge_people": "Personen können nicht zusammengeführt werden",
   "cannot_undo_this_action": "Diese Aktion kann nicht rückgängig gemacht werden!",
   "cannot_update_the_description": "Beschreibung kann nicht aktualisiert werden",
-  "cant_apply_changes": "Änderungen können nicht übernommen werden",
-  "cant_get_faces": "Es konnten keine Gesichter festgestellt werden",
-  "cant_search_people": "Es konnte nicht nach Personen gesucht werden",
-  "cant_search_places": "Es konnte nicht nach Orten gesucht werden",
   "change_date": "Datum ändern",
   "change_expiration_time": "Verfallszeitpunkt ändern",
   "change_location": "Ort ändern",
   "change_name": "Name ändern",
   "change_name_successfully": "Name wurde erfolgreich geändert",
-  "change_password": "Passwort Ändern",
+  "change_password": "Passwort ändern",
   "change_password_description": "Dies ist entweder das erste Mal, dass du dich im System anmeldest, oder es wurde eine Anfrage zur Änderung deines Passworts gestellt. Bitte gib unten dein neues Passwort ein.",
   "change_your_password": "Ändere dein Passwort",
   "changed_visibility_successfully": "Die Sichtbarkeit wurde erfolgreich geändert",
@@ -453,22 +468,23 @@
   "clockwise": "Im Uhrzeigersinn",
   "close": "Schließen",
   "collapse": "Zusammenklappen",
-  "collapse_all": "Alles aufklappen",
+  "collapse_all": "Alle zusammenklappen",
   "color": "Farbe",
   "color_theme": "Farb-Theme",
   "comment_deleted": "Kommentar gelöscht",
-  "comment_options": "Kommentar-Optionen",
+  "comment_options": "Kommentaroptionen",
   "comments_and_likes": "Kommentare & Likes",
   "comments_are_disabled": "Kommentare sind deaktiviert",
   "confirm": "Bestätigen",
   "confirm_admin_password": "Administrator Passwort bestätigen",
   "confirm_delete_shared_link": "Bist du sicher, dass du diesen geteilten Link löschen willst?",
+  "confirm_keep_this_delete_others": "Alle anderen Dateien im Stapel bis auf diese werden gelöscht. Bist du sicher, dass du fortfahren möchten?",
   "confirm_password": "Passwort bestätigen",
   "contain": "Vollständig",
   "context": "Kontext",
   "continue": "Fortsetzen",
   "copied_image_to_clipboard": "Das Bild wurde in die Zwischenablage kopiert.",
-  "copied_to_clipboard": "In Zwischenablage kopiert!",
+  "copied_to_clipboard": "In die Zwischenablage kopiert!",
   "copy_error": "Kopier-Fehler",
   "copy_file_path": "Dateipfad kopieren",
   "copy_image": "Bild kopieren",
@@ -489,7 +505,7 @@
   "create_new_person_hint": "Ausgewählte Dateien einer neuen Person zuweisen",
   "create_new_user": "Neuen Nutzer erstellen",
   "create_tag": "Tag erstellen",
-  "create_tag_description": "Erstelle einen neuen Tag. Für verschachtelte Tags, gib den gesamten Pfad inklusive Slash an.",
+  "create_tag_description": "Erstelle einen neuen Tag. Für verschachtelte Tags, gib den gesamten Pfad inklusive Schrägstrich an.",
   "create_user": "Nutzer erstellen",
   "created": "Erstellt",
   "current_device": "Aktuelles Gerät",
@@ -508,47 +524,44 @@
   "delete": "Löschen",
   "delete_album": "Album löschen",
   "delete_api_key_prompt": "Bist du sicher, dass du diesen API-Schlüssel löschen willst?",
-  "delete_duplicates_confirmation": "Bist du sicher, dass du diese Duplikate dauerhaft löschen willst?",
+  "delete_duplicates_confirmation": "Bist du sicher, dass du diese Duplikate endgültig löschen willst?",
   "delete_key": "Schlüssel löschen",
   "delete_library": "Bibliothek löschen",
   "delete_link": "Link löschen",
+  "delete_others": "Andere löschen",
   "delete_shared_link": "geteilten Link löschen",
   "delete_tag": "Tag löschen",
   "delete_tag_confirmation_prompt": "Bist du sicher, dass der Tag {tagName} gelöscht werden soll?",
   "delete_user": "Nutzer löschen",
   "deleted_shared_link": "Geteilten Link gelöscht",
+  "deletes_missing_assets": "Löscht Dateien, die auf der Festplatte fehlen",
   "description": "Beschreibung",
   "details": "Details",
   "direction": "Richtung",
   "disabled": "Deaktiviert",
   "disallow_edits": "Bearbeitungen verbieten",
+  "discord": "Discord",
   "discover": "Entdecken",
   "dismiss_all_errors": "Alle Fehler ignorieren",
   "dismiss_error": "Fehler ignorieren",
   "display_options": "Anzeigeoptionen",
   "display_order": "Anzeigereihenfolge",
   "display_original_photos": "Originale Fotos anzeigen",
-  "display_original_photos_setting_description": "Bei der Anzeige eines Bildes wird bevorzugt das Originalfoto statt der Miniaturansicht angezeigt, sofern das Original webkompatibel ist. Dies kann zu einer langsameren Ladezeit der Fotos führen.",
+  "display_original_photos_setting_description": "Bei der Anzeige eines Bildes wird bevorzugt das Originalfoto statt der Miniaturansicht angezeigt, sofern das Original webkompatibel ist. Dies kann zu einer längeren Ladezeit der Fotos führen.",
   "do_not_show_again": "Diese Nachricht nicht erneut anzeigen",
-  "done": "Erledigt",
-  "download": "Download",
+  "documentation": "Dokumentation",
+  "done": "Fertig",
+  "download": "Herunterladen",
   "download_include_embedded_motion_videos": "Eingebettete Videos",
   "download_include_embedded_motion_videos_description": "Videos, die in Bewegungsfotos eingebettet sind, als separate Datei einfügen",
   "download_settings": "Download",
-  "download_settings_description": "Einstellungen für den Dateidownload verwalten",
-  "downloading": "Downloaden",
+  "download_settings_description": "Einstellungen für das Herunterladen von Dateien verwalten",
+  "downloading": "Herunterladen",
   "downloading_asset_filename": "Datei {filename} wird heruntergeladen",
   "drop_files_to_upload": "Lade Dateien hoch, indem du sie hierhin ziehst",
   "duplicates": "Duplikate",
   "duplicates_description": "Löse jede Gruppe auf, indem du angibst, welche, wenn überhaupt, Duplikate sind",
   "duration": "Dauer",
-  "durations": {
-    "days": "{days, plural, one {Tag} other {{days, number} Tage}}",
-    "hours": "{hours, plural, one {eine Stunde} other {{hours, number} Stunden}}",
-    "minutes": "{minutes, plural, one {eine minute} other {{minutes, number} minuten}}",
-    "months": "{months, plural, one {ein Monat} other {{months, number} Monate}}",
-    "years": "{years, plural, one {ein Jahr} other {{years, number} Jahre}}"
-  },
   "edit": "Bearbeiten",
   "edit_album": "Album bearbeiten",
   "edit_avatar": "Avatar bearbeiten",
@@ -568,15 +581,13 @@
   "edit_user": "Nutzer bearbeiten",
   "edited": "Bearbeitet",
   "editor": "Bearbeiter",
-  "editor_close_without_save_prompt": "Diese Änderungen werden nicht gespeichert",
+  "editor_close_without_save_prompt": "Die Änderungen werden nicht gespeichert",
   "editor_close_without_save_title": "Editor schließen?",
   "editor_crop_tool_h2_aspect_ratios": "Seitenverhältnisse",
-  "editor_crop_tool_h2_rotation": "Rotation",
+  "editor_crop_tool_h2_rotation": "Drehung",
   "email": "E-Mail",
-  "empty": "Leer",
-  "empty_album": "Leeres Album",
   "empty_trash": "Papierkorb leeren",
-  "empty_trash_confirmation": "Bist du sicher, dass du den Papierkorb leeren willst?\nDies entfernt alle Dateien im Papierkorb permanent aus Immich und kann nicht rückgängig gemacht werden!",
+  "empty_trash_confirmation": "Bist du sicher, dass du den Papierkorb leeren willst?\nDies entfernt alle Dateien im Papierkorb endgültig aus Immich und kann nicht rückgängig gemacht werden!",
   "enable": "Aktivieren",
   "enabled": "Aktiviert",
   "end_date": "Enddatum",
@@ -608,6 +619,7 @@
     "failed_to_create_shared_link": "Geteilter Link konnte nicht erstellt werden",
     "failed_to_edit_shared_link": "Geteilter Link konnte nicht bearbeitet werden",
     "failed_to_get_people": "Personen konnten nicht abgerufen werden",
+    "failed_to_keep_this_delete_others": "Fehler beim Löschen der anderen Dateien",
     "failed_to_load_asset": "Fehler beim Laden der Datei",
     "failed_to_load_assets": "Fehler beim Laden der Dateien",
     "failed_to_load_people": "Fehler beim Laden von Personen",
@@ -618,7 +630,7 @@
     "incorrect_email_or_password": "Ungültige E-Mail oder Passwort",
     "paths_validation_failed": "{paths, plural, one {# Pfad konnte} other {# Pfade konnten}} nicht validiert werden",
     "profile_picture_transparent_pixels": "Profilbilder dürfen keine transparenten Pixel haben. Bitte zoome heran und/oder verschiebe das Bild.",
-    "quota_higher_than_disk_size": "Dein festgelegtes Kontingent ist grösser als der verfügbare Speicher",
+    "quota_higher_than_disk_size": "Dein festgelegtes Kontingent ist größer als der verfügbare Speicher",
     "repair_unable_to_check_items": "{count, select, one {Eintrag konnte} other {Einträge konnten}} nicht überprüft werden",
     "unable_to_add_album_users": "Benutzer konnten nicht zum Album hinzugefügt werden",
     "unable_to_add_assets_to_shared_link": "Datei konnte nicht zum geteilten Link hinzugefügt werden",
@@ -635,12 +647,10 @@
     "unable_to_change_location": "Ort kann nicht verändert werden",
     "unable_to_change_password": "Passwort konnte nicht geändert werden",
     "unable_to_change_visibility": "Sichtbarkeit von {count, plural, one {einer Person} other {# Personen}} konnte nicht geändert werden",
-    "unable_to_check_item": "Objekt kann nicht überprüft werden",
-    "unable_to_check_items": "Objekte konnten nicht überprüft werden",
     "unable_to_complete_oauth_login": "OAuth-Anmeldung konnte nicht abgeschlossen werden",
     "unable_to_connect": "Verbindung konnte nicht hergestellt werden",
     "unable_to_connect_to_server": "Verbindung zum Server konnte nicht hergestellt werden",
-    "unable_to_copy_to_clipboard": "Konnte nicht in die Zwischenablage kopieren, stelle sicher, dass du per https auf die Seite zugreiffst",
+    "unable_to_copy_to_clipboard": "Konnte nicht in die Zwischenablage kopieren, stelle sicher, dass du per https auf die Seite zugreifst",
     "unable_to_create_admin_account": "Administratorkonto konnte nicht erstellt werden",
     "unable_to_create_api_key": "Es konnte kein API-Schlüssel erstellt werden",
     "unable_to_create_library": "Bibliothek konnte nicht erstellt werden",
@@ -661,7 +671,7 @@
     "unable_to_get_comments_number": "Anzahl der Kommentare konnte nicht abgerufen werden",
     "unable_to_get_shared_link": "Fehler beim Abrufen des Freigabelinks",
     "unable_to_hide_person": "Person kann nicht versteckt werden",
-    "unable_to_link_motion_video": "Bewegungsvideo kann nicht verlinkt werden",
+    "unable_to_link_motion_video": "Bewegungsvideo kann nicht verknüpft werden",
     "unable_to_link_oauth_account": "OAuth-Konto kann nicht verknüpft werden",
     "unable_to_load_album": "Album kann nicht geladen werden",
     "unable_to_load_asset_activity": "Foto-Aktivität konnte nicht geladen werden",
@@ -677,12 +687,10 @@
     "unable_to_remove_album_users": "Mitglieder der Alben können nicht entfernt werden",
     "unable_to_remove_api_key": "API-Schlüssel konnte nicht entfernt werden",
     "unable_to_remove_assets_from_shared_link": "Dateien konnten nicht von geteiltem Link entfernt werden",
-    "unable_to_remove_comment": "Kommentar kann nicht entfernt werden",
+    "unable_to_remove_deleted_assets": "Offline-Dateien konnten nicht entfernt werden",
     "unable_to_remove_library": "Bibliothek kann nicht entfernt werden",
-    "unable_to_remove_offline_files": "Offline-Dateien konnten nicht entfernt werden",
     "unable_to_remove_partner": "Partner kann nicht entfernt werden",
     "unable_to_remove_reaction": "Reaktion kann nicht entfernt werden",
-    "unable_to_remove_user": "Benutzer kann nicht entfernt werden",
     "unable_to_repair_items": "Objekte können nicht repariert werden",
     "unable_to_reset_password": "Passwort kann nicht zurückgesetzt werden",
     "unable_to_resolve_duplicate": "Duplikate können nicht aufgelöst werden",
@@ -699,10 +707,10 @@
     "unable_to_scan_library": "Bibliothek konnte nicht gescannt werden",
     "unable_to_set_feature_photo": "Hauptfoto konnte nicht festgelegt werden",
     "unable_to_set_profile_picture": "Profilbild konnte nicht gesetzt werden",
-    "unable_to_submit_job": "Auftrag konnte nicht übermittelt werden",
+    "unable_to_submit_job": "Aufgabe konnte nicht eingereicht werden",
     "unable_to_trash_asset": "Objekte konnten nicht gelöscht werden",
     "unable_to_unlink_account": "Die Verknüpfung des Kontos kann nicht aufgehoben werden",
-    "unable_to_unlink_motion_video": "Verlinkung zum Bewegungsvideo kann nicht aufgehoben werden",
+    "unable_to_unlink_motion_video": "Verknüpfung zum Bewegungsvideo kann nicht aufgehoben werden",
     "unable_to_update_album_cover": "Album-Cover konnte nicht aktualisiert werden",
     "unable_to_update_album_info": "Album-Info konnte nicht aktualisiert werden",
     "unable_to_update_library": "Die Bibliothek konnte nicht aktualisiert werden",
@@ -712,16 +720,12 @@
     "unable_to_update_user": "Der Nutzer konnte nicht aktualisiert werden",
     "unable_to_upload_file": "Datei konnte nicht hochgeladen werden"
   },
-  "every_day_at_onepm": "Täglich 13.00 Uhr",
-  "every_night_at_midnight": "Täglich um Mitternacht",
-  "every_night_at_twoam": "Jede Nacht um 2.00 Uhr",
-  "every_six_hours": "Alle 6 Stunden",
   "exif": "EXIF",
   "exit_slideshow": "Diashow beenden",
-  "expand_all": "Alle erweitern",
+  "expand_all": "Alle aufklappen",
   "expire_after": "Verfällt nach",
   "expired": "Verfallen",
-  "expires_date": "Läuft am {date} ab",
+  "expires_date": "Läuft {date} ab",
   "explore": "Erkunden",
   "explorer": "Datei-Explorer",
   "export": "Exportieren",
@@ -730,33 +734,28 @@
   "external": "Extern",
   "external_libraries": "Externe Bibliotheken",
   "face_unassigned": "Nicht zugewiesen",
-  "failed_to_get_people": "Personen konnten nicht ermittelt werden",
+  "failed_to_load_assets": "Laden der Assets fehlgeschlagen",
   "favorite": "Favorit",
   "favorite_or_unfavorite_photo": "Favorisiertes oder nicht favorisiertes Foto",
   "favorites": "Favoriten",
-  "feature": "Funktion",
   "feature_photo_updated": "Profilbild aktualisiert",
-  "featurecollection": "Funktionssammlung",
   "features": "Funktionen",
   "features_setting_description": "Funktionen der App verwalten",
   "file_name": "Dateiname",
   "file_name_or_extension": "Dateiname oder -erweiterung",
   "filename": "Dateiname",
-  "files": "",
   "filetype": "Dateityp",
   "filter_people": "Personen filtern",
   "find_them_fast": "Finde sie schneller mit der Suche nach Namen",
   "fix_incorrect_match": "Fehlerhafte Übereinstimmung beheben",
   "folders": "Ordner",
   "folders_feature_description": "Durchsuchen der Ordneransicht für Fotos und Videos im Dateisystem",
-  "force_re-scan_library_files": "Erzwingen des erneuten Scannens aller Bibliotheksdateien",
-  "forward": "Weiterleiten",
+  "forward": "Vorwärts",
   "general": "Allgemein",
-  "get_help": "Erhalte Hilfe",
+  "get_help": "Hilfe erhalten",
   "getting_started": "Erste Schritte",
   "go_back": "Zurück",
   "go_to_search": "Zur Suche gehen",
-  "go_to_share_page": "Zur Freigabeseite gehen",
   "group_albums_by": "Alben gruppieren nach...",
   "group_no": "Keine Gruppierung",
   "group_owner": "Gruppierung nach Besitzer",
@@ -782,12 +781,8 @@
   "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen in {city}, {country} mit {person1} und {person2} am {date}",
   "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen in {city}, {country} mit {person1}, {person2}, und {person3} am {date}",
   "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen in {city}, {country} mit {person1}, {person2}, und {additionalCount, number} anderen am {date}",
-  "image_alt_text_people": "{count, plural, =1 {mit {person1}} =2 {mit {person1} und {person2}} =3 {mit {person1}, {person2} und {person3}} other {mit {person1}, {person2} und {others, number} anderen}}",
-  "image_alt_text_place": "in {city}, {country}",
-  "image_taken": "{isVideo, select, true {Video aufgenommen} other {Bild aufgenommen}}",
-  "img": "Img",
   "immich_logo": "Immich-Logo",
-  "immich_web_interface": "Immich Webschnittstelle",
+  "immich_web_interface": "Immich-Web-Oberfläche",
   "import_from_json": "Aus JSON importieren",
   "import_path": "Importpfad",
   "in_albums": "In {count, plural, one {# Album} other {# Alben}}",
@@ -798,18 +793,19 @@
   "individual_share": "Individuelle Freigabe",
   "info": "Info",
   "interval": {
-    "day_at_onepm": "Täglich 13.00 Uhr",
+    "day_at_onepm": "Täglich um 13:00 Uhr",
     "hours": "{hours, plural, one {Jede Stunde} other {Alle {hours, number} Stunden}}",
     "night_at_midnight": "Täglich um Mitternacht",
-    "night_at_twoam": "Täglich Nachts um 2.00 Uhr"
+    "night_at_twoam": "Täglich nachts um 2:00 Uhr"
   },
   "invite_people": "Personen einladen",
   "invite_to_album": "Zum Album einladen",
   "items_count": "{count, plural, one {# Eintrag} other {# Einträge}}",
-  "job_settings_description": "Parallelität von Jobs verwalten",
   "jobs": "Aufgaben",
   "keep": "Behalten",
   "keep_all": "Alle behalten",
+  "keep_this_delete_others": "Dieses behalten, andere löschen",
+  "kept_this_deleted_others": "Diese Datei behalten und {count, plural, one {# Datei} other {# Dateien}} gelöscht",
   "keyboard_shortcuts": "Tastenkürzel",
   "language": "Sprache",
   "language_setting_description": "Wähle deine bevorzugte Sprache",
@@ -821,34 +817,9 @@
   "level": "Level",
   "library": "Bibliothek",
   "library_options": "Bibliotheksoptionen",
-  "license_account_info": "Dein Account ist lizensiert",
-  "license_activated_subtitle": "Wir danken dir für die Unterstützung von Immich und Open-Source-Software",
-  "license_activated_title": "Deine Lizenz wurde erfolgreich aktiviert",
-  "license_button_activate": "Aktivieren",
-  "license_button_buy": "Kaufen",
-  "license_button_buy_license": "Lizenz erwerben",
-  "license_button_select": "Auswählen",
-  "license_failed_activation": "Die Aktivierung der Lizenz ist fehlgeschlagen. Bitte überprüfe deine E-Mail, um den korrekten Lizenzschlüssel zu finden!",
-  "license_individual_description_1": "1 Lizenz pro Benutzer auf einem beliebigen Server",
-  "license_individual_title": "Individuelle Lizenz",
-  "license_info_licensed": "Lizensiert",
-  "license_info_unlicensed": "Unlizensiert",
-  "license_input_suggestion": "Hast du bereits eine Lizenz? Gib den Key unten ein",
-  "license_license_subtitle": "Erwerbe eine Lizenz zur Unterstützung von Immich",
-  "license_license_title": "LIZENZ",
-  "license_lifetime_description": "Lebenslange Lizenz",
-  "license_per_server": "Pro Server",
-  "license_per_user": "Pro Nutzer",
-  "license_server_description_1": "1 Lizenz pro Server",
-  "license_server_description_2": "Lizenz für alle Nutzer des Servers",
-  "license_server_title": "Serverlizenz",
-  "license_trial_info_1": "Du verwendest eine unlizenzierte Version von Immich",
-  "license_trial_info_2": "Du benutzt Immich seit ungefähr",
-  "license_trial_info_3": "{accountAge, plural, one {# Tag} other {# Tage}}",
-  "license_trial_info_4": "Bitte erwäge den Kauf einer Lizenz, um die kontinuierliche Weiterentwicklung des Dienstes zu unterstützen",
   "light": "Hell",
   "like_deleted": "Like gelöscht",
-  "link_motion_video": "Link Bewegungsvideo",
+  "link_motion_video": "Bewegungsvideo verknüpfen",
   "link_options": "Link-Optionen",
   "link_to_oauth": "Link zu OAuth",
   "linked_oauth_account": "Verknüpftes OAuth-Konto",
@@ -867,6 +838,7 @@
   "look": "Erscheinungsbild",
   "loop_videos": "Loop-Videos",
   "loop_videos_description": "Aktiviere diese Option, um eine automatische Videoschleife in der Detailansicht zu erstellen.",
+  "main_branch_warning": "Du benutzt eine Entwicklungsversion. Wir empfehlen dringend, eine Release-Version zu verwenden!",
   "make": "Marke",
   "manage_shared_links": "Freigegebene Links verwalten",
   "manage_sharing_with_partners": "Gemeinsame Nutzung mit Partnern verwalten",
@@ -876,8 +848,8 @@
   "manage_your_devices": "Deine eingeloggten Geräte verwalten",
   "manage_your_oauth_connection": "Deine OAuth-Verbindung verwalten",
   "map": "Karte",
-  "map_marker_for_images": "Kartemarkierung für Bilder, die in {city}, {country} aufgenommen wurden",
-  "map_marker_with_image": "Kartenmarker mit Bild",
+  "map_marker_for_images": "Kartenmarkierung für Bilder, die in {city}, {country} aufgenommen wurden",
+  "map_marker_with_image": "Kartenmarkierung mit Bild",
   "map_settings": "Karteneinstellungen",
   "matches": "Treffer",
   "media_type": "Medientyp",
@@ -918,9 +890,9 @@
   "no_albums_yet": "Es sieht so aus, als hättest du noch keine Alben.",
   "no_archived_assets_message": "Archiviere Fotos und Videos, um sie aus deiner Fotoansicht zu entfernen",
   "no_assets_message": "KLICKE, UM DEIN ERSTES FOTO HOCHZULADEN",
-  "no_duplicates_found": "Keine Duplikate wurden gefunden.",
-  "no_exif_info_available": "Keine Exif-Informationen vorhanden",
-  "no_explore_results_message": "Lade weitere Fotos hoch, um deine Sammlung zu vergrößern.",
+  "no_duplicates_found": "Es wurden keine Duplikate gefunden.",
+  "no_exif_info_available": "Keine EXIF-Informationen vorhanden",
+  "no_explore_results_message": "Lade weitere Fotos hoch, um deine Sammlung zu erkunden.",
   "no_favorites_message": "Füge Favoriten hinzu, um deine besten Bilder und Videos schnell zu finden",
   "no_libraries_message": "Eine externe Bibliothek erstellen, um deine Fotos und Videos anzusehen",
   "no_name": "Kein Name",
@@ -929,13 +901,14 @@
   "no_results_description": "Versuche es mit einem Synonym oder einem allgemeineren Stichwort",
   "no_shared_albums_message": "Erstelle ein Album, um Fotos und Videos mit Personen in deinem Netzwerk zu teilen",
   "not_in_any_album": "In keinem Album",
-  "note_apply_storage_label_to_previously_uploaded assets": "Hinweis: Um ein Storage-Label zu verwenden, starte den",
+  "note_apply_storage_label_to_previously_uploaded assets": "Hinweis: Um eine Speicherpfadbezeichnung anzuwenden, starte den",
   "note_unlimited_quota": "Hinweis: Verwende 0 für ein unlimitiertes Kontingent",
   "notes": "Notizen",
   "notification_toggle_setting_description": "E-Mail-Benachrichtigungen aktivieren",
   "notifications": "Benachrichtigungen",
   "notifications_setting_description": "Benachrichtigungen verwalten",
   "oauth": "OAuth",
+  "official_immich_resources": "Offizielle Immich Quellen",
   "offline": "Offline",
   "offline_paths": "Offline-Pfade",
   "offline_paths_description": "Diese Ergebnisse können auf das manuelle Löschen von Dateien zurückzuführen sein, die nicht Teil einer externen Bibliothek sind.",
@@ -948,7 +921,6 @@
   "onboarding_welcome_user": "Willkommen, {user}",
   "online": "Online",
   "only_favorites": "Nur Favoriten",
-  "only_refreshes_modified_files": "Nur geänderte Dateien aktualisieren",
   "open_in_map_view": "In Kartenansicht öffnen",
   "open_in_openstreetmap": "In OpenStreetMap öffnen",
   "open_the_search_filters": "Die Suchfilter öffnen",
@@ -957,7 +929,7 @@
   "organize_your_library": "Organisiere deine Bibliothek",
   "original": "Original",
   "other": "Sonstiges",
-  "other_devices": "Sonstige Geräte",
+  "other_devices": "Andere Geräte",
   "other_variables": "Sonstige Variablen",
   "owned": "Eigenes",
   "owner": "Besitzer",
@@ -984,17 +956,15 @@
   "pending": "Ausstehend",
   "people": "Personen",
   "people_edits_count": "{count, plural, one {# Person} other {# Personen}} bearbeitet",
-  "people_feature_description": "Durchsuchen von Fotos und Videos nach Personen gruppiert",
+  "people_feature_description": "Fotos und Videos nach Personen gruppiert durchsuchen",
   "people_sidebar_description": "Eine Verknüpfung zu Personen in der Seitenleiste anzeigen",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "Warnung vor endgültiger Löschung",
-  "permanent_deletion_warning_setting_description": "Anzeige einer Warnung beim permanenten Löschen von Objekten",
-  "permanently_delete": "Dauerhaft löschen",
-  "permanently_delete_assets_count": "{count, plural, one {Datei} other {Dateien}} dauerhaft gelöscht",
-  "permanently_delete_assets_prompt": "Bist du sicher, dass {count, plural, one {diese Datei} other {diese <b>#</b> Dateien}} dauerhaft gelöscht werden soll? Dadurch werden diese auch aus deinen Alben entfernt.",
-  "permanently_deleted_asset": "Dauerhaft gelöschtes Objekt",
-  "permanently_deleted_assets": "{count, plural, one {# Objekt} other {# Objekte}} dauerhaft gelöscht",
-  "permanently_deleted_assets_count": "{count, plural, one {# Datei} other {# Dateien}} dauerhaft gelöscht",
+  "permanent_deletion_warning_setting_description": "Anzeige einer Warnung beim endgültigen Löschen von Objekten",
+  "permanently_delete": "Endgültig löschen",
+  "permanently_delete_assets_count": "{count, plural, one {Datei} other {Dateien}} endgültig löschen",
+  "permanently_delete_assets_prompt": "Bist du sicher, dass {count, plural, one {diese Datei} other {diese <b>#</b> Dateien}} endgültig gelöscht werden soll? Dadurch {count, plural, one {wird} other {werden}} diese auch aus deinen Alben entfernt.",
+  "permanently_deleted_asset": "Endgültig gelöschtes Objekt",
+  "permanently_deleted_assets_count": "{count, plural, one {# Datei} other {# Dateien}} endgültig gelöscht",
   "person": "Person",
   "person_hidden": "{name}{hidden, select, true { (verborgen)} other {}}",
   "photo_shared_all_users": "Es sieht so aus, als hättest du deine Fotos mit allen Benutzern geteilt oder du hast keine Benutzer, mit denen du teilen kannst.",
@@ -1002,14 +972,13 @@
   "photos_and_videos": "Fotos & Videos",
   "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}",
   "photos_from_previous_years": "Fotos von vorherigen Jahren",
-  "pick_a_location": "Wählen einen Ort",
+  "pick_a_location": "Wähle einen Ort",
   "place": "Ort",
   "places": "Orte",
   "play": "Abspielen",
   "play_memories": "Erinnerungen abspielen",
   "play_motion_photo": "Bewegte Bilder abspielen",
-  "play_or_pause_video": "Video Abspielen oder Pausieren",
-  "point": "Hinweis",
+  "play_or_pause_video": "Video abspielen oder pausieren",
   "port": "Port",
   "preset": "Voreinstellung",
   "preview": "Vorschau",
@@ -1021,7 +990,7 @@
   "profile_image_of_user": "Profilbild von {user}",
   "profile_picture_set": "Profilbild gesetzt.",
   "public_album": "Öffentliches Album",
-  "public_share": "Öffentliche Teilung",
+  "public_share": "Öffentliche Freigabe",
   "purchase_account_info": "Unterstützer",
   "purchase_activated_subtitle": "Danke für die Unterstützung von Immich und Open-Source Software",
   "purchase_activated_time": "Aktiviert am {date, date}",
@@ -1038,43 +1007,44 @@
   "purchase_individual_description_2": "Unterstützerstatus",
   "purchase_individual_title": "Einzelperson",
   "purchase_input_suggestion": "Besitzen Sie bereits einen Produktschlüssel? Bitte geben Sie diesen unten ein",
-  "purchase_license_subtitle": "Kaufe Immich um eine fortlaufende Entwicklung zu unterstützen",
+  "purchase_license_subtitle": "Kaufe Immich, um die fortlaufende Entwicklung zu unterstützen",
   "purchase_lifetime_description": "Lebenslange Gültigkeit",
-  "purchase_option_title": "KAUF OPTIONEN",
-  "purchase_panel_info_1": "Die Entwicklung von Immich erfordert viel Zeit und Mühe, und wir haben Vollzeit- Entwickler, die so gut wie möglich daran arbeiten. Unser Ziel ist es, dass Open-Source-Software und moralische Geschäftsmethoden zu einer nachhaltigen Einkommensquelle für Entwickler werden und ein datenschutzfreundliches Ökosystem mit echten Alternativen zu ausbeuterischen Cloud-Diensten geschaffen wird.",
-  "purchase_panel_info_2": "Weil wir davon überzeugt sind keine Paywalls zu haben, wird dieser Kauf keine zusätzlichen Funktionen in Immich freischalten. Wir verlassen uns auf Nutzende wie dich, um Entwicklung von Immich zu unterstützen.",
+  "purchase_option_title": "KAUFOPTIONEN",
+  "purchase_panel_info_1": "Die Entwicklung von Immich erfordert viel Zeit und Mühe, und wir haben Vollzeit-Entwickler, die daran arbeiten es möglichst perfekt zu machen. Unser Ziel ist es, dass Open-Source-Software und moralische Geschäftsmethoden zu einer nachhaltigen Einkommensquelle für Entwickler werden und ein datenschutzfreundliches Ökosystem mit echten Alternativen zu ausbeuterischen Cloud-Diensten geschaffen wird.",
+  "purchase_panel_info_2": "Weil wir davon überzeugt sind keine Paywalls zu haben, wird dieser Kauf keine zusätzlichen Funktionen in Immich freischalten. Wir verlassen uns auf Nutzende wie dich, um die Entwicklung von Immich zu unterstützen.",
   "purchase_panel_title": "Das Projekt unterstützen",
   "purchase_per_server": "Pro Server",
   "purchase_per_user": "Pro Benutzer",
   "purchase_remove_product_key": "Produktschlüssel entfernen",
   "purchase_remove_product_key_prompt": "Sicher, dass der Produktschlüssel entfernt werden soll?",
-  "purchase_remove_server_product_key": "Server Produktschlüssel entfernen",
-  "purchase_remove_server_product_key_prompt": "Sicher, dass der Server Produktschlüssel entfernt werden soll?",
+  "purchase_remove_server_product_key": "Server-Produktschlüssel entfernen",
+  "purchase_remove_server_product_key_prompt": "Sicher, dass der Server-Produktschlüssel entfernt werden soll?",
   "purchase_server_description_1": "Für den gesamten Server",
   "purchase_server_description_2": "Unterstützerstatus",
   "purchase_server_title": "Server",
-  "purchase_settings_server_activated": "Der Server Produktschlüssel wird durch den Administrator verwaltet",
-  "range": "Reichweite",
+  "purchase_settings_server_activated": "Der Server-Produktschlüssel wird durch den Administrator verwaltet",
   "rating": "Bewertung",
   "rating_clear": "Bewertung löschen",
   "rating_count": "{count, plural, one {# Stern} other {# Sterne}}",
   "rating_description": "Stellt die EXIF-Bewertung im Informationsbereich dar",
-  "raw": "RAW",
   "reaction_options": "Reaktionsmöglichkeiten",
   "read_changelog": "Changelog lesen",
   "reassign": "Neu zuweisen",
-  "reassigned_assets_to_existing_person": "{count, plural, one {# Datei} other {# Dateien}} wurden {name, select, null {einer vorhandenen Person} other {{name}}} zugewiesen",
-  "reassigned_assets_to_new_person": "{count, plural, one {# Datei} other {# Dateien}} wurden einer neuen Person zugewiesen",
+  "reassigned_assets_to_existing_person": "{count, plural, one {# Datei wurde} other {# Dateien wurden}} {name, select, null {einer vorhandenen Person} other {{name}}} zugewiesen",
+  "reassigned_assets_to_new_person": "{count, plural, one {# Datei wurde} other {# Dateien wurden}} einer neuen Person zugewiesen",
   "reassing_hint": "Markierte Dateien einer vorhandenen Person zuweisen",
   "recent": "Neuste",
+  "recent-albums": "Neuste Alben",
   "recent_searches": "Letzte Suchen",
   "refresh": "Aktualisieren",
-  "refresh_encoded_videos": "Codierte Videos aktualisieren",
+  "refresh_encoded_videos": "Kodierte Videos aktualisieren",
+  "refresh_faces": "Gesichter aktualisieren",
   "refresh_metadata": "Metadaten aktualisieren",
   "refresh_thumbnails": "Miniaturansichten aktualisieren",
   "refreshed": "Aktualisiert",
-  "refreshes_every_file": "Jede Datei aktualisieren",
-  "refreshing_encoded_video": "Codierte Videos werden aktualisiert",
+  "refreshes_every_file": "Alle bestehenden und neuen Dateien erneut einlesen",
+  "refreshing_encoded_video": "Kodierte Videos werden aktualisiert",
+  "refreshing_faces": "Gesichter werden aktualisiert",
   "refreshing_metadata": "Metadaten werden aktualisiert",
   "regenerating_thumbnails": "Miniaturansichten werden neu erstellt",
   "remove": "Entfernen",
@@ -1082,15 +1052,16 @@
   "remove_assets_shared_link_confirmation": "Bist du sicher, dass du {count, plural, one {# Datei} other {# Dateien}} von diesem geteilten Link entfernen willst?",
   "remove_assets_title": "Dateien entfernen?",
   "remove_custom_date_range": "Benutzerdefinierten Datumsbereich entfernen",
+  "remove_deleted_assets": "Offline-Dateien entfernen",
   "remove_from_album": "Aus Album entfernen",
   "remove_from_favorites": "Aus Favoriten entfernen",
-  "remove_from_shared_link": "Aus geteilten Link entfernen",
-  "remove_offline_files": "Offline-Dateien entfernen",
+  "remove_from_shared_link": "Aus geteiltem Link entfernen",
+  "remove_url": "URL entfernen",
   "remove_user": "Nutzer entfernen",
   "removed_api_key": "API-Schlüssel {name} wurde entfernt",
   "removed_from_archive": "Aus dem Archiv entfernt",
-  "removed_from_favorites": "Von Favoriten entfernt",
-  "removed_from_favorites_count": "{count, plural, other {#}} von Favoriten entfernt",
+  "removed_from_favorites": "Aus den Favoriten entfernt",
+  "removed_from_favorites_count": "{count, plural, other {#}} aus den Favoriten entfernt",
   "removed_tagged_assets": "Tag von {count, plural, one {# Datei} other {# Dateien}} entfernt",
   "rename": "Umbenennen",
   "repair": "Reparatur",
@@ -1102,7 +1073,6 @@
   "reset": "Zurücksetzen",
   "reset_password": "Passwort zurücksetzen",
   "reset_people_visibility": "Sichtbarkeit von Personen zurücksetzen",
-  "reset_settings_to_default": "Einstellungen auf Standardwerte zurücksetzen",
   "reset_to_default": "Auf Standard zurücksetzen",
   "resolve_duplicates": "Duplikate entfernen",
   "resolved_all_duplicates": "Alle Duplikate aufgelöst",
@@ -1122,8 +1092,7 @@
   "saved_settings": "Einstellungen gespeichert",
   "say_something": "Etwas sagen",
   "scan_all_libraries": "Alle Bibliotheken scannen",
-  "scan_all_library_files": "Alle Bibliotheksdateien erneut scannen",
-  "scan_new_library_files": "Neue Bibliotheksdateien scannen",
+  "scan_library": "Scannen",
   "scan_settings": "Scan-Einstellungen",
   "scanning_for_album": "Nach Alben scannen...",
   "search": "Suche",
@@ -1141,6 +1110,7 @@
   "search_options": "Suchoptionen",
   "search_people": "Suche nach Personen",
   "search_places": "Suche nach Orten",
+  "search_settings": "Suche nach Einstellungen",
   "search_state": "Suche nach Bundesland / Provinz...",
   "search_tags": "Sache nach Tags...",
   "search_timezone": "Suche nach Zeitzone...",
@@ -1160,18 +1130,17 @@
   "select_library_owner": "Bibliotheksbesitzer auswählen",
   "select_new_face": "Neues Gesicht auswählen",
   "select_photos": "Fotos auswählen",
-  "select_trash_all": "Alle Löschen",
+  "select_trash_all": "Alle löschen",
   "selected": "Ausgewählt",
   "selected_count": "{count, plural, other {# ausgewählt}}",
   "send_message": "Nachricht senden",
   "send_welcome_email": "Begrüssungsmail senden",
-  "server": "Server",
-  "server_offline": "Server Offline",
-  "server_online": "Server Online",
+  "server_offline": "Server offline",
+  "server_online": "Server online",
   "server_stats": "Server-Statistiken",
   "server_version": "Server-Version",
   "set": "Speichern",
-  "set_as_album_cover": "Als Albumcover gesetzt",
+  "set_as_album_cover": "Als Albumcover festlegen",
   "set_as_profile_picture": "Als Profilbild festlegen",
   "set_date_of_birth": "Geburtsdatum festlegen",
   "set_profile_picture": "Profilbild einstellen",
@@ -1182,7 +1151,7 @@
   "shared": "Geteilt",
   "shared_by": "Geteilt von",
   "shared_by_user": "Von {user} geteilt",
-  "shared_by_you": "Geteilt von dir",
+  "shared_by_you": "Von dir geteilt",
   "shared_from_partner": "Fotos von {partner}",
   "shared_link_options": "Optionen für geteilten Link",
   "shared_links": "Geteilte Links",
@@ -1200,7 +1169,7 @@
   "show_gallery": "Galerie anzeigen",
   "show_hidden_people": "Ausgeblendete Personen anzeigen",
   "show_in_timeline": "In Zeitleiste anzeigen",
-  "show_in_timeline_setting_description": "Fotos und Videos dieses Benutzers in deiner Timeline anzeigen",
+  "show_in_timeline_setting_description": "Fotos und Videos dieses Benutzers in deiner Zeitleiste anzeigen",
   "show_keyboard_shortcuts": "Tastaturkürzel anzeigen",
   "show_metadata": "Metadaten anzeigen",
   "show_or_hide_info": "Informationen ein- oder ausblenden",
@@ -1208,11 +1177,12 @@
   "show_person_options": "Personen-Optionen anzeigen",
   "show_progress_bar": "Fortschrittsbalken anzeigen",
   "show_search_options": "Suchoptionen anzeigen",
+  "show_slideshow_transition": "Slideshow-Übergang anzeigen",
   "show_supporter_badge": "Unterstützerabzeichen",
   "show_supporter_badge_description": "Zeige Unterstützerabzeichen",
   "shuffle": "Durchmischen",
   "sidebar": "Seitenleiste",
-  "sidebar_display_description": "Zeigt einen Link zu der Ansicht in der Seitenleiste an",
+  "sidebar_display_description": "Zeige einen Link zu der Ansicht in der Seitenleiste an",
   "sign_out": "Abmelden",
   "sign_up": "Registrieren",
   "size": "Größe",
@@ -1220,7 +1190,7 @@
   "skip_to_folders": "Springe zu Ordnern",
   "skip_to_tags": "Springe zu Tags",
   "slideshow": "Diashow",
-  "slideshow_settings": "Diashow Einstellungen",
+  "slideshow_settings": "Diashow-Einstellungen",
   "sort_albums_by": "Alben sortieren nach...",
   "sort_created": "Erstellungsdatum",
   "sort_items": "Anzahl der Einträge",
@@ -1228,7 +1198,7 @@
   "sort_oldest": "Ältestes Foto",
   "sort_recent": "Neustes Foto",
   "sort_title": "Titel",
-  "source": "Quelle",
+  "source": "Quellcode",
   "stack": "Stapel",
   "stack_duplicates": "Duplikate stapeln",
   "stack_select_one_photo": "Hauptfoto für den Stapel auswählen",
@@ -1243,19 +1213,22 @@
   "stop_photo_sharing": "Deine Fotos nicht mehr teilen?",
   "stop_photo_sharing_description": "{partner} wird keinen Zugriff mehr auf deine Fotos haben.",
   "stop_sharing_photos_with_user": "Aufhören Fotos mit diesem Benutzer zu teilen",
-  "storage": "Speicher",
+  "storage": "Speicherplatz",
   "storage_label": "Speicherpfad",
   "storage_usage": "{used} von {available} verwendet",
   "submit": "Bestätigen",
   "suggestions": "Vorschläge",
   "sunrise_on_the_beach": "Sonnenaufgang am Strand",
+  "support": "Unterstützung",
+  "support_and_feedback": "Unterstützung & Feedback",
+  "support_third_party_description": "Deine Immich-Installation wurde von einem Drittanbieter zusammengestellt. Probleme, die bei dir auftreten, können durch dieses Paket verursacht werden. Bitte wende dich daher in erster Linie an diesen Anbieter, indem du die unten stehenden Links verwendest.",
   "swap_merge_direction": "Vertauschen der Zusammenführungsrichtung",
   "sync": "Synchronisieren",
   "tag": "Tag",
   "tag_assets": "Dateien taggen",
   "tag_created": "Tag erstellt: {tag}",
   "tag_feature_description": "Durchsuchen von Fotos und Videos, gruppiert nach logischen Tag-Themen",
-  "tag_not_found_question": "Kein Tag zu finden? Erstelle einen <link>hier</link>",
+  "tag_not_found_question": "Kein Tag zu finden? <link>Erstelle einen neuen Tag.</link>",
   "tag_updated": "Tag aktualisiert: {tag}",
   "tagged_assets": "{count, plural, one {# Datei} other {# Dateien}} getagged",
   "tags": "Tags",
@@ -1264,41 +1237,40 @@
   "theme_selection": "Themenauswahl",
   "theme_selection_description": "Automatische Einstellung des Themes auf Hell oder Dunkel, je nach Systemeinstellung des Browsers",
   "they_will_be_merged_together": "Sie werden zusammengeführt",
+  "third_party_resources": "Drittanbieter-Quellen",
   "time_based_memories": "Zeitbasierte Erinnerungen",
+  "timeline": "Zeitleiste",
   "timezone": "Zeitzone",
   "to_archive": "Archivieren",
   "to_change_password": "Passwort ändern",
   "to_favorite": "Zu Favoriten hinzufügen",
   "to_login": "Anmelden",
   "to_parent": "Gehe zum Übergeordneten",
-  "to_root": "Zur Wurzel",
-  "to_trash": "Zum Papierkorb verschieben",
+  "to_trash": "In den Papierkorb verschieben",
   "toggle_settings": "Einstellungen umschalten",
   "toggle_theme": "Dunkles Theme umschalten",
-  "toggle_visibility": "Sichtbarkeit umschalten",
+  "total": "Gesamt",
   "total_usage": "Gesamtnutzung",
   "trash": "Papierkorb",
-  "trash_all": "Alles im Papierkorb",
+  "trash_all": "Alle löschen",
   "trash_count": "Papierkorb {count, number}",
   "trash_delete_asset": "Datei löschen/in den Papierkorb verschieben",
   "trash_no_results_message": "Gelöschte Fotos und Videos werden hier angezeigt.",
   "trashed_items_will_be_permanently_deleted_after": "Gelöschte Objekte werden nach {days, plural, one {# Tag} other {# Tagen}} endgültig gelöscht.",
   "type": "Typ",
-  "unarchive": "Unarchivieren",
-  "unarchived": "Unarchiviert",
-  "unarchived_count": "{count, plural, other {# Entarchiviert}}",
+  "unarchive": "Entarchivieren",
+  "unarchived_count": "{count, plural, other {# entarchiviert}}",
   "unfavorite": "Entfavorisieren",
   "unhide_person": "Person einblenden",
   "unknown": "Unbekannt",
-  "unknown_album": "Unbekanntes Album",
   "unknown_year": "Unbekanntes Jahr",
   "unlimited": "Unlimitiert",
-  "unlink_motion_video": "Verlinkung zum Bewegungsvideo aufheben",
+  "unlink_motion_video": "Verknüpfung zum Bewegungsvideo aufheben",
   "unlink_oauth": "OAuth entfernen",
   "unlinked_oauth_account": "Nicht verknüpftes OAuth-Konto",
   "unnamed_album": "Unbenanntes Album",
   "unnamed_album_delete_confirmation": "Bist du sicher, dass du dieses Album löschen willst?",
-  "unnamed_share": "Unbenannte Teilung",
+  "unnamed_share": "Unbenannte Freigabe",
   "unsaved_change": "Ungespeicherte Änderung",
   "unselect_all": "Alles abwählen",
   "unselect_all_duplicates": "Alle Duplikate abwählen",
@@ -1310,7 +1282,7 @@
   "updated_password": "Passwort aktualisiert",
   "upload": "Hochladen",
   "upload_concurrency": "Parallelität beim Hochladen",
-  "upload_errors": "Hochladen abgeschlossen mit {count, plural, one {# Fehler} other {# Fehlern}}, aktualisiere die Seite, um neu hochgeladene Dateien zu sehen.",
+  "upload_errors": "Hochladen mit {count, plural, one {# Fehler} other {# Fehlern}} abgeschlossen, aktualisiere die Seite, um neu hochgeladene Dateien zu sehen.",
   "upload_progress": "{remaining, number} verbleibend - {processed, number}/{total, number} verarbeitet",
   "upload_skipped_duplicates": "{count, plural, one {# doppelte Datei} other {# doppelte Dateien}} ausgelassen",
   "upload_status_duplicates": "Duplikate",
@@ -1322,13 +1294,13 @@
   "use_custom_date_range": "Stattdessen einen benutzerdefinierten Datumsbereich verwenden",
   "user": "Nutzer",
   "user_id": "Nutzer-ID",
-  "user_license_settings": "Lizenz",
-  "user_license_settings_description": "Verwalte deine Lizenz",
   "user_liked": "{type, select, photo {Dieses Foto} video {Dieses Video} asset {Diese Datei} other {Dies}} gefällt {user}",
   "user_purchase_settings": "Kauf",
   "user_purchase_settings_description": "Kauf verwalten",
   "user_role_set": "{user} als {role} festlegen",
   "user_usage_detail": "Nutzungsdetails der Nutzer",
+  "user_usage_stats": "Statistiken zur Kontonutzung",
+  "user_usage_stats_description": "Statistiken zur Kontonutzung anzeigen",
   "username": "Nutzername",
   "users": "Benutzer",
   "utilities": "Hilfsmittel",
@@ -1336,7 +1308,9 @@
   "variables": "Variablen",
   "version": "Version",
   "version_announcement_closing": "Dein Freund, Alex",
-  "version_announcement_message": "Hallo Freund, es gibt eine neue Version dieser Anwendung. Bitte nimm dir Zeit, die <link>Versionshinweise</link> zu lesen und stelle sicher, dass deine <code>docker-compose.yml</code>- und <code>.env</code>-Konfiguration auf dem neuesten Stand ist, um Fehlkonfigurationen zu vermeiden, insbesondere wenn du WatchTower oder ein anderes Verfahren verwendest, das deine Anwendung automatisch aktualisiert.",
+  "version_announcement_message": "Hi! Es gibt eine neue Version von Immich. Bitte nimm dir Zeit, die <link>Versionshinweise</link> zu lesen, um Fehlkonfigurationen zu vermeiden, insbesondere wenn du WatchTower oder ein anderes Verfahren verwendest, das Immich automatisch aktualisiert.",
+  "version_history": "Versionshistorie",
+  "version_history_item": "{version} am {date} installiert",
   "video": "Video",
   "video_hover_setting": "Videovorschau beim Hovern abspielen",
   "video_hover_setting_description": "Video-Miniaturansicht wiedergeben, wenn der Mauszeiger über dem Element verweilt. Auch wenn diese Funktion deaktiviert ist, kann die Wiedergabe gestartet werden, indem der Mauszeiger auf das Wiedergabesymbol bewegt wird.",
@@ -1348,12 +1322,12 @@
   "view_all_users": "Alle Nutzer anzeigen",
   "view_in_timeline": "In Zeitleiste anzeigen",
   "view_links": "Links anzeigen",
+  "view_name": "Ansicht",
   "view_next_asset": "Nächste Datei anzeigen",
   "view_previous_asset": "Vorherige Datei anzeigen",
   "view_stack": "Stapel anzeigen",
-  "viewer": "Zuschauer",
   "visibility_changed": "Sichtbarkeit für {count, plural, one {# Person} other {# Personen}} geändert",
-  "waiting": "Warte",
+  "waiting": "Wartend",
   "warning": "Warnung",
   "week": "Woche",
   "welcome": "Willkommen",
diff --git a/i18n/el.json b/i18n/el.json
new file mode 100644
index 0000000000..daad424022
--- /dev/null
+++ b/i18n/el.json
@@ -0,0 +1,1340 @@
+{
+  "about": "Σχετικά",
+  "account": "Λογαριασμός",
+  "account_settings": "Ρυθμίσεις Λογαριασμού",
+  "acknowledge": "Έλαβα γνώση",
+  "action": "Ενέργεια",
+  "actions": "Ενέργειες",
+  "active": "Ενεργά",
+  "activity": "Δραστηριότητα",
+  "activity_changed": "Η δραστηριότητα είναι {enabled, select, true {ενεργοποιημένη} other {απενεργοποιημένη}}",
+  "add": "Προσθήκη",
+  "add_a_description": "Προσθήκη περιγραφής",
+  "add_a_location": "Προσθήκη μίας τοποθεσίας",
+  "add_a_name": "Προσθήκη ονόματος",
+  "add_a_title": "Προσθήκη τίτλου",
+  "add_exclusion_pattern": "Προσθήκη μοτίβου αποκλεισμού",
+  "add_import_path": "Προσθήκη μονοπατιού εισαγωγής",
+  "add_location": "Προσθήκη τοποθεσίας",
+  "add_more_users": "Προσθήκη επιπλέον χρηστών",
+  "add_partner": "Προσθήκη συνεργάτη",
+  "add_path": "Προσθήκη διαδρομής",
+  "add_photos": "Προσθήκη φωτογραφιών",
+  "add_to": "Προσθήκη σε...",
+  "add_to_album": "Προσθήκη σε άλμπουμ",
+  "add_to_shared_album": "Προσθήκη σε κοινόχρηστο άλμπουμ",
+  "add_url": "Προσθήκη Συνδέσμου",
+  "added_to_archive": "Προστέθηκε στο αρχείο",
+  "added_to_favorites": "Προστέθηκε στα αγαπημένα",
+  "added_to_favorites_count": "Προστέθηκαν {count, number} στα αγαπημένα",
+  "admin": {
+    "add_exclusion_pattern_description": "Προσθέστε μοτίβα αποκλεισμού. Υποστηρίζεται η επιλογή πολλών με *, **, και ?. Για να αγνοηθούν όλα τα αρχεία σε έναν φάκελο με το όνομα \"Raw\", χρησιμοποιήστε \"**/Raw/**\". Για να αγνοηθούν όλα τα αρχεία με κατάληξη \".tif\", χρησιμοποιήστε \"**/*.tif\". Για να αγνοηθεί μία απόλυτη διαδρομή, χρησιμοποιήστε \"/path/to/ignore/**\".",
+    "asset_offline_description": "Αυτό το στοιχείο εξωτερικής βιβλιοθήκης δε βρίσκεται πλέον στο δίσκο και έχει μεταφερθεί στα απορρίμματα. Εάν το αρχείο έχει μετακινηθεί εντός της βιβλιοθήκης, ελέγξτε το χρονολόγιο φωτογραφιών σας για το νέο αντίστοιχο στοιχείο. Για να επαναφέρετε αυτό το στοιχείο, βεβαιωθείτε ότι το παρακάτω μονοπάτι αρχείου είναι προσβάσιμο από το Immich και σαρώστε τη βιβλιοθήκη.",
+    "authentication_settings": "Ρυθμίσεις Ελέγχου Ταυτότητας",
+    "authentication_settings_description": "Διαχείριση κωδικού πρόσβασης, OAuth και άλλων ρυθμίσεων ελέγχου ταυτότητας",
+    "authentication_settings_disable_all": "Είστε βέβαιοι ότι θέλετε να απενεργοποιήσετε όλες τις μεθόδους σύνδεσης; Η σύνδεση θα απενεργοποιηθεί πλήρως.",
+    "authentication_settings_reenable": "Για επανενεργοποίηση, χρησιμοποιήστε μία <link>Εντολή Διακομιστή</link>.",
+    "background_task_job": "Εργασίες Παρασκηνίου",
+    "backup_database": "Δημιουργία Αντιγράφου Ασφαλείας της Βάσης Δεδομένων",
+    "backup_database_enable_description": "Ενεργοποίηση αντιγράφων ασφαλείας της βάσης δεδομένων",
+    "backup_keep_last_amount": "Αριθμός προηγούμενων αντιγράφων ασφαλείας για διατήρηση",
+    "backup_settings": "Ρυθμίσεις Αντιγράφων Ασφαλείας",
+    "backup_settings_description": "Διαχείρηση ρυθμίσεων των αντιγράφων ασφαλείας της βάσης δεδομένων",
+    "check_all": "Έλεγχος Όλων",
+    "cleared_jobs": "Εκκαθαρίστηκαν οι εργασίες για: {job}",
+    "config_set_by_file": "Η παραμετροποίηση γίνεται, προς το παρόν, μέσω ενός αρχείου παραμέτρων",
+    "confirm_delete_library": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τη βιβλιοθήκη {library};",
+    "confirm_delete_library_assets": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτή τη βιβλιοθήκη; Αυτό θα διαγράψει τα {count, plural, one {# contained asset} other {all # contained assets}} από το Immich και δεν μπορεί να αναιρεθεί. Τα αρχεία θα παραμείνουν στον δίσκο.",
+    "confirm_email_below": "Για επιβεβαίωση, πληκτρολογήστε \"{email}\" παρακάτω",
+    "confirm_reprocess_all_faces": "Είστε βέβαιοι ότι θέλετε να επεξεργαστείτε ξανά όλα τα πρόσωπα; Αυτό θα εκκαθαρίσει ακόμα και τα άτομα στα οποία έχετε ήδη ορίσει το όνομα.",
+    "confirm_user_password_reset": "Είστε βέβαιοι ότι θέλετε να επαναφέρετε τον κωδικό πρόσβασης του χρήστη {user};",
+    "create_job": "Δημιουργία εργασίας",
+    "cron_expression": "Σύνταξη Cron",
+    "cron_expression_description": "Ορίστε το διάστημα σάρωσης χρησιμοποιώντας τη μορφή cron. Για περισσότερες πληροφορίες, ανατρέξτε π.χ. στο <link>Crontab Guru</link>",
+    "cron_expression_presets": "Προκαθορισμένες εκφράσεις cron",
+    "disable_login": "Απενεργοποίηση σύνδεσης",
+    "duplicate_detection_job_description": "Εκτελέστε μηχανική μάθηση σε στοιχεία για να εντοπίσετε παρόμοιες εικόνες. Βασίζεται στην Έξυπνη Αναζήτηση",
+    "exclusion_pattern_description": "Τα μοτίβα αποκλεισμού σας επιτρέπουν να αγνοείται αρχεία και φακέλους κατά τη σάρωση της βιβλιοθήκης σας. Αυτό είναι χρήσιμο εάν εχετε φακέλους που περιέχουν αρχεία που δεν θέλετε να εισάγετε, όπως αρχεία RAW.",
+    "external_library_created_at": "Εξωτερική βιβλιοθήκη (δημιουργήθηκε {date})",
+    "external_library_management": "Διαχείριση Εξωτερικών Βιβλιοθηκών",
+    "face_detection": "Ανίχνευση προσώπου",
+    "face_detection_description": "Ανιχνεύστε τα πρόσωπα σε στοιχεία χρησιμοποιώντας μηχανική μάθηση. Για βίντεο, λαμβάνεται υπόψη μόνο η μικρογραφία. Η επιλογή \"Ανανέωση\" επεξεργάζεται εκ νέου όλα τα στοιχεία. Η επιλογή \"Επαναφορά\", επιπλέον εκκαθαρίζει όλα τα δεδομένα προσώπου. Η επιλογή \"Ελλείποντα\" προσθέτει στην ουρά στοιχεία που δεν έχουν υποστεί ακόμη επεξεργασία. Τα πρόσωπα που έχουν εντοπιστεί θα μπουν στην ουρά για την Αναγνώριση Προσώπου μετά την ολοκλήρωση της Ανίχνευσης Προσώπου, ομαδοποιώντας τα σε υπάρχοντα ή νέα άτομα.",
+    "facial_recognition_job_description": "Ομαδοποιήστε ανιχνευμένα πρόσωπα σε άτομα. Αυτό το βήμα εκτελείται αφού ολοκληρωθεί η Ανίχνευση Προσώπου. Η επιλογή \"Επαναφορά\" ομαδοποιεί εκ νέου όλα τα πρόσωπα. Η επιλογή \"Ελλείποντα\" βάζει στην ουρά για ομαδοποίηση πρόσωπα που δεν έχουν αντιστοιχηθεί σε κάποιο άτομο.",
+    "failed_job_command": "Η εντολή {command} απέτυχε για την εργασία: {job}",
+    "force_delete_user_warning": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Αυτό θα αφαιρέσει άμεσα τον χρήστη και όλα τα στοιχεία. Αυτό δεν μπορεί να αναιρεθεί και τα αρχεία δεν μπορούν να ανακτηθούν.",
+    "forcing_refresh_library_files": "Εξαναγκαστική ανανέωση όλων των αρχείων της βιβλιοθήκης",
+    "image_format": "Μορφή",
+    "image_format_description": "Η μορφή WebP παράγει μικρότερα αρχεία από τη μορφή JPEG, αλλά είναι πιο αργή στην κωδικοποίηση.",
+    "image_prefer_embedded_preview": "Προτίμηση ενσωματωμένης προεπισκόπησης",
+    "image_prefer_embedded_preview_setting_description": "Χρήση ενσωματωμένων προεπισκοπίσεων σε RAW εικόνες ως είσοδο για την επεξεργασία εικόνας εφόσον είναι διαθέσιμες. Αυτό μπορεί να δημιουργήσει πιο ακριβή χρώματα για κάποιες εικόνες, αλλά η ποιότητα των προεπισκοπίσεων εξαρτάται από την κάμερα και ενδέχεται να υπάρχουν περισσότερες αλλοιώσεις στην εικόνα λόγω συμπίεσης.",
+    "image_prefer_wide_gamut": "Προτίμηση ευρέος φάσματος",
+    "image_prefer_wide_gamut_setting_description": "Χρήση Display P3 για τις μικρογραφίες. Αυτό διατηρεί καλύτερα την ζωντάνια των χρωμάτων σε εικόνες μεγάλου χρωματικού εύρους, αλλά ενδέχεται να εμφανίζονται αλλιώς σε παλαιότερες συσκευές με παλαιότερες εκδόσεις περιηγητών. Οι εικόνες sRGB μένουν ως έχουν για να αποφευχθούν χρωματικές αλλαγές.",
+    "image_preview_description": "Μεσαίου μεγέθους εικόνες, χωρίς μεταδεδομένα, οι οποίες χρησιμοποιούνται στην προβολή ενός αντικειμένου και για μηχανική μάθηση",
+    "image_preview_quality_description": "Ποιότητα προεπισκόπησης από 1 έως 100. Όσο μεγαλύτερη τιμή τόσο καλύτερη η ποιότητα, αλλά παράγονται μεγαλύτερα αρχεία που ενδέχεται να μειώσουν την ταχύτητα απόκρισης της εφαρμογής. Οι χαμηλές τιμές μπορεί να επηρεάσουν τη ποιότητα της μηχανικής μάθησης.",
+    "image_preview_title": "Ρυθμίσεις Προεπισκόπισης",
+    "image_quality": "Ποιότητα",
+    "image_resolution": "Ανάλυση",
+    "image_resolution_description": "Υψηλότερες αναλύσεις μπορούν να διατηρήσουν περισσότερες λεπτομέρειες, αλλά χρειάζονται περισσότερο χρόνο να κωδικοποιηθούν, έχουν μεγαλύτερα μεγέθη αρχείων και μπορούν να μειώσουν την απόκριση της εφαρμογής.",
+    "image_settings": "Ρυθμίσεις Εικόνας",
+    "image_settings_description": "Διαχείριση της ποιότητας και της ανάλυσης των παραγόμενων εικόνων",
+    "image_thumbnail_description": "Μικρογραφία εικόνας χωρίς μεταδεδομένα, που χρησιμοποιείται όταν προβάλλονται ομάδες φωτογραφιών όπως το κύριο χρονολόγιο",
+    "image_thumbnail_quality_description": "Ποιότητα μικρογραφίας από 1 έως 100. Υψηλότερες τιμές είναι καλύτερες, αλλά παράγουν μεγαλύτερα αρχεία που μπορεί να μειώσουν την ταχύτητα απόκρισης της εφαρμογής.",
+    "image_thumbnail_title": "Ρυθμίσεις Μικρογραφίας",
+    "job_concurrency": "Ταυτόχρονη εκτέλεση {job}",
+    "job_created": "Εργασία δημιουργήθηκε",
+    "job_not_concurrency_safe": "Αυτή η εργασία δεν είναι ασφαλής για ταυτόχρονη εκτέλεση.",
+    "job_settings": "Ρυθμίσεις Εργασίας",
+    "job_settings_description": "Διαχείριση ταυτόχρονης εκτέλεσης εργασίας",
+    "job_status": "Κατάσταση Εργασίας",
+    "jobs_delayed": "{jobCount, plural, one {# καθυστέρησε} other {# καθυστέρησαν}}",
+    "jobs_failed": "{jobCount, plural, one {# απέτυχε} other {# απέτυχαν}}",
+    "library_created": "Δημιουργήθηκε η βιβλιοθήκη: {library}",
+    "library_deleted": "Η βιβλιοθήκη διαγράφηκε",
+    "library_import_path_description": "Καθορίστε έναν φάκελο για εισαγωγή. Αυτός ο φάκελος, συμπεριλαμβανομένων των υποφακέλων του, θα σαρωθεί για εικόνες και βίντεο.",
+    "library_scanning": "Περιοδική Σάρωση",
+    "library_scanning_description": "Ρύθμιση περιοδικής σάρωσης βιβλιοθήκης",
+    "library_scanning_enable_description": "Ενεργοποίηση περιοδικής σάρωσης βιβλιοθήκης",
+    "library_settings": "Εξωτερική Βιβλιοθήκη",
+    "library_settings_description": "Διαχείριση ρυθμίσεων εξωτερικής βιβλιοθήκης",
+    "library_tasks_description": "Εκτελούν εργασίες της βιβλιοθήκης",
+    "library_watching_enable_description": "Παρακολούθηση εξωτερικών βιβλιοθηκών για τροποποιήσεις αρχείων",
+    "library_watching_settings": "Παρακολούθηση βιβλιοθήκης (ΠΕΙΡΑΜΑΤΙΚΟ)",
+    "library_watching_settings_description": "Αυτόματη παρακολούθηση για τροποποιημένα αρχεία",
+    "logging_enable_description": "Ενεργοποίηση καταγραφής συμβάντων",
+    "logging_level_description": "Το επίπεδο καταγραφής συμβάντων που θα εφαρμοστεί, όταν αυτή είναι ενεργοποιημένη.",
+    "logging_settings": "Καταγραφή Συμβάντων",
+    "machine_learning_clip_model": "Μοντέλο CLIP",
+    "machine_learning_clip_model_description": "Το όνομα ενός μοντέλου CLIP που αναφέρεται <link>εδώ</link>. Σημειώστε ότι πρέπει να επανεκτελέσετε την εργασία 'Έξυπνη Αναζήτηση' για όλες τις εικόνες μετά την αλλαγή μοντέλου.",
+    "machine_learning_duplicate_detection": "Εντοπισμός Διπλότυπων",
+    "machine_learning_duplicate_detection_enabled": "Ενεργοποίηση εντοπισμού διπλότυπων",
+    "machine_learning_duplicate_detection_enabled_description": "Εάν απενεργοποιηθεί, απολύτως παρόμοια στοιχεία θα συνεχίσουν να εκκαθαρίζονται από διπλότυπα.",
+    "machine_learning_duplicate_detection_setting_description": "Χρησιμοποιήστε τις ενσωματώσεις CLIP για να βρείτε πιθανά διπλότυπα",
+    "machine_learning_enabled": "Ενεργοποίηση μηχανικής μάθησης",
+    "machine_learning_enabled_description": "Εάν απενεργοποιηθεί, όλες οι λειτουργίες μηχανικής μάθησης θα απενεργοποιηθούν, ανεξάρτητα από τις παρακάτω ρυθμίσεις.",
+    "machine_learning_facial_recognition": "Αναγνώριση Προσώπου",
+    "machine_learning_facial_recognition_description": "Εντοπισμός, αναγνώριση και ομαδοποίηση προσώπων που υπάρχουν σε εικόνες",
+    "machine_learning_facial_recognition_model": "Μοντέλο αναγνώρισης προσώπου",
+    "machine_learning_facial_recognition_model_description": "Τα μοντέλα παρατίθενται με φθίνουσα σειρά μεγέθους. Τα μεγαλύτερα μοντέλα είναι πιο αργά και χρησιμοποιούν περισσότερη μνήμη, αλλά παράγουν καλύτερα αποτελέσματα. Σημειώστε ότι πρέπει να εκτελέσετε ξανά την εργασία Ανίχνευση προσώπου για όλες τις εικόνες κατά την αλλαγή ενός μοντέλου.",
+    "machine_learning_facial_recognition_setting": "Ενεργοποίηση αναγνώρισης προσώπου",
+    "machine_learning_facial_recognition_setting_description": "Αν απενεργοποιηθεί, οι εικόνες δεν θα κωδικοποιούνται για αναγνώριση προσώπου και δεν θα συμπληρώνουν την ενότητα Άτομα στη σελίδα Περιήγησης.",
+    "machine_learning_max_detection_distance": "Μέγιστη απόσταση ανίχνευσης",
+    "machine_learning_max_detection_distance_description": "Η μέγιστη απόσταση μεταξύ δύο εικόνων για να θεωρηθούν διπλότυπες, που κυμαίνεται από 0,001-0,1. Οι υψηλότερες τιμές θα εντοπίσουν περισσότερες διπλότυπες, αλλά μπορεί να οδηγήσουν σε ψευδώς θετικά αποτελέσματα.",
+    "machine_learning_max_recognition_distance": "Μέγιστη απόσταση αναγνώρισης",
+    "machine_learning_max_recognition_distance_description": "Η μέγιστη απόσταση μεταξύ δύο προσώπων για να θεωρείται το ίδιο άτομο, που κυμαίνεται από 0-2. Η μείωση αυτή μπορεί να αποτρέψει την επισήμανση δύο ατόμων ως το ίδιο άτομο, ενώ η αύξηση της μπορεί να αποτρέψει την επισήμανση του ίδιου ατόμου ως δύο διαφορετικών ατόμων. Λάβετε υπόψη ότι είναι πιο εύκολο να συγχωνεύσετε δύο άτομα παρά να χωρίσετε ένα άτομο στα δύο, οπότε προτιμήστε ένα χαμηλότερο όριο, όταν αυτό είναι δυνατό.",
+    "machine_learning_min_detection_score": "Ελάχιστο σκορ ανίχνευσης",
+    "machine_learning_min_detection_score_description": "Ελάχιστο σκορ εμπιστοσύνης για ανίχνευση προσώπου από 0-1. Οι χαμηλότερες τιμές θα εντοπίσουν περισσότερα πρόσωπα, αλλά μπορεί να οδηγήσουν σε ψευδώς θετικά αποτελέσματα.",
+    "machine_learning_min_recognized_faces": "Ελάχιστα αναγνωρισμένα πρόσωπα",
+    "machine_learning_min_recognized_faces_description": "Ο ελάχιστος αριθμός αναγνωρισμένων προσώπων για ένα άτομο που θα δημιουργηθεί. Η αύξηση αυτή καθιστά την Αναγνώριση Προσώπου πιο ακριβή με το κόστος να αυξηθεί η πιθανότητα να μην εκχωρηθεί ένα πρόσωπο σε ένα άτομο.",
+    "machine_learning_settings": "Ρυθμίσεις Μηχανικής Εκμάθησης",
+    "machine_learning_settings_description": "Διαχειριστείτε τις λειτουργίες και τις ρυθμίσεις μηχανικής εκμάθησης",
+    "machine_learning_smart_search": "Έξυπνη Αναζήτηση",
+    "machine_learning_smart_search_description": "Αναζητήστε εικόνες σημασιολογικά χρησιμοποιώντας ενσωματώσεις CLIP",
+    "machine_learning_smart_search_enabled": "Ενεργοποίηση έξυπνης αναζήτησης",
+    "machine_learning_smart_search_enabled_description": "Αν απενεργοποιηθεί, οι εικόνες δεν θα κωδικοποιούνται για έξυπνη αναζήτηση.",
+    "machine_learning_url_description": "Η διεύθυνση URL του διακομιστή μηχανικής εκμάθησης. Αν παρέχονται περισσότερες από μία διευθύνσεις URL, τότε, κάθε διακομιστής θα προσπαθήσει να συνδεθεί διαδοχικά, από την πρώτη μέχρι την τελευταία, έως ότου απαντήσει επιτυχώς.",
+    "manage_concurrency": "Διαχείριση ταυτόχρονη εκτέλεσης",
+    "manage_log_settings": "Διαχείριση ρυθμίσεων αρχείου καταγραφής",
+    "map_dark_style": "Σκούρο Θέμα",
+    "map_enable_description": "Ενεργοποίηση λειτουργιών χάρτη",
+    "map_gps_settings": "Ρυθμίσεις Χάρτη & GPS",
+    "map_gps_settings_description": "Διαχείριση Ρυθμίσεων Χάρτη & GPS (Αντίστροφη γεωκωδικοποίηση)",
+    "map_implications": "Η λειτουργία χάρτη βασίζεται σε εξωτερικές υπηρεσίες για τα πλακίδια (tiles.immich.cloud)",
+    "map_light_style": "Φωτεινό Θέμα",
+    "map_manage_reverse_geocoding_settings": "Διαχείριση ρυθμίσεων <link>Αντίστροφης Γεωκωδικοποίησης</link>",
+    "map_reverse_geocoding": "Αντίστροφη Γεωκωδικοποίηση",
+    "map_reverse_geocoding_enable_description": "Ενεργοποίηση Αντίστροφης Γεωκωδικοποίησης",
+    "map_reverse_geocoding_settings": "Ρυθμίσεις Αντίστροφης Γεωκωδικοποίησης",
+    "map_settings": "Χάρτης",
+    "map_settings_description": "Διαχείριση ρυθμίσεων χάρτη",
+    "map_style_description": "URL προς αρχείο θέματος του χάρτη style.json",
+    "metadata_extraction_job": "Εξαγωγή μεταδεδομένων",
+    "metadata_extraction_job_description": "Εξαγωγή μεταδεδομένων από κάθε αρχείο, όπως τοποθεσία, πρόσωπα και ανάλυση",
+    "metadata_faces_import_setting": "Ενεργοποίηση εισαγωγής προσώπων",
+    "metadata_faces_import_setting_description": "Εισαγωγή προσώπων από EXIF εικόνων και παρόμοια αρχεία ( sidecar files)",
+    "metadata_settings": "Ρυθμίσεις μεταδεδομένων",
+    "metadata_settings_description": "Διαχείρηση ρυθμίσεων μεταδεδομένων",
+    "migration_job": "Μεταφορά δεδομένων (Migration)",
+    "migration_job_description": "Μεταφορά των εικονιδίων για αρχεία και πρόσωπα στην πιο πρόσφατη δομή αρχείων",
+    "no_paths_added": "Δεν προστέθηκαν διαδρομές",
+    "no_pattern_added": "Δεν προστέθηκε μοτίβο",
+    "note_apply_storage_label_previous_assets": "Σημείωση: Για να εφαρμοστεί η Ετικέτα Αποθήκευσης σε στοιχεία που είχαν αναρτηθεί παλαιότερα, εκτέλεσε το",
+    "note_cannot_be_changed_later": "ΣΗΜΕΊΩΣΗ: Αυτό δεν μπορεί να τροποποιηθεί αργότερα!",
+    "note_unlimited_quota": "Σημείωση: Εισαγάγετε 0 για απεριόριστο όριο",
+    "notification_email_from_address": "Διεύθυνση αποστολέα",
+    "notification_email_from_address_description": "Διεύθυνση αποστολέα, πχ: \"Immich Photo Server <noreply@example.com>\"",
+    "notification_email_host_description": "Πάροχος του email server (πχ smtp.immich.app)",
+    "notification_email_ignore_certificate_errors": "Παράβλεψη των σφαλμάτων πιστοποίησης",
+    "notification_email_ignore_certificate_errors_description": "Παράβλεψη σφαλμάτων επικύρωσης της πιστοποίησης TLS (δεν προτείνεται)",
+    "notification_email_password_description": "Κωδικός για την αυθεντικοποίηση με τον server του email",
+    "notification_email_port_description": "Θύρα του email server (πχ 25, 465, ή 587)",
+    "notification_email_sent_test_email_button": "Αποστολή test email και αποθήκευση",
+    "notification_email_setting_description": "Ρυθμίσεις για την αποστολή ειδοποιήσεων μέσω email",
+    "notification_email_test_email": "Αποστολή test email",
+    "notification_email_test_email_failed": "Αποτυχία αποστολής test email, ελέγξτε τις ρυθμίσεις",
+    "notification_email_test_email_sent": "Ένα test email στάλθηκε στην διεύθυνση {email}. Παρακαλώ ελέγξτε τα εισερχόμενα σας.",
+    "notification_email_username_description": "Όνομα χρήστη για την αυθεντικοποίηση με τον server του email",
+    "notification_enable_email_notifications": "Ενεργοποίηση ειδοποιήσεων μέσω email",
+    "notification_settings": "Ρυθμίσεις ειδοποιήσεων",
+    "notification_settings_description": "Διαχείρηση ρυθμίσεων ειδοποιήσεων, συμπεριλαμβανομένου του email",
+    "oauth_auto_launch": "Αυτόματη εκκίνηση",
+    "oauth_auto_launch_description": "Αυτόματη εκκίνιση της υπηρεσίας OAuth με την πλοήγηση στην σελίδα σύνδεσης",
+    "oauth_auto_register": "Αυτόματη καταχώρηση",
+    "oauth_auto_register_description": "Αυτόματη καταχώρηση νέου χρήστη αφού συνδεθεί με OAuth",
+    "oauth_button_text": "Κείμενο κουμπιού",
+    "oauth_client_id": "Ταυτότητα πελάτη (Client)",
+    "oauth_client_secret": "Μυστικός κωδικός πελάτη",
+    "oauth_enable_description": "Σύνδεση με OAuth",
+    "oauth_issuer_url": "Διεύθυνση URL εκδότη",
+    "oauth_mobile_redirect_uri": "URI Ανακατεύθυνσης για κινητά τηλέφωνα",
+    "oauth_mobile_redirect_uri_override": "Προσπέλαση URI ανακατεύθυνσης για κινητά τηλέφωνα",
+    "oauth_mobile_redirect_uri_override_description": "Ενεργοποιήστε το όταν ο πάροχος OAuth δεν επιτρέπει μια URI για κινητά, όπως το '{callback}'",
+    "oauth_profile_signing_algorithm": "Αλγόριθμος σύνδεσης προφίλ",
+    "oauth_profile_signing_algorithm_description": "Αλγόριθμος που χρησιμοποιείται για την σύνδεση των χρηστών.",
+    "oauth_scope": "Εύρος",
+    "oauth_settings": "OAuth",
+    "oauth_settings_description": "Διαχείριση ρυθμίσεων σύνδεσης OAuth",
+    "oauth_settings_more_details": "Για περισσότερες λεπτομέρειες σχετικά με αυτήν τη δυνατότητα, ανατρέξτε στην <link>τεκμηρίωση</link>.",
+    "oauth_signing_algorithm": "Αλγόριθμος υπογραφής",
+    "oauth_storage_label_claim": "Δήλωση ετικέτας αποθήκευσης",
+    "oauth_storage_label_claim_description": "Ορίζει αυτόματα την ετικέτα αποθήκευσης του χρήστη στη δηλωμένη τιμή.",
+    "oauth_storage_quota_claim": "Δήλωση ποσοστού αποθήκευσης",
+    "oauth_storage_quota_claim_description": "Ορίζει αυτόματα το ποσοστό αποθήκευσης του χρήστη στη δηλωμένη τιμή.",
+    "oauth_storage_quota_default": "Προεπιλεγμένο όριο αποθήκευσης (GiB)",
+    "oauth_storage_quota_default_description": "Ποσοστό σε GiB που θα χρησιμοποιηθεί όταν δεν ορίζεται από τη δηλωμένη τιμή (Εισάγετε 0 για απεριόριστο ποσοστό).",
+    "offline_paths": "Διαδρομές αρχείων εκτός σύνδεσης",
+    "offline_paths_description": "Αυτά τα αποτελέσματα μπορεί να οφείλονται σε χειροκίνητη διαγραφή αρχείων που δεν ανήκουν σε εξωτερική βιβλιοθήκη.",
+    "password_enable_description": "Σύνδεση με ηλεκτρονικό ταχυδρομείο",
+    "password_settings": "Σύνδεση με κωδικό",
+    "password_settings_description": "Διαχείριση ρυθμίσεων σύνδεσης μέσω κωδικού πρόσβασης",
+    "paths_validated_successfully": "Όλες οι διαδρομές επικυρώθηκαν επιτυχώς",
+    "person_cleanup_job": "Καθαρισμός ατόμου",
+    "quota_size_gib": "Μέγεθος ορίου (GiB)",
+    "refreshing_all_libraries": "Επαναφόρτωση όλων των βιβλιοθηκών",
+    "registration": "Εγγραφή Διαχειριστή",
+    "registration_description": "Δεδομένου ότι είστε ο πρώτος χρήστης στο σύστημα, θα ανατεθείτε ως Διαχειριστής και θα είστε υπεύθυνος για τις διαχειριστικές εργασίες, ενώ οι επιπλέον χρήστες θα δημιουργούνται από εσάς.",
+    "repair_all": "Επιδιόρθωση όλων των στοιχείων",
+    "repair_matched_items": "Αντιστοιχίστηκαν {count, plural, one {# αντικείμενο} other {# αντικείμενα}}",
+    "repaired_items": "Επιδιορθώθηκαν {count, plural, one {# αντικείμενο} other {# αντικείμενα}}",
+    "require_password_change_on_login": "Απαιτείται από τον χρήστη να αλλάξει τον κωδικό πρόσβασης κατά την πρώτη σύνδεση",
+    "reset_settings_to_default": "Επαναφορά προεπιλεγμένων ρυθμίσεων",
+    "reset_settings_to_recent_saved": "Επαναφορά ρυθμίσεων στις πρόσφατα αποθηκευμένες ρυθμίσεις",
+    "scanning_library": "Σάρωση βιβλιοθήκης",
+    "search_jobs": "Αναζήτηση εργασιών...",
+    "send_welcome_email": "Αποστολή email καλωσορίσματος",
+    "server_external_domain_settings": "Εξωτερική διεύθυνση τομέα",
+    "server_external_domain_settings_description": "Διεύθυνση τομέα για δημόσιους κοινούς συνδέσμους, περιλαμβανομένου του http(s)://",
+    "server_public_users": "Δημόσιοι Χρήστες",
+    "server_public_users_description": "Όλοι οι χρήστες (όνομα και email) εμφανίζονται κατά την προσθήκη ενός χρήστη σε κοινόχρηστα άλμπουμ. Όταν αυτή η επιλογή είναι απενεργοποιημένη, η λίστα χρηστών θα είναι διαθέσιμη μόνο στους διαχειριστές.",
+    "server_settings": "Ρυθμίσεις Διακομιστή",
+    "server_settings_description": "Διαχείριση ρυθμίσεων διακομιστή",
+    "server_welcome_message": "Μήνυμα καλωσορίσματος",
+    "server_welcome_message_description": "Το μήνυμα που θα εμφανίζεται στη σελίδα σύνδεσης.",
+    "sidecar_job": "Μεταδεδομένα συνοδευτικού αρχείου",
+    "sidecar_job_description": "Ανακάλυψη ή συγχρονισμός των μεταδεδομένων του συνοδευτικού αρχείου από το σύστημα αρχείων",
+    "slideshow_duration_description": "Αριθμός δευτερολέπτων για την εμφάνιση κάθε εικόνας",
+    "smart_search_job_description": "Εκτέλεση της μηχανικής εκμάθησης, σε αρχεία, για την υποστήριξη της έξυπνης αναζήτησης",
+    "storage_template_date_time_description": "Η χρονική σήμανση της δημιουργίας του αρχείου, χρησιμοποιείται για τις πληροφορίες ημερομηνίας και ώρας",
+    "storage_template_date_time_sample": "Χρόνος δείγματος {date}",
+    "storage_template_enable_description": "Ενεργοποίηση του μηχανισμού των προτύπων αποθήκευσης",
+    "storage_template_hash_verification_enabled": "Ενεργοποιημένη επαλήθευση hash",
+    "storage_template_hash_verification_enabled_description": "Ενεργοποιεί την επαλήθευση hash. Μην το απενεργοποιήσεις εκτός αν είσαι βέβαιος/α για τις συνέπειες",
+    "storage_template_migration": "Μεταφορά προτύπων αποθήκευσης",
+    "storage_template_migration_description": "Εφαρμογή του τρέχοντος <link>{template}</link> στα αρχεία που έχουν ανέβει προηγουμένως",
+    "storage_template_migration_info": "Οι αλλαγές στο πρότυπο θα ισχύσουν μόνο για τα νέα αρχεία. Για να εφαρμόσετε αναδρομικά το πρότυπο, σε αρχεία που έχουν ανέβει προηγουμένως, εκτελέστε το <link>{job}</link>.",
+    "storage_template_migration_job": "Εργασία Μεταφοράς Προτύπων Αποθήκευσης",
+    "storage_template_more_details": "Για περισσότερες λεπτομέρειες σχετικά με αυτήν τη δυνατότητα, ανατρέξτε στο <template-link>Πρότυπο Αποθήκευσης</template-link> και στις <implications-link>συνέπειές</implications-link> του",
+    "storage_template_onboarding_description": "Όταν ενεργοποιηθεί, αυτή η δυνατότητα θα οργανώνει αυτόματα τα αρχεία με βάση ένα πρότυπο που καθορίζεται από τον χρήστη. Λόγω θεμάτων σταθερότητας, η δυνατότητα είναι απενεργοποιημένη από προεπιλογή. Για περισσότερες πληροφορίες, παρακαλώ δείτε την <link>τεκμηρίωση</link>.",
+    "storage_template_path_length": "Όριο μήκους διαδρομής: <b>{length, number}</b>/{limit, number}, κατά προσέγγιση",
+    "storage_template_settings": "Πρότυπο Αποθήκευσης",
+    "storage_template_settings_description": "Διαχείριση της δομής φακέλου και του ονόματος, του ανεβασμένου αρχείου",
+    "storage_template_user_label": "<code>{label}</code> είναι η Ετικέτα Αποθήκευσης του χρήστη",
+    "system_settings": "Ρυθμίσεις Συστήματος",
+    "tag_cleanup_job": "Καθαρισμός ετικετών",
+    "template_email_available_tags": "Μπορείτε να χρησιμοποιήσετε τις εξής μεταβλητές στο πρότυπό σας: {tags}",
+    "template_email_if_empty": "Αν το πρότυπο είναι κενό, θα χρησιμοποιηθεί το προεπιλεγμένο email.",
+    "template_email_invite_album": "Πρότυπο άλμπουμ πρόσκλησης",
+    "template_email_preview": "Προεπισκόπηση",
+    "template_email_settings": "Πρότυπα Email",
+    "template_email_settings_description": "Διαχείριση προσαρμοσμένων προτύπων ειδοποιήσεων email",
+    "template_email_update_album": "Ενημέρωση πρότυπου Άλμπουμ",
+    "template_email_welcome": "Πρότυπο email καλωσορίσματος",
+    "template_settings": "Πρότυπα ειδοποιήσεων",
+    "template_settings_description": "Διαχείριση προσαρμοσμένων προτύπων για ειδοποιήσεις.",
+    "theme_custom_css_settings": "Προσαρμοσμένο CSS",
+    "theme_custom_css_settings_description": "Τα Cascading Style Sheets(CSS) επιτρέπει την προσαρμογή του σχεδιασμού του Immich.",
+    "theme_settings": "Ρυθμίσεις Θέματος",
+    "theme_settings_description": "Διαχείριση της προσαρμογής του ιστότοπου του Immich",
+    "these_files_matched_by_checksum": "Αυτά τα αρχεία αντιστοιχίζονται με βάση τα checksums(μοναδικές αλγοριθμικές τιμές των περιεχομένων ενός αρχείου) τους",
+    "thumbnail_generation_job": "Δημιουργία Μικρογραφιών",
+    "thumbnail_generation_job_description": "Δημιουργία μεγάλων, μικρών και θολών μικρογραφιών για κάθε αρχείο, καθώς και μικρογραφιών για κάθε άτομο",
+    "transcoding_acceleration_api": "Επιτάχυνση API",
+    "transcoding_acceleration_api_description": "Το API που θα αλληλεπιδράσει με τη συσκευή σας για να επιταχύνει τη διαδικασία μετατροπής των δεδομένων. Αυτή η ρύθμιση είναι \"κατά το καλύτερο δυνατόν\": σε περίπτωση αποτυχίας, θα επιστραφεί στη μετατροπή δεομένων μέσω λογισμικού. Το VP9 ενδέχεται να λειτουργεί ή να μην λειτουργεί, ανάλογα με το υλικό σας.",
+    "transcoding_acceleration_nvenc": "NVENC (απαιτεί NVIDIA GPU)",
+    "transcoding_acceleration_qsv": "Quick Sync (απαιτεί επεξεργαστή Intel 7ης γενιάς ή νεότερο)",
+    "transcoding_acceleration_rkmpp": "RKMPP (μόνο σε Rockchip SOCs)",
+    "transcoding_acceleration_vaapi": "VAAPI",
+    "transcoding_accepted_audio_codecs": "Αποδεκτοί κωδικοποιητές ήχου",
+    "transcoding_accepted_audio_codecs_description": "Επιλέξτε ποιοι κωδικοποιητές ήχου δεν χρειάζεται να μετατραπούν. Χρησιμοποιείται μόνο για ορισμένες πολιτικές μετατροπής.",
+    "transcoding_accepted_containers": "Αποδεκτά κοντέινερ",
+    "transcoding_accepted_containers_description": "Επιλέξτε ποιοι τύποι κοντέινερ δεν χρειάζονται ανασυγκρότηση σε MP4. Χρησιμοποιείται μόνο για ορισμένες πολιτικές μετατροπής.",
+    "transcoding_accepted_video_codecs": "Αποδεκτοί κωδικοποιητές βίντεο",
+    "transcoding_accepted_video_codecs_description": "Επιλέξτε ποιοι κωδικοποιητές βίντεο δεν χρειάζεται να μετατραπούν. Χρησιμοποιείται μόνο για ορισμένες πολιτικές μετατροπής.",
+    "transcoding_advanced_options_description": "Επιλογές που οι περισσότεροι χρήστες δεν χρειάζεται να αλλάξουν",
+    "transcoding_audio_codec": "Κωδικοποιητής ήχου",
+    "transcoding_audio_codec_description": "Το Opus είναι η επιλογή για την υψηλότερη ποιότητα, αλλά έχει χαμηλότερη συμβατότητα με παλιές συσκευές ή λογισμικό.",
+    "transcoding_bitrate_description": "Βίντεο με ρυθμό μετάδοσης μεγαλύτερο από το μέγιστο ή που δεν είναι σε αποδεκτή μορφή",
+    "transcoding_codecs_learn_more": "Για να μάθετε περισσότερα για την ορολογία που χρησιμοποιείται εδώ, ανατρέξτε στην τεκμηρίωση του FFmpeg για τους κωδικοποιητές <h264-link>H.264</h264-link>, <hevc-link>HEVC</hevc-link> και <vp9-link>VP9</vp9-link>.",
+    "transcoding_constant_quality_mode": "Λειτουργία σταθερής ποιότητας",
+    "transcoding_constant_quality_mode_description": "Το ICQ είναι καλύτερο από το CQP, αλλά ορισμένες συσκευές επιτάχυνσης υλικού δεν υποστηρίζουν αυτήν τη λειτουργία. Η ρύθμιση αυτής της επιλογής θα προτιμήσει την καθορισμένη λειτουργία κατά τη χρήση κωδικοποίησης βάσει ποιότητας. Αγνοείται από το NVENC, καθώς δεν υποστηρίζει το ICQ.",
+    "transcoding_constant_rate_factor": "Σταθερός παράγοντας ρυθμού (-crf)",
+    "transcoding_constant_rate_factor_description": "Επίπεδο ποιότητας βίντεο. Οι τυπικές τιμές είναι οι, 23 για το H.264, 28 για το HEVC, 31 για το VP9 και 35 για το AV1. Χαμηλότερες τιμές σημαίνουν καλύτερη ποιότητα, αλλά παράγουν μεγαλύτερα αρχεία.",
+    "transcoding_disabled_description": "Να μην μετατραπεί κανένα βίντεο γιατί δύναται να προκαλέσει πρόβλημα αναπαραγωγής σε ορισμένες συσκευές/εφαρμογές",
+    "transcoding_hardware_acceleration": "Επιτάχυνση υλικού",
+    "transcoding_hardware_acceleration_description": "Πειραματικό· πολύ πιο γρήγορο, αλλά θα έχει χαμηλότερη ποιότητα με τον ίδιο ρυθμό μετάδοσης (bitrate)",
+    "transcoding_hardware_decoding": "Αποκωδικοποίηση μέσω υλικού",
+    "transcoding_hardware_decoding_setting_description": "Ενεργοποιεί την επιτάχυνση από άκρη σε άκρη αντί για μόνο επιτάχυνση της κωδικοποίησης. Μπορεί να μην λειτουργεί σε όλα τα βίντεο.",
+    "transcoding_hevc_codec": "Κωδικοποιητής HEVC",
+    "transcoding_max_b_frames": "Μέγιστος αριθμός B-frames(Bidirectional Predictive Frames)",
+    "transcoding_max_b_frames_description": "Οι υψηλότερες τιμές βελτιώνουν την αποδοτικότητα της συμπίεσης, αλλά επιβραδύνουν την κωδικοποίηση. Ενδέχεται να μην είναι συμβατές με την επιτάχυνση υλικού σε παλαιότερες συσκευές. Η τιμή 0 απενεργοποιεί τα B-frames, ενώ η -1, τη ρυθμίζει αυτόματα.",
+    "transcoding_max_bitrate": "Μέγιστος ρυθμός μετάδοσης (bitrate)",
+    "transcoding_max_bitrate_description": "Η ρύθμιση ενός μέγιστου ρυθμού μετάδοσης(bitrate) μπορεί να κάνει το μέγεθος των αρχείων πιο προβλέψιμο, αλλά με ένα μικρό κόστος στην ποιότητα. Στην ανάλυση των 720p, οι τυπικές τιμές είναι 2600k για VP9 ή HEVC, ή 4500k για H.264. Απενεργοποιείται εάν οριστεί σε 0.",
+    "transcoding_max_keyframe_interval": "Μέγιστο χρονικό διάστημα μεταξύ των καρέ αναφοράς (keyframe)",
+    "transcoding_max_keyframe_interval_description": "Ορίζει το μέγιστο διάστημα μεταξύ των καρέ αναφοράς. Χαμηλότερες τιμές μειώνουν την αποδοτικότητα συμπίεσης, αλλά βελτιώνουν τον χρόνο αναζήτησης και μπορεί να βελτιώσουν την ποιότητα σε σκηνές με γρήγορη κίνηση. Η τιμή 0 ρυθμίζει αυτό το διάστημα αυτόματα.",
+    "transcoding_optimal_description": "Βίντεο με ανώτερη ανάλυση από την επιθυμητή ή σε μη αποδεκτή μορφή",
+    "transcoding_preferred_hardware_device": "Προτιμώμενη συσκευή",
+    "transcoding_preferred_hardware_device_description": "Ισχύει μόνο για VAAPI και QSV. Ορίζει τον κόμβο DRI που χρησιμοποιείται για την επιτάχυνση υλικού κατά την κωδικοποίηση.",
+    "transcoding_preset_preset": "Προκαθορισμένη ρύθμιση (-preset)",
+    "transcoding_preset_preset_description": "Ταχύτητα συμπίεσης. Οι πιο αργές προκαθορισμένες ρυθμίσεις παράγουν μικρότερα αρχεία και βελτιώνουν την ποιότητα όταν στοχεύουν σε έναν συγκεκριμένο ρυθμό μετάδοσης (bitrate). Ο VP9 αγνοεί τις ταχύτητες πάνω από την 'πιο γρήγορη' (faster).",
+    "transcoding_reference_frames": "Καρέ αναφοράς",
+    "transcoding_reference_frames_description": "Ο αριθμός των καρέ που χρησιμοποιούνται ως αναφορά κατά τη συμπίεση ενός δεδομένου καρέ. Υψηλότερες τιμές βελτιώνουν την αποδοτικότητα της συμπίεσης, αλλά επιβραδύνουν την κωδικοποίηση. Η τιμή 0 ρυθμίζει αυτό τον αριθμό, αυτόματα.",
+    "transcoding_required_description": "Μόνο βίντεο που δεν είναι σε αποδεκτή μορφή",
+    "transcoding_settings": "Ρυθμίσεις μετατροπής βίντεο",
+    "transcoding_settings_description": "Διαχείριση της ανάλυσης και των πληροφοριών κωδικοποίησης των αρχείων βίντεο",
+    "transcoding_target_resolution": "Επιθυμητή ανάλυση",
+    "transcoding_target_resolution_description": "Οι υψηλότερες αναλύσεις μπορούν να διατηρήσουν περισσότερες λεπτομέρειες, αλλά απαιτούν περισσότερο χρόνο για κωδικοποίηση, παράγουν μεγαλύτερα αρχεία και μπορεί να μειώσουν την απόκριση της εφαρμογής.",
+    "transcoding_temporal_aq": "Χρονική Προσαρμοστική Ποιότητα AQ(Adaptive Quantization)",
+    "transcoding_temporal_aq_description": "Ισχύει μόνο για NVENC. Αυξάνει την ποιότητα σε σκηνές με υψηλή λεπτομέρεια και χαμηλή κίνηση. Ενδέχεται να μην είναι συμβατό με παλαιότερες συσκευές.",
+    "transcoding_threads": "Νήματα (παράλληλες διεργασίες)",
+    "transcoding_threads_description": "Οι υψηλότερες τιμές οδηγούν σε ταχύτερη κωδικοποίηση, αλλά αφήνουν λιγότερο χώρο στον διακομιστή για να επεξεργαστεί άλλες εργασίες όσο είναι ενεργή. Αυτή η τιμή δεν πρέπει να ξεπερνά τον αριθμό των πυρήνων του επεξεργαστή. Η μέγιστη αξιοποίηση επιτυγχάνεται αν οριστεί στο 0.",
+    "transcoding_tone_mapping": "Χαρτογράφηση χρωματικών τόνων",
+    "transcoding_tone_mapping_description": "Προσπαθεί να διατηρήσει την εμφάνιση των HDR βίντεο όταν μετατρέπονται σε SDR. Κάθε αλγόριθμος κάνει διαφορετικές επιλογές σχετικά με τα χρώματα, τις λεπτομέρειες και τη φωτεινότητα. Ο αλγόριθμος Hable διατηρεί τις λεπτομέρειες, ο Mobius διατηρεί τα χρώματα και ο Reinhard διατηρεί τη φωτεινότητα.",
+    "transcoding_transcode_policy": "Πολιτική μετατροπής (βίντεο / ήχου)",
+    "transcoding_transcode_policy_description": "Πολιτική για το πότε πρέπει να μετατραπεί ένα βίντεο. Τα βίντεο HDR θα μετατρέπονται πάντα (εκτός αν η μετατροπή είναι απενεργοποιημένη).",
+    "transcoding_two_pass_encoding": "Κωδικοποίηση δύο περασμάτων",
+    "transcoding_two_pass_encoding_setting_description": "Μετατροπή σε δύο περάσματα για την παραγωγή βίντεο με καλύτερη κωδικοποίηση. Όταν είναι ενεργοποιημένος ο μέγιστος ρυθμός μετάδοσης (απαραίτητος για λειτουργία με H.264 και HEVC), αυτή η λειτουργία χρησιμοποιεί ένα εύρος ρυθμού μετάδοσης βάσει του μέγιστου ρυθμού μετάδοσης και αγνοεί το CRF. Στον κωδικοποιητή VP9, το CRF μπορεί να χρησιμοποιηθεί εάν ο μέγιστος ρυθμός μετάδοσης είναι απενεργοποιημένος.",
+    "transcoding_video_codec": "Κωδικοποιητής Βίντεο",
+    "transcoding_video_codec_description": "Ο VP9 έχει υψηλή απόδοση και συμβατότητα με τον ιστότοπο, αλλά απαιτεί περισσότερο χρόνο για μετατροπή. Ο HEVC έχει παρόμοια απόδοση, αλλά χαμηλότερη συμβατότητα με τον ιστότοπο. Ο H.264 είναι ευρέως συμβατός και γρήγορος στη μετατροπή, αλλά παράγει πολύ μεγαλύτερα αρχεία. Ο AV1 είναι ο πιο αποδοτικός κωδικοποιητής, αλλά δεν υποστηρίζεται σε παλαιότερες συσκευές.",
+    "trash_enabled_description": "Ενεργοποίηση λειτουργιών Κάδου Απορριμμάτων",
+    "trash_number_of_days": "Αριθμός ημερών",
+    "trash_number_of_days_description": "Αριθμός ημερών παραμονής των αρχείων στον κάδο, πριν από την οριστική διαγραφή τους",
+    "trash_settings": "Ρυθμίσεις Κάδου Απορριμμάτων",
+    "trash_settings_description": "Διαχείριση ρυθίσεων κάδου απορριμμάτων",
+    "untracked_files": "Αρχεία εκτός παρακολούθησης",
+    "untracked_files_description": "Αυτά τα αρχεία δεν παρακολουθούνται από την εφαρμογή. Μπορεί να είναι αποτέλεσμα αποτυχημένων μετακινήσεων, διακοπών κατά τη μεταφόρτωση ή να έχουν παραμείνει λόγω κάποιου εσωτερικού σφάλματος",
+    "user_cleanup_job": "Εκκαθάριση χρηστών",
+    "user_delete_delay": "Ο λογαριασμός και τα αρχεία του/της <b>{user}</b> θα προγραμματιστούν για οριστική διαγραφή σε {delay, plural, one {# ημέρα} other {# ημέρες}}.",
+    "user_delete_delay_settings": "Καθυστέρηση διαγραφής",
+    "user_delete_delay_settings_description": "Αριθμός ημερών μετά την αφαίρεση, για την οριστική διαγραφή του λογαριασμού και των αρχείων ενός χρήστη. Η εργασία διαγραφής χρηστών εκτελείται τα μεσάνυχτα, για να ελέγξει ποιοι χρήστες είναι έτοιμοι για διαγραφή. Οι αλλαγές σε αυτή τη ρύθμιση θα αξιολογηθούν κατά την επόμενη εκτέλεση.",
+    "user_delete_immediately": "Ο λογαριασμός και τα αρχεία του/της <b>{user}</b> θα μπουν στην ουρά για οριστική διαγραφή, <b>άμεσα</b>.",
+    "user_delete_immediately_checkbox": "Βάλε τον χρήστη και τα αρχεία του στην ουρά για άμεση διαγραφή",
+    "user_management": "Διαχείριση χρηστών",
+    "user_password_has_been_reset": "Ο κωδικός πρόσβασης του χρήστη έχει επαναρυθμιστεί:",
+    "user_password_reset_description": "Παρακαλώ παρέχετε τον προσωρινό κωδικό πρόσβασης στον χρήστη και ενημερώστε τον ότι θα πρέπει να τον αλλάξει, κατά την επόμενη σύνδεσή του.",
+    "user_restore_description": "Ο λογαριασμός του/της <b>{user}</b> θα αποκατασταθεί.",
+    "user_restore_scheduled_removal": "Αποκατάσταση χρήστη - προγραμματισμένη διαγραφή στις {date, date, long}",
+    "user_settings": "Ρυθμίσεις χρήστη",
+    "user_settings_description": "Διαχείριση ρυθμίσεων χρήστη",
+    "user_successfully_removed": "Ο χρήστης {email} έχει αφαιρεθεί με επιτυχία.",
+    "version_check_enabled_description": "Ενεργοποίηση ελέγχου έκδοσης",
+    "version_check_implications": "Η λειτουργία ελέγχου έκδοσης, εξαρτάται από την περιοδική επικοινωνία με το github.com",
+    "version_check_settings": "Έλεγχος Έκδοσης",
+    "version_check_settings_description": "Ενεργοποίηση/απενεργοποίηση της ειδοποίησης για νέα έκδοση",
+    "video_conversion_job": "Μετατροπή βίντεο",
+    "video_conversion_job_description": "Μετατροπή βίντεο για μεγαλύτερη συμβατότητα με προγράμματα περιήγησης και συσκευές"
+  },
+  "admin_email": "Email Διαχειριστή",
+  "admin_password": "Κωδικός πρόσβασης Διαχειριστή",
+  "administration": "Διαχείριση",
+  "advanced": "Για προχωρημένους",
+  "age_months": "Η ηλικία {months, plural, one {# μήνας} other {# μήνες}}",
+  "age_year_months": "Ηλικία, 1 έτος, {months, plural, one {# μήνας} other {# μήνες}}",
+  "age_years": "{years, plural, other {Ηλικία #}}",
+  "album_added": "Το άλμπουμ, προστέθηκε",
+  "album_added_notification_setting_description": "Λάβετε ειδοποίηση μέσω email όταν προστεθείτε σε ένα κοινόχρηστο άλμπουμ",
+  "album_cover_updated": "Το εξώφυλλο του άλμπουμ, ενημερώθηκε",
+  "album_delete_confirmation": "Είστε σίγουροι ότι θέλετε να διαγράψετε το άλμπουμ {album};",
+  "album_delete_confirmation_description": "Εάν αυτό το άλμπουμ είναι κοινόχρηστο, οι άλλοι χρήστες δεν θα μπορούν να έχουν πρόσβαση.",
+  "album_info_updated": "Οι πληροφορίες του άλμπουμ, ενημερώθηκαν",
+  "album_leave": "Θέλετε να αποχωρήσετε από το άλμπουμ;",
+  "album_leave_confirmation": "Είστε σίγουροι ότι θέλετε να αποχωρήσετε από το {album};",
+  "album_name": "Ονομασία άλμπουμ",
+  "album_options": "Επιλογές άλμπουμ",
+  "album_remove_user": "Διαγραφή χρήστη;",
+  "album_remove_user_confirmation": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε τον/την {user};",
+  "album_share_no_users": "Φαίνεται ότι έχετε κοινοποιήσει αυτό το άλμπουμ σε όλους τους χρήστες ή δεν έχετε χρήστες για να το κοινοποιήσετε.",
+  "album_updated": "Το άλμπουμ, ενημερώθηκε",
+  "album_updated_setting_description": "Λάβετε ειδοποίηση μέσω email όταν ένα κοινόχρηστο άλμπουμ έχει νέα αρχεία",
+  "album_user_left": "Αποχωρήσατε από το {album}",
+  "album_user_removed": "Αφαιρέθηκε ο/η {user}",
+  "album_with_link_access": "Επιτρέψτε σε οποιονδήποτε έχει τον σύνδεσμο, να δει τις φωτογραφίες και τα άτομα σε αυτό το άλμπουμ.",
+  "albums": "Άλμπουμ",
+  "albums_count": "{count, plural, one {{count, number} Άλμπουμ} other {{count, number} Άλμπουμ}}",
+  "all": "Όλα",
+  "all_albums": "Όλα τα άλμπουμ",
+  "all_people": "Όλοι οι άνθρωποι",
+  "all_videos": "Όλα τα βίντεο",
+  "allow_dark_mode": "Επιτρέψτε τη σκοτεινή λειτουργία",
+  "allow_edits": "Επιτρέψτε τις τροποποιήσεις",
+  "allow_public_user_to_download": "Επιτρέψτε σε δημόσιο χρήστη να κατεβάσει",
+  "allow_public_user_to_upload": "Επιτρέψτε στον δημόσιο χρήστη να ανεβάσει",
+  "anti_clockwise": "Αντίθετα με τη φορά του ρολογιού",
+  "api_key": "Κλειδί API",
+  "api_key_description": "Αυτή η τιμή θα εμφανιστεί μόνο μία φορά. Παρακαλώ βεβαιωθείτε ότι την έχετε αντιγράψει πριν κλείσετε το παράθυρο.",
+  "api_key_empty": "Το όνομα του κλειδιού API, δεν πρέπει να είναι κενό",
+  "api_keys": "Κλειδιά API",
+  "app_settings": "Ρυθμίσεις εφαρμογής",
+  "appears_in": "Εμφανίζεται σε",
+  "archive": "Αρχείο",
+  "archive_or_unarchive_photo": "Αρχειοθέτηση ή αποαρχειοθέτηση φωτογραφίας",
+  "archive_size": "Μέγεθος Αρχείου",
+  "archive_size_description": "Ρυθμίστε το μέγεθος του αρχείου για λήψεις (σε GiB)",
+  "archived_count": "{count, plural, other {Αρχειοθετήθηκαν #}}",
+  "are_these_the_same_person": "Είναι το ίδιο άτομο;",
+  "are_you_sure_to_do_this": "Είστε σίγουροι ότι θέλετε να το κάνετε αυτό;",
+  "asset_added_to_album": "Προστέθηκε στο άλμπουμ",
+  "asset_adding_to_album": "Προστίθεται στο άλμπουμ...",
+  "asset_description_updated": "Η περιγραφή του αντικειμένου έχει ενημερωθεί",
+  "asset_filename_is_offline": "Το αντικείμενο {filename} είναι εκτός σύνδεσης",
+  "asset_has_unassigned_faces": "Το αντικείμενο έχει μη ανατεθειμένα πρόσωπα",
+  "asset_hashing": "Δημιουργία κατακερματισμού...",
+  "asset_offline": "Αντικείμενο εκτός σύνδεσης",
+  "asset_offline_description": "Αυτό το εξωτερικό αντικείμενο δεν βρέθηκε πλέον στον δίσκο. Παρακαλώ επικοινωνήστε με τον διαχειριστή του Immich για βοήθεια.",
+  "asset_skipped": "Παραλείφθηκε",
+  "asset_skipped_in_trash": "Στον κάδο απορριμμάτων",
+  "asset_uploaded": "Ανεβάστηκε",
+  "asset_uploading": "Ανεβάζεται...",
+  "assets": "Αντικείμενα",
+  "assets_added_count": "Προστέθηκε {count, plural, one {# αρχείο} other {# αρχεία}}",
+  "assets_added_to_album_count": "Προστέθηκε {count, plural, one {# αρχείο} other {# αρχεία}} στο άλμπουμ",
+  "assets_added_to_name_count": "Προστέθηκε {count, plural, one {# αρχείο} other {# αρχεία}} στο {hasName, select, true {<b>{name}</b>} other {νέο άλμπουμ}}",
+  "assets_count": "{count, plural, one {# αρχείο} other {# αρχεία}}",
+  "assets_moved_to_trash_count": "Μετακινήθηκε/καν {count, plural, one {# αρχείο} other {# αρχεία}} στον κάδο απορριμμάτων",
+  "assets_permanently_deleted_count": "Διαγράφηκε/καν μόνιμα {count, plural, one {# αρχείο} other {# αρχεία}}",
+  "assets_removed_count": "Αφαιρέθηκαν {count, plural, one {# αρχείο} other {# αρχεία}}",
+  "assets_restore_confirmation": "Είστε βέβαιοι ότι θέλετε να επαναφέρετε όλα τα στοιχεία που βρίσκονται στον κάδο απορριμμάτων; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί! Λάβετε υπόψη ότι δεν θα είναι δυνατή η επαναφορά στοιχείων εκτός σύνδεσης.",
+  "assets_restored_count": "Έγινε επαναφορά {count, plural, one {# στοιχείου} other {# στοιχείων}}",
+  "assets_trashed_count": "Μετακιν. στον κάδο απορριμάτων {count, plural, one {# στοιχείο} other {# στοιχεία}}",
+  "assets_were_part_of_album_count": "{count, plural, one {Το στοιχείο ανήκει} other {Τα στοιχεία ανήκουν}} ήδη στο άλμπουμ",
+  "authorized_devices": "Εξουσιοδοτημένες Συσκευές",
+  "back": "Πίσω",
+  "back_close_deselect": "Πίσω, κλείσιμο ή αποεπιλογή",
+  "backward": "Προς τα πίσω",
+  "birthdate_saved": "Η ημερομηνία γέννησης αποθηκεύτηκε επιτυχώς",
+  "birthdate_set_description": "Η ημερομηνία γέννησης χρησιμοποιείται για τον υπολογισμό της ηλικίας αυτού του ατόμου, τη χρονική στιγμή μιας φωτογραφίας.",
+  "blurred_background": "Θολό φόντο",
+  "bugs_and_feature_requests": "Σφάλματα & Αιτήματα Λειτουργιών",
+  "build": "Κατασκευή",
+  "build_image": "Κατασκευή Εικόνας",
+  "bulk_delete_duplicates_confirmation": "Είστε σίγουροι ότι θέλετε να διαγράψετε μαζικά {count, plural, one {# διπλότυπο αρχείο} other {# διπλότυπα αρχεία}}; Αυτό θα κρατήσει το μεγαλύτερο αρχείο από κάθε ομάδα και θα διαγράψει μόνιμα όλα τα υπόλοιπα διπλότυπα. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί!",
+  "bulk_keep_duplicates_confirmation": "Είστε σίγουροι ότι θέλετε να κρατήσετε {count, plural, one {# διπλότυπο αρχείο} other {# διπλότυπα αρχεία}}; Αυτό θα επιλύσει όλες τις ομάδες διπλοτύπων χωρίς να διαγράψει τίποτα.",
+  "bulk_trash_duplicates_confirmation": "Είστε σίγουροι ότι θέλετε να βάλετε στον κάδο απορριμμάτων {count, plural, one {# διπλότυπο αρχείο} other {# διπλότυπα αρχεία}}; Αυτό θα κρατήσει το μεγαλύτερο αρχείο από κάθε ομάδα και θα βάλει στον κάδο απορριμμάτων όλα τα άλλα διπλότυπα.",
+  "buy": "Αγοράστε το Immich",
+  "camera": "Κάμερα",
+  "camera_brand": "Μάρκα κάμερας",
+  "camera_model": "Μοντέλο κάμερας",
+  "cancel": "Ακύρωση",
+  "cancel_search": "Ακύρωση αναζήτησης",
+  "cannot_merge_people": "Αδύνατη η συγχώνευση προσώπων",
+  "cannot_undo_this_action": "Δεν μπορείτε να αναιρέσετε αυτήν την ενέργεια!",
+  "cannot_update_the_description": "Αδύνατη η ενημέρωση της περιγραφής",
+  "change_date": "Αλλαγή ημερομηνίας",
+  "change_expiration_time": "Αλλαγή χρόνου λήξης",
+  "change_location": "Αλλαγή τοποθεσίας",
+  "change_name": "Αλλαγή ονομασίας",
+  "change_name_successfully": "Επιτυχής αλλαγή ονομασίας",
+  "change_password": "Αλλαγή Κωδικού",
+  "change_password_description": "Αυτή είναι ή η πρώτη φορά που συνδέεστε στο σύστημα ή έχει γίνει αίτημα για αλλαγή του κωδικού σας. Παρακαλώ εισάγετε τον νέο κωδικό, παρακάτω.",
+  "change_your_password": "Αλλάξτε τον κωδικό σας",
+  "changed_visibility_successfully": "Η προβολή, άλλαξε με επιτυχία",
+  "check_all": "Επιλογή Όλων",
+  "check_logs": "Ελέγξτε τα αρχεία καταγραφής",
+  "choose_matching_people_to_merge": "Επιλέξτε τα αντίστοιχα άτομα για συγχώνευση",
+  "city": "Πόλη",
+  "clear": "Εκκαθάριση",
+  "clear_all": "Εκκαθάριση όλων",
+  "clear_all_recent_searches": "Εκκαθάριση όλων των πρόσφατων αναζητήσεων",
+  "clear_message": "Εκκαθάριση μηνύματος",
+  "clear_value": "Εκκαθάριση τιμής",
+  "clockwise": "Δεξιόστροφα",
+  "close": "Κλείσιμο",
+  "collapse": "Σύμπτυξη",
+  "collapse_all": "Σύμπτυξη όλων",
+  "color": "Χρώμα",
+  "color_theme": "Χρώμα θέματος",
+  "comment_deleted": "Το σχόλιο διαγράφηκε",
+  "comment_options": "Επιλογές σχολίου",
+  "comments_and_likes": "Σχόλια & αντιδράσεις (likes)",
+  "comments_are_disabled": "Τα σχόλια είναι απενεργοποιημένα",
+  "confirm": "Επιβεβαίωση",
+  "confirm_admin_password": "Επιβεβαίωση κωδικού Διαχειριστή",
+  "confirm_delete_shared_link": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτόν τον κοινόχρηστο σύνδεσμο;",
+  "confirm_keep_this_delete_others": "Όλα τα άλλα στοιχεία της στοίβας θα διαγραφούν, εκτός από αυτό το στοιχείο. Είστε σίγουροι ότι θέλετε να συνεχίσετε;",
+  "confirm_password": "Επιβεβαίωση κωδικού",
+  "contain": "Περιέχει",
+  "context": "Συμφραζόμενα",
+  "continue": "Συνέχεια",
+  "copied_image_to_clipboard": "Η εικόνα αντιγράφηκε στο πρόχειρο.",
+  "copied_to_clipboard": "Αντιγράφηκε στο πρόχειρο!",
+  "copy_error": "Σφάλμα αντιγραφής",
+  "copy_file_path": "Αντιγραφή διαδρομής αρχείου",
+  "copy_image": "Αντιγραφή Εικόνας",
+  "copy_link": "Αντιγραφή συνδέσμου",
+  "copy_link_to_clipboard": "Αντιγραφή συνδέσμου στο πρόχειρο",
+  "copy_password": "Αντιγραφή κωδικού",
+  "copy_to_clipboard": "Αντιγραφή στο πρόχειρο",
+  "country": "Χώρα",
+  "cover": "Εξώφυλλο",
+  "covers": "Εξώφυλλα",
+  "create": "Δημιουργία",
+  "create_album": "Δημιουργία άλμπουμ",
+  "create_library": "Δημιουργία Βιβλιοθήκης",
+  "create_link": "Δημιουργία συνδέσμου",
+  "create_link_to_share": "Δημιουργία συνδέσμου για διαμοιρασμό",
+  "create_link_to_share_description": "Επιτρέψτε σε οποιονδήποτε έχει τον σύνδεσμο να δει τη/τις επιλεγμένη/ες φωτογραφία/ες",
+  "create_new_person": "Δημιουργία νέου προσώπου",
+  "create_new_person_hint": "Αντιστοίχιση των επιλεγμένων αρχείων σε ένα νέο πρόσωπο",
+  "create_new_user": "Δημιουργία νέου χρήστη",
+  "create_tag": "Δημιουργία ετικέτας",
+  "create_tag_description": "Δημιουργία νέας ετικέτας. Για τις ένθετες ετικέτες, παρακαλώ εισάγετε τη πλήρη διαδρομή της, συμπεριλαμβανομένων των κάθετων διαχωριστικών.",
+  "create_user": "Δημιουργία χρήστη",
+  "created": "Δημιουργήθηκε",
+  "current_device": "Τρέχουσα συσκευή",
+  "custom_locale": "Προσαρμοσμένη Τοπική Ρύθμιση",
+  "custom_locale_description": "Μορφοποιήστε τις ημερομηνίες και τους αριθμούς, σύμφωνα με τη γλώσσα και την περιοχή",
+  "dark": "Σκούρο",
+  "date_after": "Ημερομηνία μετά",
+  "date_and_time": "Ημερομηνία και ώρα",
+  "date_before": "Ημερομηνία πριν",
+  "date_of_birth_saved": "Η ημερομηνία γέννησης αποθηκεύτηκε επιτυχώς",
+  "date_range": "Εύρος ημερομηνιών",
+  "day": "Ημέρα",
+  "deduplicate_all": "Αφαίρεση όλων των διπλότυπων",
+  "default_locale": "Προεπιλεγμένη Τοπική Ρύθμιση",
+  "default_locale_description": "Μορφοποιήστε τις ημερομηνίες και τους αριθμούς με βάση την τοπική ρύθμιση του προγράμματος περιήγησής σας",
+  "delete": "Διαγραφή",
+  "delete_album": "Διαγραφή άλμπουμ",
+  "delete_api_key_prompt": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό κλειδί API;",
+  "delete_duplicates_confirmation": "Είστε σίγουροι ότι επιθυμείτε τη μόνιμη διαγραφή αυτών των διπλότυπων;",
+  "delete_key": "Διαγραφή κλειδιού",
+  "delete_library": "Διαγραφή Βιβλιοθήκης",
+  "delete_link": "Διαγραφή συνδέσμου",
+  "delete_others": "Διαγραφή υπολοίπων",
+  "delete_shared_link": "Διαγραφή κοινόχρηστου συνδέσμου",
+  "delete_tag": "Διαγραφή ετικέτας",
+  "delete_tag_confirmation_prompt": "Είστε σίγουροι ότι θέλετε να διαγράψετε την ετικέτα {tagName};",
+  "delete_user": "Διαγραφή χρήστη",
+  "deleted_shared_link": "Ο κοινόχρηστος σύνδεσμος, διαγράφηκε",
+  "deletes_missing_assets": "Διαγράφει στοιχεία που λείπουν από το δίσκο",
+  "description": "Περιγραφή",
+  "details": "Λεπτομέρειες",
+  "direction": "Κατεύθυνση",
+  "disabled": "Απενεργοποιημένο",
+  "disallow_edits": "Απαγόρευση επεξεργασιών",
+  "discord": "Discord",
+  "discover": "Ανίχνευση",
+  "dismiss_all_errors": "Παράβλεψη όλων των σφαλμάτων",
+  "dismiss_error": "Παράβλεψη σφάλματος",
+  "display_options": "Επιλογές εμφάνισης",
+  "display_order": "Σειρά εμφάνισης",
+  "display_original_photos": "Εμφάνιση πρωτότυπων φωτογραφιών",
+  "display_original_photos_setting_description": "Προτίμηση εμφάνισης της αυθεντικής φωτογραφίας, κατά την προβολή ενός στοιχείου αντί για τη μικρογραφία του, όταν το αυθεντικό στοιχείο είναι συμβατό με τον ιστότοπο. Αυτό μπορεί να οδηγήσει σε πιο αργές ταχύτητες εμφάνισης φωτογραφιών.",
+  "do_not_show_again": "Να μην εμφανιστεί ξανά αυτό το μήνυμα",
+  "documentation": "Τεκμηρίωση",
+  "done": "Έγινε",
+  "download": "Λήψη",
+  "download_include_embedded_motion_videos": "Ενσωματωμένα βίντεο",
+  "download_include_embedded_motion_videos_description": "Συμπεριλάβετε τα βίντεο που είναι ενσωματωμένα σε κινούμενες φωτογραφίες ως ξεχωριστό αρχείο",
+  "download_settings": "Λήψη",
+  "download_settings_description": "Διαχείριση ρυθμίσεων που σχετίζονται με τη λήψη στοιχείων",
+  "downloading": "Γίνεται λήψη",
+  "downloading_asset_filename": "Λήψη στοιχείου {filename}",
+  "drop_files_to_upload": "Σύρετε αρχεία εδώ για να τα ανεβάσετε",
+  "duplicates": "Διπλότυπα",
+  "duplicates_description": "Επιλύστε κάθε ομάδα υποδεικνύοντας ποιες είναι διπλότυπες, εάν υπάρχουν",
+  "duration": "Διάρκεια",
+  "edit": "Επεξεργασία",
+  "edit_album": "Επεξεργασία άλμπουμ",
+  "edit_avatar": "Επεξεργασία άβαταρ",
+  "edit_date": "Επεξεργασία ημερομηνίας",
+  "edit_date_and_time": "Επεξεργασία ημερομηνίας και ώρας",
+  "edit_exclusion_pattern": "Επεξεργασία μοτίβου αποκλεισμού",
+  "edit_faces": "Επεξεργασία προσώπων",
+  "edit_import_path": "Επεξεργασία διαδρομής εισαγωγής",
+  "edit_import_paths": "Επεξεργασία Διαδρομών Εισαγωγής",
+  "edit_key": "Επεξεργασία κλειδιού",
+  "edit_link": "Επεξεργασία συνδέσμου",
+  "edit_location": "Επεξεργασία τοποθεσίας",
+  "edit_name": "Επεξεργασία ονόματος",
+  "edit_people": "Επεξεργασία ατόμων",
+  "edit_tag": "Επεξεργασία ετικέτας",
+  "edit_title": "Επεξεργασία Τίτλου",
+  "edit_user": "Επεξεργασία χρήστη",
+  "edited": "Επεξεργάστηκε",
+  "editor": "Επεξεργαστής",
+  "editor_close_without_save_prompt": "Αυτές οι αλλαγές δεν θα αποθηκευτούν",
+  "editor_close_without_save_title": "Κλείσιμο επεξεργαστή;",
+  "editor_crop_tool_h2_aspect_ratios": "Αναλογίες διαστάσεων",
+  "editor_crop_tool_h2_rotation": "Περιστροφή",
+  "email": "Email",
+  "empty_trash": "Άδειασμα κάδου απορριμμάτων",
+  "empty_trash_confirmation": "Είστε σίγουροι οτι θέλετε να αδειάσετε τον κάδο απορριμμάτων; Αυτό θα αφαιρέσει μόνιμα όλα τα στοιχεία του κάδου απορριμμάτων του Immich. \nΑυτή η ενέργεια δεν μπορεί να αναιρεθεί!",
+  "enable": "Ενεργοποίηση",
+  "enabled": "Ενεργοποιημένο",
+  "end_date": "Τελική ημερομηνία",
+  "error": "Σφάλμα",
+  "error_loading_image": "Σφάλμα κατά τη φόρτωση της εικόνας",
+  "error_title": "Σφάλμα - Κάτι πήγε στραβά",
+  "errors": {
+    "cannot_navigate_next_asset": "Δεν είναι δυνατή η πλοήγηση στο επόμενο στοιχείο",
+    "cannot_navigate_previous_asset": "Δεν είναι δυνατή η πλοήγηση στο προηγούμενο στοιχείο",
+    "cant_apply_changes": "Δεν είναι δυνατή η εφαρμογή των αλλαγών",
+    "cant_change_activity": "Δεν μπορείτε να {enabled, select, true {απενεργοποιήσετε} other {ενεργοποιήσετε}} τη δραστηριότητα",
+    "cant_change_asset_favorite": "Δεν μπορείτε να αλλάξετε το αγαπημένο για το στοιχείο",
+    "cant_change_metadata_assets_count": "Δεν μπορείτε να αλλάξετε τα μεταδεδομένα του {count, plural, one {# αρχείου} other {# αρχείων}}",
+    "cant_get_faces": "Δεν είναι δυνατή η ανάκτηση προσώπων",
+    "cant_get_number_of_comments": "Δεν είναι δυνατή η ανάκτηση του αριθμού των σχολίων",
+    "cant_search_people": "Δεν μπορείτε να αναζητήσετε άτομα",
+    "cant_search_places": "Δεν μπορείτε να αναζητήσετε τοποθεσίες",
+    "cleared_jobs": "Εκκαθαρισμένες εργασίες για: {job}",
+    "error_adding_assets_to_album": "Σφάλμα κατά την προσθήκη στοιχείων στο άλμπουμ",
+    "error_adding_users_to_album": "Σφάλμα κατά την προσθήκη χρηστών στο άλμπουμ",
+    "error_deleting_shared_user": "Σφάλμα διαγραφής κοινόχρηστου χρήστη",
+    "error_downloading": "Σφάλμα λήψης {filename}",
+    "error_hiding_buy_button": "Σφάλμα απόκρυψης κουμπιού αγοράς",
+    "error_removing_assets_from_album": "Σφάλμα αφαίρεσης στοιχείων από το άλμπουμ, ελέγξτε την κονσόλα για περισσότερες λεπτομέρειες",
+    "error_selecting_all_assets": "Σφάλμα κατά την επιλογή όλων των στοιχείων",
+    "exclusion_pattern_already_exists": "Αυτό το μοτίβο αποκλεισμού υπάρχει ήδη.",
+    "failed_job_command": "Η εντολή {command} απέτυχε για την εργασία: {job}",
+    "failed_to_create_album": "Αποτυχία δημιουργίας άλμπουμ",
+    "failed_to_create_shared_link": "Αποτυχία δημιουργίας κοινόχρηστου συνδέσμου",
+    "failed_to_edit_shared_link": "Αποτυχία επεξεργασίας κοινόχρηστου συνδέσμου",
+    "failed_to_get_people": "Αποτυχία ανάκτησης ατόμων",
+    "failed_to_keep_this_delete_others": "Αποτυχία διατήρησης αυτού του στοιχείου και διαγραφής των υπόλοιπων στοιχείων",
+    "failed_to_load_asset": "Αποτυχία φόρτωσης στοιχείου",
+    "failed_to_load_assets": "Αποτυχία φόρτωσης στοιχείων",
+    "failed_to_load_people": "Αποτυχία φόρτωσης ατόμων",
+    "failed_to_remove_product_key": "Αποτυχία αφαίρεσης κλειδιού προϊόντος",
+    "failed_to_stack_assets": "Αποτυχία στην συμπίεση των στοιχείων",
+    "failed_to_unstack_assets": "Αποτυχία στην αποσυμπίεση των στοιχείων",
+    "import_path_already_exists": "Αυτή η διαδρομή εισαγωγής υπάρχει ήδη.",
+    "incorrect_email_or_password": "Λανθασμένο email ή κωδικός πρόσβασης",
+    "paths_validation_failed": "{paths, plural, one {# διαδρομή} other {# διαδρομές}} απέτυχαν κατά την επικύρωση",
+    "profile_picture_transparent_pixels": "Οι εικόνες προφίλ δεν μπορούν να έχουν διαφανή εικονοστοιχεία. Παρακαλώ μεγεθύνετε ή/και μετακινήστε την εικόνα.",
+    "quota_higher_than_disk_size": "Έχετε ορίσει ένα όριο, μεγαλύτερο από το μέγεθος του δίσκου",
+    "repair_unable_to_check_items": "Αδυναμία ελέγχου {count, select, one {στοιχείου} other {στοιχείων}}",
+    "unable_to_add_album_users": "Αδυναμία προσθήκης χρήστη στο άλμπουμ",
+    "unable_to_add_assets_to_shared_link": "Αδυναμία προσθήκης στοιχείου στον κοινόχρηστο σύνδεσμο",
+    "unable_to_add_comment": "Αδυναμία προσθήκης σχολίου",
+    "unable_to_add_exclusion_pattern": "Αδυναμία προσθήκης μοτίβου αποκλεισμού",
+    "unable_to_add_import_path": "Αδυναμία προσθήκης διαδρομής εισαγωγής",
+    "unable_to_add_partners": "Αδυναμία προσθήκης συνεργατών",
+    "unable_to_add_remove_archive": "Αδυναμία {archived, select, true {αφαίρεσης του στοιχείου από το} other {προσθήκης του στοιχείου στο}} αρχείο",
+    "unable_to_add_remove_favorites": "Αδυναμία {favorite, select, true {προσθήκης του στοιχείου στα} other {αφαίρεσης του στοιχείου από τα}} αγαπημένα",
+    "unable_to_archive_unarchive": "Αδυναμία {archived, select, true {αρχειοθέτησης} other {αποαρχειοθέτησης}}",
+    "unable_to_change_album_user_role": "Αδυναμία αλλαγής του ρόλου του χρήστη στο άλμπουμ",
+    "unable_to_change_date": "Αδυναμία αλλάγης της ημερομηνίας",
+    "unable_to_change_favorite": "Αδυναμία αλλαγής αγαπημένου για το στοιχείο",
+    "unable_to_change_location": "Αδυναμία αλλαγής της τοποθεσίας",
+    "unable_to_change_password": "Αδυναμία αλλαγής του κωδικού πρόσβασης",
+    "unable_to_change_visibility": "Αδυναμία αλλαγής της προβολής για {count, plural, one {# άτομο} other {# άτομα}}",
+    "unable_to_complete_oauth_login": "Αδυναμία ολοκλήρωσης σύνδεσης μέσω OAuth",
+    "unable_to_connect": "Αδυναμία σύνδεσης",
+    "unable_to_connect_to_server": "Αδυναμία σύνδεσης με το διακομιστή",
+    "unable_to_copy_to_clipboard": "Αδυναμία αντιγραφής στο πρόχειρο, βεβαιωθείτε ότι έχετε πρόσβαση στη σελίδα μέσω https",
+    "unable_to_create_admin_account": "Αδυναμία δημιουργίας λογαριασμού διαχειριστή",
+    "unable_to_create_api_key": "Αδυναμία δημιουργίας ενός νέου κλειδιού API",
+    "unable_to_create_library": "Αδυναμία δημιουργίας βιβλιοθήκης",
+    "unable_to_create_user": "Αδυναμία δημιουργίας χρήστη",
+    "unable_to_delete_album": "Αδυναμία διαγραφής άλμπουμ",
+    "unable_to_delete_asset": "Αδυναμία διαγραφής στοιχείου",
+    "unable_to_delete_assets": "Σφάλμα κατα τη διαγραφή στοιχείων",
+    "unable_to_delete_exclusion_pattern": "Αδυναμία διαγραφής μοτίβου αποκλεισμού",
+    "unable_to_delete_import_path": "Αδυναμία διαγραφής διαδρομής εισαγωγής",
+    "unable_to_delete_shared_link": "Αδυναμία διαγραφής κοινόχρηστου συνδέσμου",
+    "unable_to_delete_user": "Αδυναμία διαγραφής χρήστη",
+    "unable_to_download_files": "Αδυναμία λήψης αρχείων",
+    "unable_to_edit_exclusion_pattern": "Αδυναμία επεξεργασίας μοτίβου αποκλεισμού",
+    "unable_to_edit_import_path": "Αδυναμία επεξεργασίας διαδρομής εισαγωγής",
+    "unable_to_empty_trash": "Αδυναμία αδειάσματος του κάδου απορριμμάτων",
+    "unable_to_enter_fullscreen": "Αδυναμία μετάβασης σε πλήρη οθόνη",
+    "unable_to_exit_fullscreen": "Αδυναμία εξόδου από πλήρη οθόνη",
+    "unable_to_get_comments_number": "Αδυναμία ανάκτησης του αριθμού των σχολίων",
+    "unable_to_get_shared_link": "Αδυναμία ανάκτησης κοινόχρηστου συνδέσμου",
+    "unable_to_hide_person": "Αδυναμία απόκρυψης του ατόμου",
+    "unable_to_link_motion_video": "Αδυναμία σύνδεσης βίντεο κίνησης",
+    "unable_to_link_oauth_account": "Αδυναμία σύνδεσης λογαριασμού OAuth",
+    "unable_to_load_album": "Αδυναμία φόρτωσης άλμπουμ",
+    "unable_to_load_asset_activity": "Αδυναμία φόρτωσης της δραστηριότητας του στοιχείου",
+    "unable_to_load_items": "Αδυναμία φόρτωσης αντικειμένων",
+    "unable_to_load_liked_status": "Αδυναμία φόρτωσης της κατάστασης \"μου αρέσει\"",
+    "unable_to_log_out_all_devices": "Αδυναμία αποσύνδεσης όλων των συσκευών",
+    "unable_to_log_out_device": "Αδυναμία αποσύνδεσης της συσκευής",
+    "unable_to_login_with_oauth": "Αδυναμία εισόδου μέσω OAuth",
+    "unable_to_play_video": "Αδυναμία αναπαραγωγής βίντεο",
+    "unable_to_reassign_assets_existing_person": "Αδυναμία επανακατηγοριοποίησης των στοιχείων στον/στην {name, select, null {υπάρχον άτομο} other {{name}}}",
+    "unable_to_reassign_assets_new_person": "Αδυναμία επανακατηγοριοποίησης των στοιχείων σε ένα νέο άτομο",
+    "unable_to_refresh_user": "Αδυναμία ανανέωσης χρήστη",
+    "unable_to_remove_album_users": "Αδυναμία διαγραφής χρηστών από το άλμπουμ",
+    "unable_to_remove_api_key": "Αδυναμία διαγραφής του κλειδιού API",
+    "unable_to_remove_assets_from_shared_link": "Αδυναμία διαγραφής στοιχείων από τον κοινόχρηστο σύνδεσμο",
+    "unable_to_remove_deleted_assets": "Αδυναμία αφαίρεσης αρχείων εκτός σύνδεσης",
+    "unable_to_remove_library": "Αδυναμία αφαίρεσης βιβλιοθήκης",
+    "unable_to_remove_partner": "Αδυναμία αφαίρεσης συνεργάτη",
+    "unable_to_remove_reaction": "Αδυναμία αφαίρεσης της αντίδρασης",
+    "unable_to_repair_items": "Αδυναμία επισκευής αντικειμένων",
+    "unable_to_reset_password": "Αδυναμία επαναφοράς κωδικού πρόσβασης",
+    "unable_to_resolve_duplicate": "Αδυναμία επίλυσης του διπλότυπου",
+    "unable_to_restore_assets": "Αδυναμία επαναφοράς των στοιχείων",
+    "unable_to_restore_trash": "Αδυναμία επαναφοράς του κάδου απορριμμάτων",
+    "unable_to_restore_user": "Αδυναμία επαναφοράς χρήστη",
+    "unable_to_save_album": "Αδυναμία αποθήκευσης άλμπουμ",
+    "unable_to_save_api_key": "Αδυναμία αποθήκευσης κλειδιού API",
+    "unable_to_save_date_of_birth": "Αδυναμία αποθήκευσης ημερομηνίας γέννησης",
+    "unable_to_save_name": "Αδυναμία αποθήκευσης ονόματος",
+    "unable_to_save_profile": "Αδυναμία αποθήκευσης προφίλ",
+    "unable_to_save_settings": "Αδυναμία αποθήκευσης ρυθμίσεων",
+    "unable_to_scan_libraries": "Αδυναμία σάρωσης βιβλιοθηκών",
+    "unable_to_scan_library": "Αδυναμία σάρωσης βιβλιοθήκης",
+    "unable_to_set_feature_photo": "Αδυναμία ορισμού φωτογραφίας χαρακτηριστικού",
+    "unable_to_set_profile_picture": "Αδυναμία ορισμού φωτογραφίας προφίλ",
+    "unable_to_submit_job": "Αδυναμία υποβολής εργασίας",
+    "unable_to_trash_asset": "Αδυναμία μετακίνησης του στοιχείου στον κάδο απορριμμάτων",
+    "unable_to_unlink_account": "Αδυναμία αποσύνδεσης του λογαριασμού",
+    "unable_to_unlink_motion_video": "Αδυναμία αποσύνδεσης βίντεο κίνησης",
+    "unable_to_update_album_cover": "Αδυναμία ανανέωσης του εξώφυλλου του άλμπουμ",
+    "unable_to_update_album_info": "Αδυναμία ανανέωσης των πληροφοριών του άλμπουμ",
+    "unable_to_update_library": "Αδυναμία ανανέωσης της βιβλιοθήκης",
+    "unable_to_update_location": "Αδυναμία ανανέωσης της τοποθεσίας",
+    "unable_to_update_settings": "Αδυναμία ανανέωσης των ρυθμίσεων",
+    "unable_to_update_timeline_display_status": "Αδυναμία ενημέρωσης κατάστασης της προβολής χρονολογίας",
+    "unable_to_update_user": "Αδυναμία ενημέρωσης του χρήστη",
+    "unable_to_upload_file": "Αδυναμία μεταφόρτωσης αρχείου"
+  },
+  "exif": "Μεταδεδομένα Exif",
+  "exit_slideshow": "Έξοδος από την παρουσίαση",
+  "expand_all": "Ανάπτυξη όλων",
+  "expire_after": "Λήγει μετά από",
+  "expired": "Έληξε",
+  "expires_date": "Λήγει {date}",
+  "explore": "Περιήγηση",
+  "explorer": "Περιηγητής",
+  "export": "Εξαγωγή",
+  "export_as_json": "Εξαγωγή ως JSON",
+  "extension": "Επέκταση",
+  "external": "Εξωτερικός",
+  "external_libraries": "Εξωτερικές βιβλιοθήκες",
+  "face_unassigned": "Μη ανατεθειμένο",
+  "failed_to_load_assets": "Αποτυχία φόρτωσης στοιχείων",
+  "favorite": "Αγαπημένο",
+  "favorite_or_unfavorite_photo": "Ορίστε μία φωτογραφία ως αγαπημένη ή αφαιρέστε την από τα αγαπημένα",
+  "favorites": "Αγαπημένα",
+  "feature_photo_updated": "Η φωτογραφία προβολής ενημερώθηκε",
+  "features": "Χαρακτηριστικά",
+  "features_setting_description": "Διαχειριστείτε τα χαρακτηριστικά της εφαρμογής",
+  "file_name": "Όνομα αρχείου",
+  "file_name_or_extension": "Όνομα αρχείου ή επέκταση",
+  "filename": "Ονομασία αρχείου",
+  "filetype": "Τύπος αρχείου",
+  "filter_people": "Φιλτράρισμα ατόμων",
+  "find_them_fast": "Βρείτε τους γρήγορα με αναζήτηση κατά όνομα",
+  "fix_incorrect_match": "Διόρθωση λανθασμένης αντιστοίχισης",
+  "folders": "Φάκελοι",
+  "folders_feature_description": "Περιήγηση στην προβολή φακέλου για τις φωτογραφίες και τα βίντεο στο σύστημα αρχείων",
+  "forward": "Προς τα εμπρός",
+  "general": "Γενικά",
+  "get_help": "Ζητήστε βοήθεια",
+  "getting_started": "Ξεκινώντας",
+  "go_back": "Πηγαίνετε πίσω",
+  "go_to_search": "Πηγαίνετε στην αναζήτηση",
+  "group_albums_by": "Ομαδοποίηση άλμπουμ κατά...",
+  "group_no": "Καμία ομοδοποίηση",
+  "group_owner": "Ομαδοποίηση κατά ιδιοκτήτη",
+  "group_year": "Ομαδοποίηση κατά έτος",
+  "has_quota": "Έχει ποσόστωση",
+  "hi_user": "Γειά σου {name} {email}",
+  "hide_all_people": "Απόκρυψη όλων των ατόμων",
+  "hide_gallery": "Απόκρυψη γκαλερί",
+  "hide_named_person": "Απόκρυψη του ατόμου {name}",
+  "hide_password": "Απόκρυψη κωδικού πρόσβασης",
+  "hide_person": "Απόκρυψη ατόμου",
+  "hide_unnamed_people": "Απόκρυψη ατόμων χωρίς όνομα",
+  "host": "Φιλοξενία",
+  "hour": "Ώρα",
+  "image": "Εικόνα",
+  "image_alt_text_date": "{isVideo, select, true {Βίντεο} other {Εικόνα}} που τραβήχτηκε στις {date}",
+  "image_alt_text_date_1_person": "{isVideo, select, true {Βίντεο} other {Εικόνα}} που τραβήχτηκε με τον/την {person1} στις {date}",
+  "image_alt_text_date_2_people": "{isVideo, select, true {Βίντεο} other {Εικόνα}} που τραβήχτηκε με τον/την {person1} και τον/την {person2} στις {date}",
+  "image_alt_text_date_3_people": "{isVideo, select, true {Βίντεο} other {Εικόνα}} που τραβήχτηκε με τον/την {person1}, {person2} και τον/την {person3} στις {date}",
+  "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Βίντεο} other {Εικόνα}} τραβήχτηκε με τον/την {person1}, τον/την {person2} και άλλους {additionalCount, number} στις {date}",
+  "image_alt_text_date_place": "{isVideo, select, true {Βίντεο} other {Εικόνα}} τραβήχτηκε στον/στην {city}, {country} στις {date}",
+  "image_alt_text_date_place_1_person": "{isVideo, select, true {Βίντεο} other {Εικόνα}} τραβηγμένο στην/στον {city}, {country} με τον/την {person1} στις {date}",
+  "image_alt_text_date_place_2_people": "{isVideo, select, true {Βίντεο} other {Εικόνα}} τραβηγμένο στην/στον {city}, {country} με τον/την {person1} και τον/την {person2} στις {date}",
+  "image_alt_text_date_place_3_people": "{isVideo, select, true {Βίντεο} other {Εικόνα}} τραβηγμένο στην/στον {city}, {country} με τον/την {person1}, τον/την {person2} και τον/την {person3} στις {date}",
+  "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Βίντεο} other {Εικόνα}} τραβηγμένο στον/στην {city}, {country} με τον/την {person1}, {person2}, και {additionalCount, number} άλλους στις {date}",
+  "immich_logo": "Λογότυπο Immich",
+  "immich_web_interface": "Ιστότοπος Immich",
+  "import_from_json": "Εισαγωγή από αρχείο JSON",
+  "import_path": "Εισαγωγή διαδρομής",
+  "in_albums": "Μέσα σε {count, plural, one {# άλμπουμ} other {# άλμπουμ}}",
+  "in_archive": "Μέσα στα αρχειοθετημένα",
+  "include_archived": "Συμπερίληψη αρχειοθετημένων",
+  "include_shared_albums": "Συμπερίληψη διαμοιρασμένων άλμπουμ",
+  "include_shared_partner_assets": "Συμπερίληψη των στοιχείων των συνεργατών που έχουν κοινοποιηθεί",
+  "individual_share": "Μεμονωμένος διαμοιρασμός",
+  "info": "Πληροφορίες",
+  "interval": {
+    "day_at_onepm": "Κάθε μέρα στη 1μμ",
+    "hours": "Κάθε {hours, plural, one {ώρα} other {{hours, number} ώρες}}",
+    "night_at_midnight": "Κάθε βράδυ τα μεσάνυχτα",
+    "night_at_twoam": "Κάθε βράδυ στις 2πμ"
+  },
+  "invite_people": "Πρόσκληση Ατόμων",
+  "invite_to_album": "Πρόσκληση σε άλμπουμ",
+  "items_count": "{count, plural, one {# αντικείμενο} other {# αντικείμενα}}",
+  "jobs": "Εργασίες",
+  "keep": "Διατήρηση",
+  "keep_all": "Διατήρηση Όλων",
+  "keep_this_delete_others": "Διατήρηση αυτού, διαγραφή υπολοίπων",
+  "kept_this_deleted_others": "Διατηρήθηκε αυτό το στοιχείο και διαγράφηκε/καν {count, plural, one {# στοιχείο} other {# στοιχεία}}",
+  "keyboard_shortcuts": "Συντομεύσεις πληκτρολογίου",
+  "language": "Γλώσσα",
+  "language_setting_description": "Επιλέξτε τη γλώσσα που προτιμάτε",
+  "last_seen": "Τελευταία προβολή",
+  "latest_version": "Τελευταία Έκδοση",
+  "latitude": "Γεωγραφικό πλάτος",
+  "leave": "Εγκατάλειψη",
+  "let_others_respond": "Επέτρεψε σε άλλους να απαντήσουν",
+  "level": "Επίπεδο",
+  "library": "Βιβλιοθήκη",
+  "library_options": "Επιλογές βιβλιοθήκης",
+  "light": "Φωτεινό",
+  "like_deleted": "Το \"μου αρέσει\" διαγράφηκε",
+  "link_motion_video": "Σύνδεσε βίντεο κίνησης",
+  "link_options": "Επιλογές συνδέσμου",
+  "link_to_oauth": "Σύνδεση στον OAuth",
+  "linked_oauth_account": "Ο OAuth λογαριασμός συνδέθηκε",
+  "list": "Λίστα",
+  "loading": "Φόρτωση",
+  "loading_search_results_failed": "Η φόρτωση αποτελεσμάτων αναζήτησης απέτυχε",
+  "log_out": "Αποσύνδεση",
+  "log_out_all_devices": "Αποσύνδεση από Όλες τις Συσκευές",
+  "logged_out_all_devices": "Όλες οι συσκευές αποσυνδέθηκαν",
+  "logged_out_device": "Αποσυνδεδεμένη συσκευή",
+  "login": "Είσοδος",
+  "login_has_been_disabled": "Η σύνδεση έχει απενεργοποιηθεί.",
+  "logout_all_device_confirmation": "Είστε βέβαιοι ότι θέλετε να αποσυνδεθείτε από όλες τις συσκευές;",
+  "logout_this_device_confirmation": "Είστε βέβαιοι ότι θέλετε να αποσυνδεθείτε από αυτήν τη συσκευή;",
+  "longitude": "Γεωγραφικό μήκος",
+  "look": "Εμφάνιση",
+  "loop_videos": "Επανάληψη βίντεο",
+  "loop_videos_description": "Ενεργοποιήστε την αυτόματη επανάληψη ενός βίντεο στο πρόγραμμα προβολής λεπτομερειών.",
+  "main_branch_warning": "Χρησιμοποιείτε μια έκδοση σε ανάπτυξη· συνιστούμε ανεπιφύλακτα τη χρήση μιας επίσημης έκδοσης!",
+  "make": "Κατασκευαστής",
+  "manage_shared_links": "Διαχείριση κοινόχρηστων συνδέσμων",
+  "manage_sharing_with_partners": "Διαχειριστείτε την κοινή χρήση με συνεργάτες",
+  "manage_the_app_settings": "Διαχειριστείτε τις ρυθμίσεις της εφαρμογής",
+  "manage_your_account": "Διαχειριστείτε τον λογαριασμό σας",
+  "manage_your_api_keys": "Διαχειριστείτε τα κλειδιά API",
+  "manage_your_devices": "Διαχειριστείτε τις συνδεδεμένες συσκευές σας",
+  "manage_your_oauth_connection": "Διαχειριστείτε τη σύνδεσή σας OAuth",
+  "map": "Χάρτης",
+  "map_marker_for_images": "Δείκτης χάρτη για εικόνες που τραβήχτηκαν σε {city}, {country}",
+  "map_marker_with_image": "Χάρτης δείκτη με εικόνα",
+  "map_settings": "Ρυθμίσεις χάρτη",
+  "matches": "Αντιστοιχίες",
+  "media_type": "Τύπος πολυμέσου",
+  "memories": "Αναμνήσεις",
+  "memories_setting_description": "Διαχειριστείτε τι θα εμφανίζεται στις αναμνήσεις σας",
+  "memory": "Ανάμνηση",
+  "memory_lane_title": "Διαδρομή Αναμνήσεων {title}",
+  "menu": "Μενού",
+  "merge": "Συγχώνευση",
+  "merge_people": "Συγχώνευση ατόμων",
+  "merge_people_limit": "Μπορείτε να συγχωνεύσετε μόνο έως και 5 πρόσωπα τη φορά",
+  "merge_people_prompt": "Θέλετε να συγχωνεύσετε αυτά τα άτομα; Αυτή η ενέργεια είναι μη αναστρέψιμη.",
+  "merge_people_successfully": "Τα άτομα συγχωνεύθηκαν με επιτυχία",
+  "merged_people_count": "Έγινε συγχώνευση {count, plural, one {# ατόμου} other {# ατόμων}}",
+  "minimize": "Ελαχιστοποίηση",
+  "minute": "Λεπτό",
+  "missing": "Όσα Λείπουν",
+  "model": "Μοντέλο",
+  "month": "Μήνας",
+  "more": "Περισσότερα",
+  "moved_to_trash": "Μετακινήθηκε στον κάδο απορριμμάτων",
+  "my_albums": "Τα άλμπουμ μου",
+  "name": "Όνομα",
+  "name_or_nickname": "Όνομα ή ψευδώνυμο",
+  "never": "Ποτέ",
+  "new_album": "Νέο Άλμπουμ",
+  "new_api_key": "Νέο API Key",
+  "new_password": "Νέος κωδικός πρόσβασης",
+  "new_person": "Νέο άτομο",
+  "new_user_created": "Ο νέος χρήστης δημιουργήθηκε",
+  "new_version_available": "ΔΙΑΘΕΣΙΜΗ ΝΕΑ ΕΚΔΟΣΗ",
+  "newest_first": "Τα νεότερα πρώτα",
+  "next": "Επόμενο",
+  "next_memory": "Επόμενη ανάμνηση",
+  "no": "Όχι",
+  "no_albums_message": "Δημιουργήστε ένα άλμπουμ για να οργανώσετε τις φωτογραφίες και τα βίντεό σας",
+  "no_albums_with_name_yet": "Φαίνεται ότι δεν έχετε κανένα άλμπουμ με αυτό το όνομα ακόμα.",
+  "no_albums_yet": "Φαίνεται ότι δεν έχετε κανένα άλμπουμ ακόμα.",
+  "no_archived_assets_message": "Αρχειοθετήστε φωτογραφίες και βίντεο για να τα αποκρύψετε από την Προβολή Φωτογραφιών",
+  "no_assets_message": "ΚΑΝΤΕ ΚΛΙΚ ΓΙΑ ΝΑ ΑΝΕΒΑΣΕΤΕ ΤΗΝ ΠΡΩΤΗ ΣΑΣ ΦΩΤΟΓΡΑΦΙΑ",
+  "no_duplicates_found": "Δεν βρέθηκαν διπλότυπα.",
+  "no_exif_info_available": "Καμία πληροφορία exif διαθέσιμη",
+  "no_explore_results_message": "Ανεβάστε περισσότερες φωτογραφίες για να περιηγηθείτε στη συλλογή σας.",
+  "no_favorites_message": "Προσθέστε αγαπημένα για να βρείτε γρήγορα τις καλύτερες φωτογραφίες και τα βίντεό σας",
+  "no_libraries_message": "Δημιουργήστε μια εξωτερική βιβλιοθήκη για να προβάλετε τις φωτογραφίες και τα βίντεό σας",
+  "no_name": "Χωρίς Όνομα",
+  "no_places": "Καμία τοποθεσία",
+  "no_results": "Κανένα αποτέλεσμα",
+  "no_results_description": "Δοκιμάστε ένα συνώνυμο ή πιο γενική λέξη-κλειδί",
+  "no_shared_albums_message": "Δημιουργήστε ένα άλμπουμ για να μοιράζεστε φωτογραφίες και βίντεο με άτομα στο δίκτυό σας",
+  "not_in_any_album": "Σε κανένα άλμπουμ",
+  "note_apply_storage_label_to_previously_uploaded assets": "Σημείωση: Για να εφαρμόσετε την Ετικέτα Αποθήκευσης σε στοιχεία που έχουν μεταφορτωθεί προηγουμένως, εκτελέστε το",
+  "note_unlimited_quota": "Σημείωση: Εισαγάγετε 0 για απεριόριστο όριο",
+  "notes": "Σημειώσεις",
+  "notification_toggle_setting_description": "Ενεργοποίηση ειδοποιήσεων μέσω email",
+  "notifications": "Ειδοποιήσεις",
+  "notifications_setting_description": "Διαχείριση ειδοποιήσεων",
+  "oauth": "OAuth",
+  "official_immich_resources": "Επίσημοι Πόροι του Immich",
+  "offline": "Εκτός σύνδεσης",
+  "offline_paths": "Διαδρομές εκτός σύνδεσης",
+  "offline_paths_description": "Αυτά τα αποτελέσματα μπορεί να οφείλονται στη μη αυτόματη διαγραφή αρχείων που δεν αποτελούν μέρος μιας εξωτερικής βιβλιοθήκης.",
+  "ok": "Έγινε",
+  "oldest_first": "Τα παλαιότερα πρώτα",
+  "onboarding": "Οδηγός εκκίνησης",
+  "onboarding_privacy_description": "Οι παρακάτω (προαιρετικές) λειτουργίες βασίζονται σε εξωτερικές υπηρεσίες και μπορούν να απενεργοποιηθούν ανά πάσα στιγμή από τις ρυθμίσεις διαχείρισης.",
+  "onboarding_theme_description": "Επιλέξτε ένα θέμα χρώματος για το προφίλ σας. Μπορείτε να το αλλάξετε αργότερα στις ρυθμίσεις σας.",
+  "onboarding_welcome_description": "Ας ρυθμίσουμε το προφίλ σας με ορισμένες κοινές ρυθμίσεις.",
+  "onboarding_welcome_user": "Καλωσόρισες, {user}",
+  "online": "Σε σύνδεση",
+  "only_favorites": "Μόνο αγαπημένα",
+  "open_in_map_view": "Άνοιγμα σε προβολή χάρτη",
+  "open_in_openstreetmap": "Άνοιγμα στο OpenStreetMap",
+  "open_the_search_filters": "Ανοίξτε τα φίλτρα αναζήτησης",
+  "options": "Επιλογές",
+  "or": "ή",
+  "organize_your_library": "Οργανώστε τη βιβλιοθήκη σας",
+  "original": "πρωτότυπο",
+  "other": "Άλλες",
+  "other_devices": "Άλλες συσκευές",
+  "other_variables": "Άλλες μεταβλητές",
+  "owned": "Δικά μου",
+  "owner": "Κάτοχος",
+  "partner": "Συνεργάτης",
+  "partner_can_access": "Ο χρήστης {partner} έχει πρόσβαση",
+  "partner_can_access_assets": "Όλες οι φωτογραφίες και τα βίντεό σας εκτός από αυτά που βρίσκονται στο Αρχείο και τα Διεγραμμένα",
+  "partner_can_access_location": "Η τοποθεσία όπου τραβήχτηκαν οι φωτογραφίες σας",
+  "partner_sharing": "Κοινή Χρήση Συνεργατών",
+  "partners": "Συνεργάτες",
+  "password": "Κωδικός Πρόσβασης",
+  "password_does_not_match": "Ο κωδικός πρόσβασης δεν ταιριάζει",
+  "password_required": "Απαιτείται Κωδικός Πρόσβασης",
+  "password_reset_success": "Επιτυχής επαναφορά κωδικού πρόσβασης",
+  "past_durations": {
+    "days": "Περασμένη/νες {days, plural, one {ημέρα} other {# ημέρες}}",
+    "hours": "Περασμένη/νες {hours, plural, one {ώρα} other {# ώρες}}",
+    "years": "Περασμένος/να {years, plural, one {έτος} other {# έτη}}"
+  },
+  "path": "Διαδρομή",
+  "pattern": "Μοτίβο",
+  "pause": "Πάυση",
+  "pause_memories": "Παύση αναμνήσεων",
+  "paused": "Σε Πάυση",
+  "pending": "Εκκρεμεί",
+  "people": "Άτομα",
+  "people_edits_count": "Έγινε επεξεργασία {count, plural, one {# ατόμου} other {# ατόμων}}",
+  "people_feature_description": "Περιήγηση σε φωτογραφίες και βίντεο ομαδοποιημένα ανά άτομο",
+  "people_sidebar_description": "Εμφάνιση Ατόμων στην πλαϊνή γραμμή",
+  "permanent_deletion_warning": "Προειδοποίηση οριστικής διαγραφής",
+  "permanent_deletion_warning_setting_description": "Εμφάνιση προειδοποίησης κατά την οριστική διαγραφή στοιχείων",
+  "permanently_delete": "Οριστική διαγραφή",
+  "permanently_delete_assets_count": "Οριστική διαγραφή {count, plural, one {στοιχείου} other {στοιχείων}}",
+  "permanently_delete_assets_prompt": "Είστε βέβαιοι ότι θέλετε να διαγράψετε οριστικά {count, plural, one {αυτό το στοιχείο;} other {αυτά τα <b>#</b> στοιχεία;}} Αυτό θα {count, plural, one {το} other {τα}} αφαιρέσει επίσης από τα άλμπουμ στα οποία {count, plural, one {ανήκει} other {ανήκουν}} .",
+  "permanently_deleted_asset": "Οριστικά διαγραμμένο στοιχείο",
+  "permanently_deleted_assets_count": "Οριστική διαγραφή {count, plural, one {# στοιχείου} other {# στοιχείων}}",
+  "person": "Άτομο",
+  "person_hidden": "{name}{hidden, select, true { (κρυφό)} other {}}",
+  "photo_shared_all_users": "Φαίνεται ότι μοιραστήκατε τις φωτογραφίες σας με όλους τους χρήστες ή δεν έχετε κανέναν χρήστη για κοινή χρήση.",
+  "photos": "Φωτογραφίες",
+  "photos_and_videos": "Φωτογραφίες & Βίντεο",
+  "photos_count": "{count, plural, one {{count, number} Φωτογραφία} other {{count, number} Φωτογραφίες}}",
+  "photos_from_previous_years": "Φωτογραφίες προηγούμενων ετών",
+  "pick_a_location": "Επιλέξτε μια τοποθεσία",
+  "place": "Τοποθεσία",
+  "places": "Τοποθεσίες",
+  "play": "Αναπαραγωγή",
+  "play_memories": "Αναπαραγωγή αναμνήσεων",
+  "play_motion_photo": "Αναπαραγωγή Κινούμενης Φωτογραφίας",
+  "play_or_pause_video": "Αναπαραγωγή ή παύση βίντεο",
+  "port": "Θύρα",
+  "preset": "Προκαθορισμένη ρύθμιση",
+  "preview": "Προεπισκόπηση",
+  "previous": "Προηγούμενο",
+  "previous_memory": "Προηγούμενη ανάμνηση",
+  "previous_or_next_photo": "Προηγούμενη ή επόμενη φωτογραφία",
+  "primary": "Πρωτεύων",
+  "privacy": "Ιδιωτικότητα",
+  "profile_image_of_user": "Εικόνα προφίλ του χρήστη {user}",
+  "profile_picture_set": "Ορισμός εικόνας προφίλ.",
+  "public_album": "Δημόσιο άλμπουμ",
+  "public_share": "Δημόσια Κοινή Χρήση",
+  "purchase_account_info": "Υποστηρικτής",
+  "purchase_activated_subtitle": "Σας ευχαριστούμε για την υποστήριξη του Immich και λογισμικών ανοιχτού κώδικα",
+  "purchase_activated_time": "Ενεργοποιήθηκε στις {date, date}",
+  "purchase_activated_title": "Το κλειδί σας ενεργοποιήθηκε με επιτυχία",
+  "purchase_button_activate": "Ενεργοποίηση",
+  "purchase_button_buy": "Αγορά",
+  "purchase_button_buy_immich": "Αγορά Immich",
+  "purchase_button_never_show_again": "Να μην εμφανιστεί ποτέ ξανά",
+  "purchase_button_reminder": "Υπενθύμιση σε 30 μέρες",
+  "purchase_button_remove_key": "Αφαίρεση κλειδιού",
+  "purchase_button_select": "Επιλέξτε",
+  "purchase_failed_activation": "Η ενεργοποίηση απέτυχε! Ελέγξτε το email σας για το σωστό κλειδί προϊόντος!",
+  "purchase_individual_description_1": "Για ένα άτομο",
+  "purchase_individual_description_2": "Κατάσταση υποστηρικτή",
+  "purchase_individual_title": "Ατομο",
+  "purchase_input_suggestion": "Έχετε ένα κλειδί προϊόντος; Εισαγάγετε το κλειδί παρακάτω",
+  "purchase_license_subtitle": "Αγοράστε το Immich για να υποστηρίξετε τη συνεχή ανάπτυξη της υπηρεσίας",
+  "purchase_lifetime_description": "Αγορά εφ' όρου ζωής",
+  "purchase_option_title": "ΕΠΙΛΟΓΕΣ ΑΓΟΡΑΣ",
+  "purchase_panel_info_1": "Η ανάπτυξη του Immich απαιτεί πολύ χρόνο και προσπάθεια, και έχουμε μηχανικούς πλήρους απασχόλησης που εργάζονται σε αυτό για να το κάνουμε όσο το δυνατόν καλύτερο. Η αποστολή μας είναι το λογισμικό ανοιχτού κώδικα και οι ηθικές επιχειρηματικές πρακτικές να γίνουν βιώσιμη πηγή εισοδήματος για προγραμματιστές και να δημιουργήσουμε ένα οικοσύστημα που σέβεται το απόρρητο, με πραγματικές εναλλακτικές λύσεις στις υπηρεσίες cloud που παρουσιάζουν συμπεριφορές εκμετάλλευσης.",
+  "purchase_panel_info_2": "Καθώς δεσμευόμαστε να μην προσθέσουμε φραγμούς με σκοπό το κέρδος, αυτή η αγορά δεν θα σας προσφέρει πρόσθετες δυνατότητες στο Immich. Βασιζόμαστε σε χρήστες όπως εσείς για την υποστήριξη της συνεχούς ανάπτυξης του Immich.",
+  "purchase_panel_title": "Υποστηρίξτε το πρότζεκτ",
+  "purchase_per_server": "Ανά διακομιστή",
+  "purchase_per_user": "Ανά χρήστη",
+  "purchase_remove_product_key": "Κατάργηση κλειδιού προϊόντος",
+  "purchase_remove_product_key_prompt": "Είστε βέβαιοι ότι θέλετε να αφαιρέσετε τον αριθμό-κλειδί προϊόντος;",
+  "purchase_remove_server_product_key": "Κατάργηση κλειδιού προϊόντος διακομιστή",
+  "purchase_remove_server_product_key_prompt": "Είστε βέβαιοι ότι θέλετε να καταργήσετε το κλειδί προϊόντος διακομιστή;",
+  "purchase_server_description_1": "Για ολόκληρο τον διακομιστή",
+  "purchase_server_description_2": "Κατάσταση υποστηρικτή",
+  "purchase_server_title": "Διακομιστής",
+  "purchase_settings_server_activated": "Η διαχείριση του κλειδιού προϊόντος του διακομιστή γίνεται από τον διαχειριστή",
+  "rating": "Αξιολόγηση με αστέρια",
+  "rating_clear": "Εκκαθάριση αξιολόγησης",
+  "rating_count": "{count, plural, one {# αστέρι} other {# αστέρια}}",
+  "rating_description": "Εμφάνιση της αξιολόγησης EXIF στον πίνακα πληροφοριών",
+  "reaction_options": "Επιλογές αντίδρασης",
+  "read_changelog": "Διαβάστε το Αρχείο Καταγραφής Αλλαγών",
+  "reassign": "Ανάθεση",
+  "reassigned_assets_to_existing_person": "Η ανάθεση {count, plural, one {# αρχείου} other {# αρχείων}} στον/στην {name, select, null {έναν/μία υπάρχοντα/ουσα χρήστη} other {{name}}}",
+  "reassigned_assets_to_new_person": "Η ανάθεση {count, plural, one {# αρχείου} other {# αρχείων}} σε νέο άτομο",
+  "reassing_hint": "Ανάθεση των επιλεγμένων στοιχείων σε υπάρχον άτομο",
+  "recent": "Πρόσφατα",
+  "recent-albums": "Πρόσφατα άλμπουμ",
+  "recent_searches": "Πρόσφατες αναζητήσεις",
+  "refresh": "Ανανέωση",
+  "refresh_encoded_videos": "Ανανέωση κωδικοποιημένων βίντεο",
+  "refresh_faces": "Ανανέωση προσώπων",
+  "refresh_metadata": "Ανανέωση μεταδεδομένων",
+  "refresh_thumbnails": "Ανανέωση μικρογραφιών",
+  "refreshed": "Ανανεωμένα",
+  "refreshes_every_file": "Επαναδιαβάζει όλα τα υπάρχοντα και νέα αρχεία",
+  "refreshing_encoded_video": "Ανανέωση κωδικοποιημένου βίντεο",
+  "refreshing_faces": "Ανανεώνονται πρόσωπα",
+  "refreshing_metadata": "Τα μεταδεδομένα ανανεώνονται",
+  "regenerating_thumbnails": "Οι μικρογραφίες αναγεννώνται",
+  "remove": "Αφαίρεση",
+  "remove_assets_album_confirmation": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε {count, plural, one {# στοιχείο} other {# στοιχεία}} από το άλμπουμ;",
+  "remove_assets_shared_link_confirmation": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε {count, plural, one {# στοιχείο} other {# στοιχεία}} από αυτόν τον κοινόχρηστο σύνδεσμο;",
+  "remove_assets_title": "Αφαίρεση στοιχείων;",
+  "remove_custom_date_range": "Αφαίρεση προσαρμοσμένης χρονικής περιόδου",
+  "remove_deleted_assets": "Αφαίρεση Διεγραμμένων Στοιχείων",
+  "remove_from_album": "Αφαίρεση από το άλμπουμ",
+  "remove_from_favorites": "Αφαίρεση από τα αγαπημένα",
+  "remove_from_shared_link": "Αφαίρεση από τον κοινόχρηστο σύνδεσμο",
+  "remove_url": "Αφαίρεση Συνδέσμου",
+  "remove_user": "Αφαίρεση χρήστη",
+  "removed_api_key": "Αφαιρέθηκε το API Key: {name}",
+  "removed_from_archive": "Αφαιρέθηκε/καν από το Αρχείο",
+  "removed_from_favorites": "Αφαιρέθηκε από τα αγαπημένα",
+  "removed_from_favorites_count": "Αφαιρέθηκαν {count, plural, other {#}} από τα αγαπημένα",
+  "removed_tagged_assets": "Αφαιρέθηκε η ετικέτα από {count, plural, one {# στοιχείο} other {# στοιχεία}}",
+  "rename": "Μετονομασία",
+  "repair": "Επισκευή",
+  "repair_no_results_message": "Τα αρχεία που δεν παρακολουθούνται και τα λείποντα αρχεία θα εμφανιστούν εδώ",
+  "replace_with_upload": "Αντικατάσταση με μεταφόρτωση",
+  "repository": "Αποθετήριο",
+  "require_password": "Απαιτείται κωδικός πρόσβασης",
+  "require_user_to_change_password_on_first_login": "Ο χρήστης απαιτείται να αλλάξει τον κωδικό πρόσβασής του κατά την πρώτη σύνδεση",
+  "reset": "Επαναφορά",
+  "reset_password": "Επαναφορά κωδικού πρόσβασης",
+  "reset_people_visibility": "Επαναφορά προβολής ατόμων",
+  "reset_to_default": "Επαναφορά στις προεπιλογές",
+  "resolve_duplicates": "Επίλυση διπλοτύπων",
+  "resolved_all_duplicates": "Επιλύθηκαν όλα τα διπλότυπα",
+  "restore": "Ανάκτηση",
+  "restore_all": "Ανάκτηση όλων",
+  "restore_user": "Επαναφορά χρήστη",
+  "restored_asset": "Ανακτήθηκε το αρχείο",
+  "resume": "Συνέχιση",
+  "retry_upload": "Επανάληψη ανεβάσματος",
+  "review_duplicates": "Προβολή διπλότυπων",
+  "role": "Ρόλος",
+  "role_editor": "Επεξεργαστής",
+  "role_viewer": "Θεατής",
+  "save": "Αποθήκευση",
+  "saved_api_key": "Αποθηκευμένο API key",
+  "saved_profile": "Αποθηκευμένο προφίλ",
+  "saved_settings": "Αποθηκευμένες ρυθμίσεις",
+  "say_something": "Πείτε κάτι",
+  "scan_all_libraries": "Σάρωση Όλων των Βιβλιοθηκών",
+  "scan_library": "Σάρωση",
+  "scan_settings": "Ρυθμίσεις Σάρωσης",
+  "scanning_for_album": "Σάρωση για άλμπουμ...",
+  "search": "Αναζήτηση",
+  "search_albums": "Αναζήτηση άλμπουμ",
+  "search_by_context": "Αναζήτηση με βάση το πλαίσιο",
+  "search_by_filename": "Αναζήτηση βάσει ονόματος αρχείου ή επέκτασης αρχείου",
+  "search_by_filename_example": "π.χ. IMG_1234.JPG ή PNG",
+  "search_camera_make": "Αναζήτηση κατασκευαστή κάμερας...",
+  "search_camera_model": "Αναζήτηση μοντέλου κάμερας...",
+  "search_city": "Αναζήτηση πόλης...",
+  "search_country": "Αναζήτηση χώρας...",
+  "search_for_existing_person": "Αναζήτηση υπάρχοντος ατόμου",
+  "search_no_people": "Κανένα άτομο",
+  "search_no_people_named": "Κανένα άτομο με όνομα \"{name}\"",
+  "search_options": "Επιλογές αναζήτησης",
+  "search_people": "Αναζήτηση ατόμων",
+  "search_places": "Αναζήτηση τοποθεσιών",
+  "search_settings": "Ρυθμίσεις αναζήτησης",
+  "search_state": "Αναζήτηση νομού...",
+  "search_tags": "Αναζήτηση ετικετών...",
+  "search_timezone": "Αναζήτηση ζώνης ώρας...",
+  "search_type": "Τύπος αναζήτησης",
+  "search_your_photos": "Αναζήτηση φωτογραφιών",
+  "searching_locales": "Αναζήτηση τοποθεσιών...",
+  "second": "Δευτερόλεπτο",
+  "see_all_people": "Προβολή όλων των ατόμων",
+  "select_album_cover": "Επιλέξτε εξώφυλλο άλμπουμ",
+  "select_all": "Επιλογή όλων",
+  "select_all_duplicates": "Επιλογή όλων των διπλότυπων",
+  "select_avatar_color": "Επιλέξτε χρώμα avatar",
+  "select_face": "Επιλογή προσώπου",
+  "select_featured_photo": "Επιλέξτε φωτογραφία για προβολή",
+  "select_from_computer": "Επιλέξτε από υπολογιστή",
+  "select_keep_all": "Επιλέξτε διατήρηση όλων",
+  "select_library_owner": "Επιλέξτε κάτοχο βιβλιοθήκης",
+  "select_new_face": "Επιλέξτε νέο πρόσωπο",
+  "select_photos": "Επιλέξτε φωτογραφίες",
+  "select_trash_all": "Επιλέξτε διαγραφή όλων",
+  "selected": "Επιλεγμένοι",
+  "selected_count": "{count, plural, other {# επιλεγμένοι}}",
+  "send_message": "Αποστολή μηνύματος",
+  "send_welcome_email": "Αποστολή email καλωσορίσματος",
+  "server_offline": "Διακομιστής Εκτός Σύνδεσης",
+  "server_online": "Διακομιστής Σε Σύνδεση",
+  "server_stats": "Στατιστικά Διακομιστή",
+  "server_version": "Έκδοση Διακομιστή",
+  "set": "Ορισμός",
+  "set_as_album_cover": "Ορισμός ως εξώφυλλο άλμπουμ",
+  "set_as_profile_picture": "Ορισμός ως εικόνα προφίλ",
+  "set_date_of_birth": "Ορισμός ημερομηνίας γέννησης",
+  "set_profile_picture": "Ορισμός εικόνας προφίλ",
+  "set_slideshow_to_fullscreen": "Ορίστε την παρουσίαση σε πλήρη οθόνη",
+  "settings": "Ρυθμίσεις",
+  "settings_saved": "Οι ρυθμίσεις αποθηκεύτηκαν",
+  "share": "Κοινοποίηση",
+  "shared": "Σε κοινή χρήση",
+  "shared_by": "Σε κοινή χρήση από",
+  "shared_by_user": "Σε κοινή χρήση από {user}",
+  "shared_by_you": "Σε κοινή χρήση από εσάς",
+  "shared_from_partner": "Φωτογραφίες από {partner}",
+  "shared_link_options": "Επιλογές κοινόχρηστου συνδέσμου",
+  "shared_links": "Κοινόχρηστοι σύνδεσμοι",
+  "shared_photos_and_videos_count": "{assetCount, plural, other {# κοινόχρηστες φωτογραφίες & βίντεο.}}",
+  "shared_with_partner": "Σε κοινή χρήση με {partner}",
+  "sharing": "Κοινοποίηση",
+  "sharing_enter_password": "Εισαγάγετε τον κωδικό πρόσβασης για να δείτε αυτήν τη σελίδα.",
+  "sharing_sidebar_description": "Εμφανίστε έναν σύνδεσμο για Κοινή χρήση στην πλαϊνή γραμμή",
+  "shift_to_permanent_delete": "πατήστε ⇧ για οριστική διαγραφή στοιχείου",
+  "show_album_options": "Εμφάνιση επιλογών άλμπουμ",
+  "show_albums": "Προβολή των άλμπουμ",
+  "show_all_people": "Προβολή όλων των ατόμων",
+  "show_and_hide_people": "Εμφάνιση & απόκρυψη ατόμων",
+  "show_file_location": "Εμφάνιση θέσης αρχείου",
+  "show_gallery": "Εμφάνιση γκαλερί",
+  "show_hidden_people": "Εμφάνιση κρυμμένων ατόμων",
+  "show_in_timeline": "Εμφάνιση στο χρονολόγιο",
+  "show_in_timeline_setting_description": "Εμφάνιση φωτογραφιών και βίντεο από αυτόν τον χρήστη στο χρονολόγιό σας",
+  "show_keyboard_shortcuts": "Εμφάνιση συντομεύσεων πληκτρολογίου",
+  "show_metadata": "Εμφάνιση μεταδεδομένων",
+  "show_or_hide_info": "Εμφάνιση ή απόκρυψη πληροφοριών",
+  "show_password": "Εμφάνιση κωδικού",
+  "show_person_options": "Εμφάνιση επιλογών ατόμου",
+  "show_progress_bar": "Εμφάνιση γραμμής προόδου",
+  "show_search_options": "Εμφάνιση επιλογών αναζήτησης",
+  "show_slideshow_transition": "Εμφάνιση μετάβασης παρουσίασης",
+  "show_supporter_badge": "Σήμα υποστηρικτή",
+  "show_supporter_badge_description": "Εμφάνιση σήματος υποστηρικτή",
+  "shuffle": "Ανάμειξη",
+  "sidebar": "Πλαϊνή μπάρα",
+  "sidebar_display_description": "Εμφάνιση συνδέσμου για προβολή στην πλαϊνή μπάρα",
+  "sign_out": "Αποσύνδεση",
+  "sign_up": "Εγγραφή",
+  "size": "Μέγεθος",
+  "skip_to_content": "Μετάβαση στο περιεχόμενο",
+  "skip_to_folders": "Παράκαμψη στους φακέλους",
+  "skip_to_tags": "Παράκαμψη στις ετικέτες",
+  "slideshow": "Παρουσίαση",
+  "slideshow_settings": "Ρυθμίσεις παρουσίασης",
+  "sort_albums_by": "Ταξινόμηση άλμπουμ κατά...",
+  "sort_created": "Ημερομηνία Δημιουργίας",
+  "sort_items": "Αριθμός αντικειμένων",
+  "sort_modified": "Ημερομηνία τροποποίησης",
+  "sort_oldest": "Η πιο παλιά φωτογραφία",
+  "sort_recent": "Η πιο πρόσφατη φωτογραφία",
+  "sort_title": "Τίτλος",
+  "source": "Πηγή",
+  "stack": "Στοίβα",
+  "stack_duplicates": "Στοίβαξη διπλότυπων",
+  "stack_select_one_photo": "Επιλέξτε μια κύρια φωτογραφία για τη στοίβαξη",
+  "stack_selected_photos": "Στοίβαγμα επιλεγμένων φωτογραφιών",
+  "stacked_assets_count": "Στοιβάχτηκαν {count, plural, one {# στοιχείο} other {# στοιχεία}}",
+  "stacktrace": "Καταγραφή στοίβας",
+  "start": "Έναρξη",
+  "start_date": "Από",
+  "state": "Νομός",
+  "status": "Κατάσταση",
+  "stop_motion_photo": "Διέκοψε την Φωτογραφία Κίνησης",
+  "stop_photo_sharing": "Διακοπή κοινής χρήσης των φωτογραφιών σας;",
+  "stop_photo_sharing_description": "Ο χρήστης {partner} δεν θα έχει πλέον πρόσβαση στις φωτογραφίες σας.",
+  "stop_sharing_photos_with_user": "Διακοπή κοινής χρήσης των φωτογραφιών σας με αυτό το χρήστη",
+  "storage": "Χώρος αποθήκευσης",
+  "storage_label": "Ετικέτα αποθήκευσης",
+  "storage_usage": "{used} από {available} σε χρήση",
+  "submit": "Υποβολή",
+  "suggestions": "Προτάσεις",
+  "sunrise_on_the_beach": "Ηλιοβασίλεμα στην παραλία",
+  "support": "Υποστήριξη",
+  "support_and_feedback": "Υποστήριξη & Σχόλια",
+  "support_third_party_description": "Η εγκατάσταση του Immich που χρησιμοποιείτε, έχει πακεταριστεί από τρίτους. Τα προβλήματα που αντιμετωπίζετε μπορεί να οφείλονται σε αυτό το πακέτο, οπότε παρακαλούμε να αναφέρετε τα προβλήματα πρώτα σε εκείνους, χρησιμοποιώντας τους παρακάτω συνδέσμους.",
+  "swap_merge_direction": "Εναλλαγή κατεύθυνσης συγχώνευσης",
+  "sync": "Συγχρονισμός",
+  "tag": "Ετικέτα",
+  "tag_assets": "Ετικετοποίηση στοιχείων",
+  "tag_created": "Δημιουργήθηκε ετικέτα: {tag}",
+  "tag_feature_description": "Περιήγηση σε φωτογραφίες και βίντεο που είναι οργανωμένα σύμφωνα με λογικά θέματα ετικετών",
+  "tag_not_found_question": "Δεν μπορείτε να βρείτε μια ετικέτα; <link>Δημιουργήστε μια νέα ετικέτα.</link>",
+  "tag_updated": "Ενημερώθηκε η ετικέτα: {tag}",
+  "tagged_assets": "Ετικετοποιημένο/α {count, plural, one {# στοιχείο} other {# στοιχεία}}",
+  "tags": "Ετικέτες",
+  "template": "Πρότυπο",
+  "theme": "Θέμα",
+  "theme_selection": "Επιλογή θέματος",
+  "theme_selection_description": "Ρυθμίστε αυτόματα το θέμα σε ανοιχτό ή σκούρο με βάση τις προτιμήσεις συστήματος του προγράμματος περιήγησής σας",
+  "they_will_be_merged_together": "Θα συγχωνευθούν μαζί",
+  "third_party_resources": "Πόροι τρίτων",
+  "time_based_memories": "Μνήμες βασισμένες στο χρόνο",
+  "timeline": "Χρονολόγιο",
+  "timezone": "Ζώνη ώρας",
+  "to_archive": "Αρχειοθέτηση",
+  "to_change_password": "Αλλαγή κωδικού πρόσβασης",
+  "to_favorite": "Αγαπημένο",
+  "to_login": "Είσοδος",
+  "to_parent": "Μεταβείτε στο γονικό φάκελο",
+  "to_trash": "Κάδος απορριμμάτων",
+  "toggle_settings": "Εναλλαγή ρυθμίσεων",
+  "toggle_theme": "Εναλλαγή θέματος",
+  "total": "Σύνολο",
+  "total_usage": "Συνολική χρήση",
+  "trash": "Κάδος απορριμμάτων",
+  "trash_all": "Διαγραφή Όλων",
+  "trash_count": "Διαγραφή {count, number}",
+  "trash_delete_asset": "Διαγραφή/Οριστ. Διαγραφή Αντικειμένου",
+  "trash_no_results_message": "Οι φωτογραφίες και τα βίντεο που βρίσκονται στον κάδο απορριμμάτων θα εμφανίζονται εδώ.",
+  "trashed_items_will_be_permanently_deleted_after": "Τα στοιχεία που βρίσκονται στον κάδο απορριμμάτων θα διαγραφούν οριστικά μετά από {days, plural, one {# ημέρα} other {# ημέρες}}.",
+  "type": "Τύπος",
+  "unarchive": "Αναίρεση αρχειοθέτησης",
+  "unarchived_count": "{count, plural, other {Αρχειοθετήσεις αναιρέθηκαν #}}",
+  "unfavorite": "Αποεπιλογή από τα αγαπημένα",
+  "unhide_person": "Αναίρεση απόκρυψης ατόμου",
+  "unknown": "Άγνωστο",
+  "unknown_year": "Άγνωστο Έτος",
+  "unlimited": "Απεριόριστο",
+  "unlink_motion_video": "Αποσυνδέστε το βίντεο κίνησης",
+  "unlink_oauth": "Αποσύνδεση OAuth",
+  "unlinked_oauth_account": "Ο λογαριασμός OAuth αποσυνδέθηκε",
+  "unnamed_album": "Ανώνυμο Άλμπουμ",
+  "unnamed_album_delete_confirmation": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το άλμπουμ;",
+  "unnamed_share": "Ανώνυμη Κοινή Χρήση",
+  "unsaved_change": "Μη αποθηκευμένη αλλαγή",
+  "unselect_all": "Αποεπιλογή όλων",
+  "unselect_all_duplicates": "Αποεπιλογή όλων των διπλότυπων",
+  "unstack": "Αποστοίβαξη",
+  "unstacked_assets_count": "Αποστοιβάξατε {count, plural, one {# στοιχείο} other {# στοιχεία}}",
+  "untracked_files": "Μη παρακολουθούμενα αρχεία",
+  "untracked_files_decription": "Αυτά τα αρχεία δεν παρακολουθούνται από την εφαρμογή. Μπορεί να είναι αποτελέσματα αποτυχημένων μετακινήσεων, αποτυχημένες μεταφορτώσεις ή εναπομείναντα λόγω σφάλματος",
+  "up_next": "Ακολουθεί",
+  "updated_password": "Ο κωδικός πρόσβασης ενημερώθηκε",
+  "upload": "Μεταφόρτωση",
+  "upload_concurrency": "Συγχρονισμός μεταφόρτωσης",
+  "upload_errors": "Η μεταφόρτωση ολοκληρώθηκε με {count, plural, one {# σφάλμα} other {# σφάλματα}}, ανανεώστε τη σελίδα για να δείτε νέα στοιχεία μεταφόρτωσης.",
+  "upload_progress": "Απομένουν {remaining, number} - Ολοκληρώθηκαν {processed, number}/{total, number}",
+  "upload_skipped_duplicates": "Παραλείφθηκαν {count, plural, one {# διπλότυπο στοιχείο} other {# διπλότυπα στοιχεία}}",
+  "upload_status_duplicates": "Διπλότυπα",
+  "upload_status_errors": "Σφάλματα",
+  "upload_status_uploaded": "Μεταφορτώθηκαν",
+  "upload_success": "Η μεταφόρτωση ολοκληρώθηκε, ανανεώστε τη σελίδα για να δείτε τα νέα αντικείμενα.",
+  "url": "URL",
+  "usage": "Χρήση",
+  "use_custom_date_range": "Χρήση προσαρμοσμένου εύρους ημερομηνιών",
+  "user": "Χρήστης",
+  "user_id": "ID Χρήστη",
+  "user_liked": "Στο χρήστη {user} αρέσει {type, select, photo {αυτή η φωτογραφία} video {αυτό το βίντεο} asset {αυτό το αντικείμενο} other {it}}",
+  "user_purchase_settings": "Αγορά",
+  "user_purchase_settings_description": "Διαχείριση Αγοράς",
+  "user_role_set": "Ορισμός {user} ως {role}",
+  "user_usage_detail": "Λεπτομέρειες χρήσης του χρήστη",
+  "user_usage_stats": "Στατιστικά χρήσης λογαριασμού",
+  "user_usage_stats_description": "Προβολή στατιστικών χρήσης λογαριασμού",
+  "username": "Όνομα Χρήστη",
+  "users": "Χρήστες",
+  "utilities": "Βοηθητικά προγράμματα",
+  "validate": "Επικύρωση",
+  "variables": "Μεταβλητές",
+  "version": "Έκδοση",
+  "version_announcement_closing": "Ο φίλος σου, Alex",
+  "version_announcement_message": "Γειά σας! Μια νέα έκδοση του Immich είναι διαθέσιμη. Παρακαλούμε αφιερώστε λίγο χρόνο για να διαβάσετε τις <link>σημειώσεις έκδοσης</link> ώστε να βεβαιωθείτε ότι η ρύθμιση σας είναι ενημερωμένη και να αποφύγετε τυχόν σφάλματα, ειδικά αν χρησιμοποιείτε το WatchTower ή οποιοδήποτε μηχανισμό που διαχειρίζεται αυτόματα την ενημέρωση της εγκατάστασης του Immich σας.",
+  "version_history": "Ιστορικό Εκδόσεων",
+  "version_history_item": "Εγκαταστάθηκε {version} στις {date}",
+  "video": "Βίντεο",
+  "video_hover_setting": "Προεπισκόπηση βίντεο με το δείκτη του ποντικιού",
+  "video_hover_setting_description": "Προεπισκόπηση βίντεο όταν το ποντίκι βρίσκεται πάνω από το στοιχείο. Ακόμη και όταν είναι απενεργοποιημένη, η αναπαραγωγή μπορεί να ξεκινήσει τοποθετώντας το δείκτη του ποντικιού πάνω από το εικονίδιο αναπαραγωγής.",
+  "videos": "Βίντεο",
+  "videos_count": "{count, plural, one {# Βίντεο} other {# Βίντεο}}",
+  "view": "Προβολή",
+  "view_album": "Προβολή Άλμπουμ",
+  "view_all": "Προβολή Όλων",
+  "view_all_users": "Προβολή όλων των χρηστών",
+  "view_in_timeline": "Προβολή στο χρονοδιάγραμμα",
+  "view_links": "Προβολή συνδέσμων",
+  "view_name": "Προβολή",
+  "view_next_asset": "Προβολή επόμενου στοιχείου",
+  "view_previous_asset": "Προβολή προηγούμενου στοιχείου",
+  "view_stack": "Προβολή της στοίβας",
+  "visibility_changed": "Η ορατότητα άλλαξε για {count, plural, one {# άτομο} other {# άτομα}}",
+  "waiting": "Στοιχεία σε αναμονή",
+  "warning": "Προειδοποίηση",
+  "week": "Εβδομάδα",
+  "welcome": "Καλωσορίσατε",
+  "welcome_to_immich": "Καλωσορίσατε στο Ιmmich",
+  "year": "Έτος",
+  "years_ago": "πριν από {years, plural, one {# χρόνο} other {# χρόνια}}",
+  "yes": "Ναι",
+  "you_dont_have_any_shared_links": "Δεν έχετε κοινόχρηστους συνδέσμους",
+  "zoom_image": "Ζουμ Εικόνας"
+}
diff --git a/web/src/lib/i18n/en.json b/i18n/en.json
similarity index 91%
rename from web/src/lib/i18n/en.json
rename to i18n/en.json
index e11c0c63e3..e412989b25 100644
--- a/web/src/lib/i18n/en.json
+++ b/i18n/en.json
@@ -23,16 +23,23 @@
   "add_to": "Add to...",
   "add_to_album": "Add to album",
   "add_to_shared_album": "Add to shared album",
+  "add_url": "Add URL",
   "added_to_archive": "Added to archive",
   "added_to_favorites": "Added to favorites",
   "added_to_favorites_count": "Added {count, number} to favorites",
   "admin": {
     "add_exclusion_pattern_description": "Add exclusion patterns. Globbing using *, **, and ? is supported. To ignore all files in any directory named \"Raw\", use \"**/Raw/**\". To ignore all files ending in \".tif\", use \"**/*.tif\". To ignore an absolute path, use \"/path/to/ignore/**\".",
+    "asset_offline_description": "This external library asset is no longer found on disk and has been moved to trash. If the file was moved within the library, check your timeline for the new corresponding asset. To restore this asset, please ensure that the file path below can be accessed by Immich and scan the library.",
     "authentication_settings": "Authentication Settings",
     "authentication_settings_description": "Manage password, OAuth, and other authentication settings",
     "authentication_settings_disable_all": "Are you sure you want to disable all login methods? Login will be completely disabled.",
     "authentication_settings_reenable": "To re-enable, use a <link>Server Command</link>.",
     "background_task_job": "Background Tasks",
+    "backup_database": "Backup Database",
+    "backup_database_enable_description": "Enable database backups",
+    "backup_keep_last_amount": "Amount of previous backups to keep",
+    "backup_settings": "Backup Settings",
+    "backup_settings_description": "Manage database backup settings",
     "check_all": "Check All",
     "cleared_jobs": "Cleared jobs for: {job}",
     "config_set_by_file": "Config is currently set by a config file",
@@ -41,33 +48,40 @@
     "confirm_email_below": "To confirm, type \"{email}\" below",
     "confirm_reprocess_all_faces": "Are you sure you want to reprocess all faces? This will also clear named people.",
     "confirm_user_password_reset": "Are you sure you want to reset {user}'s password?",
+    "create_job": "Create job",
+    "cron_expression": "Cron expression",
+    "cron_expression_description": "Set the scanning interval using the cron format. For more information please refer to e.g. <link>Crontab Guru</link>",
+    "cron_expression_presets": "Cron expression presets",
     "disable_login": "Disable login",
     "duplicate_detection_job_description": "Run machine learning on assets to detect similar images. Relies on Smart Search",
     "exclusion_pattern_description": "Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have folders that contain files you don't want to import, such as RAW files.",
     "external_library_created_at": "External library (created on {date})",
     "external_library_management": "External Library Management",
     "face_detection": "Face detection",
-    "face_detection_description": "Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. \"All\" (re-)processes all assets. \"Missing\" queues assets that haven't been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.",
-    "facial_recognition_job_description": "Group detected faces into people. This step runs after Face Detection is complete. \"All\" (re-)clusters all faces. \"Missing\" queues faces that don't have a person assigned.",
+    "face_detection_description": "Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. \"Refresh\" (re-)processes all assets. \"Reset\" additionally clears all current face data. \"Missing\" queues assets that haven't been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.",
+    "facial_recognition_job_description": "Group detected faces into people. This step runs after Face Detection is complete. \"Reset\" (re-)clusters all faces. \"Missing\" queues faces that don't have a person assigned.",
     "failed_job_command": "Command {command} failed for job: {job}",
     "force_delete_user_warning": "WARNING: This will immediately remove the user and all assets. This cannot be undone and the files cannot be recovered.",
     "forcing_refresh_library_files": "Forcing refresh of all library files",
+    "image_format": "Format",
     "image_format_description": "WebP produces smaller files than JPEG, but is slower to encode.",
     "image_prefer_embedded_preview": "Prefer embedded preview",
     "image_prefer_embedded_preview_setting_description": "Use embedded previews in RAW photos as the input to image processing when available. This can produce more accurate colors for some images, but the quality of the preview is camera-dependent and the image may have more compression artifacts.",
     "image_prefer_wide_gamut": "Prefer wide gamut",
     "image_prefer_wide_gamut_setting_description": "Use Display P3 for thumbnails. This better preserves the vibrance of images with wide colorspaces, but images may appear differently on old devices with an old browser version. sRGB images are kept as sRGB to avoid color shifts.",
-    "image_preview_format": "Preview format",
-    "image_preview_resolution": "Preview resolution",
-    "image_preview_resolution_description": "Used when viewing a single photo and for machine learning. Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.",
+    "image_preview_description": "Medium-size image with stripped metadata, used when viewing a single asset and for machine learning",
+    "image_preview_quality_description": "Preview quality from 1-100. Higher is better, but produces larger files and can reduce app responsiveness. Setting a low value may affect machine learning quality.",
+    "image_preview_title": "Preview Settings",
     "image_quality": "Quality",
-    "image_quality_description": "Image quality from 1-100. Higher is better for quality but produces larger files, this option affects the Preview and Thumbnail images.",
+    "image_resolution": "Resolution",
+    "image_resolution_description": "Higher resolutions can preserve more detail but take longer to encode, have larger file sizes and can reduce app responsiveness.",
     "image_settings": "Image Settings",
     "image_settings_description": "Manage the quality and resolution of generated images",
-    "image_thumbnail_format": "Thumbnail format",
-    "image_thumbnail_resolution": "Thumbnail resolution",
-    "image_thumbnail_resolution_description": "Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.",
+    "image_thumbnail_description": "Small thumbnail with stripped metadata, used when viewing groups of photos like the main timeline",
+    "image_thumbnail_quality_description": "Thumbnail quality from 1-100. Higher is better, but produces larger files and can reduce app responsiveness.",
+    "image_thumbnail_title": "Thumbnail Settings",
     "job_concurrency": "{job} concurrency",
+    "job_created": "Job created",
     "job_not_concurrency_safe": "This job is not concurrency-safe.",
     "job_settings": "Job Settings",
     "job_settings_description": "Manage job concurrency",
@@ -75,9 +89,6 @@
     "jobs_delayed": "{jobCount, plural, other {# delayed}}",
     "jobs_failed": "{jobCount, plural, other {# failed}}",
     "library_created": "Created library: {library}",
-    "library_cron_expression": "Cron expression",
-    "library_cron_expression_description": "Set the scanning interval using the cron format. For more information please refer to e.g. <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Cron expression presets",
     "library_deleted": "Library deleted",
     "library_import_path_description": "Specify a folder to import. This folder, including subfolders, will be scanned for images and videos.",
     "library_scanning": "Periodic Scanning",
@@ -120,7 +131,7 @@
     "machine_learning_smart_search_description": "Search for images semantically using CLIP embeddings",
     "machine_learning_smart_search_enabled": "Enable smart search",
     "machine_learning_smart_search_enabled_description": "If disabled, images will not be encoded for smart search.",
-    "machine_learning_url_description": "URL of the machine learning server",
+    "machine_learning_url_description": "The URL of the machine learning server. If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last.",
     "manage_concurrency": "Manage Concurrency",
     "manage_log_settings": "Manage log settings",
     "map_dark_style": "Dark style",
@@ -150,7 +161,7 @@
     "note_cannot_be_changed_later": "NOTE: This cannot be changed later!",
     "note_unlimited_quota": "Note: Enter 0 for unlimited quota",
     "notification_email_from_address": "From address",
-    "notification_email_from_address_description": "Sender email address, for example: \"Immich Photo Server <noreply@immich.app>\"",
+    "notification_email_from_address_description": "Sender email address, for example: \"Immich Photo Server <noreply@example.com>\"",
     "notification_email_host_description": "Host of the email server (e.g. smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Ignore certificate errors",
     "notification_email_ignore_certificate_errors_description": "Ignore TLS certificate validation errors (not recommended)",
@@ -196,22 +207,24 @@
     "password_settings": "Password Login",
     "password_settings_description": "Manage password login settings",
     "paths_validated_successfully": "All paths validated successfully",
+    "person_cleanup_job": "Person cleanup",
     "quota_size_gib": "Quota Size (GiB)",
     "refreshing_all_libraries": "Refreshing all libraries",
     "registration": "Admin Registration",
     "registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.",
-    "removing_offline_files": "Removing Offline Files",
     "repair_all": "Repair All",
     "repair_matched_items": "Matched {count, plural, one {# item} other {# items}}",
     "repaired_items": "Repaired {count, plural, one {# item} other {# items}}",
     "require_password_change_on_login": "Require user to change password on first login",
     "reset_settings_to_default": "Reset settings to default",
     "reset_settings_to_recent_saved": "Reset settings to the recent saved settings",
-    "scanning_library_for_changed_files": "Scanning library for changed files",
-    "scanning_library_for_new_files": "Scanning library for new files",
+    "scanning_library": "Scanning library",
+    "search_jobs": "Search jobs...",
     "send_welcome_email": "Send welcome email",
     "server_external_domain_settings": "External domain",
     "server_external_domain_settings_description": "Domain for public shared links, including http(s)://",
+    "server_public_users": "Public Users",
+    "server_public_users_description": "All users (name and email) are listed when adding a user to shared albums. When disabled, the user list will only be available to admin users.",
     "server_settings": "Server Settings",
     "server_settings_description": "Manage server settings",
     "server_welcome_message": "Welcome message",
@@ -236,6 +249,17 @@
     "storage_template_settings_description": "Manage the folder structure and file name of the upload asset",
     "storage_template_user_label": "<code>{label}</code> is the user's Storage Label",
     "system_settings": "System Settings",
+    "tag_cleanup_job": "Tag cleanup",
+    "template_email_available_tags": "You can use the following variables in your template: {tags}",
+    "template_email_if_empty": "If the template is empty, the default email will be used.",
+    "template_email_invite_album": "Invite Album Template",
+    "template_email_preview": "Preview",
+    "template_email_settings": "Email Templates",
+    "template_email_settings_description": "Manage custom email notification templates",
+    "template_email_update_album": "Update Album Template",
+    "template_email_welcome": "Welcome email template",
+    "template_settings": "Notification Templates",
+    "template_settings_description": "Manage custom templates for notifications.",
     "theme_custom_css_settings": "Custom CSS",
     "theme_custom_css_settings_description": "Cascading Style Sheets allow the design of Immich to be customized.",
     "theme_settings": "Theme Settings",
@@ -268,7 +292,7 @@
     "transcoding_hardware_acceleration": "Hardware Acceleration",
     "transcoding_hardware_acceleration_description": "Experimental; much faster, but will have lower quality at the same bitrate",
     "transcoding_hardware_decoding": "Hardware decoding",
-    "transcoding_hardware_decoding_setting_description": "Applies only to NVENC, QSV and RKMPP. Enables end-to-end acceleration instead of only accelerating encoding. May not work on all videos.",
+    "transcoding_hardware_decoding_setting_description": "Enables end-to-end acceleration instead of only accelerating encoding. May not work on all videos.",
     "transcoding_hevc_codec": "HEVC codec",
     "transcoding_max_b_frames": "Maximum B-frames",
     "transcoding_max_b_frames_description": "Higher values improve compression efficiency, but slow down encoding. May not be compatible with hardware acceleration on older devices. 0 disables B-frames, while -1 sets this value automatically.",
@@ -294,8 +318,6 @@
     "transcoding_threads_description": "Higher values lead to faster encoding, but leave less room for the server to process other tasks while active. This value should not be more than the number of CPU cores. Maximizes utilization if set to 0.",
     "transcoding_tone_mapping": "Tone-mapping",
     "transcoding_tone_mapping_description": "Attempts to preserve the appearance of HDR videos when converted to SDR. Each algorithm makes different tradeoffs for color, detail and brightness. Hable preserves detail, Mobius preserves color, and Reinhard preserves brightness.",
-    "transcoding_tone_mapping_npl": "Tone-mapping NPL",
-    "transcoding_tone_mapping_npl_description": "Colors will be adjusted to look normal for a display of this brightness. Counter-intuitively, lower values increase the brightness of the video and vice versa since it compensates for the brightness of the display. 0 sets this value automatically.",
     "transcoding_transcode_policy": "Transcode policy",
     "transcoding_transcode_policy_description": "Policy for when a video should be transcoded. HDR videos will always be transcoded (except if transcoding is disabled).",
     "transcoding_two_pass_encoding": "Two-pass encoding",
@@ -309,6 +331,7 @@
     "trash_settings_description": "Manage trash settings",
     "untracked_files": "Untracked Files",
     "untracked_files_description": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug",
+    "user_cleanup_job": "User cleanup",
     "user_delete_delay": "<b>{user}</b>'s account and assets will be scheduled for permanent deletion in {delay, plural, one {# day} other {# days}}.",
     "user_delete_delay_settings": "Delete delay",
     "user_delete_delay_settings_description": "Number of days after removal to permanently delete a user's account and assets. The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution.",
@@ -384,8 +407,8 @@
   "asset_filename_is_offline": "Asset {filename} is offline",
   "asset_has_unassigned_faces": "Asset has unassigned faces",
   "asset_hashing": "Hashing...",
-  "asset_offline": "Asset offline",
-  "asset_offline_description": "This asset is offline. Immich can not access its file location. Please ensure the asset is available and then rescan the library.",
+  "asset_offline": "Asset Offline",
+  "asset_offline_description": "This external asset is no longer found on disk. Please contact your Immich administrator for help.",
   "asset_skipped": "Skipped",
   "asset_skipped_in_trash": "In trash",
   "asset_uploaded": "Uploaded",
@@ -398,7 +421,7 @@
   "assets_moved_to_trash_count": "Moved {count, plural, one {# asset} other {# assets}} to trash",
   "assets_permanently_deleted_count": "Permanently deleted {count, plural, one {# asset} other {# assets}}",
   "assets_removed_count": "Removed {count, plural, one {# asset} other {# assets}}",
-  "assets_restore_confirmation": "Are you sure you want to restore all your trashed assets? You cannot undo this action!",
+  "assets_restore_confirmation": "Are you sure you want to restore all your trashed assets? You cannot undo this action! Note that any offline assets cannot be restored this way.",
   "assets_restored_count": "Restored {count, plural, one {# asset} other {# assets}}",
   "assets_trashed_count": "Trashed {count, plural, one {# asset} other {# assets}}",
   "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} already part of the album",
@@ -409,6 +432,7 @@
   "birthdate_saved": "Date of birth saved successfully",
   "birthdate_set_description": "Date of birth is used to calculate the age of this person at the time of a photo.",
   "blurred_background": "Blurred background",
+  "bugs_and_feature_requests": "Bugs & Feature Requests",
   "build": "Build",
   "build_image": "Build Image",
   "bulk_delete_duplicates_confirmation": "Are you sure you want to bulk delete {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will keep the largest asset of each group and permanently delete all other duplicates. You cannot undo this action!",
@@ -454,6 +478,7 @@
   "confirm": "Confirm",
   "confirm_admin_password": "Confirm Admin Password",
   "confirm_delete_shared_link": "Are you sure you want to delete this shared link?",
+  "confirm_keep_this_delete_others": "All other assets in the stack will be deleted except for this asset. Are you sure you want to continue?",
   "confirm_password": "Confirm password",
   "contain": "Contain",
   "context": "Context",
@@ -501,18 +526,21 @@
   "delete_api_key_prompt": "Are you sure you want to delete this API key?",
   "delete_duplicates_confirmation": "Are you sure you want to permanently delete these duplicates?",
   "delete_key": "Delete key",
-  "delete_library": "Delete library",
+  "delete_library": "Delete Library",
   "delete_link": "Delete link",
+  "delete_others": "Delete others",
   "delete_shared_link": "Delete shared link",
   "delete_tag": "Delete tag",
   "delete_tag_confirmation_prompt": "Are you sure you want to delete {tagName} tag?",
   "delete_user": "Delete user",
   "deleted_shared_link": "Deleted shared link",
+  "deletes_missing_assets": "Deletes assets missing from disk",
   "description": "Description",
   "details": "Details",
   "direction": "Direction",
   "disabled": "Disabled",
   "disallow_edits": "Disallow edits",
+  "discord": "Discord",
   "discover": "Discover",
   "dismiss_all_errors": "Dismiss all errors",
   "dismiss_error": "Dismiss error",
@@ -521,6 +549,7 @@
   "display_original_photos": "Display original photos",
   "display_original_photos_setting_description": "Prefer to display the original photo when viewing an asset rather than thumbnails when the original asset is web-compatible. This may result in slower photo display speeds.",
   "do_not_show_again": "Do not show this message again",
+  "documentation": "Documentation",
   "done": "Done",
   "download": "Download",
   "download_include_embedded_motion_videos": "Embedded videos",
@@ -590,6 +619,7 @@
     "failed_to_create_shared_link": "Failed to create shared link",
     "failed_to_edit_shared_link": "Failed to edit shared link",
     "failed_to_get_people": "Failed to get people",
+    "failed_to_keep_this_delete_others": "Failed to keep this asset and delete the other assets",
     "failed_to_load_asset": "Failed to load asset",
     "failed_to_load_assets": "Failed to load assets",
     "failed_to_load_people": "Failed to load people",
@@ -657,8 +687,8 @@
     "unable_to_remove_album_users": "Unable to remove users from album",
     "unable_to_remove_api_key": "Unable to remove API Key",
     "unable_to_remove_assets_from_shared_link": "Unable to remove assets from shared link",
+    "unable_to_remove_deleted_assets": "Unable to remove offline files",
     "unable_to_remove_library": "Unable to remove library",
-    "unable_to_remove_offline_files": "Unable to remove offline files",
     "unable_to_remove_partner": "Unable to remove partner",
     "unable_to_remove_reaction": "Unable to remove reaction",
     "unable_to_repair_items": "Unable to repair items",
@@ -704,6 +734,7 @@
   "external": "External",
   "external_libraries": "External Libraries",
   "face_unassigned": "Unassigned",
+  "failed_to_load_assets": "Failed to load assets",
   "favorite": "Favorite",
   "favorite_or_unfavorite_photo": "Favorite or unfavorite photo",
   "favorites": "Favorites",
@@ -719,7 +750,6 @@
   "fix_incorrect_match": "Fix incorrect match",
   "folders": "Folders",
   "folders_feature_description": "Browsing the folder view for the photos and videos on the file system",
-  "force_re-scan_library_files": "Force Re-scan All Library Files",
   "forward": "Forward",
   "general": "General",
   "get_help": "Get Help",
@@ -774,6 +804,8 @@
   "jobs": "Jobs",
   "keep": "Keep",
   "keep_all": "Keep All",
+  "keep_this_delete_others": "Keep this, delete others",
+  "kept_this_deleted_others": "Kept this asset and deleted {count, plural, one {# asset} other {# assets}}",
   "keyboard_shortcuts": "Keyboard shortcuts",
   "language": "Language",
   "language_setting_description": "Select your preferred language",
@@ -806,6 +838,7 @@
   "look": "Look",
   "loop_videos": "Loop videos",
   "loop_videos_description": "Enable to automatically loop a video in the detail viewer.",
+  "main_branch_warning": "You’re using a development version; we strongly recommend using a release version!",
   "make": "Make",
   "manage_shared_links": "Manage shared links",
   "manage_sharing_with_partners": "Manage sharing with partners",
@@ -875,6 +908,7 @@
   "notifications": "Notifications",
   "notifications_setting_description": "Manage notifications",
   "oauth": "OAuth",
+  "official_immich_resources": "Official Immich Resources",
   "offline": "Offline",
   "offline_paths": "Offline paths",
   "offline_paths_description": "These results may be due to manual deletion of files that are not part of an external library.",
@@ -887,7 +921,6 @@
   "onboarding_welcome_user": "Welcome, {user}",
   "online": "Online",
   "only_favorites": "Only favorites",
-  "only_refreshes_modified_files": "Only refreshes modified files",
   "open_in_map_view": "Open in map view",
   "open_in_openstreetmap": "Open in OpenStreetMap",
   "open_the_search_filters": "Open the search filters",
@@ -1001,14 +1034,17 @@
   "reassigned_assets_to_new_person": "Re-assigned {count, plural, one {# asset} other {# assets}} to a new person",
   "reassing_hint": "Assign selected assets to an existing person",
   "recent": "Recent",
+  "recent-albums": "Recent albums",
   "recent_searches": "Recent searches",
   "refresh": "Refresh",
   "refresh_encoded_videos": "Refresh encoded videos",
+  "refresh_faces": "Refresh faces",
   "refresh_metadata": "Refresh metadata",
   "refresh_thumbnails": "Refresh thumbnails",
   "refreshed": "Refreshed",
-  "refreshes_every_file": "Refreshes every file",
+  "refreshes_every_file": "Re-reads all existing and new files",
   "refreshing_encoded_video": "Refreshing encoded video",
+  "refreshing_faces": "Refreshing faces",
   "refreshing_metadata": "Refreshing metadata",
   "regenerating_thumbnails": "Regenerating thumbnails",
   "remove": "Remove",
@@ -1016,10 +1052,11 @@
   "remove_assets_shared_link_confirmation": "Are you sure you want to remove {count, plural, one {# asset} other {# assets}} from this shared link?",
   "remove_assets_title": "Remove assets?",
   "remove_custom_date_range": "Remove custom date range",
+  "remove_deleted_assets": "Remove Deleted Assets",
   "remove_from_album": "Remove from album",
   "remove_from_favorites": "Remove from favorites",
   "remove_from_shared_link": "Remove from shared link",
-  "remove_offline_files": "Remove Offline Files",
+  "remove_url": "Remove URL",
   "remove_user": "Remove user",
   "removed_api_key": "Removed API Key: {name}",
   "removed_from_archive": "Removed from archive",
@@ -1055,8 +1092,7 @@
   "saved_settings": "Saved settings",
   "say_something": "Say something",
   "scan_all_libraries": "Scan All Libraries",
-  "scan_all_library_files": "Re-scan All Library Files",
-  "scan_new_library_files": "Scan New Library Files",
+  "scan_library": "Scan",
   "scan_settings": "Scan Settings",
   "scanning_for_album": "Scanning for album...",
   "search": "Search",
@@ -1074,6 +1110,7 @@
   "search_options": "Search options",
   "search_people": "Search people",
   "search_places": "Search places",
+  "search_settings": "Search settings",
   "search_state": "Search state...",
   "search_tags": "Search tags...",
   "search_timezone": "Search timezone...",
@@ -1105,6 +1142,7 @@
   "set": "Set",
   "set_as_album_cover": "Set as album cover",
   "set_as_profile_picture": "Set as profile picture",
+  "set_as_featured_photo": "Set as featured photo",
   "set_date_of_birth": "Set date of birth",
   "set_profile_picture": "Set profile picture",
   "set_slideshow_to_fullscreen": "Set Slideshow to fullscreen",
@@ -1140,6 +1178,7 @@
   "show_person_options": "Show person options",
   "show_progress_bar": "Show Progress Bar",
   "show_search_options": "Show search options",
+  "show_slideshow_transition": "Show slideshow transition",
   "show_supporter_badge": "Supporter badge",
   "show_supporter_badge_description": "Show a supporter badge",
   "shuffle": "Shuffle",
@@ -1158,6 +1197,7 @@
   "sort_items": "Number of items",
   "sort_modified": "Date modified",
   "sort_oldest": "Oldest photo",
+  "sort_people_by_similarity": "Sort people by similarity",
   "sort_recent": "Most recent photo",
   "sort_title": "Title",
   "source": "Source",
@@ -1181,13 +1221,16 @@
   "submit": "Submit",
   "suggestions": "Suggestions",
   "sunrise_on_the_beach": "Sunrise on the beach",
+  "support": "Support",
+  "support_and_feedback": "Support & Feedback",
+  "support_third_party_description": "Your Immich installation was packaged by a third-party. Issues you experience may be caused by that package, so please raise issues with them in the first instance using the links below.",
   "swap_merge_direction": "Swap merge direction",
   "sync": "Sync",
   "tag": "Tag",
   "tag_assets": "Tag assets",
   "tag_created": "Created tag: {tag}",
   "tag_feature_description": "Browsing photos and videos grouped by logical tag topics",
-  "tag_not_found_question": "Cannot find a tag? Create one <link>here</link>",
+  "tag_not_found_question": "Cannot find a tag? <link>Create a new tag.</link>",
   "tag_updated": "Updated tag: {tag}",
   "tagged_assets": "Tagged {count, plural, one {# asset} other {# assets}}",
   "tags": "Tags",
@@ -1196,7 +1239,9 @@
   "theme_selection": "Theme selection",
   "theme_selection_description": "Automatically set the theme to light or dark based on your browser's system preference",
   "they_will_be_merged_together": "They will be merged together",
+  "third_party_resources": "Third-Party Resources",
   "time_based_memories": "Time-based memories",
+  "timeline": "Timeline",
   "timezone": "Timezone",
   "to_archive": "Archive",
   "to_change_password": "Change password",
@@ -1206,6 +1251,7 @@
   "to_trash": "Trash",
   "toggle_settings": "Toggle settings",
   "toggle_theme": "Toggle dark theme",
+  "total": "Total",
   "total_usage": "Total usage",
   "trash": "Trash",
   "trash_all": "Trash All",
@@ -1255,6 +1301,8 @@
   "user_purchase_settings_description": "Manage your purchase",
   "user_role_set": "Set {user} as {role}",
   "user_usage_detail": "User usage detail",
+  "user_usage_stats": "Account usage statistics",
+  "user_usage_stats_description": "View account usage statistics",
   "username": "Username",
   "users": "Users",
   "utilities": "Utilities",
@@ -1262,7 +1310,9 @@
   "variables": "Variables",
   "version": "Version",
   "version_announcement_closing": "Your friend, Alex",
-  "version_announcement_message": "Hi friend, there is a new version of the application please take your time to visit the <link>release notes</link> and ensure your <code>docker-compose.yml</code>, and <code>.env</code> setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your application automatically.",
+  "version_announcement_message": "Hi there! A new version of Immich is available. Please take some time to read the <link>release notes</link> to ensure your setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your Immich instance automatically.",
+  "version_history": "Version History",
+  "version_history_item": "Installed {version} on {date}",
   "video": "Video",
   "video_hover_setting": "Play video thumbnail on hover",
   "video_hover_setting_description": "Play video thumbnail when mouse is hovering over item. Even when disabled, playback can be started by hovering over the play icon.",
@@ -1274,6 +1324,7 @@
   "view_all_users": "View all users",
   "view_in_timeline": "View in timeline",
   "view_links": "View links",
+  "view_name": "View",
   "view_next_asset": "View next asset",
   "view_previous_asset": "View previous asset",
   "view_stack": "View Stack",
@@ -1282,8 +1333,12 @@
   "warning": "Warning",
   "week": "Week",
   "welcome": "Welcome",
+<<<<<<< HEAD:web/src/lib/i18n/en.json
   "welcome_to_immich": "Welcome to immich",
   "workflows": "Workflows",
+=======
+  "welcome_to_immich": "Welcome to Immich",
+>>>>>>> 34ce61d03a206f616325491281882afff4b617f8:i18n/en.json
   "year": "Year",
   "years_ago": "{years, plural, one {# year} other {# years}} ago",
   "yes": "Yes",
diff --git a/web/src/lib/i18n/es.json b/i18n/es.json
similarity index 81%
rename from web/src/lib/i18n/es.json
rename to i18n/es.json
index f0c89ffb46..10a0085f25 100644
--- a/web/src/lib/i18n/es.json
+++ b/i18n/es.json
@@ -1,5 +1,5 @@
 {
-  "about": "Acerca de",
+  "about": "Acercade",
   "account": "Cuenta",
   "account_settings": "Ajustes de la cuenta",
   "acknowledge": "De acuerdo",
@@ -7,69 +7,81 @@
   "actions": "Acciones",
   "active": "Activo",
   "activity": "Actividad",
-  "activity_changed": "La actividad {enabled, select, true {activada} other {desactivada}}",
-  "add": "Añadir",
-  "add_a_description": "Añadir una descripción",
-  "add_a_location": "Añadir una ubicación",
-  "add_a_name": "Añadir un nombre",
-  "add_a_title": "Añadir un título",
-  "add_exclusion_pattern": "Añadir patrón de exclusión",
-  "add_import_path": "Añadir ruta de importación",
-  "add_location": "Añadir ubicación",
-  "add_more_users": "Añadir más usuarios",
-  "add_partner": "Añadir invitado",
-  "add_path": "Añadir ruta",
-  "add_photos": "Añadir fotos",
-  "add_to": "Añadir a...",
-  "add_to_album": "Añadir a un álbum",
-  "add_to_shared_album": "Añadir a un álbum compartido",
-  "added_to_archive": "Archivar",
-  "added_to_favorites": "Añadido a favoritos",
-  "added_to_favorites_count": "Añadido {count, number} a favoritos",
+  "activity_changed": "La actividad está {enabled, select, true {activada} other {desactivada}}",
+  "add": "Agregar",
+  "add_a_description": "Agregar descripción",
+  "add_a_location": "Agregar ubicación",
+  "add_a_name": "Agregar nombre",
+  "add_a_title": "Agregar título",
+  "add_exclusion_pattern": "Agregar patrón de exclusión",
+  "add_import_path": "Agregar ruta de importación",
+  "add_location": "Agregar ubicación",
+  "add_more_users": "Agregar más usuarios",
+  "add_partner": "Agregar compañero",
+  "add_path": "Agregar carpeta",
+  "add_photos": "Agregar fotos",
+  "add_to": "Agregar a...",
+  "add_to_album": "Incluir en álbum",
+  "add_to_shared_album": "Incluir en álbum compartido",
+  "add_url": "Añadir URL",
+  "added_to_archive": "Archivado",
+  "added_to_favorites": "Agregado a favoritos",
+  "added_to_favorites_count": "Agregado {count, number} a favoritos",
   "admin": {
-    "add_exclusion_pattern_description": "Añade patrones de exclusión. Puedes utilizar los caracteres *, ** y ? (globbing). Para ignorar los archivos en cualquier ruta llamada \"Raw\", utiliza \"**/Raw/**\". Para ignorar todos los archivos que terminan en \".tif\", utiliza \"**/*.tif\". Para ignorar una ruta desde la raíz, utiliza \"/carpeta/a/ignorar/**\".",
-    "authentication_settings": "Configuración de autenticación",
-    "authentication_settings_description": "Gestionar clave, Oauth y otros configuraciones de autenticación",
-    "authentication_settings_disable_all": "¿Estás seguro de que deseas desactivar todos los métodos de inicio de sesión? Se desactivará el inicio de sesión.",
-    "authentication_settings_reenable": "Para volver a habilitar, utilice un <link> Comando del servidor </link> .",
+    "add_exclusion_pattern_description": "Agrega patrones de exclusión. Puedes utilizar los caracteres *, ** y ? (globbing). Ejemplos: para ignorar todos los archivos en cualquier directorio llamado \"Raw\", utiliza \"**/Raw/**\". Para ignorar todos los archivos que terminan en \".tif\", utiliza \"**/*.tif\". Para ignorar una ruta absoluta, utiliza \"/carpeta/a/ignorar/**\".",
+    "asset_offline_description": "Este recurso externo de la biblioteca ya no se encuentra en el disco y se ha movido a la papelera. Si el archivo se movió dentro de la biblioteca, comprueba la línea temporal para el nuevo recurso correspondiente. Para restaurar este recurso, asegúrate de que Immich puede acceder a la siguiente ruta de archivo y escanear la biblioteca.",
+    "authentication_settings": "Parámetros de autenticación",
+    "authentication_settings_description": "Gestionar contraseñas, OAuth y otros parámetros de autenticación",
+    "authentication_settings_disable_all": "¿Estás seguro de que deseas desactivar todos los métodos de inicio de sesión? Esto desactivará por completo el inicio de sesión.",
+    "authentication_settings_reenable": "Para reactivarlo, utiliza un <link>Comando del servidor</link>.",
     "background_task_job": "Tareas en segundo plano",
-    "check_all": "Comprobar todo",
-    "cleared_jobs": "Trabajos realizados para: {job}",
-    "config_set_by_file": "La configuración está fijada actualmente en base a un archivo",
+    "backup_database": "Respaldar base de datos",
+    "backup_database_enable_description": "Activar respaldo de base de datos",
+    "backup_keep_last_amount": "Cantidad de respaldos previos a mantener",
+    "backup_settings": "Ajustes de respaldo",
+    "backup_settings_description": "Administrar configuración de respaldo de base de datos",
+    "check_all": "Verificar todo",
+    "cleared_jobs": "Trabajos borrados para: {job}",
+    "config_set_by_file": "La configuración está definida por un archivo de configuración",
     "confirm_delete_library": "¿Estás seguro de que quieres eliminar la biblioteca {library}?",
-    "confirm_delete_library_assets": "¿Estás seguro de que quieras eliminar esta biblioteca? Esto eliminará los {count, plural, one {# contained asset} other {all # contained assets}} elementos en Immich y no puede deshacerse. Los archivos permanecerán en tu almacenamiento.",
-    "confirm_email_below": "Para confirmar, escribe \"{email}\" debajo",
-    "confirm_reprocess_all_faces": "¿Estás seguro de que quieres volver a procesar todas las caras? Esto también eliminará las personas a las que le hayas asignado nombre.",
-    "confirm_user_password_reset": "¿Estás seguro de que quieres resetear la contraseña de {user}?",
-    "crontab_guru": "Crontab Guru",
+    "confirm_delete_library_assets": "¿Estás seguro de que quieras eliminar esta biblioteca? Esto eliminará los {count, plural, one {# contained asset} other {all # contained assets}} elementos en Immich y no puede deshacerse. Los archivos permanecerán en disco.",
+    "confirm_email_below": "Para confirmar, escribe \"{email}\" a continuación",
+    "confirm_reprocess_all_faces": "¿Estás seguro de que deseas reprocesar todas las caras? Esto borrará a todas las personas que nombraste.",
+    "confirm_user_password_reset": "¿Estás seguro de que quieres restablecer la contraseña de {user}?",
+    "create_job": "Crear trabajo",
+    "cron_expression": "Expresión CRON",
+    "cron_expression_description": "Establece el intervalo de escaneo utilizando el formato CRON. Para más información puedes consultar, por ejemplo, <link> Crontab Guru</link>",
+    "cron_expression_presets": "Valores predefinidos de expresión CRON",
     "disable_login": "Deshabilitar inicio de sesión",
-    "disabled": "Deshabilitado",
-    "duplicate_detection_job_description": "Lanza el aprendizaje automático para detectar imágenes similares. Necesita que esté activa la Búsqueda Inteligente",
-    "exclusion_pattern_description": "Los patrones de exclusión te permiten ignorar archivos y carpetas al escanear tu biblioteca. Esto es útil hay carpetas que contienen archivos que no quieres importar (por ejemplo los ficheros RAW).",
-    "external_library_created_at": "Biblioteca externa (creado el {date})",
+    "duplicate_detection_job_description": "Lanza el aprendizaje automático para detectar imágenes similares. Necesita tener activado \"Búsqueda Inteligente\"",
+    "exclusion_pattern_description": "Los patrones de exclusión te permiten ignorar archivos y carpetas al escanear tu biblioteca. Es útil si tienes carpetas que contienen archivos que no deseas importar, por ejemplo archivos RAW.",
+    "external_library_created_at": "Biblioteca externa (creada el {date})",
     "external_library_management": "Gestión de bibliotecas externas",
     "face_detection": "Detección de caras",
-    "face_detection_description": "Detecta las caras usando aprendizaje automático. Para los vídeos sólo se tiene en cuenta la imagen de previsualización. \"Todo\" implica volver a procesar todos los elementos. \"Missing\" pone en la cola los elementos que aún no han sido procesados. Las caras detectadas serán añadidas a la cola para ser procesadas posteriormente mediante Reconocimiento Facial y agrupadas en las personas que ya existan o en nuevas personas detectadas.",
-    "facial_recognition_job_description": "Agrupa las caras detectadas en las personas. Este paso se lanza tras las Detección de Caras. \"All\" reagrupa todas las caras. \"Pendiente\" añade a la colas aquellas caras que no fueron asignadas a ninguna persona.",
+    "face_detection_description": "Detecta las caras en los activos mediante aprendizaje automático. En el caso de los vídeos, solo se tiene en cuenta la miniatura. \"Actualizar\" (re)procesará todos los elementos. \"Restablecer\" borra además todos los datos de caras actuales. \"Falta\" pone en cola los elementos que aún no se han procesado. Las caras detectadas se pondrán en cola para el reconocimiento facial una vez finalizada la detección, agrupándolos en personas existentes o nuevas.",
+    "facial_recognition_job_description": "Agrupa las caras detectadas en personas. Este paso se ejecuta una vez finalizada la detección de caras. \"Restablecer\" (re)agrupa todas las caras. \"Falta\" pone en cola las caras que no tienen asignada una persona.",
     "failed_job_command": "El comando {command} ha fallado para la tarea: {job}",
-    "force_delete_user_warning": "CUIDADO: Esta acción eliminará inmediatamente el usuario y los elementos. Esta accion no se puede deshacer y los archivos no pueden ser recuperados.",
-    "forcing_refresh_library_files": "Forzar la recarga de todos los archivos de la biblioteca",
-    "image_format_description": "WebP genera archivos más pequeños que JPEG, pero es más lento al codificar.",
-    "image_prefer_embedded_preview": "Preferir vista previa incrustada",
-    "image_prefer_embedded_preview_setting_description": "Usar vistas previas incrustadas en fotos RAW como entrada para el procesamiento de imágenes cuando estén disponibles. Esto puede producir colores más precisos en algunas imágenes, pero la calidad de la vista previa depende de la cámara y la imagen puede tener más artefactos de compresión.",
-    "image_prefer_wide_gamut": "Preferir gama amplia",
-    "image_prefer_wide_gamut_setting_description": "Usar \"Display P3\" para las miniaturas. Esto preserva mejor la vivacidad de las imágenes con espacios de color amplios, pero las imágenes pueden aparecer de manera diferente en dispositivos antiguos con una versión antigua del navegador. Las imágenes sRGB se mantienen como sRGB para evitar cambios de color.",
-    "image_preview_format": "Formato de previsualización",
-    "image_preview_resolution": "Resolución de previsualización",
-    "image_preview_resolution_description": "Se utiliza al ver una sola foto y para el aprendizaje automático. Las resoluciones más altas pueden preservar más detalles, pero tardan más en codificarse, tienen tamaños de archivo más grandes y pueden reducir la capacidad de respuesta de la aplicación.",
+    "force_delete_user_warning": "CUIDADO: Esta acción eliminará inmediatamente el usuario y todos los elementos. Esta accion no se puede deshacer y los archivos no pueden ser recuperados.",
+    "forcing_refresh_library_files": "Forzando la recarga de todos los elementos en la biblioteca",
+    "image_format": "Formato",
+    "image_format_description": "WebP genera archivos más pequeños que JPEG, pero es más lento al codificarlos.",
+    "image_prefer_embedded_preview": "Preferir vista previa embebida",
+    "image_prefer_embedded_preview_setting_description": "Usar vistas previas embebidas en fotos RAW como entrada para el procesamiento de imágenes cuando estén disponibles. Esto puede producir colores más precisos en algunas imágenes, pero la calidad de la vista previa depende de la cámara y la imagen puede tener más artefactos de compresión.",
+    "image_prefer_wide_gamut": "Preferir 'gamut' amplio",
+    "image_prefer_wide_gamut_setting_description": "Usar \"Display P3\" para las miniaturas. Preserva mejor la vivacidad de las imágenes con espacios de color amplios pero las imágenes pueden aparecer de manera diferente en dispositivos antiguos con una versión antigua del navegador. Las imágenes sRGB se mantienen como sRGB para evitar cambios de color.",
+    "image_preview_description": "Imagen de tamaño mediano con metadatos eliminados. Es utilizado al visualizar un solo activo y para el aprendizaje automático",
+    "image_preview_quality_description": "Calidad de vista previa de 1 a 100. Es mejor cuanto más alta sea la calidad pero genera archivos más grandes y puede reducir la capacidad de respuesta de la aplicación. Establecer un valor bajo puede afectar la calidad del aprendizaje automático.",
+    "image_preview_title": "Ajustes de la vista previa",
     "image_quality": "Calidad",
-    "image_quality_description": "Calidad de imagen de 1 a 100. Un valor más alto mejora la calidad pero genera archivos más grandes.",
+    "image_resolution": "Resolución",
+    "image_resolution_description": "Las resoluciones más altas pueden conservar más detalles pero requieren más tiempo para codificar, tienen tamaños de archivo más grandes y pueden afectar la capacidad de respuesta de la aplicación.",
     "image_settings": "Ajustes de imagen",
     "image_settings_description": "Administrar la calidad y resolución de las imágenes generadas",
-    "image_thumbnail_format": "Formato de las miniaturas",
-    "image_thumbnail_resolution": "Resolución de las miniaturas",
-    "image_thumbnail_resolution_description": "Se utiliza para ver grupos de fotos (cronología, vista de álbum, etc.). Las resoluciones más altas pueden conservar más detalles, pero tardan más en codificarse, tienen archivos de mayor tamaño y pueden reducir la reactividad de la aplicación.",
+    "image_thumbnail_description": "Miniatura pequeña con metadatos eliminados. Se utiliza al visualizar grupos de fotos como la línea temporal principal",
+    "image_thumbnail_quality_description": "Calidad de miniatura de 1 a 100. Es mejor cuanto más alto es el valor pero genera archivos más grandes y puede reducir la capacidad de respuesta de la aplicación.",
+    "image_thumbnail_title": "Ajustes de las miniaturas",
     "job_concurrency": "{job}: Procesos simultáneos",
+    "job_created": "Trabajo creado",
     "job_not_concurrency_safe": "Esta tarea no es segura para la simultaneidad.",
     "job_settings": "Configuración tareas",
     "job_settings_description": "Administrar tareas simultáneas",
@@ -77,52 +89,49 @@
     "jobs_delayed": "{jobCount, plural, one {# retrasado} other {# retrasados}}",
     "jobs_failed": "{jobCount, plural, one {# fallido} other {# fallidos}}",
     "library_created": "La biblioteca ha sido creada: {library}",
-    "library_cron_expression": "Expresión cron",
-    "library_cron_expression_description": "Establece el intervalo de escaneo utilizando el formato cron. Para más información puede consultar, por ejemplo, <link> Crontab Guru</link>",
-    "library_cron_expression_presets": "Valores predefinidos de expresión cron",
     "library_deleted": "Biblioteca eliminada",
-    "library_import_path_description": "Indica una carpeta para importar. Esta carpeta, incluidas las subcarpetas, serán escaneadas en busca de multimedia.",
-    "library_scanning": "Escaneado periódico",
-    "library_scanning_description": "Configura el escaneo periódico de la biblioteca",
+    "library_import_path_description": "Indica una carpeta para importar. Esta carpeta y sus subcarpetas serán escaneadas en busca de elementos multimedia.",
+    "library_scanning": "Escaneo periódico",
+    "library_scanning_description": "Configurar el escaneo periódico de la biblioteca",
     "library_scanning_enable_description": "Activar el escaneo periódico de la biblioteca",
     "library_settings": "Biblioteca externa",
     "library_settings_description": "Administrar configuración biblioteca externa",
     "library_tasks_description": "Realizar tareas de biblioteca",
-    "library_watching_enable_description": "Ver las bibliotecas externas para detectar cambios en los archivos",
+    "library_watching_enable_description": "Vigilar las bibliotecas externas para detectar cambios en los archivos",
     "library_watching_settings": "Vigilancia de la biblioteca (EXPERIMENTAL)",
     "library_watching_settings_description": "Vigilar automaticamente en busca de archivos modificados",
     "logging_enable_description": "Habilitar registro",
-    "logging_level_description": "Cuando está habilitado, qué nivel de registro utilizar.",
+    "logging_level_description": "Indica el nivel de registro a utilizar cuando está habilitado.",
     "logging_settings": "Registro",
-    "machine_learning_clip_model": "Modelo de CLIP",
-    "machine_learning_clip_model_description": "El nombre de un modelo CLIP listado <link>aquí</link>. Tenga en cuenta que debe volver a ejecutar el trabajo 'Smart Search' para todas las imágenes al cambiar un modelo.",
-    "machine_learning_duplicate_detection": "Detección duplicados",
+    "machine_learning_clip_model": "Modelo CLIP (Contrastive Language-Image Pre-Training)",
+    "machine_learning_clip_model_description": "El nombre de un modelo CLIP listado <link>aquí</link>. Tendrás que relanzar el trabajo 'Búsqueda Inteligente' para todos los elementos al cambiar de modelo.",
+    "machine_learning_duplicate_detection": "Detección de duplicados",
     "machine_learning_duplicate_detection_enabled": "Habilitar detección de duplicados",
     "machine_learning_duplicate_detection_enabled_description": "Si está deshabilitado, los activos exactamente idénticos seguirán siendo eliminados.",
-    "machine_learning_duplicate_detection_setting_description": "Utilice incrustaciones de CLIP para encontrar posibles duplicados",
+    "machine_learning_duplicate_detection_setting_description": "Usa incrustaciones de CLIP (Contrastive Language-Image Pre-Training) para encontrar posibles duplicados",
     "machine_learning_enabled": "Habilitar aprendizaje automático",
-    "machine_learning_enabled_description": "Si está deshabilitada, todas las funciones de ML se deshabilitarán independientemente de la configuración a continuación.",
+    "machine_learning_enabled_description": "Al desactivarla todas las funciones de ML se deshabilitarán independientemente de la configuración a continuación.",
     "machine_learning_facial_recognition": "Reconocimiento facial",
-    "machine_learning_facial_recognition_description": "Detecta, reconoce y agrupa las caras en imágenes",
+    "machine_learning_facial_recognition_description": "Detecta, reconoce y agrupa las caras en las imágenes",
     "machine_learning_facial_recognition_model": "Modelo de reconocimiento facial",
-    "machine_learning_facial_recognition_model_description": "Los modelos se enumeran en orden descendente de tamaño. Los modelos más grandes son más lentos y utilizan más memoria, pero producen mejores resultados. Tenga en cuenta que debe volver a ejecutar la tarea de reconocimiento facial para todas las imágenes al cambiar un modelo.",
-    "machine_learning_facial_recognition_setting": "Habilitar reconocimiento de caras",
-    "machine_learning_facial_recognition_setting_description": "Si está deshabilitada, las imágenes no se procesarán para el reconocimiento facial y no se incluirán en la sección Personas en la página Explorar.",
-    "machine_learning_max_detection_distance": "Distancia máxima de detección",
-    "machine_learning_max_detection_distance_description": "Distancia máxima entre dos imágenes para considerarlas duplicadas, oscilando entre 0,001-0,1. Los valores más altos detectarán más duplicados, pero pueden generar falsos positivos.",
-    "machine_learning_max_recognition_distance": "Distancia máxima de reconocimiento",
-    "machine_learning_max_recognition_distance_description": "Distancia máxima entre dos rostros para que se consideren una misma persona, oscilando entre 0-2. Reducirlo puede evitar etiquetar a dos personas como la misma persona, mientras que aumentarlo puede evitar etiquetar a la misma persona como dos personas diferentes. Tenga en cuenta que es más fácil fusionar a dos personas que dividir a una en dos, así que opte por un umbral más bajo cuando sea posible.",
+    "machine_learning_facial_recognition_model_description": "Los modelos se muestran en orden descendente de tamaño. Los modelos más grandes son más lentos y utilizan más memoria pero producen mejores resultados. Ten en cuenta que debes volver a ejecutar la tarea de reconocimiento facial para todas las imágenes al cambiar de modelo.",
+    "machine_learning_facial_recognition_setting": "Habilitar reconocimiento facial",
+    "machine_learning_facial_recognition_setting_description": "Cuando está desactivado no se utlizará reconocimiento facial y no aparecerán nuevas caras en la sección Personas en la página Explorar.",
+    "machine_learning_max_detection_distance": "Máxima distancia de detección",
+    "machine_learning_max_detection_distance_description": "Distancia máxima entre dos imágenes para considerarlas duplicadas, oscilando entre 0,001-0,1. Los valores más altos detectarán más duplicados pero pueden generar falsos positivos.",
+    "machine_learning_max_recognition_distance": "Máxima distancia de reconocimiento",
+    "machine_learning_max_recognition_distance_description": "Distancia máxima entre dos caras para que se consideren una misma persona, oscilando entre 0-2. Reducirlo puede evitar etiquetar a dos personas como la misma persona, mientras que aumentarlo puede evitar etiquetar a la misma persona como dos personas diferentes. Ten en cuenta que es más fácil fusionar a dos personas que dividir a una en dos, así que opta por un umbral más bajo cuando sea posible.",
     "machine_learning_min_detection_score": "Puntuación mínima de detección",
-    "machine_learning_min_detection_score_description": "Puntuación de confianza mínima para que se detecte una cara de 0 a 1. Los valores más bajos detectarán más rostros, pero pueden generar falsos positivos.",
+    "machine_learning_min_detection_score_description": "Puntuación de confianza mínima para que se detecte una cara de 0 a 1. Los valores más bajos detectarán más rostros pero pueden generar falsos positivos.",
     "machine_learning_min_recognized_faces": "Rostros mínimos reconocidos",
-    "machine_learning_min_recognized_faces_description": "El número mínimo de rostros reconocidos para que se cree una persona. Aumentar esto hace que el reconocimiento facial sea más preciso a costa de aumentar la posibilidad de que no se asigne una cara a una persona.",
+    "machine_learning_min_recognized_faces_description": "El número mínimo de rostros reconocidos para que se cree una persona. Aumentar esto permite que el reconocimiento facial sea más preciso a costa de aumentar la posibilidad de que no se asigne una cara a una persona.",
     "machine_learning_settings": "Configuración de aprendizaje automático",
     "machine_learning_settings_description": "Administrar funciones y configuraciones de aprendizaje automático",
     "machine_learning_smart_search": "Busqueda inteligente",
-    "machine_learning_smart_search_description": "Busque imágenes semánticamente utilizando incrustaciones CLIP",
+    "machine_learning_smart_search_description": "Busque imágenes semánticamente utilizando incrustaciones CLIP (Contrastive Language-Image Pre-Training)",
     "machine_learning_smart_search_enabled": "Habilitar búsqueda inteligente",
-    "machine_learning_smart_search_enabled_description": "Si está deshabilitado, las imágenes no se codificarán para la búsqueda inteligente.",
-    "machine_learning_url_description": "URL del servidor de aprendizaje automático",
+    "machine_learning_smart_search_enabled_description": "Al desactivarlo las imágenes no se procesarán para usar la búsqueda inteligente.",
+    "machine_learning_url_description": "La URL del servidor de aprendizaje automático. Si se proporciona más de una URL se intentará acceder a cada servidor sucesivamente hasta que uno responda correctamente en el orden especificado.",
     "manage_concurrency": "Ajustes de concurrencia",
     "manage_log_settings": "Administrar la configuración de los registros",
     "map_dark_style": "Estilo oscuro",
@@ -152,7 +161,7 @@
     "note_cannot_be_changed_later": "NOTA: No se puede cambiar posteriormente!",
     "note_unlimited_quota": "Nota: usa 0 para espacio sin límites",
     "notification_email_from_address": "Desde",
-    "notification_email_from_address_description": "Dirección de correo electrónico del remitente, por ejemplo: \"Immich Photo Server <noreply@immich.app>\"",
+    "notification_email_from_address_description": "Dirección de correo electrónico del remitente, por ejemplo: \"Immich Photo Server <noreply@example.com>\"",
     "notification_email_host_description": "Host del servidor de correo electrónico (por ejemplo: smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Ignorar errores de certificado",
     "notification_email_ignore_certificate_errors_description": "Ignorar los errores de validación del certificado TLS (no recomendado)",
@@ -198,22 +207,24 @@
     "password_settings": "Contraseña de Acceso",
     "password_settings_description": "Administrar la configuración de inicio de sesión con contraseña",
     "paths_validated_successfully": "Todas las carpetas se han validado satisfactoriamente",
+    "person_cleanup_job": "Limpieza de personas",
     "quota_size_gib": "Tamaño de Quota (GiB)",
     "refreshing_all_libraries": "Actualizar todas las bibliotecas",
     "registration": "Registrar administrador",
     "registration_description": "Dado que eres el primer usuario del sistema, se te asignará como Admin y serás responsable de las tareas administrativas, y de crear a los usuarios adicionales.",
-    "removing_offline_files": "Eliminando archivos sin conexión",
     "repair_all": "Reparar todo",
     "repair_matched_items": "Coincidencia {count, plural, one {# elemento} other {# elementos}}",
     "repaired_items": "Reparado {count, plural, one {# elemento} other {# elementos}}",
     "require_password_change_on_login": "Requerir que el usuario cambie la contraseña en el primer inicio de sesión",
     "reset_settings_to_default": "Restablecer la configuración predeterminada",
     "reset_settings_to_recent_saved": "Restablecer la configuración a la configuración guardada recientemente",
-    "scanning_library_for_changed_files": "Escanear archivos modificados en biblioteca",
-    "scanning_library_for_new_files": "Escanear nuevos archivos en biblioteca",
+    "scanning_library": "Escaneando la biblioteca",
+    "search_jobs": "Buscar trabajo...",
     "send_welcome_email": "Enviar correo de bienvenida",
     "server_external_domain_settings": "Dominio externo",
     "server_external_domain_settings_description": "Dominio para enlaces públicos compartidos, incluidos http(s)://",
+    "server_public_users": "Usuarios públicos",
+    "server_public_users_description": "Todos los usuarios (nombre y correo electrónico) aparecen en la lista cuando se añade un usuario a los álbumes compartidos. Si se desactiva, la lista de usuarios sólo estará disponible para los usuarios administradores.",
     "server_settings": "Configuración del servidor",
     "server_settings_description": "Administrar la configuración del servidor",
     "server_welcome_message": "Mensaje de bienvenida",
@@ -238,6 +249,17 @@
     "storage_template_settings_description": "Administre la estructura de carpetas y el nombre de archivo del recurso cargado",
     "storage_template_user_label": "<code>{label}</code> es la etiqueta de almacenamiento del usuario",
     "system_settings": "Ajustes del Sistema",
+    "tag_cleanup_job": "Limpieza de etiquetas",
+    "template_email_available_tags": "Puede utilizar las siguientes variables en su plantilla: {tags}",
+    "template_email_if_empty": "Si la plantilla está vacía, se utilizará el correo electrónico predeterminado.",
+    "template_email_invite_album": "Plantilla de álbum de invitaciones",
+    "template_email_preview": "Vista previa",
+    "template_email_settings": "Modelos de correo electrónico",
+    "template_email_settings_description": "Gestionar plantillas de notificación por correo electrónico personalizadas",
+    "template_email_update_album": "Actualizar plantilla del álbum",
+    "template_email_welcome": "Plantilla de correo electrónico de bienvenida",
+    "template_settings": "Plantillas de notificación",
+    "template_settings_description": "Gestione plantillas personalizadas para las notificaciones.",
     "theme_custom_css_settings": "CSS Personalizado",
     "theme_custom_css_settings_description": "Las Hojas de Estilo (CSS) permiten personalizar el diseño de Immich.",
     "theme_settings": "Ajustes Tema",
@@ -245,7 +267,6 @@
     "these_files_matched_by_checksum": "Estos archivos coinciden con sus checksums",
     "thumbnail_generation_job": "Generar Miniaturas",
     "thumbnail_generation_job_description": "Genere miniaturas grandes, pequeñas y borrosas para cada archivo, así como miniaturas para cada persona",
-    "transcode_policy_description": "Política sobre cuándo se debe transcodificar un vídeo. Los vídeos HDR siempre se transcodificarán (excepto si la transcodificación está desactivada).",
     "transcoding_acceleration_api": "API Aceleración",
     "transcoding_acceleration_api_description": "La API que interactuará con su dispositivo para acelerar la transcodificación. Esta configuración es el \"mejor esfuerzo\": recurrirá a la transcodificación del software en caso de error. VP9 puede funcionar o no dependiendo de su hardware.",
     "transcoding_acceleration_nvenc": "NVENC (requiere GPU NVIDIA)",
@@ -262,7 +283,7 @@
     "transcoding_audio_codec": "Codec de audio",
     "transcoding_audio_codec_description": "Opus es la opción de mayor calidad, pero tiene menor compatibilidad con dispositivos o software antiguos.",
     "transcoding_bitrate_description": "Vídeos con una tasa de bits superior a la máxima o que no están en un formato aceptado",
-    "transcoding_codecs_learn_more": "Para obtener más información sobre la terminología utilizada aquí, consulte la documentación de FFmpeg para <h264-link>H.264 codec</h264-link>, <hevc-link>HEVC codec</hevc-link> y <vp9-link>VP9 codec</vp9-link>.",
+    "transcoding_codecs_learn_more": "Para obtener más información sobre la terminología utilizada aquí, consulte la documentación de FFmpeg sobre los codecs <h264-link>H.264</h264-link>, <hevc-link>HEVC</hevc-link> y <vp9-link>VP9</vp9-link>.",
     "transcoding_constant_quality_mode": "Modo de calidad constante",
     "transcoding_constant_quality_mode_description": "ICQ es mejor que CQP, pero algunos dispositivos de aceleración de hardware no admiten este modo. Al configurar esta opción, se preferirá el modo especificado cuando se utilice codificación basada en calidad. NVENC lo ignora porque no es compatible con ICQ.",
     "transcoding_constant_rate_factor": "Factor de tasa constante (-crf)",
@@ -271,7 +292,7 @@
     "transcoding_hardware_acceleration": "Aceleración por Hardware",
     "transcoding_hardware_acceleration_description": "Experimental; mucho más rápido, pero tendrá menor calidad con la misma tasa de bits",
     "transcoding_hardware_decoding": "Decodificación por hardware",
-    "transcoding_hardware_decoding_setting_description": "Se aplica únicamente a NVENC, QSV y RKMPP. Habilita la aceleración de un extremo a otro en lugar de solo acelerar la codificación. Puede que no funcione en todos los vídeos.",
+    "transcoding_hardware_decoding_setting_description": "Permite la aceleración de extremo a extremo en lugar de acelerar únicamente la codificación. Puede que no funcione en todos los vídeos.",
     "transcoding_hevc_codec": "Codec HEVC",
     "transcoding_max_b_frames": "Maximos B-frames",
     "transcoding_max_b_frames_description": "Los valores más altos mejoran la eficiencia de la compresión, pero ralentizan la codificación. Puede que no sea compatible con la aceleración de hardware en dispositivos más antiguos. 0 desactiva los fotogramas B, mientras que -1 establece este valor automáticamente.",
@@ -297,8 +318,6 @@
     "transcoding_threads_description": "Los valores más altos conducen a una codificación más rápida, pero dejan menos espacio para que el servidor procese otras tareas mientras está activo. Este valor no debe ser mayor que la cantidad de núcleos de CPU. Maximiza la utilización si se establece en 0.",
     "transcoding_tone_mapping": "Mapeo de tonos",
     "transcoding_tone_mapping_description": "Intenta preservar la apariencia de los videos HDR cuando se convierten a SDR. Cada algoritmo realiza diferentes compensaciones en cuanto a color, detalle y brillo. Hable conserva los detalles, Mobius conserva el color y Reinhard conserva el brillo.",
-    "transcoding_tone_mapping_npl": "Mapeo de tonos NPL",
-    "transcoding_tone_mapping_npl_description": "Los colores se ajustarán para que parezcan normales en una pantalla con este brillo. Contrariamente a la intuición, los valores más bajos aumentan el brillo del vídeo y viceversa, ya que compensan el brillo de la pantalla. 0 establece este valor automáticamente.",
     "transcoding_transcode_policy": "Políticas de transcodificación",
     "transcoding_transcode_policy_description": "Política sobre cuándo se debe transcodificar un vídeo. Los vídeos HDR siempre se transcodificarán (excepto si la transcodificación está desactivada).",
     "transcoding_two_pass_encoding": "Codificación en dos pasadas",
@@ -312,7 +331,8 @@
     "trash_settings_description": "Administrar la configuración de la papelera",
     "untracked_files": "Archivos sin seguimiento",
     "untracked_files_description": "La aplicación no rastrea estos archivos. Puede ser el resultado de movimientos fallidos, cargas interrumpidas o sin procesar debido a un error",
-    "user_delete_delay": "La cuenta <b>{user}</b> y los archivos se programarán para su eliminación permanente en {delay, plural, one {# day} other {# days}}.",
+    "user_cleanup_job": "Limpieza de usuarios",
+    "user_delete_delay": "La cuenta <b>{user}</b> y los archivos se programarán para su eliminación permanente en {delay, plural, one {# día} other {# días}}.",
     "user_delete_delay_settings": "Eliminar retardo",
     "user_delete_delay_settings_description": "Número de días después de la eliminación para eliminar permanentemente la cuenta y los activos de un usuario. El trabajo de eliminación de usuarios se ejecuta a medianoche para comprobar si hay usuarios que estén listos para su eliminación. Los cambios a esta configuración se evaluarán en la próxima ejecución.",
     "user_delete_immediately": "La cuenta <b>{user}</b> y los archivos se pondrán en cola para su eliminación permanente <b>inmediatamente</b>.",
@@ -336,8 +356,8 @@
   "admin_password": "Contraseña del Administrador",
   "administration": "Administración",
   "advanced": "Avanzada",
-  "age_months": "Tiempo {months, plural, one {# month} other {# months}}",
-  "age_year_months": "1 año, {months, plural, one {# month} other {# months}}",
+  "age_months": "Tiempo {months, plural, one {# mes} other {# meses}}",
+  "age_year_months": "1 año, {months, plural, one {# mes} other {# meses}}",
   "age_years": "Edad {years, plural, one {# año} other {# años}}",
   "album_added": "Álbum añadido",
   "album_added_notification_setting_description": "Reciba una notificación por correo electrónico cuando lo agreguen a un álbum compartido",
@@ -378,7 +398,6 @@
   "archive_or_unarchive_photo": "Archivar o restaurar foto",
   "archive_size": "Tamaño del archivo",
   "archive_size_description": "Configure el tamaño del archivo para descargas (en GB)",
-  "archived": "Archivado",
   "archived_count": "{count, plural, one {# archivado} other {# archivados}}",
   "are_these_the_same_person": "¿Son la misma persona?",
   "are_you_sure_to_do_this": "¿Estas seguro de que quieres hacer esto?",
@@ -388,8 +407,8 @@
   "asset_filename_is_offline": "El archivo {filename} está offline",
   "asset_has_unassigned_faces": "El archivo no tiene rostros asignados",
   "asset_hashing": "Hashing...",
-  "asset_offline": "Archivos fuera de linea",
-  "asset_offline_description": "Este archivo está offline. Immich no puede acceder a la ubicación de su archivo. Asegúrese de que el archivo esté disponible y luego vuelva a escanear la biblioteca.",
+  "asset_offline": "Archivos sin conexión",
+  "asset_offline_description": "Este activo externo ya no se encuentra en el disco. Por favor, póngase en contacto con su administrador de Immich para obtener ayuda.",
   "asset_skipped": "Omitido",
   "asset_skipped_in_trash": "En la papelera",
   "asset_uploaded": "Subido",
@@ -399,13 +418,12 @@
   "assets_added_to_album_count": "Añadido {count, plural, one {# asset} other {# assets}} al álbum",
   "assets_added_to_name_count": "Añadido {count, plural, one {# asset} other {# assets}} a {hasName, select, true {<b>{name}</b>} other {new album}}",
   "assets_count": "{count, plural, one {# activo} other {# activos}}",
-  "assets_moved_to_trash": "Se movió {count, plural, one {# activo} other {# activos}} a la papelera",
-  "assets_moved_to_trash_count": "Movido {count, plural, one {# asset} other {# assets}} a la papelera",
-  "assets_permanently_deleted_count": "Eliminado permanentemente {count, plural, one {# asset} other {# assets}}",
-  "assets_removed_count": "Eliminado {count, plural, one {# asset} other {# assets}}",
-  "assets_restore_confirmation": "¿Está seguro de que desea restaurar todos sus archivos eliminados? ¡No puedes deshacer esta acción!",
-  "assets_restored_count": "Restaurado {count, plural, one {# asset} other {# assets}}",
-  "assets_trashed_count": "Borrado {count, plural, one {# asset} other {# assets}}",
+  "assets_moved_to_trash_count": "{count, plural, one {# elemento movido} other {# elementos movidos}} a la papelera",
+  "assets_permanently_deleted_count": "Eliminado permanentemente {count, plural, one {# elemento} other {# elementos}}",
+  "assets_removed_count": "Eliminado {count, plural, one {# elemento} other {# elementos}}",
+  "assets_restore_confirmation": "¿Estás seguro de que quieres restaurar todos tus activos eliminados? ¡No puede deshacer esta acción! Tenga en cuenta que los archivos sin conexión no se pueden restaurar de esta manera.",
+  "assets_restored_count": "Restaurado {count, plural, one {# elemento} other {# elementos}}",
+  "assets_trashed_count": "Borrado {count, plural, one {# elemento} other {# elementos}}",
   "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} ya forma parte del álbum",
   "authorized_devices": "Dispositivos Autorizados",
   "back": "Atrás",
@@ -414,9 +432,10 @@
   "birthdate_saved": "Fecha de nacimiento guardada con éxito",
   "birthdate_set_description": "La fecha de nacimiento se utiliza para calcular la edad de esta persona en el momento de la fotografía.",
   "blurred_background": "Fondo borroso",
+  "bugs_and_feature_requests": "Errores y solicitudes de funciones",
   "build": "Compilación",
-  "build_image": "Construir Imagen",
-  "bulk_delete_duplicates_confirmation": "¿Estás seguro de que deseas eliminar de forma masiva {count, plural, one {# duplicate asset} other {# duplicate assets}}? Esto mantendrá el activo más grande de cada grupo y eliminará permanentemente todos los demás duplicados. ¡Esta acción no se puede deshacer!",
+  "build_image": "Imagen",
+  "bulk_delete_duplicates_confirmation": "¿Estás seguro de que deseas eliminar de forma masiva {count, plural, one {# elemento duplicado} other {# elementos duplicados}}? Esto mantendrá el activo más grande de cada grupo y eliminará permanentemente todos los demás duplicados. ¡Esta acción no se puede deshacer!",
   "bulk_keep_duplicates_confirmation": "¿Estas seguro de que desea mantener {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto resolverá todos los grupos duplicados sin borrar nada.",
   "bulk_trash_duplicates_confirmation": "¿Estas seguro de que desea eliminar masivamente {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto mantendrá el archivo más grande de cada grupo y eliminará todos los demás duplicados.",
   "buy": "Comprar Immich",
@@ -428,13 +447,9 @@
   "cannot_merge_people": "No se pueden fusionar personas",
   "cannot_undo_this_action": "¡No puedes deshacer esta acción!",
   "cannot_update_the_description": "No se puede actualizar la descripción",
-  "cant_apply_changes": "No se pueden aplicar los cambios",
-  "cant_get_faces": "No se encuentran rostros",
-  "cant_search_people": "No se pueden buscar personas",
-  "cant_search_places": "No se pueden buscar lugares",
   "change_date": "Cambiar fecha",
   "change_expiration_time": "Cambiar fecha de caducidad",
-  "change_location": "Cambiar localización",
+  "change_location": "Cambiar ubicación",
   "change_name": "Cambiar nombre",
   "change_name_successfully": "Nombre cambiado correctamente",
   "change_password": "Cambiar Contraseña",
@@ -463,6 +478,7 @@
   "confirm": "Confirmar",
   "confirm_admin_password": "Confirmar Contraseña de Administrador",
   "confirm_delete_shared_link": "¿Estás seguro de que deseas eliminar este enlace compartido?",
+  "confirm_keep_this_delete_others": "Todos los demás activos de la pila se eliminarán excepto este activo. ¿Está seguro de que quiere continuar?",
   "confirm_password": "Confirmar contraseña",
   "contain": "Incluido",
   "context": "Contexto",
@@ -512,16 +528,19 @@
   "delete_key": "Eliminar clave",
   "delete_library": "Eliminar biblioteca",
   "delete_link": "Eliminar enlace",
+  "delete_others": "Eliminar otros",
   "delete_shared_link": "Eliminar enlace compartido",
   "delete_tag": "Eliminar etiqueta",
   "delete_tag_confirmation_prompt": "¿Estás seguro de que deseas eliminar la etiqueta {tagName} ?",
   "delete_user": "Eliminar usuario",
   "deleted_shared_link": "Enlace compartido eliminado",
+  "deletes_missing_assets": "Elimina archivos que faltan en el disco duro",
   "description": "Descripción",
   "details": "DETALLES",
   "direction": "Dirección",
   "disabled": "Deshabilitado",
   "disallow_edits": "Bloquear edición",
+  "discord": "Discord",
   "discover": "Descubrir",
   "dismiss_all_errors": "Descartar todos los errores",
   "dismiss_error": "Descartar error",
@@ -530,6 +549,7 @@
   "display_original_photos": "Mostrar fotos originales",
   "display_original_photos_setting_description": "Preferir mostrar la foto original al ver un archivo en lugar de miniaturas cuando el archivo original es compatible con la web. Esto puede resultar en velocidades de visualización de fotografías más lentas.",
   "do_not_show_again": "No volver a mostrar este mensaje otra vez",
+  "documentation": "Documentación",
   "done": "Hecho",
   "download": "Descargar",
   "download_include_embedded_motion_videos": "Vídeos incrustados",
@@ -542,13 +562,6 @@
   "duplicates": "Duplicados",
   "duplicates_description": "Resuelva cada grupo indicando, en cada caso, cuales están duplicados",
   "duration": "Duración",
-  "durations": {
-    "days": "{days, plural, one {día} other {{days, number} días}}",
-    "hours": "{hours, plural, one {hora} other {{hours, number} horas}}",
-    "minutes": "{minutes, plural, one {minuto} other {{minutes, number} minutos}}",
-    "months": "{months, plural, one {mes} other {{months, number} meses}}",
-    "years": "{years, plural, one {año} other {{years, number} años}}"
-  },
   "edit": "Editar",
   "edit_album": "Editar album",
   "edit_avatar": "Editar avatar",
@@ -573,8 +586,6 @@
   "editor_crop_tool_h2_aspect_ratios": "Proporciones del aspecto",
   "editor_crop_tool_h2_rotation": "Rotación",
   "email": "Correo",
-  "empty": "",
-  "empty_album": "Álbum vacío",
   "empty_trash": "Vaciar papelera",
   "empty_trash_confirmation": "¿Estás seguro de que quieres vaciar la papelera? Esto eliminará permanentemente todos los archivos de la basura de Immich.\n¡No puedes deshacer esta acción!",
   "enable": "Habilitar",
@@ -589,7 +600,7 @@
     "cant_apply_changes": "No se pueden aplicar los cambios",
     "cant_change_activity": "No se puede realizar la actividad {enabled, select, true {disable} other {enable}}",
     "cant_change_asset_favorite": "No se puede cambiar favorito para este archivo",
-    "cant_change_metadata_assets_count": "No se pueden cambiar los metadatos de {count, plural, one {# asset} other {# assets}}",
+    "cant_change_metadata_assets_count": "No se pueden cambiar los metadatos de {count, plural, one {# elemento} other {# elementos}}",
     "cant_get_faces": "No se encuentran caras",
     "cant_get_number_of_comments": "No se puede obtener la cantidad de comentarios",
     "cant_search_people": "No se puede buscar a personas",
@@ -608,6 +619,7 @@
     "failed_to_create_shared_link": "Error al crear el enlace compartido",
     "failed_to_edit_shared_link": "Error al editar el enlace compartido",
     "failed_to_get_people": "Error al obtener personas",
+    "failed_to_keep_this_delete_others": "No se pudo conservar este activo y eliminar los demás",
     "failed_to_load_asset": "Error al cargar el elemento",
     "failed_to_load_assets": "Error al cargar los elementos",
     "failed_to_load_people": "Error al cargar a los usuarios",
@@ -616,7 +628,7 @@
     "failed_to_unstack_assets": "Error al desagrupar los archivos",
     "import_path_already_exists": "Esta ruta de importación ya existe.",
     "incorrect_email_or_password": "Contraseña o email incorrecto",
-    "paths_validation_failed": "Falló la validación en {paths, plural, one {# carpetas} other {# carpetas}}",
+    "paths_validation_failed": "Falló la validación en {paths, plural, one {# carpeta} other {# carpetas}}",
     "profile_picture_transparent_pixels": "Las imágenes de perfil no pueden tener píxeles transparentes. Por favor amplíe y/o mueva la imagen.",
     "quota_higher_than_disk_size": "Se ha establecido una cuota superior al tamaño del disco",
     "repair_unable_to_check_items": "No se puede verificar {count, select, one {elemento} other {elementos}}",
@@ -634,9 +646,7 @@
     "unable_to_change_favorite": "Imposible cambiar el archivo favorito",
     "unable_to_change_location": "No se puede cambiar de ubicación",
     "unable_to_change_password": "No se puede cambiar la contraseña",
-    "unable_to_change_visibility": "No se puede cambiar la visibilidad de {count, plural, one {# person} other {# people}}",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
+    "unable_to_change_visibility": "No se puede cambiar la visibilidad de {count, plural, one {# persona} other {# personas}}",
     "unable_to_complete_oauth_login": "No se puede completar el inicio de sesión de OAuth",
     "unable_to_connect": "No puede conectarse",
     "unable_to_connect_to_server": "Error al conectar al servidor",
@@ -661,6 +671,7 @@
     "unable_to_get_comments_number": "No se puede obtener el número de comentarios",
     "unable_to_get_shared_link": "Error al obtener el enlace compartido",
     "unable_to_hide_person": "No se puede ocultar a la persona",
+    "unable_to_link_motion_video": "No se puede enlazar el vídeo en movimiento",
     "unable_to_link_oauth_account": "No se puede vincular la cuenta OAuth",
     "unable_to_load_album": "No se puede cargar el álbum",
     "unable_to_load_asset_activity": "No se puede cargar la actividad de los archivos",
@@ -676,12 +687,10 @@
     "unable_to_remove_album_users": "No se pueden eliminar usuarios del álbum",
     "unable_to_remove_api_key": "No se puede eliminar la clave API",
     "unable_to_remove_assets_from_shared_link": "No se pueden eliminar archivos desde el enlace compartido",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_deleted_assets": "No se pueden eliminar archivos sin conexión",
     "unable_to_remove_library": "No se puede eliminar la biblioteca",
-    "unable_to_remove_offline_files": "No se pueden eliminar archivos sin conexión",
     "unable_to_remove_partner": "No se puede eliminar el invitado",
     "unable_to_remove_reaction": "No se puede eliminar la reacción",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "No se pueden reparar los items",
     "unable_to_reset_password": "No se puede restablecer la contraseña",
     "unable_to_resolve_duplicate": "No se resolver duplicado",
@@ -701,6 +710,7 @@
     "unable_to_submit_job": "No se puede enviar el trabajo",
     "unable_to_trash_asset": "No se puede eliminar el archivo",
     "unable_to_unlink_account": "No se puede desvincular la cuenta",
+    "unable_to_unlink_motion_video": "No se puede desvincular el vídeo en movimiento",
     "unable_to_update_album_cover": "No se puede actualizar la portada del álbum",
     "unable_to_update_album_info": "No se puede actualizar la información del álbum",
     "unable_to_update_library": "No se puede actualizar la biblioteca",
@@ -710,10 +720,6 @@
     "unable_to_update_user": "No se puede actualizar el usuario",
     "unable_to_upload_file": "Error al subir el archivo"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exif": "EXIF",
   "exit_slideshow": "Salir de la presentación",
   "expand_all": "Expandir todo",
@@ -728,33 +734,28 @@
   "external": "Externo",
   "external_libraries": "Bibliotecas Externas",
   "face_unassigned": "Sin asignar",
-  "failed_to_get_people": "No se pudo encontrar a personas",
+  "failed_to_load_assets": "Error al cargar los activos",
   "favorite": "Favorito",
   "favorite_or_unfavorite_photo": "Foto favorita o no favorita",
   "favorites": "Favoritos",
-  "feature": "",
   "feature_photo_updated": "Foto destacada actualizada",
-  "featurecollection": "",
   "features": "Características",
   "features_setting_description": "Administrar las funciones de la aplicación",
   "file_name": "Nombre de archivo",
   "file_name_or_extension": "Nombre del archivo o extensión",
   "filename": "Nombre del archivo",
-  "files": "",
   "filetype": "Tipo de archivo",
   "filter_people": "Filtrar personas",
   "find_them_fast": "Encuéntrelos rápidamente por nombre con la búsqueda",
   "fix_incorrect_match": "Corregir coincidencia incorrecta",
   "folders": "Carpetas",
   "folders_feature_description": "Explorar la vista de carpetas para las fotos y los videos en el sistema de archivos",
-  "force_re-scan_library_files": "Forzar reescaneo de todos los archivos de la biblioteca",
   "forward": "Reenviar",
   "general": "General",
   "get_help": "Solicitar ayuda",
   "getting_started": "Comenzamos",
   "go_back": "Volver atrás",
   "go_to_search": "Ir a búsqueda",
-  "go_to_share_page": "Ir a compartir página",
   "group_albums_by": "Agrupar albums por...",
   "group_no": "Sin agrupación",
   "group_owner": "Agrupar por propietario",
@@ -780,10 +781,6 @@
   "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} con {person1} y {person2} el {date}",
   "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} con {person1}, {person2}, y {person3} el {date}",
   "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} con {person1}, {person2}, y {additionalCount, number} más el {date}",
-  "image_alt_text_people": "{count, plural, =1 {with {person1}} =2 {with {person1} and {person2}} =3 {with {person1}, {person2}, and {person3}} other {with {person1}, {person2}, y {others, number} others}}",
-  "image_alt_text_place": "En {city}, {country}",
-  "image_taken": "{isVideo, select, true {Video taken} other {Image taken}}",
-  "img": "",
   "immich_logo": "Logo de Immich",
   "immich_web_interface": "Interfaz Web de Immich",
   "import_from_json": "Importar desde JSON",
@@ -804,10 +801,11 @@
   "invite_people": "Invitar a Personas",
   "invite_to_album": "Invitar al álbum",
   "items_count": "{count, plural, one {# elemento} other {# elementos}}",
-  "job_settings_description": "",
   "jobs": "Tareas",
   "keep": "Conservar",
   "keep_all": "Conservar Todo",
+  "keep_this_delete_others": "Mantener este, eliminar los otros",
+  "kept_this_deleted_others": "Mantuvo este activo y eliminó {count, plural, one {# activo} other {# activos}}",
   "keyboard_shortcuts": "Atajos de teclado",
   "language": "Idioma",
   "language_setting_description": "Selecciona tu idioma preferido",
@@ -819,33 +817,9 @@
   "level": "Nivel",
   "library": "Biblioteca",
   "library_options": "Opciones de biblioteca",
-  "license_account_info": "Tu cuenta tiene licencia",
-  "license_activated_subtitle": "Gracias por apoyar a Immich y al software de código abierto",
-  "license_activated_title": "Tu licencia ha sido activada exitosamente",
-  "license_button_activate": "Activar",
-  "license_button_buy": "Comprar",
-  "license_button_buy_license": "Comprar una licencia",
-  "license_button_select": "Seleccionar",
-  "license_failed_activation": "No se pudo activar la licencia. ¡Por favor, revisa tu correo electrónico para obtener la clave de licencia correcta!",
-  "license_individual_description_1": "1 licencia por usuario en cualquier servidor",
-  "license_individual_title": "Licencia individual",
-  "license_info_licensed": "Con licencia",
-  "license_info_unlicensed": "Sin licencia",
-  "license_input_suggestion": "¿Tienes una licencia? Introduzca la clave a continuación",
-  "license_license_subtitle": "Comprar una licencia para apoyar a Immich",
-  "license_license_title": "LICENCIA",
-  "license_lifetime_description": "Licencia de por vida",
-  "license_per_server": "Por servidor",
-  "license_per_user": "Por usuario",
-  "license_server_description_1": "1 licencia por servidor",
-  "license_server_description_2": "Licencia para todos los usuarios del servidor",
-  "license_server_title": "Licencia del servidor",
-  "license_trial_info_1": "Está ejecutando una versión sin licencia de Immich",
-  "license_trial_info_2": "Llevas utilizando Immich aproximadamente",
-  "license_trial_info_3": "{accountAge, plural, one {# día} other {# días}}",
-  "license_trial_info_4": "Por favor, considera la compra de una licencia para apoyar el desarrollo continuo del servicio",
   "light": "Claro",
   "like_deleted": "Me gusta eliminado",
+  "link_motion_video": "Enlazar vídeo en movimiento",
   "link_options": "Opciones de enlace",
   "link_to_oauth": "Enlace a OAuth",
   "linked_oauth_account": "Cuenta OAuth vinculada",
@@ -864,6 +838,7 @@
   "look": "Mirar",
   "loop_videos": "Vídeos en bucle",
   "loop_videos_description": "Habilite la reproducción automática de un video en el visor de detalles.",
+  "main_branch_warning": "Estás ejecutando una compilación desde la rama principal. ¡Recomendamos encarecidamente usar una versión de lanzamiento!",
   "make": "Marca",
   "manage_shared_links": "Administrar enlaces compartidos",
   "manage_sharing_with_partners": "Administrar el uso compartido con invitados",
@@ -888,7 +863,7 @@
   "merge_people_limit": "Solo puedes fusionar hasta 5 caras a la vez",
   "merge_people_prompt": "¿Quieres fusionar a estas personas? Esta acción es irreversible.",
   "merge_people_successfully": "Personas fusionadas correctamente",
-  "merged_people_count": "Fusionar {count, plural, one {# person} other {# people}}",
+  "merged_people_count": "Fusionada {count, plural, one {# persona} other {# personas}}",
   "minimize": "Minimizar",
   "minute": "Minuto",
   "missing": "Perdido",
@@ -925,7 +900,7 @@
   "no_results": "Sin resultados",
   "no_results_description": "Pruebe con un sinónimo o una palabra clave más general",
   "no_shared_albums_message": "Crea un álbum para compartir fotos y vídeos con personas de tu red",
-  "not_in_any_album": "Nada en ningún álbum",
+  "not_in_any_album": "Sin álbum",
   "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar la etiqueta de almacenamiento a los archivos cargados previamente, ejecute el",
   "note_unlimited_quota": "Nota: Ingrese 0 para cuota ilimitada",
   "notes": "Notas",
@@ -933,6 +908,7 @@
   "notifications": "Notificaciones",
   "notifications_setting_description": "Administrar notificaciones",
   "oauth": "OAuth",
+  "official_immich_resources": "Recursos oficiales de Immich",
   "offline": "Desconectado",
   "offline_paths": "Rutas sin conexión",
   "offline_paths_description": "Estos resultados pueden deberse a la eliminación manual de archivos que no forman parte de una biblioteca externa.",
@@ -945,7 +921,6 @@
   "onboarding_welcome_user": "Bienvenido, {user}",
   "online": "En línea",
   "only_favorites": "Solo favoritos",
-  "only_refreshes_modified_files": "Solo actualiza los archivos modificados",
   "open_in_map_view": "Abrir en la vista del mapa",
   "open_in_openstreetmap": "Abrir en OpenStreetMap",
   "open_the_search_filters": "Abre los filtros de búsqueda",
@@ -969,9 +944,9 @@
   "password_required": "Contraseña requerida",
   "password_reset_success": "Restablecimiento de contraseña exitoso",
   "past_durations": {
-    "days": "Pasados {days, plural, one {day} other {# days}}",
-    "hours": "Pasadas {hours, plural, one {hour} other {# hours}}",
-    "years": "Pasado(s) {years, plural, one {year} other {# years}}"
+    "days": "Pasados {days, plural, one {día} other {# días}}",
+    "hours": "Pasadas {hours, plural, one {hora} other {# horas}}",
+    "years": "Pasado(s) {years, plural, one {año} other {# años}}"
   },
   "path": "Ruta",
   "pattern": "Patrón",
@@ -980,24 +955,22 @@
   "paused": "Detenido",
   "pending": "Pendiente",
   "people": "Personas",
-  "people_edits_count": "Editado {count, plural, one {# person} other {# people}}",
+  "people_edits_count": "Editada {count, plural, one {# persona} other {# personas}}",
   "people_feature_description": "Explorar fotos y vídeos agrupados por personas",
   "people_sidebar_description": "Mostrar un enlace a Personas en la barra lateral",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "Advertencia de eliminación permanente",
   "permanent_deletion_warning_setting_description": "Mostrar una advertencia al eliminar archivos permanentemente",
   "permanently_delete": "Borrar permanentemente",
-  "permanently_delete_assets_count": "Eliminar permanentemente {count, plural, one {asset} other {assets}}",
-  "permanently_delete_assets_prompt": "¿Está seguro de que desea eliminar permanentemente {count, plural, one {¿este activo?} other {¿estos <b>#</b> activos?}} Esto también eliminará {count, plural, one {de tu} other {de tus}} álbum(es).",
+  "permanently_delete_assets_count": "Eliminar permanentemente {count, plural, one {elemento} other {elementos}}",
+  "permanently_delete_assets_prompt": "¿Está seguro de que desea eliminar permanentemente {count, plural, one {este activo?} other {estos <b>#</b> activos?}} Esto también eliminará {count, plural, one {de tu} other {de tus}} álbum(es).",
   "permanently_deleted_asset": "Archivo eliminado permanentemente",
-  "permanently_deleted_assets": "Eliminado permanentemente {count, plural, one {# activo} other {# activos}}",
-  "permanently_deleted_assets_count": "Eliminado permanentemente {count, plural, one {# asset} other {# assets}}",
+  "permanently_deleted_assets_count": "Eliminado permanentemente {count, plural, one {# elemento} other {# elementos}}",
   "person": "Persona",
   "person_hidden": "{name}{hidden, select, true { (oculto)} other {}}",
   "photo_shared_all_users": "Parece que compartiste tus fotos con todos los usuarios o no tienes ningún usuario con quien compartirlas.",
   "photos": "Fotos",
   "photos_and_videos": "Fotos y Videos",
-  "photos_count": "{count, plural, one {{count, number} foto} other {{count, number} fotos}}",
+  "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}",
   "photos_from_previous_years": "Fotos de años anteriores",
   "pick_a_location": "Elige una ubicación",
   "place": "Lugar",
@@ -1006,7 +979,6 @@
   "play_memories": "Reproducir recuerdos",
   "play_motion_photo": "Reproducir foto en movimiento",
   "play_or_pause_video": "Reproducir o pausar vídeo",
-  "point": "",
   "port": "Puerto",
   "preset": "Preestablecido",
   "preview": "Posterior",
@@ -1019,7 +991,7 @@
   "profile_picture_set": "Conjunto de imágenes de perfil.",
   "public_album": "Álbum público",
   "public_share": "Compartir públicamente",
-  "purchase_account_info": "Soporte",
+  "purchase_account_info": "Seguidor",
   "purchase_activated_subtitle": "Gracias por apoyar a Immich y al software de código abierto",
   "purchase_activated_time": "Activado el {date, date}",
   "purchase_activated_title": "Su clave ha sido activada correctamente",
@@ -1051,43 +1023,45 @@
   "purchase_server_description_2": "Estado del soporte",
   "purchase_server_title": "Servidor",
   "purchase_settings_server_activated": "La clave del producto del servidor la administra el administrador",
-  "range": "",
   "rating": "Valoración",
   "rating_clear": "Borrar calificación",
   "rating_count": "{count, plural, one {# estrella} other {# estrellas}}",
   "rating_description": "Mostrar la clasificación exif en el panel de información",
-  "raw": "",
   "reaction_options": "Opciones de reacción",
   "read_changelog": "Leer registro de cambios",
   "reassign": "Reasignar",
-  "reassigned_assets_to_existing_person": "Reasignado {count, plural, one {# asset} other {# assets}} to {name, select, null {an existing person} other {{name}}}",
-  "reassigned_assets_to_new_person": "Reasignado {count, plural, one {# asset} other {# assets}} a un nuevo usuario",
+  "reassigned_assets_to_existing_person": "Reasignado {count, plural, one {# elemento} other {# elementos}} a {name, select, null {una persona existente} other {{name}}}",
+  "reassigned_assets_to_new_person": "Reasignado {count, plural, one {# elemento} other {# elementos}} a un nuevo usuario",
   "reassing_hint": "Asignar archivos seleccionados a una persona existente",
   "recent": "Reciente",
+  "recent-albums": "Últimos álbumes",
   "recent_searches": "Búsquedas recientes",
   "refresh": "Actualizar",
   "refresh_encoded_videos": "Recargar los vídeos codificados",
-  "refresh_metadata": "Recargar los metadatos",
+  "refresh_faces": "Actualizar caras",
+  "refresh_metadata": "Recargar metadatos",
   "refresh_thumbnails": "Recargar miniaturas",
   "refreshed": "Recargado",
-  "refreshes_every_file": "Recargar cada archivo",
+  "refreshes_every_file": "Recargar todos los archivos nuevos y existentes",
   "refreshing_encoded_video": "Recargando los videos codificados",
+  "refreshing_faces": "Recargando caras",
   "refreshing_metadata": "Recargando metadatos",
   "regenerating_thumbnails": "Recargando miniaturas",
   "remove": "Eliminar",
-  "remove_assets_album_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# asset} other {# assets}} del álbum?",
-  "remove_assets_shared_link_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# asset} other {# assets}} del enlace compartido?",
+  "remove_assets_album_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# elemento} other {# elementos}} del álbum?",
+  "remove_assets_shared_link_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# elemento} other {# elementos}} del enlace compartido?",
   "remove_assets_title": "¿Eliminar activos?",
   "remove_custom_date_range": "Eliminar intervalo de fechas personalizado",
+  "remove_deleted_assets": "Eliminar archivos sin conexión",
   "remove_from_album": "Eliminar del álbum",
   "remove_from_favorites": "Quitar de favoritos",
   "remove_from_shared_link": "Eliminar desde enlace compartido",
-  "remove_offline_files": "Eliminar archivos sin conexión",
+  "remove_url": "Eliminar URL",
   "remove_user": "Eliminar usuario",
   "removed_api_key": "Clave API eliminada: {name}",
   "removed_from_archive": "Eliminado del archivo",
   "removed_from_favorites": "Eliminado de favoritos",
-  "removed_from_favorites_count": "{count, plural, other {Removed #}} de favoritos",
+  "removed_from_favorites_count": "{count, plural, other {Eliminados #}} de favoritos",
   "removed_tagged_assets": "Etiqueta eliminada de {count, plural, one {# activo} other {# activos}}",
   "rename": "Renombrar",
   "repair": "Reparar",
@@ -1099,7 +1073,6 @@
   "reset": "Reiniciar",
   "reset_password": "Restablecer la contraseña",
   "reset_people_visibility": "Restablecer la visibilidad de las personas",
-  "reset_settings_to_default": "",
   "reset_to_default": "Restablecer los valores predeterminados",
   "resolve_duplicates": "Resolver duplicados",
   "resolved_all_duplicates": "Todos los duplicados resueltos",
@@ -1119,8 +1092,7 @@
   "saved_settings": "Configuraciones guardadas",
   "say_something": "Comenta algo",
   "scan_all_libraries": "Escanear todas las bibliotecas",
-  "scan_all_library_files": "Vuelva a escanear todos los archivos de la biblioteca",
-  "scan_new_library_files": "Escanear nuevos archivos de biblioteca",
+  "scan_library": "Escanear",
   "scan_settings": "Configuración de escaneo",
   "scanning_for_album": "Buscando álbum...",
   "search": "Buscar",
@@ -1135,8 +1107,10 @@
   "search_for_existing_person": "Buscar persona existente",
   "search_no_people": "Ninguna persona",
   "search_no_people_named": "Ninguna persona llamada \"{name}\"",
+  "search_options": "Opciones de búsqueda",
   "search_people": "Buscar personas",
   "search_places": "Buscar lugar",
+  "search_settings": "Ajustes de la búsqueda",
   "search_state": "Buscar región/estado...",
   "search_tags": "Buscando etiquetas...",
   "search_timezone": "Buscar zona horaria...",
@@ -1152,19 +1126,18 @@
   "select_face": "Seleccionar cara",
   "select_featured_photo": "Seleccionar foto principal",
   "select_from_computer": "Seleccionar desde el PC",
-  "select_keep_all": "Mantener toda la selección",
+  "select_keep_all": "Conservar todo",
   "select_library_owner": "Seleccionar propietario de la biblioteca",
   "select_new_face": "Seleccionar nueva cara",
   "select_photos": "Seleccionar Fotos",
-  "select_trash_all": "Enviar la selección a la papelera",
+  "select_trash_all": "Descartar todo",
   "selected": "Seleccionado",
   "selected_count": "{count, plural, one {# seleccionado} other {# seleccionados}}",
   "send_message": "Enviar mensaje",
   "send_welcome_email": "Enviar correo de bienvenida",
-  "server": "Servidor",
   "server_offline": "Servidor desconectado",
   "server_online": "Servidor en línea",
-  "server_stats": "Estadísticas Servidor",
+  "server_stats": "Estadísticas del servidor",
   "server_version": "Versión del servidor",
   "set": "Establecer",
   "set_as_album_cover": "Establecer portada del álbum",
@@ -1204,6 +1177,7 @@
   "show_person_options": "Mostrar opciones de la persona",
   "show_progress_bar": "Mostrar barra de progreso",
   "show_search_options": "Mostrar opciones de búsqueda",
+  "show_slideshow_transition": "Mostrar la transición de las diapositivas",
   "show_supporter_badge": "Insignia de colaborador",
   "show_supporter_badge_description": "Mostrar una insignia de colaborador",
   "shuffle": "Modo aleatorio",
@@ -1224,12 +1198,12 @@
   "sort_oldest": "Foto más antigua",
   "sort_recent": "Foto más reciente",
   "sort_title": "Título",
-  "source": "Fuente",
+  "source": "Origen",
   "stack": "Apilar",
   "stack_duplicates": "Apilar duplicados",
   "stack_select_one_photo": "Selecciona una imagen principal para la pila",
   "stack_selected_photos": "Apilar fotos seleccionadas",
-  "stacked_assets_count": "Apilados {count, plural, one {# asset} other {# assets}}",
+  "stacked_assets_count": "Apilado(s) {count, plural, one {# activo} other {# activos}}",
   "stacktrace": "Stacktrace",
   "start": "Inicio",
   "start_date": "Fecha de inicio",
@@ -1245,13 +1219,16 @@
   "submit": "Enviar",
   "suggestions": "Sugerencias",
   "sunrise_on_the_beach": "Amanecer en la playa",
+  "support": "Soporte",
+  "support_and_feedback": "Soporte y comentarios",
+  "support_third_party_description": "Su instalación de immich fue empaquetada por un tercero. Los problemas que experimenta pueden ser causados por ese paquete, así que por favor plantee problemas con ellos en primer lugar usando los enlaces inferiores.",
   "swap_merge_direction": "Alternar dirección de mezcla",
   "sync": "Sincronizar",
   "tag": "Etiqueta",
   "tag_assets": "Etiquetar activos",
   "tag_created": "Etiqueta creada: {tag}",
   "tag_feature_description": "Explore fotos y videos agrupados por temas de etiquetas lógicas",
-  "tag_not_found_question": "¿No encuentras una etiqueta? Crea una <link>aquí</link>",
+  "tag_not_found_question": "¿No encuentra una etiqueta? <link>Crea una nueva etiqueta.</link>",
   "tag_updated": "Etiqueta actualizada: {tag}",
   "tagged_assets": "Etiquetado(s) {count, plural, one {# activo} other {# activos}}",
   "tags": "Etiquetas",
@@ -1260,35 +1237,35 @@
   "theme_selection": "Selección de tema",
   "theme_selection_description": "Establece el tema automáticamente como \"claro\" u \"oscuro\" según las preferencias del sistema/navegador",
   "they_will_be_merged_together": "Se fusionarán entre sí",
+  "third_party_resources": "Recursos de terceros",
   "time_based_memories": "Recuerdos basados en tiempo",
+  "timeline": "Cronología",
   "timezone": "Zona horaria",
   "to_archive": "Archivar",
   "to_change_password": "Cambiar contraseña",
   "to_favorite": "A los favoritos",
   "to_login": "Iniciar Sesión",
   "to_parent": "Ir a los padres",
-  "to_root": "Para root",
-  "to_trash": "Papelera",
+  "to_trash": "Descartar",
   "toggle_settings": "Alternar ajustes",
   "toggle_theme": "Alternar tema oscuro",
-  "toggle_visibility": "Alternar visibilidad",
+  "total": "Total",
   "total_usage": "Uso total",
   "trash": "Papelera",
-  "trash_all": "Enviar todo a la papelera",
-  "trash_count": "Papelera {count, number}",
+  "trash_all": "Descartar todo",
+  "trash_count": "Descartar {count, number}",
   "trash_delete_asset": "Borrar/Eliminar archivo",
   "trash_no_results_message": "Las fotos y videos que se envíen a la papelera aparecerán aquí.",
   "trashed_items_will_be_permanently_deleted_after": "Los elementos en la papelera serán eliminados permanentemente tras {days, plural, one {# día} other {# días}}.",
   "type": "Tipo",
   "unarchive": "Desarchivar",
-  "unarchived": "Restaurado",
   "unarchived_count": "{count, plural, one {# No archivado} other {# No archivados}}",
   "unfavorite": "Retirar favorito",
   "unhide_person": "Mostrar persona",
   "unknown": "Desconocido",
-  "unknown_album": "Álbum desconocido",
   "unknown_year": "Año desconocido",
   "unlimited": "Ilimitado",
+  "unlink_motion_video": "Desvincular vídeo en movimiento",
   "unlink_oauth": "Desvincular OAuth",
   "unlinked_oauth_account": "Cuenta OAuth desconectada",
   "unnamed_album": "Album sin nombre",
@@ -1298,14 +1275,14 @@
   "unselect_all": "Limpiar selección",
   "unselect_all_duplicates": "Deseleccionar todos los duplicados",
   "unstack": "Desapilar",
-  "unstacked_assets_count": "Sin apilar {count, plural, one {# asset} other {# assets}}",
+  "unstacked_assets_count": "Desapilado(s) {count, plural, one {# elemento} other {# elementos}}",
   "untracked_files": "Archivos no monitorizados",
   "untracked_files_decription": "Estos archivos no están siendo monitorizados por la aplicación. Es posible que sean resultado de errores al moverlos, cargas interrumpidas o por un fallo de la aplicación",
   "up_next": "A continuación",
   "updated_password": "Contraseña actualizada",
   "upload": "Subir",
   "upload_concurrency": "Cargas simultáneas",
-  "upload_errors": "Carga completada con {count, plural, one {# error} other {# errors}}, actualice la página para ver los nuevos recursos de carga.",
+  "upload_errors": "Carga completada con {count, plural, one {# error} other {# errores}}, actualice la página para ver los nuevos recursos de carga.",
   "upload_progress": "Restante {remaining, number} - Procesado {processed, number}/{total, number}",
   "upload_skipped_duplicates": "Saltado {count, plural, one {# duplicate asset} other {# duplicate assets}}",
   "upload_status_duplicates": "Duplicados",
@@ -1317,13 +1294,13 @@
   "use_custom_date_range": "Usa un intervalo de fechas personalizado",
   "user": "Usuario",
   "user_id": "ID de usuario",
-  "user_license_settings": "Licencia",
-  "user_license_settings_description": "Gestionar tu licencia",
   "user_liked": "{user} le gustó {type, select, photo {this photo} video {this video} asset {this asset} other {it}}",
   "user_purchase_settings": "Compra",
   "user_purchase_settings_description": "Gestiona tu compra",
   "user_role_set": "Carbiar {user} a {role}",
   "user_usage_detail": "Detalle del uso del usuario",
+  "user_usage_stats": "Estadísticas de uso de la cuenta",
+  "user_usage_stats_description": "Ver estadísticas de uso de la cuenta",
   "username": "Nombre de usuario",
   "users": "Usuarios",
   "utilities": "Utilidades",
@@ -1331,7 +1308,9 @@
   "variables": "Variables",
   "version": "Versión",
   "version_announcement_closing": "Tu amigo, Alex",
-  "version_announcement_message": "Hola amigo, hay una nueva versión de la aplicación, por favor tómete tu tiempo para visitar las notas de la <link>versión</link> y asegúrate de que tu <code>docker-compose.yml</code>, y la configuración <code>.env</code> esté actualizada para evitar cualquier configuración incorrecta, especialmente si usas WatchTower o cualquier mecanismo que maneje la actualización automática de tu aplicación.",
+  "version_announcement_message": "¡Hola! Hay una nueva versión de Immich disponible. Tómese un tiempo para leer las <link> notas de la versión </link> para asegurarse de que su configuración esté actualizada y evitar errores de configuración, especialmente si utiliza WatchTower o cualquier mecanismo que se encargue de actualizar su instancia de Immich automáticamente.",
+  "version_history": "Historial de versiones",
+  "version_history_item": "Instalada la {version} el {date}",
   "video": "Vídeo",
   "video_hover_setting": "Iniciar vídeo al pasar por encima",
   "video_hover_setting_description": "Reproducir el vídeo cuando el ratón está encima de un vídeo. Aunque esté desactivado, se iniciará cuando el cursor del ratón esté sobre el icono de \"reproducir\".",
@@ -1343,18 +1322,18 @@
   "view_all_users": "Mostrar todos los usuarios",
   "view_in_timeline": "Mostrar en la línea de tiempo",
   "view_links": "Mostrar enlaces",
+  "view_name": "Ver",
   "view_next_asset": "Mostrar siguiente elemento",
   "view_previous_asset": "Mostrar elemento anterior",
   "view_stack": "Ver Pila",
-  "viewer": "Visualizador",
-  "visibility_changed": "Visibilidad cambiada para {count, plural, one {# person} other {# people}}",
+  "visibility_changed": "Visibilidad cambiada para {count, plural, one {# persona} other {# personas}}",
   "waiting": "Esperando",
   "warning": "Advertencia",
   "week": "Semana",
   "welcome": "Bienvenido",
-  "welcome_to_immich": "Bienvenido a immich",
+  "welcome_to_immich": "Bienvenido a Immich",
   "year": "Año",
-  "years_ago": "Hace {years, plural, one {# year} other {# years}}",
+  "years_ago": "Hace {years, plural, one {# año} other {# años}}",
   "yes": "Sí",
   "you_dont_have_any_shared_links": "No tienes ningún enlace compartido",
   "zoom_image": "Acercar Imagen"
diff --git a/web/src/lib/i18n/et.json b/i18n/et.json
similarity index 68%
rename from web/src/lib/i18n/et.json
rename to i18n/et.json
index 25cdba4cb4..fc2cc3de93 100644
--- a/web/src/lib/i18n/et.json
+++ b/i18n/et.json
@@ -1,5 +1,5 @@
 {
-  "about": "Teave",
+  "about": "Värskenda",
   "account": "Konto",
   "account_settings": "Konto seaded",
   "acknowledge": "Sain aru",
@@ -23,16 +23,23 @@
   "add_to": "Lisa kohta...",
   "add_to_album": "Lisa albumisse",
   "add_to_shared_album": "Lisa jagatud albumisse",
+  "add_url": "Lisa URL",
   "added_to_archive": "Lisatud arhiivi",
   "added_to_favorites": "Lisatud lemmikutesse",
   "added_to_favorites_count": "{count, number} pilti lisatud lemmikutesse",
   "admin": {
     "add_exclusion_pattern_description": "Lisa välistamismustreid. Toetatud on metamärgid *, ** ja ?. Kõikide kataloogis nimega \"Raw\" olevate failide ignoreerimiseks kasuta \"**/Raw/**\". Kõikide .tif failide ignoreerimiseks kasuta \"**/*.tif\". Absouutse tee ignoreerimiseks kasuta \"/path/to/ignore/**\".",
+    "asset_offline_description": "Seda välise kogu üksust ei leitud kettalt ning see liigutati prügikasti. Kui faili asukoht kogu siseselt muutus, leiad vastava uue üksuse oma ajajoonelt. Üksuse taastamiseks veendu, et allpool toodud failitee on Immich'ile kättesaadav ning skaneeri kogu uuesti.",
     "authentication_settings": "Autentimise seaded",
     "authentication_settings_description": "Halda parooli, OAuth ja muid autentimise seadeid",
     "authentication_settings_disable_all": "Kas oled kindel, et soovid kõik sisselogimismeetodid välja lülitada? Sisselogimine lülitatakse täielikult välja.",
     "authentication_settings_reenable": "Et taas lubada, kasuta <link>serveri käsku</link>.",
     "background_task_job": "Tausttegumid",
+    "backup_database": "Varunda andmebaas",
+    "backup_database_enable_description": "Luba andmebaasi varundamine",
+    "backup_keep_last_amount": "Varukoopiate arv, mida alles hoida",
+    "backup_settings": "Varundamise seaded",
+    "backup_settings_description": "Halda andmebaasi varundamise seadeid",
     "check_all": "Märgi kõik",
     "cleared_jobs": "Tööted eemaldatud: {job}",
     "config_set_by_file": "Konfiguratsioon on määratud konfifaili abil",
@@ -41,39 +48,47 @@
     "confirm_email_below": "Kinnitamiseks sisesta allpool \"{email}\"",
     "confirm_reprocess_all_faces": "Kas oled kindel, et soovid kõik näod uuesti töödelda? See eemaldab kõik nimega isikud.",
     "confirm_user_password_reset": "Kas oled kindel, et soovid kasutaja {user} parooli lähtestada?",
+    "create_job": "Lisa tööde",
+    "cron_expression": "Cron avaldis",
+    "cron_expression_description": "Sea skaneerimise intervall cron formaadis. Rohkema info jaoks vaata nt. <link>Crontab Guru</link>",
+    "cron_expression_presets": "Eelseadistatud cron avaldised",
     "disable_login": "Keela sisselogimine",
-    "duplicate_detection_job_description": "Rakenda üksustele masinõpet, et tuvastada sarnaseid pilte. Kasutab nutiotsingut",
+    "duplicate_detection_job_description": "Rakenda üksustele masinõpet, et leida sarnaseid pilte. Kasutab nutiotsingut",
     "exclusion_pattern_description": "Välistamismustrid võimaldavad ignoreerida faile ja kaustu kogu skaneerimisel. See on kasulik, kui sul on kaustu, mis sisaldavad faile, mida sa ei soovi importida, nagu RAW failid.",
     "external_library_created_at": "Väline kogu (lisatud {date})",
     "external_library_management": "Väliste kogude haldus",
-    "face_detection": "Näotuvastus",
-    "face_detection_description": "Otsi üksustest nägusid masinõppe abil. Videote puhul kasutatakse ainult pisipilti. \"Kõik\" töötleb kõik üksused uuesti. \"Puuduvad\" võtab ette üksused, mida pole veel töödeldud. Leitud näod suunatakse näotuvastusse, et grupeerida nad olemasolevateks või uuteks isikuteks.",
-    "facial_recognition_job_description": "Grupeeri leitud näod inimesteks. See samm käivitub siis, kui näotuvastus on lõppenud. \"Kõik\" grupeerib kõik näod uuesti. \"Puuduvad\" võtab ette näod, mida pole isikuga seostatud.",
+    "face_detection": "Näoavastus",
+    "face_detection_description": "Avasta üksustest nägusid masinõppe abil. Videote puhul kasutatakse ainult pisipilti. \"Värskenda\" töötleb kõik üksused uuesti. \"Lähtesta\" kustutab lisaks kõik seni leitud näed. \"Puuduvad\" võtab ette üksused, mida pole veel töödeldud. Avastatud näod suunatakse näotuvastusse, et grupeerida nad olemasolevateks või uuteks isikuteks.",
+    "facial_recognition_job_description": "Grupeeri avastatud näod inimesteks. See samm käivitub siis, kui näoavastus on lõppenud. \"Lähtesta\" grupeerib kõik näod uuesti. \"Puuduvad\" võtab ette näod, mida pole isikuga seostatud.",
     "failed_job_command": "Käsk {command} ebaõnnestus töötes: {job}",
     "force_delete_user_warning": "HOIATUS: See kustutab koheselt kasutaja ja kõik üksused. Seda ei saa tagasi võtta ja faile ei saa taastada.",
     "forcing_refresh_library_files": "Kogu kõigi failide sundvärskendamine",
+    "image_format": "Formaat",
     "image_format_description": "WebP failid on väiksemad kui JPEG, aga kodeerimine on aeglasem.",
     "image_prefer_embedded_preview": "Eelista manustatud eelvaadet",
     "image_prefer_embedded_preview_setting_description": "Kasuta pilditöötluse sisendina võimalusel RAW fotodesse manustatud eelvaateid. See võib mõnede piltide puhul anda tulemuseks täpsemad värvid, aga eelvaate kvaliteet sõltub konkreetsest kaamerast ning pildis võib olla rohkem tihendusmüra.",
+    "image_prefer_wide_gamut": "Eelista laia värvigammat",
     "image_prefer_wide_gamut_setting_description": "Kasuta pisipiltide jaoks Display P3. See säilitab paremini laia värviruumiga piltide erksuse, aga vanematel seadmetel ja vanemate brauseritega võivad pildid teistsugused välja näha. sRGB pildid säilitatakse värvinihete vältimiseks.",
-    "image_preview_format": "Eelvaate formaat",
-    "image_preview_resolution": "Eelvaate resolutsioon",
-    "image_preview_resolution_description": "Kasutusel üksiku foto vaatamisel ja masinõppe jaoks. Kõrgem resolutsioon säilitab rohkem detaile, aga kodeerimine võtab rohkem aega, tekitab suurema faili ning võib mõjutada rakenduse töökiirust.",
+    "image_preview_description": "Keskmise suurusega pilt ilma metaandmeteta, kasutusel üksiku üksuse vaatamise ja masinõppe jaoks",
+    "image_preview_quality_description": "Eelvaate kvaliteet vahemikus 1-100. Kõrgem väärtus on parem, aga tekitab suuremaid faile ning võib mõjutada rakenduse töökiirust. Madala väärtuse seadmine võib mõjutada masinõppe kvaliteeti.",
+    "image_preview_title": "Eelvaate seaded",
     "image_quality": "Kvaliteet",
-    "image_quality_description": "Pildikvaliteet vahemikus 1-100. Kõrgem väärtus tähendab paremat kvaliteeti ja suuremaid faile. See valik mõjutab eelvaateid ja pisipilte.",
+    "image_resolution": "Resolutsioon",
+    "image_resolution_description": "Kõrgemad resolutsioonid säilitavad rohkem detaile, aga kodeerimine võtab kauem aega, tekitab suuremaid faile ning võib mõjutada rakenduse töökiirust.",
     "image_settings": "Pildi seaded",
     "image_settings_description": "Halda genereeritud piltide kvaliteeti ja resolutsiooni",
-    "image_thumbnail_format": "Pisipildi formaat",
-    "image_thumbnail_resolution": "Pisipildi resolutsioon",
-    "image_thumbnail_resolution_description": "Kasutusel fotode mitmekaupa vaatamisel (ajajoon, albumi vaade, jne). Kõrgem resolutsioon säilitab rohkem detaile, aga kodeerimine võtab rohkem aega, tekitab suurema faili ning võib mõjutada rakenduse töökiirust.",
+    "image_thumbnail_description": "Väike pisipilt ilma metaandmeteta, kasutusel fotode grupikaupa vaatamisel, näiteks ajajoonel",
+    "image_thumbnail_quality_description": "Pisipildi kvaliteet vahemikus 1-100. Kõrgem väärtus on parem, aga tekitab suuremaid faile ning võib mõjutada rakenduse töökiirust.",
+    "image_thumbnail_title": "Pisipildi seaded",
     "job_concurrency": "{job} samaaegsus",
+    "job_created": "Tööde lisatud",
+    "job_not_concurrency_safe": "Seda töödet pole ohutu samaaegselt käivitada.",
     "job_settings": "Tööte seaded",
     "job_settings_description": "Halda töödete samaaegsust",
     "job_status": "Tööte seisund",
+    "jobs_delayed": "{jobCount, plural, other {# edasi lükatud}}",
+    "jobs_failed": "{jobCount, plural, other {# ebaõnnestus}}",
     "library_created": "Lisatud kogu: {library}",
-    "library_cron_expression": "Cron avaldis",
-    "library_cron_expression_description": "Sea skaneerimise intervall cron formaadis. Rohkema info jaoks vaata nt. <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Eelseadistatud cron avaldised",
     "library_deleted": "Kogu kustutatud",
     "library_import_path_description": "Määra kaust, mida importida. Sellest kaustast ning alamkaustadest otsitakse pilte ja videosid.",
     "library_scanning": "Perioodiline skaneerimine",
@@ -81,6 +96,7 @@
     "library_scanning_enable_description": "Luba kogu perioodiline skaneerimine",
     "library_settings": "Väline kogu",
     "library_settings_description": "Halda välise kogu seadeid",
+    "library_tasks_description": "Soorita kogu toiminguid",
     "library_watching_enable_description": "Jälgi välises kogus failide muudatusi",
     "library_watching_settings": "Kogu jälgimine (EKSPERIMENTAALNE)",
     "library_watching_settings_description": "Jälgi automaatselt muutunud faile",
@@ -89,43 +105,63 @@
     "logging_settings": "Logimine",
     "machine_learning_clip_model": "CLIP mudel",
     "machine_learning_clip_model_description": "CLIP mudeli nimi, mis on loetletud <link>siin</link>. Pane tähele, et mudeli muutmisel pead kõigi piltide peal nutiotsingu tööte uuesti käivitama.",
-    "machine_learning_duplicate_detection": "Duplikaatide tuvastus",
-    "machine_learning_duplicate_detection_enabled": "Luba duplikaatide tuvastus",
+    "machine_learning_duplicate_detection": "Duplikaatide leidmine",
+    "machine_learning_duplicate_detection_enabled": "Luba duplikaatide leidmine",
     "machine_learning_duplicate_detection_enabled_description": "Kui keelatud, dedubleeritakse siiski täpselt identsed üksused.",
     "machine_learning_duplicate_detection_setting_description": "Kasuta CLIP-manuseid, et leida tõenäoliseid duplikaate",
     "machine_learning_enabled": "Luba masinõpe",
     "machine_learning_enabled_description": "Kui keelatud, lülitatakse kõik masinõppe funktsioonid välja, sõltumata allolevatest seadetest.",
     "machine_learning_facial_recognition": "Näotuvastus",
-    "machine_learning_facial_recognition_description": "Otsi, tuvasta ja grupeeri piltidel näod",
+    "machine_learning_facial_recognition_description": "Avasta, tuvasta ja grupeeri piltidel näod",
     "machine_learning_facial_recognition_model": "Näotuvastuse mudel",
-    "machine_learning_facial_recognition_model_description": "Mudelid on järjestatud suuruse järgi kahanevalt. Suuremad mudelid on aeglasemad ja kasutavad rohkem mälu, kuid annavad parema tulemuse. Mudeli muutmisel tuleb näotuvastuse tööde kõigi piltide peal uuesti käivitada.",
+    "machine_learning_facial_recognition_model_description": "Mudelid on järjestatud suuruse järgi kahanevalt. Suuremad mudelid on aeglasemad ja kasutavad rohkem mälu, kuid annavad parema tulemuse. Mudeli muutmisel tuleb näoavastuse tööde kõigi piltide peal uuesti käivitada.",
     "machine_learning_facial_recognition_setting": "Luba näotuvastus",
-    "machine_learning_max_detection_distance": "Maksimaalne tuvastuskaugus",
-    "machine_learning_max_detection_distance_description": "Maksimaalne kaugus kahe pildi vahel, mille puhul loetakse nad duplikaatideks, vahemikus 0.001-0.1. Kõrgemad väärtused tuvastavad rohkem duplikaate, aga võivad anda valepositiivseid.",
+    "machine_learning_facial_recognition_setting_description": "Kui keelatud, siis ei kodeerita pilte näotuvastuse jaoks ning isikute sektsioon Avasta lehel jääb tühjaks.",
+    "machine_learning_max_detection_distance": "Maksimaalne avastuskaugus",
+    "machine_learning_max_detection_distance_description": "Maksimaalne kaugus kahe pildi vahel, mille puhul loetakse nad duplikaatideks, vahemikus 0.001-0.1. Kõrgemad väärtused leiavad rohkem duplikaate, aga võib esineda valepositiivseid.",
+    "machine_learning_max_recognition_distance": "Maksimaalne tuvastuskaugus",
     "machine_learning_max_recognition_distance_description": "Maksimaalne kaugus kahe näo vahel, mida tuleks lugeda samaks isikuks, vahemikus 0-2. Selle vähendamine aitab vältida erinevate inimeste samaks isikuks märkimist ja tõstmine aitab vältida sama inimese kaheks erinevaks isikuks märkimist. Pane tähele, et kaht isikut ühendada on lihtsam kui üht isikut kaheks eraldada, seega võimalusel kasuta madalamat lävendit.",
-    "machine_learning_min_detection_score_description": "Minimaalne usaldusskoor näo tuvastamiseks, vahemikus 0-1. Madalamad väärtused leiavad rohkem nägusid, kuid võib esineda valepositiivseid.",
-    "machine_learning_min_recognized_faces": "Minimaalne leitud nägude arv",
-    "machine_learning_min_recognized_faces_description": "Minimaalne leitud nägude arv, mida saab isikuks grupeerida. Selle suurendamine teeb näotuvastuse täpsemaks, kuid suureneb tõenäosus, et nägu ei seostata ühegi isikuga.",
+    "machine_learning_min_detection_score": "Minimaalne avastusskoor",
+    "machine_learning_min_detection_score_description": "Minimaalne usaldusskoor näo avastamiseks, vahemikus 0-1. Madalamad väärtused leiavad rohkem nägusid, kuid võib esineda valepositiivseid.",
+    "machine_learning_min_recognized_faces": "Minimaalne tuvastatud nägude arv",
+    "machine_learning_min_recognized_faces_description": "Minimaalne tuvastatud nägude arv, mida saab isikuks grupeerida. Selle suurendamine teeb näotuvastuse täpsemaks, kuid suureneb tõenäosus, et nägu ei seostata ühegi isikuga.",
     "machine_learning_settings": "Masinõppe seaded",
     "machine_learning_settings_description": "Halda masinõppe funktsioone ja seadeid",
     "machine_learning_smart_search": "Nutiotsing",
     "machine_learning_smart_search_description": "Otsi pilte semantiliselt CLIP-manuste abil",
     "machine_learning_smart_search_enabled": "Luba nutiotsing",
     "machine_learning_smart_search_enabled_description": "Kui keelatud, siis ei kodeerita pilte nutiotsingu jaoks.",
-    "machine_learning_url_description": "Masinõppe serveri URL",
+    "machine_learning_url_description": "Masinõppe serveri URL. Kui ette on antud rohkem kui üks URL, proovitakse neid järjest ükshaaval, kuni üks edukalt vastab.",
+    "manage_concurrency": "Halda samaaegsust",
     "manage_log_settings": "Halda logi seadeid",
     "map_dark_style": "Tume stiil",
+    "map_enable_description": "Luba kaardi funktsioonid",
     "map_gps_settings": "Kaardi ja GPS-i seaded",
+    "map_gps_settings_description": "Halda kaardi ja GPS-i (pöördgeokodeerimise) seadeid",
+    "map_implications": "Kaardifunktsioon kasutab välist kaarditeenust (tiles.immich.cloud)",
     "map_light_style": "Hele stiil",
+    "map_manage_reverse_geocoding_settings": "Halda <link>pöördgeokodeerimise</link> seadeid",
+    "map_reverse_geocoding": "Pöördgeokodeerimine",
+    "map_reverse_geocoding_enable_description": "Luba pöördgeokodeerimine",
+    "map_reverse_geocoding_settings": "Pöördgeokodeerimise seaded",
     "map_settings": "Kaart",
     "map_settings_description": "Halda kaardi seadeid",
+    "map_style_description": "Kaarditeema style.json URL",
     "metadata_extraction_job": "Metaandmete eraldamine",
     "metadata_extraction_job_description": "Eralda igast üksusest metaandmed, nagu GPS-koordinaadid, näod ja resolutsioon",
+    "metadata_faces_import_setting": "Luba nägude import",
+    "metadata_faces_import_setting_description": "Impordi näod piltide EXIF andmetest ja välistest failidest",
+    "metadata_settings": "Metaandmete seaded",
+    "metadata_settings_description": "Halda metaandmete seadeid",
     "migration_job": "Migratsioon",
     "migration_job_description": "Migreeri üksuste ja nägude pisipildid uusimale kaustastruktuurile",
+    "no_paths_added": "Ühtegi teed pole",
+    "no_pattern_added": "Mustreid ei ole",
+    "note_apply_storage_label_previous_assets": "Märkus: Et rakendada talletussilt varem üleslaaditud üksustele, käivita",
     "note_cannot_be_changed_later": "MÄRKUS: Seda ei saa hiljem muuta!",
+    "note_unlimited_quota": "Märkus: Piiramatu kvoodi jaoks sisesta 0",
     "notification_email_from_address": "Saatja aadress",
-    "notification_email_from_address_description": "Saatja e-posti aadress, näiteks: \"Immich Photo Server <noreply@immich.app>\"",
+    "notification_email_from_address_description": "Saatja e-posti aadress, näiteks: \"Immich Photo Server <noreply@example.com>\"",
     "notification_email_host_description": "E-posti serveri host (nt. smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Ignoreeri sertifikaadi vigu",
     "notification_email_ignore_certificate_errors_description": "Ignoreeri TLS sertifikaadi valideerimise vigu (mittesoovituslik)",
@@ -140,40 +176,98 @@
     "notification_enable_email_notifications": "Luba e-posti teel teavitused",
     "notification_settings": "Teavituse seaded",
     "notification_settings_description": "Halda teavituste seadeid, sh. e-posti teel",
+    "oauth_auto_launch": "Automaatne käivitamine",
+    "oauth_auto_launch_description": "Alusta OAuth autentimist automaatselt sisselogimise lehele jõudmisel",
+    "oauth_auto_register": "Automaatne registreerimine",
+    "oauth_auto_register_description": "Registreeri uued kasutajad automaatselt OAuth abil sisselogimisel",
     "oauth_button_text": "Nupu tekst",
     "oauth_client_id": "Kliendi ID",
     "oauth_client_secret": "Kliendi saladus",
     "oauth_enable_description": "Sisene OAuth abil",
     "oauth_issuer_url": "Väljastaja URL",
+    "oauth_mobile_redirect_uri": "Mobiilne ümbersuunamise URI",
+    "oauth_mobile_redirect_uri_override": "Mobiilse ümbersuunamise URI ülekirjutamine",
+    "oauth_mobile_redirect_uri_override_description": "Lülita sisse, kui OAuth pakkuja ei luba mobiilset URI-d, näiteks '{callback}'",
+    "oauth_profile_signing_algorithm": "Profiili allkirjastamise algoritm",
+    "oauth_profile_signing_algorithm_description": "Algoritm, mida kasutatakse kasutajaprofiili allkirjastamiseks.",
+    "oauth_scope": "Skoop",
     "oauth_settings": "OAuth",
     "oauth_settings_description": "Halda OAuth sisselogimise seadeid",
+    "oauth_settings_more_details": "Selle funktsiooni kohta rohkem teada saamiseks loe <link>dokumentatsiooni</link>.",
+    "oauth_signing_algorithm": "Allkirjastamise algoritm",
+    "oauth_storage_label_claim": "Talletussildi väide",
+    "oauth_storage_label_claim_description": "Sea kasutaja talletussildiks automaatselt selle väite väärtus.",
+    "oauth_storage_quota_claim": "Talletuskvoodi väide",
+    "oauth_storage_quota_claim_description": "Sea kasutaja talletuskvoodiks automaatselt selle väite väärtus.",
+    "oauth_storage_quota_default": "Vaikimisi talletuskvoot (GiB)",
+    "oauth_storage_quota_default_description": "Kvoot (GiB), mida kasutada, kui ühtegi väidet pole esitatud (piiramatu kvoodi jaoks sisesta 0).",
+    "offline_paths": "Ühenduseta failiteed",
+    "offline_paths_description": "Need tulemused võivad olla põhjustatud manuaalselt kustutatud failidest, mis ei ole osa välisest kogust.",
     "password_enable_description": "Logi sisse e-posti aadressi ja parooliga",
     "password_settings": "Parooliga sisselogimine",
     "password_settings_description": "Halda parooliga sisselogimise seadeid",
     "paths_validated_successfully": "Kõik teed edukalt valideeritud",
+    "person_cleanup_job": "Isikute korrastamine",
     "quota_size_gib": "Kvoot (GiB)",
     "refreshing_all_libraries": "Kõikide kogude värskendamine",
+    "registration": "Administraatori registreerimine",
     "registration_description": "Kuna sa oled süsteemis esimene kasutaja, määratakse sind administraatoriks, ning sa saad lisada täiendavaid kasutajaid.",
+    "repair_all": "Paranda kõik",
+    "repair_matched_items": "{count, plural, one {# üksus} other {# üksust}} leitud",
+    "repaired_items": "{count, plural, one {# üksus} other {# üksust}} parandatud",
     "require_password_change_on_login": "Nõua kasutajalt esmakordsel sisenemisel parooli muutmist",
     "reset_settings_to_default": "Lähtesta seaded",
     "reset_settings_to_recent_saved": "Taasta hiljuti salvestatud seaded",
-    "scanning_library_for_changed_files": "Kogu muutunud failide skaneerimine",
-    "scanning_library_for_new_files": "Kogu uute failide skaneerimine",
+    "scanning_library": "Kogu skaneerimine",
+    "search_jobs": "Otsi töödet...",
     "send_welcome_email": "Saada tervituskiri",
     "server_external_domain_settings": "Väline domeen",
     "server_external_domain_settings_description": "Domeen avalikult jagatud linkide jaoks, k.a. http(s)://",
+    "server_public_users": "Avalikud kasutajad",
+    "server_public_users_description": "Kasutaja jagatud albumisse lisamisel kuvatakse kõiki kasutajaid (nime ja e-posti aadressiga). Kui keelatud, kuvatakse kasutajate nimekirja ainult administraatoritele.",
     "server_settings": "Serveri seaded",
     "server_settings_description": "Halda serveri seadeid",
     "server_welcome_message": "Tervitusteade",
     "server_welcome_message_description": "Teade, mida kuvatakse sisselogimise lehel.",
+    "sidecar_job": "Väliste failide metaandmed",
+    "sidecar_job_description": "Avasta või sünkroniseeri väliste failide metaandmed failisüsteemist",
     "slideshow_duration_description": "Mitu sekundit igat pilti kuvada",
     "smart_search_job_description": "Käivita üksuste peal masinõpe, et toetada nutiotsingut",
-    "storage_template_migration_info": "Malli muudatused rakenduvad ainult uutele üksustele. Et rakendada malli tagasiulatuvalt olemasolevatele üksustele, käivita <link>{job}</link>.",
+    "storage_template_date_time_description": "Kuupäeva ja kellaaja informatsiooniks kasutatakse üksuse loomise aega",
+    "storage_template_date_time_sample": "Näidisaeg {date}",
+    "storage_template_enable_description": "Lülita sisse talletusmallimootor",
+    "storage_template_hash_verification_enabled": "Räsi kontroll sisse lülitatud",
+    "storage_template_hash_verification_enabled_description": "Lülitab sisse räsi kontrolli; ära lülita seda välja, kui sa ei ole tagajärgedest teadlik",
+    "storage_template_migration": "Talletusmalli migreerimine",
+    "storage_template_migration_description": "Rakenda praegune <link>{template}</link> varem üleslaaditud üksustele",
+    "storage_template_migration_info": "Malli muudatused rakenduvad ainult uutele üksustele. Et rakendada malli tagasiulatuvalt varem üleslaaditud üksustele, käivita <link>{job}</link>.",
+    "storage_template_migration_job": "Talletusmallide migreerimise tööde",
+    "storage_template_more_details": "Et selle funktsiooni kohta rohkem teada saada, loe <template-link>talletusmallide</template-link> ja nende <implications-link>tagajärgede</implications-link> kohta",
+    "storage_template_onboarding_description": "Kui sisse lülitatud, võimaldab see faile kasutaja määratud malli alusel automaatselt organiseerida. Stabiilsusprobleemide tõttu on see funktsioon vaikimisi välja lülitatud. Rohkem infot leiad <link>dokumentatsioonist</link>.",
+    "storage_template_path_length": "Tee pikkuse umbkaudne limiit: <b>{length, number}</b>/{limit, number}",
+    "storage_template_settings": "Talletusmall",
     "storage_template_settings_description": "Halda üleslaaditud üksuse kaustastruktuuri ja failinime",
+    "storage_template_user_label": "<code>{label}</code> on kasutaja talletussilt",
     "system_settings": "Süsteemi seaded",
+    "tag_cleanup_job": "Siltide korrastamine",
+    "template_email_available_tags": "Saad mallis kasutada järgmisi muutujaid: {tags}",
+    "template_email_if_empty": "Kui mall on tühi, kasutatakse vaikimisi e-kirja.",
+    "template_email_invite_album": "Albumisse kutse mall",
+    "template_email_preview": "Eelvaade",
+    "template_email_settings": "E-posti mallid",
+    "template_email_settings_description": "Halda e-posti teavitusmalle",
+    "template_email_update_album": "Albumi muutmise mall",
+    "template_email_welcome": "Tervituskirja mall",
+    "template_settings": "Teavituse mallid",
+    "template_settings_description": "Teavituste mallide haldamine.",
+    "theme_custom_css_settings": "Kohandatud CSS",
+    "theme_custom_css_settings_description": "Cascading Style Sheets lubab Immich'i kujunduse kohandamist.",
+    "theme_settings": "Teema seaded",
     "theme_settings_description": "Halda Immich'i veebiliidese kohandamist",
-    "thumbnail_generation_job": "Genereeri pisipildid",
+    "these_files_matched_by_checksum": "Need failid ühtivad kontrollsumma alusel",
+    "thumbnail_generation_job": "Pisipiltide genereerimine",
     "thumbnail_generation_job_description": "Genereeri iga üksuse kohta suur, väike ja udustatud pisipilt ning iga isiku kohta pisipilt",
+    "transcoding_acceleration_api": "Kiirenduse API",
     "transcoding_acceleration_api_description": "API, mis suhtleb su seadmega transkodeerimise kiirendamiseks. See seadistus on 'anname parima': ebaõnnestumisel kasutatakse tarkvaralist transkodeerimist. VP9 ei pruugi töötada, sõltuvalt riistvarast.",
     "transcoding_acceleration_nvenc": "NVENC (vajab NVIDIA GPU-d)",
     "transcoding_acceleration_qsv": "Quick Sync (vajab Inteli 7. põlvkonna või uuemat CPU-d)",
@@ -192,13 +286,16 @@
     "transcoding_codecs_learn_more": "Siin kasutatud terminoloogia kohta rohkem teada saamiseks loe FFmpeg-i dokumentatsiooni <h264-link>H.264</h264-link>, <hevc-link>HEVC</hevc-link> ja <vp9-link>VP9</vp9-link> koodekite kohta.",
     "transcoding_constant_quality_mode": "Püsiva kvaliteedi režiim",
     "transcoding_constant_quality_mode_description": "ICQ on parem kui CQP, aga mõned riistvaralise kiirenduse seadmed ei toeta seda režiimi. Selle valiku seadmisel eelistatakse kvaliteedipõhise kodeerimise puhul valitud režiimi. NVENC puhul valikut ignoreeritakse, kuna see ei toeta ICQ-d.",
+    "transcoding_constant_rate_factor": "Püsiv kiirusefaktor (-crf)",
     "transcoding_constant_rate_factor_description": "Video kvaliteeditase. Tüüpilised väärtused on 23 (H.264), 28 (HEVC), 31 (VP9) ning 35 (AV1). Madal on parem, aga tulemuseks on suuremad failid.",
     "transcoding_disabled_description": "Ära transkodeeri videosid. Võib takistada taasesitamist mõnedes seadmetes",
     "transcoding_hardware_acceleration": "Riistvaraline kiirendus",
     "transcoding_hardware_acceleration_description": "Eksperimentaalne; palju kiirem, aga sama bitisageduse juures madalam kvaliteet",
     "transcoding_hardware_decoding": "Riistvaraline dekodeerimine",
-    "transcoding_hardware_decoding_setting_description": "Rakendub ainult NVENC, QSV ja RKMPP puhul. Võimaldab protsessi läbivalt kiirendada, mitte ainult kodeerimist. Ei pruugi kõigi videote puhul töötada.",
+    "transcoding_hardware_decoding_setting_description": "Võimaldab protsessi läbivalt kiirendada, mitte ainult kodeerimist. Ei pruugi kõigi videote puhul töötada.",
     "transcoding_hevc_codec": "HEVC koodek",
+    "transcoding_max_b_frames": "Maksimaalne B-kaadrite arv",
+    "transcoding_max_b_frames_description": "Kõrgemad väärtused parandavad pakkimise efektiivsust, aga aeglustavad kodeerimist. See valik ei pruugi olla ühilduv riistvaralise kiirendusega vanematel seadmetel. 0 lülitab B-kaadrid välja, -1 määrab väärtuse automaatselt.",
     "transcoding_max_bitrate": "Maksimaalne bitisagedus",
     "transcoding_max_bitrate_description": "Maksimaalse bitisageduse määramine teeb failisuurused ennustatavamaks, väikese kvaliteedikao hinnaga. 720p resolutsiooni puhul on tüüpilised väärtused 2600k (VP9 ja HEVC) või 4500k (H.264). Väärtus 0 eemaldab piirangu.",
     "transcoding_max_keyframe_interval": "Maksimaalne võtmekaadri intervall",
@@ -208,6 +305,8 @@
     "transcoding_preferred_hardware_device_description": "Rakendub ainult VAAPI ja QSV puhul. Määrab dri seadme, mida kasutatakse riistvaraliseks transkodeerimiseks.",
     "transcoding_preset_preset": "Eelseadistus (-preset)",
     "transcoding_preset_preset_description": "Pakkimiskiirus. Aeglasemad eelseadistused tekitavad väiksemaid faile ja annavad sama bitisageduse juures parema kvaliteedi. VP9 ignoreerib kiiruseid üle 'faster' taseme.",
+    "transcoding_reference_frames": "Viitekaadrid",
+    "transcoding_reference_frames_description": "Kaadrite arv, millele viidata jooksva kaadri pakkimisel. Suuremad väärtused parandavad pakkimise tõhusust, aga muudavad kodeerimise aeglasemaks. 0 määrab väärtuse automaatselt.",
     "transcoding_required_description": "Ainult mittelubatud formaadis videod",
     "transcoding_settings": "Video transkodeerimise seaded",
     "transcoding_settings_description": "Halda videofailide resolutsiooni ja kodeerimist",
@@ -219,33 +318,44 @@
     "transcoding_threads_description": "Kõrgem väärtus tähendab kiiremat kodeerimist, aga jätab serverile muude tegevuste jaoks vähem ressursse. See väärtus ei tohiks olla suurem kui protsessori tuumade arv. Väärtus 0 tähendab maksimaalset kasutust.",
     "transcoding_tone_mapping": "Toonivastendus",
     "transcoding_tone_mapping_description": "Üritab säilitada HDR videote kvaliteeti SDR-iks teisendamisel. Iga algoritm teeb värvi, detailide ja ereduse osas erinevaid kompromisse. Hable säilitab detaile, Mobius säilitab värve ning Reinhard säilitab eredust.",
-    "transcoding_tone_mapping_npl": "Toonivastendus NPL",
-    "transcoding_tone_mapping_npl_description": "Muudab värve, et need paistaksid sellise eredusega ekraanil normaalsed. Madalamad väärtused suurendavad video eredust ja vastupidi, kuna see kompenseerib ekraani eredust. 0 määrab väärtuse automaatselt.",
     "transcoding_transcode_policy": "Transkodeerimise reegel",
     "transcoding_transcode_policy_description": "Reegel, millal tuleks videot transkodeerida. HDR-videosid transkodeeritakse alati (v.a. kui transkodeerimine on keelatud).",
     "transcoding_two_pass_encoding": "Kahekäiguline kodeerimine",
     "transcoding_two_pass_encoding_setting_description": "Transkodeeri kahes osas, et parandada kodeeritud videote kvaliteeti. Maksimaalse bitisageduse puhul (mis on vajalik H.264 ja HEVC jaoks) kasutab see režiim bitisageduse vahemikku ja ignoreerib CRF-i. VP9 puhul saab kasutada CRF-i, kui maksimaalset bitisagedust pole määratud.",
     "transcoding_video_codec": "Videokoodek",
     "transcoding_video_codec_description": "VP9 on võimekas ja veebiga ühilduv, aga transkodeerimine võtab kauem aega. HEVC on sarnase jõudluse, aga mitte nii hea veebiga ühilduvusega. H.264 on laialt ühilduv ja transkodeerimine on kiire, aga tulemuseks on suuremad failid. AV1 on kõige võimekam koodek, aga pole vanematel seadmetel toetatud.",
+    "trash_enabled_description": "Luba prügikast",
     "trash_number_of_days": "Päevade arv",
     "trash_number_of_days_description": "Päevade arv, kui kaua hoida üksusi prügikastis enne nende lõplikku kustutamist",
+    "trash_settings": "Prügikasti seaded",
+    "trash_settings_description": "Halda prügikasti seadeid",
+    "untracked_files": "Mittejälgitavad failid",
+    "untracked_files_description": "Rakendus ei jälgi neid faile. Need võivad olla põhjustatud ebaõnnestunud liigutamisest, katkestatud üleslaadimisest või rakenduse veast",
+    "user_cleanup_job": "Kasutajate korrastamine",
     "user_delete_delay": "Kasutaja <b>{user}</b> konto ja üksuste lõplik kustutamine on planeeritud {delay, plural, one {# päeva} other {# päeva}} pärast.",
+    "user_delete_delay_settings": "Kustutamise viivitus",
     "user_delete_delay_settings_description": "Päevade arv, pärast mida kustutatakse eemaldatud kasutaja konto ja üksused jäädavalt. Kasutajate kustutamise tööde käivitub keskööl, et otsida kustutamiseks valmis kasutajaid. Selle seadistuse muudatused rakenduvad järgmisel käivitumisel.",
     "user_delete_immediately": "Kasutaja <b>{user}</b> konto ja üksused suunatakse <b>koheselt</b> jäädavale kustutamisele.",
     "user_delete_immediately_checkbox": "Suuna kasutaja ja üksused jäädavale kustutamisele",
+    "user_management": "Kasutajate haldus",
     "user_password_has_been_reset": "Kasutaja parool on lähtestatud:",
     "user_password_reset_description": "Sisesta kasutajale ajutine parool ja teavita teda, et järgmisel sisselogimisel tuleb parool ära muuta.",
     "user_restore_description": "Kasutaja <b>{user}</b> konto taastatakse.",
+    "user_restore_scheduled_removal": "Taasta kasutaja - eemaldamine planeeritud {date, date, long}",
+    "user_settings": "Kasutajate seaded",
+    "user_settings_description": "Halda kasutajate seadeid",
     "user_successfully_removed": "Kasutaja {email} on eemaldatud.",
     "version_check_enabled_description": "Luba versioonikontroll",
     "version_check_implications": "Versioonikontroll vajab perioodilist ühendumist github.com-iga",
     "version_check_settings": "Versioonikontroll",
     "version_check_settings_description": "Luba/keela uue versiooni teavitus",
-    "video_conversion_job": "Transkodeeri videod",
-    "video_conversion_job_description": "Transkodeeri videod suurema brauserite ja seadmetega ühilduvuse nimel"
+    "video_conversion_job": "Videote transkodeerimine",
+    "video_conversion_job_description": "Transkodeeri videod laiema brauserite ja seadmetega ühilduvuse nimel"
   },
   "admin_email": "Administraatori e-post",
   "admin_password": "Administraatori parool",
+  "administration": "Administratsioon",
+  "advanced": "Täpsemad valikud",
   "age_months": "Vanus {months, plural, one {# kuu} other {# kuud}}",
   "age_year_months": "Vanus 1 aasta, {months, plural, one {# kuu} other {# kuud}}",
   "age_years": "{years, plural, other {Vanus #}}",
@@ -261,8 +371,11 @@
   "album_options": "Albumi valikud",
   "album_remove_user": "Eemalda kasutaja?",
   "album_remove_user_confirmation": "Kas oled kindel, et soovid kasutaja {user} eemaldada?",
+  "album_share_no_users": "Paistab, et oled seda albumit kõikide kasutajatega jaganud, või pole ühtegi kasutajat, kellega jagada.",
   "album_updated": "Album muudetud",
   "album_updated_setting_description": "Saa teavitus e-posti teel, kui jagatud albumis on uusi üksuseid",
+  "album_user_left": "Lahkutud albumist {album}",
+  "album_user_removed": "Kasutaja {user} eemaldatud",
   "album_with_link_access": "Luba kõigil, kellel on link, näha selle albumi fotosid ja isikuid.",
   "albums": "Albumid",
   "albums_count": "{count, plural, one {{count, number} album} other {{count, number} albumit}}",
@@ -270,11 +383,23 @@
   "all_albums": "Kõik albumid",
   "all_people": "Kõik isikud",
   "all_videos": "Kõik videod",
+  "allow_dark_mode": "Luba tume teema",
+  "allow_edits": "Luba muutmine",
+  "allow_public_user_to_download": "Luba avalikul kasutajal alla laadida",
+  "allow_public_user_to_upload": "Luba avalikul kasutajal üles laadida",
+  "anti_clockwise": "Vastupäeva",
+  "api_key": "API võti",
   "api_key_description": "Seda väärtust kuvatakse ainult üks kord. Kopeeri see enne akna sulgemist.",
+  "api_key_empty": "Su API võtme nimi ei tohiks olla tühi",
+  "api_keys": "API võtmed",
+  "app_settings": "Rakenduse seaded",
+  "appears_in": "Albumid",
   "archive": "Arhiiv",
   "archive_or_unarchive_photo": "Arhiveeri või taasta foto",
   "archive_size": "Arhiivi suurus",
   "archive_size_description": "Seadista arhiivi suurus allalaadimiseks (GiB)",
+  "archived_count": "{count, plural, other {# arhiveeritud}}",
+  "are_these_the_same_person": "Kas need on sama isik?",
   "are_you_sure_to_do_this": "Kas oled kindel, et soovid seda teha?",
   "asset_added_to_album": "Lisatud albumisse",
   "asset_adding_to_album": "Albumisse lisamine...",
@@ -283,8 +408,9 @@
   "asset_has_unassigned_faces": "Üksusel on seostamata nägusid",
   "asset_hashing": "Räsimine...",
   "asset_offline": "Üksus pole kättesaadav",
-  "asset_offline_description": "See üksus pole kättesaadav. Immich ei saa selle asukohale ligi. Palun tee üksus kättesaadavaks ja siis skaneeri kogu uuesti.",
+  "asset_offline_description": "Seda välise kogu üksust ei leitud kettalt. Abi saamiseks palun võta ühendust oma Immich'i administraatoriga.",
   "asset_skipped": "Vahele jäetud",
+  "asset_skipped_in_trash": "Prügikastis",
   "asset_uploaded": "Üleslaaditud",
   "asset_uploading": "Üleslaadimine...",
   "assets": "Üksused",
@@ -295,25 +421,37 @@
   "assets_moved_to_trash_count": "{count, plural, one {# üksus} other {# üksust}} liigutatud prügikasti",
   "assets_permanently_deleted_count": "{count, plural, one {# üksus} other {# üksust}} jäädavalt kustutatud",
   "assets_removed_count": "{count, plural, one {# üksus} other {# üksust}} eemaldatud",
-  "assets_restore_confirmation": "Kas oled kindel, et soovid oma kustutatud üksused taastada? Seda ei saa tagasi võtta!",
+  "assets_restore_confirmation": "Kas oled kindel, et soovid oma prügikasti liigutatud üksused taastada? Seda ei saa tagasi võtta! Pane tähele, et sel meetodil ei saa taastada ühenduseta üksuseid.",
   "assets_restored_count": "{count, plural, one {# üksus} other {# üksust}} taastatud",
-  "assets_trashed_count": "{count, plural, one {# üksus} other {# üksust}} kustutatud",
+  "assets_trashed_count": "{count, plural, one {# üksus} other {# üksust}} liigutatud prügikasti",
   "assets_were_part_of_album_count": "{count, plural, one {Üksus oli} other {Üksused olid}} juba osa albumist",
   "authorized_devices": "Autoriseeritud seadmed",
   "back": "Tagasi",
+  "back_close_deselect": "Tagasi, sulge, või tühista valik",
+  "backward": "Tagasi",
   "birthdate_saved": "Sünnikuupäev salvestatud",
   "birthdate_set_description": "Sünnikuupäeva kasutatakse isiku vanuse arvutamiseks foto tegemise hetkel.",
   "blurred_background": "Udustatud taust",
+  "bugs_and_feature_requests": "Vearaportid ja täiendussoovid",
+  "build": "Kooste",
+  "build_image": "Koostetõmmis",
   "bulk_delete_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} masskustutada? Sellega jäetakse alles iga grupi suurim üksus ning duplikaadid kustutatakse jäädavalt. Seda tegevust ei saa tagasi võtta!",
   "bulk_keep_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} alles jätta? Sellega märgitakse kõik duplikaadigrupid lahendatuks ilma midagi kustutamata.",
   "bulk_trash_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} masskustutada? Sellega jäetakse alles iga grupi suurim üksus ning duplikaadid liigutatakse prügikasti.",
+  "buy": "Osta Immich",
   "camera": "Kaamera",
   "camera_brand": "Kaamera mark",
   "camera_model": "Kaamera mudel",
   "cancel": "Katkesta",
+  "cancel_search": "Katkesta otsing",
   "cannot_merge_people": "Ei saa isikuid ühendada",
   "cannot_undo_this_action": "Sa ei saa seda tagasi võtta!",
   "cannot_update_the_description": "Kirjelduse muutmine ebaõnnestus",
+  "change_date": "Muuda kuupäeva",
+  "change_expiration_time": "Muuda aegumisaega",
+  "change_location": "Muuda asukohta",
+  "change_name": "Muuda nime",
+  "change_name_successfully": "Nimi edukalt muudetud",
   "change_password": "Parooli muutmine",
   "change_password_description": "See on su esimene kord süsteemi siseneda, või on tehtud taotlus parooli muutmiseks. Palun sisesta allpool uus parool.",
   "change_your_password": "Muuda oma parooli",
@@ -322,8 +460,15 @@
   "check_logs": "Vaata logisid",
   "choose_matching_people_to_merge": "Vali kattuvad isikud, mida ühendada",
   "city": "Linn",
+  "clear": "Tühjenda",
+  "clear_all": "Tühjenda kõik",
+  "clear_all_recent_searches": "Tühjenda hiljutised otsingud",
+  "clear_message": "Tühjenda sõnum",
+  "clear_value": "Tühjenda väärtus",
   "clockwise": "Päripäeva",
   "close": "Sulge",
+  "collapse": "Peida",
+  "collapse_all": "Peida kõik",
   "color": "Värv",
   "color_theme": "Värviteema",
   "comment_deleted": "Kommentaar kustutatud",
@@ -333,8 +478,11 @@
   "confirm": "Kinnita",
   "confirm_admin_password": "Kinnita administraatori parool",
   "confirm_delete_shared_link": "Kas oled kindel, et soovid selle jagatud lingi kustutada?",
+  "confirm_keep_this_delete_others": "Kõik muud üksused selles virnas kustutatakse. Kas oled kindel, et soovid jätkata?",
   "confirm_password": "Kinnita parool",
+  "contain": "Mahuta ära",
   "context": "Kontekst",
+  "continue": "Jätka",
   "copied_image_to_clipboard": "Pilt kopeeritud lõikelauale.",
   "copied_to_clipboard": "Kopeeritud lõikelauale!",
   "copy_error": "Kopeeri viga",
@@ -345,12 +493,14 @@
   "copy_password": "Kopeeri parool",
   "copy_to_clipboard": "Kopeeri lõikelauale",
   "country": "Riik",
+  "cover": "Kata kogu ala",
   "covers": "Kaanepildid",
   "create": "Lisa",
   "create_album": "Lisa album",
   "create_library": "Lisa kogu",
   "create_link": "Lisa link",
   "create_link_to_share": "Lisa jagamiseks link",
+  "create_link_to_share_description": "Luba kõigil, kellel on link, valitud pilte näha",
   "create_new_person": "Lisa uus isik",
   "create_new_person_hint": "Seosta valitud üksused uue isikuga",
   "create_new_user": "Lisa uus kasutaja",
@@ -368,6 +518,7 @@
   "date_of_birth_saved": "Sünnikuupäev salvestatud",
   "date_range": "Kuupäevavahemik",
   "day": "Päev",
+  "deduplicate_all": "Dedubleeri kõik",
   "default_locale": "Vaikimisi lokaat",
   "default_locale_description": "Vorminda kuupäevad ja numbrid vastavalt brauseri lokaadile",
   "delete": "Kustuta",
@@ -377,19 +528,32 @@
   "delete_key": "Kustuta võti",
   "delete_library": "Kustuta kogu",
   "delete_link": "Kustuta link",
+  "delete_others": "Kustuta teised",
   "delete_shared_link": "Kustuta jagatud link",
   "delete_tag": "Kustuta silt",
   "delete_tag_confirmation_prompt": "Kas oled kindel, et soovid sildi {tagName} kustutada?",
   "delete_user": "Kustuta kasutaja",
   "deleted_shared_link": "Jagatud link kustutatud",
+  "deletes_missing_assets": "Kustutab üksused, mis on kettalt puudu",
   "description": "Kirjeldus",
+  "details": "Üksikasjad",
   "direction": "Suund",
+  "disabled": "Välja lülitatud",
+  "disallow_edits": "Keela muutmine",
+  "discord": "Discord",
   "discover": "Avasta",
+  "dismiss_all_errors": "Peida kõik veateated",
+  "dismiss_error": "Peida veateade",
   "display_options": "Kuva valikud",
+  "display_order": "Kuvamise järjekord",
+  "display_original_photos": "Kuva originaalpildid",
   "display_original_photos_setting_description": "Eelista üksuse vaatamisel pisipildile algset fotot, kui see on veebiga ühilduv. See võib mõjutada fotode kuvamise kiirust.",
   "do_not_show_again": "Ära näita enam seda teadet",
+  "documentation": "Dokumentatsioon",
   "done": "Tehtud",
   "download": "Laadi alla",
+  "download_include_embedded_motion_videos": "Manustatud videod",
+  "download_include_embedded_motion_videos_description": "Lisa liikuvatesse fotodesse manustatud videod eraldi failidena",
   "download_settings": "Allalaadimine",
   "download_settings_description": "Halda üksuste allalaadimise seadeid",
   "downloading": "Allalaadimine",
@@ -406,6 +570,7 @@
   "edit_exclusion_pattern": "Muuda välistamismustrit",
   "edit_faces": "Muuda nägusid",
   "edit_import_path": "Muuda imporditeed",
+  "edit_import_paths": "Muuda imporditeid",
   "edit_key": "Muuda võtit",
   "edit_link": "Muuda linki",
   "edit_location": "Muuda asukohta",
@@ -415,10 +580,16 @@
   "edit_title": "Muuda pealkirja",
   "edit_user": "Muuda kasutajat",
   "edited": "Muudetud",
+  "editor": "Muutja",
   "editor_close_without_save_prompt": "Muudatusi ei salvestata",
+  "editor_close_without_save_title": "Sulge muutja?",
+  "editor_crop_tool_h2_aspect_ratios": "Kuvasuhted",
+  "editor_crop_tool_h2_rotation": "Pööre",
   "email": "E-post",
   "empty_trash": "Tühjenda prügikast",
   "empty_trash_confirmation": "Kas oled kindel, et soovid prügikasti tühjendada? See eemaldab kõik seal olevad üksused Immich'ist jäädavalt.\nSeda tegevust ei saa tagasi võtta!",
+  "enable": "Luba",
+  "enabled": "Lubatud",
   "end_date": "Lõppkuupäev",
   "error": "Viga",
   "error_loading_image": "Viga pildi laadimisel",
@@ -427,12 +598,14 @@
     "cannot_navigate_next_asset": "Järgmise üksuse juurde liikumine ebaõnnestus",
     "cannot_navigate_previous_asset": "Eelmise üksuse juurde liikumine ebaõnnestus",
     "cant_apply_changes": "Muudatusi ei õnnestunud rakendada",
+    "cant_change_activity": "Aktiivsuse {enabled, select, true {keelamine} other {lubamine}} ebaõnnestus",
     "cant_change_asset_favorite": "Üksuse lemmiku staatust ei õnnestunud muuta",
     "cant_change_metadata_assets_count": "{count, plural, one {# üksuse} other {# üksuse}} metaandmeid ei õnnestunud muuta",
     "cant_get_faces": "Nägusid ei õnnestunud kätte saada",
     "cant_get_number_of_comments": "Kommentaare ei õnnestunud leida",
     "cant_search_people": "Isikuid ei õnnestunud otsida",
     "cant_search_places": "Kohti ei õnnestunud otsida",
+    "cleared_jobs": "Tööted eemaldatud: {job}",
     "error_adding_assets_to_album": "Viga üksuste albumisse lisamisel",
     "error_adding_users_to_album": "Viga kasutajate albumisse lisamisel",
     "error_deleting_shared_user": "Viga jagatud kasutaja kustutamisel",
@@ -446,14 +619,19 @@
     "failed_to_create_shared_link": "Jagatud lingi lisamine ebaõnnestus",
     "failed_to_edit_shared_link": "Jagatud lingi muutmine ebaõnnestus",
     "failed_to_get_people": "Isikute pärimine ebaõnnestus",
+    "failed_to_keep_this_delete_others": "Selle üksuse säilitamine ja ülejäänute kustutamine ebaõnnestus",
     "failed_to_load_asset": "Üksuse laadimine ebaõnnestus",
     "failed_to_load_assets": "Üksuste laadimine ebaõnnestus",
     "failed_to_load_people": "Isikute laadimine ebaõnnestus",
     "failed_to_remove_product_key": "Tootevõtme eemaldamine ebaõnnestus",
     "failed_to_stack_assets": "Üksuste virnastamine ebaõnnestus",
+    "failed_to_unstack_assets": "Üksuste eraldamine ebaõnnestus",
     "import_path_already_exists": "See imporditee on juba olemas.",
     "incorrect_email_or_password": "Vale e-posti aadress või parool",
+    "paths_validation_failed": "{paths, plural, one {# tee} other {# teed}} ei valideerunud",
     "profile_picture_transparent_pixels": "Profiilipildis ei tohi olla läbipaistvaid piksleid. Palun suumi sisse ja/või liiguta pilti.",
+    "quota_higher_than_disk_size": "Määratud kvoot on suurem kui kettamaht",
+    "repair_unable_to_check_items": "{count, select, one {Üksuse} other {Üksuste}} kontrollimine ebaõnnestus",
     "unable_to_add_album_users": "Kasutajate lisamine albumisse ebaõnnestus",
     "unable_to_add_assets_to_shared_link": "Üksuste jagatud lingile lisamine ebaõnnestus",
     "unable_to_add_comment": "Kommentaari lisamine ebaõnnestus",
@@ -463,6 +641,7 @@
     "unable_to_add_remove_archive": "{archived, select, true {Üksuse arhiivist taastamine} other {Üksuse arhiveerimine}} ebaõnnestus",
     "unable_to_add_remove_favorites": "Üksuse {favorite, select, true {lemmikuks lisamine} other {lemmikutest eemaldamine}} ebaõnnestus",
     "unable_to_archive_unarchive": "{archived, select, true {Arhiveerimine} other {Arhiivist taastamine}} ebaõnnestus",
+    "unable_to_change_album_user_role": "Kasutaja rolli albumis muutmine ebaõnnestus",
     "unable_to_change_date": "Kuupäeva muutmine ebaõnnestus",
     "unable_to_change_favorite": "Üksuse lemmiku staatuse muutmine ebaõnnestus",
     "unable_to_change_location": "Asukoha muutmine ebaõnnestus",
@@ -490,10 +669,14 @@
     "unable_to_enter_fullscreen": "Täisekraanile lülitamine ebaõnnestus",
     "unable_to_exit_fullscreen": "Täisekraanilt väljumine ebaõnnestus",
     "unable_to_get_comments_number": "Kommentaaride arvu leidmine ebaõnnestus",
+    "unable_to_get_shared_link": "Jagamise lingi loomine ebaõnnestus",
     "unable_to_hide_person": "Isiku peitmine ebaõnnestus",
+    "unable_to_link_motion_video": "Liikuva video linkimine ebaõnnestus",
     "unable_to_link_oauth_account": "OAuth konto ühendamine ebaõnnestus",
     "unable_to_load_album": "Albumi laadimine ebaõnnestus",
     "unable_to_load_asset_activity": "Üksuse aktiivsuse laadimine ebaõnnestus",
+    "unable_to_load_items": "Üksuste laadimine ebaõnnestus",
+    "unable_to_load_liked_status": "Meeldimise staatuse laadimine ebaõnnestus",
     "unable_to_log_out_all_devices": "Kõigist seadmetest väljalogimine ebaõnnestus",
     "unable_to_log_out_device": "Seadmest väljalogimine ebaõnnestus",
     "unable_to_login_with_oauth": "OAuth abil sisselogimine ebaõnnestus",
@@ -504,9 +687,11 @@
     "unable_to_remove_album_users": "Kasutajate albumist eemaldamine ebaõnnestus",
     "unable_to_remove_api_key": "API võtme eemaldamine ebaõnnestus",
     "unable_to_remove_assets_from_shared_link": "Üksuste jagatud lingilt eemaldamine ebaõnnestus",
+    "unable_to_remove_deleted_assets": "Ühenduseta failide eemaldamine ebaõnnestus",
     "unable_to_remove_library": "Kogu eemaldamine ebaõnnestus",
     "unable_to_remove_partner": "Partneri eemaldamine ebaõnnestus",
     "unable_to_remove_reaction": "Reaktsiooni eemaldamine ebaõnnestus",
+    "unable_to_repair_items": "Üksuste parandamine ebaõnnestus",
     "unable_to_reset_password": "Parooli lähtestamine ebaõnnestus",
     "unable_to_resolve_duplicate": "Duplikaadi lahendamine ebaõnnestus",
     "unable_to_restore_assets": "Üksuste taastamine ebaõnnestus",
@@ -522,41 +707,56 @@
     "unable_to_scan_library": "Kogu skaneerimine ebaõnnestus",
     "unable_to_set_feature_photo": "Esiletõstetud foto seadmine ebaõnnestus",
     "unable_to_set_profile_picture": "Profiilipildi seadmine ebaõnnestus",
-    "unable_to_trash_asset": "Üksuse kustutamine ebaõnnestus",
+    "unable_to_submit_job": "Tööte edastamine ebaõnnestus",
+    "unable_to_trash_asset": "Üksuse prügikasti liigutamine ebaõnnestus",
+    "unable_to_unlink_account": "Konto lahtiühendamine ebaõnnestus",
     "unable_to_update_album_cover": "Albumi kaanepildi muutmine ebaõnnestus",
     "unable_to_update_album_info": "Albumi info muutmine ebaõnnestus",
     "unable_to_update_library": "Kogu uuendamine ebaõnnestus",
     "unable_to_update_location": "Asukoha muutmine ebaõnnestus",
     "unable_to_update_settings": "Seadete muutmine ebaõnnestus",
+    "unable_to_update_timeline_display_status": "Ajajoonel kuvamise uuendamine ebaõnnestus",
     "unable_to_update_user": "Kasutaja muutmine ebaõnnestus",
     "unable_to_upload_file": "Faili üleslaadimine ebaõnnestus"
   },
   "exif": "Exif",
-  "expire_after": "Aegub pärast",
+  "exit_slideshow": "Sulge slaidiesitlus",
+  "expand_all": "Näita kõik",
+  "expire_after": "Aegub",
   "expired": "Aegunud",
   "expires_date": "Aegub {date}",
   "explore": "Avasta",
+  "export": "Ekspordi",
   "export_as_json": "Ekspordi JSON-formaati",
   "extension": "Laiend",
+  "external": "Väline",
+  "external_libraries": "Välised kogud",
   "face_unassigned": "Seostamata",
+  "failed_to_load_assets": "Üksuste laadimine ebaõnnestus",
   "favorite": "Lemmik",
   "favorites": "Lemmikud",
   "feature_photo_updated": "Esiletõstetud foto muudetud",
+  "features": "Funktsioonid",
+  "features_setting_description": "Halda rakenduse funktsioone",
   "file_name": "Failinimi",
   "file_name_or_extension": "Failinimi või -laiend",
   "filename": "Failinimi",
   "filetype": "Failitüüp",
   "filter_people": "Filtreeri isikuid",
+  "find_them_fast": "Leia teda kiiresti nime järgi otsides",
   "folders": "Kaustad",
   "folders_feature_description": "Kaustavaate abil failisüsteemis olevate fotode ja videote sirvimine",
-  "force_re-scan_library_files": "Sundskaneeri kogu kõik failid uuesti",
   "forward": "Edasi",
   "general": "Üldine",
+  "get_help": "Küsi abi",
+  "getting_started": "Alustamine",
   "go_back": "Tagasi",
+  "go_to_search": "Otsingusse",
   "group_albums_by": "Grupeeri albumid...",
   "group_no": "Ära grupeeri",
   "group_owner": "Grupeeri omaniku kaupa",
   "group_year": "Grupeeri aasta kaupa",
+  "has_quota": "On kvoot",
   "hi_user": "Tere {name} ({email})",
   "hide_all_people": "Peida kõik isikud",
   "hide_gallery": "Peida galerii",
@@ -572,10 +772,21 @@
   "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} koos isikutega {person1} ja {person2}",
   "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} koos isikutega {person1}, {person2} ja {person3}",
   "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} koos {person1}, {person2} ja veel {additionalCount, number} isikuga",
+  "image_alt_text_date_place": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} kohas {city}, {country}",
+  "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} kohas {city}, {country} koos isikuga {person1}",
+  "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} kohas {city}, {country} koos isikutega {person1} ja {person2}",
+  "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} kohas {city}, {country} koos isikutega {person1}, {person2} ja {person3}",
+  "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} kohas {city}, {country} koos {person1}, {person2} ja veel {additionalCount, number} isikuga",
   "immich_logo": "Immich'i logo",
   "immich_web_interface": "Immich'i veebiliides",
   "import_from_json": "Impordi JSON-formaadist",
+  "import_path": "Imporditee",
+  "in_albums": "{count, plural, one {# albumis} other {# albumis}}",
   "in_archive": "Arhiivis",
+  "include_archived": "Kaasa arhiveeritud",
+  "include_shared_albums": "Kaasa jagatud albumid",
+  "include_shared_partner_assets": "Kaasa partneri jagatud üksused",
+  "individual_share": "Jagatud üksus",
   "info": "Info",
   "interval": {
     "day_at_onepm": "Iga päev kell 13",
@@ -585,9 +796,12 @@
   },
   "invite_people": "Kutsu inimesi",
   "invite_to_album": "Kutsu albumisse",
+  "items_count": "{count, plural, one {# üksus} other {# üksust}}",
   "jobs": "Tööted",
   "keep": "Jäta alles",
   "keep_all": "Jäta kõik alles",
+  "keep_this_delete_others": "Säilita see, kustuta ülejäänud",
+  "kept_this_deleted_others": "See üksus säilitatud ning {count, plural, one {# üksus} other {# üksust}} kustutatud",
   "keyboard_shortcuts": "Kiirklahvid",
   "language": "Keel",
   "language_setting_description": "Vali oma eelistatud keel",
@@ -595,27 +809,47 @@
   "latest_version": "Uusim versioon",
   "latitude": "Laiuskraad",
   "leave": "Lahku",
+  "let_others_respond": "Luba teistel vastata",
+  "level": "Tase",
   "library": "Kogu",
   "library_options": "Kogu seaded",
+  "light": "Hele",
+  "like_deleted": "Meeldimine kustutatud",
+  "link_options": "Lingi valikud",
+  "link_to_oauth": "Ühenda OAuth",
+  "linked_oauth_account": "OAuth konto ühendatud",
   "list": "Loend",
+  "loading": "Laadimine",
   "loading_search_results_failed": "Otsitulemuste laadimine ebaõnnestus",
   "log_out": "Logi välja",
   "log_out_all_devices": "Logi kõigist seadmetest välja",
   "logged_out_all_devices": "Kõigist seadmetest välja logitud",
   "logged_out_device": "Seadmest välja logitud",
+  "login": "Logi sisse",
   "login_has_been_disabled": "Sisselogimine on keelatud.",
   "logout_all_device_confirmation": "Kas oled kindel, et soovid kõigist seadmetest välja logida?",
   "logout_this_device_confirmation": "Kas oled kindel, et soovid sellest seadmest välja logida?",
   "longitude": "Pikkuskraad",
+  "look": "Välimus",
+  "loop_videos": "Taasesita videod",
+  "loop_videos_description": "Lülita sisse, et detailvaates videot automaatselt taasesitada.",
+  "main_branch_warning": "Sa kasutad arendusversiooni; soovitame tungivalt kasutada väljalaskeversiooni!",
   "make": "Mark",
   "manage_shared_links": "Halda jagatud linke",
   "manage_sharing_with_partners": "Halda partneritega jagamist",
+  "manage_the_app_settings": "Halda rakenduse seadeid",
   "manage_your_account": "Halda oma kontot",
   "manage_your_api_keys": "Halda oma API võtmeid",
   "manage_your_devices": "Halda oma autenditud seadmeid",
+  "manage_your_oauth_connection": "Halda oma OAuth ühendust",
   "map": "Kaart",
+  "map_marker_for_images": "Kaardimarker kohas {city}, {country} tehtud piltide jaoks",
+  "map_marker_with_image": "Kaardimarker pildiga",
   "map_settings": "Kaardi seaded",
+  "matches": "Ühtivad failid",
+  "media_type": "Meedia tüüp",
   "memories": "Mälestused",
+  "memories_setting_description": "Halda, mida sa oma mälestustes näed",
   "memory": "Mälestus",
   "menu": "Menüü",
   "merge": "Ühenda",
@@ -624,13 +858,17 @@
   "merge_people_prompt": "Kas soovid need isikud ühendada? Seda tegevust ei saa tagasi võtta.",
   "merge_people_successfully": "Isikud ühendatud",
   "merged_people_count": "Ühendatud {count, plural, one {# isik} other {# isikut}}",
+  "minimize": "Minimeeri",
   "minute": "Minut",
+  "missing": "Puuduvad",
   "model": "Mudel",
   "month": "Kuu",
   "more": "Rohkem",
+  "moved_to_trash": "Liigutatud prügikasti",
   "my_albums": "Minu albumid",
   "name": "Nimi",
   "name_or_nickname": "Nimi või hüüdnimi",
+  "never": "Mitte kunagi",
   "new_album": "Uus album",
   "new_api_key": "Uus API võti",
   "new_password": "Uus parool",
@@ -642,29 +880,51 @@
   "next_memory": "Järgmine mälestus",
   "no": "Ei",
   "no_albums_message": "Lisa album fotode ja videote organiseerimiseks",
+  "no_albums_with_name_yet": "Paistab, et sul pole veel ühtegi selle nimega albumit.",
+  "no_albums_yet": "Paistab, et sul pole veel ühtegi albumit.",
   "no_archived_assets_message": "Arhiveeri fotod ja videod, et neid Fotod vaatest peita",
   "no_assets_message": "KLIKI ESIMESE FOTO ÜLESLAADIMISEKS",
   "no_duplicates_found": "Ühtegi duplikaati ei leitud.",
   "no_exif_info_available": "Exif info pole saadaval",
+  "no_explore_results_message": "Oma kogu avastamiseks laadi üles rohkem fotosid.",
   "no_favorites_message": "Lisa lemmikud, et oma parimaid fotosid ja videosid kiiresti leida",
   "no_libraries_message": "Lisa väline kogu oma fotode ja videote vaatamiseks",
+  "no_name": "Nimetu",
+  "no_places": "Kohti ei ole",
+  "no_results": "Vasteid pole",
+  "no_results_description": "Proovi sünonüümi või üldisemat märksõna",
   "no_shared_albums_message": "Lisa album, et fotosid ja videosid teistega jagada",
+  "not_in_any_album": "Pole üheski albumis",
+  "note_apply_storage_label_to_previously_uploaded assets": "Märkus: Et rakendada talletussilt varem üleslaaditud üksustele, käivita",
+  "note_unlimited_quota": "Märkus: Piiramatu kvoodi jaoks sisesta 0",
+  "notes": "Märkused",
   "notification_toggle_setting_description": "Luba e-posti teel teavitused",
   "notifications": "Teavitused",
   "notifications_setting_description": "Halda teavitusi",
   "oauth": "OAuth",
+  "official_immich_resources": "Ametlikud Immich'i ressursid",
+  "offline": "Ühendus puudub",
+  "offline_paths": "Ühenduseta failiteed",
+  "offline_paths_description": "Need tulemused võivad olla põhjustatud manuaalselt kustutatud failidest, mis ei ole osa välisest kogust.",
+  "ok": "Ok",
   "oldest_first": "Vanemad eespool",
+  "onboarding": "Kasutuselevõtt",
+  "onboarding_privacy_description": "Järgnevad (valikulised) funktsioonid sõltuvad välistest teenustest ning neid saab igal ajal administraatori seadetes välja lülitada.",
   "onboarding_theme_description": "Vali oma serverile värviteema. Saad seda hiljem seadetes muuta.",
+  "onboarding_welcome_description": "Algväärtustame mõned levinumad seaded.",
   "onboarding_welcome_user": "Tere tulemast, {user}",
+  "online": "Ühendatud",
   "only_favorites": "Ainult lemmikud",
-  "only_refreshes_modified_files": "Värskendab ainult muudetud failid",
   "open_in_map_view": "Ava kaardi vaates",
   "open_in_openstreetmap": "Ava OpenStreetMap",
+  "open_the_search_filters": "Ava otsingufiltrid",
   "options": "Valikud",
   "or": "või",
   "organize_your_library": "Korrasta oma kogu",
   "original": "originaal",
+  "other": "Muud",
   "other_devices": "Muud seadmed",
+  "other_variables": "Muud muutujad",
   "owned": "Minu omad",
   "owner": "Omanik",
   "partner": "Partner",
@@ -684,6 +944,7 @@
   },
   "path": "Tee",
   "pattern": "Muster",
+  "pause": "Peata",
   "pause_memories": "Peata mälestused",
   "paused": "Peatatud",
   "pending": "Ootel",
@@ -699,6 +960,8 @@
   "permanently_deleted_asset": "Üksus jäädavalt kustutatud",
   "permanently_deleted_assets_count": "{count, plural, one {# üksus} other {# üksust}} jäädavalt kustutatud",
   "person": "Isik",
+  "person_hidden": "{name}{hidden, select, true { (peidetud)} other {}}",
+  "photo_shared_all_users": "Paistab, et oled oma fotosid kõigi kasutajatega jaganud, või pole ühtegi kasutajat, kellega jagada.",
   "photos": "Fotod",
   "photos_and_videos": "Fotod ja videod",
   "photos_count": "{count, plural, one {{count, number} foto} other {{count, number} fotot}}",
@@ -706,7 +969,9 @@
   "pick_a_location": "Vali asukoht",
   "place": "Asukoht",
   "places": "Kohad",
+  "play": "Esita",
   "play_memories": "Esita mälestused",
+  "play_motion_photo": "Esita liikuv foto",
   "play_or_pause_video": "Esita või peata video",
   "port": "Port",
   "preset": "Eelseadistus",
@@ -714,6 +979,7 @@
   "previous": "Eelmine",
   "previous_memory": "Eelmine mälestus",
   "previous_or_next_photo": "Eelmine või järgmine foto",
+  "primary": "Peamine",
   "privacy": "Privaatsus",
   "profile_image_of_user": "Kasutaja {user} profiilipilt",
   "profile_picture_set": "Profiilipilt määratud.",
@@ -732,6 +998,7 @@
   "purchase_failed_activation": "Aktiveerimine ebaõnnestus! Kontrolli oma kirjakastist õiget tootevõtit!",
   "purchase_individual_description_1": "Üksikisikule",
   "purchase_individual_description_2": "Toetaja staatus",
+  "purchase_individual_title": "Individuaalne",
   "purchase_input_suggestion": "Sul on juba tootevõti? Sisesta see allpool",
   "purchase_license_subtitle": "Osta Immich, et toetada selle jätkuvat arendust",
   "purchase_lifetime_description": "Eluaegne ost",
@@ -739,6 +1006,8 @@
   "purchase_panel_info_1": "Immich'i arendamine nõuab palju aega ja vaeva ning meie täiskohaga insenerid töötavad selle nimel, et teha see nii heaks kui vähegi võimalik. Meie missiooniks on muuta avatud lähtekoodiga tarkvara ja eetilised äritavad arendajatele jätkusuutlikuks sissetulekuallikaks ning luua privaatsust austav ökosüsteem, mis pakub tõelisi alternatiive ekspluatatiivsetele pilveteenustele.",
   "purchase_panel_info_2": "Kuna oleme otsustanud maksumüüre mitte lisada, ei anna see ost sulle Immich'is lisavõimalusi. Me loodame Immich'i jätkuvaks arenduseks sinusuguste kasutajate toetusele.",
   "purchase_panel_title": "Toeta projekti",
+  "purchase_per_server": "Serveri kohta",
+  "purchase_per_user": "Kasutaja kohta",
   "purchase_remove_product_key": "Eemalda tootevõti",
   "purchase_remove_product_key_prompt": "Kas oled kindel, et soovid tootevõtme eemaldada?",
   "purchase_remove_server_product_key": "Eemalda serveri tootevõti",
@@ -747,30 +1016,48 @@
   "purchase_server_description_2": "Toetaja staatus",
   "purchase_server_title": "Server",
   "purchase_settings_server_activated": "Serveri tootevõtit haldab administraator",
+  "rating": "Hinnang",
+  "rating_clear": "Tühjenda hinnang",
+  "rating_count": "{count, plural, one {# tärn} other {# tärni}}",
+  "rating_description": "Kuva infopaneelis EXIF hinnangut",
+  "reaction_options": "Reaktsiooni valikud",
   "read_changelog": "Vaata muudatuste ülevaadet",
   "reassigned_assets_to_existing_person": "{count, plural, one {# üksus} other {# üksust}} seostatud {name, select, null {olemasoleva isikuga} other {isikuga {name}}}",
   "reassigned_assets_to_new_person": "{count, plural, one {# üksus} other {# üksust}} seostatud uue isikuga",
   "reassing_hint": "Seosta valitud üksused olemasoleva isikuga",
+  "recent-albums": "Hiljutised albumid",
+  "recent_searches": "Hiljutised otsingud",
   "refresh": "Värskenda",
   "refresh_encoded_videos": "Värskenda kodeeritud videod",
+  "refresh_faces": "Värskenda näod",
   "refresh_metadata": "Värskenda metaandmed",
   "refresh_thumbnails": "Värskenda pisipildid",
   "refreshed": "Värskendatud",
-  "refreshes_every_file": "Värskendab kõik failid",
+  "refreshes_every_file": "Loeb kõik olemasolevad ja uued failid uuesti",
   "refreshing_encoded_video": "Kodeeritud videote värskendamine",
+  "refreshing_faces": "Nägude värskendamine",
   "refreshing_metadata": "Metaandmete värskendamine",
   "regenerating_thumbnails": "Pisipiltide uuesti genereerimine",
   "remove": "Eemalda",
   "remove_assets_album_confirmation": "Kas oled kindel, et soovid {count, plural, one {# üksuse} other {# üksust}} albumist eemaldada?",
   "remove_assets_shared_link_confirmation": "Kas oled kindel, et soovid eemaldada {count, plural, one {# üksuse} other {# üksust}} sellelt jagatud lingilt?",
   "remove_assets_title": "Eemalda üksused?",
+  "remove_custom_date_range": "Eemalda kohandatud kuupäevavahemik",
+  "remove_deleted_assets": "Eemalda kustutatud üksused",
   "remove_from_album": "Eemalda albumist",
   "remove_from_favorites": "Eemalda lemmikutest",
+  "remove_from_shared_link": "Eemalda jagatud lingist",
+  "remove_url": "Eemalda URL",
   "remove_user": "Eemalda kasutaja",
   "removed_api_key": "API võti eemaldatud: {name}",
   "removed_from_archive": "Arhiivist eemaldatud",
   "removed_from_favorites": "Lemmikutest eemaldatud",
+  "removed_from_favorites_count": "{count, plural, other {# eemaldatud}} lemmikutest",
   "removed_tagged_assets": "Silt eemaldatud {count, plural, one {# üksuselt} other {# üksuselt}}",
+  "rename": "Nimeta ümber",
+  "repair_no_results_message": "Mittejälgitavad ja puuduvad failid kuvatakse siin",
+  "replace_with_upload": "Asenda üleslaadimisega",
+  "repository": "Koodihoidla",
   "require_password": "Nõua parooli",
   "require_user_to_change_password_on_first_login": "Nõua kasutajalt esmakordsel sisenemisel parooli muutmist",
   "reset": "Lähtesta",
@@ -784,16 +1071,20 @@
   "restore_user": "Taasta kasutaja",
   "restored_asset": "Üksus taastatud",
   "resume": "Jätka",
+  "retry_upload": "Proovi üleslaadimist uuesti",
+  "review_duplicates": "Vaata duplikaadid läbi",
   "role": "Roll",
+  "role_editor": "Muutja",
+  "role_viewer": "Vaataja",
   "save": "Salvesta",
   "saved_api_key": "API võti salvestatud",
   "saved_profile": "Profiil salvestatud",
   "saved_settings": "Seaded salvestatud",
   "say_something": "Ütle midagi",
   "scan_all_libraries": "Skaneeri kõik kogud",
-  "scan_all_library_files": "Skaneeri kogu kõik failid uuesti",
-  "scan_new_library_files": "Skaneeri kogu uued failid",
+  "scan_library": "Skaneeri",
   "scan_settings": "Skaneerimise seaded",
+  "scanning_for_album": "Albumi skaneerimine...",
   "search": "Otsi",
   "search_albums": "Otsi albumeid",
   "search_by_context": "Otsi konteksti alusel",
@@ -806,13 +1097,16 @@
   "search_for_existing_person": "Otsi olemasolevat isikut",
   "search_no_people": "Isikuid ei ole",
   "search_no_people_named": "Ei ole isikuid nimega \"{name}\"",
+  "search_options": "Otsingu valikud",
   "search_people": "Otsi inimesi",
   "search_places": "Otsi kohti",
+  "search_settings": "Otsi seadeid",
   "search_state": "Otsi osariiki...",
   "search_tags": "Otsi silte...",
   "search_timezone": "Otsi ajavööndit...",
   "search_type": "Otsingu tüüp",
   "search_your_photos": "Otsi oma fotosid",
+  "searching_locales": "Lokaatide otsimine...",
   "second": "Sekund",
   "see_all_people": "Vaata kõiki isikuid",
   "select_album_cover": "Vali albumi kaanepilt",
@@ -829,10 +1123,16 @@
   "selected_count": "{count, plural, other {# valitud}}",
   "send_message": "Saada sõnum",
   "send_welcome_email": "Saada tervituskiri",
+  "server_offline": "Serveriga ühendus puudub",
+  "server_online": "Server ühendatud",
   "server_stats": "Serveri statistika",
   "server_version": "Serveri versioon",
+  "set": "Määra",
   "set_as_album_cover": "Sea albumi kaanepildiks",
   "set_as_profile_picture": "Sea profiilipildiks",
+  "set_date_of_birth": "Määra sünnikuupäev",
+  "set_profile_picture": "Sea profiilipilt",
+  "set_slideshow_to_fullscreen": "Kuva slaidiesitlus täisekraanil",
   "settings": "Seaded",
   "settings_saved": "Seaded salvestatud",
   "share": "Jaga",
@@ -862,13 +1162,21 @@
   "show_metadata": "Kuva metaandmed",
   "show_or_hide_info": "Kuva või peida info",
   "show_password": "Kuva parooli",
+  "show_person_options": "Näita isiku valikuid",
+  "show_progress_bar": "Kuva edenemisriba",
+  "show_search_options": "Kuva otsingu valikud",
+  "show_slideshow_transition": "Kuva slaidiesitluse üleminekud",
   "show_supporter_badge": "Toetaja märk",
   "show_supporter_badge_description": "Kuva toetaja märki",
+  "shuffle": "Juhuslik",
   "sidebar": "Külgmenüü",
+  "sidebar_display_description": "Kuva külgmenüüs linki vaatele",
   "sign_out": "Logi välja",
   "sign_up": "Registreeru",
   "size": "Suurus",
   "skip_to_content": "Sisu juurde",
+  "skip_to_folders": "Kaustade juurde",
+  "skip_to_tags": "Siltide juurde",
   "slideshow": "Slaidiesitlus",
   "slideshow_settings": "Slaidiesitluse seaded",
   "sort_albums_by": "Järjesta albumid...",
@@ -876,58 +1184,105 @@
   "sort_items": "Üksuste arv",
   "sort_modified": "Muutmise aeg",
   "sort_oldest": "Vanim foto",
-  "sort_recent": "Kõige hiljutisem foto",
+  "sort_recent": "Uusim foto",
   "sort_title": "Pealkiri",
-  "stack": "Virn",
+  "source": "Lähtekood",
+  "stack": "Virnasta",
+  "stack_duplicates": "Virnasta duplikaadid",
   "stack_select_one_photo": "Vali virnale kaanefoto",
   "stack_selected_photos": "Virnasta valitud fotod",
   "stacked_assets_count": "{count, plural, one {# üksus} other {# üksust}} virnastatud",
+  "stacktrace": "Pinujälg",
+  "start": "Alusta",
   "start_date": "Alguskuupäev",
   "state": "Osariik",
   "status": "Staatus",
+  "stop_motion_photo": "Peata liikuv pilt",
   "stop_photo_sharing": "Lõpeta oma fotode jagamine?",
   "stop_photo_sharing_description": "{partner} ei pääse rohkem su fotodele ligi.",
   "stop_sharing_photos_with_user": "Lõpeta oma fotode selle kasutajaga jagamine",
+  "storage": "Talletusruum",
+  "storage_label": "Talletussilt",
   "storage_usage": "{used}/{available} kasutatud",
   "suggestions": "Soovitused",
   "sunrise_on_the_beach": "Päikesetõus rannal",
+  "support": "Tugi",
+  "support_and_feedback": "Tugi ja tagasiside",
+  "support_third_party_description": "Sinu Immich'i install on kolmanda osapoole pakendatud. Probleemid, mida täheldad, võivad olla põhjustatud selle pakendamise poolt, seega võta esmajärjekorras nendega ühendust, kasutades allolevaid linke.",
+  "swap_merge_direction": "Muuda ühendamise suunda",
+  "sync": "Sünkrooni",
   "tag": "Silt",
   "tag_assets": "Sildista üksuseid",
   "tag_created": "Lisatud silt: {tag}",
   "tag_feature_description": "Fotode ja videote lehitsemine siltide kaupa grupeeritult",
-  "tag_not_found_question": "Ei leia silti? Lisa uus <link>siin</link>",
+  "tag_not_found_question": "Ei leia silti? <link>Lisa uus silt.</link>",
   "tag_updated": "Muudetud silt: {tag}",
   "tagged_assets": "{count, plural, one {# üksus} other {# üksust}} sildistatud",
   "tags": "Sildid",
+  "template": "Mall",
   "theme": "Teema",
   "theme_selection": "Teema valik",
   "theme_selection_description": "Sea automaatselt hele või tume teema vastavalt veebilehitseja eelistustele",
+  "they_will_be_merged_together": "Nad ühendatakse kokku",
+  "third_party_resources": "Kolmanda osapoole ressursid",
+  "time_based_memories": "Ajapõhised mälestused",
+  "timeline": "Ajajoon",
   "timezone": "Ajavöönd",
   "to_archive": "Arhiivi",
   "to_change_password": "Muuda parool",
   "to_favorite": "Lemmik",
-  "to_trash": "Prügi",
+  "to_login": "Logi sisse",
+  "to_trash": "Prügikasti",
+  "toggle_settings": "Kuva/peida seaded",
+  "toggle_theme": "Lülita tume teema",
+  "total": "Kokku",
+  "total_usage": "Kogukasutus",
+  "trash": "Prügikast",
+  "trash_all": "Kõik prügikasti",
+  "trash_count": "Liiguta {count, number} prügikasti",
   "trash_delete_asset": "Kustuta üksus",
+  "trash_no_results_message": "Siia ilmuvad prügikasti liigutatud fotod ja videod.",
+  "trashed_items_will_be_permanently_deleted_after": "Prügikasti tõstetud üksused kustutatakse jäädavalt {days, plural, one {# päeva} other {# päeva}} pärast.",
   "type": "Tüüp",
   "unarchive": "Taasta arhiivist",
+  "unarchived_count": "{count, plural, other {# arhiivist taastatud}}",
+  "unfavorite": "Eemalda lemmikutest",
   "unhide_person": "Ära peida isikut",
+  "unknown": "Teadmata",
   "unknown_year": "Teadmata aasta",
   "unlimited": "Piiramatu",
+  "unlink_oauth": "Eemalda OAuth ühendus",
+  "unlinked_oauth_account": "OAuth ühendus eemaldatud",
   "unnamed_album": "Nimetu album",
   "unnamed_album_delete_confirmation": "Kas oled kindel, et soovid selle albumi kustutada?",
   "unsaved_change": "Salvestamata muudatus",
+  "unstack": "Eralda",
+  "unstacked_assets_count": "{count, plural, one {# üksus} other {# üksust}} eraldatud",
+  "untracked_files": "Mittejälgitavad failid",
+  "untracked_files_decription": "Rakendus ei jälgi neid faile. Need võivad olla põhjustatud ebaõnnestunud liigutamisest, katkestatud üleslaadimisest või rakenduse veast",
+  "up_next": "Järgmine",
   "updated_password": "Parool muudetud",
+  "upload": "Laadi üles",
+  "upload_concurrency": "Üleslaadimise samaaegsus",
   "upload_errors": "Üleslaadimine lõpetatud {count, plural, one {# veaga} other {# veaga}}, uute üksuste nägemiseks värskenda lehte.",
+  "upload_progress": "Ootel {remaining, number} - Töödeldud {processed, number}/{total, number}",
   "upload_skipped_duplicates": "{count, plural, one {# dubleeritud üksus} other {# dubleeritud üksust}} vahele jäetud",
   "upload_status_duplicates": "Duplikaadid",
   "upload_status_errors": "Vead",
   "upload_status_uploaded": "Üleslaaditud",
   "upload_success": "Üleslaadimine õnnestus, uute üksuste nägemiseks värskenda lehte.",
   "url": "URL",
+  "usage": "Kasutus",
+  "use_custom_date_range": "Kasuta kohandatud kuupäevavahemikku",
   "user": "Kasutaja",
   "user_id": "Kasutaja ID",
   "user_liked": "Kasutajale {user} meeldis {type, select, photo {see foto} video {see video} asset {see üksus} other {see}}",
+  "user_purchase_settings": "Ost",
   "user_purchase_settings_description": "Halda oma ostu",
+  "user_role_set": "Määra kasutajale {user} roll {role}",
+  "user_usage_detail": "Kasutajate kasutusandmed",
+  "user_usage_stats": "Konto kasutuse statistika",
+  "user_usage_stats_description": "Vaata konto kasutuse statistikat",
   "username": "Kasutajanimi",
   "users": "Kasutajad",
   "utilities": "Tööriistad",
@@ -935,11 +1290,15 @@
   "variables": "Muutujad",
   "version": "Versioon",
   "version_announcement_closing": "Sinu sõber, Alex",
+  "version_announcement_message": "Hei! Saadaval on uus Immich'i versioon. Palun võta aega, et lugeda <link>väljalasketeadet</link> ning veendu, et su seadistus on ajakohane, et vältida konfiguratsiooniprobleeme, eriti kui kasutad WatchTower'it või muud mehhanismi, mis Immich'it automaatselt uuendab.",
+  "version_history": "Versiooniajalugu",
+  "version_history_item": "Versioon {version} paigaldatud {date}",
   "video": "Video",
   "video_hover_setting": "Esita hõljutamisel video eelvaade",
   "video_hover_setting_description": "Esita video eelvaade, kui hiirt selle kohal hõljutada. Isegi kui keelatud, saab taasesituse alustada taasesitusnupu kohal hõljutades.",
   "videos": "Videod",
   "videos_count": "{count, plural, one {# video} other {# videot}}",
+  "view": "Vaade",
   "view_album": "Vaata albumit",
   "view_all": "Vaata kõiki",
   "view_all_users": "Vaata kõiki kasutajaid",
diff --git a/web/src/lib/i18n/fa.json b/i18n/fa.json
similarity index 94%
rename from web/src/lib/i18n/fa.json
rename to i18n/fa.json
index 0eb6b7b014..1e40996f15 100644
--- a/web/src/lib/i18n/fa.json
+++ b/i18n/fa.json
@@ -56,16 +56,9 @@
     "image_prefer_embedded_preview_setting_description": "استفاده از پیش‌نمایش داخلی در عکس‌های RAW به عنوان ورودی پردازش تصویر هنگامی که در دسترس باشد. این می‌تواند رنگ‌های دقیق‌تری را برای برخی تصاویر تولید کند، اما کیفیت پیش‌نمایش به دوربین بستگی دارد و ممکن است تصویر آثار فشرده‌سازی بیشتری داشته باشد.",
     "image_prefer_wide_gamut": "ترجیحات گستره رنگی وسیع",
     "image_prefer_wide_gamut_setting_description": "برای تصاویر کوچک از فضای رنگی Display P3 استفاده کنید. این کار باعث حفظ زنده بودن رنگ‌ها در تصاویر با گستره رنگی وسیع می‌شود، اما ممکن است تصاویر در دستگاه‌های قدیمی با نسخه‌های قدیمی مرورگر به شکل متفاوتی نمایش داده شوند. تصاویر با فضای رنگی sRGB به همان حالت sRGB نگه داشته می‌شوند تا از تغییرات رنگی جلوگیری شود.",
-    "image_preview_format": "فرمت نمایش",
-    "image_preview_resolution": "وضوح پیش نمایش",
-    "image_preview_resolution_description": "از این فرمت برای مشاهده یک عکس و همچنین برای یادگیری ماشین استفاده می‌شود. وضوح بالاتر می‌تواند جزئیات بیشتری را حفظ کند، اما زمان بیشتری برای رمزگذاری نیاز دارد، حجم فایل‌ها را بزرگتر می‌کند و ممکن است باعث کاهش پاسخگویی برنامه شود.",
     "image_quality": "کیفیت",
-    "image_quality_description": "کیفیت تصویر از 1 تا 100. هرچه بالاتر باشد، کیفیت بهتر است اما فایل‌های بزرگ‌تری تولید می‌کند. این گزینه بر روی تصاویر پیش‌نمایش و بندانگشتی تأثیر می‌گذارد.",
     "image_settings": "تنظیمات عکس",
     "image_settings_description": "مدیریت کیفیت و وضوح تصاویر تولید شده",
-    "image_thumbnail_format": "قالب تصویر بندانگشتی",
-    "image_thumbnail_resolution": "وضوح تصویر بندانگشتی",
-    "image_thumbnail_resolution_description": "از این فرمت برای مشاهده گروهی عکس‌ها (مانند صفحه اصلی، نمایش آلبوم و غیره) استفاده می‌شود. وضوح بالاتر می‌تواند جزئیات بیشتری را حفظ کند، اما زمان بیشتری برای رمزگذاری نیاز دارد، حجم فایل‌ها را بزرگتر می‌کند و ممکن است باعث کاهش پاسخگویی برنامه شود.",
     "job_concurrency": "همزمانی {job}",
     "job_not_concurrency_safe": "این کار ایمنی همزمانی را تضمین نمی‌کند.",
     "job_settings": "تنظیمات کار",
@@ -74,9 +67,6 @@
     "jobs_delayed": "",
     "jobs_failed": "",
     "library_created": "کتابخانه ایجاد شده: {library}",
-    "library_cron_expression": "عبارت کرون",
-    "library_cron_expression_description": "تنظیم فاصله زمانی اسکن با استفاده از فرمت کرون. برای اطلاعات بیشتر لطفا به مثال‌های <link>Crontab Guru</link> مراجعه کنید",
-    "library_cron_expression_presets": "پیش‌تنظیمات عبارت Cron",
     "library_deleted": "کتابخانه حذف شد",
     "library_import_path_description": "یک پوشه برای وارد کردن مشخص کنید. این پوشه، به همراه زیرپوشه‌ها، برای یافتن تصاویر و ویدیوها اسکن خواهد شد.",
     "library_scanning": "اسکن دوره ای",
@@ -144,7 +134,7 @@
     "note_cannot_be_changed_later": "توجه: این را نمی توان بعداً تغییر داد!",
     "note_unlimited_quota": "توجه: برای سهمیه نامحدود، عدد 0 را وارد کنید",
     "notification_email_from_address": "آدرس فرستنده",
-    "notification_email_from_address_description": "آدرس ایمیل فرستنده، به عنوان مثال:\"Immich سرور عکس <noreply@immich.app>\"",
+    "notification_email_from_address_description": "آدرس ایمیل فرستنده، به عنوان مثال:\"Immich سرور عکس <noreply@example.com>\"",
     "notification_email_host_description": "میزبان سرور ایمیل (مثلاً smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "خطاهای گواهی را نادیده بگیر",
     "notification_email_ignore_certificate_errors_description": "خطاهای اعتبارسنجی گواهی TLS را نادیده بگیر (توصیه نمی‌شود)",
@@ -194,15 +184,12 @@
     "refreshing_all_libraries": "بروز رسانی همه کتابخانه ها",
     "registration": "ثبت نام مدیر",
     "registration_description": "از آنجایی که شما اولین کاربر در سیستم هستید، به عنوان مدیر تعیین شده‌اید و مسئولیت انجام وظایف مدیریتی بر عهده شما خواهد بود و کاربران اضافی توسط شما ایجاد خواهند شد.",
-    "removing_offline_files": "حذف فایل‌های آفلاین",
     "repair_all": "بازسازی همه",
     "repair_matched_items": "",
     "repaired_items": "",
     "require_password_change_on_login": "الزام کاربر به تغییر گذرواژه در اولین ورود",
     "reset_settings_to_default": "بازنشانی تنظیمات به حالت پیش‌فرض",
     "reset_settings_to_recent_saved": "بازنشانی تنظیمات به آخرین تنظیمات ذخیره شده",
-    "scanning_library_for_changed_files": "اسکن کتابخانه برای فایل‌های تغییر یافته",
-    "scanning_library_for_new_files": "اسکن کتابخانه برای یافتن فایل های جدید",
     "send_welcome_email": "ارسال ایمیل خوش آمد گویی",
     "server_external_domain_settings": "دامنه خارجی",
     "server_external_domain_settings_description": "دامنه برای لینک های عمومی به اشتراک گذاشته شده، شامل //:(s)http",
@@ -288,8 +275,6 @@
     "transcoding_threads_description": "مقادیر بالاتر منجر به رمزگذاری سریع تر می شود، اما فضای کمتری برای پردازش سایر وظایف سرور در حین فعالیت باقی می گذارد. این مقدار نباید بیشتر از تعداد هسته های CPU باشد. اگر روی 0 تنظیم شود، بیشترین استفاده را خواهد داشت.",
     "transcoding_tone_mapping": "",
     "transcoding_tone_mapping_description": "تلاش برای حفظ ظاهر ویدیوهای HDR هنگام تبدیل به SDR. هر الگوریتم تعادل های متفاوتی را برای رنگ، جزئیات و روشنایی ایجاد می کند. Hable جزئیات را حفظ می کند، Mobius رنگ را حفظ می کند و Reinhard روشنایی را حفظ می کند.",
-    "transcoding_tone_mapping_npl": "",
-    "transcoding_tone_mapping_npl_description": "رنگ ها برای ظاهر طبیعی در یک نمایشگر با این روشنایی تنظیم خواهند شد. برخلاف انتظار، مقادیر پایین تر باعث افزایش روشنایی ویدیو و برعکس می شوند، زیرا آن را برای روشنایی نمایشگر جبران می کند. مقدار 0 این مقدار را به طور خودکار تنظیم می کند.",
     "transcoding_transcode_policy": "سیاست رمزگذاری",
     "transcoding_transcode_policy_description": "سیاست برای زمانی که ویدیویی باید مجددا تبدیل (رمزگذاری) شود. ویدیوهای HDR همیشه تبدیل (رمزگذاری) مجدد خواهند شد (مگر رمزگذاری مجدد غیرفعال باشد).",
     "transcoding_two_pass_encoding": "تبدیل (رمزگذاری) دو مرحله ای",
@@ -349,10 +334,8 @@
   "archive_or_unarchive_photo": "",
   "archive_size": "",
   "archive_size_description": "",
-  "archived": "",
   "asset_offline": "",
   "assets": "",
-  "assets_moved_to_trash": "",
   "authorized_devices": "",
   "back": "",
   "backward": "",
@@ -367,10 +350,6 @@
   "cancel_search": "",
   "cannot_merge_people": "",
   "cannot_update_the_description": "",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
   "change_date": "",
   "change_expiration_time": "",
   "change_location": "",
@@ -524,8 +503,8 @@
     "unable_to_refresh_user": "",
     "unable_to_remove_album_users": "",
     "unable_to_remove_api_key": "",
+    "unable_to_remove_deleted_assets": "",
     "unable_to_remove_library": "",
-    "unable_to_remove_offline_files": "",
     "unable_to_remove_partner": "",
     "unable_to_remove_reaction": "",
     "unable_to_repair_items": "",
@@ -561,7 +540,6 @@
   "extension": "",
   "external": "",
   "external_libraries": "",
-  "failed_to_get_people": "",
   "favorite": "",
   "favorite_or_unfavorite_photo": "",
   "favorites": "",
@@ -573,14 +551,12 @@
   "filter_people": "",
   "find_them_fast": "",
   "fix_incorrect_match": "",
-  "force_re-scan_library_files": "",
   "forward": "",
   "general": "",
   "get_help": "",
   "getting_started": "",
   "go_back": "",
   "go_to_search": "",
-  "go_to_share_page": "",
   "group_albums_by": "",
   "has_quota": "",
   "hide_gallery": "",
@@ -701,7 +677,6 @@
   "oldest_first": "",
   "online": "",
   "only_favorites": "",
-  "only_refreshes_modified_files": "",
   "open_the_search_filters": "",
   "options": "",
   "organize_your_library": "",
@@ -737,7 +712,6 @@
   "permanent_deletion_warning_setting_description": "",
   "permanently_delete": "",
   "permanently_deleted_asset": "",
-  "permanently_deleted_assets": "",
   "person": "",
   "photos": "",
   "photos_count": "",
@@ -766,10 +740,10 @@
   "refreshed": "",
   "refreshes_every_file": "",
   "remove": "",
+  "remove_deleted_assets": "",
   "remove_from_album": "",
   "remove_from_favorites": "",
   "remove_from_shared_link": "",
-  "remove_offline_files": "",
   "removed_api_key": "",
   "rename": "",
   "repair": "",
@@ -794,8 +768,6 @@
   "saved_settings": "",
   "say_something": "",
   "scan_all_libraries": "",
-  "scan_all_library_files": "",
-  "scan_new_library_files": "",
   "scan_settings": "",
   "scanning_for_album": "",
   "search": "",
@@ -827,7 +799,6 @@
   "selected": "",
   "send_message": "",
   "send_welcome_email": "",
-  "server": "",
   "server_stats": "",
   "set": "",
   "set_as_album_cover": "",
@@ -899,7 +870,6 @@
   "to_trash": "",
   "toggle_settings": "",
   "toggle_theme": "",
-  "toggle_visibility": "",
   "total_usage": "",
   "trash": "",
   "trash_all": "",
@@ -908,7 +878,6 @@
   "trashed_items_will_be_permanently_deleted_after": "",
   "type": "",
   "unarchive": "",
-  "unarchived": "",
   "unfavorite": "",
   "unhide_person": "",
   "unknown": "",
@@ -949,7 +918,6 @@
   "view_links": "",
   "view_next_asset": "",
   "view_previous_asset": "",
-  "viewer": "",
   "waiting": "",
   "week": "",
   "welcome": "",
diff --git a/web/src/lib/i18n/fi.json b/i18n/fi.json
similarity index 64%
rename from web/src/lib/i18n/fi.json
rename to i18n/fi.json
index da9a71379c..aa9f029cd8 100644
--- a/web/src/lib/i18n/fi.json
+++ b/i18n/fi.json
@@ -1,5 +1,5 @@
 {
-  "about": "Tietoja",
+  "about": "Päivitä",
   "account": "Tili",
   "account_settings": "Tilin asetukset",
   "acknowledge": "Tiedostan",
@@ -17,22 +17,29 @@
   "add_import_path": "Lisää tuontipolku",
   "add_location": "Lisää sijainti",
   "add_more_users": "Lisää käyttäjiä",
-  "add_partner": "Lisää kaveri",
+  "add_partner": "Lisää kumppani",
   "add_path": "Lisää polku",
   "add_photos": "Lisää kuvia",
   "add_to": "Lisää...",
   "add_to_album": "Lisää albumiin",
   "add_to_shared_album": "Lisää jaettuun albumiin",
+  "add_url": "Lisää URL",
   "added_to_archive": "Arkistoitu",
   "added_to_favorites": "Lisätty suosikkeihin",
-  "added_to_favorites_count": "{count} lisätty suosikkeihin",
+  "added_to_favorites_count": "{count, number} lisätty suosikkeihin",
   "admin": {
     "add_exclusion_pattern_description": "Lisää mallit, jonka mukaan jätetään tiedostoja pois. Jokerimerkit *, ** ja ? ovat tuettuna. Jättääksesi pois kaikki tiedostot mistä tahansa löytyvästä kansiosta \"Raw\" käytä \"**/Raw/**\". Jättääksesi pois kaikki \". tif\" päätteiset tiedot, käytä \"**/*.tif\". Jättääksesi pois tarkan tiedostopolun, käytä \"/path/to/ignore/**\".",
+    "asset_offline_description": "Ulkoista kirjaston resurssia ei enää löydy levyltä, ja se on siirretty roskakoriin. Jos tiedosto siirrettiin kirjaston sisällä, tarkista aikajanaltasi uusi vastaava resurssi. Palautaaksesi tämän resurssin, varmista, että alla oleva tiedostopolku on Immichin käytettävissä ja skannaa kirjasto uudelleen.",
     "authentication_settings": "Autentikointiasetukset",
     "authentication_settings_description": "Hallitse salasana-, OAuth- ja muut autentikoinnin asetukset",
     "authentication_settings_disable_all": "Haluatko varmasti poistaa kaikki kirjautumistavat käytöstä? Kirjautuminen on tämän jälkeen mahdotonta.",
     "authentication_settings_reenable": "Ottaaksesi uudestaan käyttöön, käytä <link>Palvelin Komentoa</link>.",
     "background_task_job": "Taustatyöt",
+    "backup_database": "Varmuuskopioi Tietokanta",
+    "backup_database_enable_description": "Ota käyttöön tietokannan varmuuskopiointi",
+    "backup_keep_last_amount": "Varmuuskopioiden lukumäärä",
+    "backup_settings": "Varmuuskopioinnin asetukset",
+    "backup_settings_description": "Hallitse tietokannan varmuuskopioiden asetuksia",
     "check_all": "Tarkista kaikki",
     "cleared_jobs": "Työn {job} tehtävät tyhjennetty",
     "config_set_by_file": "Asetukset on tällä hetkellä määritelty tiedostosta",
@@ -41,45 +48,47 @@
     "confirm_email_below": "Kirjota \"{email}\" vahvistaaksesi",
     "confirm_reprocess_all_faces": "Haluatko varmasti käsitellä uudelleen kaikki kasvot? Tämä poistaa myös nimetyt henkilöt.",
     "confirm_user_password_reset": "Haluatko varmasti nollata käyttäjän {user} salasanan?",
-    "crontab_guru": "Crontab Guru",
+    "create_job": "Luo tehtävä",
+    "cron_expression": "Cron-lauseke",
+    "cron_expression_description": "Aseta skannausväli käyttämällä cron-formaattia. Lisätietoja linkistä. <link>Crontab Guru</link>",
+    "cron_expression_presets": "Esiasetetut Cron-lausekkeet",
     "disable_login": "Poista kirjautuminen käytöstä",
-    "disabled": "Ei käytössä",
     "duplicate_detection_job_description": "Tunnista samankaltaiset kuvat käyttäen koneoppimista. Tukeutuu Smart Search:iin",
-    "exclusion_pattern_description": "Poissulkevat määritteet mahdollistavat tiettyjen tiedostojen ja kansioiden jättämisen pois kirjastoasi skannatessa. Tästä on hyötyä jos kansiot sisältävät tiedostoja mitä et halua tuoda, kuten RAW-tiedostot.",
+    "exclusion_pattern_description": "Poissulkemismallit mahdollistavat tiettyjen tiedostojen ja kansioiden jättämisen pois kirjastoasi skannatessa. Tästä on hyötyä jos kansiot sisältävät tiedostoja mitä et halua tuoda, kuten RAW-tiedostot.",
     "external_library_created_at": "Ulkoinen kirjasto (luotu {date})",
     "external_library_management": "Ulkoisen kirjaston hallinta",
-    "face_detection": "Kasvojen haitseminen",
-    "face_detection_description": "Tunnista sisällön kasvoja käyttäen koneoppimista. Videojen osalta vain pikkukuva tunnistetaan. \"Kaikki\" (uudelleen)prosessoi koko sisällön. \"Puuttuvat\" prosessoi sisällön, jota ei vielä ole käyty läpi. Havaitut kasvot ryhmitellään jo tunnistettujen kanssa, tai lisätään uusina henkilöinä.",
-    "facial_recognition_job_description": "Ryhmitä havaitut kasvot henkilöihin. Tämä vaihe suoritetaan kun kasvot on ensin havaittu. \"Kaikki\" ryhmittelee kaikki kasvot. \"Puuttuvat\" vain ne, joille ei ole määritetty henkilöä.",
+    "face_detection": "Kasvojen havaitseminen",
+    "face_detection_description": "Tunnista sisällön kasvoja käyttäen koneoppimista. Videoiden osalta vain pikkukuva tunnistetaan. \"Päivitä\" (uudelleen)prosessoi koko sisällön.\"Nollaa\" lisäksi puhdistaa kaiken kasvo-datan. \"Puuttuvat\" prosessoi sisällön, jota ei vielä ole käyty läpi. Havaitut kasvot ryhmitellään jo tunnistettujen kanssa, tai lisätään uusina henkilöinä.",
+    "facial_recognition_job_description": "Ryhmitä havaitut kasvot henkilöihin. Tämä vaihe suoritetaan, kun kasvot on ensin havaittu. \"Nollaus\" (uudelleen-)ryhmittelee kaikki kasvot. \"Puuttuvat\" vain ne, joille ei ole määritetty henkilöä.",
     "failed_job_command": "Komento {command} epäonnistui työlle {job}",
     "force_delete_user_warning": "VAROITUS: Tämä poistaa käyttäjän ja kaikki mediat. Tätä ei voi perua, eikä tiedostoja voi palauttaa.",
     "forcing_refresh_library_files": "Pakotetaan virkistämään kaikkien kirjastojen tiedostot",
+    "image_format": "Tiedostomuoto",
     "image_format_description": "WebP tuottaa pienempiä tiedostoja kuin JPEG, mutta on hitaampi pakata.",
     "image_prefer_embedded_preview": "Suosi upotettua esikatselua",
     "image_prefer_embedded_preview_setting_description": "Käytä RAW-kuvissa upotettuja esikatselukuvia aina kun mahdollista. Tämä voi joissain kuvissa tuottaa tarkemmat värit, mutta esikatselun laatu on riippuvainen kamerasta ja kuvassa voi olla enemmän pakkauksesta aiheutuvia häiriöitä.",
     "image_prefer_wide_gamut": "Suosi laajaa väriskaalaa",
     "image_prefer_wide_gamut_setting_description": "Käytä Display P3 -nimiavaruutta pikkukuville. Tämä säilöö värien vivahteet paremmin, mutta kuvat saattavat näyttää erilaisilta vanhemmissa laitteissa. sRGB-kuvat pidetään muuttumattomina, jottei värit muuttuisi.",
-    "image_preview_format": "Esikatselun muoto",
-    "image_preview_resolution": "Esikatselun resoluutio",
-    "image_preview_resolution_description": "Käytetään kun katsellaan yksittäisiä kuvia, tai koneoppimiseen. Suurempi resoluutio voi säilyttää paremmin yksityiskohtia. Tosin koodaus kestää kauemmin, tiedostokoko kasvaa, ja se saattaa hidastaa sovelluksen responsiivisuutta.",
+    "image_preview_description": "Keskikokoinen kuva, josta metatiedot on poistettu, käytetään yksittäisen resurssin katseluun ja koneoppimiseen",
+    "image_preview_quality_description": "Esikatselulaatu 1-100. Korkeampi arvo on parempi, mutta tuottaa suurempia tiedostoja ja voi heikentää sovelluksen reagointikykyä. Matalan arvon asettaminen voi vaikuttaa koneoppimisen laatuun.",
+    "image_preview_title": "Esikatselun asetukset",
     "image_quality": "Laatu",
-    "image_quality_description": "Kuvan laatu välillä 1-100. Suurempi arvo on paremman laatuinen, mutta tuottaa kookkaampia tiedostoja. Tämä asetus vaikuttaa esikatselu- ja pikkukuviin.",
+    "image_resolution": "Resoluutio",
+    "image_resolution_description": "Korkeammat resoluutiot voivat säilyttää enemmän yksityiskohtia, mutta niiden koodaus kestää kauemmin, tiedostokoot ovat suurempia ja ne voivat heikentää sovelluksen reagointikykyä.",
     "image_settings": "Kuva-asetukset",
-    "image_settings_description": "Hallitse luotujen kuvien laatua ja resolutiota",
-    "image_thumbnail_format": "Pikkukuvien muoto",
-    "image_thumbnail_resolution": "Pikkukuvien resoluutio",
-    "image_thumbnail_resolution_description": "Käytetään katsottaessa useita kuvia kerralla (aikajana, albuminäkymä, jne.) Korkeampi resoluutio antaa enemmän yksityiskohtia, mutta niiden luonti kestää kauemmin, tiedostokoot ovat isompia ja voivat heikentää sovelluksen responsiivisuutta.",
-    "job_concurrency": "{job} yhtäaikaisuus",
+    "image_settings_description": "Hallitse luotujen kuvien laatua ja resoluutiota",
+    "image_thumbnail_description": "Pieni pikkukuva, josta metatiedot on poistettu, käytetään valokuvaryhmien katseluun, kuten pääaikajanalla",
+    "image_thumbnail_quality_description": "Pikkukuvan laatu 1-100. Korkeampi arvo on parempi, mutta tuottaa suurempia tiedostoja ja voi heikentää sovelluksen reagointikykyä.",
+    "image_thumbnail_title": "Pikkukuva-asetukset",
+    "job_concurrency": "Tehtävän \"{job}\" samanaikaisuus",
+    "job_created": "Tehtävä luotu",
     "job_not_concurrency_safe": "Tätä tehtävää ei ole turvallista ajaa yhtäaikaisesti.",
     "job_settings": "Tehtävän asetukset",
     "job_settings_description": "Hallitse tehtävän samanaikaisuusasetuksia",
     "job_status": "Tehtävän tila",
-    "jobs_delayed": "{jobCount} tehtävää viivästetty",
-    "jobs_failed": "{jobCount} epäonnistui",
+    "jobs_delayed": "{jobCount, plural, other {# viivästynyttä}}",
+    "jobs_failed": "{jobCount, plural, other {# epäonnistunutta}}",
     "library_created": "Kirjasto {library} luotu",
-    "library_cron_expression": "Cron-lauseke",
-    "library_cron_expression_description": "Anna skannaustiheys cron-formaatissa. Saadaksesi lisätietoja katso esimerkiksi <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Cron-lausekkeen esiasetukset",
     "library_deleted": "Kirjasto poistettu",
     "library_import_path_description": "Määritä kansio joka tuodaan. Kuvat ja videot skannataan tästä kansiosta, sekä alikansioista.",
     "library_scanning": "Ajoittainen skannaus",
@@ -99,7 +108,7 @@
     "machine_learning_duplicate_detection": "Kaksoiskappaleiden tunnistus",
     "machine_learning_duplicate_detection_enabled": "Ota käyttöön kaksoiskappaleiden tunnistus",
     "machine_learning_duplicate_detection_enabled_description": "Jos ei käytössä, täsmälleen samojen aineistojen kaksoiskappaleet tullaan silti poistamaan.",
-    "machine_learning_duplicate_detection_setting_description": "Etsi todennäköisiä kaksoiskappaleita CLIP upotuksien avulla",
+    "machine_learning_duplicate_detection_setting_description": "Etsi todennäköisiä kaksoiskappaleita CLIP-upotuksien avulla",
     "machine_learning_enabled": "Ota käyttöön koneoppiminen",
     "machine_learning_enabled_description": "Jos poistettu käytöstä, kaikki koneoppimistoiminnot ovat pois käytöstä riippumatta alla olevista asetuksista.",
     "machine_learning_facial_recognition": "Kasvojen tunnistus",
@@ -119,7 +128,7 @@
     "machine_learning_settings": "Koneoppimisen asetukset",
     "machine_learning_settings_description": "Koneoppimisen ominaisuudet ja asetukset",
     "machine_learning_smart_search": "Älykäs etsintä",
-    "machine_learning_smart_search_description": "Etsi kuvia merkityksellisemmin käyttäen CLIP upotuksia",
+    "machine_learning_smart_search_description": "Etsi kuvia merkityksellisemmin käyttäen CLIP-upotuksia",
     "machine_learning_smart_search_enabled": "Ota käyttöön älykäs haku",
     "machine_learning_smart_search_enabled_description": "Jos ei käytössä, kuvia ei koodata älykkäälle etsinnälle.",
     "machine_learning_url_description": "Koneoppimispalvelimen URL",
@@ -127,19 +136,23 @@
     "manage_log_settings": "Hallitse lokien asetuksia",
     "map_dark_style": "Tumma teema",
     "map_enable_description": "Ota käyttöön karttatoiminnot",
-    "map_gps_settings": "Kartta & GPS- asetukset",
-    "map_gps_settings_description": "Hallitse Kartan & GPS (Käänteinen Geokoodaus) Asetuksia",
-    "map_implications": "Kartta -ominaisuus käyttää ulkoista karttapalvelua",
+    "map_gps_settings": "Kartta- ja GPS-asetukset",
+    "map_gps_settings_description": "Hallitse kartan ja GPS:n (käänteisen geokoodauksen) asetuksia",
+    "map_implications": "Karttaominaisuus käyttää ulkoista karttapalvelua (tiles.immich.cloud)",
     "map_light_style": "Vaalea teema",
     "map_manage_reverse_geocoding_settings": "Hallitse <link>käänteisen geokoodauksen</link> asetuksia",
     "map_reverse_geocoding": "Käänteinen Geokoodaus",
     "map_reverse_geocoding_enable_description": "Ota käyttöön osoitteiden poiminta karttakoordinaateista",
-    "map_reverse_geocoding_settings": "Käänteisen Geokoodauksen asetukset",
-    "map_settings": "Kartta-asetukset",
+    "map_reverse_geocoding_settings": "Käänteisen geokoodauksen asetukset",
+    "map_settings": "Kartta",
     "map_settings_description": "Hallitse kartan asetuksia",
-    "map_style_description": "style.json -karttateeman URL",
+    "map_style_description": "style.json-karttateeman URL",
     "metadata_extraction_job": "Kerää metadata",
-    "metadata_extraction_job_description": "Poimi metatiedot aineistoista, kuten GPS ja resoluutio",
+    "metadata_extraction_job_description": "Poimi metatiedot aineistoista, kuten GPS, kasvot ja resoluutio",
+    "metadata_faces_import_setting": "Ota käyttöön kasvojen tuonti",
+    "metadata_faces_import_setting_description": "Tuo kasvot kuvan EXIF- ja kylkiäistiedostoista",
+    "metadata_settings": "Metatietoasetukset",
+    "metadata_settings_description": "Hallitse metatietoja",
     "migration_job": "Migrointi",
     "migration_job_description": "Migroi aineiston pikkukuvat ja kasvot uusimpaan kansiorakenteeseen",
     "no_paths_added": "Polkuja ei asetettu",
@@ -148,10 +161,10 @@
     "note_cannot_be_changed_later": "Huom: Tätä ei voi enää myöhemmin vaihtaa!",
     "note_unlimited_quota": "Huom: Määritä 0 rajoittamattomaksi kiintiöksi",
     "notification_email_from_address": "Lähettäjän osoite",
-    "notification_email_from_address_description": "Lähettäjän sähköpostiosoite. Esimerkiksi \"Immich Kuvapalvelin <noreply@immich.app>\"",
+    "notification_email_from_address_description": "Lähettäjän sähköpostiosoite. Esimerkiksi \"Immich-kuvapalvelin <noreply@example.com>\"",
     "notification_email_host_description": "Sähköpostipalvelin (esim. smtp.immich.app)",
-    "notification_email_ignore_certificate_errors": "Älä huomioi sertifikaattivirheitä",
-    "notification_email_ignore_certificate_errors_description": "Älä huomioi TLS sertifikaattien validointivirheitä (ei suositeltu)",
+    "notification_email_ignore_certificate_errors": "Älä huomioi varmennevirheitä",
+    "notification_email_ignore_certificate_errors_description": "Älä huomioi TLS-varmenteiden validointivirheitä (ei suositeltu)",
     "notification_email_password_description": "Sähköpostipalvelimen salasana",
     "notification_email_port_description": "Sähköpostipalvelimen portti (esim. 25, 465, tai 587)",
     "notification_email_sent_test_email_button": "Lähetä testaussähköposti ja tallenna",
@@ -164,22 +177,22 @@
     "notification_settings": "Ilmoitusasetukset",
     "notification_settings_description": "Hallitse ilmoitusasetuksia, myös sähköpostin",
     "oauth_auto_launch": "Automaattinen käynnistys",
-    "oauth_auto_launch_description": "Aloita OAuth kirjautuminen heti kun saavutaan kirjautumissivulle",
+    "oauth_auto_launch_description": "Aloita OAuth-kirjautumisvuo heti kun saavutaan kirjautumissivulle",
     "oauth_auto_register": "Automaattinen rekisteröinti",
     "oauth_auto_register_description": "Rekisteröi uudet OAuth:lla kirjautuvat käyttäjät automaattisesti",
     "oauth_button_text": "Painikkeen teksti",
     "oauth_client_id": "Client ID",
     "oauth_client_secret": "Client Secret",
-    "oauth_enable_description": "Kirjaudu käyttäen OAuth:ia",
+    "oauth_enable_description": "Kirjaudu käyttäen OAuthia",
     "oauth_issuer_url": "Toimitsijan URL",
     "oauth_mobile_redirect_uri": "Mobiilin uudellenohjaus-URI",
     "oauth_mobile_redirect_uri_override": "Ohita mobiilin uudelleenohjaus-URI",
-    "oauth_mobile_redirect_uri_override_description": "Ota käyttöön kun 'app.immich:/' -ohjausta ei tueta.",
+    "oauth_mobile_redirect_uri_override_description": "Ota käyttöön kun OAuth tarjoaja ei salli mobiili URI:a, kuten '{callback}'",
     "oauth_profile_signing_algorithm": "Profiilin allekirjoitusalgoritmi",
-    "oauth_profile_signing_algorithm_description": "Algoritmi, jota käytetään käyttäjäprofiilin allekirjoituksessa",
+    "oauth_profile_signing_algorithm_description": "Algoritmi, jota käytetään käyttäjäprofiilin allekirjoittamiseen.",
     "oauth_scope": "Skooppi (Scope)",
     "oauth_settings": "OAuth",
-    "oauth_settings_description": "Hallitse OAuth kirjautumisen asetuksia",
+    "oauth_settings_description": "Hallitse OAuth-kirjautumisen asetuksia",
     "oauth_settings_more_details": "Saadaksesi lisätietoja tästä toiminnosta, katso <link>dokumentaatio</link>.",
     "oauth_signing_algorithm": "Allekirjoitusalgoritmi",
     "oauth_storage_label_claim": "Tallennustilan nimikkeen valtuutusväittämä (claim)",
@@ -194,22 +207,24 @@
     "password_settings": "Kirjaudu salasanalla",
     "password_settings_description": "Hallitse salasanakirjautumisen asetuksia",
     "paths_validated_successfully": "Kaikki polut validoitu",
+    "person_cleanup_job": "Henkilöpuhdistus",
     "quota_size_gib": "Kiintiön koko (Gt)",
     "refreshing_all_libraries": "Virkistetään kaikki kirjastot",
     "registration": "Pääkäyttäjän rekisteröinti",
     "registration_description": "Pääkäyttäjänä olet vastuussa järjestelmän hallinnallisista tehtävistä ja uusien käyttäjien luomisesta.",
-    "removing_offline_files": "Poistetaan Offline-tiedostot",
     "repair_all": "Korjaa kaikki",
     "repair_matched_items": "Löytyi {count, plural, one {# osuma} other {# osumaa}}",
     "repaired_items": "Korjattiin {count, plural, one {# kohta} other {# kohtaa}}",
     "require_password_change_on_login": "Vaadi käyttäjää vaihtamaan salasana ensimmäisellä kirjautumiskerralla",
     "reset_settings_to_default": "Nollaa asetukset oletuksille",
     "reset_settings_to_recent_saved": "Palauta aiemmin tallennetut asetukset",
-    "scanning_library_for_changed_files": "Etsitään kirjaston muuttuneita tiedostoja",
-    "scanning_library_for_new_files": "Etsitään uusia tiedostoja",
+    "scanning_library": "Kirjastoa skannataan",
+    "search_jobs": "Etsi tehtäviä...",
     "send_welcome_email": "Lähetä tervetuloviesti",
     "server_external_domain_settings": "Ulkoinen osoite",
     "server_external_domain_settings_description": "Osoite julkisille linkeille, http(s):// mukaan lukien",
+    "server_public_users": "Julkiset käyttäjät",
+    "server_public_users_description": "Kaikki käyttäjät (nimi ja sähköpostiosoite) luetellaan, kun käyttäjä lisätään jaettuihin albumeihin. Kun toiminto on poistettu käytöstä, käyttäjäluettelo on vain pääkäyttäjien käytettävissä.",
     "server_settings": "Palvelimen asetukset",
     "server_settings_description": "Ylläpidä palvelimen asetuksia",
     "server_welcome_message": "Tervetuloviesti",
@@ -222,9 +237,9 @@
     "storage_template_date_time_sample": "Esimerkki päivämäärä {date}",
     "storage_template_enable_description": "Ota käyttöön tallennustilan mallit",
     "storage_template_hash_verification_enabled": "Tarkistussumman varmennus käytössä",
-    "storage_template_hash_verification_enabled_description": "Ottaa käyttöön varmistussummien laskennan. Älä poista käytöstä jollet ole aivan varma seurauksista",
+    "storage_template_hash_verification_enabled_description": "Ottaa käyttöön tarkistussummien laskennan. Älä poista käytöstä, ellet ole aivan varma seurauksista",
     "storage_template_migration": "Tallennustilan mallien migraatio",
-    "storage_template_migration_description": "Käytä nykyistä <link>{template}:a</link> aikaisemmin lähetettyihin kohteisiin",
+    "storage_template_migration_description": "Käytä nykyistä <link>{template}a</link> aikaisemmin lähetettyihin kohteisiin",
     "storage_template_migration_info": "Malli vaikuttaa vain uusiin kohteisiin. Käyttääksesi mallia jo olemassa oleviin kohteisiin, aja <link>{job}</link>.",
     "storage_template_migration_job": "Tallennustilan mallin muutostyö",
     "storage_template_more_details": "Saadaksesi lisätietoa tästä ominaisuudesta, katso <template-link>Tallennustilan Mallit</template-link> sekä <implications-link>mihin se vaikuttaa</implications-link>",
@@ -234,14 +249,15 @@
     "storage_template_settings_description": "Hallitse palvelimelle ladatun aineiston kansiorakennetta ja tiedostonimiä",
     "storage_template_user_label": "<code>{label}</code> on käyttäjän Tallennustilan Tunniste",
     "system_settings": "Järjestelmäasetukset",
+    "tag_cleanup_job": "Merkintäpuhdistus",
+    "template_email_preview": "Esikatselu",
     "theme_custom_css_settings": "Mukautettu CSS",
-    "theme_custom_css_settings_description": "Kustomoi Immichin ulkoasua Cascading Style Sheets:llä.",
+    "theme_custom_css_settings_description": "Mukauta Immichin ulkoasua CSS:llä.",
     "theme_settings": "Teeman asetukset",
     "theme_settings_description": "Kustomoi Immichin web-käyttöliittymää",
     "these_files_matched_by_checksum": "Näillä tiedostoilla on yhteinen tarkistussumma",
     "thumbnail_generation_job": "Generoi pikkukuvat",
     "thumbnail_generation_job_description": "Generoi isot, pienet sekä sumeat pikkukuvat jokaisesta aineistosta, kuten myös henkilöistä",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "Kiihdytysrajapinta",
     "transcoding_acceleration_api_description": "Rajapinta, jolla keskustellaan laittesi kanssa nopeuttaaksemme koodausta. Tämä asetus on paras mahdollinen: Mikäli ongelmia ilmenee, palataan käyttämään ohjelmistopohjaista koodausta. VP9 voi toimia tai ei, riippuen laitteistosi kokoonpanosta.",
     "transcoding_acceleration_nvenc": "NVENC (vaatii NVIDIA:n grafiikkasuorittimen)",
@@ -258,16 +274,16 @@
     "transcoding_audio_codec": "Äänikoodekki",
     "transcoding_audio_codec_description": "Opus on paras laadultaan, mutta ei välttämättä ole yhteensopiva vanhempien laitteiden tai sovellusten kanssa.",
     "transcoding_bitrate_description": "Videot, jotka ylittävät enimmäisbittinopeuden tai eivät ole hyväksytyssä muodossa",
-    "transcoding_codecs_learn_more": "Oppiaksesi lisää tässä käytetystä terminologiasta, tutustu FFmpeg- dokumentaatioon <h264-link>H.264 koodaaja</h264-link>, <hevc-link>HEVC koodaaja</hevc-link> sekä <vp9-link>VP9 koodaaja</vp9-link>.",
+    "transcoding_codecs_learn_more": "Oppiaksesi lisää käytetystä terminologiasta, tutustu FFmpeg-dokumentaatioon: <h264-link>H.264-koodaaja</h264-link>, <hevc-link>HEVC-koodaaja</hevc-link> ja <vp9-link>VP9-koodaaja</vp9-link>.",
     "transcoding_constant_quality_mode": "Tasaisen laadun tyyppi",
     "transcoding_constant_quality_mode_description": "ICQ on parempi kuin CQP, mutta jotkut laitteistokiihdyttimet eivät tue sitä. Tätä asetusta käytetään oletuksena laatuun pohjautuvissa muunnoksissa, paitsi NVENC mikä ei tue ICQ:ta.",
-    "transcoding_constant_rate_factor": "",
+    "transcoding_constant_rate_factor": "Vakionopeustekijä",
     "transcoding_constant_rate_factor_description": "Videon laatu. Yleisimmät arvot ovat 23 H.264:lle, 28 HEVC:lle, 31 VP9:lle ja 35 AV1:lle. Matalampi arvo on parempi, mutta tekee isompia tiedostoja.",
     "transcoding_disabled_description": "Älä muunna videoita. Voi joissakin päätelaitteissa aiheuttaa videotoiston toimimattomuutta",
     "transcoding_hardware_acceleration": "Laitteistokiihdytys",
     "transcoding_hardware_acceleration_description": "Kokeellinen. Paljon nopeampi, mutta huonompaa laatua samalla bittinopeudella",
     "transcoding_hardware_decoding": "Laitteiston dekoodaus",
-    "transcoding_hardware_decoding_setting_description": "Vaikuttaa vain NVENC ja RKMPP -moottoreihin. Ottaa käyttöön end-to-end kiihdytyksen pelkän muuntamisen sijasta. Ei välttämättä toimi kaikissa videoissa.",
+    "transcoding_hardware_decoding_setting_description": "Ottaa käyttöön end-to-end kiihdytyksen pelkän muuntamisen sijasta. Ei välttämättä toimi kaikissa videoissa.",
     "transcoding_hevc_codec": "HEVC koodekki",
     "transcoding_max_b_frames": "B-kehysten enimmäismäärä",
     "transcoding_max_b_frames_description": "Korkeampi arvo parantaa pakkausta, mutta hidastaa enkoodausta. Ei välttämättä ole yhteensopiva vanhempien laitteiden kanssa. 0 poistaa B-kehykset käytöstä, -1 määrittää arvon automaattisesti.",
@@ -279,7 +295,7 @@
     "transcoding_preferred_hardware_device": "Ensisijainen laite",
     "transcoding_preferred_hardware_device_description": "On voimassa vain VAAPI ja QSV -määritteille. Asettaa laitteistokoodauksessa käytetyn DRI noodin.",
     "transcoding_preset_preset": "Esiasetus (-asetus)",
-    "transcoding_preset_preset_description": "Pakkausnopeus. Hitaampi tuottaa pienempiä tiedostoja ja parantaa laatua, kun kohdistetaan tiettyyn bittinopeuteen. VP9 ei huomioi korkeampaa kuin `faster`.",
+    "transcoding_preset_preset_description": "Pakkausnopeus. Hitaampi tuottaa pienempiä tiedostoja ja parantaa laatua, kun kohdistetaan tiettyyn bittinopeuteen. VP9 ei huomioi korkeampaa kuin 'faster'.",
     "transcoding_reference_frames": "Kehysviitteet",
     "transcoding_reference_frames_description": "Viittaavien kehysten määrä kun tiettyä kehystä pakataan. Korkeampi arvo parantaa pakkausta mutta hidastaa enkoodausta. 0 määrittää arvon automaattisesti.",
     "transcoding_required_description": "Vain videoille, jotka eivät ole hyväksytyssä muodossa",
@@ -293,30 +309,35 @@
     "transcoding_threads_description": "Korkeampi arvo nopeuttaa enkoodausta, mutta vie tilaa palvelimen muilta tehtäviltä. Tämä arvo ei tulisi olla suurempi mitä suorittimen ytimien määrä. Suurin mahdollinen käyttö, mikäli arvo on 0.",
     "transcoding_tone_mapping": "Sävykartoitus",
     "transcoding_tone_mapping_description": "Pyrkii säilömään HDR-kuvien ulkonäön, kun muunnetaan peruskuvaksi. Jokaisella algoritmilla on omat heikkoutensa värien, yksityiskohtien tai kirkkauksien kesken. Hable säilöö yksityiskohdat, Mobius värit ja Reinhard kirkkaudet.",
-    "transcoding_tone_mapping_npl": "Sävykartoitus (NPL)",
-    "transcoding_tone_mapping_npl_description": "Värejä säädetään niin, että ne näyttävät luonnollisilta tällä kirkkaudella. Päinvastoin kuin luulisi, alempi arvo nostaa kirkkautta ja päinvastoin, koska se kompensoi näytön kirkkautta. 0 määrittää tason automaattisesti.",
     "transcoding_transcode_policy": "Transkoodauskäytäntö",
     "transcoding_transcode_policy_description": "Käytäntö miten video tulisi transkoodata. HDR videot transkoodataan aina, paitsi jos transkoodaus on poistettu käytöstä.",
     "transcoding_two_pass_encoding": "Two-pass enkoodaus",
-    "transcoding_two_pass_encoding_setting_description": "",
+    "transcoding_two_pass_encoding_setting_description": "Transkoodaa kahdessa vaiheessa tuottaaksesi paremmin koodattuja videoita. Kun maksimibittinopeus on käytössä (vaaditaan H.264- ja HEVC-koodaukselle), tämä tila käyttää bittinopeusaluetta, joka perustuu maksimibittinopeuteen ja ohittaa CRF. VP9 osalta CRF:ää voidaan käyttää, jos maksimibittinopeus on poistettu käytöstä.",
     "transcoding_video_codec": "Videokoodekki",
     "transcoding_video_codec_description": "VP9 on tehokkain ja web-yhteensopiva, mutta muuntaminen kestää kauemmin. HEVC suoriutuu yhtäläisesti, mutta ei ole ihan yhtä yhteensopiva. H.264 on hyvin yhteensopiva ja nopea muuntaa, mutta tuottaa paljon suurempia tiedostoja. AV1 on kaikkein tehokkain koodekki, mutta vanhemmat laitteet eivät sitä tue.",
     "trash_enabled_description": "Ota käyttöön roskakori",
     "trash_number_of_days": "Päivien lukumäärä",
-    "trash_number_of_days_description": "Montako päivää aineistoja pidetään roskakorissa ennen pysyvää poistamista",
+    "trash_number_of_days_description": "Kuinka monta päivää aineistoja pidetään roskakorissa ennen pysyvää poistamista",
     "trash_settings": "Roskakorin asetukset",
     "trash_settings_description": "Hallitse roskakoriasetuksia",
     "untracked_files": "Tiedostot joita ei seurata",
     "untracked_files_description": "Nämä tiedostot eivät ole ohjelman hallitsemia. Ne voivat olla virheellisten siirtojen tai keskeytyneiden latausten tulosta, tai bugista johtuvia jälkeen jääneitä",
+    "user_cleanup_job": "Käyttäjien puhdistus",
+    "user_delete_delay": "Käyttäjän <b>{user}</b> tili ja aineistot aikataulutetaan poistettavaksi ajan kuluttua: {delay, plural, one {# day} other {# days}}.",
     "user_delete_delay_settings": "Poiston viive",
-    "user_delete_delay_settings_description": "Montako päivää poistamisen jälkeen käyttäjä ja hänen aineistonsa poistetaan pysyvästi. Joka keskiyö käydään läpi poistetuiksi merkityt käyttäjät. Tämä muutos astuu voimaan seuraavalla ajokerralla.",
+    "user_delete_delay_settings_description": "Kuinka monta päivää poistamisen jälkeen käyttäjä ja hänen aineistonsa poistetaan pysyvästi. Joka keskiyö käydään läpi poistettavaksi merkityt käyttäjät. Tämä muutos astuu voimaan seuraavalla ajokerralla.",
+    "user_delete_immediately": "<b>{user}</b>:n tili ja sen kohteet on ajastettu poistettavaksi <b>heti</b>.",
+    "user_delete_immediately_checkbox": "Aseta tili ja sen kohteet jonoon välitöntä poistoa varten",
     "user_management": "Käyttäjien hallinta",
     "user_password_has_been_reset": "Käyttäjän salasana on nollattu:",
     "user_password_reset_description": "Anna väliaikainen salasana ja ohjeista käyttäjää vaihtamaan se seuraavan kirjautumisen yhteydessä.",
+    "user_restore_description": "<b>{user}</b>:n tili palautetaan.",
+    "user_restore_scheduled_removal": "Palauta käyttäjä - Aikataulutettu poisto tapahtuu {date, date, long}",
     "user_settings": "Käyttäjäasetukset",
     "user_settings_description": "Hallitse käyttäjäasetuksia",
     "user_successfully_removed": "Käyttäjä {email} on poistettu.",
-    "version_check_enabled_description": "Ota käyttöön säännölliset uusien versioiden tarkistukset GitHubista",
+    "version_check_enabled_description": "Ota käyttöön versiotarkastus",
+    "version_check_implications": "Versiotarkistus vaatii säännöllisen yhteyden github.comiin",
     "version_check_settings": "Versiotarkistus",
     "version_check_settings_description": "Ota käyttöön ilmoitukset, kun uusi versio on saatavilla",
     "video_conversion_job": "Transkoodaa videot",
@@ -332,17 +353,21 @@
   "album_added": "Albumi lisätty",
   "album_added_notification_setting_description": "Saa sähköpostia kun sinut lisätään jaettuun albumiin",
   "album_cover_updated": "Albumin kansikuva päivitetty",
-  "album_delete_confirmation": "Haluatko varmasti poistaa albumin {album}?\nJos albumi on jaettu, muut eivät pääse siihen enää.",
+  "album_delete_confirmation": "Haluatko varmasti poistaa albumin {album}?",
+  "album_delete_confirmation_description": "Jos albumi on jaettu, muut eivät pääse siihen enää.",
   "album_info_updated": "Albumin tiedot päivitetty",
   "album_leave": "Poistu albumista?",
+  "album_leave_confirmation": "Haluatko varmasti poistua albumista {album}?",
   "album_name": "Albumin nimi",
   "album_options": "Albumin asetukset",
   "album_remove_user": "Poista käyttäjä?",
-  "album_remove_user_confirmation": "Oletko varma että haluat poistaa  {user}?",
+  "album_remove_user_confirmation": "Oletko varma että haluat poistaa {user}?",
   "album_share_no_users": "Näyttää että olet jakanut tämän albumin kaikkien kanssa, tai sinulla ei ole käyttäjiä joille jakaa.",
   "album_updated": "Albumi päivitetty",
   "album_updated_setting_description": "Saa sähköpostia kun jaetussa albumissa on uutta sisältöä",
+  "album_user_left": "Poistuttiin albumista {album}",
   "album_user_removed": "{user} poistettu",
+  "album_with_link_access": "Anna kenen tahansa nähdä linkin kautta tämän albumin valokuvat ja henkilöt.",
   "albums": "Albumit",
   "albums_count": "{count, plural, one {{count, number} albumi} other {{count, number} albumia}}",
   "all": "Kaikki",
@@ -351,7 +376,12 @@
   "all_videos": "Kaikki videot",
   "allow_dark_mode": "Salli tumma tila",
   "allow_edits": "Salli muutokset",
+  "allow_public_user_to_download": "Salli julkisten käyttäjien ladata tiedostoja",
+  "allow_public_user_to_upload": "Salli julkisten käyttäjien lähettää tiedostoja",
+  "anti_clockwise": "Vastapäivään",
   "api_key": "API-avain",
+  "api_key_description": "Tämä arvo näytetään vain kerran. Varmista, että olet kopioinut sen ennen kuin suljet ikkunan.",
+  "api_key_empty": "API-avaimesi ei pitäisi olla tyhjä",
   "api_keys": "API-avaimet",
   "app_settings": "Sovellusasetukset",
   "appears_in": "Esiintyy albumeissa",
@@ -359,41 +389,47 @@
   "archive_or_unarchive_photo": "Arkistoi kuva tai palauta arkistosta",
   "archive_size": "Arkiston koko",
   "archive_size_description": "Määritä arkiston koko latauksissa (Gt)",
-  "archived": "Arkistoitu",
   "archived_count": "{count, plural, other {Arkistoitu #}}",
   "are_these_the_same_person": "Ovatko he sama henkilö?",
   "are_you_sure_to_do_this": "Haluatko varmasti tehdä tämän?",
   "asset_added_to_album": "Lisätty albumiin",
   "asset_adding_to_album": "Lisätään albumiin...",
+  "asset_description_updated": "Kohteen kuvaus on päivitetty",
+  "asset_filename_is_offline": "Kohde {filename} on offline-tilassa",
+  "asset_has_unassigned_faces": "Kohteella on määrittämättömiä kasvoja",
+  "asset_hashing": "Hajautetaan...",
   "asset_offline": "Aineisto offline-tilassa",
+  "asset_offline_description": "Tätä ulkoista resurssia ei enää löydy levyltä. Ole hyvä ja ota yhteyttä Immich-järjestelmänvalvojaan saadaksesi apua.",
   "asset_skipped": "Ohitettu",
+  "asset_skipped_in_trash": "Roskakorissa",
   "asset_uploaded": "Lähetetty",
   "asset_uploading": "Lähetetään…",
   "assets": "kohdetta",
   "assets_added_count": "Lisätty {count, plural, one {# kohde} other {# kohdetta}}",
   "assets_added_to_album_count": "Albumiin lisätty {count, plural, one {# kohde} other {# kohdetta}}",
-  "assets_added_to_name_count": "{name}:n lisätty {count, plural, one {# media} other {# mediaa}}",
+  "assets_added_to_name_count": "Lisätty {count, plural, one {# kohde} other {# kohdetta}} {hasName, select, true {<b>{name}</b>} other {uuteen albumiin}}",
   "assets_count": "{count, plural, one {# media} other {# mediaa}}",
-  "assets_moved_to_trash": "Siirretty {count, plural, one {# aineisto} other {# aineistoa}} roskakoriin",
   "assets_moved_to_trash_count": "Siirretty {count, plural, one {# media} other {# mediaa}} roskakoriin",
   "assets_permanently_deleted_count": "{count, plural, one {# media} other {# mediaa}} poistettu pysyvästi",
   "assets_removed_count": "{count, plural, one {# media} other {# mediaa}} poistettu",
-  "assets_restore_confirmation": "Haluatko varmasti palauttaa kaikki roskakoriin siirretyt mediat? Et voi perua tätä toimintoa!",
+  "assets_restore_confirmation": "Haluatko varmasti palauttaa kaikki roskakoriisi siirretyt resurssit? Tätä toimintoa ei voi peruuttaa! Huomaa, että offline-resursseja ei voida palauttaa tällä tavalla.",
   "assets_restored_count": "{count, plural, one {# media} other {# mediaa}} palautettu",
   "assets_trashed_count": "{count, plural, one {# media} other {# mediaa}} siirretty roskakoriin",
   "assets_were_part_of_album_count": "{count, plural, one {Media oli} other {Mediat olivat}} jo albumissa",
-  "authorized_devices": "Auktorisoidut laitteet",
+  "authorized_devices": "Valtuutetut laitteet",
   "back": "Takaisin",
   "back_close_deselect": "Palaa, sulje tai poista valinnat",
   "backward": "Taaksepäin",
   "birthdate_saved": "Syntymäaika tallennettu",
   "birthdate_set_description": "Syntymäaikaa käytetään laskemaan henkilön ikä kuvanottohetkellä.",
   "blurred_background": "Sumennettu tausta",
-  "build": "Rakenna",
-  "build_image": "Rakenna kuva",
+  "bugs_and_feature_requests": "Bugit ja ominaisuuspyynnöt",
+  "build": "Koontiversio",
+  "build_image": "Koontiversion kuva",
   "bulk_delete_duplicates_confirmation": "Haluatko varmasti poistaa {count, plural, one {# kaksoiskappaleen} other {# kaksoiskappaleet}} kerralla? Tämä säilyttää kustakin mediasta kookkaimman ja poistaa loput pysyvästi. Et voi perua tätä!",
   "bulk_keep_duplicates_confirmation": "Haluatko varmasti säilyttää {count, plural, one {# kaksoiskappaleen} other {# kaksoiskappaleet}}? Tämä merkitsee kaikki kaksoiskappaleet ratkaistuiksi, eikä poista mitään.",
   "bulk_trash_duplicates_confirmation": "Haluatko varmasti siirtää {count, plural, one {# kaksoiskappaleen} other {# kaksoiskappaleet}} roskakoriin? Tämä säilyttää kustakin mediasta kookkaimman ja siirtää loput roskakoriin.",
+  "buy": "Osta lisenssi Immich:iin",
   "camera": "Kamera",
   "camera_brand": "Kameran merkki",
   "camera_model": "Kameran malli",
@@ -402,16 +438,12 @@
   "cannot_merge_people": "Ihmisiä ei voitu yhdistää",
   "cannot_undo_this_action": "Et voi perua tätä toimintoa!",
   "cannot_update_the_description": "Kuvausta ei voi päivittää",
-  "cant_apply_changes": "Asetuksia ei voitu määrittää",
-  "cant_get_faces": "Kasvoja ei voinut hakea",
-  "cant_search_people": "Ihmisiä ei voinut etsiä",
-  "cant_search_places": "Sijainteja ei voinut etsiä",
   "change_date": "Vaihda päiväys",
   "change_expiration_time": "Muuta erääntymisaikaa",
   "change_location": "Vaihda sijainti",
   "change_name": "Vaihda nimi",
   "change_name_successfully": "Nimi vaihdettu",
-  "change_password": "Vaihda salasana",
+  "change_password": "Vaihda Salasana",
   "change_password_description": "Tämä on joko ensimmäinen kertasi kun kirjaudut järjestelmään, tai salasanasi on pyydetty vaihtamaan. Määritä uusi salasana alle.",
   "change_your_password": "Vaihda salasanasi",
   "changed_visibility_successfully": "Näkyvyys vaihdettu",
@@ -421,11 +453,14 @@
   "city": "Kaupunki",
   "clear": "Tyhjennä",
   "clear_all": "Tyhjennä kaikki",
+  "clear_all_recent_searches": "Tyhjennä viimeisimmät haut",
   "clear_message": "Tyhjennä viesti",
   "clear_value": "Tyhjää arvo",
+  "clockwise": "Myötäpäivään",
   "close": "Sulje",
   "collapse": "Supista",
   "collapse_all": "Sulje kaikki",
+  "color": "Väri",
   "color_theme": "Väriteema",
   "comment_deleted": "Kommentti poistettu",
   "comment_options": "Kommentin valinnat",
@@ -434,6 +469,7 @@
   "confirm": "Vahvista",
   "confirm_admin_password": "Vahvista ylläpitäjän salasana",
   "confirm_delete_shared_link": "Haluatko varmasti poistaa tämän jaetun linkin?",
+  "confirm_keep_this_delete_others": "Kuvapinon muut kuvat tätä lukuunottamatta poistetaan. Oletko varma, että haluat jatkaa?",
   "confirm_password": "Vahvista salasana",
   "contain": "Mahduta",
   "context": "Konteksti",
@@ -459,13 +495,15 @@
   "create_new_person": "Luo uusi henkilö",
   "create_new_person_hint": "Määritä valitut mediat uudelle henkilölle",
   "create_new_user": "Luo uusi käyttäjä",
+  "create_tag": "Luo tunniste",
+  "create_tag_description": "Luo uusi tunniste. Sisäkkäisiä tunnisteita varten syötä tunnisteen täydellinen polku kauttaviivat mukaan luettuna.",
   "create_user": "Luo käyttäjä",
   "created": "Luotu",
   "current_device": "Nykyinen laite",
   "custom_locale": "Muokatut maa-asetukset",
   "custom_locale_description": "Muotoile päivämäärät ja numerot perustuen alueen kieleen",
   "dark": "Tumma",
-  "date_after": "Päivä jälkeen",
+  "date_after": "Päivämäärän jälkeen",
   "date_and_time": "Päivämäärä ja aika",
   "date_before": "Päivä ennen",
   "date_of_birth_saved": "Syntymäaika tallennettu",
@@ -481,14 +519,19 @@
   "delete_key": "Poista avain",
   "delete_library": "Poista kirjasto",
   "delete_link": "Poista linkki",
+  "delete_others": "Poista muut",
   "delete_shared_link": "Poista jaettu linkki",
+  "delete_tag": "Poista tunniste",
+  "delete_tag_confirmation_prompt": "Haluatko varmasti poistaa tunnisteen {tagName}?",
   "delete_user": "Poista käyttäjä",
   "deleted_shared_link": "Jaettu linkki poistettu",
+  "deletes_missing_assets": "Poistaa levyltä puuttuvat resurssit",
   "description": "Kuvaus",
   "details": "TIEDOT",
   "direction": "Suunta",
   "disabled": "Poistettu käytöstä",
   "disallow_edits": "Älä salli muokkauksia",
+  "discord": "Discord",
   "discover": "Tutki",
   "dismiss_all_errors": "Sivuuta kaikki virheet",
   "dismiss_error": "Sivuuta virhe",
@@ -497,8 +540,11 @@
   "display_original_photos": "Näytä alkuperäiset kuvat",
   "display_original_photos_setting_description": "Näytä mieluiten alkuperäinen kuva peukalokuvan sijasta kun alkuperäinen aineisto on web-yhteensopiva. Tämä voi aiheuttaa kuvien näyttämisen hitautta.",
   "do_not_show_again": "Älä näytä tätä enää",
+  "documentation": "Dokumentaatio",
   "done": "Valmis",
   "download": "Lataa",
+  "download_include_embedded_motion_videos": "Upotetut videot",
+  "download_include_embedded_motion_videos_description": "Sisällytä liikekuviin upotetut videot erillisinä tiedostoina",
   "download_settings": "Lataukset",
   "download_settings_description": "Hallitse aineiston lataukseen liittyviä asetuksia",
   "downloading": "Ladataan",
@@ -507,13 +553,6 @@
   "duplicates": "Kaksoiskappaleet",
   "duplicates_description": "Selvitä jokaisen kohdalla mitkä (jos yksikään) ovat kaksoiskappaleita",
   "duration": "Kesto",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
   "edit": "Muokkaa",
   "edit_album": "Muokkaa albumia",
   "edit_avatar": "Muokkaa avataria",
@@ -528,13 +567,16 @@
   "edit_location": "Muokkaa sijaintia",
   "edit_name": "Muokkaa nimeä",
   "edit_people": "Muokkaa henkilöitä",
+  "edit_tag": "Muokkaa tunnistetta",
   "edit_title": "Muokkaa otsikkoa",
   "edit_user": "Muokkaa käyttäjää",
   "edited": "Muokattu",
-  "editor": "",
+  "editor": "Editori",
+  "editor_close_without_save_prompt": "Muutoksia ei tallenneta",
+  "editor_close_without_save_title": "Suljetaanko editori?",
+  "editor_crop_tool_h2_aspect_ratios": "Kuvasuhteet",
+  "editor_crop_tool_h2_rotation": "Rotaatio",
   "email": "Sähköposti",
-  "empty": "",
-  "empty_album": "",
   "empty_trash": "Tyhjennä roskakori",
   "empty_trash_confirmation": "Haluatko varmasti tyhjentää roskakorin? Tämä poistaa pysyvästi kaikki tiedostot Immich:stä.\nToimintoa ei voi perua!",
   "enable": "Ota käyttöön",
@@ -559,6 +601,7 @@
     "error_adding_users_to_album": "Käyttäjiä ei voitu lisätä albumiin",
     "error_deleting_shared_user": "Jaettua käyttäjää ei voitu poistaa",
     "error_downloading": "Tiedostoa {filename} ei voitu ladata",
+    "error_hiding_buy_button": "Virhe osta-painikkeen piilottamisessa",
     "error_removing_assets_from_album": "Medioiden poisto epäonnistui. Katso konsolista lisätietoja",
     "error_selecting_all_assets": "Kaikkia medioita ei voitu valita",
     "exclusion_pattern_already_exists": "Tämä poissulkemismalli on jo olemassa.",
@@ -567,8 +610,11 @@
     "failed_to_create_shared_link": "Jaetun linkin luonti epäonnistui",
     "failed_to_edit_shared_link": "Jaetun linkin muokkaus epäonnistui",
     "failed_to_get_people": "Henkilöiden haku epäonnistui",
+    "failed_to_keep_this_delete_others": "Muiden kohteiden poisto epäonnistui",
     "failed_to_load_asset": "Kohteen lataus epäonnistui",
     "failed_to_load_assets": "Kohteiden lataus epäonnistui",
+    "failed_to_load_people": "Henkilöiden lataus epäonnistui",
+    "failed_to_remove_product_key": "Tuoteavaimen poistaminen epäonnistui",
     "failed_to_stack_assets": "Medioiden pinoaminen epäonnistui",
     "failed_to_unstack_assets": "Medioiden pinoamisen purku epäonnistui",
     "import_path_already_exists": "Tämä tuontipolku on jo olemassa.",
@@ -576,54 +622,86 @@
     "paths_validation_failed": "{paths, plural, one {# polun} other {# polun}} validointi epäonnistui",
     "profile_picture_transparent_pixels": "Profiilikuvassa ei voi olla läpinäkyviä pikseleitä. Zoomaa lähemmäs ja/tai siirrä kuvaa.",
     "quota_higher_than_disk_size": "Asettamasi kiintiö on suurempi kuin levyn koko",
-    "unable_to_add_album_users": "",
-    "unable_to_add_comment": "",
-    "unable_to_add_partners": "",
-    "unable_to_change_album_user_role": "",
-    "unable_to_change_date": "",
+    "repair_unable_to_check_items": "Ei voida tarkistaa {count, select, one {kohdetta} other {kohteita}}",
+    "unable_to_add_album_users": "Käyttäjiä ei voi lisätä albumiin",
+    "unable_to_add_assets_to_shared_link": "Medioiden lisääminen jaettuun linkkiin epäonnistui",
+    "unable_to_add_comment": "Kommentin lisääminen epäonnistui",
+    "unable_to_add_exclusion_pattern": "Ei voida lisätä poissulkemismallia",
+    "unable_to_add_import_path": "Tuontipolkua ei voitu lisätä",
+    "unable_to_add_partners": "Kumppaneita ei voitu lisätä",
+    "unable_to_add_remove_archive": "Ei voida {archived, select, true {poistaa kohdetta arkistosta} other {lisätä kohdetta arkistoon}}",
+    "unable_to_add_remove_favorites": "Ei voida {favorite, select, true {lisätä kohdetta suosikkeihin} other {poistaa kohdetta suosikeista}}",
+    "unable_to_archive_unarchive": "Ei voida {archived, select, true {arkistoida} other {poistaa arkistosta}}",
+    "unable_to_change_album_user_role": "Albumin käyttäjän roolia ei voitu muuttaa",
+    "unable_to_change_date": "Päivämäärää ei voitu muuttaa",
+    "unable_to_change_favorite": "Ei voida muuttaa suosikkia kohteelle",
     "unable_to_change_location": "Sijainnin muuttaminen epäonnistui",
     "unable_to_change_password": "Salasanan vaihto epäonnistui",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
+    "unable_to_change_visibility": "Ei voida muuttaa näkyvyyttä {count, plural, one {# henkilölle} other {# henkilölle}}",
+    "unable_to_complete_oauth_login": "OAuth-kirjautumista ei voitu suorittaa loppuun",
+    "unable_to_connect": "Yhteyttä ei voitu muodostaa",
     "unable_to_connect_to_server": "Palvelimeen ei saatu yhteyttä",
+    "unable_to_copy_to_clipboard": "Leikepöydälle ei voitu kopioida, varmista että käytät sivua https-yhteyden kautta",
     "unable_to_create_admin_account": "Pääkäyttäjän luominen epäonnistui",
-    "unable_to_create_library": "",
-    "unable_to_create_user": "",
-    "unable_to_delete_album": "",
-    "unable_to_delete_asset": "",
-    "unable_to_delete_user": "",
-    "unable_to_empty_trash": "",
-    "unable_to_enter_fullscreen": "",
-    "unable_to_exit_fullscreen": "",
-    "unable_to_hide_person": "",
-    "unable_to_load_album": "",
-    "unable_to_load_asset_activity": "",
-    "unable_to_load_items": "",
-    "unable_to_load_liked_status": "",
-    "unable_to_play_video": "",
-    "unable_to_refresh_user": "",
-    "unable_to_remove_album_users": "",
-    "unable_to_remove_comment": "",
-    "unable_to_remove_library": "",
-    "unable_to_remove_partner": "",
-    "unable_to_remove_reaction": "",
-    "unable_to_remove_user": "",
-    "unable_to_repair_items": "",
-    "unable_to_reset_password": "",
-    "unable_to_resolve_duplicate": "",
-    "unable_to_restore_assets": "",
-    "unable_to_restore_trash": "",
-    "unable_to_restore_user": "",
-    "unable_to_save_album": "",
-    "unable_to_save_name": "",
-    "unable_to_save_profile": "",
-    "unable_to_save_settings": "",
-    "unable_to_scan_libraries": "",
-    "unable_to_scan_library": "",
+    "unable_to_create_api_key": "Uuden API-avaimen luominen epäonnistui",
+    "unable_to_create_library": "Kirjaston luominen epäonnistui",
+    "unable_to_create_user": "Käyttäjän luominen epäonnistui",
+    "unable_to_delete_album": "Albumin poistaminen epäonnistui",
+    "unable_to_delete_asset": "Kohteen poistaminen epäonnistui",
+    "unable_to_delete_assets": "Virhe kohteen poistamisessa",
+    "unable_to_delete_exclusion_pattern": "Ei voida poistaa poissulkemismallia",
+    "unable_to_delete_import_path": "Tuontipolkua ei voitu poistaa",
+    "unable_to_delete_shared_link": "Jaetun linkin poistaminen epäonnistui",
+    "unable_to_delete_user": "Käyttäjän poistaminen epäonnistui",
+    "unable_to_download_files": "Tiedostojen lataaminen epäonnistui",
+    "unable_to_edit_exclusion_pattern": "Ei voida muokata poissulkemismallia",
+    "unable_to_edit_import_path": "Tuontipolkua ei voitu muokata",
+    "unable_to_empty_trash": "Roskakorin tyhjentäminen epäonnistui",
+    "unable_to_enter_fullscreen": "Koko ruudun tilaan siirtyminen epäonnistui",
+    "unable_to_exit_fullscreen": "Koko ruudun tilasta poistuminen epäonnistui",
+    "unable_to_get_comments_number": "Kommenttien määrän hakeminen epäonnistui",
+    "unable_to_get_shared_link": "Jaetun linkin hakeminen epäonnistui",
+    "unable_to_hide_person": "Henkilön piilottaminen epäonnistui",
+    "unable_to_link_motion_video": "Liikekuvan linkitys epäonnistui",
+    "unable_to_link_oauth_account": "OAuth-tilin linkittäminen epäonnistui",
+    "unable_to_load_album": "Albumin lataaminen epäonnistui",
+    "unable_to_load_asset_activity": "Ei voitu ladata kohteen toimintaa",
+    "unable_to_load_items": "Kohteiden lataaminen epäonnistui",
+    "unable_to_load_liked_status": "Ei voitu ladata tykkäyksen tilaa",
+    "unable_to_log_out_all_devices": "Kaikkien laitteiden uloskirjautuminen epäonnistui",
+    "unable_to_log_out_device": "Laitteen uloskirjautuminen epäonnistui",
+    "unable_to_login_with_oauth": "OAuth-kirjautuminen epäonnistui",
+    "unable_to_play_video": "Videon toistaminen epäonnistui",
+    "unable_to_reassign_assets_existing_person": "Ei voida siirtää kohteita {name, select, null {olemassa olevalle henkilölle} other {{name}}}",
+    "unable_to_reassign_assets_new_person": "Ei voida siirtää kohteita uudelle henkilölle",
+    "unable_to_refresh_user": "Käyttäjän päivittäminen epäonnistui",
+    "unable_to_remove_album_users": "Käyttäjien poistaminen albumista epäonnistui",
+    "unable_to_remove_api_key": "API-avaimen poistaminen epäonnistui",
+    "unable_to_remove_assets_from_shared_link": "kohteiden poistaminen jaetusta linkistä epäonnistui",
+    "unable_to_remove_deleted_assets": "Offline-tiedostoja ei voitu poistaa",
+    "unable_to_remove_library": "Kirjaston poistaminen epäonnistui",
+    "unable_to_remove_partner": "Kumppanin poistaminen epäonnistui",
+    "unable_to_remove_reaction": "Reaktion poistaminen epäonnistui",
+    "unable_to_repair_items": "Kohteiden korjaaminen epäonnistui",
+    "unable_to_reset_password": "Salasanan nollaaminen epäonnistui",
+    "unable_to_resolve_duplicate": "Virheilmoitus näkyy, kun palvelin palauttaa virheen painettaessa roskakorin tai säilytä-painiketta.",
+    "unable_to_restore_assets": "Kohteen palauttaminen epäonnistui",
+    "unable_to_restore_trash": "Kohteiden palauttaminen epäonnistui",
+    "unable_to_restore_user": "Käyttäjän palauttaminen epäonnistui",
+    "unable_to_save_album": "Albumin tallentaminen epäonnistui",
+    "unable_to_save_api_key": "API-avaimen tallentaminen epäonnistui",
+    "unable_to_save_date_of_birth": "Syntymäajan tallentaminen epäonnistui",
+    "unable_to_save_name": "Nimen tallentaminen epäonnistui",
+    "unable_to_save_profile": "Profiilin tallentaminen epäonnistui",
+    "unable_to_save_settings": "Asetusten tallentaminen epäonnistui",
+    "unable_to_scan_libraries": "Kirjastojen skannaaminen epäonnistui",
+    "unable_to_scan_library": "Kirjaston skannaaminen epäonnistui",
+    "unable_to_set_feature_photo": "Ei voida asettaa ominaiskuvaa",
     "unable_to_set_profile_picture": "Profiilikuvan asetus epäonnistui",
     "unable_to_submit_job": "Työtä ei voitu lähettää",
     "unable_to_trash_asset": "Median siirto roskakoriin epäonnistui",
     "unable_to_unlink_account": "Tunnuksen irroitus epäonnistui",
+    "unable_to_unlink_motion_video": "Ei voida irrottaa liikevideota",
     "unable_to_update_album_cover": "Albumin kannen päivitys epäonnistui",
     "unable_to_update_album_info": "Albumin tietojen päivitys epäonnistui",
     "unable_to_update_library": "Kirjaston päivitys epäonnistui",
@@ -633,10 +711,6 @@
     "unable_to_update_user": "Käyttäjän muokkaus epäonnistui",
     "unable_to_upload_file": "Tiedostoa ei voitu ladata"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exif": "Exif",
   "exit_slideshow": "Poistu diaesityksestä",
   "expand_all": "Laajenna kaikki",
@@ -644,113 +718,140 @@
   "expired": "Voimassaolo päättynyt",
   "expires_date": "Vanhenee {date}",
   "explore": "Tutki",
+  "explorer": "Selain",
   "export": "Vie",
   "export_as_json": "Vie JSON-muodossa",
-  "extension": "",
-  "external_libraries": "",
-  "failed_to_get_people": "",
+  "extension": "Tiedostopääte",
+  "external": "Ulkoisesta",
+  "external_libraries": "Ulkoiset kirjastot",
+  "face_unassigned": "Ei määritelty",
   "favorite": "Suosikki",
-  "favorite_or_unfavorite_photo": "",
+  "favorite_or_unfavorite_photo": "Suosikki- tai ei-suosikkikuva",
   "favorites": "Suosikit",
-  "feature": "",
   "feature_photo_updated": "Kansikuva ladattu",
-  "featurecollection": "",
-  "file_name": "",
-  "file_name_or_extension": "",
+  "features": "Ominaisuudet",
+  "features_setting_description": "Hallitse sovelluksen ominaisuuksia",
+  "file_name": "Tiedoston nimi",
+  "file_name_or_extension": "Tiedostonimi tai tiedostopääte",
   "filename": "Tiedostonimi",
-  "files": "",
   "filetype": "Tiedostotyyppi",
-  "filter_people": "",
-  "fix_incorrect_match": "",
-  "force_re-scan_library_files": "",
+  "filter_people": "Suodata henkilöt",
+  "find_them_fast": "Löydä nopeasti hakemalla nimellä",
+  "fix_incorrect_match": "Korjaa virheellinen osuma",
+  "folders": "Kansiot",
+  "folders_feature_description": "Käytetään kansionäkymää valokuvien ja videoiden selaamiseen järjestelmässä",
   "forward": "Eteenpäin",
-  "general": "",
-  "get_help": "",
-  "getting_started": "",
+  "general": "Yleinen",
+  "get_help": "Hae apua",
+  "getting_started": "Aloittaminen",
   "go_back": "Palaa",
-  "go_to_search": "",
-  "go_to_share_page": "",
-  "group_albums_by": "",
+  "go_to_search": "Siirry hakuun",
+  "group_albums_by": "Ryhmitä albumi...",
   "group_no": "Ei ryhmitystä",
   "group_owner": "Ryhmitä omistajan mukaan",
   "group_year": "Ryhmitä vuoden mukaan",
-  "has_quota": "",
+  "has_quota": "On kiintiö",
   "hi_user": "Hei {name} ({email})",
-  "hide_gallery": "",
-  "hide_password": "",
-  "hide_person": "",
-  "host": "",
+  "hide_all_people": "Piilota kaikki henkilöt",
+  "hide_gallery": "Piilota galleria",
+  "hide_named_person": "Piilota henkilön {name}",
+  "hide_password": "Piilota salasana",
+  "hide_person": "Piilota henkilö",
+  "hide_unnamed_people": "Piilota nimeämättömät henkilöt",
+  "host": "Isäntä",
   "hour": "Tunti",
   "image": "Kuva",
-  "img": "",
-  "immich_logo": "",
-  "import_path": "",
+  "image_alt_text_date": "{isVideo, select, true {Video} other {Kuva}} otettu {date}",
+  "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Kuva}} otettu {person1} kanssa {date}",
+  "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Kuva}} otettu {person1}n ja {person2}n kanssa {date}",
+  "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Kuva}} otettu {person1}n, {person2}n ja {person3}n kanssa {date}",
+  "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Kuva}} otettu {person1}n, {person2}n ja {additionalCount, number} muissa kanssa {date}",
+  "image_alt_text_date_place": "{isVideo, select, true {Video} other {Kuva}} otettu {city}ssä, {country}ssä {date}",
+  "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Kuva}} otettu {city}ssä, {country}ssä {person1}n kanssa {date}",
+  "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Kuva}} otettu {city}ssä, {country}ssä {person1}n ja {person2}n kanssa {date}",
+  "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Kuva}} otettu {city}ssä, {country}ssä {person1}n, {person2}n ja {person3}n kanssa {date}",
+  "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Kuva}} otettu {city}ssä, {country}ssä {person1}n, {person2}n ja {additionalCount, number} muun kanssa {date}",
+  "immich_logo": "Immich-logo",
+  "immich_web_interface": "Immich-verkkokäyttöliittymä",
+  "import_from_json": "Tuo JSON-tiedostosta",
+  "import_path": "Tuontipolku",
   "in_albums": "{count, plural, one {# Albumissa} other {# albumissa}}",
   "in_archive": "Arkistossa",
   "include_archived": "Sisällytä arkistoidut",
-  "include_shared_albums": "",
-  "include_shared_partner_assets": "",
-  "individual_share": "",
+  "include_shared_albums": "Sisällytä jaetut albumit",
+  "include_shared_partner_assets": "Sisällytä jaetut kumppanikohteet",
+  "individual_share": "Yksittäinen jako",
   "info": "Lisätietoja",
   "interval": {
-    "day_at_onepm": "",
-    "hours": "",
-    "night_at_midnight": "",
-    "night_at_twoam": ""
+    "day_at_onepm": "Joka päivä klo 13:00",
+    "hours": "Joka {hours, plural, one {tunti} other {{hours, number} tuntia}}",
+    "night_at_midnight": "Joka yö keskiyöllä",
+    "night_at_twoam": "Joka yö klo 02:00"
   },
   "invite_people": "Kutsu ihmisiä",
   "invite_to_album": "Kutsu albumiin",
   "items_count": "{count, plural, one {# kpl} other {# kpl}}",
-  "job_settings_description": "",
   "jobs": "Taustatehtävät",
   "keep": "Säilytä",
   "keep_all": "Säilytä kaikki",
+  "keep_this_delete_others": "Säilytä tämä, poista muut",
+  "kept_this_deleted_others": "Tämä kohde säilytettiin. {count, plural, one {# asset} other {# assets}} poistettiin",
   "keyboard_shortcuts": "Pikanäppäimet",
   "language": "Kieli",
   "language_setting_description": "Valitse suosimasi kieli",
   "last_seen": "Viimeksi nähty",
   "latest_version": "Viimeisin versio",
+  "latitude": "Leveysaste",
   "leave": "Lähde",
   "let_others_respond": "Anna muiden vastata",
   "level": "Taso",
   "library": "Kirjasto",
-  "library_options": "",
-  "license_button_buy": "Osta",
-  "license_button_select": "Valitse",
+  "library_options": "Kirjastovaihtoehdot",
   "light": "Vaalea",
-  "link_options": "",
-  "link_to_oauth": "",
-  "linked_oauth_account": "",
+  "like_deleted": "Tykkäys poistettu",
+  "link_motion_video": "Linkitä liikevideo",
+  "link_options": "Linkin asetukset",
+  "link_to_oauth": "Linkki OAuth",
+  "linked_oauth_account": "Linkitetty OAuth-tili",
   "list": "Lista",
   "loading": "Ladataan",
-  "loading_search_results_failed": "",
+  "loading_search_results_failed": "Hakutulosten lataaminen epäonnistui",
   "log_out": "Kirjaudu ulos",
   "log_out_all_devices": "Kirjaudu ulos kaikilta laitteilta",
+  "logged_out_all_devices": "Kaikki laitteet kirjattu ulos",
+  "logged_out_device": "Laite kirjattu ulos",
   "login": "Kirjaudu",
   "login_has_been_disabled": "Kirjautuminen on otettu pois käytöstä.",
   "logout_all_device_confirmation": "Haluatko varmasti kirjautua ulos kaikilta laitteilta?",
   "logout_this_device_confirmation": "Haluatko varmasti kirjautua ulos näiltä laitteilta?",
+  "longitude": "Pituusaste",
   "look": "Tyyli",
-  "loop_videos": "",
-  "loop_videos_description": "",
+  "loop_videos": "Toista videot uudelleen",
+  "loop_videos_description": "Ota käyttöön videon automaattinen toisto tarkemmassa näkymässä.",
+  "main_branch_warning": "Käytät kehitysversiota; suosittelemme vahvasti käyttämään julkaisuversiota!",
   "make": "Valmistaja",
   "manage_shared_links": "Hallitse jaettuja linkkejä",
-  "manage_sharing_with_partners": "",
+  "manage_sharing_with_partners": "Hallitse jakamista kumppaneille",
   "manage_the_app_settings": "Hallitse sovelluksen asetuksia",
   "manage_your_account": "Hallitse tiliäsi",
-  "manage_your_api_keys": "",
-  "manage_your_devices": "",
-  "manage_your_oauth_connection": "",
+  "manage_your_api_keys": "Hallitse API-avaimiasi",
+  "manage_your_devices": "Hallitse sisäänkirjautuneita laitteitasi",
+  "manage_your_oauth_connection": "Hallitse OAuth-yhteyttäsi",
   "map": "Kartta",
-  "map_marker_with_image": "",
+  "map_marker_for_images": "Karttamarkerointi kuville, jotka on otettu kaupungissa {city}, maassa {country}",
+  "map_marker_with_image": "Karttamarkerointi kuvalla",
   "map_settings": "Kartta-asetukset",
+  "matches": "Osumia",
   "media_type": "Median tyyppi",
-  "memories": "",
-  "memories_setting_description": "",
+  "memories": "Muistoja",
+  "memories_setting_description": "Hallitse mitä näet muistoissasi",
   "memory": "Muisto",
+  "memory_lane_title": "Muistojen polku {title}",
   "menu": "Valikko",
   "merge": "Yhdistä",
   "merge_people": "Yhdistä henkilöt",
+  "merge_people_limit": "Voit yhdistää vain enintään 5 kasvoa kerrallaan",
+  "merge_people_prompt": "Haluatko yhdistää nämä henkilöt? Tätä valintaa ei voi peruuttaa.",
   "merge_people_successfully": "Henkilöt yhdistetty",
   "merged_people_count": "{count, plural, one {# Henkilö} other {# henkilöä}} yhdistetty",
   "minimize": "PIenennä",
@@ -764,7 +865,8 @@
   "name": "Nimi",
   "name_or_nickname": "Nimi tai lempinimi",
   "never": "ei koskaan",
-  "new_api_key": "Uusi API Key",
+  "new_album": "Uusi Albumi",
+  "new_api_key": "Uusi API-avain",
   "new_password": "Uusi salasana",
   "new_person": "Uusi henkilö",
   "new_user_created": "Uusi käyttäjä lisätty",
@@ -776,42 +878,55 @@
   "no_albums_message": "Luo albumi pitääksesi kuvat ja videot järjestyksessä",
   "no_albums_with_name_yet": "Näyttää siltä, ettei sinulla ole yhtään tämän nimistä albumia.",
   "no_albums_yet": "Näyttää siltä, ettei sinulla ole vielä yhtään albumia.",
-  "no_archived_assets_message": "",
+  "no_archived_assets_message": "Arkistoi kuvia ja videoita piilottaaksesi ne kuvat näkymästä",
   "no_assets_message": "NAPAUTA LATAAKSESI ENSIMMÄISEN KUVASI",
+  "no_duplicates_found": "Kaksoiskappaleita ei löytynyt.",
   "no_exif_info_available": "EXIF-tietoa ei saatavilla",
-  "no_explore_results_message": "",
+  "no_explore_results_message": "Lataa lisää kuvia tutkiaksesi kokoelmaasi.",
   "no_favorites_message": "Lisää suosikkeja löytääksesi nopeasti parhaat kuvasi ja videosi",
-  "no_libraries_message": "",
+  "no_libraries_message": "Luo ulkoinen kirjasto nähdäksesi valokuvasi ja videot",
   "no_name": "Ei nimeä",
-  "no_places": "",
+  "no_places": "Ei paikkoja",
   "no_results": "Ei tuloksia",
+  "no_results_description": "Kokeile synonyymiä tai yleisempää avainsanaa",
   "no_shared_albums_message": "Luo albumi, jotta voit jakaa kuvia ja videoita toisille",
   "not_in_any_album": "Ei yhdessäkään albumissa",
+  "note_apply_storage_label_to_previously_uploaded assets": "Huom: Jotta voit soveltaa tallennustunnistetta aiemmin ladattuihin kohteisiin, suorita",
+  "note_unlimited_quota": "Huomio: Syötä 0 rajoittamatonta kiintiötä varten",
   "notes": "Muistiinpanot",
-  "notification_toggle_setting_description": "Ota sähköpostilmoitukset käyttöön",
+  "notification_toggle_setting_description": "Ota sähköposti-ilmoitukset käyttöön",
   "notifications": "Ilmoitukset",
   "notifications_setting_description": "Hallitse ilmoituksia",
   "oauth": "OAuth",
-  "offline": "",
+  "official_immich_resources": "Viralliset Immich-resurssit",
+  "offline": "Offline",
+  "offline_paths": "Offline-polut",
+  "offline_paths_description": "Nämä tulokset voivat johtua tiedostojen manuaalisesta poistamisesta, jotka eivät ole osa ulkoista kirjastoa.",
   "ok": "Ok",
   "oldest_first": "Vanhin ensin",
+  "onboarding": "Käyttöönotto",
+  "onboarding_privacy_description": "Seuraavat (valinnaiset) ominaisuudet perustuvat ulkoisiin palveluihin, ja ne voidaan poistaa käytöstä milloin tahansa hallinta asetuksista.",
+  "onboarding_theme_description": "Valitse väriteema istunnollesi. Voit muuttaa tämän myöhemmin asetuksistasi.",
+  "onboarding_welcome_description": "Aloitetaa laittamalla istuntoosi joitakin yleisiä asetuksia.",
   "onboarding_welcome_user": "Tervetuloa {user}",
   "online": "Online",
   "only_favorites": "Vain suosikit",
-  "only_refreshes_modified_files": "",
+  "open_in_map_view": "Avaa karttanäkymässä",
   "open_in_openstreetmap": "Avaa OpenStreetMapissa",
-  "open_the_search_filters": "",
+  "open_the_search_filters": "Avaa hakusuodattimet",
   "options": "Vaihtoehdot",
   "or": "tai",
   "organize_your_library": "Järjestele kirjastosi",
   "original": "alkuperäinen",
   "other": "Muut",
   "other_devices": "Toiset laitteet",
-  "other_variables": "",
+  "other_variables": "Muut muuttujat",
   "owned": "Omistettu",
   "owner": "Omistaja",
   "partner": "Kumppani",
   "partner_can_access": "{partner} voi päästä",
+  "partner_can_access_assets": "Kaikki valokuvasi ja videosi, lukuun ottamatta arkistoituja ja poistettuja",
+  "partner_can_access_location": "Sijainti, jossa kuvasi on otettu",
   "partner_sharing": "Kumppanijako",
   "partners": "Kumppanit",
   "password": "Salasana",
@@ -819,22 +934,25 @@
   "password_required": "Salasana vaaditaan",
   "password_reset_success": "Salasanan nollaus onnistui",
   "past_durations": {
-    "days": "{years, plural, one {Viimeisin päivä} other {Viimeiset # päivää}}",
-    "hours": "{years, plural, one {Viimeisin tunti} other {Viimeiset # tuntia}}",
+    "days": "Viime {days, plural, one {päivä} other {# päivää}}",
+    "hours": "Viime {hours, plural, one {tunti} other {# tuntia}}",
     "years": "{years, plural, one {Viimeisin vuosi} other {Viimeiset # vuotta}}"
   },
   "path": "Polku",
-  "pattern": "",
+  "pattern": "Kaava",
   "pause": "Tauko",
-  "pause_memories": "",
+  "pause_memories": "Pysäytä muistot",
   "paused": "Tauotettu",
   "pending": "Odottaa",
   "people": "Ihmiset",
-  "people_sidebar_description": "",
-  "perform_library_tasks": "",
-  "permanent_deletion_warning": "",
-  "permanent_deletion_warning_setting_description": "",
+  "people_edits_count": "Muokattu {count, plural, one {# henkilö} other {# henkilöä}}",
+  "people_feature_description": "Selataan valokuvia ja videoita, jotka on ryhmitelty henkilöiden mukaan",
+  "people_sidebar_description": "Näytä linkki Henkilöihin sivupalkissa",
+  "permanent_deletion_warning": "Pysyvän poiston varoitus",
+  "permanent_deletion_warning_setting_description": "Näytä varoitus, kun poistat kohteita pysyvästi",
   "permanently_delete": "Poista pysyvästi",
+  "permanently_delete_assets_count": "Poista pysyvästi {count, plural, one {kohde} other {kohteita}}",
+  "permanently_delete_assets_prompt": "Oletko varma, että haluat poistaa pysyvästi {count, plural, one {tämän kohteen?} other {nämä <b>#</b> kohteet?}} Tämä poistaa myös {count, plural, one {sen sen} other {ne niiden}} albumista.",
   "permanently_deleted_asset": "Media poistettu pysyvästi",
   "permanently_deleted_assets_count": "{count, plural, one {# media} other {# mediaa}} poistettu pysyvästi",
   "person": "Henkilö",
@@ -849,9 +967,8 @@
   "places": "Paikat",
   "play": "Toista",
   "play_memories": "Toista muistot",
-  "play_motion_photo": "",
+  "play_motion_photo": "Toista Liikekuva",
   "play_or_pause_video": "Toista tai keskeytä video",
-  "point": "",
   "port": "Portti",
   "preset": "Asetus",
   "preview": "Esikatselu",
@@ -859,26 +976,64 @@
   "previous_memory": "Edellinen muisto",
   "previous_or_next_photo": "Edellinen tai seuraava kuva",
   "primary": "Ensisijainen",
+  "privacy": "Yksityisyys",
   "profile_image_of_user": "Käyttäjän {user} profiilikuva",
   "profile_picture_set": "Profiilikuva asetettu.",
   "public_album": "Julkinen albumi",
   "public_share": "Julkinen jako",
-  "range": "",
-  "raw": "",
-  "reaction_options": "",
+  "purchase_account_info": "Tukija",
+  "purchase_activated_subtitle": "Kiitos Immichin ja avoimen lähdekoodin ohjelmiston tukemisesta",
+  "purchase_activated_time": "Aktivoitu {date, date}",
+  "purchase_activated_title": "Avaimesi on aktivoitu onnistuneesti",
+  "purchase_button_activate": "Aktivoi",
+  "purchase_button_buy": "Osta",
+  "purchase_button_buy_immich": "Osta Immich",
+  "purchase_button_never_show_again": "Älä näytä koskaan uudelleen",
+  "purchase_button_reminder": "Muistuta minua 30 päivän kuluessa",
+  "purchase_button_remove_key": "Poista avain",
+  "purchase_button_select": "Valitse",
+  "purchase_failed_activation": "Aktivointi epäonnistui! Tarkista sähköpostisi oikean tuoteavaimen varalta!",
+  "purchase_individual_description_1": "Yksittäiselle henkilölle",
+  "purchase_individual_description_2": "Tukijan tila",
+  "purchase_individual_title": "Yksittäinen",
+  "purchase_input_suggestion": "Onko sinulla tuoteavain? Syötä avain alle",
+  "purchase_license_subtitle": "Osta Immich tukeaksesi palvelun jatkuvaa kehittämistä",
+  "purchase_lifetime_description": "Elinikäinen osto",
+  "purchase_option_title": "OSTOVAIHTOEHDOT",
+  "purchase_panel_info_1": "Immichin rakentaminen vie paljon aikaa ja vaivannäköä, ja meillä on kokopäiväisiä insinöörejä työskentelemässä sen parissa, jotta voimme tehdä siitä mahdollisimman hyvän. Missiomme on, että avoimen lähdekoodin ohjelmistosta ja eettisistä liiketoimintakäytännöistä tulee kestävä tulonlähde kehittäjille, sekä luoda yksityisyyttä kunnioittava ekosysteemi, jossa on todellisia vaihtoehtoja hyväksikäyttöön perustuville pilvipalveluille.",
+  "purchase_panel_info_2": "Koska olemme sitoutuneet siihen, ettemme lisää maksumuuria, tämä osto ei anna sinulle mitään lisäominaisuuksia Immichissa. Luotamme kaltaisiisi käyttäjiin tukeaksemme Immichin jatkuvaa kehittämistä.",
+  "purchase_panel_title": "Tue projektia",
+  "purchase_per_server": "Per palvelin",
+  "purchase_per_user": "Per käyttäjä",
+  "purchase_remove_product_key": "Poista Tuoteavain",
+  "purchase_remove_product_key_prompt": "Haluatko varmasti poistaa tuoteavaimen?",
+  "purchase_remove_server_product_key": "Poista palvelimen tuoteavain",
+  "purchase_remove_server_product_key_prompt": "Haluatko varmasti poistaa palvelimen tuoteavaimen?",
+  "purchase_server_description_1": "Koko palvelimelle",
+  "purchase_server_description_2": "Tukijan tila",
+  "purchase_server_title": "Palvelin",
+  "purchase_settings_server_activated": "Palvelimen tuoteavainta hallinnoi ylläpitäjä",
+  "rating": "Tähtiarvostelu",
+  "rating_clear": "Tyhjennä arvostelu",
+  "rating_count": "{count, plural, one {# tähti} other {# tähteä}}",
+  "rating_description": "Näytä EXIF-arvosana lisätietopaneelissa",
+  "reaction_options": "Reaktioasetukset",
   "read_changelog": "Lue muutosloki",
   "reassign": "Määritä uudelleen",
+  "reassigned_assets_to_existing_person": "Uudelleen määritetty {count, plural, one {# kohde} other {# kohdetta}} {name, select, null {olemassa olevalle henkilölle} other {{name}}}",
   "reassigned_assets_to_new_person": "Määritetty {count, plural, one {# media} other {# mediaa}} uudelle henkilölle",
   "reassing_hint": "Määritä valitut mediat käyttäjälle",
   "recent": "Viimeisin",
   "recent_searches": "Edelliset haut",
   "refresh": "Päivitä",
   "refresh_encoded_videos": "Päivitä enkoodatut videot",
+  "refresh_faces": "Päivitä kasvot",
   "refresh_metadata": "Päivitä metadata",
   "refresh_thumbnails": "Päivitä pikkukuvat",
   "refreshed": "Päivitetty",
-  "refreshes_every_file": "Päivittää jokaisen tiedoston",
+  "refreshes_every_file": "Lukee uudelleen kaikki olemassa olevat ja uudet tiedostot",
   "refreshing_encoded_video": "Päivitetään enkoodattu video",
+  "refreshing_faces": "Päivitetään kasvoja",
   "refreshing_metadata": "Päivitetään metadata",
   "regenerating_thumbnails": "Regeneroidaan pikkukuvia",
   "remove": "Poista",
@@ -886,18 +1041,19 @@
   "remove_assets_shared_link_confirmation": "Haluatko varmasti poistaa {count, plural, one {# median} other {# mediaa}} tästä jakolinkistä?",
   "remove_assets_title": "Poistetaanko?",
   "remove_custom_date_range": "Poista aikaväliltä",
+  "remove_deleted_assets": "Poista Offline-tiedostot",
   "remove_from_album": "Poista albumista",
   "remove_from_favorites": "Poista suosikeista",
   "remove_from_shared_link": "Poista jakolinkistä",
-  "remove_offline_files": "Poista Offline-tiedostot",
   "remove_user": "Poista käyttäjä",
-  "removed_api_key": "API Key {name} poistettu",
+  "removed_api_key": "API-avain {name} poistettu",
   "removed_from_archive": "Poistettu arkistosta",
   "removed_from_favorites": "Poistettu suosikeista",
   "removed_from_favorites_count": "{count, plural, other {Poistettu #}} suosikeista",
+  "removed_tagged_assets": "Poistettu tunniste {count, plural, one {# kohteesta} other {# kohteesta}}",
   "rename": "Nimeä uudelleen",
   "repair": "Korjaa",
-  "repair_no_results_message": "",
+  "repair_no_results_message": "Seuraamattomat ja puuttuvat tiedostot näkyvät täällä",
   "replace_with_upload": "Korvaa tiedostolla",
   "repository": "Tietovarasto",
   "require_password": "Vaadi salasana",
@@ -905,8 +1061,8 @@
   "reset": "Nollaa",
   "reset_password": "Nollaa salasana",
   "reset_people_visibility": "Nollaa henkilöiden näkyvyysasetukset",
-  "reset_settings_to_default": "",
   "reset_to_default": "Palauta oletusasetukset",
+  "resolve_duplicates": "Ratkaise kaksoiskappaleet",
   "resolved_all_duplicates": "Kaikki kaksoiskappaleet selvitetty",
   "restore": "Palauta",
   "restore_all": "Palauta kaikki",
@@ -916,21 +1072,22 @@
   "retry_upload": "Yritä latausta uudelleen",
   "review_duplicates": "Tarkastele kaksoiskappaleita",
   "role": "Rooli",
-  "role_editor": "Muokkain",
+  "role_editor": "Editori",
   "role_viewer": "Toistin",
   "save": "Tallenna",
-  "saved_api_key": "API Key tallennettu",
+  "saved_api_key": "API-avain tallennettu",
   "saved_profile": "Profiili tallennettu",
   "saved_settings": "Asetukset tallennettu",
   "say_something": "Sano jotain",
   "scan_all_libraries": "Skannaa kaikki kirjastot",
-  "scan_all_library_files": "Skannaa uudelleen kaikki kirjastotiedostot",
-  "scan_new_library_files": "Skannaa uusia kirjastotiedostoja",
+  "scan_library": "Skannaa",
   "scan_settings": "Skannausasetukset",
   "scanning_for_album": "Etsitään albumia...",
   "search": "Haku",
   "search_albums": "Etsi albumeita",
   "search_by_context": "Etsi kontekstin perusteella",
+  "search_by_filename": "Hae tiedostonimen tai -päätteen mukaan",
+  "search_by_filename_example": "esim. IMG_1234.JPG tai PNG",
   "search_camera_make": "Etsi kameramerkkiä...",
   "search_camera_model": "Etsi kameramallia...",
   "search_city": "Etsi kaupunkia...",
@@ -938,9 +1095,12 @@
   "search_for_existing_person": "Etsi olemassa olevaa henkilöä",
   "search_no_people": "Ei henkilöitä",
   "search_no_people_named": "Ei \"{name}\" nimisiä henkilöitä",
+  "search_options": "Hakuvaihtoehdot",
   "search_people": "Etsi ihmisiä",
   "search_places": "Etsi paikkoja",
+  "search_settings": "Hakuasetukset",
   "search_state": "Etsi tilaa...",
+  "search_tags": "Etsi tunnisteita...",
   "search_timezone": "Etsi aikavyöhyke...",
   "search_type": "Etsinnän tyyppi",
   "search_your_photos": "Etsi kuvia",
@@ -949,6 +1109,7 @@
   "see_all_people": "Näytä kaikki henkilöt",
   "select_album_cover": "Valitse albmin kansi",
   "select_all": "Valitse kaikki",
+  "select_all_duplicates": "Valitse kaikki kaksoiskappaleet",
   "select_avatar_color": "Valitse avatarin väri",
   "select_face": "Valitse kasvo",
   "select_featured_photo": "Valitse esittelykuva",
@@ -962,7 +1123,8 @@
   "selected_count": "{count, plural, other {# valittu}}",
   "send_message": "Lähetä viesti",
   "send_welcome_email": "Lähetä tervetuloviesti",
-  "server": "Palvelin",
+  "server_offline": "Palvelin Offline-tilassa",
+  "server_online": "Palvelin Online-tilassa",
   "server_stats": "Palvelimen tilastot",
   "server_version": "Palvelimen versio",
   "set": "Aseta",
@@ -978,15 +1140,17 @@
   "shared_by": "Jakanut",
   "shared_by_user": "Käyttäjän {user} jakama",
   "shared_by_you": "Sinun jakamasi",
-  "shared_from_partner": "{partner}n kuvia",
+  "shared_from_partner": "Kumppanin {partner} kuvia",
+  "shared_link_options": "Jaetun linkin vaihtoehdot",
   "shared_links": "Jaetut linkit",
   "shared_photos_and_videos_count": "{assetCount, plural, other {# jaettua kuvaa ja videota.}}",
-  "shared_with_partner": "Jaa {partner} kanssa",
+  "shared_with_partner": "Jaa kumppanin {partner} kanssa",
   "sharing": "Jakaminen",
   "sharing_enter_password": "Nähdäksesi sivun sinun tulee antaa salasana.",
   "sharing_sidebar_description": "Näytä jakamislinkki sivupalkissa",
   "shift_to_permanent_delete": "Paina ⇧ poistaaksesi median pysyvästi",
   "show_album_options": "Näytä albumin asetukset",
+  "show_albums": "Näytä albumit",
   "show_all_people": "Näytä kaikki henkilöt",
   "show_and_hide_people": "Näytä / piilota henkilöitä",
   "show_file_location": "Näytä tiedostosijainti",
@@ -1001,11 +1165,18 @@
   "show_person_options": "Näytä henkilöasetukset",
   "show_progress_bar": "Näytä eteneminen",
   "show_search_options": "Näytä hakuvaihtoehdot",
+  "show_slideshow_transition": "Näytä diaesitys siirtymä",
+  "show_supporter_badge": "Kannattajan merkki",
+  "show_supporter_badge_description": "Näytä kannattajan merkki",
   "shuffle": "Sekoita",
+  "sidebar": "Sivupalkki",
+  "sidebar_display_description": "Näytä linkki näkymään sivupalkissa",
   "sign_out": "Kirjaudu ulos",
   "sign_up": "Rekisteröidy",
   "size": "Koko",
   "skip_to_content": "Siirry sisältöön",
+  "skip_to_folders": "Siirry kansioihin",
+  "skip_to_tags": "Siirry tunnisteisiin",
   "slideshow": "Diaesitys",
   "slideshow_settings": "Diaesityksen asetukset",
   "sort_albums_by": "Järjestä albumit...",
@@ -1015,13 +1186,15 @@
   "sort_oldest": "Vanhin kuva",
   "sort_recent": "Tuorein kuva",
   "sort_title": "Otsikko",
-  "source": "Lähde",
+  "source": "Lähdekoodi",
   "stack": "Pinoa",
+  "stack_duplicates": "Pinoa kaksoiskappaleet",
+  "stack_select_one_photo": "Valitse yksi pääkuva pinolle",
   "stack_selected_photos": "Pinoa valitut kuvat",
   "stacked_assets_count": "Pinottu {count, plural, one {# media} other {# mediaa}}",
   "stacktrace": "Vianetsintätiedot",
   "start": "Aloita",
-  "start_date": "Alkupäivämäärä",
+  "start_date": "Alkupäivä",
   "state": "Maakunta/osavaltio",
   "status": "Tila",
   "stop_motion_photo": "Pysäytä liikkuva kuva",
@@ -1034,47 +1207,63 @@
   "submit": "Lähetä",
   "suggestions": "Ehdotukset",
   "sunrise_on_the_beach": "Auringonnousu rannalla",
+  "support": "Tuki",
+  "support_and_feedback": "Tuki ja palaute",
+  "support_third_party_description": "Immich-asennuksesi on pakattu kolmannen osapuolen toimesta. Kohtaamasi ongelmat saattavat johtua tästä paketista, joten ilmoita niistä ensisijaisesti heille alla olevien linkkien kautta.",
   "swap_merge_direction": "Käännä yhdistämissuunta",
   "sync": "Synkronoi",
+  "tag": "Lisää tunniste",
+  "tag_assets": "Lisää tunnisteita",
+  "tag_created": "Luotu tunniste: {tag}",
+  "tag_feature_description": "Selaa valokuvia ja videoita, jotka on ryhmitelty loogisten tunnisteotsikoiden mukaan",
+  "tag_not_found_question": "Etkö löydä tunnistetta? <link> Luo uusi tunniste </link>",
+  "tag_updated": "Päivitetty tunniste: {tag}",
+  "tagged_assets": "Tunnistettu {count, plural, one {# kohde} other {# kohdetta}}",
+  "tags": "Tunnisteet",
   "template": "Template",
   "theme": "Teema",
   "theme_selection": "Teeman valinta",
   "theme_selection_description": "Aseta vaalea tai tumma tila automaattisesti perustuen selaimesi asetuksiin",
   "they_will_be_merged_together": "Nämä tullaan yhdistämään",
+  "third_party_resources": "Kolmannen osapuolen resurssit",
   "time_based_memories": "Aikaan perustuvat muistot",
+  "timeline": "Aikajana",
   "timezone": "Aikavyöhyke",
   "to_archive": "Arkistoi",
   "to_change_password": "Vaihda salasana",
   "to_favorite": "Aseta suosikiksi",
   "to_login": "Kirjaudu sisään",
+  "to_parent": "Siirry vanhempaan",
   "to_trash": "Roskakoriin",
   "toggle_settings": "Määritä asetukset",
-  "toggle_theme": "Aseta teema",
-  "toggle_visibility": "Aseta näkyvyys",
+  "toggle_theme": "Aseta tumma teema",
+  "total": "Yhteensä",
   "total_usage": "Käyttö yhteensä",
   "trash": "Roskakori",
   "trash_all": "Vie kaikki roskakoriin",
-  "trash_count": "Vie {count} roskakoriin",
+  "trash_count": "Roskakori {count, number}",
   "trash_delete_asset": "Poista / vie roskakoriin",
   "trash_no_results_message": "Roskakorissa olevat kuvat ja videot näytetään täällä.",
   "trashed_items_will_be_permanently_deleted_after": "Roskakorin kohteet poistetaan pysyvästi {days, plural, one {# päivän} other {# päivän}} päästä.",
   "type": "Tyyppi",
   "unarchive": "Palauta arkistosta",
-  "unarchived": "",
   "unarchived_count": "{count, plural, other {# poistettu arkistosta}}",
   "unfavorite": "Poista suosikeista",
   "unhide_person": "Poista henkilö piilosta",
   "unknown": "Tuntematon",
-  "unknown_album": "",
   "unknown_year": "Tuntematon vuosi",
   "unlimited": "Rajoittamaton",
+  "unlink_motion_video": "Poista liikevideon linkitys",
   "unlink_oauth": "Poista OAuth-linkitys",
   "unlinked_oauth_account": "Linkittämätön OAuth-tili",
   "unnamed_album": "Nimetön albumi",
+  "unnamed_album_delete_confirmation": "Haluatko varmasti poistaa tämän albumin?",
   "unnamed_share": "Nimetön jako",
   "unsaved_change": "Tallentamaton muutos",
   "unselect_all": "Poista valinnat",
+  "unselect_all_duplicates": "Poista kaikkien kaksoiskappaleiden valinta",
   "unstack": "Pura pino",
+  "unstacked_assets_count": "Poistettu pinosta {count, plural, one {# kohde} other {# kohdetta}}",
   "untracked_files": "Tiedostot joita ei seurata",
   "untracked_files_decription": "Järjestelmä ei seuraa näitä tiedostoja. Ne voivat johtua epäonnistuneista siirroista, keskeytyneistä latauksista, tai ovat jääneet ohjelmavian seurauksena",
   "up_next": "Seuraavaksi",
@@ -1082,7 +1271,7 @@
   "upload": "Siirrä palvelimelle",
   "upload_concurrency": "Latausten samanaikaisuus",
   "upload_errors": "Lataus valmistui {count, plural, one {# virheen} other {# virheen}} kanssa. Päivitä sivu nähdäksesi ladatut tiedot.",
-  "upload_progress": "{remaining} jäljellä - {processed}/{total} käsitelty",
+  "upload_progress": "Jäljellä {remaining, number} - Käsitelty {processed, number}/{total, number}",
   "upload_skipped_duplicates": "Ohitettiin {count, plural, one {# kaksoiskappale} other {# kaksoiskappaletta}}",
   "upload_status_duplicates": "Kaksoiskappaleet",
   "upload_status_errors": "Virheet",
@@ -1094,8 +1283,12 @@
   "user": "Käyttäjä",
   "user_id": "Käyttäjän ID",
   "user_liked": "{user} tykkäsi {type, select, photo {kuvasta} video {videosta} asset {mediasta} other {tästä}}",
+  "user_purchase_settings": "Osta",
+  "user_purchase_settings_description": "Hallitse ostostasi",
   "user_role_set": "Tee käyttäjästä {user} {role}",
   "user_usage_detail": "Käyttäjän käytön tiedot",
+  "user_usage_stats": "Tilin käyttötilastot",
+  "user_usage_stats_description": "Näytä tilin käyttötilastot",
   "username": "Käyttäjänimi",
   "users": "Käyttäjät",
   "utilities": "Apuohjelmat",
@@ -1103,7 +1296,9 @@
   "variables": "Muuttujat",
   "version": "Versio",
   "version_announcement_closing": "Ystäväsi Alex",
-  "version_announcement_message": "Hei! Sovelluksen uusi versio on saatavilla. Käythän vilkaisemassa <link>julkaisun tiedot</link> ja varmistathan, että <code>docker-compose.yml</code> ja <code>.env</code> määritykset ovat ajan tasalla. Näin varmistat järjestelmän toimivuuden, varsinkin jos käytät WatchToweria tai muuta automaattista päivitysjärjestelmää.",
+  "version_announcement_message": "Hei! Sovelluksen uusi versio on saatavilla. Käythän vilkaisemassa <link>julkaisun tiedot</link> ja varmistathan, että ohjelman määritykset ovat ajan tasalla. Erityisesti, jos käytössä on Watchtower tai jokin muu mekanismi Immich-sovelluksen automaattista päivitystä varten.",
+  "version_history": "Versiohistoria",
+  "version_history_item": "Asennettu {version} päivänä {date}",
   "video": "Video",
   "video_hover_setting": "Toista esikatselun video kun kursori viedään sen päälle",
   "video_hover_setting_description": "Toista videon esikatselukuva kun kursori on kuvan päällä. Vaikka toiminto on pois käytöstä, toiston voi aloittaa viemällä kursori toistokuvakkeen päälle.",
@@ -1113,11 +1308,12 @@
   "view_album": "Näytä albumi",
   "view_all": "Näytä kaikki",
   "view_all_users": "Näytä kaikki käyttäjät",
+  "view_in_timeline": "Näytä aikajanalla",
   "view_links": "Näytä linkit",
+  "view_name": "Näkymä",
   "view_next_asset": "Näytä seuraava",
   "view_previous_asset": "Näytä edellinen",
   "view_stack": "Näytä pinona",
-  "viewer": "",
   "visibility_changed": "{count, plural, one {# henkilön} other {# henkilöiden}} näkyvyys vaihdettu",
   "waiting": "Odottaa",
   "warning": "Varoitus",
diff --git a/i18n/fil.json b/i18n/fil.json
new file mode 100644
index 0000000000..c296e59dd1
--- /dev/null
+++ b/i18n/fil.json
@@ -0,0 +1,25 @@
+{
+  "about": "I-refresh",
+  "account": "Account",
+  "account_settings": "Mga Setting ng Account",
+  "acknowledge": "Tanggapin",
+  "action": "Aksyon",
+  "actions": "Mga Aksyon",
+  "active": "Tumatakbo",
+  "activity": "Mga Aktibidad",
+  "activity_changed": "Ang aktibidad ay {enabled, select, true {naka-enable} other {hindi naka-enable}}",
+  "add": "Mag dagdag",
+  "add_a_description": "Dagdagan ng deskripsyon",
+  "add_a_location": "Dagdagan ng lugar",
+  "add_a_name": "Dagdagan ng pangalan",
+  "add_a_title": "Dagdagan ng pamagat",
+  "add_location": "Magdagdag ng lugar",
+  "add_more_users": "Magdagdag ng mga user",
+  "add_photos": "Magdagdag ng litrato",
+  "add_to": "Idagdag sa...",
+  "add_to_album": "Idagdag sa album",
+  "add_to_shared_album": "Idagdag sa shared album",
+  "added_to_archive": "Idinagdag sa archive",
+  "added_to_favorites": "Idinagdag sa mga paborito",
+  "added_to_favorites_count": "Idinagdag ang {count, number} sa mga paborito"
+}
diff --git a/web/src/lib/i18n/fr.json b/i18n/fr.json
similarity index 84%
rename from web/src/lib/i18n/fr.json
rename to i18n/fr.json
index 9edcb1fdd2..ad141208c6 100644
--- a/web/src/lib/i18n/fr.json
+++ b/i18n/fr.json
@@ -5,16 +5,16 @@
   "acknowledge": "Compris",
   "action": "Action",
   "actions": "Actions",
-  "active": "En cours d'exécution",
+  "active": "En cours",
   "activity": "Activité",
   "activity_changed": "Activité {enabled, select, true {autorisée} other {interdite}}",
   "add": "Ajouter",
   "add_a_description": "Ajouter une description",
-  "add_a_location": "Ajouter un emplacement",
+  "add_a_location": "Ajouter une localisation",
   "add_a_name": "Ajouter un nom",
   "add_a_title": "Ajouter un titre",
   "add_exclusion_pattern": "Ajouter un schéma d'exclusion",
-  "add_import_path": "Ajouter un chemin d'importation",
+  "add_import_path": "Ajouter un chemin à importer",
   "add_location": "Ajouter un lieu",
   "add_more_users": "Ajouter plus d'utilisateurs",
   "add_partner": "Ajouter un partenaire",
@@ -23,53 +23,65 @@
   "add_to": "Ajouter à…",
   "add_to_album": "Ajouter à l'album",
   "add_to_shared_album": "Ajouter à l'album partagé",
+  "add_url": "Ajouter l'URL",
   "added_to_archive": "Ajouté à l'archive",
   "added_to_favorites": "Ajouté aux favoris",
   "added_to_favorites_count": "{count, number} ajouté(s) aux favoris",
   "admin": {
     "add_exclusion_pattern_description": "Ajouter des schémas d'exclusion. Les caractères génériques *, ** et ? sont pris en charge. Pour ignorer tous les fichiers dans un répertoire nommé « Raw », utilisez « **/Raw/** ». Pour ignorer tous les fichiers se terminant par « .tif », utilisez « **/*.tif ». Pour ignorer un chemin absolu, utilisez « /chemin/à/ignorer/** ».",
+    "asset_offline_description": "Ce média de la bibliothèque externe n'est plus présent sur le disque et a été déplacé vers la corbeille. Si le fichier a été déplacé dans la bibliothèque, vérifiez votre chronologie pour le nouveau média correspondant. Pour restaurer ce média, veuillez vous assurer que le chemin du fichier ci-dessous peut être accédé par Immich et lancez l'analyse de la bibliothèque.",
     "authentication_settings": "Paramètres d'authentification",
-    "authentication_settings_description": "Gérer le mot de passe, la délégation d'authentification OAuth et d'autres paramètres d'authentification",
+    "authentication_settings_description": "Gérer le mot de passe, l'authentification OAuth et d'autres paramètres d'authentification",
     "authentication_settings_disable_all": "Êtes-vous sûr de vouloir désactiver toutes les méthodes de connexion ? La connexion sera complètement désactivée.",
     "authentication_settings_reenable": "Pour réactiver, utilisez une <link>Commande Serveur</link>.",
     "background_task_job": "Tâches de fond",
-    "check_all": "Vérifier tout",
-    "cleared_jobs": "Tâches supprimées pour : {job}",
+    "backup_database": "Sauvegarde de la base de données",
+    "backup_database_enable_description": "Activer la sauvegarde",
+    "backup_keep_last_amount": "Nombre de sauvegardes à conserver",
+    "backup_settings": "Paramètres de la sauvegarde",
+    "backup_settings_description": "Gérer les paramètres de la sauvegarde",
+    "check_all": "Tout cocher",
+    "cleared_jobs": "Tâches supprimées pour : {job}",
     "config_set_by_file": "La configuration est actuellement définie par un fichier de configuration",
     "confirm_delete_library": "Êtes-vous sûr de vouloir supprimer la bibliothèque {library} ?",
     "confirm_delete_library_assets": "Êtes-vous sûr de vouloir supprimer cette bibliothèque ? Cette opération supprimera d'Immich {count, plural, one {le média} other {les # médias}} qu'elle contient et ne pourra pas être annulée. Les fichiers resteront sur le disque.",
     "confirm_email_below": "Pour confirmer, tapez « {email} » ci-dessous",
     "confirm_reprocess_all_faces": "Êtes-vous sûr de vouloir retraiter tous les visages ? Cela effacera également les personnes déjà identifiées.",
     "confirm_user_password_reset": "Êtes-vous sûr de vouloir réinitialiser le mot de passe de {user} ?",
-    "crontab_guru": "Générateur de règles Cron",
+    "create_job": "Créer une tâche",
+    "cron_expression": "Expression cron",
+    "cron_expression_description": "Définir l'intervalle d'analyse à l'aide d'une expression cron. Pour plus d'informations, voir <link>Crontab Guru</link>",
+    "cron_expression_presets": "Préréglages d'expression cron",
     "disable_login": "Désactiver la connexion",
-    "disabled": "Désactivé",
-    "duplicate_detection_job_description": "Exécution de l'apprentissage automatique sur les médias pour détecter les images similaires. S'appuie sur la recherche intelligente",
+    "duplicate_detection_job_description": "Lancement de l'apprentissage automatique sur les médias pour détecter les images similaires. Se base sur la recherche intelligente",
     "exclusion_pattern_description": "Les schémas d'exclusion vous permettent d'ignorer des fichiers et des dossiers lors de l'analyse de votre bibliothèque. Cette fonction est utile si des dossiers contiennent des fichiers que vous ne souhaitez pas importer, tels que des fichiers RAW.",
     "external_library_created_at": "Bibliothèque externe (créée le {date})",
     "external_library_management": "Gestion de la bibliothèque externe",
     "face_detection": "Détection des visages",
-    "face_detection_description": "Détection des visages dans les médias à l'aide de l'apprentissage automatique. Pour les vidéos, seule la miniature est prise en compte. « Tout » (re)traite tous les médias. « Manquant » met en file d'attente les médias qui n'ont pas encore été traités. Les visages détectés seront mis en file d'attente pour la reconnaissance faciale une fois la détection des visages terminée, les regroupant en personnes existantes ou nouvelles.",
-    "facial_recognition_job_description": "Regrouper les visages détectés en personnes. Cette étape est exécutée une fois la détection des visages terminée. « Tout » (re)regroupe tous les visages. « Manquant » met en file d'attente les visages auxquels aucune personne n'a été attribuée.",
+    "face_detection_description": "Détection des visages dans les médias à l'aide de l'apprentissage automatique. Pour les vidéos, seule la miniature est prise en compte. « Actualiser » (re)traite tous les médias. « Réinitialiser » efface en plus toutes les données actuelles de visages. « Manquants » Les visages détectés seront mis en file d'attente pour la reconnaissance faciale. Une fois la détection des visages terminée, les regroupant en personnes existantes ou nouvelles.",
+    "facial_recognition_job_description": "Regrouper les visages détectés en personnes. Cette étape est exécutée une fois la détection des visages terminée. « Rafraichir» (re)regroupe tous les visages. « Manquant» met en file d'attente les visages auxquels aucune personne n'a été attribuée.",
     "failed_job_command": "La commande {command} a échoué pour la tâche : {job}",
     "force_delete_user_warning": "ATTENTION : Cette opération entraîne la suppression immédiate de l'utilisateur et de tous ses médias. Cette opération ne peut être annulée et les fichiers ne peuvent être récupérés.",
     "forcing_refresh_library_files": "Forcer le rafraîchissement de tous les fichiers de la bibliothèque",
+    "image_format": "Format",
     "image_format_description": "WebP produit des fichiers plus petits que JPEG, mais son encodage est plus lent.",
     "image_prefer_embedded_preview": "Préférer l'aperçu intégré",
     "image_prefer_embedded_preview_setting_description": "Utiliser les miniatures intégrées dans les photos au format RAW comme entrées pour le traitement d'image quand elles sont disponibles. Cela peut donner des couleurs plus justes pour certaines images, mais la qualité des miniatures est dépendant de l'appareil photo et l'image peut avoir des artéfacts de compression.",
     "image_prefer_wide_gamut": "Préférer une gamme de couleurs étendue",
-    "image_prefer_wide_gamut_setting_description": "Utiliser Display P3 pour les miniatures. Cela préserve mieux la vibrance des images avec des espaces colorimétriques étendus, mais les images peuvent apparaître différemment sur les anciens appareils avec une ancienne version du navigateur. Conserver les images sRGB en sRGB pour éviter les décalages de couleur.",
-    "image_preview_format": "Format des aperçus",
-    "image_preview_resolution": "Résolution des aperçus",
-    "image_preview_resolution_description": "Utilisé lors de l'affichage d'une seule photo et pour l'apprentissage automatique. Des résolutions plus élevées peuvent préserver plus de détails mais prennent plus de temps à encoder, ont des tailles de fichiers plus importantes et peuvent réduire la réactivité de l'application.",
+    "image_prefer_wide_gamut_setting_description": "Utiliser Display P3 pour les miniatures. Cela préserve mieux la vivacité des images avec des espaces colorimétriques étendus, mais les images peuvent apparaître différemment sur les anciens appareils avec une ancienne version du navigateur. Conserver les images sRGB en sRGB pour éviter les décalages de couleur.",
+    "image_preview_description": "Image de taille moyenne avec métadonnées retirées, utilisée lors de la visualisation d'un seul média et pour l'apprentissage automatique",
+    "image_preview_quality_description": "Qualité de l'aperçu : de 1 à 100. Une valeur plus élevée produit de meilleurs résultats, mais elle produit des fichiers plus volumineux et peut réduire la réactivité de l'application. Une valeur trop basse peut affecter la qualité de l'apprentissage automatique.",
+    "image_preview_title": "Paramètres de prévisualisation",
     "image_quality": "Qualité",
-    "image_quality_description": "Qualité d'image de 1 à 100. Une valeur plus élevée offre une meilleure qualité mais produit des fichiers plus volumineux. Cette option affecte les images d'aperçu et de miniature.",
+    "image_resolution": "Résolution",
+    "image_resolution_description": "Les résolutions plus élevées permettent de préserver davantage de détails, mais l'encodage est plus long, les fichiers sont plus volumineux et la réactivité de l'application peut s'en trouver réduite.",
     "image_settings": "Paramètres d'image",
-    "image_settings_description": "Gérer la qualité et la résolution des images générées",
-    "image_thumbnail_format": "Format des miniatures",
-    "image_thumbnail_resolution": "Résolution des miniatures",
-    "image_thumbnail_resolution_description": "Utilisée lors du visionnage de groupes de photos (vue principale, albums, etc.). Une résolution plus élevée préserve davantage de détails, mais est plus longue à encoder, produit des fichiers plus lourds, et peut réduire la réactivité de l'application.",
+    "image_settings_description": "Gestion de la qualité et résolution des images générées",
+    "image_thumbnail_description": "Petite vignette avec métadonnées retirées, utilisée lors de la visualisation de groupes de photos comme sur la vue chronologique principale",
+    "image_thumbnail_quality_description": "Qualité des vignettes : de 1 à 100. Une valeur élevée produit de meilleurs résultats, mais elle produit des fichiers plus volumineux et peut réduire la réactivité de l'application.",
+    "image_thumbnail_title": "Paramètres des vignettes",
     "job_concurrency": "{job} : nombre de tâches simultanées",
+    "job_created": "Tâche créée",
     "job_not_concurrency_safe": "Cette tâche ne peut pas être exécutée en multitâche de façon sûre.",
     "job_settings": "Paramètres des tâches",
     "job_settings_description": "Gestion des tâches simultanées",
@@ -77,16 +89,13 @@
     "jobs_delayed": "{jobCount, plural, other {# retardés}}",
     "jobs_failed": "{jobCount, plural, other {# en échec}}",
     "library_created": "Bibliothèque créée : {library}",
-    "library_cron_expression": "Expression Cron",
-    "library_cron_expression_description": "Réglez l'intervalle d'analyse en utilisant le format cron. Pour plus d'informations, veuillez consulter par exemple <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Expressions Cron enregistrées",
     "library_deleted": "Bibliothèque supprimée",
-    "library_import_path_description": "Spécifier un dossier à importer. Ce dossier, y compris les sous-dossiers, sera analysé à la recherche d'images et de vidéos.",
+    "library_import_path_description": "Spécifier un dossier à importer. Ce dossier, y compris ses sous-dossiers, sera analysé à la recherche d'images et de vidéos.",
     "library_scanning": "Analyse périodique",
     "library_scanning_description": "Configurer l'analyse périodique de la bibliothèque",
     "library_scanning_enable_description": "Activer l'analyse périodique de la bibliothèque",
     "library_settings": "Bibliothèque externe",
-    "library_settings_description": "Gérer les paramètres de bibliothèque externe",
+    "library_settings_description": "Gestion des paramètres des bibliothèques externes",
     "library_tasks_description": "Exécution d'actions sur la bibliothèque",
     "library_watching_enable_description": "Surveiller les modifications de fichiers dans les bibliothèques externes",
     "library_watching_settings": "Surveillance de bibliothèque (EXPÉRIMENTAL)",
@@ -122,7 +131,7 @@
     "machine_learning_smart_search_description": "Rechercher des images de manière sémantique en utilisant les intégrations CLIP",
     "machine_learning_smart_search_enabled": "Activer la recherche intelligente",
     "machine_learning_smart_search_enabled_description": "Si cette option est désactivée, les images ne seront pas encodées pour la recherche intelligente.",
-    "machine_learning_url_description": "URL du serveur d'apprentissage automatique",
+    "machine_learning_url_description": "L’URL du serveur d'apprentissage automatique. Si plusieurs URL sont fournies, chaque serveur sera essayé un par un jusqu’à ce que l’un d’eux réponde avec succès, dans l’ordre de la première à la dernière.",
     "manage_concurrency": "Gérer du multitâche",
     "manage_log_settings": "Gérer les paramètres de journalisation",
     "map_dark_style": "Thème sombre",
@@ -148,11 +157,11 @@
     "migration_job_description": "Migration des miniatures pour les médias et les visages vers la dernière structure de dossiers",
     "no_paths_added": "Aucun chemin n'a été ajouté",
     "no_pattern_added": "Aucun schéma d'exclusion n'a été ajouté",
-    "note_apply_storage_label_previous_assets": "Remarque : pour appliquer l'étiquette de stockage à des médias précédemment téléversés, exécutez la commande",
+    "note_apply_storage_label_previous_assets": "Remarque : pour appliquer l'étiquette de stockage à des médias précédemment envoyés, exécutez la commande",
     "note_cannot_be_changed_later": "REMARQUE : Il n'est pas possible de modifier ce paramètre ultérieurement !",
     "note_unlimited_quota": "Note : saisir 0 pour un quota illimité",
     "notification_email_from_address": "Depuis l'adresse",
-    "notification_email_from_address_description": "Adresse courriel de l'expéditeur, par exemple : « Serveur de photos Immich <nepasrepondre@immich.app> »",
+    "notification_email_from_address_description": "Adresse courriel de l'expéditeur, par exemple : « Serveur de photos Immich <nepasrepondre@exemple.org> »",
     "notification_email_host_description": "Hôte du serveur de messagerie électronique (par exemple, smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Ignorer les erreurs de certificat",
     "notification_email_ignore_certificate_errors_description": "Ignorer les erreurs de validation du certificat TLS (non recommandé)",
@@ -181,7 +190,7 @@
     "oauth_mobile_redirect_uri_override_description": "Activer quand le fournisseur d'OAuth ne permet pas un URI mobile, comme '{callback} '",
     "oauth_profile_signing_algorithm": "Algorithme de signature de profil",
     "oauth_profile_signing_algorithm_description": "Algorithme utilisé pour signer le profil utilisateur.",
-    "oauth_scope": "Portée",
+    "oauth_scope": "Périmètre",
     "oauth_settings": "OAuth",
     "oauth_settings_description": "Gérer les paramètres de connexion OAuth",
     "oauth_settings_more_details": "Pour plus de détails sur cette fonctionnalité, consultez <link>ce lien</link>.",
@@ -198,22 +207,24 @@
     "password_settings": "Connexion par mot de passe",
     "password_settings_description": "Gérer les paramètres de connexion par mot de passe",
     "paths_validated_successfully": "Tous les chemins ont été validés avec succès",
+    "person_cleanup_job": "Nettoyage des personnes",
     "quota_size_gib": "Taille du quota (Go)",
     "refreshing_all_libraries": "Actualisation de toutes les bibliothèques",
     "registration": "Enregistrement de l'administrateur",
     "registration_description": "Puisque vous êtes le premier utilisateur sur le système, vous serez désigné en tant qu'administrateur et responsable des tâches administratives, et vous pourrez alors créer d'autres utilisateurs.",
-    "removing_offline_files": "Suppression des fichiers hors ligne",
     "repair_all": "Réparer tout",
     "repair_matched_items": "{count, plural, one {# Élément correspondant} other {# Éléments correspondants}}",
     "repaired_items": "{count, plural, one {# Élément corrigé} other {# Éléments corrigés}}",
     "require_password_change_on_login": "Demander à l'utilisateur de changer son mot de passe lors de sa première connexion",
     "reset_settings_to_default": "Réinitialiser les paramètres par défaut",
     "reset_settings_to_recent_saved": "Paramètres réinitialisés avec les derniers paramètres enregistrés",
-    "scanning_library_for_changed_files": "Recherche de fichiers modifiés dans la bibliothèque",
-    "scanning_library_for_new_files": "Recherche de nouveaux fichiers dans la bibliothèque",
+    "scanning_library": "Analyse de la bibliothèque",
+    "search_jobs": "Recherche des tâches ...",
     "send_welcome_email": "Envoyer un courriel de bienvenue",
     "server_external_domain_settings": "Domaine externe",
     "server_external_domain_settings_description": "Nom de domaine pour les liens partagés publics, y compris http(s)://",
+    "server_public_users": "Utilisateurs publics",
+    "server_public_users_description": "Tous les utilisateurs (nom et courriel) sont listés lors de l'ajout d'un utilisateur à des albums partagés. Quand cela est désactivé, la liste des utilisateurs est uniquement disponible pour les comptes administrateurs.",
     "server_settings": "Paramètres du serveur",
     "server_settings_description": "Gérer les paramètres du serveur",
     "server_welcome_message": "Message de bienvenue",
@@ -228,24 +239,34 @@
     "storage_template_hash_verification_enabled": "Vérification du hachage activée",
     "storage_template_hash_verification_enabled_description": "Active la vérification du hachage, ne désactivez pas cette option à moins d'être sûr de ce que vous faites",
     "storage_template_migration": "Migration du modèle de stockage",
-    "storage_template_migration_description": "Appliquer le modèle courant <link>{template}</link> aux médias précédemment téléchargés",
-    "storage_template_migration_info": "Les changements de modèle ne s'appliqueront qu'aux nouveaux médias. Pour appliquer rétroactivement le modèle aux médias précédemment téléchargés, exécutez la tâche <link>{job}</link>.",
+    "storage_template_migration_description": "Appliquer le modèle courant <link>{template}</link> aux médias précédemment envoyés",
+    "storage_template_migration_info": "Les changements de modèle ne s'appliqueront qu'aux nouveaux médias. Pour appliquer rétroactivement le modèle aux médias précédemment envoyés, exécutez la tâche <link>{job}</link>.",
     "storage_template_migration_job": "Tâche de migration du modèle de stockage",
     "storage_template_more_details": "Pour plus de détails sur cette fonctionnalité, reportez-vous au <template-link>Modèle de stockage</template-link> et à ses <implications-link>implications</implications-link>",
     "storage_template_onboarding_description": "Lorsqu'elle est activée, cette fonctionnalité réorganise les fichiers basés sur un modèle défini par l'utilisateur. En raison de problèmes de stabilité, la fonction a été désactivée par défaut. Pour plus d'informations, veuillez consulter la <link>documentation</link>.",
     "storage_template_path_length": "Limite approximative de la longueur du chemin : <b>{length, number}</b>/{limit, number}",
     "storage_template_settings": "Modèle de stockage",
-    "storage_template_settings_description": "Gérer la structure des dossiers et le nom des fichiers du média téléversé",
+    "storage_template_settings_description": "Gérer la structure des dossiers et le nom des fichiers du média envoyé",
     "storage_template_user_label": "<code>{label}</code> est l'étiquette de stockage de l'utilisateur",
     "system_settings": "Paramètres du système",
+    "tag_cleanup_job": "Nettoyage des étiquettes",
+    "template_email_available_tags": "Vous pouvez utiliser les variables suivantes dans votre modèle : {tags}",
+    "template_email_if_empty": "Si le modèle est vide, l’e-mail par défaut sera utilisé.",
+    "template_email_invite_album": "Modèle d'invitation à un album",
+    "template_email_preview": "Prévisualiser",
+    "template_email_settings": "Modèles de courriel",
+    "template_email_settings_description": "Gérer les modèles de notifications par courriel personnalisés",
+    "template_email_update_album": "Mettre à jour le modèle d’album",
+    "template_email_welcome": "Modèle de courriel de bienvenue",
+    "template_settings": "Modèles de notifications",
+    "template_settings_description": "Gérer les modèles personnalisés pour les notifications.",
     "theme_custom_css_settings": "CSS personnalisé",
     "theme_custom_css_settings_description": "Les feuilles de style en cascade (CSS) permettent de personnaliser l'apparence d'Immich.",
     "theme_settings": "Paramètres du thème",
     "theme_settings_description": "Gérer la personnalisation de l'interface web d'Immich",
-    "these_files_matched_by_checksum": "Ces fichiers correspondent par leur somme de contrôle",
+    "these_files_matched_by_checksum": "Ces fichiers sont identiques d'après leur somme de contrôle",
     "thumbnail_generation_job": "Génération des miniatures",
     "thumbnail_generation_job_description": "Génération des miniatures pour chaque média ainsi que pour les visages détectés",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "API d'accélération",
     "transcoding_acceleration_api_description": "Il s'agit de l'API qui interagira avec votre appareil pour accélérer le transcodage. Ce paramètre fait au mieux : il basculera vers le transcodage logiciel en cas d'échec. Le codec vidéo VP9 peut fonctionner ou non selon votre matériel.",
     "transcoding_acceleration_nvenc": "NVENC (nécessite un GPU NVIDIA)",
@@ -254,7 +275,7 @@
     "transcoding_acceleration_vaapi": "VAAPI",
     "transcoding_accepted_audio_codecs": "Codecs audio acceptés",
     "transcoding_accepted_audio_codecs_description": "Sélectionnez les codecs audio qui n'ont pas besoin d'être transcodés. Utilisé uniquement pour certaines politiques de transcodage.",
-    "transcoding_accepted_containers": "Containers acceptés",
+    "transcoding_accepted_containers": "Conteneurs acceptés",
     "transcoding_accepted_containers_description": "Sélectionnez les formats de conteneurs qui n'ont pas besoin d'être remuxés en MP4. Utilisé uniquement pour certaines politiques de transcodage.",
     "transcoding_accepted_video_codecs": "Codecs vidéo acceptés",
     "transcoding_accepted_video_codecs_description": "Sélectionnez les codecs vidéo qui n'ont pas besoin d'être transcodés. Utilisé uniquement pour certaines politiques de transcodage.",
@@ -271,7 +292,7 @@
     "transcoding_hardware_acceleration": "Accélération matérielle",
     "transcoding_hardware_acceleration_description": "Expérimental ; beaucoup plus rapide, mais aura une qualité inférieure pour un même débit binaire",
     "transcoding_hardware_decoding": "Décodage matériel",
-    "transcoding_hardware_decoding_setting_description": "S'applique uniquement à NVENC, QSV et RKMPP. Active l'accélération de bout en bout au lieu d'accélérer uniquement l'encodage. Peut ne pas fonctionner sur toutes les vidéos.",
+    "transcoding_hardware_decoding_setting_description": "Active l'accélération de bout en bout au lieu d'accélérer uniquement l'encodage. Peut ne pas fonctionner sur toutes les vidéos.",
     "transcoding_hevc_codec": "Codec HEVC",
     "transcoding_max_b_frames": "Nombre maximum de trames B",
     "transcoding_max_b_frames_description": "Des valeurs plus élevées améliorent l'efficacité de la compression, mais ralentissent l'encodage. Elles peuvent ne pas être compatibles avec l'accélération matérielle sur les anciens appareils. Une valeur de 0 désactive les trames B, tandis qu'une valeur de -1 définit automatiquement ce paramètre.",
@@ -291,14 +312,12 @@
     "transcoding_settings_description": "Gérer les informations de résolution et d'encodage des fichiers vidéo",
     "transcoding_target_resolution": "Résolution cible",
     "transcoding_target_resolution_description": "Des résolutions plus élevées peuvent préserver plus de détails, mais prennent plus de temps à encoder, ont de plus grandes tailles de fichiers, et peuvent réduire la réactivité de l'application.",
-    "transcoding_temporal_aq": "AQ temporelle",
+    "transcoding_temporal_aq": "Quantification adaptative temporelle (temporal AQ)",
     "transcoding_temporal_aq_description": "S'applique uniquement à NVENC. Améliore la qualité des scènes riches en détails et à faible mouvement. Peut ne pas être compatible avec les anciens appareils.",
     "transcoding_threads": "Processus",
     "transcoding_threads_description": "Une valeur plus élevée entraîne un encodage plus rapide, mais laisse moins de place au serveur pour traiter d'autres tâches pendant son activité. Cette valeur ne doit pas être supérieure au nombre de cœurs de CPU. Une valeur égale à 0 maximise l'utilisation.",
     "transcoding_tone_mapping": "Mappage tonal",
     "transcoding_tone_mapping_description": "Tente de préserver l'apparence des vidéos HDR lorsqu'elles sont converties en SDR. Chaque algorithme effectue différents compromis pour la couleur, les détails et la luminosité. Hable préserve les détails, Mobius préserve la couleur, et Reinhard préserve la luminosité.",
-    "transcoding_tone_mapping_npl": "Mappage tonal NPL",
-    "transcoding_tone_mapping_npl_description": "Les couleurs seront ajustées pour paraître normales sur un écran de cette luminosité. De manière contre-intuitive, des valeurs plus basses augmentent la luminosité de la vidéo et vice versa, car cela compense la luminosité de l'écran. 0 configure cette valeur automatiquement.",
     "transcoding_transcode_policy": "Politique de transcodage",
     "transcoding_transcode_policy_description": "Politique indiquant quand une vidéo doit être transcodée. Les vidéos HDR seront toujours transcodées (sauf si le transcodage est désactivé).",
     "transcoding_two_pass_encoding": "Encodage en deux passes",
@@ -311,7 +330,8 @@
     "trash_settings": "Corbeille",
     "trash_settings_description": "Gérer les paramètres de la corbeille",
     "untracked_files": "Fichiers non suivis",
-    "untracked_files_description": "Ces fichiers ne sont pas suivis par l'application. Ils peuvent être le résultat d'erreurs de déplacement, téléchargements interrompus, ou abandons en raison d'un bug",
+    "untracked_files_description": "Ces fichiers ne sont pas suivis par l'application. Ils peuvent être le résultat d'erreurs de déplacement, d'envois interrompus, ou d'abandons en raison d'un bug",
+    "user_cleanup_job": "Nettoyage des utilisateurs",
     "user_delete_delay": "La suppression définitive du compte et des médias de <b>{user}</b> sera programmée dans {delay, plural, one {# jour} other {# jours}}.",
     "user_delete_delay_settings": "Délai de suppression",
     "user_delete_delay_settings_description": "Nombre de jours après la validation pour supprimer définitivement le compte et les médias d'un utilisateur. La suppression des utilisateurs se lance à minuit. Les modifications apportées à ce paramètre seront pris en compte lors de la prochaine exécution.",
@@ -319,7 +339,7 @@
     "user_delete_immediately_checkbox": "Mise en file d'attente d'un utilisateur et de médias en vue d'une suppression immédiate",
     "user_management": "Gestion des utilisateurs",
     "user_password_has_been_reset": "Le mot de passe de l'utilisateur a été réinitialisé :",
-    "user_password_reset_description": "Veuillez saisir un mot de passe temporaire à l'utilisateur et informez-le qu'il devra le changer à sa première connexion.",
+    "user_password_reset_description": "Veuillez fournir le mot de passe temporaire à l'utilisateur et informez-le qu'il devra le changer à sa première connexion.",
     "user_restore_description": "Le compte de <b>{user}</b> sera restauré.",
     "user_restore_scheduled_removal": "Restaurer l'utilisateur - suppression programmée le {date, date, long}",
     "user_settings": "Paramètres utilisateur",
@@ -343,7 +363,7 @@
   "album_added_notification_setting_description": "Recevoir une notification par courriel lorsque vous êtes ajouté(e) à un album partagé",
   "album_cover_updated": "Couverture de l'album mise à jour",
   "album_delete_confirmation": "Êtes-vous sûr de vouloir supprimer l'album {album} ?",
-  "album_delete_confirmation_description": "Si cet album est partagé, d'autres utilisateurs ne pourront plus y accéder.",
+  "album_delete_confirmation_description": "Si cet album est partagé, les autres utilisateurs ne pourront plus y accéder.",
   "album_info_updated": "Détails de l'album mis à jour",
   "album_leave": "Quitter l'album ?",
   "album_leave_confirmation": "Êtes-vous sûr de vouloir quitter l'album {album} ?",
@@ -366,7 +386,7 @@
   "allow_dark_mode": "Autoriser le mode sombre",
   "allow_edits": "Autoriser les modifications",
   "allow_public_user_to_download": "Permettre aux utilisateurs non connectés de télécharger",
-  "allow_public_user_to_upload": "Permettre aux utilisateurs non connectés de téléverser",
+  "allow_public_user_to_upload": "Permettre l'envoi aux utilisateurs non connectés",
   "anti_clockwise": "Sens anti-horaire",
   "api_key": "Clé API",
   "api_key_description": "Cette valeur ne sera affichée qu'une seule fois. Assurez-vous de la copier avant de fermer la fenêtre.",
@@ -378,7 +398,6 @@
   "archive_or_unarchive_photo": "Archiver ou désarchiver une photo",
   "archive_size": "Taille de l'archive",
   "archive_size_description": "Configurer la taille de l'archive maximale pour les téléchargements (en Go)",
-  "archived": "Archivé",
   "archived_count": "{count, plural, one {# archivé} other {# archivés}}",
   "are_these_the_same_person": "Est-ce la même personne ?",
   "are_you_sure_to_do_this": "Êtes-vous sûr de vouloir faire ceci ?",
@@ -386,23 +405,23 @@
   "asset_adding_to_album": "Ajout à l'album...",
   "asset_description_updated": "La description du média a été mise à jour",
   "asset_filename_is_offline": "Le média {filename} est hors ligne",
-  "asset_has_unassigned_faces": "Le média a des visages non assignés",
+  "asset_has_unassigned_faces": "Le média a des visages non attribués",
   "asset_hashing": "Hachage...",
   "asset_offline": "Média hors ligne",
-  "asset_offline_description": "Ce média est hors ligne. Immich ne peut pas accéder à son emplacement physique. Veuillez vous assurez que le média est disponible, puis relancez l'analyse de la bibliothèque.",
+  "asset_offline_description": "Ce média externe n'est plus accessible sur le disque. Veuillez contacter votre administrateur Immich pour obtenir de l'aide.",
   "asset_skipped": "Sauté",
-  "asset_uploaded": "Téléversé",
-  "asset_uploading": "Chargement...",
+  "asset_skipped_in_trash": "À la corbeille",
+  "asset_uploaded": "Envoyé",
+  "asset_uploading": "Envoi...",
   "assets": "Médias",
   "assets_added_count": "{count, plural, one {# média ajouté} other {# médias ajoutés}}",
   "assets_added_to_album_count": "{count, plural, one {# média ajouté} other {# médias ajoutés}} à l'album",
   "assets_added_to_name_count": "{count, plural, one {# média ajouté} other {# médias ajoutés}} à {hasName, select, true {<b>{name}</b>} other {new album}}",
   "assets_count": "{count, plural, one {# média} other {# médias}}",
-  "assets_moved_to_trash": "{count, plural, one {# média déplacé} other {# médias déplacés}} vers la corbeille",
   "assets_moved_to_trash_count": "{count, plural, one {# média déplacé} other {# médias déplacés}} dans la corbeille",
   "assets_permanently_deleted_count": "{count, plural, one {# média supprimé} other {# médias supprimés}} définitivement",
   "assets_removed_count": "{count, plural, one {# média supprimé} other {# médias supprimés}}",
-  "assets_restore_confirmation": "Êtes-vous sûr de vouloir restaurer tous vos médias de la corbeille ? Vous ne pouvez pas annuler cette action !",
+  "assets_restore_confirmation": "Êtes-vous sûr de vouloir restaurer tous vos médias de la corbeille ? Vous ne pouvez pas annuler cette action ! Notez que les médias hors ligne ne peuvent être restaurés de cette façon.",
   "assets_restored_count": "{count, plural, one {# média restauré} other {# médias restaurés}}",
   "assets_trashed_count": "{count, plural, one {# média} other {# médias}} mis à la corbeille",
   "assets_were_part_of_album_count": "{count, plural, one {Un média est} other {Des médias sont}} déjà dans l'album",
@@ -413,6 +432,7 @@
   "birthdate_saved": "Date de naissance sauvée avec succès",
   "birthdate_set_description": "La date de naissance est utilisée pour calculer l'âge de cette personne au moment où la photo a été prise.",
   "blurred_background": "Arrière-plan flouté",
+  "bugs_and_feature_requests": "Bugs & demandes d'évolutions",
   "build": "Version",
   "build_image": "Image de la version",
   "bulk_delete_duplicates_confirmation": "Êtes-vous sûr de vouloir supprimer {count, plural, one {# doublon} other {# doublons}} ? Cette opération conservera le plus grand média de chaque groupe et supprimera définitivement tous les autres doublons. Vous ne pouvez pas annuler cette action !",
@@ -427,10 +447,6 @@
   "cannot_merge_people": "Impossible de fusionner les personnes",
   "cannot_undo_this_action": "Vous ne pouvez pas annuler cette action !",
   "cannot_update_the_description": "Impossible de mettre à jour la description",
-  "cant_apply_changes": "Impossible d'enregistrer les changements",
-  "cant_get_faces": "Aucun visage détecté",
-  "cant_search_people": "Impossible de rechercher des personnes",
-  "cant_search_places": "Impossible de rechercher des lieux",
   "change_date": "Changer la date",
   "change_expiration_time": "Modifier le délai d'expiration",
   "change_location": "Changer la localisation",
@@ -462,6 +478,7 @@
   "confirm": "Confirmer",
   "confirm_admin_password": "Confirmer le mot de passe Admin",
   "confirm_delete_shared_link": "Voulez-vous vraiment supprimer ce lien partagé ?",
+  "confirm_keep_this_delete_others": "Tous les autres médias dans la pile seront supprimés sauf celui-ci. Êtes-vous sûr de vouloir continuer ?",
   "confirm_password": "Confirmer le mot de passe",
   "contain": "Contenu",
   "context": "Contexte",
@@ -487,8 +504,8 @@
   "create_new_person": "Créer une nouvelle personne",
   "create_new_person_hint": "Attribuer les médias sélectionnés à une nouvelle personne",
   "create_new_user": "Créer un nouvel utilisateur",
-  "create_tag": "Créer un tag",
-  "create_tag_description": "Créer un nouveau tag. Pour les tags imbriqués, veuillez entrer le chemin complet du tag, y compris les \"/\" avant.",
+  "create_tag": "Créer une étiquette",
+  "create_tag_description": "Créer une nouvelle étiquette. Pour les étiquettes imbriquées, veuillez entrer le chemin complet de l'étiquette, y compris les caractères \"/\".",
   "create_user": "Créer un utilisateur",
   "created": "Créé",
   "current_device": "Appareil actuel",
@@ -511,43 +528,40 @@
   "delete_key": "Supprimer la clé",
   "delete_library": "Supprimer la bibliothèque",
   "delete_link": "Supprimer le lien",
+  "delete_others": "Supprimer les autres",
   "delete_shared_link": "Supprimer le lien partagé",
-  "delete_tag": "Supprimer le tag",
-  "delete_tag_confirmation_prompt": "Êtes-vous sûr de vouloir supprimer le tag {tagName} ?",
+  "delete_tag": "Supprimer l'étiquette",
+  "delete_tag_confirmation_prompt": "Êtes-vous sûr de vouloir supprimer l'étiquette {tagName} ?",
   "delete_user": "Supprimer l'utilisateur",
   "deleted_shared_link": "Lien partagé supprimé",
+  "deletes_missing_assets": "Supprimer les médias manquants du disque",
   "description": "Description",
   "details": "Détails",
   "direction": "Direction",
   "disabled": "Désactivé",
   "disallow_edits": "Ne pas autoriser les modifications",
+  "discord": "Discord",
   "discover": "Découvrir",
   "dismiss_all_errors": "Ignorer toutes les erreurs",
   "dismiss_error": "Ignorer l'erreur",
   "display_options": "Afficher les options",
   "display_order": "Ordre d'affichage",
   "display_original_photos": "Afficher les photos originales",
-  "display_original_photos_setting_description": "Préférer afficher la photo originale lors de la visualisation d'un média plutôt que sa miniature lorsque cela est possible. Cela peut entraîner des vitesses d'affichage plus lentes.",
+  "display_original_photos_setting_description": "Afficher de préférence la photo originale lors de la visualisation d'un média plutôt que sa miniature lorsque cela est possible. Cela peut entraîner des vitesses d'affichage plus lentes.",
   "do_not_show_again": "Ne plus afficher ce message",
+  "documentation": "Documentation",
   "done": "Terminé",
   "download": "Télécharger",
-  "download_include_embedded_motion_videos": "Vidéos embarquées",
+  "download_include_embedded_motion_videos": "Vidéos intégrées",
   "download_include_embedded_motion_videos_description": "Inclure des vidéos intégrées dans les photos de mouvement comme un fichier séparé",
   "download_settings": "Télécharger",
   "download_settings_description": "Gérer les paramètres de téléchargement des médias",
   "downloading": "Téléchargement",
   "downloading_asset_filename": "Téléchargement du média {filename}",
-  "drop_files_to_upload": "Déposer des fichiers n'importe où pour téléverser",
+  "drop_files_to_upload": "Déposez les fichiers n'importe où pour envoyer",
   "duplicates": "Doublons",
   "duplicates_description": "Examiner chaque groupe et indiquer s'il y a des doublons",
   "duration": "Durée",
-  "durations": {
-    "days": "{days, plural, one {jour} other {{days, number} jours}}",
-    "hours": "{hours, plural, one{une heure} other {{hours, number} heures}}",
-    "minutes": "{minutes, plural, one {minute} other {{minutes, number} minutes}}",
-    "months": "{months, plural, one {mois} other {{months, number} mois}}",
-    "years": "{years, plural, one {an} other {{years, number} ans}}"
-  },
   "edit": "Modifier",
   "edit_album": "Modifier l'album",
   "edit_avatar": "Modifier l'avatar",
@@ -562,8 +576,8 @@
   "edit_location": "Modifier la localisation",
   "edit_name": "Modifier le nom",
   "edit_people": "Modifier les personnes",
-  "edit_tag": "Modifier le tag",
-  "edit_title": "Modifier le title",
+  "edit_tag": "Modifier l'étiquette",
+  "edit_title": "Modifier le titre",
   "edit_user": "Modifier l'utilisateur",
   "edited": "Modifié",
   "editor": "Editeur",
@@ -572,8 +586,6 @@
   "editor_crop_tool_h2_aspect_ratios": "Rapports hauteur/largeur",
   "editor_crop_tool_h2_rotation": "Rotation",
   "email": "Courriel",
-  "empty": "",
-  "empty_album": "Album vide",
   "empty_trash": "Vider la corbeille",
   "empty_trash_confirmation": "Êtes-vous sûr de vouloir vider la corbeille ? Cela supprimera définitivement de Immich tous les médias qu'elle contient.\nVous ne pouvez pas annuler cette action !",
   "enable": "Active",
@@ -588,15 +600,15 @@
     "cant_apply_changes": "Impossible d'appliquer les changements",
     "cant_change_activity": "Impossible {enabled, select, true {d'interdire} other {d'autoriser}} l'activité",
     "cant_change_asset_favorite": "Impossible de changer le favori du média",
-    "cant_change_metadata_assets_count": "Impossible de modifier les métadonnées de {count, plural, one {# média} other {# médias}}",
-    "cant_get_faces": "Impossible d'obtenir de visages",
+    "cant_change_metadata_assets_count": "Impossible de modifier les métadonnées {count, plural, one {d'un média} other {de # médias}}",
+    "cant_get_faces": "Impossible d'obtenir des visages",
     "cant_get_number_of_comments": "Impossible d'obtenir le nombre de commentaires",
     "cant_search_people": "Impossible de rechercher des personnes",
     "cant_search_places": "Impossible de rechercher des lieux",
     "cleared_jobs": "Tâches supprimées pour : {job}",
     "error_adding_assets_to_album": "Erreur lors de l'ajout des médias à l'album",
     "error_adding_users_to_album": "Erreur lors de l'ajout d'utilisateurs à l'album",
-    "error_deleting_shared_user": "Erreur lors de la suppression l'utilisateur partagé",
+    "error_deleting_shared_user": "Erreur lors de la suppression de l'utilisateur partagé",
     "error_downloading": "Erreur lors du téléchargement de {filename}",
     "error_hiding_buy_button": "Impossible de masquer le bouton d'achat",
     "error_removing_assets_from_album": "Erreur lors de la suppression des médias de l'album, vérifier la console pour plus de détails",
@@ -607,13 +619,14 @@
     "failed_to_create_shared_link": "Impossible de créer le lien partagé",
     "failed_to_edit_shared_link": "Impossible de modifier le lien partagé",
     "failed_to_get_people": "Impossible d'obtenir les personnes",
+    "failed_to_keep_this_delete_others": "Impossible de conserver ce média et de supprimer les autres médias",
     "failed_to_load_asset": "Impossible de charger le média",
     "failed_to_load_assets": "Impossible de charger les médias",
     "failed_to_load_people": "Impossible de charger les personnes",
     "failed_to_remove_product_key": "Échec de suppression de la clé du produit",
     "failed_to_stack_assets": "Impossible d'empiler les médias",
     "failed_to_unstack_assets": "Impossible de dépiler les médias",
-    "import_path_already_exists": "Ce chemin d'import existe déjà.",
+    "import_path_already_exists": "Ce chemin d'importation existe déjà.",
     "incorrect_email_or_password": "Courriel ou mot de passe incorrect",
     "paths_validation_failed": "Validation échouée pour {paths, plural, one {# un chemin} other {# plusieurs chemins}}",
     "profile_picture_transparent_pixels": "Les images de profil ne peuvent pas avoir de pixels transparents. Veuillez agrandir et/ou déplacer l'image.",
@@ -623,7 +636,7 @@
     "unable_to_add_assets_to_shared_link": "Impossible d'ajouter des médias au lien partagé",
     "unable_to_add_comment": "Impossible d'ajouter un commentaire",
     "unable_to_add_exclusion_pattern": "Impossible d'ajouter un schéma d'exclusion",
-    "unable_to_add_import_path": "Impossible d'ajouter un chemin d'import",
+    "unable_to_add_import_path": "Impossible d'ajouter le chemin d'importation",
     "unable_to_add_partners": "Impossible d'ajouter des partenaires",
     "unable_to_add_remove_archive": "Impossible {archived, select, true {de supprimer des médias de} other {d'ajouter des médias à}} l'archive",
     "unable_to_add_remove_favorites": "Impossible {favorite, select, true {d'ajouter des médias aux} other {de supprimer des médias des}} favoris",
@@ -634,32 +647,31 @@
     "unable_to_change_location": "Impossible de changer la localisation",
     "unable_to_change_password": "Impossible de changer le mot de passe",
     "unable_to_change_visibility": "Impossible de changer la visibilité pour {count, plural, one {# personne} other {# personnes}}",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_complete_oauth_login": "Impossible de terminer la connexion OAuth",
     "unable_to_connect": "Impossible de se connecter",
     "unable_to_connect_to_server": "Impossible de se connecter au serveur",
     "unable_to_copy_to_clipboard": "Impossible de copier dans le presse-papiers, assurez-vous que vous accédez à la page via https",
     "unable_to_create_admin_account": "Impossible de créer le compte administrateur",
     "unable_to_create_api_key": "Impossible de créer une nouvelle clé API",
-    "unable_to_create_library": "Création de bibliothèque impossible",
-    "unable_to_create_user": "Création de l'utilisateur impossible",
-    "unable_to_delete_album": "Suppression de l'album impossible",
-    "unable_to_delete_asset": "Suppression du média impossible",
+    "unable_to_create_library": "Impossible de créer la bibliothèque",
+    "unable_to_create_user": "Impossible de créer l'utilisateur",
+    "unable_to_delete_album": "Impossible de supprimer l'album",
+    "unable_to_delete_asset": "Impossible de supprimer le média",
     "unable_to_delete_assets": "Erreur lors de la suppression des médias",
-    "unable_to_delete_exclusion_pattern": "Suppression du modèle d'exclusion impossible",
-    "unable_to_delete_import_path": "Suppression du chemin d'import impossible",
-    "unable_to_delete_shared_link": "Suppression du lien de partage impossible",
-    "unable_to_delete_user": "Suppression de l'utilisateur impossible",
+    "unable_to_delete_exclusion_pattern": "Impossible de supprimer le modèle d'exclusion",
+    "unable_to_delete_import_path": "Impossible de supprimer le chemin d'importation",
+    "unable_to_delete_shared_link": "Impossible de supprimer le lien de partage",
+    "unable_to_delete_user": "Impossible de supprimer l'utilisateur",
     "unable_to_download_files": "Impossible de télécharger les fichiers",
-    "unable_to_edit_exclusion_pattern": "Modification du modèle d'exclusion impossible",
-    "unable_to_edit_import_path": "Modification du chemin d'import impossible",
+    "unable_to_edit_exclusion_pattern": "Impossible de modifier le modèle d'exclusion",
+    "unable_to_edit_import_path": "Impossible de modifier le chemin d'importation",
     "unable_to_empty_trash": "Impossible de vider la corbeille",
     "unable_to_enter_fullscreen": "Mode plein écran indisponible",
-    "unable_to_exit_fullscreen": "Sortie du mode plein écran impossible",
+    "unable_to_exit_fullscreen": "Impossible de sortir du mode plein écran",
     "unable_to_get_comments_number": "Impossible d'obtenir le nombre de commentaires",
     "unable_to_get_shared_link": "Échec de la récupération du lien partagé",
     "unable_to_hide_person": "Impossible de cacher la personne",
+    "unable_to_link_motion_video": "Impossible de lier la photo animée",
     "unable_to_link_oauth_account": "Impossible de lier le compte OAuth",
     "unable_to_load_album": "Impossible de charger l'album",
     "unable_to_load_asset_activity": "Impossible de charger l'activité du média",
@@ -669,18 +681,16 @@
     "unable_to_log_out_device": "Impossible de déconnecter l'appareil",
     "unable_to_login_with_oauth": "Impossible de se connecter avec OAuth",
     "unable_to_play_video": "Impossible de jouer la vidéo",
-    "unable_to_reassign_assets_existing_person": "Incapable de réaffecter des médias à {name, select, null {une personne existante} other {{name}}}",
-    "unable_to_reassign_assets_new_person": "Impossible de réaffecter les médias à une nouvelle personne",
+    "unable_to_reassign_assets_existing_person": "Impossible de réattribuer les médias à {name, select, null {une personne existante} other {{name}}}",
+    "unable_to_reassign_assets_new_person": "Impossible de réattribuer les médias à une nouvelle personne",
     "unable_to_refresh_user": "Impossible d'actualiser l'utilisateur",
     "unable_to_remove_album_users": "Impossible de supprimer les utilisateurs de l'album",
     "unable_to_remove_api_key": "Impossible de supprimer la clé API",
     "unable_to_remove_assets_from_shared_link": "Impossible de supprimer des médias du lien partagé",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_deleted_assets": "Impossible de supprimer les fichiers hors ligne",
     "unable_to_remove_library": "Impossible de supprimer la bibliothèque",
-    "unable_to_remove_offline_files": "Impossible de supprimer les fichiers hors ligne",
     "unable_to_remove_partner": "Impossible de supprimer le partenaire",
     "unable_to_remove_reaction": "Impossible de supprimer la réaction",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "Impossible de réparer les éléments",
     "unable_to_reset_password": "Impossible de réinitialiser le mot de passe",
     "unable_to_resolve_duplicate": "Impossible de résoudre le doublon",
@@ -691,7 +701,7 @@
     "unable_to_save_api_key": "Impossible de sauvegarder la clé API",
     "unable_to_save_date_of_birth": "Impossible de sauvegarder la date de naissance",
     "unable_to_save_name": "Impossible de sauvegarder le nom",
-    "unable_to_save_profile": "Impossible de sauvegarder le profile",
+    "unable_to_save_profile": "Impossible de sauvegarder le profil",
     "unable_to_save_settings": "Impossible d'enregistrer les préférences",
     "unable_to_scan_libraries": "Impossible de scanner les bibliothèques",
     "unable_to_scan_library": "Impossible de scanner la bibliothèque",
@@ -700,19 +710,16 @@
     "unable_to_submit_job": "Impossible d'exécuter la tâche",
     "unable_to_trash_asset": "Impossible de mettre le média à la corbeille",
     "unable_to_unlink_account": "Impossible de détacher le compte",
+    "unable_to_unlink_motion_video": "Impossible de détacher la photo animée",
     "unable_to_update_album_cover": "Impossible de mettre à jour la couverture de l'album",
     "unable_to_update_album_info": "Impossible de mettre à jour les informations de l'album",
     "unable_to_update_library": "Impossible de mettre à jour la bibliothèque",
     "unable_to_update_location": "Impossible de mettre à jour la localisation",
     "unable_to_update_settings": "Impossible de mettre à jour les paramètres",
-    "unable_to_update_timeline_display_status": "Impossible de mettre à jour le statut d'affichage de la timeline",
+    "unable_to_update_timeline_display_status": "Impossible de mettre à jour le statut d'affichage de la vue chronologique",
     "unable_to_update_user": "Impossible de mettre à jour l'utilisateur",
-    "unable_to_upload_file": "Impossible de téléverser le fichier"
+    "unable_to_upload_file": "Impossible d'envoyer le fichier"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exif": "Exif",
   "exit_slideshow": "Quitter le diaporama",
   "expand_all": "Tout développer",
@@ -727,33 +734,28 @@
   "external": "Externe",
   "external_libraries": "Bibliothèques externes",
   "face_unassigned": "Non attribué",
-  "failed_to_get_people": "Impossible d'obtenir les personnes",
+  "failed_to_load_assets": "Échec du chargement des ressources",
   "favorite": "Favori",
   "favorite_or_unfavorite_photo": "Ajouter ou supprimer des favoris",
   "favorites": "Favoris",
-  "feature": "",
   "feature_photo_updated": "Photo de la personne mise à jour",
-  "featurecollection": "",
   "features": "Fonctionnalités",
   "features_setting_description": "Gérer les fonctionnalités de l'application",
   "file_name": "Nom du fichier",
   "file_name_or_extension": "Nom du fichier ou extension",
   "filename": "Nom du fichier",
-  "files": "",
   "filetype": "Type de fichier",
   "filter_people": "Filtrer les personnes",
   "find_them_fast": "Pour les retrouver rapidement par leur nom",
   "fix_incorrect_match": "Corriger une association incorrecte",
   "folders": "Dossiers",
   "folders_feature_description": "Parcourir l'affichage par dossiers pour les photos et les vidéos sur le système de fichiers",
-  "force_re-scan_library_files": "Forcer la réactualisation de tous les fichiers de la bibliothèque",
   "forward": "Avant",
   "general": "Général",
   "get_help": "Obtenir de l'aide",
   "getting_started": "Commencer",
   "go_back": "Retour",
   "go_to_search": "Faire une recherche",
-  "go_to_share_page": "Aller sur la page des Partages",
   "group_albums_by": "Grouper les albums par...",
   "group_no": "Pas de groupe",
   "group_owner": "Groupe par propriétaire",
@@ -779,10 +781,6 @@
   "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} prise à {city}, {country} avec {person1} et {person2} le {date}",
   "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} prise à {city}, {country} avec {person1}, {person2}, et {person3} le {date}",
   "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} prise à {city}, {country} avec {person1}, {person2} et {additionalCount, number} autres personnes le {date}",
-  "image_alt_text_people": "{count, plural, =1 {with {person1}} =2 {with {person1} and {person2}} =3 {with {person1}, {person2}, and {person3}} other {with {person1}, {person2}, and {others, number} others}}",
-  "image_alt_text_place": "à {city}, {country}",
-  "image_taken": "{isVideo, select, true {Video prise} other {Image prise}}",
-  "img": "",
   "immich_logo": "Logo Immich",
   "immich_web_interface": "Interface Web Immich",
   "import_from_json": "Importer depuis un fichier JSON",
@@ -803,10 +801,11 @@
   "invite_people": "Inviter une personne",
   "invite_to_album": "Inviter à l'album",
   "items_count": "{count, plural, one {# élément} other {# éléments}}",
-  "job_settings_description": "",
   "jobs": "Tâches",
   "keep": "Conserver",
   "keep_all": "Les conserver tous",
+  "keep_this_delete_others": "Conserver celui-ci, supprimer les autres",
+  "kept_this_deleted_others": "Ce média a été conservé, et {count, plural, one {un autre a été supprimé} other {# autres ont été supprimés}}",
   "keyboard_shortcuts": "Raccourcis clavier",
   "language": "Langue",
   "language_setting_description": "Sélectionnez votre langue préférée",
@@ -818,33 +817,9 @@
   "level": "Niveau",
   "library": "Bibliothèque",
   "library_options": "Options de bibliothèque",
-  "license_account_info": "Ton compte a une licence",
-  "license_activated_subtitle": "Merci de soutenir Immich ainsi que les logiciels open source",
-  "license_activated_title": "Votre licence a été activée avec succès",
-  "license_button_activate": "Activer",
-  "license_button_buy": "Acheter",
-  "license_button_buy_license": "Acheter une licence",
-  "license_button_select": "Sélectionner",
-  "license_failed_activation": "Echec lors de l'activation de la licence. Merci de vérifier la clef reçu par mail !",
-  "license_individual_description_1": "1 licence par utilisateur sur n'importe quel serveur",
-  "license_individual_title": "Licence individuelle",
-  "license_info_licensed": "Licence active",
-  "license_info_unlicensed": "Sans licence",
-  "license_input_suggestion": "Vous avez une licence ? Renseignez la clef ci-dessous",
-  "license_license_subtitle": "Acheter une licence pour soutenir Immich",
-  "license_license_title": "LICENCE",
-  "license_lifetime_description": "Licence à vie",
-  "license_per_server": "Par serveur",
-  "license_per_user": "Par utilisateur",
-  "license_server_description_1": "1 licence par serveur",
-  "license_server_description_2": "Licence pour tous les utilisateurs du serveur",
-  "license_server_title": "Licence serveur",
-  "license_trial_info_1": "Vous utilisez une version Sans Licence de Immich",
-  "license_trial_info_2": "Vous utilisez Immich depuis approximativement",
-  "license_trial_info_3": "{accountAge, plural, one {# jour} other {# jours}}",
-  "license_trial_info_4": "Pensez à acheter une licence pour soutenir le développement du service",
   "light": "Clair",
   "like_deleted": "Réaction « j'aime » supprimée",
+  "link_motion_video": "Lier la photo animée",
   "link_options": "Options de lien",
   "link_to_oauth": "Lien au service OAuth",
   "linked_oauth_account": "Compte OAuth rattaché",
@@ -863,6 +838,7 @@
   "look": "Regarder",
   "loop_videos": "Vidéos en boucle",
   "loop_videos_description": "Activer pour voir la vidéo en boucle dans le lecteur détaillé.",
+  "main_branch_warning": "Vous utilisez une version de développement. Nous vous recommandons fortement d'utiliser une version stable !",
   "make": "Marque",
   "manage_shared_links": "Gérer les liens partagés",
   "manage_sharing_with_partners": "Gérer le partage avec les partenaires",
@@ -879,7 +855,7 @@
   "media_type": "Type de média",
   "memories": "Souvenirs",
   "memories_setting_description": "Gérer ce que vous voyez dans vos souvenirs",
-  "memory": "Mémoire",
+  "memory": "Souvenir",
   "memory_lane_title": "Fil de souvenirs {title}",
   "menu": "Menu",
   "merge": "Fusionner",
@@ -913,10 +889,10 @@
   "no_albums_with_name_yet": "Il semble que vous n'ayez pas encore d'albums avec ce nom.",
   "no_albums_yet": "Il semble que vous n'ayez pas encore d'album.",
   "no_archived_assets_message": "Archiver des photos et vidéos pour les masquer dans votre bibliothèque",
-  "no_assets_message": "CLIQUER ICI POUR IMPORTER VOTRE PREMIÈRE PHOTO",
+  "no_assets_message": "CLIQUER ICI POUR ENVOYER VOTRE PREMIÈRE PHOTO",
   "no_duplicates_found": "Aucun doublon n'a été trouvé.",
   "no_exif_info_available": "Aucune information exif disponible",
-  "no_explore_results_message": "Importer plus de photos pour explorer votre collection.",
+  "no_explore_results_message": "Envoyez plus de photos pour explorer votre collection.",
   "no_favorites_message": "Ajouter des photos et vidéos à vos favoris pour les retrouver plus rapidement",
   "no_libraries_message": "Créer une bibliothèque externe pour voir vos photos et vidéos dans un autre espace de stockage",
   "no_name": "Pas de nom",
@@ -925,13 +901,14 @@
   "no_results_description": "Essayez un synonyme ou un mot-clé plus général",
   "no_shared_albums_message": "Créer un album pour partager vos photos et vidéos avec les personnes de votre réseau",
   "not_in_any_album": "Dans aucun album",
-  "note_apply_storage_label_to_previously_uploaded assets": "Note : Pour appliquer l'étiquette de stockage aux médias déjà importés, lancer la",
+  "note_apply_storage_label_to_previously_uploaded assets": "Note : Pour appliquer l'étiquette de stockage aux médias déjà envoyés, lancer la",
   "note_unlimited_quota": "Note : Saisir 0 pour définir un quota illimité",
   "notes": "Notes",
   "notification_toggle_setting_description": "Activer les notifications par courriel",
   "notifications": "Notifications",
   "notifications_setting_description": "Gérer les notifications",
   "oauth": "OAuth",
+  "official_immich_resources": "Ressources Immich officielles",
   "offline": "Hors ligne",
   "offline_paths": "Chemins hors ligne",
   "offline_paths_description": "Ces résultats peuvent être causés par la suppression manuelle de fichiers qui n'étaient pas dans une bibliothèque externe.",
@@ -944,7 +921,6 @@
   "onboarding_welcome_user": "Bienvenue {user}",
   "online": "En ligne",
   "only_favorites": "Uniquement les favoris",
-  "only_refreshes_modified_files": "Actualise les fichiers modifiés uniquement",
   "open_in_map_view": "Montrer sur la carte",
   "open_in_openstreetmap": "Ouvrir dans OpenStreetMap",
   "open_the_search_filters": "Ouvrir les filtres de recherche",
@@ -982,14 +958,12 @@
   "people_edits_count": "{count, plural, one {# personne éditée} other {# personnes éditées}}",
   "people_feature_description": "Parcourir les photos et vidéos groupées par personnes",
   "people_sidebar_description": "Afficher le menu Personnes dans la barre latérale",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "Avertissement avant suppression définitive",
   "permanent_deletion_warning_setting_description": "Afficher un avertissement avant la suppression définitive d'un média",
   "permanently_delete": "Supprimer définitivement",
-  "permanently_delete_assets_count": "Suppression définitive de {count, plural, one {média} other {médias}}",
+  "permanently_delete_assets_count": "Suppression définitive {count, plural, one {du média} other {des médias}}",
   "permanently_delete_assets_prompt": "Êtes-vous sûr de vouloir supprimer définitivement {count, plural, one {ce média ?} other {ces <b>#</b> médias ?}} Cela {count, plural, one {le} other {les}} supprimera aussi de {count, plural, one {son (ses)} other {leur(s)}} album(s).",
   "permanently_deleted_asset": "Média supprimé définitivement",
-  "permanently_deleted_assets": "{count, plural, one {# média supprimé} other {# médias supprimés}} définitivement",
   "permanently_deleted_assets_count": "{count, plural, one {# média définitivement supprimé} other {# médias définitivement supprimés}}",
   "person": "Personne",
   "person_hidden": "{name}{hidden, select, true { (caché)} other {}}",
@@ -1005,7 +979,6 @@
   "play_memories": "Lancer les souvenirs",
   "play_motion_photo": "Jouer la photo animée",
   "play_or_pause_video": "Jouer ou mettre en pause la vidéo",
-  "point": "",
   "port": "Port",
   "preset": "Préréglage",
   "preview": "Aperçu",
@@ -1019,7 +992,7 @@
   "public_album": "Album public",
   "public_share": "Partage public",
   "purchase_account_info": "Contributeur",
-  "purchase_activated_subtitle": "Merci d'avoir apporté votre soutien à Immich et les logiciels open source",
+  "purchase_activated_subtitle": "Merci d'avoir apporté votre soutien à Immich et aux logiciels open source",
   "purchase_activated_time": "Activé le {date, date}",
   "purchase_activated_title": "Votre clé a été activée avec succès",
   "purchase_button_activate": "Activer",
@@ -1029,7 +1002,7 @@
   "purchase_button_reminder": "Me le rappeler dans 30 jours",
   "purchase_button_remove_key": "Supprimer la clé",
   "purchase_button_select": "Sélectionner",
-  "purchase_failed_activation": "Erreur à l'activation. Veuillez vérifier votre e-mail pour obtenir la clé du produit correcte !",
+  "purchase_failed_activation": "Erreur à l'activation. Veuillez vérifier votre courriel pour obtenir la clé du produit correcte !",
   "purchase_individual_description_1": "Pour un utilisateur",
   "purchase_individual_description_2": "Statut de contributeur",
   "purchase_individual_title": "Utilisateur",
@@ -1038,50 +1011,52 @@
   "purchase_lifetime_description": "Achat à vie",
   "purchase_option_title": "OPTIONS D'ACHAT",
   "purchase_panel_info_1": "Développer Immich nécessite du temps et de l'énergie, et nous avons des ingénieurs qui travaillent à plein temps pour en faire le meilleur produit possible. Notre mission est de générer, pour les logiciels open source et les pratiques de travail éthique, une source de revenus suffisante pour les développeurs et de créer un écosystème respectueux de la vie privée grâce a des alternatives crédibles aux services cloud peu scrupuleux.",
-  "purchase_panel_info_2": "Étant donné que nous nous engageons à ne pas ajouter de murs de paiement, cet achat ne vous donnera pas de fonctionnalités supplémentaires dans Immich. Nous comptons sur des utilisateurs comme vous pour soutenir le développement continu d'Immich.",
+  "purchase_panel_info_2": "Étant donné que nous nous engageons à ne pas ajouter de fonctionnalités payantes, cet achat ne vous donnera pas de fonctionnalités supplémentaires dans Immich. Nous comptons sur des utilisateurs comme vous pour soutenir le développement continu d'Immich.",
   "purchase_panel_title": "Soutenir le projet",
   "purchase_per_server": "Par serveur",
   "purchase_per_user": "Par utilisateur",
   "purchase_remove_product_key": "Supprimer la clé du produit",
   "purchase_remove_product_key_prompt": "Êtes-vous sûr de vouloir supprimer la clé du produit ?",
   "purchase_remove_server_product_key": "Supprimer la clé du produit pour le Serveur",
-  "purchase_remove_server_product_key_prompt": "Êtes-vous sûr de vouloir supprimer la clé du produit pour le serveur ?",
+  "purchase_remove_server_product_key_prompt": "Êtes-vous sûr de vouloir supprimer la clé du produit pour le Serveur ?",
   "purchase_server_description_1": "Pour l'ensemble du serveur",
   "purchase_server_description_2": "Statut de contributeur",
   "purchase_server_title": "Serveur",
   "purchase_settings_server_activated": "La clé du produit pour le Serveur est gérée par l'administrateur",
-  "range": "",
   "rating": "Étoile d'évaluation",
   "rating_clear": "Effacer l'évaluation",
   "rating_count": "{count, plural, one {# étoile} other {# étoiles}}",
   "rating_description": "Afficher l'évaluation EXIF dans le panneau d'information",
-  "raw": "",
   "reaction_options": "Options de réaction",
   "read_changelog": "Lire les changements",
-  "reassign": "Réaffecter",
-  "reassigned_assets_to_existing_person": "{count, plural, one {# média réaffecté} other {# médias réaffectés}} à {name, select, null {une personne existante} other {{name}}}",
-  "reassigned_assets_to_new_person": "{count, plural, one {# média réassigné} other {# médias réassignés}} à une nouvelle personne",
+  "reassign": "Réattribuer",
+  "reassigned_assets_to_existing_person": "{count, plural, one {# média réattribué} other {# médias réattribués}} à {name, select, null {une personne existante} other {{name}}}",
+  "reassigned_assets_to_new_person": "{count, plural, one {# média réattribué} other {# médias réattribués}} à une nouvelle personne",
   "reassing_hint": "Attribuer ces médias à une personne existante",
   "recent": "Récent",
+  "recent-albums": "Albums récents",
   "recent_searches": "Recherches récentes",
   "refresh": "Actualiser",
   "refresh_encoded_videos": "Actualiser les vidéos encodées",
+  "refresh_faces": "Actualiser les visages",
   "refresh_metadata": "Actualiser les métadonnées",
   "refresh_thumbnails": "Actualiser les vignettes",
   "refreshed": "Actualisé",
-  "refreshes_every_file": "Actualise tous les fichiers",
+  "refreshes_every_file": "Actualise tous les fichiers (existants et nouveaux)",
   "refreshing_encoded_video": "Actualisation de la vidéo encodée",
+  "refreshing_faces": "Actualisation des visages",
   "refreshing_metadata": "Actualisation des métadonnées",
-  "regenerating_thumbnails": "Régénération des vignettes",
+  "regenerating_thumbnails": "Regénération des vignettes",
   "remove": "Supprimer",
   "remove_assets_album_confirmation": "Êtes-vous sûr de vouloir supprimer {count, plural, one {# média} other {# médias}} de l'album ?",
   "remove_assets_shared_link_confirmation": "Êtes-vous sûr de vouloir supprimer {count, plural, one {# média} other {# médias}} de ce lien partagé ?",
   "remove_assets_title": "Supprimer les médias ?",
   "remove_custom_date_range": "Supprimer la plage de date personnalisée",
+  "remove_deleted_assets": "Supprimer les fichiers hors ligne",
   "remove_from_album": "Supprimer de l'album",
   "remove_from_favorites": "Supprimer des favoris",
   "remove_from_shared_link": "Supprimer des liens partagés",
-  "remove_offline_files": "Supprimer les fichiers hors ligne",
+  "remove_url": "Supprimer l'URL",
   "remove_user": "Supprimer l'utilisateur",
   "removed_api_key": "Clé API supprimée : {name}",
   "removed_from_archive": "Supprimé de l'archive",
@@ -1098,7 +1073,6 @@
   "reset": "Réinitialiser",
   "reset_password": "Réinitialiser le mot de passe",
   "reset_people_visibility": "Réinitialiser la visibilité des personnes",
-  "reset_settings_to_default": "",
   "reset_to_default": "Rétablir les valeurs par défaut",
   "resolve_duplicates": "Résoudre les doublons",
   "resolved_all_duplicates": "Résolution de tous les doublons",
@@ -1118,8 +1092,7 @@
   "saved_settings": "Paramètres enregistrés",
   "say_something": "Réagir",
   "scan_all_libraries": "Analyser toutes les bibliothèques",
-  "scan_all_library_files": "Analyser tous les fichiers",
-  "scan_new_library_files": "Analyser les nouveaux fichiers",
+  "scan_library": "Analyser",
   "scan_settings": "Paramètres d'analyse",
   "scanning_for_album": "Recherche d'albums en cours...",
   "search": "Recherche",
@@ -1134,10 +1107,12 @@
   "search_for_existing_person": "Rechercher une personne existante",
   "search_no_people": "Aucune personne",
   "search_no_people_named": "Aucune personne nommée « {name} »",
+  "search_options": "Rechercher une option",
   "search_people": "Rechercher une personne",
   "search_places": "Rechercher un lieu",
+  "search_settings": "Paramètres de recherche",
   "search_state": "Rechercher par état/région...",
-  "search_tags": "Recherche de tags...",
+  "search_tags": "Recherche d'étiquettes...",
   "search_timezone": "Rechercher par fuseau horaire...",
   "search_type": "Rechercher par type",
   "search_your_photos": "Rechercher vos photos",
@@ -1160,10 +1135,9 @@
   "selected_count": "{count, plural, one {# sélectionné} other {# sélectionnés}}",
   "send_message": "Envoyer un message",
   "send_welcome_email": "Envoyer un courriel de bienvenue",
-  "server": "Serveur",
   "server_offline": "Serveur hors ligne",
   "server_online": "Serveur en ligne",
-  "server_stats": "Statistiques Serveur",
+  "server_stats": "Statistiques du serveur",
   "server_version": "Version du serveur",
   "set": "Définir",
   "set_as_album_cover": "Définir comme couverture d'album",
@@ -1185,17 +1159,17 @@
   "shared_with_partner": "Partagé avec {partner}",
   "sharing": "Partage",
   "sharing_enter_password": "Veuillez saisir le mot de passe pour visualiser cette page.",
-  "sharing_sidebar_description": "Afficher un lien vers Partage dans la barre latérale",
+  "sharing_sidebar_description": "Afficher un lien vers Partager dans la barre latérale",
   "shift_to_permanent_delete": "appuyez sur ⇧ pour supprimer définitivement le média",
   "show_album_options": "Afficher les options de l'album",
   "show_albums": "Montrer les albums",
   "show_all_people": "Montrer toutes les personnes",
   "show_and_hide_people": "Afficher / Masquer les personnes",
   "show_file_location": "Afficher l'emplacement du fichier",
-  "show_gallery": "Afficher la gallerie",
+  "show_gallery": "Afficher la galerie",
   "show_hidden_people": "Afficher les personnes masquées",
-  "show_in_timeline": "Afficher dans la chronologie",
-  "show_in_timeline_setting_description": "Afficher les photos et vidéos de cet utilisateur dans votre timeline",
+  "show_in_timeline": "Afficher dans la vue chronologique",
+  "show_in_timeline_setting_description": "Afficher les photos et vidéos de cet utilisateur dans votre vue chronologique",
   "show_keyboard_shortcuts": "Afficher les raccourcis clavier",
   "show_metadata": "Afficher les métadonnées",
   "show_or_hide_info": "Afficher ou masquer les informations",
@@ -1203,6 +1177,7 @@
   "show_person_options": "Afficher les options de personnes",
   "show_progress_bar": "Afficher la barre de progression",
   "show_search_options": "Afficher les options de recherche",
+  "show_slideshow_transition": "Afficher la transition du diaporama",
   "show_supporter_badge": "Badge de contributeur",
   "show_supporter_badge_description": "Afficher le badge de contributeur",
   "shuffle": "Mélanger",
@@ -1213,7 +1188,7 @@
   "size": "Taille",
   "skip_to_content": "Passer",
   "skip_to_folders": "Passer vers les dossiers",
-  "skip_to_tags": "Passer vers les tags",
+  "skip_to_tags": "Passer vers les étiquettes",
   "slideshow": "Diaporama",
   "slideshow_settings": "Paramètres du diaporama",
   "sort_albums_by": "Trier les albums par...",
@@ -1225,7 +1200,7 @@
   "sort_title": "Titre",
   "source": "Source",
   "stack": "Empiler",
-  "stack_duplicates": "Empiler les duplications",
+  "stack_duplicates": "Empiler les doublons",
   "stack_select_one_photo": "Sélectionnez une photo principale pour la pile",
   "stack_selected_photos": "Empiler les photos sélectionnées",
   "stacked_assets_count": "{count, plural, one {# média empilé} other {# médias empilés}}",
@@ -1243,51 +1218,54 @@
   "storage_usage": "{used} sur {available} utilisé",
   "submit": "Soumettre",
   "suggestions": "Suggestions",
-  "sunrise_on_the_beach": "Aurore sur la plage",
+  "sunrise_on_the_beach": "Lever de soleil sur la plage",
+  "support": "Support",
+  "support_and_feedback": "Support & Retours",
+  "support_third_party_description": "Votre installation d'Immich est packagée via une application tierce. Si vous rencontrez des anomalies, elles peuvent venir de ce packaging tiers, merci de créer les anomalies avec ces tiers en premier lieu en utilisant les liens ci-dessous.",
   "swap_merge_direction": "Inverser la direction de fusion",
   "sync": "Synchroniser",
-  "tag": "Tag",
-  "tag_assets": "Taguer les médias",
-  "tag_created": "Tag créé : {tag}",
+  "tag": "Étiquette",
+  "tag_assets": "Étiqueter les médias",
+  "tag_created": "Étiquette créée : {tag}",
   "tag_feature_description": "Parcourir les photos et vidéos groupées par thèmes logiques",
-  "tag_not_found_question": "Vous ne trouvez pas un tag ? Créez-en un <link>ici</link>",
-  "tag_updated": "Tag mis à jour : {tag}",
-  "tagged_assets": "Tag ajouté à {count, plural, one {# média} other {# médias}}",
-  "tags": "Tags",
+  "tag_not_found_question": "Vous ne trouvez pas une étiquette ? <link>Créer une nouvelle étiquette.</link>",
+  "tag_updated": "Étiquette mise à jour : {tag}",
+  "tagged_assets": "Étiquette ajoutée à {count, plural, one {# média} other {# médias}}",
+  "tags": "Étiquettes",
   "template": "Modèle",
   "theme": "Thème",
   "theme_selection": "Sélection du thème",
   "theme_selection_description": "Ajuster automatiquement le thème clair ou sombre via les préférences système",
   "they_will_be_merged_together": "Elles seront fusionnées ensemble",
+  "third_party_resources": "Ressources tierces",
   "time_based_memories": "Souvenirs basés sur la date",
+  "timeline": "Vue chronologique",
   "timezone": "Fuseau horaire",
   "to_archive": "Archiver",
   "to_change_password": "Modifier le mot de passe",
   "to_favorite": "Ajouter aux favoris",
   "to_login": "Se connecter",
   "to_parent": "Aller au dossier parent",
-  "to_root": "Vers la racine",
   "to_trash": "Corbeille",
   "toggle_settings": "Inverser les paramètres",
   "toggle_theme": "Inverser le thème sombre",
-  "toggle_visibility": "Modifier la visibilité",
+  "total": "Total",
   "total_usage": "Utilisation globale",
   "trash": "Corbeille",
   "trash_all": "Tout supprimer",
   "trash_count": "Corbeille {count, number}",
-  "trash_delete_asset": "Corbeille/Suppression d'un média",
+  "trash_delete_asset": "Mettre à la corbeille/Supprimer un média",
   "trash_no_results_message": "Les photos et vidéos supprimées s'afficheront ici.",
   "trashed_items_will_be_permanently_deleted_after": "Les éléments dans la corbeille seront supprimés définitivement après {days, plural, one {# jour} other {# jours}}.",
   "type": "Type",
   "unarchive": "Désarchiver",
-  "unarchived": "Non archivé",
   "unarchived_count": "{count, plural, one {# supprimé} other {# supprimés}} de l'archive",
   "unfavorite": "Enlever des favoris",
   "unhide_person": "Afficher la personne",
   "unknown": "Inconnu",
-  "unknown_album": "",
   "unknown_year": "Année inconnue",
   "unlimited": "Illimité",
+  "unlink_motion_video": "Détacher la photo animée",
   "unlink_oauth": "Déconnecter OAuth",
   "unlinked_oauth_account": "Compte OAuth non connecté",
   "unnamed_album": "Album sans nom",
@@ -1299,30 +1277,30 @@
   "unstack": "Désempiler",
   "unstacked_assets_count": "{count, plural, one {# média dépilé} other {# médias dépilés}}",
   "untracked_files": "Fichiers non suivis",
-  "untracked_files_decription": "Ces fichiers ne sont pas suivis par l'application. Ils peuvent être le résultat de déplacements échoués, de téléchargements interrompus ou laissés pour compte à cause d'un bug",
+  "untracked_files_decription": "Ces fichiers ne sont pas suivis par l'application. Ils peuvent être le résultat de déplacements échoués, d'envois interrompus ou laissés pour compte à cause d'un bug",
   "up_next": "Suite",
   "updated_password": "Mot de passe mis à jour",
-  "upload": "Téléverser",
-  "upload_concurrency": "Envoi simultané",
-  "upload_errors": "Le téléversement s'est achevé avec {count, plural, one {# erreur} other {# erreurs}}. Rafraîchir la page pour voir les nouveaux médias téléversés.",
+  "upload": "Envoyer",
+  "upload_concurrency": "Envois simultanés",
+  "upload_errors": "L'envoi s'est achevé avec {count, plural, one {# erreur} other {# erreurs}}. Rafraîchir la page pour voir les nouveaux médias envoyés.",
   "upload_progress": "{remaining, number} restant(s) - {processed, number} traité(s)/{total, number}",
   "upload_skipped_duplicates": "{count, plural, one {# doublon ignoré} other {# doublons ignorés}}",
   "upload_status_duplicates": "Doublons",
   "upload_status_errors": "Erreurs",
-  "upload_status_uploaded": "Téléversé",
-  "upload_success": "Téléversement réussi. Rafraîchir la page pour voir les nouveaux médias téléversés.",
+  "upload_status_uploaded": "Envoyé",
+  "upload_success": "Envoi réussi. Rafraîchir la page pour voir les nouveaux médias envoyés.",
   "url": "URL",
   "usage": "Utilisation",
   "use_custom_date_range": "Utilisez une plage de date personnalisée à la place",
   "user": "Utilisateur",
   "user_id": "ID Utilisateur",
-  "user_license_settings": "Licence",
-  "user_license_settings_description": "Gérer votre licence",
   "user_liked": "{user} a aimé {type, select, photo {cette photo} video {cette vidéo} asset {ce média} other {ceci}}",
   "user_purchase_settings": "Achat",
   "user_purchase_settings_description": "Gérer votre achat",
   "user_role_set": "Définir {user} comme {role}",
   "user_usage_detail": "Détail de l'utilisation des utilisateurs",
+  "user_usage_stats": "Statistiques d'utilisation du compte",
+  "user_usage_stats_description": "Voir les statistiques d'utilisation du compte",
   "username": "Nom d'utilisateur",
   "users": "Utilisateurs",
   "utilities": "Utilitaires",
@@ -1330,7 +1308,9 @@
   "variables": "Variables",
   "version": "Version",
   "version_announcement_closing": "Ton ami, Alex",
-  "version_announcement_message": "Bonjour, il y a une nouvelle version de l'application. Prenez le temps de consulter les <link>notes de version</link> et assurez-vous que vos fichiers <code>docker-compose.yml</code> et <code>.env</code> sont à jour pour éviter toute erreur de configuration, surtout si vous utilisez WatchTower ou tout autre mécanisme qui gère la mise à jour de votre application automatiquement.",
+  "version_announcement_message": "Bonjour, il y a une nouvelle version de l'application. Prenez le temps de consulter les <link>notes de version</link> et assurez vous que votre installation est à jour pour éviter toute erreur de configuration, surtout si vous utilisez WatchTower ou tout autre mécanisme qui gère automatiquement la mise à jour de votre application.",
+  "version_history": "Historique de version",
+  "version_history_item": "Version {version} installée le {date}",
   "video": "Vidéo",
   "video_hover_setting": "Lire la miniature des vidéos au survol",
   "video_hover_setting_description": "Jouer la prévisualisation vidéo au survol. Si désactivé, la lecture peut quand même être démarrée en survolant le bouton Play.",
@@ -1340,18 +1320,18 @@
   "view_album": "Afficher l'album",
   "view_all": "Voir tout",
   "view_all_users": "Voir tous les utilisateurs",
-  "view_in_timeline": "Voir dans la timeline",
+  "view_in_timeline": "Voir dans la vue chronologique",
   "view_links": "Voir les liens",
+  "view_name": "Vue",
   "view_next_asset": "Voir le média suivant",
   "view_previous_asset": "Voir le média précédent",
   "view_stack": "Afficher la pile",
-  "viewer": "Vue",
   "visibility_changed": "Visibilité changée pour {count, plural, one {# personne} other {# personnes}}",
   "waiting": "En attente",
   "warning": "Attention",
   "week": "Semaine",
   "welcome": "Bienvenue",
-  "welcome_to_immich": "Bienvenue sur immich",
+  "welcome_to_immich": "Bienvenue sur Immich",
   "year": "Année",
   "years_ago": "Il y a {years, plural, one {# an} other {# ans}}",
   "yes": "Oui",
diff --git a/web/src/lib/i18n/he.json b/i18n/he.json
similarity index 90%
rename from web/src/lib/i18n/he.json
rename to i18n/he.json
index aefa831897..71b6ab3c91 100644
--- a/web/src/lib/i18n/he.json
+++ b/i18n/he.json
@@ -23,16 +23,23 @@
   "add_to": "הוסף ל..",
   "add_to_album": "הוסף לאלבום",
   "add_to_shared_album": "הוסף לאלבום משותף",
+  "add_url": "הוספת קישור",
   "added_to_archive": "נוסף לארכיון",
   "added_to_favorites": "נוסף למועדפים",
   "added_to_favorites_count": "{count, number} נוספו למועדפים",
   "admin": {
     "add_exclusion_pattern_description": "הוסף דפוסי החרגה. נתמכת התאמת דפוסים באמצעות *, ** ו-?. כדי להתעלם מכל הקבצים בתיקיה כלשהי בשם \"Raw\", השתמש ב \"**/Raw/**\". כדי להתעלם מכל הקבצים המסתיימים ב \"tif.\", השתמש ב \"tif.*/**\". כדי להתעלם מנתיב מוחלט, השתמש ב \"**/נתיב/להתעלמות\".",
-    "authentication_settings": "הגדרות אימות",
-    "authentication_settings_description": "נהל סיסמה, OAuth, והגדרות אימות אחרות",
-    "authentication_settings_disable_all": "האם את/ה בטוח/ה שברצונך להשבית את כל שיטות ההתחברות? כניסה למערכת תהיה מושבתת לחלוטין.",
+    "asset_offline_description": "נכס ספרייה חיצונית זה לא נמצא יותר בדיסק והועבר לאשפה. אם הקובץ הועבר מתוך הספרייה, בדוק את ציר הזמן שלך עבור הנכס המקביל החדש. כדי לשחזר נכס זה, נא לוודא ש-Immich יכול לגשת אל נתיב הקובץ למטה וסרוק מחדש את הספרייה.",
+    "authentication_settings": "הגדרות התחברות",
+    "authentication_settings_description": "נהל סיסמה, OAuth, והגדרות התחברות אחרות",
+    "authentication_settings_disable_all": "האם ברצונך להשבית את כל שיטות ההתחברות? כניסה למערכת תהיה מושבתת לחלוטין.",
     "authentication_settings_reenable": "כדי לאפשר מחדש, השתמש ב<link>פקודת שרת</link>.",
     "background_task_job": "משימות רקע",
+    "backup_database": "גיבוי מסד נתונים",
+    "backup_database_enable_description": "אפשר גיבויי מסד נתונים",
+    "backup_keep_last_amount": "כמות של גיבויים קודמים שיש לשמור",
+    "backup_settings": "הגדרות גיבוי",
+    "backup_settings_description": "נהל הגדרות גיבוי מסד נתונים",
     "check_all": "סמן הכל",
     "cleared_jobs": "נוקו משימות עבור: {job}",
     "config_set_by_file": "התצורה מוגדרת כעת על ידי קובץ תצורה",
@@ -41,35 +48,40 @@
     "confirm_email_below": "כדי לאשר, יש להקליד \"{email}\" למטה",
     "confirm_reprocess_all_faces": "האם את/ה בטוח/ה שברצונך לעבד מחדש את כל הפנים? זה גם ינקה אנשים בעלי שם.",
     "confirm_user_password_reset": "האם את/ה בטוח/ה שברצונך לאפס את הסיסמה של המשתמש {user}?",
-    "crontab_guru": "Crontab Guru",
+    "create_job": "צור עבודה",
+    "cron_expression": "ביטוי cron",
+    "cron_expression_description": "הגדר את מרווח הסריקה באמצעות תבנית ה- cron. למידע נוסף נא לפנות למשל אל <link>Crontab Guru</link>",
+    "cron_expression_presets": "הגדרות קבועות מראש של ביטוי cron",
     "disable_login": "השבת כניסה",
-    "disabled": "מושבת",
     "duplicate_detection_job_description": "הפעל למידת מכונה על נכסים כדי לזהות תמונות דומות. נשען על חיפוש חכם",
     "exclusion_pattern_description": "דפוסי החרגה מאפשרים לך להתעלם מקבצים ומתיקיות בעת סריקת הספרייה שלך. זה שימושי אם יש לך תיקיות המכילות קבצים שאינך רוצה לייבא, כגון קובצי RAW.",
     "external_library_created_at": "ספרייה חיצונית (נוצרה ב-{date})",
     "external_library_management": "ניהול ספרייה חיצונית",
     "face_detection": "איתור פנים",
-    "face_detection_description": "אתר את הפנים בנכסים באמצעות למידת מכונה. עבור סרטונים, רק התמונה הממוזערת נלקחת בחשבון. \"הכל\" מעבד (מחדש) את כל הנכסים. \"חסרים\" מוסיף לתור נכסים שלא עובדו עדיין. לאחר שאיתור הפנים הושלם, פנים שאותרו יעמדו בתור לזיהוי פנים המשייך אותן לאנשים קיימים או חדשים.",
-    "facial_recognition_job_description": "קבץ פנים שאותרו לתוך אנשים. שלב זה מורץ לאחר השלמת איתור פנים. \"הכל\" מקבץ (מחדש) את כל הפרצופים. \"חסרים\" מוסיף לתור פנים שלא הוקצה להם אדם.",
+    "face_detection_description": "אתר את הפנים בנכסים באמצעות למידת מכונה. עבור סרטונים, רק התמונה הממוזערת נלקחת בחשבון. \"רענון\" מעבד (מחדש) את כל הנכסים. \"איפוס\" מנקה בנוסף את כל נתוני הפנים הנוכחיים. \"חסרים\" מוסיף לתור נכסים שלא עובדו עדיין. לאחר שאיתור הפנים הושלם, פנים שאותרו יעמדו בתור לזיהוי פנים המשייך אותן לאנשים קיימים או חדשים.",
+    "facial_recognition_job_description": "קבץ פנים שאותרו לתוך אנשים. שלב זה מורץ לאחר השלמת איתור פנים. \"איפוס\" מקבץ (מחדש) את כל הפרצופים. \"חסרים\" מוסיף לתור פנים שלא הוקצה להם אדם.",
     "failed_job_command": "הפקודה {command} נכשלה עבור המשימה: {job}",
     "force_delete_user_warning": "אזהרה: פעולה זו תסיר מיד את המשתמש ואת כל הנכסים. לא ניתן לבטל פעולה זו והקבצים לא ניתנים לשחזור.",
     "forcing_refresh_library_files": "כפיית רענון של כל קבצי הספרייה",
+    "image_format": "פורמט",
     "image_format_description": "WebP מפיק קבצים קטנים יותר מ JPEG, אך הוא איטי יותר לקידוד.",
     "image_prefer_embedded_preview": "העדף תצוגה מקדימה מוטמעת",
     "image_prefer_embedded_preview_setting_description": "השתמש בתצוגות מקדימות מוטמעות בתמונות RAW כקלט לעיבוד תמונה כאשר זמינות. זה יכול להפיק צבעים מדויקים יותר עבור תמונות מסוימות, אבל האיכות של התצוגה המקדימה היא תלוית מצלמה ולתמונה עשויים להיות יותר פגמי דחיסה.",
     "image_prefer_wide_gamut": "העדף סולם צבעים רחב",
     "image_prefer_wide_gamut_setting_description": "השתמש ב-Display P3 לתמונות ממוזערות. זה משמר טוב יותר את החיוניות של תמונות עם מרחבי צבע רחבים, אבל תמונות עשויות להופיע אחרת במכשירים ישנים עם גרסת דפדפן ישנה. תמונות sRGB נשמרות כ-sRGB כדי למנוע שינויי צבע.",
-    "image_preview_format": "פורמט תצוגה מקדימה",
-    "image_preview_resolution": "רזולוציית תצוגה מקדימה",
-    "image_preview_resolution_description": "משמש בעת צפייה בתמונה בודדת ועבור למידת מכונה. רזולוציות גבוהות יותר יכולות לשמר פירוט רב יותר אך לוקחות יותר זמן לקידוד, יש להן גדלי קבצים גדולים יותר, ויכולות להפחית את תגובתיות היישום.",
+    "image_preview_description": "תמונה בגודל בינוני עם מטא-נתונים שהוסרו, משמשת בעת צפייה בנכס בודד ועבור למידת מכונה",
+    "image_preview_quality_description": "איכות תצוגה מקדימה בין 1-100. גבוה יותר הוא טוב יותר, אבל מייצר קבצים גדולים יותר ויכול להפחית את תגובתיות היישום. הגדרת ערך נמוך עשויה להשפיע על איכות תוצאות של למידת מכונה.",
+    "image_preview_title": "הגדרות תצוגה מקדימה",
     "image_quality": "איכות",
-    "image_quality_description": "איכות תמונה מ-1 עד 100. ערך גבוה יותר עדיף לאיכות אך מייצר קבצים גדולים יותר, אפשרות זו משפיעה על התצוגה המקדימה ותמונות ממוזערות.",
+    "image_resolution": "רזולוציה",
+    "image_resolution_description": "רזולוציות גבוהות יותר יכולות לשמר פרטים רבים יותר אך לוקחות זמן רב יותר לקידוד, יש להן גדלי קבצים גדולים יותר ויכולות להפחית את תגובתיות היישום.",
     "image_settings": "הגדרות תמונה",
     "image_settings_description": "נהל את האיכות והרזולוציה של תמונות שנוצרו",
-    "image_thumbnail_format": "פורמט תמונה ממוזערת",
-    "image_thumbnail_resolution": "רזולוציית תמונה ממוזערת",
-    "image_thumbnail_resolution_description": "משמש בעת צפייה בקבוצות של תמונות (ציר זמן ראשי, תצוגת אלבום וכו'). רזולוציות גבוהות יותר יכולות לשמר פירוט רב יותר אך לוקחות יותר זמן לקידוד, יש להן גדלי קבצים גדולים יותר, ויכולות להפחית את תגובתיות היישום.",
+    "image_thumbnail_description": "תמונה ממוזערת קטנה עם מטא-נתונים שהוסרו, משמשת בעת צפייה בקבוצות של תמונות כמו ציר הזמן הראשי",
+    "image_thumbnail_quality_description": "איכות תמונה ממוזערת בין 1-100. גבוה יותר הוא טוב יותר, אבל מייצר קבצים גדולים יותר ויכול להפחית את תגובתיות היישום.",
+    "image_thumbnail_title": "הגדרות תמונה ממוזערת",
     "job_concurrency": "בו-זמניות של {job}",
+    "job_created": "עבודה נוצרה",
     "job_not_concurrency_safe": "משימה זו אינה בטוחה במקביל.",
     "job_settings": "הגדרות משימה",
     "job_settings_description": "ניהול בו-זמניות של משימה",
@@ -77,9 +89,6 @@
     "jobs_delayed": "{jobCount, plural, other {# עוכבו}}",
     "jobs_failed": "{jobCount, plural, other {# נכשלו}}",
     "library_created": "נוצרה ספרייה: {library}",
-    "library_cron_expression": "ביטוי cron",
-    "library_cron_expression_description": "הגדר את מרווח הסריקה באמצעות פורמט ה-cron. למידע נוסף אנא פנה למשל אל <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "הגדרות ביטוי cron קבועות מראש",
     "library_deleted": "ספרייה נמחקה",
     "library_import_path_description": "ציין תיקיה לייבוא. תיקייה זו, כולל תיקיות משנה, תיסרק עבור תמונות וסרטונים.",
     "library_scanning": "סריקה תקופתית",
@@ -122,7 +131,7 @@
     "machine_learning_smart_search_description": "חפש תמונות באופן סמנטי באמצעות הטמעות של CLIP",
     "machine_learning_smart_search_enabled": "אפשר חיפוש חכם",
     "machine_learning_smart_search_enabled_description": "אם מושבת, תמונות לא יקודדו לחיפוש חכם.",
-    "machine_learning_url_description": "כתובת האתר של שרת למידת המכונה",
+    "machine_learning_url_description": "כתובת האתר של שרת למידת המכונה. אם ניתן יותר מכתובת אחת, כל שרת ינסה בתורו עד אשר יענה בחיוב, בסדר התחלתי.",
     "manage_concurrency": "נהל בו-זמניות",
     "manage_log_settings": "נהל הגדרות רישום ביומן",
     "map_dark_style": "עיצוב כהה",
@@ -139,7 +148,11 @@
     "map_settings_description": "נהל הגדרות מפה",
     "map_style_description": "כתובת אתר לערכת נושא של מפה style.json",
     "metadata_extraction_job": "חלץ מטא-נתונים",
-    "metadata_extraction_job_description": "חלץ מידע מטא-נתונים מכל נכס, כגון GPS ורזולוציה",
+    "metadata_extraction_job_description": "חלץ מידע מטא-נתונים מכל נכס, כגון GPS, פנים ורזולוציה",
+    "metadata_faces_import_setting": "אפשר יבוא פנים",
+    "metadata_faces_import_setting_description": "יבא פנים מנתוני EXIF של תמונה ומקבצים נלווים",
+    "metadata_settings": "הגדרות מטא-נתונים",
+    "metadata_settings_description": "נהל הגדרות מטא-נתונים",
     "migration_job": "העברה",
     "migration_job_description": "העבר תמונות ממוזערות של נכסים ופנים למבנה התיקיות העדכני ביותר",
     "no_paths_added": "לא נוספו נתיבים",
@@ -148,7 +161,7 @@
     "note_cannot_be_changed_later": "הערה: אי אפשר לשנות זאת מאוחר יותר!",
     "note_unlimited_quota": "הערה: הזן 0 עבור מכסת אחסון בלתי מוגבלת",
     "notification_email_from_address": "מכתובת",
-    "notification_email_from_address_description": "כתובת דוא\"ל של השולח, לדוגמה: \"Immich שרת תמונות <noreply@immich.app>\"",
+    "notification_email_from_address_description": "כתובת דוא\"ל של השולח, לדוגמה: \"Immich שרת תמונות <noreply@example.com>\"",
     "notification_email_host_description": "מארח שרת הדוא\"ל (למשל smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "התעלם משגיאות תעודה",
     "notification_email_ignore_certificate_errors_description": "התעלם משגיאות אימות תעודת TLS (לא מומלץ)",
@@ -194,22 +207,24 @@
     "password_settings": "סיסמת התחברות",
     "password_settings_description": "נהל הגדרות סיסמת התחברות",
     "paths_validated_successfully": "כל הנתיבים אומתו בהצלחה",
+    "person_cleanup_job": "ניקוי אדם",
     "quota_size_gib": "גודל מכסה (GiB)",
     "refreshing_all_libraries": "מרענן את כל הספריות",
     "registration": "רישום מנהל מערכת",
     "registration_description": "מכיוון שאתה המשתמש הראשון במערכת, אתה תוקצה כמנהל ואתה אחראי על משימות ניהול, ומשתמשים נוספים ייווצרו על ידך.",
-    "removing_offline_files": "הסרת קבצים לא מקוונים",
     "repair_all": "תקן הכל",
     "repair_matched_items": "{count, plural, one {פריט # תואם} other {# פריטים תואמים}}",
     "repaired_items": "{count, plural, one {פריט # תוקן} other {# פריטים תוקנו}}",
     "require_password_change_on_login": "דרוש מהמשתמש לשנות סיסמה בכניסה הראשונה",
     "reset_settings_to_default": "אפס הגדרות לברירת המחדל",
     "reset_settings_to_recent_saved": "אפס הגדרות להגדרות שנשמרו לאחרונה",
-    "scanning_library_for_changed_files": "סורק ספרייה לאיתור קבצים שהשתנו",
-    "scanning_library_for_new_files": "סורק ספרייה לאיתור קבצים חדשים",
+    "scanning_library": "סורק ספרייה",
+    "search_jobs": "חיפוש עבודות...",
     "send_welcome_email": "שלח דוא\"ל ברוכים הבאים",
     "server_external_domain_settings": "דומיין חיצוני",
     "server_external_domain_settings_description": "דומיין עבור קישורים משותפים ציבוריים, כולל http(s)://",
+    "server_public_users": "משתמשים ציבוריים",
+    "server_public_users_description": "כל המשתמשים (שם ודוא\"ל) מופיעים בעת הוספת משתמש לאלבומים משותפים. כאשר התכונה מושבתת, רשימת המשתמשים תהיה זמינה רק למשתמשים בעלי הרשאות מנהל.",
     "server_settings": "הגדרות שרת",
     "server_settings_description": "נהל הגדרות שרת",
     "server_welcome_message": "הודעת פתיחה",
@@ -234,6 +249,17 @@
     "storage_template_settings_description": "נהל את מבנה התיקיות ואת שם הקובץ של נכס ההעלאה",
     "storage_template_user_label": "<code>{label}</code> היא תווית האחסון של המשתמש",
     "system_settings": "הגדרות מערכת",
+    "tag_cleanup_job": "ניקוי תגים",
+    "template_email_available_tags": "ניתן להשתמש במשתנים הבאים בתבנית שלך: {tags}",
+    "template_email_if_empty": "אם התבנית ריקה, ייעשה שימוש בדוא\"ל ברירת המחדל.",
+    "template_email_invite_album": "תבנית הזמנת אלבום",
+    "template_email_preview": "תצוגה מקדימה",
+    "template_email_settings": "תבניות דוא\"ל",
+    "template_email_settings_description": "נהל תבניות התראת דוא\"ל מותאמות אישית",
+    "template_email_update_album": "עדכון תבנית אלבום",
+    "template_email_welcome": "תבנית דוא\"ל ברוכים הבאים",
+    "template_settings": "תבניות התראה",
+    "template_settings_description": "נהל תבניות מותאמות אישית עבור התראות.",
     "theme_custom_css_settings": "CSS בהתאמה אישית",
     "theme_custom_css_settings_description": "גיליונות סגנון מדורגים (CSS) מאפשרים התאמה אישית של העיצוב של Immich.",
     "theme_settings": "הגדרות ערכת נושא",
@@ -241,7 +267,6 @@
     "these_files_matched_by_checksum": "קבצים אלה תואמים לפי סיכומי הביקורת שלהם",
     "thumbnail_generation_job": "צור תמונות ממוזערות",
     "thumbnail_generation_job_description": "יוצר תמונות ממוזערות גדולות, קטנות ומטושטשות עבור כל נכס, כמו גם תמונות ממוזערות עבור כל אדם",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "API האצה",
     "transcoding_acceleration_api_description": "ה-API שייצור אינטראקציה עם המכשיר שלך כדי להאיץ את המרת הקידוד. הגדרה זו היא 'המאמץ הטוב ביותר': היא תחזור לקידוד תוכנה במקרה של כשל. VP9 עשוי לעבוד או לא, תלוי בחומרה שלך.",
     "transcoding_acceleration_nvenc": "NVENC (דורש כרטיס מסך של NVIDIA)",
@@ -262,12 +287,12 @@
     "transcoding_constant_quality_mode": "מצב איכות קבועה",
     "transcoding_constant_quality_mode_description": "ICQ טוב יותר מ-CQP, אך חלק מהתקני האצת החומרה אינם תומכים במצב זה. הגדרת אפשרות זו תעדיף את המצב שצוין בעת שימוש בקידוד מבוסס איכות. בהתעלמות מצד NVENC מכיוון שהוא אינו תומך ב-ICQ.",
     "transcoding_constant_rate_factor": "גורם קצב קבוע (-crf)",
-    "transcoding_constant_rate_factor_description": "רמת איכות וידאו. ערכים אופייניים הם הערך 23 עבור H.264, הערך 28 עבור HEVC, הערך 31 עבור VP9, והערך 35 עבור AV1. נמוך יותר טוב יותר, אבל מייצר קבצים גדולים יותר.",
+    "transcoding_constant_rate_factor_description": "רמת איכות וידאו. ערכים אופייניים הם הערך 23 עבור H.264, הערך 28 עבור HEVC, הערך 31 עבור VP9, והערך 35 עבור AV1. נמוך יותר הוא טוב יותר, אבל מייצר קבצים גדולים יותר.",
     "transcoding_disabled_description": "אין להמיר את הקידוד של שום סרטון, עלול לגרום לכך שהניגון לא יפעל במכשירים מסוימים",
     "transcoding_hardware_acceleration": "האצת חומרה",
     "transcoding_hardware_acceleration_description": "ניסיוני; המרה הרבה יותר מהירה, אבל תהיה באיכות נמוכה יותר באותו קצב סיביות",
     "transcoding_hardware_decoding": "פענוח חומרה",
-    "transcoding_hardware_decoding_setting_description": "חל רק על NVENC, QSV ו-RKMPP. מאפשר האצה מקצה לקצה במקום רק להאיץ קידוד. ייתכן שלא יפעל על כל הסרטונים.",
+    "transcoding_hardware_decoding_setting_description": "מאפשר האצה מקצה לקצה במקום רק האצת קידוד. ייתכן שלא יפעל על כל הסרטונים.",
     "transcoding_hevc_codec": "קידוד HEVC",
     "transcoding_max_b_frames": "B-פריימים מרביים",
     "transcoding_max_b_frames_description": "ערכים גבוהים יותר משפרים את יעילות הדחיסה, אך מאטים את הקידוד. ייתכן שלא יהיה תואם עם האצת חומרה במכשירים ישנים יותר. 0 משבית את B-פריימים, בעוד ש1- מגדיר את הערך זה באופן אוטומטי.",
@@ -293,8 +318,6 @@
     "transcoding_threads_description": "ערכים גבוהים יותר מובילים לקידוד מהיר יותר, אך משאירים פחות מקום לשרת לעבד משימות אחרות בעודו פעיל. ערך זה לא אמור להיות יותר ממספר ליבות המעבד. ממקסם את הניצול אם מוגדר ל-0.",
     "transcoding_tone_mapping": "מיפוי גוונים",
     "transcoding_tone_mapping_description": "מנסה לשמר את המראה של סרטוני HDR כשהם מומרים ל-SDR. כל אלגוריתם עושה פשרות שונות עבור צבע, פירוט ובהירות. Hable משמר פרטים, Mobius משמר צבע, ו-Reinhard משמר בהירות.",
-    "transcoding_tone_mapping_npl": "בהירות שיא נומינלית למיפוי גוונים",
-    "transcoding_tone_mapping_npl_description": "הצבעים יותאמו כך שיראו נורמליים לתצוגה של בהירות זו. באופן מנוגד לאינטואיציה, ערכים נמוכים מגבירים את בהירות הווידאו ולהפך מכיוון שזה מפצה על בהירות התצוגה. 0 מגדיר ערך זה באופן אוטומטי.",
     "transcoding_transcode_policy": "מדיניות המרת קידוד",
     "transcoding_transcode_policy_description": "מדיניות לגבי מתי יש להמיר קידוד של סרטון. תמיד יומר הקידוד של סרטוני HDR (למעט אם המרת קידוד מושבתת).",
     "transcoding_two_pass_encoding": "קידוד בשני מעברים",
@@ -308,6 +331,7 @@
     "trash_settings_description": "נהל את הגדרות האשפה",
     "untracked_files": "קבצים ללא מעקב",
     "untracked_files_description": "קבצים אלה אינם נמצאים במעקב של היישום. הם יכולים להיות תוצאות של העברות כושלות, העלאות שנקטעו, או שנותרו מאחור בגלל שיבוש בתוכנה",
+    "user_cleanup_job": "ניקוי משתמשים",
     "user_delete_delay": "החשבון והנכסים של <b>{user}</b> יתוזמנו למחיקה לצמיתות בעוד {delay, plural, one {יום #} other {# ימים}}.",
     "user_delete_delay_settings": "עיכוב מחיקה",
     "user_delete_delay_settings_description": "מספר הימים לאחר ההסרה עד מחיקה לצמיתות של החשבון והנכסים של המשתמש. משימת מחיקת המשתמש פועלת בחצות כדי לבדוק אם יש משתמשים שמוכנים למחיקה. שינויים בהגדרה זו יוערכו בביצוע הבא.",
@@ -374,7 +398,6 @@
   "archive_or_unarchive_photo": "העבר תמונה לארכיון או הוצא אותה משם",
   "archive_size": "גודל הארכיון",
   "archive_size_description": "הגדר את גודל הארכיון להורדות (ב-GiB)",
-  "archived": "בארכיון",
   "archived_count": "{count, plural, other {# הועברו לארכיון}}",
   "are_these_the_same_person": "האם אלה אותו האדם?",
   "are_you_sure_to_do_this": "האם את/ה בטוח/ה שברצונך לעשות את זה?",
@@ -385,8 +408,9 @@
   "asset_has_unassigned_faces": "לנכס יש פנים שלא הוקצו",
   "asset_hashing": "מגבב...",
   "asset_offline": "נכס לא מקוון",
-  "asset_offline_description": "הנכס הזה אינו מקוון. Immich לא יכול לגשת למיקום הקובץ שלו. נא לוודא שהנכס זמין ואז סרוק מחדש את הספרייה.",
+  "asset_offline_description": "הנכס החיצוני הזה כבר לא נמצא בדיסק. אנא צור קשר עם מנהל Immich שלך לקבלת עזרה.",
   "asset_skipped": "דילג",
+  "asset_skipped_in_trash": "באשפה",
   "asset_uploaded": "הועלה",
   "asset_uploading": "מעלה...",
   "assets": "נכסים",
@@ -394,11 +418,10 @@
   "assets_added_to_album_count": "{count, plural, one {נוסף נכס #} other {נוספו # נכסים}} לאלבום",
   "assets_added_to_name_count": "{count, plural, one {נכס # נוסף} other {# נכסים נוספו}} אל {hasName, select, true {<b>{name}</b>} other {אלבום חדש}}",
   "assets_count": "{count, plural, one {נכס #} other {# נכסים}}",
-  "assets_moved_to_trash": "Moved {count, plural, one {# asset} other {# assets}} to trash",
   "assets_moved_to_trash_count": "{count, plural, one {נכס # הועבר} other {# נכסים הועברו}} לאשפה",
   "assets_permanently_deleted_count": "{count, plural, one {נכס # נמחק} other {# נכסים נמחקו}} לצמיתות",
   "assets_removed_count": "{count, plural, one {נכס # הוסר} other {# נכסים הוסרו}}",
-  "assets_restore_confirmation": "האם את/ה בטוח/ה שברצונך לשחזר את כל הנכסים שבאשפה? את/ה לא יכול/ה לבטל את הפעולה הזו!",
+  "assets_restore_confirmation": "האם את/ה בטוח/ה שברצונך לשחזר את כל הנכסים שבאשפה? את/ה לא יכול/ה לבטל את הפעולה הזו! שים לב שלא ניתן לשחזר נכסים לא מקוונים בדרך זו.",
   "assets_restored_count": "{count, plural, one {נכס # שוחזר} other {# נכסים שוחזרו}}",
   "assets_trashed_count": "{count, plural, one {נכס # הושלך} other {# נכסים הושלכו}} לאשפה",
   "assets_were_part_of_album_count": "{count, plural, one {נכס היה} other {נכסים היו}} כבר חלק מהאלבום",
@@ -409,6 +432,7 @@
   "birthdate_saved": "תאריך לידה נשמר בהצלחה",
   "birthdate_set_description": "תאריך לידה משמש לחישוב הגיל של האדם הזה בזמן תצלום.",
   "blurred_background": "רקע מטושטש",
+  "bugs_and_feature_requests": "באגים & בקשות לתכונות",
   "build": "Build",
   "build_image": "Build Image",
   "bulk_delete_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך למחוק בכמות גדולה {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה ישמור על הנכס הכי גדול של כל קבוצה וימחק לצמיתות את כל שאר הכפילויות. את/ה לא יכול/ה לבטל את הפעולה הזו!",
@@ -423,10 +447,6 @@
   "cannot_merge_people": "לא ניתן למזג אנשים",
   "cannot_undo_this_action": "את/ה לא יכול/ה לבטל את הפעולה הזו!",
   "cannot_update_the_description": "לא ניתן לעדכן את התיאור",
-  "cant_apply_changes": "לא ניתן להחיל שינויים",
-  "cant_get_faces": "לא ניתן לאחזר פרצופים",
-  "cant_search_people": "לא ניתן לחפש אנשים",
-  "cant_search_places": "לא ניתן לחפש מקומות",
   "change_date": "שנה תאריך",
   "change_expiration_time": "שנה את זמן התפוגה",
   "change_location": "שנה מיקום",
@@ -458,6 +478,7 @@
   "confirm": "אישור",
   "confirm_admin_password": "אשר סיסמת מנהל",
   "confirm_delete_shared_link": "האם את/ה בטוח/ה שברצונך למחוק את הקישור המשותף הזה?",
+  "confirm_keep_this_delete_others": "כל שאר הנכסים בערימה יימחקו למעט נכס זה. האם את/ה בטוח/ה שברצונך להמשיך?",
   "confirm_password": "אשר סיסמה",
   "contain": "מכיל",
   "context": "הקשר",
@@ -507,16 +528,19 @@
   "delete_key": "מחק מפתח",
   "delete_library": "מחק ספרייה",
   "delete_link": "מחק קישור",
+  "delete_others": "מחק אחרים",
   "delete_shared_link": "מחק קישור משותף",
   "delete_tag": "מחק תג",
   "delete_tag_confirmation_prompt": "האם את/ה בטוח/ה שברצונך למחוק תג {tagName}?",
   "delete_user": "מחק משתמש",
   "deleted_shared_link": "קישור משותף נמחק",
+  "deletes_missing_assets": "מוחק נכסים שחסרים בדיסק",
   "description": "תיאור",
   "details": "פרטים",
   "direction": "כיוון",
   "disabled": "מושבת",
   "disallow_edits": "אל תאפשר עריכות",
+  "discord": "דיסקורד",
   "discover": "גלה",
   "dismiss_all_errors": "התעלם מכל השגיאות",
   "dismiss_error": "התעלם מהשגיאה",
@@ -525,6 +549,7 @@
   "display_original_photos": "הצג תמונות מקוריות",
   "display_original_photos_setting_description": "העדף להציג את התמונה המקורית בעת צפיית נכס במקום תמונות ממוזערות כאשר הנכס המקורי תומך בתצוגה בדפדפן. זה עלול לגרום לתמונות להיות מוצגות באיטיות.",
   "do_not_show_again": "אל תציג את ההודעה הזאת שוב",
+  "documentation": "תיעוד",
   "done": "סיום",
   "download": "הורדה",
   "download_include_embedded_motion_videos": "סרטונים מוטמעים",
@@ -537,13 +562,6 @@
   "duplicates": "כפילויות",
   "duplicates_description": "הפרד כל קבוצה על ידי ציון אילו, אם בכלל, הן כפילויות",
   "duration": "משך זמן",
-  "durations": {
-    "days": "{days, plural, one {day} other {{days, number} days}}",
-    "hours": "{hours, plural, one {hour} other {{hours, number} hours}}",
-    "minutes": "{minutes, plural, one {minute} other {{minutes, number} minutes}}",
-    "months": "{months, plural, one {month} other {{months, number} months}}",
-    "years": "{years, plural, one {year} other {{years, number} years}}"
-  },
   "edit": "ערוך",
   "edit_album": "ערוך אלבום",
   "edit_avatar": "ערוך תמונת פרופיל",
@@ -568,8 +586,6 @@
   "editor_crop_tool_h2_aspect_ratios": "יחסי רוחב גובה",
   "editor_crop_tool_h2_rotation": "סיבוב",
   "email": "דוא\"ל",
-  "empty": "",
-  "empty_album": "אלבום ריק",
   "empty_trash": "רוקן אשפה",
   "empty_trash_confirmation": "האם את/ה בטוח/ה שברצונך לרוקן את האשפה? זה יסיר לצמיתות את כל הנכסים באשפה מImmich.\nאת/ה לא יכול/ה לבטל פעולה זו!",
   "enable": "אפשר",
@@ -603,6 +619,7 @@
     "failed_to_create_shared_link": "יצירת קישור משותף נכשלה",
     "failed_to_edit_shared_link": "עריכת קישור משותף נכשלה",
     "failed_to_get_people": "קבלת אנשים נכשלה",
+    "failed_to_keep_this_delete_others": "נכשל לשמור את הנכס הזה ולמחוק את הנכסים האחרים",
     "failed_to_load_asset": "טעינת נכס נכשלה",
     "failed_to_load_assets": "טעינת נכסים נכשלה",
     "failed_to_load_people": "נכשל באחזור אנשים",
@@ -630,8 +647,6 @@
     "unable_to_change_location": "לא ניתן לשנות מיקום",
     "unable_to_change_password": "לא ניתן לשנות סיסמה",
     "unable_to_change_visibility": "לא ניתן לשנות את הנראות עבור {count, plural, one {אדם #} other {# אנשים}}",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_complete_oauth_login": "לא ניתן להשלים התחברות OAuth",
     "unable_to_connect": "לא ניתן להתחבר",
     "unable_to_connect_to_server": "לא ניתן להתחבר לשרת",
@@ -656,6 +671,7 @@
     "unable_to_get_comments_number": "לא ניתן להשיג את מספר התגובות",
     "unable_to_get_shared_link": "קבלת קישור משותף נכשלה",
     "unable_to_hide_person": "לא ניתן להסתיר אדם",
+    "unable_to_link_motion_video": "לא ניתן לקשר סרטון תנועה",
     "unable_to_link_oauth_account": "לא ניתן לקשר חשבון OAuth",
     "unable_to_load_album": "לא ניתן לטעון אלבום",
     "unable_to_load_asset_activity": "לא ניתן לטעון את פעילות הנכס",
@@ -671,12 +687,10 @@
     "unable_to_remove_album_users": "לא ניתן להסיר משתמשים מהאלבום",
     "unable_to_remove_api_key": "לא ניתן להסיר מפתח API",
     "unable_to_remove_assets_from_shared_link": "לא ניתן להסיר נכסים מקישור משותף",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_deleted_assets": "לא ניתן להסיר קבצים לא מקוונים",
     "unable_to_remove_library": "לא ניתן להסיר ספרייה",
-    "unable_to_remove_offline_files": "לא ניתן להסיר קבצים לא מקוונים",
     "unable_to_remove_partner": "לא ניתן להסיר שותף",
     "unable_to_remove_reaction": "לא ניתן להסיר תגובה",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "לא ניתן לתקן פריטים",
     "unable_to_reset_password": "לא ניתן לאפס סיסמה",
     "unable_to_resolve_duplicate": "לא ניתן לפתור כפילות",
@@ -696,6 +710,7 @@
     "unable_to_submit_job": "לא ניתן לשלוח משימה",
     "unable_to_trash_asset": "לא ניתן להעביר נכס לאשפה",
     "unable_to_unlink_account": "לא ניתן לבטל קישור חשבון",
+    "unable_to_unlink_motion_video": "לא ניתן לבטל קישור סרטון תנועה",
     "unable_to_update_album_cover": "לא ניתן לעדכן עטיפת אלבום",
     "unable_to_update_album_info": "לא ניתן לעדכן פרטי אלבום",
     "unable_to_update_library": "לא ניתן לעדכן ספרייה",
@@ -705,10 +720,6 @@
     "unable_to_update_user": "לא ניתן לעדכן משתמש",
     "unable_to_upload_file": "לא ניתן להעלות קובץ"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exif": "Exif",
   "exit_slideshow": "צא ממצגת שקופיות",
   "expand_all": "הרחב הכל",
@@ -723,33 +734,28 @@
   "external": "חיצוני",
   "external_libraries": "ספריות חיצוניות",
   "face_unassigned": "לא מוקצה",
-  "failed_to_get_people": "נכשל באחזור אנשים",
+  "failed_to_load_assets": "טעינת נכסים נכשלה",
   "favorite": "מועדף",
   "favorite_or_unfavorite_photo": "הוסף או הסר תמונה מהמועדפים",
   "favorites": "מועדפים",
-  "feature": "",
   "feature_photo_updated": "תמונה מייצגת עודכנה",
-  "featurecollection": "",
   "features": "תכונות",
   "features_setting_description": "נהל את תכונות היישום",
   "file_name": "שם הקובץ",
   "file_name_or_extension": "שם קובץ או סיומת",
   "filename": "שם קובץ",
-  "files": "",
   "filetype": "סוג קובץ",
   "filter_people": "סנן אנשים",
   "find_them_fast": "מצא אותם מהר לפי שם עם חיפוש",
   "fix_incorrect_match": "תקן התאמה שגויה",
   "folders": "תיקיות",
   "folders_feature_description": "עיון בתצוגת התיקייה עבור התמונות והסרטונים שבמערכת הקבצים",
-  "force_re-scan_library_files": "כפה סריקה מחדש של כל קבצי הספרייה",
   "forward": "קדימה",
   "general": "כללי",
   "get_help": "קבל עזרה",
   "getting_started": "תחילת העבודה",
   "go_back": "חזור",
   "go_to_search": "עבור לחיפוש",
-  "go_to_share_page": "עבור לדף השיתוף",
   "group_albums_by": "קבץ אלבומים לפי..",
   "group_no": "אין קיבוץ",
   "group_owner": "קבץ לפי בעלים",
@@ -775,10 +781,6 @@
   "image_alt_text_date_place_2_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{city}, {country} עם {person1} ו-{person2} ב-{date}",
   "image_alt_text_date_place_3_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{city}, {country} עם {person1}, {person2}, ו-{person3} ב-{date}",
   "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{city}, {country} עם {person1}, {person2}, ו-{additionalCount, number} אחרים ב-{date}",
-  "image_alt_text_people": "{count, plural, =1 {עם {person1}} =2 {עם {person1} ו{person2}} =3 {עם {person1}, {person2}, ו{person3}} other {עם {person1}, {person2}, ו{others, number} אחרים}}",
-  "image_alt_text_place": "ב{city}, {country}",
-  "image_taken": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}}",
-  "img": "",
   "immich_logo": "הלוגו של Immich",
   "immich_web_interface": "ממשק האינטרנט של Immich",
   "import_from_json": "ייבוא מ-JSON",
@@ -799,10 +801,11 @@
   "invite_people": "הזמן אנשים",
   "invite_to_album": "הזמן לאלבום",
   "items_count": "{count, plural, one {פריט #} other {# פריטים}}",
-  "job_settings_description": "",
   "jobs": "משימות",
   "keep": "שמור",
   "keep_all": "שמור הכל",
+  "keep_this_delete_others": "שמור על זה, מחק אחרים",
+  "kept_this_deleted_others": "נכס זה נשמר ונמחקו {count, plural, one {נכס #} other {# נכסים}}",
   "keyboard_shortcuts": "קיצורי מקלדת",
   "language": "שפה",
   "language_setting_description": "בחר/י את השפה המועדפת עליך",
@@ -814,33 +817,9 @@
   "level": "רמה",
   "library": "ספרייה",
   "library_options": "אפשרויות ספרייה",
-  "license_account_info": "החשבון שלך מורשה",
-  "license_activated_subtitle": "תודה לך על התמיכה ב-Immich ובתוכנות קוד פתוח",
-  "license_activated_title": "הרישיון שלך הופעל בהצלחה",
-  "license_button_activate": "הפעל",
-  "license_button_buy": "קנה",
-  "license_button_buy_license": "קנה רישיון",
-  "license_button_select": "בחר",
-  "license_failed_activation": "הפעלת הרישיון נכשלה. נא לבדוק את הדוא\"ל שלך כדי למצוא את מפתח הרישיון הנכון!",
-  "license_individual_description_1": "רישיון 1 למשתמש בכל שרת",
-  "license_individual_title": "רישיון אישי",
-  "license_info_licensed": "מורשה",
-  "license_info_unlicensed": "ללא רשיון",
-  "license_input_suggestion": "יש לך רישיון? הזן את המפתח למטה",
-  "license_license_subtitle": "רכוש רישיון כדי לתמוך ב Immich",
-  "license_license_title": "רישיון",
-  "license_lifetime_description": "רישיון לכל החיים",
-  "license_per_server": "עבור שרת",
-  "license_per_user": "עבור משתמש",
-  "license_server_description_1": "רישיון 1 עבור שרת",
-  "license_server_description_2": "רישיון לכל המשתמשים בשרת",
-  "license_server_title": "רישיון שרת",
-  "license_trial_info_1": "אתה מפעיל גרסה ללא רישיון של Immich",
-  "license_trial_info_2": "אתה משתמש ב Immich קרוב ל",
-  "license_trial_info_3": "{accountAge, plural, one {יום #} other {# ימים}}",
-  "license_trial_info_4": "אנא שקול לרכוש רישיון כדי לתמוך בפיתוח המתמשך של השירות",
   "light": "בהיר",
   "like_deleted": "לייק נמחק",
+  "link_motion_video": "קשר סרטון תנועה",
   "link_options": "אפשרויות קישור",
   "link_to_oauth": "קישור ל-OAuth",
   "linked_oauth_account": "חשבון OAuth מקושר",
@@ -859,6 +838,7 @@
   "look": "מראה",
   "loop_videos": "הפעלה חוזרת של סרטונים",
   "loop_videos_description": "אפשר הפעלה חוזרת אוטומטית של סרטון במציג הפרטים.",
+  "main_branch_warning": "את/ה משתמש/ת בגרסת פיתוח; אנחנו ממליצים בחום להשתמש בגרסה יציבה!",
   "make": "תוצרת",
   "manage_shared_links": "נהל קישורים משותפים",
   "manage_sharing_with_partners": "נהל שיתוף עם שותפים",
@@ -928,6 +908,7 @@
   "notifications": "התראות",
   "notifications_setting_description": "נהל התראות",
   "oauth": "OAuth",
+  "official_immich_resources": "משאבי Immich רשמיים",
   "offline": "לא מקוון",
   "offline_paths": "נתיבים לא מקוונים",
   "offline_paths_description": "תוצאות אלו עשויות להיות עקב מחיקה ידנית של קבצים שאינם חלק מספרייה חיצונית.",
@@ -940,7 +921,6 @@
   "onboarding_welcome_user": "ברוכ/ה הבא/ה, {user}",
   "online": "מקוון",
   "only_favorites": "רק מועדפים",
-  "only_refreshes_modified_files": "מרענן רק קבצים שהשתנו",
   "open_in_map_view": "פתח בתצוגת מפה",
   "open_in_openstreetmap": "פתח ב-OpenStreetMap",
   "open_the_search_filters": "פתח את מסנני החיפוש",
@@ -978,14 +958,12 @@
   "people_edits_count": "{count, plural, one {אדם # נערך} other {# אנשים נערכו}}",
   "people_feature_description": "עיון בתמונות וסרטונים שקובצו על ידי אנשים",
   "people_sidebar_description": "הצג קישור אל אנשים בסרגל הצד",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "אזהרת מחיקה לצמיתות",
   "permanent_deletion_warning_setting_description": "הצג אזהרה בעת מחיקת נכסים לצמיתות",
   "permanently_delete": "מחק לצמיתות",
   "permanently_delete_assets_count": "מחק לצמיתות {count, plural, one {נכס} other {נכסים}}",
   "permanently_delete_assets_prompt": "האם את/ה בטוח/ה שברצונך למחוק לצמיתות {count, plural, one {נכס זה?} other {<b>#</b> נכסים אלה?}}זה גם יסיר {count, plural, one {אותו מאלבומו} other {אותם מאלבומם}}.",
   "permanently_deleted_asset": "נכס נמחק לצמיתות",
-  "permanently_deleted_assets": "Permanently deleted {count, plural, one {# asset} other {# assets}}",
   "permanently_deleted_assets_count": "{count, plural, one {נכס # נמחק} other {# נכסים נמחקו}} לצמיתות",
   "person": "אדם",
   "person_hidden": "{name}{hidden, select, true { (מוסתר)} other {}}",
@@ -1001,7 +979,6 @@
   "play_memories": "נגן זכרונות",
   "play_motion_photo": "הפעל תמונה עם תנועה",
   "play_or_pause_video": "הפעל או השהה סרטון",
-  "point": "",
   "port": "יציאה",
   "preset": "הגדרות קבועות מראש",
   "preview": "תצוגה מקדימה",
@@ -1046,12 +1023,10 @@
   "purchase_server_description_2": "מעמד תומך",
   "purchase_server_title": "שרת",
   "purchase_settings_server_activated": "מפתח המוצר של השרת מנוהל על ידי מנהל המערכת",
-  "range": "",
   "rating": "דירוג כוכב",
   "rating_clear": "נקה דירוג",
   "rating_count": "{count, plural, one {כוכב #} other {# כוכבים}}",
   "rating_description": "הצג את דירוג ה-EXIF בלוח המידע",
-  "raw": "",
   "reaction_options": "אפשרויות הגבה",
   "read_changelog": "קרא את יומן השינויים",
   "reassign": "הקצה מחדש",
@@ -1059,14 +1034,17 @@
   "reassigned_assets_to_new_person": "{count, plural, one {נכס # הוקצה} other {# נכסים הוקצו}} מחדש לאדם חדש",
   "reassing_hint": "הקצה נכסים שנבחרו לאדם קיים",
   "recent": "חדש",
+  "recent-albums": "אלבומים אחרונים",
   "recent_searches": "חיפושים אחרונים",
   "refresh": "רענן",
   "refresh_encoded_videos": "רענן סרטונים מקודדים",
+  "refresh_faces": "רענן פנים",
   "refresh_metadata": "רענן מטא-נתונים",
   "refresh_thumbnails": "רענן תמונות ממוזערות",
   "refreshed": "רוענן",
-  "refreshes_every_file": "מרענן כל קובץ",
+  "refreshes_every_file": "קורא מחדש את כל הקבצים הקיימים והחדשים",
   "refreshing_encoded_video": "מרענן סרטון מקודד",
+  "refreshing_faces": "מרענן פרצופים",
   "refreshing_metadata": "מרענן מטא-נתונים",
   "regenerating_thumbnails": "מחדש תמונות ממוזערות",
   "remove": "הסר",
@@ -1074,10 +1052,11 @@
   "remove_assets_shared_link_confirmation": "האם את/ה בטוח/ה שברצונך להסיר {count, plural, one {נכס #} other {# נכסים}} מהקישור המשותף הזה?",
   "remove_assets_title": "הסר נכסים?",
   "remove_custom_date_range": "הסר טווח תאריכים מותאם",
+  "remove_deleted_assets": "הסר קבצים לא מקוונים",
   "remove_from_album": "הסר מאלבום",
   "remove_from_favorites": "הסר מהמועדפים",
   "remove_from_shared_link": "הסר מקישור משותף",
-  "remove_offline_files": "הסר קבצים לא מקוונים",
+  "remove_url": "הסר URL",
   "remove_user": "הסר משתמש",
   "removed_api_key": "מפתח API הוסר: {name}",
   "removed_from_archive": "הוסר מארכיון",
@@ -1094,7 +1073,6 @@
   "reset": "איפוס",
   "reset_password": "איפוס סיסמה",
   "reset_people_visibility": "אפס את נראות האנשים",
-  "reset_settings_to_default": "",
   "reset_to_default": "אפס לברירת מחדל",
   "resolve_duplicates": "פתור כפילויות",
   "resolved_all_duplicates": "כל הכפילויות נפתרו",
@@ -1114,8 +1092,7 @@
   "saved_settings": "הגדרות שמורות",
   "say_something": "תגיד/י משהו",
   "scan_all_libraries": "סרוק את כל הספריות",
-  "scan_all_library_files": "סרוק מחדש את כל קבצי הספרייה",
-  "scan_new_library_files": "סרוק קבצי ספרייה חדשים",
+  "scan_library": "סרוק",
   "scan_settings": "הגדרות סריקה",
   "scanning_for_album": "סורק אחר אלבום...",
   "search": "חפש",
@@ -1130,8 +1107,10 @@
   "search_for_existing_person": "חפש אדם קיים",
   "search_no_people": "אין אנשים",
   "search_no_people_named": "אין אנשים בשם \"{name}\"",
+  "search_options": "אפשרויות חיפוש",
   "search_people": "חפש אנשים",
   "search_places": "חפש מקומות",
+  "search_settings": "הגדרות חיפוש",
   "search_state": "חפש מדינה...",
   "search_tags": "חיפוש תגים...",
   "search_timezone": "חפש אזור זמן...",
@@ -1156,7 +1135,6 @@
   "selected_count": "{count, plural, other {# נבחרו}}",
   "send_message": "שלח הודעה",
   "send_welcome_email": "שלח דוא\"ל קבלת פנים",
-  "server": "שרת",
   "server_offline": "שרת לא מקוון",
   "server_online": "שרת מקוון",
   "server_stats": "סטטיסטיקות שרת",
@@ -1199,6 +1177,7 @@
   "show_person_options": "הצג אפשרויות אדם",
   "show_progress_bar": "הצג סרגל התקדמות",
   "show_search_options": "הצג אפשרויות חיפוש",
+  "show_slideshow_transition": "הצג מעבר מצגת",
   "show_supporter_badge": "תג תומך",
   "show_supporter_badge_description": "הצג תג תומך",
   "shuffle": "ערבוב",
@@ -1208,6 +1187,8 @@
   "sign_up": "הרשמה",
   "size": "גודל",
   "skip_to_content": "דלג לתוכן",
+  "skip_to_folders": "דלג לתיקיות",
+  "skip_to_tags": "דלג לתגים",
   "slideshow": "מצגת שקופיות",
   "slideshow_settings": "הגדרות מצגת שקופיות",
   "sort_albums_by": "מיין אלבומים לפי...",
@@ -1238,13 +1219,16 @@
   "submit": "שלח",
   "suggestions": "הצעות",
   "sunrise_on_the_beach": "Sunrise on the beach (מומלץ לחפש באנגלית לתוצאות טובות יותר)",
+  "support": "תמיכה",
+  "support_and_feedback": "תמיכה & משוב",
+  "support_third_party_description": "התקנת ה-Immich שלך נארזה על ידי צד שלישי. בעיות שאתה חווה עשויות להיגרם על ידי חבילה זו, אז בבקשה תעלה בעיות איתם ראשית כל באמצעות הקישורים למטה.",
   "swap_merge_direction": "החלף כיוון מיזוג",
   "sync": "סנכרן",
   "tag": "תג",
   "tag_assets": "תיוג נכסים",
   "tag_created": "נוצר תג: {tag}",
   "tag_feature_description": "עיון בתמונות וסרטונים שקובצו על ידי נושאי תג לוגיים",
-  "tag_not_found_question": "לא מצליח למצוא תג? צור אחד <link>כאן</link>",
+  "tag_not_found_question": "לא מצליח למצוא תג? <link>צור תג חדש</link>",
   "tag_updated": "תג מעודכן: {tag}",
   "tagged_assets": "תויגו {count, plural, one {נכס #} other {# נכסים}}",
   "tags": "תגים",
@@ -1253,18 +1237,19 @@
   "theme_selection": "בחירת ערכת נושא",
   "theme_selection_description": "הגדר אוטומטית את ערכת הנושא לבהיר או כהה בהתבסס על העדפת המערכת של הדפדפן שלך",
   "they_will_be_merged_together": "הם יתמזגו יחד",
+  "third_party_resources": "משאבי צד שלישי",
   "time_based_memories": "זכרונות מבוססי זמן",
+  "timeline": "ציר זמן",
   "timezone": "אזור זמן",
   "to_archive": "העבר לארכיון",
   "to_change_password": "שנה סיסמה",
   "to_favorite": "מועדף",
   "to_login": "כניסה",
   "to_parent": "לך להורה",
-  "to_root": "לשורש",
   "to_trash": "אשפה",
   "toggle_settings": "החלף מצב הגדרות",
   "toggle_theme": "החלף ערכת נושא כהה",
-  "toggle_visibility": "החלף נראות",
+  "total": "סה\"כ",
   "total_usage": "שימוש כולל",
   "trash": "אשפה",
   "trash_all": "העבר הכל לאשפה",
@@ -1274,14 +1259,13 @@
   "trashed_items_will_be_permanently_deleted_after": "פריטים באשפה ימחקו לצמיתות לאחר {days, plural, one {יום #} other {# ימים}}.",
   "type": "סוג",
   "unarchive": "הוצא מארכיון",
-  "unarchived": "הוצא מהארכיון",
   "unarchived_count": "{count, plural, other {# הוצאו מהארכיון}}",
   "unfavorite": "לא מועדף",
   "unhide_person": "בטל הסתרת אדם",
   "unknown": "לא ידוע",
-  "unknown_album": "אלבום לא ידוע",
   "unknown_year": "שנה לא ידועה",
   "unlimited": "בלתי מוגבל",
+  "unlink_motion_video": "בטל קישור סרטון תנועה",
   "unlink_oauth": "בטל קישור OAuth",
   "unlinked_oauth_account": "בוטל קישור חשבון OAuth",
   "unnamed_album": "אלבום ללא שם",
@@ -1310,13 +1294,13 @@
   "use_custom_date_range": "השתמש בטווח תאריכים מותאם במקום",
   "user": "משתמש",
   "user_id": "מזהה משתמש",
-  "user_license_settings": "רישיון",
-  "user_license_settings_description": "נהל את הרישיון שלך",
   "user_liked": "{user} אהב את {type, select, photo {התמונה הזאת} video {הסרטון הזה} asset {הנכס הזה} other {זה}}",
   "user_purchase_settings": "רכישה",
   "user_purchase_settings_description": "נהל את הרכישה שלך",
   "user_role_set": "הגדר את {user} בתור {role}",
   "user_usage_detail": "פרטי השימוש של המשתמש",
+  "user_usage_stats": "סטטיסטיקות שימוש בחשבון",
+  "user_usage_stats_description": "הצג סטטיסטיקות שימוש בחשבון",
   "username": "שם משתמש",
   "users": "משתמשים",
   "utilities": "כלים",
@@ -1324,7 +1308,9 @@
   "variables": "משתנים",
   "version": "גרסה",
   "version_announcement_closing": "החבר שלך, אלכס",
-  "version_announcement_message": "הי חבר/ה, יש מהדורה חדשה של היישום, אנא קח/י את הזמן שלך לבקר ב <link>הערות פרסום</link> ולוודא שמבנה ה-<code>docker-compose.yml</code>, וה-<code>.env</code> שלך עדכני כדי למנוע תצורות שגויות, במיוחד אם את/ה משתמש/ת ב-WatchTower או בכל מנגנון שמטפל בעדכון היישום שלך באופן אוטומטי.",
+  "version_announcement_message": "שלום לך! זמינה גרסה חדשה של Immich. אנא קח/י זמן מה לקרוא את <link>הערות הפרסום</link> כדי לוודא שההתקנה שלך עדכנית על מנת למנוע תצורות שגויות, במיוחד אם את/ה משתמש/ת ב-WatchTower או בכל מנגנון שמטפל בעדכון מופע ה-Immich שלך באופן אוטומטי.",
+  "version_history": "היסטוריית גרסאות",
+  "version_history_item": "{version} הותקנה ב-{date}",
   "video": "סרטון",
   "video_hover_setting": "הפעל תצוגת סרטון מקדימה בעת ריחוף",
   "video_hover_setting_description": "הפעל תצוגת סרטון מקדימה כאשר העכבר מרחף מעל פריט. אפילו כשהגדרה זו מושבתת, ניתן להתחיל את הניגון על ידי ריחוף מעל סמל ההפעלה.",
@@ -1336,10 +1322,10 @@
   "view_all_users": "הצג את כל המשתמשים",
   "view_in_timeline": "ראה בציר הזמן",
   "view_links": "הצג קישורים",
+  "view_name": "הצג",
   "view_next_asset": "הצג את הנכס הבא",
   "view_previous_asset": "הצג את הנכס הקודם",
   "view_stack": "הצג ערימה",
-  "viewer": "מציג",
   "visibility_changed": "הנראות השתנתה עבור {count, plural, one {אדם #} other {# אנשים}}",
   "waiting": "ממתין",
   "warning": "אזהרה",
@@ -1350,5 +1336,5 @@
   "years_ago": "לפני {years, plural, one {שנה #} other {# שנים}}",
   "yes": "כן",
   "you_dont_have_any_shared_links": "אין לך קישורים משותפים",
-  "zoom_image": "התקרב לתמונה"
+  "zoom_image": "זום לתמונה"
 }
diff --git a/web/src/lib/i18n/hi.json b/i18n/hi.json
similarity index 95%
rename from web/src/lib/i18n/hi.json
rename to i18n/hi.json
index 2f2aabfb7e..5df14e7296 100644
--- a/web/src/lib/i18n/hi.json
+++ b/i18n/hi.json
@@ -23,6 +23,7 @@
   "add_to": "इसमें जोड़ें..।",
   "add_to_album": "एल्बम में जोड़ें",
   "add_to_shared_album": "साझा एल्बम में जोड़ें",
+  "add_url": "URL जोड़ें",
   "added_to_archive": "संग्रहीत कर दिया गया है",
   "added_to_favorites": "पसंदीदा में जोड़ा गया",
   "added_to_favorites_count": "पसंदीदा में {count, number} जोड़ा गया",
@@ -41,9 +42,7 @@
     "confirm_email_below": "पुष्टि करने के लिए नीचे \"{email}\" टाइप करें",
     "confirm_reprocess_all_faces": "क्या आप वाकई सभी चेहरों को दोबारा संसाधित करना चाहते हैं? इससे नामित लोग भी साफ हो जायेंगे।",
     "confirm_user_password_reset": "क्या आप वाकई {user} का पासवर्ड रीसेट करना चाहते हैं?",
-    "crontab_guru": "",
     "disable_login": "लॉगिन अक्षम करें",
-    "disabled": "",
     "duplicate_detection_job_description": "समान छवियों का पता लगाने के लिए संपत्तियों पर मशीन लर्निंग चलाएं। यह कार्यक्षमता स्मार्ट खोज पर निर्भर करती है",
     "exclusion_pattern_description": "Exclusion पैटर्न आपको अपनी लाइब्रेरी को स्कैन करते समय फ़ाइलों और फ़ोल्डरों को अनदेखा करने देता है। यह उपयोगी है यदि आपके पास ऐसे फ़ोल्डर हैं जिनमें ऐसी फ़ाइलें हैं जिन्हें आप आयात नहीं करना चाहते हैं, जैसे RAW फ़ाइलें।",
     "external_library_created_at": "बाहरी लाइब्रेरी ({date} को बनाई गई)",
@@ -59,16 +58,9 @@
     "image_prefer_embedded_preview_setting_description": "जब उपलब्ध हो तो RAW फ़ोटो में एम्बेडेड पूर्वावलोकन का उपयोग इमेज प्रोसेसिंग के इनपुट के रूप में करें। यह कुछ छवियों के लिए अधिक सटीक रंग उत्पन्न कर सकता है, लेकिन पूर्वावलोकन की गुणवत्ता कैमरे पर निर्भर करती है और छवि में अधिक संपीड़न कलाकृतियाँ हो सकती हैं।",
     "image_prefer_wide_gamut": "विस्तृत सरगम को प्राथमिकता दें",
     "image_prefer_wide_gamut_setting_description": "थंबनेल के लिए डिस्प्ले P3 का उपयोग करें। यह विस्तृत कलरस्पेस वाली छवियों की जीवंतता को बेहतर ढंग से संरक्षित करता है, लेकिन पुराने ब्राउज़र संस्करण वाले पुराने डिवाइस पर छवियां अलग-अलग दिखाई दे सकती हैं। रंग परिवर्तन से बचने के लिए sRGB छवियों को sRGB के रूप में रखा जाता है।",
-    "image_preview_format": "पूर्वावलोकन प्रारूप",
-    "image_preview_resolution": "पूर्वावलोकन रिज़ॉल्यूशन",
-    "image_preview_resolution_description": "एकल फ़ोटो देखते समय और मशीन लर्निंग के लिए उपयोग किया जाता है। उच्च रिज़ॉल्यूशन अधिक विवरण को संरक्षित कर सकता है लेकिन एन्कोड करने में अधिक समय लेता है, फ़ाइल आकार बड़ा होता है, और ऐप की प्रतिक्रियाशीलता कम हो सकती है।",
     "image_quality": "गुणवत्ता",
-    "image_quality_description": "छवि गुणवत्ता 1-100 तक। उच्च गुणवत्ता बेहतर है लेकिन बड़ी फ़ाइलें बनाती है, यह विकल्प पूर्वावलोकन और थंबनेल छवियों को प्रभावित करता है।",
     "image_settings": "छवि सेटिंग्स",
     "image_settings_description": "उत्पन्न छवियों की गुणवत्ता और रिज़ॉल्यूशन प्रबंधित करें",
-    "image_thumbnail_format": "थंबनेल प्रारूप",
-    "image_thumbnail_resolution": "थंबनेल रिज़ॉल्यूशन",
-    "image_thumbnail_resolution_description": "फ़ोटो के समूह (मुख्य टाइमलाइन, एल्बम दृश्य, आदि) देखते समय उपयोग किया जाता है। उच्च रिज़ॉल्यूशन अधिक विवरण को संरक्षित कर सकता है लेकिन एन्कोड करने में अधिक समय लेता है, फ़ाइल आकार बड़ा होता है, और ऐप की प्रतिक्रियाशीलता कम हो सकती है।",
     "job_concurrency": "{job} समरूपता",
     "job_not_concurrency_safe": "यह कार्य (जॉब) समवर्ती-सुरक्षित नहीं है।",
     "job_settings": "कार्य (जॉब) सेटिंग्स",
@@ -77,9 +69,6 @@
     "jobs_delayed": "{jobCount, plural, other {# विलंबित}}",
     "jobs_failed": "{jobCount, plural, other {# असफल}}",
     "library_created": "निर्मित संग्रह: {library}",
-    "library_cron_expression": "क्रॉन व्यंजक",
-    "library_cron_expression_description": "क्रॉन प्रारूप का उपयोग करके स्कैनिंग अंतराल सेट करें। अधिक जानकारी के लिए कृपया उदाहरण के लिए <link>Crontab Guru</link> देखें",
-    "library_cron_expression_presets": "क्रॉन व्यंजक प्रीसेट",
     "library_deleted": "संग्रह हटा दिया गया",
     "library_import_path_description": "आयात करने के लिए एक फ़ोल्डर निर्दिष्ट करें। सबफ़ोल्डर्स सहित इस फ़ोल्डर को छवियों और वीडियो के लिए स्कैन किया जाएगा।",
     "library_scanning": "सामयिक स्कैनिंग",
@@ -147,7 +136,7 @@
     "note_cannot_be_changed_later": "नोट: इसे बाद में बदला नहीं जा सकता!",
     "note_unlimited_quota": "नोट: असीमित कोटा के लिए 0 दर्ज करें",
     "notification_email_from_address": "इस पते से",
-    "notification_email_from_address_description": "प्रेषक का ईमेल पता, उदाहरण के लिए: \"इमिच फोटो सर्वर <noreply@immich.app>\"",
+    "notification_email_from_address_description": "प्रेषक का ईमेल पता, उदाहरण के लिए: \"इमिच फोटो सर्वर <noreply@example.com>\"",
     "notification_email_host_description": "ईमेल सर्वर का होस्ट (उदा. smtp.immitch.app)",
     "notification_email_ignore_certificate_errors": "प्रमाणपत्र त्रुटियों पर ध्यान न दें",
     "notification_email_ignore_certificate_errors_description": "टीएलएस प्रमाणपत्र सत्यापन त्रुटियों पर ध्यान न दें (अनुशंसित नहीं)",
@@ -197,13 +186,10 @@
     "refreshing_all_libraries": "सभी पुस्तकालयों को ताज़ा किया जा रहा है",
     "registration": "व्यवस्थापक पंजीकरण",
     "registration_description": "चूंकि आप सिस्टम पर पहले उपयोगकर्ता हैं, इसलिए आपको व्यवस्थापक के रूप में नियुक्त किया जाएगा और आप प्रशासनिक कार्यों के लिए जिम्मेदार होंगे, और अतिरिक्त उपयोगकर्ता आपके द्वारा बनाए जाएंगे।",
-    "removing_offline_files": "ऑफ़लाइन फ़ाइलें हटाना",
     "repair_all": "सभी की मरम्मत",
     "require_password_change_on_login": "उपयोगकर्ता को पहले लॉगिन पर पासवर्ड बदलने की आवश्यकता है",
     "reset_settings_to_default": "सेटिंग्स को डिफ़ॉल्ट पर रीसेट करें",
     "reset_settings_to_recent_saved": "सेटिंग्स को हाल ही में सहेजी गई सेटिंग्स पर रीसेट करें",
-    "scanning_library_for_changed_files": "परिवर्तित फ़ाइलों के लिए लाइब्रेरी को स्कैन करना",
-    "scanning_library_for_new_files": "नई फ़ाइलों के लिए लाइब्रेरी को स्कैन करना",
     "send_welcome_email": "स्वागत ईमेल भेजें",
     "server_external_domain_settings": "बाहरी डोमेन",
     "server_external_domain_settings_description": "सार्वजनिक साझा लिंक के लिए डोमेन, जिसमें http(s):// शामिल है",
@@ -233,7 +219,6 @@
     "these_files_matched_by_checksum": "इन फ़ाइलों का मिलान उनके चेकसम से किया जाता है",
     "thumbnail_generation_job": "थंबनेल उत्पन्न करें",
     "thumbnail_generation_job_description": "प्रत्येक संपत्ति के लिए बड़े, छोटे और धुंधले थंबनेल, साथ ही प्रत्येक व्यक्ति के लिए थंबनेल बनाएं",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "त्वरण एपीआई",
     "transcoding_acceleration_api_description": "एपीआई जो ट्रांसकोडिंग को तेज करने के लिए आपके डिवाइस के साथ इंटरैक्ट करेगा।",
     "transcoding_acceleration_nvenc": "NVENC (NVIDIA GPU की आवश्यकता है)",
@@ -285,8 +270,6 @@
     "transcoding_threads_description": "उच्च मान तेज़ एन्कोडिंग की ओर ले जाते हैं, लेकिन सक्रिय रहते हुए सर्वर के लिए अन्य कार्यों को संसाधित करने के लिए कम जगह छोड़ते हैं।",
     "transcoding_tone_mapping": "टोन-मैपिंग",
     "transcoding_tone_mapping_description": "एसडीआर में परिवर्तित होने पर एचडीआर वीडियो की उपस्थिति को संरक्षित करने का प्रयास।",
-    "transcoding_tone_mapping_npl": "टोन-मैपिंग एनपीएल",
-    "transcoding_tone_mapping_npl_description": "इस चमक के प्रदर्शन को सामान्य दिखाने के लिए रंगों को समायोजित किया जाएगा।",
     "transcoding_transcode_policy": "ट्रांसकोड नीति",
     "transcoding_transcode_policy_description": "किसी वीडियो को कब ट्रांसकोड किया जाना चाहिए, इसके लिए नीति।",
     "transcoding_two_pass_encoding": "दो-पास एन्कोडिंग",
@@ -349,7 +332,6 @@
   "archive_or_unarchive_photo": "फ़ोटो को संग्रहीत या असंग्रहीत करें",
   "archive_size": "पुरालेख आकार",
   "archive_size_description": "डाउनलोड के लिए संग्रह आकार कॉन्फ़िगर करें (GiB में)",
-  "archived": "",
   "are_these_the_same_person": "क्या ये वही व्यक्ति हैं?",
   "are_you_sure_to_do_this": "क्या आप वास्तव में इसे करना चाहते हैं?",
   "asset_added_to_album": "एल्बम में जोड़ा गया",
@@ -382,10 +364,6 @@
   "cannot_merge_people": "लोगों का विलय नहीं हो सकता",
   "cannot_undo_this_action": "आप इस क्रिया को पूर्ववत नहीं कर सकते!",
   "cannot_update_the_description": "विवरण अद्यतन नहीं किया जा सकता",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
   "change_date": "बदलाव दिनांक",
   "change_expiration_time": "समाप्ति समय बदलें",
   "change_location": "स्थान बदलें",
@@ -487,13 +465,6 @@
   "duplicates": "डुप्लिकेट",
   "duplicates_description": "प्रत्येक समूह को यह इंगित करके हल करें कि कौन सा, यदि कोई है, डुप्लिकेट है",
   "duration": "अवधि",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
   "edit": "संपादन करना",
   "edit_album": "एल्बम संपादित करें",
   "edit_avatar": "अवतार को एडिट करें",
@@ -513,8 +484,6 @@
   "edited": "संपादित",
   "editor": "",
   "email": "ईमेल",
-  "empty": "",
-  "empty_album": "",
   "empty_trash": "कूड़ेदान खाली करें",
   "empty_trash_confirmation": "क्या आपको यकीन है कि आप कचरा खाली करना चाहते हैं? यह इमिच से स्थायी रूप से कचरा में सभी संपत्तियों को हटा देगा।\nआप इस कार्रवाई को नहीं रोक सकते!",
   "enable": "सक्षम",
@@ -564,8 +533,6 @@
     "unable_to_change_favorite": "संपत्ति के लिए पसंदीदा बदलने में असमर्थ",
     "unable_to_change_location": "स्थान बदलने में असमर्थ",
     "unable_to_change_password": "पासवर्ड बदलने में असमर्थ",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_complete_oauth_login": "OAuth लॉगिन पूर्ण करने में असमर्थ",
     "unable_to_connect": "कनेक्ट करने में असमर्थ",
     "unable_to_connect_to_server": "सर्वर से कनेक्ट करने में असमर्थ है",
@@ -604,12 +571,10 @@
     "unable_to_remove_album_users": "उपयोगकर्ताओं को एल्बम से निकालने में असमर्थ",
     "unable_to_remove_api_key": "API कुंजी निकालने में असमर्थ",
     "unable_to_remove_assets_from_shared_link": "साझा लिंक से संपत्तियों को निकालने में असमर्थ",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_deleted_assets": "ऑफ़लाइन फ़ाइलें निकालने में असमर्थ",
     "unable_to_remove_library": "लाइब्रेरी हटाने में असमर्थ",
-    "unable_to_remove_offline_files": "ऑफ़लाइन फ़ाइलें निकालने में असमर्थ",
     "unable_to_remove_partner": "पार्टनर को हटाने में असमर्थ",
     "unable_to_remove_reaction": "प्रतिक्रिया निकालने में असमर्थ",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "वस्तुओं की मरम्मत करने में असमर्थ",
     "unable_to_reset_password": "पासवर्ड रीसेट करने में असमर्थ",
     "unable_to_resolve_duplicate": "डुप्लिकेट का समाधान करने में असमर्थ",
@@ -638,10 +603,6 @@
     "unable_to_update_user": "उपयोगकर्ता को अद्यतन करने में असमर्थ",
     "unable_to_upload_file": "फाइल अपलोड करने में असमर्थ"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exif": "एक्सिफ",
   "exit_slideshow": "स्लाइड शो से बाहर निकलें",
   "expand_all": "सभी का विस्तार",
@@ -654,29 +615,23 @@
   "external": "बाहरी",
   "external_libraries": "बाहरी पुस्तकालय",
   "face_unassigned": "सौंपे नहीं गए",
-  "failed_to_get_people": "",
   "favorite": "पसंदीदा",
   "favorite_or_unfavorite_photo": "पसंदीदा या नापसंद फोटो",
   "favorites": "पसंदीदा",
-  "feature": "",
   "feature_photo_updated": "फ़ीचर फ़ोटो अपडेट किया गया",
-  "featurecollection": "",
   "file_name": "फ़ाइल का नाम",
   "file_name_or_extension": "फ़ाइल का नाम या एक्सटेंशन",
   "filename": "फ़ाइल का नाम",
-  "files": "",
   "filetype": "फाइल का प्रकार",
   "filter_people": "लोगों को फ़िल्टर करें",
   "find_them_fast": "खोज के साथ नाम से उन्हें तेजी से ढूंढें",
   "fix_incorrect_match": "ग़लत मिलान ठीक करें",
-  "force_re-scan_library_files": "सभी लाइब्रेरी फ़ाइलों को बलपूर्वक पुनः स्कैन करें",
   "forward": "आगे",
   "general": "सामान्य",
   "get_help": "मदद लें",
   "getting_started": "शुरू करना",
   "go_back": "वापस जाओ",
   "go_to_search": "खोज पर जाएँ",
-  "go_to_share_page": "शेयर पेज पर जाएं",
   "group_albums_by": "इनके द्वारा समूह एल्बम..।",
   "group_no": "कोई समूहीकरण नहीं",
   "group_owner": "स्वामी द्वारा समूह",
@@ -690,7 +645,6 @@
   "host": "मेज़बान",
   "hour": "घंटा",
   "image": "छवि",
-  "img": "",
   "immich_logo": "Immich लोगो",
   "immich_web_interface": "इमिच वेब इंटरफ़ेस",
   "import_from_json": "JSON से आयात करें",
@@ -709,7 +663,6 @@
   },
   "invite_people": "लोगो को निमंत्रण भेजो",
   "invite_to_album": "एल्बम के लिए आमंत्रित करें",
-  "job_settings_description": "",
   "jobs": "नौकरियां",
   "keep": "रखना",
   "keep_all": "सभी रखना",
@@ -820,7 +773,6 @@
   "onboarding_welcome_description": "आइए कुछ सामान्य सेटिंग्स के साथ अपना इंस्टेंस सेट अप करें।",
   "online": "ऑनलाइन",
   "only_favorites": "केवल पसंदीदा",
-  "only_refreshes_modified_files": "केवल संशोधित फ़ाइलों को ताज़ा करता है",
   "open_in_openstreetmap": "OpenStreetMap में खोलें",
   "open_the_search_filters": "खोज फ़िल्टर खोलें",
   "options": "विकल्प",
@@ -854,7 +806,6 @@
   "pending": "लंबित",
   "people": "लोग",
   "people_sidebar_description": "साइडबार में लोगों के लिए एक लिंक प्रदर्शित करें",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "स्थायी विलोपन चेतावनी",
   "permanent_deletion_warning_setting_description": "संपत्तियों को स्थायी रूप से हटाते समय एक चेतावनी दिखाएं",
   "permanently_delete": "स्थायी रूप से हटाना",
@@ -871,7 +822,6 @@
   "play_memories": "यादें खेलें",
   "play_motion_photo": "मोशन फ़ोटो चलाएं",
   "play_or_pause_video": "वीडियो चलाएं या रोकें",
-  "point": "",
   "port": "पत्तन",
   "preset": "प्रीसेट",
   "preview": "पूर्व दर्शन",
@@ -913,8 +863,6 @@
   "purchase_server_description_2": "समर्थक स्थिति",
   "purchase_server_title": "सर्वर",
   "purchase_settings_server_activated": "सर्वर उत्पाद कुंजी व्यवस्थापक द्वारा प्रबंधित की जाती है",
-  "range": "",
-  "raw": "",
   "reaction_options": "प्रतिक्रिया विकल्प",
   "read_changelog": "चेंजलॉग पढ़ें",
   "reassign": "पुनः असाइन",
@@ -933,10 +881,10 @@
   "remove": "निकालना",
   "remove_assets_title": "संपत्तियाँ हटाएँ?",
   "remove_custom_date_range": "कस्टम दिनांक सीमा हटाएँ",
+  "remove_deleted_assets": "ऑफ़लाइन फ़ाइलें हटाएँ",
   "remove_from_album": "एल्बम से हटाएँ",
   "remove_from_favorites": "पसंदीदा से निकालें",
   "remove_from_shared_link": "साझा लिंक से हटाएँ",
-  "remove_offline_files": "ऑफ़लाइन फ़ाइलें हटाएँ",
   "remove_user": "उपयोगकर्ता को हटाएँ",
   "removed_from_archive": "संग्रह से हटा दिया गया",
   "removed_from_favorites": "पसंदीदा से हटाया गया",
@@ -950,7 +898,6 @@
   "reset": "रीसेट",
   "reset_password": "पासवर्ड रीसेट",
   "reset_people_visibility": "लोगों की दृश्यता रीसेट करें",
-  "reset_settings_to_default": "",
   "reset_to_default": "वितथ पर ले जाएं",
   "resolve_duplicates": "डुप्लिकेट का समाधान करें",
   "resolved_all_duplicates": "सभी डुप्लिकेट का समाधान किया गया",
@@ -970,8 +917,6 @@
   "saved_settings": "सहेजी गई सेटिंग्स",
   "say_something": "कुछ कहें",
   "scan_all_libraries": "सभी पुस्तकालयों को स्कैन करें",
-  "scan_all_library_files": "सभी लाइब्रेरी फ़ाइलों को पुनः स्कैन करें",
-  "scan_new_library_files": "नई लाइब्रेरी फ़ाइलें स्कैन करें",
   "scan_settings": "सेटिंग्स स्कैन करें",
   "scanning_for_album": "एल्बम के लिए स्कैन किया जा रहा है..।",
   "search": "खोज",
@@ -1009,7 +954,6 @@
   "selected": "चयनित",
   "send_message": "मेसेज भेजें",
   "send_welcome_email": "स्वागत ईमेल भेजें",
-  "server": "",
   "server_offline": "सर्वर ऑफ़लाइन",
   "server_online": "सर्वर ऑनलाइन",
   "server_stats": "सर्वर आँकड़े",
@@ -1094,7 +1038,6 @@
   "to_trash": "कचरा",
   "toggle_settings": "सेटिंग्स टॉगल करें",
   "toggle_theme": "थीम टॉगल करें",
-  "toggle_visibility": "",
   "total_usage": "कुल उपयोग",
   "trash": "कचरा",
   "trash_all": "सब कचरा",
@@ -1102,11 +1045,9 @@
   "trash_no_results_message": "ट्रैश की गई फ़ोटो और वीडियो यहां दिखाई देंगे।",
   "type": "प्रकार",
   "unarchive": "संग्रह से निकालें",
-  "unarchived": "",
   "unfavorite": "नापसंद करें",
   "unhide_person": "व्यक्ति को उजागर करें",
   "unknown": "अज्ञात",
-  "unknown_album": "",
   "unknown_year": "अज्ञात वर्ष",
   "unlimited": "असीमित",
   "unlink_oauth": "OAuth को अनलिंक करें",
@@ -1155,7 +1096,6 @@
   "view_next_asset": "अगली संपत्ति देखें",
   "view_previous_asset": "पिछली संपत्ति देखें",
   "view_stack": "ढेर देखें",
-  "viewer": "",
   "waiting": "इंतज़ार में",
   "warning": "चेतावनी",
   "week": "सप्ताह",
diff --git a/i18n/hr.json b/i18n/hr.json
new file mode 100644
index 0000000000..d4273b8741
--- /dev/null
+++ b/i18n/hr.json
@@ -0,0 +1,1253 @@
+{
+  "about": "O",
+  "account": "Račun",
+  "account_settings": "Postavke računa",
+  "acknowledge": "Potvrdi",
+  "action": "Akcija",
+  "actions": "Akcije",
+  "active": "Aktivno",
+  "activity": "Aktivnost",
+  "activity_changed": "Aktivnost je {enabled, select, true {omogućena} other {onemogućena}}",
+  "add": "Dodaj",
+  "add_a_description": "Dodaj opis",
+  "add_a_location": "Dodaj lokaciju",
+  "add_a_name": "Dodaj ime",
+  "add_a_title": "Dodaj naslov",
+  "add_exclusion_pattern": "Dodaj uzorak izuzimanja",
+  "add_import_path": "Dodaj import folder",
+  "add_location": "Dodaj lokaciju",
+  "add_more_users": "Dodaj još korisnika",
+  "add_partner": "Dodaj partnera",
+  "add_path": "Dodaj putanju",
+  "add_photos": "Dodaj slike",
+  "add_to": "Dodaj u...",
+  "add_to_album": "Dodaj u album",
+  "add_to_shared_album": "Dodaj u dijeljeni album",
+  "added_to_archive": "Dodano u arhivu",
+  "added_to_favorites": "Dodano u omiljeno",
+  "added_to_favorites_count": "Dodano {count, number} u omiljeno",
+  "admin": {
+    "add_exclusion_pattern_description": "Dodajte uzorke izuzimanja. Globiranje pomoću *, ** i ? je podržano. Za ignoriranje svih datoteka u bilo kojem direktoriju pod nazivom \"Raw\", koristite \"**/Raw/**\". Da biste zanemarili sve datoteke koje završavaju na \".tif\", koristite \"**/*.tif\". Da biste zanemarili apsolutni put, koristite \"/path/to/ignore/**\".",
+    "asset_offline_description": "Ovo sredstvo vanjske knjižnice više nije pronađeno na disku i premješteno je u smeće. Ako je datoteka premještena unutar biblioteke, provjerite svoju vremensku traku za novo odgovarajuće sredstvo. Da biste vratili ovo sredstvo, provjerite može li Immich pristupiti donjoj stazi datoteke i skenirajte biblioteku.",
+    "authentication_settings": "Postavke autentikacije",
+    "authentication_settings_description": "Uredi lozinku, OAuth, i druge postavke autentikacije",
+    "authentication_settings_disable_all": "Jeste li sigurni da želite onemogućenit sve načine prijave? Prijava će biti potpuno onemogućena.",
+    "authentication_settings_reenable": "Za ponovno uključivanje upotrijebite <link>naredbu poslužitelja</link>.",
+    "background_task_job": "Pozadinski zadaci",
+    "backup_database": "Sigurnosna kopija baze podataka",
+    "backup_database_enable_description": "Omogućite sigurnosne kopije baze podataka",
+    "backup_keep_last_amount": "Količina prethodnih sigurnosnih kopija za čuvanje",
+    "backup_settings": "Postavke sigurnosne kopije",
+    "backup_settings_description": "Upravljanje postavkama sigurnosne kopije baze podataka",
+    "check_all": "Provjeri sve",
+    "cleared_jobs": "Izbrisani poslovi za: {job}",
+    "config_set_by_file": "Konfiguracija je trenutno postavljena konfiguracijskom datotekom",
+    "confirm_delete_library": "Jeste li sigurni da želite izbrisati biblioteku {library}?",
+    "confirm_delete_library_assets": "Jeste li sigurni da želite izbrisati ovu biblioteku? Time će se izbrisati sva {count} sadržana sredstva iz Immicha i ne može se poništiti. Datoteke će ostati na disku.",
+    "confirm_email_below": "Za potvrdu upišite \"{email}\" ispod",
+    "confirm_reprocess_all_faces": "Jeste li sigurni da želite ponovno obraditi sva lica? Ovo će također obrisati imenovane osobe.",
+    "confirm_user_password_reset": "Jeste li sigurni da želite poništiti lozinku korisnika {user}?",
+    "create_job": "Izradi zadatak",
+    "cron_expression": "Cron izraz (expression)",
+    "cron_expression_description": "Postavite interval skeniranja koristeći cron format. Za više informacija pogledajte npr. <link>Crontab Guru</link>",
+    "cron_expression_presets": "Cron unaprijed postavljene postavke izraza",
+    "disable_login": "Onemogući prijavu",
+    "duplicate_detection_job_description": "Pokrenite strojno učenje na materijalima kako biste otkrili slične slike. Oslanja se na Pametno Pretraživanje",
+    "exclusion_pattern_description": "Uzorci izuzimanja omogućuju vam da zanemarite datoteke i mape prilikom skeniranja svoje biblioteke. Ovo je korisno ako imate mape koje sadrže datoteke koje ne želite uvesti, kao što su RAW datoteke.",
+    "external_library_created_at": "Vanjska biblioteka (stvorena: {date})",
+    "external_library_management": "Upravljanje vanjskom knjižnicom",
+    "face_detection": "Detekcija lica",
+    "face_detection_description": "Prepoznajte lica u sredstvima pomoću strojnog učenja. Za videozapise u obzir se uzima samo minijaturni prikaz. \"Sve\" (ponovno) obrađuje svu imovinu. \"Nedostaje\" stavlja u red čekanja sredstva koja još nisu obrađena. Otkrivena lica bit će stavljena u red čekanja za prepoznavanje lica nakon dovršetka prepoznavanja lica, grupirajući ih u postojeće ili nove osobe.",
+    "facial_recognition_job_description": "Grupirajte otkrivena lica u osobe. Ovaj se korak pokreće nakon dovršetka prepoznavanja lica. \"Sve\" (ponovno) grupira sva lica. \"Nedostajuća\" lica u redovima kojima nije dodijeljena osoba.",
+    "failed_job_command": "Naredba {command} nije uspjela za posao: {job}",
+    "force_delete_user_warning": "UPOZORENJE: Ovo će odmah ukloniti korisnika i sve pripadajuće podatke. Ovo se ne može poništiti i datoteke se ne mogu vratiti.",
+    "forcing_refresh_library_files": "Prisilno osvježavanje svih datoteka knjižnice",
+    "image_format": "Format",
+    "image_format_description": "WebP proizvodi manje datoteke od JPEG-a, ali se sporije kodira.",
+    "image_prefer_embedded_preview": "Preferiraj ugrađeni pregled",
+    "image_prefer_embedded_preview_setting_description": "Koristite ugrađene preglede u RAW fotografije kao ulaz za obradu slike kada su dostupni. To može proizvesti preciznije boje za neke slike, ali kvaliteta pregleda ovisi o kameri i slika može imati više artefakata kompresije.",
+    "image_prefer_wide_gamut": "Preferirajte široku gamu",
+    "image_prefer_wide_gamut_setting_description": "Koristite Display P3 za sličice. Ovo bolje čuva živost slika sa širokim prostorima boja, ali slike mogu izgledati drugačije na starim uređajima sa starom verzijom preglednika. sRGB slike čuvaju se kao sRGB kako bi se izbjegle promjene boja.",
+    "image_preview_description": "Slika srednje veličine s ogoljenim metapodacima, koristi se prilikom pregledavanja jednog sredstva i za strojno učenje",
+    "image_preview_quality_description": "Kvaliteta pregleda od 1-100. Više je bolje, ali proizvodi veće datoteke i može smanjiti odziv aplikacije. Postavljanje niske vrijednosti može utjecati na kvalitetu strojnog učenja.",
+    "image_preview_title": "Postavke pregleda",
+    "image_quality": "Kvaliteta",
+    "image_resolution": "Rezolucija",
+    "image_resolution_description": "Veće razlučivosti mogu sačuvati više detalja, ali trebaju dulje za kodiranje, imaju veće veličine datoteka i mogu smanjiti odziv aplikacije.",
+    "image_settings": "Postavke slike",
+    "image_settings_description": "Upravljajte kvalitetom i rezolucijom generiranih slika",
+    "image_thumbnail_description": "Mala minijatura s ogoljenim metapodacima, koristi se pri gledanju grupa fotografija poput glavne vremenske trake",
+    "image_thumbnail_quality_description": "Kvaliteta sličica od 1-100. Više je bolje, ali proizvodi veće datoteke i može smanjiti odziv aplikacije.",
+    "image_thumbnail_title": "Postavke sličica",
+    "job_concurrency": "{job} istovremenost",
+    "job_created": "Zadatak je kreiran",
+    "job_not_concurrency_safe": "Ovaj posao nije siguran za istovremenost.",
+    "job_settings": "Postavke posla",
+    "job_settings_description": "Upravljajte istovremenošću poslova",
+    "job_status": "Status posla",
+    "jobs_delayed": "{jobCount, plural, other {# delayed}}",
+    "jobs_failed": "{jobCount, plural, other {# failed}}",
+    "library_created": "Stvorena biblioteka: {library}",
+    "library_deleted": "Biblioteka izbrisana",
+    "library_import_path_description": "Navedite mapu za uvoz. Ova će se mapa, uključujući podmape, skenirati u potrazi za slikama i videozapisima.",
+    "library_scanning": "Periodično Skeniranje",
+    "library_scanning_description": "Konfigurirajte periodično skeniranje biblioteke",
+    "library_scanning_enable_description": "Omogući periodično skeniranje biblioteke",
+    "library_settings": "Externa biblioteka",
+    "library_settings_description": "Upravljajte postavkama vanjske biblioteke",
+    "library_tasks_description": "Obavljati bibliotekne zadatke",
+    "library_watching_enable_description": "Pratite vanjske biblioteke za promjena datoteke",
+    "library_watching_settings": "Gledanje biblioteke (EKSPERIMENTALNO)",
+    "library_watching_settings_description": "Automatsko praćenje promijenjenih datoteke",
+    "logging_enable_description": "Omogući zapisivanje",
+    "logging_level_description": "Kada je omogućeno, koju razinu zapisivanja koristiti.",
+    "logging_settings": "Zapisivanje",
+    "machine_learning_clip_model": "CLIP model",
+    "machine_learning_clip_model_description": "Naziv CLIP modela navedenog <link>ovdje</link>. Imajte na umu da morate ponovno pokrenuti posao 'Pametno Pretraživanje' za sve slike nakon promjene modela.",
+    "machine_learning_duplicate_detection": "Detekcija Duplikata",
+    "machine_learning_duplicate_detection_enabled": "Omogući detekciju duplikata",
+    "machine_learning_duplicate_detection_enabled_description": "Ako je onemogućeno, potpuno identična sredstva i dalje će biti deduplicirana.",
+    "machine_learning_duplicate_detection_setting_description": "Upotrijebite CLIP ugradnje da biste pronašli vjerojatne duplikate",
+    "machine_learning_enabled": "Uključi strojsko učenje",
+    "machine_learning_enabled_description": "Ukoliko je ovo isključeno, sve funkcije strojnoga učenja biti će isključene bez obzira na postavke ispod.",
+    "machine_learning_facial_recognition": "Detekcija lica",
+    "machine_learning_facial_recognition_description": "Detektiraj, prepoznaj i grupiraj lica u fotografijama",
+    "machine_learning_facial_recognition_model": "Model prepoznavanja lica",
+    "machine_learning_facial_recognition_model_description": "Modeli su navedeni silaznim redoslijedom veličine. Veći modeli su sporiji i koriste više memorije, ali daju bolje rezultate. Imajte na umu da morate ponovno pokrenuti posao detekcije lica za sve slike nakon promjene modela.",
+    "machine_learning_facial_recognition_setting": "Omogući prepoznavanje lica",
+    "machine_learning_facial_recognition_setting_description": "Ako je onemogućeno, slike neće biti kodirane za prepoznavanje lica i neće popuniti odjeljak Ljudi na stranici Istraživanje.",
+    "machine_learning_max_detection_distance": "Maksimalna udaljenost za detektiranje",
+    "machine_learning_max_detection_distance_description": "Maksimalna udaljenost između dvije slike da bi se smatrale duplikatima, u rasponu od 0,001-0,1. Više vrijednosti otkrit će više duplikata, ali mogu rezultirati netočnim rezultatima.",
+    "machine_learning_max_recognition_distance": "Maksimalna udaljenost za detekciju",
+    "machine_learning_max_recognition_distance_description": "Maksimalna udaljenost između dva lica koja se smatraju istom osobom, u rasponu od 0-2. Snižavanje može spriječiti označavanje dvije osobe kao iste osobe, dok podizanje može spriječiti označavanje iste osobe kao dvije različite osobe. Imajte na umu da je lakše spojiti dvije osobe nego jednu osobu podijeliti na dvije, stoga koristite niži prag kada je to moguće.",
+    "machine_learning_min_detection_score": "Minimalni rezultat otkrivanja",
+    "machine_learning_min_detection_score_description": "Minimalni rezultat pouzdanosti za detektirano lice od 0-1. Niže vrijednosti otkrit će više lica, ali mogu dovesti do lažno pozitivnih rezultata.",
+    "machine_learning_min_recognized_faces": "Minimum prepoznatih lica",
+    "machine_learning_min_recognized_faces_description": "Najmanji broj prepoznatih lica za osobu koja se stvara. Povećanje toga čini prepoznavanje lica preciznijim po cijenu povećanja šanse da lice nije dodijeljeno osobi.",
+    "machine_learning_settings": "Postavke strojnog učenja",
+    "machine_learning_settings_description": "Upravljajte značajkama i postavkama strojnog učenja",
+    "machine_learning_smart_search": "Pametna pretraga",
+    "machine_learning_smart_search_description": "Pretražujte slike semantički koristeći CLIP ugradnje",
+    "machine_learning_smart_search_enabled": "Omogući pametno pretraživanje",
+    "machine_learning_smart_search_enabled_description": "Ako je onemogućeno, slike neće biti kodirane za pametno pretraživanje.",
+    "machine_learning_url_description": "URL poslužitelja strojnog učenja",
+    "manage_concurrency": "Upravljanje Istovremenošću",
+    "manage_log_settings": "Upravljanje postavkama zapisivanje",
+    "map_dark_style": "Tamni stil",
+    "map_enable_description": "Omogući značajke karte",
+    "map_gps_settings": "Postavke Karte i GPS-a",
+    "map_gps_settings_description": "Upravljajte Postavkama Karte i GPS-a (Obrnuto Geokodiranje)",
+    "map_implications": "Značajka karte se oslanja na vanjsku uslugu pločica (tiles.immich.cloud)",
+    "map_light_style": "Svijetli stil",
+    "map_manage_reverse_geocoding_settings": "Upravljajte postavkama <link>Obrnutog Geokodiranja</link>",
+    "map_reverse_geocoding": "Obrnuto Geokodiranje",
+    "map_reverse_geocoding_enable_description": "Omogući obrnuto geokodiranje",
+    "map_reverse_geocoding_settings": "Postavke Obrnuto Geokodiranje",
+    "map_settings": "Karta",
+    "map_settings_description": "Upravljanje postavkama karte",
+    "map_style_description": "URL na style.json temu karte",
+    "metadata_extraction_job": "Izdvoj metapodatke",
+    "metadata_extraction_job_description": "Izdvojite podatke o metapodacima iz svakog sredstva, kao što su GPS, lica i rezolucija",
+    "metadata_faces_import_setting": "Omogući uvoz lica",
+    "metadata_faces_import_setting_description": "Uvezite lica iz EXIF podataka slike i sidecar datoteka",
+    "metadata_settings": "Postavke Metapodataka",
+    "metadata_settings_description": "Upravljanje postavkama metapodataka",
+    "migration_job": "Migracija",
+    "migration_job_description": "Premjestite minijature za sredstva i lica u najnoviju strukturu mapa",
+    "no_paths_added": "Nema dodanih putanja",
+    "no_pattern_added": "Nije dodan uzorak",
+    "note_apply_storage_label_previous_assets": "Napomena: da biste primijenili Oznaku Pohrane na prethodno prenesena sredstva, pokrenite",
+    "note_cannot_be_changed_later": "NAPOMENA: Ovo se ne može promijeniti kasnije!",
+    "note_unlimited_quota": "Napomena: Unesite 0 za neograničenu kvotu",
+    "notification_email_from_address": "Od adrese",
+    "notification_email_from_address_description": "E-mail adresa pošiljatelja, na primjer: \"Immich Photo Server <noreply@example.com>\"",
+    "notification_email_host_description": "Poslužitelja e-pošte (npr. smtp.immich.app)",
+    "notification_email_ignore_certificate_errors": "Ignoriraj pogreške certifikata",
+    "notification_email_ignore_certificate_errors_description": "Ignoriraj pogreške provjere valjanosti TLS certifikata (nije preporučeno)",
+    "notification_email_password_description": "Lozinka za korištenje pri autentifikaciji s poslužiteljem e-pošte",
+    "notification_email_port_description": "Port poslužitelja e-pošte (npr. 25, 465, ili 587)",
+    "notification_email_sent_test_email_button": "Pošaljite probni e-mail i spremi",
+    "notification_email_setting_description": "Postavke za slanje e-mail obavijeste",
+    "notification_email_test_email": "Pošalji probni e-mail",
+    "notification_email_test_email_failed": "Slanje testne e-pošte nije uspjelo, provjerite svoje postavke",
+    "notification_email_test_email_sent": "Testna e-poruka poslana je na {email}. Provjerite svoju pristiglu poštu.",
+    "notification_email_username_description": "Korisničko ime koje se koristi pri autentifikaciji s poslužiteljem e-pošte",
+    "notification_enable_email_notifications": "Omogući obavijesti putem e-pošte",
+    "notification_settings": "Postavke Obavijesti",
+    "notification_settings_description": "Upravljanje postavkama obavijesti, uključujući e-poštu",
+    "oauth_auto_launch": "Automatsko pokretanje",
+    "oauth_auto_launch_description": "Automatski pokrenite OAuth prijavu nakon navigacije na stranicu za prijavu",
+    "oauth_auto_register": "Automatska registracija",
+    "oauth_auto_register_description": "Automatski registrirajte nove korisnike nakon prijave s OAuth",
+    "oauth_button_text": "Tekst gumba",
+    "oauth_client_id": "ID Klijenta",
+    "oauth_client_secret": "Tajna Klijenta",
+    "oauth_enable_description": "Prijavite se putem OAutha",
+    "oauth_issuer_url": "URL Izdavatelja",
+    "oauth_mobile_redirect_uri": "Mobilnog Preusmjeravanja URI",
+    "oauth_mobile_redirect_uri_override": "Nadjačavanje URI-preusmjeravanja za mobilne uređaje",
+    "oauth_mobile_redirect_uri_override_description": "Omogući kada pružatelj OAuth ne dopušta mobilni URI, poput '{callback}'",
+    "oauth_profile_signing_algorithm": "Algoritam za potpisivanje profila",
+    "oauth_profile_signing_algorithm_description": "Algoritam koji se koristi za potpisivanje korisničkog profila.",
+    "oauth_scope": "Opseg",
+    "oauth_settings": "OAuth",
+    "oauth_settings_description": "Upravljanje postavkama za prijavu kroz OAuth",
+    "oauth_settings_more_details": "Za više pojedinosti o ovoj značajci pogledajte <link>uputstva</link>.",
+    "oauth_signing_algorithm": "Algoritam potpisivanja",
+    "oauth_storage_label_claim": "Potraživanje oznake za pohranu",
+    "oauth_storage_label_claim_description": "Automatski postavite korisničku oznaku za pohranu na vrijednost ovog zahtjeva.",
+    "oauth_storage_quota_claim": "Zahtjev za kvotom pohrane",
+    "oauth_storage_quota_claim_description": "Automatski postavite korisničku kvotu pohrane na vrijednost ovog zahtjeva.",
+    "oauth_storage_quota_default": "Zadana kvota pohrane (GiB)",
+    "oauth_storage_quota_default_description": "Kvota u GiB koja će se koristiti kada nema zahtjeva (unesite 0 za neograničenu kvotu).",
+    "offline_paths": "Izvanmrežne putanje",
+    "offline_paths_description": "Ovi rezultati mogu biti posljedica ručnog brisanja datoteka koje nisu dio vanjske biblioteke.",
+    "password_enable_description": "Prijava s email adresom i zaporkom",
+    "password_settings": "Prijava zaporkom",
+    "password_settings_description": "Upravljanje postavkama za prijavu zaporkom",
+    "paths_validated_successfully": "Sve su putanje uspješno potvrđene",
+    "person_cleanup_job": "Čišćenje lica",
+    "quota_size_gib": "Veličina kvote (GiB)",
+    "refreshing_all_libraries": "Osvježavanje svih biblioteka",
+    "registration": "Registracija administratora",
+    "registration_description": "Budući da ste prvi korisnik na sustavu, bit ćete dodijeljeni administratorsku ulogu i odgovorni ste za administrativne poslove, a dodatne korisnike kreirat ćete sami.",
+    "repair_all": "Popravi sve",
+    "repair_matched_items": "Podudaranje {count, plural, one {# item} other {# items}}",
+    "repaired_items": "Popravljeno {count, plural, one {# item} other {# items}}",
+    "require_password_change_on_login": "Zahtijevajte od korisnika promjenu lozinke pri prvoj prijavi",
+    "reset_settings_to_default": "Vrati postavke na zadane",
+    "reset_settings_to_recent_saved": "Resetirajte postavke na nedavno spremljene postavke",
+    "scanning_library": "Skeniranje biblioteke",
+    "search_jobs": "Traži zadatke…",
+    "send_welcome_email": "Pošaljite email dobrodošlice",
+    "server_external_domain_settings": "Vanjska domena",
+    "server_external_domain_settings_description": "Domena za javno dijeljene linkove, uključujući http(s)://",
+    "server_public_users": "Javni korisnici",
+    "server_public_users_description": "Svi korisnici (ime i e-pošta) navedeni su prilikom dodavanja korisnika u dijeljene albume. Kada je onemogućeno, popis korisnika bit će dostupan samo korisnicima administratora.",
+    "server_settings": "Postavke servera",
+    "server_settings_description": "Upravljanje postavkama servera",
+    "server_welcome_message": "Poruka dobrodošlice",
+    "server_welcome_message_description": "Poruka koja je prikazana na prijavi.",
+    "sidecar_job": "Sidecar metapodaci",
+    "sidecar_job_description": "Otkrijte ili sinkronizirajte sidecar metapodatke iz datotečnog sustava",
+    "slideshow_duration_description": "Broj sekundi za prikaz svake slike",
+    "smart_search_job_description": "Pokrenite strojno učenje na sredstvima za podršku pametnog pretraživanja",
+    "storage_template_date_time_description": "Vremenska oznaka stvaranja sredstva koristi se za informacije o datumu i vremenu",
+    "storage_template_date_time_sample": "Vrijeme uzorka {date}",
+    "storage_template_enable_description": "Omogući mehanizam predloška za pohranu",
+    "storage_template_hash_verification_enabled": "Omogućena hash provjera",
+    "storage_template_hash_verification_enabled_description": "Omogućuje hash provjeru, nemojte je onemogućiti osim ako niste sigurni u implikacije",
+    "storage_template_migration": "Migracija predloška za pohranu",
+    "storage_template_migration_description": "Primijenite trenutni <link>{template}</link> na prethodno prenesena sredstva",
+    "storage_template_migration_info": "Promjene predloška primjenjivat će se samo na nova sredstva. Za retroaktivnu primjenu predloška na prethodno prenesena sredstva, pokrenite <link>{job}</link>.",
+    "storage_template_migration_job": "Posao Migracije Predloška Pohrane",
+    "storage_template_more_details": "Za više pojedinosti o ovoj značajci pogledajte <template-link>Predložak pohrane</template-link> i njegove <implications-link>implikacije</implications-link>",
+    "storage_template_onboarding_description": "Kada je omogućena, ova će značajka automatski organizirati datoteke na temelju korisnički definiranog predloška. Zbog problema sa stabilnošću značajka je isključena prema zadanim postavkama. Za više informacija pogledajte <link>dokumentaciju</link>.",
+    "storage_template_path_length": "Približno ograničenje duljine putanje: <b>{length, number}</b>/{limit, number}",
+    "storage_template_settings": "Predložak pohrane",
+    "storage_template_settings_description": "Upravljajte strukturom mape i nazivom datoteke učitanog sredstva",
+    "storage_template_user_label": "<code>{label}</code> je korisnička oznaka za pohranu",
+    "system_settings": "Postavke Sustava",
+    "tag_cleanup_job": "Čišćenje oznaka",
+    "theme_custom_css_settings": "Prilagođeni CSS",
+    "theme_custom_css_settings_description": "Kaskadni listovi stilova (CSS) omogućuju prilagođavanje dizajna Immicha.",
+    "theme_settings": "Postavke tema",
+    "theme_settings_description": "Upravljajte prilagodbom Immich web sučelja",
+    "these_files_matched_by_checksum": "Ove datoteke se podudaraju prema njihovim kontrolnim zbrojevima",
+    "thumbnail_generation_job": "Generirajte sličice",
+    "thumbnail_generation_job_description": "Generirajte velike, male i zamućene sličice za svaki materijal, kao i sličice za svaku osobu",
+    "transcoding_acceleration_api": "API ubrzanja",
+    "transcoding_acceleration_api_description": "API koji će komunicirati s vašim uređajem radi ubrzanja transkodiranja. Ova postavka je 'najveći trud': vratit će se na softversko transkodiranje u slučaju kvara. VP9 može ili ne mora raditi ovisno o vašem hardveru.",
+    "transcoding_acceleration_nvenc": "NVENC (zahtjeva NVIDIA GPU)",
+    "transcoding_acceleration_qsv": "Quick Sync (zahtjeva Intel CPU sedme ili veće generacije)",
+    "transcoding_acceleration_rkmpp": "RKMPP (samo na Rockchip SOCima)",
+    "transcoding_acceleration_vaapi": "VAAPI",
+    "transcoding_accepted_audio_codecs": "Prihvačeni audio kodeci",
+    "transcoding_accepted_audio_codecs_description": "Odaberite koji audio kodeci ne trebaju biti transkodirani. Samo korišteno za neka pravila za transkodiranje.",
+    "transcoding_accepted_containers": "Prihvaćeni kontenjeri",
+    "transcoding_accepted_containers_description": "Odaberite koji formati spremnika ne moraju biti remulksirani u MP4. Koristi se samo za određena pravila transkodiranja.",
+    "transcoding_accepted_video_codecs": "Prihvaćeni video kodeci",
+    "transcoding_accepted_video_codecs_description": "Odaberite koje video kodeke nije potrebno transkodirati. Koristi se samo za određena pravila transkodiranja.",
+    "transcoding_advanced_options_description": "Postavke većina korisnika ne treba mjenjati",
+    "transcoding_audio_codec": "Audio kodek",
+    "transcoding_audio_codec_description": "Opus je opcija s najvećom kvalitetom, no ima manju podršku s starim uređajima i softverima.",
+    "transcoding_bitrate_description": "Videozapisi veći od maksimalne brzine prijenosa ili nisu u prihvatljivom formatu",
+    "transcoding_codecs_learn_more": "Da biste saznali više o terminologiji koja se ovdje koristi, pogledajte FFmpeg dokumentaciju za <h264-link>H.264 kodek</h264-link>, <hevc-link>HEVC kodek</hevc-link> i <vp9-link>VP9 kodek</vp9-link>.",
+    "transcoding_constant_quality_mode": "Način stalne kvalitete",
+    "transcoding_constant_quality_mode_description": "ICQ je bolji od CQP-a, ali neki uređaji za hardversko ubrzanje ne podržavaju ovaj način rada. Postavljanje ove opcije daje prednost navedenom načinu rada kada se koristi kodiranje temeljeno na kvaliteti. NVENC je zanemaren jer ne podržava ICQ.",
+    "transcoding_constant_rate_factor": "Faktor konstantne stope (-crf)",
+    "transcoding_constant_rate_factor_description": "Razina kvalitete videa. Uobičajene vrijednosti su 23 za H.264, 28 za HEVC, 31 za VP9 i 35 za AV1. Niže je bolje, ali stvara veće datoteke.",
+    "transcoding_disabled_description": "Nemojte transkodirati nijedan videozapis, može prekinuti reprodukciju na nekim klijentima",
+    "transcoding_hardware_acceleration": "Hardversko Ubrzanje",
+    "transcoding_hardware_acceleration_description": "Eksperimentalno; puno brže, ali će imati nižu kvalitetu pri istoj bitrate postavci",
+    "transcoding_hardware_decoding": "Hardversko dekodiranje",
+    "transcoding_hardware_decoding_setting_description": "Odnosi se samo na NVENC, QSV i RKMPP. Omogućuje ubrzanje s kraja na kraj umjesto samo ubrzavanja kodiranja. Možda neće raditi na svim videozapisima.",
+    "transcoding_hevc_codec": "HEVC kodek",
+    "transcoding_max_b_frames": "Maksimalni B-frameovi",
+    "transcoding_max_b_frames_description": "Više vrijednosti poboljšavaju učinkovitost kompresije, ali usporavaju kodiranje. Možda nije kompatibilan s hardverskim ubrzanjem na starijim uređajima. 0 onemogućuje B-frameove, dok -1 automatski postavlja ovu vrijednost.",
+    "transcoding_max_bitrate": "Maksimalne brzina prijenosa (bitrate)",
+    "transcoding_max_bitrate_description": "Postavljanje maksimalne brzine prijenosa može učiniti veličine datoteka predvidljivijima uz manji trošak za kvalitetu. Pri 720p, tipične vrijednosti su 2600k za VP9 ili HEVC ili 4500k za H.264. Onemogućeno ako je postavljeno na 0.",
+    "transcoding_max_keyframe_interval": "Maksimalni interval ključnih sličica",
+    "transcoding_max_keyframe_interval_description": "Postavlja maksimalnu udaljenost slika između ključnih kadrova. Niže vrijednosti pogoršavaju učinkovitost kompresije, ali poboljšavaju vrijeme traženja i mogu poboljšati kvalitetu u scenama s brzim kretanjem. 0 automatski postavlja ovu vrijednost.",
+    "transcoding_optimal_description": "Videozapisi koji su veći od ciljne rezolucije ili nisu u prihvatljivom formatu",
+    "transcoding_preferred_hardware_device": "Preferirani hardverski uređaj",
+    "transcoding_preferred_hardware_device_description": "Odnosi se samo na VAAPI i QSV. Postavlja dri node koji se koristi za hardversko transkodiranje.",
+    "transcoding_preset_preset": "Preset (-preset)",
+    "transcoding_preset_preset_description": "Brzina kompresije. Sporije postavke proizvode manje datoteke i povećavaju kvalitetu pri ciljanju određene postavke bitratea. VP9 zanemaruje brzine iznad 'brže'.",
+    "transcoding_reference_frames": "Referentne slike",
+    "transcoding_reference_frames_description": "Broj slika za referencu prilikom komprimiranja određene slike. Više vrijednosti poboljšavaju učinkovitost kompresije, ali usporavaju kodiranje. 0 automatski postavlja ovu vrijednost.",
+    "transcoding_required_description": "Samo videozapisi koji nisu u prihvaćenom formatu",
+    "transcoding_settings": "Postavke Video Transkodiranja",
+    "transcoding_settings_description": "Upravljajte informacijama o razlučivosti i kodiranju video datoteka",
+    "transcoding_target_resolution": "Ciljana rezolucija",
+    "transcoding_target_resolution_description": "Veće razlučivosti mogu sačuvati više detalja, ali trebaju dulje za kodiranje, imaju veće veličine datoteka i mogu smanjiti odziv aplikacije.",
+    "transcoding_temporal_aq": "Vremenski AQ",
+    "transcoding_temporal_aq_description": "Odnosi se samo na NVENC. Povećava kvalitetu scena s puno detalja i malo pokreta. Možda nije kompatibilan sa starijim uređajima.",
+    "transcoding_threads": "Sljedovi (Threads)",
+    "transcoding_threads_description": "Više vrijednosti dovode do bržeg kodiranja, ali ostavljaju manje prostora poslužitelju za obradu drugih zadataka dok je aktivan. Ova vrijednost ne smije biti veća od broja CPU jezgri. Maksimalno povećava iskorištenje ako je postavljeno na 0.",
+    "transcoding_tone_mapping": "Tonsko preslikavanje",
+    "transcoding_tone_mapping_description": "Pokušava sačuvati izgled HDR videozapisa kada se pretvori u SDR. Svaki algoritam čini različite kompromise za boju, detalje i svjetlinu. Hable čuva detalje, Mobius čuva boju, a Reinhard svjetlinu.",
+    "transcoding_transcode_policy": "Pravila transkodiranja",
+    "transcoding_transcode_policy_description": "Pravila o tome kada se video treba transkodirati. HDR videozapisi uvijek će biti transkodirani (osim ako je transkodiranje onemogućeno).",
+    "transcoding_two_pass_encoding": "Kodiranje u dva prolaza",
+    "transcoding_two_pass_encoding_setting_description": "Transkodiranje u dva prolaza za proizvodnju bolje kodiranih videozapisa. Kada je omogućena maksimalna brzina prijenosa (potrebna za rad s H.264 i HEVC), ovaj način rada koristi raspon brzine prijenosa na temelju maksimalne brzine prijenosa i zanemaruje CRF. Za VP9, CRF se može koristiti ako je maksimalna brzina prijenosa onemogućena.",
+    "transcoding_video_codec": "Video Kodek",
+    "transcoding_video_codec_description": "VP9 ima visoku učinkovitost i web-kompatibilnost, ali treba dulje za transkodiranje. HEVC ima sličnu izvedbu, ali ima slabiju web kompatibilnost. H.264 široko je kompatibilan i brzo se transkodira, ali proizvodi mnogo veće datoteke. AV1 je najučinkovitiji kodek, ali nema podršku na starijim uređajima.",
+    "trash_enabled_description": "Omogućite značajke Smeća",
+    "trash_number_of_days": "Broj dana",
+    "trash_number_of_days_description": "Broj dana za držanje sredstava u smeću prije njihovog trajnog uklanjanja",
+    "trash_settings": "Postavke Smeća",
+    "trash_settings_description": "Upravljanje postavkama smeća",
+    "untracked_files": "Nepraćene datoteke",
+    "untracked_files_description": "Aplikacija ne prati ove datoteke. Mogu biti rezultat neuspjelih premještanja, prekinutih prijenosa ili izostale zbog pogreške",
+    "user_cleanup_job": "Čišćenje korisnika",
+    "user_delete_delay": "Račun i sredstva korisnika <b>{user}</b> bit će zakazani za trajno brisanje za {delay, plural, one {# day} other {# days}}.",
+    "user_delete_delay_settings": "Brisanje odgode",
+    "user_delete_delay_settings_description": "Broj dana nakon uklanjanja za trajno brisanje korisničkog računa i imovine. Posao brisanja korisnika pokreće se u ponoć kako bi se provjerili korisnici koji su spremni za brisanje. Promjene ove postavke bit će procijenjene pri sljedećem izvršavanju.",
+    "user_delete_immediately": "Račun i sredstva korisnika <b>{user}</b> bit će stavljeni u red čekanja za trajno brisanje <b>odmah</b>.",
+    "user_delete_immediately_checkbox": "Stavite korisnika i imovinu u red za trenutačno brisanje",
+    "user_management": "Upravljanje Korisnicima",
+    "user_password_has_been_reset": "Korisnička lozinka je poništena:",
+    "user_password_reset_description": "Molimo dostavite privremenu lozinku korisniku i obavijestite ga da će morati promijeniti lozinku pri sljedećoj prijavi.",
+    "user_restore_description": "Račun korisnika <b>{user}</b> bit će vraćen.",
+    "user_restore_scheduled_removal": "Vrati korisnika - zakazano uklanjanje {date, date, long}",
+    "user_settings": "Korisničke Postavke",
+    "user_settings_description": "Upravljanje korisničkim postavkama",
+    "user_successfully_removed": "Korisnik {email} je uspješno uklonjen.",
+    "version_check_enabled_description": "Omogući provjeru verzije",
+    "version_check_implications": "Značajka provjere verzije oslanja se na periodičnu komunikaciju s github.com",
+    "version_check_settings": "Provjera Verzije",
+    "version_check_settings_description": "Omogućite/onemogućite obavijest o novoj verziji",
+    "video_conversion_job": "Transkodiranje videozapisa",
+    "video_conversion_job_description": "Transkodiranje videozapisa za veću kompatibilnost s preglednicima i uređajima"
+  },
+  "admin_email": "E-pošta administratora",
+  "admin_password": "Admin Lozinka",
+  "administration": "Administracija",
+  "advanced": "Napredno",
+  "age_months": "Dob {months, plural, one {# month} other {# months}}",
+  "age_year_months": "Dob 1 godina, {months, plural, one {# month} other {# months}}",
+  "age_years": "{years, plural, other {Age #}}",
+  "album_added": "Album dodan",
+  "album_added_notification_setting_description": "Primite obavijest e-poštom kada ste dodani u dijeljeni album",
+  "album_cover_updated": "Naslovnica albuma ažurirana",
+  "album_delete_confirmation": "Jeste li sigurni da želite izbrisati album {album}?",
+  "album_delete_confirmation_description": "Ako se ovaj album dijeli, drugi korisnici mu više neće moći pristupiti.",
+  "album_info_updated": "Podaci o albumu ažurirani",
+  "album_leave": "Napustiti album?",
+  "album_leave_confirmation": "Jeste li sigurni da želite napustiti {album}?",
+  "album_name": "Naziv Albuma",
+  "album_options": "Opcije albuma",
+  "album_remove_user": "Ukloni korisnika?",
+  "album_remove_user_confirmation": "Jeste li sigurni da želite ukloniti {user}?",
+  "album_share_no_users": "Čini se da ste podijelili ovaj album sa svim korisnicima ili nemate nijednog korisnika s kojim biste ga dijelili.",
+  "album_updated": "Album ažuriran",
+  "album_updated_setting_description": "Primite obavijest e-poštom kada dijeljeni album ima nova sredstva",
+  "album_user_left": "Napušten {album}",
+  "album_user_removed": "Uklonjen {user}",
+  "album_with_link_access": "Dopusti svima s poveznicom pristup fotografijama i osobama u ovom albumu.",
+  "albums": "Albumi",
+  "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albumi}}",
+  "all": "Sve",
+  "all_albums": "Svi albumi",
+  "all_people": "Svi ljudi",
+  "all_videos": "Svi videi",
+  "allow_dark_mode": "Dozvoli tamni način",
+  "allow_edits": "Dozvoli izmjene",
+  "allow_public_user_to_download": "Dopusti javnom korisniku preuzimanje",
+  "allow_public_user_to_upload": "Dopusti javnom korisniku učitavanje",
+  "anti_clockwise": "Suprotno smjeru kazaljke na satu",
+  "api_key": "API Ključ",
+  "api_key_description": "Ova će vrijednost biti prikazana samo jednom. Obavezno ju kopirajte prije zatvaranja prozora.",
+  "api_key_empty": "Naziv vašeg API ključa ne smije biti prazan",
+  "api_keys": "API Ključevi",
+  "app_settings": "Postavke Aplikacije",
+  "appears_in": "Pojavljuje se u",
+  "archive": "Arhiva",
+  "archive_or_unarchive_photo": "Arhivirajte ili dearhivirajte fotografiju",
+  "archive_size": "Veličina arhive",
+  "archive_size_description": "Konfigurirajte veličinu arhive za preuzimanja (u GiB)",
+  "archived_count": "{count, plural, other {Archived #}}",
+  "are_these_the_same_person": "Je li ovo ista osoba?",
+  "are_you_sure_to_do_this": "Jeste li sigurni da to želite učiniti?",
+  "asset_added_to_album": "Dodano u album",
+  "asset_adding_to_album": "Dodavanje u album...",
+  "asset_description_updated": "Opis imovine je ažuriran",
+  "asset_filename_is_offline": "Sredstvo {filename} je izvan mreže",
+  "asset_has_unassigned_faces": "Materijal ima nedodijeljena lica",
+  "asset_hashing": "Hashiranje...",
+  "asset_offline": "Sredstvo izvan mreže",
+  "asset_offline_description": "Ovaj materijal je izvan mreže. Immich ne može pristupiti lokaciji datoteke. Provjerite je li sredstvo dostupno, a zatim ponovno skenirajte biblioteku.",
+  "asset_skipped": "Preskočeno",
+  "asset_skipped_in_trash": "U smeću",
+  "asset_uploaded": "Učitano",
+  "asset_uploading": "Učitavanje...",
+  "assets": "Sredstva",
+  "assets_added_count": "Dodano {count, plural, one {# asset} other {# assets}}",
+  "assets_added_to_album_count": "Dodano {count, plural, one {# asset} other {# assets}} u album",
+  "assets_added_to_name_count": "Dodano {count, plural, one {# asset} other {# assets}} u {hasName, select, true {<b>{name}</b>} other {new album}}",
+  "assets_count": "{count, plural, one {# asset} other {# assets}}",
+  "assets_moved_to_trash_count": "{count, plural, one {# asset} other {# asset}} premješteno u smeće",
+  "assets_permanently_deleted_count": "Trajno izbrisano {count, plural, one {# asset} other {# assets}}",
+  "assets_removed_count": "Uklonjeno {count, plural, one {# asset} other {# assets}}",
+  "assets_restore_confirmation": "Jeste li sigurni da želite vratiti sve svoje resurse bačene u otpad? Ne možete poništiti ovu radnju!",
+  "assets_restored_count": "Vraćeno {count, plural, one {# asset} other {# assets}}",
+  "assets_trashed_count": "Bačeno u smeće {count, plural, one {# asset} other {# assets}}",
+  "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} već dio albuma",
+  "authorized_devices": "Ovlašteni Uređaji",
+  "back": "Nazad",
+  "back_close_deselect": "Natrag, zatvorite ili poništite odabir",
+  "backward": "Unazad",
+  "birthdate_saved": "Datum rođenja uspješno spremljen",
+  "birthdate_set_description": "Datum rođenja se koristi za izračunavanje godina ove osobe u trenutku fotografije.",
+  "blurred_background": "Zamućena pozadina",
+  "bugs_and_feature_requests": "Bugovi i zahtjevi za značajke",
+  "build": "Sagradi (Build)",
+  "build_image": "Sagradi (Build) Image",
+  "bulk_delete_duplicates_confirmation": "Jeste li sigurni da želite skupno izbrisati {count, plural, one {# duplicate asset} other {# duplicate asset}}? Ovo će zadržati najveće sredstvo svake grupe i trajno izbrisati sve druge duplikate. Ne možete poništiti ovu radnju!",
+  "bulk_keep_duplicates_confirmation": "Jeste li sigurni da želite zadržati {count, plural, one {# duplicate asset} other {# duplicate asset}}? Ovo će riješiti sve duplicirane grupe bez brisanja ičega.",
+  "bulk_trash_duplicates_confirmation": "Jeste li sigurni da želite na veliko baciti u smeće {count, plural, one {# duplicate asset} other {# duplicate asset}}? Ovo će zadržati najveće sredstvo svake grupe i baciti sve ostale duplikate u smeće.",
+  "buy": "Kupi Immich",
+  "camera": "Kamera",
+  "camera_brand": "Marka kamere",
+  "camera_model": "Model kamere",
+  "cancel": "Otkaži",
+  "cancel_search": "Otkaži pretragu",
+  "cannot_merge_people": "Nije moguće spojiti osobe",
+  "cannot_undo_this_action": "Ne možete poništiti ovu radnju!",
+  "cannot_update_the_description": "Nije moguće ažurirati opis",
+  "change_date": "Promjena datuma",
+  "change_expiration_time": "Promjena vremena isteka",
+  "change_location": "Promjena lokacije",
+  "change_name": "Promjena imena",
+  "change_name_successfully": "Promijena imena uspješna",
+  "change_password": "Promjena Lozinke",
+  "change_password_description": "Ovo je ili prvi put da se prijavljujete u sustav ili je poslan zahtjev za promjenom lozinke. Unesite novu lozinku ispod.",
+  "change_your_password": "Promijenite lozinku",
+  "changed_visibility_successfully": "Vidljivost je uspješno promijenjena",
+  "check_all": "Provjeri Sve",
+  "check_logs": "Provjera Zapisa",
+  "choose_matching_people_to_merge": "Odaberite odgovarajuće osobe za spajanje",
+  "city": "Grad",
+  "clear": "Očisti",
+  "clear_all": "Očisti sve",
+  "clear_all_recent_searches": "Izbriši sva nedavna pretraživanja",
+  "clear_message": "Jasna poruka",
+  "clear_value": "Očisti vrijednost",
+  "clockwise": "U smjeru kazaljke na satu",
+  "close": "Zatvori",
+  "collapse": "Sažmi",
+  "collapse_all": "Sažmi sve",
+  "color": "Boja",
+  "color_theme": "Tema boja",
+  "comment_deleted": "Komentar izbrisan",
+  "comment_options": "Opcije komentara",
+  "comments_and_likes": "Komentari i lajkovi",
+  "comments_are_disabled": "Komentari onemogućeni",
+  "confirm": "Potvrdi",
+  "confirm_admin_password": "Potvrdite lozinku administratora",
+  "confirm_delete_shared_link": "Jeste li sigurni da želite izbrisati ovu zajedničku vezu?",
+  "confirm_keep_this_delete_others": "Sva druga sredstva u nizu bit će izbrisana osim ovog sredstva. Jeste li sigurni da želite nastaviti?",
+  "confirm_password": "Potvrdite lozinku",
+  "contain": "Sadrži",
+  "context": "Kontekst",
+  "continue": "Nastavi",
+  "copied_image_to_clipboard": "Slika je kopirana u međuspremnik.",
+  "copied_to_clipboard": "Kopirano u međuspremnik!",
+  "copy_error": "Greška kopiranja",
+  "copy_file_path": "Kopiraj put datoteke",
+  "copy_image": "Kopiraj Sliku",
+  "copy_link": "Kopiraj poveznicu",
+  "copy_link_to_clipboard": "Kopiraj poveznicu u međuspremnik",
+  "copy_password": "Kopiraj lozinku",
+  "copy_to_clipboard": "Kopiraj u međuspremnik",
+  "country": "Država",
+  "cover": "Naslovnica",
+  "covers": "Naslovnice",
+  "create": "Kreiraj",
+  "create_album": "Kreiraj album",
+  "create_library": "Kreiraj Biblioteku",
+  "create_link": "Kreiraj poveznicu",
+  "create_link_to_share": "Izradite vezu za dijeljenje",
+  "create_link_to_share_description": "Dopusti svakome s vezom da vidi odabrane fotografije",
+  "create_new_person": "Stvorite novu osobu",
+  "create_new_person_hint": "Dodijelite odabrana sredstva novoj osobi",
+  "create_new_user": "Kreiraj novog korisnika",
+  "create_tag": "Stvori oznaku",
+  "create_tag_description": "Napravite novu oznaku. Za ugniježđene oznake unesite punu putanju oznake uključujući kose crte.",
+  "create_user": "Stvori korisnika",
+  "created": "Stvoreno",
+  "current_device": "Trenutačni uređaj",
+  "custom_locale": "Prilagođena Lokalizacija",
+  "custom_locale_description": "Formatiranje datuma i brojeva na temelju jezika i regije",
+  "dark": "Tamno",
+  "date_after": "Datum nakon",
+  "date_and_time": "Datum i Vrijeme",
+  "date_before": "Datum prije",
+  "date_of_birth_saved": "Datum rođenja uspješno spremljen",
+  "date_range": "Razdoblje",
+  "day": "Dan",
+  "deduplicate_all": "Dedupliciraj Sve",
+  "default_locale": "Zadana lokalizacija",
+  "default_locale_description": "Oblikujte datume i brojeve na temelju jezika preglednika",
+  "delete": "Izbriši",
+  "delete_album": "Izbriši album",
+  "delete_api_key_prompt": "Jeste li sigurni da želite izbrisati ovaj API ključ?",
+  "delete_duplicates_confirmation": "Jeste li sigurni da želite trajno izbrisati ove duplikate?",
+  "delete_key": "Ključ za brisanje",
+  "delete_library": "Izbriši knjižnicu",
+  "delete_link": "Izbriši poveznicu",
+  "delete_others": "Izbriši druge",
+  "delete_shared_link": "Izbriši dijeljenu poveznicu",
+  "delete_tag": "Izbriši oznaku",
+  "delete_tag_confirmation_prompt": "Jeste li sigurni da želite izbrisati oznaku {tagName}?",
+  "delete_user": "Izbriši korisnika",
+  "deleted_shared_link": "Izbrisana dijeljena poveznica",
+  "deletes_missing_assets": "Briše sredstva koja nedostaju s diska",
+  "description": "Opis",
+  "details": "Detalji",
+  "direction": "Smjer",
+  "disabled": "Onemogućeno",
+  "disallow_edits": "Zabrani izmjene",
+  "discord": "Discord",
+  "discover": "Otkrij",
+  "dismiss_all_errors": "Odbaci sve pogreške",
+  "dismiss_error": "Odbaci pogrešku",
+  "display_options": "Mogućnosti prikaza",
+  "display_order": "Redoslijed prikaza",
+  "display_original_photos": "Prikaz originalnih fotografija",
+  "display_original_photos_setting_description": "Radije prikažite izvornu fotografiju kada gledate materijal umjesto sličica kada je izvorni materijal kompatibilan s webom. To može rezultirati sporijim brzinama prikaza fotografija.",
+  "do_not_show_again": "Ne prikazuj više ovu poruku",
+  "documentation": "Dokumentacija",
+  "done": "Gotovo",
+  "download": "Preuzmi",
+  "download_include_embedded_motion_videos": "Ugrađeni videozapisi",
+  "download_include_embedded_motion_videos_description": "Uključite videozapise ugrađene u fotografije s pokretom kao zasebnu datoteku",
+  "download_settings": "Preuzmi",
+  "download_settings_description": "Upravljajte postavkama koje se odnose na preuzimanje sredstava",
+  "downloading": "Preuzimanje",
+  "downloading_asset_filename": "Preuzimanje materijala {filename}",
+  "drop_files_to_upload": "Ispustite datoteke bilo gdje za prijenos",
+  "duplicates": "Duplikati",
+  "duplicates_description": "Razriješite svaku grupu tako da naznačite koji su duplikati, ako ih ima",
+  "duration": "Trajanje",
+  "edit": "Izmjena",
+  "edit_album": "Uredi album",
+  "edit_avatar": "Uredi avatar",
+  "edit_date": "Uredi datum",
+  "edit_date_and_time": "Uredite datum i vrijeme",
+  "edit_exclusion_pattern": "Uredi uzorak izuzimanja",
+  "edit_faces": "Uređivanje lica",
+  "edit_import_path": "Uredi put uvoza",
+  "edit_import_paths": "Uredi Uvozne Putanje",
+  "edit_key": "Ključ za uređivanje",
+  "edit_link": "Uredi poveznicu",
+  "edit_location": "Uredi lokaciju",
+  "edit_name": "Uredi ime",
+  "edit_people": "Uredi ljude",
+  "edit_tag": "Uredi oznaku",
+  "edit_title": "Uredi Naslov",
+  "edit_user": "Uredi korisnika",
+  "edited": "Uređeno",
+  "editor": "Urednik",
+  "editor_close_without_save_prompt": "Promjene neće biti spremljene",
+  "editor_close_without_save_title": "Zatvoriti uređivač?",
+  "editor_crop_tool_h2_aspect_ratios": "Omjeri stranica",
+  "editor_crop_tool_h2_rotation": "Rotacija",
+  "email": "E-pošta",
+  "empty_trash": "Isprazni smeće",
+  "empty_trash_confirmation": "Jeste li sigurni da želite isprazniti smeće? Time će se iz Immicha trajno ukloniti sva sredstva u otpadu.\nNe možete poništiti ovu radnju!",
+  "enable": "Omogući",
+  "enabled": "Omogućeno",
+  "end_date": "Datum završetka",
+  "error": "Greška",
+  "error_loading_image": "Pogreška pri učitavanju slike",
+  "error_title": "Greška - Nešto je pošlo krivo",
+  "errors": {
+    "cannot_navigate_next_asset": "Nije moguće prijeći na sljedeći materijal",
+    "cannot_navigate_previous_asset": "Nije moguće prijeći na prethodni materijal",
+    "cant_apply_changes": "Nije moguće primijeniti promjene",
+    "cant_change_activity": "Ne mogu {enabled, select, true {disable} druge {enable}} aktivnosti",
+    "cant_change_asset_favorite": "Nije moguće promijeniti favorita za sredstvo",
+    "cant_change_metadata_assets_count": "Nije moguće promijeniti metapodatke {count, plural, one {# asset} other {# assets}}",
+    "cant_get_faces": "Ne mogu dobiti lica",
+    "cant_get_number_of_comments": "Ne mogu dobiti broj komentara",
+    "cant_search_people": "Ne mogu pretraživati ljude",
+    "cant_search_places": "Ne mogu pretraživati mjesta",
+    "cleared_jobs": "Izbrisani poslovi za: {job}",
+    "error_adding_assets_to_album": "Pogreška pri dodavanju materijala u album",
+    "error_adding_users_to_album": "Pogreška pri dodavanju korisnika u album",
+    "error_deleting_shared_user": "Pogreška pri brisanju dijeljenog korisnika",
+    "error_downloading": "Pogreška pri preuzimanju {filename}",
+    "error_hiding_buy_button": "Pogreška pri skrivanju gumba za kupnju",
+    "error_removing_assets_from_album": "Pogreška prilikom uklanjanja materijala iz albuma, provjerite konzolu za više pojedinosti",
+    "error_selecting_all_assets": "Pogreška pri odabiru svih sredstava",
+    "exclusion_pattern_already_exists": "Ovaj uzorak izuzimanja već postoji.",
+    "failed_job_command": "Naredba {command} nije uspjela za posao: {job}",
+    "failed_to_create_album": "Izrada albuma nije uspjela",
+    "failed_to_create_shared_link": "Stvaranje dijeljene veze nije uspjelo",
+    "failed_to_edit_shared_link": "Nije uspjelo uređivanje dijeljene poveznice",
+    "failed_to_get_people": "Dohvaćanje ljudi nije uspjelo",
+    "failed_to_keep_this_delete_others": "Zadržavanje ovog sredstva i brisanje ostalih sredstava nije uspjelo",
+    "failed_to_load_asset": "Učitavanje sredstva nije uspjelo",
+    "failed_to_load_assets": "Učitavanje sredstava nije uspjelo",
+    "failed_to_load_people": "Učitavanje ljudi nije uspjelo",
+    "failed_to_remove_product_key": "Uklanjanje ključa proizvoda nije uspjelo",
+    "failed_to_stack_assets": "Slaganje sredstava nije uspjelo",
+    "failed_to_unstack_assets": "Nije uspjelo uklanjanje snopa sredstava",
+    "import_path_already_exists": "Ovaj uvozni put već postoji.",
+    "incorrect_email_or_password": "Netočna adresa e-pošte ili lozinka",
+    "paths_validation_failed": "{paths, plural, one {# putanja nije prošla} other {# putanje nisu prošle}} provjeru valjanosti",
+    "profile_picture_transparent_pixels": "Profilne slike ne smiju imati prozirne piksele. Povećajte i/ili pomaknite sliku.",
+    "quota_higher_than_disk_size": "Postavili ste kvotu veću od veličine diska",
+    "repair_unable_to_check_items": "Nije moguće provjeriti {count, select, one {item} other {items}}",
+    "unable_to_add_album_users": "Nije moguće dodati korisnike u album",
+    "unable_to_add_assets_to_shared_link": "Nije moguće dodati sredstva na dijeljenu poveznicu",
+    "unable_to_add_comment": "Nije moguće dodati komentar",
+    "unable_to_add_exclusion_pattern": "Nije moguće dodati uzorak izuzimanja",
+    "unable_to_add_import_path": "Nije moguće dodati putanju uvoza",
+    "unable_to_add_partners": "Nije moguće dodati partnere",
+    "unable_to_add_remove_archive": "Nije moguće {arhivirano, odabrati, istinito {ukloniti sredstvo iz} druge {dodati sredstvo u}} arhivu",
+    "unable_to_add_remove_favorites": "Nije moguće {favorite, select, true {add asset to} other {remove asset from}} favorite",
+    "unable_to_archive_unarchive": "Nije moguće {arhivirati, odabrati, istinito {arhivirati} ostalo {dearhivirati}}",
+    "unable_to_change_album_user_role": "Nije moguće promijeniti ulogu korisnika albuma",
+    "unable_to_change_date": "Nije moguće promijeniti datum",
+    "unable_to_change_favorite": "Nije moguće promijeniti favorita za sredstvo",
+    "unable_to_change_location": "Nije moguće promijeniti lokaciju",
+    "unable_to_change_password": "Nije moguće promijeniti lozinku",
+    "unable_to_change_visibility": "Nije moguće promijeniti vidljivost za {count, plural, one {# osobu} other {# osobe}}",
+    "unable_to_complete_oauth_login": "Nije moguće dovršiti OAuth prijavu",
+    "unable_to_connect": "Povezivanje nije moguće",
+    "unable_to_connect_to_server": "Nije moguće spojiti se na poslužitelj",
+    "unable_to_copy_to_clipboard": "Nije moguće kopirati u međuspremnik, provjerite pristupate li stranici putem https-a",
+    "unable_to_create_admin_account": "Nije moguće stvoriti administratorski račun",
+    "unable_to_create_api_key": "Nije moguće izraditi novi API ključ",
+    "unable_to_create_library": "Nije moguće stvoriti biblioteku",
+    "unable_to_create_user": "Nije moguće stvoriti korisnika",
+    "unable_to_delete_album": "Nije moguće izbrisati album",
+    "unable_to_delete_asset": "Nije moguće izbrisati sredstvo",
+    "unable_to_delete_assets": "Pogreška pri brisanju sredstava",
+    "unable_to_delete_exclusion_pattern": "Nije moguće izbrisati uzorak izuzimanja",
+    "unable_to_delete_import_path": "Nije moguće izbrisati put uvoza",
+    "unable_to_delete_shared_link": "Nije moguće izbrisati dijeljenu poveznicu",
+    "unable_to_delete_user": "Nije moguće izbrisati korisnika",
+    "unable_to_download_files": "Nije moguće preuzeti datoteke",
+    "unable_to_edit_exclusion_pattern": "Nije moguće urediti uzorak izuzimanja",
+    "unable_to_edit_import_path": "Nije moguće urediti put uvoza",
+    "unable_to_empty_trash": "Nije moguće isprazniti otpad",
+    "unable_to_enter_fullscreen": "Nije moguće otvoriti cijeli zaslon",
+    "unable_to_exit_fullscreen": "Nije moguće izaći iz cijelog zaslona",
+    "unable_to_get_comments_number": "Nije moguće dobiti broj komentara",
+    "unable_to_get_shared_link": "Dohvaćanje dijeljene veze nije uspjelo",
+    "unable_to_hide_person": "Nije moguće sakriti osobu",
+    "unable_to_link_motion_video": "Nije moguće povezati videozapis pokreta",
+    "unable_to_link_oauth_account": "Nije moguće povezati OAuth račun",
+    "unable_to_load_album": "Nije moguće učitati album",
+    "unable_to_load_asset_activity": "Nije moguće učitati aktivnost sredstva",
+    "unable_to_load_items": "Nije moguće učitati stavke",
+    "unable_to_load_liked_status": "Nije moguće učitati status sviđanja",
+    "unable_to_log_out_all_devices": "Nije moguće odjaviti sve uređaje",
+    "unable_to_log_out_device": "Nije moguće odjaviti uređaj",
+    "unable_to_login_with_oauth": "Nije moguće prijaviti se pomoću OAutha",
+    "unable_to_play_video": "Nije moguće reproducirati video",
+    "unable_to_reassign_assets_existing_person": "Nije moguće ponovno dodijeliti imovinu na {name, select, null {postojeću osobu} other {{name}}}",
+    "unable_to_reassign_assets_new_person": "Nije moguće ponovno dodijeliti imovinu novoj osobi",
+    "unable_to_refresh_user": "Nije moguće osvježiti korisnika",
+    "unable_to_remove_album_users": "Nije moguće ukloniti korisnike iz albuma",
+    "unable_to_remove_api_key": "Nije moguće ukloniti API ključ",
+    "unable_to_remove_assets_from_shared_link": "Nije moguće ukloniti sredstva iz dijeljene poveznice",
+    "unable_to_remove_deleted_assets": "Nije moguće ukloniti izvanmrežne datoteke",
+    "unable_to_remove_library": "Nije moguće ukloniti biblioteku",
+    "unable_to_remove_partner": "Nije moguće ukloniti partnera",
+    "unable_to_remove_reaction": "Nije moguće ukloniti reakciju",
+    "unable_to_repair_items": "Nije moguće popraviti stavke",
+    "unable_to_reset_password": "Nije moguće ponovno postaviti lozinku",
+    "unable_to_resolve_duplicate": "Nije moguće razriješiti duplikat",
+    "unable_to_restore_assets": "Nije moguće vratiti imovinu",
+    "unable_to_restore_trash": "Nije moguće vratiti otpad",
+    "unable_to_restore_user": "Nije moguće vratiti korisnika",
+    "unable_to_save_album": "Nije moguće spremiti album",
+    "unable_to_save_api_key": "Nije moguće spremiti API ključ",
+    "unable_to_save_date_of_birth": "Nije moguće spremiti datum rođenja",
+    "unable_to_save_name": "Nije moguće spremiti ime",
+    "unable_to_save_profile": "Nije moguće spremiti profil",
+    "unable_to_save_settings": "Nije moguće spremiti postavke",
+    "unable_to_scan_libraries": "Nije moguće skenirati knjižnice",
+    "unable_to_scan_library": "Nije moguće skenirati knjižnicu",
+    "unable_to_set_feature_photo": "Nije moguće postaviti istaknutu fotografiju",
+    "unable_to_set_profile_picture": "Nije moguće postaviti profilnu sliku",
+    "unable_to_submit_job": "Nije moguće poslati posao",
+    "unable_to_trash_asset": "Nije moguće baciti sredstvo u smeće",
+    "unable_to_unlink_account": "Nije moguće prekinuti vezu računa",
+    "unable_to_unlink_motion_video": "Nije moguće prekinuti vezu videozapisa pokreta",
+    "unable_to_update_album_cover": "Nije moguće ažurirati omot albuma",
+    "unable_to_update_album_info": "Nije moguće ažurirati informacije o albumu",
+    "unable_to_update_library": "Nije moguće ažurirati biblioteku",
+    "unable_to_update_location": "Nije moguće ažurirati lokaciju",
+    "unable_to_update_settings": "Nije moguće ažurirati postavke",
+    "unable_to_update_timeline_display_status": "Nije moguće ažurirati status prikaza vremenske trake",
+    "unable_to_update_user": "Nije moguće ažurirati korisnika",
+    "unable_to_upload_file": "Nije moguće učitati datoteku"
+  },
+  "exif": "Exif",
+  "exit_slideshow": "Izađi iz projekcije slideova",
+  "expand_all": "Proširi sve",
+  "expire_after": "Istječe nakon",
+  "expired": "Isteklo",
+  "expires_date": "Ističe {date}",
+  "explore": "Istraži",
+  "explorer": "Pretraživač (Explorer)",
+  "export": "Izvoz",
+  "export_as_json": "Izvezi kao JSON",
+  "extension": "Proširenje (Extension)",
+  "external": "Vanjski",
+  "external_libraries": "Vanjske Biblioteke",
+  "face_unassigned": "Nedodijeljeno",
+  "favorite": "Omiljeno",
+  "favorite_or_unfavorite_photo": "Omiljena ili neomiljena fotografija",
+  "favorites": "Omiljene",
+  "feature_photo_updated": "Istaknuta fotografija ažurirana",
+  "features": "Značajke (Features)",
+  "features_setting_description": "Upravljajte značajkama aplikacije",
+  "file_name": "Naziv datoteke",
+  "file_name_or_extension": "Naziv ili ekstenzija datoteke",
+  "filename": "Naziv datoteke",
+  "filetype": "Vrsta datoteke",
+  "filter_people": "Filtrirajte ljude",
+  "find_them_fast": "Pronađite ih brzo po imenu pomoću pretraživanja",
+  "fix_incorrect_match": "Ispravite netočno podudaranje",
+  "folders": "Mape",
+  "folders_feature_description": "Pregledavanje prikaza mape za fotografije i videozapise u sustavu datoteka",
+  "forward": "Naprijed",
+  "general": "Općenito",
+  "get_help": "Potražite pomoć",
+  "getting_started": "Početak Rada",
+  "go_back": "Idi natrag",
+  "go_to_search": "Idi na pretragu",
+  "group_albums_by": "Grupiraj albume po...",
+  "group_no": "Nema grupiranja",
+  "group_owner": "Grupiraj po vlasniku",
+  "group_year": "Grupiraj po godini",
+  "has_quota": "Ima kvotu",
+  "hi_user": "Bok {name} ({email})",
+  "hide_all_people": "Sakrij sve ljude",
+  "hide_gallery": "Sakrij galeriju",
+  "hide_named_person": "Sakrij osobu {name}",
+  "hide_password": "Sakrij lozinku",
+  "hide_person": "Sakrij osobu",
+  "hide_unnamed_people": "Sakrij neimenovane osobe",
+  "host": "Domaćin",
+  "hour": "Sat",
+  "image": "Slika",
+  "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} snimljeno {date}",
+  "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} snimljeno s {person1} {date}",
+  "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} snimljeno s {person1} i {person2} {date}",
+  "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} snimljeno s {person1}, {person2} i {person3} {date}",
+  "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} snimljeno s {person1}, {person2} i {additionalCount, number} drugih {date}",
+  "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} {date}",
+  "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1} {date}",
+  "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1} i {person2} {date}",
+  "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1}, {person2} i {person3} {date}",
+  "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1}, {person2} i {additionalCount, number} drugih {date}",
+  "immich_logo": "Immich Logo",
+  "immich_web_interface": "Immich Web Sučelje",
+  "import_from_json": "Uvoz iz JSON-a",
+  "import_path": "Putanja uvoza",
+  "in_albums": "U {count, plural, one {# album} other {# albuma}}",
+  "in_archive": "U arhivi",
+  "include_archived": "Uključi arhivirano",
+  "include_shared_albums": "Uključi dijeljene albume",
+  "include_shared_partner_assets": "Uključite zajedničku imovinu partnera",
+  "individual_share": "Pojedinačni udio",
+  "info": "Informacije",
+  "interval": {
+    "day_at_onepm": "Svaki dan u 13 sati",
+    "hours": "{hours, plural, one {Svaki sat} few {Svakih {hours, number} sata} other {Svakih {hours, number} sati}}",
+    "night_at_midnight": "Svaku večer u ponoć",
+    "night_at_twoam": "Svake noći u 2 ujutro"
+  },
+  "invite_people": "Pozovite ljude",
+  "invite_to_album": "Pozovi u album",
+  "items_count": "{count, plural, one {# datoteka} other {# datoteke}}",
+  "jobs": "Poslovi",
+  "keep": "Zadrži",
+  "keep_all": "Zadrži Sve",
+  "keep_this_delete_others": "Zadrži ovo, izbriši ostale",
+  "kept_this_deleted_others": "Zadržana je ova datoteka i izbrisano {count, plural, one {# datoteka} other {# datoteka}}",
+  "keyboard_shortcuts": "Prečaci tipkovnice",
+  "language": "Jezik",
+  "language_setting_description": "Odaberite željeni jezik",
+  "last_seen": "Zadnji put viđen",
+  "latest_version": "Najnovija verzija",
+  "latitude": "Zemljopisna širina",
+  "leave": "Izađi",
+  "let_others_respond": "Dozvoli da drugi odgovore",
+  "level": "Razina",
+  "library": "Biblioteka",
+  "library_options": "Mogućnosti biblioteke",
+  "light": "Svjetlo",
+  "like_deleted": "Like izbrisan",
+  "link_motion_video": "Povežite videozapis pokreta",
+  "link_options": "Opcije veze",
+  "link_to_oauth": "Veza na OAuth",
+  "linked_oauth_account": "Povezani OAuth račun",
+  "list": "Popis",
+  "loading": "Učitavanje",
+  "loading_search_results_failed": "Učitavanje rezultata pretraživanja nije uspjelo",
+  "log_out": "Odjavi se",
+  "log_out_all_devices": "Odjava sa svih uređaja",
+  "logged_out_all_devices": "Odjavljeni su svi uređaji",
+  "logged_out_device": "Odjavljen uređaj",
+  "login": "Prijava",
+  "login_has_been_disabled": "Prijava je onemogućena.",
+  "logout_all_device_confirmation": "Jeste li sigurni da želite odjaviti sve uređaje?",
+  "logout_this_device_confirmation": "Jeste li sigurni da se želite odjaviti s ovog uređaja?",
+  "longitude": "Zemljopisna dužina",
+  "look": "Izgled",
+  "loop_videos": "Ponavljajte videozapise",
+  "loop_videos_description": "Omogućite automatsko ponavljanje videozapisa u pregledniku detalja.",
+  "main_branch_warning": "Koristite razvojnu verziju; strogo preporučamo korištenje izdane verzije!",
+  "make": "Proizvođač",
+  "manage_shared_links": "Upravljanje dijeljenim vezama",
+  "manage_sharing_with_partners": "Upravljajte dijeljenjem s partnerima",
+  "manage_the_app_settings": "Upravljajte postavkama aplikacije",
+  "manage_your_account": "Upravljajte svojim računom",
+  "manage_your_api_keys": "Upravljajte svojim API ključevima",
+  "manage_your_devices": "Upravljajte uređajima na kojima ste prijavljeni",
+  "manage_your_oauth_connection": "Upravljajte svojom OAuth vezom",
+  "map": "Karta",
+  "map_marker_for_images": "Oznaka karte za slike snimljene u {city}, {country}",
+  "map_marker_with_image": "Oznaka karte sa slikom",
+  "map_settings": "Postavke karte",
+  "matches": "Podudaranja",
+  "media_type": "Vrsta medija",
+  "memories": "Sjećanja",
+  "memories_setting_description": "Upravljajte onim što vidite u svojim sjećanjima",
+  "memory": "Memorija",
+  "memory_lane_title": "Traka sjećanja {title}",
+  "menu": "Izbornik",
+  "merge": "Spoji",
+  "merge_people": "Spajanje ljudi",
+  "merge_people_limit": "Možete spojiti najviše 5 lica odjednom",
+  "merge_people_prompt": "Želite li spojiti ove ljude? Ova radnja je nepovratna.",
+  "merge_people_successfully": "Uspješno spajanje ljudi",
+  "merged_people_count": "{count, plural, one {# Spojena osoba} other {# Spojene osobe}}",
+  "minimize": "Minimiziraj",
+  "minute": "Minuta",
+  "missing": "Nedostaje",
+  "model": "Model",
+  "month": "Mjesec",
+  "more": "Više",
+  "moved_to_trash": "Premješteno u smeće",
+  "my_albums": "Moji albumi",
+  "name": "Ime",
+  "name_or_nickname": "Ime ili nadimak",
+  "never": "Nikada",
+  "new_album": "Novi Album",
+  "new_api_key": "Novi API ključ",
+  "new_password": "Nova lozinka",
+  "new_person": "Nova osoba",
+  "new_user_created": "Stvoren novi korisnik",
+  "new_version_available": "DOSTUPNA NOVA VERZIJA",
+  "newest_first": "Prvo najnovije",
+  "next": "Sljedeće",
+  "next_memory": "Sljedeće sjećanje",
+  "no": "Ne",
+  "no_albums_message": "Izradite album za organiziranje svojih fotografija i videozapisa",
+  "no_albums_with_name_yet": "Čini se da još nemate nijedan album s ovim imenom.",
+  "no_albums_yet": "Čini se da još nemate nijedan album.",
+  "no_archived_assets_message": "Arhivirajte fotografije i videozapise kako biste ih sakrili iz prikaza fotografija",
+  "no_assets_message": "KLIKNITE DA PRENESETE SVOJU PRVU FOTOGRAFIJU",
+  "no_duplicates_found": "Nisu pronađeni duplikati.",
+  "no_exif_info_available": "Nema dostupnih exif podataka",
+  "no_explore_results_message": "Prenesite više fotografija da istražite svoju zbirku.",
+  "no_favorites_message": "Dodajte favorite kako biste brzo pronašli svoje najbolje slike i videozapise",
+  "no_libraries_message": "Stvorite vanjsku biblioteku za pregled svojih fotografija i videozapisa",
+  "no_name": "Bez imena",
+  "no_places": "Nema mjesta",
+  "no_results": "Nema rezultata",
+  "no_results_description": "Pokušajte sa sinonimom ili općenitijom ključnom riječi",
+  "no_shared_albums_message": "Stvorite album za dijeljenje fotografija i videozapisa s osobama u svojoj mreži",
+  "not_in_any_album": "Ni u jednom albumu",
+  "note_apply_storage_label_to_previously_uploaded assets": "Napomena: Da biste primijenili Oznaku za skladištenje na prethodno prenesena sredstva, pokrenite",
+  "note_unlimited_quota": "napomena: Unesite 0 za neograni%C4%8Denu kvotu",
+  "notes": "Bilješke",
+  "notification_toggle_setting_description": "Omogući obavijesti putem e-pošte",
+  "notifications": "Obavijesti",
+  "notifications_setting_description": "Upravljanje obavijestima",
+  "oauth": "OAuth",
+  "official_immich_resources": "Službeni Immich resursi",
+  "offline": "Izvan mreže",
+  "offline_paths": "Izvanmrežne putanje",
+  "offline_paths_description": "Ovi rezultati mogu biti posljedica ručnog brisanja datoteka koje nisu dio vanjske biblioteke.",
+  "ok": "Ok",
+  "oldest_first": "Prvo najstarije",
+  "onboarding": "Uključivanje (Onboarding)",
+  "onboarding_privacy_description": "Sljedeće (neobavezne) značajke oslanjaju se na vanjske usluge i mogu se onemogućiti u bilo kojem trenutku u postavkama administracije.",
+  "onboarding_theme_description": "Odaberite temu boja za svoj primjer. To možete kasnije promijeniti u postavkama.",
+  "onboarding_welcome_description": "Postavimo vašu instancu s nekim uobičajenim postavkama.",
+  "onboarding_welcome_user": "Dobro došli, {user}",
+  "online": "Dostupan (Online)",
+  "only_favorites": "Samo omiljeno",
+  "open_in_map_view": "Otvori u prikazu karte",
+  "open_in_openstreetmap": "Otvori u OpenStreetMap",
+  "open_the_search_filters": "Otvorite filtre pretraživanja",
+  "options": "Opcije",
+  "or": "ili",
+  "organize_your_library": "Organizirajte svoju knjižnicu",
+  "original": "original",
+  "other": "Ostalo",
+  "other_devices": "Ostali uređaji",
+  "other_variables": "Ostale varijable",
+  "owned": "Vlasništvo",
+  "owner": "Vlasnik",
+  "partner": "Partner",
+  "partner_can_access": "{partner} može pristupiti",
+  "partner_can_access_assets": "Sve vaše fotografije i videi osim onih u arhivi i smeću",
+  "partner_can_access_location": "Mjesto otkuda je slika otkinuta",
+  "partner_sharing": "Dijeljenje s partnerom",
+  "partners": "Partneri",
+  "password": "Zaporka",
+  "password_does_not_match": "Zaporka se ne podudara",
+  "password_required": "Zaporka je obavezna",
+  "password_reset_success": "Reset zaporke je uspješan",
+  "past_durations": {
+    "days": "{days, plural, one {Prošli dan} few {Prošlih # dana} other {Prošlih # dana}}",
+    "hours": "{hours, plural, one {Prošli sat} few {Prošla # sata} other {Prošlih # sati}}",
+    "years": "{years, plural, one {Prošle godine} few {Prošle # godine} other {Prošlih # godina}}"
+  },
+  "path": "Putanja",
+  "pattern": "Uzorak",
+  "pause": "Pauza",
+  "pause_memories": "Pauziraj sjećanja",
+  "paused": "Pauzirano",
+  "pending": "Na čekanju",
+  "people": "Ljudi",
+  "people_edits_count": "Izmjenjeno {count, plural, one {# osoba} other {# osobe}}",
+  "people_feature_description": "Pregledavanje fotografija i videozapisa grupiranih po osobama",
+  "people_sidebar_description": "Prikažite poveznicu na Osobe na bočnoj traci",
+  "permanent_deletion_warning": "Upozorenje za nepovratno brisanje",
+  "permanent_deletion_warning_setting_description": "Prikaži upozorenje prilikom trajnog brisanja sredstava",
+  "permanently_delete": "Nepovratno obriši",
+  "permanently_delete_assets_count": "Trajno izbriši {count, plural, one {datoteku} other {datoteke}}",
+  "permanently_delete_assets_prompt": "Da li ste sigurni da želite trajni izbrisati {count, plural, one {ovu datoteku?} other {ove <b>#</b> datoteke?}}Ovo će ih također ukloniti {count, plural, one {iz njihovog} other {iz njihovih}} albuma.",
+  "permanently_deleted_asset": "Trajno izbrisano sredstvo",
+  "permanently_deleted_assets_count": "Trajno izbrisano {count, plural, one {# datoteka} other {# datoteke}}",
+  "person": "Osoba",
+  "person_hidden": "{name}{hidden, select, true { (skriveno)} other {}}",
+  "photo_shared_all_users": "Čini se da ste svoje fotografije podijelili sa svim korisnicima ili nemate nijednog korisnika s kojim biste ih podijelili.",
+  "photos": "Fotografije",
+  "photos_and_videos": "Fotografije i videozapisi",
+  "photos_count": "{count, plural, one {{count, number} fotografija} few {{count, number} fotografije} other {{count, number} fotografija}}",
+  "photos_from_previous_years": "Fotografije iz prethodnih godina",
+  "pick_a_location": "Odaberite lokaciju",
+  "place": "Mjesto",
+  "places": "Mjesta",
+  "play": "Pokreni",
+  "play_memories": "Pokreni sjećanja",
+  "play_motion_photo": "Reproduciraj Pokretnu fotografiju",
+  "play_or_pause_video": "Reproducirajte ili pauzirajte video",
+  "port": "Port",
+  "preset": "Unaprijed postavljeno",
+  "preview": "Pregled",
+  "previous": "Prethodno",
+  "previous_memory": "Prethodno sjećanje",
+  "previous_or_next_photo": "Prethodna ili sljedeća fotografija",
+  "primary": "Primarna (Primary)",
+  "privacy": "Privatnost",
+  "profile_image_of_user": "Profilna slika korisnika {user}",
+  "profile_picture_set": "Profilna slika postavljena.",
+  "public_album": "Javni album",
+  "public_share": "Javno dijeljenje",
+  "purchase_account_info": "Podržava softver",
+  "purchase_activated_subtitle": "Hvala što podržavate Immich i softver otvorenog koda",
+  "purchase_activated_time": "Aktivirano {date, date}",
+  "purchase_activated_title": "Vaš ključ je uspješno aktiviran",
+  "purchase_button_activate": "Aktiviraj",
+  "purchase_button_buy": "Kupi",
+  "purchase_button_buy_immich": "Kupi Immich",
+  "purchase_button_never_show_again": "Nikad više ne prikazuj",
+  "purchase_button_reminder": "Podsjeti me za 30 dana",
+  "purchase_button_remove_key": "Ukloni ključ",
+  "purchase_button_select": "Odaberite",
+  "purchase_failed_activation": "Aktivacija nije uspjela! Provjerite svoju e-poštu za točan ključ proizvoda!",
+  "purchase_individual_description_1": "Za pojedinca",
+  "purchase_individual_description_2": "Status podržavanja",
+  "purchase_individual_title": "Pojedinačna licenca",
+  "purchase_input_suggestion": "Imate ključ proizvoda? Unesite ključ ispod",
+  "purchase_license_subtitle": "Kupite Immich kako biste podržali kontinuirani razvoj usluge",
+  "purchase_lifetime_description": "Doživotna kupnja",
+  "purchase_option_title": "MOGUĆNOSTI KUPNJE",
+  "purchase_panel_info_1": "Za izgradnju Immicha potrebno je puno vremena i truda, a mi imamo inženjere koji rade na tome s punim radnim vremenom kako bismo ga učinili što boljim. Naša je misija da softver otvorenog koda i etička poslovna praksa postanu održivi izvor prihoda za programere i da se stvori ekosustav koji poštuje privatnost sa stvarnim alternativama eksploatacijskim uslugama u oblaku.",
+  "purchase_panel_info_2": "Budući da se obvezujemo da nećemo dodavati dodatne pretplate, ova vam kupnja neće dodijeliti nikakve dodatne značajke u Immichu. Oslanjamo se na korisnike poput vas da podržimo stalni razvoj Immicha.",
+  "purchase_panel_title": "Podrži projekt",
+  "purchase_per_server": "Po serveru",
+  "purchase_per_user": "Po korisniku",
+  "purchase_remove_product_key": "Ukloni ključ proizvoda",
+  "purchase_remove_product_key_prompt": "Jeste li sigurni da želite ukloniti ključ proizvoda?",
+  "purchase_remove_server_product_key": "Uklonite ključ proizvoda poslužitelja (Server)",
+  "purchase_remove_server_product_key_prompt": "Jeste li sigurni da želite ukloniti ključ proizvoda poslužitelja (Server)?",
+  "purchase_server_description_1": "Za cijeli server",
+  "purchase_server_description_2": "Status podupiratelja",
+  "purchase_server_title": "Poslužitelj (Server)",
+  "purchase_settings_server_activated": "Ključem proizvoda poslužitelja upravlja administrator",
+  "rating": "Broj zvjezdica",
+  "rating_clear": "Obriši ocjenu",
+  "rating_count": "{count, plural, one {# zvijezda} other {# zvijezde}}",
+  "rating_description": "Prikaži EXIF ocjenu na info ploči",
+  "reaction_options": "Mogućnosti reakcije",
+  "read_changelog": "Pročitajte Dnevnik promjena",
+  "reassign": "Ponovno dodijeli",
+  "reassigned_assets_to_existing_person": "Ponovo dodijeljeno{count, plural, one {# datoteka} other {# datoteke}} postojećoj {name, select, null {osobi} other {{name}}}",
+  "reassigned_assets_to_new_person": "Ponovo dodijeljeno {count, plural, one {# datoteka} other {# datoteke}} novoj osobi",
+  "reassing_hint": "Dodijelite odabrane datoteke postojećoj osobi",
+  "recent": "Nedavno",
+  "recent_searches": "Nedavne pretrage",
+  "refresh": "Osvježi",
+  "refresh_encoded_videos": "Osvježite kodirane videozapise",
+  "refresh_faces": "Osvježite lica",
+  "refresh_metadata": "Osvježi metapodatke",
+  "refresh_thumbnails": "Osvježi sličice",
+  "refreshed": "Osvježeno",
+  "refreshes_every_file": "Osvježava svaku datoteku",
+  "refreshing_encoded_video": "Osvježavanje kodiranog videa",
+  "refreshing_faces": "Osvježavanje lica",
+  "refreshing_metadata": "Osvježavanje metapodataka",
+  "regenerating_thumbnails": "Obnavljanje sličica",
+  "remove": "Ukloni",
+  "remove_assets_album_confirmation": "Jeste li sigurni da želite ukloniti {count, plural, one {# datoteku} other {# datoteke}} iz albuma?",
+  "remove_assets_shared_link_confirmation": "Jeste li sigurni da želite ukloniti {count, plural, one {# datoteku} other {# datoteke}} iz ove dijeljene veze?",
+  "remove_assets_title": "Ukloniti datoteke?",
+  "remove_custom_date_range": "Ukloni prilagođeni datumski raspon",
+  "remove_deleted_assets": "Ukloni izbrisana sredstva",
+  "remove_from_album": "Ukloni iz albuma",
+  "remove_from_favorites": "Ukloni iz favorita",
+  "remove_from_shared_link": "Ukloni iz dijeljene poveznice",
+  "remove_user": "Ukloni korisnika",
+  "removed_api_key": "Uklonjen API ključ: {name}",
+  "removed_from_archive": "Uklonjeno iz arhive",
+  "removed_from_favorites": "Uklonjeno iz favorita",
+  "removed_from_favorites_count": "{count, plural, other {Uklonjeno #}} iz omiljenih",
+  "removed_tagged_assets": "Uklonjena oznaka iz {count, plural, one {# datoteke} other {# datoteka}}",
+  "rename": "Preimenuj",
+  "repair": "Popravi",
+  "repair_no_results_message": "Nepraćene datoteke i datoteke koje nedostaju pojavit će se ovdje",
+  "replace_with_upload": "Zamijeni s prijenosom",
+  "repository": "Spremište (Repository)",
+  "require_password": "Zahtijevaj lozinku",
+  "require_user_to_change_password_on_first_login": "Zahtijevajte od korisnika promjenu lozinke pri prvoj prijavi",
+  "reset": "Reset",
+  "reset_password": "Resetiraj lozinku",
+  "reset_people_visibility": "Poništi vidljivost ljudi",
+  "reset_to_default": "Vrati na zadano",
+  "resolve_duplicates": "Riješite duplikate",
+  "resolved_all_duplicates": "Razriješi sve duplikate",
+  "restore": "Oporavi",
+  "restore_all": "Oporavi sve",
+  "restore_user": "Vrati korisnika",
+  "restored_asset": "Obnovljena datoteka",
+  "resume": "Nastavi",
+  "retry_upload": "Ponovi prijenos",
+  "review_duplicates": "Pregledajte duplikate",
+  "role": "Uloga",
+  "role_editor": "Urednik",
+  "role_viewer": "Gledatelj",
+  "save": "Spremi",
+  "saved_api_key": "Spremljen API ključ",
+  "saved_profile": "Spremljen profil",
+  "saved_settings": "Spremljene postavke",
+  "say_something": "Reci nešto",
+  "scan_all_libraries": "Skeniraj sve Knjižnice",
+  "scan_library": "Skeniraj",
+  "scan_settings": "Postavke skeniranja",
+  "scanning_for_album": "Skeniranje albuma...",
+  "search": "Pretraživanje",
+  "search_albums": "Traži albume",
+  "search_by_context": "Pretraživanje po kontekstu",
+  "search_by_filename": "Pretražujte prema nazivu datoteke ili ekstenziji",
+  "search_by_filename_example": "npr. IMG_1234.JPG ili PNG",
+  "search_camera_make": "Pretražite marku kamere...",
+  "search_camera_model": "Pretražite model kamere...",
+  "search_city": "Pretražite grad...",
+  "search_country": "Pretražite državu...",
+  "search_for_existing_person": "Potražite postojeću osobu",
+  "search_no_people": "Nema ljudi",
+  "search_no_people_named": "Nema osoba s imenom \"{name}\"",
+  "search_options": "Opcije pretraživanja",
+  "search_people": "Traži ljude",
+  "search_places": "Traži mjesta",
+  "search_settings": "Postavke pretraživanja",
+  "search_state": "Država pretraživanja...",
+  "search_tags": "Traži oznake...",
+  "search_timezone": "Pretraži vremenske zone",
+  "search_type": "Vrsta pretraživanja",
+  "search_your_photos": "Pretražite svoje fotografije",
+  "searching_locales": "Traženje lokaliteta...",
+  "second": "Drugi",
+  "see_all_people": "Vidi sve ljude",
+  "select_album_cover": "Odaberite omot albuma",
+  "select_all": "Odaberi sve",
+  "select_all_duplicates": "Odaberi sve duplikate",
+  "select_avatar_color": "",
+  "select_face": "Odaberi lice",
+  "select_featured_photo": "",
+  "select_keep_all": "",
+  "select_library_owner": "",
+  "select_new_face": "",
+  "select_photos": "",
+  "select_trash_all": "",
+  "selected": "Odabrano",
+  "send_message": "",
+  "send_welcome_email": "Pošalji email dobrodošlice",
+  "server_offline": "Server izvan mreže",
+  "server_online": "Server na mreži",
+  "server_stats": "Statistike servera",
+  "server_version": "Verzija servera",
+  "set": "Postavi",
+  "set_as_album_cover": "",
+  "set_as_profile_picture": "Postavi kao profilnu sliku",
+  "set_date_of_birth": "Postavi datum rođenja",
+  "set_profile_picture": "Postavi profilnu sliku",
+  "set_slideshow_to_fullscreen": "",
+  "settings": "Postavke",
+  "settings_saved": "Postavke su spremljene",
+  "share": "Podijeli",
+  "shared": "Podijeljeno",
+  "shared_by": "Podijelio",
+  "shared_by_user": "Podijelio {user}",
+  "shared_by_you": "Podijelili vi",
+  "shared_from_partner": "Fotografije od {partner}",
+  "shared_links": "",
+  "shared_with_partner": "",
+  "sharing": "",
+  "sharing_sidebar_description": "",
+  "show_album_options": "",
+  "show_albums": "Prikaži albume",
+  "show_all_people": "Prikaži sve osobe",
+  "show_and_hide_people": "Prikaži i sakrij osobe",
+  "show_file_location": "Pokaži mjesto datoteke",
+  "show_gallery": "Prikaži galeriju",
+  "show_hidden_people": "Prikaži skrivene osobe",
+  "show_in_timeline": "Prikaži na vremenskoj crti",
+  "show_in_timeline_setting_description": "",
+  "show_keyboard_shortcuts": "",
+  "show_metadata": "",
+  "show_or_hide_info": "",
+  "show_password": "",
+  "show_person_options": "",
+  "show_progress_bar": "",
+  "show_search_options": "",
+  "shuffle": "",
+  "sign_out": "",
+  "sign_up": "",
+  "size": "",
+  "skip_to_content": "",
+  "slideshow": "",
+  "slideshow_settings": "",
+  "sort_albums_by": "",
+  "stack": "",
+  "stack_selected_photos": "",
+  "stacktrace": "",
+  "start": "",
+  "start_date": "",
+  "state": "",
+  "status": "",
+  "stop_motion_photo": "",
+  "stop_photo_sharing": "",
+  "stop_photo_sharing_description": "",
+  "stop_sharing_photos_with_user": "",
+  "storage": "",
+  "storage_label": "",
+  "storage_usage": "",
+  "submit": "",
+  "suggestions": "Prijedlozi",
+  "sunrise_on_the_beach": "Sunrise on the beach",
+  "swap_merge_direction": "",
+  "sync": "Sink.",
+  "template": "",
+  "theme": "Tema",
+  "theme_selection": "Izbor teme",
+  "theme_selection_description": "Automatski postavite temu na svijetlu ili tamnu ovisno o postavkama sustava vašeg preglednika",
+  "they_will_be_merged_together": "Oni ću biti spojeni zajedno",
+  "time_based_memories": "Uspomene temeljene na vremenu",
+  "timezone": "Vremenska zona",
+  "to_archive": "Arhivaj",
+  "to_change_password": "Promjeni lozinku",
+  "to_favorite": "Omiljeni",
+  "to_login": "Prijava",
+  "to_trash": "Smeće",
+  "toggle_settings": "Uključi/isključi postavke",
+  "toggle_theme": "Promjeni temu",
+  "total_usage": "Ukupna upotreba",
+  "trash": "Smeće",
+  "trash_all": "Stavi sve u smeće",
+  "trash_no_results_message": "Ovdje će se prikazati bačene fotografije i videozapisi.",
+  "trashed_items_will_be_permanently_deleted_after": "Stavke bačene u smeće trajno će se izbrisati nakon {days, plural, one {# day} other {# days}}.",
+  "type": "Vrsta",
+  "unarchive": "",
+  "unfavorite": "",
+  "unhide_person": "",
+  "unknown": "",
+  "unknown_year": "",
+  "unlimited": "",
+  "unlink_oauth": "",
+  "unlinked_oauth_account": "",
+  "unselect_all": "",
+  "unstack": "",
+  "untracked_files": "",
+  "untracked_files_decription": "",
+  "up_next": "",
+  "updated_password": "",
+  "upload": "",
+  "upload_concurrency": "",
+  "url": "",
+  "usage": "",
+  "user": "",
+  "user_id": "",
+  "user_usage_detail": "",
+  "user_usage_stats": "Statistika korištenja računa",
+  "user_usage_stats_description": "Pregledajte statistiku korištenja računa",
+  "username": "",
+  "users": "",
+  "utilities": "",
+  "validate": "",
+  "variables": "",
+  "version": "",
+  "video": "",
+  "video_hover_setting": "",
+  "video_hover_setting_description": "",
+  "videos": "",
+  "videos_count": "",
+  "view_all": "",
+  "view_all_users": "",
+  "view_links": "",
+  "view_next_asset": "",
+  "view_previous_asset": "",
+  "waiting": "",
+  "week": "",
+  "welcome_to_immich": "",
+  "year": "",
+  "yes": "",
+  "you_dont_have_any_shared_links": "",
+  "zoom_image": ""
+}
diff --git a/i18n/hu.json b/i18n/hu.json
new file mode 100644
index 0000000000..b6e9617993
--- /dev/null
+++ b/i18n/hu.json
@@ -0,0 +1,1340 @@
+{
+  "about": "Frissítés",
+  "account": "Fiók",
+  "account_settings": "Fiók Beállítások",
+  "acknowledge": "Megértettem",
+  "action": "Művelet",
+  "actions": "Műveletek",
+  "active": "Feldolgozás alatt",
+  "activity": "Tevékenység",
+  "activity_changed": "A tevékenység {enabled, select, true {bekapcsolva} other {kikapcsolva}}",
+  "add": "Hozzáadás",
+  "add_a_description": "Leírás hozzáadása",
+  "add_a_location": "Helyszín hozzáadása",
+  "add_a_name": "Név megadása",
+  "add_a_title": "Címadás",
+  "add_exclusion_pattern": "Kihagyási minta (pattern) hozzáadása",
+  "add_import_path": "Importálási útvonal hozzáadása",
+  "add_location": "Helyszín megadása",
+  "add_more_users": "További felhasználók hozzáadása",
+  "add_partner": "Partner hozzáadása",
+  "add_path": "Elérési útvonal megadása",
+  "add_photos": "Fotók hozzáadása",
+  "add_to": "Hozzáadás ide...",
+  "add_to_album": "Felvétel albumba",
+  "add_to_shared_album": "Felvétel megosztott albumba",
+  "add_url": "URL hozzáadása",
+  "added_to_archive": "Hozzáadva az archívumhoz",
+  "added_to_favorites": "Hozzáadva a kedvencekhez",
+  "added_to_favorites_count": "{count, number} hozzáadva a kedvencekhez",
+  "admin": {
+    "add_exclusion_pattern_description": "Kihagyási minták (pattern) megadása. A *, ** és ? helyettesítő karakterek engedélyezettek. Pl. a \"Raw\" könyvtárban tárolt összes fájl kihagyásához használható a \"**/Raw/**\". Minden \".tif\" fájl kihagyása az összes mappában: \"**/*.tif\". Abszolút elérési útvonal kihagyása: \"/kihagyni/kivant/mappa/**\".",
+    "asset_offline_description": "Ez a külső képtárban lévő elem már nem található, ezért a lomtárba került. Ha a fájl a képtáron belül lett áthelyezve, akkor ellenőrizd, hogy továbbra is látható az idővonaladon. Az elem visszaállításához győződj meg róla, hogy az alábbi mappa az Immich számára elérhető, majd újra átfésültesd át a képtárat.",
+    "authentication_settings": "Hitelesítési beállítások",
+    "authentication_settings_description": "Jelszó, OAuth és egyéb hitelesítési beállítások kezelése",
+    "authentication_settings_disable_all": "Biztosan letiltod az összes bejelentkezési módot? A bejelentkezés teljesen le lesz tiltva.",
+    "authentication_settings_reenable": "Az újbóli engedélyezéshez használj egy<link>Szerver Parancsot</link>.",
+    "background_task_job": "Háttérfeladatok",
+    "backup_database": "Tartalék Adatbázis",
+    "backup_database_enable_description": "Adatbázis biztonsági mentések engedélyezése",
+    "backup_keep_last_amount": "Megőrizendő korábbi biztonsági mentések száma",
+    "backup_settings": "Biztonsági mentés beállításai",
+    "backup_settings_description": "Adatbázis mentési beállításainak kezelése",
+    "check_all": "Összes Kipiálása",
+    "cleared_jobs": "{job}: feladatai törölve",
+    "config_set_by_file": "A konfigurációt jelenleg egy konfigurációs fájl állítja be",
+    "confirm_delete_library": "Biztosan ki szeretnéd törölni a {library} képtárat?",
+    "confirm_delete_library_assets": "Biztosan kitörlöd ezt a képtárat? Ez kitörli az Immich-ből a benne lévő {count, plural, one {#} other {#}} elemet is, és ez nem visszavonható. A fájlok fizikailag a lemezen maradnak.",
+    "confirm_email_below": "A megerősítéshez írd be, hogy \"{email}\"",
+    "confirm_reprocess_all_faces": "Biztos vagy benne, hogy újra fel szeretnéd dolgozni az összes arcot? Ez a már elnevezett személyeket is törli.",
+    "confirm_user_password_reset": "Biztosan vissza szeretnéd állítani {user} jelszavát?",
+    "create_job": "Feladat létrehozása",
+    "cron_expression": "Cron kifejezés",
+    "cron_expression_description": "A beolvasási időköz beállítása a cron formátummal. További információért lásd pl. <link>Crontab Guru</link>",
+    "cron_expression_presets": "Cron kifejezés előbeállítások",
+    "disable_login": "Belépés letiltása",
+    "duplicate_detection_job_description": "Gépi tanulás futtatása a hasonló elemek megtalálása céljából. Ez az Okos Keresés funkciót használja",
+    "exclusion_pattern_description": "A kihagyási minták (pattern) használatakor a mintának megfelelő fájlok vagy mappák át lesznek ugorva a képtár átfésülésekor. Akkor hasznos, ha a mappákban vannak olyan fájlok is, amelyeket nem szeretnél importálni, pl. nyers (RAW) fájlok.",
+    "external_library_created_at": "Külső képtár (létrehozva: {date})",
+    "external_library_management": "Külső Képtárak Kezelése",
+    "face_detection": "Arckeresés",
+    "face_detection_description": "Gépi tanulás segítségével megkeresi, hogy hol találhatóak arcok az elemeken. Videók esetében csak a bélyegképeken keres. \"Frissítés\" (újra) feldolgozza az összes elemet. \"Visszaállítás\" ezen felül törli az összes aktuális arcadatot. \"Hiányzók\" sorba állítja azokat az elemeket, amelyek eddig még nem lettek feldolgozva. A megtalált arcok ezután sorba lesznek állítva az Arcfelismeréshez, ami ezután az arcokat csoportosítja és meglevő vagy új személyekhez rendeli.",
+    "facial_recognition_job_description": "A megtalált arcokat személyekhez csoportosítja. Ez a lépés azután következik, amikor az Arckeresés lefutott. \"Visszaállítás\" (újra)csoportosítja az összes arcot. \"Hiányzók\" csak azokkal az arcokkal foglalkozik, amelyekhez még nincsen ember rendelve.",
+    "failed_job_command": "A(z) {command} parancs nem sikerült a következő feladathoz: {job}",
+    "force_delete_user_warning": "FIGYELEM: Ez azonnal eltávolítja a felhasználót és az összes hozzá tartozó elemet. A művelet nem visszavonható, és a fájlokat sem lehet később visszanyerni.",
+    "forcing_refresh_library_files": "A képtár összes fájljának frissítése",
+    "image_format": "Formátum",
+    "image_format_description": "WebP a JPEG-nél kisebb fájlokat készít, de lassabban.",
+    "image_prefer_embedded_preview": "Beágyazott előnézeti kép előnyben részesítése",
+    "image_prefer_embedded_preview_setting_description": "Nyers (RAW) fotók esetén használja a beépített előnézeti képet (ha van) a képek feldogozásához. Ez néhány kép esetében pontosabb színeket eredményezhet, de az előnézeti kép minősége erősen fényképezőgép függő, és a képen előfordulhatnak tömörítési hibák.",
+    "image_prefer_wide_gamut": "Széles színtér preferálása",
+    "image_prefer_wide_gamut_setting_description": "A bélyegképekhez DCI-P3 színtér használata. Ez a széles színteret használó képek esetén (pl: Adobe RGB, P3) jobban megőrzi az élénkebb színeket, de régebbi eszközökön vagy böngészőkben a kép színei másképpen jelenhetnek meg. Az sRGB képek a színeltolódások megelőzése érdekében nem változnak.",
+    "image_preview_description": "Közepes méretű kép eltávolított metaadatokkal, egy képes nézethez és a gépi tanuláshoz",
+    "image_preview_quality_description": "Előnézet minősége 1-100 között. A magasabb szám jobb minőséget, de nagyobb fájlokat eredményez és belassíthatja az alkalmazást. Túl alacsony érték befolyásolhatja a gépi tanulás pontosságát.",
+    "image_preview_title": "Előnézet Beállításai",
+    "image_quality": "Minőség",
+    "image_resolution": "Felbontás",
+    "image_resolution_description": "A nagyobb felbontás több részletet őriz meg, de lassabb létrehozni, nagyobb fájlt eredményez és belassíthatja az alkalmazást.",
+    "image_settings": "Képbeállítások",
+    "image_settings_description": "A létrehozott képek minőségi és felbontási beállításainak kezelése",
+    "image_thumbnail_description": "Kicsi bélyegkép eltávolított metaadatokkal, sok kis kép (pl idővonal) megjelenítéséhez",
+    "image_thumbnail_quality_description": "Bélyegkép minősége 1-100 között. A magasabb szám jobb minőséget, de nagyobb fájlméretet eredményez és belassíthatja az alkalmazást.",
+    "image_thumbnail_title": "Bélyegkép Beállítások",
+    "job_concurrency": "{job} párhuzamosság",
+    "job_created": "Feladat létrehozva",
+    "job_not_concurrency_safe": "Ez a feladat nem párhuzamosság-biztos.",
+    "job_settings": "Feladat Beállítások",
+    "job_settings_description": "Feladatok párhuzamosságának kezelése",
+    "job_status": "Feladat Állapota",
+    "jobs_delayed": "{jobCount, plural, other {# késik}}",
+    "jobs_failed": "{jobCount, plural, other {# sikertelen}}",
+    "library_created": "Képtár létrehozva: {library}",
+    "library_deleted": "Képtár törölve",
+    "library_import_path_description": "Add meg az importálandó mappát. A rendszer ebben a mappában és összes almappájában fog képeket és videókat keresni.",
+    "library_scanning": "Időszakos Átfésülés",
+    "library_scanning_description": "A képtár időszakos átfésülésének beállítása",
+    "library_scanning_enable_description": "Képtár időszakos átfésülésének engedélyezése",
+    "library_settings": "Külső Képtár",
+    "library_settings_description": "Külső képtár beállításainak kezelése",
+    "library_tasks_description": "Képtár feladatok elvégzése",
+    "library_watching_enable_description": "Külső képtár változásainak figyelése",
+    "library_watching_settings": "Képtár figyelése (KÍSÉRLETI)",
+    "library_watching_settings_description": "Megváltozott fájlok automatikus észlelése",
+    "logging_enable_description": "Naplózás engedélyezése",
+    "logging_level_description": "Ha be van kapcsolva, milyen részletességű legyen a naplózás.",
+    "logging_settings": "Naplózás",
+    "machine_learning_clip_model": "CLIP modell",
+    "machine_learning_clip_model_description": "Egy CLIP modell neve az <link>itt</link> felsoroltak közül. A modell megváltoztatása után újra kell futtatni az 'Okos Keresés' feladatot minden képre.",
+    "machine_learning_duplicate_detection": "Duplikációk Keresése",
+    "machine_learning_duplicate_detection_enabled": "Duplikációk keresésének engedélyezése",
+    "machine_learning_duplicate_detection_enabled_description": "Ha ki van kapcsolva, a pontosan azonos elemek akkor sem lesznek duplikálva.",
+    "machine_learning_duplicate_detection_setting_description": "CLIP beágyazások használata a valószínű másolatok kereséséhez",
+    "machine_learning_enabled": "Gépi tanulás engedélyezése",
+    "machine_learning_enabled_description": "Ha ki van kapcsolva, a gépi tanulási képességek az alábbi beállításoktól függetlenül ki lesznek kapcsolva.",
+    "machine_learning_facial_recognition": "Arcfelismerés",
+    "machine_learning_facial_recognition_description": "A képekben szereplő arcok megkeresése, felismerése és csoportosítása",
+    "machine_learning_facial_recognition_model": "Arcfelismerési modell",
+    "machine_learning_facial_recognition_model_description": "A modellek méret szerint csökkenő sorrendben vannak felsorolva. A nagyobb modellek lassabbak és több memóriát használnak, de jobb eredményt produkálnak. Modellváltás után az összes képen futtasd újra az Arckeresés feladatot.",
+    "machine_learning_facial_recognition_setting": "Arckeresés engedélyezése",
+    "machine_learning_facial_recognition_setting_description": "Ha ki van kapcsolva, a képek nem lesznek az arcfelismerésen lefuttatva és a Böngészés oldalon az Személyek szekcióban nem fog szerepelni senki.",
+    "machine_learning_max_detection_distance": "Maximum keresési távolság",
+    "machine_learning_max_detection_distance_description": "Két kép közötti maximális távolság, amely esetében még duplikációnak tekintendők (0.001 és 0.1 közötti érték). Minél magasabb az érték, annál több lesz a megtalált duplikáció, de a hamis találatok esélye is egyre nagyobb.",
+    "machine_learning_max_recognition_distance": "Maximum felismerési távolság",
+    "machine_learning_max_recognition_distance_description": "Két arc közötti maximális távolság, amely alapján ugyanazon személynek tekinthetők, 0 és 2 között. Ennek csökkentése megakadályozhatja, hogy két különböző személyt ugyanannak a személynek jelöljünk, míg a növelése megakadályozhatja, hogy ugyanazt a személyt két különböző személyként jelöljük. Vedd figyelembe, hogy könnyebb két személyt összevonni, mint egy személyt kettéválasztani, ezért lehetőség szerint inkább alacsonyabb küszöbértéket válassz.",
+    "machine_learning_min_detection_score": "Minimum keresési pontszám",
+    "machine_learning_min_detection_score_description": "Az arcok észleléséhez szükséges minimális megbízhatósági pontszám 0 és 1 között. Minél alacsonyabb az érték, annál több lesz a megtalált arc, de a hamis találatok esélye is egyre nagyobb.",
+    "machine_learning_min_recognized_faces": "Minimum felismert arc",
+    "machine_learning_min_recognized_faces_description": "Egy személy létrehozásához szükséges minimálisan felismert arcok száma. Ennek növelésével a arcfelismerés pontosabbá válik, azonban növeli annak az esélyét, hogy egy arc nem rendelődik hozzá egy személyhez.",
+    "machine_learning_settings": "Gépi Tanulási Beállítások",
+    "machine_learning_settings_description": "Gépi tanulási funkciók és beállítások kezelése",
+    "machine_learning_smart_search": "Okos Keresés",
+    "machine_learning_smart_search_description": "Képek szemantikai keresése CLIP beágyazások segítségével",
+    "machine_learning_smart_search_enabled": "Okos keresés engedélyezése",
+    "machine_learning_smart_search_enabled_description": "Ha ki van kapcsolva, a képek nem lesznek átalakítva okos kereséshez.",
+    "machine_learning_url_description": "Gépi tanulás szerver URL címe. Ha többi, mint egy URL van megadva, mindegyik szervert egyenként próbálja meg, amíg az egyik sikeresen nem válaszol, sorrendben az elsőtől az utólsóig.",
+    "manage_concurrency": "Párhuzamos Feladatok Kezelése",
+    "manage_log_settings": "Naplózási beállítások kezelése",
+    "map_dark_style": "Sötét stílus",
+    "map_enable_description": "Térkép funkciók engedélyezése",
+    "map_gps_settings": "Térkép és GPS Beállítások",
+    "map_gps_settings_description": "A Térkép és GPS (Fordított Geokódolás) Beállításainak Kezelése",
+    "map_implications": "A térkép szolgáltatás egy külső csempeszolgáltatót használ (tiles.immich.cloud)",
+    "map_light_style": "Világos stílus",
+    "map_manage_reverse_geocoding_settings": "A <link>Fordított Geokódolás</link> beállításainak kezelése",
+    "map_reverse_geocoding": "Fordított Geokódolás",
+    "map_reverse_geocoding_enable_description": "Fordított geokódolás engedélyezése",
+    "map_reverse_geocoding_settings": "Fordított Geokódolási Beállítások",
+    "map_settings": "Térkép",
+    "map_settings_description": "Térkép beállítások kezelése",
+    "map_style_description": "Egy style.json térképtémára mutató URL cím",
+    "metadata_extraction_job": "Metaadatok kinyerése",
+    "metadata_extraction_job_description": "Metaadat információk (pl. GPS, arcok és felbontás) kinyerése minden elemből",
+    "metadata_faces_import_setting": "Arc importálás engedélyezése",
+    "metadata_faces_import_setting_description": "Arcok importálása a kép EXIF adataiból és segédfájlokból",
+    "metadata_settings": "Metaadat Beállítások",
+    "metadata_settings_description": "Metaadat beállítások kezelése",
+    "migration_job": "Migrálás",
+    "migration_job_description": "Az elemek és arcok bélyegképeinek migrálása a legújabb mappastruktúrába",
+    "no_paths_added": "Nincs megadva elérési útvonal",
+    "no_pattern_added": "Nincs megadva minta (pattern)",
+    "note_apply_storage_label_previous_assets": "Megjegyzés: Ha a korábban feltöltött elemekhez is szeretne Tárhely Címkéket társítani, akkor futtassa ezt",
+    "note_cannot_be_changed_later": "FIGYELEM: ezt később nem lehet megváltoztatni!",
+    "note_unlimited_quota": "Megjegyzés: 0 = korlátlan kvóta",
+    "notification_email_from_address": "Feladó cím",
+    "notification_email_from_address_description": "Küldő email címe, például: \"Immich Fotószerver <noreply@example.com>\"",
+    "notification_email_host_description": "Email szerver kiszolgálója (pl. smtp.immich.app)",
+    "notification_email_ignore_certificate_errors": "Tanúsítvány hibák figyelmen kívül hagyása",
+    "notification_email_ignore_certificate_errors_description": "TLS tanúsítvány érvényességi hibák figyelmen kívül hagyása (nem ajánlott)",
+    "notification_email_password_description": "Az email szerverrel való hitelesítéshez használt jelszó",
+    "notification_email_port_description": "Email szerver portja (pl. 25, 465 vagy 587)",
+    "notification_email_sent_test_email_button": "Teszt email küldése és mentés",
+    "notification_email_setting_description": "Email értesítés küldés beállításai",
+    "notification_email_test_email": "Teszt email küldése",
+    "notification_email_test_email_failed": "Nem sikerült elküldeni a teszt emailt, ellenőrizd a beállításokat",
+    "notification_email_test_email_sent": "Egy teszt emailt küldtünk a(z) {email} címre. Figyeld a beérkező üzeneteidet.",
+    "notification_email_username_description": "Az email szerverrel való hitelesítéshez használt felhasználónév",
+    "notification_enable_email_notifications": "Email értesítések engedélyezése",
+    "notification_settings": "Értesítés Beállítások",
+    "notification_settings_description": "Értesítési és email beállítások kezelése",
+    "oauth_auto_launch": "Automatikus indítás",
+    "oauth_auto_launch_description": "Az OAuth bejelentkezési folyamat automatikus indítása a bejelentkezési oldal megnyitásakor",
+    "oauth_auto_register": "Automatikus regisztráció",
+    "oauth_auto_register_description": "Új felhasználók automatikus regisztrálása az OAuth használatával történő bejelentkezés után",
+    "oauth_button_text": "Gomb szövege",
+    "oauth_client_id": "Kliens ID",
+    "oauth_client_secret": "Kliens Titok",
+    "oauth_enable_description": "Bejelentkezés OAuth használatával",
+    "oauth_issuer_url": "Kibocsátó URL",
+    "oauth_mobile_redirect_uri": "Mobil átirányítási URI",
+    "oauth_mobile_redirect_uri_override": "Mobil átirányítási URI felülírás",
+    "oauth_mobile_redirect_uri_override_description": "Engedélyezd, ha az OAuth szolgáltató tiltja a mobil URI-t, mint például '{callback}'",
+    "oauth_profile_signing_algorithm": "Profil aláíró algoritmus",
+    "oauth_profile_signing_algorithm_description": "A felhasználói profil aláírásához használt algoritmus.",
+    "oauth_scope": "Hatókör",
+    "oauth_settings": "OAuth",
+    "oauth_settings_description": "OAuth bejelentkezési beállítások kezelése",
+    "oauth_settings_more_details": "Erről a funkcióról további információt a <link>dokumentációban</link> találsz.",
+    "oauth_signing_algorithm": "Aláírás algoritmusa",
+    "oauth_storage_label_claim": "Tárhely címke igénylés",
+    "oauth_storage_label_claim_description": "A felhasználó tárhely címkéjének automatikus beállítása az igényeltre.",
+    "oauth_storage_quota_claim": "Tárhelykvóta igénylése",
+    "oauth_storage_quota_claim_description": "A felhasználó tárhelykvótájának automatikus beállítása ennek az igényeltre.",
+    "oauth_storage_quota_default": "Alapértelmezett tárhelykvóta (GiB)",
+    "oauth_storage_quota_default_description": "Alapértelmezett tárhely kvóta GiB-ban, amennyiben a felhasználó nem jelezte az igényét (A korlátlan tárhelyhez 0-t adj meg).",
+    "offline_paths": "Offline Útvonalak",
+    "offline_paths_description": "Ezek az eredmények olyan fájlok kézi törlésének tudhatók be, amelyek nem részei külső képtárnak.",
+    "password_enable_description": "Bejelentkezés emaillel és jelszóval",
+    "password_settings": "Jelszavas Bejelentkezés",
+    "password_settings_description": "Jelszavas bejelentkezés beállítások kezelése",
+    "paths_validated_successfully": "Összes útvonal sikeresen érvényesítve",
+    "person_cleanup_job": "Személyek kipucolása",
+    "quota_size_gib": "Kvóta Mérete (GiB)",
+    "refreshing_all_libraries": "Összes képtár frissítése",
+    "registration": "Admin Regisztráció",
+    "registration_description": "Mivel ez az első felhasználó a rendszerben, ezért te leszel az Admin, aki az adminisztratív teendőkért felelős és további felhasználókat tud létrehozni.",
+    "repair_all": "Összes Javítása",
+    "repair_matched_items": "{count, plural, one {# egyezés} other {# egyezés}}",
+    "repaired_items": "Javítva {count, plural, one {# fájl} other {# fájl}}",
+    "require_password_change_on_login": "Kötelező jelszómódosítás az első bejelentkezéskor",
+    "reset_settings_to_default": "Beállítások visszaállítása az alapértelmezettre",
+    "reset_settings_to_recent_saved": "Beállítások visszaállítása a legutóbb mentettre",
+    "scanning_library": "Képtár átfésülése",
+    "search_jobs": "Feladatok keresése...",
+    "send_welcome_email": "Üdvözlő email küldése",
+    "server_external_domain_settings": "Külső domain",
+    "server_external_domain_settings_description": "Nyilvánosan megosztott linkek domainje (http(s)://-sel)",
+    "server_public_users": "Nyilvános felhasználók",
+    "server_public_users_description": "Az összes felhasználó (név és email) ki van írva, amikor egy felhasználót adsz hozzá egy megosztott albumhoz. Amikor le van tiltva, a felhasználólista csak adminok számára lesz elérhető.",
+    "server_settings": "Szerver Beállítások",
+    "server_settings_description": "Szerver beállítások kezelése",
+    "server_welcome_message": "Üdvözlő üzenet",
+    "server_welcome_message_description": "A bejelentkezőoldalon megjelenő üzenet.",
+    "sidecar_job": "Segédfájl metaadatok",
+    "sidecar_job_description": "Metaadatok keresése vagy szinkronizálása a fájlrendszeren lévő segédfájlokból",
+    "slideshow_duration_description": "Az egyes képek megjelenítésének időtartama másodpercben",
+    "smart_search_job_description": "Gépi tanulás futtatása az elemeken, ami az Okos Kereséshez szükséges",
+    "storage_template_date_time_description": "Az elem készítési időpontja lesz felhasználva az időpont információhoz",
+    "storage_template_date_time_sample": "Példa időpont {date}",
+    "storage_template_enable_description": "Tárhely sablon motor engedélyezése",
+    "storage_template_hash_verification_enabled": "Hash ellenőrzés engedélyezve",
+    "storage_template_hash_verification_enabled_description": "Engedélyezi a hash-érték ellenőrzést - csak akkor kapcsold ki, ha tisztában vagy a következményekkel",
+    "storage_template_migration": "Tárhely sablon migrálása",
+    "storage_template_migration_description": "A jelenlegi <link>{template}</link> alkalmazása a már feltöltött elemekre",
+    "storage_template_migration_info": "A megváltozott sablon csak az újonnan feltöltött elemekre vonatkozik. A korábbi elemek visszamenőleges áthelyezéséhez ezt futtasd: <link>{job}</link>.",
+    "storage_template_migration_job": "Tárhely Sablon Migrációja",
+    "storage_template_more_details": "További részletekért erről a funkcióról lásd a <template-link>Tárhely Sablon</template-link> és annak <implications-link>következményeit</implications-link> a dokumentációban",
+    "storage_template_onboarding_description": "Ha ez a funkció engedélyezve van, akkor a fájlokat automatikusan az egyéni sablon alapján rendszerezi el. Stabilitási problémák miatt a funkció alapértelmezés szerint ki van kapcsolva. További információkért lásd a <link>dokumentációt</link>.",
+    "storage_template_path_length": "Útvonal hozzávetőleges maximális hossza: <b>{length, number}</b>{limit, number}",
+    "storage_template_settings": "Tárhely Sablon",
+    "storage_template_settings_description": "A feltöltött elemek mappaszerkezetének és fájl elnevezésének kezelése",
+    "storage_template_user_label": "A felhasználó Tárhely Címkéje <code>{label}</code>",
+    "system_settings": "Rendszerbeállítások",
+    "tag_cleanup_job": "Címkék kipucolása",
+    "template_email_available_tags": "Használthatod a következő változókat a sablonodban: {tags}",
+    "template_email_if_empty": "Ha a sablon üres, akkor az alapértelmezett email lesz használva.",
+    "template_email_invite_album": "Album meghívás sablon",
+    "template_email_preview": "Előnézet",
+    "template_email_settings": "Email sablonok",
+    "template_email_settings_description": "Saját email értesítések kezelése",
+    "template_email_update_album": "Album frissítve sablon",
+    "template_email_welcome": "Üdvözlő email sablon",
+    "template_settings": "Értesítés sablon",
+    "template_settings_description": "Egyéni sablonok kezelése az értesítésekhez.",
+    "theme_custom_css_settings": "Egyedi CSS",
+    "theme_custom_css_settings_description": "CSS Stíluslapokkal az Immich stílusa megváltoztatható.",
+    "theme_settings": "Téma Beállítások",
+    "theme_settings_description": "Az Immich webes felület testreszabásának kezelése",
+    "these_files_matched_by_checksum": "Ezek a fájlok egyeznek az ellenőrző összegük alapján",
+    "thumbnail_generation_job": "Bélyegképek Generálása",
+    "thumbnail_generation_job_description": "Nagy, kicsi és elmosódott bélyegképek létrehozása minden elemhez, valamint bélyegképek generálása minden személyhez",
+    "transcoding_acceleration_api": "Gyorsító API",
+    "transcoding_acceleration_api_description": "Az átkódolás felgyorsításához használt eszközödhöz tartozó API. Ez a beállítás „legtöbb, amit megtehetünk” alapon működik: probléma esetén visszaáll szoftveres átkódolásra. A VP9 a hardvertől függően vagy működik, vagy nem.",
+    "transcoding_acceleration_nvenc": "NVENC (NVIDIA GPU-t igényel)",
+    "transcoding_acceleration_qsv": "Gyors Szinkronizálás (7. generációs vagy újabb Intel CPU-t igényel)",
+    "transcoding_acceleration_rkmpp": "RKMPP (csak Rockchip SOC-on)",
+    "transcoding_acceleration_vaapi": "VAAPI",
+    "transcoding_accepted_audio_codecs": "Elfogadott audio kodekek",
+    "transcoding_accepted_audio_codecs_description": "Válaszd ki, hogy melyik audio kodekeket nem kell átkódolni. Csak bizonyos átkódolási szabályzatokhoz használt.",
+    "transcoding_accepted_containers": "Elfogadott tárolók",
+    "transcoding_accepted_containers_description": "Válaszd ki, hogy melyik tároló formátumokat nem szükséges átkódolni MP4 formátumba. Csak bizonyos átkódolási szabályzatokhoz használt.",
+    "transcoding_accepted_video_codecs": "Elfogadott videó kodekek",
+    "transcoding_accepted_video_codecs_description": "Válaszd ki, hogy mely videó kodekeket nem kell átkódolni. Csak bizonyos átkódolási szabályzatokhoz használt.",
+    "transcoding_advanced_options_description": "Ezeket az opciókat a legtöbb felhasználónak nem kell módosítania",
+    "transcoding_audio_codec": "Audio kodek",
+    "transcoding_audio_codec_description": "Az Opus a legjobb minőségű opció (jobb hangminőség ugyanakkora tárhelyen), de kevésbé kompatibilis a régi eszközökkel vagy szoftverekkel.",
+    "transcoding_bitrate_description": "A maximum bitrátát meghaladó vagy nem megfelelő formátumú videókat",
+    "transcoding_codecs_learn_more": "Hogy többet tudj meg az itt felhasznált kifejezésekről, nézd meg az FFmpeg dokumentációt a <h264-link>H.264 kodekről</h264-link>, a <hevc-link>HEVC kodekről</hevc-link> és a <vp9-link>VP9 kodekről</vp9-link>.",
+    "transcoding_constant_quality_mode": "Állandó minőségű mód",
+    "transcoding_constant_quality_mode_description": "Az ICQ jobb, mint a CQP, viszont az előbbit nem minden hardver támogatja. A rendszer az itt beállított módot preferálja a minőség orientált kódoláshoz. Az NVENC nem használja ezt a beállítást, mivel nem támogatja az ICQ-t.",
+    "transcoding_constant_rate_factor": "Állandó ráta tényező (-crf)",
+    "transcoding_constant_rate_factor_description": "Videó minőségi szint. Tipikus értékek kodekenként: H.264: 23, HEVC: 28, VP9: 31, AV1: 35. Minél alacsonyabb, annál jobb minőséget eredményez, viszont nagyobb fájlmérettel is jár.",
+    "transcoding_disabled_description": "Ne kódolja át a videókat. Néhány kliensnél nem lejátszható videókhoz vezethet",
+    "transcoding_hardware_acceleration": "Hardveres Gyorsítás",
+    "transcoding_hardware_acceleration_description": "Kísérleti funkció. Sokkal gyorsabb, viszont azonos bitrátán is alacsonyabb minőséghez vezet",
+    "transcoding_hardware_decoding": "Hardveres dekódolás",
+    "transcoding_hardware_decoding_setting_description": "Lehetővé teszi az egész folyamat gyorsítását a pusztán kódolás gyorsítása helyett. Nem biztos, hogy minden videó esetén működik.",
+    "transcoding_hevc_codec": "HEVC kodek",
+    "transcoding_max_b_frames": "B-képkockák maximum száma",
+    "transcoding_max_b_frames_description": "Nagyobb értékek megnövelik a tömörítés hatékonyságát, de lelassítják a kódolást. Nem minden hardvereszköz támogatja. A 0 érték kikapcsolja a B-képkockákat, míg -1 esetén a szoftver magának választ értéket.",
+    "transcoding_max_bitrate": "Maximum bitráta",
+    "transcoding_max_bitrate_description": "Maximum bitráta beállítása konzisztensebb fájlméretet eredményez egy kevés minőségi romlás árán. 720p esetén jellemző érték lehet 2600k a VP9 vagy HEVC kódoláshoz, 4500k a H.264 kódoláshoz. A 0 érték esetén nincs maximum bitráta.",
+    "transcoding_max_keyframe_interval": "Maximum kulcskocka intervallum",
+    "transcoding_max_keyframe_interval_description": "Beállítja a kulcskockák közötti legnagyobb lehetséges távolságot. Alacsony érték csökkenti a tömörítési hatékonyságot, de lejátszás közben az előre- és hátratekerés gyorsabb, valamint javíthatja a gyorsan mozgó jelenetek képminőségét. 0 esetén a szoftver magának állítja be az értéket.",
+    "transcoding_optimal_description": "A célfelbontásnál nagyobb vagy a nem elfogadott formátumú videókat",
+    "transcoding_preferred_hardware_device": "Átkódoláshoz preferált hardver eszköz",
+    "transcoding_preferred_hardware_device_description": "Csak VAAPI vagy QSV esetén. Beállítja a hardveres átkódoláshoz használt DRI node-ot.",
+    "transcoding_preset_preset": "Előre Beállított (-preset)",
+    "transcoding_preset_preset_description": "Tömörítési sebesség. A lassabb beállítások kisebb fájlokat hoznak létre és növelik a minőséget az adott bitráta mellett. A VP9 kódolás figyelmen kívül hagyja a 'gyorsabb (faster)'-nél nagyobb sebességeket.",
+    "transcoding_reference_frames": "Referencia képkockák",
+    "transcoding_reference_frames_description": "A hivatkozott képkockák száma egy képkocka tömörítéséhez. Magasabb értékek növelik a tömörítési hatékonyságot, de lelassítják a kódolási folyamatot. 0 esetén a szoftver magának állítja be az értéket.",
+    "transcoding_required_description": "Csak az el nem fogadott formátumú videókat",
+    "transcoding_settings": "Videó Átkódolási Beállítások",
+    "transcoding_settings_description": "Videófájlok felbontásának és kódolásának kezelése",
+    "transcoding_target_resolution": "Célfelbontás",
+    "transcoding_target_resolution_description": "A magasabb felbontás jobb minőségben őrzi meg a részleteket, de tovább tart létrehozni, nagyobb fájlmérethez vezet és belassíthatja az alkalmazást.",
+    "transcoding_temporal_aq": "Időbeli (Temporal) AQ",
+    "transcoding_temporal_aq_description": "Csak NVENC esetén. Növeli a nagyon részletes, keveset mozgó videóanyag minőségét. Nem minden régi eszköz támogatja.",
+    "transcoding_threads": "Folyamatok száma",
+    "transcoding_threads_description": "Magas értékek esetén gyorsabban kódol, viszont kevesebb erőforrást hagy a szerver többi folyamatának. Nem ajánlott a CPU magjainak számánál nagyobb érték beállítása. A 0 érték maximalizálja a processzor kihasználását.",
+    "transcoding_tone_mapping": "Tónusleképezés (tone-mapping)",
+    "transcoding_tone_mapping_description": "Megpróbálja megőrizni a HDR videók kinézetét SDR-re való konvertálás során. Minden algoritmus különböző módon tesz kompromisszumot a színek, részletek, és a fényerő megőrzésében. A Hable inkább a részleteket őrzi meg, a Mobius a színeket, a Reinhard pedig a fényerőt.",
+    "transcoding_transcode_policy": "Átkódolási szabályzat",
+    "transcoding_transcode_policy_description": "Videó átkódolási szabályzat . HDR videók mindig átkódolásra kerülnek (kivéve, ha az átkódolás ki van kapcsolva).",
+    "transcoding_two_pass_encoding": "Átkódolás két menetben",
+    "transcoding_two_pass_encoding_setting_description": "A két menetben átkódolt videók jobb minőségűek lesznek. Ha engedélyezve van a bitráta maximalizálása (amely szükséges a H.264 és a HEVC használatakor), ez a funkció figyelmen kívül hagyja a CRF-et. VP9 használata esetén a CRF használható, ha a bitráta nincs maximalizálva (azaz ki van kapcsolva).",
+    "transcoding_video_codec": "Videó Kodek",
+    "transcoding_video_codec_description": "VP9 hatékonyabb és kompatibilisebb webre, de tovább tart az átkódolás. HEVC hasonló teljesítményű, de több web kompatibilitási problémát okozhat. H.264 széles körben kompatibilis és gyors az átkódolása, de sokkal nagyobb fájlokat készít. AV1 a leghatékonyabb kodek, de régebbi eszközök nem támogatják.",
+    "trash_enabled_description": "Lomtár engedélyezése",
+    "trash_number_of_days": "Napok száma",
+    "trash_number_of_days_description": "Hány napig legyenek a lomtárban az elemek a végleges törlés előtt",
+    "trash_settings": "Lomtár Beállítások",
+    "trash_settings_description": "Lomtár beállítások kezelése",
+    "untracked_files": "Nem Nyilvántartott Fájlok",
+    "untracked_files_description": "Ezeket a fájlokat az alkalmazás nem tartja nyilván. Ez lehetséges például meghiúsult áthelyezés vagy megszakított feltöltés miatt, illetve valamilyen alkalmazáshiba következtében",
+    "user_cleanup_job": "Felhasználók kipucolása",
+    "user_delete_delay": "<b>{user}</b> felhasználói fiókja és elemei véglegesen törölve lesznek {delay, plural, one {# nap} other {# nap}} múlva.",
+    "user_delete_delay_settings": "Törlési késleltetés",
+    "user_delete_delay_settings_description": "Hány nappal az eltávolítás után legyen véglegesen törölve a felhasználó fiókja és tárolt elemei. A végleges törlés feladat minden éjfélkor fut le, hogy ellenőrizze, hogy van-e törlendő felhasználó. Ez a beállítás a következő futtatás során lép életbe.",
+    "user_delete_immediately": "<b>{user}</b> felhasználója és összes eleme <b>azonnal</b> sorba állításra kerül a végleges törléshez .",
+    "user_delete_immediately_checkbox": "Felhasználó és tárolt elemeinek sorba állítása azonnali törlésre",
+    "user_management": "Felhasználók Kezelése",
+    "user_password_has_been_reset": "A felhasználó jelszava megváltoztatásra került:",
+    "user_password_reset_description": "Juttasd el az átmeneti jelszót a felhasználóhoz és tájékoztasd, hogy a következő belépésnél azt majd meg kell változtatnia.",
+    "user_restore_description": "<b>{user}</b> felhasználója vissza lesz állítva.",
+    "user_restore_scheduled_removal": "Felhasználó visszaállítása - törlésre jelölve: {date, date, long}",
+    "user_settings": "Felhasználó Beállítások",
+    "user_settings_description": "Felhasználó beállítások kezelése",
+    "user_successfully_removed": "{email} felhasználó sikeresen törlésre került.",
+    "version_check_enabled_description": "Új verziók elérhetőségének ellenőrzése",
+    "version_check_implications": "Az új verziók ellenőrzése időszakos kommunikációt igényel a github.com oldallal",
+    "version_check_settings": "Verzió Ellenőrzés",
+    "version_check_settings_description": "Az új verzióról való értesítés be- és kikapcsolása",
+    "video_conversion_job": "Videók Átkódolása",
+    "video_conversion_job_description": "Videók átkódolása böngészőkkel és eszközökkel való széleskörű kompatibilitás érdekében"
+  },
+  "admin_email": "Admin Email",
+  "admin_password": "Admin Jelszó",
+  "administration": "Adminisztráció",
+  "advanced": "Haladó",
+  "age_months": "Kor {months, plural, one {# hónap} other {# hónap}}",
+  "age_year_months": "Kor 1 év, {months, plural, one {# hónap} other {# hónap}}",
+  "age_years": "{years, plural, other {# év}}",
+  "album_added": "Album hozzáadva",
+  "album_added_notification_setting_description": "Email értesítőt kapsz, amikor hozzáadnak egy megosztott albumhoz",
+  "album_cover_updated": "Album borító frissítve",
+  "album_delete_confirmation": "Biztos, hogy ki szeretnéd törölni a(z) {album} albumot?",
+  "album_delete_confirmation_description": "Amennyiben ez egy megosztott album, a többi felhasználó sem fog tudni többé hozzáférni.",
+  "album_info_updated": "Album infó frissítve",
+  "album_leave": "Kilépsz az albumból?",
+  "album_leave_confirmation": "Biztos, hogy ki szeretnél lépni a(z) {album} albumból?",
+  "album_name": "Album Név",
+  "album_options": "Album beállítások",
+  "album_remove_user": "Felhasználó törlése?",
+  "album_remove_user_confirmation": "Biztos, hogy el szeretnéd távolítani {user} felhasználót?",
+  "album_share_no_users": "Úgy tűnik, hogy már minden felhasználóval megosztottad ezt az albumot, vagy nincs senki, akivel meg tudnád osztani.",
+  "album_updated": "Album frissült",
+  "album_updated_setting_description": "Küldjön email értesítőt, amikor egy megosztott albumhoz új elemeket adnak hozzá",
+  "album_user_left": "Kiléptél a(z) {album} albumból",
+  "album_user_removed": "{user} eltávolítva",
+  "album_with_link_access": "A link birtokában bárki láthatja a fotókat és a személyeket ebben az albumban.",
+  "albums": "Albumok",
+  "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Album}}",
+  "all": "Mind",
+  "all_albums": "Minden album",
+  "all_people": "Minden személy",
+  "all_videos": "Minden videó",
+  "allow_dark_mode": "Sötét téma engedélyezése",
+  "allow_edits": "Módosítások engedélyezése",
+  "allow_public_user_to_download": "Engedélyezi a letöltést publikus felhasználó számára",
+  "allow_public_user_to_upload": "Engedélyezi a feltöltést publikus felhasználó számára",
+  "anti_clockwise": "Óramutató járásával ellentétes irány",
+  "api_key": "API Kulcs",
+  "api_key_description": "Ez csak most az egyszer jelenik meg. Az ablak bezárása előtt feltétlenül másold.",
+  "api_key_empty": "Az API Kulcs név nem kéne, hogy üres legyen",
+  "api_keys": "API Kulcsok",
+  "app_settings": "Alkalmazás Beállítások",
+  "appears_in": "Itt szerepel",
+  "archive": "Archívum",
+  "archive_or_unarchive_photo": "Fotó archiválása vagy archiválásának visszavonása",
+  "archive_size": "Archívum mérete",
+  "archive_size_description": "Beállítja letöltésnél az archívum méretét (GiB)",
+  "archived_count": "{count, plural, other {Archiválva #}}",
+  "are_these_the_same_person": "Ugyanaz a személy?",
+  "are_you_sure_to_do_this": "Biztosan ezt szeretnéd csinálni?",
+  "asset_added_to_album": "Hozzáadva az albumhoz",
+  "asset_adding_to_album": "Hozzáadás az albumhoz...",
+  "asset_description_updated": "Az elem leírása frissült",
+  "asset_filename_is_offline": "A(z) {filename} elem nem elérhető, mert offline",
+  "asset_has_unassigned_faces": "Az elemnek hozzá nem rendelt arcai vannak",
+  "asset_hashing": "Hash számítása...",
+  "asset_offline": "Elem Offline",
+  "asset_offline_description": "Ez a külső elem már nem elérhető a lemezen. Kérlek, lépj kapcsolatba az Immich adminisztrátorával.",
+  "asset_skipped": "Kihagyva",
+  "asset_skipped_in_trash": "Lomtárban",
+  "asset_uploaded": "Feltöltve",
+  "asset_uploading": "Feltöltés...",
+  "assets": "Elemek",
+  "assets_added_count": "{count, plural, other {# elem}} hozzáadva",
+  "assets_added_to_album_count": "{count, plural, other {# elem}} hozzáadva az albumhoz",
+  "assets_added_to_name_count": "{count, plural, other {# elem}} hozzáadva {hasName, select, true {a(z) <b>{name}</b>} other {az új}} albumhoz",
+  "assets_count": "{count, plural, other {# elem}}",
+  "assets_moved_to_trash_count": "{count, plural, other {# elem}} áthelyezve a lomtárba",
+  "assets_permanently_deleted_count": "{count, plural, other {# elem}} véglegesen törölve",
+  "assets_removed_count": "{count, plural, other {# elem}} eltávolítva",
+  "assets_restore_confirmation": "Biztos, hogy visszaállítod a lomtárban lévő összes elemet? Ez a művelet nem visszavonható! Megjegyzés: az offline elemeket nem lehet így visszaállítani.",
+  "assets_restored_count": "{count, plural, other {# elem}} visszaállítva",
+  "assets_trashed_count": "{count, plural, other {# elem}} a lomtárba helyezve",
+  "assets_were_part_of_album_count": "{count, plural, other {# elem}} már eleve szerepelt az albumban",
+  "authorized_devices": "Engedélyezett Eszközök",
+  "back": "Vissza",
+  "back_close_deselect": "Vissza, bezárás, vagy kijelölés törlése",
+  "backward": "Visszafele",
+  "birthdate_saved": "Születésnap elmentve",
+  "birthdate_set_description": "A születés napját a rendszer arra használja, hogy kiírja, hogy a fénykép készítésekor a személy hány éves volt.",
+  "blurred_background": "Homályos háttér",
+  "bugs_and_feature_requests": "Hibabejelentés és Új Funkció Kérése",
+  "build": "Build",
+  "build_image": "Build Kép",
+  "bulk_delete_duplicates_confirmation": "Biztosan kitörölsz {count, plural, one {# duplikált elemet} other {# duplikált elemet}}? A művelet a legnagyobb méretű elemet tartja meg minden hasonló csoportból és minden másik duplikált elemet kitöröl. Ez a művelet nem visszavonható!",
+  "bulk_keep_duplicates_confirmation": "Biztosan meg szeretnél tartani {count, plural, other {# egyező elemet}}? Ez a művelet az elemek törlése nélkül megszünteti az összes duplikált csoportosítást.",
+  "bulk_trash_duplicates_confirmation": "Biztosan kitörölsz {count, plural, one {# duplikált fájlt} other {# duplikált fájlt}}? Ez a művelet megtartja minden csoportból a legnagyobb méretű elemet, és kitöröl minden másik duplikáltat.",
+  "buy": "Immich Megvásárlása",
+  "camera": "Fényképezőgép",
+  "camera_brand": "Fényképezőgép márka",
+  "camera_model": "Fényképezőgép modell",
+  "cancel": "Mégsem",
+  "cancel_search": "Keresés megszakítása",
+  "cannot_merge_people": "Személyek összevonása nem sikerült",
+  "cannot_undo_this_action": "Ez a művelet nem visszavonható!",
+  "cannot_update_the_description": "A leírás megváltoztatása nem sikerült",
+  "change_date": "Dátum változtatása",
+  "change_expiration_time": "Lejárati idő megváltoztatása",
+  "change_location": "Helyszín változtatása",
+  "change_name": "Név változtatása",
+  "change_name_successfully": "A név megváltoztatása sikeres",
+  "change_password": "Jelszócsere",
+  "change_password_description": "Most jelentkezel be a rendszerbe első alkalommal, vagy valaki jelszó-változtatást kezdeményezett. Kérjük, add meg az új jelszót.",
+  "change_your_password": "Jelszavad megváltoztatása",
+  "changed_visibility_successfully": "Láthatóság sikeresen megváltoztatva",
+  "check_all": "Mind Kijelöl",
+  "check_logs": "Hibanapló Megnyitása",
+  "choose_matching_people_to_merge": "Válaszd ki a megegyező személyeket összevonásra",
+  "city": "Város",
+  "clear": "Kitöröl",
+  "clear_all": "Alaphelyzet",
+  "clear_all_recent_searches": "Legutóbbi keresések törlése",
+  "clear_message": "Üzenet törlése",
+  "clear_value": "Érték törlése",
+  "clockwise": "Óramutató járásával megegyező irány",
+  "close": "Bezárás",
+  "collapse": "Összecsuk",
+  "collapse_all": "Mindet összecsuk",
+  "color": "Szín",
+  "color_theme": "Színtéma",
+  "comment_deleted": "Megjegyzés törölve",
+  "comment_options": "Megjegyzés beállítások",
+  "comments_and_likes": "Megjegyzések és reakciók",
+  "comments_are_disabled": "A megjegyzések le vannak tiltva",
+  "confirm": "Jóváhagy",
+  "confirm_admin_password": "Admin Jelszó Újból",
+  "confirm_delete_shared_link": "Biztosan törölni szeretnéd ezt a megosztott linket?",
+  "confirm_keep_this_delete_others": "Minden más elem a készletben törlésre kerül, kivéve ezt az elemet. Biztosan folytatni szeretnéd?",
+  "confirm_password": "Jelszó megerősítése",
+  "contain": "Belül",
+  "context": "Kontextus",
+  "continue": "Folytatás",
+  "copied_image_to_clipboard": "Kép a vágólapra másolva.",
+  "copied_to_clipboard": "Vágólapra másolva!",
+  "copy_error": "Másolási hiba",
+  "copy_file_path": "Fájlútvonal másolása",
+  "copy_image": "Kép Másolása",
+  "copy_link": "Link másolása",
+  "copy_link_to_clipboard": "Link másolása a vágólapra",
+  "copy_password": "Jelszó másolása",
+  "copy_to_clipboard": "Másolás a Vágólapra",
+  "country": "Ország",
+  "cover": "Kitöltés",
+  "covers": "Borítók",
+  "create": "Létrehoz",
+  "create_album": "Album létrehozása",
+  "create_library": "Képtár Létrehozása",
+  "create_link": "Link létrehozása",
+  "create_link_to_share": "Megosztási link létrehozása",
+  "create_link_to_share_description": "A kiválasztott fotókat mindenki láthassa, aki a linket használja",
+  "create_new_person": "Új személy létrehozása",
+  "create_new_person_hint": "A kiválasztott elemeket új személyhez rendelése",
+  "create_new_user": "Új felhasználó létrehozása",
+  "create_tag": "Címke létrehozása",
+  "create_tag_description": "Új címke létrehozása. Beágyazott címkék esetén add meg a címke teljes elérési útvonalát, beleértve a perjeleket is.",
+  "create_user": "Felhasználó létrehozása",
+  "created": "Készült",
+  "current_device": "Ez az eszköz",
+  "custom_locale": "Egyéni Területi Beállítás",
+  "custom_locale_description": "Dátumok és számok formázása a nyelv és terület szerint",
+  "dark": "Sötét",
+  "date_after": "Dátumtól",
+  "date_and_time": "Dátum és Idő",
+  "date_before": "Dátumig",
+  "date_of_birth_saved": "Születésnap sikeresen elmentve",
+  "date_range": "Dátum intervallum",
+  "day": "Nap",
+  "deduplicate_all": "Az Összes Deduplikálása",
+  "default_locale": "Alapértelmezett Területi Beállítás",
+  "default_locale_description": "Dátumok és számok formázása a böngésződ területi beállítása alapján",
+  "delete": "Törlés",
+  "delete_album": "Album törlése",
+  "delete_api_key_prompt": "Biztosan törölni szeretnéd ezt az API kulcsot?",
+  "delete_duplicates_confirmation": "Biztosan véglegesen törölni szeretnéd ezeket a duplikátumokat?",
+  "delete_key": "Kulcs törlése",
+  "delete_library": "Képtár Törlése",
+  "delete_link": "Link törlése",
+  "delete_others": "Többi törlése",
+  "delete_shared_link": "Megosztott link törlése",
+  "delete_tag": "Címke törlése",
+  "delete_tag_confirmation_prompt": "Biztosan törölni szeretnéd a(z) {tagName} címkét?",
+  "delete_user": "Felhasználó törlése",
+  "deleted_shared_link": "Törölt megosztott link",
+  "deletes_missing_assets": "Törli a fizikailag hiányzó elemeket",
+  "description": "Leírás",
+  "details": "Részletek",
+  "direction": "Irány",
+  "disabled": "Letiltott",
+  "disallow_edits": "Módosítások letiltása",
+  "discord": "Discord",
+  "discover": "Felfedez",
+  "dismiss_all_errors": "Minden hiba elvetése",
+  "dismiss_error": "Hiba elvetése",
+  "display_options": "Megjelenítési beállítások",
+  "display_order": "Megjelenítési sorrend",
+  "display_original_photos": "Eredeti fotók megjelenítése",
+  "display_original_photos_setting_description": "Egy elem nézegetése közben jelenítse meg inkább az eredeti elemet a bélyegkép helyett, ha az is web-kompatibilis. Ez lelassíthatja a fotók megjelenítését.",
+  "do_not_show_again": "Ne mutassa többé ezt az üzenetet",
+  "documentation": "Dokumentáció",
+  "done": "Kész",
+  "download": "Letöltés",
+  "download_include_embedded_motion_videos": "Beágyazott videók",
+  "download_include_embedded_motion_videos_description": "Mozgó képekbe beágyazott videók mutatása külön fájlként",
+  "download_settings": "Letöltés",
+  "download_settings_description": "Elemek letöltésével kapcsolatos beállítások kezelése",
+  "downloading": "Letöltés",
+  "downloading_asset_filename": "{filename} elem letöltése",
+  "drop_files_to_upload": "A feltöltéshez húzd bárhova a fájlokat",
+  "duplicates": "Duplikátumok",
+  "duplicates_description": "Jelöld meg a duplikátumokat (ha léteznek) a csoportokban",
+  "duration": "Időtartam",
+  "edit": "Szerkesztés",
+  "edit_album": "Album módosítása",
+  "edit_avatar": "Profilkép módosítása",
+  "edit_date": "Dátum módosítása",
+  "edit_date_and_time": "Dátum és idő módosítása",
+  "edit_exclusion_pattern": "Kizárási minta (pattern) módosítása",
+  "edit_faces": "Arcok módosítása",
+  "edit_import_path": "Importálási útvonal módosítása",
+  "edit_import_paths": "Importálási Útvonalak Módosítása",
+  "edit_key": "Kulcs módosítása",
+  "edit_link": "Link módosítása",
+  "edit_location": "Hely módosítása",
+  "edit_name": "Név módosítása",
+  "edit_people": "Személyek módosítása",
+  "edit_tag": "Címke módosítása",
+  "edit_title": "Cím Módosítása",
+  "edit_user": "Felhasználó módosítása",
+  "edited": "Módosítva",
+  "editor": "Szerkesztő",
+  "editor_close_without_save_prompt": "A változtatások nem lesznek elmentve",
+  "editor_close_without_save_title": "Szerkesztő bezárása?",
+  "editor_crop_tool_h2_aspect_ratios": "Oldalarányok",
+  "editor_crop_tool_h2_rotation": "Forgatás",
+  "email": "Email",
+  "empty_trash": "Lomtár ürítése",
+  "empty_trash_confirmation": "Biztosan kiüríted a lomtárat? Ez az Immich lomtárában lévő összes elemet véglegesen törli.\nEz a művelet nem visszavonható!",
+  "enable": "Engedélyezés",
+  "enabled": "Engedélyezve",
+  "end_date": "Vég dátum",
+  "error": "Hiba",
+  "error_loading_image": "Hiba a kép betöltése közben",
+  "error_title": "Hiba - valami félresikerült",
+  "errors": {
+    "cannot_navigate_next_asset": "Nem lehet a következő elemhez navigálni",
+    "cannot_navigate_previous_asset": "Nem lehet az előző elemhez navigálni",
+    "cant_apply_changes": "Nem lehet alkalmazni a változtatásokat",
+    "cant_change_activity": "Nem lehet {enabled, select, true {engedélyezni} other {kikapcsolni}} a tevékenységet",
+    "cant_change_asset_favorite": "Nem lehet a kedvenc állapotot megváltoztatni ehhez az elemhez",
+    "cant_change_metadata_assets_count": "Nem sikerült {count, plural, other {# elem}} metaadatát megváltoztatni",
+    "cant_get_faces": "Nem sikerült az arcok lekérdezése",
+    "cant_get_number_of_comments": "Hozzászólások számának lekérdezése sikertelen",
+    "cant_search_people": "Személyek keresése sikertelen",
+    "cant_search_places": "Helyek keresése sikertelen",
+    "cleared_jobs": "A(z) {job} feladat törölve",
+    "error_adding_assets_to_album": "Elemek albumhoz adása sikertelen",
+    "error_adding_users_to_album": "Felhasználók albumhoz adása sikertelen",
+    "error_deleting_shared_user": "Megosztott felhasználó törlése sikertelen",
+    "error_downloading": "{filename} letöltése sikertelen",
+    "error_hiding_buy_button": "A megvásárlás gomb elrejtése sikertelen",
+    "error_removing_assets_from_album": "Az elemek albumból való eltávolítása sikertelen - további információért ellenőrizd a konzol kimenetet",
+    "error_selecting_all_assets": "Az összes elem kijelölése sikertelen",
+    "exclusion_pattern_already_exists": "Ez a kizárási minta (pattern) már létezik.",
+    "failed_job_command": "A(z) {job} feladat {command} parancsa hibával zárult",
+    "failed_to_create_album": "Album készítése sikertelen",
+    "failed_to_create_shared_link": "Megosztott link készítése sikertelen",
+    "failed_to_edit_shared_link": "Megosztott link módosítása sikertelen",
+    "failed_to_get_people": "Személyek lekérdezése sikertelen",
+    "failed_to_keep_this_delete_others": "Nem sikerült megtartani ezt az elemet, és a többi elemet törölni",
+    "failed_to_load_asset": "Elem betöltése sikertelen",
+    "failed_to_load_assets": "Elemek betöltése sikertelen",
+    "failed_to_load_people": "Személyek betöltése sikertelen",
+    "failed_to_remove_product_key": "Termékkulcs eltávolítása sikertelen",
+    "failed_to_stack_assets": "Elemek csoportosítása sikertelen",
+    "failed_to_unstack_assets": "Csoportosított elemek szétszedése sikertelen",
+    "import_path_already_exists": "Ez az importálási útvonal már létezik.",
+    "incorrect_email_or_password": "Helytelen email vagy jelszó",
+    "paths_validation_failed": "A(z) {paths, plural, one {# elérési útvonal} other {# elérési útvonal}} érvényesítése sikertelen",
+    "profile_picture_transparent_pixels": "Profilképek nem tartalmazhatnak átlátszó pixeleket. Közelíts rá és/vagy mozgasd a képet.",
+    "quota_higher_than_disk_size": "Az elérhető lemezméretnél nagyobb kvótát állítottál be",
+    "repair_unable_to_check_items": "{count, select, one {elem} other {elem}} ellenőrzése sikertelen",
+    "unable_to_add_album_users": "Felhasználók albumhoz adása sikertelen",
+    "unable_to_add_assets_to_shared_link": "Elemeket megosztott linkhez adása sikertelen",
+    "unable_to_add_comment": "Hozzászólás sikertelen",
+    "unable_to_add_exclusion_pattern": "Kivétel minta (pattern) hozzáadása sikertelen",
+    "unable_to_add_import_path": "Importálási útvonal hozzáadása sikertelen",
+    "unable_to_add_partners": "Partnerek hozzáadása sikertelen",
+    "unable_to_add_remove_archive": "Az elem {archived, select, true {eltávolítása at Archívumból} other {hozzáadása Archívumhoz}} sikertelen",
+    "unable_to_add_remove_favorites": "Az elem {favorite, select, true {eltávolítása a Kedvencekből} other {hozzáadása a Kedvencekhez}} sikertelen",
+    "unable_to_archive_unarchive": "Az elem {archived, select, true {archiválása} other {kivétele az archívumból}} sikertelen",
+    "unable_to_change_album_user_role": "Az album felhasználói jogkörének megváltoztatása sikertelen",
+    "unable_to_change_date": "Dátum megváltoztatása sikertelen",
+    "unable_to_change_favorite": "Az elem kedvenc állapotának megváltoztatása sikertelen",
+    "unable_to_change_location": "Hely megváltoztatása sikertelen",
+    "unable_to_change_password": "Jelszó megváltoztatása sikertelen",
+    "unable_to_change_visibility": "{count, plural, other {# személy}} láthatóságának megváltoztatása sikertelen",
+    "unable_to_complete_oauth_login": "OAuth bejelentkezés befejezése sikertelen",
+    "unable_to_connect": "Csatlakozás sikertelen",
+    "unable_to_connect_to_server": "Szerverhez csatlakozás sikertelen",
+    "unable_to_copy_to_clipboard": "Nem lehet a vágólapra másolni. Ellenőrizd, hogy az oldalt https-en keresztül használod-e",
+    "unable_to_create_admin_account": "Admin felhasználó létrehozása sikertelen",
+    "unable_to_create_api_key": "Új API kulcs létrehozása sikertelen",
+    "unable_to_create_library": "Képtár létrehozása sikertelen",
+    "unable_to_create_user": "Felhasználó létrehozása sikertelen",
+    "unable_to_delete_album": "Album törlése sikertelen",
+    "unable_to_delete_asset": "Elem törlése sikertelen",
+    "unable_to_delete_assets": "Hiba az elemek törlésekor",
+    "unable_to_delete_exclusion_pattern": "Kizárási minta (pattern) törlése sikertelen",
+    "unable_to_delete_import_path": "Import útvonal törlése sikertelen",
+    "unable_to_delete_shared_link": "Megosztott link törlése sikertelen",
+    "unable_to_delete_user": "Felhasználó törlése sikertelen",
+    "unable_to_download_files": "Fájlok letöltése sikertelen",
+    "unable_to_edit_exclusion_pattern": "Kizárási minta (pattern) módosítása sikertelen",
+    "unable_to_edit_import_path": "Import útvonal módosítása sikertelen",
+    "unable_to_empty_trash": "Lomtár ürítése sikertelen",
+    "unable_to_enter_fullscreen": "Teljes képernyőre váltás sikertelen",
+    "unable_to_exit_fullscreen": "Kilépés a teljes képernyős módból sikertelen",
+    "unable_to_get_comments_number": "Hozzászólások számának lekérdezése sikertelen",
+    "unable_to_get_shared_link": "Megosztott link lekérdezése sikertelen",
+    "unable_to_hide_person": "Személy elrejtése sikertelen",
+    "unable_to_link_motion_video": "Motion videó összekapcsolása sikertelen",
+    "unable_to_link_oauth_account": "OAuth felhasználó hozzárendelése sikertelen",
+    "unable_to_load_album": "Album betöltése sikertelen",
+    "unable_to_load_asset_activity": "Elem aktivitásának betöltése sikertelen",
+    "unable_to_load_items": "Elemek betöltése sikertelen",
+    "unable_to_load_liked_status": "Reakció-állapot betöltése sikertelen",
+    "unable_to_log_out_all_devices": "Kijelentkezés az összes eszközből sikertelen",
+    "unable_to_log_out_device": "Kijelentkezés az eszközről sikertelen",
+    "unable_to_login_with_oauth": "OAuth bejelentkezés sikertelen",
+    "unable_to_play_video": "Videó lejátszása sikertelen",
+    "unable_to_reassign_assets_existing_person": "Nem sikerült az elemeket hozzárendelni{name, select, null { egy meglévő személyhez} other {: {name}}}",
+    "unable_to_reassign_assets_new_person": "Elemek új személyhez rendelése sikertelen",
+    "unable_to_refresh_user": "Felhasználó frissítése sikertelen",
+    "unable_to_remove_album_users": "Felhasználó eltávolítása az albumból sikertelen",
+    "unable_to_remove_api_key": "API kulcs eltávolítása sikertelen",
+    "unable_to_remove_assets_from_shared_link": "Elemek eltávolítása a megosztott linkből sikertelen",
+    "unable_to_remove_deleted_assets": "Offline fájlok eltávolítása sikertelen",
+    "unable_to_remove_library": "Képtár eltávolítása sikertelen",
+    "unable_to_remove_partner": "Partner eltávolítása sikertelen",
+    "unable_to_remove_reaction": "Reakció eltávolítása sikertelen",
+    "unable_to_repair_items": "Elemek javítása sikertelen",
+    "unable_to_reset_password": "Jelszó visszaállítása sikertelen",
+    "unable_to_resolve_duplicate": "Duplikátum feloldása sikertelen",
+    "unable_to_restore_assets": "Elemek visszaállítása sikertelen",
+    "unable_to_restore_trash": "Az összes elem visszaállítása sikertelen",
+    "unable_to_restore_user": "Felhasználó visszaállítása sikertelen",
+    "unable_to_save_album": "Album mentése sikertelen",
+    "unable_to_save_api_key": "API kulcs mentése sikertelen",
+    "unable_to_save_date_of_birth": "Születési időpont mentése sikertelen",
+    "unable_to_save_name": "Név mentése sikertelen",
+    "unable_to_save_profile": "Profil mentése sikertelen",
+    "unable_to_save_settings": "Beállítások mentése sikertelen",
+    "unable_to_scan_libraries": "A Képtárak átfésülése sikertelen",
+    "unable_to_scan_library": "A Képtár átfésülése sikertelen",
+    "unable_to_set_feature_photo": "Kijelölt fénykép beállítása sikertelen",
+    "unable_to_set_profile_picture": "Profilkép beállítása sikertelen",
+    "unable_to_submit_job": "A feladat elindítása sikertelen",
+    "unable_to_trash_asset": "Elem lomtárba helyezése sikertelen",
+    "unable_to_unlink_account": "A fiók szétkapcsolása sikertelen",
+    "unable_to_unlink_motion_video": "A motion videó szétkapcsolása sikertelen",
+    "unable_to_update_album_cover": "Albumborító beállítása sikertelen",
+    "unable_to_update_album_info": "Album információ frissítése sikertelen",
+    "unable_to_update_library": "Képtár frissítése sikertelen",
+    "unable_to_update_location": "Hely módosítása sikertelen",
+    "unable_to_update_settings": "Beállítások módosítása sikertelen",
+    "unable_to_update_timeline_display_status": "Az idővonal megjelenítési státuszának frissítése sikertelen",
+    "unable_to_update_user": "Felhasználó módosítása sikertelen",
+    "unable_to_upload_file": "Fájlfeltöltés sikertelen"
+  },
+  "exif": "Exif",
+  "exit_slideshow": "Kilépés a Diavetítésből",
+  "expand_all": "Összes kinyitása",
+  "expire_after": "Lejárati idő",
+  "expired": "Lejárt",
+  "expires_date": "Lejár: {date}",
+  "explore": "Böngészés",
+  "explorer": "Böngésző",
+  "export": "Exportálás",
+  "export_as_json": "Exportálás JSON formátumban",
+  "extension": "Kiterjesztés",
+  "external": "Külső Képtár",
+  "external_libraries": "Külső Képtárak",
+  "face_unassigned": "Nincs hozzárendelve",
+  "failed_to_load_assets": "Nem sikerült betölteni az elemeket",
+  "favorite": "Kedvenc",
+  "favorite_or_unfavorite_photo": "Fotó kedvencnek jelölése vagy annak visszavonása",
+  "favorites": "Kedvencek",
+  "feature_photo_updated": "Címlapkép frissítve",
+  "features": "Jellemzők",
+  "features_setting_description": "Az alkalmazás jellemzőinek kezelése",
+  "file_name": "Fájlnév",
+  "file_name_or_extension": "Fájlnév vagy kiterjesztés",
+  "filename": "Fájlnév",
+  "filetype": "Fájltípus",
+  "filter_people": "Személyek szűrése",
+  "find_them_fast": "Név alapján kereséssel gyorsan megtalálhatóak",
+  "fix_incorrect_match": "Hibás találat javítása",
+  "folders": "Mappák",
+  "folders_feature_description": "A fájlrendszerben lévő fényképek és videók mappanézetben való böngészése",
+  "forward": "Előre",
+  "general": "Általános",
+  "get_help": "Segítségkérés",
+  "getting_started": "Kezdő Lépések",
+  "go_back": "Visszalépés",
+  "go_to_search": "Ugrás a kereséshez",
+  "group_albums_by": "Albumok csoportosítása...",
+  "group_no": "Nincs csoportosítás",
+  "group_owner": "Csoportosítás tulajdonos szerint",
+  "group_year": "Csoportosítás év szerint",
+  "has_quota": "Kvóta",
+  "hi_user": "Szia {name} ({email})",
+  "hide_all_people": "Minden személy elrejtése",
+  "hide_gallery": "Galéria elrejtése",
+  "hide_named_person": "{name} elrejtése",
+  "hide_password": "Jelszó elrejtése",
+  "hide_person": "Személy elrejtése",
+  "hide_unnamed_people": "Név nélküli személyek elrejtése",
+  "host": "Kiszolgáló",
+  "hour": "Óra",
+  "image": "Kép",
+  "image_alt_text_date": "{isVideo, select, true {Videó} other {Kép}} készítési dátuma: {date}",
+  "image_alt_text_date_1_person": "{isVideo, select, true {Videó} other {Kép}} vele: {person1} (készült {date})",
+  "image_alt_text_date_2_people": "{isVideo, select, true {Videó} other {Kép}} velük: {person1} és {person2} (készült: {date})",
+  "image_alt_text_date_3_people": "{isVideo, select, true {Videó} other {Kép}} velük: {person1}, {person2} és {person3} (készült: {date})",
+  "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Videó} other {Kép}} velük: {person1}, {person2} és további {additionalCount, number} személy (készült: {date})",
+  "image_alt_text_date_place": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city} (készült: {date})",
+  "image_alt_text_date_place_1_person": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city}, vele: {person1} (készült: {date})",
+  "image_alt_text_date_place_2_people": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city}, velük: {person1} és {person2} (készült: {date})",
+  "image_alt_text_date_place_3_people": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city}, velük: {person1}, {person2} és {person3} (készült: {date})",
+  "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city}, velük: {person1}, {person2} és további {additionalCount, number} személy (készült: {date})",
+  "immich_logo": "Immich Logó",
+  "immich_web_interface": "Immich Webes Felület",
+  "import_from_json": "Importálás JSON formátumból",
+  "import_path": "Importálási útvonal",
+  "in_albums": "{count, plural, one {# albumban} other {# albumban}}",
+  "in_archive": "Archívumban",
+  "include_archived": "Archiváltakkal együtt",
+  "include_shared_albums": "Megosztott albumokkal együtt",
+  "include_shared_partner_assets": "Partner által megosztott elemekkel együtt",
+  "individual_share": "Egymagában megosztott elem",
+  "info": "Infó",
+  "interval": {
+    "day_at_onepm": "Minden nap 13 órakor",
+    "hours": "{hours, plural, one {óránként} other {{hours, number} óránként}}",
+    "night_at_midnight": "Minden éjjel éjfélkor",
+    "night_at_twoam": "Minden éjjel 2 órakor"
+  },
+  "invite_people": "Személyek Meghívása",
+  "invite_to_album": "Meghívás az albumba",
+  "items_count": "{count, plural, other {# elem}}",
+  "jobs": "Feladatok",
+  "keep": "Megtart",
+  "keep_all": "Összeset Megtart",
+  "keep_this_delete_others": "Ennek a meghagyása, a többi törlése",
+  "kept_this_deleted_others": "Ez az elem és a töröltek meg lettek hagyva {count, plural, one {# asset} other {# assets}}",
+  "keyboard_shortcuts": "Billentyűparancsok",
+  "language": "Nyelv",
+  "language_setting_description": "Válaszd ki preferált nyelvet",
+  "last_seen": "Utoljára láttuk",
+  "latest_version": "Legfrissebb Verzió",
+  "latitude": "Szélesség",
+  "leave": "Elhagyás",
+  "let_others_respond": "Mások is reagálhatnak",
+  "level": "Szint",
+  "library": "Képtár",
+  "library_options": "Képtár beállítások",
+  "light": "Világos",
+  "like_deleted": "Reakció törölve",
+  "link_motion_video": "Motion videó hozzárendelése",
+  "link_options": "Link beállítások",
+  "link_to_oauth": "Csatlakoztatás OAuth-hoz",
+  "linked_oauth_account": "Csatlakoztatott OAuth felhasználó",
+  "list": "Lista",
+  "loading": "Betöltés",
+  "loading_search_results_failed": "Keresési eredmények betöltése sikertelen",
+  "log_out": "Kijelentkezés",
+  "log_out_all_devices": "Kijelentkezés Minden Eszközön",
+  "logged_out_all_devices": "Minden eszköz kijelentkeztetve",
+  "logged_out_device": "Eszköz kijelentkeztetve",
+  "login": "Bejelentkezés",
+  "login_has_been_disabled": "Bejelentkezés le van tiltva.",
+  "logout_all_device_confirmation": "Biztos, hogy minden eszközön ki szeretnél jelentkezni?",
+  "logout_this_device_confirmation": "Biztos, hogy ki szeretnél jelentkezni ezen az eszközön?",
+  "longitude": "Hosszúság",
+  "look": "Megjelenítés",
+  "loop_videos": "Videók ismétlése",
+  "loop_videos_description": "Engedélyezi a videók folyamatosan ismételt lejátszását.",
+  "main_branch_warning": "Fejlesztői verziót használsz. Javasoljuk a stabil verzió használatát!",
+  "make": "Gyártó",
+  "manage_shared_links": "Megosztási linkek kezelése",
+  "manage_sharing_with_partners": "Partnerekkel való megosztás kezelése",
+  "manage_the_app_settings": "Alkalmazás beállításainak kezelése",
+  "manage_your_account": "Saját fiókod kezelése",
+  "manage_your_api_keys": "Saját API kulcsok kezelése",
+  "manage_your_devices": "Bejelentkezett eszközök kezelése",
+  "manage_your_oauth_connection": "OAuth kapcsolódás kezelése",
+  "map": "Térkép",
+  "map_marker_for_images": "{country}, {city} helyen készült képek térképjelölője",
+  "map_marker_with_image": "Térképjelölő képpel",
+  "map_settings": "Térkép beállítások",
+  "matches": "Azonosak",
+  "media_type": "Médiatípus",
+  "memories": "Emlékek",
+  "memories_setting_description": "Állítsd be, hogy mik jelenjenek meg az emlékeid közt",
+  "memory": "Emlék",
+  "memory_lane_title": "Emlékek {title}",
+  "menu": "Menü",
+  "merge": "Összevonás",
+  "merge_people": "Személyek összevonása",
+  "merge_people_limit": "Egyszerre legfeljebb 5 arcot vonhatsz össze",
+  "merge_people_prompt": "Biztosan összevonod ezeket a személyeket? Ez a művelet nem visszavonható.",
+  "merge_people_successfully": "Személyek sikeresen összevonva",
+  "merged_people_count": "Összevonva {count, plural, other {# személy}}",
+  "minimize": "Kicsinyítés",
+  "minute": "Perc",
+  "missing": "Hiányzók",
+  "model": "Modell",
+  "month": "Hónap",
+  "more": "Továbbiak",
+  "moved_to_trash": "Áthelyezve a lomtárba",
+  "my_albums": "Saját albumaim",
+  "name": "Név",
+  "name_or_nickname": "Név vagy becenév",
+  "never": "Soha",
+  "new_album": "Új Album",
+  "new_api_key": "Új API Kulcs",
+  "new_password": "Új jelszó",
+  "new_person": "Új személy",
+  "new_user_created": "Új felhasználó létrehozva",
+  "new_version_available": "ÚJ VERZIÓ ÉRHETŐ EL",
+  "newest_first": "Legújabb először",
+  "next": "Következő",
+  "next_memory": "Következő emlék",
+  "no": "Nem",
+  "no_albums_message": "Fotóid és videóid rendszerezéséhez hozz létre egy új albumot",
+  "no_albums_with_name_yet": "Úgy tűnik, hogy ilyen névvel még nincs albumod.",
+  "no_albums_yet": "Úgy tűnik, hogy még egy albumod sincs.",
+  "no_archived_assets_message": "Archiváld a fényképeket és videókat, hogy elrejtsd azokat a Képek nézetből",
+  "no_assets_message": "KATTINTS AZ ELSŐ FÉNYKÉP FELTÖLTÉSÉHEZ",
+  "no_duplicates_found": "Nem találhatók duplikátumok.",
+  "no_exif_info_available": "Nincs elérhető Exif információ",
+  "no_explore_results_message": "Tölts fel több képet, hogy böngészhesd a gyűjteményed.",
+  "no_favorites_message": "Add hozzá a kedvencekhez, hogy gyorsan megtaláld a legjobb képeidet és videóidat",
+  "no_libraries_message": "Hozz létre külső képtárat a fényképeid és videóid megtekintéséhez",
+  "no_name": "Nincs Név",
+  "no_places": "Nincsenek helyek",
+  "no_results": "Nincs találat",
+  "no_results_description": "Próbálkozz szinonimákkal vagy általánosabb kulcsszavakkal",
+  "no_shared_albums_message": "Hozz létre egy új albumot, hogy megoszthasd fényképeid és videóid másokkal",
+  "not_in_any_album": "Nincs albumban",
+  "note_apply_storage_label_to_previously_uploaded assets": "Megjegyzés: a korábban feltöltött elemek Tárhely Címkézéséhez futtasd a(z)",
+  "note_unlimited_quota": "Megjegyzés: korlátlan kvótához írj 0-t",
+  "notes": "Megjegyzések",
+  "notification_toggle_setting_description": "Email értesítések engedélyezése",
+  "notifications": "Értesítések",
+  "notifications_setting_description": "Értesítések kezelése",
+  "oauth": "OAuth",
+  "official_immich_resources": "Hivatalos Immich Források",
+  "offline": "Offline",
+  "offline_paths": "Offline útvonalak",
+  "offline_paths_description": "Ezek az eredmények annak lehetnek köszönhetők, hogy manuálisan törölték azokat a fájlokat, amik nem részei egy külső képtárnak.",
+  "ok": "Rendben",
+  "oldest_first": "Legrégebbi először",
+  "onboarding": "Első lépések",
+  "onboarding_privacy_description": "Az alábbi (nem kötelező) funkciók külsős szolgáltatásokon alapulnak és bármikor kikapcsolhatóak az adminisztrációs beállításokban.",
+  "onboarding_theme_description": "Válassz egy színtémát. Ezt bármikor megváltoztathatod a beállításokban.",
+  "onboarding_welcome_description": "Állítsunk be néhány gyakori beállítást.",
+  "onboarding_welcome_user": "Üdvözöllek {user}",
+  "online": "Online",
+  "only_favorites": "Csak kedvencek",
+  "open_in_map_view": "Megnyitás térkép nézetben",
+  "open_in_openstreetmap": "Megnyitás OpenStreetMap-ben",
+  "open_the_search_filters": "Keresési szűrők megnyitása",
+  "options": "Beállítások",
+  "or": "vagy",
+  "organize_your_library": "Rendszerezd a képtáradat",
+  "original": "eredeti",
+  "other": "Egyéb",
+  "other_devices": "Egyéb eszközök",
+  "other_variables": "Egyéb változók",
+  "owned": "Tulajdonos",
+  "owner": "Tulajdonos",
+  "partner": "Partner",
+  "partner_can_access": "{partner} hozzáférhet",
+  "partner_can_access_assets": "Minden fényképed és videód, kivéve az Archiváltak és a Töröltek",
+  "partner_can_access_location": "A helyszín, ahol a fotókat készítették",
+  "partner_sharing": "Partner Megosztás",
+  "partners": "Partnerek",
+  "password": "Jelszó",
+  "password_does_not_match": "A jelszavak nem egyeznek",
+  "password_required": "Jelszó Szükséges",
+  "password_reset_success": "A jelszó visszaállítása sikeres",
+  "past_durations": {
+    "days": "{days, plural, one {Tegnap} other {Elmúlt # nap}}",
+    "hours": "{hours, plural, one {Előző óra} other {Elmúlt # óra}}",
+    "years": "{years, plural, one {Tavaly} other {Elmúlt # év}}"
+  },
+  "path": "Útvonal",
+  "pattern": "Minta (Pattern)",
+  "pause": "Szüneteltetés",
+  "pause_memories": "Emlékek szüneteltetése",
+  "paused": "Szüneteltetve",
+  "pending": "Folyamatban lévő",
+  "people": "Személyek",
+  "people_edits_count": "{count, plural, other {# személy}} módosítva",
+  "people_feature_description": "Személyek szerint csoportosított fényképek és videók böngészése",
+  "people_sidebar_description": "Személyek link megjelenítése az oldalsávban",
+  "permanent_deletion_warning": "Figyelmeztetés végleges törlésről",
+  "permanent_deletion_warning_setting_description": "Figyelmeztessen elemek végleges törlése előtt",
+  "permanently_delete": "Végleges törlés",
+  "permanently_delete_assets_count": "{count, plural, one {Elem} other {Elemek}} végleges törlése",
+  "permanently_delete_assets_prompt": "Biztos, hogy véglegesen törölni {count, plural, one {szeretnéd ezt az elemet} other {szeretnél <b>#</b> elemet}}? Ez el fogja távolítani az {count, plural, one {elemet az albumokból, amikben szerepel} other {elemeket az albumokból, amikben szerepelnek}}.",
+  "permanently_deleted_asset": "Elem véglegesen törölve",
+  "permanently_deleted_assets_count": "{count, plural, other {# elem}} véglegesen törölve",
+  "person": "Személy",
+  "person_hidden": "{name}{hidden, select, true { (rejtett)} other {}}",
+  "photo_shared_all_users": "Úgy tűnik, hogy már mindenkivel megosztottad a fényképeidet, vagy nincs senki, akivel meg tudnád osztani.",
+  "photos": "Fényképek",
+  "photos_and_videos": "Fényképek és Videók",
+  "photos_count": "{count, plural, one {{count, number} Fotó} other {{count, number} Fotó}}",
+  "photos_from_previous_years": "Fényképek az előző évekből",
+  "pick_a_location": "Hely választása",
+  "place": "Hely",
+  "places": "Helyek",
+  "play": "Lejátszás",
+  "play_memories": "Emlékek lejátszása",
+  "play_motion_photo": "Mozgókép lejátszása",
+  "play_or_pause_video": "Videó elindítása vagy megállítása",
+  "port": "Port",
+  "preset": "Sablon",
+  "preview": "Előnézet",
+  "previous": "Előző",
+  "previous_memory": "Előző emlék",
+  "previous_or_next_photo": "Előző vagy következő fotó",
+  "primary": "Elsődleges",
+  "privacy": "Magánszféra",
+  "profile_image_of_user": "{user} profilképe",
+  "profile_picture_set": "Profilkép beállítva.",
+  "public_album": "Nyilvános album",
+  "public_share": "Nyilvános Megosztás",
+  "purchase_account_info": "Támogató",
+  "purchase_activated_subtitle": "Köszönjük, hogy támogattad az Immich-et és a nyílt forráskódú szoftvereket",
+  "purchase_activated_time": "Aktiválva ekkor: {date, date}",
+  "purchase_activated_title": "Kulcs sikeresen aktiválva",
+  "purchase_button_activate": "Aktiválás",
+  "purchase_button_buy": "Vásárlás",
+  "purchase_button_buy_immich": "Vásárold meg az Immich-et",
+  "purchase_button_never_show_again": "Soha többé ne mutassa",
+  "purchase_button_reminder": "Emlékeztessen 30 nap múlva",
+  "purchase_button_remove_key": "Kulcs eltávolítása",
+  "purchase_button_select": "Kiválaszt",
+  "purchase_failed_activation": "Sikertelen aktiválás! A helyes termékkulcsot az email fiókodban találhatod meg!",
+  "purchase_individual_description_1": "Egy magánszemélynek",
+  "purchase_individual_description_2": "Támogató állapot",
+  "purchase_individual_title": "Magánszemély",
+  "purchase_input_suggestion": "Van egy termékkulcsod? Add meg a kulcsot alább",
+  "purchase_license_subtitle": "Az Immich megvásárlásával támogasd a szolgáltatás folyamatos fejlesztését",
+  "purchase_lifetime_description": "Élettartamra szóló vásárlás",
+  "purchase_option_title": "VÁSÁRLÁSI LEHETŐSÉGEK",
+  "purchase_panel_info_1": "Az Immich készítése sok időt és erőfeszítést igényel, ezért főállásban foglalkoztatunk szoftvermérnököket, hogy annyira jó programmá tegyük, amennyire csak lehet. Küldetésünk, hogy a nyílt forráskódú szoftver és etikus üzleti gyakorlat fenntartható bevételi forrás legyen a fejlesztőknek, és hogy létrehozzunk egy magánszférát tiszteletben tartó ökoszisztémát, ami valódi alternatíváját jelenti a felhasználókat kizsákmányoló felhőalapú szolgáltatásoknak.",
+  "purchase_panel_info_2": "Mivel elkötelezettek vagyunk amellett, hogy ne vezessünk be csak pénzért elérhető extrákat, ezért ez a vásárlás nem biztosít új funkciókat az Immich-ben. Az Immich folyamatos fejlesztését az olyan felhasználók támogatására építjük mint Te.",
+  "purchase_panel_title": "Támogasd a projektet",
+  "purchase_per_server": "Szerverenként",
+  "purchase_per_user": "Felhasználónként",
+  "purchase_remove_product_key": "Termékkulcs Eltávolítása",
+  "purchase_remove_product_key_prompt": "Biztosan el szeretnéd távolítani a termékkulcsot?",
+  "purchase_remove_server_product_key": "Szerver termékkulcs eltávolítása",
+  "purchase_remove_server_product_key_prompt": "Biztosan el szeretnéd távolítani a szerver termékkulcsot?",
+  "purchase_server_description_1": "Az egész szerverre",
+  "purchase_server_description_2": "Támogató státusz",
+  "purchase_server_title": "Szerver",
+  "purchase_settings_server_activated": "A szerver termékkulcsot az admin kezeli",
+  "rating": "Értékelés csillagokkal",
+  "rating_clear": "Értékelés törlése",
+  "rating_count": "{count, plural, one {# csillag} other {# csillag}}",
+  "rating_description": "Exif értékelés megjelenítése az infópanelen",
+  "reaction_options": "Reakció lehetőségek",
+  "read_changelog": "Változásnapló Elolvasása",
+  "reassign": "Hozzárendel",
+  "reassigned_assets_to_existing_person": "{count, plural, other {# elem}} hozzárendelve{name, select, null { egy létező személyhez} other {: {name}}}",
+  "reassigned_assets_to_new_person": "{count, plural, other {# elem}} hozzárendelve egy új személyhez",
+  "reassing_hint": "Kijelölt elemek létező személyhez rendelése",
+  "recent": "Friss",
+  "recent-albums": "Legutóbbi albumok",
+  "recent_searches": "Legutóbbi keresések",
+  "refresh": "Frissítés",
+  "refresh_encoded_videos": "Átkódolt videók frissítése",
+  "refresh_faces": "Arcok frissítése",
+  "refresh_metadata": "Metaadatok frissítése",
+  "refresh_thumbnails": "Bélyegképek frissítése",
+  "refreshed": "Frissítve",
+  "refreshes_every_file": "Minden létező és új fájl újraolvasása",
+  "refreshing_encoded_video": "Átkódolt videók frissítése folyamatban",
+  "refreshing_faces": "Arcok frissítése folyamatban",
+  "refreshing_metadata": "Metaadatok frissítése folyamatban",
+  "regenerating_thumbnails": "Bélyegképek újragenerálása folyamatban",
+  "remove": "Eltávolítás",
+  "remove_assets_album_confirmation": "Biztosan el szeretnél távolítani {count, plural, one {# elemet} other {# elemet}} az albumból?",
+  "remove_assets_shared_link_confirmation": "Biztosan el szeretnél távolítani {count, plural, one {# elemet} other {# elemet}} ebből a megosztott linkből?",
+  "remove_assets_title": "Elemek eltávolítása?",
+  "remove_custom_date_range": "Egyéni időintervallum eltávolítása",
+  "remove_deleted_assets": "Törölt Elemek Eltávolítása",
+  "remove_from_album": "Eltávolítás az albumból",
+  "remove_from_favorites": "Eltávolítás a kedvencekből",
+  "remove_from_shared_link": "Eltávolítás a megosztott linkből",
+  "remove_url": "URL eltávolítása",
+  "remove_user": "Felhasználó eltávolítása",
+  "removed_api_key": "API Kulcs eltávolítva: {name}",
+  "removed_from_archive": "Archívumból eltávolítva",
+  "removed_from_favorites": "Kedvencekből eltávolítva",
+  "removed_from_favorites_count": "A kedvencekből {count, plural, other {# elem}} eltávolítva",
+  "removed_tagged_assets": "Címke eltávolítva {count, plural, one {# elemről} other {# elemről}}",
+  "rename": "Átnevezés",
+  "repair": "Javítás",
+  "repair_no_results_message": "A nem nyilvántartott és a hiányzó fájlok itt jelennek meg",
+  "replace_with_upload": "Csere feltöltéssel",
+  "repository": "Tároló",
+  "require_password": "Jelszó megadása szükséges",
+  "require_user_to_change_password_on_first_login": "A felhasználónak kötelező megváltoztatnia a jelszavát az első bejelentkezéskor",
+  "reset": "Visszaállítás",
+  "reset_password": "Jelszó visszaállítása",
+  "reset_people_visibility": "Személyek láthatóságának visszaállítása",
+  "reset_to_default": "Visszaállítás alapállapotba",
+  "resolve_duplicates": "Duplikátumok feloldása",
+  "resolved_all_duplicates": "Minden duplikátum feloldása",
+  "restore": "Visszaállít",
+  "restore_all": "Minden visszaállítása",
+  "restore_user": "Felhasználó visszaállítása",
+  "restored_asset": "Visszaállított elem",
+  "resume": "Folytatás",
+  "retry_upload": "Feltöltés újrapróbálása",
+  "review_duplicates": "Duplikátumok áttekintése",
+  "role": "Jogkör",
+  "role_editor": "Szerkesztő",
+  "role_viewer": "Megjelenítő",
+  "save": "Mentés",
+  "saved_api_key": "API Kulcs Elmentve",
+  "saved_profile": "Profil elmentve",
+  "saved_settings": "Elmentett beállítások",
+  "say_something": "Szólj hozzá",
+  "scan_all_libraries": "Minden Képtár Átfésülése",
+  "scan_library": "Átfésülés",
+  "scan_settings": "Átfésülési Beállítások",
+  "scanning_for_album": "Albumok átfésülése...",
+  "search": "Keresés",
+  "search_albums": "Albumok keresése",
+  "search_by_context": "Keresés tartalom alapján",
+  "search_by_filename": "Keresés fájlnév vagy kiterjesztés alapján",
+  "search_by_filename_example": "például IMG_1234.JPG vagy PNG",
+  "search_camera_make": "Kameragyártó keresése...",
+  "search_camera_model": "Kameramodell keresése...",
+  "search_city": "Város keresése...",
+  "search_country": "Ország keresése...",
+  "search_for_existing_person": "Már meglévő személy keresése",
+  "search_no_people": "Nincs személy",
+  "search_no_people_named": "Nincs \"{name}\" nevű személy",
+  "search_options": "Keresési lehetőségek",
+  "search_people": "Személyek keresése",
+  "search_places": "Helyek keresése",
+  "search_settings": "Keresési beállítások",
+  "search_state": "Megye/Állam keresése...",
+  "search_tags": "Címkék keresése...",
+  "search_timezone": "Időzóna keresése...",
+  "search_type": "Típus keresése",
+  "search_your_photos": "Fotóid keresése",
+  "searching_locales": "Helyszín keresése...",
+  "second": "Másodperc",
+  "see_all_people": "Minden személy megtekintése",
+  "select_album_cover": "Albumborító kiválasztása",
+  "select_all": "Összes kijelölése",
+  "select_all_duplicates": "Minden duplikátum kijelölése",
+  "select_avatar_color": "Avatár színének választása",
+  "select_face": "Arc kiválasztása",
+  "select_featured_photo": "Alapértelmezett fénykép kiválasztása",
+  "select_from_computer": "Kiválasztás a számítógépről",
+  "select_keep_all": "'Megtart' kijelölése",
+  "select_library_owner": "Válaszd ki a képtár tulajdonosát",
+  "select_new_face": "Új arc választása",
+  "select_photos": "Fotók választása",
+  "select_trash_all": "'Lomtár' kijelölése",
+  "selected": "Kiválasztott",
+  "selected_count": "{count, plural, other {# kiválasztva}}",
+  "send_message": "Üzenet küldése",
+  "send_welcome_email": "Üdvözlő email küldése",
+  "server_offline": "Szerver Nem Elérhető",
+  "server_online": "Szerver Elérhető",
+  "server_stats": "Szerver Statisztikák",
+  "server_version": "Szerver Verzió",
+  "set": "Beállít",
+  "set_as_album_cover": "Beállítás albumborítóként",
+  "set_as_profile_picture": "Beállítás profilképként",
+  "set_date_of_birth": "Születési dátum beállítása",
+  "set_profile_picture": "Profilkép beállítása",
+  "set_slideshow_to_fullscreen": "Diavetítés teljes képernyőre állítása",
+  "settings": "Beállítások",
+  "settings_saved": "Beállítások elmentve",
+  "share": "Megosztás",
+  "shared": "Megosztva",
+  "shared_by": "Megosztotta",
+  "shared_by_user": "{user} osztotta meg",
+  "shared_by_you": "Te osztottad meg",
+  "shared_from_partner": "{partner} fényképei",
+  "shared_link_options": "Megosztott link beállításai",
+  "shared_links": "Megosztott linkek",
+  "shared_photos_and_videos_count": "{assetCount, plural, other {# megosztott kép és videó.}}",
+  "shared_with_partner": "Megosztva {partner} partnereddel",
+  "sharing": "Megosztás",
+  "sharing_enter_password": "Add meg a jelszót az oldal megtekintéséhez.",
+  "sharing_sidebar_description": "Megosztás link megjelenítése az oldalsávban",
+  "shift_to_permanent_delete": "nyomd meg a ⇧ nyilat az elem végleges törléséhez",
+  "show_album_options": "Album beállítások mutatása",
+  "show_albums": "Albumok mutatása",
+  "show_all_people": "Minden személy mutatása",
+  "show_and_hide_people": "Személyek mutatása és elrejtése",
+  "show_file_location": "Fájl helyének mutatása",
+  "show_gallery": "Galéria mutatása",
+  "show_hidden_people": "Rejtett személyek mutatása",
+  "show_in_timeline": "Mutatás az idővonalon",
+  "show_in_timeline_setting_description": "Ennek a felhasználónak a képei és videói jelenjenek meg az idővonaladon",
+  "show_keyboard_shortcuts": "Billentyűparancsok mutatása",
+  "show_metadata": "Metaadatok mutatása",
+  "show_or_hide_info": "Info mutatása vagy elrejtése",
+  "show_password": "Jelszó mutatása",
+  "show_person_options": "Személy beállítások mutatása",
+  "show_progress_bar": "Folyamatjelző Mutatása",
+  "show_search_options": "Keresési lehetőségek mutatása",
+  "show_slideshow_transition": "Vetítés áttűnési effekt mutatása",
+  "show_supporter_badge": "Támogató jelvény",
+  "show_supporter_badge_description": "Támogató jelvény mutatása",
+  "shuffle": "Véletlenszerű",
+  "sidebar": "Oldalsáv",
+  "sidebar_display_description": "Nézet link megjelenítése az oldalsávban",
+  "sign_out": "Kijelentkezés",
+  "sign_up": "Regisztráció",
+  "size": "Méret",
+  "skip_to_content": "Ugrás a tartalomhoz",
+  "skip_to_folders": "Ugrás a mappákhoz",
+  "skip_to_tags": "Ugrás a címkékhez",
+  "slideshow": "Diavetítés",
+  "slideshow_settings": "Diavetítés beállításai",
+  "sort_albums_by": "Albumok rendezése...",
+  "sort_created": "Létrehozás dátuma",
+  "sort_items": "Elemek száma",
+  "sort_modified": "Módosítás dátuma",
+  "sort_oldest": "Legrégebbi fénykép",
+  "sort_recent": "Legújabb fénykép",
+  "sort_title": "Cím",
+  "source": "Forrás",
+  "stack": "Fotók csoportosítása",
+  "stack_duplicates": "Duplikátumok csoportosítása",
+  "stack_select_one_photo": "Válassz egy fő képet a csoportból",
+  "stack_selected_photos": "Kiválasztott fényképek csoportosítása",
+  "stacked_assets_count": "{count, plural, other {# elem}} csoportosítva",
+  "stacktrace": "Stacktrace",
+  "start": "Elindít",
+  "start_date": "Kezdő dátum",
+  "state": "Megye/Állam",
+  "status": "Állapot",
+  "stop_motion_photo": "Mozgókép Megállítása",
+  "stop_photo_sharing": "Fotóid megosztásának megszüntetése?",
+  "stop_photo_sharing_description": "{partner} mostantól nem fog tudni hozzáférni a fényképeidhez.",
+  "stop_sharing_photos_with_user": "Fényképeid megosztásának megszüntetése ezzel a felhasználóval",
+  "storage": "Tárhely",
+  "storage_label": "Tárhely címke",
+  "storage_usage": "{used}/{available} használatban",
+  "submit": "Beküldés",
+  "suggestions": "Javaslatok",
+  "sunrise_on_the_beach": "Napkelte a tengerparton",
+  "support": "Támogatás",
+  "support_and_feedback": "Támogatás és Visszajelzés",
+  "support_third_party_description": "Az Immich telepítésedet egy harmadik fél csomagolta. Mivel elképzelhető, hogy az esetlegesen felmerülő problémákat ez a csomag okozza, ezért kérjük, először velük közöld a problémákat az alábbi linkek segítségével.",
+  "swap_merge_direction": "Egyesítés irányának megfordítása",
+  "sync": "Szinkronizálás",
+  "tag": "Címke",
+  "tag_assets": "Elemek címkézése",
+  "tag_created": "Létrehozott címke: {tag}",
+  "tag_feature_description": "Fényképek és videók böngészése a címkék témája szerint csoportosítva",
+  "tag_not_found_question": "Nem találod a címkét? Hozz létre egy <link>új címkét</link>",
+  "tag_updated": "Frissített címke: {tag}",
+  "tagged_assets": "{count, plural, one {# elem} other {# elem}} felcímkézve",
+  "tags": "Címkék",
+  "template": "Sablon",
+  "theme": "Téma",
+  "theme_selection": "Témaválasztás",
+  "theme_selection_description": "A böngésző beállításának megfelelően automatikusan használjon világos vagy sötét témát",
+  "they_will_be_merged_together": "Egyesítve lesznek",
+  "third_party_resources": "Harmadik Féltől Származó Források",
+  "time_based_memories": "Emlékek idő alapján",
+  "timeline": "Idővonal",
+  "timezone": "Időzóna",
+  "to_archive": "Archiválás",
+  "to_change_password": "Jelszó megváltoztatása",
+  "to_favorite": "Kedvenc",
+  "to_login": "Bejelentkezés",
+  "to_parent": "Egy szinttel feljebb",
+  "to_trash": "Lomtárba helyezés",
+  "toggle_settings": "Beállítások átállítása",
+  "toggle_theme": "Sötét téma átváltása",
+  "total": "Összesen",
+  "total_usage": "Összesen használatban",
+  "trash": "Lomtár",
+  "trash_all": "Mindet lomtárba",
+  "trash_count": "{count, number} elem lomtárba helyezése",
+  "trash_delete_asset": "Elem Törlése / Lomtárba Helyezése",
+  "trash_no_results_message": "Itt lesznek láthatóak a lomtárba tett képek és videók.",
+  "trashed_items_will_be_permanently_deleted_after": "A lomtárban lévő elemek véglegesen törlésre kerülnek {days, plural, other {# nap}} múlva.",
+  "type": "Típus",
+  "unarchive": "Archívumból kivesz",
+  "unarchived_count": "{count, plural, other {# elem kivéve az archívumból}}",
+  "unfavorite": "Kedvenc közül kivesz",
+  "unhide_person": "Nem rejtett személy",
+  "unknown": "Ismeretlen",
+  "unknown_year": "Ismeretlen Év",
+  "unlimited": "Korlátlan",
+  "unlink_motion_video": "Mozgókép leválasztása",
+  "unlink_oauth": "OAuth leválasztása",
+  "unlinked_oauth_account": "Leválasztott OAuth fiók",
+  "unnamed_album": "Névtelen Album",
+  "unnamed_album_delete_confirmation": "Biztosan törölni szeretnéd ezt az albumot?",
+  "unnamed_share": "Névtelen Megosztás",
+  "unsaved_change": "Nem mentett változtatás",
+  "unselect_all": "Kijelölések megszüntetése",
+  "unselect_all_duplicates": "Duplikátumok kijelölésének megszüntetése",
+  "unstack": "Csoport Szétszedése",
+  "unstacked_assets_count": "{count, plural, other {# elemből}} álló csoport szétszedve",
+  "untracked_files": "Nem nyilvántartott fájlok",
+  "untracked_files_decription": "Ezeket a fájlokat az alkalmazás nem tartja nyilván. Ez lehetséges például meghiúsult áthelyezés vagy megszakított feltöltés miatt, illetve valamilyen alkalmazáshiba következtében",
+  "up_next": "Következik",
+  "updated_password": "Jelszó megváltoztatva",
+  "upload": "Feltöltés",
+  "upload_concurrency": "Párhuzamos feltöltés",
+  "upload_errors": "Feltöltés befejezve {count, plural, other {# hibával}}, frissítsd az oldalt az újonnan feltöltött elemek megtekintéséhez.",
+  "upload_progress": "Hátra van {remaining, number} - Feldolgozva {processed, number}/{total, number}",
+  "upload_skipped_duplicates": "{count, plural, other {# duplikátum}} kihagyva",
+  "upload_status_duplicates": "Duplikátumok",
+  "upload_status_errors": "Hibák",
+  "upload_status_uploaded": "Feltöltve",
+  "upload_success": "Feltöltés sikeres, frissítsd az oldalt az újonnan feltöltött elemek megtekintéséhez.",
+  "url": "URL",
+  "usage": "Használat",
+  "use_custom_date_range": "Szabadon megadott időintervallum használata",
+  "user": "Felhasználó",
+  "user_id": "Felhasználó azonosítója",
+  "user_liked": "{user} felhasználónak {type, select, photo {ez a fénykép} video {ez a videó} asset {ez az elem} other {ez}} tetszik",
+  "user_purchase_settings": "Megvásárlás",
+  "user_purchase_settings_description": "Vásárlás kezelése",
+  "user_role_set": "{user} felhasználónak {role} jogkör biztosítása",
+  "user_usage_detail": "Felhasználó használati adatai",
+  "user_usage_stats": "Fiók használati statisztikái",
+  "user_usage_stats_description": "Fiók használati statisztikáinak megtekintése",
+  "username": "Felhasználónév",
+  "users": "Felhasználók",
+  "utilities": "Segédeszközök",
+  "validate": "Ellenőrzés",
+  "variables": "Változók",
+  "version": "Verzió",
+  "version_announcement_closing": "Barátsággal, Alex",
+  "version_announcement_message": "Szia! Az Immich-nek elérhető egy új verziója. Kérjük, szánj időt a <link>verzióinformáció</link> elolvasására, hogy meggyőződj róla, hogy a beállításaid naprakészek, így elkerülj egy esetleges félrekonfigurálást. Különösen, ha WatchTower-t vagy más automatikus frissítési megoldást használsz.",
+  "version_history": "Verziótörténet",
+  "version_history_item": "{version} telepítve: {date}",
+  "video": "Videó",
+  "video_hover_setting": "Kisméretű videó elindítása, ha az egér az elem felé megy",
+  "video_hover_setting_description": "Ha az egér az elem felé megy, akkor induljon el a kisméretű videó lejátszása. Még ha ez az opció ki is van kapcsolva, a lejátszás akkor is elindítható a lejátszás gombbal.",
+  "videos": "Videók",
+  "videos_count": "{count, plural, one {# Videó} other {# Videó}}",
+  "view": "Nézet",
+  "view_album": "Album Megtekintése",
+  "view_all": "Összes Megtekintése",
+  "view_all_users": "Minden Felhasználó Megtekintése",
+  "view_in_timeline": "Megtekintés az idővonalon",
+  "view_links": "Linkek megtekintése",
+  "view_name": "Megtekintés",
+  "view_next_asset": "Következő elem megtekintése",
+  "view_previous_asset": "Előző elem megtekintése",
+  "view_stack": "Csoport Megtekintése",
+  "visibility_changed": "{count, plural, other {# személy}} láthatósága megváltozott",
+  "waiting": "Várakozás",
+  "warning": "Figyelmeztetés",
+  "week": "Hét",
+  "welcome": "Üdvözlünk",
+  "welcome_to_immich": "Üdvözlünk az Immich-ben",
+  "year": "Év",
+  "years_ago": "{years, plural, one {# évvel} other {# évvel}} ezelőtt",
+  "yes": "Igen",
+  "you_dont_have_any_shared_links": "Nincsenek megosztott linkjeid",
+  "zoom_image": "Kép Nagyítása"
+}
diff --git a/web/src/lib/i18n/hy.json b/i18n/hy.json
similarity index 95%
rename from web/src/lib/i18n/hy.json
rename to i18n/hy.json
index f5273c4ac9..a0ebff369c 100644
--- a/web/src/lib/i18n/hy.json
+++ b/i18n/hy.json
@@ -33,7 +33,6 @@
     "confirm_email_below": "",
     "confirm_reprocess_all_faces": "",
     "confirm_user_password_reset": "",
-    "crontab_guru": "",
     "disable_login": "",
     "duplicate_detection_job_description": "",
     "exclusion_pattern_description": "",
@@ -49,16 +48,9 @@
     "image_prefer_embedded_preview_setting_description": "",
     "image_prefer_wide_gamut": "",
     "image_prefer_wide_gamut_setting_description": "",
-    "image_preview_format": "",
-    "image_preview_resolution": "",
-    "image_preview_resolution_description": "",
     "image_quality": "",
-    "image_quality_description": "",
     "image_settings": "",
     "image_settings_description": "",
-    "image_thumbnail_format": "",
-    "image_thumbnail_resolution": "",
-    "image_thumbnail_resolution_description": "",
     "job_concurrency": "",
     "job_not_concurrency_safe": "",
     "job_settings": "",
@@ -67,8 +59,6 @@
     "jobs_delayed": "",
     "jobs_failed": "",
     "library_created": "",
-    "library_cron_expression": "",
-    "library_cron_expression_presets": "",
     "library_deleted": "",
     "library_import_path_description": "",
     "library_scanning": "",
@@ -172,15 +162,12 @@
     "paths_validated_successfully": "",
     "quota_size_gib": "",
     "refreshing_all_libraries": "",
-    "removing_offline_files": "",
     "repair_all": "",
     "repair_matched_items": "",
     "repaired_items": "",
     "require_password_change_on_login": "",
     "reset_settings_to_default": "",
     "reset_settings_to_recent_saved": "",
-    "scanning_library_for_changed_files": "",
-    "scanning_library_for_new_files": "",
     "send_welcome_email": "",
     "server_external_domain_settings": "",
     "server_external_domain_settings_description": "",
@@ -255,8 +242,6 @@
     "transcoding_threads_description": "",
     "transcoding_tone_mapping": "",
     "transcoding_tone_mapping_description": "",
-    "transcoding_tone_mapping_npl": "",
-    "transcoding_tone_mapping_npl_description": "",
     "transcoding_transcode_policy": "",
     "transcoding_transcode_policy_description": "",
     "transcoding_two_pass_encoding": "",
@@ -308,7 +293,6 @@
   "appears_in": "",
   "archive": "",
   "archive_or_unarchive_photo": "",
-  "archived": "",
   "asset_offline": "",
   "assets": "",
   "authorized_devices": "",
@@ -322,10 +306,6 @@
   "cancel_search": "",
   "cannot_merge_people": "",
   "cannot_update_the_description": "",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
   "change_date": "",
   "change_expiration_time": "",
   "change_location": "",
@@ -412,13 +392,6 @@
   "downloading": "",
   "duplicates": "",
   "duration": "",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
   "edit_album": "",
   "edit_avatar": "",
   "edit_date": "",
@@ -437,7 +410,6 @@
   "edited": "",
   "editor": "",
   "email": "",
-  "empty_album": "",
   "empty_trash": "",
   "enable": "",
   "enabled": "",
@@ -486,8 +458,8 @@
     "unable_to_refresh_user": "",
     "unable_to_remove_album_users": "",
     "unable_to_remove_api_key": "",
+    "unable_to_remove_deleted_assets": "",
     "unable_to_remove_library": "",
-    "unable_to_remove_offline_files": "",
     "unable_to_remove_partner": "",
     "unable_to_remove_reaction": "",
     "unable_to_repair_items": "",
@@ -523,7 +495,6 @@
   "extension": "",
   "external": "",
   "external_libraries": "",
-  "failed_to_get_people": "",
   "favorite": "",
   "favorite_or_unfavorite_photo": "",
   "favorites": "",
@@ -535,14 +506,12 @@
   "filter_people": "",
   "find_them_fast": "",
   "fix_incorrect_match": "",
-  "force_re-scan_library_files": "",
   "forward": "",
   "general": "",
   "get_help": "",
   "getting_started": "",
   "go_back": "",
   "go_to_search": "",
-  "go_to_share_page": "",
   "group_albums_by": "",
   "has_quota": "",
   "hide_gallery": "",
@@ -658,7 +627,6 @@
   "oldest_first": "",
   "online": "",
   "only_favorites": "",
-  "only_refreshes_modified_files": "",
   "open_the_search_filters": "",
   "options": "",
   "organize_your_library": "",
@@ -720,10 +688,10 @@
   "refreshed": "",
   "refreshes_every_file": "",
   "remove": "",
+  "remove_deleted_assets": "",
   "remove_from_album": "",
   "remove_from_favorites": "",
   "remove_from_shared_link": "",
-  "remove_offline_files": "",
   "removed_api_key": "",
   "rename": "",
   "repair": "",
@@ -747,8 +715,6 @@
   "saved_settings": "",
   "say_something": "",
   "scan_all_libraries": "",
-  "scan_all_library_files": "",
-  "scan_new_library_files": "",
   "scan_settings": "",
   "search": "",
   "search_albums": "",
@@ -779,7 +745,6 @@
   "selected": "",
   "send_message": "",
   "send_welcome_email": "",
-  "server": "",
   "server_stats": "",
   "set": "",
   "set_as_album_cover": "",
@@ -850,7 +815,6 @@
   "to_favorite": "",
   "toggle_settings": "",
   "toggle_theme": "",
-  "toggle_visibility": "",
   "total_usage": "",
   "trash": "",
   "trash_all": "",
@@ -858,11 +822,9 @@
   "trashed_items_will_be_permanently_deleted_after": "",
   "type": "",
   "unarchive": "",
-  "unarchived": "",
   "unfavorite": "",
   "unhide_person": "",
   "unknown": "",
-  "unknown_album": "",
   "unknown_year": "",
   "unlimited": "",
   "unlink_oauth": "",
@@ -896,7 +858,6 @@
   "view_links": "",
   "view_next_asset": "",
   "view_previous_asset": "",
-  "viewer": "",
   "waiting": "",
   "week": "",
   "welcome": "",
diff --git a/web/src/lib/i18n/id.json b/i18n/id.json
similarity index 92%
rename from web/src/lib/i18n/id.json
rename to i18n/id.json
index ea5ad94b27..f44bcf8905 100644
--- a/web/src/lib/i18n/id.json
+++ b/i18n/id.json
@@ -23,16 +23,23 @@
   "add_to": "Tambahkan ke...",
   "add_to_album": "Tambahkan ke album",
   "add_to_shared_album": "Tambahkan ke album terbagi",
+  "add_url": "Tambahkan URL",
   "added_to_archive": "Ditambahkan ke arsip",
   "added_to_favorites": "Ditambahkan ke favorit",
   "added_to_favorites_count": "Ditambahkan {count, number} ke favorit",
   "admin": {
     "add_exclusion_pattern_description": "Tambahkan pola pengecualian. Glob menggunakan *, **, dan ? didukung. Untuk mengabaikan semua berkas dalam direktori apa pun bernama \"Raw\", gunakan \"**/Raw/**\". Untuk mengabaikan semua berkas berakhiran dengan \".tif\", gunakan \"**/*.tif\". Untuk mengabaikan jalur absolut, gunakan \"/jalur/untuk/diabaikan/**\".",
+    "asset_offline_description": "Aset pustaka eksternal ini tidak ada di diska dan telah dipindahkan ke tempat sampah. Jika berkasnya dipindah dalam pustaka, periksa lini masa Anda untuk aset baru yang cocok. Untuk memulihkan aset ini, pastikan jalur berkas di bawah dapat diakses oleh Immich dan pindai pustaka.",
     "authentication_settings": "Pengaturan Autentikasi",
     "authentication_settings_description": "Kelola kata sandi, OAuth, dan pengaturan autentikasi lainnya",
     "authentication_settings_disable_all": "Anda yakin untuk menonaktifkan semua cara login? Login akan dinonaktikan secara menyeluruh.",
     "authentication_settings_reenable": "Untuk mengaktifkan ulang, gunakan <link>Perintah Server</link>.",
     "background_task_job": "Tugas Latar Belakang",
+    "backup_database": "Basis Data Cadangan",
+    "backup_database_enable_description": "Aktifkan pencadangan basis data",
+    "backup_keep_last_amount": "Jumlah cadangan untuk disimpan",
+    "backup_settings": "Pengaturan Pencadangan",
+    "backup_settings_description": "Kelola pengaturan pencadangan basis data",
     "check_all": "Periksa Semua",
     "cleared_jobs": "Tugas terselesaikan untuk: {job}",
     "config_set_by_file": "Konfigurasi saat ini ditetapkan oleh berkas konfigurasi",
@@ -41,33 +48,40 @@
     "confirm_email_below": "Untuk mengonfirmasi, ketik \"{email}\" di bawah",
     "confirm_reprocess_all_faces": "Apakah Anda yakin ingin memproses semua wajah? Ini juga akan menghapus nama orang.",
     "confirm_user_password_reset": "Apakah Anda yakin ingin mengatur ulang kata sandi {user}?",
+    "create_job": "Buat tugas",
+    "cron_expression": "Ekspresi cron",
+    "cron_expression_description": "Tetapkan interval pemindaian menggunakan format cron. Untuk informasi lebih lanjut, silakan merujuk misalnya ke <link>Crontab Guru</link>",
+    "cron_expression_presets": "Prasetel ekspresi cron",
     "disable_login": "Nonaktifkan log masuk",
     "duplicate_detection_job_description": "Jalankan pembelajaran mesin pada aset untuk mendeteksi gambar yang serupa. Bergantung pada Pencarian Pintar",
     "exclusion_pattern_description": "Pola pengecualian memungkinkan Anda mengabaikan berkas dan folder ketika memindai pustaka Anda. Ini berguna jika Anda memiliki folder yang berisi berkas yang tidak ingin diimpor, seperti berkas RAW.",
     "external_library_created_at": "Pustaka eksternal (dibuat pada {date})",
     "external_library_management": "Pengelolaan Pustaka Eksternal",
     "face_detection": "Deteksi wajah",
-    "face_detection_description": "Deteksikan wajah dalam aset menggunakan pembelajaran mesin. Untuk video, hanya gambar kecilnya yang disertakan. \"Semua\" memproses (ulang) semua aset. \"Hilang\" mengantrekan aset yang belum diproses. Wajah yang dideteksi akan diantrekan untuk Pengenalan Wajah setelah Pendeteksian Wajah selesai, mengelompokkan mereka dalam orang yang sudah ada atau yang baru.",
-    "facial_recognition_job_description": "Kelompokkan wajah yang telah dideteksi menjadi orang. Langkah ini berjalan setelah Deteksi Wajah selesai. \"Semua\" mengelompokkan (ulang) semua wajah. \"Hilang\" mengantrekan wajah yang belum ditetapkan dengan seseorang.",
+    "face_detection_description": "Deteksikan wajah dalam aset menggunakan pembelajaran mesin. Untuk video, hanya gambar kecilnya yang disertakan. \"Segarkan\" memproses (ulang) semua aset. \"Segarkan\" juga menghapus data wajah terkini. \"Hilang\" mengantrekan aset yang belum diproses. Wajah yang dideteksi akan diantrekan untuk Pengenalan Wajah setelah Pendeteksian Wajah selesai, mengelompokkan mereka dalam orang yang sudah ada atau yang baru.",
+    "facial_recognition_job_description": "Kelompokkan wajah yang telah dideteksi menjadi orang. Langkah ini berjalan setelah Deteksi Wajah selesai. \"Segarkan\" mengelompokkan (ulang) semua wajah. \"Hilang\" mengantrekan wajah yang belum ditetapkan dengan seseorang.",
     "failed_job_command": "Perintah {command} gagal untuk tugas: {job}",
     "force_delete_user_warning": "PERINGATAN: Ini akan segera menghapus pengguna dan semua asetnya. Ini tidak dapat diurungkan dan semua berkasnya tidak dapat dipulihkan.",
     "forcing_refresh_library_files": "Memaksakan penyegaran semua berkas pustaka",
+    "image_format": "Format",
     "image_format_description": "WebP menghasilkan ukuran berkas yang lebih kecil daripada JPEG, tetapi lebih lambat untuk dienkode.",
     "image_prefer_embedded_preview": "Utamakan pratinjau tersemat",
     "image_prefer_embedded_preview_setting_description": "Gunakan pratinjau tersemat dalam foto RAW sebagai masukan dalam pemrosesan gambar ketika tersedia. Ini dapat menghasilkan warna yang lebih akurat untuk beberapa gambar, tetapi kualitas pratinjau bergantung pada kamera dan gambarnya dapat memiliki lebih banyak artefak kompresi.",
     "image_prefer_wide_gamut": "Utamakan gamut luas",
     "image_prefer_wide_gamut_setting_description": "Gunakan Display P3 untuk gambar kecil. Ini menjaga kecerahan gambar dengan ruang warna yang luas, tetapi gambar dapat terlihat beda pada perangkat lawas dengan versi peramban yang lawas. Gambar sRGB tetap dalam sRGB untuk menghindari perubahan warna.",
-    "image_preview_format": "Format pratinjau",
-    "image_preview_resolution": "Resolusi pratinjau",
-    "image_preview_resolution_description": "Digunakan ketika menampilkan satu foto untuk pembelajaran mesin. Resolusi yang lebih tinggi dapat menjaga lebih banyak detail tetapi dapat membutuhkan waktu lama untuk mengode, memiliki ukuran berkas yang lebih besar, dan dapat mengurangi respons aplikasi.",
+    "image_preview_description": "Gambar berukuran sedang tanpa metadata, digunakan ketika melihat aset satuan dan untuk pembelajaran mesin",
+    "image_preview_quality_description": "Kualitas pratinjau dari 1-100. Lebih tinggi lebih baik, tetapi menghasilkan berkas lebih besar dan respons aplikasi. Menetapkan nilai rendah dapat memengaruhi kualitas pembelajaran mesin.",
+    "image_preview_title": "Pengaturan Pratinjau",
     "image_quality": "Kualitas",
-    "image_quality_description": "Kualitas gambar dari 1 sampai 100. Lebih tinggi baik untuk kualitas tetapi menghasilkan berkas lebih besar, opsi ini memengaruhi gambar Pratinjau dan Gambar Kecil.",
+    "image_resolution": "Resolusi",
+    "image_resolution_description": "Resolusi lebih tinggi dapat menjaga lebih banyak detail tetapi dapat memerlukan waktu lebih lama untuk dienkode, memiliki ukuran berkas yang lebih besar, dan dapat mengurangi respons aplikasi.",
     "image_settings": "Pengaturan Gambar",
     "image_settings_description": "Kelola kualitas dan resolusi gambar yang dibuat",
-    "image_thumbnail_format": "Format gambar kecil",
-    "image_thumbnail_resolution": "Resolusi gambar kecil",
-    "image_thumbnail_resolution_description": "Digunakan ketika menampilkan kelompok foto (lini masa utama, tampilan album, dll.). Resolusi yang lebih tinggi dapat menjaga lebih banyak detail tetapi memerlukan waktu lama untuk mengode, memiliki ukuran berkas yang lebih besar, dan dapat mengurangi respons aplikasi.",
+    "image_thumbnail_description": "Gambar kecil tanpa metadata, digunakan ketika melihat kelompok foto seperti lini masa utama",
+    "image_thumbnail_quality_description": "Kualitas gambar kecil dari 1-100. Lebih tinggi lebih baik, tetapi menghasilkan berkas lebih besar dan dapat mengurangi respons aplikasi.",
+    "image_thumbnail_title": "Pengaturan Gambar Kecil",
     "job_concurrency": "Konkurensi {job}",
+    "job_created": "Tugas telah dibuat",
     "job_not_concurrency_safe": "Tugas ini tidak aman untuk konkurensi.",
     "job_settings": "Pengaturan Tugas",
     "job_settings_description": "Kelola konkurensi tugas",
@@ -75,9 +89,6 @@
     "jobs_delayed": "{jobCount, plural, other {# tertunda}}",
     "jobs_failed": "{jobCount, plural, other {# gagal}}",
     "library_created": "Pustaka dibuat: {library}",
-    "library_cron_expression": "Ekspresi cron",
-    "library_cron_expression_description": "Menetapkan interval pemindaian menggunakan format cron. Untuk informasi lanjut silakan merujuk ke mis. <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Prasetel ekspresi cron",
     "library_deleted": "Pustaka dihapus",
     "library_import_path_description": "Tentukan folder untuk diimpor. Folder ini, termasuk subfolder, akan dipindai gambar dan videonya.",
     "library_scanning": "Pemindaian Berkala",
@@ -120,7 +131,7 @@
     "machine_learning_smart_search_description": "Cari gambar secara semantik menggunakan penyematan CLIP",
     "machine_learning_smart_search_enabled": "Aktifkan pencarian pintar",
     "machine_learning_smart_search_enabled_description": "Jika dinonaktifkan, gambar tidak akan dienkode untuk pencarian pintar.",
-    "machine_learning_url_description": "URL server pembelajaran mesin",
+    "machine_learning_url_description": "URL server pembelajaran mesin. Jika lebih dari satu URL disediakan, setiap server akan dicoba satu per satu sampai salah satu berhasil merespons, dari urutan pertama sampai terakhir.",
     "manage_concurrency": "Kelola Konkurensi",
     "manage_log_settings": "Kelola pengaturan log",
     "map_dark_style": "Gaya gelap",
@@ -150,7 +161,7 @@
     "note_cannot_be_changed_later": "CATATAN: Ini tidak akan dapat diubah lagi!",
     "note_unlimited_quota": "Catatan: Masukkan 0 untuk kuota tidak terbatas",
     "notification_email_from_address": "Dari alamat",
-    "notification_email_from_address_description": "Alamat surel pengirim, misalnya: \"Server Foto Immich <noreply@immich.app>\"",
+    "notification_email_from_address_description": "Alamat surel pengirim, misalnya: \"Server Foto Immich <noreply@example.com>\"",
     "notification_email_host_description": "Hos server surel (mis. smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Abaikan eror sertifikat",
     "notification_email_ignore_certificate_errors_description": "Abaikan eror validasi sertifikat TLS (tidak disarankan)",
@@ -196,22 +207,24 @@
     "password_settings": "Log Masuk Kata Sandi",
     "password_settings_description": "Kelola pengaturan log masuk kata sandi",
     "paths_validated_successfully": "Semua jalur berhasil divalidasi",
+    "person_cleanup_job": "Pembersihan data pribadi",
     "quota_size_gib": "Ukuran Kuota (GiB)",
     "refreshing_all_libraries": "Menyegarkan semua pustaka",
     "registration": "Pendaftaran Admin",
     "registration_description": "Karena Anda merupakan pengguna pertama dalam sistem, Anda akan ditetapkan sebagai Admin dan bertanggung jawab atas tugas administratif dan pengguna tambahan akan dibuat oleh Anda.",
-    "removing_offline_files": "Menghapus Berkas Luring",
     "repair_all": "Perbaiki Semua",
     "repair_matched_items": "{count, plural, one {# item} other {# item}} dicocokkan",
     "repaired_items": "{count, plural, one {# item} other {# item}} diperbaiki",
     "require_password_change_on_login": "Memerlukan pengguna untuk mengubah kata sandi pada log masuk pertama",
     "reset_settings_to_default": "Atur ulang pengaturan ke bawaan",
     "reset_settings_to_recent_saved": "Atur ulang pengaturan ke pengaturan tersimpan terkini",
-    "scanning_library_for_changed_files": "Memindai pustaka untuk berkas yang telah diubah",
-    "scanning_library_for_new_files": "Memindai pustaka untuk berkas baru",
+    "scanning_library": "Memindai pustaka",
+    "search_jobs": "Mencari tugas...",
     "send_welcome_email": "Kirim surel selamat datang",
     "server_external_domain_settings": "Domain eksternal",
     "server_external_domain_settings_description": "Domain untuk tautan terbagi publik, termasuk http(s)://",
+    "server_public_users": "Pengguna Publik",
+    "server_public_users_description": "Semua pengguna (nama dan email) didaftarkan ketika menambahkan pengguna ke album terbagi. Ketika dinonaktifkan, daftar pengguna hanya akan tersedia kepada pengguna admin.",
     "server_settings": "Pengaturan Server",
     "server_settings_description": "Kelola pengaturan server",
     "server_welcome_message": "Pesan selamat datang",
@@ -236,6 +249,17 @@
     "storage_template_settings_description": "Kelola struktur folder dan nama berkas dari aset yang diunggah",
     "storage_template_user_label": "<code>{label}</code> adalah Label Penyimpanan pengguna",
     "system_settings": "Pengaturan Sistem",
+    "tag_cleanup_job": "Pembersihan tag",
+    "template_email_available_tags": "Anda dapat menggunakan variabel berikut dalam templat Anda: {tags}",
+    "template_email_if_empty": "Jika templat kosong, surel bawaan akan digunakan.",
+    "template_email_invite_album": "Templat Undangan Album",
+    "template_email_preview": "Pratinjau",
+    "template_email_settings": "Templat Surel",
+    "template_email_settings_description": "Kelola templat notifikasi surel kustom",
+    "template_email_update_album": "Perbarui Templat Album",
+    "template_email_welcome": "Templat surel selamat datang",
+    "template_settings": "Templat Notifikasi",
+    "template_settings_description": "Kelola templat kustom untuk notifikasi.",
     "theme_custom_css_settings": "CSS Kustom",
     "theme_custom_css_settings_description": "CSS memungkinkan desain Immich untuk diubah.",
     "theme_settings": "Pengaturan Tema",
@@ -268,7 +292,7 @@
     "transcoding_hardware_acceleration": "Akselerasi Perangkat Keras",
     "transcoding_hardware_acceleration_description": "Uji coba; lebih cepat, tetapi akan memiliki kualitas lebih rendah pada kecepatan bit yang sama",
     "transcoding_hardware_decoding": "Dekode perangkat keras",
-    "transcoding_hardware_decoding_setting_description": "Hanya diterapkan pada NVENC dan RKMPP. Mengaktifkan akselerasi ujung ke ujung daripada hanya mengakselerasi pengodean. Mungkin tidak berfungsi pada semua video.",
+    "transcoding_hardware_decoding_setting_description": "Mengaktifkan akselerasi ujung ke ujung daripada hanya mengakselerasi pengodean. Mungkin tidak berfungsi pada semua video.",
     "transcoding_hevc_codec": "Kodek HEVC",
     "transcoding_max_b_frames": "Bingkai B maksimum",
     "transcoding_max_b_frames_description": "Nilai yang lebih tinggi meningkatkan efisiensi kompresi, tetapi membuat pengodean lebih lambat. Mungkin tidak kompatibel dengan akselerasi perangkat keras pada perangkat lawas. 0 menonaktifkan bingkai B, sedangkan -1 mengatur nilai ini secara otomatis.",
@@ -294,8 +318,6 @@
     "transcoding_threads_description": "Nilai yang lebih tinggi dapat mengode dengan cepat, tetapi mengurangi ruang bagi server untuk memproses tugas lain selagi aktif. Nilai ini seharusnya tidak lebih dari jumlah inti CPU. Memaksimalkan pemakaian jika ditetapkan ke 0.",
     "transcoding_tone_mapping": "Pemetaan nada",
     "transcoding_tone_mapping_description": "Mencoba menjaga tampilan video HDR ketika dikonversikan ke SDR. Setiap algoritma memiliki kekurangan pada warna, detail, dan kecerahan. Hable menjaga detail, Mobius menjaga warna, dan Reinhard menjada kecerahan.",
-    "transcoding_tone_mapping_npl": "NPL pemetaan nada",
-    "transcoding_tone_mapping_npl_description": "Warna akan disesuaikan agar terlihat normal untuk tampilan kecerahan ini. Nilai yang lebih rendah meningkatkan kecerahan video dan sebaliknya, karena nilai ini mengimbangi kecerahan tampilan. 0 menetapkan nilai ini secara otomatis.",
     "transcoding_transcode_policy": "Kebijakan transkode",
     "transcoding_transcode_policy_description": "Kebijakan untuk kapan sebuah video harus ditranskode. Video HDR akan selalu ditranskode (kecuali jika transkode dinonaktifkan).",
     "transcoding_two_pass_encoding": "Pengodean dua arah",
@@ -309,6 +331,7 @@
     "trash_settings_description": "Kelola pengaturan sampah",
     "untracked_files": "Berkas yang Belum Dilacak",
     "untracked_files_description": "Berkas ini tidak dilacak oleh aplikasi. Mereka dapat diakibatkan oleh pemindahan gagal, pengunggahan terganggu, atau tertinggal karena oleh kutu",
+    "user_cleanup_job": "Pembersihan data pengguna",
     "user_delete_delay": "Akun dan aset <b>{user}</b> akan dijadwalkan untuk penghapusan permanen dalam {delay, plural, one {# hari} other {# hari}}.",
     "user_delete_delay_settings": "Jeda penghapusan",
     "user_delete_delay_settings_description": "Jumlah hari setelah penghapusan untuk menghapus akun dan aset pengguna secara permanen. Tugas penghapusan pengguna berjalan pada tengah malam untuk memeriksa pengguna yang siap untuk dihapus. Perubahan pengaturan ini akan dievaluasi pada eksekusi berikutnya.",
@@ -375,7 +398,6 @@
   "archive_or_unarchive_photo": "Arsipkan atau batalkan pengarsipan foto",
   "archive_size": "Ukuran arsip",
   "archive_size_description": "Atur ukuran arsip untuk unduhan (dalam GiB)",
-  "archived": "",
   "archived_count": "{count, plural, other {# terarsip}}",
   "are_these_the_same_person": "Apakah ini adalah orang yang sama?",
   "are_you_sure_to_do_this": "Apakah Anda yakin ingin melakukan ini?",
@@ -385,8 +407,8 @@
   "asset_filename_is_offline": "Aset {filename} sedang luring",
   "asset_has_unassigned_faces": "Aset memiliki wajah yang belum ditetapkan",
   "asset_hashing": "Memilah...",
-  "asset_offline": "Aset luring",
-  "asset_offline_description": "Aset ini sedang luring. Immich tidak dapat mengakses lokasi berkasnya. Pastikan aset tersebut tersedia lalu pindai ulang pustaka.",
+  "asset_offline": "Aset Luring",
+  "asset_offline_description": "Aset eksternal ini tidak ada lagi di diska. Silakan hubungi administrator Immich Anda untuk bantuan.",
   "asset_skipped": "Dilewati",
   "asset_skipped_in_trash": "Dalam sampah",
   "asset_uploaded": "Sudah diunggah",
@@ -396,11 +418,10 @@
   "assets_added_to_album_count": "Ditambahkan {count, plural, one {# aset} other {# aset}} ke album",
   "assets_added_to_name_count": "Ditambahkan {count, plural, one {# aset} other {# aset}} ke {hasName, select, true {<b>{name}</b>} other {album baru}}",
   "assets_count": "{count, plural, one {# aset} other {# aset}}",
-  "assets_moved_to_trash": "",
   "assets_moved_to_trash_count": "Dipindahkan {count, plural, one {# aset} other {# aset}} ke sampah",
   "assets_permanently_deleted_count": "{count, plural, one {# aset} other {# aset}} dihapus secara permanen",
   "assets_removed_count": "{count, plural, one {# aset} other {# aset}} dihapus",
-  "assets_restore_confirmation": "Apakah Anda yakin ingin memulihkan semua aset yang dibuang? Anda tidak dapat mengurungkan tindakan ini!",
+  "assets_restore_confirmation": "Apakah Anda yakin ingin memulihkan semua aset yang dibuang? Anda tidak dapat mengurungkan tindakan ini! Perlu diingat bahwa aset luring tidak dapat dipulihkan.",
   "assets_restored_count": "{count, plural, one {# aset} other {# aset}} dipulihkan",
   "assets_trashed_count": "{count, plural, one {# aset} other {# aset}} dibuang ke sampah",
   "assets_were_part_of_album_count": "{count, plural, one {Aset telah} other {Aset telah}} menjadi bagian dari album",
@@ -411,6 +432,7 @@
   "birthdate_saved": "Tanggal lahir berhasil disimpan",
   "birthdate_set_description": "Tanggal lahir digunakan untuk menghitung umur orang ini pada saat foto diambil.",
   "blurred_background": "Latar belakang buram",
+  "bugs_and_feature_requests": "Permintaan Kutu dan Fitur",
   "build": "Versi",
   "build_image": "Versi Citra",
   "bulk_delete_duplicates_confirmation": "Apakah Anda yakin ingin menghapus {count, plural, one {# aset duplikat} other {# aset duplikat}} secara bersamaan? Ini akan menjaga aset terbesar dari setiap kelompok dan menghapus semua duplikat lain secara permanen. Anda tidak dapat mengurungkan tindakan ini!",
@@ -425,10 +447,6 @@
   "cannot_merge_people": "Tidak dapat menggabungkan orang",
   "cannot_undo_this_action": "Anda tidak dapat mengurungkan tindakan ini!",
   "cannot_update_the_description": "Tidak dapat memperbarui deskripsi",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
   "change_date": "Ubah tanggal",
   "change_expiration_time": "Ubah waktu kedaluwarsa",
   "change_location": "Ubah lokasi",
@@ -460,6 +478,7 @@
   "confirm": "Konfirmasi",
   "confirm_admin_password": "Konfirmasi Kata Sandi Admin",
   "confirm_delete_shared_link": "Apakah Anda yakin ingin menghapus tautan terbagi ini?",
+  "confirm_keep_this_delete_others": "Semua aset lain di dalam stack akan dihapus kecuali aset ini. Anda yakin untuk melanjutkan?",
   "confirm_password": "Konfirmasi kata sandi",
   "contain": "Berisi",
   "context": "Konteks",
@@ -507,18 +526,21 @@
   "delete_api_key_prompt": "Apakah Anda yakin ingin menghapus kunci API ini?",
   "delete_duplicates_confirmation": "Apakah Anda yakin ingin menghapus duplikat ini secara permanen?",
   "delete_key": "Hapus kunci",
-  "delete_library": "Hapus pustaka",
+  "delete_library": "Hapus Pustaka",
   "delete_link": "Hapus tautan",
+  "delete_others": "Hapus lainnya",
   "delete_shared_link": "Hapus tautan terbagi",
   "delete_tag": "Hapus tag",
   "delete_tag_confirmation_prompt": "Apakah Anda yakin ingin menghapus label tag {tagName}?",
   "delete_user": "Hapus pengguna",
   "deleted_shared_link": "Tautan terbagi dihapus",
+  "deletes_missing_assets": "Menghapus aset yang hilang dari diska",
   "description": "Deskripsi",
   "details": "Detail",
   "direction": "Arah",
   "disabled": "Dinonaktifkan",
   "disallow_edits": "Jangan perbolehkan penyuntingan",
+  "discord": "Discord",
   "discover": "Jelajahi",
   "dismiss_all_errors": "Abaikan semua eror",
   "dismiss_error": "Abaikan eror",
@@ -527,6 +549,7 @@
   "display_original_photos": "Tampilkan foto asli",
   "display_original_photos_setting_description": "Lebih suka menampilkan foto ketika menampilkan aset daripada gambar kecil ketika aset asli kompatibel dengan web. Ini dapat mengakibatkan kecepatan pemuatan foto yang lebih lambat.",
   "do_not_show_again": "Jangan tampilkan pesan ini lagi",
+  "documentation": "Dokumentasi",
   "done": "Selesai",
   "download": "Unduh",
   "download_include_embedded_motion_videos": "Video tersematkan",
@@ -596,6 +619,7 @@
     "failed_to_create_shared_link": "Gagal membuat tautan terbagi",
     "failed_to_edit_shared_link": "Gagal menyunting tautan terbagi",
     "failed_to_get_people": "Gagal mendapatkan orang",
+    "failed_to_keep_this_delete_others": "Gagal mempertahankan aset ini dan hapus aset-aset lainnya",
     "failed_to_load_asset": "Gagal membuka aset",
     "failed_to_load_assets": "Gagal membuka aset-aset",
     "failed_to_load_people": "Gagal mengunggah orang",
@@ -663,8 +687,8 @@
     "unable_to_remove_album_users": "Tidak dapat mengeluarkan pengguna dari album",
     "unable_to_remove_api_key": "Tidak dapat menghapus Kunci API",
     "unable_to_remove_assets_from_shared_link": "Tidak dapat menghapus aset dari tautan terbagi",
+    "unable_to_remove_deleted_assets": "Tidak dapat menghapus berkas luring",
     "unable_to_remove_library": "Tidak dapat menghapus pustaka",
-    "unable_to_remove_offline_files": "Tidak dapat menghapus berkas luring",
     "unable_to_remove_partner": "Tidak dapat menghapus partner",
     "unable_to_remove_reaction": "Tidak dapat menghapus reaksi",
     "unable_to_repair_items": "Tidak dapat memperbaiki item",
@@ -710,7 +734,7 @@
   "external": "Eksternal",
   "external_libraries": "Pustaka Eksternal",
   "face_unassigned": "Tidak ada nama",
-  "failed_to_get_people": "",
+  "failed_to_load_assets": "Gagal memuat aset",
   "favorite": "Favorit",
   "favorite_or_unfavorite_photo": "Favorit atau batalkan pemfavoritan foto",
   "favorites": "Favorit",
@@ -726,14 +750,12 @@
   "fix_incorrect_match": "Perbaiki pencocokan salah",
   "folders": "Berkas",
   "folders_feature_description": "Menjelajahi tampilan folder untuk foto dan video pada sistem file",
-  "force_re-scan_library_files": "Paksa Pindai Ulang Semua Berkas Pustaka",
   "forward": "Maju",
   "general": "Umum",
   "get_help": "Dapatkan Bantuan",
   "getting_started": "Memulai",
   "go_back": "Kembali",
   "go_to_search": "Pergi ke pencarian",
-  "go_to_share_page": "Pergi ke laman pembagian",
   "group_albums_by": "Kelompokkan album berdasarkan...",
   "group_no": "Tidak ada pengelompokan",
   "group_owner": "Kelompokkan berdasarkan pemilik",
@@ -759,9 +781,6 @@
   "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} diambil di {city}, {country} oleh {person1} dan {person2} pada {date}",
   "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} diambil di {city}, {country} oleh {person1}, {person2}, dan {person3} pada {date}",
   "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} diambil di {city}, {country} oleh {person1}, {person2}, dan {additionalCount, number} lainnya pada {date}",
-  "image_alt_text_people": "{count, plural, =1 {dengan {person1}} =2 {dengan {person1} dan {person2}} =3 {dengan {person1}, {person2}, dan {person3}} other {dengan {person1}, {person2}, dan {others, number} lainnya}}",
-  "image_alt_text_place": "di {city}, {country}",
-  "image_taken": "{isVideo, select, true {Video diambil} other {Gambar diambil}}",
   "immich_logo": "Logo Immich",
   "immich_web_interface": "Antarmuka Web Immich",
   "import_from_json": "Impor dari JSON",
@@ -785,6 +804,8 @@
   "jobs": "Tugas",
   "keep": "Simpan",
   "keep_all": "Simpan Semua",
+  "keep_this_delete_others": "Pertahankan ini, hapus lainnya",
+  "kept_this_deleted_others": "Aset ini dipertahankan dan {count, plural, one {# asset} other {# assets}} dihapus",
   "keyboard_shortcuts": "Pintasan papan ketik",
   "language": "Bahasa",
   "language_setting_description": "Pilih bahasa Anda yang disukai",
@@ -796,31 +817,6 @@
   "level": "Tingkat",
   "library": "Pustaka",
   "library_options": "Opsi pustaka",
-  "license_account_info": "Akun Anda sudah berlisensi",
-  "license_activated_subtitle": "Terima kasih atas dukungan Immich dan perangkat lunak bersumber terbuka",
-  "license_activated_title": "Lisensi Anda berhasil diaktifkan",
-  "license_button_activate": "Aktivasikan",
-  "license_button_buy": "Beli",
-  "license_button_buy_license": "Beli Lisensi",
-  "license_button_select": "Pilih",
-  "license_failed_activation": "Gagal mengaktivasi lisensi. Silakan periksa surel Anda untuk mendapatkan kunci yang benar!",
-  "license_individual_description_1": "1 lisensi per pengguna di server mana pun",
-  "license_individual_title": "Lisensi Individu",
-  "license_info_licensed": "Berlisensi",
-  "license_info_unlicensed": "Tidak Berlisensi",
-  "license_input_suggestion": "Ada lisensi? Masukan kuncinya di bawah",
-  "license_license_subtitle": "Beli lisensi untuk mendukung Immich",
-  "license_license_title": "LISENSI",
-  "license_lifetime_description": "Lisensi seumur hidup",
-  "license_per_server": "Per server",
-  "license_per_user": "Per pengguna",
-  "license_server_description_1": "1 lisensi per server",
-  "license_server_description_2": "Lisensi untuk semua pengguna di server",
-  "license_server_title": "Lisensi Server",
-  "license_trial_info_1": "Anda menjalankan versi Immich yang Tidak Berlisensi",
-  "license_trial_info_2": "Anda telah menggunakan Immich sekitar",
-  "license_trial_info_3": "{accountAge, plural, one {# hari} other {# hari}}",
-  "license_trial_info_4": "Pertimbangkan membeli lisensi untuk mendukung keberlanjutan pengembangan layanan",
   "light": "Terang",
   "like_deleted": "Suka dihapus",
   "link_motion_video": "Tautan video gerak",
@@ -842,6 +838,7 @@
   "look": "Tampilan",
   "loop_videos": "Ulangi video",
   "loop_videos_description": "Aktifkan untuk mengulangi video secara otomatis dalam penampil detail.",
+  "main_branch_warning": "Anda menggunakan versi pengembangan; kami sangat menyarankan menggunakan versi rilis!",
   "make": "Merek",
   "manage_shared_links": "Kelola tautan terbagi",
   "manage_sharing_with_partners": "Kelola pembagian dengan partner",
@@ -911,6 +908,7 @@
   "notifications": "Notifikasi",
   "notifications_setting_description": "Kelola notifikasi",
   "oauth": "OAuth",
+  "official_immich_resources": "Sumber Daya Immich Resmi",
   "offline": "Luring",
   "offline_paths": "Jalur luring",
   "offline_paths_description": "Hasil berikut dapat diakibatkan oleh penghapusan berkas manual yang bukan bagian dari pustaka eksternal.",
@@ -923,7 +921,6 @@
   "onboarding_welcome_user": "Selamat datang, {user}",
   "online": "Daring",
   "only_favorites": "Hanya favorit",
-  "only_refreshes_modified_files": "Hanya menyegarkan berkas yang diubah",
   "open_in_map_view": "Buka dalam tampilan peta",
   "open_in_openstreetmap": "Buka di OpenStreetMap",
   "open_the_search_filters": "Buka saringan pencarian",
@@ -967,7 +964,6 @@
   "permanently_delete_assets_count": "Hapus {count, plural, one {aset} other {aset}} secara permanen",
   "permanently_delete_assets_prompt": "Apakah Anda yakin untuk menghapus {count, plural, one {aset ini secara permanen?} other {sebanyak <b>#</b> aset-aset berikut secara permanen?}} Ini juga akan menghapus {count, plural, one {ini dari} other {semua dari}} album-albumnya.",
   "permanently_deleted_asset": "Aset dihapus secara permanen",
-  "permanently_deleted_assets": "",
   "permanently_deleted_assets_count": "{count, plural, one {# aset} other {# aset}} dihapus secara permanen",
   "person": "Orang",
   "person_hidden": "{name}{hidden, select, true { (tersembunyi)} other {}}",
@@ -1038,14 +1034,17 @@
   "reassigned_assets_to_new_person": "Menetapkan ulang {count, plural, one {# aset} other {# aset}} kepada orang baru",
   "reassing_hint": "Tetapkan aset yang dipilih ke orang yang sudah ada",
   "recent": "Terkini",
+  "recent-albums": "Album terkini",
   "recent_searches": "Pencarian terkini",
   "refresh": "Segarkan",
   "refresh_encoded_videos": "Segarkan video terenkode",
+  "refresh_faces": "Segarkan wajah",
   "refresh_metadata": "Segarkan metadata",
   "refresh_thumbnails": "Segarkan gambar kecil",
   "refreshed": "Disegarkan",
-  "refreshes_every_file": "Menyegarkan setiap berkas",
+  "refreshes_every_file": "Membaca ulang semua berkas yang sudah ada dan yang baru",
   "refreshing_encoded_video": "Menyegarkan video terenkode",
+  "refreshing_faces": "Menyegarkan wajah",
   "refreshing_metadata": "Menyegarkan metadata",
   "regenerating_thumbnails": "Membuat ulang gambar kecil",
   "remove": "Hapus",
@@ -1053,10 +1052,11 @@
   "remove_assets_shared_link_confirmation": "Apakah Anda yakin ingin menghapus {count, plural, one {# aset} other {# aset}} dari tautan terbagi ini?",
   "remove_assets_title": "Hapus aset?",
   "remove_custom_date_range": "Hapus jangka tanggal khusus",
+  "remove_deleted_assets": "Hapus Berkas Luring",
   "remove_from_album": "Hapus dari album",
   "remove_from_favorites": "Hapus dari favorit",
   "remove_from_shared_link": "Hapus dari tautan terbagi",
-  "remove_offline_files": "Hapus Berkas Luring",
+  "remove_url": "Hapus URL",
   "remove_user": "Keluarkan pengguna",
   "removed_api_key": "Kunci API Dihapus: {name}",
   "removed_from_archive": "Dihapus dari arsip",
@@ -1092,8 +1092,7 @@
   "saved_settings": "Pengaturan disimpan",
   "say_something": "Ucapkan sesuatu",
   "scan_all_libraries": "Pindai Semua Pustaka",
-  "scan_all_library_files": "Pindai Ulang Semua Berkas Pustaka",
-  "scan_new_library_files": "Pindai Berkas Pustaka Baru",
+  "scan_library": "Pindai",
   "scan_settings": "Pengaturan Pemindaian",
   "scanning_for_album": "Memindai album...",
   "search": "Cari",
@@ -1111,6 +1110,7 @@
   "search_options": "Pilihan pencarian",
   "search_people": "Cari orang",
   "search_places": "Cari tempat",
+  "search_settings": "Pengaturan pencarian",
   "search_state": "Cari negara bagian...",
   "search_tags": "Cari tag...",
   "search_timezone": "Cari zona waktu...",
@@ -1135,7 +1135,6 @@
   "selected_count": "{count, plural, other {# dipilih}}",
   "send_message": "Kirim pesan",
   "send_welcome_email": "Kirim surel selamat datang",
-  "server": "Server",
   "server_offline": "Server Luring",
   "server_online": "Server Daring",
   "server_stats": "Statistik Server",
@@ -1178,6 +1177,7 @@
   "show_person_options": "Tampilkan opsi orang",
   "show_progress_bar": "Tampilkan Bilah Progres",
   "show_search_options": "Tampilkan opsi pencarian",
+  "show_slideshow_transition": "Tampilkan transisi salindia",
   "show_supporter_badge": "Lencana suporter",
   "show_supporter_badge_description": "Tampilkan lencana suporter",
   "shuffle": "Acak",
@@ -1219,13 +1219,16 @@
   "submit": "Kirim",
   "suggestions": "Saran",
   "sunrise_on_the_beach": "Matahari terbit di pantai",
+  "support": "Dukungan",
+  "support_and_feedback": "Dukungan & Masukan",
+  "support_third_party_description": "Pemasangan Immich Anda telah dipaketkan oleh pihak ketiga. Masalah yang Anda alami dapat disebabkan oleh paket tersebut, jadi silakan ajukan isu dengan masalah tersebut menggunakan tautan di bawah.",
   "swap_merge_direction": "Ganti arah penggabungan",
   "sync": "Sinkronisasikan",
   "tag": "Tag",
   "tag_assets": "Tag aset",
   "tag_created": "Tag yang di buat: {tag}",
   "tag_feature_description": "Menjelajahi foto dan video yang dikelompokkan berdasarkan topik tag logis",
-  "tag_not_found_question": "Tidak dapat menemukan tag? Buat satu <link>disini</link>",
+  "tag_not_found_question": "Tidak dapat menemukan tag? <link>Buat tag baru.</link>",
   "tag_updated": "Tag yang diperbarui: {tag}",
   "tagged_assets": "Ditandai {count, plural, one {# aset} other {# aset}}",
   "tags": "Tag",
@@ -1234,18 +1237,19 @@
   "theme_selection": "Pemilihan tema",
   "theme_selection_description": "Tetapkan tema ke terang atau gelap secara otomatis berdasarkan preferensi sistem peramban Anda",
   "they_will_be_merged_together": "Mereka akan digabungkan bersama",
+  "third_party_resources": "Sumber Daya Pihak Ketiga",
   "time_based_memories": "Kenangan berbasis waktu",
+  "timeline": "Lini masa",
   "timezone": "Zona waktu",
   "to_archive": "Arsipkan",
   "to_change_password": "Ubah kata sandi",
   "to_favorite": "Favorit",
   "to_login": "Log masuk",
   "to_parent": "Ke induk",
-  "to_root": "Untuk melakukan root",
   "to_trash": "Sampah",
   "toggle_settings": "Saklar pengaturan",
   "toggle_theme": "Beralih tema gelap",
-  "toggle_visibility": "Saklar keterlihatan",
+  "total": "Jumlah",
   "total_usage": "Jumlah penggunaan",
   "trash": "Sampah",
   "trash_all": "Buang Semua",
@@ -1255,7 +1259,6 @@
   "trashed_items_will_be_permanently_deleted_after": "Item yang dibuang akan dihapus secara permanen setelah {days, plural, one {# hari} other {# hari}}.",
   "type": "Jenis",
   "unarchive": "Keluarkan dari arsip",
-  "unarchived": "",
   "unarchived_count": "{count, plural, other {# dipindahkan dari arsip}}",
   "unfavorite": "Hapus favorit",
   "unhide_person": "Munculkan orang",
@@ -1291,13 +1294,13 @@
   "use_custom_date_range": "Gunakan jangka tanggal khusus saja",
   "user": "Pengguna",
   "user_id": "ID Pengguna",
-  "user_license_settings": "Lisensi",
-  "user_license_settings_description": "Kelola lisensi Anda",
   "user_liked": "{user} menyukai {type, select, photo {foto ini} video {tayangan ini} asset {aset ini} other {ini}}",
   "user_purchase_settings": "Pembelian",
   "user_purchase_settings_description": "Atur pembelian kamu",
   "user_role_set": "Tetapkan {user} sebagai {role}",
   "user_usage_detail": "Detail penggunaan pengguna",
+  "user_usage_stats": "Statistik penggunaan akun",
+  "user_usage_stats_description": "Tampilkan statistik penggunaan akun",
   "username": "Nama pengguna",
   "users": "Pengguna",
   "utilities": "Peralatan",
@@ -1305,7 +1308,9 @@
   "variables": "Variabel",
   "version": "Versi",
   "version_announcement_closing": "Temanmu, Alex",
-  "version_announcement_message": "Halo, ada versi aplikasi yang baru. Silakan luangkan waktu Anda untuk mengunjungi <link>catatan rilis</link> dan pastikan pengaturan <code>docker-compose.yml</code> dan <code>.env</code> Anda sudah terkini untuk menghindari kesalahan dalam pengaturan, terutama jika Anda menggunakan WatchTower atau mekanisme lain yang menangani pembaruan aplikasi Anda secara otomatis.",
+  "version_announcement_message": "Hai! Versi baru Immich telah tersedia. Harap luangkan waktu untuk membaca <link>catatan rilis</link> untuk memastikan pengaturan Anda terkini untuk mencegah kesalahan konfigurasi, terutama jika Anda menggunakan WatchTower atau mekanisme apa pun yang menangani pembaruan server Immich secara otomatis.",
+  "version_history": "Riwayat Versi",
+  "version_history_item": "Terpasang {version} pada {date}",
   "video": "Video",
   "video_hover_setting": "Putar gambar kecil video saat kursor di atas",
   "video_hover_setting_description": "Putar gambar kecil video ketika tetikus berada di atas item. Bahkan saat dinonaktifkan, pemutaran dapat dimulai dengan mengambang di atas ikon putar.",
@@ -1317,16 +1322,16 @@
   "view_all_users": "Tampilkan semua pengguna",
   "view_in_timeline": "Lihat di timeline",
   "view_links": "Tampilkan tautan",
+  "view_name": "Tampilkan",
   "view_next_asset": "Tampilkan aset berikutnya",
   "view_previous_asset": "Tampilkan aset sebelumnya",
   "view_stack": "Tampilkan Tumpukan",
-  "viewer": "",
   "visibility_changed": "Keterlihatan diubah untuk {count, plural, one {# orang} other {# orang}}",
   "waiting": "Menunggu",
   "warning": "Peringatan",
   "week": "Pekan",
   "welcome": "Selamat datang",
-  "welcome_to_immich": "Selamat datang di immich",
+  "welcome_to_immich": "Selamat datang di Immich",
   "year": "Tahun",
   "years_ago": "{years, plural, one {# tahun} other {# tahun}} yang lalu",
   "yes": "Ya",
diff --git a/web/src/lib/i18n/it.json b/i18n/it.json
similarity index 89%
rename from web/src/lib/i18n/it.json
rename to i18n/it.json
index 6782b8fbb9..57e6fc1ab7 100644
--- a/web/src/lib/i18n/it.json
+++ b/i18n/it.json
@@ -1,8 +1,8 @@
 {
-  "about": "Informazioni",
+  "about": "Informazioni su",
   "account": "Profilo",
   "account_settings": "Impostazioni Account",
-  "acknowledge": "Ho capito",
+  "acknowledge": "Acconsento",
   "action": "Azione",
   "actions": "Azioni",
   "active": "Attivi",
@@ -23,16 +23,23 @@
   "add_to": "Aggiungi a...",
   "add_to_album": "Aggiungi all'album",
   "add_to_shared_album": "Aggiungi all'album condiviso",
+  "add_url": "Aggiungi URL",
   "added_to_archive": "Aggiunto all'archivio",
   "added_to_favorites": "Aggiunto ai preferiti",
   "added_to_favorites_count": "Aggiunti {count, number} ai preferiti",
   "admin": {
     "add_exclusion_pattern_description": "Aggiungi modelli di esclusione. È supportato il globbing utilizzando *, ** e ?. Per ignorare tutti i file in qualsiasi directory denominata \"Raw\", usa \"**/Raw/**\". Per ignorare tutti i file con estensione \".tif\", usa \"**/*.tif\". Per ignorare un percorso assoluto, usa \"/percorso/da/ignorare/**\".",
+    "asset_offline_description": "Questa risorsa della libreria esterna non si trova più sul disco ed è stata spostata nel cestino. Se il file è stato spostato all'interno della libreria, controlla la timeline per la nuova risorsa corrispondente. Per ripristinare questa risorsa, assicurati che Immich possa accedere al percorso del file seguente ed esegui la scansione della libreria.",
     "authentication_settings": "Autenticazione",
     "authentication_settings_description": "Gestisci password, OAuth e altre impostazioni di autenticazione",
     "authentication_settings_disable_all": "Sei sicuro di voler disabilitare tutte le modalità di accesso? Il login verrà disabilitato completamente.",
     "authentication_settings_reenable": "Per riabilitare, utilizza un <link>Comando Server</link>.",
     "background_task_job": "Attività in Background",
+    "backup_database": "Backup Database",
+    "backup_database_enable_description": "Abilita i backup del database",
+    "backup_keep_last_amount": "Quantità di backup precedenti da mantenere",
+    "backup_settings": "Impostazioni backup",
+    "backup_settings_description": "Gestisci le impostazioni dei backup",
     "check_all": "Controlla Tutto",
     "cleared_jobs": "Cancellati i processi per: {job}",
     "config_set_by_file": "La configurazione è attualmente impostata da un file di configurazione",
@@ -41,35 +48,40 @@
     "confirm_email_below": "Per confermare, scrivi \"{email}\" qui sotto",
     "confirm_reprocess_all_faces": "Sei sicuro di voler riprocessare tutti i volti? Questo cancellerà tutte le persone nominate.",
     "confirm_user_password_reset": "Sei sicuro di voler resettare la password di {user}?",
-    "crontab_guru": "Crontab Guru",
+    "create_job": "creare lavoro",
+    "cron_expression": "Espressione Cron",
+    "cron_expression_description": "Imposta il tempo di scansione utilizzando il formato Cron. Per ulteriori informazioni fare riferimento a <link>Crontab Guru</link>",
+    "cron_expression_presets": "Espressione Cron preimpostata",
     "disable_login": "Disabilita login",
-    "disabled": "Disattivato",
     "duplicate_detection_job_description": "Esegui il machine learning sugli assets per rilevare immagini simili. Basato su Ricerca Intelligente",
     "exclusion_pattern_description": "I modelli di esclusione ti permettono di ignorare file e cartelle durante la scansione della tua libreria. Questo è utile se hai cartelle che contengono file che non vuoi importare, come ad esempio, i file RAW.",
     "external_library_created_at": "Libreria esterna (creata il {date})",
     "external_library_management": "Gestione Librerie Esterne",
     "face_detection": "Rilevamento Volti",
-    "face_detection_description": "Rileva i volti presenti negli assets utilizzando il machine learning. Per i video, viene presa in considerazione solo la miniatura. \"Tutto\" (ri-)processerà tutti gli assets. \"Mancanti\" seleziona solo gli assets che non sono ancora stati processati. I volti rilevati verranno selezionati per il riconoscimento facciale dopo che il rilevamento dei volti sarà stato completato, raggruppandoli in persone esistenti e/o nuove.",
-    "facial_recognition_job_description": "Raggruppa i volti rilevati in persone. Questo processo viene eseguito dopo che il rilevamento volti è stato completato. \"Tutti\" (ri-)unisce tutti i volti. \"Mancanti\" processa i volti che non hanno una persona assegnata.",
+    "face_detection_description": "Rileva i volti presenti negli assets utilizzando il machine learning. Per i video, viene presa in considerazione solo la miniatura. \"Aggiorna\" (ri-)processerà tutti gli assets. \"Reset\" inoltre elimina tutti i dati dei volti correnti. \"Mancanti\" seleziona solo gli assets che non sono ancora stati processati. I volti rilevati verranno selezionati per il riconoscimento facciale dopo che il rilevamento dei volti sarà stato completato, raggruppandoli in persone esistenti e/o nuove.",
+    "facial_recognition_job_description": "Raggruppa i volti rilevati in persone. Questo processo viene eseguito dopo che il rilevamento volti è stato completato. \"Reset\" (ri-)unisce tutti i volti. \"Mancanti\" processa i volti che non hanno una persona assegnata.",
     "failed_job_command": "Il comando {command} è fallito per il processo: {job}",
     "force_delete_user_warning": "ATTENZIONE: Questo rimuoverà immediatamente l'utente e tutti i suoi assets. Non è possibile tornare indietro e i file non potranno essere recuperati.",
     "forcing_refresh_library_files": "Forzando l'aggiornamento completo della libreria",
+    "image_format": "formato",
     "image_format_description": "WebP produce file più piccoli rispetto a JPEG, ma l'encoding è più lento.",
     "image_prefer_embedded_preview": "Preferisci l'anteprima integrata",
     "image_prefer_embedded_preview_setting_description": "Usa l'anteprima integrata nelle foto RAW come input per l'elaborazione delle immagini, se disponibile. Questo permette un miglioramento dei colori per alcune immagini, ma la qualità delle anteprime dipende dalla macchina fotografica. Inoltre le immagini potrebbero presentare artefatti di compressione.",
     "image_prefer_wide_gamut": "Preferisci gamut più ampio",
     "image_prefer_wide_gamut_setting_description": "Usa lo spazio colore Display P3 per le anteprime. Questo aiuta a mantenere la vivacità delle immagini con spazi colore più ampi, tuttavia potrebbe non mostrare correttamente le immagini con dispositivi e browser obsoleti. Le immagini sRGB vengono preservate per evitare alterazioni del colore.",
-    "image_preview_format": "Formato anteprima",
-    "image_preview_resolution": "Risoluzione anteprima",
-    "image_preview_resolution_description": "Usata per visualizzazione individuale di foto e per machine learning. Risoluzioni più alte possono preservare più dettagli ma richiedono un encoding più lento, occupano più spazio, e possono ridurre la responsività della app.",
+    "image_preview_description": "Immagine di medie dimensioni con metadati eliminati, utilizzata durante la visualizzazione di una singola risorsa e per l'apprendimento automatico",
+    "image_preview_quality_description": "Qualità dell'anteprima da 1 a 100. Elevata è migliore ma produce file più pesanti e può ridurre la reattività dell'app. Impostare un valore basso può influenzare negativamente la qualità del machine learning.",
+    "image_preview_title": "Impostazioni dell'anteprima",
     "image_quality": "Qualità",
-    "image_quality_description": "Qualità dell'immagine da 1 a 100. Un valore più alto risulta in una migliore qualità, ma produce file più grandi.",
+    "image_resolution": "Risoluzione",
+    "image_resolution_description": "Risoluzioni più elevate possono preservare più dettagli ma richiedere più tempo per la codifica, avere dimensioni di file più grandi e possono ridurre la reattività dell'app.",
     "image_settings": "Impostazioni delle immagini",
     "image_settings_description": "Gestisci qualità e risoluzione delle immagini generate",
-    "image_thumbnail_format": "Formato miniatura",
-    "image_thumbnail_resolution": "Risoluzione miniatura",
-    "image_thumbnail_resolution_description": "Utilizzato per vedere gruppi di foto (linea temporale, vista album, etc.). Risoluzioni più alte possono mantenere più dettaglio però l'encoding sarà più lungo, i file avranno dimensioni maggiori e potrebbero causare una riduzione nella responsività dell'applicazione.",
+    "image_thumbnail_description": "Miniatura piccola senza metadati, utilizzata durante la visualizzazione di gruppi di foto come la sequenza temporale principale",
+    "image_thumbnail_quality_description": "Qualità delle miniature da 1 a 100. Un valore più alto è migliore, ma produce file più grandi e può ridurre la reattività dell'app.",
+    "image_thumbnail_title": "Impostazioni della copertina",
     "job_concurrency": "Concorrenza {job}",
+    "job_created": "Lavoro creato",
     "job_not_concurrency_safe": "Questo processo non è eseguibile in maniera concorrente.",
     "job_settings": "Impostazioni dei processi",
     "job_settings_description": "Gestisci la concorrenza dei processi",
@@ -77,9 +89,6 @@
     "jobs_delayed": "{jobCount, plural, one {# posticipato} other {# posticipati}}",
     "jobs_failed": "{jobCount, plural, one {# fallito} other {# falliti}}",
     "library_created": "Creata libreria: {library}",
-    "library_cron_expression": "Espressione cron",
-    "library_cron_expression_description": "Imposta l'intervallo di rilevazione utilizzando il formato cron. Per più informazioni consulta es. <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Espressioni cron preimpostate",
     "library_deleted": "Libreria eliminata",
     "library_import_path_description": "Specifica una cartella da importare. Questa cartella e le sue sottocartelle, verranno analizzate per cercare immagini e video.",
     "library_scanning": "Scansione periodica",
@@ -122,7 +131,7 @@
     "machine_learning_smart_search_description": "Cerca immagini semanticamente utilizzato gli embedding CLIP",
     "machine_learning_smart_search_enabled": "Attiva ricerca intelligente",
     "machine_learning_smart_search_enabled_description": "Se disabilitato le immagini non saranno codificate per la ricerca intelligente.",
-    "machine_learning_url_description": "URL del server machine learning",
+    "machine_learning_url_description": "URL del server machine learning. Se sono stati forniti più di un URL, verrà testato un server alla volta finché uno non risponderà, in ordine dal primo all'ultimo.",
     "manage_concurrency": "Gestisci Concorrenza",
     "manage_log_settings": "Gestisci le impostazioni dei log",
     "map_dark_style": "Tema scuro",
@@ -152,7 +161,7 @@
     "note_cannot_be_changed_later": "NOTA: Non potrà essere modificato in futuro!",
     "note_unlimited_quota": "Nota: Inserisci 0 per una quota illimitata",
     "notification_email_from_address": "Indirizzo mittente",
-    "notification_email_from_address_description": "Indirizzo email mittente, ad esempio: \"Server Foto Immich <noreply@immich.app>\"",
+    "notification_email_from_address_description": "Indirizzo email mittente, ad esempio: \"Server Foto Immich <noreply@example.com>\"",
     "notification_email_host_description": "Host del server email (es. smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Ignora errori di certificato",
     "notification_email_ignore_certificate_errors_description": "Ignora errori di validazione del certificato TLS (sconsigliato)",
@@ -198,22 +207,24 @@
     "password_settings": "Login con password",
     "password_settings_description": "Gestisci impostazioni del login con password",
     "paths_validated_successfully": "Percorsi validati con successo",
+    "person_cleanup_job": "Pulizia Persona",
     "quota_size_gib": "Dimensione Archiviazione (GiB)",
     "refreshing_all_libraries": "Aggiorna tutte le librerie",
     "registration": "Registrazione amministratore",
     "registration_description": "Poiché sei il primo utente del sistema, sarai assegnato come Amministratore e sarai responsabile dei task amministrativi, e utenti aggiuntivi saranno creati da te.",
-    "removing_offline_files": "Cancella File Offline",
     "repair_all": "Ripara Tutto",
     "repair_matched_items": "{count, plural, one {Rilevato # elemento} other {Rilevati # elementi}}",
     "repaired_items": "{count, plural, one {Riparato # elemento} other {Riparati # elementi}}",
     "require_password_change_on_login": "Richiedi all'utente di cambiare password al primo accesso",
     "reset_settings_to_default": "Ripristina impostazioni predefinite",
     "reset_settings_to_recent_saved": "Ripristina impostazioni alle impostazioni salvate di recente",
-    "scanning_library_for_changed_files": "Scansiona la libreria per file modificati",
-    "scanning_library_for_new_files": "Scansiona la libreria per nuovi file",
+    "scanning_library": "Scansione della libreria",
+    "search_jobs": "Cerca Jobs...",
     "send_welcome_email": "Invia email di benvenuto",
     "server_external_domain_settings": "Dominio esterno",
     "server_external_domain_settings_description": "Dominio per link condivisi pubblicamente, incluso http(s)://",
+    "server_public_users": "Utenti Pubblici",
+    "server_public_users_description": "Tutti gli utenti (nome ed e-mail) sono elencati quando si aggiunge un utente agli album condivisi. Quando disabilitato, l'elenco degli utenti sarà disponibile solo per gli utenti amministratori.",
     "server_settings": "Impostazioni Server",
     "server_settings_description": "Gestisci le impostazioni del server",
     "server_welcome_message": "Messaggio di benvenuto",
@@ -231,13 +242,24 @@
     "storage_template_migration_description": "Applica il <link>{template}</link> attuale agli asset caricati in precedenza",
     "storage_template_migration_info": "Le modifiche al modello di archiviazione verranno applicate solo agli asset nuovi. Per applicare le modifiche retroattivamente esegui <link>{job}</link>.",
     "storage_template_migration_job": "Processo Migrazione Modello di Archiviazione",
-    "storage_template_more_details": "Per più informazioni riguardo a questa funzionalità, consulta il <template-link>Modello Archiviazione</template-link> e le sue <implications-link>conseguenze</implications-link>",
+    "storage_template_more_details": "Per maggiori informazioni riguardo a questa funzionalità, consulta il <template-link>Modello Archiviazione</template-link> e le sue <implications-link>conseguenze</implications-link>",
     "storage_template_onboarding_description": "Quando attivata, questa funzionalità organizzerà automaticamente i file utilizzando il modello di archiviazione definito dall'utente. Per ragioni di stabilità, questa funzionalità è disabilitata per impostazione predefinita. Per più informazioni, consulta <link>la documentazione</link>.",
     "storage_template_path_length": "Limite approssimativo lunghezza percorso: <b>{length, number}</b>/{limit, number}",
     "storage_template_settings": "Modello Archiviazione",
     "storage_template_settings_description": "Gestisci la struttura delle cartelle e il nome degli asset caricati",
     "storage_template_user_label": "<code>{label}</code> è l'etichetta di archiviazione dell'utente",
     "system_settings": "Impostazioni di sistema",
+    "tag_cleanup_job": "Pulisci Tag",
+    "template_email_available_tags": "Puoi usare le seguenti variabili nel tuo modello: {tags}",
+    "template_email_if_empty": "Se il modello è vuoto, verrà usata l'email di default.",
+    "template_email_invite_album": "Modello di invito all'album",
+    "template_email_preview": "Anteprima",
+    "template_email_settings": "Template Email",
+    "template_email_settings_description": "Gestisci i modelli personalizzati di notifiche email",
+    "template_email_update_album": "Modello di aggiornamento dell'album",
+    "template_email_welcome": "Modello di email di benvenuto",
+    "template_settings": "Templates Notifiche",
+    "template_settings_description": "Gestisci i modelli personalizzati per le notifiche.",
     "theme_custom_css_settings": "CSS Personalizzato",
     "theme_custom_css_settings_description": "I Cascading Style Sheets (CSS) permettono di personalizzare l'interfaccia di Immich.",
     "theme_settings": "Impostazioni Tema",
@@ -245,7 +267,6 @@
     "these_files_matched_by_checksum": "File abbinati per checksum",
     "thumbnail_generation_job": "Generazione Miniature",
     "thumbnail_generation_job_description": "Genera miniature grandi, piccole e sfocate per ogni asset, oltre a miniature per ogni persona",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "API di accelerazione",
     "transcoding_acceleration_api_description": "L'API che interagirà con il tuo dispositivo per accelerare la transcodifica. Questa impostazione è \"best effort\": ripiegherà sulla transcodifica software in caso di fallimento. VP9 potrebbe funzionare o meno a seconda del tuo hardware.",
     "transcoding_acceleration_nvenc": "NVENC (richiede GPU NVIDIA)",
@@ -271,7 +292,7 @@
     "transcoding_hardware_acceleration": "Accelerazione Hardware",
     "transcoding_hardware_acceleration_description": "Sperimentale; molto più veloce, ma avrà una qualità inferiore allo stesso bitrate",
     "transcoding_hardware_decoding": "Decodifica hardware",
-    "transcoding_hardware_decoding_setting_description": "Si applica solo a NVENC, QSV e RKMPP. Abilita l'accelerazione end-to-end anziché solo l'accelerazione dell'encoding. Potrebbe non funzionare su tutti i video.",
+    "transcoding_hardware_decoding_setting_description": "Abilita l'accelerazione end-to-end anziché accelerare solo la codifica. Potrebbe non funzionare su tutti i video.",
     "transcoding_hevc_codec": "Codec HEVC",
     "transcoding_max_b_frames": "B-frames Massimi",
     "transcoding_max_b_frames_description": "Valori più alti migliorano l'efficienza di compressione, ma rallentano l'encoding. Potrebbero non essere compatibili con l'accelerazione hardware su dispositivi più vecchi. 0 disabilita i B-frames, mentre -1 imposta questo valore automaticamente.",
@@ -297,8 +318,6 @@
     "transcoding_threads_description": "Valori più alti portano a una codifica più veloce, ma lasciano meno spazio al server per elaborare altre attività durante l'attività. Questo valore non dovrebbe essere superiore al numero di core CPU. Massimizza l'utilizzo se impostato su 0.",
     "transcoding_tone_mapping": "Mappatura della tonalità",
     "transcoding_tone_mapping_description": "Tenta di preservare l'aspetto dei video HDR quando convertiti in SDR. Ciascun algoritmo fa diversi compromessi per colore, dettaglio e luminosità. Hable conserva il dettaglio, Mobius conserva il colore e Reinhard conserva la luminosità.",
-    "transcoding_tone_mapping_npl": "Mappatura della tonalità NPL",
-    "transcoding_tone_mapping_npl_description": "I colori verranno regolati per apparire normali su uno schermo di questa luminosità. Contrariamente all'intuito, valori più bassi aumentano la luminosità del video e viceversa poiché compensano la luminosità dello schermo. 0 imposta questo valore automaticamente.",
     "transcoding_transcode_policy": "Politica di transcodifica",
     "transcoding_transcode_policy_description": "Politica che determina quando un video deve essere trascodificato. I video HDR verranno sempre trascodificati (eccetto quando la trascodifica è disabilitata).",
     "transcoding_two_pass_encoding": "Codifica a due passaggi",
@@ -312,6 +331,7 @@
     "trash_settings_description": "Gestisci impostazioni cestino",
     "untracked_files": "File non tracciati",
     "untracked_files_description": "Questi file non sono tracciati dall'applicazione. Potrebbero essere il risultato di spostamenti falliti, caricamenti interrotti o abbandonati a causa di un bug",
+    "user_cleanup_job": "Pulizia Utente",
     "user_delete_delay": "L'account e gli asset dell'utente <b>{user}</b> verranno programmati per la cancellazione definitiva tra {delay, plural, one {# giorno} other {# giorni}}.",
     "user_delete_delay_settings": "Ritardo eliminazione",
     "user_delete_delay_settings_description": "Numero di giorni dopo l'eliminazione per cancellare in modo definitivo l'account e gli asset di un utente. Il processo di cancellazione dell'utente viene eseguito a mezzanotte per verificare se esistono utenti pronti a essere eliminati. Le modifiche a questa impostazioni saranno prese in considerazione dalla prossima esecuzione.",
@@ -363,22 +383,21 @@
   "all_albums": "Tutti gli album",
   "all_people": "Tutte le persone",
   "all_videos": "Tutti i video",
-  "allow_dark_mode": "Permetti tema scuro",
-  "allow_edits": "Permetti modifiche",
-  "allow_public_user_to_download": "Permetti di scaricare agli utenti pubblici",
-  "allow_public_user_to_upload": "Permetti di caricare agli utenti pubblici",
-  "anti_clockwise": "Senso antiorario",
+  "allow_dark_mode": "Permetti Tema Scuro",
+  "allow_edits": "Permetti Modifiche",
+  "allow_public_user_to_download": "Permetti agli utenti pubblici di scaricare",
+  "allow_public_user_to_upload": "Permetti agli utenti pubblici di caricare",
+  "anti_clockwise": "Senso Anti-Orario",
   "api_key": "Chiave API",
   "api_key_description": "Il campo verrà mostrato solo una volta. Abbi cura di copiarlo prima di chiudere la finestra.",
-  "api_key_empty": "Il valore del nome dell'API Key non può essere vuoto",
+  "api_key_empty": "Il Nome dell'API Key non può essere vuoto",
   "api_keys": "Chiavi API",
   "app_settings": "Impostazioni Applicazione",
   "appears_in": "Compare in",
   "archive": "Archivio",
   "archive_or_unarchive_photo": "Archivia o ripristina foto",
-  "archive_size": "Dimensioni archivio",
+  "archive_size": "Dimensioni Archivio",
   "archive_size_description": "Imposta le dimensioni dell'archivio per i download (in GiB)",
-  "archived": "Archiviato",
   "archived_count": "{count, plural, other {Archiviati #}}",
   "are_these_the_same_person": "Sono la stessa persona?",
   "are_you_sure_to_do_this": "Sei sicuro di voler procedere?",
@@ -387,9 +406,9 @@
   "asset_description_updated": "La descrizione del media non è stata aggiornata",
   "asset_filename_is_offline": "Il media {filename} è offline",
   "asset_has_unassigned_faces": "Il media ha dei volti non categorizzati",
-  "asset_hashing": "Hashing...",
-  "asset_offline": "Risorsa offline",
-  "asset_offline_description": "Il media è offline. Immich non è in grado di accedere al percorso del file. Assicurarsi che il media sia disponibile e riscansionare la libreria.",
+  "asset_hashing": "Hashing in corso ...",
+  "asset_offline": "Risorsa Offline",
+  "asset_offline_description": "Questo media non è stato trovato nel disco. Contatta il tuo amministratore di Immich per assistenza.",
   "asset_skipped": "Saltato",
   "asset_skipped_in_trash": "In cestino",
   "asset_uploaded": "Caricato",
@@ -399,11 +418,10 @@
   "assets_added_to_album_count": "{count, plural, one {# asset aggiunto} other {# asset aggiunti}} all'album",
   "assets_added_to_name_count": "Aggiunti {count, plural, one {# asset} other {# assets}} a {hasName, select, true {<b>{name}</b>} other {new album}}",
   "assets_count": "{count, plural, other {# asset}}",
-  "assets_moved_to_trash": "{count, plural, one {Spostato # asset} other {Spostati # asset}} nel cestino",
   "assets_moved_to_trash_count": "{count, plural, one {# asset spostato} other {# asset spostati}} nel cestino",
   "assets_permanently_deleted_count": "{count, plural, one {# asset cancellato} other {# asset cancellati}} definitivamente",
   "assets_removed_count": "{count, plural, one {# asset rimosso} other {# asset rimossi}}",
-  "assets_restore_confirmation": "Sei sicuro di voler ripristinare tutti gli asset cancellati? Non puoi annullare questa azione!",
+  "assets_restore_confirmation": "Sei sicuro di voler ripristinare tutti gli asset cancellati? Non puoi annullare questa azione! Tieni presente che eventuali risorse offline NON possono essere ripristinate in questo modo.",
   "assets_restored_count": "{count, plural, one {# asset ripristinato} other {# asset ripristinati}}",
   "assets_trashed_count": "{count, plural, one {Spostato # asset} other {Spostati # assets}} nel cestino",
   "assets_were_part_of_album_count": "{count, plural, one {L'asset era} other {Gli asset erano}} già parte dell'album",
@@ -414,6 +432,7 @@
   "birthdate_saved": "Data di nascita salvata con successo",
   "birthdate_set_description": "La data di nascita è usata per calcolare l'età di questa persona nel momento dello scatto della foto.",
   "blurred_background": "Sfondo sfocato",
+  "bugs_and_feature_requests": "Bug & Richieste di nuove funzionalità",
   "build": "Compilazione",
   "build_image": "Compila Immagine",
   "bulk_delete_duplicates_confirmation": "Sei sicuro di voler cancellare {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione manterrà l'asset più pesante di ogni gruppo e cancellerà permanentemente tutti gli altri duplicati. Non puoi annullare questa operazione!",
@@ -428,10 +447,6 @@
   "cannot_merge_people": "Impossibile unire le persone",
   "cannot_undo_this_action": "Non puoi annullare questa azione!",
   "cannot_update_the_description": "Impossibile aggiornare la descrizione",
-  "cant_apply_changes": "Impossibile applicare le modifiche",
-  "cant_get_faces": "Impossibile caricare i volti",
-  "cant_search_people": "Impossibile cercare le persone",
-  "cant_search_places": "Impossibile cercare i luoghi",
   "change_date": "Modifica data",
   "change_expiration_time": "Modifica tempo di scadenza",
   "change_location": "Modifica posizione",
@@ -463,6 +478,7 @@
   "confirm": "Conferma",
   "confirm_admin_password": "Conferma password amministratore",
   "confirm_delete_shared_link": "Sei sicuro di voler eliminare questo link condiviso?",
+  "confirm_keep_this_delete_others": "Tutti gli altri asset nello stack saranno eliminati, eccetto questo asset. Vuoi continuare?",
   "confirm_password": "Conferma password",
   "contain": "Adatta",
   "context": "Contesto",
@@ -510,45 +526,42 @@
   "delete_api_key_prompt": "Sei sicuro di voler eliminare questa chiave API?",
   "delete_duplicates_confirmation": "Sei sicuro di voler eliminare questi duplicati per sempre?",
   "delete_key": "Elimina chiave",
-  "delete_library": "Elimina libreria",
+  "delete_library": "Elimina Libreria",
   "delete_link": "Elimina link",
+  "delete_others": "Elimina gli altri",
   "delete_shared_link": "Elimina link condiviso",
   "delete_tag": "Elimina tag",
   "delete_tag_confirmation_prompt": "Sei sicuro di voler cancellare il tag {tagName}?",
   "delete_user": "Elimina utente",
   "deleted_shared_link": "Elimina link condiviso",
+  "deletes_missing_assets": "Cancella gli asset mancanti dal disco",
   "description": "Descrizione",
   "details": "Dettagli",
   "direction": "Direzione",
   "disabled": "Disabilitato",
   "disallow_edits": "Blocca modifiche",
+  "discord": "Discord",
   "discover": "Scopri",
   "dismiss_all_errors": "Ignora tutti gli errori",
   "dismiss_error": "Ignora errore",
   "display_options": "Impostazioni visualizzazione",
-  "display_order": "Ordine visualizzazione",
+  "display_order": "Ordine di visualizzazione",
   "display_original_photos": "Visualizza foto originali",
   "display_original_photos_setting_description": "Visualizza la foto originale anziché le miniature quando l'asset originale è compatibile con il web. Questo potrebbe causare un ritardo nella visualizzazione delle foto.",
-  "do_not_show_again": "Non mostrare questo messaggio di nuovo",
+  "do_not_show_again": "Non mostrare più questo messaggio",
+  "documentation": "Documentazione",
   "done": "Fatto",
   "download": "Scarica",
   "download_include_embedded_motion_videos": "Video incorporati",
   "download_include_embedded_motion_videos_description": "Includere i video incorporati nelle foto in movimento come file separato",
   "download_settings": "Scarica",
-  "download_settings_description": "Gestisci le impostazioni relative al download degli asset",
+  "download_settings_description": "Gestisci le impostazioni relative al download delle risorse",
   "downloading": "Scaricando",
-  "downloading_asset_filename": "Scaricando l'asset {filename}",
+  "downloading_asset_filename": "Scaricando la risorsa {filename}",
   "drop_files_to_upload": "Rilascia i file ovunque per caricarli",
   "duplicates": "Duplicati",
   "duplicates_description": "Risolvi ciascun gruppo indicando quali sono, se esistono, i duplicati",
   "duration": "Durata",
-  "durations": {
-    "days": "{days, plural, one {giorno} other {{days, number} giorni}}",
-    "hours": "{hours, plural, one {ora} other {{hours, number} ore}}",
-    "minutes": "{minutes, plural, one {minuto} other {{minutes, number} minuti}}",
-    "months": "{months, plural, one {mese} other {{months, number} mesi}}",
-    "years": "{years, plural, one {anno} other {{years, number} anni}}"
-  },
   "edit": "Modifica",
   "edit_album": "Modifica album",
   "edit_avatar": "Modifica avatar",
@@ -573,10 +586,8 @@
   "editor_crop_tool_h2_aspect_ratios": "Proporzioni",
   "editor_crop_tool_h2_rotation": "Rotazione",
   "email": "Email",
-  "empty": "",
-  "empty_album": "Album Vuoto",
   "empty_trash": "Svuota cestino",
-  "empty_trash_confirmation": "Sei sicuro di volere svuotare il cestino? Questo rimuoverà tutti gli asset nel cestino in modo permanente da Immich.\nNon puoi annullare questa azione!",
+  "empty_trash_confirmation": "Sei sicuro di volere svuotare il cestino? Questo rimuoverà tutte le risorse nel cestino in modo permanente da Immich.\nNon puoi annullare questa azione!",
   "enable": "Abilita",
   "enabled": "Abilitato",
   "end_date": "Data Fine",
@@ -584,8 +595,8 @@
   "error_loading_image": "Errore nel caricamento dell'immagine",
   "error_title": "Errore - Qualcosa è andato storto",
   "errors": {
-    "cannot_navigate_next_asset": "Impossibile passare all'asset successivo",
-    "cannot_navigate_previous_asset": "Impossibile passare all'asset precedente",
+    "cannot_navigate_next_asset": "Impossibile passare alla risorsa successiva",
+    "cannot_navigate_previous_asset": "Impossibile passare alla risorsa precedente",
     "cant_apply_changes": "Impossibile applicare le modifiche",
     "cant_change_activity": "Impossibile {enabled, select, true {disabilitare} other {abilitare}} l'attività",
     "cant_change_asset_favorite": "Impossibile cambiare il preferito per l'asset",
@@ -593,24 +604,25 @@
     "cant_get_faces": "Impossibile ottenere i volti",
     "cant_get_number_of_comments": "Impossibile ottenere il numero di commenti",
     "cant_search_people": "Impossibile cercare persone",
-    "cant_search_places": "Impossibile cercare posti",
-    "cleared_jobs": "Puliti i processi per: {job}",
-    "error_adding_assets_to_album": "Errore aggiungendo gli asset all'album",
+    "cant_search_places": "Impossibile cercare luoghi",
+    "cleared_jobs": "Eliminati i processi per: {job}",
+    "error_adding_assets_to_album": "Errore aggiungendo le risorse all'album",
     "error_adding_users_to_album": "Errore aggiungendo gli utenti all'album",
     "error_deleting_shared_user": "Errore durante la cancellazione dell'utente condiviso",
     "error_downloading": "Errore scaricando {filename}",
     "error_hiding_buy_button": "Errore nel nascondere il pulsante di acquisto",
-    "error_removing_assets_from_album": "Errore rimuovendo gli asset dall'album, controlla la console per ulteriori dettagli",
-    "error_selecting_all_assets": "Errore selezionando tutti gli asset",
-    "exclusion_pattern_already_exists": "Questo pattern di esclusione già esiste.",
+    "error_removing_assets_from_album": "Errore rimuovendo le risorse dall'album, controlla la console per ulteriori dettagli",
+    "error_selecting_all_assets": "Errore selezionando tutte le risorse",
+    "exclusion_pattern_already_exists": "Questo pattern di esclusione è già presente.",
     "failed_job_command": "Il comando {command} è fallito per il processo: {job}",
     "failed_to_create_album": "Creazione dell'album non riuscita",
-    "failed_to_create_shared_link": "Creazione del link condiviso non riuscita",
-    "failed_to_edit_shared_link": "Errore durante la modifica del link condiviso",
+    "failed_to_create_shared_link": "Creazione del link condivisibile non riuscita",
+    "failed_to_edit_shared_link": "Errore durante la modifica del link condivisibile",
     "failed_to_get_people": "Impossibile ottenere le persone",
-    "failed_to_load_asset": "Errore durante il caricamento dell'asset",
-    "failed_to_load_assets": "Errore durante il caricamento degli assets",
-    "failed_to_load_people": "Caricamento delle persone fallito",
+    "failed_to_keep_this_delete_others": "Impossibile conservare questa risorsa ed eliminare le altre risorse",
+    "failed_to_load_asset": "Errore durante il caricamento della risorsa",
+    "failed_to_load_assets": "Errore durante il caricamento delle risorse",
+    "failed_to_load_people": "Caricamento delle persone non riuscito",
     "failed_to_remove_product_key": "Rimozione del codice del prodotto fallita",
     "failed_to_stack_assets": "Errore durante il raggruppamento degli assets",
     "failed_to_unstack_assets": "Errore durante la separazione degli assets",
@@ -635,8 +647,6 @@
     "unable_to_change_location": "Impossibile modificare posizione",
     "unable_to_change_password": "Impossibile modificare password",
     "unable_to_change_visibility": "Errore durante la modifica della visibilità per {count, plural, one {# persona} other {# persone}}",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_complete_oauth_login": "Errore durante l'accesso tramite OAuth",
     "unable_to_connect": "Impossibile connettersi",
     "unable_to_connect_to_server": "Impossibile connettersi al server",
@@ -661,6 +671,7 @@
     "unable_to_get_comments_number": "Impossibile ottenere il numero di commenti",
     "unable_to_get_shared_link": "Impossibile ottenere il link condiviso",
     "unable_to_hide_person": "Impossibile nascondere persona",
+    "unable_to_link_motion_video": "Impossibile collegare video in movimento",
     "unable_to_link_oauth_account": "Impossibile collegare l'account OAuth",
     "unable_to_load_album": "Impossibile caricare l'album",
     "unable_to_load_asset_activity": "Impossibile caricare l'attività dell'asset",
@@ -676,12 +687,10 @@
     "unable_to_remove_album_users": "Impossibile rimuovere gli utenti dall'album",
     "unable_to_remove_api_key": "Impossibile rimuovere la chiave API",
     "unable_to_remove_assets_from_shared_link": "Errore durante la rimozione degli assets da un link condiviso",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_deleted_assets": "Impossibile rimuovere i file offline",
     "unable_to_remove_library": "Impossibile rimuovere libreria",
-    "unable_to_remove_offline_files": "Impossibile rimuovere i file offline",
     "unable_to_remove_partner": "Impossibile rimuovere compagno",
     "unable_to_remove_reaction": "Impossibile rimuovere reazione",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "Impossibile riparare elementi",
     "unable_to_reset_password": "Impossibile reimpostare la password",
     "unable_to_resolve_duplicate": "Impossibile risolvere duplicato",
@@ -701,6 +710,7 @@
     "unable_to_submit_job": "Impossibile eseguire l'attività",
     "unable_to_trash_asset": "Impossibile cestinare l'asset",
     "unable_to_unlink_account": "Impossibile scollegare l'account",
+    "unable_to_unlink_motion_video": "Impossibile scollegare video in movimento",
     "unable_to_update_album_cover": "Errore durante l'aggiornamento della copertina dell'album",
     "unable_to_update_album_info": "Impossibile aggiornare le informazioni sull'album",
     "unable_to_update_library": "Impossibile aggiornare la libreria",
@@ -710,10 +720,6 @@
     "unable_to_update_user": "Impossibile aggiornare l'utente",
     "unable_to_upload_file": "Impossibile caricare il file"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exif": "Exif",
   "exit_slideshow": "Esci dalla presentazione",
   "expand_all": "Espandi tutto",
@@ -728,33 +734,28 @@
   "external": "Esterno",
   "external_libraries": "Librerie esterne",
   "face_unassigned": "Non assegnata",
-  "failed_to_get_people": "Impossibile recuperare persone",
+  "failed_to_load_assets": "Impossibile caricare gli asset",
   "favorite": "Preferito",
   "favorite_or_unfavorite_photo": "Aggiungi o rimuovi foto da preferiti",
   "favorites": "Preferiti",
-  "feature": "",
   "feature_photo_updated": "Foto in evidenza aggiornata",
-  "featurecollection": "",
   "features": "Funzionalità",
   "features_setting_description": "Gestisci le funzionalità dell'app",
   "file_name": "Nome file",
   "file_name_or_extension": "Nome file o estensione",
   "filename": "Nome file",
-  "files": "",
   "filetype": "Tipo file",
   "filter_people": "Filtra persone",
   "find_them_fast": "Trovale velocemente con la ricerca",
   "fix_incorrect_match": "Correggi corrispondenza errata",
   "folders": "Cartelle",
   "folders_feature_description": "Navigare la visualizzazione a cartelle per le foto e i video sul file system",
-  "force_re-scan_library_files": "Forza nuova scansione di tutti i file della libreria",
   "forward": "Avanti",
   "general": "Generale",
   "get_help": "Chiedi Aiuto",
   "getting_started": "Iniziamo",
   "go_back": "Torna indietro",
   "go_to_search": "Vai alla ricerca",
-  "go_to_share_page": "Vai alla pagina condivisione",
   "group_albums_by": "Raggruppa album in base a...",
   "group_no": "Nessun raggruppamento",
   "group_owner": "Raggruppa in base al proprietario",
@@ -780,10 +781,6 @@
   "image_alt_text_date_place_2_people": "{isVideo, select, true {Video girato} other {Foto scattata}} a {city}, {country} con {person1} e {person2} il giorno {date}",
   "image_alt_text_date_place_3_people": "{isVideo, select, true {Video girato} other {Foto scattata}} a {city}, {country} con {person1}, {person2}, e {person3} il giorno {date}",
   "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video girato} other {Foto scattata}} a {city}, {country} con {person1}, {person2} e {additionalCount, number} altre persone il {date}",
-  "image_alt_text_people": "{count, plural, =1 {con {person1}} =2 {con {person1} e {person2}} =3 {con {person1}, {person2} e {person3}} other {con {person1}, {person2} e {others, number} altri}}",
-  "image_alt_text_place": "a {city}, {country}",
-  "image_taken": "{isVideo, select, true {Video registrato} other {Immagine scattata}}",
-  "img": "",
   "immich_logo": "Logo Immich",
   "immich_web_interface": "Interfaccia Web Immich",
   "import_from_json": "Importa da JSON",
@@ -804,10 +801,11 @@
   "invite_people": "Invita Persone",
   "invite_to_album": "Invita nell'album",
   "items_count": "{count, plural, one {# elemento} other {# elementi}}",
-  "job_settings_description": "",
   "jobs": "Processi",
   "keep": "Mantieni",
   "keep_all": "Tieni tutto",
+  "keep_this_delete_others": "Tieni questo, elimina gli altri",
+  "kept_this_deleted_others": "Mantenuto questo asset ed eliminati {count, plural, one {# asset} other {# assets}}",
   "keyboard_shortcuts": "Scorciatoie da tastiera",
   "language": "Lingua",
   "language_setting_description": "Seleziona la tua lingua predefinita",
@@ -819,31 +817,6 @@
   "level": "Livello",
   "library": "Libreria",
   "library_options": "Impostazioni Libreria",
-  "license_account_info": "Il tuo account è attivo",
-  "license_activated_subtitle": "Grazie per supportare Immich e il software open-source",
-  "license_activated_title": "La tua licenza è stata attivata con successo",
-  "license_button_activate": "Attiva",
-  "license_button_buy": "Sborsa",
-  "license_button_buy_license": "Sborsa per la Licenza",
-  "license_button_select": "Seleziona",
-  "license_failed_activation": "Attivazione licenza fallita. Per favore controlla la tua email per la chiave di licenza corretta!",
-  "license_individual_description_1": "1 licenza per utente su qualsiasi server",
-  "license_individual_title": "Licenza Individuale",
-  "license_info_licensed": "Con Licenza",
-  "license_info_unlicensed": "Senza Licenza",
-  "license_input_suggestion": "Hai una licenza? Inserisci la chiave qua sotto",
-  "license_license_subtitle": "Sborsa per una licenza per sopportare Immich",
-  "license_license_title": "LICENZA",
-  "license_lifetime_description": "Licenza Lifetime",
-  "license_per_server": "Per server",
-  "license_per_user": "Per utente",
-  "license_server_description_1": "1 licenza per server",
-  "license_server_description_2": "Licenza per tutti gli utenti sul server",
-  "license_server_title": "Licenza Server",
-  "license_trial_info_1": "Stai eseguendo una versione di Immich senza licenza",
-  "license_trial_info_2": "Stai usando Immich basatamente da circa",
-  "license_trial_info_3": "{accountAge, plural, one {# day} other {# days}}",
-  "license_trial_info_4": "Per favore considera sborsare soldi per una licenza e per sopportare il continuo sviluppo del servizio",
   "light": "Chiaro",
   "like_deleted": "Mi piace rimosso",
   "link_motion_video": "Collega video in movimento",
@@ -865,6 +838,7 @@
   "look": "Guarda",
   "loop_videos": "Riproduci video in loop",
   "loop_videos_description": "Abilita per riprodurre automaticamente un video in loop nella vista dettagli.",
+  "main_branch_warning": "Stai usando una versione di sviluppo. Consigliamo vivamente di utilizzare una versione di rilascio!",
   "make": "Produttore",
   "manage_shared_links": "Gestisci link condivisi",
   "manage_sharing_with_partners": "Gestisci la condivisione con i compagni",
@@ -934,6 +908,7 @@
   "notifications": "Notifiche",
   "notifications_setting_description": "Gestisci notifiche",
   "oauth": "OAuth",
+  "official_immich_resources": "Risorse Ufficiali Immich",
   "offline": "Offline",
   "offline_paths": "Percorsi offline",
   "offline_paths_description": "Questi risultati potrebbero essere causati dall'eliminazione manuale di file che non fanno parte di una libreria esterna.",
@@ -946,7 +921,6 @@
   "onboarding_welcome_user": "Benvenuto, {user}",
   "online": "Online",
   "only_favorites": "Solo preferiti",
-  "only_refreshes_modified_files": "Aggiorna solo i file modificati",
   "open_in_map_view": "Apri nella visualizzazione mappa",
   "open_in_openstreetmap": "Apri su OpenStreetMap",
   "open_the_search_filters": "Apri filtri di ricerca",
@@ -984,29 +958,27 @@
   "people_edits_count": "{count, plural, one {Modificata # persona} other {Modificate # persone}}",
   "people_feature_description": "Navigare foto e video raggruppati da persone",
   "people_sidebar_description": "Mostra un link alle persone nella barra laterale",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "Avviso eliminazione permanente",
   "permanent_deletion_warning_setting_description": "Mostra un avviso all'eliminazione definitiva di un asset",
   "permanently_delete": "Elimina definitivamente",
   "permanently_delete_assets_count": "Cancella definitivamente {count, plural, one {l'asset} other {gli assets}}",
   "permanently_delete_assets_prompt": "Sei sicuro di voler cancellare definitivamente {count, plural, one {questo asset?} other {<b>#</b> assets?}} Questa operazione {count, plural, one {lo cancellerà dal suo} other {li cancellerà dai loro}} album.",
-  "permanently_deleted_asset": "Elimina asset definitivamente",
+  "permanently_deleted_asset": "Asset eliminato definitivamente",
   "permanently_deleted_assets_count": "Cancellati {count, plural, one {# asset} other {# assets}} definitivamente",
   "person": "Persona",
   "person_hidden": "{name}{hidden, select, true { (nascosto)} other {}}",
-  "photo_shared_all_users": "Sembra che tu abbia condiviso le foto con tutti gli utenti (oppure non hai utenti con cui condividerle).",
+  "photo_shared_all_users": "Sembra che tu abbia condiviso le foto con tutti gli utenti, oppure che non ci siano utenti con i quali condividerle.",
   "photos": "Foto",
   "photos_and_videos": "Foto & Video",
   "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Foto}}",
   "photos_from_previous_years": "Foto degli anni scorsi",
   "pick_a_location": "Scegli una posizione",
   "place": "Posizione",
-  "places": "Location",
+  "places": "Luoghi",
   "play": "Avvia",
   "play_memories": "Avvia ricordi",
   "play_motion_photo": "Avvia Foto in movimento",
   "play_or_pause_video": "Avvia o metti in pausa il video",
-  "point": "",
   "port": "Porta",
   "preset": "Preimpostazione",
   "preview": "Anteprima",
@@ -1051,12 +1023,10 @@
   "purchase_server_description_2": "Stato di Contributore",
   "purchase_server_title": "Server",
   "purchase_settings_server_activated": "La chiave del prodotto del server è gestita dall'amministratore",
-  "range": "",
   "rating": "Valutazione a stelle",
   "rating_clear": "Crea valutazione",
   "rating_count": "{count, plural, one {# stella} other {# stelle}}",
   "rating_description": "Visualizza la valutazione EXIF nel pannello informazioni",
-  "raw": "",
   "reaction_options": "Impostazioni Reazioni",
   "read_changelog": "Leggi Riepilogo Modifiche",
   "reassign": "Riassegna",
@@ -1064,14 +1034,17 @@
   "reassigned_assets_to_new_person": "{count, plural, one {Riassegnato # asset} other {Riassegnati # assets}} ad una nuova persona",
   "reassing_hint": "Assegna gli assets selezionati ad una persona esistente",
   "recent": "Recenti",
+  "recent-albums": "Album recenti",
   "recent_searches": "Ricerche recenti",
   "refresh": "Aggiorna",
   "refresh_encoded_videos": "Ricarica video codificati",
+  "refresh_faces": "Aggiorna facce",
   "refresh_metadata": "Ricarica metadati",
   "refresh_thumbnails": "Ricarica anteprime",
   "refreshed": "Aggiornato",
-  "refreshes_every_file": "Aggiorna ogni file",
+  "refreshes_every_file": "Rilegge tutti i file esistenti e nuovi",
   "refreshing_encoded_video": "Ricaricando il video codificato",
+  "refreshing_faces": "Aggiorna Facce",
   "refreshing_metadata": "Ricaricando i metadati",
   "regenerating_thumbnails": "Rigenerando le anteprime",
   "remove": "Rimuovi",
@@ -1079,10 +1052,11 @@
   "remove_assets_shared_link_confirmation": "Sei sicuro di voler rimuovere {count, plural, one {# asset} other {# asset}} da questo link condiviso?",
   "remove_assets_title": "Rimuovere asset?",
   "remove_custom_date_range": "Rimuovi intervallo data personalizzato",
+  "remove_deleted_assets": "Rimuovi file offline",
   "remove_from_album": "Rimuovere dall'album",
   "remove_from_favorites": "Rimuovi dai preferiti",
   "remove_from_shared_link": "Rimuovi dal link condiviso",
-  "remove_offline_files": "Rimuovi file offline",
+  "remove_url": "Rimuovi URL",
   "remove_user": "Rimuovi utente",
   "removed_api_key": "Rimossa chiave API: {name}",
   "removed_from_archive": "Rimosso dall'archivio",
@@ -1099,7 +1073,6 @@
   "reset": "Ripristina",
   "reset_password": "Ripristina password",
   "reset_people_visibility": "Ripristina visibilità persone",
-  "reset_settings_to_default": "",
   "reset_to_default": "Ripristina i valori predefiniti",
   "resolve_duplicates": "Risolvi duplicati",
   "resolved_all_duplicates": "Tutti i duplicati sono stati risolti",
@@ -1119,8 +1092,7 @@
   "saved_settings": "Impostazioni salvate",
   "say_something": "Dici qualcosa",
   "scan_all_libraries": "Analizza tutte le librerie",
-  "scan_all_library_files": "Scansiona nuovamente tutti i file della libreria",
-  "scan_new_library_files": "Analizza i File Nuovi della Libreria",
+  "scan_library": "Scansione",
   "scan_settings": "Impostazioni Analisi",
   "scanning_for_album": "Sto cercando l'album...",
   "search": "Cerca",
@@ -1138,6 +1110,7 @@
   "search_options": "Opzioni Ricerca",
   "search_people": "Cerca persone",
   "search_places": "Cerca luoghi",
+  "search_settings": "Cerca Impostazioni",
   "search_state": "Cerca stato...",
   "search_tags": "Cerca tag...",
   "search_timezone": "Cerca fuso orario...",
@@ -1162,7 +1135,6 @@
   "selected_count": "{count, plural, one {# selezionato} other {# selezionati}}",
   "send_message": "Manda messaggio",
   "send_welcome_email": "Invia email di benvenuto",
-  "server": "Server",
   "server_offline": "Server Offline",
   "server_online": "Server Online",
   "server_stats": "Statistiche Server",
@@ -1205,6 +1177,7 @@
   "show_person_options": "Mostra opzioni persona",
   "show_progress_bar": "Mostra Barra Avanzamento",
   "show_search_options": "Mostra impostazioni di ricerca",
+  "show_slideshow_transition": "Mostra la transizione della presentazione",
   "show_supporter_badge": "Medaglia di Contributore",
   "show_supporter_badge_description": "Mostra la medaglia di contributore",
   "shuffle": "Casuale",
@@ -1246,13 +1219,16 @@
   "submit": "Invia",
   "suggestions": "Suggerimenti",
   "sunrise_on_the_beach": "Tramonto sulla spiaggia",
+  "support": "Supporto",
+  "support_and_feedback": "Supporto & Feedback",
+  "support_third_party_description": "La tua installazione di Immich è stata costruita da terze parti. I problemi che riscontri potrebbero essere causati da altri pacchetti, quindi ti preghiamo di sollevare il problema in prima istanza utilizzando i link sottostanti.",
   "swap_merge_direction": "Scambia direzione di unione",
   "sync": "Sincronizza",
   "tag": "Tag",
   "tag_assets": "Tagga risorse",
   "tag_created": "Tag creata: {tag}",
   "tag_feature_description": "Navigazione foto e video raggruppati per argomenti tag logici",
-  "tag_not_found_question": "Non riesci a trovare una tag? Creane una <link>qui</link>",
+  "tag_not_found_question": "Non riesci a trovare un tag? <link>Creane uno nuovo.</link>",
   "tag_updated": "Tag {tag} aggiornata",
   "tagged_assets": "{count, plural, one {# asset etichettato} other {# asset etichettati}}",
   "tags": "Tag",
@@ -1261,18 +1237,19 @@
   "theme_selection": "Selezione tema",
   "theme_selection_description": "Imposta automaticamente il tema chiaro o scuro in base all'impostazione del tuo browser",
   "they_will_be_merged_together": "Verranno uniti insieme",
+  "third_party_resources": "Risorse di Terze Parti",
   "time_based_memories": "Ricordi basati sul tempo",
+  "timeline": "Linea temporale",
   "timezone": "Fuso orario",
   "to_archive": "Archivio",
   "to_change_password": "Modifica password",
   "to_favorite": "Preferito",
   "to_login": "Login",
   "to_parent": "Sali di un livello",
-  "to_root": "Alla radice",
   "to_trash": "Cancella",
   "toggle_settings": "Attiva/disattiva impostazioni",
   "toggle_theme": "Abilita tema scuro",
-  "toggle_visibility": "Cambia visibilità",
+  "total": "Totale",
   "total_usage": "Utilizzo totale",
   "trash": "Cestino",
   "trash_all": "Cestina Tutto",
@@ -1282,14 +1259,13 @@
   "trashed_items_will_be_permanently_deleted_after": "Gli elementi cestinati saranno eliminati definitivamente dopo {days, plural, one {# giorno} other {# giorni}}.",
   "type": "Tipo",
   "unarchive": "Annulla l'archiviazione",
-  "unarchived": "Rimosso dall'archivio",
   "unarchived_count": "{count, plural, other {Non archiviati #}}",
   "unfavorite": "Rimuovi preferito",
   "unhide_person": "Mostra persona",
   "unknown": "Sconosciuto",
-  "unknown_album": "Album sconosciuto",
   "unknown_year": "Anno sconosciuto",
   "unlimited": "Illimitato",
+  "unlink_motion_video": "Scollega video in movimento",
   "unlink_oauth": "Scollega OAuth",
   "unlinked_oauth_account": "Scollega account OAuth",
   "unnamed_album": "Album senza nome",
@@ -1318,13 +1294,13 @@
   "use_custom_date_range": "Altrimenti utilizza un intervallo date personalizzato",
   "user": "Utente",
   "user_id": "ID utente",
-  "user_license_settings": "Licenza",
-  "user_license_settings_description": "Gestisci la tua licenza",
   "user_liked": "A {user} piace {type, select, photo {questa foto} video {questo video} asset {questo asset} other {questo elemento}}",
   "user_purchase_settings": "Acquisto",
   "user_purchase_settings_description": "Gestisci il tuo acquisto",
   "user_role_set": "Imposta {user} come {role}",
   "user_usage_detail": "Dettagli utilizzo utente",
+  "user_usage_stats": "Statistiche d'uso",
+  "user_usage_stats_description": "Consulta le statistiche d'uso dell'account",
   "username": "Nome utente",
   "users": "Utenti",
   "utilities": "Utilità",
@@ -1332,7 +1308,9 @@
   "variables": "Variabili",
   "version": "Versione",
   "version_announcement_closing": "Il tuo amico, Alex",
-  "version_announcement_message": "Ehilà! È stata rilasciata una nuova versione dell'applicazione. Leggi le <link>note di rilascio</link> e assicurati che i tuoi file <code>docker-compose.yml</code>/<code>.env</code> siano aggiornati per evitare problemi e incongruenze, soprattutto se utilizzi WatchTower o altri strumenti per aggiornare l'applicazione in automatico.",
+  "version_announcement_message": "Ehilà! È stata rilasciata una nuova versione di Immich. Leggi le <link>note di rilascio</link> e assicurati che i tuoi file <code>docker-compose.yml</code>/<code>.env</code> siano aggiornati per evitare problemi e incongruenze, soprattutto se utilizzi WatchTower o altri strumenti per aggiornare Immich in automatico.",
+  "version_history": "Storico delle Versioni",
+  "version_history_item": "Versione installata {version} il {date}",
   "video": "Video",
   "video_hover_setting": "Riproduci l'anteprima del video al passaggio del mouse",
   "video_hover_setting_description": "Riproduci miniatura video quando il mouse passa sopra l'elemento. Anche se disabilitato, la riproduzione può essere avviata passando con il mouse sopra l'icona riproduci.",
@@ -1344,16 +1322,16 @@
   "view_all_users": "Visualizza tutti gli utenti",
   "view_in_timeline": "Visualizza in timeline",
   "view_links": "Visualizza i link",
+  "view_name": "Visualizza",
   "view_next_asset": "Visualizza risorsa successiva",
   "view_previous_asset": "Visualizza risorsa precedente",
   "view_stack": "Visualizza Raggruppamento",
-  "viewer": "Visualizzatore",
   "visibility_changed": "Visibilità modificata per {count, plural, one {# persona} other {# persone}}",
   "waiting": "In Attesa",
   "warning": "Attenzione",
   "week": "Settimana",
   "welcome": "Benvenuto",
-  "welcome_to_immich": "Benvenuto in immich",
+  "welcome_to_immich": "Benvenuto in Immich",
   "year": "Anno",
   "years_ago": "{years, plural, one {# anno} other {# anni}} fa",
   "yes": "Si",
diff --git a/web/src/lib/i18n/ja.json b/i18n/ja.json
similarity index 95%
rename from web/src/lib/i18n/ja.json
rename to i18n/ja.json
index 017d52fb30..0eb2c880f1 100644
--- a/web/src/lib/i18n/ja.json
+++ b/i18n/ja.json
@@ -41,9 +41,7 @@
     "confirm_email_below": "確認のため、以下に \"{email}\" と入力してください",
     "confirm_reprocess_all_faces": "本当にすべての顔を再処理しますか? これにより名前が付けられた人物も消去されます。",
     "confirm_user_password_reset": "本当に {user} のパスワードをリセットしますか?",
-    "crontab_guru": "Crontab Guru",
     "disable_login": "ログインを無効にする",
-    "disabled": "",
     "duplicate_detection_job_description": "機械学習を用いて類似画像の検出を行います。(スマートサーチに依存)",
     "exclusion_pattern_description": "除外パターンを使用すると、ライブラリをスキャンする際にファイルやフォルダを無視することができます。RAWファイルなど、インポートしたくないファイルを含むフォルダがある場合に便利です。",
     "external_library_created_at": "外部ライブラリ(作成日:{date})",
@@ -59,16 +57,9 @@
     "image_prefer_embedded_preview_setting_description": "RAW写真の埋め込みプレビューが利用可能な場合に画像処理の入力として使用します。これにより、いくつかの画像でより正確な色を得ることができますが、プレビューの品質はカメラによって異なり、画像により多くの圧縮アーティファクトが含まれる場合があります。",
     "image_prefer_wide_gamut": "広色域に対応させる",
     "image_prefer_wide_gamut_setting_description": "サムネイルにはDisplay P3を使用します。これにより、広色域の画像の鮮やかさをよりよく保つことができますが、古いデバイスや古いブラウザバージョンでは画像が異なって見える場合があります。sRGBの画像は、色の変化を避けるためにsRGBのままにします。",
-    "image_preview_format": "プレビューのファイル形式",
-    "image_preview_resolution": "プレビュー解像度",
-    "image_preview_resolution_description": "単一写真のプレビューや機械学習で使用する解像度を設定します。解像度を高くすると細かなディテールを保持できますが、エンコードに時間がかかり、ファイルサイズが大きくなり、アプリの応答性が低下する可能性があります。",
     "image_quality": "品質",
-    "image_quality_description": "画像の品質を1から100の範囲で設定します。数値が高いほど品質が良くなりますが、ファイルサイズも大きくなります。このオプションは、プレビュー画像とサムネイル画像に影響します。",
     "image_settings": "画像設定",
     "image_settings_description": "生成される画像の品質と解像度の設定",
-    "image_thumbnail_format": "サムネイルフォーマット",
-    "image_thumbnail_resolution": "サムネイル解像度",
-    "image_thumbnail_resolution_description": "複数の写真を閲覧する際(タイムライン、アルバムビューなど)に使用されます。解像度を高くすると細かなディテールを保持できますが、エンコードに時間がかかり、ファイルサイズが大きくなり、アプリの応答性が低下する可能性があります。",
     "job_concurrency": "{job} の同時実行数",
     "job_not_concurrency_safe": "このジョブは安全に同時実行できません。",
     "job_settings": "ジョブ設定",
@@ -77,9 +68,6 @@
     "jobs_delayed": "{jobCount, plural, other {#件}}の遅延",
     "jobs_failed": "{jobCount, plural, other {#件}}の失敗",
     "library_created": "作成されたライブラリ:{library}",
-    "library_cron_expression": "Cron表記",
-    "library_cron_expression_description": "cron形式を使用してスキャン間隔を設定します。 詳細については、<link>Crontab Guru</link> などを参照してください",
-    "library_cron_expression_presets": "Cron表記プリセット",
     "library_deleted": "ライブラリは削除されました",
     "library_import_path_description": "インポートするフォルダを指定します。このフォルダはサブフォルダを含めて、画像と動画のスキャンが行われます。",
     "library_scanning": "定期スキャン",
@@ -148,7 +136,7 @@
     "note_cannot_be_changed_later": "注意: 後から変更できません!",
     "note_unlimited_quota": "注意: 無制限にする場合は0を入力してください",
     "notification_email_from_address": "送信メールアドレス",
-    "notification_email_from_address_description": "送信メールアドレスを設定します(例: \"Immich Photo Server <noreply@immich.app>\" )",
+    "notification_email_from_address_description": "送信メールアドレスを設定します(例: \"Immich Photo Server <noreply@example.com>\" )",
     "notification_email_host_description": "送信メールサーバーを設定します(例:smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "証明書エラーを無視",
     "notification_email_ignore_certificate_errors_description": "TLS証明書の検証エラーを無視します(非推奨)",
@@ -198,15 +186,12 @@
     "refreshing_all_libraries": "すべてのライブラリを更新",
     "registration": "管理者登録",
     "registration_description": "あなたはシステムの最初のユーザーであるため、管理者として割り当てられ、管理タスクを担当し、追加のユーザーはあなたによって作成されます。",
-    "removing_offline_files": "オフライン ファイルを削除します",
     "repair_all": "すべてを修復",
     "repair_matched_items": "一致: {count, plural, one {#件} other {#件}}",
     "repaired_items": "修復済み: {count, plural, one {#件} other {#件}}",
     "require_password_change_on_login": "初回ログイン時にパスワード変更を要求する",
     "reset_settings_to_default": "設定をデフォルトにリセットします",
     "reset_settings_to_recent_saved": "前回の設定値に戻す",
-    "scanning_library_for_changed_files": "変更されたファイルを検出するためにライブラリをスキャン中",
-    "scanning_library_for_new_files": "新しいファイルを検出するためにライブラリをスキャン中",
     "send_welcome_email": "ウェルカム メール を送信します",
     "server_external_domain_settings": "外部ドメイン",
     "server_external_domain_settings_description": "公開共有リンク用のドメイン( http(s):// を含める)",
@@ -241,7 +226,6 @@
     "these_files_matched_by_checksum": "これらのファイルはチェックサムによって照合されます",
     "thumbnail_generation_job": "サムネイル生成",
     "thumbnail_generation_job_description": "各アセットのサムネイル(大、小、ぼかし)と、各人物のサムネイルを生成します",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "アクセラレーション API",
     "transcoding_acceleration_api_description": "デバイスでハードウェアトランスコードを行うためのAPIです。この設定は『ベストエフォート』であり、失敗した場合はソフトウェアトランスコードになります。VP9はハードウェアによって機能する場合としない場合があります。",
     "transcoding_acceleration_nvenc": "NVEnc(NVIDIA GPUが必要)",
@@ -293,8 +277,6 @@
     "transcoding_threads_description": "値を高くするとエンコード速度が速くなりますが、アクティブな間はサーバーが他のタスクを処理する余裕が少なくなります。この値はCPUのコア数を超えないようにする必要があります。\"0\" に設定すると、最大限利用されます。",
     "transcoding_tone_mapping": "トーンマッピング",
     "transcoding_tone_mapping_description": "HDR動画をSDRに変換する際に見た目を維持しようと試みます。各アルゴリズムは、色、詳細、明るさに対して異なるトレードオフを行います。Hableは詳細を維持し、Mobiusは色を維持し、Reinhardは明るさを維持します。",
-    "transcoding_tone_mapping_npl": "トーンマッピング NPL",
-    "transcoding_tone_mapping_npl_description": "この明るさの表示で正常に見えるように色が調整されます。直観に反しますが、値を低くするとディスプレイの明るさが補正されてビデオの明るさが増加し、その逆も同様です。0にするとこの値は自動で設定されます。",
     "transcoding_transcode_policy": "トランスコードポリシー",
     "transcoding_transcode_policy_description": "動画がトランスコードされるべきかを決めるポリシー。HDR動画は常にトランスコードされます(トランスコードが無効化されている場合を除く)。",
     "transcoding_two_pass_encoding": "Two-passエンコード",
@@ -374,7 +356,6 @@
   "archive_or_unarchive_photo": "写真をアーカイブまたはアーカイブ解除",
   "archive_size": "アーカイブサイズ",
   "archive_size_description": "ダウンロードのアーカイブ サイズを設定(GiB 単位)",
-  "archived": "",
   "archived_count": "アーカイブされた{count, plural, other {#個の項目}}",
   "are_these_the_same_person": "これらは同じ人物ですか?",
   "are_you_sure_to_do_this": "本当にこれを行いますか?",
@@ -422,10 +403,6 @@
   "cannot_merge_people": "人物を統合できません",
   "cannot_undo_this_action": "この操作は元に戻せません!",
   "cannot_update_the_description": "説明を更新できません",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
   "change_date": "日時を変更",
   "change_expiration_time": "有効期限を変更",
   "change_location": "場所を変更",
@@ -536,13 +513,6 @@
   "duplicates": "重複",
   "duplicates_description": "もしあれば、重複しているグループを示すことで解決します",
   "duration": "間隔",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
   "edit": "編集",
   "edit_album": "アルバムを編集",
   "edit_avatar": "アバターを編集",
@@ -567,8 +537,6 @@
   "editor_crop_tool_h2_aspect_ratios": "アスペクト比",
   "editor_crop_tool_h2_rotation": "回転",
   "email": "メールアドレス",
-  "empty": "",
-  "empty_album": "",
   "empty_trash": "コミ箱を空にする",
   "empty_trash_confirmation": "本当にゴミ箱を空にしますか? これにより、ゴミ箱内のすべてのアセットが Immich から永久に削除されます。\nこの操作を元に戻すことはできません!",
   "enable": "有効化",
@@ -629,8 +597,6 @@
     "unable_to_change_location": "場所を変更できません",
     "unable_to_change_password": "パスワードを変更できません",
     "unable_to_change_visibility": "{count, plural, one {#人} other {#人}}の人物の非表示設定を変更できません",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_complete_oauth_login": "OAuth ログインを完了できません",
     "unable_to_connect": "接続できません",
     "unable_to_connect_to_server": "サーバーに接続できません",
@@ -670,12 +636,10 @@
     "unable_to_remove_album_users": "アルバムからユーザーを削除できません",
     "unable_to_remove_api_key": "API キーを削除できません",
     "unable_to_remove_assets_from_shared_link": "共有リンクからアセットを削除できません",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_deleted_assets": "オフラインのファイルを削除できません",
     "unable_to_remove_library": "ライブラリを削除できません",
-    "unable_to_remove_offline_files": "オフラインのファイルを削除できません",
     "unable_to_remove_partner": "パートナーを削除できません",
     "unable_to_remove_reaction": "リアクションを削除できません",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "アイテムを修復できません",
     "unable_to_reset_password": "パスワードをリセットできません",
     "unable_to_resolve_duplicate": "重複を解決できません",
@@ -704,10 +668,6 @@
     "unable_to_update_user": "ユーザーを更新できません",
     "unable_to_upload_file": "ファイルをアップロードできません"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exif": "Exif",
   "exit_slideshow": "スライドショーを終わる",
   "expand_all": "全て展開",
@@ -722,33 +682,27 @@
   "external": "外部",
   "external_libraries": "外部ライブラリ",
   "face_unassigned": "未割り当て",
-  "failed_to_get_people": "",
   "favorite": "お気に入り",
   "favorite_or_unfavorite_photo": "写真をお気に入りまたはお気に入り解除",
   "favorites": "お気に入り",
-  "feature": "",
   "feature_photo_updated": "人物画像が更新されました",
-  "featurecollection": "",
   "features": "機能",
   "features_setting_description": "アプリの機能を管理する",
   "file_name": "ファイル名",
   "file_name_or_extension": "ファイル名または拡張子",
   "filename": "ファイル名",
-  "files": "",
   "filetype": "ファイルタイプ",
   "filter_people": "人物を絞り込み",
   "find_them_fast": "名前で検索して素早く発見",
   "fix_incorrect_match": "間違った一致を修正",
   "folders": "フォルダ",
   "folders_feature_description": "ファイルシステム上の写真と動画のフォルダビューを閲覧する",
-  "force_re-scan_library_files": "強制的に全てのライブラリのファイルを再スキャン",
   "forward": "前へ",
   "general": "一般",
   "get_help": "助けを求める",
   "getting_started": "はじめる",
   "go_back": "戻る",
   "go_to_search": "検索へ",
-  "go_to_share_page": "共有ページへ",
   "group_albums_by": "これでアルバムをグループ化…",
   "group_no": "グループ化なし",
   "group_owner": "所有者でグループ化",
@@ -774,9 +728,6 @@
   "image_alt_text_date_place_2_people": "{date}の、{country}、{city}での{person1}と{person2}の{isVideo, select, true {動画} other {画像}}",
   "image_alt_text_date_place_3_people": "{date}の、{country}、{city}での{person1}と{person2}、そして{person3}の{isVideo, select, true {動画} other {画像}}",
   "image_alt_text_date_place_4_or_more_people": "{date}の、{country}、{city}での{person1}と{person2}、そしてその他{additionalCount, number}人の{isVideo, select, true {動画} other {画像}}",
-  "image_alt_text_place": "{country} {city}で撮影",
-  "image_taken": "{isVideo, select, true {動画は} other {写真は}}",
-  "img": "",
   "immich_logo": "Immich ロゴ",
   "immich_web_interface": "Immich Webインターフェース",
   "import_from_json": "JSONからインポート",
@@ -797,7 +748,6 @@
   "invite_people": "人々を招待",
   "invite_to_album": "アルバムに招待",
   "items_count": "{count, plural, one {#個} other {#個}}の項目",
-  "job_settings_description": "",
   "jobs": "ジョブ",
   "keep": "保持",
   "keep_all": "全て保持",
@@ -913,7 +863,6 @@
   "onboarding_welcome_user": "ようこそ、{user} さん",
   "online": "オンライン",
   "only_favorites": "お気に入りのみ",
-  "only_refreshes_modified_files": "変更されたファイルのみを更新します",
   "open_in_map_view": "地図表示で見る",
   "open_in_openstreetmap": "OpenStreetMapで開く",
   "open_the_search_filters": "検索フィルタを開く",
@@ -951,7 +900,6 @@
   "people_edits_count": "{count, plural, one {#人} other {#人}}が編集済",
   "people_feature_description": "人物でグループ化された写真と動画を閲覧する",
   "people_sidebar_description": "人物へのリンクをサイドバーに表示",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "永久削除の警告",
   "permanent_deletion_warning_setting_description": "アセットを完全に削除するときに警告を表示する",
   "permanently_delete": "完全に削除",
@@ -973,7 +921,6 @@
   "play_memories": "メモリーを再生",
   "play_motion_photo": "モーションビデオを再生",
   "play_or_pause_video": "動画を再生または一時停止",
-  "point": "",
   "port": "ポートレート",
   "preset": "プリセット",
   "preview": "プレビュー",
@@ -1018,12 +965,10 @@
   "purchase_server_description_2": "サポーターの状態",
   "purchase_server_title": "サーバー",
   "purchase_settings_server_activated": "サーバーのプロダクトキーは管理者に管理されています",
-  "range": "",
   "rating": "星での評価",
   "rating_clear": "評価を取り消す",
   "rating_count": "星{count, plural, one {#つ} other {#つ}}",
   "rating_description": "情報欄にEXIFの評価を表示",
-  "raw": "",
   "reaction_options": "リアクションの選択",
   "read_changelog": "変更履歴を読む",
   "reassign": "再割り当て",
@@ -1046,10 +991,10 @@
   "remove_assets_shared_link_confirmation": "本当にこの共有リンクから{count, plural, one {#個} other {#個}}のアセットを削除しますか?",
   "remove_assets_title": "アセットを削除しますか?",
   "remove_custom_date_range": "カスタム日付範囲を削除",
+  "remove_deleted_assets": "オフラインのファイルを削除",
   "remove_from_album": "アルバムから削除",
   "remove_from_favorites": "お気に入りから削除",
   "remove_from_shared_link": "共有リンクから削除",
-  "remove_offline_files": "オフラインのファイルを削除",
   "remove_user": "ユーザーを削除",
   "removed_api_key": "削除されたAPI キー: {name}",
   "removed_from_archive": "アーカイブから削除されました",
@@ -1066,7 +1011,6 @@
   "reset": "リセット",
   "reset_password": "パスワードをリセット",
   "reset_people_visibility": "人物の非表示設定をリセット",
-  "reset_settings_to_default": "",
   "reset_to_default": "デフォルトにリセット",
   "resolve_duplicates": "重複を解決する",
   "resolved_all_duplicates": "全ての重複を解決しました",
@@ -1086,8 +1030,6 @@
   "saved_settings": "設定を保存しました",
   "say_something": "何か書き込みましょう",
   "scan_all_libraries": "全てのライブラリをスキャン",
-  "scan_all_library_files": "全てのライブラリのファイルを再スキャン",
-  "scan_new_library_files": "新しいライブラリのファイルをスキャン",
   "scan_settings": "スキャン設定",
   "scanning_for_album": "アルバムをスキャン中…",
   "search": "検索",
@@ -1128,7 +1070,6 @@
   "selected_count": "{count, plural, other {#個選択済み}}",
   "send_message": "メッセージを送信",
   "send_welcome_email": "ウェルカムメールを送信",
-  "server": "サーバー",
   "server_offline": "サーバーがオフラインです",
   "server_online": "サーバーがオンラインです",
   "server_stats": "サーバー統計",
@@ -1230,11 +1171,9 @@
   "to_change_password": "パスワードを変更",
   "to_favorite": "お気に入り",
   "to_login": "ログイン",
-  "to_root": "最上層のフォルダへ",
   "to_trash": "ゴミ箱",
   "toggle_settings": "設定をトグル",
   "toggle_theme": "ダークテーマを切り替え",
-  "toggle_visibility": "",
   "total_usage": "総使用量",
   "trash": "ゴミ箱",
   "trash_all": "全て削除",
@@ -1244,12 +1183,10 @@
   "trashed_items_will_be_permanently_deleted_after": "ゴミ箱に入れられたアイテムは{days, plural, one {#日} other {#日}}後に完全に削除されます。",
   "type": "タイプ",
   "unarchive": "アーカイブを解除",
-  "unarchived": "",
   "unarchived_count": "{count, plural, other {#枚アーカイブしました}}",
   "unfavorite": "お気に入りから外す",
   "unhide_person": "人物の非表示を解除",
   "unknown": "不明",
-  "unknown_album": "",
   "unknown_year": "不明な年",
   "unlimited": "無制限",
   "unlink_oauth": "OAuthのリンクを解除",
@@ -1307,7 +1244,6 @@
   "view_next_asset": "次のアセットを見る",
   "view_previous_asset": "前のアセットを見る",
   "view_stack": "ビュースタック",
-  "viewer": "",
   "visibility_changed": "{count, plural, one {#人} other {#人}}の人物の非表示設定が変更されました",
   "waiting": "待機中",
   "warning": "警告",
diff --git a/web/src/lib/i18n/kmr.json b/i18n/kmr.json
similarity index 95%
rename from web/src/lib/i18n/kmr.json
rename to i18n/kmr.json
index 9e0be0afbd..a764851442 100644
--- a/web/src/lib/i18n/kmr.json
+++ b/i18n/kmr.json
@@ -34,7 +34,6 @@
     "confirm_email_below": "",
     "confirm_reprocess_all_faces": "",
     "confirm_user_password_reset": "",
-    "crontab_guru": "",
     "disable_login": "",
     "duplicate_detection_job_description": "",
     "exclusion_pattern_description": "",
@@ -50,16 +49,9 @@
     "image_prefer_embedded_preview_setting_description": "",
     "image_prefer_wide_gamut": "",
     "image_prefer_wide_gamut_setting_description": "",
-    "image_preview_format": "",
-    "image_preview_resolution": "",
-    "image_preview_resolution_description": "",
     "image_quality": "",
-    "image_quality_description": "",
     "image_settings": "",
     "image_settings_description": "",
-    "image_thumbnail_format": "",
-    "image_thumbnail_resolution": "",
-    "image_thumbnail_resolution_description": "",
     "job_concurrency": "",
     "job_not_concurrency_safe": "",
     "job_settings": "",
@@ -68,8 +60,6 @@
     "jobs_delayed": "",
     "jobs_failed": "",
     "library_created": "",
-    "library_cron_expression": "",
-    "library_cron_expression_presets": "",
     "library_deleted": "",
     "library_import_path_description": "",
     "library_scanning": "",
@@ -177,15 +167,12 @@
     "paths_validated_successfully": "",
     "quota_size_gib": "",
     "refreshing_all_libraries": "",
-    "removing_offline_files": "",
     "repair_all": "",
     "repair_matched_items": "",
     "repaired_items": "",
     "require_password_change_on_login": "",
     "reset_settings_to_default": "",
     "reset_settings_to_recent_saved": "",
-    "scanning_library_for_changed_files": "",
-    "scanning_library_for_new_files": "",
     "send_welcome_email": "",
     "server_external_domain_settings": "",
     "server_external_domain_settings_description": "",
@@ -260,8 +247,6 @@
     "transcoding_threads_description": "",
     "transcoding_tone_mapping": "",
     "transcoding_tone_mapping_description": "",
-    "transcoding_tone_mapping_npl": "",
-    "transcoding_tone_mapping_npl_description": "",
     "transcoding_transcode_policy": "",
     "transcoding_transcode_policy_description": "",
     "transcoding_two_pass_encoding": "",
@@ -315,7 +300,6 @@
   "archive_or_unarchive_photo": "",
   "archive_size": "",
   "archive_size_description": "",
-  "archived": "",
   "asset_offline": "",
   "assets": "",
   "authorized_devices": "",
@@ -329,10 +313,6 @@
   "cancel_search": "",
   "cannot_merge_people": "",
   "cannot_update_the_description": "",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
   "change_date": "",
   "change_expiration_time": "",
   "change_location": "",
@@ -421,13 +401,6 @@
   "downloading": "",
   "duplicates": "",
   "duration": "",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
   "edit_album": "",
   "edit_avatar": "",
   "edit_date": "",
@@ -446,7 +419,6 @@
   "edited": "",
   "editor": "",
   "email": "",
-  "empty_album": "",
   "empty_trash": "",
   "end_date": "",
   "error": "",
@@ -493,8 +465,8 @@
     "unable_to_refresh_user": "",
     "unable_to_remove_album_users": "",
     "unable_to_remove_api_key": "",
+    "unable_to_remove_deleted_assets": "",
     "unable_to_remove_library": "",
-    "unable_to_remove_offline_files": "",
     "unable_to_remove_partner": "",
     "unable_to_remove_reaction": "",
     "unable_to_repair_items": "",
@@ -530,7 +502,6 @@
   "extension": "",
   "external": "",
   "external_libraries": "",
-  "failed_to_get_people": "",
   "favorite": "",
   "favorite_or_unfavorite_photo": "",
   "favorites": "",
@@ -542,14 +513,12 @@
   "filter_people": "",
   "find_them_fast": "",
   "fix_incorrect_match": "",
-  "force_re-scan_library_files": "",
   "forward": "",
   "general": "",
   "get_help": "",
   "getting_started": "",
   "go_back": "",
   "go_to_search": "",
-  "go_to_share_page": "",
   "group_albums_by": "",
   "has_quota": "",
   "hide_gallery": "",
@@ -665,7 +634,6 @@
   "oldest_first": "",
   "online": "",
   "only_favorites": "",
-  "only_refreshes_modified_files": "",
   "open_the_search_filters": "",
   "options": "",
   "organize_your_library": "",
@@ -727,10 +695,10 @@
   "refreshed": "",
   "refreshes_every_file": "",
   "remove": "",
+  "remove_deleted_assets": "",
   "remove_from_album": "",
   "remove_from_favorites": "",
   "remove_from_shared_link": "",
-  "remove_offline_files": "",
   "removed_api_key": "",
   "rename": "",
   "repair": "",
@@ -754,8 +722,6 @@
   "saved_settings": "",
   "say_something": "",
   "scan_all_libraries": "",
-  "scan_all_library_files": "",
-  "scan_new_library_files": "",
   "scan_settings": "",
   "search": "",
   "search_albums": "",
@@ -786,7 +752,6 @@
   "selected": "",
   "send_message": "",
   "send_welcome_email": "",
-  "server": "",
   "server_stats": "",
   "set": "",
   "set_as_album_cover": "",
@@ -857,7 +822,6 @@
   "to_favorite": "",
   "toggle_settings": "",
   "toggle_theme": "",
-  "toggle_visibility": "",
   "total_usage": "",
   "trash": "",
   "trash_all": "",
@@ -865,11 +829,9 @@
   "trashed_items_will_be_permanently_deleted_after": "",
   "type": "",
   "unarchive": "",
-  "unarchived": "",
   "unfavorite": "",
   "unhide_person": "",
   "unknown": "",
-  "unknown_album": "",
   "unknown_year": "",
   "unlimited": "",
   "unlink_oauth": "",
@@ -903,7 +865,6 @@
   "view_links": "",
   "view_next_asset": "",
   "view_previous_asset": "",
-  "viewer": "",
   "waiting": "",
   "week": "",
   "welcome": "",
diff --git a/web/src/lib/i18n/ko.json b/i18n/ko.json
similarity index 83%
rename from web/src/lib/i18n/ko.json
rename to i18n/ko.json
index cd3db13102..a58e20244a 100644
--- a/web/src/lib/i18n/ko.json
+++ b/i18n/ko.json
@@ -23,16 +23,22 @@
   "add_to": "앨범에 추가...",
   "add_to_album": "앨범에 추가",
   "add_to_shared_album": "공유 앨범에 추가",
-  "added_to_archive": "보관함으로 이동되었습니다.",
+  "added_to_archive": "보관함에 추가되었습니다.",
   "added_to_favorites": "즐겨찾기에 추가되었습니다.",
   "added_to_favorites_count": "즐겨찾기에 항목 {count, number}개 추가됨",
   "admin": {
-    "add_exclusion_pattern_description": "규칙에 *, ** 및 ? 를 사용할 수 있습니다. \"Raw\" 디렉터리의 모든 파일을 제외하려면 **/Raw/**를, \".tif\"로 끝나는 파일을 제외하려면 **/*.tif를 사용합니다. 절대 경로는 /path/to/ignore/** 와 같은 방식으로 사용하세요.",
+    "add_exclusion_pattern_description": "규칙에 *, ** 및 ? 를 사용할 수 있습니다. 이름이 \"Raw\"인 디렉터리의 모든 파일을 제외하려면 \"**/Raw/**\"를, \".tif\"로 끝나는 모든 파일을 제외하려면 \"**/*.tif\"를 사용하고, 절대 경로의 경우 \"/path/to/ignore/**\"와 같은 방식으로 사용합니다.",
+    "asset_offline_description": "외부 라이브러리에 포함된 이 항목을 디스크에서 더이상 찾을 수 없어 휴지통으로 이동되었습니다. 파일이 라이브러리 내에서 이동된 경우 타임라인에서 새로 연결된 항목을 확인하세요. 이 항목을 복원하려면 아래 파일 경로에 Immich가 접근할 수 있는지 확인하고 라이브러리 스캔을 진행하세요.",
     "authentication_settings": "인증 설정",
     "authentication_settings_description": "비밀번호, OAuth 및 기타 인증 설정 관리",
     "authentication_settings_disable_all": "로그인 기능을 모두 비활성화하시겠습니까? 로그인하지 않아도 서버에 접근할 수 있습니다.",
     "authentication_settings_reenable": "다시 활성화하려면 <link>서버 커맨드</link>를 사용하세요.",
     "background_task_job": "백그라운드 작업",
+    "backup_database": "데이터베이스 백업",
+    "backup_database_enable_description": "데이터베이스 백업 활성화",
+    "backup_keep_last_amount": "보관할 백업의 개수",
+    "backup_settings": "백업 설정",
+    "backup_settings_description": "데이터베이스 백업 설정 관리",
     "check_all": "모두 확인",
     "cleared_jobs": "작업 중단: {job}",
     "config_set_by_file": "현재 설정은 구성 파일에 의해 관리됩니다.",
@@ -41,35 +47,40 @@
     "confirm_email_below": "계속 진행하려면 아래에 \"{email}\" 입력",
     "confirm_reprocess_all_faces": "모든 얼굴을 다시 처리하시겠습니까? 이름이 지정된 인물을 포함한 모든 인물이 삭제됩니다.",
     "confirm_user_password_reset": "{user}님의 비밀번호를 재설정하시겠습니까?",
-    "crontab_guru": "Crontab Guru",
+    "create_job": "작업 생성",
+    "cron_expression": "Cron 표현식",
+    "cron_expression_description": "Cron 형식을 사용하여 스캔 주기를 설정합니다. 자세한 내용과 예시는 <link>Crontab Guru</link>를 참조하세요.",
+    "cron_expression_presets": "Cron 표현식 사전 설정",
     "disable_login": "로그인 비활성화",
-    "disabled": "비활성화",
     "duplicate_detection_job_description": "기계 학습을 통해 유사한 이미지를 감지합니다. 스마트 검색이 활성화되어 있어야 합니다.",
-    "exclusion_pattern_description": "제외 규칙을 사용하면 스캔 중 특정 파일과 폴더를 제외할 수 있습니다. 가져오고 싶지 않은 파일(RAW 파일 등)이 존재하는 경우 유용합니다.",
+    "exclusion_pattern_description": "제외 규칙을 사용하여 라이브러리 스캔 시 특정 파일과 폴더를 제외할 수 있습니다. 폴더에 원하지 않는 파일(RAW 파일 등)이 존재하는 경우 유용합니다.",
     "external_library_created_at": "외부 라이브러리 ({date}에 생성됨)",
     "external_library_management": "외부 라이브러리 관리",
     "face_detection": "얼굴 감지",
-    "face_detection_description": "기계 학습을 통해 항목에서 얼굴을 감지합니다. 동영상의 경우 섬네일만 사용합니다. \"모두\"는 이미 처리된 항목을 포함한 모든 항목을 대기열에 추가합니다. \"누락\"은 처리되지 않은 항목만 대기열에 추가합니다.",
-    "facial_recognition_job_description": "감지된 얼굴을 인물로 그룹화합니다. 얼굴 감지 작업이 완료된 후 진행되며, \"모두\"는 이미 그룹화된 얼굴을 포함한 모든 얼굴을 대기열에 추가합니다. \"누락\"은 그룹화되지 않은 얼굴만 대기열에 추가합니다.",
+    "face_detection_description": "기계 학습을 통해 항목에 존재하는 얼굴을 감지합니다. 동영상의 경우 섬네일만 사용합니다. \"새로고침\"은 이미 처리된 항목을 포함한 모든 항목을 다시 처리합니다. \"초기화\"는 모든 얼굴 데이터를 삭제합니다. \"누락\"은 처리되지 않은 항목을 대기열에 추가합니다. 얼굴 감지 작업이 완료되면 얼굴 인식 작업이 진행되어 감지된 얼굴을 기존 인물이나 새 인물로 그룹화합니다.",
+    "facial_recognition_job_description": "감지된 얼굴을 인물로 그룹화합니다. 이 작업은 얼굴 감지 작업이 완료된 후 진행됩니다. \"초기화\"는 모든 얼굴의 그룹화를 다시 진행합니다. \"누락\"은 그룹화되지 않은 얼굴을 대기열에 추가합니다.",
     "failed_job_command": "{job} 작업에서 {command} 실패",
     "force_delete_user_warning": "경고: 사용자 및 사용자가 업로드한 모든 항목이 즉시 삭제됩니다. 이 작업은 되돌릴 수 없으며 파일을 복구할 수 없습니다.",
-    "forcing_refresh_library_files": "모든 파일을 다시 스캔하는 중...",
+    "forcing_refresh_library_files": "라이브러리의 모든 파일을 다시 스캔하는 중...",
+    "image_format": "형식",
     "image_format_description": "WebP는 JPEG보다 파일 크기가 작지만 변환에 더 많은 시간이 소요됩니다.",
     "image_prefer_embedded_preview": "포함된 미리 보기 선호",
     "image_prefer_embedded_preview_setting_description": "가능한 경우 이미지 처리 시 RAW 사진에 포함된 미리 보기를 사용합니다. 포함된 미리 보기는 카메라에서 생성된 것으로 카메라마다 품질이 다릅니다. 일부 이미지의 경우 더 정확한 색상이 표현될 수 있지만 반대로 더 많은 아티팩트가 있을 수도 있습니다.",
     "image_prefer_wide_gamut": "넓은 색 영역 선호",
     "image_prefer_wide_gamut_setting_description": "섬네일 이미지에 Display P3을 사용합니다. 많은 색상을 표현할 수 있어 더 정확한 표현이 가능하지만, 오래된 브라우저를 사용하는 경우 이미지가 다르게 보일 수 있습니다. 색상 왜곡을 방지하기 위해 sRGB 이미지는 이 설정이 적용되지 않습니다.",
-    "image_preview_format": "미리 보기 형식",
-    "image_preview_resolution": "미리 보기 해상도",
-    "image_preview_resolution_description": "사진을 보거나 기계 학습을 실행할 때 사용되는 사진의 해상도를 설정합니다. 높은 해상도를 선택하면 세부 묘사의 손실을 최소화할 수 있지만, 인코딩 시간과 파일 크기가 증가하여 앱의 반응 속도가 느려질 수 있습니다.",
+    "image_preview_description": "메타데이터를 제거한 중간 크기의 이미지, 단일 항목을 보는 경우 및 기계 학습에 사용됨",
+    "image_preview_quality_description": "1부터 100 사이의 미리보기 품질. 값이 높을수록 좋지만 파일 크기가 커져 앱의 반응성이 떨어질 수 있으며, 값이 낮으면 기계 학습의 품질이 떨어질 수 있습니다.",
+    "image_preview_title": "미리보기 설정",
     "image_quality": "품질",
-    "image_quality_description": "이미지 품질을 1에서 100 사이로 설정합니다. 높은 품질을 선택하면 파일 크기가 증가하지만 생성된 이미지의 품질이 향상됩니다. 이 옵션은 미리 보기 및 섬네일 이미지에 영향을 미칩니다.",
+    "image_resolution": "해상도",
+    "image_resolution_description": "해상도가 높을 수록 디테일이 보존되지만 파일이 크고 인코딩이 오래 걸리며 앱 응답성이 떨어질 수 있습니다.",
     "image_settings": "이미지 설정",
     "image_settings_description": "생성된 이미지의 품질 및 해상도 관리",
-    "image_thumbnail_format": "섬네일 형식",
-    "image_thumbnail_resolution": "섬네일 해상도",
-    "image_thumbnail_resolution_description": "여러 항목을 표시할 때 사용되는 사진의 해상도를 설정합니다. (메인 타임라인, 앨범 보기 등) 높은 해상도를 선택하면 세부 묘사의 손실을 최소화할 수 있지만, 인코딩 시간과 파일 크기가 증가하여 앱의 반응 속도가 느려질 수 있습니다.",
+    "image_thumbnail_description": "메타데이터가 제거된 작은 섬네일 이미지, 타임라인 등 사진을 그룹화하여 보는 경우에 사용됨",
+    "image_thumbnail_quality_description": "섬네일 품질(1~100). 높을수록 좋지만 파일크기가 커져 앱의 반응성이 떨어질 수 있습니다.",
+    "image_thumbnail_title": "섬네일 설정",
     "job_concurrency": "{job} 동시성",
+    "job_created": "작업이 생성되었습니다.",
     "job_not_concurrency_safe": "이 작업은 동시 실행이 제한됩니다.",
     "job_settings": "작업 설정",
     "job_settings_description": "작업 동시성 관리",
@@ -77,9 +88,6 @@
     "jobs_delayed": "{jobCount, plural, other {#개}} 지연",
     "jobs_failed": "{jobCount, plural, other {#개}} 실패",
     "library_created": "{library} 라이브러리를 생성했습니다.",
-    "library_cron_expression": "Cron 표현식",
-    "library_cron_expression_description": "cron 형식을 사용하여 스캔 주기를 설정합니다. 자세한 내용 및 예제는 <link>Crontab Guru</link>를 참조하세요.",
-    "library_cron_expression_presets": "Cron 표현식 사전 설정",
     "library_deleted": "라이브러리가 삭제되었습니다.",
     "library_import_path_description": "가져올 폴더를 선택하세요. 선택한 폴더 및 하위 폴더에서 사진과 동영상을 스캔합니다.",
     "library_scanning": "주기적 스캔",
@@ -90,38 +98,38 @@
     "library_tasks_description": "라이브러리 구성 및 확인 작업 수행",
     "library_watching_enable_description": "외부 라이브러리의 파일 변경 감시",
     "library_watching_settings": "라이브러리 감시 (실험 기능)",
-    "library_watching_settings_description": "변경된 파일을 자동으로 감지",
-    "logging_enable_description": "로깅 활성화",
-    "logging_level_description": "로깅이 활성화된 경우 사용할 로그 레벨을 선택합니다.",
-    "logging_settings": "로깅",
+    "library_watching_settings_description": "파일 변겅을 자동으로 감지",
+    "logging_enable_description": "로그 기록 활성화",
+    "logging_level_description": "활성화된 경우 사용할 로그 레벨을 선택합니다.",
+    "logging_settings": "로그 설정",
     "machine_learning_clip_model": "CLIP 모델",
-    "machine_learning_clip_model_description": "CLIP 모델의 종류는 <link>이곳</link>을 참조하세요. 한국어로 검색하려면 Multilingual CLIP 모델을 선택하세요. 변경 후 모든 항목에 대한 스마트 검색 작업을 다시 진행해야 합니다.",
+    "machine_learning_clip_model_description": "CLIP 모델의 종류는 <link>이곳</link>을 참조하세요. 한국어 등 다국어 검색을 사용하려면 Multilingual CLIP 모델을 선택하세요. 모델을 변경한 후 모든 항목에 대한 스마트 검색 작업을 다시 진행해야 합니다.",
     "machine_learning_duplicate_detection": "비슷한 항목 감지",
     "machine_learning_duplicate_detection_enabled": "비슷한 항목 감지 활성화",
-    "machine_learning_duplicate_detection_enabled_description": "비활성화된 경우에도 완전히 일치하는 항목은 여전히 감지됩니다.",
+    "machine_learning_duplicate_detection_enabled_description": "비활성화된 경우에도 완전히 동일한 항목은 중복 제거됩니다.",
     "machine_learning_duplicate_detection_setting_description": "CLIP 임베딩을 사용하여 비슷한 항목 찾기",
     "machine_learning_enabled": "기계 학습 활성화",
-    "machine_learning_enabled_description": "비활성화하는 경우 기계 학습 설정 여부와 관계없이 모든 기계 학습 기능이 비활성화됩니다.",
+    "machine_learning_enabled_description": "비활성화된 경우 아래 설정 여부와 관계없이 모든 기계 학습 기능이 비활성화됩니다.",
     "machine_learning_facial_recognition": "얼굴 인식",
     "machine_learning_facial_recognition_description": "이미지에서 얼굴 감지, 인식 및 그룹화",
     "machine_learning_facial_recognition_model": "얼굴 인식 모델",
-    "machine_learning_facial_recognition_model_description": "크기에 따라 내림차순으로 나열됩니다. 크기가 큰 모델은 느리고 메모리를 많이 사용하지만 더 나은 결과를 생성합니다. 변경 후 모든 항목의 얼굴 감지 작업을 다시 진행해야 합니다.",
+    "machine_learning_facial_recognition_model_description": "크기에 따라 내림차순으로 나열됩니다. 크기가 큰 모델은 느리고 메모리를 많이 사용하지만 더 나은 결과를 보입니다. 모델을 변경한 이후 모든 항목의 얼굴 감지 작업을 다시 진행해야 합니다.",
     "machine_learning_facial_recognition_setting": "얼굴 인식 활성화",
     "machine_learning_facial_recognition_setting_description": "비활성화된 경우 이미지에서 얼굴 인식을 진행하지 않으며, 탐색 페이지에 인물 목록이 표시되지 않습니다.",
     "machine_learning_max_detection_distance": "최대 감지 거리",
     "machine_learning_max_detection_distance_description": "두 이미지를 유사한 이미지로 간주하는 거리의 최댓값을 0.001에서 0.1 사이로 설정합니다. 값이 높으면 민감도가 낮아져 유사한 이미지로 감지하는 비율이 높아지나, 잘못된 결과를 보일 수 있습니다.",
     "machine_learning_max_recognition_distance": "최대 인식 거리",
-    "machine_learning_max_recognition_distance_description": "두 얼굴을 동일한 인물로 판단하는 거리의 최댓값을 0에서 2 사이로 설정합니다. 이 값을 낮추면 다른 인물을 동일한 인물로 판단하는 것을 방지할 수 있고, 값을 높이면 동일한 인물을 다른 인물로 판단하는 것을 방지할 수 있습니다. 두 인물을 병합하는 것이 하나의 인물을 둘로 나누는 것보다 쉽기에, 가능한 낮은 임계값을 사용하세요.",
-    "machine_learning_min_detection_score": "최소 탐지 점수",
-    "machine_learning_min_detection_score_description": "감지된 얼굴의 최소 신뢰 점수를 0에서 1 사이로 설정합니다. 값이 낮으면 많은 얼굴을 감지하지만 잘못된 결과를 보일 수 있습니다.",
+    "machine_learning_max_recognition_distance_description": "두 얼굴을 동일인으로 인식하는 거리의 최댓값을 0에서 2 사이로 설정합니다. 이 값을 낮추면 다른 인물을 동일인으로 인식하는 것을 방지할 수 있고, 값을 높이면 동일인을 다른 인물로 인식하는 것을 방지할 수 있습니다. 두 인물을 병합하는 것이 한 인물을 두 명으로 분리하는 것보다 쉬우므로, 가능한 낮은 임계값을 사용하세요.",
+    "machine_learning_min_detection_score": "최소 신뢰도 점수",
+    "machine_learning_min_detection_score_description": "감지된 얼굴의 최소 신뢰도 점수를 0에서 1 사이로 설정합니다. 값이 낮으면 많은 얼굴을 감지하지만 잘못된 결과를 보일 수 있습니다.",
     "machine_learning_min_recognized_faces": "최소 인식 얼굴",
-    "machine_learning_min_recognized_faces_description": "얼굴을 인식하여 인물을 생성하기 위한 최소 인식 얼굴 수를 설정합니다. 값이 높으면 얼굴 인식이 정확해지지만, 감지된 얼굴이 인물로 그룹화되지 않을 가능성이 증가합니다.",
+    "machine_learning_min_recognized_faces_description": "인물을 생성하기 위해 인식할 얼굴 수의 최솟값을 설정합니다. 값이 높으면 얼굴 인식이 정확해지지만 감지된 얼굴이 인물에 할당되지 않을 가능성이 증가합니다.",
     "machine_learning_settings": "기계 학습 설정",
     "machine_learning_settings_description": "기계 학습 기능 및 설정 관리",
     "machine_learning_smart_search": "스마트 검색",
-    "machine_learning_smart_search_description": "CLIP 임베딩을 사용하여 이미지 자연어 검색 지원",
+    "machine_learning_smart_search_description": "CLIP 임베딩으로 자연어를 사용하여 이미지 검색",
     "machine_learning_smart_search_enabled": "스마트 검색 활성화",
-    "machine_learning_smart_search_enabled_description": "비활성화 시 스마트 검색을 위한 이미지 처리를 진행하지 않습니다.",
+    "machine_learning_smart_search_enabled_description": "비활성화된 경우 스마트 검색을 위한 이미지 처리를 진행하지 않습니다.",
     "machine_learning_url_description": "기계 학습 서버 URL",
     "manage_concurrency": "동시성 관리",
     "manage_log_settings": "로그 설정 관리",
@@ -129,7 +137,7 @@
     "map_enable_description": "지도 기능 활성화",
     "map_gps_settings": "지도 및 GPS 설정",
     "map_gps_settings_description": "지도 및 GPS (역지오코딩) 설정 관리",
-    "map_implications": "지도 기능은 외부 타일 서비스(tiles.immich.clou를 사용합니다.",
+    "map_implications": "지도 기능은 외부 타일 서비스(tiles.immich.cloud)에 의존합니다.",
     "map_light_style": "라이트 스타일",
     "map_manage_reverse_geocoding_settings": "<link>역지오코딩</link> 설정 관리",
     "map_reverse_geocoding": "역지오코딩",
@@ -139,16 +147,20 @@
     "map_settings_description": "지도 설정 관리",
     "map_style_description": "지도 테마 style.json URL",
     "metadata_extraction_job": "메타데이터 추출",
-    "metadata_extraction_job_description": "각 항목에서 GPS, 해상도 등의 메타데이터 정보 추출",
+    "metadata_extraction_job_description": "각 항목에서 GPS, 인물 및 해상도 등의 메타데이터 정보 추출",
+    "metadata_faces_import_setting": "얼굴 가져오기 활성화",
+    "metadata_faces_import_setting_description": "사이드카 파일의 이미지 EXIF 데이터에서 얼굴 가져오기",
+    "metadata_settings": "메타데이터 설정",
+    "metadata_settings_description": "메타데이터 설정 관리",
     "migration_job": "마이그레이션",
     "migration_job_description": "각 항목의 섬네일 및 인물의 얼굴을 최신 폴더 구조로 마이그레이션",
     "no_paths_added": "추가된 경로 없음",
     "no_pattern_added": "추가된 규칙 없음",
     "note_apply_storage_label_previous_assets": "참고: 이전에 업로드한 항목에도 스토리지 레이블을 적용하려면 다음을 실행합니다,",
     "note_cannot_be_changed_later": "주의: 추후 변경할 수 없습니다!",
-    "note_unlimited_quota": "참고: 할당량을 설정하지 않으려면 0을 입력하세요.",
+    "note_unlimited_quota": "참고: 무제한 할당량의 경우 0을 입력하세요.",
     "notification_email_from_address": "보낸 사람 이메일",
-    "notification_email_from_address_description": "보낸 사람의 이메일 주소, 예: \"Immich Photo Server <noreply@immich.app>\"",
+    "notification_email_from_address_description": "보낸 사람의 이메일 주소, 예: \"Immich Photo Server <noreply@example.com>\"",
     "notification_email_host_description": "이메일 서버의 호스트 (예: smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "인증서 오류 무시",
     "notification_email_ignore_certificate_errors_description": "TLS 인증서 유효성 검사 오류 무시 (권장되지 않음)",
@@ -174,49 +186,49 @@
     "oauth_issuer_url": "발급자 URL",
     "oauth_mobile_redirect_uri": "모바일 리다이렉트 URI",
     "oauth_mobile_redirect_uri_override": "모바일 리다이렉트 URI 재정의",
-    "oauth_mobile_redirect_uri_override_description": "OAuth 공급자가 '{callback}' 과 같은 모바일 URI를 제공하지 않는 경우 활성화하세요.",
+    "oauth_mobile_redirect_uri_override_description": "OAuth 공급자가 '{callback}'과 같은 모바일 URI를 제공하지 않는 경우 활성화하세요.",
     "oauth_profile_signing_algorithm": "사용자 정보 서명 알고리즘",
     "oauth_profile_signing_algorithm_description": "사용자 정보 서명에 사용되는 알고리즘을 선택합니다.",
     "oauth_scope": "스코프",
     "oauth_settings": "OAuth",
     "oauth_settings_description": "OAuth 로그인 설정 관리",
-    "oauth_settings_more_details": "이 기능에 대한 자세한 내용은 <link>공식 문서</link>를 참조하세요.",
+    "oauth_settings_more_details": "이 기능에 대한 자세한 내용은 <link>문서</link>를 참조하세요.",
     "oauth_signing_algorithm": "서명 알고리즘",
     "oauth_storage_label_claim": "스토리지 레이블 선택",
     "oauth_storage_label_claim_description": "스토리지 레이블을 사용자가 입력한 값으로 자동 설정합니다.",
     "oauth_storage_quota_claim": "스토리지 할당량 선택",
     "oauth_storage_quota_claim_description": "스토리지 할당량을 사용자가 입력한 값으로 자동 설정합니다.",
     "oauth_storage_quota_default": "스토리지 할당량 기본값 (GiB)",
-    "oauth_storage_quota_default_description": "입력하지 않은 경우 사용할 GiB 단위의 기본 할당량 (할당량을 설정하지 않으려면 0 입력)",
+    "oauth_storage_quota_default_description": "입력하지 않은 경우 사용할 GiB 단위의 기본 할당량 (무제한 할당량의 경우 0 입력)",
     "offline_paths": "누락된 파일",
     "offline_paths_description": "외부 라이브러리의 항목이 아닌 파일을 수동으로 삭제한 경우 발생할 수 있습니다.",
     "password_enable_description": "이메일과 비밀번호로 로그인",
     "password_settings": "비밀번호 로그인",
     "password_settings_description": "비밀번호 로그인 설정 관리",
     "paths_validated_successfully": "모든 경로를 성공적으로 검증했습니다.",
+    "person_cleanup_job": "인물 정리",
     "quota_size_gib": "할당량 (GiB)",
     "refreshing_all_libraries": "모든 라이브러리 다시 스캔 중...",
-    "registration": "관리자 가입",
-    "registration_description": "첫 번째 사용자이기 때문에 관리자로 지정되었습니다. 관리 작업 및 사용자 생성이 가능합니다.",
-    "removing_offline_files": "누락된 파일을 제거하는 중...",
+    "registration": "관리자 계정 생성",
+    "registration_description": "첫 번째로 생성되는 사용자는 관리자 권한을 부여받으며, 관리 및 사용자 생성이 가능합니다.",
     "repair_all": "모두 수리",
-    "repair_matched_items": "동일한 항목 {count, plural, one {#개} other {#개}}를 확인했습니다.",
+    "repair_matched_items": "동일 항목 {count, plural, one {#개} other {#개}}를 확인했습니다.",
     "repaired_items": "항목 {count, plural, one {#개} other {#개}}를 수리했습니다.",
     "require_password_change_on_login": "첫 로그인 시 비밀번호 변경 요구",
     "reset_settings_to_default": "설정을 기본값으로 복원",
     "reset_settings_to_recent_saved": "마지막으로 저장된 설정으로 복원",
-    "scanning_library_for_changed_files": "라이브러리 변경 사항 확인 중...",
-    "scanning_library_for_new_files": "라이브러리에서 새 파일 스캔 중...",
+    "scanning_library": "라이브러리 스캔 중",
+    "search_jobs": "작업 검색...",
     "send_welcome_email": "환영 이메일 전송",
     "server_external_domain_settings": "외부 도메인",
     "server_external_domain_settings_description": "공개 공유 링크에 사용할 도메인 (http(s):// 포함)",
     "server_settings": "서버 설정",
     "server_settings_description": "서버 설정 관리",
     "server_welcome_message": "환영 메시지",
-    "server_welcome_message_description": "로그인 페이지에 표시되는 메시지를 설정합니다.",
+    "server_welcome_message_description": "로그인 페이지에 표시되는 메시지입니다.",
     "sidecar_job": "사이드카 메타데이터",
     "sidecar_job_description": "파일 시스템에서 사이드카 메타데이터 파일 탐색 및 동기화",
-    "slideshow_duration_description": "각 사진을 표시할 초 단위의 시간",
+    "slideshow_duration_description": "개별 사진이 표시되는 초 단위의 시간",
     "smart_search_job_description": "기계 학습을 진행하여 스마트 검색 기능 지원",
     "storage_template_date_time_description": "항목이 생성된 날짜의 타임스탬프를 날짜 및 시간 정보로 사용합니다.",
     "storage_template_date_time_sample": "시간 형식 예: {date}",
@@ -234,14 +246,14 @@
     "storage_template_settings_description": "업로드된 항목의 폴더 구조 및 파일 이름 관리",
     "storage_template_user_label": "사용자의 스토리지 레이블: <code>{label}</code>",
     "system_settings": "시스템 설정",
+    "tag_cleanup_job": "태그 정리",
     "theme_custom_css_settings": "사용자 정의 CSS",
     "theme_custom_css_settings_description": "Immich에 적용할 사용자 정의 CSS(Cascading Style Sheets) 설정",
     "theme_settings": "테마 설정",
     "theme_settings_description": "Immich 웹 인터페이스 사용자 정의",
-    "these_files_matched_by_checksum": "동일한 체크섬을 가진 파일 목록입니다.",
+    "these_files_matched_by_checksum": "체크섬이 동일한 파일 목록입니다.",
     "thumbnail_generation_job": "섬네일 생성",
     "thumbnail_generation_job_description": "각 항목에 대한 큰 섬네일, 작은 섬네일, 흐린 섬네일 및 인물 섬네일 생성",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "가속 API",
     "transcoding_acceleration_api_description": "트랜스코딩 가속을 위해 기기와 상호 작용할 API입니다. 이 설정은 '최선의 노력'으로, 실패 시 소프트웨어 트랜스코딩을 사용합니다. VP9의 작동 여부는 하드웨어에 따라 달라질 수 있습니다.",
     "transcoding_acceleration_nvenc": "NVENC (NVIDIA GPU 필요)",
@@ -258,7 +270,7 @@
     "transcoding_audio_codec": "오디오 코덱",
     "transcoding_audio_codec_description": "Opus는 가장 좋은 품질의 옵션이지만 기기 및 소프트웨어가 오래된 경우 호환되지 않을 수 있습니다.",
     "transcoding_bitrate_description": "최대 비트레이트를 초과하는 동영상 또는 허용되지 않는 형식의 동영상",
-    "transcoding_codecs_learn_more": "이곳에서 사용되는 용어에 대한 자세한 내용은 FFmpeg 문서의 <h264-link>H.264 코덱</h264-link>, <hevc-link>HEVC 코덱</hevc-link> 및 <vp9-link>VP9 코덱</vp9-link>을 참조하세요.",
+    "transcoding_codecs_learn_more": "여기에서 사용되는 용어에 대한 자세한 내용은 FFmpeg 문서의 <h264-link>H.264 코덱</h264-link>, <hevc-link>HEVC 코덱</hevc-link> 및 <vp9-link>VP9 코덱</vp9-link> 항목을 참조하세요.",
     "transcoding_constant_quality_mode": "Constant quality mode",
     "transcoding_constant_quality_mode_description": "ICQ는 CQP보다 나은 성능을 보이나 일부 기기의 하드웨어 가속에서 지원되지 않을 수 있습니다. 이 옵션을 설정하면 품질 기반 인코딩 시 지정된 모드를 우선적으로 사용합니다. NVENC에서는 ICQ를 지원하지 않아 이 설정이 적용되지 않습니다.",
     "transcoding_constant_rate_factor": "Constant rate factor (-crf)",
@@ -267,7 +279,7 @@
     "transcoding_hardware_acceleration": "하드웨어 가속",
     "transcoding_hardware_acceleration_description": "실험적인 기능입니다. 속도가 향상되지만 동일 비트레이트에서 품질이 상대적으로 낮을 수 있습니다.",
     "transcoding_hardware_decoding": "하드웨어 디코딩",
-    "transcoding_hardware_decoding_setting_description": "인코딩 가속을 위해 엔드 투 엔드 가속을 사용합니다. 모든 동영상에서 작동하지 않을 수 있습니다. (NVENC, QSV 및 RKMPP만 해당)",
+    "transcoding_hardware_decoding_setting_description": "인코딩 가속을 위해 엔드 투 엔드 가속을 사용합니다. 모든 동영상에서 작동하지 않을 수 있습니다.",
     "transcoding_hevc_codec": "HEVC 코덱",
     "transcoding_max_b_frames": "최대 B 프레임",
     "transcoding_max_b_frames_description": "값이 높으면 압축 효율이 향상되지만 인코딩 속도가 저하됩니다. 오래된 기기의 하드웨어 가속과 호환되지 않을 수 있습니다. 0을 입력한 경우 B 프레임을 비활성화하며, -1을 입력한 경우 자동으로 설정합니다.",
@@ -293,8 +305,6 @@
     "transcoding_threads_description": "값이 높으면 인코딩 속도가 향상되지만 리소스 사용량이 증가합니다. 값은 CPU 코어 수보다 작아야 하며, 설정하지 않으려면 0을 입력합니다.",
     "transcoding_tone_mapping": "톤 매핑",
     "transcoding_tone_mapping_description": "HDR 동영상을 SDR로 변환할 때 사용할 톤 매핑 알고리즘을 설정합니다. 알고리즘마다 중점을 두는 부분에 차이가 있습니다. Hable 알고리즘은 세부 묘사를 보존하고, Mobius 알고리즘은 색상을 보존하며, Reinhard 알고리즘은 밝기를 보존합니다.",
-    "transcoding_tone_mapping_npl": "톤 매핑 NPL",
-    "transcoding_tone_mapping_npl_description": "현재 화면의 밝기에서 색상이 정상적으로 보이도록 조정합니다. 화면 밝기를 보정하기에 낮은 값과 높은 값 모두 동영상의 밝기를 높입니다. 0을 입력한 경우 자동으로 설정합니다.",
     "transcoding_transcode_policy": "트랜스코드 정책",
     "transcoding_transcode_policy_description": "트랜스코딩할 동영상을 설정합니다. HDR 영상은 항상 트랜스코딩을 진행합니다. (트랜스코딩이 비활성화된 경우 제외)",
     "transcoding_two_pass_encoding": "투 패스 인코딩",
@@ -308,6 +318,7 @@
     "trash_settings_description": "휴지통 설정 관리",
     "untracked_files": "추적되지 않는 파일",
     "untracked_files_description": "애플리케이션에서 추적되지 않는 파일 목록입니다. 이동 실패, 업로드 중단 또는 버그로 인해 발생할 수 있습니다.",
+    "user_cleanup_job": "사용자 정리",
     "user_delete_delay": "<b>{user}</b>님이 업로드한 항목이 {delay, plural, one {#일} other {#일}} 후 영구적으로 삭제됩니다.",
     "user_delete_delay_settings": "삭제 보류 기간",
     "user_delete_delay_settings_description": "사용자를 영구적으로 삭제하기 전 보류 기간을 설정합니다. 사용자 삭제는 매일 밤 자정, 보류 기간이 지난 사용자를 확인한 후 진행됩니다. 변경 사항은 다음 작업부터 적용됩니다.",
@@ -322,7 +333,7 @@
     "user_settings_description": "사용자 설정 관리",
     "user_successfully_removed": "{email}이(가) 성공적으로 제거되었습니다.",
     "version_check_enabled_description": "버전 확인 활성화",
-    "version_check_implications": "버전 확인 기능은 주기적으로 github.com에 요청을 보냅니다.",
+    "version_check_implications": "주기적으로 github.com에 요청을 보내 최신 버전을 확인합니다.",
     "version_check_settings": "버전 확인",
     "version_check_settings_description": "최신 버전 알림 설정 관리",
     "video_conversion_job": "동영상 트랜스코드",
@@ -337,10 +348,10 @@
   "age_years": "{years, plural, other {#세}}",
   "album_added": "공유 앨범 초대",
   "album_added_notification_setting_description": "공유 앨범으로 초대를 받은 경우 이메일 알림 받기",
-  "album_cover_updated": "앨범 커버를 변경했습니다.",
+  "album_cover_updated": "앨범 커버 업데이트됨",
   "album_delete_confirmation": "{album} 앨범을 삭제하시겠습니까?",
   "album_delete_confirmation_description": "이 앨범을 공유한 경우 다른 사용자가 더 이상 앨범에 접근할 수 없습니다.",
-  "album_info_updated": "앨범 정보가 수정되었습니다.",
+  "album_info_updated": "앨범 정보 업데이트됨",
   "album_leave": "앨범에서 나가시겠습니까?",
   "album_leave_confirmation": "{album} 앨범에서 나가시겠습니까?",
   "album_name": "앨범 이름",
@@ -350,8 +361,8 @@
   "album_share_no_users": "이미 모든 사용자와 앨범을 공유 중이거나 다른 사용자가 없는 것 같습니다.",
   "album_updated": "항목 추가 알림",
   "album_updated_setting_description": "공유 앨범에 항목이 추가된 경우 이메일 알림 받기",
-  "album_user_left": "{album} 앨범에서 나왔습니다.",
-  "album_user_removed": "{user}님을 앨범에서 제거했습니다.",
+  "album_user_left": "{album} 앨범에서 나옴",
+  "album_user_removed": "{user}님을 앨범에서 제거함",
   "album_with_link_access": "링크가 있는 경우 누구나 이 앨범의 사진과 인물을 볼 수 있습니다.",
   "albums": "앨범",
   "albums_count": "앨범 {count, plural, one {{count, number}개} other {{count, number}개}}",
@@ -365,7 +376,7 @@
   "allow_public_user_to_upload": "모든 사용자의 업로드 허용",
   "anti_clockwise": "반시계 방향",
   "api_key": "API 키",
-  "api_key_description": "이 값은 한 번만 표시됩니다. 창을 닫기 전 반드시 복사하세요.",
+  "api_key_description": "이 값은 한 번만 표시됩니다. 창을 닫기 전 반드시 복사해주세요.",
   "api_key_empty": "키 이름은 비어 있을 수 없습니다.",
   "api_keys": "API 키",
   "app_settings": "앱 설정",
@@ -374,19 +385,19 @@
   "archive_or_unarchive_photo": "보관함으로 이동 또는 제거",
   "archive_size": "압축 파일 크기",
   "archive_size_description": "다운로드할 압축 파일의 크기 구성 (GiB 단위)",
-  "archived": "보관됨",
   "archived_count": "보관함으로 항목 {count, plural, other {#개}} 이동됨",
   "are_these_the_same_person": "동일한 인물인가요?",
   "are_you_sure_to_do_this": "계속 진행하시겠습니까?",
   "asset_added_to_album": "앨범에 추가되었습니다.",
   "asset_adding_to_album": "앨범에 추가 중...",
-  "asset_description_updated": "설명이 변경되었습니다.",
-  "asset_filename_is_offline": "{filename} 항목이 누락되었습니다.",
-  "asset_has_unassigned_faces": "항목에 알 수 없는 인물이 있습니다.",
+  "asset_description_updated": "항목의 설명이 업데이트되었습니다.",
+  "asset_filename_is_offline": "{filename} 항목 누락됨",
+  "asset_has_unassigned_faces": "항목에 할당되지 않은 얼굴이 있음",
   "asset_hashing": "해시 확인 중...",
   "asset_offline": "누락된 항목",
-  "asset_offline_description": "이 항목은 누락되었습니다. Immich가 파일 위치에 접근할 수 없습니다. 해당 위치에 접근이 가능하거나 파일이 존재하는지 확인한 뒤 라이브러리를 다시 스캔하세요.",
+  "asset_offline_description": "디스크에서 항목을 더이상 찾을 수 없습니다. 서버 관리자에게 연락하여 도움을 받으세요.",
   "asset_skipped": "건너뜀",
+  "asset_skipped_in_trash": "휴지통의 항목",
   "asset_uploaded": "업로드 완료",
   "asset_uploading": "업로드 중...",
   "assets": "항목",
@@ -394,11 +405,10 @@
   "assets_added_to_album_count": "앨범에 항목 {count, plural, one {#개} other {#개}} 추가됨",
   "assets_added_to_name_count": "{hasName, select, true {<b>{name}</b>} other {새 앨범}}에 항목 {count, plural, one {#개} other {#개}} 추가됨",
   "assets_count": "{count, plural, one {#개} other {#개}} 항목",
-  "assets_moved_to_trash": "항목 {count, plural, one {#개} other {#개}}를 휴지통으로 이동함",
   "assets_moved_to_trash_count": "휴지통으로 항목 {count, plural, one {#개} other {#개}} 이동됨",
   "assets_permanently_deleted_count": "항목 {count, plural, one {#개} other {#개}}가 영구적으로 삭제됨",
   "assets_removed_count": "항목 {count, plural, one {#개} other {#개}}를 제거했습니다.",
-  "assets_restore_confirmation": "휴지통으로 이동된 항목을 모두 복원하시겠습니까? 이 작업은 되돌릴 수 없습니다!",
+  "assets_restore_confirmation": "휴지통으로 이동된 항목을 모두 복원하시겠습니까? 이 작업은 되돌릴 수 없습니다! 누락된 항목의 경우 복원되지 않습니다.",
   "assets_restored_count": "항목 {count, plural, one {#개} other {#개}}를 복원했습니다.",
   "assets_trashed_count": "휴지통으로 항목 {count, plural, one {#개} other {#개}} 이동됨",
   "assets_were_part_of_album_count": "앨범에 이미 존재하는 {count, plural, one {항목} other {항목}}입니다.",
@@ -409,6 +419,7 @@
   "birthdate_saved": "생년월일이 성공적으로 저장되었습니다.",
   "birthdate_set_description": "생년월일은 사진 촬영 당시 인물의 나이를 계산하는 데 사용됩니다.",
   "blurred_background": "흐린 배경",
+  "bugs_and_feature_requests": "버그 제보 & 기능 요청",
   "build": "빌드",
   "build_image": "빌드 이미지",
   "bulk_delete_duplicates_confirmation": "비슷한 항목 {count, plural, one {#개} other {#개}}를 삭제하시겠습니까? 크기가 가장 큰 항목을 제외한 나머지 항목들이 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다!",
@@ -423,10 +434,6 @@
   "cannot_merge_people": "인물을 병합할 수 없습니다.",
   "cannot_undo_this_action": "이 작업은 되돌릴 수 없습니다!",
   "cannot_update_the_description": "설명을 변경할 수 없습니다.",
-  "cant_apply_changes": "변경 사항을 적용할 수 없음",
-  "cant_get_faces": "얼굴을 가져올 수 없음",
-  "cant_search_people": "인물을 검색할 수 없음",
-  "cant_search_places": "장소를 검색할 수 없음",
   "change_date": "날짜 변경",
   "change_expiration_time": "만료일 변경",
   "change_location": "위치 변경",
@@ -435,7 +442,7 @@
   "change_password": "비밀번호 변경",
   "change_password_description": "첫 로그인이거나 비밀번호가 초기화되어 비밀번호를 설정해야 합니다. 아래에 새 비밀번호를 입력하세요.",
   "change_your_password": "비밀번호 변경",
-  "changed_visibility_successfully": "숨김 여부가 성공적으로 변경되었습니다.",
+  "changed_visibility_successfully": "표시 여부가 성공적으로 변경되었습니다.",
   "check_all": "모두 확인",
   "check_logs": "로그 확인",
   "choose_matching_people_to_merge": "병합할 인물 선택",
@@ -512,11 +519,13 @@
   "delete_tag_confirmation_prompt": "{tagName} 태그를 삭제하시겠습니까?",
   "delete_user": "사용자 삭제",
   "deleted_shared_link": "공유 링크가 삭제되었습니다.",
+  "deletes_missing_assets": "디스크에 존재하지 않는 항목 제거",
   "description": "설명",
   "details": "상세 정보",
   "direction": "방향",
   "disabled": "비활성화됨",
   "disallow_edits": "뷰어로 설정",
+  "discord": "Discord",
   "discover": "탐색",
   "dismiss_all_errors": "모든 오류 무시",
   "dismiss_error": "오류 무시",
@@ -524,7 +533,8 @@
   "display_order": "표시 순서",
   "display_original_photos": "원본 이미지 표시",
   "display_original_photos_setting_description": "원본 사진이 웹과 호환되는 경우 섬네일 대신 원본을 표시합니다. 사진이 표시되는 속도가 느려질 수 있습니다.",
-  "do_not_show_again": "다시 표시하지 않음",
+  "do_not_show_again": "이 메시지를 다시 표시하지 않음",
+  "documentation": "문서",
   "done": "완료",
   "download": "다운로드",
   "download_include_embedded_motion_videos": "내장된 동영상",
@@ -537,75 +547,66 @@
   "duplicates": "비슷한 항목",
   "duplicates_description": "비슷한 항목들을 확인하고, 유지하거나 삭제할 항목 선택",
   "duration": "기간",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
   "edit": "편집",
   "edit_album": "앨범 수정",
-  "edit_avatar": "프로필 편집",
+  "edit_avatar": "프로필 수정",
   "edit_date": "날짜 변경",
   "edit_date_and_time": "날짜 및 시간 변경",
-  "edit_exclusion_pattern": "제외 규칙 편집",
-  "edit_faces": "인물 변경",
-  "edit_import_path": "가져올 경로 편집",
-  "edit_import_paths": "가져올 경로 편집",
+  "edit_exclusion_pattern": "제외 규칙 수정",
+  "edit_faces": "얼굴 수정",
+  "edit_import_path": "가져올 경로 수정",
+  "edit_import_paths": "가져올 경로 수정",
   "edit_key": "키 수정",
-  "edit_link": "링크 편집",
+  "edit_link": "링크 수정",
   "edit_location": "위치 변경",
   "edit_name": "이름 변경",
-  "edit_people": "인물 변경",
-  "edit_tag": "태그 편집",
+  "edit_people": "인물 수정",
+  "edit_tag": "태그 수정",
   "edit_title": "제목 변경",
   "edit_user": "사용자 수정",
-  "edited": "펀집되었습니다.",
+  "edited": "공유 링크가 수정되었습니다.",
   "editor": "편집자",
-  "editor_close_without_save_prompt": "변경 사항이 반영되지 않습니다.",
+  "editor_close_without_save_prompt": "변경 사항이 저장되지 않습니다.",
   "editor_close_without_save_title": "편집을 종료하시겠습니까?",
   "editor_crop_tool_h2_aspect_ratios": "종횡비",
   "editor_crop_tool_h2_rotation": "회전",
   "email": "이메일",
-  "empty": "",
-  "empty_album": "",
   "empty_trash": "휴지통 비우기",
-  "empty_trash_confirmation": "휴지통을 비우시겠습니까? 휴지통에 있는 모든 항목이 Immich에서 영구적으로 제거됩니다. 이 작업은 되돌릴 수 없습니다!",
+  "empty_trash_confirmation": "휴지통을 비우시겠습니까? 휴지통에 있는 모든 항목이 Immich에서 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다!",
   "enable": "활성화",
   "enabled": "활성화됨",
   "end_date": "종료일",
   "error": "오류",
-  "error_loading_image": "사진을 불러오는 중 문제가 발생했습니다.",
+  "error_loading_image": "이미지 로드 오류",
   "error_title": "오류 - 문제가 발생했습니다",
   "errors": {
     "cannot_navigate_next_asset": "다음 항목으로 이동할 수 없습니다.",
     "cannot_navigate_previous_asset": "이전 항목으로 이동할 수 없습니다.",
     "cant_apply_changes": "변경 사항을 적용할 수 없습니다.",
     "cant_change_activity": "활동을 {enabled, select, true {비활성화} other {활성화}}할 수 없습니다.",
-    "cant_change_asset_favorite": "즐겨찾기 상태를 변경할 수 없습니다.",
+    "cant_change_asset_favorite": "즐겨찾기에 추가/제거할 수 없습니다.",
     "cant_change_metadata_assets_count": "항목 {count, plural, one {#개} other {#개}}의 메타데이터를 변경할 수 없습니다.",
-    "cant_get_faces": "얼굴을 불러올 수 없습니다.",
-    "cant_get_number_of_comments": "댓글의 개수를 불러올 수 없습니다.",
-    "cant_search_people": "인물을 검색할 수 없습니다.",
-    "cant_search_places": "장소를 검색할 수 없습니다.",
+    "cant_get_faces": "얼굴을 불러올 수 없음",
+    "cant_get_number_of_comments": "댓글 수를 불러올 수 없음",
+    "cant_search_people": "인물을 검색할 수 없음",
+    "cant_search_places": "장소를 검색할 수 없음",
     "cleared_jobs": "{job} 작업 중단됨",
-    "error_adding_assets_to_album": "앨범에 항목을 추가하는 중 문제가 발생했습니다.",
-    "error_adding_users_to_album": "앨범에 사용자를 추가하는 중 문제가 발생했습니다.",
-    "error_deleting_shared_user": "공유한 사용자를 제거하는 중 문제가 발생했습니다.",
-    "error_downloading": "{filename} 다운로드 중 문제가 발생했습니다.",
-    "error_hiding_buy_button": "구매 버튼을 숨기는 중 문제가 발생했습니다.",
-    "error_removing_assets_from_album": "앨범에서 항목을 제거하는 중 문제가 발생했습니다. 콘솔에서 세부 정보를 확인하세요.",
-    "error_selecting_all_assets": "모든 항목을 선택하는 중 문제가 발생했습니다.",
+    "error_adding_assets_to_album": "앨범에 항목을 추가하던 중 오류가 발생했습니다.",
+    "error_adding_users_to_album": "앨범에 사용자를 추가하던 중 오류가 발생했습니다.",
+    "error_deleting_shared_user": "공유된 사용자를 제거하던 중 오류가 발생했습니다.",
+    "error_downloading": "{filename} 다운로드 오류",
+    "error_hiding_buy_button": "구매 버튼을 숨기던 중 오류가 발생했습니다.",
+    "error_removing_assets_from_album": "앨범에서 항목을 제거하던 중 오류가 발생했습니다. 콘솔에서 세부 정보를 확인하세요.",
+    "error_selecting_all_assets": "모든 항목을 선택하던 중 오류가 발생했습니다.",
     "exclusion_pattern_already_exists": "이 제외 규칙은 이미 존재합니다.",
     "failed_job_command": "{job} 작업 {command} 실패",
     "failed_to_create_album": "앨범을 생성하지 못했습니다.",
     "failed_to_create_shared_link": "공유 링크를 생성하지 못했습니다.",
-    "failed_to_edit_shared_link": "공유 링크를 편집하지 못했습니다.",
-    "failed_to_get_people": "인물을 불러오지 못했습니다.",
-    "failed_to_load_asset": "항목을 불러오지 못했습니다.",
-    "failed_to_load_assets": "항목을 불러오지 못했습니다.",
-    "failed_to_load_people": "인물을 불러오지 못했습니다.",
+    "failed_to_edit_shared_link": "공유 링크를 수정하지 못했습니다.",
+    "failed_to_get_people": "인물 로드 실패",
+    "failed_to_load_asset": "항목 로드 실패",
+    "failed_to_load_assets": "항목 로드 실패",
+    "failed_to_load_people": "인물 로드 실패",
     "failed_to_remove_product_key": "제품 키를 제거하지 못했습니다.",
     "failed_to_stack_assets": "스택을 만들지 못했습니다.",
     "failed_to_unstack_assets": "스택을 해제하지 못했습니다.",
@@ -626,14 +627,12 @@
     "unable_to_archive_unarchive": "{archived, select, true {보관함으로 항목을 이동할} other {보관함에서 항목을 제거할}} 수 없습니다.",
     "unable_to_change_album_user_role": "사용자의 역할을 변경할 수 없습니다.",
     "unable_to_change_date": "날짜를 변경할 수 없습니다.",
-    "unable_to_change_favorite": "즐겨찾기 상태를 변경할 수 없습니다.",
+    "unable_to_change_favorite": "즐겨찾기에 추가/제거할 수 없습니다.",
     "unable_to_change_location": "위치를 변경할 수 없습니다.",
     "unable_to_change_password": "비밀번호를 변경할 수 없습니다.",
-    "unable_to_change_visibility": "인물 {count, plural, one {#명} other {#명}}의 숨김 여부를 변경할 수 없습니다.",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
+    "unable_to_change_visibility": "인물 {count, plural, one {#명} other {#명}}의 표시 여부를 변경할 수 없음",
     "unable_to_complete_oauth_login": "OAuth 로그인을 완료할 수 없습니다.",
-    "unable_to_connect": "연결할 수 없습니다.",
+    "unable_to_connect": "연결할 수 없음",
     "unable_to_connect_to_server": "서버에 연결할 수 없습니다.",
     "unable_to_copy_to_clipboard": "클립보드에 복사할 수 없습니다. https를 통해 접속 중인지 확인하세요.",
     "unable_to_create_admin_account": "관리자 계정을 생성할 수 없습니다.",
@@ -642,20 +641,21 @@
     "unable_to_create_user": "사용자를 생성할 수 없습니다.",
     "unable_to_delete_album": "앨범을 삭제할 수 없습니다.",
     "unable_to_delete_asset": "항목을 삭제할 수 없습니다.",
-    "unable_to_delete_assets": "항목을 삭제하는 중 문제가 발생했습니다.",
+    "unable_to_delete_assets": "항목 삭제 중 오류 발생",
     "unable_to_delete_exclusion_pattern": "제외 규칙을 삭제할 수 없습니다.",
-    "unable_to_delete_import_path": "가져올 경로를 삭제할 수 없습니다.",
+    "unable_to_delete_import_path": "가져오기 경로를 삭제할 수 없습니다.",
     "unable_to_delete_shared_link": "공유 링크를 삭제할 수 없습니다.",
     "unable_to_delete_user": "사용자를 삭제할 수 없습니다.",
     "unable_to_download_files": "파일을 다운로드할 수 없습니다.",
-    "unable_to_edit_exclusion_pattern": "제외 규칙을 편집할 수 없습니다.",
-    "unable_to_edit_import_path": "가져올 경로를 편집할 수 없습니다.",
+    "unable_to_edit_exclusion_pattern": "제외 규칙을 수정할 수 없습니다.",
+    "unable_to_edit_import_path": "가져오기 경로를 수정할 수 없습니다.",
     "unable_to_empty_trash": "휴지통을 비울 수 없습니다.",
     "unable_to_enter_fullscreen": "전체 화면으로 전환할 수 없습니다.",
-    "unable_to_exit_fullscreen": "전체 화면을 종료할 수 없습니다.",
-    "unable_to_get_comments_number": "댓글의 개수를 불러올 수 없습니다.",
+    "unable_to_exit_fullscreen": "전체 화면에서 나갈 수 없습니다.",
+    "unable_to_get_comments_number": "댓글 수를 불러올 수 없습니다.",
     "unable_to_get_shared_link": "공유 링크를 불러오지 못했습니다.",
     "unable_to_hide_person": "인물을 숨길 수 없습니다.",
+    "unable_to_link_motion_video": "모션 비디오를 연결할 수 없습니다",
     "unable_to_link_oauth_account": "OAuth 계정을 연결할 수 없습니다.",
     "unable_to_load_album": "앨범을 불러올 수 없습니다.",
     "unable_to_load_asset_activity": "사용자의 반응을 불러올 수 없습니다.",
@@ -665,23 +665,21 @@
     "unable_to_log_out_device": "기기에서 로그아웃할 수 없습니다.",
     "unable_to_login_with_oauth": "OAuth로 로그인할 수 없습니다.",
     "unable_to_play_video": "동영상을 재생할 수 없습니다.",
-    "unable_to_reassign_assets_existing_person": "항목을 {name, select, null {다른 인물에} other {#에}} 할당할 수 없습니다.",
+    "unable_to_reassign_assets_existing_person": "항목을 {name, select, null {다른 인물에게} other {{name}에게}} 할당할 수 없습니다.",
     "unable_to_reassign_assets_new_person": "항목을 새 인물에 할당할 수 없습니다.",
     "unable_to_refresh_user": "사용자를 새로 고칠 수 없습니다.",
     "unable_to_remove_album_users": "앨범에서 사용자를 제거할 수 없습니다.",
     "unable_to_remove_api_key": "API 키를 삭제할 수 없습니다.",
     "unable_to_remove_assets_from_shared_link": "공유 링크에서 항목을 제거할 수 없습니다.",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_deleted_assets": "누락된 파일을 제거할 수 없습니다.",
     "unable_to_remove_library": "라이브러리를 제거할 수 없습니다.",
-    "unable_to_remove_offline_files": "누락된 파일을 제거할 수 없습니다.",
     "unable_to_remove_partner": "파트너를 제거할 수 없습니다.",
     "unable_to_remove_reaction": "반응을 제거할 수 없습니다.",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "항목을 수리할 수 없습니다.",
     "unable_to_reset_password": "비밀번호를 초기화할 수 없습니다.",
     "unable_to_resolve_duplicate": "비슷한 항목을 처리할 수 없습니다.",
     "unable_to_restore_assets": "항목을 복원할 수 없습니다.",
-    "unable_to_restore_trash": "휴지통을 복원할 수 없습니다.",
+    "unable_to_restore_trash": "휴지통에서 항목을 복원할 수 없음",
     "unable_to_restore_user": "사용자 삭제를 취소할 수 없습니다.",
     "unable_to_save_album": "앨범을 저장할 수 없습니다.",
     "unable_to_save_api_key": "API 키를 수정할 수 없습니다.",
@@ -694,21 +692,18 @@
     "unable_to_set_feature_photo": "대표 사진을 지정할 수 없습니다.",
     "unable_to_set_profile_picture": "프로필 사진을 설정할 수 없습니다.",
     "unable_to_submit_job": "작업을 수행할 수 없습니다.",
-    "unable_to_trash_asset": "휴지통으로 이동할 수 없습니다.",
+    "unable_to_trash_asset": "휴지통으로 항목을 이동할 수 없음",
     "unable_to_unlink_account": "계정 연결을 해제할 수 없습니다.",
+    "unable_to_unlink_motion_video": "모션 비디오 연결을 해제할 수 없습니다.",
     "unable_to_update_album_cover": "앨범 커버를 변경할 수 없습니다.",
     "unable_to_update_album_info": "앨범 정보를 변경할 수 없습니다.",
     "unable_to_update_library": "라이브러리를 업데이트할 수 없습니다.",
     "unable_to_update_location": "위치를 변경할 수 없습니다.",
     "unable_to_update_settings": "설정을 변경할 수 없습니다.",
-    "unable_to_update_timeline_display_status": "타임라인 표시 설정을 변경할 수 없습니다.",
+    "unable_to_update_timeline_display_status": "타임라인 표시 여부를 변경할 수 없습니다.",
     "unable_to_update_user": "사용자를 업데이트할 수 없습니다.",
     "unable_to_upload_file": "파일을 업로드할 수 없습니다."
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exif": "EXIF",
   "exit_slideshow": "슬라이드 쇼 종료",
   "expand_all": "모두 확장",
@@ -723,33 +718,27 @@
   "external": "외부",
   "external_libraries": "외부 라이브러리",
   "face_unassigned": "알 수 없음",
-  "failed_to_get_people": "인물 불러오기 실패",
   "favorite": "즐겨찾기",
-  "favorite_or_unfavorite_photo": "즐겨찾기 추가 및 제거",
+  "favorite_or_unfavorite_photo": "즐겨찾기 추가/제거",
   "favorites": "즐겨찾기",
-  "feature": "",
-  "feature_photo_updated": "대표 사진이 설정되었습니다.",
-  "featurecollection": "",
+  "feature_photo_updated": "대표 사진 업데이트됨",
   "features": "기능",
   "features_setting_description": "앱 기능 관리",
   "file_name": "파일 이름",
   "file_name_or_extension": "파일명 또는 확장자",
   "filename": "파일명",
-  "files": "",
   "filetype": "파일 형식",
   "filter_people": "인물 필터",
   "find_them_fast": "이름으로 검색하여 빠르게 찾기",
   "fix_incorrect_match": "잘못된 분류 수정",
   "folders": "폴더",
   "folders_feature_description": "파일 시스템의 사진 및 동영상을 폴더 뷰로 탐색",
-  "force_re-scan_library_files": "모든 파일 강제 다시 스캔",
   "forward": "앞으로",
   "general": "일반",
   "get_help": "도움 요청",
   "getting_started": "시작하기",
   "go_back": "뒤로",
   "go_to_search": "검색으로 이동",
-  "go_to_share_page": "공유 페이지로 이동",
   "group_albums_by": "다음으로 앨범 그룹화...",
   "group_no": "그룹화 없음",
   "group_owner": "소유자로 그룹화",
@@ -775,10 +764,6 @@
   "image_alt_text_date_place_2_people": "{date} {country}, {city}에서 {person1}, {person2}님과 함께한 {isVideo, select, true {동영상} other {사진}}",
   "image_alt_text_date_place_3_people": "{date} {country}, {city}에서 {person1}, {person2}님 및 {person3}님과 함께한 {isVideo, select, true {동영상} other {사진}}",
   "image_alt_text_date_place_4_or_more_people": "{date} {country}, {city}에서 {person1}, {person2}님 및 {additionalCount, number}명과 함께한 {isVideo, select, true {동영상} other {사진}}",
-  "image_alt_text_people": "{count, plural, =1 {{person1}님과 함께,} =2 {{person1} 및 {person2}님과 함께,} =3 {{person1}, {person2} 및 {person3}님과 함께,} other {{person1}, {person2}, 및 {others, number}명과 함께,}}",
-  "image_alt_text_place": "{country}, {city}에서",
-  "image_taken": "{isVideo, select, true {동영상} other {사진}},",
-  "img": "",
   "immich_logo": "Immich 로고",
   "immich_web_interface": "Immich 웹 인터페이스",
   "import_from_json": "JSON에서 가져오기",
@@ -799,7 +784,6 @@
   "invite_people": "사용자 초대",
   "invite_to_album": "앨범으로 초대",
   "items_count": "{count, plural, one {#개} other {#개}} 항목",
-  "job_settings_description": "",
   "jobs": "작업",
   "keep": "유지",
   "keep_all": "모두 유지",
@@ -814,16 +798,9 @@
   "level": "레벨",
   "library": "라이브러리",
   "library_options": "라이브러리 옵션",
-  "license_account_info": "라이선스가 등록된 계정입니다.",
-  "license_activated_subtitle": "Immich와 오픈소스 소프트웨어를 지원해주셔서 감사합니다.",
-  "license_activated_title": "라이선스가 성공적으로 활성화되었습니다.",
-  "license_button_activate": "활성화",
-  "license_button_buy": "구입",
-  "license_button_buy_license": "라이선스 구입",
-  "license_button_select": "선택",
-  "license_failed_activation": "라이선스를 활성화하지 못했습니다. 이메일로 발송된 키를 정확히 입력했는지 확인하세요!",
   "light": "라이트",
   "like_deleted": "좋아요가 삭제되었습니다.",
+  "link_motion_video": "모션 비디오 링크",
   "link_options": "링크 옵션",
   "link_to_oauth": "OAuth에 연결",
   "linked_oauth_account": "OAuth 계정이 연결되었습니다.",
@@ -841,7 +818,8 @@
   "longitude": "경도",
   "look": "보기",
   "loop_videos": "동영상 반복",
-  "loop_videos_description": "상세 보기에서 동영상을 자동으로 반복 재생합니다.",
+  "loop_videos_description": "상세 보기에서 자동으로 동영상을 반복 재생합니다.",
+  "main_branch_warning": "현재 개발 버전을 사용 중입니다. 정식 버전을 사용하는 것을 강력히 권장합니다!",
   "make": "제조사",
   "manage_shared_links": "공유 링크 관리",
   "manage_sharing_with_partners": "파트너와 공유 관리",
@@ -863,10 +841,10 @@
   "menu": "메뉴",
   "merge": "병합",
   "merge_people": "인물 병합",
-  "merge_people_limit": "한 번에 최대 5개의 얼굴만 병합할 수 있습니다.",
+  "merge_people_limit": "한 번에 최대 5개의 얼굴만 합칠 수 있습니다.",
   "merge_people_prompt": "인물들을 병합하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
-  "merge_people_successfully": "인물을 성공적으로 병합했습니다.",
-  "merged_people_count": "인물 {count, plural, one {#명} other {#명}}을 병합했습니다.",
+  "merge_people_successfully": "인물을 성공적으로 합쳤습니다.",
+  "merged_people_count": "인물 {count, plural, one {#명} other {#명}}을 합쳤습니다.",
   "minimize": "최소화",
   "minute": "분",
   "missing": "누락",
@@ -892,12 +870,12 @@
   "no_albums_with_name_yet": "아직 해당하는 이름의 앨범이 없는 것 같습니다.",
   "no_albums_yet": "아직 앨범이 없는 것 같습니다.",
   "no_archived_assets_message": "사진과 동영상을 보관함으로 이동하여 목록에서 숨기기",
-  "no_assets_message": "이곳을 클릭하여 첫 이미지를 업로드하세요",
+  "no_assets_message": "여기를 클릭하여 첫 사진을 업로드하세요.",
   "no_duplicates_found": "비슷한 항목을 찾을 수 없습니다.",
   "no_exif_info_available": "EXIF 정보 없음",
   "no_explore_results_message": "더 많은 사진을 업로드하여 탐색 기능을 사용하세요.",
   "no_favorites_message": "즐겨찾기에 좋아하는 사진과 동영상을 추가하기",
-  "no_libraries_message": "외부 라이브러리를 생성하여 사진과 동영상 가져오기",
+  "no_libraries_message": "외부 라이브러리를 생성하여 기존 사진과 동영상을 확인하세요.",
   "no_name": "이름 없음",
   "no_places": "장소 없음",
   "no_results": "결과가 없습니다.",
@@ -911,6 +889,7 @@
   "notifications": "알림",
   "notifications_setting_description": "알림 설정 관리",
   "oauth": "OAuth",
+  "official_immich_resources": "Immich 공식 리소스",
   "offline": "오프라인",
   "offline_paths": "누락된 파일",
   "offline_paths_description": "외부 라이브러리의 항목이 아닌 파일을 수동으로 삭제한 경우 발생할 수 있습니다.",
@@ -918,14 +897,12 @@
   "oldest_first": "오래된 순",
   "onboarding": "온보딩",
   "onboarding_privacy_description": "이 선택적 기능은 외부 서비스를 사용하며, 관리자 설정에서 언제든 비활성화할 수 있습니다.",
-  "onboarding_storage_template_description": "활성화한 경우, 사용자 정의 템플릿을 기반으로 파일을 자동 분류합니다. 안정성 문제로 인해 해당 기능은 기본적으로 비활성화 되어 있습니다. 자세한 내용은 [공식 문서]를 참조하세요.",
   "onboarding_theme_description": "색상 테마를 선택하세요. 나중에 설정에서 변경할 수 있습니다.",
   "onboarding_welcome_description": "몇 가지 일반적인 설정을 진행하겠습니다.",
   "onboarding_welcome_user": "{user}님, 환영합니다",
   "online": "온라인",
-  "only_favorites": "즐겨찾기만 표시",
-  "only_refreshes_modified_files": "변경된 파일만 다시 스캔",
-  "open_in_map_view": "지도 뷰에서 보기",
+  "only_favorites": "즐겨찾기만",
+  "open_in_map_view": "지도 보기에서 열기",
   "open_in_openstreetmap": "OpenStreetMap에서 열기",
   "open_the_search_filters": "검색 필터 열기",
   "options": "옵션",
@@ -959,17 +936,15 @@
   "paused": "일시 정지됨",
   "pending": "진행 중",
   "people": "인물",
-  "people_edits_count": "인물 {count, plural, one {#명} other {#명}}을 변경했습니다.",
+  "people_edits_count": "인물 {count, plural, one {#명} other {#명}}을 수정했습니다.",
   "people_feature_description": "사진 및 동영상을 인물 그룹별로 탐색",
   "people_sidebar_description": "사이드바에 인물 링크 표시",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "영구 삭제 경고",
   "permanent_deletion_warning_setting_description": "항목을 영구적으로 삭제하기 전 경고 메시지 표시",
   "permanently_delete": "영구 삭제",
   "permanently_delete_assets_count": "{count, plural, one {항목} other {항목}} 영구 삭제",
   "permanently_delete_assets_prompt": "{count, plural, one {이 항목을} other {항목 <b>#</b>개를}} 영구적으로 삭제하시겠습니까? {count, plural, one {항목이} other {항목이}} 앨범에 포함된 경우 앨범에서도 제거됩니다.",
   "permanently_deleted_asset": "항목이 영구적으로 삭제되었습니다.",
-  "permanently_deleted_assets": "항목 {count, plural, one {#개} other {#개}}가 영구적으로 삭제됨",
   "permanently_deleted_assets_count": "항목 {count, plural, one {#개} other {#개}}가 영구적으로 삭제되었습니다.",
   "person": "인물",
   "person_hidden": "{name}{hidden, select, true { (숨김)} other {}}",
@@ -985,7 +960,6 @@
   "play_memories": "추억 재생",
   "play_motion_photo": "모션 포토 재생",
   "play_or_pause_video": "동영상 재생, 일시 정지",
-  "point": "",
   "port": "포트",
   "preset": "사전 설정",
   "preview": "미리 보기",
@@ -993,7 +967,7 @@
   "previous_memory": "이전 추억",
   "previous_or_next_photo": "이전 또는 다음 이미지로",
   "primary": "주요",
-  "privacy": "프라이버시",
+  "privacy": "개인 정보",
   "profile_image_of_user": "{user}님의 프로필 이미지",
   "profile_picture_set": "프로필 사진이 설정되었습니다.",
   "public_album": "공개 앨범",
@@ -1011,7 +985,7 @@
   "purchase_button_select": "선택",
   "purchase_failed_activation": "등록하지 못했습니다. 이메일로 전송된 키를 정확히 입력했는지 확인하세요!",
   "purchase_individual_description_1": "개인 사용자용",
-  "purchase_individual_description_2": "서포터 배지 및 표시",
+  "purchase_individual_description_2": "서포터 배지",
   "purchase_individual_title": "개인",
   "purchase_input_suggestion": "제품 키를 보유하고 있나요? 아래에 제품 키를 입력하세요.",
   "purchase_license_subtitle": "Immich를 구매하여 지속적인 개발에 도움을 주세요.",
@@ -1027,41 +1001,41 @@
   "purchase_remove_server_product_key": "서버 제품 키 제거",
   "purchase_remove_server_product_key_prompt": "서버 제품 키를 제거하시겠습니까?",
   "purchase_server_description_1": "서버 전체에 적용",
-  "purchase_server_description_2": "서포터 배지 및 표시",
+  "purchase_server_description_2": "서포터 배지",
   "purchase_server_title": "서버",
   "purchase_settings_server_activated": "서버 제품 키는 관리자가 관리합니다.",
-  "range": "",
   "rating": "등급",
   "rating_clear": "등급 초기화",
   "rating_count": "{count, plural, one {#점} other {#점}}",
-  "rating_description": "상세 정보에 EXIF의 등급 정보 표시",
-  "raw": "",
+  "rating_description": "상세 정보 패널에 EXIF 등급 태그 표시",
   "reaction_options": "반응 옵션",
   "read_changelog": "변경 사항 보기",
   "reassign": "다시 할당",
   "reassigned_assets_to_existing_person": "항목 {count, plural, one {#개} other {#개}}가 {name, select, null {다른 인물에} other {{name}에}} 할당되었습니다.",
   "reassigned_assets_to_new_person": "항목 {count, plural, one {#개} other {#개}}가 새 인물에 할당되었습니다.",
-  "reassing_hint": "선택한 항목의 인물 변경",
+  "reassing_hint": "기존 인물에 선택한 항목 할당",
   "recent": "최근",
   "recent_searches": "최근 검색",
   "refresh": "새로고침",
   "refresh_encoded_videos": "동영상 재인코딩",
+  "refresh_faces": "얼굴 새로고침",
   "refresh_metadata": "메타데이터 갱신",
   "refresh_thumbnails": "섬네일 다시 생성",
   "refreshed": "새로고침이 완료되었습니다.",
-  "refreshes_every_file": "모든 파일을 다시 스캔",
+  "refreshes_every_file": "기존 파일 및 새 파일 스캔",
   "refreshing_encoded_video": "인코딩을 다시 진행하는 중...",
-  "refreshing_metadata": "메타데이터를 갱신하는 중...",
+  "refreshing_faces": "얼굴 새로고침 중...",
+  "refreshing_metadata": "메타데이터를 새로 고치는 중...",
   "regenerating_thumbnails": "섬네일을 다시 생성하는 중...",
   "remove": "제거",
   "remove_assets_album_confirmation": "앨범에서 항목 {count, plural, one {#개} other {#개}}를 제거하시겠습니까?",
   "remove_assets_shared_link_confirmation": "공유 링크에서 항목 {count, plural, one {#개} other {#개}}를 제거하시겠습니까?",
   "remove_assets_title": "항목을 제거하시겠습니까?",
   "remove_custom_date_range": "맞춤 기간 제거",
+  "remove_deleted_assets": "누락된 파일 제거",
   "remove_from_album": "앨범에서 제거",
   "remove_from_favorites": "즐겨찾기에서 제거",
   "remove_from_shared_link": "공유 링크에서 제거",
-  "remove_offline_files": "누락된 파일 제거",
   "remove_user": "사용자 삭제",
   "removed_api_key": "API 키 삭제: {name}",
   "removed_from_archive": "보관함에서 제거되었습니다.",
@@ -1070,15 +1044,14 @@
   "removed_tagged_assets": "항목 {count, plural, one {#개} other {#개}}에서 태그를 제거함",
   "rename": "이름 바꾸기",
   "repair": "수리",
-  "repair_no_results_message": "추적되지 않거나 누락된 파일이 이곳에 표시됩니다.",
+  "repair_no_results_message": "추적되지 않거나 누락된 파일이 여기에 표시됩니다.",
   "replace_with_upload": "파일 바꾸기",
   "repository": "리포지터리",
   "require_password": "비밀번호 필요",
   "require_user_to_change_password_on_first_login": "사용자가 처음 로그인할 때 비밀번호를 변경하도록 요구",
   "reset": "초기화",
   "reset_password": "비밀번호 재설정",
-  "reset_people_visibility": "인물 숨김 여부 초기화",
-  "reset_settings_to_default": "",
+  "reset_people_visibility": "인물 표시 여부 초기화",
   "reset_to_default": "기본값으로 복원",
   "resolve_duplicates": "비슷한 항목 확인",
   "resolved_all_duplicates": "비슷한 항목을 모두 확인했습니다.",
@@ -1098,8 +1071,7 @@
   "saved_settings": "설정이 저장되었습니다.",
   "say_something": "댓글을 입력하세요",
   "scan_all_libraries": "모든 라이브러리 스캔",
-  "scan_all_library_files": "모든 파일 다시 스캔",
-  "scan_new_library_files": "새 라이브러리 파일 스캔",
+  "scan_library": "스캔",
   "scan_settings": "스캔 설정",
   "scanning_for_album": "앨범을 스캔하는 중...",
   "search": "검색",
@@ -1114,8 +1086,10 @@
   "search_for_existing_person": "존재하는 인물 검색",
   "search_no_people": "인물이 없습니다.",
   "search_no_people_named": "\"{name}\" 인물을 찾을 수 없음",
+  "search_options": "검색 옵션",
   "search_people": "인물 검색",
   "search_places": "장소 검색",
+  "search_settings": "설정 검색",
   "search_state": "지역 검색...",
   "search_tags": "태그로 검색...",
   "search_timezone": "시간대 검색...",
@@ -1140,7 +1114,6 @@
   "selected_count": "{count, plural, other {#개}} 항목 선택됨",
   "send_message": "메시지 전송",
   "send_welcome_email": "환영 이메일 전송",
-  "server": "서버",
   "server_offline": "오프라인",
   "server_online": "온라인",
   "server_stats": "서버 통계",
@@ -1173,25 +1146,28 @@
   "show_and_hide_people": "인물 숨기기",
   "show_file_location": "파일 위치 표시",
   "show_gallery": "갤러리 표시",
-  "show_hidden_people": "숨긴 인물 표시",
+  "show_hidden_people": "숨겨진 인물 표시",
   "show_in_timeline": "타임라인에 표시",
-  "show_in_timeline_setting_description": "이 사용자의 사진 및 동영상을 타임라인에 표시",
+  "show_in_timeline_setting_description": "타임라인에 이 사용자의 사진과 동영상을 표시",
   "show_keyboard_shortcuts": "키보드 단축키 표시",
   "show_metadata": "메타데이터 표시",
-  "show_or_hide_info": "정보 표시 및 숨기기",
+  "show_or_hide_info": "정보 표시/숨기기",
   "show_password": "비밀번호 표시",
   "show_person_options": "인물 옵션 표시",
   "show_progress_bar": "진행 표시줄 표시",
   "show_search_options": "검색 옵션 표시",
+  "show_slideshow_transition": "슬라이드 전환 표시",
   "show_supporter_badge": "서포터 배지",
   "show_supporter_badge_description": "서포터 배지 표시",
   "shuffle": "셔플",
   "sidebar": "사이드바",
-  "sidebar_display_description": "뷰 링크를 사이드바에 표시",
+  "sidebar_display_description": "보기 링크를 사이드바에 표시",
   "sign_out": "로그아웃",
   "sign_up": "로그인",
   "size": "크기",
   "skip_to_content": "항목으로 건너뛰기",
+  "skip_to_folders": "폴더로 건너뛰기",
+  "skip_to_tags": "태그로 건너뛰기",
   "slideshow": "슬라이드 쇼",
   "slideshow_settings": "슬라이드 쇼 설정",
   "sort_albums_by": "다음으로 앨범 정렬...",
@@ -1206,7 +1182,7 @@
   "stack_duplicates": "비슷한 항목 스택",
   "stack_select_one_photo": "스택의 대표 사진 선택",
   "stack_selected_photos": "선택한 이미지 스택",
-  "stacked_assets_count": "항목 {count, plural, one {#개} other {#개}}의 스택을 만들었습니다.",
+  "stacked_assets_count": "항목 {count, plural, one {#개} other {#개}} 스택됨",
   "stacktrace": "스택 추적",
   "start": "시작",
   "start_date": "시작일",
@@ -1222,14 +1198,17 @@
   "submit": "확인",
   "suggestions": "추천",
   "sunrise_on_the_beach": "동해안에서 맞이하는 새해 일출",
+  "support": "지원",
+  "support_and_feedback": "지원 & 제안",
+  "support_third_party_description": "Immich가 서드파티 패키지로 설치 되었습니다. 링크를 눌러 먼저 패키지 문제인지 확인해 보세요.",
   "swap_merge_direction": "병합 방향 변경",
   "sync": "동기화",
   "tag": "태그",
   "tag_assets": "항목 태그",
-  "tag_created": "{tag} 태그가 생성되었습니다.",
+  "tag_created": "태그 생성됨: {tag}",
   "tag_feature_description": "사진 및 동영상을 주제별 그룹화된 태그로 탐색",
-  "tag_not_found_question": "태그를 찾을 수 없나요? <link>이곳</link>에서 생성하세요.",
-  "tag_updated": "{tag} 태그를 수정했습니다.",
+  "tag_not_found_question": "태그를 찾을 수 없나요? <link>새 태그를 생성하세요.</link>",
+  "tag_updated": "태그 업데이트됨: {tag}",
   "tagged_assets": "항목 {count, plural, one {#개} other {#개}}에 태그를 적용함",
   "tags": "태그",
   "template": "템플릿",
@@ -1237,34 +1216,33 @@
   "theme_selection": "테마 설정",
   "theme_selection_description": "브라우저 및 시스템 기본 설정에 따라 라이트 모드와 다크 모드를 자동으로 설정",
   "they_will_be_merged_together": "선택한 인물들이 병합됩니다.",
+  "third_party_resources": "서드 파티 리소스",
   "time_based_memories": "시간 기준 추억",
   "timezone": "시간대",
   "to_archive": "보관함으로 이동",
   "to_change_password": "비밀번호 변경",
   "to_favorite": "즐겨찾기",
   "to_login": "로그인",
-  "to_root": "루트",
+  "to_parent": "상위 항목으로",
   "to_trash": "삭제",
   "toggle_settings": "설정 변경",
   "toggle_theme": "다크 모드 사용",
-  "toggle_visibility": "숨김 여부 변경",
   "total_usage": "총 사용량",
   "trash": "휴지통",
   "trash_all": "모두 삭제",
   "trash_count": "{count, number}개 삭제",
   "trash_delete_asset": "휴지통 이동/삭제",
-  "trash_no_results_message": "휴지통으로 이동된 항목이 이곳에 표시됩니다.",
+  "trash_no_results_message": "삭제된 사진과 동영상이 여기에 표시됩니다.",
   "trashed_items_will_be_permanently_deleted_after": "휴지통으로 이동된 항목은 {days, plural, one {#일} other {#일}} 후 영구적으로 삭제됩니다.",
   "type": "형식",
   "unarchive": "보관함에서 제거",
-  "unarchived": "보관 해제됨",
   "unarchived_count": "보관함에서 항목 {count, plural, other {#개}} 제거됨",
   "unfavorite": "즐겨찾기 해제",
   "unhide_person": "인물 숨김 해제",
   "unknown": "알 수 없음",
-  "unknown_album": "",
   "unknown_year": "알 수 없는 연도",
   "unlimited": "무제한",
+  "unlink_motion_video": "모션 비디오 링크 해제",
   "unlink_oauth": "OAuth 연결 해제",
   "unlinked_oauth_account": "OAuth 계정 연결이 해제되었습니다.",
   "unnamed_album": "이름 없는 앨범",
@@ -1281,7 +1259,7 @@
   "updated_password": "비밀번호가 변경되었습니다.",
   "upload": "업로드",
   "upload_concurrency": "업로드 동시성",
-  "upload_errors": "업로드가 완료되었습니다. 항목 {count, plural, one {#개} other {#개}}는 업로드하지 못했습니다. 업로드된 항목을 보려면 페이지를 새로고침하세요.",
+  "upload_errors": "업로드가 완료되었습니다. 항목 {count, plural, one {#개} other {#개}}를 업로드하지 못했습니다. 업로드된 항목을 보려면 페이지를 새로고침하세요.",
   "upload_progress": "전체 {total, number}개 중 {processed, number}개 완료, {remaining, number}개 대기 중",
   "upload_skipped_duplicates": "동일한 항목 {count, plural, one {#개} other {#개}}를 건너뛰었습니다.",
   "upload_status_duplicates": "중복",
@@ -1306,6 +1284,8 @@
   "version": "버전",
   "version_announcement_closing": "당신의 친구, Alex가",
   "version_announcement_message": "안녕하세요, 새 버전의 Immich를 사용할 수 있습니다. 자세한 내용은 <link>릴리스 노트</link>를 참조하세요. WatchTower 등의 자동 업데이트 기능을 사용하는 경우 의도하지 않은 동작을 방지하기 위해 <code>docker-compose.yml</code> 및 <code>.env</code> 구성이 최신인지 확인하세요.",
+  "version_history": "버전 기록",
+  "version_history_item": "{date} 버전 {version} 설치",
   "video": "동영상",
   "video_hover_setting": "마우스 오버 재생",
   "video_hover_setting_description": "마우스를 동영상 위에 올리면 재생이 시작됩니다. 비활성화된 경우에도 재생 아이콘에 마우스를 올리면 재생이 시작됩니다.",
@@ -1316,12 +1296,11 @@
   "view_all": "모두 보기",
   "view_all_users": "모든 사용자 보기",
   "view_in_timeline": "타임라인에서 보기",
-  "view_links": "링크 보기",
+  "view_links": "링크 확인",
   "view_next_asset": "다음 항목 보기",
   "view_previous_asset": "이전 항목 보기",
   "view_stack": "스택 보기",
-  "viewer": "뷰어",
-  "visibility_changed": "인물 {count, plural, one {#명} other {#명}}의 숨김 여부가 변경되었습니다.",
+  "visibility_changed": "인물 {count, plural, one {#명} other {#명}}의 표시 여부가 변경됨",
   "waiting": "대기",
   "warning": "경고",
   "week": "주",
@@ -1331,5 +1310,5 @@
   "years_ago": "{years, plural, one {#년} other {#년}} 전",
   "yes": "네",
   "you_dont_have_any_shared_links": "생성한 공유 링크가 없습니다.",
-  "zoom_image": "확대"
+  "zoom_image": "이미지 확대"
 }
diff --git a/web/src/lib/i18n/az.json b/i18n/lb.json
similarity index 100%
rename from web/src/lib/i18n/az.json
rename to i18n/lb.json
diff --git a/web/src/lib/i18n/lt.json b/i18n/lt.json
similarity index 77%
rename from web/src/lib/i18n/lt.json
rename to i18n/lt.json
index 18f4ee7c7f..cfb9701c16 100644
--- a/web/src/lib/i18n/lt.json
+++ b/i18n/lt.json
@@ -1,5 +1,5 @@
 {
-  "about": "Apie",
+  "about": "Atnaujinti",
   "account": "Paskyra",
   "account_settings": "Paskyros nustatymai",
   "acknowledge": "Patvirtinti",
@@ -16,62 +16,60 @@
   "add_exclusion_pattern": "Pridėti išimčių šabloną",
   "add_import_path": "Pridėti importavimo kelią",
   "add_location": "Pridėti vietovę",
-  "add_more_users": "Pridėti daugiau vartotojų",
+  "add_more_users": "Pridėti daugiau naudotojų",
   "add_partner": "Pridėti partnerį",
   "add_path": "Pridėti kelią",
   "add_photos": "Pridėti nuotraukų",
   "add_to": "Pridėti į ...",
   "add_to_album": "Pridėti į albumą",
   "add_to_shared_album": "Pridėti į bendrinamą albumą",
+  "add_url": "Pridėti URL",
   "added_to_archive": "Pridėta į archyvą",
   "added_to_favorites": "Pridėta prie mėgstamiausių",
-  "added_to_favorites_count": "{count, number} pridėta prie mėgstamiausių",
+  "added_to_favorites_count": "{count, plural, one {# pridėtas} few {# pridėti} other {# pridėta}} prie mėgstamiausių",
   "admin": {
     "authentication_settings": "Autentifikavimo nustatymai",
     "authentication_settings_description": "Tvarkyti slaptažodžių, OAuth ir kitus autentifikavimo parametrus",
     "authentication_settings_disable_all": "Ar tikrai norite išjungti visus prisijungimo būdus? Prisijungimas bus visiškai išjungtas.",
+    "authentication_settings_reenable": "Norėdami vėl įjungti, naudokite <link>Serverio komandą</link>.",
     "background_task_job": "Foninės užduotys",
+    "backup_database": "Duomenų bazės atsarginė kopija",
+    "backup_database_enable_description": "Įgalinti duomenų bazės atsarginė kopijas",
+    "backup_keep_last_amount": "Išsaugomų ankstesnių atsarginių duomenų bazės kopijų skaičius",
+    "backup_settings": "Atsarginės kopijos nustatymai",
     "check_all": "Pažymėti viską",
     "config_set_by_file": "Konfigūracija dabar nustatyta konfigūracinio failo",
     "confirm_delete_library": "Ar tikrai norite ištrinti {library} biblioteką?",
     "confirm_email_below": "Patvirtinimui įveskite \"{email}\" žemiau",
     "confirm_reprocess_all_faces": "Ar tikrai norite iš naujo apdoroti visus veidus? Tai taip pat ištrins įvardytus asmenis.",
     "confirm_user_password_reset": "Ar tikrai norite iš naujo nustatyti {user} slaptažodį?",
-    "crontab_guru": "",
     "disable_login": "Išjungti prisijungimą",
-    "disabled": "",
     "duplicate_detection_job_description": "Vykdykite mašininį mokymąsi tam, kad aptiktumėte panašius vaizdus. Nuo šios funkcijos priklauso išmanioji paieška",
     "exclusion_pattern_description": "Išimčių šablonai leidžia nepaisyti failų ir aplankų skenuojant jūsų biblioteką. Tai yra naudinga, jei turite aplankų su failais, kurių nenorite importuoti, pavyzdžiui, RAW failai.",
     "external_library_created_at": "Išorinė biblioteka (sukurta {date})",
     "external_library_management": "Išorinių bibliotekų tvarkymas",
-    "face_detection": "Veido atpažinimas",
+    "face_detection": "Veidų aptikimas",
+    "face_detection_description": "Veidų aptikimas bibliotekos elementuose naudojant mašininį mokymąsi. Vaizdo įrašų atveju naudojama tik miniatiūra. \"Atnaujinti\" iš naujo nuskaito visus bibliotekos elementus. \"Atstatyti\" ne tik atnaujina, bet ir išvalo visus esamus veidų duomenis. \"Trūkstami\" nuskaito tik dar nenuskaitytus bibliotekos elementus. Veidų aptikimo darbui pasibaigus, aptikti veidai patenka į veidų atpažinimo darbų eilę, kur jie priskiriami jau esamiems ar naujai atpažintiems žmonėms.",
+    "facial_recognition_job_description": "Aptiktų veidų atpažinimas ir priskyrimas žmonėms. Šis darbas vykdomas pasibaigus \"veidų aptikimo\" darbui. \"Atstatyti\" (per)grupuoja visus aptiktus veidus. \"Trūkstami\" apdoroja jokiam žmogui dar nepriskirtus aptiktus veidus.",
     "failed_job_command": "Darbo {job} komanda {command} nepavyko",
     "force_delete_user_warning": "ĮSPĖJIMAS: Šis veiksmas iš karto pašalins naudotoją ir visą jo informaciją. Šis žingsnis nesugrąžinamas ir failų nebus galima atkurti.",
     "forcing_refresh_library_files": "Priverstinai atnaujinami visi failai bilbiotekoje",
+    "image_format": "Formatas",
     "image_format_description": "WebP sukuria mažesnius failus nei JPEG, bet lėčiau juos apdoroja.",
     "image_prefer_embedded_preview": "Pageidautinai rodyti įterptą peržiūrą",
     "image_prefer_embedded_preview_setting_description": "",
     "image_prefer_wide_gamut": "Teikti pirmenybę plačiai gamai",
     "image_prefer_wide_gamut_setting_description": "",
-    "image_preview_format": "Peržiūros formatas",
-    "image_preview_resolution": "Peržiūros rezoliucija",
-    "image_preview_resolution_description": "Naudojama peržiūrint vieną nuotrauką ir mašininiam mokymui. Didesnė rezoliucija gali išsaugoti daugiau detalių, bet ilgiau užtrukti apdoroti ir sumažinti programos greitumą.",
     "image_quality": "Kokybė",
-    "image_quality_description": "Vaizdo kokybė nuo 1 iki 100. Aukštesnė kokybė yra geresnė, tačiau sukuriami didesni failai. Ši parinktis turi įtakos peržiūros ir miniatiūrų vaizdams.",
+    "image_resolution": "Rezoliucija",
     "image_settings": "Nuotraukos nustatymai",
     "image_settings_description": "Keisti sugeneruotų nuotraukų kokybę ir rezoliuciją",
-    "image_thumbnail_format": "Miniatūros formatas",
-    "image_thumbnail_resolution": "Miniatūros rezoliucija",
-    "image_thumbnail_resolution_description": "Naudojama žiūrint nuotraukų grupes (pagrindinis nuotraukų puslapis, albumų peržiūra ir t.t.). Aukštesnė rezoliucija gali išlaikyti daugiau detalių, bet užtrunka ilgiau apdoroti, gali turėti didesnius failų dydžius ir gali sumažinti programos greitumą.",
     "job_concurrency": "{job} lygiagretumas",
     "job_not_concurrency_safe": "Šis darbas nėra saugus apdoroti lygiagrečiai.",
-    "job_settings": "Darbo nustatymai",
+    "job_settings": "Darbų nustatymai",
     "job_settings_description": "Keisti darbų lygiagretumą",
     "job_status": "Darbų būsenos",
     "library_created": "Sukurta biblioteka: {library}",
-    "library_cron_expression": "Cron išraiška",
-    "library_cron_expression_description": "Nustatykite nuskaitymo intervalą naudodami „cron“ formatą. Daugiau informacijos rasite pvz. <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "",
     "library_deleted": "Biblioteka ištrinta",
     "library_import_path_description": "Nurodykite aplanką, kurį norite importuoti. Šiame aplanke, įskaitant poaplankius, bus nuskaityti vaizdai ir vaizdo įrašai.",
     "library_scanning": "Periodinis skanavimas",
@@ -93,19 +91,19 @@
     "machine_learning_duplicate_detection_setting_description": "",
     "machine_learning_enabled": "Įgalinti mašininį mokymąsi",
     "machine_learning_enabled_description": "Jei išjungta, visos „ML“ funkcijos bus išjungtos, nepaisant toliau pateiktų nustatymų.",
-    "machine_learning_facial_recognition": "Veido atpažinimas",
+    "machine_learning_facial_recognition": "Veidų atpažinimas",
     "machine_learning_facial_recognition_description": "Aptikti, atpažinti ir sugrupuoti veidus nuotraukose",
-    "machine_learning_facial_recognition_model": "Veido atpažinimo modelis",
+    "machine_learning_facial_recognition_model": "Veidų atpažinimo modelis",
     "machine_learning_facial_recognition_model_description": "",
-    "machine_learning_facial_recognition_setting": "Įgalinti veido atpažinimą",
+    "machine_learning_facial_recognition_setting": "Įgalinti veidų atpažinimą",
     "machine_learning_facial_recognition_setting_description": "",
-    "machine_learning_max_detection_distance": "",
+    "machine_learning_max_detection_distance": "Maksimalus aptikimo atstumas",
     "machine_learning_max_detection_distance_description": "Didžiausias atstumas tarp dviejų vaizdų, kad jie būtų laikomi dublikatais, svyruoja nuo 0,001 iki 0,1. Didesnės vertės aptiks daugiau dublikatų, tačiau gali būti klaidingai teigiami.",
     "machine_learning_max_recognition_distance": "Maksimalus atpažinimo atstumas",
     "machine_learning_max_recognition_distance_description": "",
     "machine_learning_min_detection_score": "",
     "machine_learning_min_detection_score_description": "",
-    "machine_learning_min_recognized_faces": "",
+    "machine_learning_min_recognized_faces": "Mažiausias atpažintų veidų skaičius",
     "machine_learning_min_recognized_faces_description": "Mažiausias atpažintų veidų skaičius asmeniui, kurį reikia sukurti. Tai padidinus, veido atpažinimas tampa tikslesnis, bet padidėja tikimybė, kad veidas žmogui nepriskirtas.",
     "machine_learning_settings": "Mašininio mokymosi nustatymai",
     "machine_learning_settings_description": "Tvarkyti mašininio mokymosi funkcijas ir nustatymus",
@@ -114,20 +112,29 @@
     "machine_learning_smart_search_enabled": "Įjungti išmaniąją paiešką",
     "machine_learning_smart_search_enabled_description": "Jei išjungta, vaizdai nebus užkoduoti išmaniajai paieškai.",
     "machine_learning_url_description": "Mašininio mokymosi serverio URL",
+    "manage_concurrency": "Tvarkyti lygiagretumą",
     "manage_log_settings": "",
     "map_dark_style": "Tamsioji tema",
-    "map_enable_description": "",
+    "map_enable_description": "Įgalinti žemėlapio funkcijas",
+    "map_gps_settings": "Žemėlapio ir GPS nustatymai",
+    "map_gps_settings_description": "Tvarkyti žemėlapio ir GPS (atvirkštinio geokodavimo) nustatymus",
     "map_light_style": "Šviesioji tema",
+    "map_manage_reverse_geocoding_settings": "Tvarkyti <link>atvirkštinio geokodavimo</link> nustatymus",
     "map_reverse_geocoding": "Atvirkštinis geokodavimas",
-    "map_reverse_geocoding_enable_description": "",
+    "map_reverse_geocoding_enable_description": "Įjungti atvirkštinį geokodavimą",
     "map_reverse_geocoding_settings": "Atvirkštinio geokodavimo nustatymai",
-    "map_settings": "Žemėlapio nustatymai",
+    "map_settings": "Žemėlapis",
     "map_settings_description": "Tvarkyti žemėlapio parametrus",
     "map_style_description": "",
-    "metadata_extraction_job_description": "",
+    "metadata_extraction_job": "Metaduomenų nuskaitymas",
+    "metadata_extraction_job_description": "Kiekvieno bibliotekos elemento metaduomenų nuskaitymas, tokių kaip GPS koordinatės, veidai ar rezoliucija",
+    "metadata_settings": "Metaduomenų nustatymai",
+    "metadata_settings_description": "Tvarkyti metaduomenų nustatymus",
+    "migration_job": "Migracija",
     "migration_job_description": "",
     "no_paths_added": "Keliai nepridėti",
     "no_pattern_added": "Šablonas nepridėtas",
+    "note_cannot_be_changed_later": "PASTABA: Vėliau to pakeisti negalima!",
     "notification_email_from_address": "",
     "notification_email_from_address_description": "",
     "notification_email_host_description": "",
@@ -141,7 +148,7 @@
     "notification_email_test_email_failed": "Nepavyko išsiųsti bandomojo el. laiško, patikrinkite savo nustatymus",
     "notification_email_test_email_sent": "Bandomasis el. laiškas buvo išsiųstas į {email}. Patikrinkite savo pašto dėžutę.",
     "notification_email_username_description": "",
-    "notification_enable_email_notifications": "",
+    "notification_enable_email_notifications": "Įgalinti el. pašto pranešimus",
     "notification_settings": "Pranešimų nustatymai",
     "notification_settings_description": "Tvarkyti pranešimų nustatymus, įskaitant el. pašto",
     "oauth_auto_launch": "Paleisti automatiškai",
@@ -172,19 +179,22 @@
     "password_settings_description": "Tvarkyti prisijungimo slaptažodžiu nustatymus",
     "paths_validated_successfully": "Visi keliai patvirtinti sėkmingai",
     "refreshing_all_libraries": "Perkraunamos visos bibliotekos",
-    "registration_description": "Kadangi esate pirmasis šio sistemos vartotojas, jums bus priskirta administratorius rolė, ir būsite atsakingas už administracines užduotis ir papildomų vartotojų kūrimą.",
+    "registration": "Administratoriaus registracija",
+    "registration_description": "Kadangi esate pirmasis šio sistemos naudotojas, jums bus priskirta administratoriaus rolė, ir būsite atsakingas už administracines užduotis ir papildomų naudotojų kūrimą.",
     "repair_all": "Pataisyti visus",
-    "require_password_change_on_login": "Reikalauti, kad vartotojas pasikeistų slaptažodį po pirmojo prisijungimo",
+    "require_password_change_on_login": "Reikalauti, kad naudotojas pasikeistų slaptažodį po pirmojo prisijungimo",
     "reset_settings_to_default": "Atstatyti nustatymus į numatytuosius",
+    "reset_settings_to_recent_saved": "Nustatymų atstatymas į neseniai išsaugotus nustatymus",
+    "send_welcome_email": "Siųsti sveikinimo el. laišką",
     "server_external_domain_settings": "Išorinis domenas",
     "server_external_domain_settings_description": "",
     "server_settings": "Serverio nustatymai",
     "server_settings_description": "Tvarkyti serverio nustatymus",
-    "server_welcome_message": "",
+    "server_welcome_message": "Sveikinimo pranešimas",
     "server_welcome_message_description": "Žinutė, rodoma prisijungimo puslapyje.",
     "sidecar_job_description": "",
     "slideshow_duration_description": "",
-    "smart_search_job_description": "",
+    "smart_search_job_description": "Vykdykite mašininį mokymąsi bibliotekos elementų išmaniajai paieškai",
     "storage_template_enable_description": "",
     "storage_template_hash_verification_enabled": "",
     "storage_template_hash_verification_enabled_description": "",
@@ -192,13 +202,13 @@
     "storage_template_settings": "",
     "storage_template_settings_description": "",
     "system_settings": "Sistemos nustatymai",
-    "theme_custom_css_settings": "",
+    "tag_cleanup_job": "Žymų išvalymas",
+    "theme_custom_css_settings": "Individualizuotas CSS",
     "theme_custom_css_settings_description": "",
     "theme_settings": "Temos nustatymai",
     "theme_settings_description": "",
     "thumbnail_generation_job": "Generuoti miniatiūras",
-    "thumbnail_generation_job_description": "",
-    "transcode_policy_description": "",
+    "thumbnail_generation_job_description": "Didelių, mažų ir neryškių miniatiūrų generavimas kiekvienam bibliotekos elementui, taip pat miniatiūrų generavimas kiekvienam asmeniui",
     "transcoding_acceleration_api": "Spartinimo API",
     "transcoding_acceleration_api_description": "",
     "transcoding_acceleration_nvenc": "NVENC (reikalinga NVIDIA GPU)",
@@ -210,7 +220,7 @@
     "transcoding_accepted_containers": "Priimami konteineriai",
     "transcoding_accepted_video_codecs": "",
     "transcoding_accepted_video_codecs_description": "",
-    "transcoding_advanced_options_description": "Parinktys, kurių daugelis vartotojų keisti neturėtų",
+    "transcoding_advanced_options_description": "Parinktys, kurių daugelis naudotojų keisti neturėtų",
     "transcoding_audio_codec": "Garso kodekas",
     "transcoding_audio_codec_description": "Opus yra aukščiausios kokybės variantas, tačiau turi mažesnį suderinamumą su senesniais įrenginiais ar programine įranga.",
     "transcoding_bitrate_description": "Vaizdo įrašai viršija maksimalią leistiną bitų spartą arba nėra priimtino formato",
@@ -219,9 +229,9 @@
     "transcoding_constant_rate_factor": "",
     "transcoding_constant_rate_factor_description": "",
     "transcoding_disabled_description": "",
-    "transcoding_hardware_acceleration": "",
+    "transcoding_hardware_acceleration": "Techninės įrangos spartinimas",
     "transcoding_hardware_acceleration_description": "",
-    "transcoding_hardware_decoding": "",
+    "transcoding_hardware_decoding": "Aparatinis dekodavimas",
     "transcoding_hardware_decoding_setting_description": "",
     "transcoding_hevc_codec": "HEVC kodekas",
     "transcoding_max_b_frames": "",
@@ -241,21 +251,19 @@
     "transcoding_settings": "",
     "transcoding_settings_description": "",
     "transcoding_target_resolution": "",
-    "transcoding_target_resolution_description": "",
+    "transcoding_target_resolution_description": "Didesnės skiriamosios gebos gali išsaugoti daugiau detalių, tačiau jas koduoti užtrunka ilgiau, failų dydžiai yra didesni ir gali sumažėti programos jautrumas.",
     "transcoding_temporal_aq": "",
     "transcoding_temporal_aq_description": "",
     "transcoding_threads": "",
     "transcoding_threads_description": "",
     "transcoding_tone_mapping": "",
     "transcoding_tone_mapping_description": "",
-    "transcoding_tone_mapping_npl": "",
-    "transcoding_tone_mapping_npl_description": "",
     "transcoding_transcode_policy": "",
     "transcoding_two_pass_encoding": "",
     "transcoding_two_pass_encoding_setting_description": "",
     "transcoding_video_codec": "Video kodekas",
     "transcoding_video_codec_description": "",
-    "trash_enabled_description": "",
+    "trash_enabled_description": "Įgalinti šiukšliadėžės funkcijas",
     "trash_number_of_days": "Dienų skaičius",
     "trash_number_of_days_description": "",
     "trash_settings": "Šiukšliadėžės nustatymai",
@@ -263,15 +271,17 @@
     "untracked_files": "Nesekami failai",
     "user_delete_delay_settings": "Ištrynimo delsa",
     "user_delete_delay_settings_description": "",
-    "user_password_has_been_reset": "Vartotojo slaptažodis buvo iš naujo nustatytas:",
-    "user_restore_description": "Vartotojo <b>{user}</b> paskyra bus atkurta.",
-    "user_settings": "Vartotojo nustatymai",
-    "user_settings_description": "Valdyti vartotojo nustatymus",
-    "user_successfully_removed": "Vartotojas {email} sėkmingai pašalintas.",
+    "user_management": "Naudotojų valdymas",
+    "user_password_has_been_reset": "Naudotojo slaptažodis buvo iš naujo nustatytas:",
+    "user_restore_description": "Naudotojo <b>{user}</b> paskyra bus atkurta.",
+    "user_settings": "Naudotojo nustatymai",
+    "user_settings_description": "Valdyti naudotojo nustatymus",
+    "user_successfully_removed": "Naudotojas {email} sėkmingai pašalintas.",
     "version_check_enabled_description": "",
     "version_check_settings": "Versijos tikrinimas",
     "version_check_settings_description": "Įjungti/išjungti naujos versijos pranešimus",
-    "video_conversion_job_description": ""
+    "video_conversion_job": "Vaizdo įrašų konvertavimas",
+    "video_conversion_job_description": "Vaizdo įrašų konvertavimas platesniam suderinamumui su naršyklėmis ir įrenginiais"
   },
   "admin_email": "Administratoriaus el. paštas",
   "admin_password": "Administratoriaus slaptažodis",
@@ -280,20 +290,21 @@
   "album_added": "Albumas pridėtas",
   "album_added_notification_setting_description": "Gauti el. pašto pranešimą, kai būsite pridėtas prie bendrinamo albumo",
   "album_cover_updated": "Albumo viršelis atnaujintas",
-  "album_delete_confirmation": "Ar tikrai norite ištrinti albumą {album}?\nJei šis albumas yra bendrinamas, kiti vartotojai nebegalės jo pasiekti.",
+  "album_delete_confirmation": "Ar tikrai norite ištrinti albumą {album}?",
   "album_info_updated": "Albumo informacija atnaujinta",
   "album_leave": "Palikti albumą?",
   "album_leave_confirmation": "Ar tikrai norite palikti albumą {album}?",
   "album_name": "Albumo pavadinimas",
   "album_options": "Albumo parinktys",
-  "album_remove_user": "Pašalinti vartotoją?",
-  "album_remove_user_confirmation": "Ar tikrai norite pašalinti vartotoją {user}?",
-  "album_share_no_users": "Atrodo, kad bendrinate šį albumą su visais vartotojais, arba neturite vartotojų, su kuriais galėtumėte bendrinti.",
+  "album_remove_user": "Pašalinti naudotoją?",
+  "album_remove_user_confirmation": "Ar tikrai norite pašalinti naudotoją {user}?",
+  "album_share_no_users": "Atrodo, kad bendrinate šį albumą su visais naudotojais, arba neturite naudotojų, su kuriais galėtumėte bendrinti.",
   "album_updated": "Albumas atnaujintas",
   "album_updated_setting_description": "Gauti pranešimą el. paštu, kai bendrinamas albumas turi naujų elementų",
   "album_user_removed": "Pašalintas {user}",
   "album_with_link_access": "Tegul visi, turintys nuorodą, mato šio albumo nuotraukas ir žmones.",
   "albums": "Albumai",
+  "albums_count": "{count, plural, one {# albumas} few {# albumai} other {# albumų}}",
   "all": "Visi",
   "all_albums": "Visi albumai",
   "all_people": "Visi žmonės",
@@ -307,11 +318,11 @@
   "api_keys": "API raktai",
   "app_settings": "Programos nustatymai",
   "appears_in": "",
-  "archive": "",
+  "archive": "Archyvas",
   "archive_or_unarchive_photo": "Archyvuoti arba išarchyvuoti nuotrauką",
   "archive_size": "Archyvo dydis",
   "archive_size_description": "Konfigūruoti archyvo dydį atsisiuntimams (GiB)",
-  "archived": "",
+  "archived_count": "{count, plural, other {# suarchyvuota}}",
   "are_these_the_same_person": "Ar tai tas pats asmuo?",
   "are_you_sure_to_do_this": "Ar tikrai norite tai daryti?",
   "asset_added_to_album": "Pridėta į albumą",
@@ -320,13 +331,23 @@
   "asset_offline": "",
   "asset_uploaded": "Įkelta",
   "asset_uploading": "Įkeliama...",
-  "assets": "",
+  "assets": "Elementai",
+  "assets_added_count": "{count, plural, one {Pridėtas # elementas} few {Pridėti # elementai} other {Pridėta # elementų}}",
+  "assets_added_to_album_count": "Į albumą {count, plural, one {įtrauktas # elementas} few {įtraukti # elementai} other {įtraukta # elementų}}",
+  "assets_added_to_name_count": "Į {hasName, select, true {<b>{name}</b>} other {naują}} albumą {count, plural, one {įtrauktas # elementas} few {įtraukti # elementai} other {įtraukta # elementų}}",
+  "assets_count": "{count, plural, one {# elementas} few {# elementai} other {# elementų}}",
+  "assets_moved_to_trash_count": "{count, plural, one {# elementas perkeltas} few {# elementai perkelti} other {# elementų perkelta}} į šiukšliadėžę",
+  "assets_permanently_deleted_count": "{count, plural, one {# elementas ištrintas} few {# elementai ištrinti} other {# elementų ištrinta}} visam laikui",
+  "assets_removed_count": "{count, plural, one {Pašalintas # elementas} few {Pašalinti # elementai} other {Pašalinta # elementų}}",
+  "assets_restored_count": "{count, plural, one {Atkurtas # elementas} few {Atkurti # elementai} other {Atkurta # elementų}}",
+  "assets_were_part_of_album_count": "{count, plural, one {# elementas} few {# elementai} other {# elementų}} jau prieš tai buvo albume",
   "authorized_devices": "Autorizuoti įrenginiai",
   "back": "Atgal",
   "back_close_deselect": "Atgal, uždaryti arba atžymėti",
   "backward": "",
   "birthdate_saved": "Sėkmingai išsaugota gimimo data",
   "blurred_background": "Neryškus fonas",
+  "bugs_and_feature_requests": "Klaidų ir funkcijų užklausos",
   "buy": "Įsigyti Immich",
   "camera": "Fotoaparatas",
   "camera_brand": "Fotoaparato prekės ženklas",
@@ -335,10 +356,6 @@
   "cancel_search": "Atšaukti paiešką",
   "cannot_merge_people": "Negalima sujungti asmenų",
   "cannot_update_the_description": "Negalima atnaujinti aprašymo",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
   "change_date": "Pakeisti datą",
   "change_expiration_time": "Pakeisti galiojimo trukmę",
   "change_location": "Pakeisti vietovę",
@@ -391,7 +408,9 @@
   "create_new_person": "Sukurti naują žmogų",
   "create_new_person_hint": "Priskirti pasirinktus elementus naujam žmogui",
   "create_new_user": "Sukurti naują varotoją",
-  "create_user": "Sukurti vartotoją",
+  "create_tag": "Sukurti žymą",
+  "create_tag_description": "Sukurti naują žymą. Įdėtinėms žymoms įveskite pilną kelią, įskaitant pasviruosius brūkšnius.",
+  "create_user": "Sukurti naudotoją",
   "created": "Sukurta",
   "current_device": "Dabartinis įrenginys",
   "custom_locale": "",
@@ -413,7 +432,9 @@
   "delete_library": "Ištrinti biblioteką",
   "delete_link": "Ištrinti nuorodą",
   "delete_shared_link": "Ištrinti bendrinamą nuorodą",
-  "delete_user": "Ištrinti vartotoją",
+  "delete_tag": "Ištrinti žymą",
+  "delete_tag_confirmation_prompt": "Ar tikrai norite ištrinti žymą {tagName}?",
+  "delete_user": "Ištrinti naudotoją",
   "deleted_shared_link": "Bendrinama nuoroda ištrinta",
   "description": "Aprašymas",
   "details": "Detalės",
@@ -428,19 +449,13 @@
   "display_original_photos": "Rodyti originalias nuotraukas",
   "display_original_photos_setting_description": "",
   "do_not_show_again": "Daugiau nerodyti šio pranešimo",
+  "documentation": "Dokumentacija",
   "done": "",
   "download": "Atsisiųsti",
   "download_settings": "Atsisiųsti",
   "downloading": "Siunčiama",
   "duplicates": "Dublikatai",
   "duration": "Trukmė",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
   "edit": "Redaguoti",
   "edit_album": "Redaguoti albumą",
   "edit_avatar": "Redaguoti avatarą",
@@ -455,13 +470,12 @@
   "edit_location": "Redaguoti vietovę",
   "edit_name": "Redaguoti vardą",
   "edit_people": "Redaguoti žmones",
+  "edit_tag": "Redaguoti žymą",
   "edit_title": "Redaguoti antraštę",
-  "edit_user": "Redaguoti vartotoją",
+  "edit_user": "Redaguoti naudotoją",
   "edited": "Redaguota",
   "editor": "",
   "email": "El. paštas",
-  "empty": "",
-  "empty_album": "",
   "empty_trash": "Ištuštinti šiukšliadėžę",
   "enable": "Įgalinti",
   "enabled": "Įgalintas",
@@ -472,7 +486,7 @@
   "errors": {
     "cant_apply_changes": "Negalima taikyti pakeitimų",
     "error_adding_assets_to_album": "Klaida pridedant elementus į albumą",
-    "error_adding_users_to_album": "Klaida pridedant vartotojus prie albumo",
+    "error_adding_users_to_album": "Klaida pridedant naudotojus prie albumo",
     "error_downloading": "Klaida atsisiunčiant {filename}",
     "error_hiding_buy_button": "Klaida slepiant pirkimo mygtuką",
     "error_removing_assets_from_album": "Klaida šalinant elementus iš albumo, patikrinkite konsolę dėl išsamesnės informacijos",
@@ -482,34 +496,34 @@
     "failed_to_edit_shared_link": "Nepavyko redaguoti bendrinamos nuorodos",
     "failed_to_load_people": "Nepavyko užkrauti žmonių",
     "failed_to_remove_product_key": "Nepavyko pašalinti produkto rakto",
+    "failed_to_stack_assets": "Nepavyko sugrupuoti elementų",
+    "failed_to_unstack_assets": "Nepavyko išgrupuoti elementų",
     "import_path_already_exists": "Šis importavimo kelias jau egzistuoja.",
     "incorrect_email_or_password": "Neteisingas el. pašto adresas arba slaptažodis",
     "profile_picture_transparent_pixels": "Profilio nuotrauka negali turėti permatomų pikselių. Prašome priartinti ir/arba perkelkite nuotrauką.",
     "quota_higher_than_disk_size": "Nustatyta kvota, viršija disko dydį",
-    "unable_to_add_album_users": "Nepavyksta pridėti vartotojų prie albumo",
+    "unable_to_add_album_users": "Nepavyksta pridėti naudotojų prie albumo",
     "unable_to_add_comment": "Nepavyksta pridėti komentaro",
     "unable_to_add_exclusion_pattern": "Nepavyksta pridėti išimčių šablono",
     "unable_to_add_import_path": "Nepavyksta pridėti importavimo kelio",
     "unable_to_add_partners": "Nepavyksta pridėti partnerių",
-    "unable_to_change_album_user_role": "Nepavyksta pakeisti albumo vartoto rolės",
+    "unable_to_change_album_user_role": "Nepavyksta pakeisti albumo naudotojo rolės",
     "unable_to_change_date": "Negalima pakeisti datos",
     "unable_to_change_location": "Negalima pakeisti vietos",
     "unable_to_change_password": "Negalima pakeisti slaptažodžio",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_connect": "Nepavyko prisijungti",
     "unable_to_connect_to_server": "Nepavyko prisijungti prie serverio",
     "unable_to_copy_to_clipboard": "Negalima kopijuoti į iškarpinę, įsitikinkite, kad prie puslapio prieinate per https",
     "unable_to_create_admin_account": "Nepavyko sukurti administratoriaus paskyros",
     "unable_to_create_api_key": "Nepavyko sukurti naujo API rakto",
     "unable_to_create_library": "Nepavyko sukurti bibliotekos",
-    "unable_to_create_user": "Nepavyko sukurti vartotojo",
+    "unable_to_create_user": "Nepavyko sukurti naudotojo",
     "unable_to_delete_album": "Nepavyksta ištrinti albumo",
     "unable_to_delete_asset": "",
     "unable_to_delete_exclusion_pattern": "Nepavyksta ištrinti išimčių šablono",
     "unable_to_delete_import_path": "Nepavyksta ištrinti importavimo kelio",
     "unable_to_delete_shared_link": "Nepavyksta ištrinti bendrinimo nuorodos",
-    "unable_to_delete_user": "Nepavyksta ištrinti vartotojo",
+    "unable_to_delete_user": "Nepavyksta ištrinti naudotojo",
     "unable_to_edit_exclusion_pattern": "Nepavyksta redaguoti išimčių šablono",
     "unable_to_edit_import_path": "Nepavyksta redaguoti išimčių kelio",
     "unable_to_empty_trash": "",
@@ -525,14 +539,12 @@
     "unable_to_log_out_device": "Nepavyksta atjungti įrenginio",
     "unable_to_login_with_oauth": "Nepavyksta prisijungti su OAuth",
     "unable_to_play_video": "Nepavyksta paleisti vaizdo įrašo",
-    "unable_to_refresh_user": "Nepavyksta atnaujinti vartotojo",
+    "unable_to_refresh_user": "Nepavyksta atnaujinti naudotojo",
     "unable_to_remove_album_users": "",
     "unable_to_remove_api_key": "Nepavyko pašalinti API rakto",
-    "unable_to_remove_comment": "",
     "unable_to_remove_library": "Nepavyksta pašalinti bibliotekos",
     "unable_to_remove_partner": "Nepavyksta pašalinti partnerio",
     "unable_to_remove_reaction": "Nepavyksta pašalinti reakcijos",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "",
     "unable_to_reset_password": "",
     "unable_to_resolve_duplicate": "",
@@ -556,10 +568,6 @@
     "unable_to_update_user": "",
     "unable_to_upload_file": "Nepavyksta įkelti failo"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exif": "Exif",
   "exit_slideshow": "",
   "expand_all": "Išskleisti viską",
@@ -573,28 +581,23 @@
   "external": "Išorinis",
   "external_libraries": "Išorinės bibliotekos",
   "face_unassigned": "Nepriskirta",
-  "failed_to_get_people": "",
-  "favorite": "Mėgstamiausias",
-  "favorite_or_unfavorite_photo": "",
+  "favorite": "Mėgstamiausi",
+  "favorite_or_unfavorite_photo": "Įtraukti prie arba pašalinti iš mėgstamiausių",
   "favorites": "Mėgstamiausi",
-  "feature": "",
   "feature_photo_updated": "",
-  "featurecollection": "",
   "file_name": "Failo pavadinimas",
   "file_name_or_extension": "Failo pavadinimas arba plėtinys",
   "filename": "",
-  "files": "",
   "filetype": "Failo tipas",
   "filter_people": "Filtruoti žmones",
   "fix_incorrect_match": "",
-  "force_re-scan_library_files": "",
+  "folders": "Aplankai",
   "forward": "",
   "general": "",
   "get_help": "Gauti pagalbos",
   "getting_started": "",
   "go_back": "",
   "go_to_search": "",
-  "go_to_share_page": "",
   "group_albums_by": "Grupuoti albumus pagal...",
   "group_no": "Negrupuoti",
   "group_owner": "Grupuoti pagal savininką",
@@ -610,7 +613,6 @@
   "host": "",
   "hour": "Valanda",
   "image": "Nuotrauka",
-  "img": "",
   "immich_logo": "Immich logotipas",
   "import_from_json": "Importuoti iš JSON",
   "import_path": "Importavimo kelias",
@@ -628,7 +630,7 @@
   },
   "invite_people": "Kviesti žmones",
   "invite_to_album": "Pakviesti į albumą",
-  "job_settings_description": "",
+  "items_count": "{count, plural, one {# elementas} few {# elementai} other {# elementų}}",
   "jobs": "Darbai",
   "keep": "Palikti",
   "keep_all": "Palikti visus",
@@ -683,6 +685,7 @@
   "merge_people_limit": "Vienu metu galite sujungti tik iki 5 veidų",
   "merge_people_prompt": "Ar norite sujungti šiuos asmenis? Šis veiksmas yra negrįžtamas.",
   "merge_people_successfully": "Asmenys sėkmingai sujungti",
+  "merged_people_count": "{count, plural, one {Sujungtas # asmuo} few {Sujungti # asmenys} other {Sujungta # asmenų}}",
   "minimize": "Sumažinti",
   "minute": "Minutė",
   "missing": "Trūkstami",
@@ -694,11 +697,11 @@
   "name": "Vardas",
   "name_or_nickname": "Vardas arba slapyvardis",
   "never": "Niekada",
-  "new_album": "",
+  "new_album": "Naujas albumas",
   "new_api_key": "Naujas API raktas",
   "new_password": "Naujas slaptažodis",
   "new_person": "Naujas asmuo",
-  "new_user_created": "Sukurtas naujas vartotojas",
+  "new_user_created": "Naujas naudotojas sukurtas",
   "new_version_available": "PRIEINAMA NAUJA VERSIJA",
   "newest_first": "Pirmiausia naujausi",
   "next": "Sekantis",
@@ -726,13 +729,13 @@
   "notifications": "Pranešimai",
   "notifications_setting_description": "Tvarkyti pranešimus",
   "oauth": "",
+  "official_immich_resources": "Oficialūs Immich ištekliai",
   "offline": "Neprisijungęs",
   "ok": "Ok",
   "oldest_first": "Seniausias pirmas",
   "onboarding_welcome_user": "Sveiki atvykę, {user}",
   "online": "Prisijungęs",
   "only_favorites": "Tik mėgstamiausi",
-  "only_refreshes_modified_files": "Atnaujina tik modifikuotus failus",
   "open_the_search_filters": "Atidaryti paieškos filtrus",
   "options": "Pasirinktys",
   "or": "arba",
@@ -765,12 +768,14 @@
   "paused": "Sustabdyta",
   "pending": "Laukiama",
   "people": "Asmenys",
+  "people_edits_count": "{count, plural, one {Redaguotas # asmuo} few {Redaguoti # asmenys} other {Redaguota # asmenų}}",
   "people_sidebar_description": "",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "",
   "permanent_deletion_warning_setting_description": "",
   "permanently_delete": "Ištrinti visam laikui",
+  "permanently_delete_assets_count": "Visam laikui ištrinti {count, plural, one {# elementą} few {# elementus} other {# elementų}}",
   "permanently_deleted_asset": "",
+  "permanently_deleted_assets_count": "Visam laikui {count, plural, one {ištrintas # elementas} few {ištrinti # elementai} other {ištrinta # elementų}}",
   "photos": "Nuotraukos",
   "photos_and_videos": "Nuotraukos ir vaizdo įrašai",
   "photos_count": "{count, plural, one {{count, number} nuotrauka} few {{count, number} nuotraukos} other {{count, number} nuotraukų}}",
@@ -782,7 +787,6 @@
   "play_memories": "",
   "play_motion_photo": "",
   "play_or_pause_video": "",
-  "point": "",
   "port": "",
   "preset": "",
   "preview": "",
@@ -811,8 +815,8 @@
   "purchase_license_subtitle": "Įsigykite „Immich“, kad palaikytumėte tolesnį paslaugos vystymą",
   "purchase_lifetime_description": "Pirkimas visam gyvenimui",
   "purchase_option_title": "PIRKIMO PASIRINKIMAS",
-  "purchase_panel_info_1": "„Immich“ kūrimas užima daug laiko ir pastangų, o visą darbo dieną dirba inžinieriai, kad jis būtų kuo geresnis. Mūsų misija yra, kad atvirojo kodo programinė įranga ir etiška verslo praktika taptų tvariu programuotojų pajamų šaltiniu ir sukurtų privatumą gerbiančią ekosistemą su realiomis alternatyvomis išnaudojamoms debesijos paslaugoms.",
-  "purchase_panel_info_2": "Kadangi esame įsipareigoję nepridėti mokamų sienų, šis pirkinys nesuteiks jums jokių papildomų „Immich“ funkcijų. Mes tikime, kad tokie vartotojai kaip jūs palaikys nuolatinį „Immich“ vystymąsi.",
+  "purchase_panel_info_1": "„Immich“ kūrimas užima daug laiko ir pastangų, o visą darbo dieną dirba inžinieriai, kad jis būtų kuo geresnis. Mūsų misija yra, kad atvirojo kodo programinė įranga ir etiška verslo praktika taptų tvariu kūrėjų pajamų šaltiniu ir sukurtų privatumą gerbiančią ekosistemą su realiomis alternatyvomis išnaudojamoms debesijos paslaugoms.",
+  "purchase_panel_info_2": "Kadangi esame įsipareigoję nepridėti mokamų sienų, šis pirkinys nesuteiks jums jokių papildomų „Immich“ funkcijų. Mes tikime, kad tokie naudotojai kaip jūs palaikys nuolatinį „Immich“ vystymąsi.",
   "purchase_panel_title": "Palaikykite projektą",
   "purchase_per_server": "Vienam serveriui",
   "purchase_per_user": "Vienam naudotojui",
@@ -824,36 +828,38 @@
   "purchase_server_description_2": "Rėmėjo statusas",
   "purchase_server_title": "Serveris",
   "purchase_settings_server_activated": "Serverio produkto raktas yra tvarkomas administratoriaus",
-  "range": "",
   "rating": "Įvertinimas žvaigždutėmis",
-  "raw": "",
+  "rating_count": "{count, plural, one {# įvertinimas} few {# įvertinimai} other {# įvertinimų}}",
   "reaction_options": "",
   "read_changelog": "",
   "recent": "",
   "recent_searches": "",
-  "refresh": "",
-  "refreshed": "",
+  "refresh": "Atnaujinti",
+  "refreshed": "Atnaujinta",
   "refreshes_every_file": "",
   "remove": "Pašalinti",
+  "remove_deleted_assets": "",
   "remove_from_album": "Pašalinti iš albumo",
   "remove_from_favorites": "Pašalinti iš mėgstamiausių",
   "remove_from_shared_link": "",
-  "remove_offline_files": "",
-  "remove_user": "Pašalinti vartotoją",
+  "remove_user": "Pašalinti naudotoją",
   "removed_api_key": "Pašalintas API Raktas: {name}",
+  "removed_from_archive": "Pašalinta iš archyvo",
+  "removed_from_favorites": "Pašalinta iš mėgstamiausių",
+  "removed_from_favorites_count": "{count, plural, one {# pašalintas} few {# pašalinti} other {# pašalinta}} iš mėgstamiausių",
+  "removed_tagged_assets": "Žyma pašalinta iš {count, plural, one {# elemento} other {# elementų}}",
   "rename": "Pervadinti",
   "repair": "Pataisyti",
   "repair_no_results_message": "",
   "replace_with_upload": "",
   "require_password": "Reikalauti slaptažodžio",
-  "reset": "",
+  "reset": "Atstatyti",
   "reset_password": "",
   "reset_people_visibility": "",
-  "reset_settings_to_default": "",
   "resolved_all_duplicates": "Išspręsti visi dublikatai",
   "restore": "Atkurti",
   "restore_all": "Atkurti visus",
-  "restore_user": "Atkurti vartotoją",
+  "restore_user": "Atkurti naudotoją",
   "retry_upload": "",
   "review_duplicates": "",
   "role": "",
@@ -863,9 +869,8 @@
   "saved_settings": "Išsaugoti nustatymai",
   "say_something": "Ką nors pasakykite",
   "scan_all_libraries": "Skenuoti visas bibliotekas",
-  "scan_all_library_files": "",
-  "scan_new_library_files": "",
-  "scan_settings": "",
+  "scan_library": "Skenuoti",
+  "scan_settings": "Skenavimo nustatymai",
   "search": "Ieškoti",
   "search_albums": "",
   "search_by_context": "Ieškoti pagal kontekstą",
@@ -874,12 +879,14 @@
   "search_camera_make": "",
   "search_camera_model": "",
   "search_city": "",
-  "search_country": "",
+  "search_country": "Ieškoti šalies...",
   "search_for_existing_person": "",
   "search_no_people_named": "Nėra žmonių vardu „{name}“",
-  "search_people": "",
-  "search_places": "",
+  "search_people": "Ieškoti žmonių",
+  "search_places": "Ieškoti vietų",
+  "search_settings": "Ieškoti nustatymų",
   "search_state": "",
+  "search_tags": "Ieškoti žymų...",
   "search_timezone": "",
   "search_type": "Paieškos tipas",
   "search_your_photos": "Ieškoti nuotraukų",
@@ -890,14 +897,16 @@
   "select_all_duplicates": "Pasirinkti visus dublikatus",
   "select_avatar_color": "Pasirinkti avataro spalvą",
   "select_face": "Pasirinkti veidą",
-  "select_featured_photo": "",
-  "select_library_owner": "",
+  "select_featured_photo": "Pasirinkti rodomą nuotrauką",
+  "select_library_owner": "Pasirinkti bibliotekos savininką",
   "select_new_face": "",
   "select_photos": "",
   "selected": "Pasirinkta",
+  "selected_count": "{count, plural, one {# pasirinktas} few {# pasirinkti} other {# pasirinktų}}",
   "send_message": "Siųsti žinutę",
   "send_welcome_email": "Siųsti sveikinimo el. laišką",
-  "server": "Serveris",
+  "server_offline": "Serveris nepasiekiamas",
+  "server_online": "Serveris pasiekiamas",
   "server_stats": "Serverio statistika",
   "server_version": "Serverio versija",
   "set": "Nustatyti",
@@ -913,6 +922,7 @@
   "shared_by": "",
   "shared_by_you": "",
   "shared_links": "",
+  "shared_photos_and_videos_count": "{assetCount, plural, one {# bendrinama nuotrauka ir vaizdo įrašas} few {# bendrinamos nuotraukos ir vaizdo įrašai} other {# bendrinamų nuotraukų ir vaizdo įrašų}}",
   "shared_with_partner": "Pasidalinta su {partner}",
   "sharing": "Dalijimasis",
   "sharing_enter_password": "Norėdami peržiūrėti šį puslapį, įveskite slaptažodį.",
@@ -930,6 +940,8 @@
   "show_person_options": "",
   "show_progress_bar": "",
   "show_search_options": "Rodyti paieškos parinktis",
+  "show_supporter_badge": "Rėmėjo ženklelis",
+  "show_supporter_badge_description": "Rodyti rėmėjo ženklelį",
   "shuffle": "",
   "sign_out": "Atsijungti",
   "sign_up": "Užsiregistruoti",
@@ -944,9 +956,13 @@
   "sort_recent": "Naujausia nuotrauka",
   "sort_title": "Pavadinimas",
   "source": "Šaltinis",
-  "stack": "",
-  "stack_selected_photos": "",
+  "stack": "Grupuoti",
+  "stack_duplicates": "Grupuoti dublikatus",
+  "stack_select_one_photo": "Pasirinkti pagrindinę grupės nuotrauką",
+  "stack_selected_photos": "Grupuoti pasirinktas nuotraukas",
+  "stacked_assets_count": "{count, plural, one {Sugrupuotas # elementas} few {Sugrupuoti # elementai} other {Sugrupuota # elementų}}",
   "stacktrace": "",
+  "start": "Pradėti",
   "start_date": "Pradžios data",
   "state": "",
   "status": "Statusas",
@@ -957,38 +973,47 @@
   "submit": "Pateikti",
   "suggestions": "",
   "sunrise_on_the_beach": "Saulėtekis paplūdimyje",
+  "support_and_feedback": "Palaikymas ir atsiliepimai",
   "swap_merge_direction": "",
   "sync": "Sinchronizuoti",
+  "tag": "Žyma",
+  "tag_created": "Sukurta žyma: {tag}",
+  "tag_not_found_question": "Nerandate žymos? <link>Sukurti naują žymą.</link>",
+  "tag_updated": "Atnaujinta žyma: {tag}",
+  "tagged_assets": "Žyma pridėta prie {count, plural, one {# elemento} other {# elementų}}",
+  "tags": "Žymos",
   "template": "Šablonas",
   "theme": "Tema",
   "theme_selection": "",
   "theme_selection_description": "",
   "time_based_memories": "",
   "timezone": "Laiko juosta",
-  "to_archive": "Archyvas",
+  "to_archive": "Archyvuoti",
   "to_change_password": "Pakeisti slaptažodį",
-  "to_favorite": "Mėgstamiausi",
+  "to_favorite": "Įtraukti prie mėgstamiausių",
   "toggle_settings": "",
   "toggle_theme": "",
-  "toggle_visibility": "",
   "total_usage": "",
   "trash": "Šiukšliadėžė",
   "trash_all": "Ištrinti visus",
   "trash_count": "Šiukšliadėžė {count, number}",
   "trash_no_results_message": "",
-  "type": "",
+  "trashed_items_will_be_permanently_deleted_after": "Į šiukšliadėžę perkelti elementai bus visam laikui ištrinti po {days, plural, one {# dienos} other {# dienų}}.",
+  "type": "Tipas",
   "unarchive": "Išarchyvuoti",
-  "unarchived": "",
-  "unfavorite": "",
+  "unarchived_count": "{count, plural, other {# išarchyvuota}}",
+  "unfavorite": "Pašalinti iš mėgstamiausių",
   "unhide_person": "",
   "unknown": "",
-  "unknown_album": "",
   "unknown_year": "Nežinomi metai",
   "unlink_oauth": "",
   "unlinked_oauth_account": "",
+  "unnamed_album_delete_confirmation": "Ar tikrai norite ištrinti šį albumą?",
   "unsaved_change": "Neišsaugoti pakeitimai",
   "unselect_all": "",
-  "unstack": "",
+  "unselect_all_duplicates": "Atžymėti visus dublikatus",
+  "unstack": "Išgrupuoti",
+  "unstacked_assets_count": "{count, plural, one {Išgrupuotas # elementas} few {Išgrupuoti # elementai} other {Išgrupuota # elementų}}",
   "up_next": "",
   "updated_password": "Slaptažodis atnaujintas",
   "upload": "Įkelti",
@@ -997,33 +1022,38 @@
   "upload_status_duplicates": "Dublikatai",
   "upload_status_errors": "Klaidos",
   "upload_status_uploaded": "Įkelta",
-  "url": "",
+  "url": "URL",
   "usage": "",
-  "user": "Vartotojas",
-  "user_id": "Vartotojo ID",
+  "user": "Naudotojas",
+  "user_id": "Naudotojo ID",
   "user_usage_detail": "",
-  "username": "Vartotojo vardas",
-  "users": "Vartotojai",
-  "utilities": "",
-  "validate": "",
+  "user_usage_stats": "Paskyros naudojimo statistika",
+  "user_usage_stats_description": "Žiūrėti paskyros naudojimo statistiką",
+  "username": "Naudotojo vardas",
+  "users": "Naudotojai",
+  "utilities": "Priemonės",
+  "validate": "Validuoti",
   "variables": "Kintamieji",
   "version": "Versija",
   "version_announcement_closing": "Tavo draugas, Alex",
+  "version_history": "Versijų istorija",
+  "version_history_item": "Versija {version} įdiegta {date}",
   "video": "Vaizdo įrašas",
-  "video_hover_setting_description": "",
+  "video_hover_setting_description": "Atkurti vaizdo įrašo miniatiūrą, kai pelė užvedama ant elemento. Net ir išjungus, atkūrimą galima pradėti užvedus pelės žymeklį ant atkūrimo piktogramos.",
   "videos": "Video",
+  "videos_count": "{count, plural, one {# vaizdo įrašas} few {# vaizdo įrašai} other {# vaizdo įrašų}}",
   "view": "Rodyti",
   "view_album": "Rodyti albumą",
-  "view_all": "",
-  "view_all_users": "Rodyti visus vartotojus",
+  "view_all": "Peržiūrėti viską",
+  "view_all_users": "Peržiūrėti visus naudotojus",
   "view_links": "Rodyti nuorodas",
   "view_next_asset": "",
   "view_previous_asset": "",
-  "viewer": "",
+  "view_stack": "Peržiūrėti grupę",
   "waiting": "Laukiama",
   "warning": "Įspėjimas",
   "week": "Savaitė",
-  "welcome_to_immich": "",
+  "welcome_to_immich": "Sveiki atvykę į Immich",
   "year": "Metai",
   "yes": "Taip",
   "zoom_image": "Priartinti vaizdą"
diff --git a/web/src/lib/i18n/lv.json b/i18n/lv.json
similarity index 57%
rename from web/src/lib/i18n/lv.json
rename to i18n/lv.json
index bf17ccb813..c6cbc9c3b7 100644
--- a/web/src/lib/i18n/lv.json
+++ b/i18n/lv.json
@@ -2,7 +2,7 @@
   "about": "Par",
   "account": "Konts",
   "account_settings": "Konta iestatījumi",
-  "acknowledge": "Atzīt",
+  "acknowledge": "Pieņemt",
   "action": "Darbība",
   "actions": "Darbības",
   "active": "Aktīvs",
@@ -23,9 +23,10 @@
   "add_to": "Pievienot ..",
   "add_to_album": "Pievienot albumam",
   "add_to_shared_album": "Pievienot koplietotam albumam",
+  "add_url": "Pievienot URL",
   "added_to_archive": "Pievienots arhīvam",
   "added_to_favorites": "Pievienots izlasei",
-  "added_to_favorites_count": "Pievienots {count} izlasei",
+  "added_to_favorites_count": "Pievienots {count, number} izlasei",
   "admin": {
     "add_exclusion_pattern_description": "Pievienojiet izlaišanas shēmas. Aizstājējzīmju izmantoša *, **, un ? tiek atbalstīta. Lai ignorētu visus failus jebkurā direktorijā ar nosaukumu “RAW”, izmantojiet “**/RAW/**”. Lai ignorētu visus failus, kas beidzas ar “. tif”, izmantojiet “**/*. tif”. Lai ignorētu absolūto ceļu, izmantojiet “/path/to/ignore/**”.",
     "authentication_settings": "Autentifikācijas iestatījumi",
@@ -40,29 +41,29 @@
     "confirm_email_below": "Lai apstiprinātu, zemāk ierakstiet “{email}”",
     "confirm_reprocess_all_faces": "Vai tiešām vēlaties atkārtoti apstrādāt visas sejas? Tas arī atiestatīs cilvēkus ar vārdiem.",
     "confirm_user_password_reset": "Vai tiešām vēlaties atiestatīt lietotāja {user} paroli?",
-    "crontab_guru": "",
+    "create_job": "Izveidot darbu",
+    "cron_expression": "Cron izteiksme",
     "disable_login": "Atspējot pieteikšanos",
-    "disabled": "",
     "duplicate_detection_job_description": "Palaidiet mašīnmācīšanos uz līdzekļiem, lai noteiktu līdzīgus attēlus. Paļaujas uz Viedo Meklēšanu",
+    "external_library_created_at": "Ārēja bibliotēka (izveidota {date})",
+    "external_library_management": "Ārējo bibliotēku pārvaldība",
+    "face_detection": "Seju noteikšana",
+    "image_format": "Formāts",
     "image_format_description": "",
     "image_prefer_embedded_preview": "",
     "image_prefer_embedded_preview_setting_description": "",
     "image_prefer_wide_gamut": "",
     "image_prefer_wide_gamut_setting_description": "",
-    "image_preview_format": "Priekšskatījuma formāts",
-    "image_preview_resolution": "Priekšskatījuma izšķirtspēja",
-    "image_preview_resolution_description": "",
     "image_quality": "Kvalitāte",
-    "image_quality_description": "Attēla kvalitāte no 1 līdz 100. Augstāka kvalitāte ir labāka, bet veido lielākus failus. Šī opcija ietekmē Priekšskatījums un Sīktēls attēlus.",
+    "image_resolution": "Izšķirtspēja",
     "image_settings": "Attēla Iestatījumi",
     "image_settings_description": "Ģenerēto attēlu kvalitātes un izšķirtspējas pārvaldība",
-    "image_thumbnail_format": "Sīktēlu formāts",
-    "image_thumbnail_resolution": "Sīktēlu izšķirtspēja",
-    "image_thumbnail_resolution_description": "",
+    "image_thumbnail_title": "Sīktēlu iestatījumi",
+    "job_created": "Darbs izveidots",
     "job_settings": "",
     "job_settings_description": "",
-    "library_cron_expression": "",
-    "library_cron_expression_presets": "",
+    "job_status": "Darbu statuss",
+    "library_deleted": "Bibliotēka dzēsta",
     "library_scanning": "",
     "library_scanning_description": "",
     "library_scanning_enable_description": "",
@@ -75,14 +76,14 @@
     "logging_enable_description": "",
     "logging_level_description": "",
     "logging_settings": "",
-    "machine_learning_clip_model": "",
-    "machine_learning_duplicate_detection": "",
+    "machine_learning_clip_model": "CLIP modelis",
+    "machine_learning_duplicate_detection": "Dublikātu noteikšana",
     "machine_learning_duplicate_detection_enabled_description": "",
     "machine_learning_duplicate_detection_setting_description": "",
     "machine_learning_enabled_description": "",
-    "machine_learning_facial_recognition": "",
+    "machine_learning_facial_recognition": "Seju atpazīšana",
     "machine_learning_facial_recognition_description": "",
-    "machine_learning_facial_recognition_model": "",
+    "machine_learning_facial_recognition_model": "Seju atpazīšanas modelis",
     "machine_learning_facial_recognition_model_description": "",
     "machine_learning_facial_recognition_setting_description": "",
     "machine_learning_max_detection_distance": "",
@@ -93,68 +94,83 @@
     "machine_learning_min_detection_score_description": "",
     "machine_learning_min_recognized_faces": "",
     "machine_learning_min_recognized_faces_description": "",
-    "machine_learning_settings": "",
+    "machine_learning_settings": "Mašīnmācīšanās iestatījumi",
     "machine_learning_settings_description": "",
-    "machine_learning_smart_search": "",
+    "machine_learning_smart_search": "Viedā meklēšana",
     "machine_learning_smart_search_description": "",
     "machine_learning_smart_search_enabled_description": "",
-    "machine_learning_url_description": "",
+    "machine_learning_url_description": "Mašīnmācīšanās servera URL",
     "manage_log_settings": "",
     "map_dark_style": "",
     "map_enable_description": "",
+    "map_gps_settings": "Kartes un GPS iestatījumi",
+    "map_gps_settings_description": "Pārvaldīt karšu un GPS (apgrieztās ģeokodēšanas) iestatījumus",
     "map_light_style": "",
     "map_reverse_geocoding": "",
     "map_reverse_geocoding_enable_description": "",
     "map_reverse_geocoding_settings": "",
-    "map_settings": "",
+    "map_settings": "Karte",
     "map_settings_description": "",
     "map_style_description": "",
+    "metadata_extraction_job": "Metadatu iegūšana",
     "metadata_extraction_job_description": "",
+    "metadata_settings": "Metadatu iestatījumi",
+    "migration_job": "Migrācija",
     "migration_job_description": "",
-    "notification_email_from_address": "",
-    "notification_email_from_address_description": "",
+    "note_cannot_be_changed_later": "PIEZĪME: Vēlāk to vairs nevar mainīt!",
+    "notification_email_from_address": "No adreses",
+    "notification_email_from_address_description": "Sūtītāja e-pasta adrese, piemēram: “Immich foto serveris <noreply@example.com>”",
     "notification_email_host_description": "",
-    "notification_email_ignore_certificate_errors": "",
-    "notification_email_ignore_certificate_errors_description": "",
+    "notification_email_ignore_certificate_errors": "Ignorēt sertifikātu kļūdas",
+    "notification_email_ignore_certificate_errors_description": "Ignorēt TLS sertifikāta apstiprināšanas kļūdas (nav ieteicams)",
     "notification_email_password_description": "",
-    "notification_email_port_description": "",
-    "notification_email_sent_test_email_button": "",
+    "notification_email_port_description": "e-pasta servera ports (piemēram, 25, 465 vai 587)",
+    "notification_email_sent_test_email_button": "Nosūtīt testa e-pastu un saglabāt",
     "notification_email_setting_description": "",
+    "notification_email_test_email": "Nosūtīt testa e-pastu",
     "notification_email_test_email_failed": "",
     "notification_email_test_email_sent": "",
     "notification_email_username_description": "",
     "notification_enable_email_notifications": "",
-    "notification_settings": "",
+    "notification_settings": "Paziņojumu iestatījumi",
     "notification_settings_description": "",
     "oauth_auto_launch": "",
     "oauth_auto_launch_description": "",
     "oauth_auto_register": "",
     "oauth_auto_register_description": "",
-    "oauth_button_text": "",
-    "oauth_client_id": "",
-    "oauth_client_secret": "",
-    "oauth_enable_description": "",
+    "oauth_button_text": "Pogas teksts",
+    "oauth_client_id": "Klienta ID",
+    "oauth_client_secret": "Klienta noslēpums",
+    "oauth_enable_description": "Pieslēgties ar OAuth",
     "oauth_issuer_url": "",
     "oauth_mobile_redirect_uri": "",
     "oauth_mobile_redirect_uri_override": "",
     "oauth_mobile_redirect_uri_override_description": "",
+    "oauth_profile_signing_algorithm": "Profila parakstīšanas algoritms",
+    "oauth_profile_signing_algorithm_description": "Lietotāja profila parakstīšanai izmantotais algoritms.",
     "oauth_scope": "",
     "oauth_settings": "OAuth",
     "oauth_settings_description": "",
-    "oauth_signing_algorithm": "",
+    "oauth_signing_algorithm": "Parakstīšanas algoritms",
     "oauth_storage_label_claim": "",
     "oauth_storage_label_claim_description": "",
     "oauth_storage_quota_claim": "",
     "oauth_storage_quota_claim_description": "",
     "oauth_storage_quota_default": "",
     "oauth_storage_quota_default_description": "",
-    "password_enable_description": "",
-    "password_settings": "",
-    "password_settings_description": "",
+    "password_enable_description": "Pieteikšanās ar e-pasta adresi un paroli",
+    "password_settings": "Pieteikšanās ar paroli",
+    "password_settings_description": "Pārvaldīt pieteikšanās ar paroli iestatījumus",
+    "person_cleanup_job": "Personu tīrīšana",
+    "quota_size_gib": "Kvotas izmērs (GiB)",
+    "registration": "Administratora reģistrācija",
+    "repair_all": "Salabot visu",
+    "require_password_change_on_login": "Pieprasīt lietotājam mainīt paroli pēc pirmās pieteikšanās",
+    "scanning_library": "Skenē bibliotēku",
     "server_external_domain_settings": "",
     "server_external_domain_settings_description": "",
-    "server_settings": "",
-    "server_settings_description": "",
+    "server_settings": "Servera iestatījumi",
+    "server_settings_description": "Pārvaldīt servera iestatījumus",
     "server_welcome_message": "",
     "server_welcome_message_description": "",
     "sidecar_job_description": "",
@@ -166,24 +182,24 @@
     "storage_template_migration_job": "",
     "storage_template_settings": "",
     "storage_template_settings_description": "",
-    "theme_custom_css_settings": "",
+    "system_settings": "Sistēmas iestatījumi",
+    "theme_custom_css_settings": "Pielāgots CSS",
     "theme_custom_css_settings_description": "",
     "theme_settings": "",
     "theme_settings_description": "",
     "thumbnail_generation_job_description": "",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "",
     "transcoding_acceleration_api_description": "",
-    "transcoding_acceleration_nvenc": "",
-    "transcoding_acceleration_qsv": "",
-    "transcoding_acceleration_rkmpp": "",
-    "transcoding_acceleration_vaapi": "",
+    "transcoding_acceleration_nvenc": "NVENC (nepieciešams NVIDIA GPU)",
+    "transcoding_acceleration_qsv": "Quick Sync (nepieciešams 7. paaudzes vai jaunāks Intel procesors)",
+    "transcoding_acceleration_rkmpp": "RKMPP (tikai Rockchip SOC)",
+    "transcoding_acceleration_vaapi": "VAAPI",
     "transcoding_accepted_audio_codecs": "",
     "transcoding_accepted_audio_codecs_description": "",
     "transcoding_accepted_video_codecs": "",
     "transcoding_accepted_video_codecs_description": "",
     "transcoding_advanced_options_description": "",
-    "transcoding_audio_codec": "",
+    "transcoding_audio_codec": "Audio kodeks",
     "transcoding_audio_codec_description": "",
     "transcoding_bitrate_description": "",
     "transcoding_constant_quality_mode": "",
@@ -216,97 +232,114 @@
     "transcoding_target_resolution_description": "",
     "transcoding_temporal_aq": "",
     "transcoding_temporal_aq_description": "",
-    "transcoding_threads": "",
+    "transcoding_threads": "Pavedieni",
     "transcoding_threads_description": "",
     "transcoding_tone_mapping": "",
     "transcoding_tone_mapping_description": "",
-    "transcoding_tone_mapping_npl": "",
-    "transcoding_tone_mapping_npl_description": "",
     "transcoding_transcode_policy": "",
     "transcoding_two_pass_encoding": "",
     "transcoding_two_pass_encoding_setting_description": "",
-    "transcoding_video_codec": "",
+    "transcoding_video_codec": "Video kodeks",
     "transcoding_video_codec_description": "",
     "trash_enabled_description": "",
-    "trash_number_of_days": "",
+    "trash_number_of_days": "Dienu skaits",
     "trash_number_of_days_description": "",
     "trash_settings": "",
     "trash_settings_description": "",
     "user_delete_delay_settings": "",
     "user_delete_delay_settings_description": "",
+    "user_management": "Lietotāju pārvaldība",
+    "user_password_has_been_reset": "Lietotāja parole ir atiestatīta:",
+    "user_restore_description": "<b>{user}</b> konts tiks atjaunots.",
     "user_settings": "",
     "user_settings_description": "",
-    "version_check_enabled_description": "",
-    "version_check_settings": "",
+    "version_check_enabled_description": "Ieslēgt versijas pārbaudi",
+    "version_check_implications": "Versiju pārbaudes funkcija ir atkarīga no periodiskas saziņas ar github.com",
+    "version_check_settings": "Versijas pārbaude",
     "version_check_settings_description": "",
     "video_conversion_job_description": ""
   },
-  "admin_email": "",
-  "admin_password": "",
-  "administration": "",
+  "admin_email": "Administratora e-pasts",
+  "admin_password": "Administratora parole",
+  "administration": "Administrēšana",
   "advanced": "Papildu",
-  "album_added": "",
+  "album_added": "Albums pievienots",
   "album_added_notification_setting_description": "",
-  "album_cover_updated": "",
-  "album_info_updated": "",
-  "album_name": "",
+  "album_cover_updated": "Albuma attēls atjaunināts",
+  "album_info_updated": "Albuma informācija atjaunināta",
+  "album_leave": "Pamest albumu?",
+  "album_name": "Albuma nosaukums",
   "album_options": "",
-  "album_updated": "",
+  "album_remove_user": "Noņemt lietotāju?",
+  "album_updated": "Albums atjaunināts",
   "album_updated_setting_description": "",
-  "albums": "",
+  "album_user_left": "Pameta {album}",
+  "album_user_removed": "Noņēma {user}",
+  "albums": "Albumi",
   "all": "Viss",
-  "all_people": "",
-  "allow_dark_mode": "",
-  "allow_edits": "",
-  "api_key": "",
-  "api_keys": "",
+  "all_albums": "Visi albumi",
+  "all_people": "Visi cilvēki",
+  "all_videos": "Visi video",
+  "allow_dark_mode": "Atļaut tumšo režīmu",
+  "allow_edits": "Atļaut labošanu",
+  "allow_public_user_to_download": "Atļaut lejupielādēt publiskiem lietotājiem",
+  "allow_public_user_to_upload": "Atļaut augšupielādēt publiskiem lietotājiem",
+  "anti_clockwise": "Pretēji pulksteņrādītāja virzienam",
+  "api_key": "API atslēga",
+  "api_key_description": "Šī vērtība tiks parādīta tikai vienu reizi. Nokopējiet to pirms loga aizvēršanas.",
+  "api_keys": "API atslēgas",
   "app_settings": "",
   "appears_in": "",
   "archive": "Arhīvs",
   "archive_or_unarchive_photo": "",
-  "archived": "",
+  "archive_size": "Arhīva izmērs",
+  "are_these_the_same_person": "Vai šī ir tā pati persona?",
+  "asset_adding_to_album": "Pievieno albumam...",
   "asset_offline": "",
+  "asset_uploading": "Augšupielādē...",
   "assets": "aktīvi",
   "authorized_devices": "",
   "back": "Atpakaļ",
   "backward": "",
+  "birthdate_saved": "Dzimšanas datums veiksmīgi saglabāts",
+  "birthdate_set_description": "Dzimšanas datums tiek izmantots, lai aprēķinātu šīs personas vecumu fotogrāfijas uzņemšanas brīdī.",
   "blurred_background": "",
   "camera": "",
   "camera_brand": "",
   "camera_model": "",
   "cancel": "Atcelt",
   "cancel_search": "",
-  "cannot_merge_people": "",
+  "cannot_merge_people": "Nevar apvienot cilvēkus",
   "cannot_update_the_description": "",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
-  "change_date": "",
+  "change_date": "Mainīt datumu",
   "change_expiration_time": "Izmainīt derīguma termiņu",
-  "change_location": "",
-  "change_name": "",
-  "change_name_successfully": "",
-  "change_password": "Nomainīt Paroli",
+  "change_location": "Mainīt atrašanās vietu",
+  "change_name": "Mainīt nosaukumu",
+  "change_name_successfully": "Vārds veiksmīgi nomainīts",
+  "change_password": "Nomainīt paroli",
   "change_your_password": "",
   "changed_visibility_successfully": "",
   "check_logs": "",
+  "choose_matching_people_to_merge": "Izvēlies atbilstošus cilvēkus apvienošanai",
   "city": "Pilsēta",
   "clear": "Notīrīt",
-  "clear_all": "",
+  "clear_all": "Notīrīt visu",
   "clear_message": "",
   "clear_value": "",
-  "close": "",
-  "collapse_all": "",
+  "close": "Aizvērt",
+  "collapse": "Sakļaut",
+  "collapse_all": "Sakļaut visu",
+  "color": "Krāsa",
   "color_theme": "",
+  "comment_deleted": "Komentārs dzēsts",
   "comment_options": "",
   "comments_are_disabled": "",
   "confirm": "Apstiprināt",
   "confirm_admin_password": "",
-  "confirm_password": "Apstiprināt Paroli",
+  "confirm_password": "Apstiprināt paroli",
   "contain": "",
-  "context": "",
-  "continue": "",
+  "context": "Konteksts",
+  "continue": "Turpināt",
   "copied_image_to_clipboard": "",
   "copy_error": "",
   "copy_file_path": "",
@@ -324,8 +357,8 @@
   "create_link": "Izveidot saiti",
   "create_link_to_share": "Izveidot kopīgošanas saiti",
   "create_new_person": "",
-  "create_new_user": "",
-  "create_user": "",
+  "create_new_user": "Izveidot jaunu lietotāju",
+  "create_user": "Izveidot lietotāju",
   "created": "",
   "current_device": "",
   "custom_locale": "",
@@ -334,6 +367,7 @@
   "date_after": "",
   "date_and_time": "Datums un Laiks",
   "date_before": "",
+  "date_of_birth_saved": "Dzimšanas datums veiksmīgi saglabāts",
   "date_range": "Datumu diapazons",
   "day": "",
   "default_locale": "",
@@ -344,11 +378,11 @@
   "delete_library": "",
   "delete_link": "",
   "delete_shared_link": "Dzēst Kopīgošanas saiti",
-  "delete_user": "",
+  "delete_user": "Dzēst lietotāju",
   "deleted_shared_link": "",
   "description": "Apraksts",
   "details": "INFORMĀCIJA",
-  "direction": "",
+  "direction": "Virziens",
   "disallow_edits": "",
   "discover": "",
   "dismiss_all_errors": "",
@@ -357,17 +391,13 @@
   "display_order": "",
   "display_original_photos": "",
   "display_original_photos_setting_description": "",
+  "documentation": "Dokumentācija",
   "done": "Gatavs",
   "download": "Lejupielādēt",
+  "download_settings": "Lejupielāde",
   "downloading": "",
+  "duplicates": "Dublikāti",
   "duration": "",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
   "edit_album": "",
   "edit_avatar": "",
   "edit_date": "",
@@ -382,12 +412,12 @@
   "edit_name": "Rediģēt vārdu",
   "edit_people": "",
   "edit_title": "",
-  "edit_user": "",
+  "edit_user": "Labot lietotāju",
   "edited": "",
   "editor": "",
+  "editor_close_without_save_prompt": "Izmaiņas netiks saglabātas",
+  "editor_close_without_save_title": "Aizvērt redaktoru?",
   "email": "E-pasts",
-  "empty": "",
-  "empty_album": "",
   "empty_trash": "Iztukšot atkritni",
   "enable": "",
   "enabled": "",
@@ -395,24 +425,25 @@
   "error": "",
   "error_loading_image": "",
   "errors": {
+    "cant_get_faces": "Nevar iegūt sejas",
+    "cant_search_people": "Neizdevās veikt peronu meklēšanu",
+    "failed_to_create_album": "Neizdevās izveidot albumu",
     "unable_to_add_album_users": "",
     "unable_to_add_comment": "",
     "unable_to_add_partners": "",
     "unable_to_change_album_user_role": "",
     "unable_to_change_date": "",
     "unable_to_change_location": "",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_create_admin_account": "",
     "unable_to_create_library": "",
-    "unable_to_create_user": "",
+    "unable_to_create_user": "Neizdevās izveidot lietotāju",
     "unable_to_delete_album": "",
     "unable_to_delete_asset": "",
-    "unable_to_delete_user": "",
+    "unable_to_delete_user": "Neizdevās dzēst lietotāju",
     "unable_to_empty_trash": "",
     "unable_to_enter_fullscreen": "",
     "unable_to_exit_fullscreen": "",
-    "unable_to_hide_person": "",
+    "unable_to_hide_person": "Neizdevās paslēpt personu",
     "unable_to_load_album": "",
     "unable_to_load_asset_activity": "",
     "unable_to_load_items": "",
@@ -420,11 +451,9 @@
     "unable_to_play_video": "",
     "unable_to_refresh_user": "",
     "unable_to_remove_album_users": "",
-    "unable_to_remove_comment": "",
     "unable_to_remove_library": "",
     "unable_to_remove_partner": "",
     "unable_to_remove_reaction": "",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "",
     "unable_to_reset_password": "",
     "unable_to_resolve_duplicate": "",
@@ -432,6 +461,7 @@
     "unable_to_restore_trash": "",
     "unable_to_restore_user": "",
     "unable_to_save_album": "",
+    "unable_to_save_date_of_birth": "Neizdevās saglabāt dzimšanas datumu",
     "unable_to_save_name": "",
     "unable_to_save_profile": "",
     "unable_to_save_settings": "",
@@ -446,87 +476,83 @@
     "unable_to_update_settings": "",
     "unable_to_update_user": ""
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
-  "exit_slideshow": "",
+  "exit_slideshow": "Iziet no slīdrādes",
   "expand_all": "",
   "expire_after": "Derīguma termiņš beidzas pēc",
   "expired": "Derīguma termiņš beidzās",
-  "explore": "",
+  "explore": "Izpētīt",
   "extension": "",
   "external_libraries": "",
-  "failed_to_get_people": "",
   "favorite": "Izlase",
   "favorite_or_unfavorite_photo": "",
   "favorites": "Izlase",
-  "feature": "",
   "feature_photo_updated": "",
-  "featurecollection": "",
   "file_name": "",
   "file_name_or_extension": "",
   "filename": "",
-  "files": "",
   "filetype": "",
   "filter_people": "",
   "fix_incorrect_match": "",
-  "force_re-scan_library_files": "",
+  "folders": "Mapes",
   "forward": "",
   "general": "",
   "get_help": "",
   "getting_started": "",
   "go_back": "",
   "go_to_search": "",
-  "go_to_share_page": "",
   "group_albums_by": "",
-  "has_quota": "",
+  "has_quota": "Ir kvota",
   "hide_gallery": "",
+  "hide_named_person": "Paslēpt personu {name}",
   "hide_password": "",
-  "hide_person": "",
+  "hide_person": "Paslēpt personu",
   "host": "",
   "hour": "",
   "image": "Attēls",
-  "img": "",
-  "immich_logo": "",
-  "import_path": "",
-  "in_archive": "",
-  "include_archived": "Iekļaut Arhivētos",
-  "include_shared_albums": "",
+  "immich_logo": "Immich logo",
+  "import_from_json": "Importēt no JSON",
+  "import_path": "Importa ceļš",
+  "in_albums": "{count, plural, one {# albumā} other {# albumos}}",
+  "in_archive": "Arhīvā",
+  "include_archived": "Iekļaut arhivētos",
+  "include_shared_albums": "Iekļaut koplietotos albumus",
   "include_shared_partner_assets": "",
   "individual_share": "",
-  "info": "",
+  "info": "Informācija",
   "interval": {
-    "day_at_onepm": "",
+    "day_at_onepm": "Katru dienu 13.00",
     "hours": "",
-    "night_at_midnight": "",
-    "night_at_twoam": ""
+    "night_at_midnight": "Katru dienu pusnaktī",
+    "night_at_twoam": "Katru dienu 2.00 naktī"
   },
-  "invite_people": "",
+  "invite_people": "Ielūgt cilvēkus",
   "invite_to_album": "Uzaicināt albumā",
-  "job_settings_description": "",
-  "jobs": "",
-  "keep": "",
-  "keyboard_shortcuts": "",
-  "language": "",
-  "language_setting_description": "",
-  "last_seen": "",
-  "leave": "",
+  "jobs": "Darbi",
+  "keep": "Paturēt",
+  "keep_all": "Paturēt visus",
+  "keyboard_shortcuts": "Tastatūras saīsnes",
+  "language": "Valoda",
+  "language_setting_description": "Izvēlieties vēlamo valodu",
+  "last_seen": "Pēdējo reizi redzēts",
+  "latest_version": "Jaunākā versija",
+  "latitude": "Ģeogrāfiskais platums",
+  "leave": "Paturēt",
   "let_others_respond": "Ļaut citiem atbildēt",
-  "level": "",
+  "level": "Līmenis",
   "library": "Bibliotēka",
   "library_options": "",
   "light": "",
   "link_options": "",
   "link_to_oauth": "",
   "linked_oauth_account": "",
-  "list": "",
-  "loading": "",
+  "list": "Saraksts",
+  "loading": "Ielādē",
   "loading_search_results_failed": "",
   "log_out": "Izrakstīties",
   "log_out_all_devices": "",
   "login_has_been_disabled": "",
-  "look": "",
+  "longitude": "Ģeogrāfiskais garums",
+  "look": "Izskats",
   "loop_videos": "",
   "loop_videos_description": "Iespējot, lai automātiski videoklips tiktu cikliski palaists detaļu skatītājā.",
   "make": "Firma",
@@ -537,70 +563,82 @@
   "manage_your_api_keys": "",
   "manage_your_devices": "",
   "manage_your_oauth_connection": "",
-  "map": "",
-  "map_marker_with_image": "",
+  "map": "Karte",
+  "map_marker_for_images": "Kartes marķieris attēliem, kas uzņemti {city}, {country}",
+  "map_marker_with_image": "Kartes marķieris ar attēlu",
   "map_settings": "Kartes Iestatījumi",
-  "media_type": "",
-  "memories": "",
+  "matches": "Atbilstības",
+  "media_type": "Multivides veids",
+  "memories": "Atmiņas",
   "memories_setting_description": "",
-  "menu": "",
-  "merge": "",
-  "merge_people": "",
-  "merge_people_successfully": "",
-  "minimize": "",
-  "minute": "",
-  "missing": "",
+  "memory": "Atmiņa",
+  "menu": "Izvēlne",
+  "merge": "Apvienot",
+  "merge_people": "Cilvēku apvienošana",
+  "merge_people_limit": "Vienlaikus var apvienot ne vairāk kā 5 sejas",
+  "merge_people_prompt": "Vai vēlies apvienot šos cilvēkus? Šī darbība ir neatgriezeniska.",
+  "merge_people_successfully": "Cilvēki veiksmīgi apvienoti",
+  "minimize": "Minimizēt",
+  "minute": "Minūte",
+  "missing": "Trūkstošie",
   "model": "Modelis",
   "month": "Mēnesis",
-  "more": "",
+  "more": "Vairāk",
   "moved_to_trash": "",
-  "my_albums": "",
+  "my_albums": "Mani albumi",
   "name": "Vārds",
-  "name_or_nickname": "",
+  "name_or_nickname": "Vārds vai iesauka",
   "never": "nekad",
-  "new_api_key": "",
-  "new_password": "Jauna Parole",
-  "new_person": "",
-  "new_user_created": "",
+  "new_album": "Jauns albums",
+  "new_api_key": "Jauna API atslēga",
+  "new_password": "Jaunā parole",
+  "new_person": "Jauna persona",
+  "new_user_created": "Izveidots jauns lietotājs",
+  "new_version_available": "PIEEJAMA JAUNA VERSIJA",
   "newest_first": "",
   "next": "Nākošais",
-  "next_memory": "",
-  "no": "",
+  "next_memory": "Nākamā atmiņa",
+  "no": "Nē",
   "no_albums_message": "",
   "no_archived_assets_message": "",
-  "no_assets_message": "",
-  "no_exif_info_available": "",
+  "no_assets_message": "NOKLIKŠĶINIET, LAI AUGŠUPIELĀDĒTU SAVU PIRMO FOTOATTĒLU",
+  "no_duplicates_found": "Dublikāti netika atrasti.",
+  "no_exif_info_available": "Nav pieejama exif informācija",
   "no_explore_results_message": "",
   "no_favorites_message": "",
   "no_libraries_message": "",
-  "no_name": "",
-  "no_places": "",
-  "no_results": "",
+  "no_name": "Nav nosaukuma",
+  "no_places": "Nav atrašanās vietu",
+  "no_results": "Nav rezultātu",
+  "no_results_description": "Izmēģiniet sinonīmu vai vispārīgāku atslēgvārdu",
   "no_shared_albums_message": "",
-  "not_in_any_album": "",
-  "notes": "",
-  "notification_toggle_setting_description": "",
+  "not_in_any_album": "Nav nevienā albumā",
+  "notes": "Piezīmes",
+  "notification_toggle_setting_description": "Ieslēgt e-pasta paziņojumus",
   "notifications": "Paziņojumi",
   "notifications_setting_description": "",
-  "oauth": "",
-  "offline": "",
-  "ok": "",
+  "oauth": "OAuth",
+  "official_immich_resources": "Oficiālie Immich resursi",
+  "offline": "Bezsaistē",
+  "ok": "Labi",
   "oldest_first": "",
-  "online": "",
-  "only_favorites": "",
-  "only_refreshes_modified_files": "",
-  "open_the_search_filters": "",
+  "online": "Tiešsaistē",
+  "only_favorites": "Tikai izlase",
+  "open_in_map_view": "Atvērt kartes skatā",
+  "open_in_openstreetmap": "Atvērt OpenStreetMap",
+  "open_the_search_filters": "Atvērt meklēšanas filtrus",
   "options": "Iestatījumi",
+  "or": "vai",
   "organize_your_library": "",
-  "other": "",
-  "other_devices": "",
-  "other_variables": "",
+  "other": "Citi",
+  "other_devices": "Citas ierīces",
+  "other_variables": "Citi mainīgie",
   "owned": "Īpašumā",
   "owner": "Īpašnieks",
   "partner_sharing": "",
   "partners": "",
   "password": "Parole",
-  "password_does_not_match": "",
+  "password_does_not_match": "Parole nesakrīt",
   "password_required": "",
   "password_reset_success": "",
   "past_durations": {
@@ -616,7 +654,6 @@
   "pending": "",
   "people": "Cilvēki",
   "people_sidebar_description": "",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "",
   "permanent_deletion_warning_setting_description": "",
   "permanently_delete": "",
@@ -630,7 +667,6 @@
   "play_memories": "",
   "play_motion_photo": "",
   "play_or_pause_video": "",
-  "point": "",
   "port": "",
   "preset": "",
   "preview": "",
@@ -640,59 +676,81 @@
   "primary": "",
   "profile_picture_set": "",
   "public_share": "",
-  "range": "",
-  "raw": "",
+  "purchase_button_never_show_again": "Nekad vairs nerādīt",
+  "purchase_button_reminder": "Atgādināt man pēc 30 dienām",
+  "purchase_button_remove_key": "Noņemt atslēgu",
+  "purchase_button_select": "Izvēlēties",
+  "purchase_individual_description_2": "Atbalstītāja statuss",
+  "purchase_panel_title": "Atbalstīt projektu",
+  "purchase_remove_product_key": "Noņemt produkta atslēgu",
+  "purchase_server_description_1": "Visam serverim",
+  "purchase_server_description_2": "Atbalstītāja statuss",
+  "purchase_server_title": "Serveris",
   "reaction_options": "",
-  "read_changelog": "",
+  "read_changelog": "Lasīt izmaiņu sarakstu",
   "recent": "",
   "recent_searches": "",
   "refresh": "",
   "refreshed": "",
   "refreshes_every_file": "",
-  "remove": "",
+  "remove": "Noņemt",
+  "remove_deleted_assets": "",
   "remove_from_album": "Noņemt no albuma",
-  "remove_from_favorites": "",
+  "remove_from_favorites": "Noņemt no izlases",
   "remove_from_shared_link": "",
-  "remove_offline_files": "",
-  "repair": "",
+  "remove_user": "Noņemt lietotāju",
+  "removed_api_key": "Noņēma API atslēgu: {name}",
+  "removed_from_archive": "Noņēma no arhīva",
+  "removed_from_favorites": "Noņēma no izlases",
+  "rename": "Pārsaukt",
+  "repair": "Remonts",
   "repair_no_results_message": "",
-  "replace_with_upload": "",
+  "replace_with_upload": "Aizstāt ar augšupielādi",
   "require_password": "",
+  "require_user_to_change_password_on_first_login": "Pieprasīt lietotājam mainīt paroli pēc pirmās pieteikšanās",
   "reset": "",
   "reset_password": "",
   "reset_people_visibility": "",
-  "reset_settings_to_default": "",
+  "resolve_duplicates": "Atrisināt dublēšanās gadījumus",
+  "resolved_all_duplicates": "Visi dublikāti ir atrisināti",
   "restore": "Atjaunot",
-  "restore_user": "",
-  "retry_upload": "",
-  "review_duplicates": "",
-  "role": "",
+  "restore_all": "Atjaunot visu",
+  "restore_user": "Atjaunot lietotāju",
+  "resume": "Turpināt",
+  "retry_upload": "Atkārtot augšupielādi",
+  "review_duplicates": "Pārskatīt dublikātus",
+  "role": "Loma",
+  "role_editor": "Redaktors",
+  "role_viewer": "Skatītājs",
   "save": "Saglabāt",
-  "saved_profile": "",
-  "saved_settings": "",
+  "saved_api_key": "API atslēga saglabāta",
+  "saved_profile": "Profils saglabāts",
+  "saved_settings": "Iestatījumi saglabāti",
   "say_something": "Teikt kaut ko",
   "scan_all_libraries": "",
-  "scan_all_library_files": "",
-  "scan_new_library_files": "",
   "scan_settings": "",
   "search": "Meklēt",
-  "search_albums": "",
+  "search_albums": "Meklēt albumus",
   "search_by_context": "",
+  "search_by_filename_example": "piemēram, IMG_1234.JPG vai PNG",
   "search_camera_make": "",
   "search_camera_model": "",
   "search_city": "",
   "search_country": "",
   "search_for_existing_person": "",
-  "search_people": "",
+  "search_no_people": "Nav cilvēku",
+  "search_no_people_named": "Nav cilvēku ar vārdu \"{name}\"",
+  "search_people": "Meklēt cilvēkus",
   "search_places": "",
   "search_state": "",
   "search_timezone": "",
   "search_type": "",
   "search_your_photos": "Meklēt Jūsu fotoattēlus",
   "searching_locales": "",
-  "second": "",
-  "select_album_cover": "",
+  "second": "Sekunde",
+  "select_album_cover": "Izvēlieties albuma vāciņu",
   "select_all": "",
+  "select_all_duplicates": "Atlasīt visus dublikātus",
   "select_avatar_color": "",
   "select_face": "",
   "select_featured_photo": "",
@@ -701,12 +759,13 @@
   "select_photos": "Fotoattēlu Izvēle",
   "selected": "",
   "send_message": "",
-  "server": "",
-  "server_stats": "",
+  "server_online": "Serveris tiešsaistē",
+  "server_stats": "Servera statistika",
+  "server_version": "Servera versija",
   "set": "",
   "set_as_album_cover": "",
   "set_as_profile_picture": "",
-  "set_date_of_birth": "",
+  "set_date_of_birth": "Iestatīt dzimšanas datumu",
   "set_profile_picture": "",
   "set_slideshow_to_fullscreen": "",
   "settings": "Iestatījumi",
@@ -715,13 +774,16 @@
   "shared": "Kopīgots",
   "shared_by": "",
   "shared_by_you": "",
-  "shared_links": "Kopīgotas Saites",
+  "shared_links": "Kopīgotās saites",
   "sharing": "Kopīgošana",
   "sharing_sidebar_description": "",
-  "show_album_options": "",
-  "show_file_location": "",
-  "show_gallery": "",
-  "show_hidden_people": "",
+  "show_album_options": "Rādīt albuma iespējas",
+  "show_albums": "Rādīt albumus",
+  "show_all_people": "Rādīt visus cilvēkus",
+  "show_and_hide_people": "Rādīt un slēpt cilvēkus",
+  "show_file_location": "Rādīt faila atrašanās vietu",
+  "show_gallery": "Rādīt galeriju",
+  "show_hidden_people": "Rādīt paslēptos cilvēkus",
   "show_in_timeline": "",
   "show_in_timeline_setting_description": "",
   "show_keyboard_shortcuts": "",
@@ -731,67 +793,83 @@
   "show_person_options": "",
   "show_progress_bar": "",
   "show_search_options": "",
+  "show_supporter_badge": "Atbalstītāja nozīmīte",
+  "show_supporter_badge_description": "Rādīt atbalstītāja nozīmīti",
   "shuffle": "",
   "sign_up": "",
-  "size": "",
+  "size": "Izmērs",
   "skip_to_content": "",
-  "slideshow": "",
-  "slideshow_settings": "",
-  "sort_albums_by": "",
-  "stack": "Steks",
+  "slideshow": "Slīdrāde",
+  "slideshow_settings": "Slīdrādes iestatījumi",
+  "sort_albums_by": "Kārtot albumus pēc...",
+  "sort_created": "Izveides datums",
+  "sort_items": "Vienību skaits",
+  "sort_modified": "Izmaiņu datums",
+  "sort_oldest": "Vecākā fotogrāfija",
+  "sort_recent": "Nesenākā fotogrāfija",
+  "sort_title": "Nosaukums",
+  "source": "Avots",
+  "stack": "Apvienot kaudzē",
   "stack_selected_photos": "",
   "stacktrace": "",
   "start_date": "",
   "state": "Štats",
-  "status": "",
+  "status": "Statuss",
   "stop_motion_photo": "",
   "stop_photo_sharing": "Beigt kopīgot jūsu fotogrāfijas?",
-  "storage": "",
+  "storage": "Uzglabāšanas vieta",
   "storage_label": "",
-  "submit": "",
+  "storage_usage": "{used} no {available} izmantoti",
+  "submit": "Iesniegt",
   "suggestions": "Ieteikumi",
-  "sunrise_on_the_beach": "",
+  "sunrise_on_the_beach": "Saullēkts pludmalē",
+  "support": "Atbalsts",
+  "support_and_feedback": "Atbalsts un atsauksmes",
   "swap_merge_direction": "",
-  "sync": "",
+  "sync": "Sinhronizēt",
   "template": "",
   "theme": "Dizains",
   "theme_selection": "",
   "theme_selection_description": "",
+  "they_will_be_merged_together": "Tās tiks apvienotas",
   "time_based_memories": "",
   "timezone": "Laika zona",
-  "toggle_settings": "",
+  "to_archive": "Arhivēt",
+  "to_change_password": "Mainīt paroli",
+  "toggle_settings": "Pārslēgt iestatījumus",
   "toggle_theme": "",
-  "toggle_visibility": "",
-  "total_usage": "",
+  "total_usage": "Kopējais lietojums",
   "trash": "Atkritne",
   "trash_all": "",
   "trash_no_results_message": "",
   "type": "",
   "unarchive": "Atarhivēt",
-  "unarchived": "",
-  "unfavorite": "Noņemt no Izlases",
-  "unhide_person": "",
+  "unfavorite": "Noņemt no izlases",
+  "unhide_person": "Atcelt personas slēpšanu",
   "unknown": "",
-  "unknown_album": "",
-  "unknown_year": "",
+  "unknown_year": "Nezināms gads",
+  "unlimited": "Neierobežots",
   "unlink_oauth": "",
   "unlinked_oauth_account": "",
+  "unnamed_album": "Albums bez nosaukuma",
+  "unsaved_change": "Nesaglabāta izmaiņa",
   "unselect_all": "",
   "unstack": "At-Stekot",
   "up_next": "",
   "updated_password": "",
   "upload": "Augšupielādēt",
   "upload_concurrency": "",
+  "upload_status_duplicates": "Dublikāti",
   "upload_status_errors": "Kļūdas",
   "upload_status_uploaded": "Augšupielādēts",
   "url": "",
-  "usage": "",
+  "usage": "Lietojums",
   "user": "Lietotājs",
   "user_id": "Lietotāja ID",
-  "user_usage_detail": "",
+  "user_usage_detail": "Informācija par lietotāju lietojumu",
   "username": "",
   "users": "Lietotāji",
-  "utilities": "",
+  "utilities": "Rīki",
   "validate": "",
   "variables": "",
   "version": "Versija",
@@ -803,11 +881,11 @@
   "view_links": "",
   "view_next_asset": "",
   "view_previous_asset": "",
-  "viewer": "",
-  "waiting": "",
+  "waiting": "Gaida",
   "week": "",
   "welcome_to_immich": "",
   "year": "",
+  "years_ago": "Pirms {years, plural, one {# gada} other {# gadiem}}",
   "yes": "Jā",
   "zoom_image": "Pietuvināt attēlu"
 }
diff --git a/web/src/lib/i18n/be.json b/i18n/mfa.json
similarity index 100%
rename from web/src/lib/i18n/be.json
rename to i18n/mfa.json
diff --git a/i18n/mk.json b/i18n/mk.json
new file mode 100644
index 0000000000..0bfbcfefe9
--- /dev/null
+++ b/i18n/mk.json
@@ -0,0 +1,28 @@
+{
+  "about": "Освежи",
+  "account": "Профил",
+  "account_settings": "Поставки за профилот",
+  "acknowledge": "Означи како прочитано",
+  "action": "Акција",
+  "actions": "Акции",
+  "active": "Активни",
+  "activity": "Активности",
+  "add": "Додај",
+  "add_a_description": "Додај опис",
+  "add_a_location": "Додај локација",
+  "add_a_name": "Додај име",
+  "add_a_title": "Додај наслов",
+  "add_exclusion_pattern": "Додај патерн за игнотирање",
+  "add_import_path": "Додај патека за импортирање",
+  "add_location": "Додај локација",
+  "add_more_users": "Додај уште корисници",
+  "add_partner": "Додај партнер",
+  "add_path": "Додај патека",
+  "add_photos": "Додај слики",
+  "add_to": "Додај во...",
+  "add_to_album": "Додај во албум",
+  "add_to_shared_album": "Додај во споделен албум",
+  "added_to_archive": "Додадено во архива",
+  "added_to_favorites": "Додадено во омилени",
+  "added_to_favorites_count": "Додадени {count, number} во омилени"
+}
diff --git a/web/src/lib/i18n/mn.json b/i18n/mn.json
similarity index 93%
rename from web/src/lib/i18n/mn.json
rename to i18n/mn.json
index 1bd96a43fd..b1a8a7970e 100644
--- a/web/src/lib/i18n/mn.json
+++ b/i18n/mn.json
@@ -28,11 +28,10 @@
   "added_to_favorites_count": "Дуртай зурагнуудад {count, number} нэмэгдлээ",
   "admin": {
     "authentication_settings": "Танин нэвтрэлт тохиргоо",
-    "authentication_settings_description": "",
+    "authentication_settings_description": "Нууц үгийн удирдлага, OAuth болон бусад танин нэвтрэлтийн тохиргоо",
+    "authentication_settings_disable_all": "Бүх нэвтрэх аргуудыг идэвхигүй болгохдоо итгэлтэй байна уу? Нэвтрэх үйлдэл бүрэн идэвхигүй болно.",
     "check_all": "Бүгдийг сонгох",
-    "crontab_guru": "",
     "disable_login": "",
-    "disabled": "",
     "duplicate_detection_job_description": "",
     "face_detection": "Нүүр илрүүлэх",
     "image_format_description": "",
@@ -40,21 +39,12 @@
     "image_prefer_embedded_preview_setting_description": "",
     "image_prefer_wide_gamut": "",
     "image_prefer_wide_gamut_setting_description": "",
-    "image_preview_format": "",
-    "image_preview_resolution": "",
-    "image_preview_resolution_description": "",
     "image_quality": "Чанар",
-    "image_quality_description": "",
     "image_settings": "",
     "image_settings_description": "",
-    "image_thumbnail_format": "",
-    "image_thumbnail_resolution": "",
-    "image_thumbnail_resolution_description": "",
     "job_settings": "Ажлын тохиргоо",
     "job_settings_description": "",
     "job_status": "Ажлын төлөв",
-    "library_cron_expression": "",
-    "library_cron_expression_presets": "",
     "library_scanning": "",
     "library_scanning_description": "",
     "library_scanning_enable_description": "",
@@ -165,7 +155,6 @@
     "theme_settings": "",
     "theme_settings_description": "",
     "thumbnail_generation_job_description": "",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "",
     "transcoding_acceleration_api_description": "",
     "transcoding_acceleration_nvenc": "",
@@ -214,8 +203,6 @@
     "transcoding_threads_description": "",
     "transcoding_tone_mapping": "",
     "transcoding_tone_mapping_description": "",
-    "transcoding_tone_mapping_npl": "",
-    "transcoding_tone_mapping_npl_description": "",
     "transcoding_transcode_policy": "",
     "transcoding_two_pass_encoding": "",
     "transcoding_two_pass_encoding_setting_description": "",
@@ -271,7 +258,6 @@
   "archive_or_unarchive_photo": "Зургийг архивт хийх эсвэл гаргах",
   "archive_size": "Архивын хэмжээ",
   "archive_size_description": "Татах үеийн архивын хэмжээг тохируулах (GiB-р)",
-  "archived": "",
   "asset_added_to_album": "Цомогт нэмсэн",
   "asset_adding_to_album": "Цомогт нэмж байна...",
   "asset_offline": "",
@@ -288,10 +274,6 @@
   "cancel_search": "Хайлт цуцлах",
   "cannot_merge_people": "",
   "cannot_update_the_description": "",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
   "change_date": "Огноо өөрчлөх",
   "change_expiration_time": "",
   "change_location": "Байршил өөрчлөх",
@@ -371,13 +353,6 @@
   "download": "",
   "downloading": "",
   "duration": "",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
   "edit_album": "",
   "edit_avatar": "",
   "edit_date": "",
@@ -396,8 +371,6 @@
   "edited": "",
   "editor": "",
   "email": "",
-  "empty": "",
-  "empty_album": "",
   "empty_trash": "Хогийн сав хоослох",
   "enable": "",
   "enabled": "",
@@ -411,8 +384,6 @@
     "unable_to_change_album_user_role": "",
     "unable_to_change_date": "",
     "unable_to_change_location": "",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_create_admin_account": "",
     "unable_to_create_library": "",
     "unable_to_create_user": "",
@@ -430,11 +401,9 @@
     "unable_to_play_video": "",
     "unable_to_refresh_user": "",
     "unable_to_remove_album_users": "",
-    "unable_to_remove_comment": "",
     "unable_to_remove_library": "",
     "unable_to_remove_partner": "",
     "unable_to_remove_reaction": "",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "",
     "unable_to_reset_password": "",
     "unable_to_resolve_duplicate": "",
@@ -456,10 +425,6 @@
     "unable_to_update_settings": "",
     "unable_to_update_user": ""
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exit_slideshow": "",
   "expand_all": "",
   "expire_after": "",
@@ -467,28 +432,22 @@
   "explore": "Эрж олох",
   "extension": "",
   "external_libraries": "",
-  "failed_to_get_people": "",
   "favorite": "",
   "favorite_or_unfavorite_photo": "",
   "favorites": "Дуртай",
-  "feature": "",
   "feature_photo_updated": "",
-  "featurecollection": "",
   "file_name": "",
   "file_name_or_extension": "",
   "filename": "",
-  "files": "",
   "filetype": "",
   "filter_people": "",
   "fix_incorrect_match": "",
-  "force_re-scan_library_files": "",
   "forward": "",
   "general": "",
   "get_help": "",
   "getting_started": "",
   "go_back": "",
   "go_to_search": "",
-  "go_to_share_page": "",
   "group_albums_by": "",
   "has_quota": "",
   "hide_gallery": "",
@@ -497,7 +456,6 @@
   "host": "",
   "hour": "",
   "image": "",
-  "img": "",
   "immich_logo": "",
   "import_path": "",
   "in_archive": "",
@@ -514,7 +472,6 @@
   },
   "invite_people": "Хүмүүс урих",
   "invite_to_album": "",
-  "job_settings_description": "",
   "jobs": "",
   "keep": "",
   "keyboard_shortcuts": "",
@@ -598,7 +555,6 @@
   "oldest_first": "",
   "online": "",
   "only_favorites": "Зөвхөн дуртай зурагнууд",
-  "only_refreshes_modified_files": "",
   "open_the_search_filters": "",
   "options": "",
   "organize_your_library": "",
@@ -626,7 +582,6 @@
   "pending": "",
   "people": "Хүмүүс",
   "people_sidebar_description": "",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "",
   "permanent_deletion_warning_setting_description": "",
   "permanently_delete": "",
@@ -640,7 +595,6 @@
   "play_memories": "",
   "play_motion_photo": "",
   "play_or_pause_video": "",
-  "point": "",
   "port": "",
   "preset": "",
   "preview": "",
@@ -650,8 +604,6 @@
   "primary": "",
   "profile_picture_set": "",
   "public_share": "",
-  "range": "",
-  "raw": "",
   "reaction_options": "",
   "read_changelog": "",
   "recent": "",
@@ -660,10 +612,10 @@
   "refreshed": "",
   "refreshes_every_file": "",
   "remove": "",
+  "remove_deleted_assets": "",
   "remove_from_album": "",
   "remove_from_favorites": "Дуртай зурагнуудаас хасах",
   "remove_from_shared_link": "",
-  "remove_offline_files": "",
   "removed_from_favorites": "Дуртай зурагнуудаас хасагдсан",
   "removed_from_favorites_count": "Дуртай зурагнуудаас {count, plural, other {Removed #}} хасагдлаа",
   "repair": "",
@@ -673,7 +625,6 @@
   "reset": "",
   "reset_password": "",
   "reset_people_visibility": "",
-  "reset_settings_to_default": "",
   "restore": "",
   "restore_user": "",
   "retry_upload": "",
@@ -684,8 +635,6 @@
   "saved_settings": "",
   "say_something": "",
   "scan_all_libraries": "",
-  "scan_all_library_files": "",
-  "scan_new_library_files": "",
   "scan_settings": "",
   "search": "",
   "search_albums": "",
@@ -713,7 +662,6 @@
   "select_photos": "",
   "selected": "",
   "send_message": "",
-  "server": "",
   "server_online": "Сервер Онлайн",
   "server_stats": "",
   "set": "",
@@ -775,18 +723,15 @@
   "timezone": "",
   "toggle_settings": "",
   "toggle_theme": "",
-  "toggle_visibility": "",
   "total_usage": "",
   "trash": "Хогийн сав",
   "trash_all": "",
   "trash_no_results_message": "",
   "type": "",
   "unarchive": "",
-  "unarchived": "",
   "unfavorite": "",
   "unhide_person": "",
   "unknown": "",
-  "unknown_album": "",
   "unknown_year": "",
   "unlink_oauth": "",
   "unlinked_oauth_account": "",
@@ -815,11 +760,14 @@
   "view_links": "",
   "view_next_asset": "",
   "view_previous_asset": "",
-  "viewer": "",
-  "waiting": "",
-  "week": "",
-  "welcome_to_immich": "",
-  "year": "",
-  "yes": "",
-  "zoom_image": ""
+  "waiting": "Хүлээж байна",
+  "warning": "Анхааруулга",
+  "week": "Долоо хоног",
+  "welcome": "Тавтай морил",
+  "welcome_to_immich": "Тавтай морилно уу",
+  "year": "Он",
+  "years_ago": "{years, plural, one {# year} other {# years}} өмнө",
+  "yes": "Тийм",
+  "you_dont_have_any_shared_links": "Танд хуваалцсан холбоос алга",
+  "zoom_image": "Зургийг томруулж харах"
 }
diff --git a/web/src/lib/i18n/ms.json b/i18n/mr.json
similarity index 100%
rename from web/src/lib/i18n/ms.json
rename to i18n/mr.json
diff --git a/i18n/ms.json b/i18n/ms.json
new file mode 100644
index 0000000000..f45f8e1c9f
--- /dev/null
+++ b/i18n/ms.json
@@ -0,0 +1,198 @@
+{
+  "about": "Tentang",
+  "account": "Akaun",
+  "account_settings": "Tetapan Akaun",
+  "acknowledge": "Akui",
+  "action": "Tindakan",
+  "actions": "Tindakan",
+  "active": "Aktif",
+  "activity": "Aktiviti",
+  "activity_changed": "Aktiviti {enabled, select, true {enabled} other {disabled}}",
+  "add": "Tambah",
+  "add_a_description": "Tambah penerangan",
+  "add_a_location": "Tambah lokasi",
+  "add_a_name": "Tambah nama",
+  "add_a_title": "Tambah tajuk",
+  "add_exclusion_pattern": "Tambahkan corak pengecualian",
+  "add_import_path": "Tambahkan laluan import",
+  "add_location": "Tambah lokasi",
+  "add_more_users": "Tambah user lagi",
+  "add_partner": "Tambah rakan",
+  "add_path": "Tambah laluan",
+  "add_photos": "Tambah gambar",
+  "add_to": "Tambah ke...",
+  "add_to_album": "Tambah ke album",
+  "add_to_shared_album": "Tambah ke album yang dikongsi",
+  "add_url": "Tambah URL",
+  "added_to_archive": "Tambah ke arkib",
+  "added_to_favorites": "Ditambah pada favorit",
+  "added_to_favorites_count": "Menambahkan {count, number} ke favorit",
+  "admin": {
+    "add_exclusion_pattern_description": "Tambahkan corak pengecualian. Globbing menggunakan *, **, dan ? disokong. Untuk mengabaikan semua fail dalam mana-mana direktori bernama \"Raw\", gunakan \"**/Raw/**\". Untuk mengabaikan semua fail yang berakhir dengan \".tif\", gunakan \"**/*.tif\". Untuk mengabaikan laluan mutlak, gunakan \"/path/to/ignore/**\".",
+    "asset_offline_description": "Aset pustaka luaran ini tidak lagi ditemui pada cakera dan telah dialihkan ke sampah. Jika fail telah dialihkan dalam pustaka, semak garis masa anda untuk aset baharu yang sepadan. Untuk memulihkan aset ini, sila pastikan bahawa laluan fail di bawah boleh diakses oleh Immich dan mengimbas pustaka.",
+    "authentication_settings": "Tetapan Pengesahan",
+    "authentication_settings_description": "Urus kata laluan, OAuth dan tetapan pengesahan lain",
+    "authentication_settings_disable_all": "Adakah anda pasti mahu melumpuhkan semua kaedah log masuk? Log masuk akan dilumpuhkan sepenuhnya.",
+    "authentication_settings_reenable": "Untuk menghidupkan semula, guna <link>Arahan Pelayan</link>.",
+    "background_task_job": "Tugas Latar Belakang",
+    "backup_database": "Sandar pangkalan data",
+    "backup_database_enable_description": "Aktifkan sandaran pangkalan data",
+    "backup_keep_last_amount": "Jumlah sandaran sebelumnya yang hendak disimpan",
+    "backup_settings": "Tetapan Sandaran",
+    "backup_settings_description": "Urus tetapan sandaran pangkalan data",
+    "check_all": "Tanda Semua",
+    "cleared_jobs": "Kerja telah dibersihkan untuk: {job}",
+    "config_set_by_file": "Konfigurasi kini ditetapkan oleh fail konfigurasi",
+    "confirm_delete_library": "Adakah anda pasti mahu memadamkan {library}?",
+    "confirm_delete_library_assets": "Adakah anda pasti mahu memadamkan pustaka ini? Ini akan memadam {count, plural, one {# aset yang terkandung} other {semua # aset yang terkandung}} daripada Immich dan tidak boleh dibuat asal. Fail akan kekal pada disk.",
+    "confirm_email_below": "Untuk mengesahkan, sila taip \"{email}\" dibawah",
+    "confirm_reprocess_all_faces": "Adakah anda pasti mahu memproses semula semua wajah? Ini juga akan membersihkan orang bernama.",
+    "confirm_user_password_reset": "Adakah anda pasti mahu menetapkan semula kata laluan {user}?",
+    "create_job": "Cipta tugas",
+    "cron_expression": "Ungkapan cron",
+    "cron_expression_description": "Tetapkan selang imbasan menggunakan format cron. Untuk maklumat lanjut, sila rujuk ke sebagai contoh <link>Crontab Guru</link>",
+    "cron_expression_presets": "Pratetap-pratetap ungkapan Cron",
+    "disable_login": "Lumpuhkan fungsi log masuk",
+    "duplicate_detection_job_description": "Jalankan pembelajaran mesin pada aset untuk mengesan imej yang serupa. Bergantung pada Carian Pintar",
+    "exclusion_pattern_description": "Corak pengecualian membolehkan anda mengabaikan fail dan folder semasa mengimbas pustaka anda. Ini berguna jika anda mempunyai folder yang mengandungi fail yang anda tidak mahu import, seperti fail RAW.",
+    "external_library_created_at": "Pustaka luaran (dicipta pada {date})",
+    "external_library_management": "Pengurusan Perpustakaan Luar",
+    "face_detection": "Pengesanan wajah",
+    "face_detection_description": "Kesan wajah dalam aset menggunakan pembelajaran mesin. Untuk video, hanya lakaran kecil dipertimbangkan. \"Segar Semula\" memproses semula semua aset. \"Tetapkan Semula\" juga mengosongkan semua data wajah semasa. \"Hilang\" baris gilir aset yang belum diproses lagi. Wajah yang dikesan akan beratur untuk Pengecaman Wajah selepas Pengesanan Wajah selesai, menghimpunkannya kepada orang sedia ada atau baharu.",
+    "facial_recognition_job_description": "Kumpulan wajah yang dikesan ke dalam orang. Langkah ini dijalankan selepas Pengesanan Wajah selesai. \"Tetapkan semula\" mengelompokkan semula semua wajah. \"Hilang\" jalankan proses pada wajah yang tidak mempunyai orang yang ditetapkan.",
+    "failed_job_command": "Perintah {command} gagal untuk kerja: {job}",
+    "force_delete_user_warning": "AMARAN: Ini akan mengalih keluar pengguna dan semua aset dengan serta-merta. Ia tidak boleh dibuat asal dan fail tidak boleh dipulihkan.",
+    "forcing_refresh_library_files": "Memaksa muat semula semua fail perpustakaan",
+    "image_format": "Format",
+    "image_format_description": "WebP menghasilkan fail yang lebih kecil daripada JPEG, tetapi lebih perlahan untuk mengekod.",
+    "image_prefer_embedded_preview": "Cadangkan pratonton terbenam",
+    "image_prefer_embedded_preview_setting_description": "Gunakan pratonton terbenam dalam foto RAW sebagai input kepada pemprosesan imej apabila tersedia. Cara ini boleh menghasilkan warna yang lebih tepat untuk sesetengah imej, tetapi kualiti pratonton bergantung pada kamera dan imej mungkin mempunyai lebih banyak artifak mampatan.",
+    "image_prefer_wide_gamut": "Cadangkan warna gamut yang luas",
+    "image_prefer_wide_gamut_setting_description": "Gunakan Paparan P3 untuk lakaran kenit. Ini lebih baik mengekalkan kerancakan imej dengan ruang warna yang luas, tetapi imej mungkin kelihatan berbeza pada peranti lama dengan versi penyemak imbas lama. Imej sRGB disimpan sebagai sRGB untuk mengelakkan peralihan warna.",
+    "image_preview_description": "Imej bersaiz sederhana dengan metadata yang dilucutkan, digunakan semasa melihat aset tunggal dan untuk pembelajaran mesin",
+    "image_preview_quality_description": "Kualiti pratonton dari 1-100. Lebih tinggi adalah lebih baik, tetapi menghasilkan fail yang lebih besar dan boleh mengurangkan responsif apl. Menetapkan nilai yang rendah boleh menjejaskan kualiti pembelajaran mesin.",
+    "image_preview_title": "Tetapan Pratonton",
+    "image_quality": "Kualiti",
+    "image_resolution": "Resolusi",
+    "image_resolution_description": "Resolusi yang lebih tinggi boleh meningkatkan ketajaman imej tetapi mengambil masa yang lebih lama untuk mengekod, mempunyai saiz fail yang lebih besar dan boleh mengurangkan responsif apl.",
+    "image_settings": "Tetapan Imej",
+    "image_settings_description": "Urus kualiti dan resolusi imej yang dihasilkan",
+    "image_thumbnail_description": "Lakaran kecil dengan metadata yang dilucutkan, digunakan semasa melihat kumpulan foto seperti garis masa utama",
+    "image_thumbnail_quality_description": "Kualiti lakaran kenit daripada 1-100. Lebih tinggi adalah lebih baik, tetapi menghasilkan fail yang lebih besar dan boleh mengurangkan responsif apl.",
+    "image_thumbnail_title": "Tetapan Lakaran Kenit",
+    "job_concurrency": "Konkurensi {job}",
+    "job_created": "Tugas yang dicipta",
+    "job_not_concurrency_safe": "Konkurensi tugas ini tidak selamat.",
+    "job_settings": "Tetapan Tugas",
+    "job_settings_description": "Urus konkurensi tugas",
+    "job_status": "Status Tugasan",
+    "jobs_delayed": "{jobCount, plural, other {# tertangguh}}",
+    "jobs_failed": "{jobCount, plural, other {# gagal}}",
+    "library_created": "Pustaka dicipta: {library}",
+    "library_deleted": "Pustaka dipadamkan",
+    "library_import_path_description": "Tentukan folder untuk diimport. Folder ini, termasuk subfolder, akan diimbas untuk imej dan video.",
+    "library_scanning": "Pengimbasan Berkala",
+    "library_scanning_description": "Konfigurasikan pengimbasan perpustakaan berkala",
+    "library_scanning_enable_description": "Dayakan pengimbasan perpustakaan berkala",
+    "library_settings": "Perpustakaan Luaran",
+    "library_settings_description": "Urus tetapan perpustakaan luaran",
+    "library_tasks_description": "Laksanakan tugas perpustakaan",
+    "library_watching_enable_description": "Perhatikan perpustakaan luaran untuk perubahan fail",
+    "library_watching_settings": "Perhati perpustakaan (EKSPERIMEN)",
+    "library_watching_settings_description": "Perhati fail yang diubah secara automatik",
+    "logging_enable_description": "Dayakan pengelogan",
+    "logging_level_description": "Apabila didayakan, tahap log yang hendak digunakan.",
+    "logging_settings": "Log",
+    "machine_learning_clip_model": "Model CLIP",
+    "machine_learning_clip_model_description": "Nama model CLIP disenaraikan <link>di sini</link>. Ambil perhatian bahawa anda mesti menjalankan semula tugas 'Carian Pintar' untuk semua imej selepas menukar model.",
+    "machine_learning_duplicate_detection": "Pengesanan Pendua",
+    "machine_learning_duplicate_detection_enabled": "Dayakan pengesanan pendua",
+    "machine_learning_duplicate_detection_enabled_description": "Jika dilumpuhkan, aset yang betul-betul serupa masih akan dinyahduakan.",
+    "machine_learning_duplicate_detection_setting_description": "Gunakan pembenaman CLIP untuk mencari kemungkinan pendua",
+    "machine_learning_enabled": "Dayakan pembelajaran mesin",
+    "machine_learning_enabled_description": "Jika dilumpuhkan, semua ciri Pembelajaran Mesin akan dilumpuhkan tanpa mengira tetapan di bawah.",
+    "machine_learning_facial_recognition": "Pengecaman Wajah",
+    "machine_learning_facial_recognition_description": "Mengesan, mengecam dan mengumpulkan wajah dalam imej",
+    "machine_learning_facial_recognition_model": "Model pengecaman wajah",
+    "machine_learning_facial_recognition_model_description": "Model disenaraikan dalam susunan saiz menurun. Model yang lebih besar adalah lebih perlahan dan menggunakan lebih banyak memori, tetapi menghasilkan hasil yang lebih baik. Ambil perhatian bahawa anda mesti menjalankan semula kerja Pengesanan Wajah untuk semua imej apabila menukar model.",
+    "machine_learning_facial_recognition_setting": "Dayakan pengecaman wajah",
+    "machine_learning_facial_recognition_setting_description": "Jika dilumpuhkan, imej tidak akan dikodkan untuk pengecaman wajah dan tidak akan mengisi bahagian Orang dalam halaman Teroka.",
+    "machine_learning_max_detection_distance": "Jarak pengesanan maksimum",
+    "machine_learning_max_detection_distance_description": "Jarak maksimum antara dua imej untuk menganggapnya sebagai pendua, antara 0.001-0.1. Nilai yang lebih tinggi akan mengesan lebih banyak pendua, tetapi mungkin menghasilkan positif palsu.",
+    "machine_learning_max_recognition_distance": "Jarak pengecaman maksimum",
+    "machine_learning_max_recognition_distance_description": "Jarak maksimum antara dua muka untuk dianggap sebagai orang yang sama, antara 0-2. Menurunkan ini boleh menghalang pelabelan dua orang sebagai orang yang sama, manakala menaikkannya boleh menghalang pelabelan orang yang sama sebagai dua orang yang berbeza. Ambil perhatian bahawa adalah lebih mudah untuk menggabungkan dua orang daripada membelah satu orang kepada dua, jadi silap pada bahagian ambang yang lebih rendah apabila boleh.",
+    "machine_learning_min_detection_score": "Skor pengesanan minimum",
+    "machine_learning_min_detection_score_description": "Skor keyakinan minimum untuk wajah dikesan dari 0-1. Nilai yang lebih rendah akan mengesan lebih banyak muka tetapi mungkin menghasilkan positif palsu.",
+    "machine_learning_min_recognized_faces": "Minimum mengenali wajah",
+    "machine_learning_min_recognized_faces_description": "Bilangan minima wajah yang dikenali untuk seseorang dicipta. Peningkatan ini menjadikan Pengecaman Wajah lebih tepat atas kos meningkatkan peluang wajah tidak diberikan kepada seseorang.",
+    "machine_learning_settings": "Tetapan Pembelajaran Mesin",
+    "machine_learning_smart_search_enabled_description": "Jika ditutup, gambar-gambar tidak akan dikodkan untuk carian pintar.",
+    "map_dark_style": "Tema gelap",
+    "map_enable_description": "Aktifkan ciri peta",
+    "map_gps_settings": "Tetapan Peta & GPS",
+    "map_light_style": "Tema terang",
+    "map_reverse_geocoding_enable_description": "Dayakan pengekodan geo terbalik",
+    "map_reverse_geocoding_settings": "Tetapan Pengekodan Geo Terbalik",
+    "map_settings": "Peta",
+    "map_settings_description": "Urus tetapan peta",
+    "metadata_extraction_job": "Sari metadata",
+    "metadata_extraction_job_description": "Sari maklumat metadata dari setiap aset, seperti GPS, muka-muka, dan pelaraian",
+    "metadata_faces_import_setting": "Dayakan import muka",
+    "metadata_settings": "Tetapan Metadata",
+    "metadata_settings_description": "Urus tetapan metadata",
+    "migration_job": "Migrasi",
+    "migration_job_description": "Pindahkan imej kecil untuk aset-aset dan muka-muka kepada struktur folder terkini",
+    "no_paths_added": "Tiada laluan yang ditambah",
+    "no_pattern_added": "Tiada corak ditambah",
+    "note_cannot_be_changed_later": "NOTA: Ini tidak boleh diubah kemudian!",
+    "note_unlimited_quota": "Nota: Masukkan 0 untuk kuota tanpa had",
+    "notification_email_from_address": "Dari alamat",
+    "notification_email_from_address_description": "Alamat e-mel penghantar, sebagai contoh: \"Immich Photo Server <noreply@example.com>\"",
+    "notification_email_host_description": "Hos e-mel pelayan (cth. smtp.immich.app)",
+    "notification_email_ignore_certificate_errors": "Abaikan ralat-ralat sijil",
+    "notification_email_password_description": "Kata laluan untuk digunakan semasa membuat pengesahan dengan pelayan e-mel",
+    "notification_email_port_description": "Port pelayan e-mel (cth 25, 465 atau 587)",
+    "notification_email_sent_test_email_button": "Hantar e-mel ujian dan simpan",
+    "notification_email_setting_description": "Tetapan-tetapan untuk menghantar notifikasi e-mel",
+    "notification_email_test_email": "Hantar e-mel ujian",
+    "notification_email_test_email_failed": "Gagal untuk menghantar e-mel ujian, sila semak nilai-nilai anda",
+    "notification_email_test_email_sent": "E-mel ujian telah dihantar ke {email}. Sila semak peti masuk anda.",
+    "notification_email_username_description": "Nama pengguna untuk digunakan semasa mengesahkan dengan pelayan e-mel",
+    "notification_enable_email_notifications": "Dayakan notifikasi-notifikasi e-mel",
+    "notification_settings": "Tetapan Pemberitahuan",
+    "notification_settings_description": "Urus tetapan-tetapan notifikasi, termasuk e-mel",
+    "oauth_auto_launch": "Pelancaran automatik",
+    "oauth_auto_register": "Daftar secara automatik",
+    "oauth_auto_register_description": "Daftar secara automatik pengguna-pengguna baharu selepas mendaftar masuk dengan OAuth",
+    "oauth_button_text": "Teks butang",
+    "oauth_client_id": "ID Pelanggan",
+    "oauth_client_secret": "Rahsia Pelanggan",
+    "oauth_enable_description": "Log masuk dengan OAuth",
+    "oauth_issuer_url": "URL Pengeluar",
+    "oauth_mobile_redirect_uri": "URI ubah hala mudah alih",
+    "oauth_scope": "Skop",
+    "oauth_settings": "OAuth",
+    "oauth_settings_description": "Urus tetapan-tetapan log masuk OAuth",
+    "oauth_settings_more_details": "Untuk maklumat lanjut tentang ciri ini, rujuk ke <link>dokumen</link>.",
+    "oauth_storage_quota_default": "Kuota storan lalai (GiB)",
+    "password_enable_description": "Log masuk dengan e-mel dan kata laluan",
+    "password_settings": "Kata Laluan Log Masuk",
+    "password_settings_description": "Urus tetapan-tetapan kata laluan log masuk",
+    "paths_validated_successfully": "Semua laluan berjaya disahkan",
+    "quota_size_gib": "Saiz Kuota (GiB)",
+    "registration": "Pendaftaran Admin",
+    "registration_description": "Memandangkan anda adalah pengguna pertama pada sistem, anda akan ditugaskan sebagai Admin dan bertanggungjawab untuk tugas pentadbiran, serta pengguna tambahan yang akan anda tambah.",
+    "repair_all": "Baiki Semua",
+    "require_password_change_on_login": "Perlukan pengguna menukar kata laluan ketika log masuk pertama",
+    "reset_settings_to_default": "Tetapkan semula tetapan kepada lalai",
+    "reset_settings_to_recent_saved": "Tetapkan semula tetapan kepada tetapan yang disimpan baru-baru ini"
+  },
+  "timeline": "Garis masa",
+  "total": "Jumlah",
+  "user_usage_stats": "Statistik penggunaan akaun",
+  "user_usage_stats_description": "Papar statistik penggunaan akaun",
+  "year": "Tahun",
+  "yes": "Ya",
+  "you_dont_have_any_shared_links": "Anda tidak mempunyai apa-apa pautan yang dikongsi",
+  "zoom_image": "Zum Gambar"
+}
diff --git a/web/src/lib/i18n/nb_NO.json b/i18n/nb_NO.json
similarity index 93%
rename from web/src/lib/i18n/nb_NO.json
rename to i18n/nb_NO.json
index df56d27a23..2d78ea414a 100644
--- a/web/src/lib/i18n/nb_NO.json
+++ b/i18n/nb_NO.json
@@ -33,6 +33,11 @@
     "authentication_settings_disable_all": "Er du sikker på at du ønsker å deaktivere alle innloggingsmetoder? Innlogging vil bli fullstendig deaktivert.",
     "authentication_settings_reenable": "For å aktivere på nytt, bruk en <link>Server Command</link>.",
     "background_task_job": "Bakgrunnsjobber",
+    "backup_database": "Backupdatabase",
+    "backup_database_enable_description": "Aktiver databasebackup",
+    "backup_keep_last_amount": "Antall backuper å beholde",
+    "backup_settings": "Backupinnstillinger",
+    "backup_settings_description": "Håndter innstillinger for databasebackup",
     "check_all": "Merk Alle",
     "cleared_jobs": "Ryddet opp jobber for: {job}",
     "config_set_by_file": "Konfigurasjonen er for øyeblikket satt av en konfigurasjonsfil",
@@ -41,9 +46,8 @@
     "confirm_email_below": "For å bekrefte, skriv inn \"{email}\" nedenfor",
     "confirm_reprocess_all_faces": "Er du sikker på at du vil behandle alle ansikter på nytt? Dette vil også fjerne navngitte personer.",
     "confirm_user_password_reset": "Er du sikker på at du vil tilbakestille passordet til {user}?",
-    "crontab_guru": "Crontab Guru",
+    "create_job": "Lag jobb",
     "disable_login": "Deaktiver innlogging",
-    "disabled": "Deaktivert",
     "duplicate_detection_job_description": "Kjør maskinlæring på filer for å oppdage lignende bilder. Krever bruk av Smart Search",
     "exclusion_pattern_description": "Ekskluderingsmønstre lar deg ignorere filer og mapper når du skanner biblioteket ditt. Dette er nyttig hvis du har mapper som inneholder filer du ikke vil importere, for eksempel RAW-filer.",
     "external_library_created_at": "Ekstern bibliotek (opprettet {date})",
@@ -54,21 +58,17 @@
     "failed_job_command": "Kommandoen {command} feilet for jobben: {job}",
     "force_delete_user_warning": "ADVARSEL: Dette vil umiddelbart fjerne brukeren og alle eiendeler. Dette kan ikke angres, og filene kan ikke gjenopprettes.",
     "forcing_refresh_library_files": "Tvinger oppdatering av alle bibliotekfiler",
+    "image_format": "Format",
     "image_format_description": "WebP gir mindre filer enn JPEG, men er tregere å lage.",
     "image_prefer_embedded_preview": "Foretrekk innebygd forhåndsvisning",
     "image_prefer_embedded_preview_setting_description": "Bruk innebygd forhåndsvisning i RAW-bilder som inndata til bildebehandling når tilgjengelig. Dette kan gi mer nøyaktige farger for noen bilder, men kvaliteten er avhengig av kamera og bildet kan ha komprimeringsartefakter.",
     "image_prefer_wide_gamut": "Foretrekk bredt fargespekter",
     "image_prefer_wide_gamut_setting_description": "Bruk Display P3 for miniatyrbilder. Dette bevarer glød bedre i bilder med bredt fargerom, men det kan hende bilder ser annerledes ut på gamle enheter med en gammel nettleserversjon. sRBG bilder beholdes som sRGB for å unngå fargeforskyvninger.",
-    "image_preview_format": "Forhåndsvisningsformat",
-    "image_preview_resolution": "Oppløsning for forhåndsvisninger",
-    "image_preview_resolution_description": "Brukes ved visning av enkeltbilder og for maskinlæring. Høyere oppløsning kan bevare flere detaljer men tar lengre tid, gir større filer, og kan redusere appens responsivitet.",
+    "image_preview_title": "Forhåndsvisningsinnstillinger",
     "image_quality": "Kvalitet",
-    "image_quality_description": "Bildekvalitet fra 1-100. Høyere verdi gir bedre kvalitet men større filer. Dette valget påvirker forhåndsvisning og miniatyrbilder.",
+    "image_resolution": "Oppløsning",
     "image_settings": "Bildeinnstilliinger",
     "image_settings_description": "Administrer kvalitet og oppløsning på genererte bilder",
-    "image_thumbnail_format": "Miniatyrbildeformat",
-    "image_thumbnail_resolution": "Miniatyrbildeoppløsning",
-    "image_thumbnail_resolution_description": "Brukes ved visning av grupper av bilder (hovedtidslinje, albumvisning, osv.). Høyere oppløsning kan bevare flere detaljer men tar lengre tid å lage, gir større filer, og kan redusere appens responsivitet.",
     "job_concurrency": "{job} samtidighet",
     "job_not_concurrency_safe": "Denne jobben er ikke samtidlighet sikker.",
     "job_settings": "Jobbinnstillinger",
@@ -77,9 +77,6 @@
     "jobs_delayed": "{jobCount, plural, other {# forsinket}}",
     "jobs_failed": "{jobCount, plural, other {# mislyktes}}",
     "library_created": "Opprettet bibliotek: {library}",
-    "library_cron_expression": "Cron-uttrykk",
-    "library_cron_expression_description": "Sett skanneintervallet ved å bruke cron-formatering. For mer informasjon, vennligst se f.eks. <link>Crontab Guru</link>.",
-    "library_cron_expression_presets": "Forhåndsinnstilte Cron-uttrykk",
     "library_deleted": "Bibliotek slettet",
     "library_import_path_description": "Spesifiser en mappe for importering. Denne mappen, inkludert undermapper, vil bli skannet for bilder og videoer.",
     "library_scanning": "Periodisk skanning",
@@ -122,7 +119,7 @@
     "machine_learning_smart_search_description": "Søk etter bilder semantisk ved å bruke CLIP-embeddings",
     "machine_learning_smart_search_enabled": "Aktiver smart søk",
     "machine_learning_smart_search_enabled_description": "Hvis deaktivert, vil bilder ikke bli enkodet for smart søk.",
-    "machine_learning_url_description": "URL til maskinlærings-serveren",
+    "machine_learning_url_description": "URL til maskinlærings-serveren. Hvis mer enn en URL er lagt inn, hver server vill bli forsøkt en om gangen frem til en svarer suksessfullt, i rekkefølge fra først til sist.",
     "manage_concurrency": "Administrer samtidighet",
     "manage_log_settings": "Administrer logginnstillinger",
     "map_dark_style": "Mørk stil",
@@ -138,6 +135,8 @@
     "map_style_description": "URL til et style.json-karttema",
     "metadata_extraction_job": "Hent metadata",
     "metadata_extraction_job_description": "Hent metadatainformasjon fra hver fil, for eksempel GPS-posisjon og oppløsning",
+    "metadata_settings": "Metadatainnstillinger",
+    "metadata_settings_description": "Administrer metadatainnstillinger",
     "migration_job": "Migrering",
     "migration_job_description": "Migrer miniatyrbilder for filer og ansikter til den nyeste mappestrukturen",
     "no_paths_added": "Ingen filbaner lagt til",
@@ -146,7 +145,7 @@
     "note_cannot_be_changed_later": "MERK: Dette kan ikke endres senere!",
     "note_unlimited_quota": "Merk: Skriv inn 0 for ubegrenset kvote",
     "notification_email_from_address": "Fra adresse",
-    "notification_email_from_address_description": "Avsenderens e-postadresse, for eksempel: \"Immich Photo Server <noreply@immich.app>\"",
+    "notification_email_from_address_description": "Avsenderens e-postadresse, for eksempel: \"Immich Photo Server <noreply@example.com>\"",
     "notification_email_host_description": "Verten til e-posts serveren (f.eks. smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Ignorer sertifikatfeil",
     "notification_email_ignore_certificate_errors_description": "Ignorer valideringsfeil for TLS-sertifikat (ikke anbefalt)",
@@ -196,15 +195,12 @@
     "refreshing_all_libraries": "Oppdaterer alle biblioteker",
     "registration": "Administrator registrering",
     "registration_description": "Siden du er den første brukeren på systemet, vil du bli utnevnt til administrator og ha ansvar for administrative oppgaver. Du vil også opprette eventuelle nye brukere.",
-    "removing_offline_files": "Fjerner frakoblede filer",
     "repair_all": "Reparer alle",
     "repair_matched_items": "Samsvarte med {count, plural, one {# element} other {# elementer}}",
     "repaired_items": "Reparerte {count, plural, one {# item} other {# items}}",
     "require_password_change_on_login": "Krev at brukeren endrer passord ved første pålogging",
     "reset_settings_to_default": "Tilbakestill innstillinger til standard",
     "reset_settings_to_recent_saved": "Tilbakestill innstillingene til de nylig lagrede innstillingene",
-    "scanning_library_for_changed_files": "Skanner biblioteket for endrede filer",
-    "scanning_library_for_new_files": "Skanner biblioteket for nye filer",
     "send_welcome_email": "Send velkomst-e-post",
     "server_external_domain_settings": "Eksternt domene",
     "server_external_domain_settings_description": "Domene for offentlige delingslenker, inkludert http(s)://",
@@ -239,7 +235,6 @@
     "these_files_matched_by_checksum": "Disse filene er matchet ved hjelp av deres checksum",
     "thumbnail_generation_job": "Generer miniatyrbilder",
     "thumbnail_generation_job_description": "Generer store, små og uskarpe miniatyrbilder for hver fil, samt miniatyrbilder for hver person",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "Akselerasjons-API",
     "transcoding_acceleration_api_description": "API-et som vil samhandle med enheten din for å akselerere transcoding. Denne innstillingen er 'best effort': den vil falle tilbake til programvaretranscoding ved feil. VP9 kan eller kan ikke fungere avhengig av maskinvaren din.",
     "transcoding_acceleration_nvenc": "NVENC (krever NVIDIA GPU)",
@@ -291,8 +286,6 @@
     "transcoding_threads_description": "Høyere verdier fører til raskere koding, men gir mindre plass for serveren til å behandle andre oppgaver mens den er aktiv. Verdien bør ikke være mer enn antall CPU-kjerner. Maksimerer utnyttelsen hvis satt til 0.",
     "transcoding_tone_mapping": "Tone mapping",
     "transcoding_tone_mapping_description": "Forsøker å bevare utseendet til HDR-videoer når de konverteres til SDR. Hver algoritme gjør ulike avveininger mellom farge, detaljer og lysstyrke. Hable bevarer detaljer, Mobius bevarer farge, og Reinhard bevarer lysstyrke.",
-    "transcoding_tone_mapping_npl": "Tone mapping NPL",
-    "transcoding_tone_mapping_npl_description": "Fargene vil bli justert for å se normale ut på en skjerm med denne lysstyrken. Motintuitivt øker lavere verdier lysstyrken på videoen, og omvendt, siden det kompenserer for skjermens lysstyrke. 0 setter denne verdien automatisk.",
     "transcoding_transcode_policy": "Transkode retningslinjer",
     "transcoding_transcode_policy_description": "Retningslinjer for når en video skal transkodes. HDR-videoer vil alltid bli transkodet (unntatt hvis transkoding er deaktivert).",
     "transcoding_two_pass_encoding": "To-passert koding",
@@ -371,7 +364,6 @@
   "archive_or_unarchive_photo": "Arkiver eller ta ut av arkivet",
   "archive_size": "Arkivstørrelse",
   "archive_size_description": "Konfigurer arkivstørrelsen for nedlastinger (i GiB)",
-  "archived": "Arkivert",
   "archived_count": "{count, plural, other {Arkivert #}}",
   "are_these_the_same_person": "Er disse samme person?",
   "are_you_sure_to_do_this": "Er du sikker på at du vil gjøre dette?",
@@ -384,11 +376,11 @@
   "asset_offline": "Fil utilgjengelig",
   "asset_offline_description": "Dette elementet er offline. Immich kan ikke aksessere dets lokasjon. Vennlist påse at elementet er tilgijengelig og skann så biblioteket på nytt.",
   "asset_skipped": "Hoppet over",
+  "asset_skipped_in_trash": "I søppelbøtten",
   "asset_uploaded": "Lastet opp",
   "asset_uploading": "Laster opp...",
   "assets": "Filer",
   "assets_added_count": "Lagt til {count, plural, one {# element} other {# elementer}}",
-  "assets_moved_to_trash": "Flyttet {count, plural, one {# fil} other {# filer}} til papirkurv",
   "assets_restore_confirmation": "Er du sikker på at du vil gjenopprette alle slettede eiendeler? Denne handlingen kan ikke angres!",
   "authorized_devices": "Autoriserte enheter",
   "back": "Tilbake",
@@ -407,10 +399,6 @@
   "cannot_merge_people": "Kan ikke slå sammen personer",
   "cannot_undo_this_action": "Du kan ikke gjøre om denne handlingen!",
   "cannot_update_the_description": "Kan ikke oppdatere beskrivelsen",
-  "cant_apply_changes": "Kan ikke gjennomføre endringene",
-  "cant_get_faces": "Kan ikke hente ansikter",
-  "cant_search_people": "Kan ikke søke etter personer",
-  "cant_search_places": "Kan ikke søke etter steder",
   "change_date": "Endre dato",
   "change_expiration_time": "Endre utløpstid",
   "change_location": "Endre sted",
@@ -510,13 +498,6 @@
   "duplicates": "Duplikater",
   "duplicates_description": "Løs hver gruppe ved å angi hvilke, hvis noen, er duplikater",
   "duration": "Varighet",
-  "durations": {
-    "days": "{days, plural, one {dag} other {{days, number} dager}}",
-    "hours": "{hours, plural, one {time} other {{hours, number} timer}}",
-    "minutes": "{minutes, plural, one {minutt} other {{minutes, number} minutter}}",
-    "months": "{months, plural, one {måned} other {{months, number} måneder}}",
-    "years": "{years, plural, one {år} other {{years, number} år}}"
-  },
   "edit_album": "Rediger album",
   "edit_avatar": "Rediger avatar",
   "edit_date": "Rediger dato",
@@ -535,8 +516,6 @@
   "edited": "Redigert",
   "editor": "Redaktør",
   "email": "E-postadresse",
-  "empty": "",
-  "empty_album": "Tomt Album",
   "empty_trash": "Tøm papirkurv",
   "enable": "",
   "enabled": "",
@@ -560,8 +539,6 @@
     "unable_to_change_date": "Kan ikke endre dato",
     "unable_to_change_location": "Kan ikke endre plassering",
     "unable_to_change_password": "Kan ikke endre passord",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_copy_to_clipboard": "Kan ikke kopiere til utklippstavlen, sørg for at du får tilgang til siden via HTTPS",
     "unable_to_create_admin_account": "",
     "unable_to_create_api_key": "Kan ikke opprette en ny API-nøkkel",
@@ -588,12 +565,10 @@
     "unable_to_refresh_user": "Kan ikke oppdatere bruker",
     "unable_to_remove_album_users": "Kan ikke fjerne brukere fra album",
     "unable_to_remove_api_key": "Kan ikke fjerne API-nøkkel",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_deleted_assets": "Kan ikke fjerne offlinefiler",
     "unable_to_remove_library": "Kan ikke fjerne bibliotek",
-    "unable_to_remove_offline_files": "Kan ikke fjerne offlinefiler",
     "unable_to_remove_partner": "Kan ikke fjerne partner",
     "unable_to_remove_reaction": "Kan ikke fjerne reaksjon",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "Kan ikke reparere elementer",
     "unable_to_reset_password": "Kan ikke tilbakestille passord",
     "unable_to_resolve_duplicate": "Kan ikke løse duplikat",
@@ -617,10 +592,6 @@
     "unable_to_update_timeline_display_status": "Kan ikke oppdatere visningsstatus for tidslinje",
     "unable_to_update_user": "Kan ikke oppdatere bruker"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exit_slideshow": "Avslutt lysbildefremvisning",
   "expand_all": "Utvid alle",
   "expire_after": "Utgå etter",
@@ -631,29 +602,23 @@
   "extension": "Utvidelse",
   "external": "Ekstern",
   "external_libraries": "Eksterne Bibliotek",
-  "failed_to_get_people": "Kunne ikke hente personer",
   "favorite": "Favoritt",
   "favorite_or_unfavorite_photo": "Merk som favoritt eller fjern som favoritt",
   "favorites": "Favoritter",
-  "feature": "",
   "feature_photo_updated": "Fremhevet bilde oppdatert",
-  "featurecollection": "",
   "file_name": "Filnavn",
   "file_name_or_extension": "Filnavn eller filtype",
   "filename": "Filnavn",
-  "files": "",
   "filetype": "Filtype",
   "filter_people": "Filtrer personer",
   "find_them_fast": "Finn dem raskt ved søking av navn",
   "fix_incorrect_match": "Fiks feilaktig match",
-  "force_re-scan_library_files": "Tving til å skanne alle bibliotekfiler på nytt",
   "forward": "Fremover",
   "general": "Generelt",
   "get_help": "Få Hjelp",
   "getting_started": "Kom i gang",
   "go_back": "Gå tilbake",
   "go_to_search": "Gå til søk",
-  "go_to_share_page": "Gå til delingssiden",
   "group_albums_by": "Grupper album etter...",
   "has_quota": "Har kvote",
   "hide_gallery": "Skjul galleri",
@@ -662,7 +627,6 @@
   "host": "Vert",
   "hour": "Time",
   "image": "Bilde",
-  "img": "",
   "immich_logo": "Immich Logo",
   "immich_web_interface": "Immich webgrensesnitt",
   "import_from_json": "Importer fra JSON",
@@ -681,7 +645,6 @@
   },
   "invite_people": "Inviter Personer",
   "invite_to_album": "Inviter til album",
-  "job_settings_description": "",
   "jobs": "Oppgaver",
   "keep": "Behold",
   "keep_all": "Behold alle",
@@ -774,7 +737,6 @@
   "oldest_first": "Eldste først",
   "online": "Tilkoblet",
   "only_favorites": "Bare favoritter",
-  "only_refreshes_modified_files": "Oppdaterer bare endrede filer",
   "open_the_search_filters": "Åpne søkefiltrene",
   "options": "Valg",
   "organize_your_library": "Organiser biblioteket ditt",
@@ -805,12 +767,10 @@
   "pending": "Avventer",
   "people": "Folk",
   "people_sidebar_description": "Vis en lenke til Personer i sidepanelet",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "Advarsel om permanent sletting",
   "permanent_deletion_warning_setting_description": "Vis en advarsel ved permanent sletting av filer",
   "permanently_delete": "Slett permanent",
   "permanently_deleted_asset": "Filen har blitt permanent slettet",
-  "permanently_deleted_assets": "Permanent slettet {count, plural, one {# asset} other {# assets}}",
   "photos": "Bilder",
   "photos_count": "{count, plural, one {{count, number} Bilde} other {{count, number} Bilder}}",
   "photos_from_previous_years": "Bilder fra tidliger år",
@@ -821,7 +781,6 @@
   "play_memories": "Spill av minner",
   "play_motion_photo": "Spill av bevegelsesbilde",
   "play_or_pause_video": "Spill av eller pause video",
-  "point": "",
   "port": "Port",
   "preset": "Forhåndsinstilling",
   "preview": "Forhåndsvis",
@@ -831,8 +790,6 @@
   "primary": "Primær",
   "profile_picture_set": "Profilbildet er satt.",
   "public_share": "Offentlig deling",
-  "range": "",
-  "raw": "",
   "reaction_options": "Reaksjonsalternativer",
   "read_changelog": "Les endringslogg",
   "recent": "Nylig",
@@ -841,10 +798,10 @@
   "refreshed": "Oppdatert",
   "refreshes_every_file": "Oppdaterer alle filer",
   "remove": "Fjern",
+  "remove_deleted_assets": "Fjern fra frakoblede filer",
   "remove_from_album": "Fjern fra album",
   "remove_from_favorites": "Fjern fra favoritter",
   "remove_from_shared_link": "Fjern fra delt lenke",
-  "remove_offline_files": "Fjern fra frakoblede filer",
   "removed_api_key": "Fjernet API-nøkkel: {name}",
   "rename": "Gi nytt navn",
   "repair": "Reparer",
@@ -855,7 +812,6 @@
   "reset": "Tilbakestill",
   "reset_password": "Tilbakestill passord",
   "reset_people_visibility": "Tilbakestill personsynlighet",
-  "reset_settings_to_default": "",
   "resolved_all_duplicates": "Løste alle duplikater",
   "restore": "Gjenopprett",
   "restore_all": "Gjenopprett alle",
@@ -870,8 +826,6 @@
   "saved_settings": "Lagret instillinger",
   "say_something": "Si noe",
   "scan_all_libraries": "Skann alle biblioteker",
-  "scan_all_library_files": "Skann alle bibliotekfiler på nytt",
-  "scan_new_library_files": "Skann nye bibliotekfiler",
   "scan_settings": "Skanneinnstillinger",
   "search": "Søk",
   "search_albums": "Søk i album",
@@ -902,7 +856,6 @@
   "selected": "Valgt",
   "send_message": "Send melding",
   "send_welcome_email": "Send velkomstmelding",
-  "server": "Server",
   "server_stats": "Server Statistikk",
   "set": "Sett",
   "set_as_album_cover": "Sett som albumomslag",
@@ -974,7 +927,6 @@
   "to_trash": "Papirkurv",
   "toggle_settings": "Bytt innstillinger",
   "toggle_theme": "Bytt tema",
-  "toggle_visibility": "Bytt synlighet",
   "total_usage": "Totalt brukt",
   "trash": "Papirkurv",
   "trash_all": "Slett alt",
@@ -982,11 +934,9 @@
   "trashed_items_will_be_permanently_deleted_after": "Elementer i papirkurven vil bli permanent slettet etter {days, plural, one {# dag} other {# dager}}.",
   "type": "Type",
   "unarchive": "Fjern fra arkiv",
-  "unarchived": "Uarkivert",
   "unfavorite": "Fjern favoritt",
   "unhide_person": "Vis person",
   "unknown": "Ukjent",
-  "unknown_album": "Ukjent Album",
   "unknown_year": "Ukjent År",
   "unlimited": "Ubegrenset",
   "unlink_oauth": "Fjern kobling til OAuth",
@@ -1021,11 +971,10 @@
   "view_links": "Vis lenker",
   "view_next_asset": "Vis neste fil",
   "view_previous_asset": "Vis forrige fil",
-  "viewer": "Seer",
   "waiting": "Venter",
   "week": "Uke",
   "welcome": "Velkommen",
-  "welcome_to_immich": "Velkommen til immich",
+  "welcome_to_immich": "Velkommen til Immich",
   "year": "År",
   "yes": "Ja",
   "you_dont_have_any_shared_links": "Du har ingen delte lenker",
diff --git a/web/src/lib/i18n/nl.json b/i18n/nl.json
similarity index 90%
rename from web/src/lib/i18n/nl.json
rename to i18n/nl.json
index d6b3373152..de88cba9da 100644
--- a/web/src/lib/i18n/nl.json
+++ b/i18n/nl.json
@@ -23,16 +23,23 @@
   "add_to": "Toevoegen aan...",
   "add_to_album": "Aan album toevoegen",
   "add_to_shared_album": "Aan gedeeld album toevoegen",
+  "add_url": "URL toevoegen",
   "added_to_archive": "Toegevoegd aan archief",
   "added_to_favorites": "Toegevoegd aan favorieten",
   "added_to_favorites_count": "{count, number} toegevoegd aan favorieten",
   "admin": {
     "add_exclusion_pattern_description": "Uitsluitingspatronen toevoegen. Globbing met *, ** en ? wordt ondersteund. Om alle bestanden in een map met de naam \"Raw\" te negeren, gebruik \"**/Raw/**\". Om alle bestanden die eindigen op \".tif\" te negeren, gebruik \"**/*.tif\". Om een absoluut pad te negeren, gebruik \"/path/to/ignore/**\".",
+    "asset_offline_description": "Deze asset uit een externe bibliotheek is niet meer beschikbaar op de schijf en is naar de prullenbak verplaatst. Als het bestand binnen de bibliotheek is verplaatst, controleer dan je tijdlijn voor de nieuwe bijbehorende asset. Om dit bestand te herstellen, zorg ervoor dat het onderstaande bestandspad toegankelijk is voor Immich en scan de bibliotheek opnieuw.",
     "authentication_settings": "Authenticatie-instellingen",
     "authentication_settings_description": "Wachtwoord, OAuth, en andere authenticatie-instellingen beheren",
     "authentication_settings_disable_all": "Weet je zeker dat je alle inlogmethoden wilt uitschakelen? Inloggen zal volledig worden uitgeschakeld.",
     "authentication_settings_reenable": "Gebruik een <link>servercommando</link> om opnieuw in te schakelen.",
     "background_task_job": "Achtergrondtaken",
+    "backup_database": "Backup Database",
+    "backup_database_enable_description": "Database back-ups activeren",
+    "backup_keep_last_amount": "Maximaal aantal back-ups om te bewaren",
+    "backup_settings": "Back-up instellingen",
+    "backup_settings_description": "Database back-up instellingen beheren",
     "check_all": "Controleer het logboek",
     "cleared_jobs": "Taken gewist voor: {job}",
     "config_set_by_file": "Instellingen worden momenteel beheerd door een configuratiebestand",
@@ -41,35 +48,40 @@
     "confirm_email_below": "Typ hieronder \"{email}\" ter bevestiging",
     "confirm_reprocess_all_faces": "Weet je zeker dat je alle gezichten opnieuw wilt verwerken? Hiermee worden ook alle mensen gewist.",
     "confirm_user_password_reset": "Weet u zeker dat je het wachtwoord van {user} wilt resetten?",
-    "crontab_guru": "Crontab Guru",
+    "create_job": "Taak maken",
+    "cron_expression": "Cron expressie",
+    "cron_expression_description": "Stel de scaninterval in met het cron-formaat. Voor meer informatie kun je kijken naar bijvoorbeeld <link>Crontab Guru</link>",
+    "cron_expression_presets": "Cron-expressie presets",
     "disable_login": "Inloggen uitschakelen",
-    "disabled": "Uitgeschakeld",
     "duplicate_detection_job_description": "Machine learning uitvoeren op assets om vergelijkbare assets te vinden. Dit is gebaseerd op Slim Zoeken",
     "exclusion_pattern_description": "Met uitsluitingspatronen kun je bestanden en mappen negeren bij het scannen van je bibliotheek. Dit is handig als je mappen hebt met bestanden die je niet wilt importeren, zoals RAW bestanden.",
     "external_library_created_at": "Externe bibliotheek (gemaakt op {date})",
     "external_library_management": "Externe bibliotheek beheren",
     "face_detection": "Gezichtsdetectie",
-    "face_detection_description": "Detecteer gezichten in assets met behulp van machine learning. Voor video's wordt alleen de thumbnail gebruikt. \"Alle\" verwerkt alle assets (opnieuw). \"Missend\" plaatst assets in de wachtrij die nog niet zijn verwerkt. Gedetecteerde gezichten worden in de wachtrij geplaatst voor gezichtsherkenning nadat gezichtsdetectie is voltooid, waarbij ze worden gegroepeerd in bestaande of nieuwe mensen.",
-    "facial_recognition_job_description": "Groepeer gedetecteerde gezichten tot mensen. Deze stap wordt uitgevoerd nadat gezichtsdetectie is voltooid. \"Alle\" (her-)clustert alle gezichten. \"Missend\" plaatst gezichten in de wachtrij waaraan geen persoon is toegewezen.",
+    "face_detection_description": "Detecteer gezichten in assets met behulp van machine learning. Voor video's wordt alleen de thumbnail gebruikt. \"Resetten\" verwerkt alle assets (opnieuw). \"Reset\" verwijdert daarnaast alle huidige gezichtgegevens. \"Missend\" plaatst assets in de wachtrij die nog niet zijn verwerkt. Gedetecteerde gezichten worden in de wachtrij geplaatst voor gezichtsherkenning nadat gezichtsdetectie is voltooid, waarbij ze worden gegroepeerd in bestaande of nieuwe mensen.",
+    "facial_recognition_job_description": "Groepeer gedetecteerde gezichten tot mensen. Deze stap wordt uitgevoerd nadat gezichtsdetectie is voltooid. \"Resetten\" (her-)clustert alle gezichten. \"Missend\" plaatst gezichten in de wachtrij waaraan geen persoon is toegewezen.",
     "failed_job_command": "Commando {command} mislukt voor taak: {job}",
     "force_delete_user_warning": "WAARSCHUWING: Hiermee worden de gebruiker en alle assets onmiddellijk verwijderd. Dit kan niet ongedaan worden gemaakt en de bestanden kunnen niet worden hersteld.",
     "forcing_refresh_library_files": "Geforceerd vernieuwen van alle bibliotheekbestanden",
+    "image_format": "Formaat",
     "image_format_description": "WebP produceert kleinere bestanden dan JPEG, maar is langzamer om te verwerken.",
     "image_prefer_embedded_preview": "Ingebedde voorbeeldafbeelding gebruiken",
     "image_prefer_embedded_preview_setting_description": "Ingebedde voorbeeldafbeelding van RAW bestanden gebruiken als invoer voor beeldverwerking wanneer beschikbaar. Dit kan preciezere kleuren produceren voor sommige afbeeldingen, maar de kwaliteit van het voorbeeld is afhankelijk van de camera en de afbeelding kan mogelijk meer compressie-artefacten hebben.",
     "image_prefer_wide_gamut": "Voorkeur geven aan wide gamut",
     "image_prefer_wide_gamut_setting_description": "Display P3 gebruiken voor voorbeeldafbeeldingen. Dit behoudt de levendigheid van afbeeldingen met brede kleurruimtes beter, maar afbeeldingen kunnen er anders uitzien op oude apparaten met een oude browserversie. sRGB-afbeeldingen blijven sRGB gebruiken om kleurverschuivingen te vermijden.",
-    "image_preview_format": "Voorbeeldformaat",
-    "image_preview_resolution": "Voorbeeldresolutie",
-    "image_preview_resolution_description": "Gebruikt bij het tonen van een enkele foto en voor machine learning. Hogere resoluties kunnen meer detail behouden maar duren langer om te verwerken, hebben hogere bestandsgrootte, en kunnen de applicatie langzamer maken.",
+    "image_preview_description": "Middelgrote afbeelding met verwijderde metadata, gebruikt bij het bekijken van een enkele asset en voor machine learning",
+    "image_preview_quality_description": "Voorbeeldafbeelding kwaliteit van 1-100. Hoger is beter, maar produceert grotere bestanden en kan de app vertragen. Een lage waarde kan de kwaliteit van machine learning beïnvloeden.",
+    "image_preview_title": "Voorbeeldafbeelding instellingen",
     "image_quality": "Kwaliteit",
-    "image_quality_description": "Afbeeldingskwaliteit van 1-100. Een hoger getal zorgt voor een betere fotokwaliteit, maar produceert grotere bestanden. Dit heeft effect op voorbeeldfoto's en thumbnails.",
+    "image_resolution": "Resolutie",
+    "image_resolution_description": "Hogere resoluties behouden meer details, maar verhogen de coderingstijd, bestandsgrootte en kunnen de app vertragen.",
     "image_settings": "Afbeeldings instellingen",
     "image_settings_description": "Beheer de kwaliteit en resolutie van gegenereerde afbeeldingen",
-    "image_thumbnail_format": "Thumbnail bestandsformaat",
-    "image_thumbnail_resolution": "Thumbnail resolutie",
-    "image_thumbnail_resolution_description": "Gebruikt wanneer groepen foto's bekeken worden (hoofdtijdslijn, album, enzo). Hogere resoluties kunnen meer detail behouden maar duren langer om te verwerken, hebben hogere bestandsgrootte, en kunnen de applicatie langzamer maken.",
+    "image_thumbnail_description": "Kleine thumbnail zonder metadata, gebruikt voor het bekijken van groepen met foto's zoals de tijdlijn",
+    "image_thumbnail_quality_description": "Thumbnail kwaliteit van 1-100. Hoger is beter, maar produceert grotere bestanden en kan de app vertragen.",
+    "image_thumbnail_title": "Thumbnail instellingen",
     "job_concurrency": "{job} gelijktijdigheid",
+    "job_created": "Taak aangemaakt",
     "job_not_concurrency_safe": "Deze taak kan niet gelijktijdig worden uitgevoerd.",
     "job_settings": "Achtergrondtaak-instellingen",
     "job_settings_description": "Beheer gelijktijdige taken",
@@ -77,9 +89,6 @@
     "jobs_delayed": "{jobCount, plural, other {# vertraagd}}",
     "jobs_failed": "{jobCount, plural, other {# mislukt}}",
     "library_created": "Bibliotheek aangemaakt: {library}",
-    "library_cron_expression": "Cron expressie",
-    "library_cron_expression_description": "Stel het scaninterval in met het cron-formaat. Voor meer informatie kun je kijken naar bijvoorbeeld <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Standaard cron expressies",
     "library_deleted": "Bibliotheek verwijderd",
     "library_import_path_description": "Voer een map in om te importeren. Deze map, inclusief submappen, wordt gescand op afbeeldingen en video's.",
     "library_scanning": "Periodiek scannen",
@@ -122,7 +131,7 @@
     "machine_learning_smart_search_description": "Semantisch zoeken naar afbeeldingen met CLIP-embeddings",
     "machine_learning_smart_search_enabled": "Slim zoeken inschakelen",
     "machine_learning_smart_search_enabled_description": "Indien uitgeschakeld, worden afbeeldingen niet verwerkt voor slim zoeken.",
-    "machine_learning_url_description": "URL van de machine learning server",
+    "machine_learning_url_description": "De URL van de machine learning server. Als er meer dan één URL is opgegeven, wordt elke server geprobeerd totdat er een succesvol reageert, op volgorde van eerste tot laatste.",
     "manage_concurrency": "Beheer gelijktijdigheid",
     "manage_log_settings": "Beheer logboekinstellingen",
     "map_dark_style": "Donkere stijl",
@@ -152,7 +161,7 @@
     "note_cannot_be_changed_later": "LET OP: Dit kan later niet meer worden gewijzigd!",
     "note_unlimited_quota": "Opmerking: voer 0 in voor onbeperkt",
     "notification_email_from_address": "Adres afzender",
-    "notification_email_from_address_description": "E-mailadres van de afzender, bijvoorbeeld: \"Immich Foto Server <noreply@immich.app>\"",
+    "notification_email_from_address_description": "E-mailadres van de afzender, bijvoorbeeld: \"Immich Foto Server <noreply@example.com>\"",
     "notification_email_host_description": "Host van de e-mailserver (bijv. smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Negeer certificaatfouten",
     "notification_email_ignore_certificate_errors_description": "Negeer TLS certificaat validatiefouten (niet aanbevolen)",
@@ -198,27 +207,29 @@
     "password_settings": "Inloggen met wachtwoord",
     "password_settings_description": "Beheer instellingen voor inloggen met wachtwoord",
     "paths_validated_successfully": "Alle paden succesvol gevalideerd",
+    "person_cleanup_job": "Persoon opschoning",
     "quota_size_gib": "Opslaglimiet (GiB)",
-    "refreshing_all_libraries": "Alle bibliotheken vernieuwen",
+    "refreshing_all_libraries": "Alle bibliotheken aan het vernieuwen",
     "registration": "Beheerder registratie",
     "registration_description": "Omdat je de eerste gebruiker in het systeem bent, word je toegewezen als beheerder en ben je verantwoordelijk voor administratieve taken. Extra gebruikers kunnen door jou worden aangemaakt.",
-    "removing_offline_files": "Offline bestanden verwijderen",
     "repair_all": "Repareer alle",
     "repair_matched_items": "Overeenkomend {count, plural, one {# item} other {# items}}",
     "repaired_items": "Gerepareerd {count, plural, one {# item} other {# items}}",
     "require_password_change_on_login": "Vereisen dat de gebruiker het wachtwoord wijzigt bij de eerste keer inloggen",
     "reset_settings_to_default": "Instellingen teruggezet naar standaard",
     "reset_settings_to_recent_saved": "Instellingen zijn gereset naar de recent opgeslagen instellingen",
-    "scanning_library_for_changed_files": "Bibliotheek scannen op gewijzigde bestanden",
-    "scanning_library_for_new_files": "Bibliotheek scannen op nieuwe bestanden",
+    "scanning_library": "Bibliotheek scannen",
+    "search_jobs": "Taak zoeken...",
     "send_welcome_email": "Stuur een welkomstmail",
     "server_external_domain_settings": "Extern domein",
     "server_external_domain_settings_description": "Domein voor openbaar gedeelde links, inclusief http(s)://",
+    "server_public_users": "Openbare gebruikerslijst",
+    "server_public_users_description": "Alle gebruikers (met naam en e-mailadres) worden weergegeven wanneer een gebruiker wordt toegevoegd aan gedeelde albums. Wanneer uitgeschakeld, is de gebruikerslijst alleen beschikbaar voor beheerders.",
     "server_settings": "Serverinstellingen",
     "server_settings_description": "Beheer serverinstellingen",
     "server_welcome_message": "Welkomstbericht",
     "server_welcome_message_description": "Een bericht dat op de inlogpagina wordt weergegeven.",
-    "sidecar_job": "Sidecar metadata",
+    "sidecar_job": "Sidecar metagegevens",
     "sidecar_job_description": "Zoek of synchroniseer sidecar metadata van het bestandssysteem",
     "slideshow_duration_description": "Aantal seconden dat iedere afbeelding wordt getoond",
     "smart_search_job_description": "Voer machine learning uit op assets om te gebruiken voor slim zoeken",
@@ -238,6 +249,17 @@
     "storage_template_settings_description": "Beheer de mapstructuur en bestandsnaam van geüploade bestanden",
     "storage_template_user_label": "<code>{label}</code> is het opslaglabel van de gebruiker",
     "system_settings": "Systeeminstellingen",
+    "tag_cleanup_job": "Tag opschoning",
+    "template_email_available_tags": "Je kan de volgende tags gebruiken in een template: {tags}",
+    "template_email_if_empty": "Wanneer het sjabloon leeg is, wordt de standaard mail gebruikt.",
+    "template_email_invite_album": "Uitgenodigd in album sjabloon",
+    "template_email_preview": "Voorbeeld",
+    "template_email_settings": "Email",
+    "template_email_settings_description": "Beheer aangepaste email melding sjablonen",
+    "template_email_update_album": "Update in album sjabloon",
+    "template_email_welcome": "Welkom email sjabloon",
+    "template_settings": "Melding sjablonen",
+    "template_settings_description": "Beheer aangepast sjablonen voor meldingen.",
     "theme_custom_css_settings": "Aangepaste CSS",
     "theme_custom_css_settings_description": "Met Cascading Style Sheets kan het ontwerp van Immich worden aangepast.",
     "theme_settings": "Thema instellingen",
@@ -245,7 +267,6 @@
     "these_files_matched_by_checksum": "Deze bestanden worden gematcht op basis van hun checksums",
     "thumbnail_generation_job": "Thumbnail genereren",
     "thumbnail_generation_job_description": "Genereer grote, kleine en vervaagde thumbnails voor iedere asset, en genereer thumbnails voor iedere persoon",
-    "transcode_policy_description": "Beleid voor wanneer een video moet worden getranscodeerd. HDR-video's worden altijd getranscodeerd (behalve als transcodering is uitgeschakeld).",
     "transcoding_acceleration_api": "Acceleration API",
     "transcoding_acceleration_api_description": "De API die met je apparaat zal communiceren om transcodering te versnellen. Deze instelling is 'best effort': wanneer fouten optreden wordt teruggevallen op softwaretranscodering. VP9 kan wel of niet werken, afhankelijk van je hardware.",
     "transcoding_acceleration_nvenc": "NVENC (vereist NVIDIA GPU)",
@@ -271,7 +292,7 @@
     "transcoding_hardware_acceleration": "Hardware acceleratie",
     "transcoding_hardware_acceleration_description": "Experimenteel; veel sneller, maar zal een lagere kwaliteit hebben bij dezelfde bitrate",
     "transcoding_hardware_decoding": "Hardware decodering",
-    "transcoding_hardware_decoding_setting_description": "Geldt alleen voor NVENC, QSV en RKMPP. Maakt end-to-end versnelling mogelijk in plaats van alleen de codering te versnellen. Werkt mogelijk niet op alle video's.",
+    "transcoding_hardware_decoding_setting_description": "Maakt end-to-end versnelling mogelijk in plaats van alleen de codering te versnellen. Werkt mogelijk niet op alle video's.",
     "transcoding_hevc_codec": "HEVC codec",
     "transcoding_max_b_frames": "Maximum B-Frames",
     "transcoding_max_b_frames_description": "Hogere waarden verbeteren de compressie efficiëntie, maar vertragen de codering. Is mogelijk niet compatibel met hardwareversnelling op oudere apparaten. 0 schakelt B-frames uit, terwijl -1 deze waarde automatisch instelt.",
@@ -284,7 +305,7 @@
     "transcoding_preferred_hardware_device_description": "Geldt alleen voor VAAPI en QSV. Stelt de dri node in die wordt gebruikt voor hardwaretranscodering.",
     "transcoding_preset_preset": "Preset (-preset)",
     "transcoding_preset_preset_description": "Compressiesnelheid. Langzamere presets produceren kleinere bestanden en verhogen de kwaliteit bij het targeten van een bepaalde bitrate. VP9 negeert snelheden boven 'faster'.",
-    "transcoding_reference_frames": "Reference frames",
+    "transcoding_reference_frames": "Referentie frames",
     "transcoding_reference_frames_description": "Het aantal frames om naar te verwijzen bij het comprimeren van een bepaald frame. Hogere waarden verbeteren de compressie-efficiëntie, maar vertragen de codering. Bij 0 wordt deze waarde automatisch ingesteld.",
     "transcoding_required_description": "Alleen video's die geen geaccepteerd formaat hebben",
     "transcoding_settings": "Instellingen voor videotranscodering",
@@ -297,11 +318,9 @@
     "transcoding_threads_description": "Hogere waarden leiden tot snellere codering, maar laten minder ruimte over voor de server om andere taken te verwerken terwijl deze actief is. Deze waarde mag niet groter zijn dan het aantal CPU cores. Maximaliseert het gebruik als deze is ingesteld op 0.",
     "transcoding_tone_mapping": "Tone-mapping",
     "transcoding_tone_mapping_description": "Probeert het uiterlijk van HDR-video's te behouden wanneer ze worden geconverteerd naar SDR. Elk algoritme maakt verschillende afwegingen voor kleur, detail en helderheid. Hable behoudt detail, Mobius behoudt kleur en Reinhard behoudt helderheid.",
-    "transcoding_tone_mapping_npl": "Tone-mapping NPL",
-    "transcoding_tone_mapping_npl_description": "Kleuren zullen aangepast worden om normaal te lijken voor een display van deze helderheid. Contra-intuïtief, lagere waarden verhogen de helderheid van de video en vice versa sinds het compenseert voor de helderheid van de tentoonstelling. 0 zet deze waarde automatisch.",
     "transcoding_transcode_policy": "Transcodeerbeleid",
     "transcoding_transcode_policy_description": "Beleid voor wanneer een video getranscodeerd moet worden. HDR-video's worden altijd getranscodeerd (behalve als transcodering is uitgeschakeld).",
-    "transcoding_two_pass_encoding": "Two-pass encoding",
+    "transcoding_two_pass_encoding": "Two-pass encodering",
     "transcoding_two_pass_encoding_setting_description": "Transcodeer in twee passes om beter gecodeerde video's te produceren. Wanneer de maximale bitrate is ingeschakeld (vereist om te werken met H.264 en HEVC), gebruikt deze modus een bitraterange op basis van de maximale bitrate en negeert CRF. Voor VP9 kan CRF worden gebruikt als de maximale bitrate is uitgeschakeld.",
     "transcoding_video_codec": "Video codec",
     "transcoding_video_codec_description": "VP9 heeft een hoge efficiëntie en webcompatibiliteit, maar duurt langer om te transcoderen. HEVC presteert vergelijkbaar, maar heeft een lagere webcompatibiliteit. H.264 is breed compatibel en snel om te transcoderen, maar produceert veel grotere bestanden. AV1 is de meest efficiënte codec, maar mist ondersteuning op oudere apparaten.",
@@ -312,6 +331,7 @@
     "trash_settings_description": "Beheer prullenbak instellingen",
     "untracked_files": "Niet bijgehouden bestanden",
     "untracked_files_description": "Deze bestanden worden niet bijgehouden door de applicatie. Dit kan het resultaat zijn van een mislukte verplaatsing, onderbroken upload of een bug",
+    "user_cleanup_job": "Gebruiker opschoning",
     "user_delete_delay": "Het account en de assets van <b>{user}</b> worden over {delay, plural, one {# dag} other {# dagen}} permanent verwijderd.",
     "user_delete_delay_settings": "Verwijder vertraging",
     "user_delete_delay_settings_description": "Aantal dagen na verwijdering om het account en de assets van een gebruiker permanent te verwijderen. De taak voor het verwijderen van gebruikers wordt om middernacht uitgevoerd om te controleren of gebruikers verwijderd kunnen worden. Wijzigingen in deze instelling worden bij de volgende uitvoering meegenomen.",
@@ -378,7 +398,6 @@
   "archive_or_unarchive_photo": "Foto archiveren of uit het archief halen",
   "archive_size": "Archiefgrootte",
   "archive_size_description": "Configureer de archiefgrootte voor downloads (in GiB)",
-  "archived": "Gearchiveerd",
   "archived_count": "{count, plural, other {# gearchiveerd}}",
   "are_these_the_same_person": "Zijn dit dezelfde personen?",
   "are_you_sure_to_do_this": "Weet je zeker dat je dit wilt doen?",
@@ -389,8 +408,9 @@
   "asset_has_unassigned_faces": "Asset heeft niet-toegewezen gezichten",
   "asset_hashing": "Hashen...",
   "asset_offline": "Asset offline",
-  "asset_offline_description": "Deze asset is offline. Immich kan de bestandslocatie niet openen. Controleer of de asset beschikbaar is en scan de bibliotheek opnieuw.",
+  "asset_offline_description": "Deze externe asset is niet meer op de schijf te vinden. Neem contact op met de Immich beheerder voor hulp.",
   "asset_skipped": "Overgeslagen",
+  "asset_skipped_in_trash": "In prullenbak",
   "asset_uploaded": "Geüpload",
   "asset_uploading": "Uploaden...",
   "assets": "Assets",
@@ -398,11 +418,10 @@
   "assets_added_to_album_count": "{count, plural, one {# asset} other {# assets}} aan het album toegevoegd",
   "assets_added_to_name_count": "{count, plural, one {# asset} other {# assets}} toegevoegd aan {hasName, select, true {<b>{name}</b>} other {nieuw album}}",
   "assets_count": "{count, plural, one {# asset} other {# assets}}",
-  "assets_moved_to_trash": "{count, plural, one {# asset} other {# assets}} naar de prullenbak verplaatst",
   "assets_moved_to_trash_count": "{count, plural, one {# asset} other {# assets}} verplaatst naar prullenbak",
   "assets_permanently_deleted_count": "{count, plural, one {# asset} other {# assets}} permanent verwijderd",
   "assets_removed_count": "{count, plural, one {# asset} other {# assets}} verwijderd",
-  "assets_restore_confirmation": "Weet je zeker dat je alle verwijderde assets wilt herstellen? Je kunt deze actie niet ongedaan maken!",
+  "assets_restore_confirmation": "Weet je zeker dat je alle verwijderde assets wilt herstellen? Je kunt deze actie niet ongedaan maken! Offline assets kunnen op deze manier niet worden hersteld.",
   "assets_restored_count": "{count, plural, one {# asset} other {# assets}} hersteld",
   "assets_trashed_count": "{count, plural, one {# asset} other {# assets}} naar prullenbak verplaatst",
   "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets waren}} al onderdeel van het album",
@@ -413,6 +432,7 @@
   "birthdate_saved": "Geboortedatum succesvol opgeslagen",
   "birthdate_set_description": "De geboortedatum wordt gebruikt om de leeftijd van deze persoon op het moment van de foto te berekenen.",
   "blurred_background": "Vervaagde achtergrond",
+  "bugs_and_feature_requests": "Bugs & functieverzoeken",
   "build": "Build",
   "build_image": "Build image",
   "bulk_delete_duplicates_confirmation": "Weet je zeker dat je {count, plural, one {# duplicate asset} other {# duplicate assets}} in bulk wilt verwijderen? Dit zal de grootste asset van elke groep behouden en alle andere duplicaten permanent verwijderen. Je kunt deze actie niet ongedaan maken!",
@@ -427,10 +447,6 @@
   "cannot_merge_people": "Kan mensen niet samenvoegen",
   "cannot_undo_this_action": "Je kunt deze actie niet ongedaan maken!",
   "cannot_update_the_description": "Kan de beschrijving niet bijwerken",
-  "cant_apply_changes": "Kan wijzigingen niet toepassen",
-  "cant_get_faces": "Kan gezichten niet ophalen",
-  "cant_search_people": "Kan mensen niet zoeken",
-  "cant_search_places": "Kan plaatsen niet zoeken",
   "change_date": "Wijzig datum",
   "change_expiration_time": "Wijzig verlooptijd",
   "change_location": "Wijzig locatie",
@@ -462,6 +478,7 @@
   "confirm": "Bevestigen",
   "confirm_admin_password": "Bevestig beheerder wachtwoord",
   "confirm_delete_shared_link": "Weet je zeker dat je deze gedeelde link wilt verwijderen?",
+  "confirm_keep_this_delete_others": "Alle andere assets in de stack worden verwijderd, behalve deze. Weet je zeker dat je wilt doorgaan?",
   "confirm_password": "Bevestig wachtwoord",
   "contain": "Bevat",
   "context": "Context",
@@ -511,16 +528,19 @@
   "delete_key": "Verwijder sleutel",
   "delete_library": "Verwijder bibliotheek",
   "delete_link": "Verwijder link",
+  "delete_others": "Andere verwijderen",
   "delete_shared_link": "Verwijder gedeelde link",
   "delete_tag": "Tag verwijderen",
   "delete_tag_confirmation_prompt": "Weet je zeker dat je de tag {tagName} wilt verwijderen?",
   "delete_user": "Verwijder gebruiker",
   "deleted_shared_link": "Gedeelde link verwijderd",
+  "deletes_missing_assets": "Verwijdert assets die ontbreken op de schijf",
   "description": "Beschrijving",
   "details": "Details",
   "direction": "Richting",
   "disabled": "Uitgeschakeld",
   "disallow_edits": "Geen bewerkingen toestaan",
+  "discord": "Discord",
   "discover": "Zoeken",
   "dismiss_all_errors": "Negeer alle fouten",
   "dismiss_error": "Negeer fout",
@@ -529,6 +549,7 @@
   "display_original_photos": "Toon originele foto's",
   "display_original_photos_setting_description": "Geef de voorkeur aan het weergeven van de originele foto bij het bekijken van een asset in plaats van thumbnails wanneer de originele asset webcompatibel is. Dit kan resulteren in lagere weergavesnelheid van foto's.",
   "do_not_show_again": "Laat dit bericht niet meer zien",
+  "documentation": "Documentatie",
   "done": "Klaar",
   "download": "Downloaden",
   "download_include_embedded_motion_videos": "Ingesloten video's",
@@ -541,13 +562,6 @@
   "duplicates": "Duplicaten",
   "duplicates_description": "Kies voor iedere groep welke, indien aanwezig, duplicaten zijn",
   "duration": "Tijdsduur",
-  "durations": {
-    "days": "{days, plural, one {dag} other {{days, number} dagen}}",
-    "hours": "{hours, plural, one {uur} other {{hours, number} uren}}",
-    "minutes": "{minutes, plural, one {minuut} other {{minutes, number} minuten}}",
-    "months": "{months, plural, one {maand} other {{months, number} maanden}}",
-    "years": "{years, plural, one {jaar} other {{years, number} jaren}}"
-  },
   "edit": "Bewerken",
   "edit_album": "Album bewerken",
   "edit_avatar": "Avatar bewerken",
@@ -572,8 +586,6 @@
   "editor_crop_tool_h2_aspect_ratios": "Beeldverhoudingen",
   "editor_crop_tool_h2_rotation": "Rotatie",
   "email": "E-mailadres",
-  "empty": "",
-  "empty_album": "Leeg album",
   "empty_trash": "Prullenbak leegmaken",
   "empty_trash_confirmation": "Weet je zeker dat je de prullenbak wilt legen? Hiermee worden alle assets in de prullenbak permanent uit Immich verwijderd.\nJe kunt deze actie niet ongedaan maken!",
   "enable": "Inschakelen",
@@ -607,6 +619,7 @@
     "failed_to_create_shared_link": "Fout bij maken van gedeelde link",
     "failed_to_edit_shared_link": "Fout bij bewerken van gedeelde link",
     "failed_to_get_people": "Fout bij ophalen van mensen",
+    "failed_to_keep_this_delete_others": "Het is niet gelukt om dit asset te behouden en de andere assets te verwijderen",
     "failed_to_load_asset": "Kan asset niet laden",
     "failed_to_load_assets": "Kan assets niet laden",
     "failed_to_load_people": "Kan mensen niet laden",
@@ -634,8 +647,6 @@
     "unable_to_change_location": "Kan locatie niet wijzigen",
     "unable_to_change_password": "Kan wachtwoord niet veranderen",
     "unable_to_change_visibility": "Kan de zichtbaarheid van {count, plural, one {# persoon} other {# mensen}} niet wijzigen",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_complete_oauth_login": "Kan inloggen met OAuth niet voltooie",
     "unable_to_connect": "Kan niet verbinden",
     "unable_to_connect_to_server": "Kan geen verbinding maken met server",
@@ -657,9 +668,10 @@
     "unable_to_empty_trash": "Kan prullenbak niet legen",
     "unable_to_enter_fullscreen": "Kan volledig scherm niet openen",
     "unable_to_exit_fullscreen": "Kan volledig scherm niet afsluiten",
-    "unable_to_get_comments_number": "Kan het aantal opmerkingen niet ophalen",
+    "unable_to_get_comments_number": "Niet mogelijk om het aantal opmerkingen op te halen",
     "unable_to_get_shared_link": "Kan gedeelde link niet ophalen",
     "unable_to_hide_person": "Kan persoon niet verbergen",
+    "unable_to_link_motion_video": "Kan bewegende video niet verbinden",
     "unable_to_link_oauth_account": "Kan OAuth account niet koppelen",
     "unable_to_load_album": "Kan album niet laden",
     "unable_to_load_asset_activity": "Kan asset activiteit niet laden",
@@ -675,12 +687,10 @@
     "unable_to_remove_album_users": "Kan gebruiker niet van album verwijderen",
     "unable_to_remove_api_key": "Kan API sleutel niet verwijderen",
     "unable_to_remove_assets_from_shared_link": "Kan assets niet verwijderen uit gedeelde link",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_deleted_assets": "Kan offline bestanden niet verwijderen",
     "unable_to_remove_library": "Kan bibliotheek niet verwijderen",
-    "unable_to_remove_offline_files": "Kan offline bestanden niet verwijderen",
     "unable_to_remove_partner": "Kan partner niet verwijderen",
     "unable_to_remove_reaction": "Kan reactie niet verwijderen",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "Kan items niet repareren",
     "unable_to_reset_password": "Kan wachtwoord niet resetten",
     "unable_to_resolve_duplicate": "Kan duplicaat niet oplossen",
@@ -700,6 +710,7 @@
     "unable_to_submit_job": "Kan taak niet uitvoeren",
     "unable_to_trash_asset": "Kan asset niet naar prullenbak verplaatsen",
     "unable_to_unlink_account": "Kan account niet ontkoppelen",
+    "unable_to_unlink_motion_video": "Kan bewegende video niet los maken",
     "unable_to_update_album_cover": "Kan album cover niet bijwerken",
     "unable_to_update_album_info": "Kan albumgegevens niet bijwerken",
     "unable_to_update_library": "Kan bibliotheek niet bijwerken",
@@ -709,10 +720,6 @@
     "unable_to_update_user": "Kan gebruiker niet bijwerken",
     "unable_to_upload_file": "Kan bestand niet uploaden"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exif": "Exif",
   "exit_slideshow": "Diavoorstelling sluiten",
   "expand_all": "Alles uitvouwen",
@@ -727,33 +734,28 @@
   "external": "Extern",
   "external_libraries": "Externe bibliotheken",
   "face_unassigned": "Niet toegewezen",
-  "failed_to_get_people": "Kan mensen niet ophalen",
+  "failed_to_load_assets": "Kan assets niet laden",
   "favorite": "Favoriet",
   "favorite_or_unfavorite_photo": "Foto markeren als of verwijderen uit favorieten",
   "favorites": "Favorieten",
-  "feature": "",
   "feature_photo_updated": "Uitgelichte afbeelding bijgewerkt",
-  "featurecollection": "",
   "features": "Functies",
   "features_setting_description": "Beheer de app functies",
   "file_name": "Bestandsnaam",
   "file_name_or_extension": "Bestandsnaam of extensie",
   "filename": "Bestandsnaam",
-  "files": "",
   "filetype": "Bestandstype",
   "filter_people": "Filter op mensen",
   "find_them_fast": "Vind ze snel op naam door te zoeken",
   "fix_incorrect_match": "Onjuiste overeenkomst corrigeren",
   "folders": "Mappen",
   "folders_feature_description": "Bladeren door de mapweergave van de foto's en video's op het bestandssysteem",
-  "force_re-scan_library_files": "Forceer herscan van alle bibliotheekbestanden",
   "forward": "Vooruit",
   "general": "Algemeen",
   "get_help": "Krijg hulp",
   "getting_started": "Aan de slag",
   "go_back": "Ga terug",
   "go_to_search": "Ga naar zoeken",
-  "go_to_share_page": "Ga naar de deelpagina",
   "group_albums_by": "Groepeer albums op...",
   "group_no": "Niet groeperen",
   "group_owner": "Groeperen op eigenaar",
@@ -779,10 +781,6 @@
   "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} genomen in {city}, {country} met {person1} en {person2} op {date}",
   "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} genomen in {city}, {country} met {person1}, {person2}, en {person3} op {date}",
   "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} genomen in {city}, {country} met {person1}, {person2}, en {additionalCount, number} anderen op {date}",
-  "image_alt_text_people": "{count, plural, =1 {met {person1}} =2 {met {person1} en {person2}} =3 {met {person1}, {person2} en {person3}} other {met {person1}, {person2} en {others, number} anderen}}",
-  "image_alt_text_place": "in {city}, {country}",
-  "image_taken": "{isVideo, select, true {Video gemaakt} other {Afbeelding genomen}}",
-  "img": "",
   "immich_logo": "Immich logo",
   "immich_web_interface": "Immich Web Interface",
   "import_from_json": "Importeren vanuit JSON",
@@ -803,10 +801,11 @@
   "invite_people": "Mensen uitnodigen",
   "invite_to_album": "Uitnodigen voor album",
   "items_count": "{count, plural, one {# item} other {# items}}",
-  "job_settings_description": "",
   "jobs": "Taken",
   "keep": "Behouden",
   "keep_all": "Behoud alle",
+  "keep_this_delete_others": "Deze behouden, andere verwijderen",
+  "kept_this_deleted_others": "Deze asset behouden en {count, plural, one {# andere asset} other {# andere assets}} verwijderd",
   "keyboard_shortcuts": "Sneltoetsen",
   "language": "Taal",
   "language_setting_description": "Selecteer je voorkeurstaal",
@@ -818,33 +817,9 @@
   "level": "Niveau",
   "library": "Bibliotheek",
   "library_options": "Bibliotheek opties",
-  "license_account_info": "Je account heeft een licentie",
-  "license_activated_subtitle": "Bedankt voor het ondersteunen van Immich en open-source software",
-  "license_activated_title": "Je licentie is succesvol geactiveerd",
-  "license_button_activate": "Activeren",
-  "license_button_buy": "Kopen",
-  "license_button_buy_license": "Koop licentie",
-  "license_button_select": "Selecteren",
-  "license_failed_activation": "Activeren licentie mislukt. Controleer je e-mail voor de juiste licentiesleutel!",
-  "license_individual_description_1": "1 licentie per gebruiker op iedere server",
-  "license_individual_title": "Individuele licentie",
-  "license_info_licensed": "Gelicentieerd",
-  "license_info_unlicensed": "Ongelicentieerd",
-  "license_input_suggestion": "Heb je een licentie? Voer de sleutel hieronder in",
-  "license_license_subtitle": "Koop een licentie om Immich te ondersteunen",
-  "license_license_title": "LICENTIE",
-  "license_lifetime_description": "Levenslange licentie",
-  "license_per_server": "Per server",
-  "license_per_user": "Per gebruiker",
-  "license_server_description_1": "1 licentie per server",
-  "license_server_description_2": "Licentie voor alle gebruikers op de server",
-  "license_server_title": "Serverlicentie",
-  "license_trial_info_1": "Je gebruikt een niet-gelicentieerde versie van Immich",
-  "license_trial_info_2": "Je hebt Immich al gebruikt voor ongeveer",
-  "license_trial_info_3": "{accountAge, plural, one {# dag} other {# dagen}}",
-  "license_trial_info_4": "Overweeg een licentie te kopen om de verdere ontwikkeling van de service te ondersteunen",
   "light": "Licht",
   "like_deleted": "Like verwijderd",
+  "link_motion_video": "verbind bewegende video",
   "link_options": "Opties voor link",
   "link_to_oauth": "Koppel OAuth",
   "linked_oauth_account": "Gekoppeld OAuth account",
@@ -863,6 +838,7 @@
   "look": "Uiterlijk",
   "loop_videos": "Video's herhalen",
   "loop_videos_description": "Inschakelen om video's automatisch te herhalen in de detailweergave.",
+  "main_branch_warning": "U gebruikt een ontwikkelingsversie. Wij raden u ten zeerste aan een releaseversie te gebruiken!",
   "make": "Merk",
   "manage_shared_links": "Beheer gedeelde links",
   "manage_sharing_with_partners": "Beheer delen met partners",
@@ -932,6 +908,7 @@
   "notifications": "Meldingen",
   "notifications_setting_description": "Beheer meldingen",
   "oauth": "OAuth",
+  "official_immich_resources": "Officiële Immich bronnen",
   "offline": "Offline",
   "offline_paths": "Offline paden",
   "offline_paths_description": "Deze resultaten kunnen te wijten zijn aan het handmatig verwijderen van bestanden die geen deel uitmaken van een externe bibliotheek.",
@@ -939,13 +916,11 @@
   "oldest_first": "Oudste eerst",
   "onboarding": "Onboarding",
   "onboarding_privacy_description": "De volgende (optionele) functies zijn afhankelijk van externe services en kunnen op elk moment worden uitgeschakeld in de beheerdersinstellingen.",
-  "onboarding_storage_template_description": "Wanneer ingeschakeld, zal deze functie bestanden automatisch organiseren gebaseerd op een gebruiker-definieerd template. Gezien de stabiliteitsproblemen is de functie standaard uitgeschakeld. Voor meer informatie, bekijk de [documentatie].",
   "onboarding_theme_description": "Kies een kleurenthema voor de applicatie. Dit kun je later wijzigen in je instellingen.",
   "onboarding_welcome_description": "Laten we de applicatie instellen met enkele veelgebruikte instellingen.",
   "onboarding_welcome_user": "Welkom, {user}",
   "online": "Online",
   "only_favorites": "Alleen favorieten",
-  "only_refreshes_modified_files": "Vernieuwt alleen gewijzigde bestanden",
   "open_in_map_view": "Openen in kaartweergave",
   "open_in_openstreetmap": "Openen in OpenStreetMap",
   "open_the_search_filters": "Open de zoekfilters",
@@ -983,14 +958,12 @@
   "people_edits_count": "{count, plural, one {# persoon} other {# mensen}} bijgewerkt",
   "people_feature_description": "Bladeren door foto's en video's gegroepeerd op personen",
   "people_sidebar_description": "Toon een link naar Mensen in de zijbalk",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "Waarschuwing voor permanent verwijderen",
   "permanent_deletion_warning_setting_description": "Toon een waarschuwing bij het permanent verwijderen van assets",
   "permanently_delete": "Permanent verwijderen",
   "permanently_delete_assets_count": "{count, plural, one {Asset} other {Assets}} permanent verwijderen",
   "permanently_delete_assets_prompt": "Weet je zeker dat je deze {count, plural, one {asset} other {<b>#</b> assets}} permanent wilt verwijderen? Hiermee {count, plural, one {wordt} other {worden}} deze ook uit de bijbehorende album(s) verwijderd.",
   "permanently_deleted_asset": "Asset permanent verwijderd",
-  "permanently_deleted_assets": "{count, plural, one {# asset} other {# assets}} permanent verwijderd",
   "permanently_deleted_assets_count": "{count, plural, one {# asset} other {# assets}} permanent verwijderd",
   "person": "Persoon",
   "person_hidden": "{name}{hidden, select, true { (verborgen)} other {}}",
@@ -1006,7 +979,6 @@
   "play_memories": "Herinneringen afspelen",
   "play_motion_photo": "Bewegingsfoto afspelen",
   "play_or_pause_video": "Video afspelen of pauzeren",
-  "point": "",
   "port": "Poort",
   "preset": "Voorinstelling",
   "preview": "Voorbeeld",
@@ -1051,12 +1023,10 @@
   "purchase_server_description_2": "Supporter badge",
   "purchase_server_title": "Server",
   "purchase_settings_server_activated": "De productcode van de server wordt beheerd door de beheerder",
-  "range": "",
   "rating": "Ster waardering",
   "rating_clear": "Waardering verwijderen",
   "rating_count": "{count, plural, one {# ster} other {# sterren}}",
   "rating_description": "De EXIF-waardering weergeven in het infopaneel",
-  "raw": "",
   "reaction_options": "Reactie opties",
   "read_changelog": "Lees wijzigingen",
   "reassign": "Opnieuw toewijzen",
@@ -1064,14 +1034,17 @@
   "reassigned_assets_to_new_person": "{count, plural, one {# asset} other {# assets}} opnieuw toegewezen aan een nieuw persoon",
   "reassing_hint": "Geselecteerde assets toewijzen aan een bestaand persoon",
   "recent": "Recent",
+  "recent-albums": "Recente albums",
   "recent_searches": "Recente zoekopdrachten",
   "refresh": "Vernieuwen",
   "refresh_encoded_videos": "Vernieuw gecodeerde video's",
+  "refresh_faces": "Vernieuw gezichten",
   "refresh_metadata": "Vernieuw metadata",
   "refresh_thumbnails": "Vernieuw thumbnails",
   "refreshed": "Verniewd",
-  "refreshes_every_file": "Vernieuwt elk bestand",
+  "refreshes_every_file": "Vernieuwt alle bestaande en nieuwe bestanden",
   "refreshing_encoded_video": "Gecodeerde video aan het vernieuwen",
+  "refreshing_faces": "Gezichten aan het vernieuwen",
   "refreshing_metadata": "Metadata aan het vernieuwen",
   "regenerating_thumbnails": "Thumbnails opnieuw aan het genereren",
   "remove": "Verwijderen",
@@ -1079,10 +1052,11 @@
   "remove_assets_shared_link_confirmation": "Weet je zeker dat je {count, plural, one {# asset} other {# assets}} uit deze gedeelde link wilt verwijderen?",
   "remove_assets_title": "Assets verwijderen?",
   "remove_custom_date_range": "Aangepast datumbereik verwijderen",
+  "remove_deleted_assets": "Verwijder offline bestanden",
   "remove_from_album": "Verwijder uit album",
   "remove_from_favorites": "Verwijderen uit favorieten",
   "remove_from_shared_link": "Verwijderen uit gedeelde link",
-  "remove_offline_files": "Verwijder offline bestanden",
+  "remove_url": "Verwijder URL",
   "remove_user": "Gebruiker verwijderen",
   "removed_api_key": "API sleutel verwijderd: {name}",
   "removed_from_archive": "Verwijderd uit archief",
@@ -1099,7 +1073,6 @@
   "reset": "Resetten",
   "reset_password": "Wachtwoord resetten",
   "reset_people_visibility": "Zichtbaarheid mensen resetten",
-  "reset_settings_to_default": "",
   "reset_to_default": "Resetten naar standaard",
   "resolve_duplicates": "Duplicaten oplossen",
   "resolved_all_duplicates": "Alle duplicaten verwerkt",
@@ -1119,8 +1092,7 @@
   "saved_settings": "Instellingen opgeslagen",
   "say_something": "Zeg iets",
   "scan_all_libraries": "Scan alle bibliotheken",
-  "scan_all_library_files": "Herscan alle bibliotheekbestanden",
-  "scan_new_library_files": "Scan nieuwe bibliotheekbestanden",
+  "scan_library": "Scannen",
   "scan_settings": "Scaninstellingen",
   "scanning_for_album": "Scannen voor album...",
   "search": "Zoeken",
@@ -1135,8 +1107,10 @@
   "search_for_existing_person": "Zoek naar bestaande persoon",
   "search_no_people": "Geen mensen",
   "search_no_people_named": "Geen mensen genaamd \"{name}\"",
+  "search_options": "Zoekopties",
   "search_people": "Zoek mensen",
   "search_places": "Zoek plaatsen",
+  "search_settings": "Zoek instellingen",
   "search_state": "Zoek staat...",
   "search_tags": "Tags zoeken...",
   "search_timezone": "Zoek tijdzone...",
@@ -1161,7 +1135,6 @@
   "selected_count": "{count, plural, other {# geselecteerd}}",
   "send_message": "Bericht versturen",
   "send_welcome_email": "Stuur welkomstmail",
-  "server": "Server",
   "server_offline": "Server offline",
   "server_online": "Server online",
   "server_stats": "Serverstatistieken",
@@ -1204,6 +1177,7 @@
   "show_person_options": "Toon persoonopties",
   "show_progress_bar": "Toon voortgangsbalk",
   "show_search_options": "Zoekopties weergeven",
+  "show_slideshow_transition": "Diavoorstellingsovergang tonen",
   "show_supporter_badge": "Supporter badge",
   "show_supporter_badge_description": "Toon een supporterbadge",
   "shuffle": "Willekeurig",
@@ -1245,13 +1219,16 @@
   "submit": "Verzenden",
   "suggestions": "Suggesties",
   "sunrise_on_the_beach": "Zonsopkomst op het strand",
+  "support": "Ondersteuning",
+  "support_and_feedback": "Ondersteuning & feedback",
+  "support_third_party_description": "Je Immich installatie is door een derde partij samengesteld. Problemen die je ervaart, kunnen door dat pakket veroorzaakt zijn. Meld problemen in eerste instantie bij hen via de onderstaande links.",
   "swap_merge_direction": "Wissel richting voor samenvoegen om",
   "sync": "Sync",
   "tag": "Tag",
   "tag_assets": "Assets taggen",
   "tag_created": "Tag aangemaakt: {tag}",
   "tag_feature_description": "Bladeren door foto's en video's gegroepeerd op tags",
-  "tag_not_found_question": "Kun je een tag niet vinden? Maak er <link>hier</link> een aan",
+  "tag_not_found_question": "Kun je een tag niet vinden? <link>Maak een nieuwe tag.</link>",
   "tag_updated": "Tag bijgewerkt: {tag}",
   "tagged_assets": "{count, plural, one {# asset} other {# assets}} getagd",
   "tags": "Tags",
@@ -1260,18 +1237,19 @@
   "theme_selection": "Thema selectie",
   "theme_selection_description": "Stel het thema automatisch in op licht of donker op basis van de systeemvoorkeuren van je browser",
   "they_will_be_merged_together": "Zij zullen worden samengevoegd",
+  "third_party_resources": "Bronnen van derden",
   "time_based_memories": "Tijdgebaseerde herinneringen",
+  "timeline": "Tijdlijn",
   "timezone": "Tijdzone",
   "to_archive": "Archiveren",
   "to_change_password": "Wijzig wachtwoord",
   "to_favorite": "Toevoegen aan favorieten",
   "to_login": "Inloggen",
   "to_parent": "Ga naar hoofdmap",
-  "to_root": "Naar hoofdmap",
   "to_trash": "Prullenbak",
   "toggle_settings": "Zichtbaarheid instellingen wisselen",
   "toggle_theme": "Donker thema toepassen",
-  "toggle_visibility": "Zichtbaarheid wisselen",
+  "total": "Totaal",
   "total_usage": "Totaal gebruik",
   "trash": "Prullenbak",
   "trash_all": "Verplaats alle naar prullenbak",
@@ -1281,14 +1259,13 @@
   "trashed_items_will_be_permanently_deleted_after": "Items in de prullenbak worden na {days, plural, one {# dag} other {# dagen}} permanent verwijderd.",
   "type": "Type",
   "unarchive": "Herstellen uit archief",
-  "unarchived": "Hersteld uit archief",
   "unarchived_count": "{count, plural, other {# verwijderd uit archief}}",
   "unfavorite": "Verwijderen uit favorieten",
   "unhide_person": "Persoon zichtbaar maken",
   "unknown": "Onbekend",
-  "unknown_album": "Onbekend album",
   "unknown_year": "Onbekend jaar",
   "unlimited": "Onbeperkt",
+  "unlink_motion_video": "Maak bewegende video los",
   "unlink_oauth": "Ontkoppel OAuth",
   "unlinked_oauth_account": "OAuth account ontkoppeld",
   "unnamed_album": "Naamloos album",
@@ -1317,13 +1294,13 @@
   "use_custom_date_range": "Gebruik in plaats daarvan een aangepast datumbereik",
   "user": "Gebruiker",
   "user_id": "Gebruikers ID",
-  "user_license_settings": "Licentie",
-  "user_license_settings_description": "Beheer je licentie",
   "user_liked": "{user} heeft {type, select, photo {deze foto} video {deze video} asset {deze asset} other {dit}} geliket",
   "user_purchase_settings": "Kopen",
   "user_purchase_settings_description": "Beheer je aankoop",
   "user_role_set": "{user} instellen als {role}",
   "user_usage_detail": "Gedetailleerd gebruik van gebruikers",
+  "user_usage_stats": "Statistieken van accountgebruik",
+  "user_usage_stats_description": "Bekijk statistieken van accountgebruik",
   "username": "Gebruikersnaam",
   "users": "Gebruikers",
   "utilities": "Gereedschap",
@@ -1331,7 +1308,9 @@
   "variables": "Variabelen",
   "version": "Versie",
   "version_announcement_closing": "Je vriend, Alex",
-  "version_announcement_message": "Hallo vriend, er is een nieuwe versie van de applicatie beschikbaar. Neem de tijd om de <link>release notes</link> te bekijken en zorg ervoor dat je <code>docker-compose.yml</code> en <code>.env</code> up-to-date zijn om misconfiguraties te voorkomen, vooral als je WatchTower of een andere automatische update-mechanisme gebruikt.",
+  "version_announcement_message": "Hallo! Er is een nieuwe versie van Immich beschikbaar. Neem even de tijd om de <link>release notes</link> te lezen en zorg ervoor dat je setup up-to-date is om misconfiguraties te voorkomen, vooral als je WatchTower of een andere update-mechanisme gebruikt.",
+  "version_history": "Versiegeschiedenis",
+  "version_history_item": "{version} geïnstalleerd op {date}",
   "video": "Video",
   "video_hover_setting": "Speel video thumbnail af bij hoveren",
   "video_hover_setting_description": "Speel video thumbnail af wanneer de muis over het item beweegt. Zelfs wanneer uitgeschakeld, kan het afspelen worden gestart door de muis over het afspeelpictogram te bewegen.",
@@ -1343,10 +1322,10 @@
   "view_all_users": "Bekijk alle gebruikers",
   "view_in_timeline": "Bekijk in tijdlijn",
   "view_links": "Links bekijken",
+  "view_name": "Bekijken",
   "view_next_asset": "Bekijk volgende asset",
   "view_previous_asset": "Bekijk vorige asset",
   "view_stack": "Bekijk stapel",
-  "viewer": "Bekijker",
   "visibility_changed": "Zichtbaarheid gewijzigd voor {count, plural, one {# persoon} other {# mensen}}",
   "waiting": "Wachtend",
   "warning": "Waarschuwing",
diff --git a/i18n/nn.json b/i18n/nn.json
new file mode 100644
index 0000000000..bfbb2dc2ac
--- /dev/null
+++ b/i18n/nn.json
@@ -0,0 +1,33 @@
+{
+  "about": "Om",
+  "account": "Konto",
+  "account_settings": "Kontoinnstillingar",
+  "acknowledge": "Godkjenn",
+  "action": "Handling",
+  "actions": "Handlingar",
+  "active": "Aktiv",
+  "activity": "Aktivitet",
+  "activity_changed": "Aktivitet er {enabled, select, true {aktivert} other {deaktivert}}",
+  "add": "Legg til",
+  "add_a_description": "Legg til ei skildring",
+  "add_a_location": "Legg til ein stad",
+  "add_a_name": "Legg til eit namn",
+  "add_a_title": "Legg til ein tittel",
+  "add_exclusion_pattern": "Legg til ekskluderingsmønster",
+  "add_import_path": "Legg til sti for importering",
+  "add_location": "Legg til stad",
+  "add_more_users": "Legg til fleire brukarar",
+  "add_partner": "Legg til partnar",
+  "add_path": "Legg til sti",
+  "add_photos": "Legg til bilete",
+  "add_to": "Legg til...",
+  "add_to_album": "Legg til album",
+  "add_to_shared_album": "Legg til delt album",
+  "add_url": "Legg til URL",
+  "added_to_archive": "Lagt til arkiv",
+  "added_to_favorites": "Lagt til favorittar",
+  "added_to_favorites_count": "Lagt {count, number} til favorittar",
+  "admin": {
+    "confirm_delete_library": "Er du sikker på at du vil slette biblioteket {library}?"
+  }
+}
diff --git a/web/src/lib/i18n/pl.json b/i18n/pl.json
similarity index 91%
rename from web/src/lib/i18n/pl.json
rename to i18n/pl.json
index ff06e41233..5aa732327e 100644
--- a/web/src/lib/i18n/pl.json
+++ b/i18n/pl.json
@@ -1,8 +1,8 @@
 {
-  "about": "O aplikacji",
+  "about": "O",
   "account": "Konto",
-  "account_settings": "Ustawienia Konta",
-  "acknowledge": "Rozumiem",
+  "account_settings": "Ustawienia konta",
+  "acknowledge": "Zrozumiałem/łam",
   "action": "Akcja",
   "actions": "Akcje/i",
   "active": "Aktywne",
@@ -23,16 +23,23 @@
   "add_to": "Dodaj do...",
   "add_to_album": "Dodaj do albumu",
   "add_to_shared_album": "Dodaj do udostępnionego albumu",
+  "add_url": "Dodaj URL",
   "added_to_archive": "Dodano do archiwum",
   "added_to_favorites": "Dodano do ulubionych",
   "added_to_favorites_count": "Dodano {count, number} do ulubionych",
   "admin": {
     "add_exclusion_pattern_description": "Dodaj wzorce wykluczające. Wspierane są specjalne sekwencje (glob) *, ** oraz ?. Aby ignorować całą zawartość wszystkich folderów nazwanych \"Raw\", użyj \"**/Raw/**\". Aby ignorować wszystkie pliki kończące się na \".tif\", użyj \"**/*.tif\". Aby ignorować ścieżkę absolutną, użyj \"/ścieżka/do/ignorowania/**\".",
+    "asset_offline_description": "Ten zewnętrzny zasób biblioteki nie jest już dostępny na dysku i został przeniesiony do kosza. Jeśli plik został przeniesiony w obrębie biblioteki, sprawdź swoją oś czasu pod kątem nowego odpowiadającego zasobu. Aby przywrócić ten zasób, upewnij się, że ścieżka pliku poniżej jest dostępna dla Immich i przeskanuj bibliotekę.",
     "authentication_settings": "Ustawienia Uwierzytelnienia",
     "authentication_settings_description": "Zarządzaj hasłem, OAuth i innymi ustawienia uwierzytelnienia",
     "authentication_settings_disable_all": "Czy jesteś pewny, że chcesz wyłączyć wszystkie metody logowania? Logowanie będzie całkowicie wyłączone.",
     "authentication_settings_reenable": "Aby ponownie włączyć, użyj <link>Polecenia serwera</link>.",
     "background_task_job": "Zadania w Tle",
+    "backup_database": "Kopia zapasowa bazy danych",
+    "backup_database_enable_description": "Włącz kopię zapasową bazy danych",
+    "backup_keep_last_amount": "Ile poprzednich kopii zapasowych przechowywać",
+    "backup_settings": "Ustawienia kopii zapasowej",
+    "backup_settings_description": "Zarządzaj ustawieniami kopii zapasowej bazy dnaych",
     "check_all": "Zaznacz Wszystko",
     "cleared_jobs": "Usunięto zadania dla: {job}",
     "config_set_by_file": "Konfiguracja pochodzi z pliku konfiguracyjnego",
@@ -41,35 +48,40 @@
     "confirm_email_below": "Aby potwierdzić, wpisz \"{email}\" poniżej",
     "confirm_reprocess_all_faces": "Czy na pewno chcesz ponownie przetworzyć wszystkie twarze? Spowoduje to utratę nazwanych osób.",
     "confirm_user_password_reset": "Czy na pewno chcesz zresetować hasło użytkownika {user}?",
-    "crontab_guru": "Crontab Guru",
+    "create_job": "Utwórz zadanie",
+    "cron_expression": "Wyrażenie Cron",
+    "cron_expression_description": "Ustaw intwerwał skanowania przy pomocy formatu Cron'a. Po więcej informacji na temat formatu Cron zobacz . <link>Crontab Guru</link>",
+    "cron_expression_presets": "Predefiniowane wyrażenia Cron'a",
     "disable_login": "Wyłącz logowanie",
-    "disabled": "Wyłączone",
     "duplicate_detection_job_description": "Włącz uczenie maszynowe na zasobie aby wykrywać podobne obrazy. Ta funkcja opiera się na inteligentnym wyszukiwaniu",
     "exclusion_pattern_description": "Wzory wykluczające pozwalają na ignorowanie plików i folderów podczas skanowania Twojej biblioteki. Są one przydatne na przykład gdy nie chcesz importować zdjęć w formacie RAW.",
     "external_library_created_at": "Biblioteka zewnętrzna (stworzona dnia {date})",
     "external_library_management": "Zarządzanie Bibliotekami Zewnętrznymi",
     "face_detection": "Wykrywanie twarzy",
-    "face_detection_description": "Wykrywanie twarzy w zasobach używając uczenia maszynowego. Twarze w filmach wykryte zostaną tylko jeżeli są widoczne w miniaturze. \"Wszystkie\" ponownie przetwarza wszystkie zasoby. \"Brakujące\" dodaje do kolejki tylko zasoby, które nie zostały jeszcze przetworzone. Wykryte twarze zostaną dodane do kolejki Rozpoznawania Twarzy, aby związać je z istniejącą osobą albo stworzyć nową osobę.",
+    "face_detection_description": "Wykrywanie twarzy w zasobach używając uczenia maszynowego. Twarze w filmach wykryte zostaną tylko jeżeli są widoczne w miniaturze. \"Wszystkie\" ponownie przetwarza wszystkie zasoby. \"Reset\" dodatkowo usuwa wszystkie bieżące dane twarzy. \"Brakujące\" dodaje do kolejki tylko zasoby, które nie zostały jeszcze przetworzone. Wykryte twarze zostaną dodane do kolejki Rozpoznawania Twarzy, aby związać je z istniejącą osobą albo stworzyć nową osobę.",
     "facial_recognition_job_description": "Grupuj wykryte twarze. Ten krok uruchamiany jest po zakończeniu wykrywania twarzy. „Wszystkie” – ponownie kategoryzuje wszystkie twarze. „Brakujące” – kategoryzuje twarze, do których nie przypisano osoby.",
     "failed_job_command": "Polecenie {command} nie powiodło się dla zadania: {job}",
     "force_delete_user_warning": "UWAGA: Użytkownik i wszystkie zasoby użytkownika zostaną natychmiast trwale usunięte. Nie można tego cofnąć, a plików nie będzie można przywrócić.",
     "forcing_refresh_library_files": "Wymuś skanowanie wszystkich pliki w bibliotece",
+    "image_format": "Format",
     "image_format_description": "Użycie formatu WebP skutkuje utworzeniem plików o rozmiarze mniejszym niż w przypadku JPEG ale jego kodowanie trwa dłużej.",
     "image_prefer_embedded_preview": "Preferuj podgląd wbudowany",
     "image_prefer_embedded_preview_setting_description": "Jeśli to możliwe, używaj osadzonych podglądów w zdjęciach RAW jako danych wejściowych do przetwarzania obrazu. Może to zapewnić dokładniejsze kolory w przypadku niektórych obrazów, ale jakość podglądu zależy od aparatu, a obraz może zawierać więcej artefaktów kompresji.",
-    "image_prefer_wide_gamut": "Preferuj szeroką gamę kolorów",
+    "image_prefer_wide_gamut": "Preferuj szeroką paletę barw",
     "image_prefer_wide_gamut_setting_description": "Do wyświetlania miniatur użyj wyświetlacza P3. Dzięki temu lepiej zachowuje się intensywność obrazów o dużej ilości kolorów, ale obrazy mogą wyglądać inaczej na starych urządzeniach ze starą wersją przeglądarki. Obrazy sRGB są zachowywane jako sRGB, aby uniknąć przesunięć kolorów.",
-    "image_preview_format": "Format podglądu",
-    "image_preview_resolution": "Rozdzielczość podglądu",
-    "image_preview_resolution_description": "Używane podczas przeglądania pojedynczego zdjęcia i do uczenia maszynowego. Wyższe rozdzielczości pozwalają zachować więcej szczegółów, ale kodowanie zajmuje więcej czasu, powoduje to też większe rozmiary plików i może zmniejszyć czas reakcji aplikacji.",
+    "image_preview_description": "Obraz średniej wielkości z wyciętymi metadanymi, używany podczas przeglądania pojedynczego zasobu i do uczenia maszynowego",
+    "image_preview_quality_description": "Jakość podglądu od 1 do 100. Wyższa jest lepsza, ale powoduje większe pliki i może zmniejszyć responsywność aplikacji. Ustawienie niskiej wartości może wpłynąć na jakość uczenia maszynowego.",
+    "image_preview_title": "Ustawienia podglądu",
     "image_quality": "Jakość",
-    "image_quality_description": "Jakość obrazu od 1 do 100. Wyższe wartości pozwalają uzyskać lepszą jakość ale skutkują większym rozmiarem pliku. Ta opcja wpływa na Podgląd i Miniaturki.",
+    "image_resolution": "Rozdzielczość",
+    "image_resolution_description": "Wyższe rozdzielczości pozwalają zachować więcej szczegółów, ale wymagają dłuższego kodowania, mają większy rozmiar pliku i mogą spowalniać reakcję aplikacji.",
     "image_settings": "Ustawienia Obrazu",
     "image_settings_description": "Zarządzaj jakością i rozdzielczością generowanych obrazów",
-    "image_thumbnail_format": "Format miniatury",
-    "image_thumbnail_resolution": "Rozdzielczość miniatury",
-    "image_thumbnail_resolution_description": "Używane podczas przeglądania grup zdjęć (głównej osi czasu, widoku albumu itp.). Wyższe rozdzielczości pozwalają zachować więcej szczegółów, ale wyświetlenie ich zajmuje więcej czasu, powoduje też zwiększenie rozmiaru plików i może zmniejszyć czas reakcji aplikacji.",
+    "image_thumbnail_description": "Mała miniatura z wyciętymi metadanymi, używana podczas przeglądania grup zdjęć, takich jak główna oś czasu",
+    "image_thumbnail_quality_description": "Jakość miniatur od 1 do 100. Im wyższa, tym lepsza, ale powoduje to większy rozmiar plików i może spowolnić reakcję aplikacji.",
+    "image_thumbnail_title": "Ustawienia miniatur",
     "job_concurrency": "{job} współbieżność",
+    "job_created": "Zadanie utworzone",
     "job_not_concurrency_safe": "To zadanie nie może zostać wykonane w wielu wątkach.",
     "job_settings": "Ustawienia Zadań",
     "job_settings_description": "Zarządzaj współbieżnością zadań",
@@ -77,9 +89,6 @@
     "jobs_delayed": "{jobCount, plural, other {# oczekujących}}",
     "jobs_failed": "{jobCount, plural, other {# nieudane}}",
     "library_created": "Utworzono bibliotekę: {library}",
-    "library_cron_expression": "Wyrażenie Cron",
-    "library_cron_expression_description": "Ustaw interwał skanowania, używając formatu cron. Więcej informacji znajdziesz m.in. <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Proponowane wyrażenia Cron",
     "library_deleted": "Biblioteka usunięta",
     "library_import_path_description": "Określ folder do załadowania plików. Ten folder, łącznie z podfolderami, zostanie przeskanowany w poszukiwaniu obrazów i filmów.",
     "library_scanning": "Okresowe Skanowanie",
@@ -122,7 +131,7 @@
     "machine_learning_smart_search_description": "Szukaj obrazów semantycznie za pomocą CLIP",
     "machine_learning_smart_search_enabled": "Włącz inteligentne wyszukiwanie",
     "machine_learning_smart_search_enabled_description": "Jeżeli wyłączone, obrazy nie będą przygotowywane do inteligentnego wyszukiwania.",
-    "machine_learning_url_description": "URL serwera uczenia maszynowego",
+    "machine_learning_url_description": "URL serwera uczenia maszynowego. Jeżeli podano więcej niż jeden URL, do każdego serwera będzie wysłane żądanie do tej pory dopóki chociaż jeden nie odpowie, w kolejności od pierwszego do ostatniego.",
     "manage_concurrency": "Zarządzaj współbieżnością zadań",
     "manage_log_settings": "Zarządzaj ustawieniami logów",
     "map_dark_style": "Styl ciemny",
@@ -152,7 +161,7 @@
     "note_cannot_be_changed_later": "UWAŻAJ: Nie można tego później zmienić!",
     "note_unlimited_quota": "Wpisz by wyłączyć limit",
     "notification_email_from_address": "Z adresu",
-    "notification_email_from_address_description": "Adres e-mail nadawcy, na przykład: „Immich Photo Server <noreply@immich.app>”",
+    "notification_email_from_address_description": "Adres e-mail nadawcy, na przykład: „Immich Photo Server <noreply@example.com>”",
     "notification_email_host_description": "Host serwera e-mail (np. smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Ignoruj niepoprawny certyfikat",
     "notification_email_ignore_certificate_errors_description": "Ignoruj błąd walidacji certyfikatu TLS (nie zalecane)",
@@ -198,22 +207,24 @@
     "password_settings": "Logowanie Hasłem",
     "password_settings_description": "Zarządzaj ustawieniami logowania hasłem",
     "paths_validated_successfully": "Wszystkie ścieżki zostały pomyślnie zweryfikowane",
+    "person_cleanup_job": "Porządkowanie osób",
     "quota_size_gib": "Wielkość Magazynu (GiB)",
     "refreshing_all_libraries": "Wszystkie biblioteki zostaną odświeżone",
     "registration": "Rejestracja Administratora",
     "registration_description": "Jesteś pierwszym użytkownikiem aplikacji, więc twoje konto jest administratorem. Możesz zarządzać platformą, w tym dodawać nowych użytkowników.",
-    "removing_offline_files": "Niedostępne pliki zostaną usunięte",
     "repair_all": "Napraw Wszystko",
     "repair_matched_items": "Powiązano {count, plural, one {# element} few {# elementy} other {# elementów}}",
     "repaired_items": "Naprawiono {count, plural, one {# element} few {# elementy} other {# elementów}}",
     "require_password_change_on_login": "Wymagaj zmiany hasła po pierwszym zalogowaniu",
     "reset_settings_to_default": "Przywróć ustawienia fabryczne",
     "reset_settings_to_recent_saved": "Przywróć ustawienia do ostatnio zapisanych",
-    "scanning_library_for_changed_files": "Przeszukaj bibliotekę w poszukiwaniu zmian w plikach",
-    "scanning_library_for_new_files": "Przeszukaj bibliotekę w poszukiwaniu nowych plików",
+    "scanning_library": "Skanowanie biblioteki",
+    "search_jobs": "Zadania przeszukiwania...",
     "send_welcome_email": "Wyślij powitalny e-mail",
     "server_external_domain_settings": "Domena zewnętrzna",
     "server_external_domain_settings_description": "Domena dla publicznie udostępnionych linków, wraz z http(s)://",
+    "server_public_users": "Użytkownicy publiczni",
+    "server_public_users_description": "Wszyscy użytkownicy (nazwa i adres e-mail) są wymienieni podczas dodawania użytkownika do udostępnionych albumów. Po wyłączeniu lista użytkowników będzie dostępna tylko dla administratorów.",
     "server_settings": "Ustawienia Serwera",
     "server_settings_description": "Zarządzaj ustawieniami serwera",
     "server_welcome_message": "Wiadomość powitalna",
@@ -238,6 +249,17 @@
     "storage_template_settings_description": "Zarządzaj strukturą folderów i nazwą pliku przesyłanego zasobu",
     "storage_template_user_label": "<code>{label}</code> to jest etykieta przechowywania użytkownika",
     "system_settings": "Ustawienia Systemowe",
+    "tag_cleanup_job": "Porządkowanie etykiet",
+    "template_email_available_tags": "Możesz uzyć tych zmiennych w swoim szablonie: {tags}",
+    "template_email_if_empty": "Zostaw puste, aby użyć domyślny adres e-mail.",
+    "template_email_invite_album": "Szablon zaproszenia do albumu",
+    "template_email_preview": "Podgląd",
+    "template_email_settings": "Szablony e-mail",
+    "template_email_settings_description": "Zarządzaj niestandardowymi e-mail powiadomieniami",
+    "template_email_update_album": "Szablon aktualizacji albumu",
+    "template_email_welcome": "Szablon powitalnego e-mail",
+    "template_settings": "Szablony Powiadomień",
+    "template_settings_description": "Zarządzaj niestandardowymi szablonami powiadomień e-mail.",
     "theme_custom_css_settings": "Własny CSS",
     "theme_custom_css_settings_description": "Właśny CSS pozwala na zmianę wyglądu aplikacji Immich.",
     "theme_settings": "Ustawienia Motywu",
@@ -245,7 +267,6 @@
     "these_files_matched_by_checksum": "Pliki te są powiązane na podstawie ich sum kontrolnych",
     "thumbnail_generation_job": "Stwórz Miniaturki",
     "thumbnail_generation_job_description": "Generuj duże, małe i rozmyte miniatury dla każdego zasobu, a także miniatury dla każdej osoby",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "API akceleracji",
     "transcoding_acceleration_api_description": "Interfejs API, używany w celu przyspieszenia transkodowania. W przypadku niepowodzenia zostanie użyte transkodowanie programowe. Format VP9 może, ale nie musi, działać w zależności od sprzętu.",
     "transcoding_acceleration_nvenc": "NVENC (wymaga NVIDIA GPU)",
@@ -271,7 +292,7 @@
     "transcoding_hardware_acceleration": "Przyspieszenie Sprzętowe",
     "transcoding_hardware_acceleration_description": "Eksperymentalny; znacznie szybszy, ale będzie miał niższą jakość przy tej samej szybkości transmisji",
     "transcoding_hardware_decoding": "Dekodowanie sprzętowe",
-    "transcoding_hardware_decoding_setting_description": "Dotyczy tylko NVENC, QSV i RKMPP. Umożliwia całkowite przyspieszenie sprzętowe zamiast tylko przyspieszania kodowania. Może nie działać we wszystkich filmach.",
+    "transcoding_hardware_decoding_setting_description": "Umożliwia całkowite przyspieszenie sprzętowe zamiast tylko przyspieszania kodowania. Może nie działać we wszystkich filmach.",
     "transcoding_hevc_codec": "Kodek HEVC",
     "transcoding_max_b_frames": "Maksymalne klatki B (B-Frames)",
     "transcoding_max_b_frames_description": "Wyższe wartości poprawiają wydajność kompresji, ale spowalniają kodowanie. Może nie być kompatybilny z akceleracją sprzętową na starszych urządzeniach. 0 wyłącza klatki B (B-frames), natomiast -1 ustawia tę wartość automatycznie.",
@@ -297,8 +318,6 @@
     "transcoding_threads_description": "Wyższe wartości prowadzą do szybszego kodowania, ale pozostawiają mniej zasobów serwerowi na przetwarzanie innych zadań, gdy jest ono aktywne. Wartość ta nie powinna być większa niż liczba rdzeni procesora. Maksymalizuje wykorzystanie, jeśli jest ustawione na 0.",
     "transcoding_tone_mapping": "Mapowanie tonów",
     "transcoding_tone_mapping_description": "Próbuje zachować wygląd filmów HDR po konwersji do SDR. Każdy algorytm dokonuje różnych kompromisów w zakresie koloru, szczegółowości i jasności. Hable zachowuje szczegóły, Mobius kolor, a Reinhard jasność.",
-    "transcoding_tone_mapping_npl": "Mapowanie tonów NPL",
-    "transcoding_tone_mapping_npl_description": "Kolory zostaną dostosowane tak, aby wyglądały normalnie w przypadku wyświetlacza o tej jasności. Wbrew intuicji niższe wartości zwiększają jasność wideo i odwrotnie, ponieważ kompensują jasność wyświetlacza. 0 ustawia tę wartość automatycznie.",
     "transcoding_transcode_policy": "Zasady transkodowania",
     "transcoding_transcode_policy_description": "Zasady dotyczące transkodowania filmu. Filmy HDR będą zawsze transkodowane (z wyjątkiem sytuacji, gdy transkodowanie jest wyłączone).",
     "transcoding_two_pass_encoding": "Kodowanie dwuprzebiegowe",
@@ -312,6 +331,7 @@
     "trash_settings_description": "Zarządzaj ustawieniami kosza",
     "untracked_files": "Nieśledzone pliki",
     "untracked_files_description": "Pliki te nie są śledzone przez aplikację. Mogą być wynikiem nieudanych przeniesień, przerwanego przesyłania lub pozostawienia z powodu błędu",
+    "user_cleanup_job": "Porządkowanie użytkownika",
     "user_delete_delay": "Konto <b>{user}</b> oraz jego zasoby zostaną zaplanowane do trwałego usunięcia za {delay, plural, one {# dzień} few {# dni} many {# dni} other {# dni}}.",
     "user_delete_delay_settings": "Usuń opóźnienie",
     "user_delete_delay_settings_description": "Liczba dni po usunięciu, po której następuje trwałe usunięcie konta użytkownika i zasobów. Zadanie usuwania użytkowników jest uruchamiane o północy w celu sprawdzenia, czy użytkownicy są gotowi do usunięcia. Zmiany tego ustawienia zostaną sprawdzone przy następnym wykonaniu.",
@@ -378,7 +398,6 @@
   "archive_or_unarchive_photo": "Dodaj lub usuń zasób z archiwum",
   "archive_size": "Rozmiar archiwum",
   "archive_size_description": "Podziel pobierane pliki na więcej niż jedno archiwum, jeżeli rozmiar archiwum przekroczy tę wartość w GiB",
-  "archived": "Zarchiwizowano",
   "archived_count": "{count, plural, other {Zarchiwizowano #}}",
   "are_these_the_same_person": "Czy to jedna i ta sama osoba?",
   "are_you_sure_to_do_this": "Czy aby na pewno chcesz to zrobić?",
@@ -389,8 +408,9 @@
   "asset_has_unassigned_faces": "Zasób ma nieprzypisane twarze",
   "asset_hashing": "Hashowanie...",
   "asset_offline": "Zasób niedostępny",
-  "asset_offline_description": "Ten zasób jest offline. Immich nie może uzyskać dostępu do jego lokalizacji pliku. Upewnij się, że zasób jest dostępny, a następnie ponownie zeskanuj bibliotekę.",
+  "asset_offline_description": "Ten zewnętrzny zasób nie jest już dostępny na dysku. Aby uzyskać pomoc, skontaktuj się z administratorem Immich.",
   "asset_skipped": "Pominięto",
+  "asset_skipped_in_trash": "W koszu",
   "asset_uploaded": "Przesłano",
   "asset_uploading": "Przesyłanie...",
   "assets": "Zasoby",
@@ -398,11 +418,10 @@
   "assets_added_to_album_count": "Dodano {count, plural, one {# zasób} few {# zasoby} many {# zasobów} other {# zasobów}} do albumu",
   "assets_added_to_name_count": "Dodano {count, plural, one {# zasób} few {# zasoby} many {# zasobów} other {# zasobów}} do {hasName, select, true {<b>{name}</b>} other {new album}}",
   "assets_count": "{count, plural, one {# zasób} few {# zasoby} many {# zasobów} other {# zasobów}}",
-  "assets_moved_to_trash": "{count, plural, one {# zasób został przeniesiony} few {# zasoby zostały przeniesione} other {# zasobów zostało przeniesione}} do kosza",
   "assets_moved_to_trash_count": "Przeniesiono {count, plural, one {# zasób} few {# zasoby} many {# zasobów} other {# zasobów}} do kosza",
   "assets_permanently_deleted_count": "Trwale usunięto {count, plural, one {# zasób} few {# zasoby} many {# zasobów} other {# zasobów}}",
   "assets_removed_count": "Usunięto {count, plural, one {# zasób} few {# zasoby} many {# zasobów} other {# zasobów}}",
-  "assets_restore_confirmation": "Na pewno chcesz przywrócić wszystkie zasoby z kosza? Nie da się tego cofnąć!",
+  "assets_restore_confirmation": "Na pewno chcesz przywrócić wszystkie zasoby z kosza? Nie da się tego cofnąć! Należy pamiętać, że w ten sposób nie można przywrócić zasobów offline.",
   "assets_restored_count": "Przywrócono {count, plural, one {# zasób} few {# zasoby} many {# zasobów} other {# zasobów}}",
   "assets_trashed_count": "Wrzucono do kosza {count, plural, one {# zasób} few {# zasoby} many {# zasobów} other {# zasobów}}",
   "assets_were_part_of_album_count": "{count, plural, one {Zasób był} few {Zasoby były} many {Zasobów było} other {Zasobów było}} już częścią albumu",
@@ -413,6 +432,7 @@
   "birthdate_saved": "Data urodzenia zapisana pomyślnie",
   "birthdate_set_description": "Data urodzenia jest używana do obliczenia wieku danej osoby podczas wykonania zdjęcia.",
   "blurred_background": "Rozmyte tło",
+  "bugs_and_feature_requests": "Błędy i prośby o funkcje",
   "build": "Kompilacja",
   "build_image": "Obraz Buildu",
   "bulk_delete_duplicates_confirmation": "Czy na pewno chcesz trwale usunąć {count, plural, one {# zduplikowany zasób} few {# zduplikowane zasoby} many {# zduplikowanych zasobów} other {# zduplikowanych zasobów}}? Zostanie zachowany największy zasób z każdej grupy, a wszystkie pozostałe duplikaty zostaną trwale usunięte. Nie można cofnąć tej operacji!",
@@ -427,10 +447,6 @@
   "cannot_merge_people": "Złączenie osób nie powiodło się",
   "cannot_undo_this_action": "Nie da się tego cofnąć!",
   "cannot_update_the_description": "Nie można zaktualizować opisu",
-  "cant_apply_changes": "Nie można zapisać zmian",
-  "cant_get_faces": "Nie można pobrać twarzy",
-  "cant_search_people": "Nie można wyszukiwać osób",
-  "cant_search_places": "Nie można wyszukiwać miejsc",
   "change_date": "Zmień datę",
   "change_expiration_time": "Zmień czas ważności",
   "change_location": "Zmień lokalizację",
@@ -462,6 +478,7 @@
   "confirm": "Potwierdź",
   "confirm_admin_password": "Potwierdź Hasło Administratora",
   "confirm_delete_shared_link": "Czy na pewno chcesz usunąć ten udostępniony link?",
+  "confirm_keep_this_delete_others": "Wszystkie inne zasoby zostaną usunięte poza tym zasobem. Czy jesteś pewien, że chcesz kontynuować?",
   "confirm_password": "Potwierdź hasło",
   "contain": "Zawiera",
   "context": "Kontekst",
@@ -511,16 +528,19 @@
   "delete_key": "Usuń klucz",
   "delete_library": "Usuń bibliotekę",
   "delete_link": "Usuń link",
+  "delete_others": "Usuń inne",
   "delete_shared_link": "Usuń udostępniony link",
   "delete_tag": "Usuń etykietę",
   "delete_tag_confirmation_prompt": "Czy na pewno chcesz usunąć etykietę {tagName}?",
   "delete_user": "Usuń użytkownika",
   "deleted_shared_link": "Pomyślnie usunięto udostępniony link",
+  "deletes_missing_assets": "Usuwa brakujące zasoby z dysku",
   "description": "Opis",
   "details": "Szczegóły",
   "direction": "Kierunek",
   "disabled": "Wyłączone",
   "disallow_edits": "Nie pozwalaj edytować",
+  "discord": "Discord",
   "discover": "Odkryj",
   "dismiss_all_errors": "Odrzuć wszystkie błędy",
   "dismiss_error": "Odrzuć błąd",
@@ -529,6 +549,7 @@
   "display_original_photos": "Wyświetlaj oryginalne zdjęcia",
   "display_original_photos_setting_description": "Wyświetlając zdjęcia i filmy, preferuj oryginalny plik zamiast miniatur jeżeli jest działa on w przeglądarce. Może to skutkować wolniejszym ładowaniem zdjęć i filmów.",
   "do_not_show_again": "Nie pokazuj więcej tej wiadomości",
+  "documentation": "Dokumentacja",
   "done": "Gotowe",
   "download": "Pobierz",
   "download_include_embedded_motion_videos": "Osadzone filmy",
@@ -541,13 +562,6 @@
   "duplicates": "Duplikaty",
   "duplicates_description": "Rozstrzygnij każdą grupę, określając, które zasoby, jeśli takie istnieją, są duplikatami",
   "duration": "Czas trwania",
-  "durations": {
-    "days": "{days, plural, one {dzień} other {{days, number} dni}}",
-    "hours": "{hours, plural, one {godzina} few {{hours, number} godziny} other {{hours, number} godzin}}",
-    "minutes": "{minutes, plural, one {minuta} few {{minutes, number} minuty} other {{minutes, number} minut}}",
-    "months": "{months, plural, one {miesiąc} few {{months, number} miesiące} other {{months, number} miesięcy}}",
-    "years": "{years, plural, one {rok} few {{years, number} lata} other {{years, number} lat}}"
-  },
   "edit": "Edytuj",
   "edit_album": "Edytuj album",
   "edit_avatar": "Edytuj awatar",
@@ -572,8 +586,6 @@
   "editor_crop_tool_h2_aspect_ratios": "Proporcje obrazu",
   "editor_crop_tool_h2_rotation": "Obrót",
   "email": "E-mail",
-  "empty": "",
-  "empty_album": "Pusty Album",
   "empty_trash": "Opróżnij kosz",
   "empty_trash_confirmation": "Czy na pewno chcesz opróżnić kosz? Spowoduje to trwałe usunięcie wszystkich zasobów znajdujących się w koszu z Immich.\nNie można cofnąć tej operacji!",
   "enable": "Włącz",
@@ -607,6 +619,7 @@
     "failed_to_create_shared_link": "Nie udało się utworzyć udostępnionego linku",
     "failed_to_edit_shared_link": "Nie udało się edytować udostępnionego linku",
     "failed_to_get_people": "Nie udało się pozyskać osób",
+    "failed_to_keep_this_delete_others": "Nie udało się zachować tego zasobu i usunąć innych zasobów",
     "failed_to_load_asset": "Nie udało się załadować zasobu",
     "failed_to_load_assets": "Nie udało się załadować zasobów",
     "failed_to_load_people": "Błąd pobierania ludzi",
@@ -634,8 +647,6 @@
     "unable_to_change_location": "Nie można zmienić lokalizacji",
     "unable_to_change_password": "Nie można zmienić hasła",
     "unable_to_change_visibility": "Nie można zmienić widoczności dla {count, plural, one {# osoby} other {# osób}}",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_complete_oauth_login": "Nie można ukończyć logowania przy użyciu OAuth",
     "unable_to_connect": "Nie można się połączyć",
     "unable_to_connect_to_server": "Nie można się połączyć z serwerem",
@@ -660,6 +671,7 @@
     "unable_to_get_comments_number": "Nie udało się uzyskać liczby komentarzy",
     "unable_to_get_shared_link": "Nie udało się uzyskać udostępnionego linku",
     "unable_to_hide_person": "Ukrycie osoby nie powiodło się",
+    "unable_to_link_motion_video": "Nie można podłączyć ruchome wideo",
     "unable_to_link_oauth_account": "Nie można powiązać konta OAuth",
     "unable_to_load_album": "Ładowanie albumu nie powiodło się",
     "unable_to_load_asset_activity": "Ładowanie aktywności nie powiodło się",
@@ -675,12 +687,10 @@
     "unable_to_remove_album_users": "Usunięcie użytkowników z albumu nie powiodło się",
     "unable_to_remove_api_key": "Usunięcie Klucza API nie powiodło się",
     "unable_to_remove_assets_from_shared_link": "Nie można usunąć zasobów z udostępnionego linku",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_deleted_assets": "Usunięcie niedostępnych plików nie powiodło się",
     "unable_to_remove_library": "Usunięcie biblioteki nie powiodło się",
-    "unable_to_remove_offline_files": "Usunięcie niedostępnych plików nie powiodło się",
     "unable_to_remove_partner": "Nie można usunąć partnerów",
     "unable_to_remove_reaction": "Usunięcie reakcji nie powiodło się",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "Naprawianie elementów nie powiodło się",
     "unable_to_reset_password": "Nie można resetować hasła",
     "unable_to_resolve_duplicate": "Usuwanie duplikatów nie powiodło się",
@@ -700,6 +710,7 @@
     "unable_to_submit_job": "Nie można przesłać zadania",
     "unable_to_trash_asset": "Przeniesienie zasobu do kosza nie powiodło się",
     "unable_to_unlink_account": "Odłączenie konta nie powiodło się",
+    "unable_to_unlink_motion_video": "Nie można odłączyć ruchomego wideo",
     "unable_to_update_album_cover": "Nie można zaktualizować okładki albumu",
     "unable_to_update_album_info": "Nie można zaktualizować informacji o albumie",
     "unable_to_update_library": "Nie można zaktualizować biblioteki",
@@ -709,10 +720,6 @@
     "unable_to_update_user": "Zmiana użytkownika nie powiodła się",
     "unable_to_upload_file": "Nie można przesłać pliku"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exif": "Metadane EXIF",
   "exit_slideshow": "Zamknij Pokaz Slajdów",
   "expand_all": "Rozwiń wszystko",
@@ -727,33 +734,28 @@
   "external": "Zewnętrzny",
   "external_libraries": "Biblioteki Zewnętrzne",
   "face_unassigned": "Nieprzypisany",
-  "failed_to_get_people": "Pobieranie osób nie powiodło się",
+  "failed_to_load_assets": "Nie udało się załadować zasobów",
   "favorite": "Ulubione",
   "favorite_or_unfavorite_photo": "Dodaj lub usuń z ulubionych",
   "favorites": "Ulubione",
-  "feature": "",
   "feature_photo_updated": "Pomyślnie zmieniono główne zdjęcie",
-  "featurecollection": "",
   "features": "Funkcje",
   "features_setting_description": "Zarządzaj funkcjami aplikacji",
   "file_name": "Nazwa pliku",
   "file_name_or_extension": "Nazwie lub rozszerzeniu pliku",
   "filename": "Nazwa pliku",
-  "files": "",
   "filetype": "Typ pliku",
   "filter_people": "Szukaj osoby",
   "find_them_fast": "Wyszukuj szybciej przypisując nazwę",
   "fix_incorrect_match": "Napraw nieprawidłowe dopasowanie",
   "folders": "Foldery",
   "folders_feature_description": "Przeglądanie zdjęć i filmów w widoku folderów",
-  "force_re-scan_library_files": "Wymuś ponowne przeskanowanie wszystkich plików biblioteki",
   "forward": "Do przodu",
   "general": "Ogólne",
   "get_help": "Pomoc",
   "getting_started": "Pierwsze kroki",
   "go_back": "Wstecz",
   "go_to_search": "Przejdź do wyszukiwania",
-  "go_to_share_page": "Przejdź na udostępnioną stronę",
   "group_albums_by": "Grupuj albumy...",
   "group_no": "Brak grupowania",
   "group_owner": "Grupuj według właściciela",
@@ -779,10 +781,6 @@
   "image_alt_text_date_place_2_people": "{isVideo, select, true {Wideo} other {Zdjęcie}} zrobione w {city}, {country} z {person1} i {person2} dnia {date}",
   "image_alt_text_date_place_3_people": "{isVideo, select, true {Wideo} other {Zdjęcie}} zrobione w {city}, {country} z {person1}, {person2} i {person3} dnia {date}",
   "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Wideo} other {Zdjęcie}} zrobione w {city}, {country} z {person1}, {person2} i {additionalCount, number} innymi dnia {date}",
-  "image_alt_text_people": "{count, plural, =1 {z {person1}} =2 {z {person1} i {person2}} =3 {z {person1}, {person2} i {person3}} other {z {person1}, {person2} i {others, number} innymi}}",
-  "image_alt_text_place": "w {city}, {country}",
-  "image_taken": "{isVideo, select, true {nagrany film} other {zrobione zdjęcie}}",
-  "img": "",
   "immich_logo": "Logo Immich",
   "immich_web_interface": "Interfejs internetowy Immich",
   "import_from_json": "Wczytaj z JSON",
@@ -803,10 +801,11 @@
   "invite_people": "Zaproś Osoby",
   "invite_to_album": "Zaproś do albumu",
   "items_count": "{count, plural, one {# element} other {# elementy}}",
-  "job_settings_description": "",
   "jobs": "Zadania",
   "keep": "Zachowaj",
   "keep_all": "Zachowaj wszystko",
+  "keep_this_delete_others": "Zachowaj to, usuń inne",
+  "kept_this_deleted_others": "Zachowano ten zasób i usunięto {count, plural, one {#zasób} other {#zasoby}}",
   "keyboard_shortcuts": "Skróty klawiaturowe",
   "language": "Język",
   "language_setting_description": "Wybierz swój preferowany język",
@@ -820,6 +819,7 @@
   "library_options": "Opcje biblioteki",
   "light": "Jasny",
   "like_deleted": "Polubienie usunięte",
+  "link_motion_video": "Podłącz ruchome wideo",
   "link_options": "Opcje linku",
   "link_to_oauth": "Połącz z OAuth",
   "linked_oauth_account": "Połączone konto OAuth",
@@ -838,6 +838,7 @@
   "look": "Wygląd",
   "loop_videos": "Powtarzaj filmy",
   "loop_videos_description": "Włącz automatyczne zapętlanie wideo w przeglądarce szczegółów.",
+  "main_branch_warning": "Używasz wersji deweloperskiej. Rekomendujemy instalację stabilnej wersji aplikacji!",
   "make": "Marka",
   "manage_shared_links": "Zarządzaj udostępnionymi linkami",
   "manage_sharing_with_partners": "Zarządzaj dzieleniem z partnerami",
@@ -907,6 +908,7 @@
   "notifications": "Powiadomienia",
   "notifications_setting_description": "Zarządzanie powiadomieniami",
   "oauth": "OAuth",
+  "official_immich_resources": "Oficjalne zasoby Immicha",
   "offline": "Offline",
   "offline_paths": "Ścieżki offline",
   "offline_paths_description": "Te wyniki mogą być spowodowane ręcznym usunięciem plików, które nie są częścią zewnętrznej biblioteki.",
@@ -919,7 +921,6 @@
   "onboarding_welcome_user": "Witaj, {user}",
   "online": "Połączony",
   "only_favorites": "Tylko ulubione",
-  "only_refreshes_modified_files": "Odświeża tylko zmodyfikowane pliki",
   "open_in_map_view": "Otwórz w widoku mapy",
   "open_in_openstreetmap": "Otwórz w OpenStreetMap",
   "open_the_search_filters": "Otwórz filtry wyszukiwania",
@@ -957,7 +958,6 @@
   "people_edits_count": "Edytowano {count, plural, one {# osoba} few {# osoby} many {# osób} other {# osób}}",
   "people_feature_description": "Przeglądanie zdjęć i filmów pogrupowanych według osób",
   "people_sidebar_description": "Pokazuj link do Osób w panelu bocznym",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "Ostrzeżenie o trwałym usunięciu",
   "permanent_deletion_warning_setting_description": "Pokaż ostrzeżenie przy trwałym usuwaniu zasobów",
   "permanently_delete": "Usuń trwale",
@@ -979,7 +979,6 @@
   "play_memories": "Odtwórz wspomnienia",
   "play_motion_photo": "Odtwórz Ruchome Zdjęcie",
   "play_or_pause_video": "Odtwórz lub wstrzymaj wideo",
-  "point": "",
   "port": "Port",
   "preset": "Ustawienie",
   "preview": "Podgląd",
@@ -1024,12 +1023,10 @@
   "purchase_server_description_2": "Status wspierającego",
   "purchase_server_title": "Serwer",
   "purchase_settings_server_activated": "Klucz produktu serwera jest zarządzany przez administratora",
-  "range": "",
   "rating": "Ocena gwiazdkowa",
   "rating_clear": "Wyczyść oceną",
   "rating_count": "{count, plural, one {# gwiazdka} other {# gwiazdek}}",
   "rating_description": "Wyświetl ocenę z EXIF w panelu informacji",
-  "raw": "",
   "reaction_options": "Opcje reakcji",
   "read_changelog": "Zobacz Zmiany",
   "reassign": "Przypisz ponownie",
@@ -1037,14 +1034,17 @@
   "reassigned_assets_to_new_person": "Przypisano ponownie {count, plural, one {# zasób} other {# zasobów}} do nowej osoby",
   "reassing_hint": "Przypisz wybrane zasoby do istniejącej osoby",
   "recent": "Ostatnie",
+  "recent-albums": "Ostatnie albumy",
   "recent_searches": "Ostatnie wyszukiwania",
   "refresh": "Odśwież",
   "refresh_encoded_videos": "Odśwież enkodowane wideo",
+  "refresh_faces": "Odśwież twarze",
   "refresh_metadata": "Odśwież metadane",
   "refresh_thumbnails": "Odśwież miniatury",
   "refreshed": "Odświeżone",
-  "refreshes_every_file": "Odświeża każdy plik",
+  "refreshes_every_file": "Ponownie odczytuje wszystkie istniejące i nowe pliki",
   "refreshing_encoded_video": "Odświeżanie enkodowanych wideo",
+  "refreshing_faces": "Odświeżanie twarzy",
   "refreshing_metadata": "Odświeżanie metadanych",
   "regenerating_thumbnails": "Regenerowanie miniatur",
   "remove": "Usuń",
@@ -1052,10 +1052,11 @@
   "remove_assets_shared_link_confirmation": "Czy na pewno chcesz usunąć {count, plural, one {# zasób} other {# zasoby}} z tego udostępnionego linku?",
   "remove_assets_title": "Usunąć zasoby?",
   "remove_custom_date_range": "Usuń niestandardowy zakres dat",
+  "remove_deleted_assets": "Usuń Niedostępne Pliki",
   "remove_from_album": "Usuń z albumu",
   "remove_from_favorites": "Usuń z ulubionych",
   "remove_from_shared_link": "Usuń z udostępnionego linku",
-  "remove_offline_files": "Usuń Niedostępne Pliki",
+  "remove_url": "Usuń URL",
   "remove_user": "Usuń użytkownika",
   "removed_api_key": "Usunięto Klucz API: {name}",
   "removed_from_archive": "Usunięto z archiwum",
@@ -1072,7 +1073,6 @@
   "reset": "Reset",
   "reset_password": "Resetuj hasło",
   "reset_people_visibility": "Zresetuj widoczność osób",
-  "reset_settings_to_default": "",
   "reset_to_default": "Przywróć ustawienia domyślne",
   "resolve_duplicates": "Rozwiąż problemy z duplikatami",
   "resolved_all_duplicates": "Rozwiązano wszystkie duplikaty",
@@ -1092,8 +1092,7 @@
   "saved_settings": "Zapisane ustawienia",
   "say_something": "Powiedz coś",
   "scan_all_libraries": "Skanuj wszystkie biblioteki",
-  "scan_all_library_files": "Przeskanuj ponownie wszystkie biblioteki",
-  "scan_new_library_files": "Skanuj nowe pliki biblioteki",
+  "scan_library": "Skanuj",
   "scan_settings": "Ustawienia Skanowania",
   "scanning_for_album": "Skanuję album...",
   "search": "Szukaj",
@@ -1108,8 +1107,10 @@
   "search_for_existing_person": "Wyszukaj istniejącą osobę",
   "search_no_people": "Brak osób",
   "search_no_people_named": "Brak osób nazwanych \"{name}\"",
+  "search_options": "Opcje wyszukiwania",
   "search_people": "Wyszukaj osoby",
   "search_places": "Wyszukaj miejsca",
+  "search_settings": "Ustawienia przeszukiwania",
   "search_state": "Wyszukaj stan...",
   "search_tags": "Wyszukaj etykiety...",
   "search_timezone": "Wyszukaj strefę czasową...",
@@ -1134,7 +1135,6 @@
   "selected_count": "{count, plural, other {# wybrane}}",
   "send_message": "Wyślij wiadomość",
   "send_welcome_email": "Wyślij e-mail powitalny",
-  "server": "Serwer",
   "server_offline": "Serwer Offline",
   "server_online": "Serwer Online",
   "server_stats": "Statystyki serwera",
@@ -1177,6 +1177,7 @@
   "show_person_options": "Pokaż opcje osoby",
   "show_progress_bar": "Pokaż pasek postępu",
   "show_search_options": "Wyświetl opcje wyszukiwania",
+  "show_slideshow_transition": "Pokaż przejście pokazu slajdów",
   "show_supporter_badge": "Odznaka wspierającego",
   "show_supporter_badge_description": "Pokaż odznakę wspierającego",
   "shuffle": "Losuj",
@@ -1218,13 +1219,16 @@
   "submit": "Zatwierdź",
   "suggestions": "Sugestie",
   "sunrise_on_the_beach": "Wschód słońca na plaży",
+  "support": "Wsparcie",
+  "support_and_feedback": "Wsparcie i opinie",
+  "support_third_party_description": "Twoja instalacja immich została spakowana przez trzecią stronę. Problemy, które napotykasz, mogą być spowodowane przez ten pakiet, więc w pierwszej kolejności zgłaszaj problemy u nich, korzystając z poniższych linków.",
   "swap_merge_direction": "Zmień kierunek złączenia",
   "sync": "Synchronizuj",
   "tag": "Etykieta",
   "tag_assets": "Ustaw etykiety zasobów",
   "tag_created": "Stworzono etykietę: {tag}",
   "tag_feature_description": "Przeglądanie zdjęć i filmów pogrupowanych według logicznych etykiet wskazujących temat",
-  "tag_not_found_question": "Nie możesz znaleźć etykiety? Utwórz ją <link>tutaj</link>",
+  "tag_not_found_question": "Nie możesz znaleźć etykiety? <link>Utwórz ją tutaj</link>",
   "tag_updated": "Uaktualniono etykietę: {tag}",
   "tagged_assets": "Przypisano etykietę {count, plural, one {# zasobowi} other {# zasobom}}",
   "tags": "Etykiety",
@@ -1233,16 +1237,19 @@
   "theme_selection": "Wybór motywu",
   "theme_selection_description": "Automatycznie zmień motyw na jasny lub ciemny zależnie od ustawień przeglądarki",
   "they_will_be_merged_together": "Zostaną one ze sobą połączone",
+  "third_party_resources": "Zasoby stron trzecich",
   "time_based_memories": "Wspomnienia oparte na czasie",
+  "timeline": "Oś czasu",
   "timezone": "Strefa czasowa",
   "to_archive": "Archiwum",
   "to_change_password": "Zmień hasło",
   "to_favorite": "Dodaj do ulubionych",
   "to_login": "Login",
+  "to_parent": "Idź do rodzica",
   "to_trash": "Kosz",
   "toggle_settings": "Przełącz ustawienia",
   "toggle_theme": "Przełącz ciemny motyw",
-  "toggle_visibility": "Zmień widoczność",
+  "total": "Całkowity",
   "total_usage": "Całkowite wykorzystanie",
   "trash": "Kosz",
   "trash_all": "Usuń wszystko",
@@ -1252,14 +1259,13 @@
   "trashed_items_will_be_permanently_deleted_after": "Wyrzucone zasoby zostaną trwale usunięte po {days, plural, one {jednym dniu} other {{days, number} dniach}}.",
   "type": "Typ",
   "unarchive": "Cofnij archiwizację",
-  "unarchived": "",
   "unarchived_count": "{count, plural, other {Niezarchiwizowane #}}",
   "unfavorite": "Usuń z ulubionych",
   "unhide_person": "Przywróć osobę",
   "unknown": "Nieznany",
-  "unknown_album": "Nieznany album",
   "unknown_year": "Rok nieznany",
   "unlimited": "Nieograniczony",
+  "unlink_motion_video": "Rozłącz ruchome wideo",
   "unlink_oauth": "Odłącz OAuth",
   "unlinked_oauth_account": "Odłączone konto OAuth",
   "unnamed_album": "Nienazwany album",
@@ -1293,6 +1299,8 @@
   "user_purchase_settings_description": "Zarządzaj swoim zakupem",
   "user_role_set": "Ustaw {user} jako {role}",
   "user_usage_detail": "Szczegóły używania przez użytkownika",
+  "user_usage_stats": "Statystyki użytkowania konta",
+  "user_usage_stats_description": "Wyświetl statystyki użytkowania konta",
   "username": "Nazwa użytkownika",
   "users": "Użytkownicy",
   "utilities": "Narzędzia",
@@ -1300,7 +1308,9 @@
   "variables": "Zmienne",
   "version": "Wersja",
   "version_announcement_closing": "Twój przyjaciel Aleks",
-  "version_announcement_message": "Witaj przyjacielu, dostępna jest nowa wersja aplikacji. Poświęć trochę czasu na zapoznanie się z <link>informacjami o wydaniu</link> i upewnij się, że pliki <code>docker-compose.yml</code> i <code>.env</code> konfiguracja jest aktualna, aby zapobiec błędnym konfiguracjom, zwłaszcza jeśli używasz WatchTower lub dowolnego mechanizmu, który obsługuje automatyczne aktualizowanie aplikacji.",
+  "version_announcement_message": "Witaj! Dostępna jest nowa wersja Immich. Poświęć trochę czasu na zapoznanie się z <link>informacjami o wydaniu</link>, aby upewnić się, że twoja konfiguracja jest aktualna, aby uniknąć błędów, szczególnie jeśli używasz WatchTower lub jakiegokolwiek mechanizmu odpowiedzialnego za automatyczne aktualizowanie Immich.",
+  "version_history": "Historia wersji",
+  "version_history_item": "Zainstalowano {version} w {date}",
   "video": "Wideo",
   "video_hover_setting": "Odtwórz miniaturę wideo po najechaniu kursorem",
   "video_hover_setting_description": "Odtwórz miniaturę wideo po najechaniu myszką na element. Nawet jeśli jest wyłączone, odtwarzanie można rozpocząć, najeżdżając kursorem na ikonę odtwarzania.",
@@ -1312,10 +1322,10 @@
   "view_all_users": "Pokaż wszystkich użytkowników",
   "view_in_timeline": "Pokaż na osi czasu",
   "view_links": "Pokaż łącza",
+  "view_name": "Widok",
   "view_next_asset": "Wyświetl następny zasób",
   "view_previous_asset": "Wyświetl poprzedni zasób",
   "view_stack": "Zobacz Ułożenie",
-  "viewer": "Oglądający",
   "visibility_changed": "Zmieniono widoczność dla {count, plural, one {# osoba} other {# osoby}}",
   "waiting": "Oczekiwanie",
   "warning": "Ostrzeżenie",
diff --git a/i18n/pt.json b/i18n/pt.json
new file mode 100644
index 0000000000..d34e0424bc
--- /dev/null
+++ b/i18n/pt.json
@@ -0,0 +1,1340 @@
+{
+  "about": "Sobre",
+  "account": "Conta",
+  "account_settings": "Definições de Conta",
+  "acknowledge": "Aceitar",
+  "action": "Ação",
+  "actions": "Ações",
+  "active": "Em execução",
+  "activity": "Atividade",
+  "activity_changed": "A atividade está {enabled, select, true {ativada} other {desativada}}",
+  "add": "Adicionar",
+  "add_a_description": "Adicionar uma descrição",
+  "add_a_location": "Adicionar localização",
+  "add_a_name": "Adicionar um nome",
+  "add_a_title": "Adicionar um título",
+  "add_exclusion_pattern": "Adicionar um padrão de exclusão",
+  "add_import_path": "Adicionar um caminho de importação",
+  "add_location": "Adicionar localização",
+  "add_more_users": "Adicionar mais utilizadores",
+  "add_partner": "Adicionar parceiro",
+  "add_path": "Adicionar caminho",
+  "add_photos": "Adicionar fotos",
+  "add_to": "Adicionar a...",
+  "add_to_album": "Adicionar ao álbum",
+  "add_to_shared_album": "Adicionar ao álbum partilhado",
+  "add_url": "Adicionar URL",
+  "added_to_archive": "Adicionado ao arquivo",
+  "added_to_favorites": "Adicionado aos favoritos",
+  "added_to_favorites_count": "{count, plural, one {{count, number} adicionado aos favoritos} other {{count, number} adicionados aos favoritos}}",
+  "admin": {
+    "add_exclusion_pattern_description": "Adicione padrões de exclusão. Utilizar *, ** ou ? são suportados. Para ignorar todos os ficheiros em qualquer diretório chamado \"Raw\", use \"**/Raw/**'. Para ignorar todos os ficheiros que finalizam em \".tif\", use \"**/*.tif\". Para ignorar um caminho absoluto, use \"/caminho/para/ignorar/**\".",
+    "asset_offline_description": "Este ficheiro proveniente de uma biblioteca externa deixou de estar disponível no disco e foi movido para a reciclagem. Se o ficheiro foi movido no interior da biblioteca, procure na linha de tempo pelo novo ficheiro correspondente. Para restaurar este ficheiro, certifique-se que o caminho do ficheiro abaixo pode ser acedido pelo Immich e analise a biblioteca.",
+    "authentication_settings": "Definições de Autenticação",
+    "authentication_settings_description": "Gerir palavras-passe, OAuth, e outras definições de autenticação",
+    "authentication_settings_disable_all": "Tem a certeza que deseja desativar todos os métodos de início de sessão? O início de sessão será completamente desativado.",
+    "authentication_settings_reenable": "Para reativar, use um <link>Comando de servidor</link>.",
+    "background_task_job": "Tarefas em segundo plano",
+    "backup_database": "Cópia de Segurança da Base de Dados",
+    "backup_database_enable_description": "Ativar cópias de segurança da base de dados",
+    "backup_keep_last_amount": "Quantidade de cópias de segurança anteriores a manter",
+    "backup_settings": "Definições de Cópia de Segurança",
+    "backup_settings_description": "Gerir definições de cópia de segurança da base de dados",
+    "check_all": "Selecionar Tudo",
+    "cleared_jobs": "Eliminadas as tarefas de: {job}",
+    "config_set_by_file": "A configuração está atualmente definida por um ficheiro de configuração",
+    "confirm_delete_library": "Tem a certeza de que deseja eliminar a biblioteca {library} ?",
+    "confirm_delete_library_assets": "Tem a certeza de que deseja eliminar esta biblioteca? Isto eliminará {count, plural, one {# ficheiro incluído} other {todos os # ficheiros incluídos}} do Immich e esta ação não pode ser anulada. Os ficheiros permanecerão no disco.",
+    "confirm_email_below": "Para confirmar, escreva \"{email}\" abaixo",
+    "confirm_reprocess_all_faces": "Tem a certeza de que deseja reprocessar todos os rostos? Isto também limpará os nomes das pessoas.",
+    "confirm_user_password_reset": "Tem a certeza de que deseja redefinir a palavra-passe de {user}?",
+    "create_job": "Criar tarefa",
+    "cron_expression": "Expressão Cron",
+    "cron_expression_description": "Definir o intervalo de análise utilizando o formato Cron. Para mais informações, por favor veja o <link>Crontab Guru</link>",
+    "cron_expression_presets": "Predefinições das expressões Cron",
+    "disable_login": "Desativar inicio de sessão",
+    "duplicate_detection_job_description": "Executa a aprendizagem de máquina em ficheiros para detetar imagens semelhantes. Depende da Pesquisa Inteligente",
+    "exclusion_pattern_description": "Os padrões de exclusão permitem ignorar ficheiros e pastas ao analisar a sua biblioteca. Isto é útil se tiver pastas que contenham ficheiros que não deseja importar, como ficheiros RAW.",
+    "external_library_created_at": "Biblioteca externa (criada em {date})",
+    "external_library_management": "Gestão de bibliotecas externas",
+    "face_detection": "Deteção de Rostos",
+    "face_detection_description": "Deteta rostos em ficheiros utilizando aprendizagem automática. Para vídeos, apenas a miniatura é considerada. \"Atualizar\" (re)processa todos os ficheiros, enquanto \"Redefinir\" elimina todos os dados de rostos. \"Em falta\" coloca em fila ficheiros que ainda não foram processados. Os rostos detetados serão colocados em fila para Reconhecimento Facial após a conclusão da Deteção de Rostos, agrupando-os em pessoas novas ou já existentes.",
+    "facial_recognition_job_description": "Agrupa rostos detetadas em pessoas. Esta etapa é executada após a conclusão da Deteção de Rostos. \"Redefinir\" (re)agrupa todos os rostos. \"Em falta\" coloca em fila rostos que ainda não têm uma pessoa atribuída.",
+    "failed_job_command": "Comando {command} falhou para a tarefa: {job}",
+    "force_delete_user_warning": "AVISO: Isto removerá imediatamente o utilizador e todos os ficheiros. Isso não pode ser revertido e os ficheiros não poderão ser recuperados.",
+    "forcing_refresh_library_files": "A forçar a atualização de todos os ficheiros da biblioteca",
+    "image_format": "Formato",
+    "image_format_description": "WebP produz ficheiros mais pequenos do que JPEG, mas é mais lento para codificar.",
+    "image_prefer_embedded_preview": "Preferir visualização incorporada",
+    "image_prefer_embedded_preview_setting_description": "Utilizar visualizações incorporadas em fotos RAW como entrada para processamento de imagem, quando disponível. Isto pode produzir cores mais precisas para algumas imagens, mas a qualidade da visualização depende da câmara e a imagem pode ter mais artefatos de compressão.",
+    "image_prefer_wide_gamut": "Prefira ampla gama",
+    "image_prefer_wide_gamut_setting_description": "Utilizar Display P3 para miniaturas. Isso preserva melhor a vibrância das imagens com espaços de cores amplos, mas as imagens podem aparecer de maneira diferente em dispositivos antigos com uma versão antiga do navegador. As imagens sRGB são mantidas como sRGB para evitar mudanças de cores.",
+    "image_preview_description": "Imagem de tamanho médio sem metadados, utilizada ao visualizar um único ficheiro e pela aprendizagem de máquina",
+    "image_preview_quality_description": "Qualidade de pré-visualização de 1 a 100. Maior é melhor, mas produz ficheiros maiores e pode reduzir a capacidade de resposta da aplicação. Definir um valor demasiado baixo pode afetar a qualidade da aprendizagem de máquina.",
+    "image_preview_title": "Definições de Pré-visualização",
+    "image_quality": "Qualidade",
+    "image_resolution": "Resolução",
+    "image_resolution_description": "Resoluções mais altas podem ajudar a preservar mais detalhes mas demoram mais a codificar, têm tamanhos de ficheiro maiores e podem reduzir a capacidade de resposta da aplicação.",
+    "image_settings": "Definições de imagem",
+    "image_settings_description": "Gerir a qualidade e resolução das imagens geradas",
+    "image_thumbnail_description": "Miniatura de tamanho pequena e sem metadados, utilizada ao visualizar grupos de fotos como, por exemplo, na linha de tempo principal",
+    "image_thumbnail_quality_description": "Qualidade das miniaturas de 1 a 100. Maior é melhor, mas produz tamanhos de ficheiro maiores e podem reduzir a capacidade de resposta da aplicação.",
+    "image_thumbnail_title": "Definições de Miniaturas",
+    "job_concurrency": "{job} em simultâneo",
+    "job_created": "Tarefa criada",
+    "job_not_concurrency_safe": "Esta tarefa não pode ser executada em simultâneo.",
+    "job_settings": "Definições de Tarefas",
+    "job_settings_description": "Gerir tarefas em simultâneo",
+    "job_status": "Estado das Tarefas",
+    "jobs_delayed": "{jobCount, plural, one {# adiado} other {# adiados}}",
+    "jobs_failed": "{jobCount, plural, one {# falhou} other {# falharam}}",
+    "library_created": "Criada biblioteca: {library}",
+    "library_deleted": "Biblioteca eliminada",
+    "library_import_path_description": "Especifique uma pasta para importar. Esta pasta, incluindo sub-pastas, será analisada por imagens e vídeos.",
+    "library_scanning": "Análise periódica",
+    "library_scanning_description": "Configurar a análise periódica da biblioteca",
+    "library_scanning_enable_description": "Ativar análise periódica da biblioteca",
+    "library_settings": "Biblioteca Externa",
+    "library_settings_description": "Gerir definições de biblioteca externa",
+    "library_tasks_description": "Executa tarefas de biblioteca",
+    "library_watching_enable_description": "Analisar bibliotecas externas por alterações de ficheiros",
+    "library_watching_settings": "Análise de biblioteca (EXPERIMENTAL)",
+    "library_watching_settings_description": "Analise automaticamente por ficheiros alterados",
+    "logging_enable_description": "Ativar registo",
+    "logging_level_description": "Quando ativado, qual o nível de log a usar.",
+    "logging_settings": "Registo",
+    "machine_learning_clip_model": "Modelo CLIP",
+    "machine_learning_clip_model_description": "O nome do modelo CLIP definido <link>aqui</link>. Tome nota de que é necessário voltar a executar a tarefa de \"Pesquisa Inteligente\" para todas as imagens depois de alterar o modelo.",
+    "machine_learning_duplicate_detection": "Deteção de Itens Duplicados",
+    "machine_learning_duplicate_detection_enabled": "Ativar deteção de itens duplicados",
+    "machine_learning_duplicate_detection_enabled_description": "Se desativado, ficheiros exatamente idênticos serão desduplicados na mesma.",
+    "machine_learning_duplicate_detection_setting_description": "Utilizar embeddings CLIP para encontrar itens possivelmente duplicados",
+    "machine_learning_enabled": "Ativar a aprendizagem de máquina",
+    "machine_learning_enabled_description": "Se desativado, todos as funcionalidades de ML serão desativados, independentemente das definições abaixo.",
+    "machine_learning_facial_recognition": "Reconhecimento Facial",
+    "machine_learning_facial_recognition_description": "Detetar, reconhecer e agrupar rostos em imagens",
+    "machine_learning_facial_recognition_model": "Modelo de reconhecimento facial",
+    "machine_learning_facial_recognition_model_description": "Os modelos estão ordenados por ordem decrescente de tamanho. Modelos maiores são mais lentos e utilizam mais memória, mas produzem melhores resultados. Tome conta de que ao alterar um modelo, deve executar novamente a tarefa de \"Deteção de Rostos\" para todas as imagens.",
+    "machine_learning_facial_recognition_setting": "Ativar reconhecimento facial",
+    "machine_learning_facial_recognition_setting_description": "Se desativado, as imagens não serão codificadas para reconhecimento facial e não preencherão a secção Pessoas na página Explorar.",
+    "machine_learning_max_detection_distance": "Distância máxima de deteção",
+    "machine_learning_max_detection_distance_description": "Distância máxima entre duas imagens para considerá-las duplicadas, variando entre 0,001 e 0,1. Valores mais altos detetarão mais duplicidades, mas poderão resultar em falsos positivos.",
+    "machine_learning_max_recognition_distance": "Distância máxima de reconhecimento",
+    "machine_learning_max_recognition_distance_description": "Distância máxima entre dois rostos para serem considerados a mesma pessoa, variando de 0 a 2. Valores menores evitam rotular dois rostos como a mesma pessoa, enquanto valores maiores evitam rotular o mesmo rosto como duas pessoas diferentes. Tenha em conta de que é mais fácil unir duas pessoas do que dividir uma pessoa em duas, portanto tenha preferência por valores mais baixos quando possível.",
+    "machine_learning_min_detection_score": "Pontuação mínima de deteção",
+    "machine_learning_min_detection_score_description": "Pontuação mínima de confiança para um rosto ser detetado, de 0 a 1. Valores mais baixos detetam mais rostos, mas poderão resultar em falsos positivos.",
+    "machine_learning_min_recognized_faces": "Mínimo de rostos reconhecidos",
+    "machine_learning_min_recognized_faces_description": "O número mínimo de faces reconhecidas para uma pessoa ser criada na lista. Aumentar isto torna o Reconhecimento Facial mais preciso, no entanto aumenta a probabilidade de um rosto não ser atribuído a uma pessoa.",
+    "machine_learning_settings": "Definições de aprendizagem de máquina (Machine Learning)",
+    "machine_learning_settings_description": "Gerir funcionalidades e definições de aprendizagem de máquina",
+    "machine_learning_smart_search": "Pesquisa Inteligente",
+    "machine_learning_smart_search_description": "Pesquise imagens semanticamente utilizando embeddings CLIP",
+    "machine_learning_smart_search_enabled": "Ativar a Pesquisa Inteligente",
+    "machine_learning_smart_search_enabled_description": "Se desativado, as imagens não serão codificadas para Pesquisa Inteligente.",
+    "machine_learning_url_description": "A URL do servidor de aprendizagem de máquina. Se for fornecido mais do que um URL, cada servidor será testado, um a um, até um deles responder com sucesso, por ordem do primeiro ao último.",
+    "manage_concurrency": "Gerir simultaneidade",
+    "manage_log_settings": "Gerir definições de registo",
+    "map_dark_style": "Tema Escuro",
+    "map_enable_description": "Ativar funcionalidades de mapa",
+    "map_gps_settings": "Mapas e Definições de GPS",
+    "map_gps_settings_description": "Gerir Definições de Mapas e GPS (Geocodificação Reversa)",
+    "map_implications": "A funcionalidade do mapa necessita um serviço externo (tiles.immich.cloud)",
+    "map_light_style": "Tema Claro",
+    "map_manage_reverse_geocoding_settings": "Gerir definições de <link>Geocodificação Reversa</link>",
+    "map_reverse_geocoding": "Geocodificação Reversa",
+    "map_reverse_geocoding_enable_description": "Ativar Geocodificação Reversa",
+    "map_reverse_geocoding_settings": "Definições de Geocodificação Reversa",
+    "map_settings": "Mapa",
+    "map_settings_description": "Gerir definições do mapa",
+    "map_style_description": "URL para um tema de mapa style.json",
+    "metadata_extraction_job": "Extrair metadados",
+    "metadata_extraction_job_description": "Extrai informações de metadados de cada ficheiro, como GPS, rostos e resolução",
+    "metadata_faces_import_setting": "Ativar a importação facial",
+    "metadata_faces_import_setting_description": "Importar rostos a partir dos dados EXIF da imagem e ficheiros anexos",
+    "metadata_settings": "Definições de metadados",
+    "metadata_settings_description": "Gerir definições de metadados",
+    "migration_job": "Migração",
+    "migration_job_description": "Migra miniaturas de ficheiros e rostos para a estrutura de pastas mais recente",
+    "no_paths_added": "Nenhum caminho adicionado",
+    "no_pattern_added": "Nenhum padrão adicionado",
+    "note_apply_storage_label_previous_assets": "Observação: Para aplicar o Rótulo de Armazenamento a ficheiros carregados anteriormente, execute o",
+    "note_cannot_be_changed_later": "NOTA: Isto não pode ser alterado posteriormente!",
+    "note_unlimited_quota": "Observação: insira 0 para quota ilimitada",
+    "notification_email_from_address": "A partir do endereço",
+    "notification_email_from_address_description": "Endereço de e-mail do remetente, por exemplo: \"Servidor de Fotos Immich <noreply@example.com>\"",
+    "notification_email_host_description": "Host do servidor de e-mail (por exemplo, smtp.immich.app)",
+    "notification_email_ignore_certificate_errors": "Ignorar erros de certificado",
+    "notification_email_ignore_certificate_errors_description": "Ignorar erros de validação de certificado TLS (não recomendado)",
+    "notification_email_password_description": "Palavra-passe a ser usada ao autenticar no servidor de e-mail",
+    "notification_email_port_description": "Porta do servidor de e-mail (por exemplo, 25, 465 ou 587)",
+    "notification_email_sent_test_email_button": "Enviar e-mail de teste e gravar",
+    "notification_email_setting_description": "Definições para envio de notificações por e-mail",
+    "notification_email_test_email": "Enviar e-mail de teste",
+    "notification_email_test_email_failed": "Falha ao enviar e-mail de teste, verifique os valores",
+    "notification_email_test_email_sent": "Um email de teste foi enviado para {email}. Por favor, verifique a sua caixa de entrada.",
+    "notification_email_username_description": "Nome de utilizador a ser usado ao autenticar com o servidor de e-mail",
+    "notification_enable_email_notifications": "Ativar notificações por e-mail",
+    "notification_settings": "Definições de notificações",
+    "notification_settings_description": "Gerir definições de notificações, incluindo e-mail",
+    "oauth_auto_launch": "Arranque automático",
+    "oauth_auto_launch_description": "Iniciar o fluxo de login do OAuth automaticamente ao navegar até a página de inicio de sessão",
+    "oauth_auto_register": "Registo automático",
+    "oauth_auto_register_description": "Registar automaticamente novos utilizadores após iniciarem sessão com o OAuth",
+    "oauth_button_text": "Texto do botão",
+    "oauth_client_id": "ID do Cliente",
+    "oauth_client_secret": "Segredo do cliente",
+    "oauth_enable_description": "Iniciar sessão com o OAuth",
+    "oauth_issuer_url": "URL do emissor",
+    "oauth_mobile_redirect_uri": "URI de redirecionamento móvel",
+    "oauth_mobile_redirect_uri_override": "Substituição de URI de redirecionamento móvel",
+    "oauth_mobile_redirect_uri_override_description": "Ative quando o provedor do OAuth não permite um URI móvel, como '{callback}'",
+    "oauth_profile_signing_algorithm": "Algoritmo de assinatura de perfis",
+    "oauth_profile_signing_algorithm_description": "Algoritmo utilizado para assinar o perfil de utilizador.",
+    "oauth_scope": "Escopo",
+    "oauth_settings": "OAuth",
+    "oauth_settings_description": "Gerir definições de inicio de sessão do OAuth",
+    "oauth_settings_more_details": "Para mais informações sobre esta funcionalidade, veja a <link>documentação</link>.",
+    "oauth_signing_algorithm": "Algoritmo de assinatura",
+    "oauth_storage_label_claim": "Reivindicação de Rótulo de Armazenamento",
+    "oauth_storage_label_claim_description": "Definir automaticamente o Rótulo de Armazenamento do utilizador para o valor desta declaração.",
+    "oauth_storage_quota_claim": "Reivindicação de quota de armazenamento",
+    "oauth_storage_quota_claim_description": "Definir automaticamente a quota de armazenamento do utilizador para o valor desta declaração.",
+    "oauth_storage_quota_default": "Quota de armazenamento padrão (GiB)",
+    "oauth_storage_quota_default_description": "Quota em GiB a ser usada quando nenhuma reivindicação for fornecida (insira 0 para quota ilimitada).",
+    "offline_paths": "Caminhos Offline",
+    "offline_paths_description": "Estes resultados podem ser devidos à eliminação manual de ficheiros que não fazem parte de uma biblioteca externa.",
+    "password_enable_description": "Iniciar sessão com e-mail e palavra-passe",
+    "password_settings": "Palavra-passe de acesso",
+    "password_settings_description": "Gerir definições de inicio de sessão e palavra-passe",
+    "paths_validated_successfully": "Todos os caminhos validados com sucesso",
+    "person_cleanup_job": "Limpeza de pessoas",
+    "quota_size_gib": "Tamanho da quota (GiB)",
+    "refreshing_all_libraries": "A atualizar todas as bibliotecas",
+    "registration": "Registo de Administrador",
+    "registration_description": "Como é o primeiro utilizador no sistema, será marcado como administrador, e será responsável pelas tarefas administrativas, sendo que utilizadores adicionais serão criados por si.",
+    "repair_all": "Reparar tudo",
+    "repair_matched_items": "{count, plural, one {Encontrado # item} other {Encontrados # itens}}",
+    "repaired_items": "{count, plural, one {Reparado # item} other {Reparados # itens}}",
+    "require_password_change_on_login": "Exigir que o utilizador altere a palavra-passe no primeiro início de sessão",
+    "reset_settings_to_default": "Redefinir as definições para o padrão",
+    "reset_settings_to_recent_saved": "Redefinir as definições para as guardadas mais recentemente",
+    "scanning_library": "A analisar biblioteca",
+    "search_jobs": "Pesquisar tarefas...",
+    "send_welcome_email": "Enviar e-mail de boas-vindas",
+    "server_external_domain_settings": "Domínio externo",
+    "server_external_domain_settings_description": "Domínio para links públicos partilhados, incluindo http(s)://",
+    "server_public_users": "Utilizadores Públicos",
+    "server_public_users_description": "Todos os utilizadores (nome e e-mail) serão listados quando adicionar um utilizador a álbuns partilhados. Quando desativado, a lista de utilizadores só será visível a administradores.",
+    "server_settings": "Definições do Servidor",
+    "server_settings_description": "Gerir definições do servidor",
+    "server_welcome_message": "Mensagem de boas-vindas",
+    "server_welcome_message_description": "Uma mensagem que é exibida na página de inicio de sessão.",
+    "sidecar_job": "Metadados secundários",
+    "sidecar_job_description": "Descobrir ou sincronizar metadados secundários a partir do sistema de ficheiros",
+    "slideshow_duration_description": "Tempo em segundos para exibir cada imagem",
+    "smart_search_job_description": "Execute a aprendizagem automática em ficheiros para oferecer apoio à Pesquisa Inteligente",
+    "storage_template_date_time_description": "O registo de data e hora de criação do ficheiro é usado para fornecer essas informações",
+    "storage_template_date_time_sample": "Exemplo de tempo {date}",
+    "storage_template_enable_description": "Ativar mecanismo de modelo de armazenamento",
+    "storage_template_hash_verification_enabled": "Verificação de hash ativada",
+    "storage_template_hash_verification_enabled_description": "Ativa a verificação de hash, não desative esta opção a menos que tenha a certeza das implicações",
+    "storage_template_migration": "Migração de modelo de armazenamento",
+    "storage_template_migration_description": "Aplica o <link>{template}</link> atual para ficheiros previamente carregados",
+    "storage_template_migration_info": "As mudanças do modelo apenas se aplicarão a novos ficheiros. Para aplicar o modelo retroativamente para os ficheiros carregados anteriormente, execute o <link>{job}</link>.",
+    "storage_template_migration_job": "Tarefa de Migração do Modelo de Armazenamento",
+    "storage_template_more_details": "Para mais informações sobre esta funcionalidade, dirija-se a <template-link>Modelo de Armazenamento</template-link> e às suas <implications-link>implicações</implications-link>",
+    "storage_template_onboarding_description": "Quando ativada, esta funcionalidade irá organizar os ficheiros automaticamente baseando-se num modelo definido pelo utilizador. Devido a problemas de estabilidade esta funcionalidade está desativada por padrão. Para mais informações, por favor leia a <link>documentação</link>.",
+    "storage_template_path_length": "Limite aproximado do tamanho do caminho: <b>{length, number}</b>{limit, number}",
+    "storage_template_settings": "Modelo de Armazenamento",
+    "storage_template_settings_description": "Gerir a estrutura de pastas e o nome do ficheiro carregado",
+    "storage_template_user_label": "<code>{label}</code> é o Rótulo do Armazenamento do utilizador",
+    "system_settings": "Definições de Sistema",
+    "tag_cleanup_job": "Limpeza de etiquetas",
+    "template_email_available_tags": "Pode usar as seguintes variáveis no modelo: {tags}",
+    "template_email_if_empty": "Se o modelo estiver em branco, o modelo de e-mail padrão será utilizado.",
+    "template_email_invite_album": "Modelo do e-mail de convite para álbum",
+    "template_email_preview": "Pré-visualizar",
+    "template_email_settings": "Modelos de e-mail",
+    "template_email_settings_description": "Gerir modelos personalizados de e-mail de notificação",
+    "template_email_update_album": "Modelo do e-mail de atualização do álbum",
+    "template_email_welcome": "Modelos do email de boas vindas",
+    "template_settings": "Modelos de notificação",
+    "template_settings_description": "Gerir modelos personalizados para notificações.",
+    "theme_custom_css_settings": "CSS Personalizado",
+    "theme_custom_css_settings_description": "Folhas de estilo em cascata (CSS) permitem que o design do Immich seja personalizado.",
+    "theme_settings": "Definições de Tema",
+    "theme_settings_description": "Gerir a personalização da interface web do Immich",
+    "these_files_matched_by_checksum": "Estes ficheiros são correspondidos pelas suas somas de verificação",
+    "thumbnail_generation_job": "Gerar miniaturas",
+    "thumbnail_generation_job_description": "Gera miniaturas grandes, pequenas e desfocadas para cada ficheiro, bem como miniaturas para cada pessoa",
+    "transcoding_acceleration_api": "API de aceleração",
+    "transcoding_acceleration_api_description": "A API que irá interagir com o seu dispositivo para acelerar a transcodificação. Esta definição é a 'melhor opção': ela voltará à transcodificação de software em caso de falha. O VP9 pode não funcionar dependendo do seu hardware.",
+    "transcoding_acceleration_nvenc": "NVENC (requer GPU NVIDIA)",
+    "transcoding_acceleration_qsv": "Quick Sync (requer CPU Intel de 7ª geração ou posterior)",
+    "transcoding_acceleration_rkmpp": "RKMPP (apenas em SOCs Rockchip)",
+    "transcoding_acceleration_vaapi": "VAAPI",
+    "transcoding_accepted_audio_codecs": "Codecs de áudio aceites",
+    "transcoding_accepted_audio_codecs_description": "Selecione os codecs de áudio que não precisam de ser transcodificados. Usado apenas para determinadas políticas de transcodificação.",
+    "transcoding_accepted_containers": "Contentores aceites",
+    "transcoding_accepted_containers_description": "Selecione os formatos de contentores que não precisam de ser remisturados para MP4. Usado apenas para algumas políticas de transcodificação.",
+    "transcoding_accepted_video_codecs": "Codecs de vídeo aceitos",
+    "transcoding_accepted_video_codecs_description": "Selecione quais os codecs de vídeo que não precisam de ser transcodificados. Usado apenas para determinadas políticas de transcodificação.",
+    "transcoding_advanced_options_description": "Opções que a maioria dos utilizadores não deverá precisar de alterar",
+    "transcoding_audio_codec": "Codec de áudio",
+    "transcoding_audio_codec_description": "Opus é a opção de mais alta qualidade, mas tem menor compatibilidade com dispositivos ou software antigos.",
+    "transcoding_bitrate_description": "Vídeos com taxa de bits superior à máxima ou que não estão num formato aceite",
+    "transcoding_codecs_learn_more": "Para saber mais sobre as terminologias utilizadas aqui, consulte a documentação do FFmpeg para o <h264-link>codec H.264</h264-link>, <hevc-link>codec HEVC</hevc-link> e <vp9-link>codec VP9</vp9-link>.",
+    "transcoding_constant_quality_mode": "Modo de qualidade fixa",
+    "transcoding_constant_quality_mode_description": "ICQ é melhor que CQP, mas alguns dispositivos de aceleração de hardware não suportam este modo. Definir esta opção dará preferência ao modo especificado ao usar codificação baseada em qualidade. Ignorado pelo NVENC porque não suporta ICQ.",
+    "transcoding_constant_rate_factor": "Fator de taxa constante (-crf)",
+    "transcoding_constant_rate_factor_description": "Nível de qualidade do vídeo. Os valores típicos são 23 para H.264, 28 para HEVC, 31 para VP9 e 35 para AV1. Menor é melhor, mas produz ficheiros maiores.",
+    "transcoding_disabled_description": "Não transcodificar nenhum vídeo, no entanto pode causar erros de reprodução em alguns clientes",
+    "transcoding_hardware_acceleration": "Aceleração de hardware",
+    "transcoding_hardware_acceleration_description": "Experimental; muito mais rápido, mas terá qualidade inferior com a mesma taxa de bits",
+    "transcoding_hardware_decoding": "Decodificação de hardware",
+    "transcoding_hardware_decoding_setting_description": "Permite a aceleração ponta a ponta em vez de apenas acelerar a codificação. Pode não funcionar em todos os formatos de arquivo.",
+    "transcoding_hevc_codec": "Codec HEVC",
+    "transcoding_max_b_frames": "Máximo de quadros B",
+    "transcoding_max_b_frames_description": "Valores mais altos melhoram a eficiência da compressão, mas tornam a codificação mais lenta. Pode não ser compatível com aceleração de hardware em dispositivos mais antigos. 0 desativa os quadros B, enquanto -1 define esse valor automaticamente.",
+    "transcoding_max_bitrate": "Taxa de bits máxima",
+    "transcoding_max_bitrate_description": "Definir uma taxa de bits máxima pode tornar os tamanhos dos ficheiros mais previsíveis com um custo menor de qualidade. Em 720p, os valores típicos são 2.600k para VP9 ou HEVC, ou 4.500k para H.264. Desativado se definido como 0.",
+    "transcoding_max_keyframe_interval": "Intervalo máximo de quadro-chave",
+    "transcoding_max_keyframe_interval_description": "Define a distância máxima do quadro entre os quadros-chave. Valores mais baixos pioram a eficiência da compressão, mas melhoram os tempos de procura e podem melhorar a qualidade em cenas com movimento rápido. 0 define esse valor automaticamente.",
+    "transcoding_optimal_description": "Vídeos com resolução superior à desejada ou num formato não aceite",
+    "transcoding_preferred_hardware_device": "Dispositivo de hardware preferido",
+    "transcoding_preferred_hardware_device_description": "Aplica-se apenas a VAAPI e QSV. Define o nó dri usado para transcodificação de hardware.",
+    "transcoding_preset_preset": "Predefinição (-preset)",
+    "transcoding_preset_preset_description": "Velocidade de compressão. Predefinições mais lentas produzem ficheiros menores e aumentam a qualidade ao atingir uma determinada taxa de bits. VP9 ignora velocidades acima de \"mais rápido\".",
+    "transcoding_reference_frames": "Quadros de referência",
+    "transcoding_reference_frames_description": "O número de quadros a serem referenciados ao comprimir um determinado quadro. Valores mais altos melhoram a eficiência da compressão, mas tornam a codificação mais lenta. 0 define esse valor automaticamente.",
+    "transcoding_required_description": "Apenas vídeos que não estejam num formato aceite",
+    "transcoding_settings": "Definições de transcodificação de vídeo",
+    "transcoding_settings_description": "Gerir as informações de resolução e codificação dos ficheiros de vídeo",
+    "transcoding_target_resolution": "Resolução desejada",
+    "transcoding_target_resolution_description": "Resoluções mais altas podem preservar mais detalhes, mas demoram mais para codificar, têm tamanhos de ficheiro maiores e podem reduzir a capacidade de resposta da aplicação.",
+    "transcoding_temporal_aq": "QA temporal",
+    "transcoding_temporal_aq_description": "Aplica-se apenas ao NVENC. Aumenta a qualidade de cenas com alto detalhe e pouco movimento. Pode não ser compatível com dispositivos mais antigos.",
+    "transcoding_threads": "Threads",
+    "transcoding_threads_description": "Valores mais altos levam a uma codificação mais rápida, mas deixam menos espaço para o servidor processar outras tarefas enquanto estiver ativo. Este valor não deve ser superior ao número de núcleos do CPU. Maximiza a utilização se definido como 0.",
+    "transcoding_tone_mapping": "Mapeamento de tons",
+    "transcoding_tone_mapping_description": "Tenta preservar a aparência dos vídeos HDR quando convertidos para SDR. Cada algoritmo faz compensações diferentes em termos de cor, detalhes e brilho. Hable preserva os detalhes, Mobius preserva as cores e Reinhard preserva o brilho.",
+    "transcoding_transcode_policy": "Política de transcodificação",
+    "transcoding_transcode_policy_description": "Política para quando um vídeo deve ser transcodificado. Os vídeos HDR serão sempre transcodificados (exceto se a transcodificação estiver desativada).",
+    "transcoding_two_pass_encoding": "Codificação em duas passagens",
+    "transcoding_two_pass_encoding_setting_description": "Transcodificar em duas passagens para produzir vídeos melhor codificados. Quando a taxa de bits máxima está ativada (necessário para funcionar com H.264 e HEVC), este modo usa um intervalo de taxa de bits baseado na taxa de bits máxima e ignora o CRF. Para VP9, o CRF pode ser usado se a taxa de bits máxima estiver desativada.",
+    "transcoding_video_codec": "Codec de vídeo",
+    "transcoding_video_codec_description": "O VP9 tem alta eficiência e compatibilidade com a web, mas leva mais tempo para transcodificar. HEVC tem desempenho semelhante, mas tem menor compatibilidade com a web. H.264 é amplamente compatível e rápido de transcodificar, mas produz ficheiros muito maiores. AV1 é o codec mais eficiente, mas não possui suporte em dispositivos mais antigos.",
+    "trash_enabled_description": "Ativar funcionalidade da Reciclagem",
+    "trash_number_of_days": "Número de dias",
+    "trash_number_of_days_description": "Número de dias para manter os ficheiros na reciclagem antes de os eliminar permanentemente",
+    "trash_settings": "Definições da Reciclagem",
+    "trash_settings_description": "Gerir definições da reciclagem",
+    "untracked_files": "Ficheiros não monitorizados",
+    "untracked_files_description": "Estes ficheiros não são monitorizados pela aplicação. Podem ser o resultado de transferências mal-sucedidas, carregamentos interrompidos ou deixados para trás devido a um problema",
+    "user_cleanup_job": "Limpeza de utilizadores",
+    "user_delete_delay": "A conta e os ficheiros de <b>{user}</b> serão agendados para eliminação permanente dentro de {delay, plural, one {# dia} other {# dias}}.",
+    "user_delete_delay_settings": "Atraso de eliminação",
+    "user_delete_delay_settings_description": "Número de dias após a remoção para excluir permanentemente a conta e os ficheiros de um utilizador. A tarefa de eliminação de utilizadores é executada à meia-noite para verificar utilizadores que estão prontos para eliminação. As alterações a esta definição serão avaliadas na próxima execução.",
+    "user_delete_immediately": "A conta e os ficheiros de <b>{user}</b> serão colocados em fila para eliminação permanente <b>de imediato</b>.",
+    "user_delete_immediately_checkbox": "Adicionar utilizador e ficheiros à fila para eliminação imediata",
+    "user_management": "Gestão de utilizadores",
+    "user_password_has_been_reset": "A palavra-passe do utilizador foi redefinida:",
+    "user_password_reset_description": "Por favor forneça a palavra-passe temporária ao utilizador e informe-o(a) de que será necessário alterá-la próximo início de sessão.",
+    "user_restore_description": "A conta de <b>{user}</b> será restaurada.",
+    "user_restore_scheduled_removal": "Restaurar utilizador - remoção agendada em {date, date, long}",
+    "user_settings": "Definições do Utilizador",
+    "user_settings_description": "Gerir definições do utilizador",
+    "user_successfully_removed": "O utilizador {email} foi removido com sucesso.",
+    "version_check_enabled_description": "Ativa verificação de novas versões",
+    "version_check_implications": "A funcionalidade de verificação da versão necessita de comunicação periódica com o github.com",
+    "version_check_settings": "Verificação de versão",
+    "version_check_settings_description": "Ativar/desativar a notificação de nova versão",
+    "video_conversion_job": "Transcodificar vídeos",
+    "video_conversion_job_description": "Transcodifica vídeos para maior compatibilidade com navegadores e dispositivos"
+  },
+  "admin_email": "E-mail do administrador",
+  "admin_password": "Palavra-passe do administrador",
+  "administration": "Administração",
+  "advanced": "Avançado",
+  "age_months": "Idade {months, plural, one {# mês} other {# meses}}",
+  "age_year_months": "Idade 1 ano, {months, plural, one {# mês} other {# meses}}",
+  "age_years": "{years, plural, one{# ano} other {# anos}}",
+  "album_added": "Álbum adicionado",
+  "album_added_notification_setting_description": "Receber uma notificação por e-mail quando for adicionado a um álbum partilhado",
+  "album_cover_updated": "Capa do álbum atualizada",
+  "album_delete_confirmation": "Tem a certeza de que quer eliminar o álbum {album}?",
+  "album_delete_confirmation_description": "Se este álbum for partilhado, os outros utilizadores deixam de o poder aceder.",
+  "album_info_updated": "Informações do álbum atualizadas",
+  "album_leave": "Sair do álbum?",
+  "album_leave_confirmation": "Tem a certeza de que quer sair de {album}?",
+  "album_name": "Nome do álbum",
+  "album_options": "Opções de álbum",
+  "album_remove_user": "Remover utilizador?",
+  "album_remove_user_confirmation": "Tem a certeza de que quer remover {user}?",
+  "album_share_no_users": "Parece que tem este álbum partilhado com todos os utilizadores ou que não existem utilizadores com quem o partilhar.",
+  "album_updated": "Álbum atualizado",
+  "album_updated_setting_description": "Receber uma notificação por e-mail quando um álbum partilhado tiver novos ficheiros",
+  "album_user_left": "Saíu do {album}",
+  "album_user_removed": "Utilizador {user} removido",
+  "album_with_link_access": "Permite o acesso a fotos e pessoas deste álbum por qualquer pessoa com o link.",
+  "albums": "Álbuns",
+  "albums_count": "{count, plural, one {{count, number} Álbum} other {{count, number} Álbuns}}",
+  "all": "Todos",
+  "all_albums": "Todos os álbuns",
+  "all_people": "Todas as pessoas",
+  "all_videos": "Todos os vídeos",
+  "allow_dark_mode": "Permitir modo escuro",
+  "allow_edits": "Permitir edições",
+  "allow_public_user_to_download": "Permitir que utilizadores públicos façam transferências",
+  "allow_public_user_to_upload": "Permitir que utilizadores públicos façam carregamentos",
+  "anti_clockwise": "Sentido anti-horário",
+  "api_key": "Chave de API",
+  "api_key_description": "Este valor será apresentado apenas uma única vez. Por favor, certifique-se que o copiou antes de fechar a janela.",
+  "api_key_empty": "O nome da chave a API não pode estar vazio",
+  "api_keys": "Chaves de API",
+  "app_settings": "Definições da Aplicação",
+  "appears_in": "Aparece em",
+  "archive": "Arquivo",
+  "archive_or_unarchive_photo": "Arquivar ou desarquivar foto",
+  "archive_size": "Tamanho do arquivo",
+  "archive_size_description": "Configure o tamanho do arquivo para transferências (em GiB)",
+  "archived_count": "{count, plural, one {#Arquivado # item} other {Arquivados # itens}}",
+  "are_these_the_same_person": "Estas pessoas são a mesma pessoa?",
+  "are_you_sure_to_do_this": "Tem a certeza de que quer fazer isto?",
+  "asset_added_to_album": "Adicionado ao álbum",
+  "asset_adding_to_album": "A adicionar ao álbum...",
+  "asset_description_updated": "A descrição do ficheiro foi atualizada",
+  "asset_filename_is_offline": "O ficheiro {filename} não está disponível",
+  "asset_has_unassigned_faces": "O ficheiro tem rostos não atribuídas",
+  "asset_hashing": "A criar hash...",
+  "asset_offline": "Ficheiro Indisponível",
+  "asset_offline_description": "Este ficheiro externo deixou de estar disponível no disco. Contacte o seu administrador do Immich para obter ajuda.",
+  "asset_skipped": "Ignorado",
+  "asset_skipped_in_trash": "Na reciclagem",
+  "asset_uploaded": "Enviado",
+  "asset_uploading": "A enviar...",
+  "assets": "Ficheiros",
+  "assets_added_count": "{count, plural, one {# ficheiro adicionado} other {# ficheiros adicionados}}",
+  "assets_added_to_album_count": "{count, plural, one {# ficheiro adicionado} other {# ficheiros adicionados}} ao álbum",
+  "assets_added_to_name_count": "{count, plural, one {# ficheiro adicionado} other {# ficheiros adicionados}} a {hasName, select, true {<b>{name}</b>} other {novo álbum}}",
+  "assets_count": "{count, plural, one {# ficheiro} other {# ficheiros}}",
+  "assets_moved_to_trash_count": "{count, plural, one {# ficheiro movido} other {# ficheiros movidos}} para a reciclagem",
+  "assets_permanently_deleted_count": "{count, plural, one {# ficheiro} other {# ficheiros}} eliminados permanentemente",
+  "assets_removed_count": "{count, plural, one {# ficheiro eliminado} other {# ficheiros eliminados}}",
+  "assets_restore_confirmation": "Tem a certeza de que quer recuperar todos os ficheiros apagados? Não é possível anular esta ação! Tenha em conta de que quaisquer ficheiros indisponíveis não podem ser restaurados desta forma.",
+  "assets_restored_count": "{count, plural, one {# ficheiro restaurado} other {# ficheiros restaurados}}",
+  "assets_trashed_count": "{count, plural, one {# ficheiro enviado} other {# ficheiros enviados}} para a reciclagem",
+  "assets_were_part_of_album_count": "{count, plural, one {O ficheiro já fazia} other {Os ficheiros já faziam}} parte do álbum",
+  "authorized_devices": "Dispositivos Autorizados",
+  "back": "Voltar",
+  "back_close_deselect": "Voltar, fechar ou desmarcar",
+  "backward": "Para trás",
+  "birthdate_saved": "Data de nascimento guardada com sucesso",
+  "birthdate_set_description": "A data de nascimento é utilizada para calcular a idade desta pessoa no momento em que uma fotografia foi tirada.",
+  "blurred_background": "Fundo desfocado",
+  "bugs_and_feature_requests": "Relatar problemas ou pedir novas funcionalidades",
+  "build": "Versão de compilação",
+  "build_image": "Imagem de compilação",
+  "bulk_delete_duplicates_confirmation": "Tem a certeza de que deseja eliminar {count, plural, one {# ficheiro duplicado} other {# ficheiros duplicados}}? Esta ação mantém o maior ficheiro de cada grupo e elimina permanentemente todos os outros duplicados. Não é possível anular esta ação!",
+  "bulk_keep_duplicates_confirmation": "Tem a certeza de que deseja manter {count, plural, one {# ficheiro duplicado} other {# ficheiros duplicados}}? Isto resolverá todos os grupos duplicados sem eliminar nada.",
+  "bulk_trash_duplicates_confirmation": "Tem a certeza de que deseja mover para a reciclagem {count, plural, one {# ficheiro duplicado} other {# ficheiros duplicados}}? Isto manterá o maior ficheiro de cada grupo e irá mover para a reciclagem todos os outros duplicados.",
+  "buy": "Comprar Immich",
+  "camera": "Câmara",
+  "camera_brand": "Marca da câmara",
+  "camera_model": "Modelo da câmara",
+  "cancel": "Cancelar",
+  "cancel_search": "Cancelar pesquisa",
+  "cannot_merge_people": "Não foi possível unir pessoas",
+  "cannot_undo_this_action": "Não é possível anular esta ação!",
+  "cannot_update_the_description": "Não foi possível atualizar a descrição",
+  "change_date": "Alterar data",
+  "change_expiration_time": "Alterar o prazo de validade",
+  "change_location": "Alterar localização",
+  "change_name": "Alterar nome",
+  "change_name_successfully": "Nome alterado com sucesso",
+  "change_password": "Alterar a palavra-passe",
+  "change_password_description": "Esta é a primeira vez que está a entrar no sistema ou um pedido foi feito para alterar a sua palavra-passe. Insira a nova palavra-passe abaixo.",
+  "change_your_password": "Alterar a sua palavra-passe",
+  "changed_visibility_successfully": "Visibilidade alterada com sucesso",
+  "check_all": "Verificar tudo",
+  "check_logs": "Verificar registos",
+  "choose_matching_people_to_merge": "Escolha pessoas correspondentes para unir",
+  "city": "Cidade",
+  "clear": "Limpar",
+  "clear_all": "Limpar tudo",
+  "clear_all_recent_searches": "Limpar todas as pesquisas recentes",
+  "clear_message": "Limpar mensagem",
+  "clear_value": "Limpar valor",
+  "clockwise": "Sentido horário",
+  "close": "Fechar",
+  "collapse": "Colapsar",
+  "collapse_all": "Colapsar tudo",
+  "color": "Cor",
+  "color_theme": "Esquema de cores",
+  "comment_deleted": "Comentário eliminado",
+  "comment_options": "Opções de comentário",
+  "comments_and_likes": "Comentários e gostos",
+  "comments_are_disabled": "Comentários estão desativados",
+  "confirm": "Confirmar",
+  "confirm_admin_password": "Confirmar palavra-passe de administrador",
+  "confirm_delete_shared_link": "Tem a certeza de que deseja eliminar este link partilhado?",
+  "confirm_keep_this_delete_others": "Todos os outros ficheiros na pilha serão eliminados, exceto este ficheiro. Tem a certeza de que deseja continuar?",
+  "confirm_password": "Confirmar a palavra-passe",
+  "contain": "Ajustar",
+  "context": "Contexto",
+  "continue": "Continuar",
+  "copied_image_to_clipboard": "Imagem copiada para a área de transferência.",
+  "copied_to_clipboard": "Copiado para a área de transferência!",
+  "copy_error": "Copiar erro",
+  "copy_file_path": "Copiar caminho do ficheiro",
+  "copy_image": "Copiar Imagem",
+  "copy_link": "Copiar link",
+  "copy_link_to_clipboard": "Copiar link para a área de transferência",
+  "copy_password": "Copiar palavra-passe",
+  "copy_to_clipboard": "Copiar para a área de transferência",
+  "country": "País",
+  "cover": "Preencher",
+  "covers": "Capas",
+  "create": "Criar",
+  "create_album": "Criar álbum",
+  "create_library": "Criar biblioteca",
+  "create_link": "Criar link",
+  "create_link_to_share": "Criar link para partilhar",
+  "create_link_to_share_description": "Permitir a visualização desta(s) imagem(s) a qualquer pessoa com o link",
+  "create_new_person": "Criar nova pessoa",
+  "create_new_person_hint": "Associe os ficheiros a uma nova pessoa",
+  "create_new_user": "Criar novo utilizador",
+  "create_tag": "Criar etiqueta",
+  "create_tag_description": "Criar uma nova etiqueta. Para etiquetas compostas, introduza o caminho completo, incluindo as barras.",
+  "create_user": "Criar utilizador",
+  "created": "Criado",
+  "current_device": "Dispositivo atual",
+  "custom_locale": "Localização Personalizada",
+  "custom_locale_description": "Formatar datas e números baseados na língua e na região",
+  "dark": "Escuro",
+  "date_after": "Data após",
+  "date_and_time": "Data e Hora",
+  "date_before": "Data antes",
+  "date_of_birth_saved": "Data de nascimento guardada com sucesso",
+  "date_range": "Intervalo de datas",
+  "day": "Dia",
+  "deduplicate_all": "Limpar todos os itens duplicados",
+  "default_locale": "Localização Padrão",
+  "default_locale_description": "Formatar datas e números baseados na linguagem do seu navegador",
+  "delete": "Eliminar",
+  "delete_album": "Eliminar álbum",
+  "delete_api_key_prompt": "Tem a certeza de que deseja eliminar esta chave de API?",
+  "delete_duplicates_confirmation": "Tem a certeza de que deseja eliminar permanentemente estes itens duplicados?",
+  "delete_key": "Eliminar chave",
+  "delete_library": "Eliminar Biblioteca",
+  "delete_link": "Eliminar link",
+  "delete_others": "Excluir outros",
+  "delete_shared_link": "Eliminar link de partilha",
+  "delete_tag": "Eliminar etiqueta",
+  "delete_tag_confirmation_prompt": "Tem a certeza de que pretende eliminar a etiqueta {tagName} ?",
+  "delete_user": "Eliminar utilizador",
+  "deleted_shared_link": "Link de partilha eliminado",
+  "deletes_missing_assets": "Elimina os ficheiros que estejam em falta no disco",
+  "description": "Descrição",
+  "details": "Detalhes",
+  "direction": "Direção",
+  "disabled": "Desativado",
+  "disallow_edits": "Não permitir edições",
+  "discord": "Discord",
+  "discover": "Descobrir",
+  "dismiss_all_errors": "Dispensar todos os erros",
+  "dismiss_error": "Dispensar erro",
+  "display_options": "Opções de exibição",
+  "display_order": "Ordem de exibição",
+  "display_original_photos": "Exibir fotos originais",
+  "display_original_photos_setting_description": "Preferir a exibição da foto original ao visualizar um ficheiro em vez de miniaturas quando o ficheiro original é compatível com a web. Isso pode diminuir a velocidade de exibição das fotos.",
+  "do_not_show_again": "Não mostrar esta mensagem novamente",
+  "documentation": "Documentação",
+  "done": "Feito",
+  "download": "Transferir",
+  "download_include_embedded_motion_videos": "Vídeos incorporados",
+  "download_include_embedded_motion_videos_description": "Incluir vídeos incorporados em fotos em movimento como um ficheiro separado",
+  "download_settings": "Transferir",
+  "download_settings_description": "Gerir definições relacionadas com a transferência de ficheiros",
+  "downloading": "A transferir",
+  "downloading_asset_filename": "A transferir o ficheiro {filename}",
+  "drop_files_to_upload": "Solte os ficheiros em qualquer lugar para os enviar",
+  "duplicates": "Itens duplicados",
+  "duplicates_description": "Marque cada grupo indicando quais ficheiros, se algum, são duplicados",
+  "duration": "Duração",
+  "edit": "Editar",
+  "edit_album": "Editar álbum",
+  "edit_avatar": "Editar imagem de perfil",
+  "edit_date": "Editar data",
+  "edit_date_and_time": "Editar data e hora",
+  "edit_exclusion_pattern": "Editar o padrão de exclusão",
+  "edit_faces": "Editar rostos",
+  "edit_import_path": "Editar caminho de importação",
+  "edit_import_paths": "Editar caminhos de importação",
+  "edit_key": "Editar chave",
+  "edit_link": "Editar link",
+  "edit_location": "Editar Localização",
+  "edit_name": "Editar nome",
+  "edit_people": "Editar pessoas",
+  "edit_tag": "Editar etiqueta",
+  "edit_title": "Editar Título",
+  "edit_user": "Editar utilizador",
+  "edited": "Editado",
+  "editor": "Editor",
+  "editor_close_without_save_prompt": "As alterações não serão guardadas",
+  "editor_close_without_save_title": "Fechar editor?",
+  "editor_crop_tool_h2_aspect_ratios": "Relação de aspeto",
+  "editor_crop_tool_h2_rotation": "Rotação",
+  "email": "E-mail",
+  "empty_trash": "Esvaziar reciclagem",
+  "empty_trash_confirmation": "Tem a certeza de que deseja esvaziar a reciclagem? Isto removerá todos os ficheiros da reciclagem do Immich permanentemente.\nNão é possível anular esta ação!",
+  "enable": "Ativar",
+  "enabled": "Ativado",
+  "end_date": "Data final",
+  "error": "Erro",
+  "error_loading_image": "Erro ao carregar a imagem",
+  "error_title": "Erro - Algo correu mal",
+  "errors": {
+    "cannot_navigate_next_asset": "Não foi possível navegar para o próximo ficheiro",
+    "cannot_navigate_previous_asset": "Não foi possível navegar para o ficheiro anterior",
+    "cant_apply_changes": "Não foi possível aplicar as alterações",
+    "cant_change_activity": "Não foi possível {enabled, select, true {desativar} other {ativar}} atividade",
+    "cant_change_asset_favorite": "Não foi possível alterar o favorito deste ficheiro",
+    "cant_change_metadata_assets_count": "Não foi possível alterar os metadados de {count, plural, one {# ficheiro} other {# ficheiros}}",
+    "cant_get_faces": "Não foi possível obter os rostos",
+    "cant_get_number_of_comments": "Não foi possível obter o número de comentários",
+    "cant_search_people": "Não foi possível pesquisar pessoas",
+    "cant_search_places": "Não foi possível pesquisar locais",
+    "cleared_jobs": "Tarefas eliminadas para: {job}",
+    "error_adding_assets_to_album": "Erro ao adicionar ficheiros ao álbum",
+    "error_adding_users_to_album": "Erro ao adicionar utilizador ao álbum",
+    "error_deleting_shared_user": "Erro ao apagar o utilizador partilhado",
+    "error_downloading": "Erro ao transferir {filename}",
+    "error_hiding_buy_button": "Erro ao esconder botão de compra",
+    "error_removing_assets_from_album": "Erro ao eliminar ficheiros do álbum, verifique a consola para mais detalhes",
+    "error_selecting_all_assets": "Erro ao selecionar todos os ficheiros",
+    "exclusion_pattern_already_exists": "Este padrão de exclusão já existe.",
+    "failed_job_command": "Comando {command} falhou para a tarefa: {job}",
+    "failed_to_create_album": "Não foi possível criar álbum",
+    "failed_to_create_shared_link": "Não foi possível criar o link partilhado",
+    "failed_to_edit_shared_link": "Não foi possível editar o link partilhado",
+    "failed_to_get_people": "Não foi possível obter pessoas",
+    "failed_to_keep_this_delete_others": "Ocorreu um erro ao manter este ficheiro e eliminar os outros",
+    "failed_to_load_asset": "Não foi possível ler o ficheiro",
+    "failed_to_load_assets": "Não foi possível ler ficheiros",
+    "failed_to_load_people": "Não foi possível carregar pessoas",
+    "failed_to_remove_product_key": "Não foi possível remover chave de produto",
+    "failed_to_stack_assets": "Não foi possível empilhar os ficheiros",
+    "failed_to_unstack_assets": "Não foi possível desempilhar ficheiros",
+    "import_path_already_exists": "Este caminho de importação já existe.",
+    "incorrect_email_or_password": "Email ou palavra-passe incorretos",
+    "paths_validation_failed": "A validação de {paths, plural, one {# caminho falhou} other {# caminhos falharam}}",
+    "profile_picture_transparent_pixels": "Imagem de perfil não pode ter pixeis transparentes. Por favor amplie e/ou mova a imagem.",
+    "quota_higher_than_disk_size": "Definiu uma quota maior do que o tamanho do disco",
+    "repair_unable_to_check_items": "Não foi possível verificar {count, select, one {um item} other {alguns itens}}",
+    "unable_to_add_album_users": "Não foi possível adicionar utilizadores ao álbum",
+    "unable_to_add_assets_to_shared_link": "Não foi possível adicionar os ficheiros ao link partilhado",
+    "unable_to_add_comment": "Não foi possível adicionar o comentário",
+    "unable_to_add_exclusion_pattern": "Não foi possível adicionar o padrão de exclusão",
+    "unable_to_add_import_path": "Não foi possível adicionar o caminho de importação",
+    "unable_to_add_partners": "Não foi possível adicionar parceiros",
+    "unable_to_add_remove_archive": "Não foi possível {archived, select, true {remover o ficheiro de} other {adicionar o ficheiro}}",
+    "unable_to_add_remove_favorites": "Não foi possível {favorite, select, true {adicionar ficheiro aos} other {remover ficheiro dos}} favoritos",
+    "unable_to_archive_unarchive": "Não foi possível {archived, select, true {arquivar} other {desarquivar}}",
+    "unable_to_change_album_user_role": "Não foi possível alterar a permissão do utilizador no álbum",
+    "unable_to_change_date": "Não foi possível alterar a data",
+    "unable_to_change_favorite": "Não foi possível mudar o favorito do ficheiro",
+    "unable_to_change_location": "Não foi possível alterar a localização",
+    "unable_to_change_password": "Não foi possível alterar a palavra-passe",
+    "unable_to_change_visibility": "Não é possível alterar a visibilidade de {count, plural, one {# pessoa} other {# pessoas}}",
+    "unable_to_complete_oauth_login": "Não foi possível completar o início de sessão com OAuth",
+    "unable_to_connect": "Não é possível ligar",
+    "unable_to_connect_to_server": "Não foi possível ligar ao servidor",
+    "unable_to_copy_to_clipboard": "Não foi possível copiar para a área de transferência, certifique-se de que está a aceder à pagina através de https",
+    "unable_to_create_admin_account": "Não foi possível criar conta de administrador",
+    "unable_to_create_api_key": "Não foi possível criar uma nova Chave de API",
+    "unable_to_create_library": "Não foi possível criar a biblioteca",
+    "unable_to_create_user": "Não foi possível criar o utilizador",
+    "unable_to_delete_album": "Não foi possível eliminar o álbum",
+    "unable_to_delete_asset": "Não foi possível eliminar o ficheiro",
+    "unable_to_delete_assets": "Erro ao eliminar ficheiros",
+    "unable_to_delete_exclusion_pattern": "Não foi possível eliminar o padrão de exclusão",
+    "unable_to_delete_import_path": "Não foi possível eliminar o caminho de importação",
+    "unable_to_delete_shared_link": "Não foi possível eliminar o link compartilhado",
+    "unable_to_delete_user": "Não foi possível eliminar o utilizador",
+    "unable_to_download_files": "Não foi possível transferir ficheiros",
+    "unable_to_edit_exclusion_pattern": "Não foi possível editar o padrão de exclusão",
+    "unable_to_edit_import_path": "Não foi possível editar o caminho de importação",
+    "unable_to_empty_trash": "Não foi possível esvaziar a reciclagem",
+    "unable_to_enter_fullscreen": "Não foi possível entrar em modo de ecrã inteiro",
+    "unable_to_exit_fullscreen": "Não foi possível sair do modo de ecrã inteiro",
+    "unable_to_get_comments_number": "Não foi possível obter número de comentários",
+    "unable_to_get_shared_link": "Não foi possível obter link partilhado",
+    "unable_to_hide_person": "Não foi possível esconder a pessoa",
+    "unable_to_link_motion_video": "Não foi possível relacionar o video animado",
+    "unable_to_link_oauth_account": "Não foi possível associar a conta OAuth",
+    "unable_to_load_album": "Não foi possível carregar o álbum",
+    "unable_to_load_asset_activity": "Não foi possível carregar a atividade do ficheiro",
+    "unable_to_load_items": "Não foi possível carregar os itens",
+    "unable_to_load_liked_status": "Não foi possível carregar o estado de gostos",
+    "unable_to_log_out_all_devices": "Não foi possível terminar a sessão em todos os dispositivos",
+    "unable_to_log_out_device": "Não foi possível terminar a sessão no dispositivo",
+    "unable_to_login_with_oauth": "Não foi possível iniciar sessão com OAuth",
+    "unable_to_play_video": "Não foi possível reproduzir o vídeo",
+    "unable_to_reassign_assets_existing_person": "Não foi possível reatribuir ficheiros para {name, select, null {uma pessoa existente} other {{name}}}",
+    "unable_to_reassign_assets_new_person": "Não foi possível reatribuir os ficheiros a uma nova pessoa",
+    "unable_to_refresh_user": "Não foi possível recarregar o utilizador",
+    "unable_to_remove_album_users": "Não foi possível remover utilizador do álbum",
+    "unable_to_remove_api_key": "Não foi possível remover a Chave de API",
+    "unable_to_remove_assets_from_shared_link": "Não foi possível remover os ficheiros do link partilhado",
+    "unable_to_remove_deleted_assets": "Não foi possível remover ficheiros indisponíveis",
+    "unable_to_remove_library": "Não foi possível remover a biblioteca",
+    "unable_to_remove_partner": "Não foi possível remover parceiro",
+    "unable_to_remove_reaction": "Não foi possível remover a reação",
+    "unable_to_repair_items": "Não foi possível reparar os itens",
+    "unable_to_reset_password": "Não foi possível redefinir a palavra-passe",
+    "unable_to_resolve_duplicate": "Não foi possível resolver as duplicidades",
+    "unable_to_restore_assets": "Não foi possível restaurar ficheiros",
+    "unable_to_restore_trash": "Não foi possível restaurar itens da reciclagem",
+    "unable_to_restore_user": "Não foi possível restaurar utilizador",
+    "unable_to_save_album": "Não foi possível guardar o álbum",
+    "unable_to_save_api_key": "Não foi possível guardar a Chave de API",
+    "unable_to_save_date_of_birth": "Não foi possível guardar a data de nascimento",
+    "unable_to_save_name": "Não foi possível guardar o nome",
+    "unable_to_save_profile": "Não foi possível guardar o perfil",
+    "unable_to_save_settings": "Não foi possível guardar as definições",
+    "unable_to_scan_libraries": "Não foi possível analisar as bibliotecas",
+    "unable_to_scan_library": "Não foi possível analisar a biblioteca",
+    "unable_to_set_feature_photo": "Não foi possível definir a foto de destaque",
+    "unable_to_set_profile_picture": "Não foi possível definir a foto de perfil",
+    "unable_to_submit_job": "Não foi possível enviar a tarefa",
+    "unable_to_trash_asset": "Não foi possível enviar o ficheiro para a reciclagem",
+    "unable_to_unlink_account": "Não foi possível desvincular conta",
+    "unable_to_unlink_motion_video": "Não foi possível remover a relação com o video animado",
+    "unable_to_update_album_cover": "Não foi possível atualizar a capa do álbum",
+    "unable_to_update_album_info": "Não foi possível atualizar informações do álbum",
+    "unable_to_update_library": "Não foi possível atualizar a biblioteca",
+    "unable_to_update_location": "Não foi possível atualizar a localização",
+    "unable_to_update_settings": "Não foi possível atualizar as definições",
+    "unable_to_update_timeline_display_status": "Não foi possível atualizar o modo de visualização da linha do tempo",
+    "unable_to_update_user": "Não foi possível atualizar o utilizador",
+    "unable_to_upload_file": "Não foi possível carregar o ficheiro"
+  },
+  "exif": "Exif",
+  "exit_slideshow": "Sair da apresentação",
+  "expand_all": "Expandir tudo",
+  "expire_after": "Expira depois de",
+  "expired": "Expirou",
+  "expires_date": "Expira em {date}",
+  "explore": "Explorar",
+  "explorer": "Explorador",
+  "export": "Exportar",
+  "export_as_json": "Exportar como JSON",
+  "extension": "Extensão",
+  "external": "Externo",
+  "external_libraries": "Bibliotecas externas",
+  "face_unassigned": "Sem atribuição",
+  "failed_to_load_assets": "Falha ao carregar ficheiros",
+  "favorite": "Favorito",
+  "favorite_or_unfavorite_photo": "Marcar ou desmarcar a foto como favorita",
+  "favorites": "Favoritos",
+  "feature_photo_updated": "Foto principal atualizada",
+  "features": "Funcionalidades",
+  "features_setting_description": "Configurar as funcionalidades da aplicação",
+  "file_name": "Nome do ficheiro",
+  "file_name_or_extension": "Nome do ficheiro ou extensão",
+  "filename": "Nome do ficheiro",
+  "filetype": "Tipo de ficheiro",
+  "filter_people": "Filtrar pessoas",
+  "find_them_fast": "Encontre-as mais rapidamente pelo nome numa pesquisa",
+  "fix_incorrect_match": "Corrigir correspondência incorreta",
+  "folders": "Pastas",
+  "folders_feature_description": "Navegar na vista de pastas por fotos e vídeos no sistema de ficheiros",
+  "forward": "Para a frente",
+  "general": "Geral",
+  "get_help": "Obter Ajuda",
+  "getting_started": "Primeiros Passos",
+  "go_back": "Regressar",
+  "go_to_search": "Ir para a pesquisa",
+  "group_albums_by": "Agrupar álbuns por...",
+  "group_no": "Sem agrupamento",
+  "group_owner": "Agrupar por dono",
+  "group_year": "Agrupar por ano",
+  "has_quota": "Tem quota",
+  "hi_user": "Olá {name} ({email})",
+  "hide_all_people": "Ocultar todas as pessoas",
+  "hide_gallery": "Ocultar galeria",
+  "hide_named_person": "Ocultar pessoa {name}",
+  "hide_password": "Ocultar palavra-passe",
+  "hide_person": "Ocultar pessoa",
+  "hide_unnamed_people": "Ocultar pessoas sem nome",
+  "host": "Host",
+  "hour": "Hora",
+  "image": "Imagem",
+  "image_alt_text_date": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {date}",
+  "image_alt_text_date_1_person": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1} em {date}",
+  "image_alt_text_date_2_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1} e {person2} em {date}",
+  "image_alt_text_date_3_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1}, {person2}, e {person3} em {date}",
+  "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1}, {person2}, e outras {additionalCount, number} pessoas em {date}",
+  "image_alt_text_date_place": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} em {date}",
+  "image_alt_text_date_place_1_person": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1} em {date}",
+  "image_alt_text_date_place_2_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1} e {person2} em {date}",
+  "image_alt_text_date_place_3_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1}, {person2}, e {person3} em {date}",
+  "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1}, {person2}, e outras {additionalCount, number} pessoas em {date}",
+  "immich_logo": "Logotipo do Immich",
+  "immich_web_interface": "Interface Web do Immich",
+  "import_from_json": "Importar a partir de JSON",
+  "import_path": "Caminho de importação",
+  "in_albums": "Em {count, plural, one {# álbum} other {# álbuns}}",
+  "in_archive": "Arquivado",
+  "include_archived": "Incluir arquivados",
+  "include_shared_albums": "Incluir álbuns partilhados",
+  "include_shared_partner_assets": "Incluir ficheiros partilhados por parceiros",
+  "individual_share": "Partilha individual",
+  "info": "Informações",
+  "interval": {
+    "day_at_onepm": "Todos os dias, às 13:00",
+    "hours": "A cada {hours, plural, one {hora} other {{hours, number} horas}}",
+    "night_at_midnight": "Todas as noites, à meia noite",
+    "night_at_twoam": "Todas as noites, às 02:00"
+  },
+  "invite_people": "Convidar Pessoas",
+  "invite_to_album": "Convidar para o álbum",
+  "items_count": "{count, plural, one {item #} other {itens #}}",
+  "jobs": "Tarefas",
+  "keep": "Manter",
+  "keep_all": "Manter Todos",
+  "keep_this_delete_others": "Manter este ficheiro, eliminar os outros",
+  "kept_this_deleted_others": "Foi mantido ficheiro e {count, plural, one {eliminado # outro} other {eliminados # outros}}",
+  "keyboard_shortcuts": "Atalhos do teclado",
+  "language": "Idioma",
+  "language_setting_description": "Selecione o seu Idioma preferido",
+  "last_seen": "Visto pela ultima vez",
+  "latest_version": "Versão mais recente",
+  "latitude": "Latitude",
+  "leave": "Sair",
+  "let_others_respond": "Permitir respostas",
+  "level": "Nível",
+  "library": "Biblioteca",
+  "library_options": "Opções da biblioteca",
+  "light": "Claro",
+  "like_deleted": "Gosto removido",
+  "link_motion_video": "Relacionar video animado",
+  "link_options": "Opções do Link",
+  "link_to_oauth": "Link do OAuth",
+  "linked_oauth_account": "Conta OAuth Associada",
+  "list": "Lista",
+  "loading": "A Carregar",
+  "loading_search_results_failed": "Não foi possível carregar os resultados da pesquisa",
+  "log_out": "Sair",
+  "log_out_all_devices": "Terminar a sessão de todos os dispositivos",
+  "logged_out_all_devices": "Sessão terminada em todos os dispositivos",
+  "logged_out_device": "Sessão terminada no dispositivo",
+  "login": "Iniciar sessão",
+  "login_has_been_disabled": "Início de sessão foi desativado.",
+  "logout_all_device_confirmation": "Tem a certeza de que deseja terminar a sessão em todos os dispositivos?",
+  "logout_this_device_confirmation": "Tem a certeza de que deseja terminar a sessão deste dispositivo?",
+  "longitude": "Longitude",
+  "look": "Estilo",
+  "loop_videos": "Repetir vídeos",
+  "loop_videos_description": "Ativar para repetir os vídeos automaticamente durante a exibição.",
+  "main_branch_warning": "Está a utilizar uma versão de desenvolvimento, recomendamos vivamente que utilize uma versão estável!",
+  "make": "Marca",
+  "manage_shared_links": "Gerir links partilhados",
+  "manage_sharing_with_partners": "Gerir partilha com parceiros",
+  "manage_the_app_settings": "Gerir definições da aplicação",
+  "manage_your_account": "Gerir a sua conta",
+  "manage_your_api_keys": "Gerir as suas Chaves de API",
+  "manage_your_devices": "Gerir os seus dispositivos com sessão iniciada",
+  "manage_your_oauth_connection": "Gerir a sua ligação ao OAuth",
+  "map": "Mapa",
+  "map_marker_for_images": "Marcador no mapa para fotos tiradas em {city}, {country}",
+  "map_marker_with_image": "Marcador de mapa com imagem",
+  "map_settings": "Definições do mapa",
+  "matches": "Correspondências",
+  "media_type": "Tipo de média",
+  "memories": "Memórias",
+  "memories_setting_description": "Gerir o que vê nas suas memórias",
+  "memory": "Memória",
+  "memory_lane_title": "Memórias {title}",
+  "menu": "Menu",
+  "merge": "Unir",
+  "merge_people": "Unir pessoas",
+  "merge_people_limit": "Só é possível unir até 5 rostos de cada vez",
+  "merge_people_prompt": "Tem a certeza de que deseja unir estas pessoas? Esta ação é irreversível.",
+  "merge_people_successfully": "Pessoas unidas com sucesso",
+  "merged_people_count": "Unidas {count, plural, one {# pessoa} other {# pessoas}}",
+  "minimize": "Minimizar",
+  "minute": "Minuto",
+  "missing": "Em falta",
+  "model": "Modelo",
+  "month": "Mês",
+  "more": "Mais",
+  "moved_to_trash": "Enviado para a reciclagem",
+  "my_albums": "Os meus álbuns",
+  "name": "Nome",
+  "name_or_nickname": "Nome ou alcunha",
+  "never": "Nunca",
+  "new_album": "Novo Álbum",
+  "new_api_key": "Nova Chave de API",
+  "new_password": "Nova palavra-passe",
+  "new_person": "Nova Pessoa",
+  "new_user_created": "Novo utilizador criado",
+  "new_version_available": "NOVA VERSÃO DISPONÍVEL",
+  "newest_first": "Mais recente primeiro",
+  "next": "Avançar",
+  "next_memory": "Próxima memória",
+  "no": "Não",
+  "no_albums_message": "Crie um álbum para organizar as suas fotos e vídeos",
+  "no_albums_with_name_yet": "Parece que ainda não tem nenhum álbum com este nome.",
+  "no_albums_yet": "Parece que ainda não tem nenhum álbum.",
+  "no_archived_assets_message": "Arquive fotos e vídeos para os ocultar da sua visualização de fotos",
+  "no_assets_message": "FAÇA CLIQUE PARA CARREGAR A SUA PRIMEIRA FOTO",
+  "no_duplicates_found": "Nenhum item duplicado foi encontrado.",
+  "no_exif_info_available": "Sem informações exif disponíveis",
+  "no_explore_results_message": "Carregue mais fotos para explorar a sua coleção.",
+  "no_favorites_message": "Adicione aos favoritos para encontrar as suas melhores fotos e vídeos rapidamente",
+  "no_libraries_message": "Crie uma biblioteca externa para ver as suas fotos e vídeos",
+  "no_name": "Sem nome",
+  "no_places": "Sem lugares",
+  "no_results": "Sem resultados",
+  "no_results_description": "Tente um sinónimo ou uma palavra-chave mais comum",
+  "no_shared_albums_message": "Crie um álbum para partilhar fotos e vídeos com pessoas na sua rede",
+  "not_in_any_album": "Não está em nenhum álbum",
+  "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar o Rótulo de Armazenamento a ficheiros carregados anteriormente, execute o",
+  "note_unlimited_quota": "Nota: Escreva 0 para quota ilimitada",
+  "notes": "Notas",
+  "notification_toggle_setting_description": "Ativar notificações por e-mail",
+  "notifications": "Notificações",
+  "notifications_setting_description": "Gerir notificações",
+  "oauth": "OAuth",
+  "official_immich_resources": "Recursos oficiais do Immich",
+  "offline": "Offline",
+  "offline_paths": "Caminhos offline",
+  "offline_paths_description": "Estes resultados podem ser devidos a ficheiros eliminados manualmente e que não fazem parte de uma biblioteca externa.",
+  "ok": "Ok",
+  "oldest_first": "Mais antigo primeiro",
+  "onboarding": "Integração",
+  "onboarding_privacy_description": "As seguintes funcionalidades opcionais dependem de serviços externos e podem ser desativados a qualquer momento nas definições de administração.",
+  "onboarding_theme_description": "Escolha um tema de cor para sua instância. Pode alterar isto mais tarde nas suas definições.",
+  "onboarding_welcome_description": "Vamos configurar a sua instância com algumas definições comuns.",
+  "onboarding_welcome_user": "Bem-vindo(a), {user}",
+  "online": "Online",
+  "only_favorites": "Apenas favoritos",
+  "open_in_map_view": "Abrir na visualização de mapa",
+  "open_in_openstreetmap": "Abrir no OpenStreetMap",
+  "open_the_search_filters": "Abrir os filtros de pesquisa",
+  "options": "Opções",
+  "or": "ou",
+  "organize_your_library": "Organizar a sua biblioteca",
+  "original": "original",
+  "other": "Outro",
+  "other_devices": "Outros dispositivos",
+  "other_variables": "Outras variáveis",
+  "owned": "Seu",
+  "owner": "Dono",
+  "partner": "Parceiro",
+  "partner_can_access": "{partner} pode aceder",
+  "partner_can_access_assets": "Todas as suas fotos e vídeos, exceto os Arquivados ou Eliminados",
+  "partner_can_access_location": "A localização onde as fotos foram tiradas",
+  "partner_sharing": "Partilha com Parceiro",
+  "partners": "Parceiros",
+  "password": "Palavra-passe",
+  "password_does_not_match": "As palavras-passe não condizem",
+  "password_required": "A palavra-passe é obrigatória",
+  "password_reset_success": "Palavra-passe redefinida com sucesso",
+  "past_durations": {
+    "days": "{days, plural, one {Último dia} other {# últimos dias}}",
+    "hours": "Últimas {hours, plural, one {horas} other {# horas}}",
+    "years": "{years, plural, one {Último ano} other {Últimos # anos}}"
+  },
+  "path": "Caminho",
+  "pattern": "Padrão",
+  "pause": "Pausa",
+  "pause_memories": "Pausar memórias",
+  "paused": "Em Pausa",
+  "pending": "Pendente",
+  "people": "Pessoas",
+  "people_edits_count": "{count, plural, one {# pessoa editada} other {# pessoas editadas}}",
+  "people_feature_description": "Navegar por fotos e vídeos agrupados por pessoas",
+  "people_sidebar_description": "Exibir o link Pessoas na barra lateral",
+  "permanent_deletion_warning": "Aviso de eliminação permanente",
+  "permanent_deletion_warning_setting_description": "Exibir um aviso ao eliminar ficheiros de forma permanente",
+  "permanently_delete": "Eliminar permanentemente",
+  "permanently_delete_assets_count": "Eliminar permanentemente {count, plural, one {ficheiro} other {ficheiros}}",
+  "permanently_delete_assets_prompt": "Tem a certeza de que deseja eliminar permanentemente {count, plural, one {este ficheiro?} other {estes <b>#</b> ficheiros?}} Esta ação também removerá {count, plural, one {isto do álbum} other {isto dos álbuns}}.",
+  "permanently_deleted_asset": "Ficheiro eliminado permanentemente",
+  "permanently_deleted_assets_count": "{count, plural, one {# Ficheiro eliminado} other {# Ficheiros eliminados}} permanentemente",
+  "person": "Pessoa",
+  "person_hidden": "{name}{hidden, select, true { (oculto)} other {}}",
+  "photo_shared_all_users": "Parece que partilhou as suas fotos com todos os utilizadores ou não tem nenhum utilizador para partilhar.",
+  "photos": "Fotos",
+  "photos_and_videos": "Fotos & Vídeos",
+  "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}",
+  "photos_from_previous_years": "Fotos de anos anteriores",
+  "pick_a_location": "Selecione uma localização",
+  "place": "Lugar",
+  "places": "Lugares",
+  "play": "Reproduzir",
+  "play_memories": "Reproduzir memórias",
+  "play_motion_photo": "Reproduzir foto em movimento",
+  "play_or_pause_video": "Reproduzir ou Pausar vídeo",
+  "port": "Porta",
+  "preset": "Predefinição",
+  "preview": "Pré-visualizar",
+  "previous": "Anterior",
+  "previous_memory": "Memória anterior",
+  "previous_or_next_photo": "Foto anterior ou próxima",
+  "primary": "Primário",
+  "privacy": "Privacidade",
+  "profile_image_of_user": "Imagem de perfil de {user}",
+  "profile_picture_set": "Foto de perfil definida.",
+  "public_album": "Álbum público",
+  "public_share": "Partilhar Publicamente",
+  "purchase_account_info": "Apoiante",
+  "purchase_activated_subtitle": "Agradecemos por apoiar o Immich e software de código aberto",
+  "purchase_activated_time": "Ativado em {date, date}",
+  "purchase_activated_title": "A sua chave foi ativada com sucesso",
+  "purchase_button_activate": "Ativar",
+  "purchase_button_buy": "Comprar",
+  "purchase_button_buy_immich": "Comprar Immich",
+  "purchase_button_never_show_again": "Não mostrar de novo",
+  "purchase_button_reminder": "Relembrar-me daqui a 30 dias",
+  "purchase_button_remove_key": "Remover chave",
+  "purchase_button_select": "Selecionar",
+  "purchase_failed_activation": "Não foi possível ativar! Verifique o seu e-mail para obter a chave de produto correta!",
+  "purchase_individual_description_1": "Para uma pessoa individual",
+  "purchase_individual_description_2": "Status de apoiante",
+  "purchase_individual_title": "Particular",
+  "purchase_input_suggestion": "Tem uma chave de produto? Insira a chave abaixo",
+  "purchase_license_subtitle": "Compre o Immich para apoiar o desenvolvimento contínuo do serviço",
+  "purchase_lifetime_description": "Compra vitalícia",
+  "purchase_option_title": "OPÇÕES DE COMPRA",
+  "purchase_panel_info_1": "O desenvolvimento do Immich requer muito tempo e esforço, e temos engenheiros a tempo inteiro a trabalhar nele para melhorá-lo quanto possível. A nossa missão é para que o software de código aberto e práticas de negócio éticas se tornem numa fonte de rendimento sustentável para os desenvolvedores e criar um ecossistema que respeite a privacidade dos utilizadores e que ofereça alternativas reais a serviços cloud explorativos.",
+  "purchase_panel_info_2": "Como estamos comprometidos a não adicionar acesso pago, esta compra não lhe dará acesso a nenhuma funcionalidade adicional do Immich. Contamos com utilizadores como você para dar suporte ao desenvolvimento contínuo do Immich.",
+  "purchase_panel_title": "Apoie o projeto",
+  "purchase_per_server": "Por servidor",
+  "purchase_per_user": "Por utilizador",
+  "purchase_remove_product_key": "Remover chave de produto",
+  "purchase_remove_product_key_prompt": "Tem a certeza de que deseja remover a chave do produto?",
+  "purchase_remove_server_product_key": "Remover chave do produto do servidor",
+  "purchase_remove_server_product_key_prompt": "Tem a certeza de que deseja remover a chave do produto do servidor?",
+  "purchase_server_description_1": "Para o servidor inteiro",
+  "purchase_server_description_2": "Status de apoiante",
+  "purchase_server_title": "Servidor",
+  "purchase_settings_server_activated": "A chave de produto do servidor é gerida pelo administrador",
+  "rating": "Classificação por estrelas",
+  "rating_clear": "Limpar classificação",
+  "rating_count": "{count, plural, one {# estrela} other {# estrelas}}",
+  "rating_description": "Mostrar a classificação EXIF no painel de informações",
+  "reaction_options": "Opções de reação",
+  "read_changelog": "Ler Novidades",
+  "reassign": "Reatribuir",
+  "reassigned_assets_to_existing_person": "Reatribuir {count, plural, one {# ficheiro} other {# ficheiros}} para {name, select, null {uma pessoa existente} other {{name}}}",
+  "reassigned_assets_to_new_person": "Reatribuído {count, plural, one {# ficheiro} other {# ficheiros}} a uma nova pessoa",
+  "reassing_hint": "Atribuir ficheiros selecionados a uma pessoa existente",
+  "recent": "Recentes",
+  "recent-albums": "Álbuns recentes",
+  "recent_searches": "Pesquisas recentes",
+  "refresh": "Atualizar",
+  "refresh_encoded_videos": "Atualizar vídeos codificados",
+  "refresh_faces": "Atualizar rostos",
+  "refresh_metadata": "Atualizar metadados",
+  "refresh_thumbnails": "Atualizar miniaturas",
+  "refreshed": "Atualizado",
+  "refreshes_every_file": "Recarrega todos os ficheiros já existentes e novos",
+  "refreshing_encoded_video": "A atualizar vídeo codificado",
+  "refreshing_faces": "A atualizar rostos",
+  "refreshing_metadata": "A atualizar metadados",
+  "regenerating_thumbnails": "A atualizar miniaturas",
+  "remove": "Remover",
+  "remove_assets_album_confirmation": "Tem a certeza de que deseja remover {count, plural, one {# ficheiro} other {# ficheiros}} do álbum?",
+  "remove_assets_shared_link_confirmation": "Tem certeza de que deseja remover {count, plural, one {# ficheiro} other {# ficheiros}} deste link partilhado?",
+  "remove_assets_title": "Remover ficheiros?",
+  "remove_custom_date_range": "Remover intervalo de datas personalizado",
+  "remove_deleted_assets": "Remover ficheiros indisponíveis",
+  "remove_from_album": "Remover do álbum",
+  "remove_from_favorites": "Remover dos favoritos",
+  "remove_from_shared_link": "Remover do link partilhado",
+  "remove_url": "Remover URL",
+  "remove_user": "Remover utilizador",
+  "removed_api_key": "Foi removida a Chave de API: {name}",
+  "removed_from_archive": "Removido do arquivo",
+  "removed_from_favorites": "Removido dos favoritos",
+  "removed_from_favorites_count": "{count, plural, other {Removidos #}} dos favoritos",
+  "removed_tagged_assets": "Removida a etiqueta de {count, plural, one {# ficheiro} other {# ficheiros}}",
+  "rename": "Mudar o nome",
+  "repair": "Reparar",
+  "repair_no_results_message": "Ficheiros em falta ou não monitorizados irão aparecer aqui",
+  "replace_with_upload": "Substituir pelo ficheiro carregado",
+  "repository": "Repositório",
+  "require_password": "Proteger com palavra-passe",
+  "require_user_to_change_password_on_first_login": "Obrigar utilizador a alterar a palavra-passe após o primeiro início de sessão",
+  "reset": "Redefinir",
+  "reset_password": "Redefinir palavra-passe",
+  "reset_people_visibility": "Redefinir pessoas ocultas",
+  "reset_to_default": "Repor predefinições",
+  "resolve_duplicates": "Resolver itens duplicados",
+  "resolved_all_duplicates": "Todos os itens duplicados resolvidos",
+  "restore": "Restaurar",
+  "restore_all": "Restaurar tudo",
+  "restore_user": "Restaurar utilizador",
+  "restored_asset": "Ficheiro restaurado",
+  "resume": "Continuar",
+  "retry_upload": "Tentar carregar novamente",
+  "review_duplicates": "Rever itens duplicados",
+  "role": "Função",
+  "role_editor": "Editor",
+  "role_viewer": "Visualizador",
+  "save": "Guardar",
+  "saved_api_key": "Chave de API guardada",
+  "saved_profile": "Perfil guardado",
+  "saved_settings": "Definições guardadas",
+  "say_something": "Diga alguma coisa",
+  "scan_all_libraries": "Analisar todas as bibliotecas",
+  "scan_library": "Analisar",
+  "scan_settings": "Opções de análise",
+  "scanning_for_album": "A analisar por álbum...",
+  "search": "Pesquisar",
+  "search_albums": "Pesquisar álbuns",
+  "search_by_context": "Pesquisar por contexto",
+  "search_by_filename": "Pesquisar por nome de ficheiro ou extensão",
+  "search_by_filename_example": "por exemplo, IMG_1234.JPG ou PNG",
+  "search_camera_make": "Pesquisar por marca da câmara...",
+  "search_camera_model": "Pesquisar por modelo da câmara...",
+  "search_city": "Pesquisar cidade...",
+  "search_country": "Pesquisar país...",
+  "search_for_existing_person": "Pesquisar por pessoas existentes",
+  "search_no_people": "Sem pessoas",
+  "search_no_people_named": "Nenhuma pessoa chamada \"{name}\"",
+  "search_options": "Opções de pesquisa",
+  "search_people": "Pesquisar pessoas",
+  "search_places": "Pesquisar lugares",
+  "search_settings": "Definições de pesquisa",
+  "search_state": "Pesquisar estado/distrito...",
+  "search_tags": "Pesquisar etiquetas...",
+  "search_timezone": "Pesquisar fuso horário...",
+  "search_type": "Tipo de pesquisa",
+  "search_your_photos": "Pesquisar fotos",
+  "searching_locales": "A pesquisar Lugares....",
+  "second": "Segundo",
+  "see_all_people": "Ver todas as pessoas",
+  "select_album_cover": "Escolher capa do álbum",
+  "select_all": "Selecionar todos",
+  "select_all_duplicates": "Selecionar todos os itens duplicados",
+  "select_avatar_color": "Selecionar cor do avatar",
+  "select_face": "Selecionar rosto",
+  "select_featured_photo": "Selecionar foto principal",
+  "select_from_computer": "Selecionar a partir do computador",
+  "select_keep_all": "Selecionar manter todos",
+  "select_library_owner": "Selecionar o dono da biblioteca",
+  "select_new_face": "Selecionar novo rosto",
+  "select_photos": "Selecionar fotos",
+  "select_trash_all": "Selecionar todos para reciclagem",
+  "selected": "Selecionados",
+  "selected_count": "{count, plural, other {# selecionados}}",
+  "send_message": "Enviar mensagem",
+  "send_welcome_email": "Enviar E-mail de boas vindas",
+  "server_offline": "Servidor Offline",
+  "server_online": "Servidor Online",
+  "server_stats": "Estado do servidor",
+  "server_version": "Versão do servidor",
+  "set": "Definir",
+  "set_as_album_cover": "Definir como capa do álbum",
+  "set_as_profile_picture": "Definir como foto de perfil",
+  "set_date_of_birth": "Definir data de nascimento",
+  "set_profile_picture": "Definir foto de perfil",
+  "set_slideshow_to_fullscreen": "Apresentação em ecrã inteiro",
+  "settings": "Definições",
+  "settings_saved": "Definições guardadas",
+  "share": "Partilhar",
+  "shared": "Partilhado",
+  "shared_by": "Partilhado por",
+  "shared_by_user": "Partilhado por {user}",
+  "shared_by_you": "Partilhado por si",
+  "shared_from_partner": "Fotos de {partner}",
+  "shared_link_options": "Opções de link partilhado",
+  "shared_links": "Links partilhados",
+  "shared_photos_and_videos_count": "{assetCount, plural, other {# Fotos & videos partilhados.}}",
+  "shared_with_partner": "Partilhado com {partner}",
+  "sharing": "Partilha",
+  "sharing_enter_password": "Por favor, insira a palavra-passe para ver esta página.",
+  "sharing_sidebar_description": "Exibe o link para Partilhar na barra lateral",
+  "shift_to_permanent_delete": "Pressione ⇧ para eliminar o ficheiro permanentemente",
+  "show_album_options": "Exibir opções do álbum",
+  "show_albums": "Mostrar álbuns",
+  "show_all_people": "Mostrar todas as pessoas",
+  "show_and_hide_people": "Mostrar & ocultar pessoas",
+  "show_file_location": "Exibir localização do ficheiro",
+  "show_gallery": "Exibir galeria",
+  "show_hidden_people": "Exibir pessoas ocultadas",
+  "show_in_timeline": "Exibir na linha do tempo",
+  "show_in_timeline_setting_description": "Exibe fotos e vídeos deste utilizador na sua linha do tempo",
+  "show_keyboard_shortcuts": "Exibir atalhos do teclado",
+  "show_metadata": "Mostrar metadados",
+  "show_or_hide_info": "Exibir ou ocultar informações",
+  "show_password": "Mostrar palavra-passe",
+  "show_person_options": "Exibir opções da pessoa",
+  "show_progress_bar": "Exibir barra de progresso",
+  "show_search_options": "Exibir opções de pesquisa",
+  "show_slideshow_transition": "Mostrar transições no Modo de Apresentação",
+  "show_supporter_badge": "Emblema de apoiante",
+  "show_supporter_badge_description": "Mostrar um emblema de apoiante",
+  "shuffle": "Aleatório",
+  "sidebar": "Barra lateral",
+  "sidebar_display_description": "Mostrar um link para a vista na barra lateral",
+  "sign_out": "Terminar sessão",
+  "sign_up": "Criar conta",
+  "size": "Tamanho",
+  "skip_to_content": "Saltar para o conteúdo",
+  "skip_to_folders": "Saltar para pastas",
+  "skip_to_tags": "Saltar para as etiquetas",
+  "slideshow": "Apresentação",
+  "slideshow_settings": "Definições de apresentação",
+  "sort_albums_by": "Ordenar álbuns por...",
+  "sort_created": "Data de criação",
+  "sort_items": "Número de itens",
+  "sort_modified": "Data de modificação",
+  "sort_oldest": "Foto mais antiga",
+  "sort_recent": "Foto mais recente",
+  "sort_title": "Título",
+  "source": "Fonte",
+  "stack": "Empilhar",
+  "stack_duplicates": "Empilhar itens duplicados",
+  "stack_select_one_photo": "Selecione uma foto principal para a pilha",
+  "stack_selected_photos": "Empilhar fotos selecionadas",
+  "stacked_assets_count": "Empilhado {count, plural, one {# ficheiro} other {# ficheiros}}",
+  "stacktrace": "Stacktrace",
+  "start": "Iniciar",
+  "start_date": "Data de início",
+  "state": "Estado",
+  "status": "Estado",
+  "stop_motion_photo": "Parar foto em movimento",
+  "stop_photo_sharing": "Deixar de partilhar as suas fotos?",
+  "stop_photo_sharing_description": "{partner} deixará de ter acesso às suas fotos.",
+  "stop_sharing_photos_with_user": "Deixar de partilhar as fotos com este utilizador",
+  "storage": "Espaço de armazenamento",
+  "storage_label": "Rótulo de Armazenamento",
+  "storage_usage": "Utilizado {used} de {available}",
+  "submit": "Enviar",
+  "suggestions": "Sugestões",
+  "sunrise_on_the_beach": "Nascer do sol na praia",
+  "support": "Apoio",
+  "support_and_feedback": "Apoio e feedback",
+  "support_third_party_description": "A sua instalação do Immich foi empacotada por terceiros. Quaisquer problemas que possa vir a ter poderão ser causados por esse pacote, por isso, em primeiro lugar, relate problemas aos criadores desse pacote utilizando os links abaixo.",
+  "swap_merge_direction": "Alternar direção da união",
+  "sync": "Sincronizar",
+  "tag": "Etiqueta",
+  "tag_assets": "Etiquetar ficheiros",
+  "tag_created": "Criada a etiqueta {tag}",
+  "tag_feature_description": "A mostrar fotos e videos agrupados por tópicos lógicos de etiquetas",
+  "tag_not_found_question": "Não consegue encontrar a etiqueta? <link>Crie uma nova etiqueta.</link>",
+  "tag_updated": "Atualizada a etiqueta: {tag}",
+  "tagged_assets": "Etiquetado {count, plural, one {# ficheiros} other {# ficheiros}}",
+  "tags": "Etiquetas",
+  "template": "Modelo",
+  "theme": "Tema",
+  "theme_selection": "Selecionar tema",
+  "theme_selection_description": "Definir automaticamente o tema como claro ou escuro com base na preferência do sistema do seu navegador",
+  "they_will_be_merged_together": "Eles serão unidos",
+  "third_party_resources": "Recursos de terceiros",
+  "time_based_memories": "Memórias baseadas no tempo",
+  "timeline": "Linha de tempo",
+  "timezone": "Fuso horário",
+  "to_archive": "Arquivar",
+  "to_change_password": "Alterar palavra-passe",
+  "to_favorite": "Favorito",
+  "to_login": "Iniciar Sessão",
+  "to_parent": "Subir um nível",
+  "to_trash": "Reciclagem",
+  "toggle_settings": "Alternar configurações",
+  "toggle_theme": "Ativar modo escuro",
+  "total": "Total",
+  "total_usage": "Total utilizado",
+  "trash": "Reciclagem",
+  "trash_all": "Mover todos para a reciclagem",
+  "trash_count": "Reciclar {count, number}",
+  "trash_delete_asset": "Eliminar ficheiro",
+  "trash_no_results_message": "Fotos e vídeos enviados para a reciclagem aparecem aqui.",
+  "trashed_items_will_be_permanently_deleted_after": "Os itens da reciclagem são eliminados permanentemente após {days, plural, one {# dia} other {# dias}}.",
+  "type": "Tipo",
+  "unarchive": "Desarquivar",
+  "unarchived_count": "{count, plural, other {Não arquivado #}}",
+  "unfavorite": "Remover favorito",
+  "unhide_person": "Exibir pessoa",
+  "unknown": "Desconhecido",
+  "unknown_year": "Ano desconhecido",
+  "unlimited": "Ilimitado",
+  "unlink_motion_video": "Remover relação com video animado",
+  "unlink_oauth": "Desvincular OAuth",
+  "unlinked_oauth_account": "Conta OAuth desvinculada",
+  "unnamed_album": "Álbum sem nome",
+  "unnamed_album_delete_confirmation": "Tem a certeza de que pretende eliminar este álbum?",
+  "unnamed_share": "Partilha sem nome",
+  "unsaved_change": "Alteração não guardada",
+  "unselect_all": "Limpar seleção",
+  "unselect_all_duplicates": "Remover seleção de todos os itens duplicados",
+  "unstack": "Desempilhar",
+  "unstacked_assets_count": "Desempilhados {count, plural, one {# ficheiro} other {# ficheiros}}",
+  "untracked_files": "Ficheiros não monitorizados",
+  "untracked_files_decription": "Estes ficheiros não são monitorizados pela aplicação. Podem ser resultados de falhas numa movimentação, carregamentos interrompidos, ou deixados para trás por causa de um problema",
+  "up_next": "A seguir",
+  "updated_password": "Palavra-passe atualizada",
+  "upload": "Carregar",
+  "upload_concurrency": "Carregamentos em simultâneo",
+  "upload_errors": "Envio completo com {count, plural, one {# erro} other {# erros}}, atualize a página para ver os novos ficheiros enviados.",
+  "upload_progress": "Restante(s) {remaining, number} - Processado(s) {processed, number}/{total, number}",
+  "upload_skipped_duplicates": "{count, plural, one {# Ignorado ficheiro duplicado} other {# Ignorados ficheiros duplicados}}",
+  "upload_status_duplicates": "Duplicados",
+  "upload_status_errors": "Erros",
+  "upload_status_uploaded": "Enviado",
+  "upload_success": "Carregamento realizado com sucesso, atualize a página para ver os novos ficheiros carregados.",
+  "url": "URL",
+  "usage": "Utilização",
+  "use_custom_date_range": "Utilizar um intervalo de datas personalizado",
+  "user": "Utilizador",
+  "user_id": "ID do utilizador",
+  "user_liked": "{user} gostou {type, select, photo {desta fotografia} video {deste video} asset {deste ficheiro} other {disto}}",
+  "user_purchase_settings": "Comprar",
+  "user_purchase_settings_description": "Gerir a sua compra",
+  "user_role_set": "Definir {user} como {role}",
+  "user_usage_detail": "Detalhes de utilização do utilizador",
+  "user_usage_stats": "Estatísticas de utilização de conta",
+  "user_usage_stats_description": "Ver estatísticas de utilização de conta",
+  "username": "Nome de utilizador",
+  "users": "Utilizadores",
+  "utilities": "Ferramentas",
+  "validate": "Validar",
+  "variables": "Variáveis",
+  "version": "Versão",
+  "version_announcement_closing": "O seu amigo, Alex",
+  "version_announcement_message": "Olá! Está disponível uma nova versão do Immich. Por favor leia as <link>notas de lançamento</link> para garantir que as suas configurações estão atualizadas e para evitar quaisquer erros, especialmente se usar o WatchTower ou qualquer mecanismo que lide com a atualização automática do Immich.",
+  "version_history": "Histórico de versões",
+  "version_history_item": "Instalado {version} em {date}",
+  "video": "Vídeo",
+  "video_hover_setting": "Reproduzir vídeo em miniatura quando passar com o cursor por cima",
+  "video_hover_setting_description": "Reproduzir vídeo em miniatura quando o cursor está sobre o item. Mesmo quando está desativado, a reprodução ainda pode ser iniciada passando sobre o ícone de reproduzir.",
+  "videos": "Vídeos",
+  "videos_count": "{count, plural, one {# Vídeo} other {# Vídeos}}",
+  "view": "Ver",
+  "view_album": "Ver Álbum",
+  "view_all": "Ver tudo",
+  "view_all_users": "Ver todos os utilizadores",
+  "view_in_timeline": "Ver na linha do tempo",
+  "view_links": "Ver links",
+  "view_name": "Ver",
+  "view_next_asset": "Ver próximo ficheiro",
+  "view_previous_asset": "Ver ficheiro anterior",
+  "view_stack": "Ver pilha",
+  "visibility_changed": "Visibilidade alterada para {count, plural, one {# pessoa} other {# pessoas}}",
+  "waiting": "Em fila",
+  "warning": "Aviso",
+  "week": "Semana",
+  "welcome": "Bem-vindo(a)",
+  "welcome_to_immich": "Bem-vindo(a) ao Immich",
+  "year": "Ano",
+  "years_ago": "Há {years, plural, one {# ano} other {# anos}} atrás",
+  "yes": "Sim",
+  "you_dont_have_any_shared_links": "Não tem links partilhados",
+  "zoom_image": "Ampliar/Reduzir imagem"
+}
diff --git a/web/src/lib/i18n/pt_BR.json b/i18n/pt_BR.json
similarity index 88%
rename from web/src/lib/i18n/pt_BR.json
rename to i18n/pt_BR.json
index 1e0de69aed..ccddd1cd3b 100644
--- a/web/src/lib/i18n/pt_BR.json
+++ b/i18n/pt_BR.json
@@ -23,53 +23,65 @@
   "add_to": "Adicionar a...",
   "add_to_album": "Adicionar ao álbum",
   "add_to_shared_album": "Adicionar ao álbum compartilhado",
+  "add_url": "Adicionar URL",
   "added_to_archive": "Adicionado ao arquivo",
   "added_to_favorites": "Adicionado aos favoritos",
   "added_to_favorites_count": "{count, plural, one {{count, number} adicionado aos favoritos} other {{count, number} adicionados aos favoritos}}",
   "admin": {
     "add_exclusion_pattern_description": "Adicione padrões de exclusão. Utilizar *, ** ou ? são suportados. Para ignorar todos os arquivos em qualquer diretório chamado \"Raw\", use \"**/Raw/**'. Para ignorar todos os arquivos que terminam em \".tif\", use \"**/*.tif\". Para ignorar um caminho absoluto, use \"/caminho/para/ignorar/**\".",
+    "asset_offline_description": "Este arquivo não foi encontrado na biblioteca externa, então foi enviado para a lixeira. Se o arquivo foi movido para outra pasta dentro da biblioteca, verifique sua linha do tempo para encontrar o arquivo novamente. Para restaurar este arquivo, certifique-se de que o caminho descrito abaixo pode ser acessado pelo Immich e então escaneie a biblioteca.",
     "authentication_settings": "Configurações de Autenticação",
     "authentication_settings_description": "Gerenciar senhas, OAuth, e outras configurações de autenticação",
     "authentication_settings_disable_all": "Tem certeza de que deseja desativar todos os métodos de login? O login será completamente desativado.",
     "authentication_settings_reenable": "Para reabilitar, use um <link>Comando do Servidor</link>.",
     "background_task_job": "Tarefas em segundo plano",
+    "backup_database": "Backup do banco de dados",
+    "backup_database_enable_description": "Ativar backup do banco de dados",
+    "backup_keep_last_amount": "Quantidade de backups anteriores para manter salvo",
+    "backup_settings": "Configurações de backup",
+    "backup_settings_description": "Gerenciar configurações de backup",
     "check_all": "Selecionar Tudo",
     "cleared_jobs": "Tarefas removidas de: {job}",
     "config_set_by_file": "A configuração está atualmente definida por um arquivo de configuração",
     "confirm_delete_library": "Você tem certeza que deseja excluir a biblioteca {library} ?",
     "confirm_delete_library_assets": "Você tem certeza que deseja excluir esta biblioteca? Isso excluirá {count, plural, one {# arquivo contido do Immich e não poderá ser desfeito. O arquivo permanecerá no disco} other {todos os # arquivos contidos do Immich e não poderá ser desfeito. Os arquivos permanecerão no disco}}.",
-    "confirm_email_below": "Para confirmar, digite o {email} abaixo",
+    "confirm_email_below": "Para confirmar, digite \"{email}\" abaixo",
     "confirm_reprocess_all_faces": "Tem certeza de que deseja reprocessar todos os rostos? Isso também limpará as pessoas nomeadas.",
     "confirm_user_password_reset": "Tem certeza de que deseja redefinir a senha de {user}?",
-    "crontab_guru": "Guru do Crontab",
+    "create_job": "Criar tarefa",
+    "cron_expression": "Expressão CRON",
+    "cron_expression_description": "Defina o intervalo de análise no formato Cron. Para mais informações, por favor veja o <link>Crontab Guru</link>",
+    "cron_expression_presets": "Sugestões de expressão Cron",
     "disable_login": "Desabilitar login",
-    "disabled": "",
-    "duplicate_detection_job_description": "Execute o aprendizado de máquina em arquivos para detectar imagens semelhantes. Depende da Pesquisa Inteligente",
+    "duplicate_detection_job_description": "Execute a inteligência artificial em arquivos para detectar imagens semelhantes. Depende da Pesquisa Inteligente",
     "exclusion_pattern_description": "Os padrões de exclusão permitem ignorar arquivos e pastas ao escanear sua biblioteca. Isso é útil se você tiver pastas que contenham arquivos que não deseja importar, como arquivos RAW.",
     "external_library_created_at": "Biblioteca externa (criada em {date})",
     "external_library_management": "Gerenciamento de bibliotecas externas",
     "face_detection": "Detecção de rostos",
-    "face_detection_description": "Detecta rostos em arquivos com inteligência artificial. Para vídeos, apenas a miniatura é considerada. \"Todos\" (re)processa todos os arquivos. \"Ausente\" enfileira arquivos que ainda não foram processados. Os rostos detectados serão enfileirados para reconhecimento facial após a conclusão da detecção de rostos, agrupando-os em pessoas novas ou existentes.",
-    "facial_recognition_job_description": "Agrupa rostos detectados em pessoas. Esta etapa é executada após a conclusão da detecção de rostos. \"Todos\" (re)agrupa todos os rostos. \"Ausentes\" enfileira rostos que ainda não têm uma pessoa atribuída.",
+    "face_detection_description": "Detectar rostos nos arquivos usando aprendizado de máquina. Para vídeos, apenas a miniatura é considerada. ‘Atualizar’ (re)processa todos os arquivos. ‘Resetar’ também limpa todos os dados de rosto atuais. ‘Faltando’ coloca em fila os arquivos que ainda não foram processados. Rostos detectados serão colocados em fila para o Reconhecimento Facial após a conclusão da Detecção de Rostos, agrupando-os em pessoas existentes ou novas.",
+    "facial_recognition_job_description": "Agrupar rostos detectados em pessoas. Esta etapa é executada após a conclusão da Detecção de Rostos. ‘Resetar’ (re)agrupará todos os rostos. ‘Faltando’ coloca em fila os rostos que não têm uma pessoa atribuída.",
     "failed_job_command": "O comando {command} falhou para a tarefa: {job}",
     "force_delete_user_warning": "AVISO: Isso removerá imediatamente o usuário e todos os arquivos. Isso não pode ser desfeito e os arquivos não podem ser recuperados.",
     "forcing_refresh_library_files": "Forçando a atualização de todos os arquivos da biblioteca",
+    "image_format": "Formato",
     "image_format_description": "WebP produz arquivos menores que JPEG, mas é mais lento para codificar.",
-    "image_prefer_embedded_preview": "Prefira visualização incorporada",
+    "image_prefer_embedded_preview": "Preferir visualização incorporada",
     "image_prefer_embedded_preview_setting_description": "Use visualizações incorporadas em fotos RAW como entrada para processamento de imagem, quando disponível. Isso pode produzir cores mais precisas para algumas imagens, mas a qualidade da visualização depende da câmera e a imagem pode ter mais artefatos de compactação.",
     "image_prefer_wide_gamut": "Prefira ampla gama",
     "image_prefer_wide_gamut_setting_description": "Use o Display P3 para miniaturas. Isso preserva melhor a vibração das imagens com espaços de cores amplos, mas as imagens podem aparecer de maneira diferente em dispositivos antigos com uma versão antiga do navegador. As imagens sRGB são mantidas como sRGB para evitar mudanças de cores.",
-    "image_preview_format": "Formato de visualização",
-    "image_preview_resolution": "Resolução de visualização",
-    "image_preview_resolution_description": "Usado ao visualizar uma única foto e para aprendizado de máquina. Resoluções mais altas podem preservar mais detalhes, mas demoram mais para codificar, têm tamanhos de arquivo maiores e podem reduzir a capacidade de resposta do aplicativo.",
+    "image_preview_description": "Imagem de tamanho médio sem os metadados, utilizado quando visualizar um único arquivo e também pela inteligência artificial",
+    "image_preview_quality_description": "Qualidade da pré-visualização, de 1-100. Maior é melhor, mas produz arquivos maiores e pode reduzir a velocidade do aplicativo. Definir um valor muito baixo pode afetar a qualidade da inteligência artificial.",
+    "image_preview_title": "Configurações de pré-visualização",
     "image_quality": "Qualidade",
-    "image_quality_description": "Qualidade de imagem de 1 a 100. Quanto maior, melhor para a qualidade, mas produz arquivos maiores. Esta opção afeta as imagens de visualização e miniatura.",
+    "image_resolution": "Resolução",
+    "image_resolution_description": "Resoluções mais altas preservam mais detalhes, porém demoram mais para processar, tem um tamanho de arquivo maior e pode reduzir a velocidade do aplicativo.",
     "image_settings": "Configurações de imagem",
     "image_settings_description": "Gerenciar a qualidade e resolução das imagens geradas",
-    "image_thumbnail_format": "Formato de miniatura",
-    "image_thumbnail_resolution": "Resolução de miniatura",
-    "image_thumbnail_resolution_description": "Usado ao visualizar grupos de fotos (linha do tempo principal, visualização de álbum, etc.). Resoluções mais altas podem preservar mais detalhes, mas demoram mais para codificar, têm tamanhos de arquivo maiores e podem reduzir a capacidade de resposta do aplicativo.",
+    "image_thumbnail_description": "Miniatura sem metadados, utilizado quando visualizar um grupos de fotos, como por exemplo, a linha do tempo principal",
+    "image_thumbnail_quality_description": "Qualidade da miniatura, de 1 a 100. Maior é melhor, mas produz arquivos maiores e pode reduzir a velocidade do aplicativo.",
+    "image_thumbnail_title": "Configurações de Miniaturas",
     "job_concurrency": "{job} simultâneo",
+    "job_created": "Tarefa criada",
     "job_not_concurrency_safe": "Esta tarefa não é compatível com simultaneidade.",
     "job_settings": "Configurações de Tarefa",
     "job_settings_description": "Gerenciar simultaneidade das tarefas",
@@ -77,14 +89,11 @@
     "jobs_delayed": "{jobCount, plural, one {# atrasado} other {# atrasados}}",
     "jobs_failed": "{jobCount, plural, one {# falhou} other {# falharam}}",
     "library_created": "Criado biblioteca: {library}",
-    "library_cron_expression": "Expressão Cron",
-    "library_cron_expression_description": "Defina o intervalo de varredura usando o formato cron. Para mais informações, consulte, por exemplo, <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Predefinições de expressão Cron",
     "library_deleted": "Biblioteca excluída",
     "library_import_path_description": "Especifique uma pasta para importar. Esta pasta, incluindo subpastas, será escaneada em busca de imagens e vídeos.",
-    "library_scanning": "Escanear periódicamente",
-    "library_scanning_description": "Configurar o escaneamento periódico da biblioteca",
-    "library_scanning_enable_description": "Habilitar escaneamento periódico da biblioteca",
+    "library_scanning": "Verificação Periódica",
+    "library_scanning_description": "Configurar verificação periódica da biblioteca",
+    "library_scanning_enable_description": "Habilitar verificação periódica da biblioteca",
     "library_settings": "Biblioteca Externa",
     "library_settings_description": "Gerenciar configurações de biblioteca externa",
     "library_tasks_description": "Execute tarefas de biblioteca",
@@ -100,7 +109,7 @@
     "machine_learning_duplicate_detection_enabled": "Habilitar detecção de duplicidade",
     "machine_learning_duplicate_detection_enabled_description": "Se desativado, arquivos exatamente idênticos ainda serão desduplicados.",
     "machine_learning_duplicate_detection_setting_description": "Use embeddings CLIP para encontrar prováveis duplicidades",
-    "machine_learning_enabled": "Habilitar o aprendizado da máquina",
+    "machine_learning_enabled": "Habilitar a inteligência artificial",
     "machine_learning_enabled_description": "Se desativado, todos os recursos de ML serão desativados, independentemente das configurações abaixo.",
     "machine_learning_facial_recognition": "Reconhecimento Facial",
     "machine_learning_facial_recognition_description": "Detectar, reconhecer e agrupar rostos em imagens",
@@ -116,13 +125,13 @@
     "machine_learning_min_detection_score_description": "Pontuação mínima de confiança para um rosto ser detectado, de 0 a 1. Valores mais baixos detectam mais rostos, mas poderão resultar em falsos positivos.",
     "machine_learning_min_recognized_faces": "Mínimo de rostos reconhecidos",
     "machine_learning_min_recognized_faces_description": "O número mínimo de rostos reconhecidos para uma pessoa ser criada na lista. Aumentar isso torna o Reconhecimento Facial mais preciso, ao custo de aumentar a chance de um rosto não ser atribuído a uma pessoa.",
-    "machine_learning_settings": "Configurações de aprendizado de máquina (Machine Learning)",
-    "machine_learning_settings_description": "Gerenciar recursos e configurações de aprendizado de máquina",
+    "machine_learning_settings": "Configurações de inteligência artificial",
+    "machine_learning_settings_description": "Gerenciar recursos e configurações da inteligência artificial",
     "machine_learning_smart_search": "Pesquisa Inteligente",
     "machine_learning_smart_search_description": "Buscar imagens semanticamente usando embeddings CLIP",
     "machine_learning_smart_search_enabled": "Habilitar a Pesquisa Inteligente",
     "machine_learning_smart_search_enabled_description": "Se desativado, as imagens não serão codificadas para pesquisa inteligente.",
-    "machine_learning_url_description": "URL do servidor de aprendizado de máquina",
+    "machine_learning_url_description": "A URL do servidor de inteligência artificial. Se mais de uma URL for configurada, o servidor irá tentar uma de cada vez até que uma delas responda com sucesso, em ordem sequencial igual a configurada.",
     "manage_concurrency": "Gerenciar simultaneidade",
     "manage_log_settings": "Gerenciar configurações de registro",
     "map_dark_style": "Tema Escuro",
@@ -139,7 +148,11 @@
     "map_settings_description": "Gerenciar configurações do mapa",
     "map_style_description": "URL para um tema de mapa style.json",
     "metadata_extraction_job": "Extrair metadados",
-    "metadata_extraction_job_description": "Extraia informações de metadados de cada arquivo, como GPS e resolução",
+    "metadata_extraction_job_description": "Extraia informações dos metadados de cada arquivo, como GPS, rostos e resolução",
+    "metadata_faces_import_setting": "Ativar a importação de rostos",
+    "metadata_faces_import_setting_description": "Importar rostos a partir dos metadados EXIF da imagem e arquivos auxiliares",
+    "metadata_settings": "Configurações de Metadados",
+    "metadata_settings_description": "Gerenciar configurações de metadados",
     "migration_job": "Migração",
     "migration_job_description": "Migrar miniaturas de arquivos e rostos para a estrutura de pastas mais recente",
     "no_paths_added": "Nenhum caminho adicionado",
@@ -147,8 +160,8 @@
     "note_apply_storage_label_previous_assets": "Observação: Para aplicar o rótulo de armazenamento a arquivos carregados anteriormente, execute o",
     "note_cannot_be_changed_later": "NOTA: Isto não pode ser alterado posteriormente!",
     "note_unlimited_quota": "Observação: insira 0 para cota ilimitada",
-    "notification_email_from_address": "A partir do endereço",
-    "notification_email_from_address_description": "Endereço de e-mail do remetente, por exemplo: \"Immich Photo Server <noreply@immich.app>\"",
+    "notification_email_from_address": "E-mail de origem",
+    "notification_email_from_address_description": "Endereço de e-mail do remetente, por exemplo: \"Immich Photo Server <noreply@example.com>\"",
     "notification_email_host_description": "Host do servidor de e-mail (por exemplo, smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Ignorar erros de certificado",
     "notification_email_ignore_certificate_errors_description": "Ignorar erros de validação de certificado TLS (não recomendado)",
@@ -158,8 +171,8 @@
     "notification_email_setting_description": "Configurações para envio de notificações por e-mail",
     "notification_email_test_email": "Enviar e-mail de teste",
     "notification_email_test_email_failed": "Falha ao enviar e-mail de teste. Verifique seus valores",
-    "notification_email_test_email_sent": "Um email de teste foi enviado para {email}. Por favor, verifique sua caixa de entrada.",
-    "notification_email_username_description": "Nome de usuário a ser usado ao autenticar com o servidor de e-mail",
+    "notification_email_test_email_sent": "Um e-mail de teste foi enviado para {email}. Por favor, verifique sua caixa de entrada.",
+    "notification_email_username_description": "Nome de usuário que será usado para autenticar com o servidor de e-mail",
     "notification_enable_email_notifications": "Habilitar notificações por e-mail",
     "notification_settings": "Configurações de notificação",
     "notification_settings_description": "Gerenciar configurações de notificação, incluindo e-mail",
@@ -174,7 +187,7 @@
     "oauth_issuer_url": "URL do emissor",
     "oauth_mobile_redirect_uri": "URI de redirecionamento móvel",
     "oauth_mobile_redirect_uri_override": "Substituição de URI de redirecionamento móvel",
-    "oauth_mobile_redirect_uri_override_description": "Ative quando 'app.immich:/' for um URI de redirecionamento inválido.",
+    "oauth_mobile_redirect_uri_override_description": "Ative quando o provedor do OAuth não suportar uma URI de aplicativo, por exemplo '{callback}'",
     "oauth_profile_signing_algorithm": "Algoritmo de assinatura de perfis",
     "oauth_profile_signing_algorithm_description": "Algoritmo usado para assinar o perfil do usuário.",
     "oauth_scope": "Escopo",
@@ -194,22 +207,24 @@
     "password_settings": "Senha de acesso",
     "password_settings_description": "Gerenciar configurações de login e senha",
     "paths_validated_successfully": "Todos os caminhos validados com sucesso",
+    "person_cleanup_job": "Limpeza de pessoas",
     "quota_size_gib": "Tamanho da cota (GiB)",
     "refreshing_all_libraries": "Atualizando todas as bibliotecas",
     "registration": "Registro de Administrador",
     "registration_description": "Como você é o primeiro usuário no sistema, será designado como o Administrador e será responsável pelas tarefas administrativas. Você também poderá criar usuários adicionais.",
-    "removing_offline_files": "Removendo arquivos offline",
     "repair_all": "Reparar tudo",
     "repair_matched_items": "{count, plural, one {# item encontrado} other {# itens encontrados}}",
     "repaired_items": "{count, plural, one {# item reparado} other {# itens reparados}}",
     "require_password_change_on_login": "Exigir que o usuário altere a senha no primeiro login",
     "reset_settings_to_default": "Redefinir as configurações para o padrão",
     "reset_settings_to_recent_saved": "Redefinir as configurações para as configurações salvas recentemente",
-    "scanning_library_for_changed_files": "Escaneando a biblioteca em busca de arquivos alterados",
-    "scanning_library_for_new_files": "Escaneando a biblioteca em busca de novos arquivos",
+    "scanning_library": "Analisando a biblioteca",
+    "search_jobs": "Pesquisar tarefas...",
     "send_welcome_email": "Enviar e-mail de boas-vindas",
     "server_external_domain_settings": "Domínio externo",
     "server_external_domain_settings_description": "Domínio para links públicos compartilhados, incluindo http(s)://",
+    "server_public_users": "Usuários públicos",
+    "server_public_users_description": "Todos os usuários (nome e e-mail) serão exibidos na lista de adicionar usuários em álbuns compartilhados. Quando desativado, essa lista de usuários só será visível aos administradores.",
     "server_settings": "Configurações do servidor",
     "server_settings_description": "Gerenciar configurações do servidor",
     "server_welcome_message": "Mensagem de boas-vindas",
@@ -217,7 +232,7 @@
     "sidecar_job": "Metadados secundários",
     "sidecar_job_description": "Descubra ou sincronize metadados secundários do sistema de arquivos",
     "slideshow_duration_description": "Tempo em segundos para exibir cada imagem",
-    "smart_search_job_description": "Execute o aprendizado de máquina em arquivos para oferecer suporte à pesquisa inteligente",
+    "smart_search_job_description": "Execute a inteligência artificial em arquivos para oferecer suporte à pesquisa inteligente",
     "storage_template_date_time_description": "A data e hora da criação do ativo é usado para a informações de data e hora",
     "storage_template_date_time_sample": "Exemplo {date}",
     "storage_template_enable_description": "Habilitar mecanismo de modelo de armazenamento",
@@ -234,6 +249,17 @@
     "storage_template_settings_description": "Gerencie a estrutura de pasta e o nome do arquivo carregado",
     "storage_template_user_label": "<code>{label}</code> é o Rótulo de Armazenamento do usuário",
     "system_settings": "Configurações do Sistema",
+    "tag_cleanup_job": "Limpeza de tags",
+    "template_email_available_tags": "Você pode usar as seguintes variáveis no modelo: {tags}",
+    "template_email_if_empty": "Se o modelo estiver em branco, o modelo de e-mail padrão será usado.",
+    "template_email_invite_album": "Modelo do e-mail de convite para álbum",
+    "template_email_preview": "Pré visualização",
+    "template_email_settings": "Modelos de e-mail",
+    "template_email_settings_description": "Gerenciar modelos personalizados de e-mail de notificação",
+    "template_email_update_album": "Modelo do e-mail de atualização do álbum",
+    "template_email_welcome": "Modelo do e-mail de boas vindas",
+    "template_settings": "Modelos de notificação",
+    "template_settings_description": "Gerenciar modelos personalizados para notificações.",
     "theme_custom_css_settings": "CSS customizado",
     "theme_custom_css_settings_description": "Folhas de estilo em cascata permitem que o design do Immich seja personalizado.",
     "theme_settings": "Configurações de tema",
@@ -241,7 +267,6 @@
     "these_files_matched_by_checksum": "Esses arquivos são correspondidos por seus checksum",
     "thumbnail_generation_job": "Gerar Miniaturas",
     "thumbnail_generation_job_description": "Gere miniaturas grandes, pequenas e desfocadas para cada arquivo, bem como miniaturas para cada pessoa",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "API de aceleração",
     "transcoding_acceleration_api_description": "A API que irá interagir com o seu dispositivo para acelerar a transcodificação. Esta configuração é a 'melhor opção': ela retornará à transcodificação de software em caso de falha. O VP9 pode não funcionar dependendo do seu hardware.",
     "transcoding_acceleration_nvenc": "NVENC (requer GPU NVIDIA)",
@@ -267,7 +292,7 @@
     "transcoding_hardware_acceleration": "Aceleração de hardware",
     "transcoding_hardware_acceleration_description": "Experimental; muito mais rápido, mas terá qualidade inferior com a mesma taxa de bits",
     "transcoding_hardware_decoding": "Decodificação de hardware",
-    "transcoding_hardware_decoding_setting_description": "Aplica-se apenas a NVENC, QSV e RKMPP. Permite aceleração ponta a ponta em vez de apenas acelerar a codificação. Pode não funcionar em todos os vídeos.",
+    "transcoding_hardware_decoding_setting_description": "Habilita a aceleração de ponta a ponta, em vez de apenas acelerar a codificação. Pode não funcionar em todos os vídeos.",
     "transcoding_hevc_codec": "Codec HEVC",
     "transcoding_max_b_frames": "Máximo de quadros B",
     "transcoding_max_b_frames_description": "Valores mais altos melhoram a eficiência da compactação, mas retardam a codificação. Pode não ser compatível com aceleração de hardware em dispositivos mais antigos. 0 desativa os quadros B, enquanto -1 define esse valor automaticamente.",
@@ -293,8 +318,6 @@
     "transcoding_threads_description": "Valores mais altos levam a uma codificação mais rápida, mas deixam menos espaço para o servidor processar outras tarefas enquanto estiver ativo. Este valor não deve ser superior ao número de núcleos da CPU. Maximiza a utilização se definido como 0.",
     "transcoding_tone_mapping": "Mapeamento de tons",
     "transcoding_tone_mapping_description": "Tenta preservar a aparência dos vídeos HDR quando convertidos para SDR. Cada algoritmo faz compensações diferentes em termos de cor, detalhes e brilho. Hable preserva os detalhes, Mobius preserva as cores e Reinhard preserva o brilho.",
-    "transcoding_tone_mapping_npl": "NPL de mapeamento de tons",
-    "transcoding_tone_mapping_npl_description": "As cores serão ajustadas para parecerem normais para uma exibição com esse brilho. Contra-intuitivamente, valores mais baixos aumentam o brilho do vídeo e vice-versa, uma vez que compensam o brilho da tela. 0 define esse valor automaticamente.",
     "transcoding_transcode_policy": "Política de transcodificação",
     "transcoding_transcode_policy_description": "Política para quando um vídeo deve ser transcodificado. Os vídeos HDR sempre serão transcodificados (exceto se a transcodificação estiver desativada).",
     "transcoding_two_pass_encoding": "Codificação de duas passagens",
@@ -308,6 +331,7 @@
     "trash_settings_description": "Gerenciar configurações da lixeira",
     "untracked_files": "Arquivos não rastreados",
     "untracked_files_description": "Esses arquivos não são rastreados pelo aplicativo. Eles podem ser o resultado de movimentos malsucedidos, carregamentos interrompidos ou deixados para trás devido a um erro",
+    "user_cleanup_job": "Limpeza de usuários",
     "user_delete_delay": "A conta e os arquivos de <b>{user}</b> serão programados para exclusão permanente em {delay, plural, one {# dia} other {# dias}}.",
     "user_delete_delay_settings": "Excluir atraso",
     "user_delete_delay_settings_description": "Número de dias após a remoção para excluir permanentemente a conta e os arquivos de um usuário. A tarefa de exclusão de usuário é executada à meia-noite para verificar usuários que estão prontos para exclusão. As alterações nesta configuração serão avaliadas na próxima execução.",
@@ -339,6 +363,7 @@
   "album_added_notification_setting_description": "Receba uma notificação por e-mail quando você for adicionado a um álbum compartilhado",
   "album_cover_updated": "Capa do álbum atualizada",
   "album_delete_confirmation": "Tem certeza de que deseja excluir o álbum {album}?",
+  "album_delete_confirmation_description": "Se este álbum é compartilhado, os outros usuários não conseguiram mais acessá-lo.",
   "album_info_updated": "Informações do álbum atualizadas",
   "album_leave": "Sair do álbum?",
   "album_leave_confirmation": "Tem certeza de que deseja sair de {album}?",
@@ -373,7 +398,6 @@
   "archive_or_unarchive_photo": "Arquivar ou desarquivar foto",
   "archive_size": "Tamanho do arquivo",
   "archive_size_description": "Configure o tamanho do arquivo para baixar (em GiB)",
-  "archived": "Arquivado",
   "archived_count": "{count, plural, one {# Arquivado} other {# Arquivados}}",
   "are_these_the_same_person": "Essas pessoas são a mesma pessoa?",
   "are_you_sure_to_do_this": "Tem certeza de que deseja fazer isso?",
@@ -384,8 +408,9 @@
   "asset_has_unassigned_faces": "O arquivo tem rostos sem nomes",
   "asset_hashing": "Processando...",
   "asset_offline": "Arquivo indisponível",
-  "asset_offline_description": "Este arquivo não está disponível. O Immich não pode acessar o local do arquivo. Certifique-se de que o arquivo esteja disponível e depois escaneie novamente a biblioteca.",
+  "asset_offline_description": "Este arquivo externo não está mais disponível. Contate seu administrador do Immich para obter ajuda.",
   "asset_skipped": "Ignorado",
+  "asset_skipped_in_trash": "Na lixeira",
   "asset_uploaded": "Carregado",
   "asset_uploading": "Carregando...",
   "assets": "Arquivos",
@@ -393,11 +418,10 @@
   "assets_added_to_album_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}} ao álbum",
   "assets_added_to_name_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}} {hasName, select, true {ao álbum <b>{name}</b>} other {em um novo álbum}}",
   "assets_count": "{count, plural, one {# arquivo} other {# arquivos}}",
-  "assets_moved_to_trash": "{count, plural, one {# ativo enviado} other {# ativos enviados}} para a lixeira",
   "assets_moved_to_trash_count": "{count, plural, one {# arquivo movido} other {# arquivos movidos}} para a lixeira",
   "assets_permanently_deleted_count": "{count, plural, one {# arquivo excluído permanentemente} other {# arquivos excluídos permanentemente}}",
   "assets_removed_count": "{count, plural, one {# arquivo removido} other {# arquivos removidos}}",
-  "assets_restore_confirmation": "Tem certeza de que deseja restaurar todos os seus arquivos na lixeira? Esta ação não pode ser desfeita!",
+  "assets_restore_confirmation": "Tem certeza de que deseja restaurar todos os seus arquivos na lixeira? Esta ação não pode ser desfeita! Nota: Arquivos externos não podem ser restaurados desta maneira.",
   "assets_restored_count": "{count, plural, one {# arquivo restaurado} other {# arquivos restaurados}}",
   "assets_trashed_count": "{count, plural, one {# arquivo movido para a lixeira} other {# arquivos movidos para a lixeira}}",
   "assets_were_part_of_album_count": "{count, plural, one {O arquivo já faz} other {Os arquivos já fazem}} parte do álbum",
@@ -408,6 +432,7 @@
   "birthdate_saved": "Data de nascimento salva com sucesso",
   "birthdate_set_description": "A data de nascimento é usada para calcular a idade da pessoa no momento em que a foto foi tirada.",
   "blurred_background": "Fundo desfocado",
+  "bugs_and_feature_requests": "Relatar problemas & Sugestões",
   "build": "Versão de compilação",
   "build_image": "Imagem de compilação",
   "bulk_delete_duplicates_confirmation": "Tem a certeza de que deseja deletar {count, plural, one {# arquivo duplicado} other {em massa # arquivos duplicados}}? Esta ação mantém o maior arquivo de cada grupo e deleta permanentemente todos as outras duplicidades. Você não pode desfazer esta ação!",
@@ -422,10 +447,6 @@
   "cannot_merge_people": "Não é possível mesclar pessoas",
   "cannot_undo_this_action": "Você não pode desfazer esta ação!",
   "cannot_update_the_description": "Não é possível atualizar a descrição",
-  "cant_apply_changes": "Não é possível aplicar alterações",
-  "cant_get_faces": "Não foi possível obter faces",
-  "cant_search_people": "Não foi possível pesquisar pessoas",
-  "cant_search_places": "Não foi possível pesquisar lugares",
   "change_date": "Alterar data",
   "change_expiration_time": "Alterar o prazo de validade",
   "change_location": "Alterar localização",
@@ -457,6 +478,7 @@
   "confirm": "Confirmar",
   "confirm_admin_password": "Confirmar senha de administrador",
   "confirm_delete_shared_link": "Tem certeza de que deseja excluir este link compartilhado?",
+  "confirm_keep_this_delete_others": "Todos os outros arquivos da pilha serão excluídos, exceto este arquivo. Tem certeza de que deseja continuar?",
   "confirm_password": "Confirme a senha",
   "contain": "Caber",
   "context": "Contexto",
@@ -483,6 +505,7 @@
   "create_new_person_hint": "Atribuir arquivos selecionados a uma nova pessoa",
   "create_new_user": "Criar novo usuário",
   "create_tag": "Criar tag",
+  "create_tag_description": "Crie uma nova tag. Para tags compostas, digite o caminho completo da tag, inclusive as barras.",
   "create_user": "Criar usuário",
   "created": "Criado",
   "current_device": "Dispositivo atual",
@@ -505,16 +528,19 @@
   "delete_key": "Excluir chave",
   "delete_library": "Excluir biblioteca",
   "delete_link": "Excluir link",
+  "delete_others": "Excluir restante",
   "delete_shared_link": "Excluir link de compartilhamento",
   "delete_tag": "Remover tag",
   "delete_tag_confirmation_prompt": "Tem certeza que deseja excluir a tag {tagName} ?",
   "delete_user": "Excluir usuário",
   "deleted_shared_link": "Link de compartilhamento excluído",
+  "deletes_missing_assets": "Excluir arquivos não encontrados",
   "description": "Descrição",
   "details": "Detalhes",
   "direction": "Direção",
   "disabled": "Desativado",
   "disallow_edits": "Não permitir edições",
+  "discord": "Discord",
   "discover": "Descobrir",
   "dismiss_all_errors": "Dispensar todos os erros",
   "dismiss_error": "Dispensar erro",
@@ -523,6 +549,7 @@
   "display_original_photos": "Exibir fotos originais",
   "display_original_photos_setting_description": "Prefira exibir a foto original ao visualizar um arquivo em vez de miniaturas quando o arquivo original é compatível com a web. Isso pode diminuir a velocidade de exibição das fotos.",
   "do_not_show_again": "Não mostrar esta mensagem novamente",
+  "documentation": "Documentação",
   "done": "Feito",
   "download": "Baixar",
   "download_include_embedded_motion_videos": "Vídeos inclusos",
@@ -535,13 +562,6 @@
   "duplicates": "Duplicados",
   "duplicates_description": "Marque cada grupo indicando quais arquivos, se algum, são duplicados",
   "duration": "Duração",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
   "edit": "Editar",
   "edit_album": "Editar álbum",
   "edit_avatar": "Editar foto de perfil",
@@ -566,8 +586,6 @@
   "editor_crop_tool_h2_aspect_ratios": "Proporções",
   "editor_crop_tool_h2_rotation": "Rotação",
   "email": "E-mail",
-  "empty": "",
-  "empty_album": "",
   "empty_trash": "Esvaziar lixo",
   "empty_trash_confirmation": "Tem certeza de que deseja esvaziar a lixeira? Isso removerá permanentemente do Immich todos os arquivos que estão na lixeira.\nVocê não pode desfazer esta ação!",
   "enable": "Habilitar",
@@ -601,6 +619,7 @@
     "failed_to_create_shared_link": "Falha ao criar o link compartilhado",
     "failed_to_edit_shared_link": "Falha ao editar o link compartilhado",
     "failed_to_get_people": "Falha na obtenção de pessoas",
+    "failed_to_keep_this_delete_others": "Falha ao manter este arquivo e excluir os outros",
     "failed_to_load_asset": "Não foi possível carregar o ativo",
     "failed_to_load_assets": "Não foi possível carregar os ativos",
     "failed_to_load_people": "Falha ao carregar pessoas",
@@ -628,8 +647,6 @@
     "unable_to_change_location": "Não foi possível alterar a localização",
     "unable_to_change_password": "Não foi possível alterar a senha",
     "unable_to_change_visibility": "Não foi possível alterar a visibilidade de {count, plural, one {# pessoa} other {# pessoas}}",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_complete_oauth_login": "Não foi possível concluir o login OAuth",
     "unable_to_connect": "Não foi possível conectar",
     "unable_to_connect_to_server": "Não foi possível se conectar ao servidor",
@@ -654,6 +671,7 @@
     "unable_to_get_comments_number": "Não foi possível obter o número de comentários",
     "unable_to_get_shared_link": "Não foi possível obter link o compartilhado",
     "unable_to_hide_person": "Não foi possível esconder a pessoa",
+    "unable_to_link_motion_video": "Não foi possível relacionar ao video animado",
     "unable_to_link_oauth_account": "Não foi possível associar a conta OAuth",
     "unable_to_load_album": "Não foi possível carregar o álbum",
     "unable_to_load_asset_activity": "Não foi possível carregar as atividades do arquivo",
@@ -669,12 +687,10 @@
     "unable_to_remove_album_users": "Não foi possível remover usuários do álbum",
     "unable_to_remove_api_key": "Não foi possível a Chave de API",
     "unable_to_remove_assets_from_shared_link": "Não foi possível remover arquivos do link compartilhado",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_deleted_assets": "Não foi possível remover arquivos offline",
     "unable_to_remove_library": "Não foi possível remover a biblioteca",
-    "unable_to_remove_offline_files": "Não foi possível remover arquivos offline",
     "unable_to_remove_partner": "Não foi possível remover parceiro",
     "unable_to_remove_reaction": "Não foi possível remover a reação",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "Não foi possível reparar os itens",
     "unable_to_reset_password": "Não foi possível resetar a senha",
     "unable_to_resolve_duplicate": "Não foi possível resolver a duplicidade",
@@ -694,6 +710,7 @@
     "unable_to_submit_job": "Não foi possível enviar a tarefa",
     "unable_to_trash_asset": "Não foi possível enviar o arquivo para a lixeira",
     "unable_to_unlink_account": "Não foi possível desvincular conta",
+    "unable_to_unlink_motion_video": "Não foi possível remover a relação com o video animado",
     "unable_to_update_album_cover": "Não foi possível atualizar a capa do álbum",
     "unable_to_update_album_info": "Não foi possível atualizar as informações do álbum",
     "unable_to_update_library": "Não foi possível atualizar a biblioteca",
@@ -703,10 +720,6 @@
     "unable_to_update_user": "Não foi possível atualizar o usuário",
     "unable_to_upload_file": "Não foi possível carregar o arquivo"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exif": "Exif",
   "exit_slideshow": "Sair da apresentação",
   "expand_all": "Expandir tudo",
@@ -721,30 +734,28 @@
   "external": "Externo",
   "external_libraries": "Bibliotecas externas",
   "face_unassigned": "Sem nome",
-  "failed_to_get_people": "Falha ao carregar as pessoas",
+  "failed_to_load_assets": "Falha ao carregar arquivos",
   "favorite": "Favorito",
   "favorite_or_unfavorite_photo": "Marque ou desmarque a foto como favorita",
   "favorites": "Favoritos",
-  "feature": "",
   "feature_photo_updated": "Foto principal atualizada",
-  "featurecollection": "",
+  "features": "Funcionalidades",
+  "features_setting_description": "Gerenciar as funcionalidades da aplicação",
   "file_name": "Nome do arquivo",
   "file_name_or_extension": "Nome do arquivo ou extensão",
   "filename": "Nome do arquivo",
-  "files": "",
   "filetype": "Tipo de arquivo",
   "filter_people": "Filtrar pessoas",
   "find_them_fast": "Encontre pelo nome em uma pesquisa",
   "fix_incorrect_match": "Corrigir correspondência incorreta",
   "folders": "Pastas",
-  "force_re-scan_library_files": "Força escanear novamente todos os arquivos da biblioteca",
+  "folders_feature_description": "Navegar pelas pastas das fotos e vídeos no sistema de arquivos",
   "forward": "Para frente",
   "general": "Geral",
   "get_help": "Obter Ajuda",
   "getting_started": "Primeiros passos",
   "go_back": "Voltar",
   "go_to_search": "Ir para a pesquisa",
-  "go_to_share_page": "Ir para a página de compartilhamento",
   "group_albums_by": "Agrupar álbuns por...",
   "group_no": "Sem agrupamento",
   "group_owner": "Agrupar por dono",
@@ -770,10 +781,6 @@
   "image_alt_text_date_place_2_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1} e {person2} em {date}",
   "image_alt_text_date_place_3_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1}, {person2}, e {person3} em {date}",
   "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1}, {person2}, e {additionalCount, number} outros em {date}",
-  "image_alt_text_people": "{count, plural, =1 {com {person1}} =2 {com {person1} e {person2}} =3 {com {person1}, {person2}, e {person3}} other {com {person1}, {person2} e outras {others, number} pessoas}}",
-  "image_alt_text_place": "em {city}, {country}",
-  "image_taken": "{isVideo, select, true {Gravado} other {Fotografado}}",
-  "img": "",
   "immich_logo": "Logo do Immich",
   "immich_web_interface": "Interface Web do Immich",
   "import_from_json": "Importar do JSON",
@@ -794,10 +801,11 @@
   "invite_people": "Convidar Pessoas",
   "invite_to_album": "Convidar para o álbum",
   "items_count": "{count, plural, one {# item} other {# itens}}",
-  "job_settings_description": "",
   "jobs": "Tarefas",
   "keep": "Manter",
   "keep_all": "Manter Todos",
+  "keep_this_delete_others": "Manter este, excluir o resto",
+  "kept_this_deleted_others": "Este foi mantido e {count, plural, one {# arquivo foi excluído} other {# arquivos foram excluídos}}",
   "keyboard_shortcuts": "Atalhos do teclado",
   "language": "Idioma",
   "language_setting_description": "Selecione seu Idioma preferido",
@@ -809,33 +817,9 @@
   "level": "Nível",
   "library": "Biblioteca",
   "library_options": "Opções da biblioteca",
-  "license_account_info": "Sua conta está licenciada",
-  "license_activated_subtitle": "Obrigado por apoiar o Immich e o software de código aberto",
-  "license_activated_title": "Sua licença foi ativada com sucesso",
-  "license_button_activate": "Ativado",
-  "license_button_buy": "Compra",
-  "license_button_buy_license": "Comprar licença",
-  "license_button_select": "Selecione",
-  "license_failed_activation": "Falha ao ativar a licença. Verifique seu e-mail para obter a chave de licença correta!",
-  "license_individual_description_1": "1 licença por usuário em qualquer servidor",
-  "license_individual_title": "Licença individual",
-  "license_info_licensed": "Licenciado",
-  "license_info_unlicensed": "Sem licença",
-  "license_input_suggestion": "Tem licença? Digite a chave abaixo",
-  "license_license_subtitle": "Comprar uma licença para apoiar Immich",
-  "license_license_title": "LICENÇA",
-  "license_lifetime_description": "Licença Vitalícia",
-  "license_per_server": "Por servidor",
-  "license_per_user": "Por usuário",
-  "license_server_description_1": "1 licença por servidor",
-  "license_server_description_2": "Licença para todos os usuários no servidor",
-  "license_server_title": "Licença de servidor",
-  "license_trial_info_1": "Você está executando uma versão não licenciada do Immich",
-  "license_trial_info_2": "Você tem usado Immich por aproximadamente",
-  "license_trial_info_3": "{accountAge, plural, um {# dia} outro {# dias}}",
-  "license_trial_info_4": "Por favor, Considere adquirir uma licença para apoiar o desenvolvimento contínuo do serviço",
   "light": "Claro",
   "like_deleted": "Curtida excluída",
+  "link_motion_video": "Relacionar video animado",
   "link_options": "Opções do Link",
   "link_to_oauth": "Link do OAuth",
   "linked_oauth_account": "Conta OAuth Vinculada",
@@ -854,6 +838,7 @@
   "look": "Estilo",
   "loop_videos": "Repetir vídeos",
   "loop_videos_description": "Ative para repetir os vídeos automaticamente durante a exibição.",
+  "main_branch_warning": "Você está utilizando a versão de desenvolvimento. É altamente recomendado que utilize a versão estável!",
   "make": "Marca",
   "manage_shared_links": "Gerir links partilhados",
   "manage_sharing_with_partners": "Gerenciar compartilhamento com parceiros",
@@ -875,10 +860,10 @@
   "menu": "Menu",
   "merge": "Mesclar",
   "merge_people": "Mesclar pessoas",
-  "merge_people_limit": "Só é possível combinar até 5 rostos de uma só vez",
+  "merge_people_limit": "Só é possível mesclar até 5 pessoas de uma só vez",
   "merge_people_prompt": "Tem certeza que deseja mesclar estas pessoas? Esta ação é irreversível.",
   "merge_people_successfully": "Pessoas mescladas com sucesso",
-  "merged_people_count": "{count, plural, one {# pessoa foi combinada} other {# pessoas foram combinadas}}",
+  "merged_people_count": "{count, plural, one {# pessoa foi mesclada} other {# pessoas foram mescladas}}",
   "minimize": "Minimizar",
   "minute": "Minuto",
   "missing": "Faltando",
@@ -923,6 +908,7 @@
   "notifications": "Notificações",
   "notifications_setting_description": "Gerenciar notificações",
   "oauth": "OAuth",
+  "official_immich_resources": "Recursos oficiais do Immich",
   "offline": "Offline",
   "offline_paths": "Caminhos offline",
   "offline_paths_description": "Estes resultados podem ser devidos a arquivos deletados manualmente e que não são parte de uma biblioteca externa.",
@@ -935,7 +921,6 @@
   "onboarding_welcome_user": "Bem-vindo, {user}",
   "online": "Online",
   "only_favorites": "Somente favoritos",
-  "only_refreshes_modified_files": "Somente atualize arquivos modificados",
   "open_in_map_view": "Mostrar no mapa",
   "open_in_openstreetmap": "Abrir no OpenStreetMap",
   "open_the_search_filters": "Abre os filtros de pesquisa",
@@ -973,14 +958,12 @@
   "people_edits_count": "{count, plural, one {# pessoa editada} other {# pessoas editadas}}",
   "people_feature_description": "Navegar por fotos e vídeos agrupados por pessoas",
   "people_sidebar_description": "Exibe o link Pessoas na barra lateral",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "Aviso para deletar permanentemente",
   "permanent_deletion_warning_setting_description": "Exibe um aviso ao deletar arquivos de forma permanente",
   "permanently_delete": "Deletar permanentemente",
   "permanently_delete_assets_count": "Excluir permanentemente {count, plural, one {asset} other {assets}}",
   "permanently_delete_assets_prompt": "Você tem certeza de que deseja excluir permanentemente {count, plural, one {este ativo?} other {estes <b>#</b> ativos?}} Esta ação também removerá {count, plural, one {o ativo} other {os ativos}} de um ou mais álbuns.",
   "permanently_deleted_asset": "Arquivo deletado permanentemente",
-  "permanently_deleted_assets": "{count, plural, one {# ativo deletado} other {# ativos deletados}} permanentemente",
   "permanently_deleted_assets_count": "{count, plural, one {# arquivo permanentemente excluído} other {# arquivos permanentemente excluídos}}",
   "person": "Pessoa",
   "person_hidden": "{name}{hidden, select, true { (oculto)} other {}}",
@@ -996,7 +979,6 @@
   "play_memories": "Reproduzir memórias",
   "play_motion_photo": "Reproduzir foto em movimento",
   "play_or_pause_video": "Reproduzir ou Pausar vídeo",
-  "point": "",
   "port": "Porta",
   "preset": "Predefinição",
   "preview": "Pré-visualizar",
@@ -1041,11 +1023,10 @@
   "purchase_server_description_2": "Status de Contribuidor",
   "purchase_server_title": "Servidor",
   "purchase_settings_server_activated": "A chave do produto para servidor é gerenciada pelo administrador",
-  "range": "",
   "rating": "Estrelas",
   "rating_clear": "Limpar classificação",
+  "rating_count": "{count, plural, one {# estrela} other {# estrelas}}",
   "rating_description": "Exibir o EXIF de classificação no painel de informações",
-  "raw": "",
   "reaction_options": "Opções de reação",
   "read_changelog": "Ler Novidades",
   "reassign": "Reatribuir",
@@ -1053,14 +1034,17 @@
   "reassigned_assets_to_new_person": "{count, plural, one {# arquivo reatribuído} other {# arquivos reatribuídos}} a uma nova pessoa",
   "reassing_hint": "Atribuir arquivos selecionados a uma pessoa existente",
   "recent": "Recente",
+  "recent-albums": "Álbuns recentes",
   "recent_searches": "Pesquisas recentes",
   "refresh": "Atualizar",
   "refresh_encoded_videos": "Atualizar vídeos codificados",
+  "refresh_faces": "Atualizar rostos",
   "refresh_metadata": "Atualizar metadados",
   "refresh_thumbnails": "Atualizar miniaturas",
   "refreshed": "Atualizado",
   "refreshes_every_file": "Atualiza todos arquivos",
   "refreshing_encoded_video": "Atualizando vídeo codificado",
+  "refreshing_faces": "Atualizando rostos",
   "refreshing_metadata": "Atualizando metadados",
   "regenerating_thumbnails": "Regenerando miniaturas",
   "remove": "Remover",
@@ -1068,15 +1052,17 @@
   "remove_assets_shared_link_confirmation": "Tem certeza de que deseja remover {count, plural, one {# arquivo} other {# arquivos}} desse link compartilhado?",
   "remove_assets_title": "Remover arquivos?",
   "remove_custom_date_range": "Remover intervalo de datas personalizado",
+  "remove_deleted_assets": "Remover arquivos offline",
   "remove_from_album": "Remover do álbum",
   "remove_from_favorites": "Remover dos favoritos",
   "remove_from_shared_link": "Remover do link compartilhado",
-  "remove_offline_files": "Remover arquivos offline",
+  "remove_url": "Remover URL",
   "remove_user": "Remover usuário",
   "removed_api_key": "Removido a Chave de API: {name}",
   "removed_from_archive": "Removido do arquivo",
   "removed_from_favorites": "Removido dos favoritos",
   "removed_from_favorites_count": "{count, plural, one {# Removido} other {# Removidos}} dos favoritos",
+  "removed_tagged_assets": "Tag removida de {count, plural, one {# arquivo} other {# arquivos}}",
   "rename": "Renomear",
   "repair": "Reparar",
   "repair_no_results_message": "Arquivos perdidos ou não rastreados aparecem aqui",
@@ -1087,7 +1073,6 @@
   "reset": "Resetar",
   "reset_password": "Resetar senha",
   "reset_people_visibility": "Resetar pessoas ocultas",
-  "reset_settings_to_default": "",
   "reset_to_default": "Redefinir para a configuração padrão",
   "resolve_duplicates": "Resolver duplicatas",
   "resolved_all_duplicates": "Todas duplicidades resolvidas",
@@ -1107,8 +1092,7 @@
   "saved_settings": "Configurações salvas",
   "say_something": "Diga algo",
   "scan_all_libraries": "Escanear Todas Bibliotecas",
-  "scan_all_library_files": "Re-escanear todos arquivos da biblioteca",
-  "scan_new_library_files": "Escanear novos arquivos na biblioteca",
+  "scan_library": "Analisar",
   "scan_settings": "Opções de escanear",
   "scanning_for_album": "Escaneando por álbum...",
   "search": "Pesquisar",
@@ -1123,8 +1107,10 @@
   "search_for_existing_person": "Pesquisar por pessoas",
   "search_no_people": "Nenhuma pessoa",
   "search_no_people_named": "Nenhuma pessoa chamada \"{name}\"",
+  "search_options": "Opções de pesquisa",
   "search_people": "Pesquisar pessoas",
   "search_places": "Pesquisar lugares",
+  "search_settings": "Configurações de pesquisa",
   "search_state": "Pesquisar estado...",
   "search_tags": "Procurar tags...",
   "search_timezone": "Pesquisar fuso horário...",
@@ -1149,7 +1135,6 @@
   "selected_count": "{count, plural, one {# selecionado} other {# selecionados}}",
   "send_message": "Enviar mensagem",
   "send_welcome_email": "Enviar E-mail de boas vindas",
-  "server": "Servidor",
   "server_offline": "Servidor Indisponível",
   "server_online": "Servidor Disponível",
   "server_stats": "Status do servidor",
@@ -1192,14 +1177,18 @@
   "show_person_options": "Exibir opções da pessoa",
   "show_progress_bar": "Exibir barra de progresso",
   "show_search_options": "Exibir opções de pesquisa",
+  "show_slideshow_transition": "Usar transições no modo de apresentação",
   "show_supporter_badge": "Insígnia de Contribuidor",
   "show_supporter_badge_description": "Mostrar a insígnia de contribuidor",
   "shuffle": "Aleatório",
   "sidebar": "Barra lateral",
+  "sidebar_display_description": "Exibir um link para visualizar na barra lateral",
   "sign_out": "Sair",
   "sign_up": "Registrar",
   "size": "Tamanho",
   "skip_to_content": "Pular para o conteúdo",
+  "skip_to_folders": "Ir para pastas",
+  "skip_to_tags": "Ir para as tags",
   "slideshow": "Apresentação",
   "slideshow_settings": "Opções de apresentação",
   "sort_albums_by": "Ordenar álbuns por...",
@@ -1230,25 +1219,37 @@
   "submit": "Enviar",
   "suggestions": "Sugestões",
   "sunrise_on_the_beach": "Nascer do sol na praia",
+  "support": "Ajuda",
+  "support_and_feedback": "Ajuda & Feedback",
+  "support_third_party_description": "Sua instalação do Immich é fornecida por terceiros. É possível que problemas sejam causados por eles, por isso, se tiver problemas, procure primeiro ajuda com eles utilizando os links abaixo.",
   "swap_merge_direction": "Alternar direção da mesclagem",
   "sync": "Sincronizar",
   "tag": "Tag",
+  "tag_assets": "Marcar com tag",
+  "tag_created": "Tag foi criada: {tag}",
+  "tag_feature_description": "Visualizar fotos e videos agrupados pelo tópico da tag",
+  "tag_not_found_question": "Não consegue encontrar a tag? <link>Crie uma tag nova aqui.</link>",
+  "tag_updated": "Tag foi atualizada: {tag}",
+  "tagged_assets": "{count, plural, one {# arquivo marcado} other {# arquivos marcados}} com a tag",
   "tags": "Tags",
   "template": "Modelo",
   "theme": "Tema",
   "theme_selection": "Selecionar tema",
   "theme_selection_description": "Defina automaticamente o tema como claro ou escuro com base na preferência do sistema do seu navegador",
-  "they_will_be_merged_together": "Eles serão combinados",
+  "they_will_be_merged_together": "Eles serão mesclados",
+  "third_party_resources": "Recursos de terceiros",
   "time_based_memories": "Memórias baseada no tempo",
+  "timeline": "Linha do tempo",
   "timezone": "Fuso horário",
   "to_archive": "Arquivar",
   "to_change_password": "Alterar senha",
   "to_favorite": "Favorito",
   "to_login": "Iniciar sessão",
+  "to_parent": "Voltar um nível acima",
   "to_trash": "Mover para a lixeira",
   "toggle_settings": "Alternar configurações",
   "toggle_theme": "Alternar tema escuro",
-  "toggle_visibility": "Alternar visibilidade",
+  "total": "Total",
   "total_usage": "Utilização total",
   "trash": "Lixeira",
   "trash_all": "Mover todos para o lixo",
@@ -1258,17 +1259,17 @@
   "trashed_items_will_be_permanently_deleted_after": "Os itens da lixeira serão deletados permanentemente após {days, plural, one {# dia} other {# dias}}.",
   "type": "Tipo",
   "unarchive": "Desarquivar",
-  "unarchived": "Restaurado do arquivo",
   "unarchived_count": "{count, plural, one {# desarquivado} other {# desarquivados}}",
   "unfavorite": "Remover favorito",
   "unhide_person": "Exibir pessoa",
   "unknown": "Desconhecido",
-  "unknown_album": "",
   "unknown_year": "Ano desconhecido",
   "unlimited": "Ilimitado",
+  "unlink_motion_video": "Remover relação com video animado",
   "unlink_oauth": "Desvincular OAuth",
   "unlinked_oauth_account": "Conta OAuth desvinculada",
   "unnamed_album": "Álbum sem nome",
+  "unnamed_album_delete_confirmation": "Tem certeza que deseja excluir este álbum?",
   "unnamed_share": "Compartilhamento sem nome",
   "unsaved_change": "Alteração não salva",
   "unselect_all": "Limpar seleção",
@@ -1293,13 +1294,13 @@
   "use_custom_date_range": "Usar intervalo de datas personalizado",
   "user": "Usuário",
   "user_id": "ID do usuário",
-  "user_license_settings": "Licença",
-  "user_license_settings_description": "Gerenciar sua licença",
   "user_liked": "{user} curtiu {type, select, photo {a foto} video {o vídeo} asset {o arquivo} other {isso}}",
   "user_purchase_settings": "Comprar",
   "user_purchase_settings_description": "Gerenciar sua compra",
   "user_role_set": "Definir {user} como {role}",
   "user_usage_detail": "Detalhes de uso do usuário",
+  "user_usage_stats": "Estatísticas de utilização de conta",
+  "user_usage_stats_description": "Ver estatísticas de utilização de conta",
   "username": "Nome do usuário",
   "users": "Usuários",
   "utilities": "Utilitários",
@@ -1307,7 +1308,9 @@
   "variables": "Variáveis",
   "version": "Versão",
   "version_announcement_closing": "De seu amigo, Alex",
-  "version_announcement_message": "Olá amigo! Uma nova versão do aplicativo está disponível. Para evitar configurações incorretas, por favor verifique com calma a página de <link>notas da versão</link> e certifique-se que os arquivos <code>docker-compose.yml</code> e <code>.env</code> estão configurados corretamente, principalmente se você usa o WatchTower ou qualquer outro mecanismo que faça atualizações automáticas.",
+  "version_announcement_message": "Olá! Uma nova versão do Immich está disponível. Para evitar configurações incorretas, leia com calma a página de <link>notas da versão</link> e verifique se é necessário alterar alguma configuração, principalmente se você usa o WatchTower ou qualquer outro mecanismo que faça atualizações automáticas do Immich.",
+  "version_history": "Histórico de versões",
+  "version_history_item": "Instalado {version} em {date}",
   "video": "Vídeo",
   "video_hover_setting": "Reproduzir miniatura do vídeo ao passar o mouse",
   "video_hover_setting_description": "Reproduzir a miniatura do vídeo ao passar o mouse sobre o item. Mesmo quando desativado, a reprodução pode ser iniciada ao passar o mouse sobre o ícone de reprodução.",
@@ -1319,10 +1322,10 @@
   "view_all_users": "Ver todos usuários",
   "view_in_timeline": "Ver na linha do tempo",
   "view_links": "Ver links",
+  "view_name": "Ver",
   "view_next_asset": "Ver próximo arquivo",
   "view_previous_asset": "Ver arquivo anterior",
   "view_stack": "Exibir Pilha",
-  "viewer": "Visualizar",
   "visibility_changed": "A visibilidade de {count, plural, one {# pessoa foi alterada} other {# pessoas foram alteradas}}",
   "waiting": "Aguardando",
   "warning": "Aviso",
diff --git a/i18n/ro.json b/i18n/ro.json
new file mode 100644
index 0000000000..878bf9dd67
--- /dev/null
+++ b/i18n/ro.json
@@ -0,0 +1,1340 @@
+{
+  "about": "Despre",
+  "account": "Cont",
+  "account_settings": "Setări Cont",
+  "acknowledge": "Văzut",
+  "action": "Acţiune",
+  "actions": "Acţiuni",
+  "active": "Activ",
+  "activity": "Activitate",
+  "activity_changed": "Activitatea este {enabled, select, true {activată} other {dezactivată}}",
+  "add": "Adaugă",
+  "add_a_description": "Adaugă o descriere",
+  "add_a_location": "Adaugă locație",
+  "add_a_name": "Adaugă un nume",
+  "add_a_title": "Adaugă un titlu",
+  "add_exclusion_pattern": "Adăugă un model de excludere",
+  "add_import_path": "Adaugă o cale de import",
+  "add_location": "Adaugă o locație",
+  "add_more_users": "Adaugă mai mulți utilizatori",
+  "add_partner": "Adaugă partener",
+  "add_path": "Adaugă o cale",
+  "add_photos": "Adaugă fotografii",
+  "add_to": "Adaugă la...",
+  "add_to_album": "Adaugă în album",
+  "add_to_shared_album": "Adaugă la album partajat",
+  "add_url": "Adăugați adresa URL",
+  "added_to_archive": "Adăugat la arhivă",
+  "added_to_favorites": "Adaugă la favorite",
+  "added_to_favorites_count": "Adăugat {count, number} la favorite",
+  "admin": {
+    "add_exclusion_pattern_description": "Adăugați modele de excludere. Globing folosind *, ** și ? este suportat. Pentru a ignora toate fișierele din orice director numit „Raw”, utilizați „**/Raw/**”. Pentru a ignora toate fișierele care se termină în „.tif”, utilizați „**/*.tif”. Pentru a ignora o cale absolută, utilizați „/path/to/ignore/**”.",
+    "asset_offline_description": "Acest material din biblioteca externă nu se mai găsește pe disc și a fost mutat în coșul de gunoi. Dacă fișierul a fost mutat în bibliotecă, verificați cronologia pentru noul material corespunzător. Pentru a restabili acest material, asigurați-vă că calea fișierului de mai jos poate fi accesată de Immich și scanați biblioteca.",
+    "authentication_settings": "Setări de Autentificare",
+    "authentication_settings_description": "Gestionează parola, OAuth și alte setări de autentificare",
+    "authentication_settings_disable_all": "Ești sigur că vrei sa dezactivezi toate metodele de autentificare? Autentificarea va fi complet dezactivată.",
+    "authentication_settings_reenable": "Pentru a reactiva, folosește <link>Comandă Server</link>.",
+    "background_task_job": "Activități de Fundal",
+    "backup_database": "Salvare Bază de Date",
+    "backup_database_enable_description": "Activare salvare bază de date",
+    "backup_keep_last_amount": "Cantitatea de copii de rezervă anterioare de păstrat",
+    "backup_settings": "Setări Copii de Rezervă",
+    "backup_settings_description": "Gestionați setările de salvare a bazei de date",
+    "check_all": "Bifează Toate",
+    "cleared_jobs": "Activități eliminate pentru: {job}",
+    "config_set_by_file": "Configurația este setată în prezent de un fișier de configurare",
+    "confirm_delete_library": "Sigur doriți să ștergeți biblioteca {library}?",
+    "confirm_delete_library_assets": "Sigur doriți să ștergeți această bibliotecă? Aceasta va șterge {count, plural, one {# contained asset} other {all # contained assets}} din Immich și nu poate fi anulată. Fișierele vor rămâne pe disc.",
+    "confirm_email_below": "Pentru a confirma, tastați „{email}” mai jos",
+    "confirm_reprocess_all_faces": "Sigur doriți să reprocesați toate fețele? Acest lucru va șterge și persoanele cu nume.",
+    "confirm_user_password_reset": "Sigur doriți să resetați parola utilizatorului {user}?",
+    "create_job": "Creează sarcină",
+    "cron_expression": "Expresia cron",
+    "cron_expression_description": "Setați intervalul de scanare folosind formatul cron. Pentru mai multe informații, consultați de ex. <link>Crontab Guru</link>",
+    "cron_expression_presets": "Presetări de expresie cron",
+    "disable_login": "Dezactivați autentificarea",
+    "duplicate_detection_job_description": "Rulați învățarea automată pe materiale pentru a detecta imagini similare. Se bazează pe Căutare Inteligentă",
+    "exclusion_pattern_description": "Modelele de excludere vă permit să ignorați fișierele și folderele atunci când vă scanați biblioteca. Acest lucru este util dacă aveți foldere care conțin fișiere pe care nu doriți să le importați, cum ar fi fișierele RAW.",
+    "external_library_created_at": "Bibliotecă externă (creată pe {date})",
+    "external_library_management": "Managementul Bibliotecii Externe",
+    "face_detection": "Detecție facială",
+    "face_detection_description": "Detectează fețele din fișiere folosind învățare automată. Pentru videoclipuri, este luată în considerare doar miniatura. „Reînprospătează” (re)procesează toate fișierele. „Resetează” adaugă în coadă fișierele care nu au fost încă procesate. Fețele detectate vor fi puse în coadă pentru recunoașterea facială după finalizarea detectării feței, grupându-le în persoane existente sau noi.",
+    "facial_recognition_job_description": "Grupați fețele detectate în persoane. Acest pas rulează după ce Detectarea Feței este finalizată. „Resetează” (re)grupează toate fețele. „Lipsă” adaugă în coadă fețe care nu au o persoană desemnată.",
+    "failed_job_command": "Comanda {command} a eșuat pentru jobul: {job}",
+    "force_delete_user_warning": "AVERTISMENT: Acest lucru va elimina imediat utilizatorul și toate activele sale. Acest lucru nu poate fi anulat și fișierele nu pot fi recuperate.",
+    "forcing_refresh_library_files": "Forțarea reîmprospătării tuturor fișierelor din bibliotecă",
+    "image_format": "Formateaza",
+    "image_format_description": "WebP produce fișiere mai mici decât JPEG, dar este mai lent de codat.",
+    "image_prefer_embedded_preview": "Preferați previzualizarea încorporată",
+    "image_prefer_embedded_preview_setting_description": "Folosiți previzualizările încorporate în fotografiile RAW ca intrare pentru procesarea imaginii, atunci când sunt disponibile. Acest lucru poate produce culori mai precise pentru unele imagini, dar calitatea previzualizării depinde de cameră și imaginea poate avea mai multe artefacte de compresie.",
+    "image_prefer_wide_gamut": "Preferă o gamă largă",
+    "image_prefer_wide_gamut_setting_description": "Utilizați Display P3 pentru miniaturi. Acest lucru păstrează mai bine vibrația imaginilor cu spații de culoare largi, dar imaginile pot apărea diferit pe dispozitivele cu o versiune mai veche de browser. Imaginile sRGB sunt păstrate ca sRGB pentru a evita schimbările de culoare.",
+    "image_preview_description": "Imagine de dimensiune medie cu metadate eliminate, utilizată la vizualizarea unui singur element și pentru învățarea automată",
+    "image_preview_quality_description": "Calitatea previzualizării de la 1 la 100. O valoare mai mare oferă o calitate mai bună, dar produce fișiere mai mari și poate reduce receptivitatea aplicației. Setarea unei valori scăzute poate afecta calitatea învățării automate.",
+    "image_preview_title": "Previzualizați Setările",
+    "image_quality": "Calitate",
+    "image_resolution": "Rezolutie",
+    "image_resolution_description": "Rezoluțiile mai mari pot păstra mai multe detalii, dar necesită mai mult timp pentru a fi codificate, au dimensiuni mai mari ale fișierelor și pot reduce răspunsul aplicației.",
+    "image_settings": "Setări Imagine",
+    "image_settings_description": "Gestionează calitatea și rezoluția imaginilor generate",
+    "image_thumbnail_description": "Miniatură mică cu metadate eliminate, utilizată la vizualizarea grupurilor de fotografii, cum ar fi în cronologia principală",
+    "image_thumbnail_quality_description": "Calitatea miniaturii de la 1 la 100. O valoare mai mare oferă o calitate mai bună, dar produce fișiere mai mari și poate reduce receptivitatea aplicației.",
+    "image_thumbnail_title": "Setari Miniaturi",
+    "job_concurrency": "Concurență {job}",
+    "job_created": "Sarcină creată",
+    "job_not_concurrency_safe": "Această sarcină nu este sigură pentru a rula în concurență.",
+    "job_settings": "Setări Sarcină",
+    "job_settings_description": "Administrează concurența sarcinilor",
+    "job_status": "Starea Sarcinii",
+    "jobs_delayed": "{jobCount, plural, other {# întârziat}}",
+    "jobs_failed": "{jobCount, plural, other {# eșuat}}",
+    "library_created": "Librărie creată:{library}",
+    "library_deleted": "Bibliotecă ștearsă",
+    "library_import_path_description": "Specificați un folder pentru a îl importa. Acest folder, inclusiv sub-folderele, vor fi scanate pentru imagini și videoclipuri.",
+    "library_scanning": "Scanare Periodică",
+    "library_scanning_description": "Configurează scanarea periodică pentru bibliotecă",
+    "library_scanning_enable_description": "Activează scanarea periodică pentru bibliotecă",
+    "library_settings": "Bibliotecă Externă",
+    "library_settings_description": "Administrează setările pentru biblioteci externe",
+    "library_tasks_description": "Efectuează sarcini asupra bibliotecii",
+    "library_watching_enable_description": "Urmărește bibliotecile externe pentru schimbări ale fișierelor",
+    "library_watching_settings": "Urmărirea bibliotecii (EXPERIMENTAL)",
+    "library_watching_settings_description": "Urmărește automat fișierele schimbate",
+    "logging_enable_description": "Activează înregistrarea log-urilor",
+    "logging_level_description": "Dacă setarea este activată, înregistrează evenimentele cu nivelul de utilizat.",
+    "logging_settings": "Înregistrare",
+    "machine_learning_clip_model": "Model CLIP",
+    "machine_learning_clip_model_description": "Numele unui model CLIP listat <link>aici</link>. Rețineți că trebuie să rulați din nou funcția „Smart Search” pentru toate imaginile la schimbarea unui model.",
+    "machine_learning_duplicate_detection": "Detectare Duplicate",
+    "machine_learning_duplicate_detection_enabled": "Activează detectarea duplicatelor",
+    "machine_learning_duplicate_detection_enabled_description": "Dacă este dezactivată, elementele identice vor fi în continuare de-duplicate.",
+    "machine_learning_duplicate_detection_setting_description": "Utilizați încorporările CLIP pentru a găsi dubluri probabile",
+    "machine_learning_enabled": "Activează algoritmii de învățare automată",
+    "machine_learning_enabled_description": "Dacă este dezactivat, toate funcțiile ML vor fi dezactivate indiferent de setările de mai jos.",
+    "machine_learning_facial_recognition": "Recunoaștere Facială",
+    "machine_learning_facial_recognition_description": "Detectează, recunoaște și grupează fețe din imagini",
+    "machine_learning_facial_recognition_model": "Model de recunoaștere facială",
+    "machine_learning_facial_recognition_model_description": "Modelele sunt aranjate descrescător după mărime. Modelele mai mari sunt lente și folosesc multă memorie, dar produc rezultate mai bune. Rețineți că va trebui să rulați din nou recunoașterea facială pentru toate imaginile dacă schimbați modelul.",
+    "machine_learning_facial_recognition_setting": "Activează recunoașterea facială",
+    "machine_learning_facial_recognition_setting_description": "Dacă este dezactivată, imaginile nu vor fi codificate pentru recunoașterea facială și nu vor popula secțiunea persoane din pagina explorare.",
+    "machine_learning_max_detection_distance": "Distanța maximă pentru recunoaștere",
+    "machine_learning_max_detection_distance_description": "Distanța maximă dintre două imagini pentru a le considera duplicate, variind între 0,001-0,1. Valorile mai mari vor detecta mai multe duplicate, dar pot duce la rezultate fals pozitive.",
+    "machine_learning_max_recognition_distance": "Distanța maximă de recunoaștere",
+    "machine_learning_max_recognition_distance_description": "Distanța maximă dintre două fețe pentru a fi considerate aceeași persoană, variind între 0-2. Reducerea acestui prag poate împiedica etichetarea a două persoane ca fiind aceeași persoană, în timp ce creșterea lui poate împiedica etichetarea aceleiași persoane ca fiind două persoane diferite. Rețineți că este mai ușor să unificați două persoane decât să împărțiți o persoană în două, deci, dacă este posibil, alegeți un prag mai mic.",
+    "machine_learning_min_detection_score": "Scor minim de detecție",
+    "machine_learning_min_detection_score_description": "Scorul minim de încredere pentru ca o față să fie detectată de la 0 la 1. Valorile mai mici vor detecta mai multe fețe, dar pot duce la fals pozitive.",
+    "machine_learning_min_recognized_faces": "Fețe minim recunoscute",
+    "machine_learning_min_recognized_faces_description": "Numărul minim de fețe recunoscute pentru ca o persoană să fie creată. Creșterea acestui număr face ca recunoașterea facială să fie mai precisă, cu prețul creșterii șanselor ca o față să nu fie atribuită unei persoane.",
+    "machine_learning_settings": "Setări de învățare automată",
+    "machine_learning_settings_description": "Gestionați caracteristicile și setările de învățare automată",
+    "machine_learning_smart_search": "Căutare inteligentă",
+    "machine_learning_smart_search_description": "Căutarea semantică a imaginilor utilizând încorporările CLIP",
+    "machine_learning_smart_search_enabled": "Activați căutarea inteligentă",
+    "machine_learning_smart_search_enabled_description": "Dacă este dezactivată, imaginile nu vor fi codificate pentru căutarea inteligentă.",
+    "machine_learning_url_description": "Adresa URL a serverului de învățare automată. Dacă sunt furnizate mai multe adrese URL, fiecare server va fi încercat unul câte unul până când unul răspunde cu succes, în ordine de la primul până la ultimul.",
+    "manage_concurrency": "Gestionarea Simultaneității",
+    "manage_log_settings": "Administrați setările jurnalului",
+    "map_dark_style": "Mod întunecat",
+    "map_enable_description": "Activați funcțiile hărții",
+    "map_gps_settings": "Setări Hartă & GPS",
+    "map_gps_settings_description": "Gestionare setări Hartă & GPS (localizare inversă)",
+    "map_implications": "Caracteristica hărții se bazează pe un serviciu extern de planșe (tiles.immich.cloud)",
+    "map_light_style": "Mod deschis",
+    "map_manage_reverse_geocoding_settings": "Gestionare setări <link>Localizare Inversă</link>",
+    "map_reverse_geocoding": "Localizare inversă",
+    "map_reverse_geocoding_enable_description": "Activați geocodarea inversă",
+    "map_reverse_geocoding_settings": "Setări geocodare inversă",
+    "map_settings": "Hartă",
+    "map_settings_description": "Gestionare setări hartă",
+    "map_style_description": "URL-ul style.json către o temă pentru hartă",
+    "metadata_extraction_job": "Extrageți metadatele",
+    "metadata_extraction_job_description": "Extragere informații metadate din fiecare fișier cum ar fi localizare GPS, fețe și rezoluție,",
+    "metadata_faces_import_setting": "Activare import fețe",
+    "metadata_faces_import_setting_description": "Importă fețe din datele EXIF ale imaginii și din fișiere tip \"sidecar\"",
+    "metadata_settings": "Setări Metadate",
+    "metadata_settings_description": "Gestionează setările pentru metadate",
+    "migration_job": "Migrare",
+    "migration_job_description": "Migrați miniaturile pentru elemente și fețe la cea mai recentă structură de foldere",
+    "no_paths_added": "Nicio cale adăugată",
+    "no_pattern_added": "Niciun tipar adăugat",
+    "note_apply_storage_label_previous_assets": "Notă: Pentru a aplica Eticheta de Stocare la elementele încărcate anterior, executați",
+    "note_cannot_be_changed_later": "NOTĂ: Nu se va mai putea modifica ulterior!",
+    "note_unlimited_quota": "Notă: Introduceți 0 pentru spațiu nelimitat",
+    "notification_email_from_address": "De la adresa",
+    "notification_email_from_address_description": "Adresa expeditorului, spre exemplu: „Immich Photo Server <noreply@example.com>”",
+    "notification_email_host_description": "Adresa serverului de email (ex. smtp.immich.app)",
+    "notification_email_ignore_certificate_errors": "Ingnoră erorile de certificat",
+    "notification_email_ignore_certificate_errors_description": "Ignoră erorile de validare a certificatului TLS (nerecomandat)",
+    "notification_email_password_description": "Parola utilizată pentru autentificarea în serverul de email",
+    "notification_email_port_description": "Portul utilizat de serverul de email (ex. 25, 465 sau 587)",
+    "notification_email_sent_test_email_button": "Trimite un email de test și salvează configurația",
+    "notification_email_setting_description": "Setări pentru trimiterea de notificări pe email",
+    "notification_email_test_email": "Trimitere email de test",
+    "notification_email_test_email_failed": "Eroare la trimiterea emailului de test, verificați setările",
+    "notification_email_test_email_sent": "Un email de test a fost trimis la adresa {email}. Vă rugăm să vă verificați căsuța de e-mail.",
+    "notification_email_username_description": "Numele de utilizator pentru autentificarea pe serverul de email",
+    "notification_enable_email_notifications": "Activare notificări pe email",
+    "notification_settings": "Setări notificare",
+    "notification_settings_description": "Gestionează setările pentru notificări, inclusiv adresa de email",
+    "oauth_auto_launch": "Pornire automată",
+    "oauth_auto_launch_description": "Lansează automat autorizarea OAuth la accesarea paginii de login",
+    "oauth_auto_register": "Auto înregistrare",
+    "oauth_auto_register_description": "Înregistrează automat utilizatori noi după autentificarea cu OAuth",
+    "oauth_button_text": "Text buton",
+    "oauth_client_id": "ID Client",
+    "oauth_client_secret": "Secret client",
+    "oauth_enable_description": "Autentifică-te cu OAuth",
+    "oauth_issuer_url": "Emitentul URL",
+    "oauth_mobile_redirect_uri": "URI de redirecționare mobilă",
+    "oauth_mobile_redirect_uri_override": "Înlocuire URI de redirecționare mobilă",
+    "oauth_mobile_redirect_uri_override_description": "Activați atunci când furnizorul OAuth nu permite un URI mobil, precum '{callback}'",
+    "oauth_profile_signing_algorithm": "Algoritm de semnare a profilului",
+    "oauth_profile_signing_algorithm_description": "Algoritm folosit pentru a semna profilul utilizatorului.",
+    "oauth_scope": "Domeniul de aplicare",
+    "oauth_settings": "OAuth",
+    "oauth_settings_description": "Gestionați setările de conectare OAuth",
+    "oauth_settings_more_details": "Pentru mai multe detalii despre aceastǎ funcționalitate, verificǎ <link>documentația</link>.",
+    "oauth_signing_algorithm": "Algoritm de semnare",
+    "oauth_storage_label_claim": "Revendicare eticheta de stocare",
+    "oauth_storage_label_claim_description": "Setați automat eticheta de stocare a utilizatorului la valoarea acestei revendicări.",
+    "oauth_storage_quota_claim": "Revendicare spațiu de stocare",
+    "oauth_storage_quota_claim_description": "Setează automat spațiul de stocare al utilizatorului la valoarea acestei cereri.",
+    "oauth_storage_quota_default": "Cota implicită a spațiului de stocare (GiB)",
+    "oauth_storage_quota_default_description": "Spațiul în GiB ce urmează a fi utilizat atunci când nu este furnizată nicio solicitare (introduceți 0 pentru spațiu nelimitat).",
+    "offline_paths": "Căi Offline",
+    "offline_paths_description": "Acestea pot fi rezultate în urma ștergerii manuale a fișierelor ce nu fac parte dintr-o bibliotecǎ externǎ.",
+    "password_enable_description": "Autentificare cu email și parolǎ",
+    "password_settings": "Autentificare cu Parolǎ",
+    "password_settings_description": "Gestioneazǎ setǎrile de autentificare cu parola",
+    "paths_validated_successfully": "Toate cǎile au fost validate cu succes",
+    "person_cleanup_job": "Ștergere persoane",
+    "quota_size_gib": "Spațiu de stocare alocat (GiB)",
+    "refreshing_all_libraries": "Bibliotecile sunt în curs de reîmprospǎtare",
+    "registration": "Înregistrare Administratori",
+    "registration_description": "Deoarece sunteți primul utilizator de pe sistem, veți fi desemnat ca administrator și sunteți responsabil pentru sarcinile administrative, iar utilizatorii suplimentari vor fi creați de dumneavoastră.",
+    "repair_all": "Reparǎ Toate",
+    "repair_matched_items": "S-au potrivit {count, plural, one {# element} other {# elemente}}",
+    "repaired_items": "S-au reparat {count, plural, one {# element} other {# elemente}}",
+    "require_password_change_on_login": "Obligǎ utilizatorul sǎ își schimbe parola la prima autentificare",
+    "reset_settings_to_default": "Reseteazǎ setǎrile la valorile implicite",
+    "reset_settings_to_recent_saved": "Reseteazǎ setǎrile la valorile salvate recent",
+    "scanning_library": "Se scanează biblioteca",
+    "search_jobs": "Caută sarcini...",
+    "send_welcome_email": "Trimite email de bun-venit",
+    "server_external_domain_settings": "Domeniu extern",
+    "server_external_domain_settings_description": "Domeniu pentru distribuire publicǎ a scurtǎturilor, incluzând http(s)://",
+    "server_public_users": "Utilizatori Publici",
+    "server_public_users_description": "Toți utilizatorii (nume și e-mail) sunt listați atunci când adăugați un utilizator la albumele partajate. Când este dezactivată, lista de utilizatori va fi disponibilă numai pentru utilizatorii admin.",
+    "server_settings": "Setǎri Server",
+    "server_settings_description": "Gestioneazǎ setǎrile serverului",
+    "server_welcome_message": "Mesaj de bun-venit",
+    "server_welcome_message_description": "Un mesaj ce este afișat pe pagina de autentificare.",
+    "sidecar_job": "Metadate sidecar",
+    "sidecar_job_description": "Descoperirea sau sincronizarea metadatelor sidecar din sistemul de fișiere",
+    "slideshow_duration_description": "Numǎrul de secunde pentru afișarea fiecǎrei imagini",
+    "smart_search_job_description": "Rulați învățarea automată pe elemente pentru a ajuta căutarea inteligentă",
+    "storage_template_date_time_description": "Momentul creării elementului este utilizat pentru informațiile privind data și ora",
+    "storage_template_date_time_sample": "Eșantion de timp {date}",
+    "storage_template_enable_description": "Activați motorul de șabloane de stocare",
+    "storage_template_hash_verification_enabled": "Verificarea hash este activată",
+    "storage_template_hash_verification_enabled_description": "Activează verificarea hash, nu o dezactivați decât dacă sunteți sigur de implicații",
+    "storage_template_migration": "Migrarea șablonului de stocare",
+    "storage_template_migration_description": "Aplicați <link>{template}</link> actual la elementele încărcate anterior",
+    "storage_template_migration_info": "Modificările de șablon se vor aplica numai materialelor noi. Pentru a aplica retroactiv șablonul la materialele încărcate anterior, rulați <link>{job}</link>.",
+    "storage_template_migration_job": "Sarcină Migrare Șablon Stocare",
+    "storage_template_more_details": "Pentru mai multe detalii despre aceasta caracteristică, accesați <template-link>Șablon stocare</template-link> si <implications-link>implicațiile</implications-link>",
+    "storage_template_onboarding_description": "Atunci când este activată, această caracteristică va organiza automat fișierele pe baza unui șablon definit de utilizator. Din cauza unor probleme de stabilitate, această caracteristică este dezactivată implicit. Pentru mai multe informații, te rog să consulți <link>documentația</link>.",
+    "storage_template_path_length": "Limita de lungime pentru calea aproximativă: <b>{length, number}</b>/{limit, number}",
+    "storage_template_settings": "Șablon Stocare",
+    "storage_template_settings_description": "Gestionează structura folderelor și numele fișierelor pentru elementele încărcate",
+    "storage_template_user_label": "<code>{label}</code> este eticheta de stocare a utilizatorului",
+    "system_settings": "Setǎri de Sistem",
+    "tag_cleanup_job": "Curățare etichete",
+    "template_email_available_tags": "Puteți utiliza următoarele variabile în șablonul dvs.: {tags}",
+    "template_email_if_empty": "Dacă șablonul este gol, va fi folosit e-mailul implicit.",
+    "template_email_invite_album": "Șablon de Album de Invitație",
+    "template_email_preview": "Previzualizare",
+    "template_email_settings": "Șabloane de E-mail",
+    "template_email_settings_description": "Gestionați șabloanele personalizate de notificare prin e-mail",
+    "template_email_update_album": "Actualizați Șablonul de Album",
+    "template_email_welcome": "Șablon de e-mail de bun venit",
+    "template_settings": "Șabloane de Notificare",
+    "template_settings_description": "Gestionați șabloanele personalizate pentru notificări.",
+    "theme_custom_css_settings": "CSS personalizat",
+    "theme_custom_css_settings_description": "Foile de stil în cascadă (CSS) permit personalizarea designului Immich.",
+    "theme_settings": "Setări Temă",
+    "theme_settings_description": "Gestionează personalizarea interfeței web Immich",
+    "these_files_matched_by_checksum": "Aceste fișiere sunt comparate folosind sumele de control",
+    "thumbnail_generation_job": "Generare Miniaturi",
+    "thumbnail_generation_job_description": "Generează miniaturi mari, mici și estompate pentru fiecare resursă, precum și miniaturi pentru fiecare persoană",
+    "transcoding_acceleration_api": "API de accelerare",
+    "transcoding_acceleration_api_description": "API-ul care va interacționa cu dispozitivul tău pentru a accelera transcodarea. Această setare este 'cel mai bun efort': va reveni la transcodarea software în caz de eșec. VP9 poate funcționa sau nu, în funcție de hardware-ul tău.",
+    "transcoding_acceleration_nvenc": "NVENC (necesitǎ GPU NVIDIA)",
+    "transcoding_acceleration_qsv": "Sincronizare Rapidă (necesitǎ CPU Intel de generația a 7-a sau mai mare)",
+    "transcoding_acceleration_rkmpp": "RKMPP (doar pe SOC-uri Rockchip)",
+    "transcoding_acceleration_vaapi": "VAAPI",
+    "transcoding_accepted_audio_codecs": "Codecuri audio acceptate",
+    "transcoding_accepted_audio_codecs_description": "Selectează care codecuri audio nu trebuie să fie transcodificate. Se utilizează doar pentru anumite politici de transcodare.",
+    "transcoding_accepted_containers": "Containere acceptate",
+    "transcoding_accepted_containers_description": "Selectează formatele de containere care nu trebuie să fie remixate în MP4. Se utilizează doar pentru anumite politici de transcodare.",
+    "transcoding_accepted_video_codecs": "Codecuri video acceptate",
+    "transcoding_accepted_video_codecs_description": "Selectează codecurile video care nu trebuie să fie transcodificate. Se utilizează doar pentru anumite politici de transcodare.",
+    "transcoding_advanced_options_description": "Opțiuni pe care majoritatea utilizatorilor nu ar trebui să fie necesar să le schimbe",
+    "transcoding_audio_codec": "Codec audio",
+    "transcoding_audio_codec_description": "Opus este opțiunea cu cea mai bună calitate, dar are o compatibilitate mai scăzută cu dispozitivele sau software-ul mai vechi.",
+    "transcoding_bitrate_description": "Videoclipuri cu rata de biți mai mare decât maximul acceptat sau care nu sunt într-un format acceptat",
+    "transcoding_codecs_learn_more": "Pentru a afla mai multe despre terminologia folosită aici, consultă documentația FFmpeg pentru <h264-link>codec-ul H.264</h264-link>, <hevc-link>codec-ul HEVC</hevc-link> și <vp9-link>codec-ul VP9</vp9-link>.",
+    "transcoding_constant_quality_mode": "Mod de calitate constantă",
+    "transcoding_constant_quality_mode_description": "ICQ este mai bun decât CQP, dar unele dispozitive de accelerare hardware nu suportă acest mod. Setarea acestei opțiuni va prefera modul specificat atunci când folosești codificarea bazată pe calitate. Ignorat de NVENC deoarece nu suportă ICQ.",
+    "transcoding_constant_rate_factor": "Factor de rată constantă (-crf)",
+    "transcoding_constant_rate_factor_description": "Nivelul de calitate al videoclipului. Valorile tipice sunt 23 pentru H.264, 28 pentru HEVC, 31 pentru VP9 și 35 pentru AV1. Cu cât valoarea este mai mică, cu atât calitatea este mai bună, dar se generează fișiere mai mari.",
+    "transcoding_disabled_description": "Nu transcodifică niciun videoclip; acest lucru poate afecta redarea pe anumite dispozitive",
+    "transcoding_hardware_acceleration": "Accelerare Hardware",
+    "transcoding_hardware_acceleration_description": "Experimental; mult mai rapid, dar va avea o calitate mai scăzută la același bitrate",
+    "transcoding_hardware_decoding": "Decodare hardware",
+    "transcoding_hardware_decoding_setting_description": "Se aplică doar pentru NVENC, QSV și RKMPP. Activează accelerarea completă în loc de doar accelerarea codificării. S-ar putea să nu funcționeze pentru toate videoclipurile.",
+    "transcoding_hevc_codec": "Codec HEVC",
+    "transcoding_max_b_frames": "Număr maxim de cadre B",
+    "transcoding_max_b_frames_description": "Valorile mai mari îmbunătățesc eficiența compresiei, dar încetinesc codarea. Este posibil să nu fie compatibile cu accelerarea hardware pe dispozitivele mai vechi. 0 dezactivează cadrele B, în timp ce -1 setează această valoare automat.",
+    "transcoding_max_bitrate": "Rata de biți maximă",
+    "transcoding_max_bitrate_description": "Setarea unei rate maxime de biți poate face dimensiunile fișierelor mai previzibile, cu un cost minor asupra calității. La 720p, valorile tipice sunt 2600k pentru VP9 sau HEVC, sau 4500k pentru H.264. Dezactivat dacă este setat la 0.",
+    "transcoding_max_keyframe_interval": "Interval maxim între cadre cheie",
+    "transcoding_max_keyframe_interval_description": "Setează distanța maximă între cadrele cheie. Valorile mai mici reduc eficiența compresiei, dar îmbunătățesc timpii de căutare și pot îmbunătăți calitatea în scenele cu mișcare rapidă. 0 setează această valoare automat.",
+    "transcoding_optimal_description": "Videoclipuri cu rezoluție mai mare decât cea țintă sau care nu sunt într-un format acceptat",
+    "transcoding_preferred_hardware_device": "Dispozitiv hardware preferat",
+    "transcoding_preferred_hardware_device_description": "Se aplică doar la VAAPI și QSV. Setează nodul DRI utilizat pentru transcodarea hardware.",
+    "transcoding_preset_preset": "Presetare (-preset)",
+    "transcoding_preset_preset_description": "Viteza de compresie. Presetările mai lente produc fișiere mai mici și îmbunătățesc calitatea atunci când vizezi o anumită rată de biți. VP9 ignoră vitezele de compresie mai mari decât 'mai rapid'.",
+    "transcoding_reference_frames": "Cadre de referință",
+    "transcoding_reference_frames_description": "Numărul de cadre de referință atunci când se comprimă un cadru dat. Valorile mai mari îmbunătățesc eficiența compresiei, dar încetinesc codarea. 0 setează această valoare automat.",
+    "transcoding_required_description": "Numai videoclipuri care nu sunt într-un format acceptat",
+    "transcoding_settings": "Setări de Transcodare Video",
+    "transcoding_settings_description": "Gestionează rezoluția și informațiile de codare ale fișierelor video",
+    "transcoding_target_resolution": "Rezoluția țintă",
+    "transcoding_target_resolution_description": "Rezoluțiile mai mari pot păstra mai multe detalii, dar necesită mai mult timp pentru codare, au dimensiuni mai mari ale fișierelor și pot reduce răspunsul aplicației.",
+    "transcoding_temporal_aq": "AQ temporal",
+    "transcoding_temporal_aq_description": "Se aplică doar la NVENC. Îmbunătățește calitatea scenelor cu detalii mari și mișcare redusă. Poate să nu fie compatibil cu dispozitivele mai vechi.",
+    "transcoding_threads": "Fire",
+    "transcoding_threads_description": "Valorile mai mari conduc la o codare mai rapidă, dar lasă mai puțin spațiu serverului pentru a procesa alte sarcini în timp ce este activ. Această valoare nu ar trebui să fie mai mare decât numărul de nuclee CPU. Maximizați utilizarea dacă este setat la 0.",
+    "transcoding_tone_mapping": "Mapare tonuri",
+    "transcoding_tone_mapping_description": "Încearcă să păstreze aspectul videoclipurilor HDR atunci când sunt convertite în SDR. Fiecare algoritm face compromisuri diferite pentru culoare, detalii și strălucire. Hable păstrează detaliile, Mobius păstrează culoarea, iar Reinhard păstrează strălucirea.",
+    "transcoding_transcode_policy": "Politica de transcodare",
+    "transcoding_transcode_policy_description": "Politica pentru momentul când un videoclip ar trebui să fie transcodificat. Videoclipurile HDR vor fi întotdeauna transcodificate (cu excepția cazului în care transcodarea este dezactivată).",
+    "transcoding_two_pass_encoding": "Codare în doi pași",
+    "transcoding_two_pass_encoding_setting_description": "Transcodificare în două treceri pentru a produce videoclipuri codificate mai bine. Când rata maximă de biți este activată (necesară pentru a funcționa cu H.264 și HEVC), acest mod utilizează un interval de rată de biți bazat pe rata maximă de biți și ignoră CRF. Pentru VP9, CRF poate fi utilizat dacă rata maximă de biți este dezactivată.",
+    "transcoding_video_codec": "Codec Video",
+    "transcoding_video_codec_description": "VP9 are eficiențǎ mare și compatibilitate web, însǎ transcodarea este de duratǎ mai mare. HEVC se comportǎ asemǎnǎtor, însǎ are compatibilitate web mai micǎ. H.264 este foarte compatibil și rapid în transcodare, însǎ genereazǎ fișiere mult mai mari. AV1 este cel mai eficient codec dar nu este compatibil cu dispozitivele mai vechi.",
+    "trash_enabled_description": "Activează funcțiile Coșului de Gunoi",
+    "trash_number_of_days": "Numǎr de zile",
+    "trash_number_of_days_description": "Numǎr de zile pentru pǎstrarea fișierelor în coșul de gunoi pânǎ la ștergerea permanentǎ",
+    "trash_settings": "Setǎri Coș de Gunoi",
+    "trash_settings_description": "Gestioneazǎ setǎrile coșului de gunoi",
+    "untracked_files": "Fișiere Neurmărite",
+    "untracked_files_description": "Aceste fișiere nu sunt urmărite de aplicație. Ele pot fi rezultatul unor mutări eșuate, încărcări întrerupte sau pot rămâne în urmă din cauza unei erori",
+    "user_cleanup_job": "Curățare utilizator",
+    "user_delete_delay": "Contul și resursele utilizatorului <b>{user}</b> vor fi programate pentru ștergere permanentă în {delay, plural, one {# zi} other {# zile}}.",
+    "user_delete_delay_settings": "Întârziere la ștergere",
+    "user_delete_delay_settings_description": "Numărul de zile după eliminare până la ștergerea permanentă a contului și a resurselor unui utilizator. Procesul de ștergere a utilizatorului rulează la miezul nopții pentru a verifica utilizatorii care sunt pregătiți pentru ștergere. Modificările aduse acestei setări vor fi evaluate la următoarea execuție.",
+    "user_delete_immediately": "Contul și resursele utilizatorului <b>{user}</b> vor fi puse în coadă pentru ștergere permanentă <b>imediat</b>.",
+    "user_delete_immediately_checkbox": "Pune utilizatorul și resursele în coadă pentru ștergere imediată",
+    "user_management": "Gestionarea Utilizatorilor",
+    "user_password_has_been_reset": "Parola utilizatorului a fost resetată:",
+    "user_password_reset_description": "Vă rugăm să furnizați utilizatorului parola temporară și să îi informați că va trebui să o schimbe la următoarea autentificare.",
+    "user_restore_description": "Contul utilizatorului <b>{user}</b> va fi restaurat.",
+    "user_restore_scheduled_removal": "Restaurare utilizator - ștergere programată pe {date, date, long}",
+    "user_settings": "Setǎri Utilizator",
+    "user_settings_description": "Gestioneazǎ setǎrile utilizatorului",
+    "user_successfully_removed": "Utilizatorul {email} a fost eliminat cu succes.",
+    "version_check_enabled_description": "Activează verificarea versiunii",
+    "version_check_implications": "Funcția de verificare a versiunii se bazează pe comunicarea periodică cu github.com",
+    "version_check_settings": "Verificare Versiune",
+    "version_check_settings_description": "Activeazǎ/dezactiveazǎ notificarea unei noi versiuni",
+    "video_conversion_job": "Transcodați videoclipuri",
+    "video_conversion_job_description": "Transcodați videoclipurile pentru o compatibilitate mai mare cu browserele și dispozitivele"
+  },
+  "admin_email": "E-mail Administrator",
+  "admin_password": "Parolă Administrator",
+  "administration": "Administrare",
+  "advanced": "Avansat",
+  "age_months": "Vârstă {months, plural, one {# lună} other {# luni}}",
+  "age_year_months": "Vârstă de 1 an, {months, plural, one {# lună} other {# luni}}",
+  "age_years": "{years, plural, other {Vârstă #}}",
+  "album_added": "Album adăugat",
+  "album_added_notification_setting_description": "Primiți o notificare prin e-mail când sunteți adăugat la un album partajat",
+  "album_cover_updated": "Coperta albumului a fost actualizată",
+  "album_delete_confirmation": "Ești sigur că vrei să ștergi albumul {album}?",
+  "album_delete_confirmation_description": "Dacă acest album este partajat, alți utilizatori nu vor mai putea accesa.",
+  "album_info_updated": "Informații album actualizate",
+  "album_leave": "Părăsiți albumul?",
+  "album_leave_confirmation": "Sigur doriți să părăsiți {album}?",
+  "album_name": "Nume Album",
+  "album_options": "Opțiuni album",
+  "album_remove_user": "Eliminare utilizator?",
+  "album_remove_user_confirmation": "Ești sigur că dorești eliminarea {user}?",
+  "album_share_no_users": "Se pare că ai partajat acest album cu toți utilizatorii sau nu ai niciun utilizator cu care să-l partajezi.",
+  "album_updated": "Album actualizat",
+  "album_updated_setting_description": "Primiți o notificare prin e-mail când un album partajat are elemente noi",
+  "album_user_left": "A părăsit {album}",
+  "album_user_removed": "{user} eliminat",
+  "album_with_link_access": "Permite oricui cu link-ul să vadă fotografiile și persoanele din acest album.",
+  "albums": "Albume",
+  "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albume}}",
+  "all": "Toate",
+  "all_albums": "Toate albumele",
+  "all_people": "Toți oamenii",
+  "all_videos": "Toate videoclipurile",
+  "allow_dark_mode": "Permite mod întunecat",
+  "allow_edits": "Permite editări",
+  "allow_public_user_to_download": "Permite utilizatorului public să descarce",
+  "allow_public_user_to_upload": "Permite utilizatorului public să încarce",
+  "anti_clockwise": "În sens invers acelor de ceasornic",
+  "api_key": "Cheie API",
+  "api_key_description": "Această valoare va fi afișată o singură dată. Vă rugăm să vă asigurați că o copiați înainte de a închide fereastra.",
+  "api_key_empty": "Numele cheii API nu trebuie să fie gol",
+  "api_keys": "Chei API",
+  "app_settings": "Setări Aplicație",
+  "appears_in": "Apare în",
+  "archive": "Arhivă",
+  "archive_or_unarchive_photo": "Arhiveazǎ sau dezarhiveazǎ fotografia",
+  "archive_size": "Mărime arhivă",
+  "archive_size_description": "Configurează dimensiunea arhivei pentru descărcări (în GiB)",
+  "archived_count": "{count, plural, other {Arhivat/e#}}",
+  "are_these_the_same_person": "Sunt aceștia aceeași persoană?",
+  "are_you_sure_to_do_this": "Sunteți sigur că doriți să faceți acest lucru?",
+  "asset_added_to_album": "Adăugat la album",
+  "asset_adding_to_album": "Se adaugă la album...",
+  "asset_description_updated": "Descrierea resursei a fost actualizată",
+  "asset_filename_is_offline": "Resursa {filename} este offline",
+  "asset_has_unassigned_faces": "Resursa are fețe neatribuite",
+  "asset_hashing": "Se face hashing...",
+  "asset_offline": "Resursă Offline",
+  "asset_offline_description": "Această resursă externă nu mai este găsită pe disc. Contactează te rog administratorul tău Immich pentru ajutor.",
+  "asset_skipped": "Sărit",
+  "asset_skipped_in_trash": "În coșul de gunoi",
+  "asset_uploaded": "Încărcat",
+  "asset_uploading": "Se incarcă...",
+  "assets": "Resurse",
+  "assets_added_count": "Adăugat {count, plural, one {# resursă} other {# resurse}}",
+  "assets_added_to_album_count": "Am adăugat {count, plural, one {# resursă} other {# resurse}} în album",
+  "assets_added_to_name_count": "Am adăugat {count, plural, one {# resursă} other {# resurse}} în {hasName, select, true {<b>{name}</b>} other {albumul nou}}",
+  "assets_count": "{count, plural, one {# resursă} other {# resurse}}",
+  "assets_moved_to_trash_count": "Am mutat {count, plural, one {# resursă} other {# resurse}} în coșul de gunoi",
+  "assets_permanently_deleted_count": "Șters permanent {count, plural, one {# resursă} other {# resurse}}",
+  "assets_removed_count": "Eliminat {count, plural, one {# resursă} other {# resurse}}",
+  "assets_restore_confirmation": "Ești sigur că vrei să restaurezi toate resursele tale din coșul de gunoi? Nu poți anula această acțiune! Ține minte că resursele offline nu se restaurează astfel.",
+  "assets_restored_count": "Restaurat {count, plural, one {# resursă} other {# resurse}}",
+  "assets_trashed_count": "Mutat în coșul de gunoi {count, plural, one {# resursă} other {# resurse}}",
+  "assets_were_part_of_album_count": "{count, plural, one {Resursa era} other {Resursele erau}} deja parte din album",
+  "authorized_devices": "Dispozitive Autorizate",
+  "back": "Înapoi",
+  "back_close_deselect": "Înapoi, închidere sau deselectare",
+  "backward": "În sens invers",
+  "birthdate_saved": "Data nașterii salvată cu succes",
+  "birthdate_set_description": "Data nașterii este utilizată pentru a calcula vârsta acestei persoane la momentul realizării fotografiei.",
+  "blurred_background": "Fundal neclar",
+  "bugs_and_feature_requests": "Erori și Solicitări de Caracteristici",
+  "build": "Versiunea",
+  "build_image": "Versiune Imagine",
+  "bulk_delete_duplicates_confirmation": "Ești sigur că vrei să ștergi în masă {count, plural, one {# resursă duplicată} other {# resurse duplicate}}? Aceasta va păstra cea mai mare resursă din fiecare grup și va șterge permanent toate celelalte duplicate. Nu poți anula această acțiune!",
+  "bulk_keep_duplicates_confirmation": "Ești sigur că vrei să păstrezi {count, plural, one {# resursă duplicată} other {# resurse duplicate}}? Aceasta va rezolva toate grupurile duplicate fără a șterge nimic.",
+  "bulk_trash_duplicates_confirmation": "Ești sigur că vrei să muți în coșul de gunoi {count, plural, one {# resursă duplicată} other {# resurse duplicate}}? Aceasta va păstra cea mai mare resursă din fiecare grup și va muta în coșul de gunoi toate celelalte duplicate.",
+  "buy": "Achiziționați Immich",
+  "camera": "Camerǎ",
+  "camera_brand": "Marcǎ cameră",
+  "camera_model": "Model cameră",
+  "cancel": "Anulați",
+  "cancel_search": "Anulați căutarea",
+  "cannot_merge_people": "Nu se pot îmbina persoanele",
+  "cannot_undo_this_action": "Nu puteți anula această acțiune!",
+  "cannot_update_the_description": "Nu se poate actualiza descrierea",
+  "change_date": "Schimbați data",
+  "change_expiration_time": "Schimbați data expirare",
+  "change_location": "Schimbați locația",
+  "change_name": "Schimbați nume",
+  "change_name_successfully": "Schimbare nume cu succes",
+  "change_password": "Schimbați parolă",
+  "change_password_description": "Aceasta este fie prima dată când te conectezi în sistem, fie s-a făcut o solicitare pentru a schimba parola ta. Te rog să introduci noua parolă mai jos.",
+  "change_your_password": "Schimbă-ți parola",
+  "changed_visibility_successfully": "Schimbare vizibilitate cu succes",
+  "check_all": "Selectați Tot",
+  "check_logs": "Verificați Jurnale",
+  "choose_matching_people_to_merge": "Alegeți persoanele care se potrivesc pentru a le fuziona",
+  "city": "Oraș",
+  "clear": "Curățați",
+  "clear_all": "Curățați tot",
+  "clear_all_recent_searches": "Curățați toate căutările recente",
+  "clear_message": "Ștergeți mesajul",
+  "clear_value": "Ștergeți valoarea",
+  "clockwise": "În sensul acelor de ceas",
+  "close": "Închideți",
+  "collapse": "Restrângeți",
+  "collapse_all": "Restrângeți toate",
+  "color": "Culoare",
+  "color_theme": "Tema de culoare",
+  "comment_deleted": "Comentariu șters",
+  "comment_options": "Opțiuni comentariu",
+  "comments_and_likes": "Comentarii & aprecieri",
+  "comments_are_disabled": "Comentariile sunt dezactivate",
+  "confirm": "Confirmați",
+  "confirm_admin_password": "Confirmați Parola de Administrator",
+  "confirm_delete_shared_link": "Sunteți sigur că doriți să ștergeți acest link partajat?",
+  "confirm_keep_this_delete_others": "Toate celelalte active din stivă vor fi șterse, cu excepția acestui material. Sunteți sigur că doriți să continuați?",
+  "confirm_password": "Confirmați parola",
+  "contain": "Încadrează",
+  "context": "Context",
+  "continue": "Continuați",
+  "copied_image_to_clipboard": "Imagine copiată în clipboard.",
+  "copied_to_clipboard": "Copiat în clipboard!",
+  "copy_error": "Eroare de copiere",
+  "copy_file_path": "Copiați calea fișierului",
+  "copy_image": "Copiere imagine",
+  "copy_link": "Copiere link",
+  "copy_link_to_clipboard": "Copiere link în clipboard",
+  "copy_password": "Copiere parola",
+  "copy_to_clipboard": "Copiere în clipboard",
+  "country": "Țara",
+  "cover": "Umple fereastra",
+  "covers": "Acoperă",
+  "create": "Creează",
+  "create_album": "Creează album",
+  "create_library": "Creează Bibliotecă",
+  "create_link": "Creează link",
+  "create_link_to_share": "Creează link pentru a distribui",
+  "create_link_to_share_description": "Permiteți oricui are link-ul să vadă fotografia (fotografiile) selectată(e)",
+  "create_new_person": "Creați o persoană nouă",
+  "create_new_person_hint": "Atribuiți resursele selectate unei persoane noi",
+  "create_new_user": "Creează utilizator nou",
+  "create_tag": "Creează etichetă",
+  "create_tag_description": "Creează o etichetă nouă. Pentru etichete imbricate, te rog să introduci calea completă a etichetei, inclusiv bare oblice (/).",
+  "create_user": "Creează utilizator",
+  "created": "Creat",
+  "current_device": "Dispozitiv curent",
+  "custom_locale": "Setare Regională Personalizată",
+  "custom_locale_description": "Formatați datele și numerele în funcție de limbă și regiune",
+  "dark": "Întunecat",
+  "date_after": "După data",
+  "date_and_time": "Dată și oră",
+  "date_before": "Anterior datei",
+  "date_of_birth_saved": "Data nașterii salvată cu succes",
+  "date_range": "Interval de date",
+  "day": "Zi",
+  "deduplicate_all": "Deduplicați Toate",
+  "default_locale": "Setare Regională Implicită",
+  "default_locale_description": "Formatați datele și numerele în funcție de regiunea browserului dvs",
+  "delete": "Ștergere",
+  "delete_album": "Ștergere album",
+  "delete_api_key_prompt": "Sunteți sigur că doriți să ștergeți această cheie API?",
+  "delete_duplicates_confirmation": "Sunteți sigur că doriți să ștergeți permanent aceste duplicate?",
+  "delete_key": "Ștergere cheie",
+  "delete_library": "Ștergere biblioteca",
+  "delete_link": "Ștergere link",
+  "delete_others": "Ștergeți celelalte",
+  "delete_shared_link": "Ștergere link partajat",
+  "delete_tag": "Ștergere etichetă",
+  "delete_tag_confirmation_prompt": "Ești sigur că vrei să ștergi eticheta {tagName} ?",
+  "delete_user": "Ștergere utilizator",
+  "deleted_shared_link": "Link partajat șters",
+  "deletes_missing_assets": "Ștergere resurse lipsă de pe disc",
+  "description": "Descriere",
+  "details": "Detalii",
+  "direction": "Direcție",
+  "disabled": "Dezactivat",
+  "disallow_edits": "Interzice modificările",
+  "discord": "Server Discord",
+  "discover": "Descoperiți",
+  "dismiss_all_errors": "Ignorați toate erorile",
+  "dismiss_error": "Ignorați eroarea",
+  "display_options": "Opțiuni de afișare",
+  "display_order": "Ordine de afișare",
+  "display_original_photos": "Afișați fotografiile originale",
+  "display_original_photos_setting_description": "Preferă să afișezi fotografia originală atunci când vizualizezi o resursă, în loc de miniaturi, atunci când resursa originală este compatibilă cu web-ul. Aceasta poate duce la viteze mai lente de afișare a fotografiilor.",
+  "do_not_show_again": "Nu mai afișa acest mesaj",
+  "documentation": "Documentație",
+  "done": "Gata",
+  "download": "Descărcați",
+  "download_include_embedded_motion_videos": "Videoclipuri încorporate",
+  "download_include_embedded_motion_videos_description": "Include videoclipurile încorporate în fotografiile în mișcare ca fișier separat",
+  "download_settings": "Descărcați",
+  "download_settings_description": "Gestionați setările legate de descărcarea resurselor",
+  "downloading": "Se descarcă",
+  "downloading_asset_filename": "Se descarcă resursa {filename}",
+  "drop_files_to_upload": "Trageți fișierele aici pentru a le încărca",
+  "duplicates": "Duplicate",
+  "duplicates_description": "Rezolvați fiecare grup indicând care sunt duplicate, dacă există",
+  "duration": "Durată",
+  "edit": "Editare",
+  "edit_album": "Editare album",
+  "edit_avatar": "Editare avatar",
+  "edit_date": "Editare dată",
+  "edit_date_and_time": "Editare dată și oră",
+  "edit_exclusion_pattern": "Editarea modelului de excludere",
+  "edit_faces": "Editare fețe",
+  "edit_import_path": "Editare cale de import",
+  "edit_import_paths": "Editare căi de import",
+  "edit_key": "Tastă de editare",
+  "edit_link": "Editare link",
+  "edit_location": "Editare locație",
+  "edit_name": "Editare nume",
+  "edit_people": "Editare persoane",
+  "edit_tag": "Editare etichetă",
+  "edit_title": "Editare Titlu",
+  "edit_user": "Editare utilizator",
+  "edited": "Editat",
+  "editor": "Editor",
+  "editor_close_without_save_prompt": "Schimbările nu vor fi salvate",
+  "editor_close_without_save_title": "Închideți editorul?",
+  "editor_crop_tool_h2_aspect_ratios": "Raporturi de aspect",
+  "editor_crop_tool_h2_rotation": "Rotire",
+  "email": "Email",
+  "empty_trash": "Goliți coșul de gunoi",
+  "empty_trash_confirmation": "Sunteți sigur că doriți să goliți coșul de gunoi? Acest lucru va elimina definitiv din Immich toate resursele din coșul de gunoi.\nNu puteți anula această acțiune!",
+  "enable": "Permite",
+  "enabled": "Activat",
+  "end_date": "Data de încheiere",
+  "error": "Eroare",
+  "error_loading_image": "Eroare la încărcarea imaginii",
+  "error_title": "Eroare - ceva nu a mers",
+  "errors": {
+    "cannot_navigate_next_asset": "Nu se poate naviga către următoarea resursă",
+    "cannot_navigate_previous_asset": "Nu se poate naviga la resursa anterioară",
+    "cant_apply_changes": "Nu se pot aplica schimbări",
+    "cant_change_activity": "Nu se poate {enabled, select, true {dezactiva} other {activa}} activitatea",
+    "cant_change_asset_favorite": "Nu pot schimba favoritul pentru resursa",
+    "cant_change_metadata_assets_count": "Nu se pot modifica metadatele pentru {count, plural, one {# resursa} other {# resurse}}",
+    "cant_get_faces": "Nu pot obține fețe",
+    "cant_get_number_of_comments": "Nu pot obține numărul de comentarii",
+    "cant_search_people": "Nu pot căuta oameni",
+    "cant_search_places": "Nu se pot căuta locații",
+    "cleared_jobs": "Sarcini terminate pentru: {job}",
+    "error_adding_assets_to_album": "Eroare la adăugarea resurselor la album",
+    "error_adding_users_to_album": "Eroare la adăugarea utilizatorilor la album",
+    "error_deleting_shared_user": "Eroare la ștergerea utilizatorului partajat",
+    "error_downloading": "Eroare la descărcarea {filename}",
+    "error_hiding_buy_button": "Eroare la ascunderea butonului de cumpărare",
+    "error_removing_assets_from_album": "Eroare la eliminarea resurselor din album, verificați consola pentru mai multe detalii",
+    "error_selecting_all_assets": "Eroare la selectarea tuturor resurselor",
+    "exclusion_pattern_already_exists": "Acest model de excludere există deja.",
+    "failed_job_command": "Comanda {command} a eșuat pentru sarcina: {job}",
+    "failed_to_create_album": "A eșuat crearea albumului",
+    "failed_to_create_shared_link": "A eșuat crearea legăturii partajate",
+    "failed_to_edit_shared_link": "A eșuat editarea legăturii partajate",
+    "failed_to_get_people": "Eșec la obținerea persoanelor",
+    "failed_to_keep_this_delete_others": "Nu s-a putut păstra acest material respectiv nu s-au putut șterge celelalte materiale",
+    "failed_to_load_asset": "Eșec la încărcarea resursei",
+    "failed_to_load_assets": "Eșec la încărcarea resurselor",
+    "failed_to_load_people": "Eșec la încărcarea persoanelor",
+    "failed_to_remove_product_key": "Eșec la eliminarea cheii de produs",
+    "failed_to_stack_assets": "Eșec la combinarea resurselor",
+    "failed_to_unstack_assets": "Eșec la desfășurarea resurselor",
+    "import_path_already_exists": "Această cale de import există deja.",
+    "incorrect_email_or_password": "E-mail sau parolă incorect/ă",
+    "paths_validation_failed": "{paths, plural, one {# cale} other {# căi}} nu a trecut validarea",
+    "profile_picture_transparent_pixels": "Pozele de profil nu pot avea pixeli transparenți. Te rugăm să mărești imaginea și/sau să o muți.",
+    "quota_higher_than_disk_size": "Ați stabilit o valoare a spațiului de stocare mai mare decât dimensiunea discului",
+    "repair_unable_to_check_items": "Imposibil de verificat {count, select, one {element} other {elemente}}",
+    "unable_to_add_album_users": "Imposibil de adăugat utilizatori în album",
+    "unable_to_add_assets_to_shared_link": "Imposibil de adăugat resurse la link-ul partajat",
+    "unable_to_add_comment": "Imposibil de adăugat comentariu",
+    "unable_to_add_exclusion_pattern": "Nu se poate adăuga modelul de excludere",
+    "unable_to_add_import_path": "Imposibil de adăugat calea de import",
+    "unable_to_add_partners": "Nu se pot adăuga parteneri",
+    "unable_to_add_remove_archive": "Nu se poate {archived, select, true {îndepărta resursa din} other {adăuga resursa în}} arhivă",
+    "unable_to_add_remove_favorites": "Nu se poate {favorite, select, true {adăuga resursa în} other {îndepărta resursa din}} favorite",
+    "unable_to_archive_unarchive": "Nu se poate {archived, select, true {arhiva} other {dezarhiva}}",
+    "unable_to_change_album_user_role": "Nu se poate schimba rolul utilizatorului de album",
+    "unable_to_change_date": "Imposibil de schimbat data",
+    "unable_to_change_favorite": "Nu se pot modifica favoritele pentru resursa",
+    "unable_to_change_location": "Imposibil de schimbat locația",
+    "unable_to_change_password": "Imposibil de schimbat parola",
+    "unable_to_change_visibility": "Nu se poate schimba vizibilitatea pentru {count, plural, one {# persoană} other {# persoane}}",
+    "unable_to_complete_oauth_login": "Nu s-a realizat logarea prin OAuth",
+    "unable_to_connect": "Nu se poate conecta",
+    "unable_to_connect_to_server": "Nu se poate conecta la server",
+    "unable_to_copy_to_clipboard": "Nu poate fi copiat, asigură-te că accesezi pagina prin https",
+    "unable_to_create_admin_account": "Nu se poate crea contul de administrator",
+    "unable_to_create_api_key": "Nu se poate crea o nouă cheie API",
+    "unable_to_create_library": "Nu se poate crea biblioteca",
+    "unable_to_create_user": "Nu se poate crea userul",
+    "unable_to_delete_album": "Nu se poate șterge albumul",
+    "unable_to_delete_asset": "Nu poate fi ștearsă resursa",
+    "unable_to_delete_assets": "Eroare la ștergerea resurselor",
+    "unable_to_delete_exclusion_pattern": "Nu se poate șterge modelul de excludere",
+    "unable_to_delete_import_path": "Nu se poate șterge calea de import",
+    "unable_to_delete_shared_link": "Nu se poate șterge linkul partajat",
+    "unable_to_delete_user": "Nu se poate șterge userul",
+    "unable_to_download_files": "Nu se pot descărca fișierele",
+    "unable_to_edit_exclusion_pattern": "Nu se poate edita modelul de excludere",
+    "unable_to_edit_import_path": "Nu se poate edita calea de import",
+    "unable_to_empty_trash": "Nu se poate goli coșul de gunoi",
+    "unable_to_enter_fullscreen": "Nu se poate accesa ecranul complet",
+    "unable_to_exit_fullscreen": "Imposibil de părăsit ecranul complet",
+    "unable_to_get_comments_number": "Nu se poate obține numărul de comentarii",
+    "unable_to_get_shared_link": "Nu s-a putut obține linkul partajat",
+    "unable_to_hide_person": "Nu se poate ascunde persoana",
+    "unable_to_link_motion_video": "Imposibil de conectat videoclipul în mișcare",
+    "unable_to_link_oauth_account": "Nu se poate conecta contul OAuth",
+    "unable_to_load_album": "Nu se poate încărca albumul",
+    "unable_to_load_asset_activity": "Nu se poate încărca activitatea cu materiale",
+    "unable_to_load_items": "Nu se pot încărca articole",
+    "unable_to_load_liked_status": "Nu se poate încărca starea de apreciat",
+    "unable_to_log_out_all_devices": "Nu se pot deconecta toate dispozitivele",
+    "unable_to_log_out_device": "Nu se poate deconecta dispozitivul",
+    "unable_to_login_with_oauth": "Nu se poate autentifica cu OAuth",
+    "unable_to_play_video": "Nu se poate reda videoul",
+    "unable_to_reassign_assets_existing_person": "Nu se pot reatribui elementele către {name, select, null {o persoană existentă} other {{name}}}",
+    "unable_to_reassign_assets_new_person": "Nu se pot reatribui resurse unei persoane noi",
+    "unable_to_refresh_user": "Nu se poate reîmprospăta utilizatorul",
+    "unable_to_remove_album_users": "Nu se pot șterge userii din album",
+    "unable_to_remove_api_key": "Nu se poate șterge cheia API",
+    "unable_to_remove_assets_from_shared_link": "Nu se pot elimina resursele din linkul partajat",
+    "unable_to_remove_deleted_assets": "Nu se pot șterge fișierele offline",
+    "unable_to_remove_library": "Nu se poate șterge biblioteca",
+    "unable_to_remove_partner": "Imposibil de eliminat partenerul",
+    "unable_to_remove_reaction": "Nu se poate elimina reacția",
+    "unable_to_repair_items": "Imposibil de reparat elementele",
+    "unable_to_reset_password": "Imposibil de resetat parola",
+    "unable_to_resolve_duplicate": "Nu se poate rezolva duplicatul",
+    "unable_to_restore_assets": "Nu se pot restaura resursele",
+    "unable_to_restore_trash": "Nu se poate restaura coșul de gunoi",
+    "unable_to_restore_user": "Nu se poate restaura utilizatorul",
+    "unable_to_save_album": "Imposibil de salvat albumul",
+    "unable_to_save_api_key": "Imposibil de salvat cheia API",
+    "unable_to_save_date_of_birth": "Imposibil de salvat data de naștere",
+    "unable_to_save_name": "Imposibil de salvat numele",
+    "unable_to_save_profile": "Imposibil de salvat profilul",
+    "unable_to_save_settings": "Nu se pot salva setările",
+    "unable_to_scan_libraries": "Nu se pot scana librăriile",
+    "unable_to_scan_library": "Nu se poate scana librăria",
+    "unable_to_set_feature_photo": "Nu se poate seta fotografia principală",
+    "unable_to_set_profile_picture": "Nu se poate seta fotografia de profil",
+    "unable_to_submit_job": "Imposibil de trimis sarcina",
+    "unable_to_trash_asset": "Nu se poate elimina resursa",
+    "unable_to_unlink_account": "Nu se poate deconecta contul",
+    "unable_to_unlink_motion_video": "Imposibil de deconectat videoclipul în mișcare",
+    "unable_to_update_album_cover": "Nu se poate actualiza coperta de album",
+    "unable_to_update_album_info": "Nu se pot actualiza informațiile albumului",
+    "unable_to_update_library": "Nu se poate actualiza biblioteca",
+    "unable_to_update_location": "Nu se poate actualiza locația",
+    "unable_to_update_settings": "Nu se pot actualiza setările",
+    "unable_to_update_timeline_display_status": "Nu se poate actualiza starea de afișare a cronologiei",
+    "unable_to_update_user": "Nu se poate actualiza utilizatorul",
+    "unable_to_upload_file": "Nu se poate încărca fișierul"
+  },
+  "exif": "Format comutabil pentru fișiere imagine",
+  "exit_slideshow": "Ieșire din Prezentare",
+  "expand_all": "Extindeți-le pe toate",
+  "expire_after": "Expiră după",
+  "expired": "Expirat",
+  "expires_date": "Expiră la {date}",
+  "explore": "Exploreazǎ",
+  "explorer": "Explorator",
+  "export": "Exportare",
+  "export_as_json": "Exportare ca JSON",
+  "extension": "Extensie",
+  "external": "Extern",
+  "external_libraries": "Biblioteci Externe",
+  "face_unassigned": "Nealocat",
+  "failed_to_load_assets": "Nu s-au încărcat activele",
+  "favorite": "Favorit",
+  "favorite_or_unfavorite_photo": "Fotografie preferată sau nepreferată",
+  "favorites": "Favorite",
+  "feature_photo_updated": "Fotografie caracteristică actualizată",
+  "features": "Caracteristici",
+  "features_setting_description": "Gestionați funcțiile aplicației",
+  "file_name": "Nume de fișier",
+  "file_name_or_extension": "Numele sau extensia fișierului",
+  "filename": "Numele fișierului",
+  "filetype": "Tipul fișierului",
+  "filter_people": "Filtrați persoanele",
+  "find_them_fast": "Găsiți-le rapid prin căutare după nume",
+  "fix_incorrect_match": "Remediați potrivirea incorectă",
+  "folders": "Foldere",
+  "folders_feature_description": "Răsfoire în conținutul folderului pentru fotografiile și videoclipurile din sistemul de fișiere",
+  "forward": "Redirecționare",
+  "general": "General",
+  "get_help": "Obțineți Ajutor",
+  "getting_started": "Noțiuni de Bază",
+  "go_back": "Întoarcere",
+  "go_to_search": "Spre căutare",
+  "group_albums_by": "Grupați albume de...",
+  "group_no": "Fără grupare",
+  "group_owner": "Grupați după proprietar",
+  "group_year": "Grupați după an",
+  "has_quota": "Are spațiu de stocare",
+  "hi_user": "Bună {name} ({email})",
+  "hide_all_people": "Ascundeți toate persoanele",
+  "hide_gallery": "Ascundeți galeria",
+  "hide_named_person": "Ascundeți persoana {name}",
+  "hide_password": "Ascundeți parola",
+  "hide_person": "Ascundeți persoana",
+  "hide_unnamed_people": "Ascundeți persoanele fără nume",
+  "host": "Gazdă",
+  "hour": "Oră",
+  "image": "Imagine",
+  "image_alt_text_date": "{isVideo, select, true {Video} other {imagine}} preluată în {date}",
+  "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {imagine}} preluată cu {person1} în {date}",
+  "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {imagine}} preluată cu {person1} și {person2} în {date}",
+  "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {imagine}} preluată cu {person1}, {person2}, și {person3} în {date}",
+  "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {imagine}} preluată cu {person1}, {person2}, și {additionalCount, number} alții în {date}",
+  "image_alt_text_date_place": "{isVideo, select, true {Video} other {imagine}} preluată în {city}, {country} în {date}",
+  "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {imagine}} preluată în {city}, {country} cu {person1} în {date}",
+  "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {imagine}} preluată în {city}, {country} cu {person1} și {person2} în {date}",
+  "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {imagine}} preluată în {city}, {country} cu {person1}, {person2}, și {person3} în {date}",
+  "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {imagine}} preluată în {city}, {country} cu {person1}, {person2}, și {additionalCount, number} alții în {date}",
+  "immich_logo": "Logo Immich",
+  "immich_web_interface": "Interfața Web Immich",
+  "import_from_json": "Importă din JSON",
+  "import_path": "Calea de import",
+  "in_albums": "În {count, plural, one {# album} other {# albume}}",
+  "in_archive": "În arhivă",
+  "include_archived": "Include resursele arhivate",
+  "include_shared_albums": "Include albumele partajate",
+  "include_shared_partner_assets": "Include resursele partenerilor partajați",
+  "individual_share": "Cota individuală",
+  "info": "Informație",
+  "interval": {
+    "day_at_onepm": "În fiecare zi la ora 13.00",
+    "hours": "La fiecare {hours, plural, one {oră} other {{hours, number} ore}}",
+    "night_at_midnight": "În fiecare noapte la miezul nopții",
+    "night_at_twoam": "În fiecare noapte la 2 dimineața"
+  },
+  "invite_people": "Invitați Persoane",
+  "invite_to_album": "Invitați în album",
+  "items_count": "{count, plural, one {# element} other{# elemente}}",
+  "jobs": "Sarcini",
+  "keep": "Păstrați",
+  "keep_all": "Păstrați Tot",
+  "keep_this_delete_others": "Păstrați asta, ștergeți celelalte",
+  "kept_this_deleted_others": "S-a păstrat acest material și s-au șters {count, plural, one {# material} other {# materiale}}",
+  "keyboard_shortcuts": "Comenzi rapide de tastatură",
+  "language": "Limbă",
+  "language_setting_description": "Selectați limba preferată",
+  "last_seen": "Văzut ultima dată",
+  "latest_version": "Ultima Versiune",
+  "latitude": "Latitudine",
+  "leave": "Părăsiți",
+  "let_others_respond": "Permite altora să răspundă",
+  "level": "Nivel",
+  "library": "Librărie",
+  "library_options": "Opțiuni de bibliotecă",
+  "light": "Lumină",
+  "like_deleted": "Preferat șters",
+  "link_motion_video": "Link video în mișcare",
+  "link_options": "Opțiuni de link",
+  "link_to_oauth": "Link către OAuth",
+  "linked_oauth_account": "Cont OAuth conectat",
+  "list": "Listă",
+  "loading": "Încărcare",
+  "loading_search_results_failed": "Încărcarea rezultatelor căutării nu a reușit",
+  "log_out": "Deconectare",
+  "log_out_all_devices": "Deconectați-vă de la toate dispozitivele",
+  "logged_out_all_devices": "S-au deconectat toate dispozitivele",
+  "logged_out_device": "Dispozitiv deconectat",
+  "login": "Conectare",
+  "login_has_been_disabled": "Conectarea a fost dezactivată.",
+  "logout_all_device_confirmation": "Sigur doriți să deconectați toate dispozitivele?",
+  "logout_this_device_confirmation": "Sigur doriți să deconectați acest dispozitiv?",
+  "longitude": "Longitudine",
+  "look": "Examinare",
+  "loop_videos": "Buclă videoclipuri",
+  "loop_videos_description": "Activați pentru a rula in buclă automat un videoclip în vizualizatorul de detalii.",
+  "main_branch_warning": "Utilizați o versiune de dezvoltare; vă recomandăm insistent să utilizați o versiune de lansare!",
+  "make": "Face",
+  "manage_shared_links": "Administrați link-urile distribuite",
+  "manage_sharing_with_partners": "Gestionați partajarea cu partenerii",
+  "manage_the_app_settings": "Gestionați setările aplicației",
+  "manage_your_account": "Gestionați-vă contul",
+  "manage_your_api_keys": "Gestionați-vă cheile API",
+  "manage_your_devices": "Gestionați-vă dispozitivele conectate",
+  "manage_your_oauth_connection": "Gestionați-vă conexiunea OAuth",
+  "map": "Hartă",
+  "map_marker_for_images": "Marcator de hartă pentru imaginile realizate în {city}, {country}",
+  "map_marker_with_image": "Marcator de hartă cu imagine",
+  "map_settings": "Setările hărții",
+  "matches": "Corespunde",
+  "media_type": "Tip media",
+  "memories": "Amintiri",
+  "memories_setting_description": "Administrați ce vedeți în amintiri",
+  "memory": "Amintire",
+  "memory_lane_title": "Banda Memoriei {title}",
+  "menu": "Meniu",
+  "merge": "Îmbinați",
+  "merge_people": "Îmbinați persoane",
+  "merge_people_limit": "Puteți îmbina până la 5 fețe simultan",
+  "merge_people_prompt": "Vreți să îmbinați aceste persoane? Această acțiune este ireversibilă.",
+  "merge_people_successfully": "Persoane îmbinate cu succes",
+  "merged_people_count": "Imbinate {count, plural, one {# persoană} other {# persoane}}",
+  "minimize": "Minimizare",
+  "minute": "Minute",
+  "missing": "Lipsă",
+  "model": "Model",
+  "month": "Lună",
+  "more": "Mai mult",
+  "moved_to_trash": "Mutat în coșul de gunoi",
+  "my_albums": "Albumele mele",
+  "name": "Nume",
+  "name_or_nickname": "Nume sau poreclǎ",
+  "never": "Niciodată",
+  "new_album": "Album Nou",
+  "new_api_key": "Cheie API nouǎ",
+  "new_password": "Parolă nouă",
+  "new_person": "Persoanǎ nouǎ",
+  "new_user_created": "Utilizator nou creat",
+  "new_version_available": "VERSIUNE NOUĂ DISPONIBILĂ",
+  "newest_first": "Cel mai nou primul",
+  "next": "Următorul",
+  "next_memory": "Următoarea amintire",
+  "no": "Nu",
+  "no_albums_message": "Creați un album pentru a vă organiza fotografiile și videoclipurile",
+  "no_albums_with_name_yet": "Se pare că nu aveți încă niciun album cu acest nume.",
+  "no_albums_yet": "Se pare că nu aveți încă niciun album.",
+  "no_archived_assets_message": "Arhivați fotografii și videoclipuri pentru a le ascunde din vizualizarea fotografii",
+  "no_assets_message": "CLICK PENTRU A ÎNCĂRCA PRIMA TA FOTOGRAFIE",
+  "no_duplicates_found": "Nu au fost găsite duplicate.",
+  "no_exif_info_available": "Nu există informații exif disponibile",
+  "no_explore_results_message": "Încarcați mai multe fotografii pentru a vă explora colecția.",
+  "no_favorites_message": "Adăugați favorite pentru a găsi rapid cele mai bune fotografii și videoclipuri",
+  "no_libraries_message": "Creați o bibliotecă externă pentru a vă vizualiza fotografiile și videoclipurile",
+  "no_name": "Fără Nume",
+  "no_places": "Nu există locuri",
+  "no_results": "Fără rezultate",
+  "no_results_description": "Încercați un sinonim sau un cuvânt cheie mai general",
+  "no_shared_albums_message": "Creați un album pentru a partaja fotografii și videoclipuri cu persoanele din rețeaua dvs",
+  "not_in_any_album": "Nu există în niciun album",
+  "note_apply_storage_label_to_previously_uploaded assets": "Notă: Pentru a aplica eticheta de stocare la resursele încărcate anterior, rulați",
+  "note_unlimited_quota": "Notă: Introduceți 0 pentru spațiu pe disc nelimitat",
+  "notes": "Note",
+  "notification_toggle_setting_description": "Activați notificările prin email",
+  "notifications": "Notificări",
+  "notifications_setting_description": "Gestionați notificările",
+  "oauth": "OAuth",
+  "official_immich_resources": "Resurse Oficiale Immich",
+  "offline": "Offline",
+  "offline_paths": "Căi offline",
+  "offline_paths_description": "Aceste rezultate se pot datora ștergerii manuale a fișierelor care nu fac parte dintr-o bibliotecă externă.",
+  "ok": "Bine",
+  "oldest_first": "Cel mai vechi mai întâi",
+  "onboarding": "Integrare",
+  "onboarding_privacy_description": "Următoarele caracteristici (opționale) se bazează pe servicii externe și pot fi dezactivate în orice moment din setările de administrare.",
+  "onboarding_theme_description": "Alegeți o temă de culoare pentru exemplul dvs. Puteți modifica acest lucru mai târziu în setări.",
+  "onboarding_welcome_description": "Să vă setăm instanța cu câteva setări comune.",
+  "onboarding_welcome_user": "Bun venit, {user}",
+  "online": "Online",
+  "only_favorites": "Doar favorite",
+  "open_in_map_view": "Deschideți în vizualizarea hărții",
+  "open_in_openstreetmap": "Deschideți în OpenStreetMap",
+  "open_the_search_filters": "Deschideți filtrele de căutare",
+  "options": "Opțiuni",
+  "or": "sau",
+  "organize_your_library": "Organizează-ți biblioteca",
+  "original": "original",
+  "other": "Alte",
+  "other_devices": "Alte dispozitive",
+  "other_variables": "Alte variabile",
+  "owned": "Deținut",
+  "owner": "Proprietar",
+  "partner": "Partener",
+  "partner_can_access": "{partner} poate accesa",
+  "partner_can_access_assets": "Toate fotografiile și videoclipurile tale, cu excepția celor din arhivate și sterse",
+  "partner_can_access_location": "Locația în care au fost făcute fotografiile dvs",
+  "partner_sharing": "Partajarea Partenerilor",
+  "partners": "Parteneri",
+  "password": "Parolă",
+  "password_does_not_match": "Parola nu se potrivește",
+  "password_required": "Parola Obligatorie",
+  "password_reset_success": "Resetarea parolei efectuată cu succes",
+  "past_durations": {
+    "days": "Ultimele {days, plural, one {zi} other {# zile}}",
+    "hours": "Ultimele {hours, plural, one {oră} other {# ore}}",
+    "years": "Ultimii {years, plural, one {an} other {# ani}}"
+  },
+  "path": "Cale",
+  "pattern": "Tipar",
+  "pause": "Pauză",
+  "pause_memories": "Opriți amintirile",
+  "paused": "Întrerupt",
+  "pending": "În așteptare",
+  "people": "Persoane",
+  "people_edits_count": "Editat {count, plural, one {# persoană} other {# persoane}}",
+  "people_feature_description": "Răsfoiți fotografii și videoclipuri grupate după persoane",
+  "people_sidebar_description": "Afișează un link către persoane în bara laterală",
+  "permanent_deletion_warning": "Avertisment de ștergere permanentă",
+  "permanent_deletion_warning_setting_description": "Afișează un avertisment la ștergerea definitivă a resurselor",
+  "permanently_delete": "Ștergeți definitiv",
+  "permanently_delete_assets_count": "Ștergeți definitiv {count, plural, one {resursă} other {resurse}}",
+  "permanently_delete_assets_prompt": "Sigur doriți să ștergeți definitiv {count, plural, one {această resursă?} other {aceste <b>#</b> resurse?}} Acest lucru va elimina și {count, plural, one {din ea} other {din ele}} album(e).",
+  "permanently_deleted_asset": "Resursă ștearsă definitiv",
+  "permanently_deleted_assets_count": "S-au șters definitiv {count, plural, one {# resursă} other {# resurse}}",
+  "person": "Persoanǎ",
+  "person_hidden": "{name}{hidden, select, true { (ascuns)} other {}}",
+  "photo_shared_all_users": "Se pare că ți-ai partajat fotografiile tuturor utilizatorilor sau că nu ai niciun utilizator căruia să le distribui.",
+  "photos": "Fotografii",
+  "photos_and_videos": "Fotografii și Videoclipuri",
+  "photos_count": "{count, plural, one {{count, number} imagine} other{{count, number} imagini}}",
+  "photos_from_previous_years": "Fotografii din anii anteriori",
+  "pick_a_location": "Alegeți o locație",
+  "place": "Loc",
+  "places": "Locații",
+  "play": "Redare",
+  "play_memories": "Redare amintiri",
+  "play_motion_photo": "Redare Fotografie în Mișcare",
+  "play_or_pause_video": "Redați sau întrerupeți videoclipul",
+  "port": "Port",
+  "preset": "Presetat",
+  "preview": "Previzualizare",
+  "previous": "Anterior",
+  "previous_memory": "Memoria anterioară",
+  "previous_or_next_photo": "Poza anterioară sau următoare",
+  "primary": "Primar",
+  "privacy": "Confidențialitate",
+  "profile_image_of_user": "Imagine de profil a lui {user}",
+  "profile_picture_set": "Poză de profil setată.",
+  "public_album": "Album public",
+  "public_share": "Distribuire Publică",
+  "purchase_account_info": "Suporter",
+  "purchase_activated_subtitle": "Vă mulțumim că susțineți Immich și software-ul open-source",
+  "purchase_activated_time": "Activat pe data de {date, date}",
+  "purchase_activated_title": "Cheia dvs. a fost activată cu succes",
+  "purchase_button_activate": "Activați",
+  "purchase_button_buy": "Cumpărați",
+  "purchase_button_buy_immich": "Cumpărați Immich",
+  "purchase_button_never_show_again": "Nu mai arăta niciodată",
+  "purchase_button_reminder": "Amintește-mi în 30 de zile",
+  "purchase_button_remove_key": "Eliminați cheia",
+  "purchase_button_select": "Selectare",
+  "purchase_failed_activation": "Activare eșuată! Vă rugăm să vă verificați e-mailul pentru cheia de produs corectă!",
+  "purchase_individual_description_1": "Pentru un individ",
+  "purchase_individual_description_2": "Statutul de suporter",
+  "purchase_individual_title": "Individual",
+  "purchase_input_suggestion": "Aveți o cheie de produs? Introduceți cheia mai jos",
+  "purchase_license_subtitle": "Cumpărați Immich pentru a sprijini dezvoltarea continuă a serviciului",
+  "purchase_lifetime_description": "Achiziție pe viață",
+  "purchase_option_title": "OPȚIUNI DE CUMPĂRARE",
+  "purchase_panel_info_1": "Dezvoltarea Immich necesită mult timp și efort și avem ingineri cu normă întreagă care lucrează la ea pentru a o face cât se poate de bună. Misiunea noastră este ca software-ul open-source și practicile de afaceri etice să devină o sursă de venit durabilă pentru dezvoltatori și să se creeze un ecosistem care să respecte confidențialitatea, cu alternative reale la serviciile cloud care exploatează.",
+  "purchase_panel_info_2": "Deoarece ne-am angajat să nu adăugăm planuri de plată, această achiziție nu vă va oferi nicio funcție suplimentară în Immich. Ne bazăm pe utilizatori ca dvs. pentru a sprijini dezvoltarea continuă a lui Immich.",
+  "purchase_panel_title": "Susțineți proiectul",
+  "purchase_per_server": "Per server",
+  "purchase_per_user": "Per user",
+  "purchase_remove_product_key": "Eliminați Cheia Produsului",
+  "purchase_remove_product_key_prompt": "Sigur doriți să eliminați cheia de produs?",
+  "purchase_remove_server_product_key": "Eliminați cheia de produs a Serverului",
+  "purchase_remove_server_product_key_prompt": "Sigur doriți să eliminați cheia de produs a Serverului?",
+  "purchase_server_description_1": "Pentru tot serverul",
+  "purchase_server_description_2": "Statutul de suporter",
+  "purchase_server_title": "Server",
+  "purchase_settings_server_activated": "Cheia de produs a serverului este gestionată de administrator",
+  "rating": "Evaluare cu stele",
+  "rating_clear": "Anulați evaluare",
+  "rating_count": "{count, plural, one {# stea} other {# stele}}",
+  "rating_description": "Afișați evaluarea EXIF în panoul de informații",
+  "reaction_options": "Opțiuni de reacție",
+  "read_changelog": "Citiți Jurnalul de Modificări",
+  "reassign": "Reatribuiți",
+  "reassigned_assets_to_existing_person": "Re-alocat {count, plural, one {# resursă} other {# resurse}} to {name, select, null {unei persoane existente} other {{name}}}",
+  "reassigned_assets_to_new_person": "Re-alocat {count, plural, one {# resursă} other {# resurse}} unei noi persoane",
+  "reassing_hint": "Atribuiți resursele selectate unei persoane existente",
+  "recent": "Recent",
+  "recent-albums": "Albume recente",
+  "recent_searches": "Căutări recente",
+  "refresh": "Reîmprospătare",
+  "refresh_encoded_videos": "Actualizează videoclipurile codificate",
+  "refresh_faces": "Reîmprospătați fețele",
+  "refresh_metadata": "Actualizați metadatele",
+  "refresh_thumbnails": "Reîmprospătați miniaturile",
+  "refreshed": "Reîmprospătat",
+  "refreshes_every_file": "Recitește toate fișierele existente și noi",
+  "refreshing_encoded_video": "Se reîmprospătează videoclipul codificat",
+  "refreshing_faces": "Se reîmprospătează fețele",
+  "refreshing_metadata": "Se reîmprospătează metadatele",
+  "regenerating_thumbnails": "Se regenerează miniaturile",
+  "remove": "Eliminați",
+  "remove_assets_album_confirmation": "Sigur doriți să eliminați {count, plural, one {# resursă} other {# resurse}} din album?",
+  "remove_assets_shared_link_confirmation": "Sigur doriți să eliminați {count, plural, one {# resursă} other {# resurse}} din acest link comun?",
+  "remove_assets_title": "Eliminați resursele?",
+  "remove_custom_date_range": "Eliminați intervalul de date personalizat",
+  "remove_deleted_assets": "Eliminați Resursele Șterse",
+  "remove_from_album": "Ștergeți din album",
+  "remove_from_favorites": "Eliminați din favorite",
+  "remove_from_shared_link": "Eliminați din linkul partajat",
+  "remove_url": "Eliminați adresa URL",
+  "remove_user": "Eliminați utilizatorul",
+  "removed_api_key": "Cheie API eliminată: {name}",
+  "removed_from_archive": "Eliminat din arhivă",
+  "removed_from_favorites": "Eliminat din favorite",
+  "removed_from_favorites_count": "{count, plural, other {Eliminat #}} din favorite",
+  "removed_tagged_assets": "Eticheta a fost eliminată din {count, plural, one {# resursă} other {# resurse}}",
+  "rename": "Redenumiți",
+  "repair": "Reparați",
+  "repair_no_results_message": "Fișierele neurmărite și lipsă vor apărea aici",
+  "replace_with_upload": "Înlocuiți cu încărcare",
+  "repository": "Repertoriu",
+  "require_password": "Necesită parolă",
+  "require_user_to_change_password_on_first_login": "Solicitați utilizatorului să schimbe parola la prima conectare",
+  "reset": "Resetare",
+  "reset_password": "Resetare parolă",
+  "reset_people_visibility": "Resetați vizibilitatea persoanelor",
+  "reset_to_default": "Resetați la valoarea implicită",
+  "resolve_duplicates": "Rezolvați duplicatele",
+  "resolved_all_duplicates": "Rezolvați toate duplicatele",
+  "restore": "Restaurați",
+  "restore_all": "Restaurați toate",
+  "restore_user": "Restabiliți utilizatorul",
+  "restored_asset": "Resursă restaurată",
+  "resume": "Reluare",
+  "retry_upload": "Reîncercați încărcarea",
+  "review_duplicates": "Examinați duplicatele",
+  "role": "Rol",
+  "role_editor": "Editor",
+  "role_viewer": "Vizualizator",
+  "save": "Salvați",
+  "saved_api_key": "Cheie API salvată",
+  "saved_profile": "Profil salvat",
+  "saved_settings": "Setări salvate",
+  "say_something": "Spuneți ceva",
+  "scan_all_libraries": "Scanați Toate Bibliotecile",
+  "scan_library": "Scanare",
+  "scan_settings": "Setări Scanare",
+  "scanning_for_album": "Se scanează după album...",
+  "search": "Căutați",
+  "search_albums": "Căutați albume",
+  "search_by_context": "Căutați după context",
+  "search_by_filename": "Căutați după numele fișierului sau extensie",
+  "search_by_filename_example": "i.e. IMG_1234.JPG sau PNG",
+  "search_camera_make": "Se caută marca camerei...",
+  "search_camera_model": "Se caută modelul camerei...",
+  "search_city": "Se caută orașul...",
+  "search_country": "Se caută țara...",
+  "search_for_existing_person": "Se caută o persoană existentă",
+  "search_no_people": "Fără persoane",
+  "search_no_people_named": "Nicio persoană numită \"{name}\"",
+  "search_options": "Opțiuni de căutare",
+  "search_people": "Căutați oameni",
+  "search_places": "Căutați locuri",
+  "search_settings": "Setări de căutare",
+  "search_state": "Starea căutării...",
+  "search_tags": "Căutați etichete...",
+  "search_timezone": "Căutați fusul orar...",
+  "search_type": "Tip cǎutare",
+  "search_your_photos": "Căutarea fotografiilor dvs",
+  "searching_locales": "Se caută regionale...",
+  "second": "Secundǎ",
+  "see_all_people": "Vizualizați toate persoanele",
+  "select_album_cover": "Selectați coperta albumului",
+  "select_all": "Selectați tot",
+  "select_all_duplicates": "Selectați toate duplicatele",
+  "select_avatar_color": "Selectați culoarea avatarului",
+  "select_face": "Selectați fața",
+  "select_featured_photo": "Selectați fotografia recomandată",
+  "select_from_computer": "Selectați din calculator",
+  "select_keep_all": "Selectați tot pentru păstrare",
+  "select_library_owner": "Selectați proprietarul bibliotecii",
+  "select_new_face": "Selectați o nouǎ fațǎ",
+  "select_photos": "Selectați fotografii",
+  "select_trash_all": "Selectați tot pentru ștergere",
+  "selected": "Selectat",
+  "selected_count": "{count, plural, other {# selectat}}",
+  "send_message": "Trimiteți mesaj",
+  "send_welcome_email": "Trimiteți email de bun venit",
+  "server_offline": "Server Offline",
+  "server_online": "Server Online",
+  "server_stats": "Statistici Server",
+  "server_version": "Versiune Server",
+  "set": "Setați",
+  "set_as_album_cover": "Setați ca și copertă a albumului",
+  "set_as_profile_picture": "Setați ca imagine de profil",
+  "set_date_of_birth": "Setați data nașterii",
+  "set_profile_picture": "Setați poza de profil",
+  "set_slideshow_to_fullscreen": "Setați Prezentare de Diapozitive la ecran complet",
+  "settings": "Setări",
+  "settings_saved": "Setările au fost salvate",
+  "share": "Distribuiți",
+  "shared": "Partajat",
+  "shared_by": "Partajat de",
+  "shared_by_user": "Partajat de {user}",
+  "shared_by_you": "Partajat de tine",
+  "shared_from_partner": "Fotografii de la {partner}",
+  "shared_link_options": "Opțiuni de link partajat",
+  "shared_links": "Link-uri distribuite",
+  "shared_photos_and_videos_count": "{assetCount, plural, other {# fotografii și videoclipuri partajate.}}",
+  "shared_with_partner": "Partajat cu {partner}",
+  "sharing": "Distribuire",
+  "sharing_enter_password": "Vă rugăm să introduceți parola pentru a vizualiza această pagină.",
+  "sharing_sidebar_description": "Afișați un link către Partajare în bara laterală",
+  "shift_to_permanent_delete": "apăsați ⇧ pentru a șterge definitiv elementul",
+  "show_album_options": "Afișați opțiunile de album",
+  "show_albums": "Afișați albume",
+  "show_all_people": "Aratați toate persoanele",
+  "show_and_hide_people": "Afișați și ascundeți persoane",
+  "show_file_location": "Afișați locația fișierului",
+  "show_gallery": "Afișați galeria",
+  "show_hidden_people": "Arătați persoanele ascunse",
+  "show_in_timeline": "Afișați în cronologie",
+  "show_in_timeline_setting_description": "Afișați fotografii și videoclipuri de la acest utilizator în cronologia dvs",
+  "show_keyboard_shortcuts": "Afișați comenzile rapide de la tastatură",
+  "show_metadata": "Arătați metadatele",
+  "show_or_hide_info": "Afișați sau ascundeți informații",
+  "show_password": "Afișați parola",
+  "show_person_options": "Afișați opțiunile persoanelor",
+  "show_progress_bar": "Afișați Bara de Progres",
+  "show_search_options": "Afișați opțiunile de căutare",
+  "show_slideshow_transition": "Afișați tranziția de prezentare",
+  "show_supporter_badge": "Insigna suporterului",
+  "show_supporter_badge_description": "Arată o insignă de suporter",
+  "shuffle": "Amestecați",
+  "sidebar": "Bara laterală",
+  "sidebar_display_description": "Afișați un link către vizualizare în bara laterală",
+  "sign_out": "Vă deconectați",
+  "sign_up": "Vă înregistrați",
+  "size": "Dimensiune",
+  "skip_to_content": "Treceți la conținut",
+  "skip_to_folders": "Treceți la foldere",
+  "skip_to_tags": "Treceți la etichete",
+  "slideshow": "Prezentare de diapozitive",
+  "slideshow_settings": "Setări pentru prezentarea de diapozitive",
+  "sort_albums_by": "Sortați albumele după...",
+  "sort_created": "Data creării",
+  "sort_items": "Numărul de articole",
+  "sort_modified": "Data modificării",
+  "sort_oldest": "Cea mai veche fotografie",
+  "sort_recent": "Cea mai recentă fotografie",
+  "sort_title": "Titlu",
+  "source": "Sursă",
+  "stack": "Stivă",
+  "stack_duplicates": "Duplicate stive",
+  "stack_select_one_photo": "Selectați o fotografie principală pentru stivă",
+  "stack_selected_photos": "Fotografie stivă selectată",
+  "stacked_assets_count": "Stivuite {count, plural, one {# resursă} other {# resurse}}",
+  "stacktrace": "Urmă stivă",
+  "start": "Început",
+  "start_date": "Data de începere",
+  "state": "Situaţie",
+  "status": "Stare",
+  "stop_motion_photo": "Opriți Fotografia in Mișcare",
+  "stop_photo_sharing": "Încetați distribuirea fotografiilor?",
+  "stop_photo_sharing_description": "{partner} nu va mai putea accesa fotografiile dvs.",
+  "stop_sharing_photos_with_user": "Nu mai partajați fotografiile cu acest utilizator",
+  "storage": "Spațiu de stocare",
+  "storage_label": "Eticheta de depozitare",
+  "storage_usage": "{used} din {available} utilizați",
+  "submit": "Trimiteți",
+  "suggestions": "Sugestii",
+  "sunrise_on_the_beach": "Rǎsǎrit pe plajǎ",
+  "support": "Suport tehnic",
+  "support_and_feedback": "Suport tehnic și feedback",
+  "support_third_party_description": "Instalarea dvs. Immich a fost pregătită de o terță parte. Problemele pe care le întâmpinați pot fi cauzate de acel pachet, așa că vă rugăm să ridicați probleme cu ei în primă instanță utilizând linkurile de mai jos.",
+  "swap_merge_direction": "Schimbați direcția de îmbinare",
+  "sync": "Sincronizare",
+  "tag": "Etichetă",
+  "tag_assets": "Eticheta resurselor",
+  "tag_created": "Etichetă creată: {tag}",
+  "tag_feature_description": "Răsfoirea fotografiilor și videoclipurilor grupate după subiecte de etichete logice",
+  "tag_not_found_question": "Nu puteți găsi o etichetă? <link>Creați o etichetă nouă.</link>",
+  "tag_updated": "Etichetă actualizată: {tag}",
+  "tagged_assets": "Etichetat {count, plural, one {# resursă} other {# resurse}}",
+  "tags": "Etichete",
+  "template": "Șablon",
+  "theme": "Temă",
+  "theme_selection": "Selectarea temei",
+  "theme_selection_description": "Setați automat tema la mod luminos sau întunecată, în funcție de preferințele de sistem ale browserului dvs",
+  "they_will_be_merged_together": "Vor fi îmbinate împreună",
+  "third_party_resources": "Resurse Terță Parte",
+  "time_based_memories": "Amintiri bazate pe timp",
+  "timeline": "Cronologie",
+  "timezone": "Fus orar",
+  "to_archive": "Arhivă",
+  "to_change_password": "Schimbaţi parola",
+  "to_favorite": "Favorit",
+  "to_login": "Conectare",
+  "to_parent": "Du-te la părinte",
+  "to_trash": "Coș de gunoi",
+  "toggle_settings": "Activați setările",
+  "toggle_theme": "Activați tema întunecată",
+  "total": "Total",
+  "total_usage": "Utilizare totală",
+  "trash": "Coș de gunoi",
+  "trash_all": "Ștergeți Tot",
+  "trash_count": "Ștergeți {count, number}",
+  "trash_delete_asset": "Coș de gunoi/Ștergeți resursa",
+  "trash_no_results_message": "Fotografiile și videoclipurile mutate în coșul de gunoi vor apărea aici.",
+  "trashed_items_will_be_permanently_deleted_after": "Elementele din coșul de gunoi vor fi șterse definitiv după {days, plural, one {# zi} other {# zile}}.",
+  "type": "Tip",
+  "unarchive": "Dezarhivați",
+  "unarchived_count": "{count, plural, other {dezarhivat #}}",
+  "unfavorite": "Ștergeți din favorite",
+  "unhide_person": "Dezvăluie persoana",
+  "unknown": "Necunoscut",
+  "unknown_year": "An Necunoscut",
+  "unlimited": "Nelimitat",
+  "unlink_motion_video": "Deconectați videoclipul în mișcare",
+  "unlink_oauth": "Deconectați OAuth",
+  "unlinked_oauth_account": "Cont OAuth deconectat",
+  "unnamed_album": "Album fără Nume",
+  "unnamed_album_delete_confirmation": "Sigur doriți să ștergeți acest album?",
+  "unnamed_share": "Partajare fără Nume",
+  "unsaved_change": "Modificare nesalvată",
+  "unselect_all": "Deselectați toate",
+  "unselect_all_duplicates": "Deselectați toate duplicatele",
+  "unstack": "Dezasamblați",
+  "unstacked_assets_count": "Nestivuit {count, plural, one {# resursă} other {# resurse}}",
+  "untracked_files": "Fișiere neurmărite",
+  "untracked_files_decription": "Aceste fișiere nu sunt urmărite de aplicație. Acestea pot fi rezultatele unor mișcări eșuate, încărcări întrerupte sau rămase din cauza unei erori",
+  "up_next": "Mai departe",
+  "updated_password": "Parolă actualizată",
+  "upload": "Încărcați",
+  "upload_concurrency": "Încărcați simultan",
+  "upload_errors": "Încărcare finalizată cu {count, plural, one {# eroare} other {# erori}}, reîmprospătați pagina pentru a reîncărca noile resurse.",
+  "upload_progress": "Rămas {remaining, number} - Procesat {processed, number}/{total, number}",
+  "upload_skipped_duplicates": "Sărit {count, plural, one {# duplicat resursă} other {# duplicate resurse}}",
+  "upload_status_duplicates": "Duplicate",
+  "upload_status_errors": "Erori",
+  "upload_status_uploaded": "Încărcat",
+  "upload_success": "Încărcare reușită, reîmprospătați pagina pentru a vedea resursele noi încărcate.",
+  "url": "URL",
+  "usage": "Utilizare",
+  "use_custom_date_range": "Utilizați în schimb un interval de date personalizat",
+  "user": "Utilizator",
+  "user_id": "ID utilizator",
+  "user_liked": "{user} a apreciat {type, select, photo {această imagine} video {acest video} asset {această resursă} other {it}}",
+  "user_purchase_settings": "Cumpărare",
+  "user_purchase_settings_description": "Gestionați-vă achiziția",
+  "user_role_set": "Setați {user} ca {role}",
+  "user_usage_detail": "Detalii despre utilizare",
+  "user_usage_stats": "Statistici de utilizare a contului",
+  "user_usage_stats_description": "Vedeți statisticile de utilizare a contului",
+  "username": "Nume de utilizator",
+  "users": "Utilizatori",
+  "utilities": "Utilitǎți",
+  "validate": "Validați",
+  "variables": "Variabile",
+  "version": "Versiune",
+  "version_announcement_closing": "Prietenul tǎu, Alex",
+  "version_announcement_message": "Bună! Este disponibilă o nouă versiune de Immich. Vă rugăm să vă faceți timp să citiți <link>notele de lansare</link> pentru a vă asigura că configurația dvs. este actualizată pentru a preveni orice configurare greșită, mai ales dacă utilizați WatchTower sau orice mecanism care se ocupă de actualizarea automată a instanței dvs. Immich.",
+  "version_history": "Istoric Versiuni",
+  "version_history_item": "Instalat {version} pe data de {date}",
+  "video": "Videoclip",
+  "video_hover_setting": "Redați miniatura video la trecerea cursorului",
+  "video_hover_setting_description": "Redați miniatura video când mouse-ul trece peste element. Chiar și atunci când este dezactivată, redarea poate fi pornită trecând cu mouse-ul peste pictograma de redare.",
+  "videos": "Videoclipuri",
+  "videos_count": "{count, plural, one {# Videoclip} other {# Videoclipuri}}",
+  "view": "Vizualizați",
+  "view_album": "Vizualizați Album",
+  "view_all": "Vizualizați Tot",
+  "view_all_users": "Vizulizați toți utilizatorii",
+  "view_in_timeline": "Vizualizați în cronologie",
+  "view_links": "Vizualizați scurtǎturi",
+  "view_name": "Vizualizare",
+  "view_next_asset": "Vizualizați următoarea resursă",
+  "view_previous_asset": "Vizualizați resursa anterioară",
+  "view_stack": "Vizualizați Stiva",
+  "visibility_changed": "Vizibilitatea schimbată pentru {count, plural, one {# persoană} other {# persoane}}",
+  "waiting": "Așteptați",
+  "warning": "Avertisment",
+  "week": "Sǎptǎmânǎ",
+  "welcome": "Bun venit",
+  "welcome_to_immich": "Bun venit la Immich",
+  "year": "An",
+  "years_ago": "acum {years, plural, one {# an} other {# ani}} în urmă",
+  "yes": "Da",
+  "you_dont_have_any_shared_links": "Nu aveți linkuri partajate",
+  "zoom_image": "Măriți Imaginea"
+}
diff --git a/web/src/lib/i18n/ru.json b/i18n/ru.json
similarity index 84%
rename from web/src/lib/i18n/ru.json
rename to i18n/ru.json
index 660f7bc84c..520a84406c 100644
--- a/web/src/lib/i18n/ru.json
+++ b/i18n/ru.json
@@ -1,7 +1,7 @@
 {
   "about": "О продукте",
   "account": "Учётная запись",
-  "account_settings": "Настройки учётной записи",
+  "account_settings": "Настройки аккаунта",
   "acknowledge": "Подтвердить",
   "action": "Действие",
   "actions": "Действия",
@@ -23,53 +23,65 @@
   "add_to": "Добавить в...",
   "add_to_album": "Добавить в альбом",
   "add_to_shared_album": "Добавить в общий альбом",
+  "add_url": "Добавить URL",
   "added_to_archive": "Добавлено в архив",
   "added_to_favorites": "Добавлено в избранное",
   "added_to_favorites_count": "Добавлено{count, number} в избранное",
   "admin": {
     "add_exclusion_pattern_description": "Добавьте шаблоны исключений. Подстановка с использованием *, ** и ? поддерживается. Чтобы игнорировать все файлы в любом каталоге с именем «Raw», используйте «**/Raw/**». Чтобы игнорировать все файлы, заканчивающиеся на «.tif», используйте «**/*.tif». Чтобы игнорировать абсолютный путь, используйте «/path/to/ignore/**».",
+    "asset_offline_description": "Этот файл внешней библиотеки не был найден на диске и был перемещён в корзину. Если файл был перемещён внутри библиотеки, проверьте временную шкалу, чтобы найти новый соответствующий ресурс. Чтобы восстановить файл, убедитесь, что путь ниже доступен для Immich и выполните сканирование библиотеки.",
     "authentication_settings": "Настройки аутентификации",
     "authentication_settings_description": "Управление паролями, OAuth и другими настройками аутентификации",
     "authentication_settings_disable_all": "Вы уверены, что хотите отключить все методы входа? Вход будет полностью отключен.",
     "authentication_settings_reenable": "Чтобы снова включить, используйте <link>Команда Сервера</link>.",
     "background_task_job": "Фоновые задачи",
+    "backup_database": "Резервное копирование базы данных",
+    "backup_database_enable_description": "Включить резервное копирование базы данных",
+    "backup_keep_last_amount": "Количество хранимых резервных копий",
+    "backup_settings": "Настройки резервного копирования",
+    "backup_settings_description": "Управление настройками резервного копирования базы данных",
     "check_all": "Проверить все",
     "cleared_jobs": "Очищены задачи для: {job}",
     "config_set_by_file": "Настроено с помощью файла конфигурации",
     "confirm_delete_library": "Вы действительно хотите удалить библиотеку \"{library}\"?",
-    "confirm_delete_library_assets": "Вы уверены, что хотите удалить эту библиотеку? Это безвозвратно удалит {count, plural, one {# содержимый объект} few {# содержимых объекта} other {all # содержимых объектов}} с Immich. Файлы останутся на диске.",
+    "confirm_delete_library_assets": "Вы уверены, что хотите удалить эту библиотеку? Это безвозвратно удалит {count, plural, one {# содержимый объект} few {# содержимых объекта} other {all # содержимых объектов}} из Immich. Файлы останутся на диске.",
     "confirm_email_below": "Чтобы подтвердить, введите \"{email}\" ниже",
     "confirm_reprocess_all_faces": "Вы уверены, что хотите повторно определить все лица? Будут также удалены имена со всех лиц.",
     "confirm_user_password_reset": "Вы уверены, что хотите сбросить пароль пользователя {user}?",
-    "crontab_guru": "Crontab Guru",
+    "create_job": "Создать задание",
+    "cron_expression": "Выражение cron",
+    "cron_expression_description": "Задайте интервал сканирований в формате cron. Для получения дополнительной информации, ознакомьтесь с <link>Crontab Guru</link>",
+    "cron_expression_presets": "Предустановки выражений cron",
     "disable_login": "Отключить вход",
-    "disabled": "Выключено",
     "duplicate_detection_job_description": "Запускает определение похожих изображений при помощи машинного зрения (зависит от умного поиска)",
     "exclusion_pattern_description": "Шаблоны исключения позволяют игнорировать файлы и папки при сканировании вашей библиотеки. Это полезно, если у вас есть папки, содержащие файлы, которые вы не хотите импортировать, например, RAW-файлы.",
     "external_library_created_at": "Внешняя библиотека (создана {date})",
     "external_library_management": "Управление внешними библиотеками",
     "face_detection": "Обнаружение лиц",
-    "face_detection_description": "Обнаруживает лица на ресурсах с помощью машинного обучения. Для видео учитывается только миниатюра. “Все” - обрабатывает все ресурсы. “Пропущенные” - в очередь помещаются только не обработанные ресурсы. Обнаруженные лица будут помещены в очередь для распознавания лиц после завершения обнаружения лиц, объединяя их в существующие или новые группы людей.",
-    "facial_recognition_job_description": "Группирует распознанные лица по людям. Этот шаг выполняется после завершения обнаружения лиц. “Все” - группирует все лица. “Пропущенные” - помещает в очередь лица, не привязанные к человеку.",
+    "face_detection_description": "Обнаруживает лица на медиа с помощью машинного обучения. Для видео учитывается только миниатюра. “Обновить” — обработать все медиа. “Сброс” — удалить все имеющиеся данные лиц и обработать заново. “Пропущенные” — добавить в очередь необработанные медиа. Обнаруженные лица будут помещены в очередь распознавания для привязки к существующим или новым людям.",
+    "facial_recognition_job_description": "Группирует распознанные лица по людям. Этот шаг выполняется после завершения обнаружения лиц. “Сброс” - группирует все лица. “Пропущенные” - помещает в очередь лица, не привязанные к человеку.",
     "failed_job_command": "Команда {command} не выполнена для задачи: {job}",
-    "force_delete_user_warning": "ПРЕДУПРЕЖДЕНИЕ: Это приведет к немедленному удалению пользователя и всех ресурсов. Это невозможно отменить, и файлы не могут быть восстановлены.",
+    "force_delete_user_warning": "ПРЕДУПРЕЖДЕНИЕ: Это приведет к немедленному удалению пользователя и его ресурсов. Это действие невозможно отменить, и файлы не могут быть восстановлены.",
     "forcing_refresh_library_files": "Принудительное обновление всех файлов библиотеки",
+    "image_format": "Формат",
     "image_format_description": "WebP создает файлы меньшего размера, чем JPEG, но кодирует медленнее.",
     "image_prefer_embedded_preview": "Предпочитать встроенное превью",
     "image_prefer_embedded_preview_setting_description": "Используйте встроенные превью в фотографиях RAW в качестве входных данных для обработки изображений, если они доступны. Это может обеспечить более точную цветопередачу для некоторых изображений, но качество предварительного просмотра зависит от камеры, и изображение может иметь больше артефактов сжатия.",
     "image_prefer_wide_gamut": "Предпочитаю широкую гамму",
     "image_prefer_wide_gamut_setting_description": "Используйте Display P3 для миниатюр. Это лучше сохраняет яркость изображений с широким цветовым пространством, но изображения могут выглядеть по-другому на старых устройствах со старой версией браузера. Изображения sRGB сохраняются в формате sRGB, что позволяет избежать цветовых сдвигов.",
-    "image_preview_format": "Формат превью",
-    "image_preview_resolution": "Разрешение превью",
-    "image_preview_resolution_description": "Используется при просмотре одной фотографии и для машинного обучения. Более высокие разрешения позволяют сохранить больше деталей, но требуют больше времени для кодирования, имеют больший размер файлов и могут снизить скорость отклика приложения.",
+    "image_preview_description": "Изображение среднего размера без метаданных, используемое при отдельном просмотре и для машинного обучения",
+    "image_preview_quality_description": "Качество предварительного просмотра от 1 до 100. Чем выше, тем лучше, но при этом создаются файлы большего размера и может снизиться скорость отклика приложения. Установка низкого значения может повлиять на качество машинного обучения.",
+    "image_preview_title": "Настройки предварительного просмотра",
     "image_quality": "Качество",
-    "image_quality_description": "Качество изображения от 1 до 100. Чем выше число, тем лучше качество и больше вес изображения.",
+    "image_resolution": "Разрешение",
+    "image_resolution_description": "Более высокое разрешение позволяет сохранить больше деталей, но требует больше времени для кодирования, приводит к увеличению размера файлов и может снизить скорость отклика приложения.",
     "image_settings": "Настройки изображений",
     "image_settings_description": "Управление качеством и разрешением создаваемых изображений",
-    "image_thumbnail_format": "Формат миниатюр",
-    "image_thumbnail_resolution": "Разрешение миниатюр",
-    "image_thumbnail_resolution_description": "Используется при просмотре групп фотографий (на временной шкале, при просмотре альбомов и т.д.). Миниатюры с более высоким разрешением сохраняют больше деталей, но требуют больше времени для кодирования, имеют больший вес и могут снизить скорость отклика приложения.",
+    "image_thumbnail_description": "Маленькая миниатюра с удаленными метаданными, используемая при просмотре групп фотографий, таких как основная временная шкала",
+    "image_thumbnail_quality_description": "Качество миниатюр от 1 до 100. Чем выше качество, тем лучше, но при этом создаются файлы большего размера и может снизиться скорость отклика приложения.",
+    "image_thumbnail_title": "Настройки миниатюр",
     "job_concurrency": "Параллельная обработка задания - {job}",
+    "job_created": "Задание создано",
     "job_not_concurrency_safe": "Эта задача не обеспечивает безопасность параллельности выполнения.",
     "job_settings": "Настройки заданий",
     "job_settings_description": "Управление параллельной обработкой заданий",
@@ -77,9 +89,6 @@
     "jobs_delayed": "{jobCount, plural, one {# отложена} other {# отложено}}",
     "jobs_failed": "{jobCount, plural, other {# не удалось выполнить}}",
     "library_created": "Созданная библиотека: {library}",
-    "library_cron_expression": "Выражение планировщика",
-    "library_cron_expression_description": "Установите интервал сканирования, используя формат планировщика. Для получения дополнительной информации, пожалуйста, обратитесь к примеру на <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Шаблоны настройки планировщика",
     "library_deleted": "Библиотека удалена",
     "library_import_path_description": "Укажите папку для импорта. Эта папка, включая вложенные папки, будет проверена на наличие изображений и видео.",
     "library_scanning": "Периодическое сканирование",
@@ -98,7 +107,7 @@
     "machine_learning_clip_model_description": "Названия моделей CLIP размещены <link>здесь</link>. Обратите внимание, что при изменении модели необходимо заново запустить задачу «Интеллектуальный поиск» для всех изображений.",
     "machine_learning_duplicate_detection": "Поиск дубликатов",
     "machine_learning_duplicate_detection_enabled": "Включить обнаружение дубликатов",
-    "machine_learning_duplicate_detection_enabled_description": "Если этот параметр отключен, абсолютно идентичные ресурсы всё равно будут удалены из дубликатов.",
+    "machine_learning_duplicate_detection_enabled_description": "Если этот параметр отключен, абсолютно идентичные файлы всё равно будут удалены из дубликатов.",
     "machine_learning_duplicate_detection_setting_description": "Используйте встраивания CLIP для поиска вероятных дубликатов",
     "machine_learning_enabled": "Включите машинное обучение",
     "machine_learning_enabled_description": "При отключении, все функции ML будут отключены независимо от следующих параметров.",
@@ -122,7 +131,7 @@
     "machine_learning_smart_search_description": "Семантический поиск изображений с использованием вложений CLIP",
     "machine_learning_smart_search_enabled": "Включить интеллектуальный поиск",
     "machine_learning_smart_search_enabled_description": "Если этот параметр отключен, изображения не будут кодироваться для интеллектуального поиска.",
-    "machine_learning_url_description": "URL-адрес сервера машинного обучения",
+    "machine_learning_url_description": "URL-адрес сервера машинного обучения. Если указаны несколько, запросы будут посланы на каждый, с первого до последнего, по очереди, пока не будет получен успешный ответ.",
     "manage_concurrency": "Управление параллельностью заданий",
     "manage_log_settings": "Управление настройками журнала",
     "map_dark_style": "Тёмный стиль",
@@ -139,20 +148,20 @@
     "map_settings_description": "Управление настройками карты",
     "map_style_description": "URL-адрес темы карты style.json",
     "metadata_extraction_job": "Извлечение метаданных",
-    "metadata_extraction_job_description": "Извлекает метаданные из каждого ресурса, такие как координаты GPS и разрешение",
+    "metadata_extraction_job_description": "Извлекает метаданные из каждого файла, такие как местоположение, лица и разрешение",
     "metadata_faces_import_setting": "Включить импорт лиц",
     "metadata_faces_import_setting_description": "Импорт лиц из изображений EXIF-данных и файлов sidecar",
     "metadata_settings": "Настройки метаданных",
     "metadata_settings_description": "Управление настройками метаданных",
     "migration_job": "Миграция",
-    "migration_job_description": "Выполняет перенос миниатюр для ресурсов и лиц в последнюю структуру папок",
+    "migration_job_description": "Выполняет перенос миниатюр ресурсов и лиц в последнюю структуру папок",
     "no_paths_added": "Пути не добавлены",
     "no_pattern_added": "Шаблон не добавлен",
-    "note_apply_storage_label_previous_assets": "Примечание: Чтобы применить тег хранилища к ранее загруженным ресурсам запустите",
+    "note_apply_storage_label_previous_assets": "Примечание: Чтобы применить тег хранилища к ранее загруженным ресурсам, запустите",
     "note_cannot_be_changed_later": "ПРИМЕЧАНИЕ: Это невозможно изменить позже!",
     "note_unlimited_quota": "Примечание: Введите 0 для неограниченной квоты или оставьте пустым",
     "notification_email_from_address": "Адрес отправителя",
-    "notification_email_from_address_description": "Адрес электронной почты отправителя, например: \"Immich Photo Server <noreply@immich.app>\"",
+    "notification_email_from_address_description": "Адрес электронной почты отправителя, например: \"Immich Photo Server <noreply@example.com>\"",
     "notification_email_host_description": "Доменное имя почтового сервера (например, smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Игнорировать ошибки сертификата",
     "notification_email_ignore_certificate_errors_description": "Игнорировать ошибки проверки сертификата TLS (не рекомендуется)",
@@ -198,22 +207,24 @@
     "password_settings": "Настройки входа с паролем",
     "password_settings_description": "Управление настройками входа по паролю",
     "paths_validated_successfully": "Все пути успешно прошли проверку",
+    "person_cleanup_job": "Очистка персоны",
     "quota_size_gib": "Размер квоты (ГБ)",
     "refreshing_all_libraries": "Обновление всех библиотек",
     "registration": "Регистрация Администратора",
     "registration_description": "Поскольку вы являетесь первым пользователем в системе, вам будет присвоена роль администратора, и вы будете отвечать за административные задачи. Дополнительных пользователей будете создавать вы.",
-    "removing_offline_files": "Удаление недоступных файлов",
     "repair_all": "Починить всё",
     "repair_matched_items": "Соответствует {count, plural, one {# элементу} few {# элементам} many {# элементам} other {# элементам}}",
     "repaired_items": "Восстановлено {count, plural, one {# элемент} few {# элемента} many {# элементов} other {# элемента}}",
     "require_password_change_on_login": "Требовать смену пароля при первом входе",
     "reset_settings_to_default": "Сброс настроек до значений по умолчанию",
     "reset_settings_to_recent_saved": "Сбросьте настройки к последним сохраненным настройкам",
-    "scanning_library_for_changed_files": "Поиск измененных файлов",
-    "scanning_library_for_new_files": "Поиск новых файлов",
+    "scanning_library": "Сканирование библиотеки",
+    "search_jobs": "Поиск заданий...",
     "send_welcome_email": "Отправить приветственное письмо",
     "server_external_domain_settings": "Внешний домен",
-    "server_external_domain_settings_description": "Домен для общедоступных ссылок, включая http(s)://",
+    "server_external_domain_settings_description": "Домен для публичных ссылок, включая http(s)://",
+    "server_public_users": "Публичные пользователи",
+    "server_public_users_description": "Отображать всех пользователей (имена и email) для добавления в общие альбомы. Когда отключено, список пользователей будет доступен только администраторам.",
     "server_settings": "Настройки сервера",
     "server_settings_description": "Управление настройками сервера",
     "server_welcome_message": "Приветственное сообщение",
@@ -221,8 +232,8 @@
     "sidecar_job": "Метаданные из sidecar-файлов",
     "sidecar_job_description": "Обнаруживает и синхронизирует метаданные из sidecar-файлов",
     "slideshow_duration_description": "Количество секунд для отображения каждого изображения",
-    "smart_search_job_description": "Запускает машинное обучение на объектах для поддержки умного поиска",
-    "storage_template_date_time_description": "Время создание объекта использовано как информация о времени съемки",
+    "smart_search_job_description": "Распознает содержимое медиафайлов для умного поиска",
+    "storage_template_date_time_description": "Время создания файла использовано как информация о времени съемки",
     "storage_template_date_time_sample": "Время выборки {date}",
     "storage_template_enable_description": "Включить механизм шаблонов хранилища",
     "storage_template_hash_verification_enabled": "Включить проверку хеша",
@@ -238,6 +249,17 @@
     "storage_template_settings_description": "Управление структурой папок и именем загружаемого файла",
     "storage_template_user_label": "<code>{label}</code> - это метка хранилища пользователя",
     "system_settings": "Системные настройки",
+    "tag_cleanup_job": "Очистка тега",
+    "template_email_available_tags": "В этом шаблоне доступны следующие переменные: {tags}",
+    "template_email_if_empty": "Оставьте пустым, чтобы использовать шаблон по умолчанию.",
+    "template_email_invite_album": "Шаблон приглашения в альбом",
+    "template_email_preview": "Предварительный просмотр",
+    "template_email_settings": "Шаблоны эл. писем",
+    "template_email_settings_description": "Настройте шаблоны уведомлений по эл. почте",
+    "template_email_update_album": "Шаблон изменения альбома",
+    "template_email_welcome": "Шаблон приветствия",
+    "template_settings": "Шаблоны уведомлений",
+    "template_settings_description": "Настройте шаблоны уведомлений.",
     "theme_custom_css_settings": "Пользовательские CSS",
     "theme_custom_css_settings_description": "Каскадные таблицы стилей позволяют настраивать дизайн Immich.",
     "theme_settings": "Настройки темы",
@@ -245,7 +267,6 @@
     "these_files_matched_by_checksum": "Эти файлы сопоставляются по их контрольным суммам",
     "thumbnail_generation_job": "Создание миниатюр",
     "thumbnail_generation_job_description": "Создает большие, маленькие и размытые миниатюры для каждого файла и человека",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "API ускорителя",
     "transcoding_acceleration_api_description": "API, который будет взаимодействовать с вашим устройством для ускорения транскодирования. Эта настройка является «наилучшим вариантом»: при сбое она будет возвращаться к программному транскодированию. VP9 может работать или не работать в зависимости от вашего оборудования.",
     "transcoding_acceleration_nvenc": "NVENC (требуется графический процессор NVIDIA)",
@@ -271,7 +292,7 @@
     "transcoding_hardware_acceleration": "Аппаратное ускорение",
     "transcoding_hardware_acceleration_description": "Экспериментальный; намного быстрее, но будет иметь более низкое качество при том же битрейте",
     "transcoding_hardware_decoding": "Аппаратное декодирование",
-    "transcoding_hardware_decoding_setting_description": "Применяется только к NVENC и RKMPP. Включает сквозное ускорение, а не только ускорение кодирования. Может работать не со всеми видео.",
+    "transcoding_hardware_decoding_setting_description": "Включает сквозное ускорение, а не только ускорение кодирования. Может работать не со всеми видео.",
     "transcoding_hevc_codec": "Кодек HEVC",
     "transcoding_max_b_frames": "Максимально промежуточных кадров",
     "transcoding_max_b_frames_description": "Более высокие значения повышают эффективность сжатия, но замедляют кодирование. Может быть несовместимо с аппаратным ускорением на старых устройствах. 0 отключает B-кадры, а -1 устанавливает это значение автоматически.",
@@ -297,8 +318,6 @@
     "transcoding_threads_description": "Более высокие значения приводят к более быстрому кодированию, но оставляют серверу меньше места для обработки других задач во время активности. Это значение не должно превышать количество ядер процессора. Максимизирует использование, если установлено значение 0.",
     "transcoding_tone_mapping": "Отображение тонов",
     "transcoding_tone_mapping_description": "Пытается сохранить внешний вид HDR-видео при преобразовании в SDR. Каждый алгоритм делает разные компромиссы между цветом, детализацией и яркостью. Hable сохраняет детали, Mobius сохраняет цвет, а Reinhard сохраняет яркость.",
-    "transcoding_tone_mapping_npl": "Отображение тонов NPL",
-    "transcoding_tone_mapping_npl_description": "Цвета будут отрегулированы так, чтобы выглядеть нормально для дисплея такой яркости. Как ни странно, более низкие значения увеличивают яркость видео и наоборот, поскольку компенсируют яркость дисплея. 0 устанавливает это значение автоматически.",
     "transcoding_transcode_policy": "Политика перекодирования",
     "transcoding_transcode_policy_description": "Правила, определяющие когда видео должно быть перекодировано. HDR-видео всегда будут перекодироваться (за исключением случаев, когда перекодирование отключено).",
     "transcoding_two_pass_encoding": "Двухпроходное кодирование",
@@ -307,16 +326,17 @@
     "transcoding_video_codec_description": "VP9 обладает высокой эффективностью и веб-совместимостью, но перекодирование занимает больше времени. HEVC работает аналогично, но имеет меньшую веб-совместимость. H.264 широко совместим и быстро перекодируется, но создает файлы гораздо большего размера. AV1 — наиболее эффективный кодек, но он не поддерживается на старых устройствах.",
     "trash_enabled_description": "Включить корзину",
     "trash_number_of_days": "Срок хранения",
-    "trash_number_of_days_description": "Количество дней, в течение которых объекты будут храниться в корзине, прежде чем они будут окончательно удалены",
+    "trash_number_of_days_description": "Количество дней, в течение которых файлы будут храниться в корзине до окончательного удаления",
     "trash_settings": "Настройки корзины",
     "trash_settings_description": "Управление настройками корзины",
     "untracked_files": "НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ",
     "untracked_files_description": "Приложение не отслеживает эти файлы. Они могут быть результатом неудачных перемещений, прерванных загрузок или пропущены из-за ошибки",
-    "user_delete_delay": "Аккаунт и ресурсы пользователя <b>{user}</b> будут запланированы для окончательного удаления через {delay, plural, one {# день} few {# дня} many {# дней} other {# дня}}.",
+    "user_cleanup_job": "Очистка пользователя",
+    "user_delete_delay": "Аккаунт и файлы пользователя <b>{user}</b> будут отложены до окончательного удаления через {delay, plural, one {# день} few {# дня} many {# дней} other {# дня}}.",
     "user_delete_delay_settings": "Отложенное удаление",
-    "user_delete_delay_settings_description": "Срок в днях, по истечение которого происходит окончательное удаление учетной записи пользователя и его ресурсов после удаления учётной записи. Задача по удалению пользователей выполняется в полночь. Изменения этой настройки будут учтены при следующем запуске задачи.",
-    "user_delete_immediately": "Аккаунт и ресурсы пользователя <b>{user}</b> будут поставлены в очередь на <b>немедленное</b> окончательное удаление.",
-    "user_delete_immediately_checkbox": "Поставить пользователя и объекты в очередь для удаления",
+    "user_delete_delay_settings_description": "Срок в днях, по истечение которого происходит окончательное удаление учетной записи пользователя и его ресурсов. Задача по удалению пользователей выполняется в полночь. Изменения этой настройки будут учтены при следующем запуске задачи.",
+    "user_delete_immediately": "Аккаунт и файлы пользователя <b>{user}</b> будут <b>немедленно</b> поставлены в очередь для окончательного удаления.",
+    "user_delete_immediately_checkbox": "Поместить пользователя и его файлы в очередь для немедленного удаления",
     "user_management": "Управление пользователями",
     "user_password_has_been_reset": "Пароль пользователя был сброшен:",
     "user_password_reset_description": "Пожалуйста, предоставьте временный пароль пользователю и сообщите ему, что при следующем входе в систему пароль нужно будет изменить.",
@@ -353,12 +373,12 @@
   "album_remove_user_confirmation": "Вы уверены, что хотите удалить пользователя {user}?",
   "album_share_no_users": "Похоже, вы поделились этим альбомом со всеми пользователями или у вас нет пользователей, с которыми можно поделиться.",
   "album_updated": "Альбом обновлён",
-  "album_updated_setting_description": "Получать уведомление по электронной почте, когда в общий альбом добавлены новые ресурсы",
+  "album_updated_setting_description": "Получать уведомление по электронной почте при добавлении новых ресурсов в общий альбом",
   "album_user_left": "Вы покинули {album}",
   "album_user_removed": "Пользователь {user} удален",
   "album_with_link_access": "Поделитесь ссылкой на альбом, чтобы ваши друзья могли его посмотреть.",
   "albums": "Альбомы",
-  "albums_count": "{count, plural, one {Альбом ({count, number})} few {Альбома ({count, number})} many {Альбомов ({count, number})} other {Альбомов ({count, number})}}",
+  "albums_count": "{count, plural, one {{count, number} альбом} few {{count, number} альбома} many {{count, number} альбомов} other {{count, number} альбомов}}",
   "all": "Все",
   "all_albums": "Все альбомы",
   "all_people": "Все люди",
@@ -378,35 +398,33 @@
   "archive_or_unarchive_photo": "Архивировать или разархивировать фото",
   "archive_size": "Размер архива",
   "archive_size_description": "Настройка размера архива для скачивания (в GiB)",
-  "archived": "Заархивировано",
   "archived_count": "{count, plural, other {Архивировано #}}",
   "are_these_the_same_person": "Это один и тот же человек?",
   "are_you_sure_to_do_this": "Вы уверены, что хотите это сделать?",
   "asset_added_to_album": "Добавлено в альбом",
   "asset_adding_to_album": "Добавление в альбом...",
-  "asset_description_updated": "Описание ресурса было обновлено",
+  "asset_description_updated": "Описание обновлено",
   "asset_filename_is_offline": "Объект {filename} находится в офлайн-режиме",
   "asset_has_unassigned_faces": "Есть не распознанные лица",
   "asset_hashing": "Хеширование...",
   "asset_offline": "Объект отключён",
-  "asset_offline_description": "Этот объект находится в офлайн-режиме. Immich не может получить доступ к его расположению. Пожалуйста, убедитесь, что объект доступен, и затем пересканируйте библиотеку.",
+  "asset_offline_description": "Этот внешний файл не найден на диске. Пожалуйста, свяжитесь с администратором Immich для получения помощи.",
   "asset_skipped": "Пропущено",
   "asset_skipped_in_trash": "В корзине",
   "asset_uploaded": "Загружено",
   "asset_uploading": "Загрузка...",
   "assets": "Объекты",
   "assets_added_count": "Добавлено {count, plural, one {# объект} few {# объекта} other {# объектов}}",
-  "assets_added_to_album_count": "Добавлено {count, plural, one {# объект} few {# объекта} other {# объектов}} в альбом",
-  "assets_added_to_name_count": "Добавлено {count, plural, one {# объект} other {# объектов}} в {hasName, select, true {<b>{name}</b>} other {новый альбом}}",
+  "assets_added_to_album_count": "В альбом добавлено {count, plural, one {# объект} few {# объекта} other {# объектов}}",
+  "assets_added_to_name_count": "Добавлено {count, plural, one {# объект} few {# объекта} other {# объектов}} в {hasName, select, true {<b>{name}</b>} other {новый альбом}}",
   "assets_count": "{count, plural, one {# объект} few {# объекта} other {# объектов}}",
-  "assets_moved_to_trash": "Перемещено {count, plural, one {# объект} few {# объекта} many {# объектов} other {# объекта}} в корзину",
   "assets_moved_to_trash_count": "{count, plural, one {# объект} few {# объекта} other {# объектов}} перемещено в корзину",
   "assets_permanently_deleted_count": "{count, plural, one {# объект} few {# объекта} other {# объектов}} удалено навсегда",
   "assets_removed_count": "{count, plural, one {# объект} few {# объекта} other {# объектов}} удалено",
-  "assets_restore_confirmation": "Вы уверены, что хотите восстановить все удаленные объекты? Это действие нельзя отменить!",
+  "assets_restore_confirmation": "Вы уверены, что хотите восстановить все объекты из корзины? Это действие нельзя отменить! Обратите внимание, что любые оффлайн-объекты не могут быть восстановлены таким способом.",
   "assets_restored_count": "{count, plural, one {# объект} few {# объекта} other {# объектов}} восстановлено",
   "assets_trashed_count": "{count, plural, one {# объект} few {# объекта} other {# объектов}} перемещено в корзину",
-  "assets_were_part_of_album_count": "{count, plural, one {# Объект} other {# Объекты}} уже часть альбома",
+  "assets_were_part_of_album_count": "{count, plural, one {# объект} few {# объекта} other {# объектов}} уже в альбоме",
   "authorized_devices": "Разрешенные устройства",
   "back": "Назад",
   "back_close_deselect": "Назад, закрыть или отменить выбор",
@@ -414,11 +432,12 @@
   "birthdate_saved": "Дата рождения успешно сохранена",
   "birthdate_set_description": "Дата рождения используется для расчета возраста этого человека на момент фотографии.",
   "blurred_background": "Размытый фон",
+  "bugs_and_feature_requests": "Ошибки и запросы",
   "build": "Сборка",
   "build_image": "Версия сборки",
-  "bulk_delete_duplicates_confirmation": "Вы уверены, что хотите массово удалить {count, plural, one {# дублирующийся ресурс} other {# дублирующихся ресурсов}}? Это сохранит самый большой ресурс из каждой группы и навсегда удалит все остальные дубликаты. Это действие нельзя отменить!",
-  "bulk_keep_duplicates_confirmation": "Вы уверены, что хотите оставить {count, plural, one {# дублирующийся ресурс} other {# дублирующихся ресурсов}}? Это разрешит все группы дубликатов без удаления чего-либо.",
-  "bulk_trash_duplicates_confirmation": "Вы уверены, что хотите массово переместить в корзину {count, plural, one {# дублирующийся ресурс} other {# дублирующихся ресурсов}}? Это сохранит самый большой ресурс из каждой группы и переместит в корзину все остальные дубликаты.",
+  "bulk_delete_duplicates_confirmation": "Вы уверены, что хотите массово удалить {count, plural, one {# дублирующийся объект} other {# дублирующихся объектов}}? Это сохранит самый большой файл из каждой группы и навсегда удалит дубликаты. Это действие нельзя отменить!",
+  "bulk_keep_duplicates_confirmation": "Вы уверены, что хотите оставить {count, plural, one {# дублирующийся объект} other {# дублирующихся объектов}}? Это сохранит все дубликаты.",
+  "bulk_trash_duplicates_confirmation": "Вы уверены, что хотите массово переместить в корзину {count, plural, one {# дублирующийся объект} other {# дублирующихся объектов}}? Это сохранит самый большой файл из каждой группы и переместит дубликаты в корзину.",
   "buy": "Приобретение лицензии Immich",
   "camera": "Камера",
   "camera_brand": "Производитель",
@@ -428,10 +447,6 @@
   "cannot_merge_people": "Невозможно объединить людей",
   "cannot_undo_this_action": "Это действие нельзя отменить!",
   "cannot_update_the_description": "Невозможно обновить описание",
-  "cant_apply_changes": "Невозможно применить изменения",
-  "cant_get_faces": "Невозможно получить лица",
-  "cant_search_people": "Невозможно искать людей",
-  "cant_search_places": "Невозможно искать места",
   "change_date": "Изменить дату",
   "change_expiration_time": "Изменить время окончания",
   "change_location": "Изменить местоположение",
@@ -463,6 +478,7 @@
   "confirm": "Подтвердить",
   "confirm_admin_password": "Подтвердите пароль Администратора",
   "confirm_delete_shared_link": "Вы уверены, что хотите удалить эту публичную ссылку?",
+  "confirm_keep_this_delete_others": "Все остальные объекты в серии будут удалены, кроме этого объекта. Вы уверены, что хотите продолжить?",
   "confirm_password": "Подтвердите пароль",
   "contain": "Вместить",
   "context": "Контекст",
@@ -512,24 +528,28 @@
   "delete_key": "Удалить ключ",
   "delete_library": "Удалить библиотеку",
   "delete_link": "Удалить ссылку",
-  "delete_shared_link": "Удалить общую ссылку",
+  "delete_others": "Удалить остальные",
+  "delete_shared_link": "Удалить публичную ссылку",
   "delete_tag": "Удалить тег",
   "delete_tag_confirmation_prompt": "Вы уверены, что хотите удалить тег {tagName}?",
   "delete_user": "Удалить пользователя",
-  "deleted_shared_link": "Удалена публичная ссылка",
+  "deleted_shared_link": "Публичная ссылка удалена",
+  "deletes_missing_assets": "Удаляет объекты, отсутствующие на диске",
   "description": "Описание",
   "details": "Подробности",
   "direction": "Направление",
   "disabled": "Отключено",
   "disallow_edits": "Запретить редактирование",
+  "discord": "Общение в Discord",
   "discover": "Обнаружить",
   "dismiss_all_errors": "Сбросить все ошибки",
   "dismiss_error": "Сбросить ошибку",
   "display_options": "Настройки отображения",
   "display_order": "Порядок отображения",
   "display_original_photos": "Отображение оригинальных фотографий",
-  "display_original_photos_setting_description": "Предпочитать отображать исходную фотографию при просмотре ресурса, а не миниатюры, если исходный ресурс совместим с Интернетом. Это может привести к снижению скорости отображения фотографий.",
+  "display_original_photos_setting_description": "Предпочитать исходную фотографию при просмотре ресурса вместо миниатюры, если исходный ресурс поддерживается. Это может снизить скорости отображения фотографий.",
   "do_not_show_again": "Не показывать это сообщение в дальнейшем",
+  "documentation": "Документация",
   "done": "Готово",
   "download": "Скачать",
   "download_include_embedded_motion_videos": "Встроенные видео",
@@ -542,13 +562,6 @@
   "duplicates": "Дубликаты",
   "duplicates_description": "Разберитесь с каждой группой, указав, какие из них являются дубликатами, если таковые имеются",
   "duration": "Продолжительность",
-  "durations": {
-    "days": "{days, plural, one {день} few {# дня} many {# дней} other {# дня}}",
-    "hours": "{hours, plural, one {час} few {# часа} many {# часов} other {# часа}}",
-    "minutes": "{minutes, plural, one {минута} other {{minutes, number} минут}}",
-    "months": "{months, plural, one {месяц} other {{months, number} месяца}}",
-    "years": "{years, plural, one {год} few {# года} many {# лет} other {# года}}"
-  },
   "edit": "Редактировать",
   "edit_album": "Редактировать альбом",
   "edit_avatar": "Редактировать аватар",
@@ -573,8 +586,6 @@
   "editor_crop_tool_h2_aspect_ratios": "Соотношения сторон",
   "editor_crop_tool_h2_rotation": "Вращение",
   "email": "Электронная почта",
-  "empty": "",
-  "empty_album": "Пустой альбом",
   "empty_trash": "Очистить корзину",
   "empty_trash_confirmation": "Вы уверены, что хотите очистить корзину? Все объекты в корзине будут навсегда удалены из Immich.\nВы не сможете отменить это действие!",
   "enable": "Включить",
@@ -584,11 +595,11 @@
   "error_loading_image": "Ошибка при загрузке изображения",
   "error_title": "Ошибка - Что-то пошло не так",
   "errors": {
-    "cannot_navigate_next_asset": "Невозможно перейти к следующему объекту",
-    "cannot_navigate_previous_asset": "Не удается перейти к предыдущему ресурсу",
+    "cannot_navigate_next_asset": "Не удалось перейти к следующему объекту",
+    "cannot_navigate_previous_asset": "Не удалось перейти к предыдущему ресурсу",
     "cant_apply_changes": "Не удается применить изменения",
     "cant_change_activity": "Не удается {enabled, select, true {отключить} other {включить}} активность",
-    "cant_change_asset_favorite": "Не удается изменить статус \"избранное\" для ресурса",
+    "cant_change_asset_favorite": "Не удалось изменить статус \"избранное\" для ресурса",
     "cant_change_metadata_assets_count": "Не удается изменить метаданные у {count, plural, one {# ресурса} few {# ресурсов} many {# ресурсов} other {# ресурсов}}",
     "cant_get_faces": "Не удается получить лица",
     "cant_get_number_of_comments": "Не удается получить количество комментариев",
@@ -605,15 +616,16 @@
     "exclusion_pattern_already_exists": "Такая модель исключения уже существует.",
     "failed_job_command": "Команда {command} не выполнена для задачи: {job}",
     "failed_to_create_album": "Не удалось создать альбом",
-    "failed_to_create_shared_link": "Не удалось создать общую ссылку",
-    "failed_to_edit_shared_link": "Не удалось изменить общую ссылку",
+    "failed_to_create_shared_link": "Не удалось создать публичную ссылку",
+    "failed_to_edit_shared_link": "Не удалось изменить публичную ссылку",
     "failed_to_get_people": "Не удалось получить информацию о людях",
+    "failed_to_keep_this_delete_others": "Не удалось сохранить этот объект и удалить другие объекты",
     "failed_to_load_asset": "Ошибка загрузки объекта",
-    "failed_to_load_assets": "Ошибка загрузки объектов",
+    "failed_to_load_assets": "Не удалось загрузить объекты",
     "failed_to_load_people": "Не удалось загрузить людей",
     "failed_to_remove_product_key": "Не удалось удалить ключ продукта",
-    "failed_to_stack_assets": "Не удалось создать стек",
-    "failed_to_unstack_assets": "Не удалось разобрать стек",
+    "failed_to_stack_assets": "Не удалось сгруппировать объекты",
+    "failed_to_unstack_assets": "Не удалось разгруппировать объекты",
     "import_path_already_exists": "Этот путь импорта уже существует.",
     "incorrect_email_or_password": "Неверный адрес электронной почты или пароль",
     "paths_validation_failed": "{paths, plural, one {# путь} other {# путей}} не прошли проверку",
@@ -621,13 +633,13 @@
     "quota_higher_than_disk_size": "Вы установили квоту, превышающую размер диска",
     "repair_unable_to_check_items": "Невозможно проверить {count, select, one {элемент} other {элементы}}",
     "unable_to_add_album_users": "Невозможно добавить пользователей в альбом",
-    "unable_to_add_assets_to_shared_link": "Не удалось добавить ресурсы к общей ссылке",
+    "unable_to_add_assets_to_shared_link": "Не удалось добавить объекты к публичной ссылке",
     "unable_to_add_comment": "Невозможно добавить комментарий",
     "unable_to_add_exclusion_pattern": "Невозможно добавить шаблон исключения",
     "unable_to_add_import_path": "Не удается добавить путь импорта",
     "unable_to_add_partners": "Невозможно добавить партнеров",
-    "unable_to_add_remove_archive": "Не удалось {archived, select, true {удалить ресурс из} other {добавить ресурс в}} архив",
-    "unable_to_add_remove_favorites": "Не удалось {favorite, select, true {добавить ресурс в} other {удалить ресурс из}} избранного",
+    "unable_to_add_remove_archive": "Не удалось {archived, select, true {удалить ресурс из архива} other {добавить ресурс в архив}}",
+    "unable_to_add_remove_favorites": "Не удалось {favorite, select, true {добавить ресурс в избранное} other {удалить ресурс из избранного}}",
     "unable_to_archive_unarchive": "Не удалось {archived, select, true {архивировать} other {разархивировать}}",
     "unable_to_change_album_user_role": "Не удалось изменить роль пользователя в альбоме",
     "unable_to_change_date": "Невозможно изменить дату",
@@ -635,8 +647,6 @@
     "unable_to_change_location": "Невозможно изменить местоположение",
     "unable_to_change_password": "Невозможно изменить пароль",
     "unable_to_change_visibility": "Не удалось изменить видимость для {count, plural, one {# человека} few {# людей} many {# людей} other {# людей}}",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_complete_oauth_login": "Не удалось выполнить вход с помощью OAuth",
     "unable_to_connect": "Не удается подключиться",
     "unable_to_connect_to_server": "Не удалось подключиться к серверу",
@@ -646,11 +656,11 @@
     "unable_to_create_library": "Не удалось создать библиотеку",
     "unable_to_create_user": "Не удалось создать пользователя",
     "unable_to_delete_album": "Не удается удалить альбом",
-    "unable_to_delete_asset": "Не удается удалить ресурс",
+    "unable_to_delete_asset": "Не удалось удалить ресурс",
     "unable_to_delete_assets": "Ошибка при удалении ресурсов",
     "unable_to_delete_exclusion_pattern": "Не удается удалить шаблон исключения",
     "unable_to_delete_import_path": "Не удается удалить путь импорта",
-    "unable_to_delete_shared_link": "Не удается удалить общую ссылку",
+    "unable_to_delete_shared_link": "Не удалось удалить публичную ссылку",
     "unable_to_delete_user": "Не удается удалить пользователя",
     "unable_to_download_files": "Невозможно скачать файлы",
     "unable_to_edit_exclusion_pattern": "Невозможно отредактировать шаблон исключения",
@@ -659,11 +669,12 @@
     "unable_to_enter_fullscreen": "Не удается войти в полноэкранный режим",
     "unable_to_exit_fullscreen": "Не удается выйти из полноэкранного режима",
     "unable_to_get_comments_number": "Не удалось получить количество комментариев",
-    "unable_to_get_shared_link": "Не удалось получить общую ссылку",
+    "unable_to_get_shared_link": "Не удалось получить публичную ссылку",
     "unable_to_hide_person": "Невозможно скрыть персону",
+    "unable_to_link_motion_video": "Не удается связать движущееся видео",
     "unable_to_link_oauth_account": "Не удается связать учетную запись OAuth",
     "unable_to_load_album": "Невозможно загрузить альбом",
-    "unable_to_load_asset_activity": "Не удалось загрузить активность объекта",
+    "unable_to_load_asset_activity": "Не удалось загрузить комментарии",
     "unable_to_load_items": "Не удалось загрузить элементы",
     "unable_to_load_liked_status": "Невозможно загрузить статус лайка",
     "unable_to_log_out_all_devices": "Невозможно выйти из всех устройств",
@@ -673,15 +684,13 @@
     "unable_to_reassign_assets_existing_person": "Невозможно переназначить ресурсы на {name, select, null {существующего человека} other {{name}}}",
     "unable_to_reassign_assets_new_person": "Не удается переназначить ресурсы новому человеку",
     "unable_to_refresh_user": "Невозможно обновить пользователя",
-    "unable_to_remove_album_users": "Не удается удалить пользователей из альбома",
+    "unable_to_remove_album_users": "Не удалось удалить пользователей из альбома",
     "unable_to_remove_api_key": "Не удается удалить ключ API",
-    "unable_to_remove_assets_from_shared_link": "Невозможно удалить объекты из общей ссылки",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_assets_from_shared_link": "Невозможно удалить объекты из публичной ссылки",
+    "unable_to_remove_deleted_assets": "Не удается удалить автономные файлы",
     "unable_to_remove_library": "Не удается удалить библиотеку",
-    "unable_to_remove_offline_files": "Не удается удалить автономные файлы",
     "unable_to_remove_partner": "Не удается удалить партнера",
     "unable_to_remove_reaction": "Не удается удалить реакцию",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "Не удалось восстановить элементы",
     "unable_to_reset_password": "Не удается сбросить пароль",
     "unable_to_resolve_duplicate": "Не удалось разрешить дубликат",
@@ -699,9 +708,10 @@
     "unable_to_set_feature_photo": "Не удалось установить фотографию на обложку",
     "unable_to_set_profile_picture": "Невозможно установить изображение профиля",
     "unable_to_submit_job": "Невозможно отправить задание",
-    "unable_to_trash_asset": "Невозможно удалить актив",
+    "unable_to_trash_asset": "Не удалось переместить объект в корзину",
     "unable_to_unlink_account": "Не удалось отсоединить учетную запись",
-    "unable_to_update_album_cover": "Невозможно обновить обложку альбома",
+    "unable_to_unlink_motion_video": "Не удается отсоединить движущееся видео",
+    "unable_to_update_album_cover": "Не удалось обновить обложку альбома",
     "unable_to_update_album_info": "Невозможно обновить информацию об альбоме",
     "unable_to_update_library": "Не удалось обновить библиотеку",
     "unable_to_update_location": "Не удалось обновить местоположение",
@@ -710,17 +720,13 @@
     "unable_to_update_user": "Не удалось обновить пользователя",
     "unable_to_upload_file": "Невозможно загрузить файл"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exif": "Exif",
   "exit_slideshow": "Выйти из слайд-шоу",
   "expand_all": "Развернуть всё",
   "expire_after": "Истекает через",
   "expired": "Срок действия истек",
   "expires_date": "Срок действия до {date}",
-  "explore": "Просмотр",
+  "explore": "Поиск",
   "explorer": "Проводник",
   "export": "Экспортировать",
   "export_as_json": "Экспорт в JSON",
@@ -728,33 +734,28 @@
   "external": "Внешний",
   "external_libraries": "Внешние библиотеки",
   "face_unassigned": "Не назначено",
-  "failed_to_get_people": "Ошибка при получении людей",
+  "failed_to_load_assets": "Не удалось загрузить объекты",
   "favorite": "Избранное",
   "favorite_or_unfavorite_photo": "Добавить или удалить фотографию из избранного",
   "favorites": "Избранное",
-  "feature": "",
   "feature_photo_updated": "Избранное фото обновлено",
-  "featurecollection": "",
   "features": "Дополнительные возможности",
   "features_setting_description": "Управление дополнительными возможностями приложения",
   "file_name": "Имя файла",
   "file_name_or_extension": "Имя файла или расширение",
   "filename": "Имя файла",
-  "files": "",
   "filetype": "Тип файла",
   "filter_people": "Фильтр по людям",
   "find_them_fast": "Быстро найдите их по имени с помощью поиска",
   "fix_incorrect_match": "Исправить неправильное соответствие",
   "folders": "Папки",
   "folders_feature_description": "Просмотр папок с фотографиями и видео в файловой системе",
-  "force_re-scan_library_files": "Принудительное повторное сканирование всех файлов библиотеки",
-  "forward": "Переслать",
+  "forward": "Вперёд",
   "general": "Общие",
   "get_help": "Получить помощь",
   "getting_started": "Приступая к работе",
   "go_back": "Назад",
   "go_to_search": "Перейти к поиску",
-  "go_to_share_page": "Перейти на страницу для обмена",
   "group_albums_by": "Группировать альбомы по...",
   "group_no": "Без группировки",
   "group_owner": "Группировать по владельцу",
@@ -780,10 +781,6 @@
   "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} снятое в {city}, {country} с {person1} и {person2} {date}",
   "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} снятое в {city}, {country} с {person1}, {person2}, и {person3} {date}",
   "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} снятое в {city}, {country} с {person1}, {person2}, и еще с {additionalCount, number} людьми {date}",
-  "image_alt_text_people": "{count, plural, =1 {с {person1}} =2 {с {person1} и {person2}} =3 {с {person1}, {person2}, и {person3}} other {с {person1}, {person2}, и {others, number} др.}}",
-  "image_alt_text_place": "в {city}, {country}",
-  "image_taken": "{isVideo, select, true {Снято видео} other {Сделано фото}}",
-  "img": "",
   "immich_logo": "Лого Immich",
   "immich_web_interface": "Веб интерфейс Immich",
   "import_from_json": "Импорт из JSON",
@@ -792,7 +789,7 @@
   "in_archive": "В архиве",
   "include_archived": "Отображать архив",
   "include_shared_albums": "Включать общие альбомы",
-  "include_shared_partner_assets": "Включать общие активы партнеров",
+  "include_shared_partner_assets": "Включать общие ресурсы партнера",
   "individual_share": "Персональный доступ",
   "info": "Информация",
   "interval": {
@@ -804,10 +801,11 @@
   "invite_people": "Пригласить",
   "invite_to_album": "Пригласить в альбом",
   "items_count": "{count, plural, one {# элемент} two {# элемента} few {# элемента} other {# элементов}}",
-  "job_settings_description": "",
   "jobs": "Задачи",
   "keep": "Оставить",
   "keep_all": "Сохранить всё",
+  "keep_this_delete_others": "Оставить этот, удалить остальные",
+  "kept_this_deleted_others": "Сохранил этот объект и удалил {count, plural, one {# объект} other {# объектов}}",
   "keyboard_shortcuts": "Сочетания клавиш",
   "language": "Язык",
   "language_setting_description": "Выберите предпочитаемый вами язык",
@@ -819,31 +817,6 @@
   "level": "Уровень",
   "library": "Библиотека",
   "library_options": "Опции библиотеки",
-  "license_account_info": "Ваш аккаунт лицензирован",
-  "license_activated_subtitle": "Спасибо за поддержку Immich и open-source ПО",
-  "license_activated_title": "Ваша лицензия была успешно активирована",
-  "license_button_activate": "Активировать",
-  "license_button_buy": "Купить",
-  "license_button_buy_license": "Купить лицензию",
-  "license_button_select": "Выбрать",
-  "license_failed_activation": "Не удалось активировать лицензию. Проверьте корректный лицензионный ключ в электронной почте!",
-  "license_individual_description_1": "1 лицензия на пользователя на любом сервере",
-  "license_individual_title": "Индивидуальная лицензия",
-  "license_info_licensed": "Лицензированный",
-  "license_info_unlicensed": "Нет лицензии",
-  "license_input_suggestion": "Уже есть лицензия? Введите ключ ниже",
-  "license_license_subtitle": "Купить лицензию для поддержки Immich",
-  "license_license_title": "ЛИЦЕНЗИЯ",
-  "license_lifetime_description": "Пожизненная лицензия",
-  "license_per_server": "за сервер",
-  "license_per_user": "за пользователя",
-  "license_server_description_1": "1 лицензия на сервер",
-  "license_server_description_2": "Лицензия для всех пользователей сервера",
-  "license_server_title": "Серверная Лицензия",
-  "license_trial_info_1": "Вы используете Immich без лицензии",
-  "license_trial_info_2": "Вы используете Immich примерно",
-  "license_trial_info_3": "{accountAge, plural, one {# день} other {# дней}}",
-  "license_trial_info_4": "Пожалуйста, рассмотрите возможность приобретения лицензии для поддержки дальнейшего развития проекта",
   "light": "Светлая",
   "like_deleted": "Лайк удален",
   "link_motion_video": "Ссылка на движущееся видео",
@@ -865,9 +838,10 @@
   "look": "Просмотр",
   "loop_videos": "Циклическое воспроизведение",
   "loop_videos_description": "Включить циклическое воспроизведение видео.",
+  "main_branch_warning": "Вы используете версию для разработки; мы настоятельно рекомендуем использовать релизную версию!",
   "make": "Производитель",
-  "manage_shared_links": "Управление общими ссылками",
-  "manage_sharing_with_partners": "Управление обменом информацией с партнерами",
+  "manage_shared_links": "Управление публичными ссылками",
+  "manage_sharing_with_partners": "Управление обменом информацией с партнерами. Эта функция позволяет вашему партнеру видеть ваши фотографии и видеозаписи, кроме тех, которые находятся в Архиве и Корзине",
   "manage_the_app_settings": "Управление настройками приложения",
   "manage_your_account": "Управление учётной записью",
   "manage_your_api_keys": "Управление API-ключами",
@@ -927,13 +901,14 @@
   "no_results_description": "Попробуйте использовать синоним или более общее ключевое слово",
   "no_shared_albums_message": "Создайте альбом для обмена фотографиями и видеозаписями с людьми в вашей сети",
   "not_in_any_album": "Ни в одном альбоме",
-  "note_apply_storage_label_to_previously_uploaded assets": "Примечание: Чтобы применить тег хранилища к ранее загруженным ресурсам запустите",
+  "note_apply_storage_label_to_previously_uploaded assets": "Примечание: Чтобы применить тег хранилища к ранее загруженным ресурсам, запустите",
   "note_unlimited_quota": "Примечание: Введите 0 для неограниченной квоты",
   "notes": "Примечание",
   "notification_toggle_setting_description": "Включить уведомления по электронной почте",
   "notifications": "Уведомления",
   "notifications_setting_description": "Управление уведомлениями",
   "oauth": "OAuth",
+  "official_immich_resources": "Официальные ресурсы Immich",
   "offline": "Недоступен",
   "offline_paths": "Недоступные пути",
   "offline_paths_description": "Эти результаты могут быть вызваны ручным удалением файлов, которые не являются частью внешней библиотеки.",
@@ -946,7 +921,6 @@
   "onboarding_welcome_user": "Добро пожаловать, {user}",
   "online": "Доступен",
   "only_favorites": "Только избранное",
-  "only_refreshes_modified_files": "Обновляет только измененные файлы",
   "open_in_map_view": "Открыть в режиме просмотра карты",
   "open_in_openstreetmap": "Открыть в OpenStreetMap",
   "open_the_search_filters": "Открыть фильтры поиска",
@@ -961,7 +935,7 @@
   "owner": "Владелец",
   "partner": "Партнер",
   "partner_can_access": "{partner} имеет доступ",
-  "partner_can_access_assets": "Все ваши фотографии и видеозаписи, кроме тех, которые находятся в Архиве и Удалены",
+  "partner_can_access_assets": "Все ваши фотографии и видеозаписи, кроме тех, которые находятся в Архиве и Корзине",
   "partner_can_access_location": "Местоположение, где были сделаны ваши фотографии",
   "partner_sharing": "Совместное использование",
   "partners": "Партнёры",
@@ -984,21 +958,19 @@
   "people_edits_count": "Изменено {count, plural, one {# человек} few {# человека} many {# людей} other {# человек}}",
   "people_feature_description": "Просмотр фотографий и видео, сгруппированных по людям",
   "people_sidebar_description": "Отображать пункт меню \"Люди\" в боковой панели",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "Предупреждение об удалении",
-  "permanent_deletion_warning_setting_description": "Отображать предупреждение при безвозвратном удалении ресурсов",
+  "permanent_deletion_warning_setting_description": "Предупреждать перед безвозвратным удалением ресурсов",
   "permanently_delete": "Удалить навсегда",
-  "permanently_delete_assets_count": "Полностью удалить {count, plural, one {ресурс} few {ресурса} many {ресурсов} other {ресурсов}}",
+  "permanently_delete_assets_count": "Безвозвратно удалить {count, plural, one {ресурс} few {ресурса} many {ресурсов} other {ресурсов}}",
   "permanently_delete_assets_prompt": "Вы действительно хотите навсегда удалить {count, plural, one {этот объект?} other {эти <b>#</b> объектов?}} Это так же удалит {count, plural, one {его} other {их}} из альбома(ов).",
-  "permanently_deleted_asset": "Удалить объект навсегда",
-  "permanently_deleted_assets": "Безвозвратно удалено {count, plural, one {# ресурс} few {# ресурса} many {# ресурсов} other {# ресурса}}",
-  "permanently_deleted_assets_count": "Полностью удалено {count, plural, one {# ресурс} few {# ресурса} many {# ресурсов} other {# ресурса}}",
+  "permanently_deleted_asset": "Удалить навсегда",
+  "permanently_deleted_assets_count": "Безвозвратно удалено {count, plural, one {# файл} few {# файла} many {# файлов} other {# файлов}}",
   "person": "Человек",
   "person_hidden": "{name}{hidden, select, true { (скрыт)} other {}}",
   "photo_shared_all_users": "Похоже, что вы поделились своими фотографиями со всеми пользователями или у вас нет пользователей, с которыми можно поделиться.",
   "photos": "Фото",
   "photos_and_videos": "Фото и Видео",
-  "photos_count": "{count, plural, one {Фотография ({count, number})} few {Фотографии ({count, number})} many {Фотографий ({count, number})} other {Фотографий ({count, number})}}",
+  "photos_count": "{count, plural, one {{count, number} Фотография} few {{count, number} Фотографии} many {{count, number} Фотографий} other {{count, number} Фотографий}}",
   "photos_from_previous_years": "Фотографии прошлых лет в этот день",
   "pick_a_location": "Выбрать местоположение",
   "place": "Места",
@@ -1007,7 +979,6 @@
   "play_memories": "Воспроизвести воспоминания",
   "play_motion_photo": "Воспроизводить движущиеся фото",
   "play_or_pause_video": "Воспроизведение или приостановка видео",
-  "point": "",
   "port": "Порт",
   "preset": "Предустановка",
   "preview": "Предварительный просмотр",
@@ -1052,38 +1023,40 @@
   "purchase_server_description_2": "Состояние поддержки",
   "purchase_server_title": "Сервер",
   "purchase_settings_server_activated": "Ключ продукта сервера управляется администратором",
-  "range": "",
   "rating": "Рейтинг звёзд",
   "rating_clear": "Очистить рейтинг",
   "rating_count": "{count, plural, one {# звезда} other {# звезд}}",
   "rating_description": "Показывать рейтинг в панели информации",
-  "raw": "",
   "reaction_options": "Опции реакций",
   "read_changelog": "Прочитать список изменений",
   "reassign": "Переназначить",
-  "reassigned_assets_to_existing_person": "Переназначено {count, plural, one {# ресурс} few {# ресурса} many {# ресурсов} other {# ресурса}} на {name, select, null {существующего человека} other {{name}}}",
-  "reassigned_assets_to_new_person": "Переназначено {count, plural, one {# ресурс} few {# ресурса} many {# ресурсов} other {# ресурса}} новому человеку",
-  "reassing_hint": "Назначить выбранные ресурсы существующему человеку",
+  "reassigned_assets_to_existing_person": "Переназначен{count, plural, one { # ресурс} few {о # ресурса} many {о # ресурсов} other {о # ресурсов}} на {name, select, null {существующего пользователя} other {{name}}}",
+  "reassigned_assets_to_new_person": "Переназначен{count, plural, one { # ресурс} few {о # ресурса} many {о # ресурсов} other {о # ресурсов}} новому человеку",
+  "reassing_hint": "Назначить выбранные ресурсы существующему пользователю",
   "recent": "Недавние",
+  "recent-albums": "Недавние альбомы",
   "recent_searches": "Недавние поисковые запросы",
   "refresh": "Обновить",
   "refresh_encoded_videos": "Обновить закодированные видео",
+  "refresh_faces": "Обновить лица",
   "refresh_metadata": "Обновить метаданные",
   "refresh_thumbnails": "Обновить миниатюры",
   "refreshed": "Обновлено",
-  "refreshes_every_file": "Обновляет каждый файл",
+  "refreshes_every_file": "Обновляет все существующие и новые файлы",
   "refreshing_encoded_video": "Обновление закодированного видео",
+  "refreshing_faces": "Обновление лиц",
   "refreshing_metadata": "Обновление метаданных",
   "regenerating_thumbnails": "Восстановление миниатюр",
   "remove": "Удалить",
-  "remove_assets_album_confirmation": "Вы уверены, что хотите удалить {count, plural, one {# ресурс} few {# ресурса} many {# ресурсов} other {# ресурса}} из альбома?",
-  "remove_assets_shared_link_confirmation": "Вы уверены, что хотите удалить {count, plural, one {# ресурс} few {# ресурса} many {# ресурсов} other {# ресурса}} из этой общей ссылки?",
+  "remove_assets_album_confirmation": "Вы уверены, что хотите удалить {count, plural, one {# объект} few {# объекта} many {# объектов} other {# объектов}} из альбома?",
+  "remove_assets_shared_link_confirmation": "Вы уверены, что хотите удалить {count, plural, one {# объект} few {# объекта} many {# объектов} other {# объектов}} из этой публичной ссылки?",
   "remove_assets_title": "Удалить объекты?",
   "remove_custom_date_range": "Удалить пользовательский диапазон дат",
+  "remove_deleted_assets": "Удаление автономных файлов",
   "remove_from_album": "Удалить из альбома",
   "remove_from_favorites": "Удалить из избранного",
-  "remove_from_shared_link": "Удалить из общей ссылки",
-  "remove_offline_files": "Удаление автономных файлов",
+  "remove_from_shared_link": "Удалить из публичной ссылки",
+  "remove_url": "Удалить URL",
   "remove_user": "Удалить пользователя",
   "removed_api_key": "Удален ключ API: {name}",
   "removed_from_archive": "Удален из архива",
@@ -1100,7 +1073,6 @@
   "reset": "Сброс",
   "reset_password": "Сброс пароля",
   "reset_people_visibility": "Восстановить видимость людей",
-  "reset_settings_to_default": "",
   "reset_to_default": "Восстановление значений по умолчанию",
   "resolve_duplicates": "Устранить дубликаты",
   "resolved_all_duplicates": "Все дубликаты устранены",
@@ -1120,8 +1092,7 @@
   "saved_settings": "Настройки сохранены",
   "say_something": "Скажите что-нибудь",
   "scan_all_libraries": "Сканировать все библиотеки",
-  "scan_all_library_files": "Повторное сканирование всех файлов библиотеки",
-  "scan_new_library_files": "Сканировать новые файлы в библиотеке",
+  "scan_library": "Сканировать",
   "scan_settings": "Настройки сканирования",
   "scanning_for_album": "Сканирование альбома...",
   "search": "Поиск",
@@ -1139,6 +1110,7 @@
   "search_options": "Параметры поиска",
   "search_people": "Поиск людей",
   "search_places": "Поиск мест",
+  "search_settings": "Настройки поиска",
   "search_state": "Поиск региона...",
   "search_tags": "Поиск по тегам...",
   "search_timezone": "Поиск часового пояса...",
@@ -1163,7 +1135,6 @@
   "selected_count": "{count, plural, one {# выбран} other {# выбрано}}",
   "send_message": "Отправить сообщение",
   "send_welcome_email": "Отправить приветственное письмо",
-  "server": "Сервер",
   "server_offline": "Сервер не в сети",
   "server_online": "Сервер в сети",
   "server_stats": "Статистика сервера",
@@ -1182,9 +1153,9 @@
   "shared_by_user": "Владелец: {user}",
   "shared_by_you": "Вы поделились",
   "shared_from_partner": "Фото от {partner}",
-  "shared_link_options": "Параметры общих ссылок",
-  "shared_links": "Общие ссылки",
-  "shared_photos_and_videos_count": "{assetCount, plural, other {# поделился фото и видео.}}",
+  "shared_link_options": "Параметры публичных ссылок",
+  "shared_links": "Публичные ссылки",
+  "shared_photos_and_videos_count": "{assetCount, plural, other {# фото и видео.}}",
   "shared_with_partner": "Совместно с {partner}",
   "sharing": "Общие",
   "sharing_enter_password": "Пожалуйста, введите пароль для просмотра этой страницы.",
@@ -1206,6 +1177,7 @@
   "show_person_options": "Показать опции персоны",
   "show_progress_bar": "Показать Индикатор Выполнения",
   "show_search_options": "Показать параметры поиска",
+  "show_slideshow_transition": "Показать слайд-шоу переход",
   "show_supporter_badge": "Значок поддержки",
   "show_supporter_badge_description": "Показать значок поддержки",
   "shuffle": "Перемешать",
@@ -1226,12 +1198,12 @@
   "sort_oldest": "Старые фото",
   "sort_recent": "Недавние фото",
   "sort_title": "Заголовок",
-  "source": "Источник",
-  "stack": "В стопку",
-  "stack_duplicates": "Стек дубликатов",
-  "stack_select_one_photo": "Выберите одну главную фотографию для стека",
-  "stack_selected_photos": "Сложить выбранные фотографии в стопку",
-  "stacked_assets_count": "{count, plural, one {# объект добавлен} few {# объекта добавлено} other {# объектов добавлено}} в стек",
+  "source": "Исходный код",
+  "stack": "Превратить в серию",
+  "stack_duplicates": "Превратить дубликаты в серию",
+  "stack_select_one_photo": "Выберите главную фотографию для серии",
+  "stack_selected_photos": "Объединить выбранные объекты в серию",
+  "stacked_assets_count": "{count, plural, one {# объект добавлен} few {# объекта добавлено} other {# объектов добавлено}} в серию",
   "stacktrace": "Трассировка стека",
   "start": "Старт",
   "start_date": "Дата начала",
@@ -1247,13 +1219,16 @@
   "submit": "Подтвердить",
   "suggestions": "Предложения",
   "sunrise_on_the_beach": "Восход солнца на пляже",
+  "support": "Поддержка",
+  "support_and_feedback": "Поддержка и обратная связь",
+  "support_third_party_description": "Ваша установка immich была упакована сторонним разработчиком. Проблемы, с которыми вы столкнулись, могут быть вызваны этим пакетом, поэтому, пожалуйста, в первую очередь обращайтесь к ним, используя ссылки ниже.",
   "swap_merge_direction": "Изменить направление слияния",
   "sync": "Синхр.",
   "tag": "Тег",
   "tag_assets": "Добавить теги",
   "tag_created": "Тег {tag} создан",
   "tag_feature_description": "Просмотр фотографий и видео, сгруппированных по тегам",
-  "tag_not_found_question": "Не удается найти тег? Создайте его <link>здесь</link>",
+  "tag_not_found_question": "Не удается найти тег? <link>Создайте новый тег.</link>",
   "tag_updated": "Тег {tag} изменен",
   "tagged_assets": "Помечено {count, plural, one {# объект} other {# объектов}}",
   "tags": "Теги",
@@ -1262,35 +1237,35 @@
   "theme_selection": "Выбор темы",
   "theme_selection_description": "Автоматически устанавливать тему в зависимости от системных настроек вашего браузера",
   "they_will_be_merged_together": "Они будут объединены вместе",
+  "third_party_resources": "Сторонние ресурсы",
   "time_based_memories": "Воспоминания, основанные на времени",
+  "timeline": "Временная шкала",
   "timezone": "Часовой пояс",
   "to_archive": "В архив",
   "to_change_password": "Изменить пароль",
   "to_favorite": "Добавить в избранное",
   "to_login": "Вход",
   "to_parent": "Вернуться назад",
-  "to_root": "В начало",
   "to_trash": "Корзина",
   "toggle_settings": "Переключение настроек",
   "toggle_theme": "Переключение темы",
-  "toggle_visibility": "Переключение видимости",
+  "total": "Всего",
   "total_usage": "Общее использование",
   "trash": "Корзина",
   "trash_all": "Удалить всё",
   "trash_count": "Удалить {count, number}",
-  "trash_delete_asset": "Удалить ресурс",
+  "trash_delete_asset": "Переместить в корзину",
   "trash_no_results_message": "Здесь будут отображаться удалённые фотографии и видео.",
   "trashed_items_will_be_permanently_deleted_after": "Элементы в корзине будут автоматически удалены через {days, plural, one {# день} other {# дней}}.",
   "type": "Тип",
   "unarchive": "Восстановить",
-  "unarchived": "Разархивирован",
   "unarchived_count": "{count, plural, other {Возвращено из архива #}}",
   "unfavorite": "Удалить из избранного",
   "unhide_person": "Показать персону",
   "unknown": "Неизвестно",
-  "unknown_album": "Неизвестный альбом",
   "unknown_year": "Неизвестный Год",
   "unlimited": "Не ограничено",
+  "unlink_motion_video": "Отсоединить движущееся видео",
   "unlink_oauth": "Отключить OAuth",
   "unlinked_oauth_account": "Отключить аккаунт OAuth",
   "unnamed_album": "Альбом без названия",
@@ -1299,17 +1274,17 @@
   "unsaved_change": "Не сохраненное изменение",
   "unselect_all": "Снять всё",
   "unselect_all_duplicates": "Отменить выбор всех дубликатов",
-  "unstack": "Разобрать стек",
-  "unstacked_assets_count": "{count, plural, one {# объект} few {# объекта} other {# объектов}} разобрано из стека",
+  "unstack": "Разгруппировать серию",
+  "unstacked_assets_count": "{count, plural, one {# объект извлечен} few {# объекта извлечено} other {# объектов извлечено}} из серии",
   "untracked_files": "НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ",
   "untracked_files_decription": "Приложение не отслеживает эти файлы. Они могут быть результатом неудачных перемещений, прерванных загрузок или пропущены из-за ошибки",
   "up_next": "Следующее",
   "updated_password": "Пароль обновлён",
   "upload": "Загрузить",
   "upload_concurrency": "Параллельность загрузки",
-  "upload_errors": "Загрузка завершена с {count, plural, one {# ошибкой} few {# ошибками} many {# ошибками} other {# ошибками}}, обновите страницу, чтобы увидеть новые загруженные ресурсы.",
+  "upload_errors": "Загрузка завершена с {count, plural, one {# ошибкой} other {# ошибками}}, обновите страницу, чтобы увидеть новые загруженные ресурсы.",
   "upload_progress": "Осталось {remaining, number} - Обработано {processed, number}/{total, number}",
-  "upload_skipped_duplicates": "Пропущено {count, plural, one {# дублирующийся ресурс} few {# дублирующихся ресурса} many {# дублирующихся ресурсов} other {# дублирующихся ресурса}}",
+  "upload_skipped_duplicates": "Пропущен{count, plural, one { # дублирующийся ресурс} few {о # дублирующихся ресурса} many {о # дублирующихся ресурсов} other {о # дублирующихся ресурса}}",
   "upload_status_duplicates": "Дубликаты",
   "upload_status_errors": "Ошибки",
   "upload_status_uploaded": "Загружено",
@@ -1319,13 +1294,13 @@
   "use_custom_date_range": "Использовать пользовательский диапазон дат",
   "user": "Пользователь",
   "user_id": "ID пользователя",
-  "user_license_settings": "Лицензия",
-  "user_license_settings_description": "Управление лицензией",
   "user_liked": "{user} отметил(а) {type, select, photo {это фото} video {это видео} asset {этот ресурс} other {этот альбом}}",
   "user_purchase_settings": "Покупка",
   "user_purchase_settings_description": "Управление покупкой",
   "user_role_set": "Установить {user} в качестве {role}",
   "user_usage_detail": "Подробная информация об использовании пользователем",
+  "user_usage_stats": "Статистика использования аккаунта",
+  "user_usage_stats_description": "Посмотреть статистику использования аккаунта",
   "username": "Имя пользователя",
   "users": "Пользователи",
   "utilities": "Утилиты",
@@ -1333,31 +1308,33 @@
   "variables": "Переменные",
   "version": "Версия",
   "version_announcement_closing": "Твой друг Алекс",
-  "version_announcement_message": "Привет, друг! В приложении доступна новая версия. Пожалуйста, посетите <link>заметки к выпуску</link> и убедитесь, что ваша настройка <code>docker-compose.yml</code> и <code>.env</code> актуальна, чтобы избежать ошибок конфигурации, особенно если вы используете WatchTower или другой механизм автоматического обновления вашего приложения.",
+  "version_announcement_message": "Здравствуйте! Доступна новая версия приложения. Пожалуйста, прочтите <link>заметки к выпуску</link> и убедитесь, что ваши параметры <code>docker-compose.yml</code> и <code>.env</code> актуальны, чтобы избежать ошибок в конфигурации, особенно если вы используете WatchTower или другой механизм автоматического обновления приложения.",
+  "version_history": "История версий",
+  "version_history_item": "Версия {version} установлена {date}",
   "video": "Видео",
   "video_hover_setting": "Воспроизведение миниатюры видео при наведении курсора мыши",
   "video_hover_setting_description": "Воспроизводить миниатюры видео при наведении курсора мыши на объект. Даже если этот параметр отключен, воспроизведение можно запустить, наведя курсор на значок воспроизведения.",
   "videos": "Видео",
-  "videos_count": "{count, plural, one {Видео (#)} few {Видео (#)} many {Видео (#)} other {Видео (#)}}",
+  "videos_count": "{count, plural, one {# Видео} few {# Видео} many {# Видео} other {# Видео}}",
   "view": "Просмотр",
   "view_album": "Просмотреть альбом",
   "view_all": "Посмотреть всё",
   "view_all_users": "Показать всех пользователей",
   "view_in_timeline": "Показать на временной шкале",
   "view_links": "Показать ссылки",
+  "view_name": "Посмотреть",
   "view_next_asset": "Показать следующий объект",
   "view_previous_asset": "Показать предыдущий объект",
   "view_stack": "Показать стек",
-  "viewer": "Наблюдатель",
   "visibility_changed": "Видимость изменена для {count, plural, one {# человека} other {# людей}}",
   "waiting": "В очереди",
   "warning": "Предупреждение",
   "week": "Неделя",
   "welcome": "Добро пожаловать",
-  "welcome_to_immich": "Добро пожаловать в immich",
+  "welcome_to_immich": "Добро пожаловать в Immich",
   "year": "Год",
   "years_ago": "{years, plural, one {# год} few {# года} many {# лет} other {# года}} назад",
   "yes": "Да",
-  "you_dont_have_any_shared_links": "У вас нет общих ссылок",
-  "zoom_image": "Увеличить"
+  "you_dont_have_any_shared_links": "У вас нет публичных ссылок",
+  "zoom_image": "Приблизить"
 }
diff --git a/i18n/sk.json b/i18n/sk.json
new file mode 100644
index 0000000000..bf67b8ab12
--- /dev/null
+++ b/i18n/sk.json
@@ -0,0 +1,1025 @@
+{
+  "about": "Obnoviť",
+  "account": "Účet",
+  "account_settings": "Nastavenia účtu",
+  "acknowledge": "Rozumiem",
+  "action": "Akcia",
+  "actions": "Akcie",
+  "active": "Aktívny",
+  "activity": "Aktivita",
+  "activity_changed": "Aktivita je {enabled, select, true{povolená} other {vypnutá}}",
+  "add": "Pridať",
+  "add_a_description": "Pridať popis",
+  "add_a_location": "Pridať polohu",
+  "add_a_name": "Pridať meno",
+  "add_a_title": "Pridať názov",
+  "add_exclusion_pattern": "Pridať vzor vylúčenia",
+  "add_import_path": "Pridať cestu pre import",
+  "add_location": "Pridať lokáciu",
+  "add_more_users": "Pridať viac používateľov",
+  "add_partner": "Pridať partnera",
+  "add_path": "Pridať cestu",
+  "add_photos": "Pridať fotografie",
+  "add_to": "Pridať do...",
+  "add_to_album": "Pridať do albumu",
+  "add_to_shared_album": "Pridať do zdieľaného albumu",
+  "add_url": "Pridaj URL",
+  "added_to_archive": "Pridané do archívu",
+  "added_to_favorites": "Pridané do obľúbených",
+  "added_to_favorites_count": "Pridané {count, number} do obľúbených",
+  "admin": {
+    "add_exclusion_pattern_description": "Pridávanie vzorov na vylúčenie. Globovanie pomocou *, ** a ? je podporované. Ak chcete ignorovať všetky súbory v akomkoľvek adresári s názvom \"Raw\", použite \"**/Raw/**\". Ak chcete ignorovať všetky súbory končiace na \".tif\", použite \"**/*.tif\". Ak chcete ignorovať absolútnu cestu, použite príkaz \"/cesta/k/ignorovanym/**\".",
+    "asset_offline_description": "Táto položka externej knižnice sa už na disku nenachádza a bola presunutá do koša. Pokiaľ bol súbor presunutý v rámci knižnice, skontrolujte časovú os a vyhľadajte nové odpovedajúce položky. Ak chcete túto položku obnoviť, uistite sa, že je cesta k nižšie uvedenému súboru prístupná pre aplikáciu Immich a prehľadajte knižnicu.",
+    "authentication_settings": "Nastavenia overovania",
+    "authentication_settings_description": "Spravovať heslo, protokol OAuth a ďalšie nastavenia overenia",
+    "authentication_settings_disable_all": "Naozaj chcete zakázať všetky spôsoby prihlásenia? Prihlásenie bude úplne zakázané.",
+    "authentication_settings_reenable": "Pre opätovné povolenie použite <link>Serverový príkaz</link>.",
+    "background_task_job": "Úlohy na pozadí",
+    "backup_database": "Zálohovať databázu",
+    "backup_database_enable_description": "Povoliť zálohovanie databázy",
+    "backup_keep_last_amount": "Množtvo predošlých záloh, ktoré sa majú zachovať",
+    "backup_settings": "Nastavenia zálohovania",
+    "backup_settings_description": "Spravovať nastavenia záloh",
+    "check_all": "Skontrolovať všetko",
+    "cleared_jobs": "Hotové úlohy pre: {job}",
+    "config_set_by_file": "Konfigurácia je v súčasnosti nastavená konfiguračným súborom",
+    "confirm_delete_library": "Naozaj chcete vymazať knižnicu {library}?",
+    "confirm_delete_library_assets": "Ste si istí, že chcete vymazať túto knižnicu? Tato operácia nenávratne odstráni {count, plural, one {# contained asset} other {all # contained assets}} súborov z Immich. Súbory budú ponechané na disku.",
+    "confirm_email_below": "Pre potvrdenie zadajte \"{email}\" nižšie",
+    "confirm_reprocess_all_faces": "Naozaj chcete spracovať všetky tváre znova? Tento proces vymaže pomenovaných ľudí.",
+    "confirm_user_password_reset": "Naozaj chcete resetovať heslo pre {user}?",
+    "create_job": "Vytvoriť úlohu",
+    "cron_expression": "Výraz cron",
+    "cron_expression_description": "Nastavte interval skenovania pomocou formátu cron. Pre viac informácií navštívte <link>Crontab Guru</link>",
+    "cron_expression_presets": "Presety cron výrazov",
+    "disable_login": "Zakázať prihlásenie",
+    "duplicate_detection_job_description": "Spustiť strojové učenie na položkách pre detekciu podobných obrázkov. Spolieha sa na inteligentné vyhľadávanie",
+    "exclusion_pattern_description": "Vylučovacie vzory Vám umožňujú ignorovať súbory a priečinky pri skenovaní Vašej knižnice. Toto je užitočné, ak máte priečinky obsahujúce súbory, ktoré nechcete importovať, napríklad RAW súbory.",
+    "external_library_created_at": "Externá knižnica (vytvorená {date})",
+    "external_library_management": "Správa Externej Knižnice",
+    "face_detection": "Detekcia tvárí",
+    "face_detection_description": "Detekujte tváre v položkách pomocou strojového učenia. Pri videách sa berie do úvahy iba miniatúra. „Obnoviť“ znovu spracuje všetky položky. „Resetovať“ navyše vymaže všetky aktuálne údaje o tvárach. „Chýbajúce“ zaradí položky, ktoré ešte neboli spracované. Detekované tváre budú zaradené na rozpoznávanie tvárí po dokončení detekcie tvárí, pričom sa zoskupia do existujúcich alebo nových osôb.",
+    "facial_recognition_job_description": "Zoskupovať detekované tváre do osôb. Tento krok sa vykoná po dokončení detekcie tvárí. „Resetovať“ (znovu) zoskupí všetky tváre. „Chýbajúce“ zaradí tváre, ktoré nemajú pridelenú osobu.",
+    "failed_job_command": "Príkaz {command} zlyhal pre úlohu: {job}",
+    "force_delete_user_warning": "VAROVANIE: Toto okamžite odstráni používateľa a všetky položky. Tento krok nie je možné vrátiť späť a súbory nebude možné obnoviť.",
+    "forcing_refresh_library_files": "Vynútenie obnovy všetkých súborov knižnice",
+    "image_format": "Formát",
+    "image_format_description": "WebP vytvára menšie súbory ako JPEG, ale kódovanie je pomalšie.",
+    "image_prefer_embedded_preview": "Uprednostňovať vstavaný náhľad",
+    "image_prefer_embedded_preview_setting_description": "Použiť vložené náhľady vo fotografiách RAW ako vstup pre spracovanie obrazu, ak sú k dispozícii. To môže vytvoriť presnejšie farby pre niektoré obrázky, ale kvalita náhľadu závisí od fotoaparátu a obrázok môže mať viac kompresných artefaktov.",
+    "image_prefer_wide_gamut": "Uprednostňovať široký farebný rozsah",
+    "image_prefer_wide_gamut_setting_description": "Použiť Display P3 pre miniatúry. Toto lepšie zachováva živosť obrázkov so širokým farebným rozsahom. Obrázky sa môžu zobraziť odlišne na starších zariadeniach so starou verziou prehliadača. sRGB obrázky zostávajú sRGB, aby sa zabránilo farebným posunom.",
+    "image_preview_description": "Stredne veľký obrázok s odstránenými metadátami, používaný pri prezeraní jednej položky a na strojové učenie",
+    "image_preview_quality_description": "Kvalita náhľadu v stupnici od 1 do 100. Vyššia hodnota znamená lepšiu kvalitu, ale produkuje väčšie súbory a môže znížiť odozvu aplikácie. Nastavenie nižšej hodnoty môže ovplyvniť kvalitu strojového učenia.",
+    "image_preview_title": "Nastavenia Náhľadov",
+    "image_quality": "Kvalita",
+    "image_resolution": "Rozlíšenie",
+    "image_resolution_description": "Vyššie rozlíšenie môže zachovať viac detailov, ale kódovanie trvá dlhšie, súbory sú väčšie a môže to znížiť rýchlosť odozvy aplikácie.",
+    "image_settings": "Nastavenia Obrázkov",
+    "image_settings_description": "Spravovať kvalitu a rozlíšenie generovaných obrázkov",
+    "image_thumbnail_description": "Malá miniatúra s odstránenými metadátami, používané pri zobrazovaní skupín fotiek ako na hlavnej časovej osi",
+    "image_thumbnail_quality_description": "Kvalita miniatúry v stupnici od 1 do 100. Vyššia hodnota znamená lepšiu kvalitu, ale produkuje väčšie súbory a môže znížiť odozvu aplikácie.",
+    "image_thumbnail_title": "Nastavenia miniatúr",
+    "job_concurrency": "Súbežnosť úlohy - {job}",
+    "job_created": "Úloha bola vytvorená",
+    "job_not_concurrency_safe": "Táto úloha nie je bezpečná pre súbežné spracovanie.",
+    "job_settings": "Nastavenia Úloh",
+    "job_settings_description": "Spravovať súbežnosť úloh",
+    "job_status": "Stav Úloh",
+    "jobs_delayed": "{jobCount, plural, one {# oneskorený} few {# oneskorené} other {# oneskorených}}",
+    "jobs_failed": "{jobCount, plural, one {# neúspešný} few {# neúspešné} other {# neúspešných}}",
+    "library_created": "Vytvorená knižnica: {library}",
+    "library_deleted": "Knižnica bola vymazaná",
+    "library_import_path_description": "Zvoľte priečinok na importovanie. Tento priečinok vrátane podpriečinkov bude skenovaný pre obrázky a videá.",
+    "library_scanning": "Pravidelné skenovanie",
+    "library_scanning_description": "Nastaviť pravidelné skenovanie knižnice",
+    "library_scanning_enable_description": "Zapnúť pravidelné skenovanie knižnice",
+    "library_settings": "Externá knižnica",
+    "library_settings_description": "Spravovať nastavenia externej knižnice",
+    "library_tasks_description": "Vykonať úlohy knižnice",
+    "library_watching_enable_description": "Sledovať externé knižnice pre zmeny v súboroch",
+    "library_watching_settings": "Sledovanie knižnice (EXPERIMENTÁLNE)",
+    "library_watching_settings_description": "Automaticky sledovať zmenené súbory",
+    "logging_enable_description": "Povoliť zaznamenávanie",
+    "logging_level_description": "Ak je povolené, akú úroveň zaznamenávania použiť.",
+    "logging_settings": "Zaznamenávanie",
+    "machine_learning_clip_model": "Model CLIP",
+    "machine_learning_clip_model_description": "Názov modelu CLIP je uvedený <link>tu</link>. Pamätajte, že pri zmene modelu je nutné znovu spustiť úlohu 'Inteligentné vyhľadávanie' pre všetky obrázky.",
+    "machine_learning_duplicate_detection": "Detekcia duplikátov",
+    "machine_learning_duplicate_detection_enabled": "Povoliť detekciu duplikátov",
+    "machine_learning_duplicate_detection_enabled_description": "Ak je vypnuté, presne identické položky budú stále deduplikované.",
+    "machine_learning_duplicate_detection_setting_description": "Použiť CLIP embeddings na identifikáciu pravdepodobných duplikátov",
+    "machine_learning_enabled": "Povoliť strojové učenie",
+    "machine_learning_enabled_description": "Ak je vypnuté, všetky funkcie strojového učenia (ML) budú vypnuté, bez ohľadu na nastavenia nižšie.",
+    "machine_learning_facial_recognition": "Rozpoznávanie tvárí",
+    "machine_learning_facial_recognition_description": "Detekovať, rozpoznať a zoskupiť tváre na obrázkoch",
+    "machine_learning_facial_recognition_model": "Model pre rozpoznávanie tvárí",
+    "machine_learning_facial_recognition_model_description": "Modely sú zoradené od najväčšieho po najmenší. Väčšie modely sú pomalšie a vyžadujú viac pamäte, ale poskytujú lepšie výsledky. Pamätajte, že po zmene modelu je potrebné znovu spustiť úlohu detekcie tvárí pre všetky obrázky.",
+    "machine_learning_facial_recognition_setting": "Povoliť rozpoznávanie tvárí",
+    "machine_learning_facial_recognition_setting_description": "Ak je vypnuté, obrázky nebudú spracované pre rozpoznávanie tvárí a nebudú sa zobrazovať v sekcii Ľudia na stránke Preskúmať.",
+    "machine_learning_max_detection_distance": "Maximálna detekčná odchylka",
+    "machine_learning_max_detection_distance_description": "Maximálna odchylka medzi dvoma obrázkami, aby boli považované za duplikáty, v rozsahu od 0.001 do 0.1. Vyššie hodnoty odhalia viac duplikátov, ale môžu viesť k falošným pozitívam.",
+    "machine_learning_max_recognition_distance": "Maximálna rozpoznávacia odchylka",
+    "machine_learning_max_recognition_distance_description": "Maximálna odchylka medzi dvoma tvárami, aby boli považované za rovnakú osobu, v rozsahu od 0 do 2. Zníženie tejto hodnoty môže zabrániť označeniu dvoch ľudí za tú istú osobu, zatiaľ čo zvýšenie môže zabrániť označeniu jednej osoby za dve rôzne osoby. Pamätajte, že je jednoduchšie spojiť dvoch ľudí ako rozdeliť jednu osobu na dve, takže je lepšie voliť nižší prah, ak je to možné.",
+    "machine_learning_min_detection_score": "Minimálne detekčné skóre",
+    "machine_learning_min_detection_score_description": "Minimálne skóre dôveryhodnosti pre detekciu tváre v rozsahu od 0 do 1. Nižšie hodnoty odhalia viac tvárí, ale môžu viesť k falošným pozitivním výsledkom.",
+    "machine_learning_min_recognized_faces": "Minimum rozpoznaných tvárí",
+    "machine_learning_min_recognized_faces_description": "Minimálny počet rozpoznaných tvárí potrebných na vytvorenie osoby. Zvýšením tejto hodnoty sa zvyšuje presnosť rozpoznávania tvárí, ale tiež sa zvyšuje pravdepodobnosť, že tvár nebude priradená osobe.",
+    "machine_learning_settings": "Nastavenia strojového učenia",
+    "machine_learning_settings_description": "Spravovať funkcie a nastavenia strojového učenia",
+    "machine_learning_smart_search": "Inteligentné vyhľadávanie",
+    "machine_learning_smart_search_description": "Významové vyhľadávanie v obrázkoch pomocou CLIP vzorov",
+    "machine_learning_smart_search_enabled": "Povoliť inteligentné vyhľadávanie",
+    "machine_learning_smart_search_enabled_description": "Ak je vypnuté, obrázky nebudú spracované pre inteligentné vyhľadávanie.",
+    "machine_learning_url_description": "URL adresa machine-learning servera. Ak je poskytnutých viacero URL adries, budú servery postupne testované od prvého po posledný, až kým jeden z nich úspešne odpovie.",
+    "manage_concurrency": "Správa súbežnosti",
+    "manage_log_settings": "Spravovať nastavenia logovania",
+    "map_dark_style": "Tmavý štýl",
+    "map_enable_description": "Povoliť funkcie mapy",
+    "map_gps_settings": "Nastavenia Mapy & GPS",
+    "map_gps_settings_description": "Správa nastavení máp a GPS reverzného geokódovania",
+    "map_implications": "Táto funkčnosť sa spolieha na externý servis spracovania mapových dlaždíc (tiles.immich.cloud)",
+    "map_light_style": "Svetlý štýl",
+    "map_manage_reverse_geocoding_settings": "Správa nastavení <link>Reverzného geokódovania</link>",
+    "map_reverse_geocoding": "Reverzné Geokódovanie",
+    "map_reverse_geocoding_enable_description": "Povoliť reverzné geokódovanie",
+    "map_reverse_geocoding_settings": "Nastavenia reverzného geokódovania",
+    "map_settings": "Mapa",
+    "map_settings_description": "Spravovať nastavenia mapy",
+    "map_style_description": "URL na motív style.json",
+    "metadata_extraction_job": "Extrahovať metadáta",
+    "metadata_extraction_job_description": "Získaj informácie metadátach z každej položky, ako napríklad GPS, tváre a rozlíšenie",
+    "metadata_faces_import_setting": "Povoliť import tváre",
+    "metadata_faces_import_setting_description": "Importuj tváre z EXIF dát obrázkov a sidecar súborov",
+    "metadata_settings": "Nastavenia metadát",
+    "metadata_settings_description": "Spravovať nastavenia metadát",
+    "migration_job": "Migrácia",
+    "migration_job_description": "Migrácia miniatúr položiek a tvárí na najnovšiu štruktúru priečinkov",
+    "no_paths_added": "Neboli pridané žiadne cesty",
+    "no_pattern_added": "Nebol pridaný žiadny vzor",
+    "note_apply_storage_label_previous_assets": "Poznámka: Ak chcete použiť Štítkovanie úložiska na predtým nahrané aktíva, spustite príkaz",
+    "note_cannot_be_changed_later": "POZNÁMKA: Toto nie je možné neskôr zmeniť!",
+    "note_unlimited_quota": "Poznámka: Použite 0 pre neobmedzený limit",
+    "notification_email_from_address": "Z adresy",
+    "notification_email_from_address_description": "E-mailová adresa odosielateľa, príklad: \"Immich Photo Server <noreply@example.com>\"",
+    "notification_email_host_description": "Adresa emailového serveru (príklad: smtp.immich.app)",
+    "notification_email_ignore_certificate_errors": "Ignorovať chyby certifikátu",
+    "notification_email_ignore_certificate_errors_description": "Ignorovať chyby pri overení TLS certifikátu (neodporúča sa)",
+    "notification_email_password_description": "Heslo pre autentifikáciu s emailovým serverom",
+    "notification_email_port_description": "Porty e-mailového servera (napr. 25, 465, alebo 587)",
+    "notification_email_sent_test_email_button": "Odoslať testovací e-mail a uložiť",
+    "notification_email_setting_description": "Nastavenie pre odosielanie e-mailových upozornení",
+    "notification_email_test_email": "Odoslať testovací email",
+    "notification_email_test_email_failed": "Odosielanie testovacieho e-mailu zlyhalo, skontrolujte hodnoty",
+    "notification_email_test_email_sent": "Testovací e-mail bol odoslaný na adresu {email}. Prosím skontrolujte si Doručenú poštu.",
+    "notification_email_username_description": "Používateľské meno, ktoré sa má použiť pri overovaní s e-mailovým serverom",
+    "notification_enable_email_notifications": "Povoliť e-mailové upozornenia",
+    "notification_settings": "Nastavenia upozornení",
+    "notification_settings_description": "Spravovať nastavenia upozornení, vrátane emailu",
+    "oauth_auto_launch": "Automatické spustenie",
+    "oauth_auto_launch_description": "Automatické spustenie OAuth prihlasovacieho toku pri otvorení prihlasovacej stránky",
+    "oauth_auto_register": "Automatická regristrácia",
+    "oauth_auto_register_description": "Automatické zaregistrovanie nového požívateľa pri prihlásení pomocou OAuth",
+    "oauth_button_text": "Text tlačítka",
+    "oauth_client_id": "Client ID",
+    "oauth_client_secret": "Client Secret",
+    "oauth_enable_description": "Prihlásiť sa pomocou OAuth",
+    "oauth_issuer_url": "Adresa URL vydavateľa",
+    "oauth_mobile_redirect_uri": "URI mobilného presmerovania",
+    "oauth_mobile_redirect_uri_override": "Prepísanie URI mobilného presmerovania",
+    "oauth_mobile_redirect_uri_override_description": "Povoľte, keď poskytovateľ protokolu OAuth nepovoľuje identifikátor URI pre mobilné zariadenia, napríklad '{callback}'",
+    "oauth_profile_signing_algorithm": "Algoritmus podpisovania profilu",
+    "oauth_profile_signing_algorithm_description": "Algoritmus používaný na prihlásenie užívateľského profilu.",
+    "oauth_scope": "Rozsah",
+    "oauth_settings": "OAuth",
+    "oauth_settings_description": "Spravovať nastavenia prihlásenia OAuth",
+    "oauth_settings_more_details": "Pre viac informácii o tejto funkcii, prejdite na <link>docs</link>.",
+    "oauth_signing_algorithm": "Algoritmus podpisovania",
+    "oauth_storage_label_claim": "Nárokovať Štítok úložiska",
+    "oauth_storage_label_claim_description": "Automaticky nastaviť Štítok úložiska používateľa na hodnotu tohto nároku.",
+    "oauth_storage_quota_claim": "Deklarácia kvóty úložiska",
+    "oauth_storage_quota_claim_description": "Automaticky nastaviť kvótu úložiska používateľa na hodnotu tejto deklarácie.",
+    "oauth_storage_quota_default": "Predvolený limit úložiska (GiB)",
+    "oauth_storage_quota_default_description": "Kvóta v GiB, ktorá sa má použiť, keď nie je poskytnutý žiadna deklarácia (zadajte 0 pre neobmedzenú kvótu).",
+    "offline_paths": "Offline cesty",
+    "offline_paths_description": "Tieto výsledky môžu byť spôsobené ručným odstránením súborov, ktoré nie sú súčasťou externej knižnice.",
+    "password_enable_description": "Prihlásiť sa pomocou emailu a hesla",
+    "password_settings": "Prihlásenie cez heslo",
+    "password_settings_description": "Spravovať nastavenia prihlásenia cez heslo",
+    "paths_validated_successfully": "Všetky cesty boli úspešne overené",
+    "person_cleanup_job": "Premazanie osôb",
+    "quota_size_gib": "Veľkosť kvóty (GiB)",
+    "refreshing_all_libraries": "Obnovujú sa všetky knižnice",
+    "registration": "Registrácia administrátora",
+    "registration_description": "Keďže ste prvým používateľom v systéme, budú vám pridelené správcovské práva na vykonávanie všetkých úloh a vrátane tvorby nových používateľov.",
+    "repair_all": "Opraviť Všetko",
+    "repair_matched_items": "Zhody {count, plural, one {# item} other {# items}}",
+    "repaired_items": "Opravených {count, plural, one {# item} other {# items}}",
+    "require_password_change_on_login": "Vyžadovať od používateľa zmenu hesla pri prvom prihlásení",
+    "reset_settings_to_default": "Obnoviť pôvodné nastavenia",
+    "reset_settings_to_recent_saved": "Obnoviť naposledy uložené nastavenia",
+    "scanning_library": "Knižnica sa skenuje",
+    "search_jobs": "Vyhľadať úlohy...",
+    "send_welcome_email": "Odoslať uvítací e-mail",
+    "server_external_domain_settings": "Externá doména",
+    "server_external_domain_settings_description": "Verejná doména pre zdieľané odkazy, vrátane http(s)://",
+    "server_public_users": "Verejní užívatelia",
+    "server_public_users_description": "Všetci užívatelia (meno a email) sú uvedení pri pridávaní užívateľa do zdieľaných albumov. Ak je táto funkcia vypnutá, zoznam užívateľov bude dostupný iba správcom.",
+    "server_settings": "Nastavenia servera",
+    "server_settings_description": "Spravovať nastavenia servera",
+    "server_welcome_message": "Uvítacia správa",
+    "server_welcome_message_description": "Správa, ktorá sa zobrazí na prihlasovacej stránke.",
+    "sidecar_job": "Sidecar metadáta",
+    "sidecar_job_description": "Objavte alebo synchronizujte metadáta Sidecar zo súborového systému",
+    "slideshow_duration_description": "Čas zobrazenia obrázku v sekundách",
+    "smart_search_job_description": "Spustite strojové učenie na médiách na podporu inteligentného vyhľadávania",
+    "storage_template_date_time_description": "Časová pečiatka vytvorenia médií sa používa pre informácie o dátume a čase",
+    "storage_template_date_time_sample": "Čas vzorky {date}",
+    "storage_template_enable_description": "Povoliť nástroj šablóny úložiska",
+    "storage_template_hash_verification_enabled": "Overenie hash povolené",
+    "storage_template_hash_verification_enabled_description": "Povolí overenie hash, nezakazujte to, pokiaľ si nie ste istí dôsledkami",
+    "storage_template_migration": "Migrácia šablóny úložiska",
+    "storage_template_migration_description": "Použite aktuálnu <link>{template}</link> na predtým nahrané médiá",
+    "storage_template_migration_info": "Zmeny šablón sa budú vzťahovať iba na nové diela. Ak chcete šablónu spätne použiť na predtým nahrané médiá, spustite <link>{job}</link>.",
+    "storage_template_migration_job": "Úloha migrácie šablóny úložiska",
+    "storage_template_more_details": "Ďalšie podrobnosti o tejto funkcii nájdete v <template-link>Šablóna úložiska</template-link> a jej <implications-link>dôsledky</implications-link>",
+    "storage_template_onboarding_description": "Keď je táto funkcia povolená, automaticky usporiada súbory na základe šablóny definovanej používateľom. Kvôli problémom so stabilitou bola funkcia predvolene vypnutá. Viac informácií nájdete v <link>dokumentácii</link>.",
+    "storage_template_path_length": "Približný limit dĺžky cesty: <b>{length, number}</b>/{limit, number}",
+    "storage_template_settings": "Šablóna úložiska",
+    "storage_template_settings_description": "Spravujte štruktúru priečinkov a názov súboru odovzdaného média",
+    "storage_template_user_label": "<code>{label}</code> je Štítok úložiska používateľa",
+    "system_settings": "Nastavenia systému",
+    "tag_cleanup_job": "Premazanie značiek",
+    "template_email_available_tags": "V šablóne môžeš použiť nasledujúce premenné: {tags}",
+    "template_email_if_empty": "Ak nie je zadaná žiadna šablóna, bude použitá predvolená šablóna.",
+    "template_email_invite_album": "Šablóna pre Pozvánka do albumu",
+    "template_email_preview": "Ukážka",
+    "template_email_settings": "Emailové šablóny",
+    "template_email_settings_description": "Spravovanie vlastných šablón pre emailové upozornenia",
+    "template_email_update_album": "Upraviť šablónu albumu",
+    "template_email_welcome": "Šablóna uvítajúceho emailu",
+    "template_settings": "Šablóna upozornení",
+    "template_settings_description": "Spravovanie vlastných šablón upozornení.",
+    "theme_custom_css_settings": "Vlastné CSS",
+    "theme_custom_css_settings_description": "CSS štýly umožňujú prispôsobiť dizajn Immich.",
+    "theme_settings": "Nastavenia témovania",
+    "theme_settings_description": "Spravovať prispôsobenie webového rozhrania Immich",
+    "these_files_matched_by_checksum": "Tieto súbory zodpovedajú kontrolným súčtom",
+    "thumbnail_generation_job": "Generovať Miniatúry",
+    "thumbnail_generation_job_description": "Generujte veľké, malé a rozmazané miniatúry pre každé médium, ako aj miniatúry pre každú osobu",
+    "transcoding_acceleration_api": "API pre akceleráciu",
+    "transcoding_acceleration_api_description": "Rozhranie API, ktoré bude interagovať s vaším zariadením s cieľom urýchliť prekódovanie. Toto nastavenie je „najlepšie úsilie“: pri zlyhaní sa vráti k softvérovému prekódovaniu. VP9 môže alebo nemusí fungovať v závislosti od vášho hardvéru.",
+    "transcoding_acceleration_nvenc": "NVENC (vyžaduje grafickú kartu NVIDIA)",
+    "transcoding_acceleration_qsv": "Quick Sync (vyžaduje 7. generáciu Intel procesora alebo novšie)",
+    "transcoding_acceleration_rkmpp": "RKMPP (iba na Rockchip SOC)",
+    "transcoding_acceleration_vaapi": "VAAPI",
+    "transcoding_accepted_audio_codecs": "Akceptované zvukové kodeky",
+    "transcoding_accepted_audio_codecs_description": "Vyberte, ktoré zvukové kodeky nie je potrebné prekódovať. Používa sa len pre určité zásady prekódovania.",
+    "transcoding_accepted_containers": "Akceptované kontajnery",
+    "transcoding_accepted_containers_description": "Vyberte, ktoré formáty kontajnerov nie je potrebné remuxovať na MP4. Používa sa len pre určité zásady prekódovania.",
+    "transcoding_accepted_video_codecs": "Akceptované video kodeky",
+    "transcoding_accepted_video_codecs_description": "Vyberte, ktoré video kodeky nie je potrebné prekódovať. Používa sa len pre určité zásady prekódovania.",
+    "transcoding_advanced_options_description": "Možnosti, ktoré by väčšina používateľov nemala meniť",
+    "transcoding_audio_codec": "Zvukový kodek",
+    "transcoding_audio_codec_description": "Opus je najkvalitnejšia možnosť, ale má nižšiu kompatibilitu so starými zariadeniami alebo softvérom.",
+    "transcoding_bitrate_description": "Videá presahujúce maximálnu bitovú rýchlosť alebo videá, ktoré nie sú v akceptovanom formáte",
+    "transcoding_codecs_learn_more": "Ak sa chcete dozvedieť viac o tu použitej terminológii, pozrite si dokumentáciu FFmpeg pre <h264-link>kodek H.264</h264-link>, <hevc-link>kodek HEVC</hevc-link> a <vp9-link>VP9 kodek</vp9-link>.",
+    "transcoding_constant_quality_mode": "Režim konštantnej kvality",
+    "transcoding_constant_quality_mode_description": "ICQ je lepšie ako CQP, ale niektoré zariadenia na hardvérovú akceleráciu tento režim nepodporujú. Nastavenie tejto možnosti uprednostní špecifikovaný režim pri použití kódovania založeného na kvalite. Ignorované spoločnosťou NVENC, pretože nepodporuje ICQ.",
+    "transcoding_constant_rate_factor": "Faktor konštantnej rýchlosti (-crf)",
+    "transcoding_constant_rate_factor_description": "Úroveň kvality videa. Typické hodnoty sú 23 pre H.264, 28 pre HEVC, 31 pre VP9 a 35 pre AV1. Nižšie je lepšie, ale vytvára väčšie súbory.",
+    "transcoding_disabled_description": "Neprekódujte žiadne videá, na niektorých klientoch môže prerušiť prehrávanie",
+    "transcoding_hardware_acceleration": "Hardvérová akcelerácia",
+    "transcoding_hardware_acceleration_description": "Experimentálne; oveľa rýchlejšie, ale bude mať nižšiu kvalitu pri rovnakej bitovej rýchlosti",
+    "transcoding_hardware_decoding": "Hardvérové dekódovanie",
+    "transcoding_hardware_decoding_setting_description": "Umožňuje end-to-end zrýchlenie namiesto iba zrýchlenia kódovania. Nemusí fungovať na všetkých videách.",
+    "transcoding_hevc_codec": "Kodek HEVC",
+    "transcoding_max_b_frames": "Maximálny počet B-snímkov",
+    "transcoding_max_b_frames_description": "Vyššie hodnoty zvyšujú účinnosť kompresie, ale spomaľujú kódovanie. Nemusí byť kompatibilný s hardvérovou akceleráciou na starších zariadeniach. Hodnota 0 zakáže B-snímky, zatiaľ čo -1 nastaví túto hodnotu automaticky.",
+    "transcoding_max_bitrate": "Maximálna bitová rýchlosť",
+    "transcoding_max_bitrate_description": "Nastavenie maximálneho dátového toku môže zvýšiť predvídateľnosť veľkosti súborov za cenu menšieho zníženia kvality. Pri rozlíšení 720p sú typické hodnoty 2600k pre VP9 alebo HEVC alebo 4500k pre H.264. Zakázané, ak je nastavená hodnota 0.",
+    "transcoding_max_keyframe_interval": "Maximálny interval medzi kľúčovými snímkami",
+    "transcoding_max_keyframe_interval_description": "Nastavuje maximálnu vzdialenosť medzi kľúčovými snímkami. Nižšie hodnoty zhoršujú účinnosť kompresie, ale zlepšujú časy vyhľadávania a môžu zlepšiť kvalitu v scénach s rýchlym pohybom. Hodnota 0 nastavuje túto hodnotu automaticky.",
+    "transcoding_optimal_description": "Videá s vyšším ako cieľovým rozlíšením alebo videá, ktoré nie sú v prijateľnom formáte",
+    "transcoding_preferred_hardware_device": "Uprednostňované hardvérové zariadenie",
+    "transcoding_preferred_hardware_device_description": "Platí len pre VAAPI a QSV. Nastavuje uzol dri, ktorý sa používa na hardvérové prekódovanie.",
+    "transcoding_preset_preset": "Prednastavenie (-preset)",
+    "transcoding_preset_preset_description": "Rýchlosť kompresie. Pomalšie predvoľby vytvárajú menšie súbory a zvyšujú kvalitu, keď sa zameriavajú na určitý dátový tok. VP9 ignoruje rýchlosti vyššie ako „rýchlejšie“.",
+    "transcoding_reference_frames": "Referenčné snímky",
+    "transcoding_reference_frames_description": "Počet snímok, na ktoré sa má odkazovať pri kompresii daného snímku. Vyššie hodnoty zvyšujú účinnosť kompresie, ale spomaľujú kódovanie. Hodnota 0 sa nastavuje automaticky.",
+    "transcoding_required_description": "Iba videá, ktoré nie sú v prijatom formáte",
+    "transcoding_settings": "Nastavenia video transkódovania",
+    "transcoding_settings_description": "Správa informácií o rozlíšení a kódovaní videosúborov",
+    "transcoding_target_resolution": "Cieľové rozlíšenie",
+    "transcoding_target_resolution_description": "Vyššie rozlíšenia môžu zachovať viac detailov, ale ich kódovanie trvá dlhšie, majú väčšiu veľkosť súborov a môžu znížiť odozvu aplikácie.",
+    "transcoding_temporal_aq": "Časové AQ",
+    "transcoding_temporal_aq_description": "Platí len pre NVENC. Zvyšuje kvalitu scén s vysokým počtom detailov a nízkym počtom pohybov. Nemusí byť kompatibilný so staršími zariadeniami.",
+    "transcoding_threads": "Vlákna",
+    "transcoding_threads_description": "Vyššie hodnoty vedú k rýchlejšiemu kódovaniu, ale ponechávajú serveru menej priestoru na spracovanie iných úloh počas aktivity. Táto hodnota by nemala byť väčšia ako počet jadier CPU. Maximalizuje využitie, ak je nastavená na hodnotu 0.",
+    "transcoding_tone_mapping": "Tónové mapovanie",
+    "transcoding_tone_mapping_description": "Snaží sa zachovať vzhľad videí HDR pri konverzii na SDR. Každý algoritmus robí rôzne kompromisy v oblasti farieb, detailov a jasu. Hable zachováva detaily, Mobius zachováva farby a Reinhard zachováva jas.",
+    "transcoding_transcode_policy": "Politika prekódovania",
+    "transcoding_transcode_policy_description": "Zásady, kedy sa má video prekódovať. Videá HDR sa vždy prekódujú (okrem prípadov, keď je prekódovanie vypnuté).",
+    "transcoding_two_pass_encoding": "Dvojpriechodové kódovanie",
+    "transcoding_two_pass_encoding_setting_description": "Prekladajte v dvoch priechodoch, aby ste vytvorili lepšie zakódované videá. Keď je povolený maximálny dátový tok (vyžaduje sa na prácu s formátmi H.264 a HEVC), tento režim používa rozsah dátového toku na základe maximálneho dátového toku a ignoruje CRF. V prípade VP9 sa CRF môže použiť, ak je max bitrate vypnutý.",
+    "transcoding_video_codec": "Video kodek",
+    "transcoding_video_codec_description": "VP9 má vysokú účinnosť a kompatibilitu s webom, ale prekódovanie trvá dlhšie. HEVC má podobnú výkonnosť, ale nižšiu kompatibilitu s webom. H.264 je široko kompatibilný a rýchlo sa prekódováva, ale vytvára oveľa väčšie súbory. AV1 je najúčinnejší kodek, ale chýba mu podpora v starších zariadeniach.",
+    "trash_enabled_description": "Povoliť funkcie koša",
+    "trash_number_of_days": "Počet dní",
+    "trash_number_of_days_description": "Počet dní, počas ktorých sa má majetok ponechať v koši pred jeho trvalým odstránením",
+    "trash_settings": "Nastavenia koša",
+    "trash_settings_description": "Spravovať nastavenia koša",
+    "untracked_files": "Nesledované súbory",
+    "untracked_files_description": "Tieto súbory aplikácia nesleduje. Môžu byť výsledkom neúspešných presunov, prerušeného odosielania alebo môžu zostať v dôsledku chyby",
+    "user_cleanup_job": "Premazanie používateľov",
+    "user_delete_delay": "Konto <b>{user}</b> a jeho médiá budú podľa plánu natrvalo vymazané za {delay, plural, one {# day} other {# days}}.",
+    "user_delete_delay_settings": "Odstrániť oneskorenie",
+    "user_delete_delay_settings_description": "Počet dní po odstránení na trvalé vymazanie účtu a aktív používateľa. Úloha odstraňovania používateľov sa spúšťa o polnoci, aby sa skontrolovali používatelia, ktorí sú pripravení na odstránenie. Zmeny tohto nastavenia sa vyhodnotia pri ďalšom spustení.",
+    "user_delete_immediately": "Konto a médiá <b>{user}</b> budú zaradené do frontu na trvalé vymazanie <b>okamžite</b>.",
+    "user_delete_immediately_checkbox": "Používateľ a médiá budú zaradení do frontu na okamžité vymazanie",
+    "user_management": "Správa používateľov",
+    "user_password_has_been_reset": "Heslo používateľa bolo resetované:",
+    "user_password_reset_description": "Poskytnite používateľovi dočasné heslo a informujte ho, že si ho bude musieť zmeniť pri ďalšom prihlásení.",
+    "user_restore_description": "<b>{user}</b> bude účet obnovený.",
+    "user_restore_scheduled_removal": "Obnoviť používateľa - plánované odstránenie na {date, date, long}",
+    "user_settings": "Nastavenia používateľa",
+    "user_settings_description": "Spravovať používateľské nastavenia",
+    "user_successfully_removed": "Používateľ {email} bol úspešne odstránený.",
+    "version_check_enabled_description": "Povoliť kontrolu verzie",
+    "version_check_implications": "Funkcia kontroly verzie sa spolieha na pravidelnú komunikáciu s github.com",
+    "version_check_settings": "Kontrola verzie",
+    "version_check_settings_description": "Povoliť/zakázať upozornenia na novú verziu",
+    "video_conversion_job": "Prekódovať videá",
+    "video_conversion_job_description": "Prekódovanie videí pre širšiu kompatibilitu s prehliadačmi a zariadeniami"
+  },
+  "admin_email": "Administrátorský email",
+  "admin_password": "Administrátorské heslo",
+  "administration": "Administrácia",
+  "advanced": "Pokročilé",
+  "age_months": "Vek {months, plural, one {# month} other {# months}}",
+  "age_year_months": "Vek 1 rok, {months, plural, one {# month} other {# months}}",
+  "age_years": "{years, plural, other {Vek #}}",
+  "album_added": "Album bol pridaný",
+  "album_added_notification_setting_description": "Obdržať upozornenie emailom, keď ste pridaní do zdieľaného albumu",
+  "album_cover_updated": "Obal albumu aktualizovaný",
+  "album_delete_confirmation": "Ste si istý, že chcete odstrániť album {album}?",
+  "album_delete_confirmation_description": "Ak je tento album zdieľaný, ostatní používatelia k nemu už nebudú mať prístup.",
+  "album_info_updated": "Informácie albumu aktualizované",
+  "album_leave": "Opustiť album?",
+  "album_leave_confirmation": "Ste si istý, že chcete opustiť album {album}?",
+  "album_name": "Názov albumu",
+  "album_options": "Nastavenia albumu",
+  "album_remove_user": "Odstrániť používateľa?",
+  "album_remove_user_confirmation": "Ste si istý, že chcete odstrániť používateľa {user}?",
+  "album_share_no_users": "Vyzerá to, že ste tento album zdieľali so všetkými používateľmi alebo nemáte žiadneho používateľa, s ktorým by ste ho mohli zdieľať.",
+  "album_updated": "Album bol aktualizovaný",
+  "album_updated_setting_description": "Obdržať e-mailové upozornenie, keď v zdieľanom albume pribudnú nové položky",
+  "album_user_left": "Opustil {album}",
+  "album_user_removed": "Odstránený {user}",
+  "album_with_link_access": "Umožnite komukoľvek s odkazom pozrieť si fotky a ľudí v tomto albume.",
+  "albums": "Albumy",
+  "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albumov}}",
+  "all": "Všetko",
+  "all_albums": "Všetky albumy",
+  "all_people": "Všetci ľudia",
+  "all_videos": "Všetky videa",
+  "allow_dark_mode": "Povoliť tmavý režim",
+  "allow_edits": "Povoliť úpravy",
+  "allow_public_user_to_download": "Povoľte verejnému používateľovi sťahovať",
+  "allow_public_user_to_upload": "Umožniť verejnému používateľovi nahrávať",
+  "anti_clockwise": "Proti smeru hodinových ručičiek",
+  "api_key": "API Klúč",
+  "api_key_description": "Táto hodnota sa zobrazí iba raz. Pred zatvorením okna ju určite skopírujte.",
+  "api_key_empty": "Názov vášho API kĺuča by nemal byť prázdny",
+  "api_keys": "API Kľúče",
+  "app_settings": "Nastavenia Aplikácie",
+  "appears_in": "Vyskytuje sa v",
+  "archive": "Archivovať",
+  "archive_or_unarchive_photo": "Archivácia alebo odarchivovanie fotografie",
+  "archive_size": "Veľkosť archívu",
+  "archive_size_description": "Konfigurácia veľkosti archívu na stiahnutie (v GiB)",
+  "archived_count": "{count, plural, other {Archivovaných #}}",
+  "are_these_the_same_person": "Ide o tú istú osobu?",
+  "are_you_sure_to_do_this": "Ste si istý, že to chcete urobiť?",
+  "asset_added_to_album": "Pridané do albumu",
+  "asset_adding_to_album": "Pridáva sa do albumu...",
+  "asset_description_updated": "Popis média bol aktualizovaný",
+  "asset_filename_is_offline": "Médium {filename} je offline",
+  "asset_has_unassigned_faces": "Položka má nepriradené tváre",
+  "asset_hashing": "Hašovanie...",
+  "asset_offline": "Médium je offline",
+  "asset_offline_description": "Toto externý obsah sa už nenachádza na disku. Požiadajte o pomoc svojho správcu Immich.",
+  "asset_skipped": "Preskočené",
+  "asset_skipped_in_trash": "V koši",
+  "asset_uploaded": "Nahrané",
+  "asset_uploading": "Nahráva sa...",
+  "assets": "Položky",
+  "assets_added_count": "{count, plural, one {Pridaná # položka} few {Pridané # položky} other {Pridaných # položek}}",
+  "assets_added_to_album_count": "Do albumu {count, plural, one {bola pridaná # položka} few {boli pridané # položky} other {bolo pridaných # položiek}}",
+  "assets_added_to_name_count": "{count, plural, one {Pridaná # položka} few {Pridané # položky} other {Pridaných # položiek}} do {hasName, select, true {alba <b>{name}</b>} other {nového albumu}}",
+  "assets_count": "{count, plural, one {# položka} few {# položky} other {# položiek}}",
+  "assets_moved_to_trash_count": "Do koša {count, plural, one {bola presunutá # položka} few {boli presunuté # položky} other {bolo presunutých # položiek}}",
+  "assets_permanently_deleted_count": "Trvalo {count, plural, one {vymazaná # položka} few {vymazané # položky} other {vymazaných # položiek}}",
+  "assets_removed_count": "{count, plural, one {Odstránená # položka} few {Odstránené # položky} other {Odstránených # položiek}}",
+  "assets_restore_confirmation": "Naozaj chcete obnoviť všetky vyhodené položky? Túto akciu nie je možné vrátiť späť! Upozorňujeme, že týmto spôsobom nie je možné obnoviť žiadne offline položky.",
+  "assets_restored_count": "{count, plural, one {Obnovená # položka} few {Obnovené # položky} other {Obnovených # položiek}}",
+  "assets_trashed_count": "{count, plural, one {Odstránená # položka} few {Odstránené # položky} other {Odstránených # položiek}}",
+  "assets_were_part_of_album_count": "{count, plural, one {Položka bola} other {Položky boli}} súčasťou albumu",
+  "authorized_devices": "Autorizované zariadenia",
+  "back": "Späť",
+  "back_close_deselect": "Späť, zavrieť alebo zrušiť výber",
+  "backward": "Spätne",
+  "birthdate_saved": "Dátum narodenia bol úspešne uložený",
+  "birthdate_set_description": "Dátum narodenia sa používa na výpočet veku tejto osoby v čase fotografie.",
+  "blurred_background": "Rozmazané pozadie",
+  "bugs_and_feature_requests": "Chyby a požiadavky na funkcie",
+  "build": "Budovať",
+  "build_image": "Vytvoriť obrázok",
+  "bulk_delete_duplicates_confirmation": "Naozaj chcete hromadne odstrániť {count, plural, one {# duplikátnu položku} few {# duplikáte položky} other {# duplikátnych položiek}}? Týmto sa zachová najväčšia položka z každej skupiny a všetky ostatné duplikáty sa natrvalo odstránia. Túto akciu nie je možné vrátiť späť!",
+  "bulk_keep_duplicates_confirmation": "Naozaj chceš ponechať {count, plural, one {# duplicitný súbor} other {# duplicitné súbory}}? Týmto sa vysporiadaš so všetkými duplicitnými skupinami bez mazania súborov.",
+  "bulk_trash_duplicates_confirmation": "Naozaj chcete hromadne vymazať {count, plural, one {# duplicitný súbor} other {# duplicitné súbory}}? Týmto si ponecháš z každej skupiny najväčší súbor a vymažeš všetky ostatné duplicitné súbory v skupine.",
+  "buy": "Kúpiť Immich",
+  "camera": "Fotoaparát",
+  "camera_brand": "Výrobca fotoaparátu",
+  "camera_model": "Model fotoaparátu",
+  "cancel": "Zrušiť",
+  "cancel_search": "Zrušiť vyhľadávanie",
+  "cannot_merge_people": "Nie je možné zlúčiť ľudí",
+  "cannot_undo_this_action": "Túto akciu nemôžete vrátiť späť!",
+  "cannot_update_the_description": "Popis nie je možné aktualizovať",
+  "change_date": "Upraviť dátum",
+  "change_expiration_time": "Zmeniť čas vypršania",
+  "change_location": "Upraviť lokáciu",
+  "change_name": "Upraviť meno",
+  "change_name_successfully": "Meno bolo zmenené",
+  "change_password": "Zmeniť Heslo",
+  "change_password_description": "Buď sa do systému prihlasujete prvýkrát, alebo bola podaná žiadosť o zmenu hesla. Nižšie zadajte nové heslo.",
+  "change_your_password": "Zmeňte si heslo",
+  "changed_visibility_successfully": "Viditeľnosť bola úspešne zmenená",
+  "check_all": "Skontrolovať Všetko",
+  "check_logs": "Skontrolovať logy",
+  "choose_matching_people_to_merge": "Vyberte rovnakých ľudí na zlúčenie",
+  "city": "Mesto",
+  "clear": "VYMAZAŤ",
+  "clear_all": "Vymazať všetko",
+  "clear_all_recent_searches": "Vymazať nedávne vyhľadávania",
+  "clear_message": "Vymazať správu",
+  "clear_value": "Vymazať hodnotu",
+  "clockwise": "V smere hodinových ručičiek",
+  "close": "Zatvoriť",
+  "collapse": "Zbaliť",
+  "collapse_all": "Zbaliť všetko",
+  "color": "Farba",
+  "color_theme": "Farba témy",
+  "comment_deleted": "Komentár bol odstránený",
+  "comment_options": "Možnosti komentára",
+  "comments_and_likes": "Komentáre a páči sa mi to",
+  "comments_are_disabled": "Komentáre sú vypnuté",
+  "confirm": "Potvrdiť",
+  "confirm_admin_password": "Potvrdiť Administrátorské Heslo",
+  "confirm_delete_shared_link": "Ste si istý, že chcete odstrániť tento zdieľaný odkaz?",
+  "confirm_keep_this_delete_others": "Všetky ostatné položky v zásobníku budú odstránené okrem tejto položky. Naozaj chcete pokračovať?",
+  "confirm_password": "Potvrdiť heslo",
+  "contain": "",
+  "context": "Kontext",
+  "continue": "Pokračovať",
+  "copied_image_to_clipboard": "Obrázok skopírovaný do schránky.",
+  "copied_to_clipboard": "Skopírované do schránky!",
+  "copy_error": "Chyba pri kopírovaní",
+  "copy_file_path": "Kopírovať cestu odkazu",
+  "copy_image": "Skopírovať obrázok",
+  "copy_link": "Skopírovať odkaz",
+  "copy_link_to_clipboard": "Skopírovať do schránky",
+  "copy_password": "Skopírovať heslo",
+  "copy_to_clipboard": "Skopírovať do schránky",
+  "country": "Štát",
+  "cover": "Titulka",
+  "covers": "Dlaždice",
+  "create": "Vytvoriť",
+  "create_album": "Vytvoriť album",
+  "create_library": "Vytvoriť knižnicu",
+  "create_link": "Vytvoriť odkaz",
+  "create_link_to_share": "Vytvoriť odkaz na zdieľanie",
+  "create_link_to_share_description": "Umožniť každému kto má odkaz zobraziť vybrané fotografie",
+  "create_new_person": "Vytvoriť novú osobu",
+  "create_new_user": "Vytvorenie nového používateľa",
+  "create_tag": "Vytvoriť značku",
+  "create_user": "Vytvoriť používateľa",
+  "created": "Vytvorené",
+  "current_device": "Aktuálne zariadenie",
+  "custom_locale": "",
+  "custom_locale_description": "",
+  "dark": "Tmavý",
+  "date_after": "Dátum po",
+  "date_and_time": "Dátum a čas",
+  "date_before": "Dátum pred",
+  "date_of_birth_saved": "Dátum narodenia uložený",
+  "date_range": "Rozsah dátumu",
+  "day": "Deň",
+  "default_locale": "",
+  "default_locale_description": "",
+  "delete": "Vymazať",
+  "delete_album": "Odstrániť album",
+  "delete_api_key_prompt": "Naozaj chcete odstrániť tento API kľúč?",
+  "delete_key": "Vymazať kľúč",
+  "delete_library": "Odstrániť knižnicu",
+  "delete_link": "Odstrániť link",
+  "delete_shared_link": "Odstrániť zdieľaný odkaz",
+  "delete_tag": "Odstrániť označenie",
+  "delete_user": "Odstrániť používateľa",
+  "deleted_shared_link": "",
+  "description": "Popis",
+  "details": "PODROBNOSTI",
+  "direction": "Smer",
+  "disabled": "Vypnuté",
+  "disallow_edits": "",
+  "discord": "Discord",
+  "discover": "Preskúmať",
+  "dismiss_all_errors": "",
+  "dismiss_error": "",
+  "display_options": "Zobraziť možnosti",
+  "display_order": "",
+  "display_original_photos": "Zobraziť pôvodné fotografie",
+  "display_original_photos_setting_description": "",
+  "do_not_show_again": "Túto správu znova nezobrazovať",
+  "documentation": "Dokumentácia",
+  "done": "Hotovo",
+  "download": "Stiahnuť",
+  "download_settings": "Stiahnuť",
+  "downloading": "Sťahovanie",
+  "downloading_asset_filename": "Stahovanie súboru {filename}",
+  "duplicates": "Duplikáty",
+  "duration": "Trvanie",
+  "edit": "Upraviť",
+  "edit_album": "Upraviť album",
+  "edit_avatar": "Upraviť postavu",
+  "edit_date": "Upraviť dátum",
+  "edit_date_and_time": "Upraviť dátum a čas",
+  "edit_exclusion_pattern": "",
+  "edit_faces": "Upraviť tváre",
+  "edit_import_path": "",
+  "edit_import_paths": "",
+  "edit_key": "Upraviť kľúč",
+  "edit_link": "Upraviť odkaz",
+  "edit_location": "Upraviť polohu",
+  "edit_name": "Upraviť meno",
+  "edit_people": "Upraviť ľudí",
+  "edit_tag": "Upraiť značku",
+  "edit_title": "Upraviť názov",
+  "edit_user": "Upraviť používateľa",
+  "edited": "Upravené",
+  "editor": "",
+  "editor_close_without_save_prompt": "Úpravy nebudú uložené",
+  "editor_crop_tool_h2_aspect_ratios": "Pomer strán",
+  "editor_crop_tool_h2_rotation": "Rotácia",
+  "email": "E-mail",
+  "empty_trash": "Vyprázdniť kôš",
+  "enable": "Aktivovať",
+  "enabled": "Aktivovaný",
+  "end_date": "",
+  "error": "Chyba",
+  "error_loading_image": "Chyba pri načítaní obrázku",
+  "error_title": "Chyba - niečo sa pokazilo",
+  "errors": {
+    "unable_to_add_album_users": "",
+    "unable_to_add_comment": "",
+    "unable_to_add_partners": "",
+    "unable_to_change_album_user_role": "",
+    "unable_to_change_date": "",
+    "unable_to_change_location": "",
+    "unable_to_create_admin_account": "",
+    "unable_to_create_library": "",
+    "unable_to_create_user": "",
+    "unable_to_delete_album": "",
+    "unable_to_delete_asset": "",
+    "unable_to_delete_user": "",
+    "unable_to_empty_trash": "",
+    "unable_to_enter_fullscreen": "",
+    "unable_to_exit_fullscreen": "",
+    "unable_to_hide_person": "",
+    "unable_to_load_album": "",
+    "unable_to_load_asset_activity": "",
+    "unable_to_load_items": "",
+    "unable_to_load_liked_status": "",
+    "unable_to_play_video": "",
+    "unable_to_refresh_user": "",
+    "unable_to_remove_album_users": "",
+    "unable_to_remove_library": "",
+    "unable_to_remove_partner": "",
+    "unable_to_remove_reaction": "",
+    "unable_to_repair_items": "",
+    "unable_to_reset_password": "",
+    "unable_to_resolve_duplicate": "",
+    "unable_to_restore_assets": "",
+    "unable_to_restore_trash": "",
+    "unable_to_restore_user": "",
+    "unable_to_save_album": "",
+    "unable_to_save_name": "",
+    "unable_to_save_profile": "",
+    "unable_to_save_settings": "",
+    "unable_to_scan_libraries": "",
+    "unable_to_scan_library": "",
+    "unable_to_set_profile_picture": "",
+    "unable_to_submit_job": "",
+    "unable_to_trash_asset": "",
+    "unable_to_unlink_account": "",
+    "unable_to_update_library": "",
+    "unable_to_update_location": "",
+    "unable_to_update_settings": "",
+    "unable_to_update_user": ""
+  },
+  "exif": "Exif",
+  "exit_slideshow": "Opustiť Slideshow",
+  "expand_all": "",
+  "expire_after": "Expiruje po",
+  "expired": "Vypršalo",
+  "explore": "Preskúmať",
+  "explorer": "Prieskumník",
+  "export": "Exportovať",
+  "export_as_json": "Exportovať ako JSON",
+  "extension": "Rozšírenie",
+  "external": "Externý",
+  "external_libraries": "",
+  "favorite": "Obľúbené",
+  "favorite_or_unfavorite_photo": "",
+  "favorites": "Obľúbené",
+  "feature_photo_updated": "",
+  "features": "Funkcie",
+  "file_name": "Meno súboru",
+  "file_name_or_extension": "",
+  "filename": "Meno súboru",
+  "filetype": "Typ súboru",
+  "filter_people": "Filtrovať ľudí",
+  "find_them_fast": "Nájdite ich rýchlejšie podľa mena",
+  "fix_incorrect_match": "",
+  "forward": "",
+  "general": "Všeobecné",
+  "get_help": "",
+  "getting_started": "",
+  "go_back": "",
+  "go_to_search": "",
+  "group_albums_by": "",
+  "has_quota": "",
+  "hide_gallery": "",
+  "hide_password": "",
+  "hide_person": "",
+  "host": "",
+  "hour": "",
+  "image": "",
+  "immich_logo": "",
+  "import_path": "",
+  "in_archive": "",
+  "include_archived": "Zahrnúť archivované",
+  "include_shared_albums": "",
+  "include_shared_partner_assets": "",
+  "individual_share": "",
+  "info": "",
+  "interval": {
+    "day_at_onepm": "",
+    "hours": "",
+    "night_at_midnight": "",
+    "night_at_twoam": ""
+  },
+  "invite_people": "",
+  "invite_to_album": "Pozvať do albumu",
+  "jobs": "",
+  "keep": "",
+  "keyboard_shortcuts": "",
+  "language": "",
+  "language_setting_description": "",
+  "last_seen": "",
+  "leave": "",
+  "let_others_respond": "Nechajte ostatných reagovať",
+  "level": "",
+  "library": "Knižnica",
+  "library_options": "",
+  "light": "",
+  "link_options": "",
+  "link_to_oauth": "",
+  "linked_oauth_account": "",
+  "list": "",
+  "loading": "",
+  "loading_search_results_failed": "",
+  "log_out": "Odhlásiť sa",
+  "log_out_all_devices": "",
+  "login_has_been_disabled": "Prihlásenie bolo vypnuté.",
+  "look": "",
+  "loop_videos": "",
+  "loop_videos_description": "",
+  "make": "",
+  "manage_shared_links": "Spravovať zdieľané odkazy",
+  "manage_sharing_with_partners": "",
+  "manage_the_app_settings": "",
+  "manage_your_account": "",
+  "manage_your_api_keys": "",
+  "manage_your_devices": "",
+  "manage_your_oauth_connection": "",
+  "map": "Mapa",
+  "map_marker_with_image": "",
+  "map_settings": "Nastavenia máp",
+  "media_type": "",
+  "memories": "",
+  "memories_setting_description": "",
+  "menu": "",
+  "merge": "",
+  "merge_people": "",
+  "merge_people_successfully": "",
+  "minimize": "",
+  "minute": "",
+  "missing": "",
+  "model": "",
+  "month": "Mesiac",
+  "more": "",
+  "moved_to_trash": "",
+  "my_albums": "",
+  "name": "Meno",
+  "name_or_nickname": "",
+  "never": "nikdy",
+  "new_api_key": "",
+  "new_password": "Nové heslo",
+  "new_person": "",
+  "new_user_created": "",
+  "new_version_available": "JE DOSTUPNÁ NOVÁ VERZIA",
+  "newest_first": "",
+  "next": "Ďalej",
+  "next_memory": "",
+  "no": "",
+  "no_albums_message": "",
+  "no_archived_assets_message": "Archivovať fotografie a videá, aby sa skryli zo zobrazenia Fotografie",
+  "no_assets_message": "",
+  "no_exif_info_available": "",
+  "no_explore_results_message": "",
+  "no_favorites_message": "",
+  "no_libraries_message": "",
+  "no_name": "",
+  "no_places": "",
+  "no_results": "",
+  "no_shared_albums_message": "",
+  "not_in_any_album": "",
+  "note_apply_storage_label_to_previously_uploaded assets": "Poznámka: Ak chcete použiť Štítok úložiska na predtým nahrané médiá, spustite príkaz",
+  "notes": "",
+  "notification_toggle_setting_description": "Povoliť e-mailové upozornenia",
+  "notifications": "Oznámenia",
+  "notifications_setting_description": "Spravovať upozornenia",
+  "oauth": "OAuth",
+  "offline": "",
+  "ok": "",
+  "oldest_first": "",
+  "onboarding_welcome_user": "Vitaj, {user}",
+  "online": "",
+  "only_favorites": "",
+  "open_the_search_filters": "",
+  "options": "Nastavenia",
+  "or": "alebo",
+  "organize_your_library": "Usporiadajte svoju knižnicu",
+  "other": "",
+  "other_devices": "Ďalšie zariadenia",
+  "other_variables": "",
+  "owned": "Vlastnené",
+  "owner": "Vlastník",
+  "partner_sharing": "",
+  "partners": "",
+  "password": "Heslo",
+  "password_does_not_match": "",
+  "password_required": "",
+  "password_reset_success": "",
+  "past_durations": {
+    "days": "",
+    "hours": "",
+    "years": ""
+  },
+  "path": "",
+  "pattern": "",
+  "pause": "",
+  "pause_memories": "",
+  "paused": "",
+  "pending": "",
+  "people": "Ľudia",
+  "people_sidebar_description": "",
+  "permanent_deletion_warning": "",
+  "permanent_deletion_warning_setting_description": "",
+  "permanently_delete": "",
+  "permanently_deleted_asset": "",
+  "photos": "Fotografie",
+  "photos_and_videos": "Fotografie & Videa",
+  "photos_from_previous_years": "",
+  "pick_a_location": "",
+  "place": "Miesto",
+  "places": "Miesta",
+  "play": "Prehrať",
+  "play_memories": "",
+  "play_motion_photo": "",
+  "play_or_pause_video": "",
+  "port": "",
+  "preset": "",
+  "preview": "",
+  "previous": "",
+  "previous_memory": "",
+  "previous_or_next_photo": "",
+  "primary": "",
+  "profile_picture_set": "",
+  "public_album": "Verejný album",
+  "public_share": "",
+  "purchase_activated_time": "Aktivované {date, date}",
+  "purchase_button_activate": "Aktivovať",
+  "purchase_button_never_show_again": "Už viac nezobrazovať",
+  "purchase_panel_title": "Podporiť projekt",
+  "reaction_options": "",
+  "read_changelog": "",
+  "recent": "Nedávne",
+  "recent_searches": "",
+  "refresh": "Obnoviť",
+  "refresh_metadata": "Obnoviť metadáta",
+  "refresh_thumbnails": "Obnoviť miniatúry",
+  "refreshed": "Aktualizované",
+  "refreshes_every_file": "",
+  "remove": "Odstrániť",
+  "remove_deleted_assets": "",
+  "remove_from_album": "Odstrániť z albumu",
+  "remove_from_favorites": "",
+  "remove_from_shared_link": "",
+  "remove_user": "Odstrániť používateľa",
+  "repair": "Opraviť",
+  "repair_no_results_message": "",
+  "replace_with_upload": "",
+  "require_password": "Vyžadovať heslo",
+  "reset": "Resetovať",
+  "reset_password": "Obnoviť heslo",
+  "reset_people_visibility": "",
+  "restore": "Obnoviť",
+  "restore_user": "Obnoviť používateľa",
+  "resume": "Pokračovať",
+  "retry_upload": "",
+  "review_duplicates": "Skontrolovať duplikáty",
+  "role": "",
+  "save": "Uložiť",
+  "saved_profile": "",
+  "saved_settings": "",
+  "say_something": "Napíšte niečo",
+  "scan_all_libraries": "",
+  "scan_settings": "Nastavenia skenovania",
+  "search": "Vyhľadávanie",
+  "search_albums": "Hľadať albumy",
+  "search_by_context": "",
+  "search_by_filename_example": "napr. IMG_1234.JPG alebo PNG",
+  "search_camera_make": "",
+  "search_camera_model": "",
+  "search_city": "",
+  "search_country": "",
+  "search_for_existing_person": "",
+  "search_people": "",
+  "search_places": "",
+  "search_settings": "Hladať v nastaveniach",
+  "search_state": "",
+  "search_timezone": "Vyhľadať časovú zónu...",
+  "search_type": "",
+  "search_your_photos": "Prehľadajte svoje obrázky",
+  "searching_locales": "",
+  "second": "",
+  "select_album_cover": "",
+  "select_all": "",
+  "select_avatar_color": "",
+  "select_face": "",
+  "select_featured_photo": "",
+  "select_library_owner": "Vybraťi vlastníka knižnice",
+  "select_new_face": "",
+  "select_photos": "Vybrať fotografie",
+  "selected": "Vybraté",
+  "send_message": "Odoslať správu",
+  "send_welcome_email": "Odoslať uvítací e-mail",
+  "server_stats": "Štatistiky servera",
+  "server_version": "Verzia servera",
+  "set": "Nastaviť",
+  "set_as_album_cover": "",
+  "set_as_profile_picture": "Nastaviť ako profilový obrázok",
+  "set_date_of_birth": "Nastaviť dátum narodenia",
+  "set_profile_picture": "Nastaviť profilový obrázok",
+  "set_slideshow_to_fullscreen": "",
+  "settings": "Nastavenia",
+  "settings_saved": "Nastavenia boli uložené",
+  "share": "Zdieľať",
+  "shared": "Zdieľané",
+  "shared_by": "",
+  "shared_by_you": "",
+  "shared_from_partner": "Fotografie od {partner}",
+  "shared_links": "Zdieľané odkazy",
+  "shared_with_partner": "Zďielané s {partner}",
+  "sharing": "Zdieľanie",
+  "sharing_sidebar_description": "",
+  "show_album_options": "Zobraziť možnosti albumu",
+  "show_albums": "Zobraziť albumy",
+  "show_file_location": "",
+  "show_gallery": "Zobraziť galériu",
+  "show_hidden_people": "",
+  "show_in_timeline": "Zobraziť na časovej osi",
+  "show_in_timeline_setting_description": "",
+  "show_keyboard_shortcuts": "Zobraziť klávesové skratky",
+  "show_metadata": "Zobraziť metadáta",
+  "show_or_hide_info": "",
+  "show_password": "Zobraziť heslo",
+  "show_person_options": "",
+  "show_progress_bar": "",
+  "show_search_options": "Zobraziť možnosti vyhľadávania",
+  "shuffle": "",
+  "sign_out": "Odhlásiť sa",
+  "sign_up": "",
+  "size": "Veľkosť",
+  "skip_to_content": "",
+  "slideshow": "",
+  "slideshow_settings": "",
+  "sort_albums_by": "Zoradiť albumy podľa...",
+  "sort_created": "Dátum vytvorenia",
+  "sort_items": "Počet položiek",
+  "sort_modified": "Dátum úpravy",
+  "sort_oldest": "Najstaršia fotografia",
+  "sort_recent": "Najnovšia fotografia",
+  "sort_title": "Názov",
+  "source": "Zdroj",
+  "stack": "Zoskupenie",
+  "stack_selected_photos": "",
+  "stacktrace": "",
+  "start_date": "",
+  "state": "",
+  "status": "",
+  "stop_motion_photo": "",
+  "stop_photo_sharing": "Zastaviť zdieľanie vašich fotiek?",
+  "storage": "Ukladací priestor",
+  "storage_label": "Štítok úložiska",
+  "submit": "Odoslať",
+  "suggestions": "Návrhy",
+  "sunrise_on_the_beach": "",
+  "swap_merge_direction": "",
+  "sync": "",
+  "tags": "Značky",
+  "template": "",
+  "theme": "Téma",
+  "theme_selection": "",
+  "theme_selection_description": "",
+  "time_based_memories": "",
+  "timezone": "Časové pásmo",
+  "to_archive": "Archivovať",
+  "to_change_password": "Zmeniť heslo",
+  "to_trash": "Kôš",
+  "toggle_settings": "",
+  "toggle_theme": "",
+  "total_usage": "",
+  "trash": "Kôš",
+  "trash_all": "",
+  "trash_no_results_message": "Vymazané fotografie a videá sa zobrazia tu.",
+  "type": "",
+  "unarchive": "Odarchivovať",
+  "unfavorite": "Odznačiť ako obľúbené",
+  "unhide_person": "",
+  "unknown": "",
+  "unknown_year": "Neznámy rok",
+  "unlink_oauth": "",
+  "unlinked_oauth_account": "",
+  "unnamed_album_delete_confirmation": "Ste si istý, že chcete zmazať tento album?",
+  "unsaved_change": "Neuložená zmena",
+  "unselect_all": "",
+  "unstack": "Odskupiť",
+  "up_next": "",
+  "updated_password": "",
+  "upload": "Nahrať",
+  "upload_concurrency": "",
+  "upload_status_duplicates": "Duplikáty",
+  "upload_status_errors": "Chyby",
+  "upload_status_uploaded": "Nahrané",
+  "upload_success": "Nahrávanie úspešné, pridané súbory sa zobrazia po obnovení stránky.",
+  "url": "Odkaz URL",
+  "usage": "Použitie",
+  "user": "Používateľ",
+  "user_id": "Používateľské ID",
+  "user_role_set": "Nastav {user} ako {role}",
+  "user_usage_detail": "",
+  "user_usage_stats": "Štatistiky využitia účtu",
+  "user_usage_stats_description": "Zobraziť štatistiky využitia účtu",
+  "username": "Používateľské meno",
+  "users": "Používatelia",
+  "utilities": "Nástroje",
+  "validate": "Validovať",
+  "variables": "Premenné",
+  "version": "Verzia",
+  "version_announcement_closing": "Tvoj kamarát, Alex",
+  "version_history": "História verzií",
+  "video": "Video",
+  "video_hover_setting_description": "",
+  "videos": "Videá",
+  "view": "Zobraziť",
+  "view_album": "Zobraziť Album",
+  "view_all": "Zobraziť všetky",
+  "view_all_users": "Zobraziť všetkých používateľov",
+  "view_in_timeline": "Zobraziť v časovej osi",
+  "view_links": "Zobraziť odkazy",
+  "view_next_asset": "Zobraziť nasledujúci súbor",
+  "view_previous_asset": "Zobraziť predchádzajúci súbor",
+  "waiting": "",
+  "warning": "Varovanie",
+  "week": "Týždeň",
+  "welcome": "Vitajte",
+  "welcome_to_immich": "Vitajte v Immich",
+  "year": "Rok",
+  "yes": "Áno",
+  "you_dont_have_any_shared_links": "Nemáte žiadne zdielané linky",
+  "zoom_image": "Priblížiť obrázok"
+}
diff --git a/i18n/sl.json b/i18n/sl.json
new file mode 100644
index 0000000000..b9a1d24d53
--- /dev/null
+++ b/i18n/sl.json
@@ -0,0 +1,1340 @@
+{
+  "about": "O programu",
+  "account": "Račun",
+  "account_settings": "Nastavitve računa",
+  "acknowledge": "Sem seznanjen",
+  "action": "Dejanje",
+  "actions": "Dejanja",
+  "active": "Aktivno",
+  "activity": "Aktivnost",
+  "activity_changed": "Aktivnost {enabled, select, true {omogočena} other {onemogočena}}",
+  "add": "Dodaj",
+  "add_a_description": "Dodaj opis",
+  "add_a_location": "Dodaj lokacijo",
+  "add_a_name": "Dodaj ime",
+  "add_a_title": "Dodaj naslov",
+  "add_exclusion_pattern": "Dodaj vzorec izključitve",
+  "add_import_path": "Dodaj pot uvoza",
+  "add_location": "Dodaj lokacijo",
+  "add_more_users": "Dodaj več uporabnikov",
+  "add_partner": "Dodaj partnerja",
+  "add_path": "Dodaj pot",
+  "add_photos": "Dodaj fotografije",
+  "add_to": "Dodaj v...",
+  "add_to_album": "Dodaj v album",
+  "add_to_shared_album": "Dodaj k deljenemu albumu",
+  "add_url": "Dodaj URL",
+  "added_to_archive": "Dodano v arhiv",
+  "added_to_favorites": "Dodano med priljubljene",
+  "added_to_favorites_count": "{count, number} dodanih med priljubljene",
+  "admin": {
+    "add_exclusion_pattern_description": "Dodajte vzorec izključitev. Globiranje z uporabo *, ** in ? je podprto. Če želite prezreti vse datoteke v katerem koli imeniku z imenom \"Raw\", uporabite \"**/Raw/**\". Če želite prezreti vse datoteke, ki se končajo na \".tif\", uporabite \"**/*.tif\". Če želite prezreti absolutno pot, uporabite \"/pot/za/ignoriranje/**\".",
+    "asset_offline_description": "Sredstva zunanje knjižnice ni več mogoče najti na disku in je bilo premaknjeno v koš. Če je bila datoteka premaknjena znotraj knjižnice, preverite svojo časovnico za novo ustrezno sredstvo. Če želite obnoviti to sredstvo, zagotovite, da ima Immich dostop do spodnje poti datoteke, in skenirajte knjižnico.",
+    "authentication_settings": "Nastavitve preverjanja pristnosti",
+    "authentication_settings_description": "Upravljanje gesel, OAuth in drugih nastavitev preverjanja pristnosti",
+    "authentication_settings_disable_all": "Ali zares želite onemogočiti vse prijavne metode? Prijava bo popolnoma onemogočena.",
+    "authentication_settings_reenable": "Ponovno omogoči z uporabo <link>strežniškega ukaza</link>.",
+    "background_task_job": "Opravila v ozadju",
+    "backup_database": "Varnostna kopija baze",
+    "backup_database_enable_description": "Omogoči varnostno kopiranje baze",
+    "backup_keep_last_amount": "Število prejšnjih obdržanih varnostnih kopij",
+    "backup_settings": "Nastavitve varnostnega kopiranja",
+    "backup_settings_description": "Upravljanje nastavitev varnostnih kopij",
+    "check_all": "Označi vse",
+    "cleared_jobs": "Razčiščeno opravilo za: {job}",
+    "config_set_by_file": "Konfiguracija je trenutno nastavljena s konfiguracijsko datoteko",
+    "confirm_delete_library": "Ali ste prepričani, da želite izbrisati knjižnico {library}?",
+    "confirm_delete_library_assets": "Ali ste prepričani, da želite izbrisati to knjižnico? To bo iz Immicha izbrisalo {count, plural, one {# contained asset} other {all # vsebovanih virov}} in tega ni možno razveljaviti. Datoteke bodo ostale na disku.",
+    "confirm_email_below": "Za potrditev vnesite \"{email}\" spodaj",
+    "confirm_reprocess_all_faces": "Ali ste prepričani, da želite znova obdelati vse obraze? S tem boste počistili tudi že imenovane osebe.",
+    "confirm_user_password_reset": "Ali ste prepričani, da želite ponastaviti geslo uporabnika {user}?",
+    "create_job": "Ustvari opravilo",
+    "cron_expression": "Nastavitveni izraz Cron",
+    "cron_expression_description": "Nastavite interval skeniranja z uporabo zapisa cron. Za več informacij poglej npr. <link>Crontab Guru</link>",
+    "cron_expression_presets": "Prednastavitve izraza Cron",
+    "disable_login": "Onemogoči prijavo",
+    "duplicate_detection_job_description": "Zaženite strojno učenje na sredstvih, da zaznate podobne slike. Zanaša se na Pametno Iskanje",
+    "exclusion_pattern_description": "Vzorci izključitev vam omogočajo, da prezrete datoteke in mape pri skeniranju knjižnice. To je uporabno, če imate mape z datotekami, ki jih ne želite uvoziti, na primer datoteke RAW.",
+    "external_library_created_at": "Zunanja knjižnica (ustvarjena dne {date})",
+    "external_library_management": "Upravljanje zunanje knjižnice",
+    "face_detection": "Zaznavanje obrazov",
+    "face_detection_description": "Zaznajte obraze v sredstvih s pomočjo strojnega učenja. Pri videoposnetkih se upošteva samo sličica. \"Vse\" (ponovno) obdela vsa sredstva. \"Manjkajoče\" postavi v čakalno vrsto sredstva, ki še niso bila obdelana. Zaznani obrazi bodo postavljeni v čakalno vrsto za prepoznavanje obrazov, ko bo zaznavanje obrazov končano, in jih bodo združili v obstoječe ali nove osebe.",
+    "facial_recognition_job_description": "Združi zaznane obraze v osebe. Ta korak se izvede po končanem zaznavanju obrazov. \"Vse\" (ponovno) združuje vse obraze. \"Manjkajoče\", doda v čakalno vrsto obraze, ki nimajo dodeljene osebe.",
+    "failed_job_command": "Za opravilo {job} ukaz {command} ni uspel",
+    "force_delete_user_warning": "OPOZORILO: S tem boste takoj odstranili uporabnika in vsa sredstva. Tega ni mogoče razveljaviti in datotek ni mogoče obnoviti.",
+    "forcing_refresh_library_files": "Vsiljena osvežitev vseh datotek knjižnice",
+    "image_format": "Format",
+    "image_format_description": "WebP ustvari manjše datoteke kot JPEG, vendar je počasnejši za kodiranje.",
+    "image_prefer_embedded_preview": "Uporabi raje vdelan predogled",
+    "image_prefer_embedded_preview_setting_description": "Uporabite vdelane predoglede v fotografije RAW kot vhod za obdelavo slik, ko so na voljo. To lahko ustvari natančnejše barve za nekatere slike, vendar je kakovost predogleda odvisna od kamere in slika ima lahko več artefaktov stiskanja.",
+    "image_prefer_wide_gamut": "Uporabi raje širok razpon",
+    "image_prefer_wide_gamut_setting_description": "Uporabite P3 Display za sličice. To bolje ohranja živahnost slik s širokimi barvnimi prostori, vendar so lahko slike videti drugače na starih napravah s staro različico brskalnika. Slike sRGB se ohranijo kot sRGB, da se izognejo barvnim zamikom.",
+    "image_preview_description": "Slika srednje velikosti z odstranjenimi metapodatki, ki se uporablja pri ogledu posameznega sredstva in za strojno učenje",
+    "image_preview_quality_description": "Kakovost predogleda od 1-100. Višje je boljše, vendar ustvarja večje datoteke in lahko zmanjša odzivnost aplikacije. Nastavitev nizke vrednosti lahko vpliva na kakovost strojnega učenja.",
+    "image_preview_title": "Nastavitve predogleda",
+    "image_quality": "Kvaliteta",
+    "image_resolution": "Resolucija",
+    "image_resolution_description": "Višje ločljivosti lahko ohranijo več podrobnosti, vendar kodiranje traja dlje, imajo večje velikosti datotek in lahko zmanjšajo odzivnost aplikacije.",
+    "image_settings": "Nastavitve slike",
+    "image_settings_description": "Upravljajte kakovost in ločljivost ustvarjenih slik",
+    "image_thumbnail_description": "Majhna sličica z odstranjenimi metapodatki, ki se uporablja pri ogledovanju skupin fotografij, kot je glavna časovnica",
+    "image_thumbnail_quality_description": "Kakovost sličic od 1-100. Višje je boljše, vendar ustvarja večje datoteke in lahko zmanjša odzivnost aplikacije.",
+    "image_thumbnail_title": "Nastavitve sličic",
+    "job_concurrency": "{job} sočasnost",
+    "job_created": "Opravilo ustvarjeno",
+    "job_not_concurrency_safe": "To opravilo ni sočasno-varno.",
+    "job_settings": "Nastavitve opravil",
+    "job_settings_description": "Upravljaj sočasnost opravil",
+    "job_status": "Status opravila",
+    "jobs_delayed": "{jobCount, plural, other {# zadržan}}",
+    "jobs_failed": "{jobCount, plural, other {# neuspešen}}",
+    "library_created": "Ustvarjena knjižnica: {library}",
+    "library_deleted": "Knjižnica izbrisana",
+    "library_import_path_description": "Določi mapo za uvoz. Ta mapa in njene podmape bodo pregledane za slike in video posnetke.",
+    "library_scanning": "Periodični pregledi",
+    "library_scanning_description": "Nastavi periodični pregled knjižnic",
+    "library_scanning_enable_description": "Omogoči periodični pregled knjižnic",
+    "library_settings": "Zunanja knjižnica",
+    "library_settings_description": "Uredi nastavitve zunanje knjižnice",
+    "library_tasks_description": "Izvedi nalogo knjižnice",
+    "library_watching_enable_description": "Opazuj spremembe datotek v zunanji knjižnici",
+    "library_watching_settings": "Opazovanje knjižnice (EKSPERIMENTALNO)",
+    "library_watching_settings_description": "Samodejno opazuj spremembo datotek",
+    "logging_enable_description": "Omogoči dnevnik",
+    "logging_level_description": "Nivo dnevnika, ko je le-ta omogočen.",
+    "logging_settings": "Dnevnik",
+    "machine_learning_clip_model": "model CLIP",
+    "machine_learning_clip_model_description": "Ime CLIP modela iz seznama <link>tukaj</link>. Vedite, da boste morali po menjavi modela ponovno zagnati opravilo za 'Pametno iskanje' za vse slike.",
+    "machine_learning_duplicate_detection": "Zaznavanje dvojnikov",
+    "machine_learning_duplicate_detection_enabled": "Omogoči zaznavanje dvojnikov",
+    "machine_learning_duplicate_detection_enabled_description": "Če je onemogočeno, bodo popolnoma enaki posnetki še vedno obravnavani.",
+    "machine_learning_duplicate_detection_setting_description": "Za iskanje verjetnih dvojnikov uporabite vdelave CLIP",
+    "machine_learning_enabled": "Omogoči strojno učenje",
+    "machine_learning_enabled_description": "Če je onemogočeno, bodo vse funkcije strojnega učenja onemogočene ne glede na spodnje nastavitve.",
+    "machine_learning_facial_recognition": "Zaznavanje obrazov",
+    "machine_learning_facial_recognition_description": "Zaznavanje, prepoznavanje in združevanje obrazov na slikah",
+    "machine_learning_facial_recognition_model": "Model za prepoznavanje obraza",
+    "machine_learning_facial_recognition_model_description": "Modeli so navedeni v padajočem vrstnem redu glede na velikost. Večji modeli so počasnejši in uporabljajo več pomnilnika, vendar dajejo boljše rezultate. Upoštevajte, da morate po spremembi modela znova zagnati opravilo zaznavanja obrazov za vse slike.",
+    "machine_learning_facial_recognition_setting": "Omogoči prepoznavanje obraza",
+    "machine_learning_facial_recognition_setting_description": "Če je onemogočeno, slike ne bodo kodirane za prepoznavanje obraza in ne bodo zapolnile razdelka Ljudje na strani Razišči.",
+    "machine_learning_max_detection_distance": "Največja razdalja zaznavanja",
+    "machine_learning_max_detection_distance_description": "Največja razdalja med dvema slikama za dvojnike, ki se giblje od 0,001 do 0,1. Višje vrednosti bodo zaznale več dvojnikov, vendar lahko povzročijo lažne pozitivne rezultate.",
+    "machine_learning_max_recognition_distance": "Največja razdalja za prepoznavanje",
+    "machine_learning_max_recognition_distance_description": "Največja razdalja med dvema obrazoma za isto osebo, ki se giblje od 0-2. Znižanje lahko prepreči označevanje dveh oseb kot iste osebe, zvišanje pa lahko prepreči označevanje iste osebe kot dve različni osebi. Upoštevajte, da je lažje združiti dve osebi kot eno osebo razdeliti na dva dela, zato se zmotite pri nižjem pragu, kadar je to mogoče.",
+    "machine_learning_min_detection_score": "Najmanjši rezultat zaznavanja",
+    "machine_learning_min_detection_score_description": "Najmanjši rezultat zaupanja za zaznavanje obraza od 0-1. Nižje vrednosti bodo zaznale več obrazov, vendar lahko povzročijo lažne pozitivne rezultate.",
+    "machine_learning_min_recognized_faces": "Najmanjše število prepoznanih obrazov",
+    "machine_learning_min_recognized_faces_description": "Najmanjše število prepoznanih obrazov za osebo, ki se ustvari. Če to povečate, postane prepoznavanje obraza natančnejše na račun večje možnosti, da obraz ni dodeljen osebi.",
+    "machine_learning_settings": "Nastavitve strojnega učenja",
+    "machine_learning_settings_description": "Upravljajte funkcije in nastavitve strojnega učenja",
+    "machine_learning_smart_search": "Pametno iskanje",
+    "machine_learning_smart_search_description": "Semantično poiščite slike z uporabo vdelav CLIP",
+    "machine_learning_smart_search_enabled": "Omogoči pametno iskanje",
+    "machine_learning_smart_search_enabled_description": "Če je onemogočeno, slike ne bodo kodirane za pametno iskanje.",
+    "machine_learning_url_description": "URL strežnika za strojno učenje. Če je na voljo več kot en URL, bo vsak strežnik poskusen posamično, dokler se eden ne odzove uspešno, v vrstnem redu od prvega do zadnjega.",
+    "manage_concurrency": "Upravljanje sočasnosti",
+    "manage_log_settings": "Upravljanje nastavitev dnevnika",
+    "map_dark_style": "Temni način",
+    "map_enable_description": "Omogoči funkcije zemljevida",
+    "map_gps_settings": "Nastavitve zemljevida in GPS",
+    "map_gps_settings_description": "Upravljajte nastavitve zemljevida in GPS (povratno geokodiranje)",
+    "map_implications": "Funkcija zemljevida se opira na zunanjo storitev ploščic (tiles.immich.cloud)",
+    "map_light_style": "Svetli način",
+    "map_manage_reverse_geocoding_settings": "Upravljanje nastavitev <link>Povratno geokodiranje</link>",
+    "map_reverse_geocoding": "Povratno geokodiranje",
+    "map_reverse_geocoding_enable_description": "Omogoči povratno geokodiranje",
+    "map_reverse_geocoding_settings": "Nastavitve povratnega geokodiranja",
+    "map_settings": "Zemljevid",
+    "map_settings_description": "Upravljanje nastavitev zemljevida",
+    "map_style_description": "URL do teme zemljevida style.json",
+    "metadata_extraction_job": "Izvleči metapodatke",
+    "metadata_extraction_job_description": "Izvleči informacije iz metapodatkov iz vseh virov, kot so GPS, obrazi in resolucija",
+    "metadata_faces_import_setting": "Omogoči uvoz obraza",
+    "metadata_faces_import_setting_description": "Uvozite obraze iz slikovnih podatkov EXIF in stranskih datotek",
+    "metadata_settings": "Nastavitve metapodatkov",
+    "metadata_settings_description": "Upravljanje nastavitev metapodatkov",
+    "migration_job": "Migracija",
+    "migration_job_description": "Preselite sličice za sredstva in obraze v najnovejšo strukturo map",
+    "no_paths_added": "Ni dodanih poti",
+    "no_pattern_added": "Brez dodanega vzorca",
+    "note_apply_storage_label_previous_assets": "Opomba: Če želite oznako za shranjevanje uporabiti za predhodno naložena sredstva, zaženite",
+    "note_cannot_be_changed_later": "OPOMBA: Tega pozneje ni mogoče spremeniti!",
+    "note_unlimited_quota": "Opomba: Vnesite 0 za neomejeno kvoto",
+    "notification_email_from_address": "Iz naslova",
+    "notification_email_from_address_description": "E-poštni naslov pošiljatelja, na primer: \"Immich Photo Server <noreply@example.com>\"",
+    "notification_email_host_description": "Gostitelj e-poštnega strežnika (npr. smtp.immich.app)",
+    "notification_email_ignore_certificate_errors": "Prezri napake potrdil",
+    "notification_email_ignore_certificate_errors_description": "Prezri napake pri preverjanju potrdila TLS (ni priporočljivo)",
+    "notification_email_password_description": "Geslo za uporabo pri preverjanju pristnosti z e-poštnim strežnikom",
+    "notification_email_port_description": "Vrata e-poštnega strežnika (npr. 25, 465 ali 587)",
+    "notification_email_sent_test_email_button": "Pošljite testno e-pošto in shranite",
+    "notification_email_setting_description": "Nastavitve za pošiljanje e-poštnih obvestil",
+    "notification_email_test_email": "Pošlji testno e-pošto",
+    "notification_email_test_email_failed": "Pošiljanje testnega e-poštnega sporočila ni uspelo, preverite svoje vrednosti",
+    "notification_email_test_email_sent": "Testno e-poštno sporočilo je bilo poslano na {email}. Prosimo, preverite svoj nabiralnik.",
+    "notification_email_username_description": "Uporabniško ime za uporabo pri preverjanju pristnosti z e-poštnim strežnikom",
+    "notification_enable_email_notifications": "Omogoči e-poštna obvestila",
+    "notification_settings": "Nastavitve obvestil",
+    "notification_settings_description": "Upravljajte nastavitve obvestil, vključno z e-pošto",
+    "oauth_auto_launch": "Samodejni zagon",
+    "oauth_auto_launch_description": "Samodejno zaženite tok prijave OAuth, ko obiščete stran za prijavo",
+    "oauth_auto_register": "Samodejna registracija",
+    "oauth_auto_register_description": "Samodejna registracija novih uporabnikov po prijavi z OAuth",
+    "oauth_button_text": "Besedilo gumba",
+    "oauth_client_id": "ID stranke",
+    "oauth_client_secret": "Skrivnost stranke",
+    "oauth_enable_description": "Prijava z OAuth",
+    "oauth_issuer_url": "URL izdajatelja",
+    "oauth_mobile_redirect_uri": "Mobilni preusmeritveni URI",
+    "oauth_mobile_redirect_uri_override": "Preglasitev URI preusmeritve za mobilne naprave",
+    "oauth_mobile_redirect_uri_override_description": "Omogoči, ko ponudnik OAuth ne dovoli mobilnega URI-ja, kot je '{callback}'",
+    "oauth_profile_signing_algorithm": "Algoritem za podpisovanje profila",
+    "oauth_profile_signing_algorithm_description": "Algoritem, ki se uporablja za podpisovanje uporabniškega profila.",
+    "oauth_scope": "Področje uporabe",
+    "oauth_settings": "OAuth",
+    "oauth_settings_description": "Upravljanje nastavitev prijave OAuth",
+    "oauth_settings_more_details": "Za več podrobnosti o tej funkciji glejte <link>dokumentacijo</link>.",
+    "oauth_signing_algorithm": "Algoritem podpisovanja",
+    "oauth_storage_label_claim": "Zahtevek za nalepko za shranjevanje",
+    "oauth_storage_label_claim_description": "Samodejno nastavi uporabnikovo oznako za shranjevanje na vrednost tega zahtevka.",
+    "oauth_storage_quota_claim": "Zahtevek za kvoto prostora za shranjevanje",
+    "oauth_storage_quota_claim_description": "Samodejno nastavi uporabnikovo kvoto shranjevanja na vrednost tega zahtevka.",
+    "oauth_storage_quota_default": "Privzeta kvota za shranjevanje (GiB)",
+    "oauth_storage_quota_default_description": "Kvota v GiB, ki se uporabi, ko ni predložen noben zahtevek (vnesite 0 za neomejeno kvoto).",
+    "offline_paths": "Poti brez povezave",
+    "offline_paths_description": "Ti rezultati so morda posledica ročnega brisanja datotek, ki niso del zunanje knjižnice.",
+    "password_enable_description": "Prijava z e-pošto in geslom",
+    "password_settings": "Prijava z geslom",
+    "password_settings_description": "Upravljajte nastavitve prijave z geslom",
+    "paths_validated_successfully": "Vse poti so bile uspešno potrjene",
+    "person_cleanup_job": "Čiščenje osebe",
+    "quota_size_gib": "Velikost kvote (GiB)",
+    "refreshing_all_libraries": "Osveževanje vseh knjižnic",
+    "registration": "Administratorska registracija",
+    "registration_description": "Ker ste prvi uporabnik v sistemu, boste dodeljeni kot skrbnik in ste odgovorni za skrbniška opravila, dodatne uporabnike pa boste ustvarili sami.",
+    "repair_all": "Popravi vse",
+    "repair_matched_items": "Ujemanje {count, plural, one {# predmet} two {# predmeta} few {# predmeti} other {# predmetov}}",
+    "repaired_items": "Popravljeno {count, plural, one {# predmet} two {# predmeta} few {# predmeti} other {# predmetov}}",
+    "require_password_change_on_login": "Od uporabnika zahtevajte spremembo gesla ob prvi prijavi",
+    "reset_settings_to_default": "Ponastavi nastavitve na privzete",
+    "reset_settings_to_recent_saved": "Ponastavite nastavitve na nedavno shranjene nastavitve",
+    "scanning_library": "Pregledovanje knjižnice",
+    "search_jobs": "Iskalna opravila...",
+    "send_welcome_email": "Pošlji pozdravno e-pošto",
+    "server_external_domain_settings": "Zunanja domena",
+    "server_external_domain_settings_description": "Domena za javne skupne povezave, vključno s http(s)://",
+    "server_public_users": "Javni uporabniki",
+    "server_public_users_description": "Vsi uporabniki (ime in e-pošta) so navedeni pri dodajanju uporabnika v albume v skupni rabi. Ko je onemogočen, bo seznam uporabnikov na voljo samo skrbniškim uporabnikom.",
+    "server_settings": "Nastavitve strežnika",
+    "server_settings_description": "Upravljanje nastavitev strežnika",
+    "server_welcome_message": "Pozdravno sporočilo",
+    "server_welcome_message_description": "Sporočilo, ki se prikaže na strani za prijavo.",
+    "sidecar_job": "Stranski metapodatki",
+    "sidecar_job_description": "Odkrijte ali sinhronizirajte stranske metapodatke iz datotečnega sistema",
+    "slideshow_duration_description": "Število sekund za prikaz posamezne slike",
+    "smart_search_job_description": "Izvedite strojno učenje na sredstvih za podporo pametnega iskanja",
+    "storage_template_date_time_description": "Časovni žig ustvarjanja sredstva se uporablja za informacije o datumu in času",
+    "storage_template_date_time_sample": "Vzorec časa {date}",
+    "storage_template_enable_description": "Omogoči mehanizem predloge za shranjevanje",
+    "storage_template_hash_verification_enabled": "Preverjanje zgoščevanja je omogočeno",
+    "storage_template_hash_verification_enabled_description": "Omogoči preverjanje zgoščene vrednosti, tega ne onemogočite, razen če niste prepričani o posledicah",
+    "storage_template_migration": "Selitev predloge za shranjevanje",
+    "storage_template_migration_description": "Uporabi trenutno <link>{template}</link> za predhodno naložena sredstva",
+    "storage_template_migration_info": "Spremembe predloge bodo veljale samo za nova sredstva. Če želite retroaktivno uporabiti predlogo za predhodno naložena sredstva, zaženite <link>{job}</link>.",
+    "storage_template_migration_job": "Opravilo selitve predloge za shranjevanje",
+    "storage_template_more_details": "Za več podrobnosti o tej funkciji si oglejte <template-link>Predlogo za shranjevanje</template-link> in njene <implications-link>posledice</implications-link>",
+    "storage_template_onboarding_description": "Ko je omogočena, bo ta funkcija samodejno organizirala datoteke na podlagi uporabniško določene predloge. Zaradi težav s stabilnostjo je bila funkcija privzeto izklopljena. Za več informacij si oglejte <link>dokumentacijo</link>.",
+    "storage_template_path_length": "Približna omejitev dolžine poti: <b>{length, number}</b>/{limit, number}",
+    "storage_template_settings": "Predloga za shranjevanje",
+    "storage_template_settings_description": "Upravljajte strukturo map in ime datoteke sredstva za nalaganje",
+    "storage_template_user_label": "<code>{label}</code> je uporabniška oznaka za shranjevanje",
+    "system_settings": "Sistemske nastavitve",
+    "tag_cleanup_job": "Čiščenje oznak",
+    "template_email_available_tags": "V svoji predlogi lahko uporabite naslednje spremenljivke: {tags}",
+    "template_email_if_empty": "Če je predloga prazna, bo uporabljena privzeta e-pošta.",
+    "template_email_invite_album": "Predloga povabila v album",
+    "template_email_preview": "Predogled",
+    "template_email_settings": "E-poštne predloge",
+    "template_email_settings_description": "Upravljajte predloge e-poštnih obvestil po meri",
+    "template_email_update_album": "Predloga posodobitve albuma",
+    "template_email_welcome": "Predloga pozdravnega e-poštnega sporočila",
+    "template_settings": "Predloge obvestil",
+    "template_settings_description": "Upravljajte predloge po meri za obvestila.",
+    "theme_custom_css_settings": "CSS po meri",
+    "theme_custom_css_settings_description": "Kaskadni slogovni listi (CSS) omogočajo prilagajanje oblikovanja Immicha.",
+    "theme_settings": "Nastavitve teme",
+    "theme_settings_description": "Upravljanje prilagajanja spletnega vmesnika Immich",
+    "these_files_matched_by_checksum": "Te datoteke se ujemajo z njihovimi kontrolnimi vsotami",
+    "thumbnail_generation_job": "Ustvarite sličice",
+    "thumbnail_generation_job_description": "Ustvari velike, majhne in zamegljene sličice za vsako sredstvo ter sličice za vsako osebo",
+    "transcoding_acceleration_api": "API za pospeševanje",
+    "transcoding_acceleration_api_description": "API, ki bo sodeloval z vašo napravo za pospešitev prekodiranja. Ta nastavitev je 'po najboljših močeh': v primeru napake se bo vrnila k programskemu prekodiranju. VP9 lahko deluje ali ne deluje, odvisno od vaše strojne opreme.",
+    "transcoding_acceleration_nvenc": "NVENC (zahteva NVIDIA GPE)",
+    "transcoding_acceleration_qsv": "Hitra sinhronizacija (zahteva procesor Intel 7. generacije ali novejši)",
+    "transcoding_acceleration_rkmpp": "RKMPP (samo na Rockchip SOC)",
+    "transcoding_acceleration_vaapi": "VAAPI",
+    "transcoding_accepted_audio_codecs": "Sprejeti zvočni kodeki",
+    "transcoding_accepted_audio_codecs_description": "Izberite, katerih zvočnih kodekov ni treba prekodirati. Uporablja se samo za določene politike prekodiranja.",
+    "transcoding_accepted_containers": "Sprejeti zabojniki",
+    "transcoding_accepted_containers_description": "Izberite, katerih formatov zabojnika ni treba ponovno muksirati v MP4. Uporablja se samo za določene politike prekodiranja.",
+    "transcoding_accepted_video_codecs": "Podprti video kodeki",
+    "transcoding_accepted_video_codecs_description": "Izberite, katerih video kodekov ni treba prekodirati. Uporablja se samo za določene politike prekodiranja.",
+    "transcoding_advanced_options_description": "Možnosti večini uporabnikov ne bi bilo treba spreminjati",
+    "transcoding_audio_codec": "Avdio kodek",
+    "transcoding_audio_codec_description": "Opus je najbolj kakovostna možnost, vendar ima slabšo združljivost s starimi napravami ali programsko opremo.",
+    "transcoding_bitrate_description": "Videoposnetki, ki presegajo največjo bitno hitrost ali niso v sprejemljivem formatu",
+    "transcoding_codecs_learn_more": "Če želite izvedeti več o tukaj uporabljeni terminologiji, glejte dokumentacijo FFmpeg za <h264-link>kodek H.264</h264-link>, <hevc-link>kodek HEVC</hevc-link> in <vp9-link>VP9 kodek</vp9-link>.",
+    "transcoding_constant_quality_mode": "Način stalne kakovosti",
+    "transcoding_constant_quality_mode_description": "ICQ je boljši od CQP, vendar nekatere naprave za pospeševanje strojne opreme ne podpirajo tega načina. Če nastavite to možnost, bo pri uporabi kodiranja na podlagi kakovosti izbran izbran način. NVENC ga ignorira, ker ne podpira ICQ.",
+    "transcoding_constant_rate_factor": "Faktor konstantne stopnje (-crf)",
+    "transcoding_constant_rate_factor_description": "Raven kakovosti videa. Tipične vrednosti so 23 za H.264, 28 za HEVC, 31 za VP9 in 35 za AV1. Nižje je boljše, vendar ustvarja večje datoteke.",
+    "transcoding_disabled_description": "Ne prekodirajte nobenih videoposnetkov, lahko prekine predvajanje na nekaterih odjemalcih",
+    "transcoding_hardware_acceleration": "Strojno pospeševanje",
+    "transcoding_hardware_acceleration_description": "Eksperimentalno; veliko hitreje, vendar bo imel slabšo kakovost pri isti bitni hitrosti",
+    "transcoding_hardware_decoding": "Strojno dekodiranje",
+    "transcoding_hardware_decoding_setting_description": "Omogoča pospeševanje od konca do konca namesto samo pospeševanja kodiranja. Morda ne bo delovalo na vseh videoposnetkih.",
+    "transcoding_hevc_codec": "Kodek HEVC",
+    "transcoding_max_b_frames": "Največji B-okvirji",
+    "transcoding_max_b_frames_description": "Višje vrednosti izboljšajo učinkovitost stiskanja, vendar upočasnijo kodiranje. Morda ni združljivo s strojnim pospeševanjem na starejših napravah. 0 onemogoči okvirje B, medtem ko -1 samodejno nastavi to vrednost.",
+    "transcoding_max_bitrate": "Največja bitna hitrost",
+    "transcoding_max_bitrate_description": "Z nastavitvijo največje bitne hitrosti so lahko velikosti datotek bolj predvidljive ob manjši ceni kakovosti. Pri 720p so tipične vrednosti 2600k za VP9 ali HEVC ali 4500k za H.264. Onemogočeno, če je nastavljeno na 0.",
+    "transcoding_max_keyframe_interval": "Največji interval ključnih sličic",
+    "transcoding_max_keyframe_interval_description": "Nastavi največjo razdaljo med ključnimi slikami. Nižje vrednosti poslabšajo učinkovitost stiskanja, vendar izboljšajo čas iskanja in lahko izboljšajo kakovost prizorov s hitrim gibanjem. 0 samodejno nastavi to vrednost.",
+    "transcoding_optimal_description": "Videoposnetki, ki so višji od ciljne ločljivosti ali niso v sprejemljivem formatu",
+    "transcoding_preferred_hardware_device": "Prednostna strojna naprava",
+    "transcoding_preferred_hardware_device_description": "Velja samo za VAAPI in QSV. Nastavi dri vozlišče, ki se uporablja za strojno prekodiranje.",
+    "transcoding_preset_preset": "Prednastavitev (-preset)",
+    "transcoding_preset_preset_description": "Hitrost stiskanja. Počasnejše prednastavitve ustvarijo manjše datoteke in povečajo kakovost pri ciljanju na določeno bitno hitrost. VP9 ignorira hitrosti nad 'hitreje'.",
+    "transcoding_reference_frames": "Referenčni okvirji",
+    "transcoding_reference_frames_description": "Število okvirjev, na katere se sklicujete pri stiskanju danega okvira. Višje vrednosti izboljšajo učinkovitost stiskanja, vendar upočasnijo kodiranje. 0 samodejno nastavi to vrednost.",
+    "transcoding_required_description": "Samo videoposnetki, ki niso v sprejemljivi obliki",
+    "transcoding_settings": "Nastavitve video transkodiranja",
+    "transcoding_settings_description": "Upravljajte podatke o ločljivosti in kodiranju video datotek",
+    "transcoding_target_resolution": "Ciljna ločljivost",
+    "transcoding_target_resolution_description": "Višje ločljivosti lahko ohranijo več podrobnosti, vendar kodiranje traja dlje, imajo večje velikosti datotek in lahko zmanjšajo odzivnost aplikacije.",
+    "transcoding_temporal_aq": "Časovni AQ",
+    "transcoding_temporal_aq_description": "Velja samo za NVENC. Poveča kakovost prizorov z veliko podrobnosti in malo gibanja. Morda ni združljiv s starejšimi napravami.",
+    "transcoding_threads": "Niti",
+    "transcoding_threads_description": "Višje vrednosti vodijo do hitrejšega kodiranja, vendar pustijo manj prostora strežniku za obdelavo drugih nalog, ko je aktiven. Ta vrednost ne sme biti večja od števila jeder procesorja. Maksimira uporabo, če je nastavljeno na 0.",
+    "transcoding_tone_mapping": "Tonska preslikava",
+    "transcoding_tone_mapping_description": "Poskuša ohraniti videz videoposnetkov HDR pri pretvorbi v SDR. Vsak algoritem naredi različne kompromise glede barve, podrobnosti in svetlosti. Hable ohrani podrobnosti, Mobius ohrani barvo, Reinhard pa svetlost.",
+    "transcoding_transcode_policy": "Politika prekodiranja",
+    "transcoding_transcode_policy_description": "Pravilnik o tem, kdaj je treba videoposnetek prekodirati. Videoposnetki HDR bodo vedno prekodirani (razen če je transkodiranje onemogočeno).",
+    "transcoding_two_pass_encoding": "Dvohodno kodiranje",
+    "transcoding_two_pass_encoding_setting_description": "Prekodirajte v dveh prehodih za ustvarjanje bolje kodiranih videoposnetkov. Ko je omogočena največja bitna hitrost (ki je potrebna za delovanje s H.264 in HEVC), ta način uporablja obseg bitne hitrosti, ki temelji na največji bitni hitrosti, in ignorira CRF. Za VP9 je mogoče uporabiti CRF, če je največja bitna hitrost onemogočena.",
+    "transcoding_video_codec": "Video kodek",
+    "transcoding_video_codec_description": "VP9 ima visoko učinkovitost in spletno združljivost, vendar traja dalj časa za prekodiranje. HEVC deluje podobno, vendar ima slabšo spletno združljivost. H.264 je široko združljiv in se hitro prekodira, vendar ustvarja veliko večje datoteke. AV1 je najučinkovitejši kodek, vendar nima podpore na starejših napravah.",
+    "trash_enabled_description": "Omogoči funkcije smetnjaka",
+    "trash_number_of_days": "Število dni",
+    "trash_number_of_days_description": "Število dni za shranjevanje sredstev v smetnjaku, preden jih trajno odstranite",
+    "trash_settings": "Nastavitve smetnjaka",
+    "trash_settings_description": "Upravljanje nastavitev smetnjaka",
+    "untracked_files": "Nesledene datoteke",
+    "untracked_files_description": "Tem datotekam aplikacija ne sledi. Lahko so posledica neuspelih premikov, prekinjenih nalaganj ali zaostalih zaradi hrošča",
+    "user_cleanup_job": "Čiščenje uporabnika",
+    "user_delete_delay": "Račun in sredstva <b>{user}</b> bodo načrtovani za trajno brisanje čez {delay, plural, one {# day} other {# days}}.",
+    "user_delete_delay_settings": "Zamakni izbris",
+    "user_delete_delay_settings_description": "Število dni po odstranitvi za trajno brisanje uporabnikovega računa in sredstev. Opravilo za brisanje uporabnikov se izvaja ob polnoči, da se preveri, ali so uporabniki pripravljeni na izbris. Spremembe te nastavitve bodo ovrednotene pri naslednji izvedbi.",
+    "user_delete_immediately": "Račun in sredstva uporabnika <b>{user}</b> bodo v čakalni vrsti za trajno brisanje <b>takoj</b>.",
+    "user_delete_immediately_checkbox": "Uporabnika in sredstva postavite v čakalno vrsto za takojšnje brisanje",
+    "user_management": "Upravljanje uporabnikov",
+    "user_password_has_been_reset": "Geslo uporabnika je bilo ponastavljeno:",
+    "user_password_reset_description": "Uporabniku posredujte začasno geslo in ga obvestite, da bo moral ob naslednji prijavi spremeniti geslo.",
+    "user_restore_description": "Račun <b>{user}</b> bo obnovljen.",
+    "user_restore_scheduled_removal": "Obnovi uporabnika – načrtovana odstranitev na {date, date, long}",
+    "user_settings": "Uporabniške nastavitve",
+    "user_settings_description": "Upravljanje uporabniških nastavitev",
+    "user_successfully_removed": "Uporabnik {email} je bil uspešno odstranjen.",
+    "version_check_enabled_description": "Omogoči preverjanje različice",
+    "version_check_implications": "Funkcija preverjanja različic se opira na občasno komunikacijo z github.com",
+    "version_check_settings": "Preverjanje različice",
+    "version_check_settings_description": "Omogoči/onemogoči obvestilo o novi različici",
+    "video_conversion_job": "Prekodiranje videoposnetkov",
+    "video_conversion_job_description": "Prekodirajte videoposnetke za večjo združljivost z brskalniki in napravami"
+  },
+  "admin_email": "Skrbniška e-pošta",
+  "admin_password": "Skrbniško geslo",
+  "administration": "Administracija",
+  "advanced": "Napredno",
+  "age_months": "Starost {months, plural, one {# month} other {# months}}",
+  "age_year_months": "Starost 1 leto, {months, plural, one {# month} other {# months}}",
+  "age_years": "{years, plural, other {starost #}}",
+  "album_added": "Album dodan",
+  "album_added_notification_setting_description": "Prejmite e-poštno obvestilo, ko ste dodani v album v skupni rabi",
+  "album_cover_updated": "Naslovnica albuma posodobljena",
+  "album_delete_confirmation": "Ali ste prepričani, da želite izbrisati album {album}?",
+  "album_delete_confirmation_description": "Če je ta album v skupni rabi, drugi uporabniki ne bodo mogli več dostopati do njega.",
+  "album_info_updated": "Podatki o albumu posodobljeni",
+  "album_leave": "Zapusti album?",
+  "album_leave_confirmation": "Ali ste prepričani, da želite zapustiti {album}?",
+  "album_name": "Ime albuma",
+  "album_options": "Možnosti albuma",
+  "album_remove_user": "Odstrani uporabnika?",
+  "album_remove_user_confirmation": "Ali ste prepričani, da želite odstraniti {user}?",
+  "album_share_no_users": "Videti je, da ste ta album dali v skupno rabo z vsemi uporabniki ali pa nimate nobenega uporabnika, s katerim bi ga lahko delili.",
+  "album_updated": "Album posodobljen",
+  "album_updated_setting_description": "Prejmite e-poštno obvestilo, ko ima album v skupni rabi nova sredstva",
+  "album_user_left": "Zapustil {album}",
+  "album_user_removed": "Odstranjen {user}",
+  "album_with_link_access": "Omogočite vsem s povezavo ogled fotografij in ljudi v tem albumu.",
+  "albums": "Albumi",
+  "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albumi}}",
+  "all": "Vse",
+  "all_albums": "Vsi albumi",
+  "all_people": "Vsi ljudje",
+  "all_videos": "Vsi videi",
+  "allow_dark_mode": "Dovoli temni način",
+  "allow_edits": "Dovoli urejanja",
+  "allow_public_user_to_download": "Dovoli javnemu uporabniku prenos",
+  "allow_public_user_to_upload": "Dovolite javnemu uporabniku nalaganje",
+  "anti_clockwise": "V nasprotni smeri urnega kazalca",
+  "api_key": "API ključ",
+  "api_key_description": "Ta vrednost bo prikazana samo enkrat. Ne pozabite jo kopirati, preden zaprete okno.",
+  "api_key_empty": "Ime ključa API ne sme biti prazno",
+  "api_keys": "API ključi",
+  "app_settings": "Nastavitve aplikacije",
+  "appears_in": "Pojavi se v",
+  "archive": "Arhiv",
+  "archive_or_unarchive_photo": "Arhivirajte ali odstranite fotografijo iz arhiva",
+  "archive_size": "Velikost arhiva",
+  "archive_size_description": "Konfigurirajte velikost arhiva za prenose (v GiB)",
+  "archived_count": "{count, plural, other {arhivirano #}}",
+  "are_these_the_same_person": "Ali je to ista oseba?",
+  "are_you_sure_to_do_this": "Ste prepričani, da želite to narediti?",
+  "asset_added_to_album": "Dodano v album",
+  "asset_adding_to_album": "Dodajanje v album ...",
+  "asset_description_updated": "Opis sredstva je posodobljen",
+  "asset_filename_is_offline": "Sredstvo {filename} je brez povezave",
+  "asset_has_unassigned_faces": "Sredstvo ima nedodeljene obraze",
+  "asset_hashing": "Zgoščevanje ...",
+  "asset_offline": "Sredstvo brez povezave",
+  "asset_offline_description": "Tega zunanjega sredstva ni več mogoče najti na disku. Za pomoč kontaktirajte Immich skrbnika.",
+  "asset_skipped": "Preskočeno",
+  "asset_skipped_in_trash": "V smetnjak",
+  "asset_uploaded": "Naloženo",
+  "asset_uploading": "Nalaganje ...",
+  "assets": "Sredstva",
+  "assets_added_count": "Dodano{count, plural, one {# sredstvo} other {# sredstev}}",
+  "assets_added_to_album_count": "Dodano{count, plural, one {# sredstvo} other {# sredstev}} v album",
+  "assets_added_to_name_count": "Dodano {count, plural, one {# sredstvo} other {# sredstev}} v {hasName, select, true {<b>{name}</b>} other {new album}}",
+  "assets_count": "{count, plural, one {# sredstvo} other {# sredstev}}",
+  "assets_moved_to_trash_count": "Premaknjeno {count, plural, one {# sredstev} other {# sredstev}} v smetnjak",
+  "assets_permanently_deleted_count": "Trajno izbrisano {count, plural, one {# sredstvo} other {# sredstev}}",
+  "assets_removed_count": "Odstranjeno {count, plural, one {# sredstvo} other {# sredstev}}",
+  "assets_restore_confirmation": "Ali ste prepričani, da želite obnoviti vsa sredstva, ki ste jih odstranili? Tega dejanja ne morete razveljaviti! Upoštevajte, da sredstev brez povezave ni mogoče obnoviti na ta način.",
+  "assets_restored_count": "Obnovljeno {count, plural, one {# sredstvo} other {# sredstev}}",
+  "assets_trashed_count": "V smetnjak {count, plural, one {# sredstvo} other {# sredstev}}",
+  "assets_were_part_of_album_count": "{count, plural, one {sredstvo je} other {sredstev je}} že del albuma",
+  "authorized_devices": "Pooblaščene naprave",
+  "back": "Nazaj",
+  "back_close_deselect": "Nazaj, zaprite ali prekličite izbiro",
+  "backward": "Nazaj",
+  "birthdate_saved": "Datum rojstva je uspešno shranjen",
+  "birthdate_set_description": "Datum rojstva se uporablja za izračun starosti te osebe v času fotografije.",
+  "blurred_background": "Zamegljeno ozadje",
+  "bugs_and_feature_requests": "Napake in zahteve po funkcijah",
+  "build": "Različica",
+  "build_image": "Različica slike",
+  "bulk_delete_duplicates_confirmation": "Ali ste prepričani, da želite množično izbrisati {count, plural, one {# dvojnik} other {# dvojnikov}}? S tem boste ohranili največje sredstvo vsake skupine in trajno izbrisali vse druge dvojnike. Tega dejanja ne morete razveljaviti!",
+  "bulk_keep_duplicates_confirmation": "Ali ste prepričani, da želite obdržati {count, plural, one {# dvojnik} other {# dvojnikov}}? S tem boste razrešili vse podvojene skupine, ne da bi karkoli izbrisali.",
+  "bulk_trash_duplicates_confirmation": "Ali ste prepričani, da želite množično vreči v smetnjak {count, plural, one {# dvojnik} other {# dvojnikov}}? S tem boste obdržali največje sredstvo vsake skupine in odstranili vse druge dvojnike.",
+  "buy": "Kupi Immich",
+  "camera": "Kamera",
+  "camera_brand": "Znamka kamere",
+  "camera_model": "Model kamere",
+  "cancel": "Prekliči",
+  "cancel_search": "Prekliči iskanje",
+  "cannot_merge_people": "Oseb ni mogoče združiti",
+  "cannot_undo_this_action": "Tega dejanja ne morete razveljaviti!",
+  "cannot_update_the_description": "Opisa ni mogoče posodobiti",
+  "change_date": "Spremeni datum",
+  "change_expiration_time": "Spremeni čas poteka",
+  "change_location": "Spremeni lokacijo",
+  "change_name": "Spremeni ime",
+  "change_name_successfully": "Sprememba imena uspešna",
+  "change_password": "Zamenjaj geslo",
+  "change_password_description": "To je bodisi prvič, da se vpisujete v sistem ali pa je bila podana zahteva za spremembo vašega gesla. Spodaj vnesite novo geslo.",
+  "change_your_password": "Spremenite geslo",
+  "changed_visibility_successfully": "Uspešno spremenjena vidnost",
+  "check_all": "Označite vse",
+  "check_logs": "Preverite dnevnike",
+  "choose_matching_people_to_merge": "Izberite ujemajoče se osebe za združitev",
+  "city": "Mesto",
+  "clear": "Počisti",
+  "clear_all": "Počisti vse",
+  "clear_all_recent_searches": "Počisti vsa nedavna iskanja",
+  "clear_message": "Počisti sporočilo",
+  "clear_value": "Počisti vrednost",
+  "clockwise": "V smeri urinega kazalca",
+  "close": "Zapri",
+  "collapse": "Strni",
+  "collapse_all": "Strni vse",
+  "color": "Barva",
+  "color_theme": "Barva teme",
+  "comment_deleted": "Komentar izbrisan",
+  "comment_options": "Možnosti komentiranja",
+  "comments_and_likes": "Komentarji in všečki",
+  "comments_are_disabled": "Komentarji so onemogočeni",
+  "confirm": "Potrdi",
+  "confirm_admin_password": "Potrdite skrbniško geslo",
+  "confirm_delete_shared_link": "Ali ste prepričani, da želite izbrisati to skupno povezavo?",
+  "confirm_keep_this_delete_others": "Vsa druga sredstva v skladu bodo izbrisana, razen tega sredstva. Ste prepričani, da želite nadaljevati?",
+  "confirm_password": "Potrdi geslo",
+  "contain": "Vsebuje",
+  "context": "Kontekst",
+  "continue": "Nadaljuj",
+  "copied_image_to_clipboard": "Slika kopirana v odložišče.",
+  "copied_to_clipboard": "Kopirano v odložišče!",
+  "copy_error": "Napaka pri kopiranju",
+  "copy_file_path": "Kopiraj pot datoteke",
+  "copy_image": "Kopiraj sliko",
+  "copy_link": "Kopiraj povezavo",
+  "copy_link_to_clipboard": "Kopiraj povezavo v odložišče",
+  "copy_password": "Kopiraj geslo",
+  "copy_to_clipboard": "Kopiraj v odložišče",
+  "country": "Država",
+  "cover": "Prekrij",
+  "covers": "Prekrivanja",
+  "create": "Ustvari",
+  "create_album": "Ustvari album",
+  "create_library": "Ustvari knjižnico",
+  "create_link": "Ustvari povezavo",
+  "create_link_to_share": "Ustvari povezavo za skupno rabo",
+  "create_link_to_share_description": "Omogoči vsem s povezavo ogled izbranih fotografij",
+  "create_new_person": "Ustvari novo osebo",
+  "create_new_person_hint": "Dodeli izbrana sredstva novi osebi",
+  "create_new_user": "Ustvari novega uporabnika",
+  "create_tag": "Ustvari oznako",
+  "create_tag_description": "Ustvarite novo oznako. Za ugnezdene oznake vnesite celotno pot oznake, vključno s poševnicami.",
+  "create_user": "Ustvari uporabnika",
+  "created": "Ustvarjeno",
+  "current_device": "Trenutna naprava",
+  "custom_locale": "Jezik po meri",
+  "custom_locale_description": "Oblikujte datume in številke glede na jezik in regijo",
+  "dark": "Temno",
+  "date_after": "Datum po",
+  "date_and_time": "Datum in ura",
+  "date_before": "Datum pred",
+  "date_of_birth_saved": "Datum rojstva je uspešno shranjen",
+  "date_range": "Časovno obdobje",
+  "day": "Dan",
+  "deduplicate_all": "Odstrani vse podvojene",
+  "default_locale": "Privzeti jezik",
+  "default_locale_description": "Oblikujte datume in številke glede na lokalne nastavitve brskalnika",
+  "delete": "Izbriši",
+  "delete_album": "Izbriši album",
+  "delete_api_key_prompt": "Ali ste prepričani, da želite izbrisati ta API ključ?",
+  "delete_duplicates_confirmation": "Ali ste prepričani, da želite trajno izbrisati te dvojnike?",
+  "delete_key": "Izbriši ključ",
+  "delete_library": "Izbriši knjižnico",
+  "delete_link": "Izbriši povezavo",
+  "delete_others": "Izbriši ostale",
+  "delete_shared_link": "Izbriši povezavo skupne rabe",
+  "delete_tag": "Izbriši oznako",
+  "delete_tag_confirmation_prompt": "Ali ste prepričani, da želite izbrisati oznako {tagName}?",
+  "delete_user": "Izbriši uporabnika",
+  "deleted_shared_link": "Izbrisana skupna povezava",
+  "deletes_missing_assets": "Izbriše sredstva, ki manjkajo na disku",
+  "description": "Opis",
+  "details": "PODROBNOSTI",
+  "direction": "Usmeritev",
+  "disabled": "Onemogočeno",
+  "disallow_edits": "Onemogoči urejanje",
+  "discord": "Discord",
+  "discover": "Odkrij",
+  "dismiss_all_errors": "Opusti vse napake",
+  "dismiss_error": "Opusti napako",
+  "display_options": "Možnosti prikaza",
+  "display_order": "Vrstni red prikaza",
+  "display_original_photos": "Prikaži izvirne fotografije",
+  "display_original_photos_setting_description": "Pri ogledu sredstva raje prikažite izvirno fotografijo kot sličice, če je izvirno sredstvo združljivo s spletom. To lahko povzroči počasnejše hitrosti prikaza fotografij.",
+  "do_not_show_again": "Ne pokaži več tega sporočila",
+  "documentation": "Dokumentacija",
+  "done": "Končano",
+  "download": "Prenesi",
+  "download_include_embedded_motion_videos": "Vdelani videoposnetki",
+  "download_include_embedded_motion_videos_description": "Videoposnetke, vdelane v fotografije gibanja, vključite kot ločeno datoteko",
+  "download_settings": "Prenos",
+  "download_settings_description": "Upravljajte nastavitve, povezane s prenosom sredstev",
+  "downloading": "Prenašanje",
+  "downloading_asset_filename": "Prenašanje sredstva {filename}",
+  "drop_files_to_upload": "Spustite datoteke kamor koli, da jih naložite",
+  "duplicates": "Dvojniki",
+  "duplicates_description": "Razrešite vsako skupino tako, da navedete, kateri so dvojniki, če obstajajo",
+  "duration": "Trajanje",
+  "edit": "Uredi",
+  "edit_album": "Uredi album",
+  "edit_avatar": "Uredi avatar",
+  "edit_date": "Uredi datum",
+  "edit_date_and_time": "Uredi datum in uro",
+  "edit_exclusion_pattern": "Uredi vzorec izključitve",
+  "edit_faces": "Uredi obraze",
+  "edit_import_path": "Uredi uvozno pot",
+  "edit_import_paths": "Uredi uvozne poti",
+  "edit_key": "Uredi ključ",
+  "edit_link": "Uredi povezavo",
+  "edit_location": "Uredi lokacijo",
+  "edit_name": "Uredi ime",
+  "edit_people": "Uredi osebe",
+  "edit_tag": "Uredi oznako",
+  "edit_title": "Uredi naslov",
+  "edit_user": "Uredi uporabnika",
+  "edited": "Urejeno",
+  "editor": "Urejevalnik",
+  "editor_close_without_save_prompt": "Spremembe ne bodo shranjene",
+  "editor_close_without_save_title": "Zapri urejevalnik?",
+  "editor_crop_tool_h2_aspect_ratios": "Razmerja stranic",
+  "editor_crop_tool_h2_rotation": "Vrtenje",
+  "email": "E-pošta",
+  "empty_trash": "Izprazni smeti",
+  "empty_trash_confirmation": "Ste prepričani, da želite izprazniti smetnjak? S tem boste iz Immicha trajno odstranili vsa sredstva v smetnjaku.\nTega dejanja ne morete razveljaviti!",
+  "enable": "Omogoči",
+  "enabled": "Omogočeno",
+  "end_date": "Končni datum",
+  "error": "Napaka",
+  "error_loading_image": "Napaka pri nalaganju slike",
+  "error_title": "Napaka - nekaj je šlo narobe",
+  "errors": {
+    "cannot_navigate_next_asset": "Ni mogoče krmariti do naslednjega sredstva",
+    "cannot_navigate_previous_asset": "Ni mogoče krmariti na prejšnje sredstvo",
+    "cant_apply_changes": "Sprememb ni mogoče uporabiti",
+    "cant_change_activity": "Ni mogoče {enabled, select, true {disable} other {enable}} dejavnosti",
+    "cant_change_asset_favorite": "Ni možno spremeniti priljubljeno za sredstvo",
+    "cant_change_metadata_assets_count": "Ni mogoče spremeniti metapodatkov za {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}",
+    "cant_get_faces": "Ne morem dobiti obrazov",
+    "cant_get_number_of_comments": "Ni mogoče pridobiti števila komentarjev",
+    "cant_search_people": "Ni mogoče iskati ljudi",
+    "cant_search_places": "Ne morem iskati mest",
+    "cleared_jobs": "Počiščena opravila za: {job}",
+    "error_adding_assets_to_album": "Napaka pri dodajanju sredstev v album",
+    "error_adding_users_to_album": "Napaka pri dodajanju uporabnikov v album",
+    "error_deleting_shared_user": "Napaka pri brisanju uporabnika v skupni rabi",
+    "error_downloading": "Napaka pri prenosu datoteke {filename}",
+    "error_hiding_buy_button": "Napaka pri skrivanju gumba za nakup",
+    "error_removing_assets_from_album": "Napaka pri odstranjevanju sredstev iz albuma, preverite konzolo za več podrobnosti",
+    "error_selecting_all_assets": "Napaka pri izbiri vseh sredstev",
+    "exclusion_pattern_already_exists": "Ta vzorec izključitve že obstaja.",
+    "failed_job_command": "Ukaz {command} ni uspel za opravilo: {job}",
+    "failed_to_create_album": "Albuma ni bilo mogoče ustvariti",
+    "failed_to_create_shared_link": "Povezave v skupni rabi ni bilo mogoče ustvariti",
+    "failed_to_edit_shared_link": "Povezave v skupni rabi ni bilo mogoče urediti",
+    "failed_to_get_people": "Oseb ni bilo mogoče pridobiti",
+    "failed_to_keep_this_delete_others": "Tega sredstva ni bilo mogoče obdržati in izbrisati ostalih sredstev",
+    "failed_to_load_asset": "Sredstva ni bilo mogoče naložiti",
+    "failed_to_load_assets": "Sredstev ni bilo mogoče naložiti",
+    "failed_to_load_people": "Oseb ni bilo mogoče naložiti",
+    "failed_to_remove_product_key": "Ključa izdelka ni bilo mogoče odstraniti",
+    "failed_to_stack_assets": "Zlaganje sredstev ni uspelo",
+    "failed_to_unstack_assets": "Sredstev ni bilo mogoče razložiti",
+    "import_path_already_exists": "Ta uvozna pot že obstaja.",
+    "incorrect_email_or_password": "Napačen e-poštni naslov ali geslo",
+    "paths_validation_failed": "{paths, plural, one {# pot} other {# poti}} ni bilo uspešno preverjeno",
+    "profile_picture_transparent_pixels": "Profilne slike ne smejo imeti prosojnih slikovnih pik. Povečajte in/ali premaknite sliko.",
+    "quota_higher_than_disk_size": "Nastavili ste kvoto, ki je višja od velikosti diska",
+    "repair_unable_to_check_items": "Ni mogoče preveriti {count, select, one {predmeta} other {predmetov}}",
+    "unable_to_add_album_users": "Uporabnikov ni mogoče dodati v album",
+    "unable_to_add_assets_to_shared_link": "Povezavi v skupni rabi ni mogoče dodati sredstev",
+    "unable_to_add_comment": "Ni mogoče dodati komentarja",
+    "unable_to_add_exclusion_pattern": "Vzorca izključitve ni mogoče dodati",
+    "unable_to_add_import_path": "Uvozne poti ni mogoče dodati",
+    "unable_to_add_partners": "Partnerjev ni mogoče dodati",
+    "unable_to_add_remove_archive": "Ni mogoče {archived, select, true {odstraniti sredstva iz} other {ter dodati sredstvo v}} archive",
+    "unable_to_add_remove_favorites": "Ni mogoče {favorite, select, true {dodati sredstva v} other {ter ga odstraniti iz}} priljubljenih",
+    "unable_to_archive_unarchive": "Ni mogoče {archived, select, true {arhivirano} other {nearhivirano}}",
+    "unable_to_change_album_user_role": "Ni mogoče spremeniti vloge uporabnika albuma",
+    "unable_to_change_date": "Datuma ni mogoče spremeniti",
+    "unable_to_change_favorite": "Ni mogoče spremeniti priljubljenega za sredstvo",
+    "unable_to_change_location": "Lokacije ni mogoče spremeniti",
+    "unable_to_change_password": "Gesla ni mogoče spremeniti",
+    "unable_to_change_visibility": "Ni mogoče spremeniti vidnosti za {count, plural, one {# osebo} other {# oseb}}",
+    "unable_to_complete_oauth_login": "Prijave OAuth ni mogoče dokončati",
+    "unable_to_connect": "Ni mogoče vzpostaviti povezave",
+    "unable_to_connect_to_server": "Ni mogoče vzpostaviti povezave s strežnikom",
+    "unable_to_copy_to_clipboard": "Ni mogoče kopirati v odložišče, preverite, ali dostopate do strani prek https",
+    "unable_to_create_admin_account": "Ni mogoče ustvariti skrbniškega računa",
+    "unable_to_create_api_key": "Ni mogoče ustvariti novega API ključa",
+    "unable_to_create_library": "Ni mogoče ustvariti knjižnice",
+    "unable_to_create_user": "Uporabnika ni mogoče ustvariti",
+    "unable_to_delete_album": "Albuma ni mogoče izbrisati",
+    "unable_to_delete_asset": "Sredstva ni mogoče izbrisati",
+    "unable_to_delete_assets": "Napaka pri brisanju sredstev",
+    "unable_to_delete_exclusion_pattern": "Vzorca izključitve ni mogoče izbrisati",
+    "unable_to_delete_import_path": "Uvozne poti ni mogoče izbrisati",
+    "unable_to_delete_shared_link": "Povezave v skupni rabi ni mogoče izbrisati",
+    "unable_to_delete_user": "Uporabnika ni mogoče izbrisati",
+    "unable_to_download_files": "Ni mogoče prenesti datotek",
+    "unable_to_edit_exclusion_pattern": "Vzorca izključitve ni mogoče urediti",
+    "unable_to_edit_import_path": "Uvozne poti ni mogoče urediti",
+    "unable_to_empty_trash": "Smetnjaka ni mogoče izprazniti",
+    "unable_to_enter_fullscreen": "Celozaslonski način ni mogoč",
+    "unable_to_exit_fullscreen": "Ni mogoče zapreti celozaslonskega načina",
+    "unable_to_get_comments_number": "Ni mogoče pridobiti števila komentarjev",
+    "unable_to_get_shared_link": "Povezave v skupni rabi ni bilo mogoče pridobiti",
+    "unable_to_hide_person": "Osebe ni mogoče skriti",
+    "unable_to_link_motion_video": "Ni mogoče povezati videa gibanja",
+    "unable_to_link_oauth_account": "Računa OAuth ni mogoče povezati",
+    "unable_to_load_album": "Albuma ni mogoče naložiti",
+    "unable_to_load_asset_activity": "Dejavnosti sredstva ni mogoče naložiti",
+    "unable_to_load_items": "Elementov ni mogoče naložiti",
+    "unable_to_load_liked_status": "Ni mogoče naložiti statusa všečka",
+    "unable_to_log_out_all_devices": "Ni mogoče odjaviti vseh naprav",
+    "unable_to_log_out_device": "Naprave ni mogoče odjaviti",
+    "unable_to_login_with_oauth": "Prijava z OAuth ni mogoča",
+    "unable_to_play_video": "Videoposnetka ni mogoče predvajati",
+    "unable_to_reassign_assets_existing_person": "Ni mogoče dodeliti sredstev {name, select, null {obstoječi osebi} other {{name}}}",
+    "unable_to_reassign_assets_new_person": "Ponovna dodelitev sredstev novi osebi ni možna",
+    "unable_to_refresh_user": "Uporabnika ni mogoče osvežiti",
+    "unable_to_remove_album_users": "Uporabnikov ni mogoče odstraniti iz albuma",
+    "unable_to_remove_api_key": "Ključa API ni mogoče odstraniti",
+    "unable_to_remove_assets_from_shared_link": "Ni mogoče odstraniti sredstev iz skupne povezave",
+    "unable_to_remove_deleted_assets": "Datotek brez povezave ni mogoče odstraniti",
+    "unable_to_remove_library": "Knjižnice ni mogoče odstraniti",
+    "unable_to_remove_partner": "Partnerja ni mogoče odstraniti",
+    "unable_to_remove_reaction": "Reakcije ni mogoče odstraniti",
+    "unable_to_repair_items": "Elementov ni mogoče popraviti",
+    "unable_to_reset_password": "Gesla ni mogoče ponastaviti",
+    "unable_to_resolve_duplicate": "Dvojnika ni mogoče razrešiti",
+    "unable_to_restore_assets": "Sredstev ni mogoče obnoviti",
+    "unable_to_restore_trash": "Smetnjaka ni mogoče obnoviti",
+    "unable_to_restore_user": "Uporabnika ni mogoče obnoviti",
+    "unable_to_save_album": "Albuma ni mogoče shraniti",
+    "unable_to_save_api_key": "Ključa API ni mogoče shraniti",
+    "unable_to_save_date_of_birth": "Datuma rojstva ni mogoče shraniti",
+    "unable_to_save_name": "Imena ni mogoče shraniti",
+    "unable_to_save_profile": "Profila ni mogoče shraniti",
+    "unable_to_save_settings": "Nastavitev ni mogoče shraniti",
+    "unable_to_scan_libraries": "Ni mogoče pregledati knjižnic",
+    "unable_to_scan_library": "Knjižnice ni mogoče pregledati",
+    "unable_to_set_feature_photo": "Ni mogoče nastaviti glavne fotografije",
+    "unable_to_set_profile_picture": "Profilne slike ni mogoče nastaviti",
+    "unable_to_submit_job": "Naloga ni mogoče oddati",
+    "unable_to_trash_asset": "Sredstva ni mogoče odstraniti v smetnjak",
+    "unable_to_unlink_account": "Povezave računa ni mogoče prekiniti",
+    "unable_to_unlink_motion_video": "Ni mogoče prekiniti povezave z videoposnetkom gibanja",
+    "unable_to_update_album_cover": "Naslovnice albuma ni mogoče posodobiti",
+    "unable_to_update_album_info": "Podatkov o albumu ni mogoče posodobiti",
+    "unable_to_update_library": "Knjižnice ni mogoče posodobiti",
+    "unable_to_update_location": "Lokacije ni mogoče posodobiti",
+    "unable_to_update_settings": "Nastavitev ni mogoče posodobiti",
+    "unable_to_update_timeline_display_status": "Ni mogoče posodobiti stanja prikaza časovnice",
+    "unable_to_update_user": "Uporabnika ni mogoče posodobiti",
+    "unable_to_upload_file": "Datoteke ni mogoče naložiti"
+  },
+  "exif": "Exif",
+  "exit_slideshow": "Zapustite diaprojekcijo",
+  "expand_all": "Razširi vse",
+  "expire_after": "Poteče čez",
+  "expired": "Poteklo",
+  "expires_date": "Poteče {date}",
+  "explore": "Razišči",
+  "explorer": "Raziskovalec",
+  "export": "Izvoz",
+  "export_as_json": "Izvozi kot JSON",
+  "extension": "Razširitev",
+  "external": "Zunanji",
+  "external_libraries": "Zunanje knjižnice",
+  "face_unassigned": "Nedodeljen",
+  "failed_to_load_assets": "Sredstev ni bilo mogoče naložiti",
+  "favorite": "Priljubljen",
+  "favorite_or_unfavorite_photo": "Priljubljena ali nepriljubljena fotografija",
+  "favorites": "Priljubljene",
+  "feature_photo_updated": "Funkcijska fotografija je posodobljena",
+  "features": "Funkcije",
+  "features_setting_description": "Upravljaj funkcije aplikacije",
+  "file_name": "Ime datoteke",
+  "file_name_or_extension": "Ime ali končnica datoteke",
+  "filename": "Ime datoteke",
+  "filetype": "Vrsta datoteke",
+  "filter_people": "Filtriraj ljudi",
+  "find_them_fast": "Z iskanjem jih hitro poiščite po imenu",
+  "fix_incorrect_match": "Popravi napačno ujemanje",
+  "folders": "Mape",
+  "folders_feature_description": "Brskanje po pogledu mape za fotografije in videoposnetke v datotečnem sistemu",
+  "forward": "Naprej",
+  "general": "Splošno",
+  "get_help": "Poiščite pomoč",
+  "getting_started": "Začetek",
+  "go_back": "Pojdi nazaj",
+  "go_to_search": "Pojdi na iskanje",
+  "group_albums_by": "Združi albume po ...",
+  "group_no": "Brez združevanja",
+  "group_owner": "Združi po lastniku",
+  "group_year": "Združi po letih",
+  "has_quota": "Ima kvoto",
+  "hi_user": "Živijo {name} ({email})",
+  "hide_all_people": "Skrij vse ljudi",
+  "hide_gallery": "Skrij galerijo",
+  "hide_named_person": "Skrij osebo {name}",
+  "hide_password": "Skrij geslo",
+  "hide_person": "Skrij osebo",
+  "hide_unnamed_people": "Skrij osebe brez imen",
+  "host": "Gostitelj",
+  "hour": "Ura",
+  "image": "Slika",
+  "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} zajet {date}",
+  "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} zajet z osebo {person1} dne {date}",
+  "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} zajet z osebo {person1} in osebo {person2} dne {date}",
+  "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} zajet z osebami {person1}, {person2}, in {person3} dne {date}",
+  "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} zajet z osebami {person1}, {person2} in ostalimi {additionalCount, number} osebami dne {date}",
+  "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} zajet/a v/na {city}, {country} dne {date}",
+  "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} zajet/a/e v/na {city}, {country} s/z {person1} dne {date}",
+  "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}}zajet/a/e/i v/na {city}, {country} s/z {person1} in {person2} dne {date}",
+  "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} zajet/a/e/i v/na {city}, {country} s/z {person1}, {person2} in {person3} dne {date}",
+  "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} zajet/a/e/i v/na {city}, {country} s/z {person1}, {person2} on ostalimi {additionalCount, number} osebami dne {date}",
+  "immich_logo": "Immich logo",
+  "immich_web_interface": "Immich spletni vmesnik",
+  "import_from_json": "Uvoz iz JSON",
+  "import_path": "Pot uvoza",
+  "in_albums": "V {count, plural, one {# album} other {# albumov}}",
+  "in_archive": "V arhiv",
+  "include_archived": "Vključi arhivirane",
+  "include_shared_albums": "Vključite skupne albume",
+  "include_shared_partner_assets": "Vključite partnerjeva skupna sredstva",
+  "individual_share": "Samostojna delitev",
+  "info": "Info",
+  "interval": {
+    "day_at_onepm": "Vsak dan ob 13h",
+    "hours": "Vsakih {hours, plural, one {uro} other {{hours, number} ur/e}}",
+    "night_at_midnight": "Vsak večer ob polnoči",
+    "night_at_twoam": "Vsako noč ob 2h"
+  },
+  "invite_people": "Povabi ljudi",
+  "invite_to_album": "Povabi v album",
+  "items_count": "{count, plural, one {# predmet} other {# predmetov}}",
+  "jobs": "Opravila",
+  "keep": "Obdrži",
+  "keep_all": "Obdrži vse",
+  "keep_this_delete_others": "Obdrži to, izbriši ostalo",
+  "kept_this_deleted_others": "Obdrži to sredstvo in izbriši {count, plural, one {# sredstvo} other {# sredstev}}",
+  "keyboard_shortcuts": "Bližnjice na tipkovnici",
+  "language": "Jezik",
+  "language_setting_description": "Izberite želeni jezik",
+  "last_seen": "Nazadnje viden",
+  "latest_version": "Najnovejša različica",
+  "latitude": "Zemljepisna širina",
+  "leave": "Zapusti",
+  "let_others_respond": "Naj drugi odgovorijo",
+  "level": "Raven",
+  "library": "Knjižnica",
+  "library_options": "Možnosti knjižnice",
+  "light": "Svetlo",
+  "like_deleted": "Všeček izbrisan",
+  "link_motion_video": "Povezava videa gibanja",
+  "link_options": "Možnosti povezave",
+  "link_to_oauth": "Povezava do OAuth",
+  "linked_oauth_account": "Povezan račun OAuth",
+  "list": "Seznam",
+  "loading": "Nalaganje",
+  "loading_search_results_failed": "Nalaganje rezultatov iskanja ni uspelo",
+  "log_out": "Odjava",
+  "log_out_all_devices": "Odjava vseh naprav",
+  "logged_out_all_devices": "Odjavljene so vse naprave",
+  "logged_out_device": "Odjavljena naprava",
+  "login": "Prijava",
+  "login_has_been_disabled": "Prijava je bila onemogočena.",
+  "logout_all_device_confirmation": "Ali ste prepričani, da želite odjaviti vse naprave?",
+  "logout_this_device_confirmation": "Ali ste prepričani, da se želite odjaviti iz te naprave?",
+  "longitude": "Zemljepisna dolžina",
+  "look": "Izgled",
+  "loop_videos": "Zanka videoposnetkov",
+  "loop_videos_description": "Omogočite samodejno ponavljanje videoposnetka v pregledovalniku podrobnosti.",
+  "main_branch_warning": "Uporabljate razvojno različico; močno priporočamo uporabo izdajne različice!",
+  "make": "Izdelava",
+  "manage_shared_links": "Upravljanje povezav v skupni rabi",
+  "manage_sharing_with_partners": "Upravljajte skupno rabo s partnerji",
+  "manage_the_app_settings": "Upravljajte nastavitve aplikacije",
+  "manage_your_account": "Upravljajte svoj račun",
+  "manage_your_api_keys": "Upravljajte svoje API ključe",
+  "manage_your_devices": "Upravljajte svoje prijavljene naprave",
+  "manage_your_oauth_connection": "Upravljajte svojo OAuth povezavo",
+  "map": "Zemljevid",
+  "map_marker_for_images": "Oznaka zemljevida za slike, posnete v {city}, {country}",
+  "map_marker_with_image": "Oznaka zemljevida s sliko",
+  "map_settings": "Nastavitve zemljevida",
+  "matches": "Ujemanja",
+  "media_type": "Vrsta medija",
+  "memories": "Spomini",
+  "memories_setting_description": "Upravljajte s tem, kar vidite v svojih spominih",
+  "memory": "Spomin",
+  "memory_lane_title": "Spominski trak {title}",
+  "menu": "Meni",
+  "merge": "Združi",
+  "merge_people": "Združi osebe",
+  "merge_people_limit": "Hkrati lahko združite največ 5 obrazov",
+  "merge_people_prompt": "Ali želite združiti te osebe? To dejanje je nepovratno.",
+  "merge_people_successfully": "Združitev ljudi uspešno",
+  "merged_people_count": "Združeno {count, plural, one {# oseba} two {# osebi} few {# osebe} other {# oseb}}",
+  "minimize": "Zmanjšaj",
+  "minute": "minuta",
+  "missing": "manjka",
+  "model": "Model",
+  "month": "Mesec",
+  "more": "Več",
+  "moved_to_trash": "Premaknjeno v smetnjak",
+  "my_albums": "Moji albumi",
+  "name": "Ime",
+  "name_or_nickname": "Ime ali vzdevek",
+  "never": "nikoli",
+  "new_album": "Nov album",
+  "new_api_key": "Nov API ključ",
+  "new_password": "Novo geslo",
+  "new_person": "Nova oseba",
+  "new_user_created": "Nov uporabnik ustvarjen",
+  "new_version_available": "NA VOLJO JE NOVA RAZLIČICA",
+  "newest_first": "Najprej najnovejše",
+  "next": "Naslednji",
+  "next_memory": "Naslednji spomin",
+  "no": "Ne",
+  "no_albums_message": "Ustvarite album za organiziranje svojih fotografij in videoposnetkov",
+  "no_albums_with_name_yet": "Videti je, da še nimate nobenega albuma s tem imenom.",
+  "no_albums_yet": "Videti je, da še nimate nobenega albuma.",
+  "no_archived_assets_message": "Arhivirajte fotografije in videoposnetke, da jih skrijete v pogledu fotografij",
+  "no_assets_message": "KLIKNITE ZA NALOŽITEV SVOJE PRVE FOTOGRAFIJE",
+  "no_duplicates_found": "Najden ni bil noben dvojnik.",
+  "no_exif_info_available": "Podatki o exif niso na voljo",
+  "no_explore_results_message": "Naložite več fotografij, da raziščete svojo zbirko.",
+  "no_favorites_message": "Dodajte priljubljene, da hitreje najdete svoje najboljše slike in videoposnetke",
+  "no_libraries_message": "Ustvarite zunanjo knjižnico za ogled svojih fotografij in videoposnetkov",
+  "no_name": "Brez imena",
+  "no_places": "Ni krajev",
+  "no_results": "Brez rezultatov",
+  "no_results_description": "Poskusite s sinonimom ali bolj splošno ključno besedo",
+  "no_shared_albums_message": "Ustvarite album za skupno rabo fotografij in videoposnetkov z osebami v vašem omrežju",
+  "not_in_any_album": "Ni v nobenem albumu",
+  "note_apply_storage_label_to_previously_uploaded assets": "Opomba: Če želite oznako za shranjevanje uporabiti za predhodno naložena sredstva, zaženite",
+  "note_unlimited_quota": "Opomba: Vnesite 0 za neomejeno kvoto",
+  "notes": "Opombe",
+  "notification_toggle_setting_description": "Omogoči e-poštna obvestila",
+  "notifications": "Obvestila",
+  "notifications_setting_description": "Upravljanje obvestil",
+  "oauth": "OAuth",
+  "official_immich_resources": "Immich uradni viri",
+  "offline": "Brez povezave",
+  "offline_paths": "Poti brez povezave",
+  "offline_paths_description": "Ti rezultati so morda posledica ročnega brisanja datotek, ki niso del zunanje knjižnice.",
+  "ok": "V redu",
+  "oldest_first": "Najprej najstarejši",
+  "onboarding": "Vkrcanje",
+  "onboarding_privacy_description": "Naslednje (neobvezne) funkcije so odvisne od zunanjih storitev in jih je mogoče kadar koli onemogočiti v skrbniških nastavitvah.",
+  "onboarding_theme_description": "Izberite barvno temo za svoj primer. To lahko pozneje spremenite v nastavitvah.",
+  "onboarding_welcome_description": "Nastavimo vaš primerek z nekaj običajnimi nastavitvami.",
+  "onboarding_welcome_user": "Pozdravljen/a, {user}",
+  "online": "Povezano",
+  "only_favorites": "Samo priljubljene",
+  "open_in_map_view": "Odpri v pogledu zemljevida",
+  "open_in_openstreetmap": "Odpri v OpenStreetMap",
+  "open_the_search_filters": "Odpri iskalne filtre",
+  "options": "Možnosti",
+  "or": "ali",
+  "organize_your_library": "Organiziraj svojo knjižnico",
+  "original": "izvirnik",
+  "other": "drugo",
+  "other_devices": "Druge naprave",
+  "other_variables": "Druge spremenljivke",
+  "owned": "V lasti",
+  "owner": "Lastnik",
+  "partner": "Partner",
+  "partner_can_access": "{partner} ima dostop",
+  "partner_can_access_assets": "Vse vaše fotografije in videoposnetki, razen tistih v arhivu in izbrisanih",
+  "partner_can_access_location": "Lokacija, kjer so bile vaše fotografije posnete",
+  "partner_sharing": "Skupna raba s partnerjem",
+  "partners": "Partnerji",
+  "password": "Geslo",
+  "password_does_not_match": "Geslo se ne ujema",
+  "password_required": "Zahtevano je geslo",
+  "password_reset_success": "Ponastavitev gesla je uspela",
+  "past_durations": {
+    "days": "Pretek-el/-lih {days, plural, one {dan} other {# dni}}",
+    "hours": "Pretek-lo/-lih {hours, plural, one {uro} other {# ur}}",
+    "years": "Pretek-lo/-lih {years, plural, one {leto} other {# let}}"
+  },
+  "path": "Pot",
+  "pattern": "Vzorec",
+  "pause": "Premor",
+  "pause_memories": "Zaustavi spomine",
+  "paused": "Zaustavljeno",
+  "pending": "V teku",
+  "people": "Osebe",
+  "people_edits_count": "Urejen-a/-ih {count, plural, one {# oseba} other {# oseb}}",
+  "people_feature_description": "Brskanje po fotografijah in videoposnetkih, razvrščenih po osebah",
+  "people_sidebar_description": "Prikažite povezavo do Ljudje v stranski vrstici",
+  "permanent_deletion_warning": "Opozorilo o trajnem izbrisu",
+  "permanent_deletion_warning_setting_description": "Pokaži opozorilo pri trajnem brisanju sredstev",
+  "permanently_delete": "Trajno izbriši",
+  "permanently_delete_assets_count": "Trajno izbriši {count, plural, one {sredstvo} other {sredstev}}",
+  "permanently_delete_assets_prompt": "Ali ste prepričani, da želite trajno izbrisati {count, plural, one {to sredstvo?} other {ta <b>#</b> sredstva?}} S tem boste odstranili tudi {count, plural, one {tega od teh} other {telih iz telih}} album- /-ov.",
+  "permanently_deleted_asset": "Trajno izbrisano sredstvo",
+  "permanently_deleted_assets_count": "Trajno izbrisano {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}",
+  "person": "Oseba",
+  "person_hidden": "{name}{hidden, select, true { (skrita)} other {}}",
+  "photo_shared_all_users": "Videti je, da ste svoje fotografije delili z vsemi uporabniki ali pa nimate nobenega uporabnika, s katerim bi jih delili.",
+  "photos": "Slike",
+  "photos_and_videos": "Fotografije & videi",
+  "photos_count": "{count, plural, one {{count, number} slika} other {{count, number} slik}}",
+  "photos_from_previous_years": "Fotografije iz prejšnjih let",
+  "pick_a_location": "Izberi lokacijo",
+  "place": "Lokacija",
+  "places": "Lokacije",
+  "play": "Predvajaj",
+  "play_memories": "Predvajaj spomine",
+  "play_motion_photo": "Predvajaj premikajočo fotografijo",
+  "play_or_pause_video": "Predvajaj ali zaustavi video",
+  "port": "Vrata",
+  "preset": "Prednastavitev",
+  "preview": "Predogled",
+  "previous": "Prejšnj-a/-i",
+  "previous_memory": "Prejšnji spomin",
+  "previous_or_next_photo": "Prejšnja ali naslednja fotografija",
+  "primary": "Primarni",
+  "privacy": "Zasebnost",
+  "profile_image_of_user": "Profilna slika uporabnika {user}",
+  "profile_picture_set": "Profilna slika nastavljena.",
+  "public_album": "Javni album",
+  "public_share": "Javno deljenje",
+  "purchase_account_info": "Podpornik",
+  "purchase_activated_subtitle": "Hvala, ker podpirate Immich in odprtokodno programsko opremo",
+  "purchase_activated_time": "Aktivirano {date, date}",
+  "purchase_activated_title": "Vaš ključ je bil uspešno aktiviran",
+  "purchase_button_activate": "Aktiviraj",
+  "purchase_button_buy": "Kupi",
+  "purchase_button_buy_immich": "Kupi Immich",
+  "purchase_button_never_show_again": "Nikoli več ne pokaži",
+  "purchase_button_reminder": "Opomni me čez 30 dni",
+  "purchase_button_remove_key": "Odstrani ključ",
+  "purchase_button_select": "Izberi",
+  "purchase_failed_activation": "Aktivacija ni uspela! Preverite svojo e-pošto za pravilen ključ izdelka!",
+  "purchase_individual_description_1": "Za posameznika",
+  "purchase_individual_description_2": "Status podpornika",
+  "purchase_individual_title": "Posamezno",
+  "purchase_input_suggestion": "Ali imate ključ izdelka? Spodaj vnesite ključ",
+  "purchase_license_subtitle": "Kupite Immich, da podprete nadaljnji razvoj storitve",
+  "purchase_lifetime_description": "Doživljenjski nakup",
+  "purchase_option_title": "MOŽNOSTI NAKUPA",
+  "purchase_panel_info_1": "Gradnja Immicha zahteva veliko časa in truda, zato imamo zaposlene inženirje, ki delajo na tem, da bi bil čim boljši. Naše poslanstvo je, da odprtokodna programska oprema in etične poslovne prakse, ki bi postale trajnostni vir dohodka za razvijalce in ustvarjanje ekosistema, ki spoštuje zasebnost z resničnimi alternativami izkoriščevalskim storitvam v oblaku.",
+  "purchase_panel_info_2": "Ker se zavezujemo, da ne bomo dodajali plačilnih storitev, vam ta nakup ne bo omogočil nobenih dodatnih funkcij v Immichu. Zanašamo se na uporabnike, kot ste vi, ki podpirajo nenehni razvoj Immicha.",
+  "purchase_panel_title": "Podpri projekt",
+  "purchase_per_server": "Na strežnik",
+  "purchase_per_user": "Na uporabnika",
+  "purchase_remove_product_key": "Odstrani ključ izdelka",
+  "purchase_remove_product_key_prompt": "Ali ste prepričani, da želite odstraniti ključ izdelka?",
+  "purchase_remove_server_product_key": "Odstranite ključ izdelka strežnika",
+  "purchase_remove_server_product_key_prompt": "Ali ste prepričani, da želite odstraniti ključ izdelka strežnika?",
+  "purchase_server_description_1": "Za celoten strežnik",
+  "purchase_server_description_2": "Status podpornika",
+  "purchase_server_title": "Strežnik",
+  "purchase_settings_server_activated": "Ključ izdelka strežnika upravlja skrbnik",
+  "rating": "Ocena z zvezdicami",
+  "rating_clear": "Počisti oceno",
+  "rating_count": "{count, plural, one {# zvezdica} two {# zvezdici} few {# zvezdice} other {# zvezdic}}",
+  "rating_description": "Prikažite oceno EXIF v informacijski plošči",
+  "reaction_options": "Možnosti reakcije",
+  "read_changelog": "Preberi dnevnik sprememb",
+  "reassign": "Prerazporedi",
+  "reassigned_assets_to_existing_person": "Ponovno dodeljeno {count, plural, one {# sredstvo} other {# sredstev}} za {name, select, null {an existing person} other {{name}}}",
+  "reassigned_assets_to_new_person": "Ponovno dodeljeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} za novo osebo",
+  "reassing_hint": "Dodeli izbrana sredstva obstoječi osebi",
+  "recent": "Nedavno",
+  "recent-albums": "Zadnji albumi",
+  "recent_searches": "Nedavna iskanja",
+  "refresh": "Osveži",
+  "refresh_encoded_videos": "Osveži kodirane videoposnetke",
+  "refresh_faces": "Osveži obraze",
+  "refresh_metadata": "Osveži metapodatke",
+  "refresh_thumbnails": "Osveži sličice",
+  "refreshed": "Osveženo",
+  "refreshes_every_file": "Ponovno prebere vse obstoječe in nove datoteke",
+  "refreshing_encoded_video": "Osveževanje kodiranega videa",
+  "refreshing_faces": "Osveževanje obrazev",
+  "refreshing_metadata": "Osveževanje metapodatkov",
+  "regenerating_thumbnails": "Obnavljanje sličic",
+  "remove": "Odstrani",
+  "remove_assets_album_confirmation": "Ali ste prepričani, da želite odstraniti {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} iz albuma?",
+  "remove_assets_shared_link_confirmation": "Ali ste prepričani, da želite odstraniti {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} iz te skupne povezave?",
+  "remove_assets_title": "Odstrani sredstva?",
+  "remove_custom_date_range": "Odstrani časovno obdobje po meri",
+  "remove_deleted_assets": "Odstrani izbrisana sredstva",
+  "remove_from_album": "Odstrani iz albuma",
+  "remove_from_favorites": "Odstrani iz priljubljenih",
+  "remove_from_shared_link": "Odstrani iz skupne povezave",
+  "remove_url": "Odstrani URL",
+  "remove_user": "Odstrani uporabnika",
+  "removed_api_key": "Odstranjen ključ API-ja: {name}",
+  "removed_from_archive": "Odstranjeno iz arhiva",
+  "removed_from_favorites": "Odstranjeno iz priljubljenih",
+  "removed_from_favorites_count": "{count, plural, other {odstranen/ih #}} iz priljubljenih",
+  "removed_tagged_assets": "Odstranjena oznaka iz {count, plural, one {# sredstva} other {# sredstev}}",
+  "rename": "Preimenuj",
+  "repair": "Popravi",
+  "repair_no_results_message": "Datoteke, ki jim ni sledi in manjkajo, bodo prikazane tukaj",
+  "replace_with_upload": "Zamenjaj z nalaganjem",
+  "repository": "Repozitorij",
+  "require_password": "Zahtevaj geslo",
+  "require_user_to_change_password_on_first_login": "Od uporabnika zahtevajte spremembo gesla ob prvi prijavi",
+  "reset": "Ponastavi",
+  "reset_password": "Ponastavi geslo",
+  "reset_people_visibility": "Ponastavi vidnost ljudi",
+  "reset_to_default": "Ponastavi na privzeto",
+  "resolve_duplicates": "Razreši dvojnike",
+  "resolved_all_duplicates": "Razrešeni vsi dvojniki",
+  "restore": "Obnovi",
+  "restore_all": "Obnovi vse",
+  "restore_user": "Obnovi uporabnika",
+  "restored_asset": "Obnovljeno sredstvo",
+  "resume": "Nadaljuj",
+  "retry_upload": "Poskusite znova naložiti",
+  "review_duplicates": "Pregled dvojnikov",
+  "role": "Dovoljenje",
+  "role_editor": "Urejevalec",
+  "role_viewer": "Gledalec",
+  "save": "Shrani",
+  "saved_api_key": "Shranjen API ključ",
+  "saved_profile": "Shranjen profil",
+  "saved_settings": "Shranjene nastavitve",
+  "say_something": "Reci kaj",
+  "scan_all_libraries": "Preglej vse knjižnice",
+  "scan_library": "Pregled",
+  "scan_settings": "Nastavitve pregleda",
+  "scanning_for_album": "Iskanje albuma...",
+  "search": "Iskanje",
+  "search_albums": "Iskanje albumov",
+  "search_by_context": "Iskanje po kontekstu",
+  "search_by_filename": "Iskanje po imenu datoteke ali priponi",
+  "search_by_filename_example": "na primer IMG_1234.JPG ali PNG",
+  "search_camera_make": "Iskanje proizvajalca kamere...",
+  "search_camera_model": "Išči model kamere...",
+  "search_city": "Iskanje mesta...",
+  "search_country": "Iskanje države...",
+  "search_for_existing_person": "Iskanje obstoječe osebe",
+  "search_no_people": "Brez oseb",
+  "search_no_people_named": "Ni oseb z imenom \"{name}\"",
+  "search_options": "Možnosti iskanja",
+  "search_people": "Iskanje oseb",
+  "search_places": "Iskanje krajev",
+  "search_settings": "Nastavitve iskanja",
+  "search_state": "Iskanje dežele...",
+  "search_tags": "Iskanje oznak...",
+  "search_timezone": "Iskanje časovnega pasu...",
+  "search_type": "Vrsta iskanja",
+  "search_your_photos": "Poišči svoje fotografije",
+  "searching_locales": "Iskanje krajev...",
+  "second": "Sekunda",
+  "see_all_people": "Oglejte si vse ljudi",
+  "select_album_cover": "Izberi naslovnico albuma",
+  "select_all": "Izberi vse",
+  "select_all_duplicates": "Izberi vse dvojnike",
+  "select_avatar_color": "Izberi barvo avatarja",
+  "select_face": "Izberi obraz",
+  "select_featured_photo": "Izberi predstavljeno fotografijo",
+  "select_from_computer": "Izberi iz računalnika",
+  "select_keep_all": "Izberi obdrži vse",
+  "select_library_owner": "Izberi lastnika knjižnice",
+  "select_new_face": "Izberi nov obraz",
+  "select_photos": "Izberi fotografije",
+  "select_trash_all": "Izberi vse v smetnjak",
+  "selected": "Izbrano",
+  "selected_count": "{count, plural, other {# izbranih}}",
+  "send_message": "Pošlji sporočilo",
+  "send_welcome_email": "Pošlji pozdravno e-pošto",
+  "server_offline": "Strežnik nima povezave",
+  "server_online": "Strežnik povezan",
+  "server_stats": "Statistika strežnika",
+  "server_version": "Različica strežnika",
+  "set": "Nastavi",
+  "set_as_album_cover": "Nastavi kot naslovnico albuma",
+  "set_as_profile_picture": "Nastavi kot profilno sliko",
+  "set_date_of_birth": "Nastavi datum rojstva",
+  "set_profile_picture": "Nastavi profilno sliko",
+  "set_slideshow_to_fullscreen": "Nastavi diaprojekcijo na celozaslonski način",
+  "settings": "Nastavitve",
+  "settings_saved": "Nastavitve shranjene",
+  "share": "Deli",
+  "shared": "V skupni rabi",
+  "shared_by": "Skupna raba s/z",
+  "shared_by_user": "Skupna raba s/z {user}",
+  "shared_by_you": "Deliš",
+  "shared_from_partner": "Fotografije od {partner}",
+  "shared_link_options": "Možnosti skupne povezave",
+  "shared_links": "Povezave v skupni rabi",
+  "shared_photos_and_videos_count": "{assetCount, plural, other {# deljenih fotografij & videoposnetkov.}}",
+  "shared_with_partner": "V skupni rabi s/z {partner}",
+  "sharing": "Skupna raba",
+  "sharing_enter_password": "Za ogled te strani vnesi geslo.",
+  "sharing_sidebar_description": "Prikažite povezavo do skupne rabe v stranski vrstici",
+  "shift_to_permanent_delete": "pritisni ⇧ za trajno brisanje sredstva",
+  "show_album_options": "Prikaži možnosti albuma",
+  "show_albums": "Prikaži albume",
+  "show_all_people": "Prikaži vse osebe",
+  "show_and_hide_people": "Prikaži & skrij osebe",
+  "show_file_location": "Pokaži lokacijo datoteke",
+  "show_gallery": "Prikaži galerijo",
+  "show_hidden_people": "Prikaži skrite osebe",
+  "show_in_timeline": "Pokaži na časovnici",
+  "show_in_timeline_setting_description": "Prikaži fotografije in videoposnetke tega uporabnika na svoji časovnici",
+  "show_keyboard_shortcuts": "Prikaži bližnjice na tipkovnici",
+  "show_metadata": "Pokaži metapodatke",
+  "show_or_hide_info": "Pokaži ali skrij podatke",
+  "show_password": "Prikaži geslo",
+  "show_person_options": "Prikaži možnosti osebe",
+  "show_progress_bar": "Prikaži vrstico napredka",
+  "show_search_options": "Prikaži možnosti iskanja",
+  "show_slideshow_transition": "Prikaži prehod diaprojekcije",
+  "show_supporter_badge": "Značka podpornika",
+  "show_supporter_badge_description": "Prikaži značko podpornika",
+  "shuffle": "Naključno",
+  "sidebar": "Stranska vrstica",
+  "sidebar_display_description": "Prikaži povezavo do pogleda v stranski vrstici",
+  "sign_out": "Odjavi se",
+  "sign_up": "Prijavi se",
+  "size": "Velikost",
+  "skip_to_content": "Preskoči na vsebino",
+  "skip_to_folders": "Preskoči na mape",
+  "skip_to_tags": "Preskoči na oznake",
+  "slideshow": "Diaprojekcija",
+  "slideshow_settings": "Nastavitve diaprojekcije",
+  "sort_albums_by": "Razvrsti albume po...",
+  "sort_created": "Datum nastanka",
+  "sort_items": "Število predmetov",
+  "sort_modified": "Datum spremembe",
+  "sort_oldest": "Najstarejša fotografija",
+  "sort_recent": "Najnovejša fotografija",
+  "sort_title": "Naslov",
+  "source": "Vir",
+  "stack": "Sklad",
+  "stack_duplicates": "Nabor dvojnikov",
+  "stack_select_one_photo": "Izberite eno glavno fotografijo za nabor",
+  "stack_selected_photos": "Nabor izbranih fotografij",
+  "stacked_assets_count": "Nabor {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}",
+  "stacktrace": "Sled nabora",
+  "start": "Začetek",
+  "start_date": "Datum začetka",
+  "state": "Dežela",
+  "status": "Status",
+  "stop_motion_photo": "Zaustavi gibljivo fotografijo",
+  "stop_photo_sharing": "Želite prenehati deliti svoje fotografije?",
+  "stop_photo_sharing_description": "{partner} ne bo mogel več dostopati do vaših fotografij.",
+  "stop_sharing_photos_with_user": "Prenehaj deliti svoje fotografije s tem uporabnikom",
+  "storage": "Prostor za shranjevanje",
+  "storage_label": "Oznaka za shranjevanje",
+  "storage_usage": "uporabljeno {used} od {available}",
+  "submit": "Predloži",
+  "suggestions": "Predlogi",
+  "sunrise_on_the_beach": "Sončni vzhod na plaži",
+  "support": "Podpora",
+  "support_and_feedback": "Podpora in povratne informacije",
+  "support_third_party_description": "Vašo namestitev Immich je pakirala tretja oseba. Težave, ki jih imate, lahko povzroči ta paket, zato prosimo, da težave najprej izpostavite njim, tako da uporabite spodnje povezave.",
+  "swap_merge_direction": "Zamenjaj smer združevanja",
+  "sync": "Sinhronizacija",
+  "tag": "Oznaka",
+  "tag_assets": "Označi sredstva",
+  "tag_created": "Ustvarjena oznaka: {tag}",
+  "tag_feature_description": "Brskanje po fotografijah in videoposnetkih, razvrščenih po temah logičnih oznak",
+  "tag_not_found_question": "Ne najdete oznake? <link>Ustvarite novo oznako.</link>",
+  "tag_updated": "Posodobljena oznaka: {tag}",
+  "tagged_assets": "Označeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}",
+  "tags": "Oznake",
+  "template": "Predloga",
+  "theme": "Tema",
+  "theme_selection": "Izbira teme",
+  "theme_selection_description": "Samodejno nastavi temo na svetlo ali temno glede na sistemske nastavitve brskalnika",
+  "they_will_be_merged_together": "Združeni bodo skupaj",
+  "third_party_resources": "Viri tretjih oseb",
+  "time_based_memories": "Časovni spomini",
+  "timeline": "Časovnica",
+  "timezone": "Časovni pas",
+  "to_archive": "Arhiv",
+  "to_change_password": "Spremeni geslo",
+  "to_favorite": "Priljubljen",
+  "to_login": "Prijava",
+  "to_parent": "Pojdi na prvotno",
+  "to_trash": "Smetnjak",
+  "toggle_settings": "Preklopi na nastavitve",
+  "toggle_theme": "Preklopi na temno temo",
+  "total": "Skupno",
+  "total_usage": "Skupna poraba",
+  "trash": "Smetnjak",
+  "trash_all": "Vse v smetnjak",
+  "trash_count": "Smetnjak {count, number}",
+  "trash_delete_asset": "V smetnjak/izbriši sredstvo",
+  "trash_no_results_message": "Fotografije in videoposnetki, ki so v smetnjaku, bodo prikazani tukaj.",
+  "trashed_items_will_be_permanently_deleted_after": "Elementi v smetnjaku bodo trajno izbrisani po {days, plural, one {# dnevu} two {# dnevih} few {# dnevih} other {# dneh}}.",
+  "type": "Vrsta",
+  "unarchive": "Odstrani iz arhiva",
+  "unarchived_count": "{count, plural, other {nearhiviranih #}}",
+  "unfavorite": "Odznači priljubljeno",
+  "unhide_person": "Prikaži osebo",
+  "unknown": "Neznano",
+  "unknown_year": "Neznano leto",
+  "unlimited": "Neomejeno",
+  "unlink_motion_video": "Prekini povezavo videoposnetka gibanja",
+  "unlink_oauth": "Prekini povezavo OAuth",
+  "unlinked_oauth_account": "Nepovezan račun OAuth",
+  "unnamed_album": "Neimenovan album",
+  "unnamed_album_delete_confirmation": "Ali ste prepričani, da želite izbrisati ta album?",
+  "unnamed_share": "Neimenovana skupna raba",
+  "unsaved_change": "Neshranjena sprememba",
+  "unselect_all": "Odznači vse",
+  "unselect_all_duplicates": "Odznači vse dvojnike",
+  "unstack": "Razklad",
+  "unstacked_assets_count": "Razloži {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}",
+  "untracked_files": "Nesledene datoteke",
+  "untracked_files_decription": "Tem datotekam aplikacija ne sledi. Lahko so posledica neuspelih premikov, prekinjenih ali zaostalih nalaganj zaradi hrošča",
+  "up_next": "Naslednja",
+  "updated_password": "Posodobljeno geslo",
+  "upload": "Naloži",
+  "upload_concurrency": "Sočasnost nalaganja",
+  "upload_errors": "Nalaganje je končano s/z {count, plural, one {# napako} two {# napakama} other {# napakami}}, osvežite stran, da vidite nova sredstva za nalaganje.",
+  "upload_progress": "Preostalo {remaining, number} - Obdelano {processed, number}/{total, number}",
+  "upload_skipped_duplicates": "Preskočeno {count, plural, one {# podvojeno sredstvo} two {# podvojeni sredstvi} few {# podvojena sredstva} other {# podvojenih sredstev}}",
+  "upload_status_duplicates": "Dvojniki",
+  "upload_status_errors": "Napake",
+  "upload_status_uploaded": "Naloženo",
+  "upload_success": "Nalaganje je uspelo, osvežite stran, da vidite nova sredstva za nalaganje.",
+  "url": "URL",
+  "usage": "Uporaba",
+  "use_custom_date_range": "Namesto tega uporabite časovno obdobje po meri",
+  "user": "Uporabnik",
+  "user_id": "ID uporabnika",
+  "user_liked": "{user} je všeč {type, select, photo {ta fotografija} video {ta video} asset {to sredstvo} other {to}}",
+  "user_purchase_settings": "Nakup",
+  "user_purchase_settings_description": "Upravljajte svoj nakup",
+  "user_role_set": "Nastavi {user} kot {role}",
+  "user_usage_detail": "Podrobnosti o uporabi uporabnika",
+  "user_usage_stats": "Statistika uporabe računa",
+  "user_usage_stats_description": "Oglejte si statistiko uporabe računa",
+  "username": "Uporabniško ime",
+  "users": "Uporabniki",
+  "utilities": "Pripomočki",
+  "validate": "Potrdi",
+  "variables": "Spremenljivke",
+  "version": "Različica",
+  "version_announcement_closing": "Tvoj prijatelj, Alex",
+  "version_announcement_message": "Pozdravljeni! Na voljo je nova različica Immich. Vzemite si nekaj časa in preberite <link>opombe ob izdaji</link>, da zagotovite, da so vaše nastavitve posodobljene, da preprečite morebitne napačne konfiguracije, zlasti če uporabljate WatchTower ali kateri koli mehanizem, ki samodejno posodablja vaš primerek Immich.",
+  "version_history": "Zgodovina različic",
+  "version_history_item": "{version} nameščena {date}",
+  "video": "Video",
+  "video_hover_setting": "Predvajaj sličico videoposnetka ob lebdenju",
+  "video_hover_setting_description": "Predvajaj sličico videoposnetka, ko se miška pomakne nad element. Tudi ko je onemogočeno, lahko predvajanje začnete tako, da miškin kazalec premaknete nad ikono za predvajanje.",
+  "videos": "Videoposnetki",
+  "videos_count": "{count, plural, one {# video} two {# videa} few {# videi} other {# videov}}",
+  "view": "Ogled",
+  "view_album": "Ogled albuma",
+  "view_all": "Poglej vse",
+  "view_all_users": "Ogled vseh uporabnikov",
+  "view_in_timeline": "Ogled na časovnici",
+  "view_links": "Ogled povezav",
+  "view_name": "Pogled",
+  "view_next_asset": "Ogled naslednjega sredstva",
+  "view_previous_asset": "Ogled prejšnjega sredstva",
+  "view_stack": "Ogled sklada",
+  "visibility_changed": "Vidnost spremenjena za {count, plural, one {# osebo} two {# osebi} few {# osebe} other {# oseb}}",
+  "waiting": "Čakanje",
+  "warning": "Opozorilo",
+  "week": "Teden",
+  "welcome": "Dobrodošli",
+  "welcome_to_immich": "Dobrodošli v Immich",
+  "year": "Leto",
+  "years_ago": "{years, plural, one {# leto} two {# leti} few {# leta} other {# let}} nazaj",
+  "yes": "Da",
+  "you_dont_have_any_shared_links": "Nimate nobenih skupnih povezav",
+  "zoom_image": "Povečava slike"
+}
diff --git a/web/src/lib/i18n/sr_Cyrl.json b/i18n/sr_Cyrl.json
similarity index 91%
rename from web/src/lib/i18n/sr_Cyrl.json
rename to i18n/sr_Cyrl.json
index 6618aeab1d..e80ea46335 100644
--- a/web/src/lib/i18n/sr_Cyrl.json
+++ b/i18n/sr_Cyrl.json
@@ -1,5 +1,5 @@
 {
-  "about": "О апликацији",
+  "about": "О Апликацији",
   "account": "Профил",
   "account_settings": "Подешавања за Профил",
   "acknowledge": "Потврди",
@@ -23,16 +23,23 @@
   "add_to": "Додај у...",
   "add_to_album": "Додај у албум",
   "add_to_shared_album": "Додај у дељен албум",
+  "add_url": "Додајте URL",
   "added_to_archive": "Додато у архиву",
   "added_to_favorites": "Додато у фаворите",
   "added_to_favorites_count": "Додато {count, number} у фаворите",
   "admin": {
     "add_exclusion_pattern_description": "Додајте обрасце искључења. Кориштење *, ** и ? је подржано. Да бисте игнорисали све датотеке у било ком директоријуму под називом „Рав“, користите „**/Рав/**“. Да бисте игнорисали све датотеке које се завршавају на „.тиф“, користите „**/*.тиф“. Да бисте игнорисали апсолутну путању, користите „/path/to/ignore/**“.",
+    "asset_offline_description": "Ово екстерно библиотечко средство се више не налази на диску и премештено је у смеће. Ако је датотека премештена унутар библиотеке, проверите своју временску линију за ново одговарајуће средство. Да бисте вратили ово средство, уверите се да Иммицх може да приступи доле наведеној путањи датотеке и скенирајте библиотеку.",
     "authentication_settings": "Подешавања за аутентификацију",
     "authentication_settings_description": "Управљајте лозинком, OAuth-om и другим подешавањима аутентификације",
     "authentication_settings_disable_all": "Да ли сте сигурни да желите да oneмогућите све методе пријављивања? Пријава ће бити потпуно oneмогућена.",
     "authentication_settings_reenable": "Да бисте поново омогућили, користите <link>команду сервера</link>.",
     "background_task_job": "Позадински задаци",
+    "backup_database": "Резервна копија базе података",
+    "backup_database_enable_description": "Омогућите резервне копије базе података",
+    "backup_keep_last_amount": "Количина претходних резервних копија за чување",
+    "backup_settings": "Подешавања резервне копије",
+    "backup_settings_description": "Управљајте поставкама резервне копије базе података",
     "check_all": "Провери све",
     "cleared_jobs": "Очишћени послови за {job}",
     "config_set_by_file": "Конфигурацију тренутно поставља конфигурациони фајл",
@@ -41,35 +48,40 @@
     "confirm_email_below": "Да бисте потврдили, унесите \"{email}\" испод",
     "confirm_reprocess_all_faces": "Да ли сте сигурни да желите да поново обрадите сва лица? Ово ће такође обрисати именоване особе.",
     "confirm_user_password_reset": "Да ли сте сигурни да желите да ресетујете лозинку корисника {user}?",
-    "crontab_guru": "Guru servisnih zadataka",
+    "create_job": "Креирајте посао",
+    "cron_expression": "Cron израз (expression)",
+    "cron_expression_description": "Подесите интервал скенирања користећи cron формат. За више информација погледајте нпр. <link>Crontab Guru</link>",
+    "cron_expression_presets": "Предефинисана подешавања Cron израза (expression)",
     "disable_login": "oneмогући пријаву",
-    "disabled": "",
     "duplicate_detection_job_description": "Покрените машинско учење на средствима да бисте открили сличне слике. Ослања се на паметну претрагу",
     "exclusion_pattern_description": "Обрасци изузимања вам омогућавају да игноришете датотеке и фасцикле када скенирате библиотеку. Ово је корисно ако имате фасцикле које садрже датотеке које не желите да увезете, као што су RAW датотеке.",
     "external_library_created_at": "Екстерна библиотека (направљена {date})",
     "external_library_management": "Управљање екстерним библиотекама",
     "face_detection": "Детекција лица",
-    "face_detection_description": "Откривање лица у датотекама помоћу машинског учења. За видео снимке се узима у обзир само сличица. „Све“ (поновно) обрађује све датотеке. „Недостају“ средства у низу која још нису обрађена. Откривена лица ће бити стављена у ред за препознавање лица након што се препознавање лица заврши, групишући их у постојеће или нове људе.",
-    "facial_recognition_job_description": "Група је детектовала лица и додала их постојецим људима. Овај корак се покреће након што је препознавање лица завршено. „Све“ (поновно) групише сва лица. „Недостају“ лица у редовима којима није додељена особа.",
+    "face_detection_description": "Откријте лица у датотекама помоћу машинског учења. За видео снимке се узима у обзир само сличица. „Освежи“ (поновно) обрађује све датотеке. „Ресетовање“ додатно брише све тренутне податке о лицу. „Недостају“ датотеке у реду које још нису обрађене. Откривена лица ће бити стављена у ред за препознавање лица након што се препознавање лица заврши, групишући их у постојеће или нове особе.",
+    "facial_recognition_job_description": "Група је детектовала лица и додала их постојећим људима. Овај корак се покреће након што је препознавање лица завршено. „Ресет“ (поновно) групише сва лица. „Недостају“ лица у редовима којима није додељена особа.",
     "failed_job_command": "Команда {command} није успела за посао {job}",
     "force_delete_user_warning": "УПОЗОРЕЊЕ: Ovo će odmah ukloniti korisnika i sve datoteke. Ovo se ne može opozvati i datoteke se ne mogu oporaviti.",
     "forcing_refresh_library_files": "Принудно освежавање свих датотека библиотеке",
+    "image_format": "Формат",
     "image_format_description": "WebP производи мање датотеке од ЈПЕГ, али се спорије кодира.",
     "image_prefer_embedded_preview": "Преферирајте уграђени преглед",
     "image_prefer_embedded_preview_setting_description": "Користите уграђене прегледе у RAW фотографије као улаз за обраду слике када су доступне. Ово може да произведе прецизније боје за неке слике, али квалитет прегледа зависи од камере и слика може имати више неправилности компресије.",
     "image_prefer_wide_gamut": "Преферирајте широк спектар",
     "image_prefer_wide_gamut_setting_description": "Користите Display П3 за сличице. Ово боље чува живописност слика са широким просторима боја, али слике могу изгледати другачије на старим уређајима са старом верзијом претраживача. сРГБ слике се чувају као сРГБ да би се избегле промене боја.",
-    "image_preview_format": "Преглед формата",
-    "image_preview_resolution": "Преглед резолуције",
-    "image_preview_resolution_description": "Користи се за гледање једне фотографије и за машинско учење. Веће резолуције могу да сачувају више детаља, али им је потребно више времена за кодирање, имају веће величине датотека и могу да смање брзину апликације.",
+    "image_preview_description": "Слика средње величине са уклоњеним метаподацима, која се користи приликом прегледа једног елемента и за машинско учење",
+    "image_preview_quality_description": "Квалитет прегледа од 1-100. Више је боље, али производи веће датотеке и може смањити одзив апликације. Постављање ниске вредности може утицати на квалитет машинског учења.",
+    "image_preview_title": "Подешавања прегледа",
     "image_quality": "Квалитет",
-    "image_quality_description": "Квалитет слике од 1-100. Више је боље за квалитет, али производи веће датотеке, ова опција утиче на преглед и сличице.",
+    "image_resolution": "Резолуција",
+    "image_resolution_description": "Веће резолуције могу да сачувају више детаља, али им је потребно више времена за кодирање, имају веће величине датотека и могу да смање одзив апликације.",
     "image_settings": "Подешавања слике",
     "image_settings_description": "Управљајте квалитетом и резолуцијом генерисаних слика",
-    "image_thumbnail_format": "Формат сличице",
-    "image_thumbnail_resolution": "Резолуција сличице",
-    "image_thumbnail_resolution_description": "Користи се приликом прегледа група фотографија (главна временска линија, приказ албума, итд.). Веће резолуције могу да сачувају више детаља, али им је потребно више времена за кодирање, имају веће величине датотека и могу да смање брзину апликације.",
+    "image_thumbnail_description": "Мала сличица са огољеним метаподацима, која се користи приликом прегледа група фотографија као што је главна временска линија",
+    "image_thumbnail_quality_description": "Квалитет сличица од 1-100. Више је боље, али производи веће датотеке и може смањити одзив апликације.",
+    "image_thumbnail_title": "Подешавања сличица",
     "job_concurrency": "{job} паралелност",
+    "job_created": "Посао креиран",
     "job_not_concurrency_safe": "Овај посао није безбедан да буде паралелно активан.",
     "job_settings": "Подешавања посла",
     "job_settings_description": "Управљајте паралелношћу послова",
@@ -77,9 +89,6 @@
     "jobs_delayed": "{jobCount, plural, other {# одложених}}",
     "jobs_failed": "{jobCount, plural, other {# неуспешних}}",
     "library_created": "Направљена библиотека {library}",
-    "library_cron_expression": "Системски посао",
-    "library_cron_expression_description": "Подесите интервал скенирања користећи црон формат. За више информација погледајте нпр. <link>Цронтаб Гуру</link>",
-    "library_cron_expression_presets": "Унапред подешене поставке системског посла",
     "library_deleted": "Библиотека је избрисана",
     "library_import_path_description": "Одредите фасциклу за увоз. Ова фасцикла, укључујући подфасцикле, биће скенирана за слике и видео записе.",
     "library_scanning": "Периодично скенирање",
@@ -122,7 +131,7 @@
     "machine_learning_smart_search_description": "Потражите слике семантички користећи уграђени ЦЛИП",
     "machine_learning_smart_search_enabled": "Омогућите паметну претрагу",
     "machine_learning_smart_search_enabled_description": "Ако је oneмогућено, слике неће бити кодиране за паметну претрагу.",
-    "machine_learning_url_description": "УРЛ сервера за машинско учење",
+    "machine_learning_url_description": "URL сервера за машинско учење. Ако је наведено више од једне URL адресе, сваки сервер ће се покушавати један по један док један не одговори успешно, редом од првог до последњег.",
     "manage_concurrency": "Управљање паралелношћу",
     "manage_log_settings": "Управљајте подешавањима евиденције",
     "map_dark_style": "Тамни стил",
@@ -152,7 +161,7 @@
     "note_cannot_be_changed_later": "НАПОМЕНА: Ovo se kasnije ne može promeniti!",
     "note_unlimited_quota": "Напомена: Unesite 0 za neograničenu kvotu",
     "notification_email_from_address": "Са адресе",
-    "notification_email_from_address_description": "Адреса е-поште пошиљаоца, на пример: \"Immich foto server <noreply@immich.app>\"",
+    "notification_email_from_address_description": "Адреса е-поште пошиљаоца, на пример: \"Immich foto server <noreply@example.com>\"",
     "notification_email_host_description": "Хост сервера е-поште (нпр. smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Занемарите грешке сертификата",
     "notification_email_ignore_certificate_errors_description": "Игноришите грешке у валидацији ТЛС сертификата (не препоручује се)",
@@ -198,22 +207,24 @@
     "password_settings": "Лозинка за пријаву",
     "password_settings_description": "Управљајте подешавањима за пријаву лозинком",
     "paths_validated_successfully": "Све путање су успешно потврђене",
+    "person_cleanup_job": "Чишћење особа",
     "quota_size_gib": "Величина квоте (ГиБ)",
     "refreshing_all_libraries": "Освежавање свих библиотека",
     "registration": "Регистрација администратора",
     "registration_description": "Пошто сте први корисник на систему, бићете додељени као Админ и одговорни сте за административне задатке, а додатне кориснике ћете креирати ви.",
-    "removing_offline_files": "Уклањање ванмрежних датотека",
     "repair_all": "Поправи све",
     "repair_matched_items": "Поклапа се са {count, plural, one {1 ставком} few {# ставке} other {# ставки}}",
     "repaired_items": "{count, plural, one {Поправљена 1 ставка} few {Поправљене # ставке} other {Поправљене # ставки}}",
     "require_password_change_on_login": "Захтевати од корисника да промени лозинку при првом пријављивању",
     "reset_settings_to_default": "Ресетујте подешавања на подразумеване вредности",
     "reset_settings_to_recent_saved": "Ресетујте подешавања на недавно сачувана подешавања",
-    "scanning_library_for_changed_files": "Скенирање библиотеке за промењене датотеке",
-    "scanning_library_for_new_files": "Скенирање библиотеке за нове датотеке",
+    "scanning_library": "Скенирање библиотеке",
+    "search_jobs": "Тражи послове...",
     "send_welcome_email": "Пошаљите е-пошту добродошлице",
     "server_external_domain_settings": "Екстерни домаин",
     "server_external_domain_settings_description": "Домаин за јавне дељене везе, укључујући http(s)://",
+    "server_public_users": "Јавни корисници",
+    "server_public_users_description": "Сви корисници (име и адреса е-поште) су наведени приликом додавања корисника у дељене албуме. Када је онемогућена, листа корисника ће бити доступна само администраторима.",
     "server_settings": "Подешавања сервера",
     "server_settings_description": "Управљајте подешавањима сервера",
     "server_welcome_message": "Порука добродошлице",
@@ -238,6 +249,17 @@
     "storage_template_settings_description": "Управљајте структуром директоријума и именом датотеке средства за отпремање",
     "storage_template_user_label": "<code>{label}</code> је ознака за складиштење корисника",
     "system_settings": "Подешавања система",
+    "tag_cleanup_job": "Чишћење ознака (tags)",
+    "template_email_available_tags": "Можете да користите следеће променљиве у свом шаблону: {tags}",
+    "template_email_if_empty": "Ако је шаблон празан, користиће се подразумевана адреса е-поште.",
+    "template_email_invite_album": "Шаблон албума позива",
+    "template_email_preview": "Преглед",
+    "template_email_settings": "Шаблони е-поште",
+    "template_email_settings_description": "Управљајте прилагођеним шаблонима обавештења путем е-поште",
+    "template_email_update_album": "Ажурирајте шаблон албума",
+    "template_email_welcome": "Шаблон е-поште добродошлице",
+    "template_settings": "Шаблони обавештења",
+    "template_settings_description": "Управљајте прилагођеним шаблонима за обавештења.",
     "theme_custom_css_settings": "Прилагођени CSS",
     "theme_custom_css_settings_description": "Каскадни листови стилова (CSS) омогућавају прилагођавање дизајна Immich-a.",
     "theme_settings": "Подешавање тема",
@@ -245,7 +267,6 @@
     "these_files_matched_by_checksum": "Овим датотекама се подударају њихови контролни-збирови",
     "thumbnail_generation_job": "Генеришите сличице",
     "thumbnail_generation_job_description": "Генеришите велике, мале и замућене сличице за свако средство, као и сличице за сваку особу",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "АПИ за убрзање",
     "transcoding_acceleration_api_description": "АПИ који ће комуницирати са вашим уређајем да би убрзао транскодирање. Ово подешавање је 'најбољи напор': vraća se na softversko transkodiranje u slučaju neuspeha. VP9 može ili ne mora da radi u zavisnosti od vašeg hardvera.",
     "transcoding_acceleration_nvenc": "НВЕНЦ (захтева NVIDIA ГПУ)",
@@ -271,7 +292,7 @@
     "transcoding_hardware_acceleration": "Хардверско убрзање",
     "transcoding_hardware_acceleration_description": "Екпериментално; много брже, али ће имати нижи квалитет при истој брзини преноса",
     "transcoding_hardware_decoding": "Хардверско декодирање",
-    "transcoding_hardware_decoding_setting_description": "Односи се само на НВЕНЦ, QSV и RKMPP. Омогућава убрзање од краја до краја уместо да само убрзава кодирање. Можда неће радити на свим видео снимцима.",
+    "transcoding_hardware_decoding_setting_description": "Омогућава убрзање од краја до краја уместо да само убрзава кодирање. Можда неће радити на свим видео снимцима.",
     "transcoding_hevc_codec": "ХЕВЦ кодек",
     "transcoding_max_b_frames": "Максимални Б-кадри",
     "transcoding_max_b_frames_description": "Више вредности побољшавају ефикасност компресије, али успоравају кодирање. Можда није компатибилно са хардверским убрзањем на старијим уређајима. 0 oneмогућава Б-кадре, док -1 аутоматски поставља ову вредност.",
@@ -297,8 +318,6 @@
     "transcoding_threads_description": "Више вредности доводе до бржег кодирања, али остављају мање простора серверу за обраду других задатака док је активан. Ова вредност не би требало да буде већа од броја CPU језгара. Максимизира искоришћеност ако је подешено на 0.",
     "transcoding_tone_mapping": "Мапирање (tone-mapping)",
     "transcoding_tone_mapping_description": "Покушава да се сачува изглед ХДР видео записа када се конвертују у СДР. Сваки алгоритам прави различите компромисе за боју, детаље и осветљеност. Хабле чува детаље, Мобиус чува боју, а Раеинхард светлину.",
-    "transcoding_tone_mapping_npl": "Tone-mapping-NPL",
-    "transcoding_tone_mapping_npl_description": "Боје ће бити подешене тако да изгледају нормално за приказ ове осветљености. Контраинтуитивно, ниже вредности повећавају осветљеност видеа и обрнуто, јер компензују осветљеност екрана. 0 аутоматски поставља ову вредност.",
     "transcoding_transcode_policy": "Услови транскодирања",
     "transcoding_transcode_policy_description": "Услови о томе када видео треба транскодирати. ХДР видео снимци ће увек бити транскодирани (осим ако је транскодирање oneмогућено).",
     "transcoding_two_pass_encoding": "Двопролазно кодирање",
@@ -312,6 +331,7 @@
     "trash_settings_description": "Управљајте подешавањима смећа",
     "untracked_files": "Непраћене датотеке",
     "untracked_files_description": "Апликација не прати ове датотеке. one могу настати због неуспешних премештења, због прекинутих отпремања или као преостатак због грешке",
+    "user_cleanup_job": "Чишћење корисника",
     "user_delete_delay": "Налог и датотеке <b>{user}</b> биће заказани за трајно брисање за {delay, plural, one {# дан} other {# дана}}.",
     "user_delete_delay_settings": "Избриши уз кашњење",
     "user_delete_delay_settings_description": "Број дана након уклањања за трајно брисање корисничког налога и датотека. Посао брисања корисника се покреће у поноћ да би се проверили корисници који су спремни за брисање. Промене ове поставке ће бити процењене при следећем извршењу.",
@@ -378,7 +398,6 @@
   "archive_or_unarchive_photo": "Архивирајте или поништите архивирање фотографије",
   "archive_size": "Величина архиве",
   "archive_size_description": "Подеси величину архиве за преузимање (у ГиБ)",
-  "archived": "Arhivirano",
   "archived_count": "{count, plural, other {Архивирано #}}",
   "are_these_the_same_person": "Да ли су ово иста особа?",
   "are_you_sure_to_do_this": "Јесте ли сигурни да желите ово да урадите?",
@@ -388,8 +407,8 @@
   "asset_filename_is_offline": "Датотека {filename} је ван мреже (offline)",
   "asset_has_unassigned_faces": "Датотека има недодељена лица",
   "asset_hashing": "Хеширање...",
-  "asset_offline": "Датотека одсутна",
-  "asset_offline_description": "Ова датотека је ван мреже. Immich не може да приступи локацији своје датотеке. Уверите се да је датотека доступна, а затим поново скенирајте библиотеку.",
+  "asset_offline": "Датотека одсутна (offline)",
+  "asset_offline_description": "Ова вањска датотека се више не налази на диску. Молимо контактирајте свог Имич администратора за помоћ.",
   "asset_skipped": "Прескочено",
   "asset_skipped_in_trash": "У отпад",
   "asset_uploaded": "Отпремљено (Уплоадед)",
@@ -399,11 +418,10 @@
   "assets_added_to_album_count": "Додато је {count, plural, one {# датотека} other {# датотека}} у албум",
   "assets_added_to_name_count": "Додато {count, plural, one {# датотека} other {# датотекa}} у {hasName, select, true {<b>{name}</b>} other {нови албум}}",
   "assets_count": "{count, plural, one {# датотека} few {# датотеке} other {# датотека}}",
-  "assets_moved_to_trash": "{count, plural, one {Premeštena # datoteka} few {Premeštene # datoteke} other {Premeštene # datoteka}} u otpad",
   "assets_moved_to_trash_count": "Премештено {count, plural, one {# датотека} few {# датотеке} other {# датотека}} у отпад",
   "assets_permanently_deleted_count": "Трајно избрисано {count, plural, one {# датотека} few {# датотеке} other {# датотека}}",
   "assets_removed_count": "Уклоњено {count, plural, one {# датотека} few {# датотеке} other {# датотека}}",
-  "assets_restore_confirmation": "Да ли сте сигурни да желите да вратите све своје датотеке које су у отпаду? Не можете поништити ову радњу!",
+  "assets_restore_confirmation": "Да ли сте сигурни да желите да вратите све своје датотеке које су у отпаду? Не можете поништити ову радњу! Имајте на уму да се ванмрежна средства не могу вратити на овај начин.",
   "assets_restored_count": "Враћено {count, plural, one {# датотека} few {# датотеке} other {# датотека}}",
   "assets_trashed_count": "Бачено у отпад {count, plural, one {# датотека} few{# датотеке} other {# датотека}}",
   "assets_were_part_of_album_count": "{count, plural, one {Датотека је} other {Датотеке су}} већ део албума",
@@ -414,7 +432,8 @@
   "birthdate_saved": "Датум рођења успешно сачуван",
   "birthdate_set_description": "Датум рођења се користи да би се израчунале године ове особе у добу одређене фотографије.",
   "blurred_background": "Замућена позадина",
-  "build": "Сагради (Буилд)",
+  "bugs_and_feature_requests": "Грешке и захтеви за функције",
+  "build": "Под-верзија (Build)",
   "build_image": "Сагради (Буилд) имаге",
   "bulk_delete_duplicates_confirmation": "Да ли сте сигурни да желите групно да избришете {count, plural, one {# дуплиран елеменат} few {# дуплирана елемента} other {# дуплираних елемената}}? Ово ће задржати највеће средство сваке групе и трајно избрисати све друге дупликате. Не можете поништити ову радњу!",
   "bulk_keep_duplicates_confirmation": "Да ли сте сигурни да желите да задржите {count, plural, one {1 дуплирану датотеку} few {# дуплиране датотеке} other {# дуплираних датотека}}? Ово ће решити све дуплиране групе без брисања било чега.",
@@ -428,10 +447,6 @@
   "cannot_merge_people": "Не може спојити особе",
   "cannot_undo_this_action": "Не можете поништити ову радњу!",
   "cannot_update_the_description": "Не може ажурирати опис",
-  "cant_apply_changes": "Ne može primeniti promene",
-  "cant_get_faces": "Ne može preuzeti lica",
-  "cant_search_people": "Ne može pretražiti osobe",
-  "cant_search_places": "Ne može pretražiti mesta",
   "change_date": "Промени датум",
   "change_expiration_time": "Промени време истека",
   "change_location": "Промени место",
@@ -463,6 +478,7 @@
   "confirm": "Потврдите",
   "confirm_admin_password": "Потврди Административну Лозинку",
   "confirm_delete_shared_link": "Да ли сте сигурни да желите да избришете овај дељени link?",
+  "confirm_keep_this_delete_others": "Свe осталe датотекe у групи ће бити избрисанe осим овe датотекe. Да ли сте сигурни да желите да наставите?",
   "confirm_password": "Поново унеси шифру",
   "contain": "Обухвати",
   "context": "Контекст",
@@ -512,16 +528,19 @@
   "delete_key": "Избриши кључ",
   "delete_library": "Обриши библиотеку",
   "delete_link": "Обриши везу",
+  "delete_others": "Избришите друге",
   "delete_shared_link": "Обриши дељену везу",
   "delete_tag": "Обриши ознаку (tag)",
   "delete_tag_confirmation_prompt": "Да ли стварно желите да избришете ознаку (tag) {tagName}?",
   "delete_user": "Обриши корисника",
   "deleted_shared_link": "Обришена дељена веза",
+  "deletes_missing_assets": "Брише датотеке које недостају са диска",
   "description": "Опис",
   "details": "Детаљи",
   "direction": "Смер",
   "disabled": "oneмогућено",
   "disallow_edits": "Забрани измене",
+  "discord": "Дискорд",
   "discover": "Откријте",
   "dismiss_all_errors": "Одбаците све грешке",
   "dismiss_error": "Одбаци грешку",
@@ -530,6 +549,7 @@
   "display_original_photos": "Прикажите оригиналне фотографије",
   "display_original_photos_setting_description": "Радије приказујете оригиналну фотографију када глеdate материјал него сличице када је оригинално дело компатибилно са webom. Ово може довести до споријег приказа фотографија.",
   "do_not_show_again": "Не прикажи поново ову поруку",
+  "documentation": "Документација",
   "done": "Урађено",
   "download": "Преузми",
   "download_include_embedded_motion_videos": "Уграђени видео снимци",
@@ -542,13 +562,6 @@
   "duplicates": "Дупликати",
   "duplicates_description": "Разрешите сваку групу тако што ћете навести дупликате, ако их има",
   "duration": "Трајање",
-  "durations": {
-    "days": "{days, plural, one {dan} other {{days, number} dana}}",
-    "hours": "{hours, plural, one {sat} other {{hours, number} sata}}",
-    "minutes": "{minutes, plural, one {minut} other {{minutes, number} minuta}}",
-    "months": "{months, plural, one {mesec} other {{months, number} meseci}}",
-    "years": "{years, plural, one {godina} other {{years, number} godina}}"
-  },
   "edit": "Уреди",
   "edit_album": "Уреди албум",
   "edit_avatar": "Уреди аватар",
@@ -573,8 +586,6 @@
   "editor_crop_tool_h2_aspect_ratios": "Пропорције (aspect ratios)",
   "editor_crop_tool_h2_rotation": "Ротација",
   "email": "Е-пошта",
-  "empty": "",
-  "empty_album": "Isprazni album",
   "empty_trash": "Испразните смеће",
   "empty_trash_confirmation": "Да ли сте сигурни да желите да испразните смеће? Ово ће трајно уклонити све датотеке у смећу из Immich-a.\nNe можете поништити ову радњу!",
   "enable": "Омогући (Енабле)",
@@ -608,6 +619,7 @@
     "failed_to_create_shared_link": "Прављење дељеног linkа није успело",
     "failed_to_edit_shared_link": "Уређивање дељеног linkа није успело",
     "failed_to_get_people": "Неуспело позивање особа",
+    "failed_to_keep_this_delete_others": "Није успело задржавање овог дела и брисање осталих датотека",
     "failed_to_load_asset": "Учитавање датотека није успело",
     "failed_to_load_assets": "Није успело учитавање датотека",
     "failed_to_load_people": "Учитавање особа није успело",
@@ -635,8 +647,6 @@
     "unable_to_change_location": "Није могуће променити локацију",
     "unable_to_change_password": "Није могуће променити лозинку",
     "unable_to_change_visibility": "Није могуће променити видљивост за {count, plural, one {# особу} other {# особе}}",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_complete_oauth_login": "Није могуће довршити OAuth пријаву",
     "unable_to_connect": "Није могуће повезати се",
     "unable_to_connect_to_server": "Немогуће је повезати се са сервером",
@@ -661,6 +671,7 @@
     "unable_to_get_comments_number": "Није могуће добити број коментара",
     "unable_to_get_shared_link": "Преузимање дељене везе није успело",
     "unable_to_hide_person": "Није могуће сакрити особу",
+    "unable_to_link_motion_video": "Није могуће повезати (link) видео снимак",
     "unable_to_link_oauth_account": "Није могуће повезати OAuth налог",
     "unable_to_load_album": "Није могуће учитати албум",
     "unable_to_load_asset_activity": "Није могуће учитати активност средстава",
@@ -676,12 +687,10 @@
     "unable_to_remove_album_users": "Није могуће уклонити кориснике из албума",
     "unable_to_remove_api_key": "Није могуће уклонити АПИ кључ (key)",
     "unable_to_remove_assets_from_shared_link": "Није могуће уклонити елементе са дељеног linkа",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_deleted_assets": "Није могуће уклонити ванмрежне датотеке",
     "unable_to_remove_library": "Није могуће уклонити библиотеку",
-    "unable_to_remove_offline_files": "Није могуће уклонити ванмрежне датотеке",
     "unable_to_remove_partner": "Није могуће уклонити партнера",
     "unable_to_remove_reaction": "Није могуће уклонити реакцију",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "Није могуће поправити ставке",
     "unable_to_reset_password": "Није могуће ресетовати лозинку",
     "unable_to_resolve_duplicate": "Није могуће разрешити дупликат",
@@ -701,6 +710,7 @@
     "unable_to_submit_job": "Није могуће предати задатак",
     "unable_to_trash_asset": "Није могуће избацити материјал у отпад",
     "unable_to_unlink_account": "Није могуће раскинути профил",
+    "unable_to_unlink_motion_video": "Није могуће прекинути везу са видео снимком",
     "unable_to_update_album_cover": "Није могуће ажурирати насловницу албума",
     "unable_to_update_album_info": "Није могуће ажурирати информације о албуму",
     "unable_to_update_library": "Није могуће ажурирати библиотеку",
@@ -710,10 +720,6 @@
     "unable_to_update_user": "Није могуће ажурирати корисника",
     "unable_to_upload_file": "Није могуће отпремити датотеку"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exif": "EXIF",
   "exit_slideshow": "Изађи из пројекције слајдова",
   "expand_all": "Прошири све",
@@ -728,33 +734,28 @@
   "external": "Спољашњи",
   "external_libraries": "Спољашње Библиотеке",
   "face_unassigned": "Нераспоређени",
-  "failed_to_get_people": "Neuspešno isčitavanje osoba",
+  "failed_to_load_assets": "Учитавање средстава није успело",
   "favorite": "Фаворит",
   "favorite_or_unfavorite_photo": "Омиљена или неомиљена фотографија",
   "favorites": "Фаворити",
-  "feature": "",
   "feature_photo_updated": "Главна фотографија је ажурирана",
-  "featurecollection": "",
   "features": "Функције",
   "features_setting_description": "Управљајте функцијама апликације",
   "file_name": "Назив документа",
   "file_name_or_extension": "Име датотеке или екстензија",
   "filename": "Име датотеке",
-  "files": "",
   "filetype": "Врста документа",
   "filter_people": "Филтрирање особа",
   "find_them_fast": "Брзо их пронађите по имену помоћу претраге",
   "fix_incorrect_match": "Исправите нетачно подударање",
   "folders": "Фасцикле (Folders)",
   "folders_feature_description": "Прегледавање приказа фасцикле за фотографије и видео записе у систему датотека",
-  "force_re-scan_library_files": "Принудно поново скенирајте све датотеке библиотеке",
   "forward": "Напред",
   "general": "Генерално",
   "get_help": "Нађи помоћ",
   "getting_started": "Почињем",
   "go_back": "Врати се",
   "go_to_search": "Иди на претрагу",
-  "go_to_share_page": "Иди на страницу за дељење",
   "group_albums_by": "Групни албуми по...",
   "group_no": "Без груписања",
   "group_owner": "Групирајте по власнику",
@@ -780,10 +781,6 @@
   "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} снимљено у {city}, {country} са {person1} и {person2} {date}",
   "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} снимљено у {city}, {country} са {person1}, {person2}, и {person3} {date}",
   "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} снимљено у {city}, {country} са {person1}, {person2}, и {additionalCount, number} других {date}",
-  "image_alt_text_people": "{count, plural, =1 {са {person1}} =2 {са {person1} и {person2}} =3 {са {person1}, {person2}, и {person3}} other {са {person1}, {person2}, и {others, number} остали}}",
-  "image_alt_text_place": "у {city}, {country}",
-  "image_taken": "{isVideo, select, true {Видео запис снимљен} other {Фотографија усликана}}",
-  "img": "",
   "immich_logo": "Лого Immich-a",
   "immich_web_interface": "Web интерфејс Immich-a",
   "import_from_json": "Увези из ЈСОН-а",
@@ -804,10 +801,11 @@
   "invite_people": "Позовите људе",
   "invite_to_album": "Позови на албум",
   "items_count": "{count, plural, one {# датотека} other {# датотека}}",
-  "job_settings_description": "",
   "jobs": "Послови",
   "keep": "Задржи",
   "keep_all": "Задржи све",
+  "keep_this_delete_others": "Задржи ово, избриши друге",
+  "kept_this_deleted_others": "Задржана је ова датотека и избрисано {count, plural, one {# датотека} other {# датотека}}",
   "keyboard_shortcuts": "Пречице на тастатури",
   "language": "Језик",
   "language_setting_description": "Изаберите жељени језик",
@@ -819,31 +817,6 @@
   "level": "Ниво",
   "library": "Библиотека",
   "library_options": "Опције библиотеке",
-  "license_account_info": "Ваш налог је лиценциран",
-  "license_activated_subtitle": "Хвала вам што подржавате Имич (Immich) и софтвер отвореног кода",
-  "license_activated_title": "Ваша лиценца је успешно активирана",
-  "license_button_activate": "Активираj",
-  "license_button_buy": "Купи",
-  "license_button_buy_license": "Купи лиценцу",
-  "license_button_select": "Изаберите",
-  "license_failed_activation": "Активација лиценце није успела. Проверите своју е-пошту да бисте пронашли исправан кључ лиценце!",
-  "license_individual_description_1": "1 лиценца по кориснику на било ком серверу",
-  "license_individual_title": "Индивидуална лиценца",
-  "license_info_licensed": "Лиценцирано",
-  "license_info_unlicensed": "Без лиценце",
-  "license_input_suggestion": "Имате лиценцу? Унесите кључ испод",
-  "license_license_subtitle": "Купите лиценцу за подршку Имич-a",
-  "license_license_title": "ЛИЦЕНЦA",
-  "license_lifetime_description": "Доживотна лиценца",
-  "license_per_server": "По серверу",
-  "license_per_user": "По кориснику",
-  "license_server_description_1": "1 лиценца по серверу",
-  "license_server_description_2": "Лиценца за све кориснике на серверу",
-  "license_server_title": "Сервер Лиценцa",
-  "license_trial_info_1": "Користите нелиценцирану верзију Имич-а",
-  "license_trial_info_2": "Користили сте Имич отприлике",
-  "license_trial_info_3": "{accountAge, plural, one {# дан} other {# данa}}",
-  "license_trial_info_4": "Молимо вас да размислите о куповини лиценце за подршку континуираном развоју услуге",
   "light": "Светло",
   "like_deleted": "Лајкуј избрисано",
   "link_motion_video": "Направи везу за видео запис",
@@ -865,6 +838,7 @@
   "look": "Погледај",
   "loop_videos": "Понављајте видео записе",
   "loop_videos_description": "Омогућите за аутоматско понављање видео записа у прегледнику детаља.",
+  "main_branch_warning": "Употребљавате развојну верзију; строго препоручујемо употребу издате верзије!",
   "make": "Креирај",
   "manage_shared_links": "Управљајте дељеним везама",
   "manage_sharing_with_partners": "Управљајте дељењем са партнерима",
@@ -934,6 +908,7 @@
   "notifications": "Нотификације",
   "notifications_setting_description": "Управљајте обавештењима",
   "oauth": "OAuth",
+  "official_immich_resources": "Званични Имич ресурси",
   "offline": "Одсутан (Offline)",
   "offline_paths": "Недоступне (Offline) путање",
   "offline_paths_description": "Ови резултати могу бити последица ручног брисања датотека које нису део спољне библиотеке.",
@@ -946,7 +921,6 @@
   "onboarding_welcome_user": "Добродошли, {user}",
   "online": "Доступан (Онлине)",
   "only_favorites": "Само фаворити",
-  "only_refreshes_modified_files": "Освежава само измењене датотеке",
   "open_in_map_view": "Отвори у приказу мапе",
   "open_in_openstreetmap": "Отворите у ОпенСтреетМап-у",
   "open_the_search_filters": "Отворите филтере за претрагу",
@@ -984,14 +958,12 @@
   "people_edits_count": "Измењено {count, plural, one {# особа} other {# особе}}",
   "people_feature_description": "Прегледавање фотографија и видео снимака груписаних по особама",
   "people_sidebar_description": "Прикажите везу до особа на бочној траци",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "Упозорење за трајно брисање",
   "permanent_deletion_warning_setting_description": "Прикажи упозорење када трајно бришете датотеке",
   "permanently_delete": "Трајно избрисати",
   "permanently_delete_assets_count": "Трајно избриши {count, plural, one {датотеку} other {датотеке}}",
   "permanently_delete_assets_prompt": "Да ли сте сигурни да желите да трајно избришете {count, plural, one {ову датотеку?} other {ове <b>#</b> датотеке?}}Ово ће их такође уклонити {count, plural, one {из њиховог} other {из њихових}} албума.",
   "permanently_deleted_asset": "Трајно избрисана датотека",
-  "permanently_deleted_assets": "Trajno izbrisano {count, plural, one {# datoteka} other {# datoteke}}",
   "permanently_deleted_assets_count": "Трајно избрисано {count, plural, one {# датотека} other {# датотеке}}",
   "person": "Особа",
   "person_hidden": "{name}{hidden, select, true { (скривено)} other {}}",
@@ -1007,7 +979,6 @@
   "play_memories": "Покрени сећања",
   "play_motion_photo": "Покрени покретну фотографију",
   "play_or_pause_video": "Покрени или паузирај видео запис",
-  "point": "",
   "port": "порт",
   "preset": "Унапред подешено",
   "preview": "Преглед",
@@ -1052,12 +1023,10 @@
   "purchase_server_description_2": "Значка подршке",
   "purchase_server_title": "Сервер",
   "purchase_settings_server_activated": "Кључем производа сервера управља администратор",
-  "range": "",
   "rating": "Оцена звездица",
   "rating_clear": "Обриши оцену",
   "rating_count": "{count, plural, one {# звезда} other {# звезде}}",
   "rating_description": "Прикажите EXIF оцену у инфо панелу",
-  "raw": "",
   "reaction_options": "Опције реакције",
   "read_changelog": "Прочитајте дневник промена",
   "reassign": "Поново додај",
@@ -1065,14 +1034,17 @@
   "reassigned_assets_to_new_person": "Поново додељено {count, plural, one {# датотека} other {# датотеке}} новој особи",
   "reassing_hint": "Доделите изабрана средства постојећој особи",
   "recent": "Скорашњи",
+  "recent-albums": "Недавни албуми",
   "recent_searches": "Скорашње претраге",
   "refresh": "Освежи",
   "refresh_encoded_videos": "Освежите кодиране (енцодед) видео записе",
+  "refresh_faces": "Освежи лица",
   "refresh_metadata": "Освежите метаподатке",
   "refresh_thumbnails": "Освежите сличице",
   "refreshed": "Освежено",
-  "refreshes_every_file": "Освежава сваку датотеку",
+  "refreshes_every_file": "Поново чита све постојеће и нове датотеке",
   "refreshing_encoded_video": "Освежавање кодираног (енцодед) видеа",
+  "refreshing_faces": "Освежавањe лица",
   "refreshing_metadata": "Освежавање мета-података",
   "regenerating_thumbnails": "Обнављање сличица",
   "remove": "Уклони",
@@ -1080,10 +1052,11 @@
   "remove_assets_shared_link_confirmation": "Да ли сте сигурни да желите да уклоните {count, plural, one {# датотеку} other {# датотеке}} са ове дељене везе?",
   "remove_assets_title": "Уклонити датотеке?",
   "remove_custom_date_range": "Уклоните прилагођени период",
+  "remove_deleted_assets": "Уклоните ванмрежне (offline) датотеке",
   "remove_from_album": "Обриши из албума",
   "remove_from_favorites": "Уклони из фаворита",
   "remove_from_shared_link": "Уклоните са дељене везе",
-  "remove_offline_files": "Уклоните ванмрежне (offline) датотеке",
+  "remove_url": "Уклони URL",
   "remove_user": "Уклони корисника",
   "removed_api_key": "Уклоњен АПИ кључ (key): {name}",
   "removed_from_archive": "Уклоњено из архиве",
@@ -1100,7 +1073,6 @@
   "reset": "Ресетовати",
   "reset_password": "Ресетовати лозинку",
   "reset_people_visibility": "Ресетујте видљивост особа",
-  "reset_settings_to_default": "",
   "reset_to_default": "Ресетујте на подразумеване вредности",
   "resolve_duplicates": "Реши дупликате",
   "resolved_all_duplicates": "Сви дупликати су разрешени",
@@ -1120,8 +1092,7 @@
   "saved_settings": "Сачувана подешавања",
   "say_something": "Реци нешто",
   "scan_all_libraries": "Скенирај све библиотеке",
-  "scan_all_library_files": "Поново скенирајте све датотеке библиотеке",
-  "scan_new_library_files": "Скенирајте нове датотеке библиотеке",
+  "scan_library": "Скенирај",
   "scan_settings": "Подешавања скенирања",
   "scanning_for_album": "Скенирање албума...",
   "search": "Претрага",
@@ -1139,6 +1110,7 @@
   "search_options": "Опције претраге",
   "search_people": "Претражи особе",
   "search_places": "Претражи места",
+  "search_settings": "Претрага подешавања",
   "search_state": "Тражи регион...",
   "search_tags": "Претражи ознаке (tags)...",
   "search_timezone": "Претражи временску зону...",
@@ -1163,7 +1135,6 @@
   "selected_count": "{count, plural, other {# изабрано}}",
   "send_message": "Пошаљи поруку",
   "send_welcome_email": "Пошаљите е-пошту добродошлице",
-  "server": "Сервер",
   "server_offline": "Сервер ван мреже (offline)",
   "server_online": "Сервер нa мрежи (online)",
   "server_stats": "Статистика сервера",
@@ -1206,6 +1177,7 @@
   "show_person_options": "Прикажи опције особе",
   "show_progress_bar": "Прикажи траку напретка",
   "show_search_options": "Прикажи опције претраге",
+  "show_slideshow_transition": "Прикажи прелаз пројекције слајдова",
   "show_supporter_badge": "Значка подршке",
   "show_supporter_badge_description": "Покажите значку подршке",
   "shuffle": "Мешање",
@@ -1247,13 +1219,16 @@
   "submit": "Достави",
   "suggestions": "Сугестије",
   "sunrise_on_the_beach": "Излазак сунца на плажи",
+  "support": "Подршка",
+  "support_and_feedback": "Подршка и повратне информације",
+  "support_third_party_description": "Ваша иммицх инсталација је спакована од стране треће стране. Проблеми са којима се суочавате могу бити узроковани тим пакетом, па вас молимо да им прво поставите проблеме користећи доње везе.",
   "swap_merge_direction": "Замените правац спајања",
   "sync": "Синхронизација",
   "tag": "Ознака (tag)",
   "tag_assets": "Означите датотеке",
   "tag_created": "Направљена ознака (tag): {tag}",
   "tag_feature_description": "Прегледавање фотографија и видео снимака груписаних по логичним темама ознака",
-  "tag_not_found_question": "Не можете да пронађете ознаку (tag)? Направите једну <link>овде</link>",
+  "tag_not_found_question": "Не можете да пронађете ознаку (tag)? <link>Направите нову ознаку</link>",
   "tag_updated": "Ажурирана ознака (tag): {tag}",
   "tagged_assets": "Означено (tagged) {count, plural, one {# датотека} other {# датотеке}}",
   "tags": "Ознаке (tags)",
@@ -1262,18 +1237,19 @@
   "theme_selection": "Избор теме",
   "theme_selection_description": "Аутоматски поставите тему на светлу или тамну на основу системских преференција вашег претраживача",
   "they_will_be_merged_together": "Они ће бити спојени заједно",
+  "third_party_resources": "Ресурси трећих страна",
   "time_based_memories": "Сећања заснована на времену",
+  "timeline": "Временска линија",
   "timezone": "Временска зона",
   "to_archive": "Архивирај",
   "to_change_password": "Промени лозинку",
   "to_favorite": "Постави као фаворит",
   "to_login": "Пријава",
   "to_parent": "Врати се назад",
-  "to_root": "На почетак",
   "to_trash": "Смеће",
   "toggle_settings": "Намести подешавања",
   "toggle_theme": "Намести тамну тему",
-  "toggle_visibility": "Namesti vidljivost",
+  "total": "Укупно",
   "total_usage": "Укупна употреба",
   "trash": "Отпад",
   "trash_all": "Баци све у отпад",
@@ -1283,14 +1259,13 @@
   "trashed_items_will_be_permanently_deleted_after": "Датотеке у отпаду ће бити трајно избрисане након {days, plural, one {# дан} few {# дана} other {# дана}}.",
   "type": "Врста",
   "unarchive": "Врати из архиве",
-  "unarchived": "Vraćeno iz arhive",
   "unarchived_count": "{count, plural, other {Nearhivirano#}}",
   "unfavorite": "Избаци из омиљених (унфаворите)",
   "unhide_person": "Откриј особу",
   "unknown": "Непознат",
-  "unknown_album": "Nepoznat Album",
   "unknown_year": "Непозната Година",
   "unlimited": "Неограничено",
+  "unlink_motion_video": "Прекините везу са видео снимком",
   "unlink_oauth": "Прекини везу са Oauth-om",
   "unlinked_oauth_account": "Опозвана веза OAuth налога",
   "unnamed_album": "Неименовани албум",
@@ -1319,13 +1294,13 @@
   "use_custom_date_range": "Уместо тога користите прилагођени период",
   "user": "Корисник",
   "user_id": "ИД корисника",
-  "user_license_settings": "Лиценца",
-  "user_license_settings_description": "Управљајте својом лиценцом",
   "user_liked": "{user} је лајковао {type, select, photo {ову фотографију} video {овај видео запис} asset {ову датотеку} other {ово}}",
   "user_purchase_settings": "Куповина",
   "user_purchase_settings_description": "Управљајте куповином",
   "user_role_set": "Постави {user} као {role}",
   "user_usage_detail": "Детаљи коришћења корисника",
+  "user_usage_stats": "Статистика коришћења налога",
+  "user_usage_stats_description": "Погледајте статистику коришћења налога",
   "username": "Корисничко име",
   "users": "Корисници",
   "utilities": "Алати",
@@ -1333,7 +1308,9 @@
   "variables": "Променљиве (вариаблес)",
   "version": "Верзија",
   "version_announcement_closing": "Твој пријатељ, Алекс",
-  "version_announcement_message": "Здраво пријатељу, постоји нова верзија апликације, молимо вас да одвојите време да посетите <link>напомене о издању</link> и уверите се у своје <code>docker-compose.yml</code>, и <code>.env</code> подешавање је ажурирано како би се спречиле било какве погрешне конфигурације, посебно ако користите WatchTower или било који механизам који аутоматски управља ажурирањем ваше апликације.",
+  "version_announcement_message": "Здраво пријатељу, постоји нова верзија апликације, молимо вас да одвојите време да посетите <link>напомене о издању</link> и уверите се да је сервер ажуриран како би се спречиле било какве погрешне конфигурације, посебно ако користите WatchTower или било који механизам који аутоматски управља ажурирањем ваше апликације.",
+  "version_history": "Историја верзија",
+  "version_history_item": "Инсталирано {version} on {date}",
   "video": "Видео запис",
   "video_hover_setting": "Пусти сличицу видеа када лебди",
   "video_hover_setting_description": "Пусти сличицу видеа када миш пређе преко ставке. Чак и када је oneмогућена, репродукција се може покренути преласком миша преко икone за репродукцију.",
@@ -1345,16 +1322,16 @@
   "view_all_users": "Прикажи све кориснике",
   "view_in_timeline": "Прикажи у временској линији",
   "view_links": "Прикажи везе",
+  "view_name": "Погледати",
   "view_next_asset": "Погледајте следећу датотеку",
   "view_previous_asset": "Погледај претходну датотеку",
   "view_stack": "Прикажи гомилу",
-  "viewer": "Preglednik (viewer)",
   "visibility_changed": "Видљивост је промењена за {count, plural, one {# особу} other {# особе}}",
   "waiting": "Чекам",
   "warning": "Упозорење",
   "week": "Недеља",
   "welcome": "Добродошли",
-  "welcome_to_immich": "Добродошли у immich",
+  "welcome_to_immich": "Добродошли у Имич (Immich)",
   "year": "Година",
   "years_ago": "пре {years, plural, one {# године} other {# година}}",
   "yes": "Да",
diff --git a/web/src/lib/i18n/sr_Latn.json b/i18n/sr_Latn.json
similarity index 91%
rename from web/src/lib/i18n/sr_Latn.json
rename to i18n/sr_Latn.json
index ea40525a81..09baf5ff9d 100644
--- a/web/src/lib/i18n/sr_Latn.json
+++ b/i18n/sr_Latn.json
@@ -1,5 +1,5 @@
 {
-  "about": "O aplikaciji",
+  "about": "O Aplikaciji",
   "account": "Profil",
   "account_settings": "Podešavanja za Profil",
   "acknowledge": "Potvrdi",
@@ -23,16 +23,23 @@
   "add_to": "Dodaj u...",
   "add_to_album": "Dodaj u album",
   "add_to_shared_album": "Dodaj u deljen album",
+  "add_url": "Dodajte URL",
   "added_to_archive": "Dodato u arhivu",
   "added_to_favorites": "Dodato u favorite",
   "added_to_favorites_count": "Dodato {count, number} u favorite",
   "admin": {
     "add_exclusion_pattern_description": "Dodajte obrasce isključenja. Korištenje *, ** i ? je podržano. Da biste ignorisali sve datoteke u bilo kom direktorijumu pod nazivom „Rav“, koristite „**/Rav/**“. Da biste ignorisali sve datoteke koje se završavaju na „.tif“, koristite „**/*.tif“. Da biste ignorisali apsolutnu putanju, koristite „/path/to/ignore/**“.",
+    "asset_offline_description": "Ovo eksterno bibliotečko sredstvo se više ne nalazi na disku i premešteno je u smeće. Ako je datoteka premeštena unutar biblioteke, proverite svoju vremensku liniju za novo odgovarajuće sredstvo. Da biste vratili ovo sredstvo, uverite se da Immich može da pristupi dole navedenoj putanji datoteke i skenirajte biblioteku.",
     "authentication_settings": "Podešavanja za autentifikaciju",
     "authentication_settings_description": "Upravljajte lozinkom, OAuth-om i drugim podešavanjima autentifikacije",
     "authentication_settings_disable_all": "Da li ste sigurni da želite da onemogućite sve metode prijavljivanja? Prijava će biti potpuno onemogućena.",
     "authentication_settings_reenable": "Da biste ponovo omogućili, koristite <link>komandu servera</link>.",
     "background_task_job": "Pozadinski zadaci",
+    "backup_database": "Rezervna kopija baze podataka",
+    "backup_database_enable_description": "Omogućite rezervne kopije baze podataka",
+    "backup_keep_last_amount": "Količina prethodnih rezervnih kopija za čuvanje",
+    "backup_settings": "Podešavanja rezervne kopije",
+    "backup_settings_description": "Upravljajte postavkama rezervne kopije baze podataka",
     "check_all": "Proveri sve",
     "cleared_jobs": "Očišćeni poslovi za: {job}",
     "config_set_by_file": "Konfiguraciju trenutno postavlja konfiguracioni fajl",
@@ -41,35 +48,40 @@
     "confirm_email_below": "Da biste potvrdili, unesite \"{email}\" ispod",
     "confirm_reprocess_all_faces": "Da li ste sigurni da želite da ponovo obradite sva lica? Ovo će takođe obrisati imenovane osobe.",
     "confirm_user_password_reset": "Da li ste sigurni da želite da resetujete lozinku korisnika {user}?",
-    "crontab_guru": "Guru servisnih zadataka",
+    "create_job": "Kreirajte posao",
+    "cron_expression": "Cron izraz (expression)",
+    "cron_expression_description": "Podesite interval skeniranja koristeći cron format. Za više informacija pogledajte npr. <link>Crontab Guru</link>",
+    "cron_expression_presets": "Predefinisana podešavanja Cron izraza (expression)",
     "disable_login": "Onemogući prijavu",
-    "disabled": "",
     "duplicate_detection_job_description": "Pokrenite mašinsko učenje na sredstvima da biste otkrili slične slike. Oslanja se na pametnu pretragu",
     "exclusion_pattern_description": "Obrasci izuzimanja vam omogućavaju da ignorišete datoteke i fascikle kada skenirate biblioteku. Ovo je korisno ako imate fascikle koje sadrže datoteke koje ne želite da uvezete, kao što su RAW datoteke.",
     "external_library_created_at": "Eksterna biblioteka (napravljena {date})",
     "external_library_management": "Upravljanje eksternim bibliotekama",
     "face_detection": "Detekcija lica",
-    "face_detection_description": "Otkrivanje lica u datotekama pomoću mašinskog učenja. Za video snimke se uzima u obzir samo sličica. „Sve“ (ponovno) obrađuje sve datoteke. „Nedostaju“ sredstva u nizu koja još nisu obrađena. Otkrivena lica će biti stavljena u red za prepoznavanje lica nakon što se prepoznavanje lica završi, grupišući ih u postojeće ili nove ljude.",
-    "facial_recognition_job_description": "Grupa je detektovala lica i dodala ih postojecim ljudima. Ovaj korak se pokreće nakon što je prepoznavanje lica završeno. „Sve“ (ponovno) grupiše sva lica. „Nedostaju“ lica u redovima kojima nije dodeljena osoba.",
+    "face_detection_description": "Otkrijte lica u datotekama pomoću mašinskog učenja. Za video snimke se uzima u obzir samo sličica. „Osveži“ (ponovno) obrađuje sve datoteke. „Resetovanje“ dodatno briše sve trenutne podatke o licu. „Nedostaju“ datoteke u redu koje još nisu obrađene. Otkrivena lica će biti stavljena u red za prepoznavanje lica nakon što se prepoznavanje lica završi, grupišući ih u postojeće ili nove osobe.",
+    "facial_recognition_job_description": "Grupa je detektovala lica i dodala ih postojećim osobama. Ovaj korak se pokreće nakon što je prepoznavanje lica završeno. „Resetuj“ (ponovno) grupiše sva lica. „Nedostaju“ lica u redovima kojima nije dodeljena osoba.",
     "failed_job_command": "Komanda {command} nije uspela za posao: {job}",
     "force_delete_user_warning": "UPOZORENJE: Ovo će odmah ukloniti korisnika i sve datoteke. Ovo se ne može opozvati i datoteke se ne mogu oporaviti.",
     "forcing_refresh_library_files": "Prinudno osvežavanje svih datoteka biblioteke",
+    "image_format": "Format",
     "image_format_description": "WebP proizvodi manje datoteke od JPEG, ali se sporije kodira.",
     "image_prefer_embedded_preview": "Preferirajte ugrađeni pregled",
     "image_prefer_embedded_preview_setting_description": "Koristite ugrađene preglede u RAW fotografije kao ulaz za obradu slike kada su dostupne. Ovo može da proizvede preciznije boje za neke slike, ali kvalitet pregleda zavisi od kamere i slika može imati više nepravilnosti kompresije.",
     "image_prefer_wide_gamut": "Preferirajte širok spektar",
     "image_prefer_wide_gamut_setting_description": "Koristite Display P3 za sličice. Ovo bolje čuva živopisnost slika sa širokim prostorima boja, ali slike mogu izgledati drugačije na starim uređajima sa starom verzijom pretraživača. sRGB slike se čuvaju kao sRGB da bi se izbegle promene boja.",
-    "image_preview_format": "Pregled formata",
-    "image_preview_resolution": "Pregled rezolucije",
-    "image_preview_resolution_description": "Koristi se za gledanje jedne fotografije i za mašinsko učenje. Veće rezolucije mogu da sačuvaju više detalja, ali im je potrebno više vremena za kodiranje, imaju veće veličine datoteka i mogu da smanje brzinu aplikacije.",
+    "image_preview_description": "Slika srednje veličine sa uklonjenim metapodacima, koja se koristi prilikom pregleda jednog elementa i za mašinsko učenje",
+    "image_preview_quality_description": "Kvalitet pregleda od 1-100. Više je bolje, ali proizvodi veće datoteke i može smanjiti odziv aplikacije. Postavljanje niske vrednosti može uticati na kvalitet mašinskog učenja.",
+    "image_preview_title": "Podešavanja pregleda",
     "image_quality": "Kvalitet",
-    "image_quality_description": "Kvalitet slike od 1-100. Više je bolje za kvalitet, ali proizvodi veće datoteke, ova opcija utiče na pregled i sličice.",
+    "image_resolution": "Rezolucija",
+    "image_resolution_description": "Veće rezolucije mogu da sačuvaju više detalja, ali im je potrebno više vremena za kodiranje, imaju veće veličine datoteka i mogu da smanje odziv aplikacije.",
     "image_settings": "Podešavanja slike",
     "image_settings_description": "Upravljajte kvalitetom i rezolucijom generisanih slika",
-    "image_thumbnail_format": "Format sličice",
-    "image_thumbnail_resolution": "Rezolucija sličice",
-    "image_thumbnail_resolution_description": "Koristi se prilikom pregleda grupa fotografija (glavna vremenska linija, prikaz albuma, itd.). Veće rezolucije mogu da sačuvaju više detalja, ali im je potrebno više vremena za kodiranje, imaju veće veličine datoteka i mogu da smanje brzinu aplikacije.",
+    "image_thumbnail_description": "Mala sličica sa ogoljenim metapodacima, koja se koristi prilikom pregleda grupa fotografija kao što je glavna vremenska linija",
+    "image_thumbnail_quality_description": "Kvalitet sličica od 1-100. Više je bolje, ali proizvodi veće datoteke i može smanjiti odziv aplikacije.",
+    "image_thumbnail_title": "Podešavanja sličica",
     "job_concurrency": "{job} paralelnost",
+    "job_created": "Posao kreiran",
     "job_not_concurrency_safe": "Ovaj posao nije bezbedan da bude paralelno aktivan.",
     "job_settings": "Podešavanja posla",
     "job_settings_description": "Upravljajte paralelnošću poslova",
@@ -77,9 +89,6 @@
     "jobs_delayed": "{jobCount, plural, one {# odloženi} few {# odložena} other {# odloženih}}",
     "jobs_failed": "{jobCount, plural, one {# neuspešni} few {# neuspešna} other {# neuspešnih}}",
     "library_created": "Napravljena biblioteka: {library}",
-    "library_cron_expression": "Sistemski posao",
-    "library_cron_expression_description": "Podesite interval skeniranja koristeći cron format. Za više informacija pogledajte npr. <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Unapred podešene postavke sistemskog posla",
     "library_deleted": "Biblioteka je izbrisana",
     "library_import_path_description": "Odredite fasciklu za uvoz. Ova fascikla, uključujući podfascikle, biće skenirana za slike i video zapise.",
     "library_scanning": "Periodično skeniranje",
@@ -122,7 +131,7 @@
     "machine_learning_smart_search_description": "Potražite slike semantički koristeći ugrađeni CLIP",
     "machine_learning_smart_search_enabled": "Omogućite pametnu pretragu",
     "machine_learning_smart_search_enabled_description": "Ako je onemogućeno, slike neće biti kodirane za pametnu pretragu.",
-    "machine_learning_url_description": "URL servera za mašinsko učenje",
+    "machine_learning_url_description": "URL servera za mašinsko učenje. Ako je obezbeđeno više URL-ova, svaki server će biti pokušan redom, jedan po jedan, dok jedan ne odgovori uspešno, po redosledu od prvog do poslednjeg.",
     "manage_concurrency": "Upravljanje paralelnošću",
     "manage_log_settings": "Upravljajte podešavanjima evidencije",
     "map_dark_style": "Tamni stil",
@@ -152,7 +161,7 @@
     "note_cannot_be_changed_later": "NAPOMENA: Ovo se kasnije ne može promeniti!",
     "note_unlimited_quota": "Napomena: Unesite 0 za neograničenu kvotu",
     "notification_email_from_address": "Sa adrese",
-    "notification_email_from_address_description": "Adresa e-pošte pošiljaoca, na primer: \"Immich foto server <noreply@immich.app>\"",
+    "notification_email_from_address_description": "Adresa e-pošte pošiljaoca, na primer: \"Immich foto server <noreply@example.com>\"",
     "notification_email_host_description": "Host servera e-pošte (npr. smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Zanemarite greške sertifikata",
     "notification_email_ignore_certificate_errors_description": "Ignorišite greške u validaciji TLS sertifikata (ne preporučuje se)",
@@ -198,22 +207,24 @@
     "password_settings": "Lozinka za prijavu",
     "password_settings_description": "Upravljajte podešavanjima za prijavu lozinkom",
     "paths_validated_successfully": "Sve putanje su uspešno potvrđene",
+    "person_cleanup_job": "Čišćenje osoba",
     "quota_size_gib": "Veličina kvote (GiB)",
     "refreshing_all_libraries": "Osvežavanje svih biblioteka",
     "registration": "Registracija administratora",
     "registration_description": "Pošto ste prvi korisnik na sistemu, bićete dodeljeni kao Admin i odgovorni ste za administrativne zadatke, a dodatne korisnike ćete kreirati vi.",
-    "removing_offline_files": "Uklanjanje vanmrežnih datoteka",
     "repair_all": "Popravi sve",
     "repair_matched_items": "Poklapa se sa {count, plural, one {1 stavkom} few {# stavke} other {# stavki}}",
     "repaired_items": "{count, plural, one {Popravljena 1 stavka} few {Popravljene # stavke} other {Popravljene # stavki}}",
     "require_password_change_on_login": "Zahtevati od korisnika da promeni lozinku pri prvom prijavljivanju",
     "reset_settings_to_default": "Resetujte podešavanja na podrazumevane vrednosti",
     "reset_settings_to_recent_saved": "Resetujte podešavanja na nedavno sačuvana podešavanja",
-    "scanning_library_for_changed_files": "Skeniranje biblioteke za promenjene datoteke",
-    "scanning_library_for_new_files": "Skeniranje biblioteke za nove datoteke",
+    "scanning_library": "Skeniranje biblioteke",
+    "search_jobs": "Traži poslove...",
     "send_welcome_email": "Pošaljite e-poštu dobrodošlice",
     "server_external_domain_settings": "Eksterni domain",
     "server_external_domain_settings_description": "Domain za javne deljene veze, uključujući http(s)://",
+    "server_public_users": "Javni korisnici",
+    "server_public_users_description": "Svi korisnici (ime i adresa e-pošte) su navedeni prilikom dodavanja korisnika u deljene albume. Kada je onemogućena, lista korisnika će biti dostupna samo administratorima.",
     "server_settings": "Podešavanja servera",
     "server_settings_description": "Upravljajte podešavanjima servera",
     "server_welcome_message": "Poruka dobrodošlice",
@@ -238,6 +249,17 @@
     "storage_template_settings_description": "Upravljajte strukturom direktorijuma i imenom datoteke sredstva za otpremanje",
     "storage_template_user_label": "<code>{label}</code> je oznaka za skladištenje korisnika",
     "system_settings": "Podešavanja sistema",
+    "tag_cleanup_job": "Čišćenje oznaka (tags)",
+    "template_email_available_tags": "Možete da koristite sledeće promenljive u svom šablonu: {tags}",
+    "template_email_if_empty": "Ako je šablon prazan, koristiće se podrazumevana adresa e-pošte.",
+    "template_email_invite_album": "Šablon za poziv u album",
+    "template_email_preview": "Pregled",
+    "template_email_settings": "Šabloni e-pošte",
+    "template_email_settings_description": "Upravljajte prilagođenim šablonima obaveštenja putem e-pošte",
+    "template_email_update_album": "Ažurirajte šablon albuma",
+    "template_email_welcome": "Šablon e-pošte dobrodošlice",
+    "template_settings": "Šabloni obaveštenja",
+    "template_settings_description": "Upravljajte prilagođenim šablonima za obaveštenja.",
     "theme_custom_css_settings": "Prilagođeni CSS",
     "theme_custom_css_settings_description": "Kaskadni listovi stilova (CSS) omogućavaju prilagođavanje dizajna Immich-a.",
     "theme_settings": "Podešavanje tema",
@@ -245,7 +267,6 @@
     "these_files_matched_by_checksum": "Ovim datotekama se podudaraju njihovi kontrolni-zbirovi",
     "thumbnail_generation_job": "Generišite sličice",
     "thumbnail_generation_job_description": "Generišite velike, male i zamućene sličice za svako sredstvo, kao i sličice za svaku osobu",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "API za ubrzanje",
     "transcoding_acceleration_api_description": "API koji će komunicirati sa vašim uređajem da bi ubrzao transkodiranje. Ovo podešavanje je 'najbolji napor': vraća se na softversko transkodiranje u slučaju neuspeha. VP9 može ili ne mora da radi u zavisnosti od vašeg hardvera.",
     "transcoding_acceleration_nvenc": "NVENC (zahteva NVIDIA GPU)",
@@ -271,7 +292,7 @@
     "transcoding_hardware_acceleration": "Hardversko ubrzanje",
     "transcoding_hardware_acceleration_description": "Ekperimentalno; mnogo brže, ali će imati niži kvalitet pri istoj brzini prenosa",
     "transcoding_hardware_decoding": "Hardversko dekodiranje",
-    "transcoding_hardware_decoding_setting_description": "Odnosi se samo na NVENC, QSV i RKMPP. Omogućava ubrzanje od kraja do kraja umesto da samo ubrzava kodiranje. Možda neće raditi na svim video snimcima.",
+    "transcoding_hardware_decoding_setting_description": "Omogućava ubrzanje od kraja do kraja umesto da samo ubrzava kodiranje. Možda neće raditi na svim video snimcima.",
     "transcoding_hevc_codec": "HEVC kodek",
     "transcoding_max_b_frames": "Maksimalni B-kadri",
     "transcoding_max_b_frames_description": "Više vrednosti poboljšavaju efikasnost kompresije, ali usporavaju kodiranje. Možda nije kompatibilno sa hardverskim ubrzanjem na starijim uređajima. 0 onemogućava B-kadre, dok -1 automatski postavlja ovu vrednost.",
@@ -297,8 +318,6 @@
     "transcoding_threads_description": "Više vrednosti dovode do bržeg kodiranja, ali ostavljaju manje prostora serveru za obradu drugih zadataka dok je aktivan. Ova vrednost ne bi trebalo da bude veća od broja CPU jezgara. Maksimizira iskorišćenost ako je podešeno na 0.",
     "transcoding_tone_mapping": "Mapiranje (tone-mapping)",
     "transcoding_tone_mapping_description": "Pokušava da se sačuva izgled HDR video zapisa kada se konvertuju u SDR. Svaki algoritam pravi različite kompromise za boju, detalje i osvetljenost. Hable čuva detalje, Mobius čuva boju, a Raeinhard svetlinu.",
-    "transcoding_tone_mapping_npl": "Tone-mapping-NPL",
-    "transcoding_tone_mapping_npl_description": "Boje će biti podešene tako da izgledaju normalno za prikaz ove osvetljenosti. Kontraintuitivno, niže vrednosti povećavaju osvetljenost videa i obrnuto, jer kompenzuju osvetljenost ekrana. 0 automatski postavlja ovu vrednost.",
     "transcoding_transcode_policy": "Uslovi transkodiranja",
     "transcoding_transcode_policy_description": "Uslovi o tome kada video treba transkodirati. HDR video snimci će uvek biti transkodirani (osim ako je transkodiranje onemogućeno).",
     "transcoding_two_pass_encoding": "Dvoprolazno kodiranje",
@@ -312,6 +331,7 @@
     "trash_settings_description": "Upravljajte podešavanjima smeća",
     "untracked_files": "Nepraćene datoteke",
     "untracked_files_description": "Aplikacija ne prati ove datoteke. One mogu nastati zbog neuspešnih premeštenja, zbog prekinutih otpremanja ili kao preostatak zbog greške",
+    "user_cleanup_job": "Čišćenje korisnika",
     "user_delete_delay": "Nalog i datoteke <b>{user}</b> biće zakazani za trajno brisanje za {delay, plural, one {# dan} other {# dana}}.",
     "user_delete_delay_settings": "Izbriši uz kašnjenje",
     "user_delete_delay_settings_description": "Broj dana nakon uklanjanja za trajno brisanje korisničkog naloga i datoteka. Posao brisanja korisnika se pokreće u ponoć da bi se proverili korisnici koji su spremni za brisanje. Promene ove postavke će biti procenjene pri sledećem izvršenju.",
@@ -378,7 +398,6 @@
   "archive_or_unarchive_photo": "Arhivirajte ili poništite arhiviranje fotografije",
   "archive_size": "Veličina arhive",
   "archive_size_description": "Podesi veličinu arhive za preuzimanje (u GiB)",
-  "archived": "Arhivirano",
   "archived_count": "{count, plural, other {Arhivirano #}}",
   "are_these_the_same_person": "Da li su ovo ista osoba?",
   "are_you_sure_to_do_this": "Jeste li sigurni da želite ovo da uradite?",
@@ -389,7 +408,7 @@
   "asset_has_unassigned_faces": "Datoteka ima nedodeljena lica",
   "asset_hashing": "Heširanje...",
   "asset_offline": "Datoteka odsutna",
-  "asset_offline_description": "Ova datoteka je van mreže. Immich ne može da pristupi lokaciji svoje datoteke. Uverite se da je datoteka dostupna, a zatim ponovo skenirajte biblioteku.",
+  "asset_offline_description": "Ova vanjska datoteka se više ne nalazi na disku. Molimo kontaktirajte svog Immich administratora za pomoć.",
   "asset_skipped": "Preskočeno",
   "asset_skipped_in_trash": "U otpad",
   "asset_uploaded": "Otpremljeno (Uploaded)",
@@ -399,11 +418,10 @@
   "assets_added_to_album_count": "Dodato je {count, plural, one {# datoteka} other {# datoteka}} u album",
   "assets_added_to_name_count": "Dodato {count, plural, one {# datoteka} other {# datoteke}} u {hasName, select, true {<b>{name}</b>} other {novi album}}",
   "assets_count": "{count, plural, one {# datoteka} few {# datoteke} other {# datoteka}}",
-  "assets_moved_to_trash": "{count, plural, one {Premeštena # datoteka} few {Premeštene # datoteke} other {Premeštene # datoteka}} u otpad",
   "assets_moved_to_trash_count": "Premešteno {count, plural, one {# datoteka} few {# datoteke} other {# datoteka}} u otpad",
   "assets_permanently_deleted_count": "Trajno izbrisano {count, plural, one {# datoteka} few {# datoteke} other {# datoteka}}",
   "assets_removed_count": "Uklonjeno {count, plural, one {# datoteka} few {# datoteke} other {# datoteka}}",
-  "assets_restore_confirmation": "Da li ste sigurni da želite da vratite sve svoje datoteke koje su u otpadu? Ne možete poništiti ovu radnju!",
+  "assets_restore_confirmation": "Da li ste sigurni da želite da vratite sve svoje datoteke koje su u otpadu? Ne možete poništiti ovu radnju! Imajte na umu da se vanmrežna sredstva ne mogu vratiti na ovaj način.",
   "assets_restored_count": "Vraćeno {count, plural, one {# datoteka} few {# datoteke} other {# datoteka}}",
   "assets_trashed_count": "Bačeno u otpad {count, plural, one {# datoteka} few{# datoteke} other {# datoteka}}",
   "assets_were_part_of_album_count": "{count, plural, one {Datoteka je} other {Datoteke su}} već deo albuma",
@@ -414,7 +432,8 @@
   "birthdate_saved": "Datum rođenja uspešno sačuvan",
   "birthdate_set_description": "Datum rođenja se koristi da bi se izračunale godine ove osobe u dobu određene fotografije.",
   "blurred_background": "Zamućena pozadina",
-  "build": "Sagradi (Build)",
+  "bugs_and_feature_requests": "Greške (bugs) i zahtevi za funkcije",
+  "build": "Pod-verzija (Build)",
   "build_image": "Sagradi (Build) image",
   "bulk_delete_duplicates_confirmation": "Da li ste sigurni da želite grupno da izbrišete {count, plural, one {# dupliran elemenat} few {# duplirana elementa} other {# dupliranih elemenata}}? Ovo će zadržati najveće sredstvo svake grupe i trajno izbrisati sve druge duplikate. Ne možete poništiti ovu radnju!",
   "bulk_keep_duplicates_confirmation": "Da li ste sigurni da želite da zadržite {count, plural, one {1 dupliranu datoteku} few {# duplirane datoteke} other {# dupliranih datoteka}}? Ovo će rešiti sve duplirane grupe bez brisanja bilo čega.",
@@ -428,10 +447,6 @@
   "cannot_merge_people": "Ne može spojiti osobe",
   "cannot_undo_this_action": "Ne možete poništiti ovu radnju!",
   "cannot_update_the_description": "Ne može ažurirati opis",
-  "cant_apply_changes": "Ne može primeniti promene",
-  "cant_get_faces": "Ne može preuzeti lica",
-  "cant_search_people": "Ne može pretražiti osobe",
-  "cant_search_places": "Ne može pretražiti mesta",
   "change_date": "Promeni datum",
   "change_expiration_time": "Promeni vreme isteka",
   "change_location": "Promeni mesto",
@@ -463,6 +478,7 @@
   "confirm": "Potvrdi",
   "confirm_admin_password": "Potvrdi Administrativnu Lozinku",
   "confirm_delete_shared_link": "Da li ste sigurni da želite da izbrišete ovaj deljeni link?",
+  "confirm_keep_this_delete_others": "Sve ostale datoteke u grupi će biti izbrisane osim ove datoteke. Da li ste sigurni da želite da nastavite?",
   "confirm_password": "Ponovo unesi šifru",
   "contain": "Obuhvati",
   "context": "Kontekst",
@@ -512,16 +528,19 @@
   "delete_key": "Izbriši ključ",
   "delete_library": "Obriši biblioteku",
   "delete_link": "Obriši vezu",
+  "delete_others": "Izbrišite druge",
   "delete_shared_link": "Obriši deljenu vezu",
   "delete_tag": "Obriši oznaku (tag)",
   "delete_tag_confirmation_prompt": "Da li stvarno želite da izbrišete oznaku {tagName}?",
   "delete_user": "Obriši korisnika",
   "deleted_shared_link": "Obrišena deljena veza",
+  "deletes_missing_assets": "Briše sredstva koja nedostaju sa diska",
   "description": "Opis",
   "details": "Detalji",
   "direction": "Smer",
   "disabled": "Onemogućeno",
   "disallow_edits": "Zabrani izmene",
+  "discord": "Diskord",
   "discover": "Otkrijte",
   "dismiss_all_errors": "Odbacite sve greške",
   "dismiss_error": "Odbaci grešku",
@@ -530,6 +549,7 @@
   "display_original_photos": "Prikažite originalne fotografije",
   "display_original_photos_setting_description": "Radije prikazujete originalnu fotografiju kada gledate materijal nego sličice kada je originalno delo kompatibilno sa webom. Ovo može dovesti do sporijeg prikaza fotografija.",
   "do_not_show_again": "Ne prikaži ponovo ovu poruku",
+  "documentation": "Dokumentacija",
   "done": "Urađeno",
   "download": "Preuzmi",
   "download_include_embedded_motion_videos": "Ugrađeni video snimci",
@@ -542,13 +562,6 @@
   "duplicates": "Duplikati",
   "duplicates_description": "Razrešite svaku grupu tako što ćete navesti duplikate, ako ih ima",
   "duration": "Trajanje",
-  "durations": {
-    "days": "{days, plural, one {dan} other {{days, number} dana}}",
-    "hours": "{hours, plural, one {sat} other {{hours, number} sata}}",
-    "minutes": "{minutes, plural, one {minut} other {{minutes, number} minuta}}",
-    "months": "{months, plural, one {mesec} other {{months, number} meseci}}",
-    "years": "{years, plural, one {godina} other {{years, number} godina}}"
-  },
   "edit": "Uredi",
   "edit_album": "Uredi album",
   "edit_avatar": "Uredi avatar",
@@ -573,8 +586,6 @@
   "editor_crop_tool_h2_aspect_ratios": "Proporcije (aspect ratios)",
   "editor_crop_tool_h2_rotation": "Rotacija",
   "email": "E-pošta",
-  "empty": "",
-  "empty_album": "Isprazni album",
   "empty_trash": "Ispraznite smeće",
   "empty_trash_confirmation": "Da li ste sigurni da želite da ispraznite smeće? Ovo će trajno ukloniti sve datoteke u smeću iz Immich-a.\nNe možete poništiti ovu radnju!",
   "enable": "Omogući (Enable)",
@@ -608,6 +619,7 @@
     "failed_to_create_shared_link": "Pravljenje deljenog linka nije uspelo",
     "failed_to_edit_shared_link": "Uređivanje deljenog linka nije uspelo",
     "failed_to_get_people": "Neuspelo pozivanje osoba",
+    "failed_to_keep_this_delete_others": "Nije uspelo zadržavanje ovog dela i brisanje ostalih datoteka",
     "failed_to_load_asset": "Učitavanje datoteka nije uspelo",
     "failed_to_load_assets": "Nije uspelo učitavanje datoteka",
     "failed_to_load_people": "Učitavanje osoba nije uspelo",
@@ -635,8 +647,6 @@
     "unable_to_change_location": "Nije moguće promeniti lokaciju",
     "unable_to_change_password": "Nije moguće promeniti lozinku",
     "unable_to_change_visibility": "Nije moguće promeniti vidljivost za {count, plural, one {# osobu} other {# osobe}}",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_complete_oauth_login": "Nije moguće dovršiti OAuth prijavu",
     "unable_to_connect": "Nije moguće povezati se",
     "unable_to_connect_to_server": "Nemoguće je povezati se sa serverom",
@@ -661,6 +671,7 @@
     "unable_to_get_comments_number": "Nije moguće dobiti broj komentara",
     "unable_to_get_shared_link": "Preuzimanje deljene veze nije uspelo",
     "unable_to_hide_person": "Nije moguće sakriti osobu",
+    "unable_to_link_motion_video": "Nije moguće povezati video sa slikom",
     "unable_to_link_oauth_account": "Nije moguće povezati OAuth nalog",
     "unable_to_load_album": "Nije moguće učitati album",
     "unable_to_load_asset_activity": "Nije moguće učitati aktivnost sredstava",
@@ -676,12 +687,10 @@
     "unable_to_remove_album_users": "Nije moguće ukloniti korisnike iz albuma",
     "unable_to_remove_api_key": "Nije moguće ukloniti API ključ (key)",
     "unable_to_remove_assets_from_shared_link": "Nije moguće ukloniti elemente sa deljenog linka",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_deleted_assets": "Nije moguće ukloniti vanmrežne datoteke",
     "unable_to_remove_library": "Nije moguće ukloniti biblioteku",
-    "unable_to_remove_offline_files": "Nije moguće ukloniti vanmrežne datoteke",
     "unable_to_remove_partner": "Nije moguće ukloniti partnera",
     "unable_to_remove_reaction": "Nije moguće ukloniti reakciju",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "Nije moguće popraviti stavke",
     "unable_to_reset_password": "Nije moguće resetovati lozinku",
     "unable_to_resolve_duplicate": "Nije moguće razrešiti duplikat",
@@ -701,6 +710,7 @@
     "unable_to_submit_job": "Nije moguće predati zadatak",
     "unable_to_trash_asset": "Nije moguće izbaciti materijal u otpad",
     "unable_to_unlink_account": "Nije moguće raskinuti profil",
+    "unable_to_unlink_motion_video": "Nije moguće odvezati video od slike",
     "unable_to_update_album_cover": "Nije moguće ažurirati naslovnicu albuma",
     "unable_to_update_album_info": "Nije moguće ažurirati informacije o albumu",
     "unable_to_update_library": "Nije moguće ažurirati biblioteku",
@@ -710,10 +720,6 @@
     "unable_to_update_user": "Nije moguće ažurirati korisnika",
     "unable_to_upload_file": "Nije moguće otpremiti datoteku"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exif": "EXIF",
   "exit_slideshow": "Izađi iz projekcije slajdova",
   "expand_all": "Proširi sve",
@@ -728,33 +734,28 @@
   "external": "Spoljašnji",
   "external_libraries": "Spoljašnje Biblioteke",
   "face_unassigned": "Neraspoređeni",
-  "failed_to_get_people": "Neuspešno isčitavanje osoba",
+  "failed_to_load_assets": "Datoteke nisu uspešno učitane",
   "favorite": "Favorit",
   "favorite_or_unfavorite_photo": "Omiljena ili neomiljena fotografija",
   "favorites": "Favoriti",
-  "feature": "",
   "feature_photo_updated": "Glavna fotografija je ažurirana",
-  "featurecollection": "",
   "features": "Funkcije (features)",
   "features_setting_description": "Upravljajte funkcijama aplikacije",
   "file_name": "Naziv dokumenta",
   "file_name_or_extension": "Ime datoteke ili ekstenzija",
   "filename": "Ime datoteke",
-  "files": "",
   "filetype": "Vrsta dokumenta",
   "filter_people": "Filtriranje osoba",
   "find_them_fast": "Brzo ih pronađite po imenu pomoću pretrage",
   "fix_incorrect_match": "Ispravite netačno podudaranje",
   "folders": "Fascikle (Folders)",
   "folders_feature_description": "Pregledavanje prikaza fascikle za fotografije i video zapisa u sistemu datoteka",
-  "force_re-scan_library_files": "Prinudno ponovo skenirajte sve datoteke biblioteke",
   "forward": "Napred",
   "general": "Generalno",
   "get_help": "Nađi pomoć",
   "getting_started": "Počinjem",
   "go_back": "Vrati se",
   "go_to_search": "Idi na pretragu",
-  "go_to_share_page": "Idi na stranicu za deljenje",
   "group_albums_by": "Grupni albumi po...",
   "group_no": "Bez grupisanja",
   "group_owner": "Grupirajte po vlasniku",
@@ -780,10 +781,6 @@
   "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} sa {person1} i {person2} {date}",
   "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} snimljenou {city}, {country} sa {person1}, {person2} i {person3} {date}",
   "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} sa {person1}, {person2} i još {additionalCount, number} drugih {date}",
-  "image_alt_text_people": "{count, plural, =1 {sa {person1}} =2 {sa {person1} i {person2}} =3 {sa {person1}, {person2}, i {person3}} other {sa {person1}, {person2}, i {others, number} others}}",
-  "image_alt_text_place": "u {city}, {country}",
-  "image_taken": "{isVideo, select, true {Video zapis snimljen} other {Fotografija uslikana}}",
-  "img": "",
   "immich_logo": "Logo Immich-a",
   "immich_web_interface": "Web interfejs Immich-a",
   "import_from_json": "Uvezi iz JSON-a",
@@ -804,10 +801,11 @@
   "invite_people": "Pozovite ljude",
   "invite_to_album": "Pozovi na album",
   "items_count": "{count, plural, one {# datoteka} other {# datoteka}}",
-  "job_settings_description": "",
   "jobs": "Poslovi",
   "keep": "Zadrži",
   "keep_all": "Zadrži sve",
+  "keep_this_delete_others": "Zadrži ovo, izbriši druge",
+  "kept_this_deleted_others": "Zadržana je ova datoteka i izbrisano {count, plural, one {# datoteka} other {# datoteka}}",
   "keyboard_shortcuts": "Prečice na tastaturi",
   "language": "Jezik",
   "language_setting_description": "Izaberite željeni jezik",
@@ -819,31 +817,6 @@
   "level": "Nivo",
   "library": "Biblioteka",
   "library_options": "Opcije biblioteke",
-  "license_account_info": "Vaš nalog je licenciran",
-  "license_activated_subtitle": "Hvala vam što podržavate Immich i softver otvorenog koda",
-  "license_activated_title": "Vaša licenca je uspešno aktivirana",
-  "license_button_activate": "Aktiviraj",
-  "license_button_buy": "Kupi",
-  "license_button_buy_license": "Kupi Licencu",
-  "license_button_select": "Odaberi",
-  "license_failed_activation": "Aktivacija licence nije uspela. Proverite svoju e-poštu da biste pronašli ispravan ključ licence!",
-  "license_individual_description_1": "1 licenca po korisniku na bilo kom serveru",
-  "license_individual_title": "Individualna licenca",
-  "license_info_licensed": "Licencirano",
-  "license_info_unlicensed": "Bez licence",
-  "license_input_suggestion": "Imate licencu? Unesite ključ ispod",
-  "license_license_subtitle": "Kupite licencu za podršku Immich-a",
-  "license_license_title": "LICENCA",
-  "license_lifetime_description": "Doživotna licenca",
-  "license_per_server": "Po serveru",
-  "license_per_user": "Po korisniku",
-  "license_server_description_1": "1 licenca po serveru",
-  "license_server_description_2": "Licenca za sve korisnike na serveru",
-  "license_server_title": "Serverska licenca",
-  "license_trial_info_1": "Koristite nelicenciranu verziju Immich-a",
-  "license_trial_info_2": "Koristili ste Immich otprilike",
-  "license_trial_info_3": "{accountAge, plural, one {# dan} other {# dana}}",
-  "license_trial_info_4": "Molimo vas da razmislite o kupovini licence za podršku kontinuiranom razvoju usluge",
   "light": "Svetlo",
   "like_deleted": "Lajkuj izbrisano",
   "link_motion_video": "Napravi vezu za video zapis",
@@ -865,6 +838,7 @@
   "look": "Pogledaj",
   "loop_videos": "Ponavljajte video zapise",
   "loop_videos_description": "Omogućite za automatsko ponavljanje video zapisa u pregledniku detalja.",
+  "main_branch_warning": "Upotrebljavate razvojnu verziju; strogo preporučujemo upotrebu izdate verzije!",
   "make": "Kreiraj",
   "manage_shared_links": "Upravljajte deljenim vezama",
   "manage_sharing_with_partners": "Upravljajte deljenjem sa partnerima",
@@ -934,6 +908,7 @@
   "notifications": "Notifikacije",
   "notifications_setting_description": "Upravljajte obaveštenjima",
   "oauth": "OAuth",
+  "official_immich_resources": "Zvanični Immich resursi",
   "offline": "Odsutan (Offline)",
   "offline_paths": "Nedostupne (Offline) putanje",
   "offline_paths_description": "Ovi rezultati mogu biti posledica ručnog brisanja datoteka koje nisu deo spoljne biblioteke.",
@@ -946,7 +921,6 @@
   "onboarding_welcome_user": "Dobrodošli, {user}",
   "online": "Dostupan (Online)",
   "only_favorites": "Samo favoriti",
-  "only_refreshes_modified_files": "Osvežava samo izmenjene datoteke",
   "open_in_map_view": "Otvorite u prikaz karte",
   "open_in_openstreetmap": "Otvorite u OpenStreetMap-u",
   "open_the_search_filters": "Otvorite filtere za pretragu",
@@ -984,14 +958,12 @@
   "people_edits_count": "Izmenjeno {count, plural, one {# osoba} other {# osobe}}",
   "people_feature_description": "Pregledavanje fotografija i video snimaka grupisanih po osobama",
   "people_sidebar_description": "Prikažite vezu do osoba na bočnoj traci",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "Upozorenje za trajno brisanje",
   "permanent_deletion_warning_setting_description": "Prikaži upozorenje kada trajno brišete datoteke",
   "permanently_delete": "Trajno izbrisati",
   "permanently_delete_assets_count": "Trajno izbriši {count, plural, one {datoteku} other {datoteke}}",
   "permanently_delete_assets_prompt": "Da li ste sigurni da želite da trajno izbrišete {count, plural, one {ovu datoteku?} other {ove <b>#</b> datoteke?}}Ovo će ih takođe ukloniti {count, plural, one {iz njihovog} other {iz njihovih}} albuma.",
   "permanently_deleted_asset": "Trajno izbrisana datoteka",
-  "permanently_deleted_assets": "Trajno izbrisano {count, plural, one {# datoteka} other {# datoteke}}",
   "permanently_deleted_assets_count": "Trajno izbrisano {count, plural, one {# datoteka} other {# datoteke}}",
   "person": "Osoba",
   "person_hidden": "{name}{hidden, select, true { (skriveno)} other {}}",
@@ -1007,7 +979,6 @@
   "play_memories": "Pokreni sećanja",
   "play_motion_photo": "Pokreni pokretnu fotografiju",
   "play_or_pause_video": "Pokreni ili pauziraj video zapis",
-  "point": "",
   "port": "port",
   "preset": "Unapred podešeno",
   "preview": "Pregled",
@@ -1052,12 +1023,10 @@
   "purchase_server_description_2": "Status podrške",
   "purchase_server_title": "Server",
   "purchase_settings_server_activated": "Ključem proizvoda servera upravlja administrator",
-  "range": "",
   "rating": "Ocena zvezdica",
   "rating_clear": "Obriši ocenu",
   "rating_count": "{count, plural, one {# zvezda} other {# zvezde}}",
   "rating_description": "Prikažite EXIF ocenu u info panelu",
-  "raw": "",
   "reaction_options": "Opcije reakcije",
   "read_changelog": "Pročitajte dnevnik promena",
   "reassign": "Ponovo dodaj",
@@ -1065,14 +1034,17 @@
   "reassigned_assets_to_new_person": "Ponovo dodeljeno {count, plural, one {# datoteka} other {# datoteke}} novoj osobi",
   "reassing_hint": "Dodelite izabrana sredstva postojećoj osobi",
   "recent": "Skorašnji",
+  "recent-albums": "Nedavni albumi",
   "recent_searches": "Skorašnje pretrage",
   "refresh": "Osveži",
   "refresh_encoded_videos": "Osvežite kodirane (encoded) video zapise",
+  "refresh_faces": "Osveži lica",
   "refresh_metadata": "Osvežite metapodatke",
   "refresh_thumbnails": "Osvežite sličice",
   "refreshed": "Osveženo",
-  "refreshes_every_file": "Osvežava svaku datoteku",
+  "refreshes_every_file": "Ponovo čita sve postojeće i nove datoteke",
   "refreshing_encoded_video": "Osvežavanje kodiranog (encoded) videa",
+  "refreshing_faces": "Osvežavanje lica",
   "refreshing_metadata": "Osvežavanje meta-podataka",
   "regenerating_thumbnails": "Obnavljanje sličica",
   "remove": "Ukloni",
@@ -1080,10 +1052,11 @@
   "remove_assets_shared_link_confirmation": "Da li ste sigurni da želite da uklonite {count, plural, one {# datoteku} other {# datoteke}} sa ove deljene veze?",
   "remove_assets_title": "Ukloniti datoteke?",
   "remove_custom_date_range": "Uklonite prilagođeni period",
+  "remove_deleted_assets": "Uklonite vanmrežne (offline) datoteke",
   "remove_from_album": "Obriši iz albuma",
   "remove_from_favorites": "Ukloni iz favorita",
   "remove_from_shared_link": "Uklonite sa deljene veze",
-  "remove_offline_files": "Uklonite vanmrežne (offline) datoteke",
+  "remove_url": "Ukloni URL",
   "remove_user": "Ukloni korisnika",
   "removed_api_key": "Uklonjen API ključ (key): {name}",
   "removed_from_archive": "Uklonjeno iz arhive",
@@ -1100,7 +1073,6 @@
   "reset": "Resetovati",
   "reset_password": "Resetovati lozinku",
   "reset_people_visibility": "Resetujte vidljivost osoba",
-  "reset_settings_to_default": "",
   "reset_to_default": "Resetujte na podrazumevane vrednosti",
   "resolve_duplicates": "Reši duplikate",
   "resolved_all_duplicates": "Svi duplikati su razrešeni",
@@ -1120,8 +1092,7 @@
   "saved_settings": "Sačuvana podešavanja",
   "say_something": "Reci nešto",
   "scan_all_libraries": "Skeniraj sve biblioteke",
-  "scan_all_library_files": "Ponovo skenirajte sve datoteke biblioteke",
-  "scan_new_library_files": "Skenirajte nove datoteke biblioteke",
+  "scan_library": "Skeniraj",
   "scan_settings": "Podešavanja skeniranja",
   "scanning_for_album": "Skeniranje albuma...",
   "search": "Pretraga",
@@ -1139,6 +1110,7 @@
   "search_options": "Opcije pretrage",
   "search_people": "Pretraži osobe",
   "search_places": "Pretraži mesta",
+  "search_settings": "Pretraga podešavanja",
   "search_state": "Traži region...",
   "search_tags": "Pretraži oznake (tags)...",
   "search_timezone": "Pretraži vremensku zonu...",
@@ -1163,7 +1135,6 @@
   "selected_count": "{count, plural, other {# izabrano}}",
   "send_message": "Pošalji poruku",
   "send_welcome_email": "Pošaljite e-poštu dobrodošlice",
-  "server": "Server",
   "server_offline": "Server van mreže (offline)",
   "server_online": "Server na mreži (online)",
   "server_stats": "Statistika servera",
@@ -1206,6 +1177,7 @@
   "show_person_options": "Prikaži opcije osobe",
   "show_progress_bar": "Prikaži traku napretka",
   "show_search_options": "Prikaži opcije pretrage",
+  "show_slideshow_transition": "Prikaži prelaz projekcije slajdova",
   "show_supporter_badge": "Značka podrške",
   "show_supporter_badge_description": "Pokažite značku podrške",
   "shuffle": "Mešanje",
@@ -1247,13 +1219,16 @@
   "submit": "Dostavi",
   "suggestions": "Sugestije",
   "sunrise_on_the_beach": "Izlazak sunca na plaži",
+  "support": "Podrška",
+  "support_and_feedback": "Podrška i povratne informacije",
+  "support_third_party_description": "Vaša immich instalacija je spakovana od strane treće strane. Problemi sa kojima se suočavate mogu biti uzrokovani tim paketom, pa vas molimo da im prvo postavite probleme koristeći donje veze.",
   "swap_merge_direction": "Zamenite pravac spajanja",
   "sync": "Sinhronizacija",
   "tag": "Oznaka (tag)",
   "tag_assets": "Označite (tag) sredstva",
   "tag_created": "Napravljena oznaka (tag): {tag}",
   "tag_feature_description": "Pregledavanje fotografija i video snimaka grupisanih po logičnim temama oznaka",
-  "tag_not_found_question": "Ne možete da pronađete oznaku (tag)? Napravite jednu <link>ovde</link>",
+  "tag_not_found_question": "Ne možete da pronađete oznaku (tag)? <link>Napravite novu oznaku</link>",
   "tag_updated": "Ažurirana oznaka (tag): {tag}",
   "tagged_assets": "Označeno (tagged) {count, plural, one {# datoteka} other {# datoteke}}",
   "tags": "Oznake (tags)",
@@ -1262,18 +1237,19 @@
   "theme_selection": "Izbor teme",
   "theme_selection_description": "Automatski postavite temu na svetlu ili tamnu na osnovu sistemskih preferencija vašeg pretraživača",
   "they_will_be_merged_together": "Oni će biti spojeni zajedno",
+  "third_party_resources": "Resursi trećih strana",
   "time_based_memories": "Sećanja zasnovana na vremenu",
+  "timeline": "Vremenska linija",
   "timezone": "Vremenska zona",
   "to_archive": "Arhiviraj",
   "to_change_password": "Promeni lozinku",
   "to_favorite": "Postavi kao favorit",
   "to_login": "Prijava",
   "to_parent": "Vrati se nazad",
-  "to_root": "Na početak",
   "to_trash": "Smeće",
   "toggle_settings": "Namesti podešavanja",
   "toggle_theme": "Namesti tamnu temu",
-  "toggle_visibility": "Namesti vidljivost",
+  "total": "Ukupno",
   "total_usage": "Ukupna upotreba",
   "trash": "Otpad",
   "trash_all": "Baci sve u otpad",
@@ -1283,14 +1259,13 @@
   "trashed_items_will_be_permanently_deleted_after": "Datoteke u otpadu će biti trajno izbrisane nakon {days, plural, one {# dan} few {# dana} other {# dana}}.",
   "type": "Vrsta",
   "unarchive": "Vrati iz arhive",
-  "unarchived": "Vraćeno iz arhive",
   "unarchived_count": "{count, plural, other {Nearhivirano#}}",
   "unfavorite": "Izbaci iz omiljenih (unfavorite)",
   "unhide_person": "Otkrij osobu",
   "unknown": "Nepoznat",
-  "unknown_album": "Nepoznat Album",
   "unknown_year": "Nepoznata Godina",
   "unlimited": "Neograničeno",
+  "unlink_motion_video": "Odveži video od slike",
   "unlink_oauth": "Prekini vezu sa Oauth-om",
   "unlinked_oauth_account": "Opozvana veza OAuth naloga",
   "unnamed_album": "Neimenovani album",
@@ -1319,13 +1294,13 @@
   "use_custom_date_range": "Umesto toga koristite prilagođeni period",
   "user": "Korisnik",
   "user_id": "ID korisnika",
-  "user_license_settings": "Licenca",
-  "user_license_settings_description": "Upravljajte svojom licencom",
   "user_liked": "{user} je lajkovao {type, select, photo {ovu fotografiju} video {ovaj video zapis} asset {ovu datoteku} other {ovo}}",
   "user_purchase_settings": "Kupovina",
   "user_purchase_settings_description": "Upravljajte kupovinom",
   "user_role_set": "Postavi {user} kao {role}",
   "user_usage_detail": "Detalji korišćenja korisnika",
+  "user_usage_stats": "Statistika korišćenja naloga",
+  "user_usage_stats_description": "Pogledajte statistiku korišćenja naloga",
   "username": "Korisničko ime",
   "users": "Korisnici",
   "utilities": "Alati",
@@ -1333,7 +1308,9 @@
   "variables": "Promenljive (variables)",
   "version": "Verzija",
   "version_announcement_closing": "Tvoj prijatelj, Aleks",
-  "version_announcement_message": "Zdravo prijatelju, postoji nova verzija aplikacije, molimo vas da odvojite vreme da posetite <link>napomene o izdanju</link> i uverite se u svoje <code>docker-compose.yml</code>, i <code>.env</code> podešavanje je ažurirano kako bi se sprečile bilo kakve pogrešne konfiguracije, posebno ako koristite WatchTower ili bilo koji mehanizam koji automatski upravlja ažuriranjem vaše aplikacije.",
+  "version_announcement_message": "Zdravo prijatelju, postoji nova verzija aplikacije, molimo vas da odvojite vreme da posetite <link>napomene o izdanju</link> i uverite se da je server ažuriran kako bi se sprečile bilo kakve pogrešne konfiguracije, posebno ako koristite WatchTower ili bilo koji mehanizam koji automatski upravlja ažuriranjem vaše aplikacije.",
+  "version_history": "Istorija verzija",
+  "version_history_item": "Instalirano {version} {date}",
   "video": "Video zapis",
   "video_hover_setting": "Pusti sličicu videa kada lebdi",
   "video_hover_setting_description": "Pusti sličicu videa kada miš pređe preko stavke. Čak i kada je onemogućena, reprodukcija se može pokrenuti prelaskom miša preko ikone za reprodukciju.",
@@ -1345,10 +1322,10 @@
   "view_all_users": "Prikaži sve korisnike",
   "view_in_timeline": "Prikaži u vremenskoj liniji",
   "view_links": "Prikaži veze",
+  "view_name": "Pogledati",
   "view_next_asset": "Pogledajte sledeću datoteku",
   "view_previous_asset": "Pogledaj prethodnu datoteku",
   "view_stack": "Prikaži gomilu",
-  "viewer": "Preglednik (viewer)",
   "visibility_changed": "Vidljivost je promenjena za {count, plural, one {# osobu} other {# osobe}}",
   "waiting": "Čekam",
   "warning": "Upozorenje",
diff --git a/web/src/lib/i18n/sv.json b/i18n/sv.json
similarity index 55%
rename from web/src/lib/i18n/sv.json
rename to i18n/sv.json
index cebb74377e..f774f3a01d 100644
--- a/web/src/lib/i18n/sv.json
+++ b/i18n/sv.json
@@ -1,5 +1,5 @@
 {
-  "about": "Om",
+  "about": "Uppdatera",
   "account": "Konto",
   "account_settings": "Kontoinställningar",
   "acknowledge": "Bekräfta",
@@ -23,16 +23,23 @@
   "add_to": "Lägg till...",
   "add_to_album": "Lägg till i album",
   "add_to_shared_album": "Lägg till i delat album",
+  "add_url": "Lägg till URL",
   "added_to_archive": "Tillagd i arkiv",
   "added_to_favorites": "Tillagd till favoriter",
   "added_to_favorites_count": "{count, number} tillagda till favoriter",
   "admin": {
     "add_exclusion_pattern_description": "Lägg till exkluderande mönster. Matchning med jokertecken *, ** samt ? är supporterat. För att ignorera alla filer i samtliga mappar som heter \"Raw\", använd \"**/Raw/**\". För att ignorera alla filer som slutar med \".tif\", använd \"**/*.tif\". För att ignorera en absolut sökväg, använd \"/sökväg/att/ignorera/**\".",
+    "asset_offline_description": "Denna externa bibliotekstillgång finns inte längre på disken och har flyttats till papperskorgen. Om filen flyttades inom biblioteket, kontrollera din tidslinje för den nya motsvarande tillgången. För att återställa denna tillgång, se till att filsökvägen nedan kan nås av Immich och skanna biblioteket.",
     "authentication_settings": "Autentiseringsinställningar",
     "authentication_settings_description": "Hantera lösenord, OAuth, och andra autentiseringsinställningar",
     "authentication_settings_disable_all": "Är du säker på att du vill inaktivera alla inloggningsmetoder? Inloggning kommer att helt inaktiveras.",
     "authentication_settings_reenable": "För att återaktivera, använd <link>Server Command</link>.",
     "background_task_job": "Bakgrundsaktiviteter",
+    "backup_database": "Databassäkerhetskopia",
+    "backup_database_enable_description": "Slå på säkerhetskopia",
+    "backup_keep_last_amount": "Antal säkerhetskopior att behålla",
+    "backup_settings": "Säkerhetskopieringsinställningar",
+    "backup_settings_description": "Hantera inställningar för säkerhetskopiering av databas",
     "check_all": "Välj alla",
     "cleared_jobs": "Rensade jobben för:{job}",
     "config_set_by_file": "Konfigurationen är satt av en konfigurationsfil",
@@ -41,9 +48,11 @@
     "confirm_email_below": "För att bekräfta, skriv ”{email}” nedan",
     "confirm_reprocess_all_faces": "Är du säker på att du vill återprocessa alla ansikten? Detta kommer också rensa namngivna personer.",
     "confirm_user_password_reset": "Är du säker på att du vill återställa {user}’s lösenord?",
-    "crontab_guru": "Crontab-guru",
+    "create_job": "Skapa jobb",
+    "cron_expression": "Cron uttryck",
+    "cron_expression_description": "Sätt skanningsintervall genom att använda cron format. För mer information se <link>Crontab Guru</link>",
+    "cron_expression_presets": "Cron uttryck förinställningar",
     "disable_login": "Inaktivera inloggning",
-    "disabled": "Inaktiverad",
     "duplicate_detection_job_description": "Kör maskininlärning på objekt för att upptäcka liknande bilder. Bygger på Smart Search",
     "exclusion_pattern_description": "Exkluderingsmönster tillåter dig att ignorera filer och mappar när skanning görs av ditt album. Detta är användbart om du har mappar som innehåller filer som du inte vill importera, t.ex. RAW-filer.",
     "external_library_created_at": "Externt bibliotek (skapat den {date})",
@@ -54,22 +63,25 @@
     "failed_job_command": "Kommando {command} misslyckades för jobb: {job}",
     "force_delete_user_warning": "VARNING: Detta tar omedelbart bort användaren och alla mediafiler. Detta kan inte ångras och filerna kan inte återställas.",
     "forcing_refresh_library_files": "Tvingar uppdatering av alla biblioteksfiler",
+    "image_format": "Format",
     "image_format_description": "WebP producerar mindre filer än JPEG, men kodas långsammare.",
     "image_prefer_embedded_preview": "Föredra inbäddad förhandsgranskning",
     "image_prefer_embedded_preview_setting_description": "Använd inbäddade förhandsvisningar i RAW-foton som indata till bildbehandling när det är tillgängligt. Detta kan ge mer exakta färger för vissa bilder, men kvaliteten på förhandsgranskningen är kameraberoende och bilden kan ha fler komprimeringsartefakter.",
     "image_prefer_wide_gamut": "Föredrar brett spektrum",
     "image_prefer_wide_gamut_setting_description": "Använd Display P3 för miniatyrer. Detta bevarar livfullheten bättre hos bilder med bred färgrymd, men bilder kan se annorlunda ut på gamla enheter med en gammal webbläsarversion. Med sRGB-bilder behålls i sitt format sRGB för att undvika färgskiftningar.",
-    "image_preview_format": "Förhandsgranskningsformat",
-    "image_preview_resolution": "Förhandsgranska upplösning",
-    "image_preview_resolution_description": "Används vid visning av ett enstaka foto och för maskininlärning. Högre upplösningar kan bevara fler detaljer men tar längre tid att koda, har större filstorlekar och kan minska appens responsiva känsla.",
+    "image_preview_description": "Mellanstor bild med avskalad metadata, används vid visning av en enskild tillgång och för maskininlärning",
+    "image_preview_quality_description": "Förhandsgranska kvalitet från 1-100. Högre är bättre, men ger större filer och kan minska appens känslighet. Att ställa in ett lågt värde kan påverka kvaliteten på maskininlärning.",
+    "image_preview_title": "Förhandsvisningsinställningar",
     "image_quality": "Kvalitet",
-    "image_quality_description": "Bildkvalitet från 1-100. Högre är bättre för kvaliteten men ger större filer, det här alternativet påverkar förhandsgranskningen och miniatyrbilderna.",
+    "image_resolution": "Upplösning",
+    "image_resolution_description": "Högre upplösningar kan bevara fler detaljer men tar längre tid att koda, har större filstorlekar och kan minska appens känslighet.",
     "image_settings": "Bildinställningar",
     "image_settings_description": "Hantera kvalitet och upplösning på genererade bilder",
-    "image_thumbnail_format": "Miniatyrformat",
-    "image_thumbnail_resolution": "Miniatyrbildsupplösning",
-    "image_thumbnail_resolution_description": "Används när du tittar på grupper av foton (huvudtidslinje, albumvy, etc.). Högre upplösningar kan bevara fler detaljer men tar längre tid att koda, har större filstorlekar och kan minska appens responsiva känsla.",
+    "image_thumbnail_description": "Liten miniatyrbild med avskalad metadata, används när du tittar på grupper av foton som huvudtidslinjen",
+    "image_thumbnail_quality_description": "Miniatyrkvalitet från 1-100. Högre är bättre, men ger större filer och kan minska appens känslighet.",
+    "image_thumbnail_title": "Miniatyrbildsinställningar",
     "job_concurrency": "{job} Samtidighet",
+    "job_created": "Jobb skapat",
     "job_not_concurrency_safe": "Det här jobbet är inte samtidighetssäkert.",
     "job_settings": "Jobbinställningar",
     "job_settings_description": "Hantera samtidiga jobb",
@@ -77,9 +89,6 @@
     "jobs_delayed": "{jobCount, plural, other {# försenad}}",
     "jobs_failed": "{jobCount, plural, other {# misslyckades}}",
     "library_created": "Skapat bibliotek: {library}",
-    "library_cron_expression": "Cron-uttryck",
-    "library_cron_expression_description": "Ställ in intervallet för skanningen med cron-formatet. För mer information gå till t.ex. <link>Crontab Guru </link>",
-    "library_cron_expression_presets": "Cron-uttrycksförinställningar",
     "library_deleted": "Biblioteket har tagits bort",
     "library_import_path_description": "Ange en mapp att importera. Den här mappen, inklusive undermappar, skannas efter bilder och videor.",
     "library_scanning": "Periodisk skanning",
@@ -122,7 +131,7 @@
     "machine_learning_smart_search_description": "Sök semantiskt efter bilder med hjälp av CLIP-inbäddningar",
     "machine_learning_smart_search_enabled": "Aktivera smart sökning",
     "machine_learning_smart_search_enabled_description": "Om inaktiverat kommer bilder inte att kodas för smart sökning.",
-    "machine_learning_url_description": "Maskininlärningsserverns URL",
+    "machine_learning_url_description": "Maskininlärningsserverns URL. Om det är mer än en URL tillagd så kommer ett försök per URL att utföras tills någon av dom svarar, försöken görs i kronologisk ordning.",
     "manage_concurrency": "Hantera samtidighet",
     "manage_log_settings": "Hantera logginställningar",
     "map_dark_style": "Mörk stil",
@@ -152,7 +161,7 @@
     "note_cannot_be_changed_later": "OBS: Detta kan inte ändras i efterhand!",
     "note_unlimited_quota": "OBS: Skriv 0 för obegränsad kvota",
     "notification_email_from_address": "Från adress",
-    "notification_email_from_address_description": "Avsändarens epost, t.ex.: \"Immich Fotoserver <noreply@immich.app>\"",
+    "notification_email_from_address_description": "Avsändarens epost, t.ex.: \"Immich Fotoserver <noreply@example.com>\"",
     "notification_email_host_description": "Värd för epostservern (t.ex. smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Ignorera certifikatfel",
     "notification_email_ignore_certificate_errors_description": "Ignorera valideringsfel för TLS-certifikat (rekommenderas ej)",
@@ -198,22 +207,24 @@
     "password_settings": "Lösenordsinloggning",
     "password_settings_description": "Hantera inställningar för lösenords-inloggning",
     "paths_validated_successfully": "Samtliga sökvägar kunde bekräftas",
+    "person_cleanup_job": "Person rensning",
     "quota_size_gib": "Lagringskvot (GiB)",
     "refreshing_all_libraries": "Samtliga bibliotek uppdateras",
     "registration": "Administratörsregistrering",
     "registration_description": "Du utses till administratör eftersom du är systemets första användare. Du ansvarar för administration och kan skapa ytterligare användare.",
-    "removing_offline_files": "Tar bort offline-filer",
     "repair_all": "Reparera alla",
     "repair_matched_items": "Matchade {count, plural, one {# föremål} other {# föremål}}",
     "repaired_items": "Reparerade {count, plural, one {# item} other {# items}}",
     "require_password_change_on_login": "Kräv av användaren att byta lösenord vid första inloggning",
     "reset_settings_to_default": "Återställ inställningar till standard",
     "reset_settings_to_recent_saved": "Återställ inställningar till de senaste sparade",
-    "scanning_library_for_changed_files": "Scannar bibliotek efter ändrade filer",
-    "scanning_library_for_new_files": "Skannar biblioteket efter nya filer",
+    "scanning_library": "Skanna bibliotek",
+    "search_jobs": "Sök Jobb...",
     "send_welcome_email": "Skicka välkomstmail",
     "server_external_domain_settings": "Extern domän",
     "server_external_domain_settings_description": "Domän för publikt delade länkar, inklusive http(s)://",
+    "server_public_users": "Vanlig användare",
+    "server_public_users_description": "Alla användare (namn och e-post) är listade när man lägger till en användare till ett delat album. Om inaktiverat, kommer användarlistan endast vara synlig för administratörer.",
     "server_settings": "Serverinställningar",
     "server_settings_description": "Hantera serverinställningar",
     "server_welcome_message": "Välkomstmeddelande",
@@ -238,6 +249,17 @@
     "storage_template_settings_description": "Hantera mappstruktur och filnamn för uppladdade resurser",
     "storage_template_user_label": "<code>{label}</code> är användarens lagringsmärkning",
     "system_settings": "Systeminställningar",
+    "tag_cleanup_job": "Markera för rensning",
+    "template_email_available_tags": "Du kan använda följande variablar i din mall: {tags}",
+    "template_email_if_empty": "Om mallen är tom, kommer standard e-post att användas.",
+    "template_email_invite_album": "Inbjudan Album Mall",
+    "template_email_preview": "Förhandsgranskning",
+    "template_email_settings": "E-post mall",
+    "template_email_settings_description": "Hantera anpassad e-postavisering mall.",
+    "template_email_update_album": "Uppdatera Album Mall",
+    "template_email_welcome": "Välkommen e-post mall",
+    "template_settings": "Notifikations Mall",
+    "template_settings_description": "Hantera anpassade mallar för notifikationer.",
     "theme_custom_css_settings": "Anpassad CSS",
     "theme_custom_css_settings_description": "Cascading Style Sheets möjliggör designanpassningar av Immich",
     "theme_settings": "Temainställningar",
@@ -245,7 +267,6 @@
     "these_files_matched_by_checksum": "Dessa filer matchas av deras kontrollsummor",
     "thumbnail_generation_job": "Generera Miniatyrer",
     "thumbnail_generation_job_description": "Generera stora, små och suddiga miniatyrer för varje objekt, samt för varje person",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "Accelerations-API",
     "transcoding_acceleration_api_description": "API som kommer att interagera med din enhet för att accelerera omkodning. Inställning är 'best effort': vid fel kommer den att återgå till mjukvarubaserad omkodning. VP9 kan fungera eller inte, beroende på din hårdvara.",
     "transcoding_acceleration_nvenc": "NVENC (kräver NVIDIA GPU)",
@@ -289,332 +310,538 @@
     "transcoding_required_description": "Enbart videos som inte är ett accepterat format",
     "transcoding_settings": "Inställningar för omkodning av video",
     "transcoding_settings_description": "Hantera upplösningen och kodningen av videofiler",
-    "transcoding_target_resolution": "",
+    "transcoding_target_resolution": "Förväntad upplösning",
     "transcoding_target_resolution_description": "En högre upplösning kan bevara fler detaljer men kan ta längre tid at koda, ha större fil storlek och kan försämra appens svarstid.",
-    "transcoding_temporal_aq": "",
-    "transcoding_temporal_aq_description": "",
+    "transcoding_temporal_aq": "Temporär AQ",
+    "transcoding_temporal_aq_description": "Gäller endast NVENC. Ökar kvaliteten på scener med hög detaljrikedom och låg rörelse. Kanske inte är kompatibel med äldre enheter.",
     "transcoding_threads": "Trådar",
-    "transcoding_threads_description": "",
-    "transcoding_tone_mapping": "",
-    "transcoding_tone_mapping_description": "",
-    "transcoding_tone_mapping_npl": "",
-    "transcoding_tone_mapping_npl_description": "",
-    "transcoding_transcode_policy": "",
-    "transcoding_two_pass_encoding": "",
-    "transcoding_two_pass_encoding_setting_description": "",
-    "transcoding_video_codec": "",
-    "transcoding_video_codec_description": "",
-    "trash_enabled_description": "",
-    "trash_number_of_days": "",
-    "trash_number_of_days_description": "",
-    "trash_settings": "",
-    "trash_settings_description": "",
-    "user_delete_delay_settings": "",
-    "user_delete_delay_settings_description": "",
+    "transcoding_threads_description": "Högre värden leder till snabbare kodning, men lämnar mindre utrymme för servern att bearbeta andra uppgifter medan den är aktiv. Detta värde bör inte vara mer än antalet CPU-kärnor. Maximerar användningen om den är inställd på 0.",
+    "transcoding_tone_mapping": "Ton mappning",
+    "transcoding_tone_mapping_description": "Försöker att bevara utseendet på HDR-videor när de konverteras till SDR. Varje algoritm gör olika avvägningar för färg, detaljer och ljusstyrka. Hable bevarar detaljer, Mobius bevarar färg och Reinhard bevarar ljusstyrkan.",
+    "transcoding_transcode_policy": "Omkodningspolicy",
+    "transcoding_transcode_policy_description": "Policy för när en video ska omkodas. HDR-videor kommer alltid att omkodas (förutom om omkodning är inaktiverad).",
+    "transcoding_two_pass_encoding": "Två-pass kodning",
+    "transcoding_two_pass_encoding_setting_description": "Koda om i två omgångar för att producera bättre kodade videor. När max bitrate är aktiverat (krävs för att det ska fungera med H.264 och HEVC), använder det här läget ett bithastighetsområde baserat på max bitrate och ignorerar CRF. För VP9 kan CRF användas om max bitrate är inaktiverat.",
+    "transcoding_video_codec": "Video Codec",
+    "transcoding_video_codec_description": "VP9 har hög effektivitet och webbkompatibilitet, men tar längre tid att omkoda. HEVC fungerar på liknande sätt, men har lägre webbkompatibilitet. H.264 är allmänt kompatibel och snabb att omkoda, men producerar mycket större filer. AV1 är den mest effektiva codec men saknar stöd på äldre enheter.",
+    "trash_enabled_description": "Aktivera papperskorgen",
+    "trash_number_of_days": "Antal dagar",
+    "trash_number_of_days_description": "Antal dagar för att förvara tillgångarna i papperskorgen innan de permanent tas bort",
+    "trash_settings": "Papperskorginställningar",
+    "trash_settings_description": "Hantera papperskorginställningar",
+    "untracked_files": "Ospårade filer",
+    "untracked_files_description": "Dessa filer spåras inte av applikationen. De kan vara resultatet av misslyckade rörelser, avbrutna uppladdningar eller kvarlämnade på grund av en bugg",
+    "user_cleanup_job": "Användarrensning",
+    "user_delete_delay": "<b>{user}</b>s konto och tillgångar kommer att schemaläggas för permanent radering om {delay, plural, one {# day} other {# days}}.",
+    "user_delete_delay_settings": "Borttagningsfördröjning",
+    "user_delete_delay_settings_description": "Antal dagar efter borttagning för att permanent radera en användares konto och tillgångar. Arbetet med borttagning av användare körs vid midnatt för att söka efter användare som är redo för radering. Ändringar av denna inställning kommer att utvärderas vid nästa körning.",
+    "user_delete_immediately": "<b>{user}</b> konto och tillgångar kommer att stå i kö för <b>permanent</b> radering.",
+    "user_delete_immediately_checkbox": "Köa användare och tillgångar för omedelbar radering",
     "user_management": "Användarhantering",
-    "user_settings": "",
-    "user_settings_description": "",
-    "version_check_enabled_description": "",
-    "version_check_settings": "",
-    "version_check_settings_description": "",
-    "video_conversion_job_description": ""
+    "user_password_has_been_reset": "Användarens lösenord har återställts:",
+    "user_password_reset_description": "Ange det tillfälliga lösenordet till användaren och informera dem om att de kommer att behöva ändra lösenordet vid nästa inloggning.",
+    "user_restore_description": "<b>{user}</b> konto kommer att återställas.",
+    "user_restore_scheduled_removal": "Återställ användare - schemalagd borttagning {date, date, long}",
+    "user_settings": "Användarinställningar",
+    "user_settings_description": "Hantera användarinställningar",
+    "user_successfully_removed": "Användaren {email} har tagits bort.",
+    "version_check_enabled_description": "Aktivera versionskontroll",
+    "version_check_implications": "Funktionen för versionskontroll är beroende av periodisk kommunikation med github.com",
+    "version_check_settings": "Versionskontroll",
+    "version_check_settings_description": "Aktivera/inaktivera meddelandet om ny versionen",
+    "video_conversion_job": "Omkoda videor",
+    "video_conversion_job_description": "Koda om videor för bredare kompatibilitet med webbläsare och enheter"
   },
-  "admin_email": "",
-  "admin_password": "",
+  "admin_email": "Admin Email",
+  "admin_password": "Admin Lösenord",
   "administration": "Administration",
-  "advanced": "Avancerad",
-  "album_added": "",
-  "album_added_notification_setting_description": "",
-  "album_cover_updated": "",
-  "album_info_updated": "",
-  "album_name": "",
-  "album_options": "",
-  "album_updated": "",
-  "album_updated_setting_description": "",
+  "advanced": "Avancerat",
+  "age_months": "Ålder {months, plural, one {# month} other {# months}}",
+  "age_year_months": "Ålder 1 år, {months, plural, one {# month} other {# months}}",
+  "age_years": "{years, plural, other {Age #}}",
+  "album_added": "Albumet har lagts till",
+  "album_added_notification_setting_description": "Få ett e-postmeddelande när du läggs till i ett delat album",
+  "album_cover_updated": "Albumomslaget uppdaterat",
+  "album_delete_confirmation": "Är du säker på att du vill ta bort albumet {album}?",
+  "album_delete_confirmation_description": "Om det här albumet delas kommer andra användare inte att kunna komma åt det längre.",
+  "album_info_updated": "Albuminformation uppdaterad",
+  "album_leave": "Lämna albumet?",
+  "album_leave_confirmation": "Är du säker på att du vill lämna {album}?",
+  "album_name": "Albumnamn",
+  "album_options": "Albumalternativ",
+  "album_remove_user": "Ta bort användare?",
+  "album_remove_user_confirmation": "Är du säker på att du vill ta bort {user}?",
+  "album_share_no_users": "Det verkar som att du har delat det här albumet med alla användare eller så har du inte någon användare att dela med.",
+  "album_updated": "Albumet uppdaterat",
+  "album_updated_setting_description": "Få ett e-postmeddelande när ett delat album har nya tillgångar",
+  "album_user_left": "Lämnade {album}",
+  "album_user_removed": "Tog bort {user}",
+  "album_with_link_access": "Låt alla med länken se foton och personer i det här albumet.",
   "albums": "Album",
+  "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albums}}",
   "all": "Allt",
-  "all_people": "",
+  "all_albums": "Alla album",
+  "all_people": "Alla personer",
   "all_videos": "Alla videor",
-  "allow_dark_mode": "",
-  "allow_edits": "",
-  "api_key": "",
-  "api_keys": "",
-  "app_settings": "",
-  "appears_in": "",
+  "allow_dark_mode": "Tillåt mörkt läge",
+  "allow_edits": "Tillåt redigeringar",
+  "allow_public_user_to_download": "Tillåt offentlig användare att ladda ner",
+  "allow_public_user_to_upload": "Tillåt en offentlig användare att ladda upp",
+  "anti_clockwise": "Moturs",
+  "api_key": "API Nyckel",
+  "api_key_description": "Detta värde kommer bara att visas en gång. Se till att kopiera det innan du stänger fönstret.",
+  "api_key_empty": "Ditt API-nyckelnamn ska inte vara tomt",
+  "api_keys": "API-Nycklar",
+  "app_settings": "Appinställningar",
+  "appears_in": "Visas i",
   "archive": "Arkiv",
-  "archive_or_unarchive_photo": "",
-  "archived": "",
-  "asset_offline": "",
+  "archive_or_unarchive_photo": "Arkivera eller oarkivera fotot",
+  "archive_size": "Arkivstorlek",
+  "archive_size_description": "Konfigurera arkivstorleken för nedladdningar (i GiB)",
+  "archived_count": "{count, plural, other {Archived #}}",
+  "are_these_the_same_person": "Är det samma person?",
+  "are_you_sure_to_do_this": "Är du säker på att du vill göra det här?",
+  "asset_added_to_album": "Lades till i album",
+  "asset_adding_to_album": "Lägger till i album...",
+  "asset_description_updated": "Tillgångens beskrivning har uppdaterats",
+  "asset_filename_is_offline": "Tillgången {filename} är offline",
+  "asset_has_unassigned_faces": "Tillgången har otilldelade ansikten",
+  "asset_hashing": "Hashing...",
+  "asset_offline": "Tillgång offline",
+  "asset_offline_description": "Denna externa tillgång finns inte längre på disken. Kontakta din Immich-administratör för hjälp.",
+  "asset_skipped": "Överhoppad",
+  "asset_skipped_in_trash": "I papperskorgen",
+  "asset_uploaded": "Uppladdad",
+  "asset_uploading": "Laddar upp...",
   "assets": "Objekt",
-  "authorized_devices": "",
+  "assets_added_count": "La till {count, plural, one {# asset} other {# assets}}",
+  "assets_added_to_album_count": "Lade till {count, plural, one {# asset} other {# assets}} i albumet",
+  "assets_added_to_name_count": "Lade till {count, plural, one {# asset} other {# assets}} till {hasName, select, true {<b>{name}</b>} other {new album}}",
+  "assets_count": "{count, plural, one {# asset} other {# assets}}",
+  "assets_moved_to_trash_count": "Flyttade {count, plural, one {# asset} other {# assets}} till papperskorgen",
+  "assets_permanently_deleted_count": "Raderad permanent {count, plural, one {# asset} other {# assets}}",
+  "assets_removed_count": "Tog bort {count, plural, one {# asset} other {# assets}}",
+  "assets_restore_confirmation": "Är du säker på att du vill återställa alla dina papperskorgen? Du kan inte ångra den här åtgärden! Observera att offlineobjekt inte kan återställas på detta sätt.",
+  "assets_restored_count": "Återställd {count, plural, one {# asset} other {# assets}}",
+  "assets_trashed_count": "Till Papperskorgen {count, plural, one {# asset} other {# assets}}",
+  "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Asset were}} är redan en del av albumet",
+  "authorized_devices": "Auktoriserade enheter",
   "back": "Bakåt",
-  "backward": "",
-  "blurred_background": "",
+  "back_close_deselect": "Tillbaka, stäng eller avmarkera",
+  "backward": "Bakåt",
+  "birthdate_saved": "Födelsedatumet har sparats",
+  "birthdate_set_description": "Födelsedatum används för att beräkna åldern på denna person vid tidpunkten för ett foto.",
+  "blurred_background": "Suddig bakgrund",
+  "bugs_and_feature_requests": "Buggar och funktionsförfrågningar",
+  "build": "Bygge",
+  "build_image": "Byggfil",
+  "bulk_delete_duplicates_confirmation": "Är du säker på att du vill massradera {count, plural, one {# duplicate asset} other {# duplicate assets}}? Detta kommer att behålla den största tillgången i varje grupp och permanent radera alla andra dubbletter. Du kan inte ångra den här åtgärden!",
+  "bulk_keep_duplicates_confirmation": "Är du säker på att du vill behålla {count, plural, one {# duplicate asset} other {# duplicate assets}}? Detta kommer att lösa alla dubbletter av grupper utan att ta bort någonting.",
+  "bulk_trash_duplicates_confirmation": "Är du säker på att du vill skicka till papperskorgen {count, plural, one {# duplicate asset} other {# duplicate assets}}? Detta kommer att behålla den största tillgången i varje grupp och alla andra dubbletter kasseras.",
+  "buy": "Köp Immich",
   "camera": "Kamera",
   "camera_brand": "Kameramärke",
   "camera_model": "Kameramodell",
   "cancel": "Avbryt",
   "cancel_search": "Avbryt sökning",
-  "cannot_merge_people": "",
-  "cannot_update_the_description": "",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
-  "change_date": "",
+  "cannot_merge_people": "Kan inte slå samman personer",
+  "cannot_undo_this_action": "Du kan inte ångra den här åtgärden!",
+  "cannot_update_the_description": "Det går inte att uppdatera beskrivningen",
+  "change_date": "Ändra datum",
   "change_expiration_time": "Ändra utgångstid",
-  "change_location": "",
-  "change_name": "",
-  "change_name_successfully": "",
+  "change_location": "Ändra plats",
+  "change_name": "Byt namn",
+  "change_name_successfully": "Bytt namn framgångsrikt",
   "change_password": "Ändra Lösenord",
-  "change_your_password": "",
-  "changed_visibility_successfully": "",
-  "check_logs": "",
+  "change_password_description": "Detta är antingen första gången du loggar in i systemet eller så har en begäran gjorts om att ändra ditt lösenord. Vänligen ange det nya lösenordet nedan.",
+  "change_your_password": "Ändra ditt lösenord",
+  "changed_visibility_successfully": "Synligheten har ändrats",
+  "check_all": "Markera alla",
+  "check_logs": "Kontrollera loggar",
+  "choose_matching_people_to_merge": "Välj matchande personer att slå samman",
   "city": "Stad",
   "clear": "Rensa",
   "clear_all": "Rensa allt",
+  "clear_all_recent_searches": "Rensa alla senaste sökningar",
   "clear_message": "Rensa meddelande",
   "clear_value": "Rensa värde",
+  "clockwise": "Medsols",
   "close": "Stäng",
-  "collapse_all": "",
+  "collapse": "Kollapsa",
+  "collapse_all": "Kollapsa alla",
+  "color": "Färg",
   "color_theme": "Färgtema",
   "comment_deleted": "Kommentar raderad",
-  "comment_options": "",
+  "comment_options": "Kommentarsalternativ",
+  "comments_and_likes": "Kommentarer & likes",
   "comments_are_disabled": "Kommentarer är avstängda",
   "confirm": "Bekräfta",
-  "confirm_admin_password": "",
+  "confirm_admin_password": "Bekräfta administratörslösenord",
+  "confirm_delete_shared_link": "Är du säker på att du vill ta bort den här delade länken?",
+  "confirm_keep_this_delete_others": "Alla andra tillgångar i stacken tas bort förutom den här tillgången. Är du säker på att du vill fortsätta.",
   "confirm_password": "Bekräfta lösenord",
-  "contain": "",
+  "contain": "Anpassa",
   "context": "Sammanhang",
   "continue": "Fortsätt",
-  "copied_image_to_clipboard": "",
-  "copy_error": "",
-  "copy_file_path": "",
+  "copied_image_to_clipboard": "Kopierade bilden till urklipp.",
+  "copied_to_clipboard": "Kopierat till urklipp!",
+  "copy_error": "Kopieringsfel",
+  "copy_file_path": "Kopiera filsökväg",
   "copy_image": "Kopiera Bild",
   "copy_link": "Kopiera länk",
-  "copy_link_to_clipboard": "",
+  "copy_link_to_clipboard": "Kopiera länken till urklipp",
   "copy_password": "Kopiera lösenord",
-  "copy_to_clipboard": "",
+  "copy_to_clipboard": "Kopiera till Urklipp",
   "country": "Land",
-  "cover": "",
-  "covers": "",
+  "cover": "Fyll",
+  "covers": "Omslag",
   "create": "Skapa",
   "create_album": "Skapa album",
   "create_library": "Skapa Bibliotek",
   "create_link": "Skapa länk",
   "create_link_to_share": "Skapa länk att dela",
-  "create_new_person": "",
+  "create_link_to_share_description": "Låt alla med länken se de valda fotona",
+  "create_new_person": "Skapa ny person",
+  "create_new_person_hint": "Tilldela valda objekt till en ny person",
   "create_new_user": "Skapa en ny användare",
+  "create_tag": "Skapa tagg",
+  "create_tag_description": "Skapa en ny tagg. För kapslade taggar anger du hela sökvägen för taggen inklusive snedstreck.",
   "create_user": "Skapa användare",
-  "created": "",
-  "current_device": "",
-  "custom_locale": "",
-  "custom_locale_description": "",
-  "dark": "",
-  "date_after": "",
+  "created": "Skapad",
+  "current_device": "Aktuell enhet",
+  "custom_locale": "Anpassad plats",
+  "custom_locale_description": "Formatera datum och siffror baserat på språket och regionen",
+  "dark": "Mörk",
+  "date_after": "Datum efter",
   "date_and_time": "Datum och Tid",
-  "date_before": "",
+  "date_before": "Datum före",
+  "date_of_birth_saved": "Födelsedatumet har sparats",
   "date_range": "Datumintervall",
   "day": "Dag",
-  "default_locale": "",
-  "default_locale_description": "",
+  "deduplicate_all": "Deduplicera alla",
+  "default_locale": "Standardplats",
+  "default_locale_description": "Formatera datum och siffror baserat på din webbläsares lokalitet",
   "delete": "Radera",
   "delete_album": "Ta bort album",
-  "delete_key": "",
+  "delete_api_key_prompt": "Är du säker på att du vill ta bort denna API-nyckel?",
+  "delete_duplicates_confirmation": "Är du säker på att du vill ta bort dessa dubbletter permanent?",
+  "delete_key": "Ta bort nyckel",
   "delete_library": "Ta bort bibliotek",
   "delete_link": "Ta bort länk",
+  "delete_others": "Radera fler",
   "delete_shared_link": "Ta bort delad länk",
+  "delete_tag": "Ta bort tagg",
+  "delete_tag_confirmation_prompt": "Är du säker på att du vill ta bort {tagName}-taggen?",
   "delete_user": "Ta bort användare",
   "deleted_shared_link": "Ta bort delad länk",
+  "deletes_missing_assets": "Tar bort objekt som saknas från disken",
   "description": "Beskrivning",
   "details": "Detaljer",
   "direction": "Riktning",
-  "disallow_edits": "",
+  "disabled": "Inaktiverad",
+  "disallow_edits": "Tillåt inte redigeringar",
+  "discord": "Discord",
   "discover": "Upptäck",
-  "dismiss_all_errors": "",
-  "dismiss_error": "",
+  "dismiss_all_errors": "Avvisa alla fel",
+  "dismiss_error": "Avvisa fel",
   "display_options": "Visningsalternativ",
-  "display_order": "",
+  "display_order": "Visa Ordning",
   "display_original_photos": "Visa originalfoton",
-  "display_original_photos_setting_description": "",
+  "display_original_photos_setting_description": "Föredrar att visa originalfotot när du visar en tillgång snarare än miniatyrbilder när den ursprungliga tillgången är webbkompatibel. Detta kan resultera i långsammare bildvisningshastigheter.",
   "do_not_show_again": "Visa inte det här meddelandet igen",
+  "documentation": "Dokumentation",
   "done": "Klart",
   "download": "Ladda ner",
+  "download_include_embedded_motion_videos": "Inbäddade videor",
+  "download_include_embedded_motion_videos_description": "Inkludera videor inbäddade i rörliga bilder som en separat fil",
+  "download_settings": "Ladda ner",
+  "download_settings_description": "Hantera inställningar relaterade till nedladdning av objekt",
   "downloading": "Laddar ner",
+  "downloading_asset_filename": "Laddar ned objekt {filename}",
+  "drop_files_to_upload": "Släpp filer var som helst för att ladda upp",
+  "duplicates": "Dubletter",
+  "duplicates_description": "Lös varje grupp genom att ange vilka, om några, är dubbletter",
   "duration": "Varaktighet",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
+  "edit": "Redigera",
   "edit_album": "Redigera album",
   "edit_avatar": "Redigera avatar",
   "edit_date": "Redigera datum",
   "edit_date_and_time": "Redigera datum och tid",
-  "edit_exclusion_pattern": "",
+  "edit_exclusion_pattern": "Redigera uteslutningsmönster",
   "edit_faces": "Redigera ansikten",
   "edit_import_path": "Redigera importsökvägar",
-  "edit_import_paths": "",
-  "edit_key": "",
+  "edit_import_paths": "Redigera importsökvägar",
+  "edit_key": "Redigera nyckel",
   "edit_link": "Redigera länk",
   "edit_location": "Redigera plats",
   "edit_name": "Redigera namn",
   "edit_people": "Redigera personer",
-  "edit_title": "",
+  "edit_tag": "Redigera tagg",
+  "edit_title": "Redigera titel",
   "edit_user": "Redigera användare",
   "edited": "Redigerad",
-  "editor": "",
+  "editor": "Redigerare",
+  "editor_close_without_save_prompt": "Ändringarna kommer inte att sparas",
+  "editor_close_without_save_title": "Stäng redigeraren?",
+  "editor_crop_tool_h2_aspect_ratios": "Bildförhållande",
+  "editor_crop_tool_h2_rotation": "Rotation",
   "email": "Epost",
-  "empty": "",
-  "empty_album": "",
   "empty_trash": "Töm papperskorg",
-  "enable": "",
-  "enabled": "",
+  "empty_trash_confirmation": "Är du säker på att du vill tömma papperskorgen? Detta tar bort alla objekt i papperskorgen permanent från Immich.\nDu kan inte ångra den här åtgärden!",
+  "enable": "Aktivera",
+  "enabled": "Aktiverad",
   "end_date": "Slutdatum",
   "error": "Fel",
   "error_loading_image": "Fel vid bildladdning",
+  "error_title": "Fel – något gick fel",
   "errors": {
-    "unable_to_add_album_users": "Kunde inte lägga till albumanvändare",
+    "cannot_navigate_next_asset": "Det går inte att navigera till nästa objekt",
+    "cannot_navigate_previous_asset": "Det går inte att navigera till föregående objekt",
+    "cant_apply_changes": "Det går inte att tillämpa ändringar",
+    "cant_change_activity": "Kan inte {enabled, select, true {disable} other {enable}} aktivitet",
+    "cant_change_asset_favorite": "Det går inte att byta favorit mot objekt",
+    "cant_change_metadata_assets_count": "Det går inte att ändra metadata för {count, plural, one {# asset} other {# assets}}",
+    "cant_get_faces": "Kan inte få ansikten",
+    "cant_get_number_of_comments": "Kan inte få antal kommentarer",
+    "cant_search_people": "Kan inte söka efter personer",
+    "cant_search_places": "Kan inte söka platser",
+    "cleared_jobs": "Raderade jobb för: {job}",
+    "error_adding_assets_to_album": "Det gick inte att lägga till objekt i albumet",
+    "error_adding_users_to_album": "Det gick inte att lägga till användare till albumet",
+    "error_deleting_shared_user": "Det gick inte att ta bort delad användare",
+    "error_downloading": "Fel vid nedladdning av {filename}",
+    "error_hiding_buy_button": "Det gick inte att dölja köpknappen",
+    "error_removing_assets_from_album": "Det gick inte att ta bort objekt från albumet, kontrollera konsolen för mer information",
+    "error_selecting_all_assets": "Fel vid val av alla objekt",
+    "exclusion_pattern_already_exists": "Detta uteslutningsmönster finns redan.",
+    "failed_job_command": "Kommandot {command} misslyckades för jobbet: {job}",
+    "failed_to_create_album": "Det gick inte att skapa album",
+    "failed_to_create_shared_link": "Det gick inte att skapa delad länk",
+    "failed_to_edit_shared_link": "Det gick inte att redigera delad länk",
+    "failed_to_get_people": "Det gick inte att hämta personer",
+    "failed_to_keep_this_delete_others": "Misslyckades att behålla detta objekt radera övriga objekt",
+    "failed_to_load_asset": "Det gick inte att ladda objekt",
+    "failed_to_load_assets": "Det gick inte att ladda objekten",
+    "failed_to_load_people": "Det gick inte att ladda personer",
+    "failed_to_remove_product_key": "Det gick inte att ta bort produktnyckeln",
+    "failed_to_stack_assets": "Det gick inte att stapla objekt",
+    "failed_to_unstack_assets": "Det gick inte att avstapla objekt",
+    "import_path_already_exists": "Denna importsökväg finns redan.",
+    "incorrect_email_or_password": "Felaktig e-postadress eller lösenord",
+    "paths_validation_failed": "{paths, plural, one {# path} other {# paths}} misslyckades valideringen",
+    "profile_picture_transparent_pixels": "Profilbilder kan inte ha genomskinliga pixlar. Zooma in och/eller flytta bilden.",
+    "quota_higher_than_disk_size": "Du har angett en kvot som är högre än diskstorleken",
+    "repair_unable_to_check_items": "Det går inte att kontrollera {count, select, one {item} other {items}}",
+    "unable_to_add_album_users": "Kunde inte lägga till använder i album",
+    "unable_to_add_assets_to_shared_link": "Det går inte att lägga till objekt till delad länk",
     "unable_to_add_comment": "Kunde inte lägga till kommentar",
+    "unable_to_add_exclusion_pattern": "Det gick inte att lägga till uteslutningsmönster",
+    "unable_to_add_import_path": "Det gick inte att lägga till importsökväg",
     "unable_to_add_partners": "Kunde inte lägga till partners",
+    "unable_to_add_remove_archive": "Det går inte att {archived, select, true {remove asset from} other {add asset to}} arkiv",
+    "unable_to_add_remove_favorites": "Det går inte att {favorite, select, true {add asset to} other {remove asset from}} favoriter",
+    "unable_to_archive_unarchive": "Det går inte att {archived, select, true {archive} other {archive}}",
     "unable_to_change_album_user_role": "Kunde inte ändra albumanvändarens roll",
     "unable_to_change_date": "Kunde inte ändra datum",
+    "unable_to_change_favorite": "Det går inte att ändra favorit för objekt",
     "unable_to_change_location": "Kunde inte ändra plats",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
-    "unable_to_create_admin_account": "",
+    "unable_to_change_password": "Det går inte att ändra lösenord",
+    "unable_to_change_visibility": "Det gick inte att ändra synligheten för {count, plural, one {# person} other {# people}}",
+    "unable_to_complete_oauth_login": "Det gick inte att slutföra OAuth-inloggning",
+    "unable_to_connect": "Det går inte att ansluta",
+    "unable_to_connect_to_server": "Det går inte att ansluta till servern",
+    "unable_to_copy_to_clipboard": "Kan inte kopiera till urklipp, se till att du kommer åt sidan via https",
+    "unable_to_create_admin_account": "Det gick inte att skapa ett administratörskonto",
+    "unable_to_create_api_key": "Det gick inte att skapa en ny API-nyckel",
     "unable_to_create_library": "Kunde inte skapa bibliotek",
     "unable_to_create_user": "Kunde inte skapa användare",
     "unable_to_delete_album": "Kunde inte ta bort album",
-    "unable_to_delete_asset": "",
+    "unable_to_delete_asset": "Det gick inte att ta bort objekt",
+    "unable_to_delete_assets": "Det gick inte att ta bort objekt",
+    "unable_to_delete_exclusion_pattern": "Det gick inte att ta bort uteslutningsmönster",
+    "unable_to_delete_import_path": "Det gick inte att ta bort importsökvägen",
+    "unable_to_delete_shared_link": "Det gick inte att ta bort delad länk",
     "unable_to_delete_user": "Kunde inte ta bort användare",
+    "unable_to_download_files": "Det går inte att ladda ner filer",
+    "unable_to_edit_exclusion_pattern": "Det gick inte att redigera uteslutningsmönster",
+    "unable_to_edit_import_path": "Det gick inte att redigera importsökvägen",
     "unable_to_empty_trash": "Kunde inte tömma papperskorgen",
     "unable_to_enter_fullscreen": "Kunde inte växla till fullskärm",
     "unable_to_exit_fullscreen": "Kunde inte avsluta fullskärm",
-    "unable_to_hide_person": "",
-    "unable_to_load_album": "",
-    "unable_to_load_asset_activity": "",
-    "unable_to_load_items": "",
-    "unable_to_load_liked_status": "",
+    "unable_to_get_comments_number": "Det gick inte att hämta antalet kommentarer",
+    "unable_to_get_shared_link": "Det gick inte att hämta delad länk",
+    "unable_to_hide_person": "Det går inte att dölja personen",
+    "unable_to_link_motion_video": "Det går inte att länka rörlig video",
+    "unable_to_link_oauth_account": "Det gick inte att länka OAuth-kontot",
+    "unable_to_load_album": "Det gick inte att ladda albumet",
+    "unable_to_load_asset_activity": "Det går inte att läsa in tillgångsaktivitet",
+    "unable_to_load_items": "Kunde inte ladda objekt",
+    "unable_to_load_liked_status": "kunde inte ladda gillade status",
+    "unable_to_log_out_all_devices": "Det gick inte att logga ut alla enheter",
+    "unable_to_log_out_device": "Det gick inte att logga ut enheten",
+    "unable_to_login_with_oauth": "Det gick inte att logga in med OAuth",
     "unable_to_play_video": "Kunde inte spela upp video",
-    "unable_to_refresh_user": "",
-    "unable_to_remove_album_users": "",
-    "unable_to_remove_comment": "",
+    "unable_to_reassign_assets_existing_person": "Det går inte att tilldela om tillgångar till {name, select, null {an existing person} other {{name}}}",
+    "unable_to_reassign_assets_new_person": "Kunde inte tilldela objekt till en annan person",
+    "unable_to_refresh_user": "Kunde inte ladda om användaren",
+    "unable_to_remove_album_users": "Kunde inte ta bort personen från albumet",
+    "unable_to_remove_api_key": "Det gick inte att ta bort API Keyet",
+    "unable_to_remove_assets_from_shared_link": "Kunde inte ta bort objekt från delade länkar",
+    "unable_to_remove_deleted_assets": "Kunde inte ta bort offline filer",
     "unable_to_remove_library": "Kunde inte ta bort bibliotek",
     "unable_to_remove_partner": "Kunde inte ta bort partner",
     "unable_to_remove_reaction": "Kunde inte ta bort reaktion",
-    "unable_to_remove_user": "",
-    "unable_to_repair_items": "",
+    "unable_to_repair_items": "kunde inte reparera objekt",
     "unable_to_reset_password": "Kunde inte återställa lösenord",
-    "unable_to_resolve_duplicate": "",
-    "unable_to_restore_assets": "",
-    "unable_to_restore_trash": "",
+    "unable_to_resolve_duplicate": "Det går inte att lösa dubbletter",
+    "unable_to_restore_assets": "Det går inte att återställa tillgångar",
+    "unable_to_restore_trash": "Det gick inte att återställa papperskorgen",
     "unable_to_restore_user": "Kunde inte återställa användare",
     "unable_to_save_album": "Kunde inte spara album",
+    "unable_to_save_api_key": "Det går inte att spara API Nyckel",
+    "unable_to_save_date_of_birth": "Det går inte att spara födelsedatum",
     "unable_to_save_name": "Kunde inte spara namn",
     "unable_to_save_profile": "Kunde inte spara profil",
     "unable_to_save_settings": "Kunde inte spara inställningar",
-    "unable_to_scan_libraries": "",
-    "unable_to_scan_library": "",
-    "unable_to_set_profile_picture": "",
-    "unable_to_submit_job": "",
-    "unable_to_trash_asset": "",
-    "unable_to_unlink_account": "",
+    "unable_to_scan_libraries": "Det går inte att söka igenom bibliotek",
+    "unable_to_scan_library": "Det går inte att skanna biblioteket",
+    "unable_to_set_feature_photo": "Det går inte att ställa in funktionsfoto",
+    "unable_to_set_profile_picture": "Det går inte att ställa in profilbilden",
+    "unable_to_submit_job": "Det går inte att skicka jobbet",
+    "unable_to_trash_asset": "Det går inte att slänga resursen",
+    "unable_to_unlink_account": "Det går inte att ta bort länken till kontot",
+    "unable_to_unlink_motion_video": "Det går inte att ta bort länken till rörelsevideo",
+    "unable_to_update_album_cover": "Det går inte att uppdatera albumomslaget",
+    "unable_to_update_album_info": "Det går inte att uppdatera albuminformationen",
     "unable_to_update_library": "Kunde inte uppdatera bibliotek",
     "unable_to_update_location": "Kunde inte uppdatera plats",
     "unable_to_update_settings": "Kunde inte uppdatera inställningar",
-    "unable_to_update_user": "Kunde inte uppdatera användare"
+    "unable_to_update_timeline_display_status": "Det går inte att uppdatera visningsstatus för tidslinjen",
+    "unable_to_update_user": "Kunde inte uppdatera användare",
+    "unable_to_upload_file": "Det går inte att ladda upp filen"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
-  "exit_slideshow": "",
-  "expand_all": "",
+  "exif": "Exif",
+  "exit_slideshow": "Avsluta bildspel",
+  "expand_all": "Expandera alla",
   "expire_after": "Går ut efter",
   "expired": "Gått ut",
+  "expires_date": "Går ut {date}",
   "explore": "Utforska",
+  "explorer": "Utforskare",
+  "export": "Exportera",
   "export_as_json": "Exportera som JSON",
-  "extension": "",
+  "extension": "Tillägg",
+  "external": "Externt",
   "external_libraries": "Externa Bibliotek",
-  "failed_to_get_people": "",
+  "face_unassigned": "Otilldelade",
+  "failed_to_load_assets": "Det gick inte att läsa in resurser",
   "favorite": "Favorit",
-  "favorite_or_unfavorite_photo": "",
+  "favorite_or_unfavorite_photo": "Favoritfoto eller icke-favoritfoto",
   "favorites": "Favoriter",
-  "feature": "",
-  "feature_photo_updated": "",
-  "featurecollection": "",
+  "feature_photo_updated": "Funktionsfoto uppdaterad",
+  "features": "Funktioner",
+  "features_setting_description": "Hantera appens funktioner",
   "file_name": "Filnamn",
   "file_name_or_extension": "Filnamn eller -tillägg",
   "filename": "Filnamn",
-  "files": "",
   "filetype": "Filtyp",
   "filter_people": "Filtrera personer",
-  "fix_incorrect_match": "",
-  "force_re-scan_library_files": "",
+  "find_them_fast": "Hitta dem snabbt efter namn med sök",
+  "fix_incorrect_match": "Fixa inkorrekt matchning",
+  "folders": "Mappar",
+  "folders_feature_description": "Bläddra i mappvyn för foton och videoklipp i filsystemet",
   "forward": "Framåt",
-  "general": "",
-  "get_help": "",
-  "getting_started": "",
+  "general": "Allmänt",
+  "get_help": "Få hjälp",
+  "getting_started": "Komma igång",
   "go_back": "Gå tillbaka",
   "go_to_search": "Gå till sök",
-  "go_to_share_page": "",
-  "group_albums_by": "",
-  "has_quota": "",
+  "group_albums_by": "Gruppera album efter...",
+  "group_no": "Ingen gruppering",
+  "group_owner": "Grupper efter ägare",
+  "group_year": "Gruppera efter årtal",
+  "has_quota": "Har kvot",
+  "hi_user": "Hej {name} ({email})",
+  "hide_all_people": "Göm alla personer",
   "hide_gallery": "Dölj galleri",
+  "hide_named_person": "Göm personen {name}",
   "hide_password": "Dölj lösenord",
   "hide_person": "Dölj person",
+  "hide_unnamed_people": "Göm personer utan namn",
   "host": "Värd",
   "hour": "Timme",
   "image": "Bild",
-  "img": "",
+  "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} tagen {date}",
+  "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} tagen med {person1} den {date}",
+  "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} tagen med {person1} och {person2} den {date}",
+  "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} tagen med {person1}, {person2}, och {person3} den {date}",
+  "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} tagen med {person1}, {person2}, och {additionalCount, number} andra den {date}",
+  "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} tagen i {city}, {country} den {date}",
+  "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} tagen i {city}, {country} med {person1} den {date}",
+  "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} tagen i {city}, {country} med {person1} och {person2} den {date}",
+  "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} tagen i {city}, {country} med {person1}, {person2}, och {person3} den {date}",
+  "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} tagen i {city}, {country} med {person1}, {person2}, och {additionalCount, number} andre den {date}",
   "immich_logo": "Immich Logo",
+  "immich_web_interface": "Immich Web gränssnitt",
   "import_from_json": "Importera från JSON",
   "import_path": "Importsökväg",
-  "in_archive": "",
+  "in_albums": "I {count, plural, one {# album} other {# albums}}",
+  "in_archive": "I arkivet",
   "include_archived": "Inkludera arkiverade",
   "include_shared_albums": "Inkludera delade album",
-  "include_shared_partner_assets": "",
-  "individual_share": "",
-  "info": "",
+  "include_shared_partner_assets": "Inkludera delade partners tillgångar",
+  "individual_share": "Enskild delning",
+  "info": "Information",
   "interval": {
-    "day_at_onepm": "",
-    "hours": "",
-    "night_at_midnight": "",
-    "night_at_twoam": ""
+    "day_at_onepm": "Alla dagar vid kl 13.00",
+    "hours": "Vid varje {hours, plural, one {hour} other {{hours, number} hours}}",
+    "night_at_midnight": "Varje natt vid midnatt.",
+    "night_at_twoam": "Varje natt vid kl 02.00"
   },
-  "invite_people": "",
+  "invite_people": "Bjud in personer",
   "invite_to_album": "Bjuder in till album",
-  "job_settings_description": "",
+  "items_count": "{count, plural, one {# item} other {# items}}",
   "jobs": "Jobb",
-  "keep": "",
-  "keyboard_shortcuts": "",
-  "language": "",
-  "language_setting_description": "",
-  "last_seen": "",
-  "leave": "",
+  "keep": "Behåll",
+  "keep_all": "Behåll alla",
+  "keep_this_delete_others": "Behåll denna, radera övriga",
+  "kept_this_deleted_others": "Behåll denna tillgång och borttagna {count, plural, one {# asset} other {# assets}}",
+  "keyboard_shortcuts": "Kortkommandon",
+  "language": "Språk",
+  "language_setting_description": "Välj önskat språk",
+  "last_seen": "Senast sedd",
+  "latest_version": "Senaste versionen",
+  "latitude": "Latitud",
+  "leave": "Lämna",
   "let_others_respond": "Låt andra svara",
-  "level": "",
+  "level": "Nivå",
   "library": "Bibliotek",
-  "library_options": "",
-  "light": "",
-  "link_options": "",
-  "link_to_oauth": "",
-  "linked_oauth_account": "",
-  "list": "",
-  "loading": "",
-  "loading_search_results_failed": "",
+  "library_options": "Nivå alternativ",
+  "light": "Ljus",
+  "like_deleted": "Gilla borttagen",
+  "link_motion_video": "Länka rörlig video",
+  "link_options": "Alternativ för länk",
+  "link_to_oauth": "Länk till OAuth",
+  "linked_oauth_account": "Länkat OAuth konto",
+  "list": "Lista",
+  "loading": "Laddar",
+  "loading_search_results_failed": "Det gick inte att läsa in sökresultat",
   "log_out": "Logga ut",
-  "log_out_all_devices": "",
-  "login_has_been_disabled": "",
-  "look": "",
-  "loop_videos": "",
+  "log_out_all_devices": "Logga ut alla enheter",
+  "logged_out_all_devices": "Loggat ut från alla enheter",
+  "logged_out_device": "Loggat ut enheten",
+  "login": "Logga in",
+  "login_has_been_disabled": "Inloggning har blivit inaktiverat.",
+  "logout_all_device_confirmation": "Är du säker på att du vill logga ut från alla enheter?",
+  "logout_this_device_confirmation": "Är du säker på att du vill logga ut från denna enhet?",
+  "longitude": "Longitud",
+  "look": "Titta",
+  "loop_videos": "Loopa videor",
   "loop_videos_description": "Aktivera för att automatiskt loopa en video i detaljvisaren.",
+  "main_branch_warning": "Du använder en utvecklingsversion. Vi rekommenderar starkt att du använder en utgiven version!",
   "make": "Tillverkare",
   "manage_shared_links": "Hantera Delade länkar",
-  "manage_sharing_with_partners": "",
+  "manage_sharing_with_partners": "Hantera delning med partner",
   "manage_the_app_settings": "",
   "manage_your_account": "Hantera ditt konto",
   "manage_your_api_keys": "",
@@ -671,7 +898,6 @@
   "oldest_first": "",
   "online": "",
   "only_favorites": "",
-  "only_refreshes_modified_files": "",
   "open_the_search_filters": "",
   "options": "Val",
   "organize_your_library": "Organisera ditt bibliotek",
@@ -699,7 +925,6 @@
   "pending": "",
   "people": "Personer",
   "people_sidebar_description": "",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "",
   "permanent_deletion_warning_setting_description": "",
   "permanently_delete": "",
@@ -714,7 +939,6 @@
   "play_memories": "",
   "play_motion_photo": "",
   "play_or_pause_video": "",
-  "point": "",
   "port": "",
   "preset": "",
   "preview": "",
@@ -724,8 +948,6 @@
   "primary": "",
   "profile_picture_set": "",
   "public_share": "",
-  "range": "",
-  "raw": "",
   "reaction_options": "",
   "read_changelog": "",
   "recent": "",
@@ -734,10 +956,10 @@
   "refreshed": "",
   "refreshes_every_file": "",
   "remove": "",
+  "remove_deleted_assets": "",
   "remove_from_album": "Ta bort från album",
   "remove_from_favorites": "",
   "remove_from_shared_link": "",
-  "remove_offline_files": "",
   "repair": "",
   "repair_no_results_message": "",
   "replace_with_upload": "",
@@ -745,7 +967,6 @@
   "reset": "",
   "reset_password": "",
   "reset_people_visibility": "",
-  "reset_settings_to_default": "",
   "restore": "Återställ",
   "restore_user": "",
   "retry_upload": "",
@@ -756,8 +977,6 @@
   "saved_settings": "",
   "say_something": "Säg något",
   "scan_all_libraries": "Skanna alla bibliotek",
-  "scan_all_library_files": "",
-  "scan_new_library_files": "",
   "scan_settings": "",
   "search": "Sök",
   "search_albums": "",
@@ -786,7 +1005,6 @@
   "select_photos": "Välj foton",
   "selected": "",
   "send_message": "",
-  "server": "Server",
   "server_stats": "Serverstatistik",
   "set": "",
   "set_as_album_cover": "",
@@ -845,35 +1063,35 @@
   "theme": "Tema",
   "theme_selection": "Val av tema",
   "theme_selection_description": "Ställ in temat automatiskt till ljust eller mörkt baserat på din webbläsares inställningar",
+  "they_will_be_merged_together": "De kommer att slås samman",
+  "third_party_resources": "Tredjepartsresurser",
   "time_based_memories": "Tidsbaserade minnen",
   "timezone": "Tidszon",
   "to_archive": "Arkivera",
   "to_change_password": "Ändra lösenord",
   "to_favorite": "Favorit",
   "to_login": "Logga in",
+  "to_trash": "Papperskorg",
   "toggle_settings": "",
   "toggle_theme": "Växla tema",
-  "toggle_visibility": "Växla synlighet",
   "total_usage": "Total användning",
   "trash": "Papperskorg",
-  "trash_all": "",
+  "trash_all": "Kasta alla",
   "trash_no_results_message": "Borttagna foton och videor kommer att visas här.",
   "trashed_items_will_be_permanently_deleted_after": "Objekt i papperskorgen raderas permanent efter {days, plural, one {# dag} other {# dagar}}.",
   "type": "Typ",
   "unarchive": "Ångra arkivering",
-  "unarchived": "",
   "unfavorite": "Avfavorisera",
   "unhide_person": "",
   "unknown": "Okänd",
-  "unknown_album": "Okänt album",
   "unknown_year": "Okänt år",
   "unlimited": "Obegränsat",
-  "unlink_oauth": "",
+  "unlink_oauth": "Ta bort länken till OAuth",
   "unlinked_oauth_account": "",
   "unsaved_change": "Osparade ändringar",
   "unselect_all": "",
   "unstack": "Stapla Av",
-  "up_next": "",
+  "up_next": "Nästa",
   "updated_password": "Lösenordet har uppdaterats",
   "upload": "Ladda upp",
   "upload_concurrency": "",
@@ -886,6 +1104,8 @@
   "user_purchase_settings": "Köp",
   "user_purchase_settings_description": "Hantera dina köp",
   "user_usage_detail": "",
+  "user_usage_stats": "Kontoinformation - statistik",
+  "user_usage_stats_description": "Se statistik - kontoanvändande",
   "username": "Användarnamn",
   "users": "Användare",
   "utilities": "Verktyg",
@@ -901,10 +1121,10 @@
   "view_album": "Visa Album",
   "view_all": "Visa alla",
   "view_all_users": "Visa alla användare",
+  "view_in_timeline": "Visa i tidslinjen",
   "view_links": "Visa länkar",
   "view_next_asset": "Visa nästa objekt",
   "view_previous_asset": "Visa föregående objekt",
-  "viewer": "",
   "waiting": "Väntar",
   "warning": "Varning",
   "week": "Vecka",
diff --git a/web/src/lib/i18n/ta.json b/i18n/ta.json
similarity index 96%
rename from web/src/lib/i18n/ta.json
rename to i18n/ta.json
index ec3f27124b..8525308e33 100644
--- a/web/src/lib/i18n/ta.json
+++ b/i18n/ta.json
@@ -41,6 +41,7 @@
     "confirm_email_below": "உறுதிப்படுத்த, கீழே \"{email}\" என தட்டச்சு செய்யவும்",
     "confirm_reprocess_all_faces": "எல்லா முகங்களையும் மீண்டும் செயலாக்க விரும்புகிறீர்களா? இது பெயரிடப்பட்ட நபர்களையும் அழிக்கும்.",
     "confirm_user_password_reset": "{user} இன் கடவுச்சொல்லை நிச்சயமாக மீட்டமைக்க விரும்புகிறீர்களா?",
+    "create_job": "வேலையை உருவாக்கு",
     "disable_login": "உள்நுழைவை முடக்கு",
     "duplicate_detection_job_description": "ஒத்த படங்களைக் கண்டறிய, சொத்துக்களில் இயந்திரக் கற்றலை இயக்கவும். ஸ்மார்ட் தேடலை நம்பியுள்ளது",
     "exclusion_pattern_description": "உங்கள் நூலகத்தை ஸ்கேன் செய்யும் போது கோப்புகளையும் கோப்புறைகளையும் புறக்கணிக்க விலக்கு வடிவங்கள் உங்களை அனுமதிக்கின்றன. RAW கோப்புகள் போன்ற நீங்கள் இறக்குமதி செய்ய விரும்பாத கோப்புகளைக் கொண்ட கோப்புறைகள் உங்களிடம் இருந்தால் இது பயனுள்ளதாக இருக்கும்.",
@@ -55,18 +56,11 @@
     "image_format_description": "WebP, JPEG ஐ விட சிறிய கோப்புகளை உருவாக்குகிறது, ஆனால் குறியாக்கம் செய்ய மெதுவாக உள்ளது.",
     "image_prefer_embedded_preview": "உட்பொதிந்த படத்தை முன்னிடு",
     "image_prefer_embedded_preview_setting_description": "",
-    "image_prefer_wide_gamut": "",
+    "image_prefer_wide_gamut": "அகன்ற வண்ணவரம்பு தேர்வு",
     "image_prefer_wide_gamut_setting_description": "",
-    "image_preview_format": "",
-    "image_preview_resolution": "",
-    "image_preview_resolution_description": "",
     "image_quality": "தரம்",
-    "image_quality_description": "படத்தின் தரம் 1-100 வரை. உயர்வானது தரத்திற்கு சிறந்தது, ஆனால் பெரிய கோப்புகளை உருவாக்குகிறது, இந்த விருப்பம் முன்னோட்டம் மற்றும் சிறுபடங்களைப் பாதிக்கிறது.",
     "image_settings": "பட அமைப்புகள்",
     "image_settings_description": "உருவாக்கப்பட்ட படங்களின் தரம் மற்றும் தெளிவுத்திறனை நிர்வகிக்கவும்",
-    "image_thumbnail_format": "சிறுபட வடிவம்",
-    "image_thumbnail_resolution": "",
-    "image_thumbnail_resolution_description": "",
     "job_concurrency": "{job} ஒத்திசைவு",
     "job_not_concurrency_safe": "இந்த வேலை ஒரே நேரத்தில் பாதுகாப்பானது அல்ல.",
     "job_settings": "",
@@ -75,9 +69,6 @@
     "jobs_delayed": "",
     "jobs_failed": "",
     "library_created": "உருவாக்கப்பட்ட புகைப்பட நூலகம்: {library}",
-    "library_cron_expression": "கிரான் வடிவம்",
-    "library_cron_expression_description": "கிரான் வடிவமைப்பைப் பயன்படுத்தி ஸ்கேனிங் இடைவெளியை அமைக்கவும். மேலும் தகவலுக்கு, எ.கா. <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "கிரான் வடிவமைப்பு முன்னமைவுகள்",
     "library_deleted": "புகைப்பட நூலகம் நீக்கப்பட்டது",
     "library_import_path_description": "இறக்குமதி செய்ய ஒரு கோப்புறையைக் குறிப்பிடவும். துணைக் கோப்புறைகள் உட்பட இந்தக் கோப்புறை படங்கள் மற்றும் வீடியோக்களுக்காக ஸ்கேன் செய்யப்படும்.",
     "library_scanning": "அவ்வப்போது ஸ்கேனிங்",
@@ -143,7 +134,7 @@
     "note_cannot_be_changed_later": "குறிப்பு: இதை பின்னர் மாற்ற முடியாது!",
     "note_unlimited_quota": "குறிப்பு: வரம்பற்ற ஒதுக்கீட்டிற்கு 0 ஐ உள்ளிடவும்",
     "notification_email_from_address": "முகவரியிலிருந்து",
-    "notification_email_from_address_description": "அனுப்புநரின் மின்னஞ்சல் முகவரி, எடுத்துக்காட்டாக: \"இம்மிச் புகைப்பட சேவையகம் <noreply@immich.app>\"",
+    "notification_email_from_address_description": "அனுப்புநரின் மின்னஞ்சல் முகவரி, எடுத்துக்காட்டாக: \"இம்மிச் புகைப்பட சேவையகம் <noreply@example.com>\"",
     "notification_email_host_description": "மின்னஞ்சல் சேவையகத்தின் ஹோஸ்ட் (எடுத்துக்காட்டாக: smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "சான்றிதழ் பிழைகளை புறக்கணிக்கவும்",
     "notification_email_ignore_certificate_errors_description": "TLS சான்றிதழ் சரிபார்ப்பு பிழைகளை புறக்கணிக்கவும் (பரிந்துரைக்கப்படவில்லை)",
@@ -191,15 +182,12 @@
     "refreshing_all_libraries": "அனைத்து நூலகங்களையும் புதுப்பிக்கிறது",
     "registration": "நிர்வாக பதிவு",
     "registration_description": "நீங்கள் கணினியில் முதல் பயனராக இருப்பதால், நீங்கள் நிர்வாகியாக நியமிக்கப்படுவீர்கள் மற்றும் நிர்வாகப் பணிகளுக்குப் பொறுப்பாவீர்கள், மேலும் உங்களால் கூடுதல் பயனர்கள் உருவாக்கப்படுவார்கள்.",
-    "removing_offline_files": "ஆஃப்லைன் கோப்புகளை நீக்குகிறது",
     "repair_all": "அனைத்தையும் பழுதுபார்க்கவும்",
     "repair_matched_items": "பொருந்தியது {count, plural, one {# உருப்படி} other {# உருப்படிகள்}}",
     "repaired_items": "பழுதுபார்க்கப்பட்டது {count, plural, one {# உருப்படி} other {# உருப்படிகள்}}",
     "require_password_change_on_login": "முதல் உள்நுழைவில் பயனர் கடவுச்சொல்லை மாற்ற வேண்டும்",
     "reset_settings_to_default": "",
     "reset_settings_to_recent_saved": "",
-    "scanning_library_for_changed_files": "",
-    "scanning_library_for_new_files": "",
     "send_welcome_email": "",
     "server_external_domain_settings": "",
     "server_external_domain_settings_description": "",
@@ -283,8 +271,6 @@
     "transcoding_threads_description": "",
     "transcoding_tone_mapping": "",
     "transcoding_tone_mapping_description": "",
-    "transcoding_tone_mapping_npl": "",
-    "transcoding_tone_mapping_npl_description": "",
     "transcoding_transcode_policy": "",
     "transcoding_transcode_policy_description": "",
     "transcoding_two_pass_encoding": "",
@@ -341,10 +327,8 @@
   "archive_or_unarchive_photo": "",
   "archive_size": "",
   "archive_size_description": "",
-  "archived": "",
   "asset_offline": "",
   "assets": "",
-  "assets_moved_to_trash": "",
   "authorized_devices": "",
   "back": "",
   "backward": "",
@@ -359,10 +343,6 @@
   "cancel_search": "",
   "cannot_merge_people": "",
   "cannot_update_the_description": "",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
   "change_date": "",
   "change_expiration_time": "",
   "change_location": "",
@@ -516,8 +496,8 @@
     "unable_to_refresh_user": "",
     "unable_to_remove_album_users": "",
     "unable_to_remove_api_key": "",
+    "unable_to_remove_deleted_assets": "",
     "unable_to_remove_library": "",
-    "unable_to_remove_offline_files": "",
     "unable_to_remove_partner": "",
     "unable_to_remove_reaction": "",
     "unable_to_repair_items": "",
@@ -553,7 +533,6 @@
   "extension": "",
   "external": "",
   "external_libraries": "",
-  "failed_to_get_people": "",
   "favorite": "",
   "favorite_or_unfavorite_photo": "",
   "favorites": "",
@@ -565,14 +544,12 @@
   "filter_people": "",
   "find_them_fast": "",
   "fix_incorrect_match": "",
-  "force_re-scan_library_files": "",
   "forward": "",
   "general": "",
   "get_help": "",
   "getting_started": "",
   "go_back": "",
   "go_to_search": "",
-  "go_to_share_page": "",
   "group_albums_by": "",
   "has_quota": "",
   "hide_gallery": "",
@@ -693,7 +670,6 @@
   "oldest_first": "",
   "online": "",
   "only_favorites": "",
-  "only_refreshes_modified_files": "",
   "open_the_search_filters": "",
   "options": "",
   "organize_your_library": "",
@@ -729,7 +705,6 @@
   "permanent_deletion_warning_setting_description": "",
   "permanently_delete": "",
   "permanently_deleted_asset": "",
-  "permanently_deleted_assets": "",
   "person": "",
   "photos": "",
   "photos_count": "",
@@ -758,10 +733,10 @@
   "refreshed": "",
   "refreshes_every_file": "",
   "remove": "",
+  "remove_deleted_assets": "",
   "remove_from_album": "",
   "remove_from_favorites": "",
   "remove_from_shared_link": "",
-  "remove_offline_files": "",
   "removed_api_key": "",
   "rename": "",
   "repair": "",
@@ -786,8 +761,6 @@
   "saved_settings": "",
   "say_something": "",
   "scan_all_libraries": "",
-  "scan_all_library_files": "",
-  "scan_new_library_files": "",
   "scan_settings": "",
   "scanning_for_album": "",
   "search": "",
@@ -819,7 +792,6 @@
   "selected": "",
   "send_message": "",
   "send_welcome_email": "",
-  "server": "",
   "server_stats": "",
   "set": "",
   "set_as_album_cover": "",
@@ -891,7 +863,6 @@
   "to_trash": "",
   "toggle_settings": "",
   "toggle_theme": "",
-  "toggle_visibility": "",
   "total_usage": "",
   "trash": "",
   "trash_all": "",
@@ -900,7 +871,6 @@
   "trashed_items_will_be_permanently_deleted_after": "",
   "type": "",
   "unarchive": "",
-  "unarchived": "",
   "unfavorite": "",
   "unhide_person": "",
   "unknown": "",
@@ -941,7 +911,6 @@
   "view_links": "",
   "view_next_asset": "",
   "view_previous_asset": "",
-  "viewer": "",
   "waiting": "",
   "week": "",
   "welcome": "",
diff --git a/web/src/lib/i18n/te.json b/i18n/te.json
similarity index 92%
rename from web/src/lib/i18n/te.json
rename to i18n/te.json
index dc92a56d57..3f0f6ff546 100644
--- a/web/src/lib/i18n/te.json
+++ b/i18n/te.json
@@ -57,16 +57,9 @@
     "image_prefer_embedded_preview_setting_description": "అందుబాటులో ఉన్నప్పుడు ఇమేజ్ ప్రాసెసింగ్‌కు ఇన్‌పుట్‌గా RAW ఫోటోలలో ఎంబెడెడ్ ప్రివ్యూలను ఉపయోగించండి. ఇది కొన్ని చిత్రాలకు మరింత ఖచ్చితమైన రంగులను ఉత్పత్తి చేయగలదు, అయితే ప్రివ్యూ నాణ్యత కెమెరాపై ఆధారపడి ఉంటుంది మరియు చిత్రం మరిన్ని కుదింపు కళాఖండాలను కలిగి ఉండవచ్చు.",
     "image_prefer_wide_gamut": "విస్తృత స్వరసప్తకానికి ప్రాధాన్యత ఇవ్వండి",
     "image_prefer_wide_gamut_setting_description": "థంబ్‌నెయిల్‌ల కోసం డిస్‌ప్లే P3ని ఉపయోగించండి. ఇది విస్తృత రంగుల ఖాళీలతో చిత్రాల వైబ్రెన్స్‌ను మెరుగ్గా భద్రపరుస్తుంది, అయితే పాత బ్రౌజర్ వెర్షన్‌తో పాత పరికరాల్లో చిత్రాలు విభిన్నంగా కనిపించవచ్చు. రంగు మార్పులను నివారించడానికి sRGB చిత్రాలు sRGB వలె ఉంచబడతాయి.",
-    "image_preview_format": "ప్రివ్యూ ఫార్మాట్",
-    "image_preview_resolution": "ప్రివ్యూ రిజల్యూషన్",
-    "image_preview_resolution_description": "ఒకే ఫోటోను చూసేటప్పుడు మరియు మెషిన్ లెర్నింగ్ కోసం ఉపయోగించబడుతుంది. అధిక రిజల్యూషన్‌లు మరింత వివరాలను భద్రపరుస్తాయి కానీ ఎన్‌కోడ్ చేయడానికి ఎక్కువ సమయం పడుతుంది, పెద్ద ఫైల్ పరిమాణాలను కలిగి ఉంటాయి మరియు యాప్ ప్రతిస్పందనను తగ్గించవచ్చు.",
     "image_quality": "నాణ్యత",
-    "image_quality_description": "1-100 నుండి చిత్ర నాణ్యత. నాణ్యత కోసం అధికమైనది ఉత్తమం కానీ పెద్ద ఫైల్‌లను ఉత్పత్తి చేస్తుంది, ఈ ఎంపిక ప్రివ్యూ మరియు థంబ్‌నెయిల్ చిత్రాలను ప్రభావితం చేస్తుంది.",
     "image_settings": "చిత్రం సెట్టింగ్‌లు",
     "image_settings_description": "రూపొందించబడిన చిత్రాల నాణ్యత మరియు రిజల్యూషన్‌ను నిర్వహించండి",
-    "image_thumbnail_format": "థంబ్‌నెయిల్ ఫార్మాట్",
-    "image_thumbnail_resolution": "థంబ్‌నెయిల్ రిజల్యూషన్",
-    "image_thumbnail_resolution_description": "ఫోటోల సమూహాలను వీక్షిస్తున్నప్పుడు ఉపయోగించబడుతుంది (ప్రధాన టైమ్‌లైన్, ఆల్బమ్ వీక్షణ మొదలైనవి). అధిక రిజల్యూషన్‌లు మరింత వివరాలను భద్రపరుస్తాయి కానీ ఎన్‌కోడ్ చేయడానికి ఎక్కువ సమయం పడుతుంది, పెద్ద ఫైల్ పరిమాణాలను కలిగి ఉంటాయి మరియు యాప్ ప్రతిస్పందనను తగ్గించవచ్చు.",
     "job_concurrency": "{job} సమ్మతి",
     "job_not_concurrency_safe": "ఈ ఉద్యోగం సమ్మతి-సురక్షితమైనది కాదు.",
     "job_settings": "ఉద్యోగ సెట్టింగ్‌లు",
@@ -75,9 +68,6 @@
     "jobs_delayed": "{jobCount, plural, other {# ఆలస్యమైంది}}",
     "jobs_failed": "{jobCount, plural, other {# విఫలమైంది}}",
     "library_created": "లైబ్రరీ సృష్టించబడింది: {library}",
-    "library_cron_expression": "క్రాన్ వ్యక్తీకరణ",
-    "library_cron_expression_description": "క్రాన్ ఆకృతిని ఉపయోగించి స్కానింగ్ విరామాన్ని సెట్ చేయండి. మరింత సమాచారం కోసం దయచేసి చూడండి ఉదా. <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "క్రాన్ వ్యక్తీకరణ ప్రీసెట్లు",
     "library_deleted": "లైబ్రరీ తొలగించబడింది",
     "library_import_path_description": "దిగుమతి చేయడానికి ఫోల్డర్‌ను పేర్కొనండి. సబ్ ఫోల్డర్‌లతో సహా ఈ ఫోల్డర్ చిత్రాలు మరియు వీడియోల కోసం స్కాన్ చేయబడుతుంది.",
     "library_scanning": "ఆవర్తన స్కానింగ్",
diff --git a/web/src/lib/i18n/th.json b/i18n/th.json
similarity index 51%
rename from web/src/lib/i18n/th.json
rename to i18n/th.json
index 19496b4238..0abeb549dc 100644
--- a/web/src/lib/i18n/th.json
+++ b/i18n/th.json
@@ -1,7 +1,7 @@
 {
-  "about": "เกี่ยวกับ",
+  "about": "รีเฟรช",
   "account": "บัญชี",
-  "account_settings": "ตั้งค่าบัญชี",
+  "account_settings": "การตั้งค่าบัญชี",
   "acknowledge": "รับทราบ",
   "action": "การดำเนินการ",
   "actions": "การดำเนินการ",
@@ -17,59 +17,68 @@
   "add_import_path": "เพิ่มพาธนำเข้า",
   "add_location": "เพิ่มตำแหน่ง",
   "add_more_users": "เพิ่มผู้ใช้งาน",
-  "add_partner": "เพิ่มพันธมิตร",
+  "add_partner": "เพิ่มคู่หู",
   "add_path": "เพิ่มพาธ",
   "add_photos": "เพิ่มรูปภาพ",
   "add_to": "เพิ่มเข้า...",
   "add_to_album": "เพิ่มเข้าอัลบั้ม",
-  "add_to_shared_album": "เพิ่มเข้าอัลบั้มที่แชร์",
+  "add_to_shared_album": "เพิ่มลงในอัลบั้มที่แชร์กัน",
+  "add_url": "เพิ่ม URL",
   "added_to_archive": "เพิ่มเข้าที่เก็บถาวร",
   "added_to_favorites": "เพิ่มเข้ารายการโปรด",
-  "added_to_favorites_count": "{count} รูปถูกเพิ่มเข้ารายการโปรด",
+  "added_to_favorites_count": "{count, number} รูปถูกเพิ่มเข้ารายการโปรด",
   "admin": {
-    "add_exclusion_pattern_description": "เพิ่มรูปแบบการยกเว้น การ Glob โดยใช้ *, ** และ ? ถูกรองรับ ถ้าต้องการละเว้นไฟล์ทั้งหมดในไดเร็กทอรีใดๆที่ชื่อว่า \"Raw\" ให้ใช้ \"**/Raw/**\" ถ้าต้องการละเว้นไฟล์ทั้งหมดที่ลงท้ายด้วย \".tif\" ให้ใช้ \"**/*.tif\" ถ้าต้องการละเว้นพาธที่เริ่มจากไดเรกทอรีบนสุดให้ใช้ \"/พาธ/ที่ต้องการ/ละเว้น/**\"",
-    "authentication_settings": "ตั้งค่าการเข้าถึง",
+    "add_exclusion_pattern_description": "เพิ่มรูปแบบข้อยกเว้น รองรับการใช้ *, ** และ ? หากต้องการละเว้นไฟล์ทั้งหมดในไดเร็กทอรีที่ชื่อว่า \"Raw\" ให้ใช้ \"**/Raw/**\" ถ้าต้องการละเว้นไฟล์ทั้งหมดที่ลงท้ายด้วย \".tif\" ให้ใช้ \"**/*.tif\" ถ้าต้องการละเว้นพาธที่เริ่มจากไดเรกทอรีบนสุดให้ใช้ \"/พาธ/ที่ต้องการ/ละเว้น/**\"",
+    "asset_offline_description": "Immich",
+    "authentication_settings": "การตั้งค่าการเข้าถึง",
     "authentication_settings_description": "จัดการรหัสผ่าน, OAuth, และตั้งค่าการเข้าถึงอื่นๆ",
     "authentication_settings_disable_all": "คุณแน่ใจว่าต้องการปิดวิธีการล็อกอินทั้งหมดหรือไม่? ล็อกอินจะถูกปิดทั้งหมด",
     "authentication_settings_reenable": "เพื่อเปิดใหม่ ให้ใช้<link>คำสั่งเซิร์ฟเวอร์</link>",
     "background_task_job": "งานเบื้องหลัง",
+    "backup_database": "สำรองฐานข้อมูล",
+    "backup_database_enable_description": "เปิดใช้งานการสำรองฐานข้อมูล",
+    "backup_keep_last_amount": "จำนวนข้อมูลสำรองก่อนหน้าที่ต้องเก็บไว้",
+    "backup_settings": "ตั้งค่ารการสำรองข้อมูล",
+    "backup_settings_description": "จัดการการตั้งค่าการสำรองฐานข้อมูล",
     "check_all": "ตรวจสอบทั้งหมด",
     "cleared_jobs": "เคลียร์งานสำหรับ: {job}",
     "config_set_by_file": "ปัจจุบันการกำหนดค่าถูกตั้งค่าโดยไฟล์กำหนดค่า",
     "confirm_delete_library": "คุณแน่ใจว่าอยากลบคลังภาพ {library} หรือไม่?",
-    "confirm_delete_library_assets": "คุณแน่ใจว่าอยากลบคลังภาพนี้หรือไม่? การกระทำนี้จะลบ {count, plural, one {# สี่อในคลัง} other {# สี่อในคลังทั้งหมด}} ออกจาก Immich โดยถาวรและไม่สามารถยกเลิกได้ ไฟล์จะยังคงอยู่บนดิสก์",
-    "confirm_email_below": "เพื่อยืนยัน พิมพ์ \"{email}\" ด้านล่าง",
-    "confirm_reprocess_all_faces": "คุณแน่ใจว่าคุณต้องการประมวลผลใบหน้าทั้งหมดใหม่หรือไม่? คนที่มีชื่อจะถูกลบไปด้วย",
+    "confirm_delete_library_assets": "คุณแน่ใจว่าอยากลบคลังภาพนี้หรือไม่? สี่อทั้งหมด {count, plural, one {# สื่อ} other {all # สื่อ}} สี่อในคลังจะถูกลบออกจาก Immich โดยถาวร ไฟล์จะยังคงอยู่บนดิสก์",
+    "confirm_email_below": "เพื่อยืนยัน พิมพ์ \"{email}\" ข้างล่าง",
+    "confirm_reprocess_all_faces": "คุณแน่ใจว่าคุณต้องการประมวลผลใบหน้าทั้งหมดใหม่? ชื่อคนจะถูกลบไปด้วย",
     "confirm_user_password_reset": "คุณแน่ใจว่าต้องการรีเซ็ตรหัสผ่านของ {user} หรือไม่?",
-    "crontab_guru": "Crontab Guru",
+    "create_job": "สร้างงาน",
+    "cron_expression": "รูปแบบ cron",
+    "cron_expression_description": "ตั้งช่วงเวลาในการสแกนโดยใช้รูปแบบ cron สำหรับข้อมูลเพิ่มเติมกรุณาอิง <link>Crontab Guru</link>",
+    "cron_expression_presets": "พรีเซ็ตรูปแบบ cron",
     "disable_login": "ปิดการล็อกอิน",
-    "disabled": "",
     "duplicate_detection_job_description": "ใช้ machine learning กับสี่อเพื่อตรวจจับรูปภาพที่คล้ายกัน โดยใช้การค้นหาอัจฉริยะ",
-    "exclusion_pattern_description": "รูปแบบการยกเว้นสามารถละเว้นไฟล์และโฟลเดอร์ขณะสแกนคลังภาพของคุณ มีประโยชน์เมื่อมีโฟลเดอร์ที่มีไฟล์ที่ไม่อยากนำเข้า เช่นไฟล์ RAW",
+    "exclusion_pattern_description": "ข้อยกเว้นสามารถละเว้นไฟล์และโฟลเดอร์ขณะสแกนคลังภาพของคุณ มีประโยชน์เมื่อโฟลเดอร์มีไฟล์ที่ไม่อยากนำเข้า เช่นไฟล์ RAW",
     "external_library_created_at": "คลังภาพภายนอก (ถูกสร้างเมื่อ {date})",
     "external_library_management": "การจัดการคลังภาพภายนอก",
     "face_detection": "การตรวจจับใบหน้า",
-    "face_detection_description": "ตรวจจับใบหน้าในสี่อโดยใช้ machine learning สำหรับวิดีโอ จะใช้ภาพตัวอย่างจากวิดีโอเท่านั้น \"ทั้งหมด\" จะประมวลผลสี่อทั้งหมด \"ขาดหาย\" จะประมวลผลสี่อที่ยังไม่ได้ประมวลผล ใบหน้าที่ถูกตรวจจับแล้วจะถูกเข้าคิวประมวลผลการจดจำใบหน้า เพิ่มเข้าไปในกลุ่มที่มีอยู่แล้วหรือคนใหม่",
+    "face_detection_description": "ตรวจจับใบหน้าในสี่อโดยใช้ machine learning วิดีโอจะใช้ภาพตัวอย่างจากวิดีโอเท่านั้น \"ทั้งหมด\" จะประมวลผลสี่อทั้งหมด \"ขาดหาย\" จะประมวลผลสี่อที่ยังไม่ได้ประมวลผล ใบหน้าที่ถูกตรวจจับแล้วจะถูกเข้าคิวประมวลผลการจดจำใบหน้า เพิ่มเข้าไปในกลุ่มที่มีอยู่แล้วหรือคนใหม่",
     "facial_recognition_job_description": "นำใบหน้าที่ตรวจจับได้ไปจับกลุ่มตามผู้คน ขั้นตอนนี้ทำงานหลังจากตรวจจับใบหน้าสำเร็จ \"ทั้งหมด\" จะจำกลุ่มใบหน้าทั้งหมดใหม่ \"ขาดหาย\" จะจัดคิวใบหน้าที่ยังไม่ได้ระบุคน",
     "failed_job_command": "คำสั่ง {command} ของงาน {job} ล้มเหลว",
-    "force_delete_user_warning": "คําเตือน: ขั้นตอนนี้จะลบผู้ใช้และสื่อทั้งหมดทันที ขั้นตอนนี้จะย้อนกลับมาไม่ได้และกู้คืนไฟล์ไม่ได้.",
+    "force_delete_user_warning": "คําเตือน: ขั้นตอนนี้จะลบผู้ใช้งานและสื่อทั้งหมดทันที ไม่สามารถย้อนกลับมาได้และกู้คืนไฟล์ไม่ได้",
     "forcing_refresh_library_files": "บังคับรีเฟรชไฟล์ทั้งหมด",
-    "image_format_description": "WebP จะสร้างไฟล์ที่เล็กกว่า JPEG แต่ใช้เวลา encode นานกว่า",
+    "image_format": "Format",
+    "image_format_description": "WebP จะให้ไฟล์ที่เล็กกว่า JPEG แต่ใช้เวลาแปลงไฟล์นานกว่า",
     "image_prefer_embedded_preview": "ใช้พรีวิวแบบฝังตัว",
     "image_prefer_embedded_preview_setting_description": "ใช้พรีวิวฝังตัวในรูปภาพ RAW ในการวิเคราะห์รูปภาพถ้ามี แต่คุณภาพรูปภาพขึ้นอยู่กับกล้อง และอาจจะมีสิ่งตกค้างจากการย่อขนาดไฟล์",
     "image_prefer_wide_gamut": "ใช้ช่วงสีกว้าง",
-    "image_prefer_wide_gamut_setting_description": "ใช้ Display P3 สำหรับภาพย่อ ซึ่งจะรักษาความมีชีวิตชีวาของภาพที่ใช้ปริภูมิสีกว้าง แต่ภาพบนอุปกรณ์หรือเบราว์เซอร์เก่าอาจปรากฏแตกต่างออกไป ภาพ sRGB จะถูกเก็บเป็น sRGB เพื่อป้องกันไม่ให้สีเคลื่อน",
-    "image_preview_format": "รูปแบบพรีวิว",
-    "image_preview_resolution": "ความละเอียดพรีวิว",
-    "image_preview_resolution_description": "ใช้เมื่อดูรูปเดียวและสำหรับ machine learning ความละเอียดสูงสามารถเก็บรายละเอียดดีกว่าแต่ใช้เวลา encode นานกว่า ขนาดไฟล์ใหญ่กว่า และลดการตอบสนองของแอป",
+    "image_prefer_wide_gamut_setting_description": "ใช้การแสดงผลแบบ P3 สําหรับภาพตัวอย่าง คงความเข้มและความกว้างขอบเขตสี แต่ภาพอาจดูแตกต่างกันในอุปกรณ์เก่าที่มีเว็บเบราว์เซอร์รุ่นเก่า ภาพ sRGB จะถูกเก็บในรูปแบบ sRGB เพื่อลดการเคลื่อนของสี",
+    "image_preview_quality_description": "คุณภาพการแสดงตัวอย่างตั้งแต่ 1-100 ยิ่งสูงก็ยิ่งดี แต่จะทำให้ไฟล์มีขนาดใหญ่ขึ้นและอาจทำให้แอปตอบสนองช้าลง การตั้งค่าต่ำอาจส่งผลต่อคุณภาพ Machine Learning",
+    "image_preview_title": "ตั้งค่าพรีวิว",
     "image_quality": "คุณภาพ",
-    "image_quality_description": "คุณภาพรูปจาก 1-100 ค่าสูงมีคุณภาพสูงกว่าแต่ขนาดไฟล์ใหญ่กว่า ตัวเลือกนี้ส่งผลต่อภาพพรีวิวและภาพขนาดย่อ",
-    "image_settings": "ตั้งค่ารูปภาพ",
-    "image_settings_description": "จัดการคุณภาพและความละเอียดของภาพที่สร้างขึ้น",
-    "image_thumbnail_format": "รูปแบบภาพย่อ",
-    "image_thumbnail_resolution": "ความละเอียดภาพย่อ",
-    "image_thumbnail_resolution_description": "ใช้เมื่อดูกลุ่มรูปภาพ (ไทม์ไลน์หลัก, หน้าอัลบั้ม, ฯลฯ) ความสะเอียดที่สูงกว่าจะเก็บรายละเอียดได้มากกว่าแต่ใช้เวลา encode นานกว่า ขนาดไฟล์ใหญ่กว่า และลดการตอบสนองของแอป",
-    "job_concurrency": "{job} พร้อมกัน",
+    "image_resolution": "ความละเอียด",
+    "image_resolution_description": "ความละเอียดสูกว่าสามารถเก็บรายละเอียดได้มากกว่าแต่ใช้เวลา encode นานกว่า ไฟล์ใหญ่กว่า และลดความตอบสนองของแอป",
+    "image_settings": "การตั้งค่ารูปภาพ",
+    "image_settings_description": "จัดการคุณภาพและความคมชัดของภาพที่สร้างขึ้น",
+    "image_thumbnail_title": "ตั้งค่า Thumbnail",
+    "job_concurrency": "{job} งานพร้อมกัน",
+    "job_created": "สร้างงานเรียบร้อย",
     "job_not_concurrency_safe": "งานนี้ทำงานพร้อมกันแบบปลอดภัยไม่ได้",
     "job_settings": "การตั้งค่างาน",
     "job_settings_description": "จัดการการทำหลายงานพร้อมกัน",
@@ -77,17 +86,14 @@
     "jobs_delayed": "{jobCount, plural, other {# ล่าช้า}}",
     "jobs_failed": "{jobCount, plural, other {# ล้มเหลว}}",
     "library_created": "สร้างคลังภาพ: {library}",
-    "library_cron_expression": "รูปแบบ Cron",
-    "library_cron_expression_description": "ตั้งช่วงเวลาสแกนโดยใช้รูปแบบ cron สามารถดูข้อมูลเพิ่มเติมได้ที่ <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "แม่แบบรูปแบบ Cron",
     "library_deleted": "คลังภาพถูกลบ",
-    "library_import_path_description": "ระบุโฟลเดอร์เพื่อนําเข้า โฟลเดอร์นี้และโฟลเดอร์ย่อยจะถูกค้นหาภาพและวิดีโอ",
+    "library_import_path_description": "ระบุโฟลเดอร์เพื่อนําเข้า โฟลเดอร์นี้และโฟลเดอร์ย่อยจะถูกค้นหาภาพและวิดีโอ.",
     "library_scanning": "การสแกนเป็นระยะ",
-    "library_scanning_description": "ตั้งค่าการสแกนเป็นระยะ",
-    "library_scanning_enable_description": "เปิดการสแกนเป็นระยะ",
+    "library_scanning_description": "ตั้งค่าการสแกนคลังภาพเป็นระยะ",
+    "library_scanning_enable_description": "เปิดการสแกนคลังภาพเป็นระยะ",
     "library_settings": "คลังภาพภายนอก",
     "library_settings_description": "จัดการการตั้งค่าคลังภาพภายนอก",
-    "library_tasks_description": "ปฏิบัติงานคลังภาพ",
+    "library_tasks_description": "ทำงานคลังภาพ",
     "library_watching_enable_description": "ดูคลังภาพภายนอกสำหรับการเปลี่ยนแปลงของไฟล์",
     "library_watching_settings": "การดูคลังภาพภายนอก (ฟีเจอร์ทดลอง)",
     "library_watching_settings_description": "หาไฟล์ที่เปลี่ยนแปลงโดยอัตโนมัติ",
@@ -102,12 +108,12 @@
     "machine_learning_duplicate_detection_setting_description": "ใช้ CLIP เพื่อแสดงที่มีแนวโน้มซ้ํา",
     "machine_learning_enabled": "เปิดใช้ machine learning",
     "machine_learning_enabled_description": "หากปิดใช้งาน คุณสมบัติ ML ทั้งหมดจะปิดการใช้งานโดยไม่คํานึงถึงการตั้งค่าด้านล่าง.",
-    "machine_learning_facial_recognition": "การตรวจจับใบหน้า",
-    "machine_learning_facial_recognition_description": "ตรวจจับ จำแนก และรวมกลุ่มใบหน้าในภาพ",
-    "machine_learning_facial_recognition_model": "โมเดลสำหรับการตรวจจับใบหน้า",
+    "machine_learning_facial_recognition": "การจดจำใบหน้า",
+    "machine_learning_facial_recognition_description": "ตรวจจับ จดจำ และจำแนกใบหน้าในภาพ",
+    "machine_learning_facial_recognition_model": "โมเดลสำหรับการจดจำใบหน้า",
     "machine_learning_facial_recognition_model_description": "โมเดลเรียงตามขนาดลดหลั่นลงมา โมเดลที่ใหญ่กว่าจะประมวลผลช้ากว่าและใช้หน่วยความจำมากกว่า แต่ให้ผลลัพธ์ที่ดีขึ้น หมายเหตุไว้ว่าเมื่อเปลี่ยนโมเดล คุณต้องรันงานตรวจจับใบหน้าทุกภาพใหม่ทั้งหมด",
     "machine_learning_facial_recognition_setting": "เปิดใช้การจดจําใบหน้า",
-    "machine_learning_facial_recognition_setting_description": "หากปิดใช้งาน จะไม่มีการตรวจจับใบหน้าบนรูปภาพและจะไม่มีส่วนผู้คนในหน้าเว็บ",
+    "machine_learning_facial_recognition_setting_description": "หากปิดใช้งาน จะไม่มีการจดจำใบหน้าบนรูปภาพและจะไม่มีส่วนผู้คนในหน้าเว็บ",
     "machine_learning_max_detection_distance": "ระยะทางการตรวจจับสูงสุด",
     "machine_learning_max_detection_distance_description": "ระยะห่างระหว่างสองภาพที่ไกลสุดที่ถือว่าเป็นภาพซ้ำ ค่าระหว่าง 0.001-0.1 ค่ายิ่งสูงจะยิ่งเจอภาพซ้ำมากขึ้น แต่อาจมีผลผิดพลาด",
     "machine_learning_max_recognition_distance": "ระยะทางการจดจำสูงสุด",
@@ -116,8 +122,8 @@
     "machine_learning_min_detection_score_description": "ค่าความมั่นใจในการตรวจจับใบหน้า จาก 0-1 ค่ายิ่งต่ำจะยิ่งตรวจจับใบหน้ามากขึ้น แต่อาจมีผลผิดพลาด",
     "machine_learning_min_recognized_faces": "จดจำใบหน้าขั้นต่ำ",
     "machine_learning_min_recognized_faces_description": "จำนวนใบหน้าขั้นต่ำที่จะสร้างคนขึ้นมา การเพิ่มค่านี้จะทำให้การจดจำใบหน้าแม่นยำกว่าแต่เพิ่มโอกาสที่ใบหน้าจะไม่ถูกมอบหมายให้กับบุคคล",
-    "machine_learning_settings": "การตั้งค่า Machine Learning",
-    "machine_learning_settings_description": "การจัดการฟีเจอร์และการตั้งค่า machine learning",
+    "machine_learning_settings": "การตั้งค่า machine learning",
+    "machine_learning_settings_description": "จัดการการตั้งค่า machine learning",
     "machine_learning_smart_search": "การค้นหาอัจฉริยะ",
     "machine_learning_smart_search_description": "ค้นหาภาพโดยใช้ความหมายจากการใช้ CLIP",
     "machine_learning_smart_search_enabled": "เปิดใช้งานการค้นหาอัจฉริยะ",
@@ -129,16 +135,21 @@
     "map_enable_description": "เปิดใช้งานแผนที่",
     "map_gps_settings": "การตั้งค่าแผนที่และ GPS",
     "map_gps_settings_description": "จัดการการตั้งค่าแผนที่และ GPS (Reverse Geocoding)",
+    "map_implications": "ฟีเจอร์แผนที่ต้องการบริการแผ่นแผนที่จากภายนอก (tiles.immich.cloud)",
     "map_light_style": "แบบสว่าง",
     "map_manage_reverse_geocoding_settings": "จัดการการตั้งค่า<link>แปลงพิกัดภูมิศาสตร์ </link>",
     "map_reverse_geocoding": "ประมวลผลชื่อทางภูมิศาสตร์",
     "map_reverse_geocoding_enable_description": "เปิดใช้งานประมวลผลชื่อทางภูมิศาสตร์",
     "map_reverse_geocoding_settings": "การตั้งค่าประมวลผลชื่อทางภูมิศาสตร์",
-    "map_settings": "การตั้งค่าแผนที่",
+    "map_settings": "การตั้งค่าแผนที่และ GPS",
     "map_settings_description": "จัดการการตั้งค่าแผนที่",
     "map_style_description": "URL ไปยังธีมแผนที่ style.json",
     "metadata_extraction_job": "ดึงข้อมูล metadata",
-    "metadata_extraction_job_description": "ดึงข้อมูล metadata จากสื่อ เช่น GPS และความละเอียด",
+    "metadata_extraction_job_description": "ดึงข้อมูล metadata จากสื่อ เช่น GPS และความคมชัด",
+    "metadata_faces_import_setting": "เปิดการนำเข้าข้อมูลใบหน้า",
+    "metadata_faces_import_setting_description": "นำเข้าข้อมูลใบหน้าจาก EXIF ของไฟล์ภาพและไฟล์ประกอบ",
+    "metadata_settings": "การตั้งค่า Metadata",
+    "metadata_settings_description": "จัดการการตั้งค่า Metadata",
     "migration_job": "การโยกย้าย",
     "migration_job_description": "ย้ายภาพตัวอย่างสื่อและใบหน้าไปยังโครงสร้างโฟลเดอร์ล่าสุด",
     "no_paths_added": "ไม่ได้เพิ่มพาธ",
@@ -153,11 +164,11 @@
     "notification_email_ignore_certificate_errors_description": "ไม่สนใจการยืนยันใบรับรอง TLS ผิดพลาด (ไม่แนะนำ)",
     "notification_email_password_description": "รหัสผ่านที่ใช้เมื่อเข้าถึงเซิร์ฟเวอร์อีเมล",
     "notification_email_port_description": "พอร์ตของเซิร์ฟเวอร์อีเมล (เช่น 25, 465, หรือ 587)",
-    "notification_email_sent_test_email_button": "ส่งอีเมลทดสอบและบันทึก",
+    "notification_email_sent_test_email_button": "ส่งอีเมลทดลองและบันทึก",
     "notification_email_setting_description": "การตั้งค่าสำหรับการส่งการแจ้งเตือนอีเมล",
     "notification_email_test_email": "ส่งอีเมลทดลอง",
-    "notification_email_test_email_failed": "ส่งอีเมลทดลองล้มเหลว โปรดตรวจสอบค่าที่ตั้ง",
-    "notification_email_test_email_sent": "อีเมลทดสอบถูกส่งไปยัง {email} กรุณาตรวจสอบกล่องจดหมาย",
+    "notification_email_test_email_failed": "ส่งอีเมลทดลองล้มเหลว โปรดตรวจสอบค่าที่ตั้งไว้",
+    "notification_email_test_email_sent": "อีเมลทดลองถูกส่งไปยัง {email} กรุณาตรวจสอบกล่องจดหมาย",
     "notification_email_username_description": "ชื่อผู้ใช้งานเมื่อเข้าถึงเซิร์ฟเวอร์อีเมล",
     "notification_enable_email_notifications": "เปิดการแจ้งเตือนผ่านอีเมล",
     "notification_settings": "การตั้งค่าการแจ้งเตือน",
@@ -173,7 +184,9 @@
     "oauth_issuer_url": "ผู้ออก URL",
     "oauth_mobile_redirect_uri": "URI เปลี่ยนเส้นทางบนโทรศัพท์",
     "oauth_mobile_redirect_uri_override": "แทนที่ URI เปลี่ยนเส้นทางบนโทรศัพท์",
-    "oauth_mobile_redirect_uri_override_description": "เปิดเมื่อ 'app.immich:/' เป็น URI เปลี่ยนเส้นทางที่ไม่ถูกต้อง",
+    "oauth_mobile_redirect_uri_override_description": "เปิดเมื่อ 'app.immich:/' เป็น URI ที่เปลี่ยนเส้นทางไม่ถูกต้อง",
+    "oauth_profile_signing_algorithm": "อัลกอริทึมการรับรองบัญชีผู้ใช้",
+    "oauth_profile_signing_algorithm_description": "อัลกอริทึมใช้ในการรับรองบัญชีผู้ใช้",
     "oauth_scope": "ขอบเขต",
     "oauth_settings": "OAuth",
     "oauth_settings_description": "จัดการการตั้งค่าล็อกอินผ่าน OAuth",
@@ -190,24 +203,24 @@
     "password_enable_description": "ล็อกอินกับอีเมลและรหัสผ่าน",
     "password_settings": "ล็อกอินผ่านรหัสผ่าน",
     "password_settings_description": "จัดการการตั้งค่าของการล็อกอินผ่านรหัสผ่าน",
-    "paths_validated_successfully": "พาธทั้งหมดถูกตรวจสอบสำเร็จแล้ว",
+    "paths_validated_successfully": "เส้นทางทั้งหมดถูกตรวจสอบสำเร็จแล้ว",
     "quota_size_gib": "โควตา (GiB)",
     "refreshing_all_libraries": "รีเฟรชคลังภาพทั้งหมด",
     "registration": "ลงทะเบียนผู้จัดการ",
     "registration_description": "เนื่องจากคุณเป็นผู้ใช้งานแรกของระบบ คุณจะถูกแต่งตั้งเป็นผู้จัดการและรับผิดชอบงานบริหาร ผู้ใช้งานเพิ่มเติมจะถูกสร้างโดยคุณ",
-    "removing_offline_files": "กำลังลบไฟล์ออฟไลน์",
     "repair_all": "ซ่อมแซมทั้งหมด",
     "repair_matched_items": "จับคู่ {count, plural, one {# รายการ} other {# รายการ}}",
     "repaired_items": "ซ่อมแซม {count, plural, one {# รายการ} other {# รายการ}}",
-    "require_password_change_on_login": "บังคับผู้ใช้ให้เปลี่ยนรหัสผ่านเมื่อเข้าสู่ระบบครั้งแรก",
+    "require_password_change_on_login": "บังคับผู้ใช้งานให้เปลี่ยนรหัสผ่านเมื่อเข้าสู่ระบบครั้งแรก",
     "reset_settings_to_default": "ตั้งค่าการตั้งค่าเป็นค่าเริ่มต้น",
     "reset_settings_to_recent_saved": "ตั้งค่าการตั้งค่าเป็นค่าล่าสุด",
-    "scanning_library_for_changed_files": "สแกนคลังภาพสำหรับไฟล์ที่เปลี่ยนไป",
-    "scanning_library_for_new_files": "สแกนคลังภาพสำหรับไฟล์ใหม่",
+    "scanning_library": "แสกนคลัง",
+    "search_jobs": "ค้นหางาน",
     "send_welcome_email": "ส่งอีเมลต้อนรับ",
     "server_external_domain_settings": "โดเมนภายนอก",
-    "server_external_domain_settings_description": "โดเมนสำหรับลิงก์แชร์สาธารณะ รวม http(s)://",
-    "server_settings": "ตั้งค่าเซิร์ฟเวอร์",
+    "server_external_domain_settings_description": "โดเมนสำหรับลิงก์แชร์สาธารณะ แบบมี http(s)://",
+    "server_public_users": "ผู้ใช้สาธารณะ",
+    "server_settings": "การตั้งค่าเซิร์ฟเวอร์",
     "server_settings_description": "จัดการการตั้งค่าเซิร์ฟเวอร์",
     "server_welcome_message": "ข้อความต้อนรับ",
     "server_welcome_message_description": "ข้อความที่แสดงบนหน้าล็อกอิน",
@@ -218,13 +231,13 @@
     "storage_template_date_time_description": "เวลาประทับบนสื่อถูกใช้สำหรับข้อมูลวันเวลา",
     "storage_template_date_time_sample": "ตัวอย่างเวลา {date}",
     "storage_template_enable_description": "เปิดใช้งานการจัดเทมเพลตที่เก็บข้อมูล",
-    "storage_template_hash_verification_enabled": "เปิดใช้การตรวจสอบ Hash แล้ว",
+    "storage_template_hash_verification_enabled": "ตรวจสอบ hash ไม่ผ่าน",
     "storage_template_hash_verification_enabled_description": "เปิดใช้งานการตรวจสอบ hash ห้ามปิดใช้งานเว้นแต่คุณจะเข้าใจผลกระทบ",
     "storage_template_migration": "การย้ายเทมเพลตที่เก็บข้อมูล",
     "storage_template_migration_description": "ใช้<link>{template}</link>ปัจจุบันกับสื่อที่อัพโหลดก่อนหน้านี้",
     "storage_template_migration_job": "",
     "storage_template_settings": "เทมเพลตการจัดเก็บข้อมูล",
-    "storage_template_settings_description": "",
+    "storage_template_settings_description": "จัดการโครงสร้างโฟลเดอร์และชื่อไฟล์ที่อัพโหลด",
     "system_settings": "การตั้งค่าระบบ",
     "theme_custom_css_settings": "CSS กําหนดเอง",
     "theme_custom_css_settings_description": "Cascading Style Sheets ช่วยให้ปรับแต่งเค้าโครง Immich ได้",
@@ -233,7 +246,6 @@
     "these_files_matched_by_checksum": "ไฟล์เหล่านี้เหมือนกันจาก checksums",
     "thumbnail_generation_job": "สร้างภาพตัวอย่าง",
     "thumbnail_generation_job_description": "สร้างภาพตัวอย่างขนาดใหญ่ ขนาดเล็กและแบบเบลอ สําหรับแต่ละสื่อและบุคคล",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "API เร่งความเร็วแปลงสื่อ",
     "transcoding_acceleration_api_description": "",
     "transcoding_acceleration_nvenc": "NVENC (ต้องมีการ์ดจอ NVIDIA)",
@@ -243,14 +255,14 @@
     "transcoding_accepted_audio_codecs": "แบบไฟล์เสียงที่ยอมรับ",
     "transcoding_accepted_audio_codecs_description": "เลือกแบบไฟล์เสียงที่จะไม่ถูกแปลงใหม่ ใช้สําหรับกฏการแปลงแบบไฟล์",
     "transcoding_accepted_video_codecs": "แบบไฟล์วิดีโอที่ยอมรับ",
-    "transcoding_accepted_video_codecs_description": "เลือกแบบไฟล์วิดีโอที่จะไม่ถูกแปลงใหม่ ใช้สําหรับกฏการแปลงแบบไฟล์",
-    "transcoding_advanced_options_description": "ตัวเลือกที่ผู้ใช้ส่วนใหญ่ไม่จำเป็นต้องเปลี่ยน",
+    "transcoding_accepted_video_codecs_description": "เลือกแบบไฟล์วิดีโอที่จะไม่ถูกแปลงใหม่ ใช้สําหรับกฎการแปลงแบบไฟล์",
+    "transcoding_advanced_options_description": "ตัวเลือกที่ผู้ใช้งานส่วนใหญ่ไม่จำเป็นต้องเปลี่ยน",
     "transcoding_audio_codec": "แบบไฟล์เสียง",
-    "transcoding_audio_codec_description": "Opus ให้คุณภาพสูงสุด แต่อุปกรณ์เก่าหรือซอฟต์แวร์เก่าอาจจะเข้ากันไม่ได้",
+    "transcoding_audio_codec_description": "Opus ให้คุณภาพสูงสุด แต่อาจจะเข้ากันไม่ได้กับอุปกรณ์เก่าหรือซอฟต์แวร์เก่า",
     "transcoding_bitrate_description": "วิดีโอมีค่า bitrate สูงกว่าค่าสูงสุดหรือไฟล์วิดีโอไม่รองรับ",
     "transcoding_constant_quality_mode": "โหมดคุณภาพคงที่",
-    "transcoding_constant_quality_mode_description": "ICQ ดีกว่า CQP แต่อุปกรณ์บางตัวอาจจะไม่รองรับโหมดนี้ การตั้งค่าตัวนี้จะเลือกโหมดที่ระบุไว้เมื่อใช้การแปลงคุณภาพไฟล์ ไม่สน NVENC เพราะไม่รองรับ ICQ",
-    "transcoding_constant_rate_factor": "ปัจจัยค่าคงที่ (-crf)",
+    "transcoding_constant_quality_mode_description": "ICQ ดีกว่า CQP แต่อุปกรณ์บางตัวอาจจะไม่รองรับโหมดนี้ การตั้งค่าตัวนี้จะเลือกโหมดที่ระบุไว้เมื่อใช้การแปลงคุณภาพไฟล์ ไม่สนใจ NVENC เพราะไม่รองรับ ICQ",
+    "transcoding_constant_rate_factor": "ตัวแปรค่าคงที่ (-crf)",
     "transcoding_constant_rate_factor_description": "คุณภาพของวิดีโอ ค่าโดยปกติคือ 23 สําหรับ H.264, 28 สําหรับ HEVC, 31 สําหรับ VP9 และ 35 สําหรับ AV1 ค่าต่ำกว่าคุณภาพจะดีกว่า แต่ไฟล์จะขนาดใหญ่กว่า",
     "transcoding_disabled_description": "ไม่แปลงไฟล์วิดีโอเลย อาจเล่นวิดีโอในเครื่องเล่นบางตัวไม่ได้",
     "transcoding_hardware_acceleration": "การเร่งความเร็วด้วยฮาร์ดแวร์",
@@ -264,7 +276,7 @@
     "transcoding_max_bitrate_description": "การตั้งค่า bitrate สูงสุดจะสามารถคาดเดาขนาดไฟล์ได้มากขึ้นโดยไม่กระทบคุณภาพ สำหรับความคมชัด 720p ค่าทั่วไปคือ 2600k สําหรับ VP9 หรือ HEVC, 4500k สําหรับ H.264 ปิดการตั้งค่าเมี่อตั้งค่าเป็น 0",
     "transcoding_max_keyframe_interval": "",
     "transcoding_max_keyframe_interval_description": "",
-    "transcoding_optimal_description": "วิดีโอที่สูงกว่าความละเอียดเป้าหมายหรือไม่ได้อยู่ในรูปแบบที่รองรับ",
+    "transcoding_optimal_description": "วีดิโอมีความคมชัดสูงกว่าเป้าหมายหรืออยู่ในรูปแบบที่รับไม่ได้",
     "transcoding_preferred_hardware_device": "",
     "transcoding_preferred_hardware_device_description": "",
     "transcoding_preset_preset": "",
@@ -273,91 +285,84 @@
     "transcoding_reference_frames_description": "",
     "transcoding_required_description": "",
     "transcoding_settings": "",
-    "transcoding_settings_description": "",
-    "transcoding_target_resolution": "ความละเอียดเป้าหมาย",
-    "transcoding_target_resolution_description": "",
+    "transcoding_settings_description": "จัดการข้อมูลความคมชัดและแบบไฟล์วิดีโอ",
+    "transcoding_target_resolution": "เป้าหมายความคมชัด",
+    "transcoding_target_resolution_description": "ความคมชัดที่สูงกว่าจะเก็บรายละเอียดดีกว่าแต่ใช้เวลาแปลงไฟล์นานกว่า ขนาดไฟล์ใหญ่กว่า และลดการตอบสนองของแอป",
     "transcoding_temporal_aq": "",
     "transcoding_temporal_aq_description": "",
     "transcoding_threads": "เธรด",
-    "transcoding_threads_description": "",
+    "transcoding_threads_description": "ค่ายิ่งเยอะจะแปลงไฟล์เร็วกว่า แต่จะเหลือพื้นที่ให้เซิร์ฟเวอร์ประมวลผลงานอื่นน้อยลงเมื่อทํางานนี้ ค่านี้ไม่ควรมากกว่าจํานวน CPU core จะประมวลผลเต็มที่เมื่อตั้งเป็น 0",
     "transcoding_tone_mapping": "การฉายโทนสี",
     "transcoding_tone_mapping_description": "",
-    "transcoding_tone_mapping_npl": "",
-    "transcoding_tone_mapping_npl_description": "",
-    "transcoding_transcode_policy": "",
-    "transcoding_two_pass_encoding": "",
-    "transcoding_two_pass_encoding_setting_description": "",
-    "transcoding_video_codec": "",
-    "transcoding_video_codec_description": "",
-    "trash_enabled_description": "",
-    "trash_number_of_days": "",
-    "trash_number_of_days_description": "",
+    "transcoding_transcode_policy": "กฎการแปลงไฟล์",
+    "transcoding_two_pass_encoding": "การแปลงไฟล์สองรอบ",
+    "transcoding_two_pass_encoding_setting_description": "การแปลงไฟล์สองรอบจะช่วยให้ได้วิดีโอที่ดีขึ้น เมื่อเปิดใช้งาน bitrate สูงสุด (จำเป็นสำหรับไฟล์ H.264 และ HEVC) โหมดนี้จะใช้ช่วง bitrate ที่ขึ้นอยู่กับค่า bitrate สูงสุดและไม่สนใจ CRF สำหรับ VP9 สามารถใช้ค่า CRF ได้ถ้าปิดใช้งาน bitrate สูงสุด",
+    "transcoding_video_codec": "แบบไฟล์วิดีโอ",
+    "transcoding_video_codec_description": "VP9 มีประสิทธิภาพสูงและเข้ากันกับเว็บได้ดี แต่ใช้เวลาแปลงไฟล์นานกว่า HEVC มีประสิทธิภาพคล้ายกัน แต่เข้ากันกับเว็บได้น้อยกว่า H.264 เข้ากันกับทุกอุปกรณ์ และแปลงไฟล์เร็ว แต่ได้ไฟล์ที่ใหญ่ขึ้น AV1 เป็นไฟล์ที่มีประสิทธิภาพมากที่สุด แต่ไม่เข้ากันกับอุปกรณ์เก่า",
+    "trash_enabled_description": "เปิดใช้งานถังขยะ",
+    "trash_number_of_days": "จํานวนวัน",
+    "trash_number_of_days_description": "จํานวนวันที่เก็บสื่อไว้ในถังขยะก่อนที่จะลบถาวร",
     "trash_settings": "การตั้งค่าถังขยะ",
     "trash_settings_description": "จัดการการตั้งค่าถังขยะ",
-    "user_delete_delay_settings": "",
-    "user_delete_delay_settings_description": "",
-    "user_settings": "",
-    "user_settings_description": "",
-    "version_check_enabled_description": "",
-    "version_check_settings": "",
-    "version_check_settings_description": "",
-    "video_conversion_job_description": ""
+    "user_delete_delay_settings": "ลบการถ่วงเวลา",
+    "user_delete_delay_settings_description": "จํานวนวันหลังจากที่เอาออกเพื่อลบบัญชีผู้ใช้และสื่อถาวร งานลบบัญชีผู้ใช้ทํางานทุกเที่ยงคืนเพื่อตรวจสอบผู้ใช้ที่พร้อมที่จะถูกลบข้อมูลแล้ว การตั้งค่าครั้งนี้จะมีผลครั้งต่อไป",
+    "user_settings": "การตั้งค่าผู้ใช้",
+    "user_settings_description": "จัดการการตั้งค่าผู้ใช้",
+    "version_check_enabled_description": "เช็ค GitHub เป็นระยะ ๆ เพื่อตรวจสอบรุ่นใหม่",
+    "version_check_settings": "ตรวจสอบรุ่น",
+    "version_check_settings_description": "เปิด/ปิดการแจ้งเตือนรุ่นใหม่",
+    "video_conversion_job_description": "แปลงไฟล์วิดีโอเพึ่อรองรับบราวเซอร์และเครื่องเล่นอื่น ๆ มากขึ้น"
   },
-  "admin_email": "อีเมลผู้ดูแล",
-  "admin_password": "รหัสผ่านผู้ดูแล",
-  "administration": "การจัดการ",
+  "admin_email": "อีเมลผู้ดูแลระบบ",
+  "admin_password": "รหัสผ่านผู้ดูแลระบบ",
+  "administration": "การดูแลระบบ",
   "advanced": "ขั้นสูง",
   "age_months": "อายุ {months, plural, one {# เดือน} other {# เดือน}}",
   "age_year_months": "อายุ 1 ปี {months, plural, one {# เดือน} other {# เดือน}}",
   "age_years": "{years, plural, other {อายุ #}}",
   "album_added": "เพิ่มอัลบั้มแล้ว",
-  "album_added_notification_setting_description": "",
-  "album_cover_updated": "",
-  "album_info_updated": "",
-  "album_name": "",
-  "album_options": "",
-  "album_updated": "",
-  "album_updated_setting_description": "",
+  "album_added_notification_setting_description": "แจ้งเตือนอีเมลเมื่อคุณถูกเพิ่มไปในอัลบั้มที่แชร์กัน",
+  "album_cover_updated": "อัพเดทหน้าปกอัลบั้มแล้ว",
+  "album_info_updated": "อัพเดทข้อมูลอัลบั้มแล้ว",
+  "album_name": "ชื่ออัลบั้ม",
+  "album_options": "ตัวเลือกอัลบั้ม",
+  "album_updated": "อัพเดทอัลบั้มแล้ว",
+  "album_updated_setting_description": "แจ้งเตือนอีเมลเมื่ออัลบั้มที่แชร์กันมีสื่อใหม่",
   "albums": "อัลบั้ม",
   "all": "ทั้งหมด",
   "all_albums": "อัลบั้มทั้งหมด",
-  "all_people": "ผู้คนทั้งหมด",
+  "all_people": "ทุกคน",
   "all_videos": "วิดีโอทั้งหมด",
-  "allow_dark_mode": "",
-  "allow_edits": "",
-  "api_key": "คีย์ API",
-  "api_keys": "คีย์ API",
+  "allow_dark_mode": "อนุญาตโหมดมืด",
+  "allow_edits": "อนุญาตให้แก้ไขได้",
+  "api_key": "กุญแจ API",
+  "api_keys": "กุญแจ API",
   "app_settings": "การตั้งค่าแอป",
-  "appears_in": "ปรากฏอยู่ใน",
+  "appears_in": "อยู่ใน",
   "archive": "เก็บถาวร",
-  "archive_or_unarchive_photo": "",
-  "archived": "เก็บถาวร",
+  "archive_or_unarchive_photo": "เก็บ/ไม่เก็บภาพถาวร",
   "are_these_the_same_person": "เป็นคนเดียวกันหรือไม่?",
-  "asset_offline": "",
+  "asset_offline": "สื่อออฟไลน์",
   "asset_skipped": "ข้ามแล้ว",
   "asset_uploaded": "อัปโหลดแล้ว",
   "asset_uploading": "กำลังอัปโหลด...",
-  "assets": "ทรัพยากร",
-  "authorized_devices": "",
+  "assets": "สื่อ",
+  "authorized_devices": "อุปกรณ์ที่ได้รับอนุญาต",
   "back": "กลับ",
   "backward": "กลับหลัง",
-  "blurred_background": "",
+  "blurred_background": "พื้นหลังแบบเบลอ",
   "camera": "กล้อง",
-  "camera_brand": "",
-  "camera_model": "",
+  "camera_brand": "ยี่ห้อกล้อง",
+  "camera_model": "รุ่นกล้อง",
   "cancel": "ยกเลิก",
-  "cancel_search": "",
-  "cannot_merge_people": "",
-  "cannot_update_the_description": "",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
-  "change_date": "",
+  "cancel_search": "ยกเลิกการค้นหา",
+  "cannot_merge_people": "ไม่สามารถรวมกลุ่มคนได้",
+  "cannot_update_the_description": "ไม่สามารถอัพเดทรายละเอียดได้",
+  "change_date": "เปลี่ยนวันที่",
   "change_expiration_time": "เปลี่ยนเวลาหมดอายุ",
-  "change_location": "",
-  "change_name": "",
-  "change_name_successfully": "",
+  "change_location": "เปลี่ยนตําแหน่ง",
+  "change_name": "เปลี่ยนชื่อ",
+  "change_name_successfully": "เปลี่ยนชื่อเรียบร้อยแล้ว",
   "change_password": "เปลี่ยนรหัสผ่าน",
   "change_your_password": "",
   "changed_visibility_successfully": "",
@@ -373,312 +378,307 @@
   "comment_options": "",
   "comments_are_disabled": "",
   "confirm": "ยืนยัน",
-  "confirm_admin_password": "",
+  "confirm_admin_password": "ยืนยันรหัสผ่านผู้ดูแลระบบ",
   "confirm_password": "ยืนยันรหัสผ่าน",
   "contain": "มี",
   "context": "บริบท",
   "continue": "ต่อไป",
-  "copied_image_to_clipboard": "",
-  "copy_error": "",
-  "copy_file_path": "คัดลอกพาธไฟล์",
-  "copy_image": "",
-  "copy_link": "",
-  "copy_link_to_clipboard": "",
-  "copy_password": "",
-  "copy_to_clipboard": "",
+  "copied_image_to_clipboard": "คัดลอกภาพไปยังคลิปบอร์ดแล้ว",
+  "copy_error": "คัดลอกข้อผิดพลาด",
+  "copy_file_path": "คัดลอกพาธของไฟล์",
+  "copy_image": "คัดลอกภาพ",
+  "copy_link": "คัดลอกลิงก์",
+  "copy_link_to_clipboard": "คัดลอกลิงก์ไปยังคลิปบอร์ด",
+  "copy_password": "คัดลอกรหัสผ่าน",
+  "copy_to_clipboard": "คัดลอกไปยังคลิปบอร์ด",
   "country": "ประเทศ",
   "cover": "ปก",
   "covers": "ปก",
   "create": "สร้าง",
   "create_album": "สร้างอัลบั้ม",
-  "create_library": "",
+  "create_library": "สร้างคลังภาพ",
   "create_link": "สร้างลิงก์",
   "create_link_to_share": "สร้างลิงก์เพื่อแชร์",
-  "create_new_person": "",
-  "create_new_user": "",
-  "create_user": "",
-  "created": "สร้าง",
-  "current_device": "",
-  "custom_locale": "",
-  "custom_locale_description": "",
+  "create_new_person": "สร้างคนใหม่",
+  "create_new_user": "สร้างผู้ใช้งานใหม่",
+  "create_user": "สร้างผู้ใช้",
+  "created": "สร้างแล้ว",
+  "current_device": "อุปกรณ์ปัจจุบัน",
+  "custom_locale": "ปรับภาษาท้องถิ่นเอง",
+  "custom_locale_description": "ใช้รูปแบบวันที่และตัวเลขจากภาษาและขอบเขต",
   "dark": "มืด",
-  "date_after": "",
+  "date_after": "วันที่หลังจาก",
   "date_and_time": "วันและเวลา",
-  "date_before": "",
+  "date_before": "วันที่ก่อน",
   "date_range": "ช่วงวันที่",
   "day": "วัน",
-  "default_locale": "",
-  "default_locale_description": "",
+  "default_locale": "ภาษาท้องถิ่นปกติ",
+  "default_locale_description": "ใช้รูปแบบวันที่และตัวเลขจากเบราว์เซอร์ของคุณ",
   "delete": "ลบออก",
   "delete_album": "ลบอัลบั้ม",
-  "delete_key": "",
-  "delete_library": "",
-  "delete_link": "",
+  "delete_key": "ลบกุญแจ",
+  "delete_library": "ลบคลังภาพ",
+  "delete_link": "ลบลิงก์",
   "delete_shared_link": "ลบลิงก์ที่แชร์",
-  "delete_user": "",
-  "deleted_shared_link": "",
+  "delete_user": "ลบผู้ใช้",
+  "deleted_shared_link": "ลบลิงก์ที่แชร์แล้ว",
   "description": "รายละเอียด",
   "details": "รายละเอียด",
-  "direction": "ทิศทาง",
-  "disallow_edits": "",
+  "direction": "เส้นทาง",
+  "disallow_edits": "ไม่อนุญาตให้แก้ไข",
   "discover": "ค้นพบ",
-  "dismiss_all_errors": "",
-  "dismiss_error": "",
+  "dismiss_all_errors": "ปฏิเสธข้อผิดพลาดทั้งหมด",
+  "dismiss_error": "ปฏิเสธข้อผิดพลาด",
   "display_options": "",
   "display_order": "",
   "display_original_photos": "",
-  "display_original_photos_setting_description": "",
+  "display_original_photos_setting_description": "เมื่อดูสื่อให้แสดงภาพต้นฉบับแทนภาพตัวอย่างเมื่อไฟล์สื่อเปิดได้บนเว็บ อาจทําให้แสดง ภาพได้ช้าลง",
   "done": "เสร็จ",
   "download": "ดาวน์โหลด",
   "downloading": "กำลังดาวน์โหลด",
   "duration": "ระยะเวลา",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
-  "edit_album": "",
-  "edit_avatar": "",
-  "edit_date": "",
-  "edit_date_and_time": "",
-  "edit_exclusion_pattern": "",
-  "edit_faces": "",
-  "edit_import_path": "แก้ไขพาธนำเข้า",
-  "edit_import_paths": "แก้ไขพาธนำเข้า",
-  "edit_key": "",
+  "edit_album": "แก้ไขอัลบั้ม",
+  "edit_avatar": "แก้ไขตัวละคร",
+  "edit_date": "แก้ไขวันที่",
+  "edit_date_and_time": "แก้ไขวันที่และเวลา",
+  "edit_exclusion_pattern": "แก้ไขข้อยกเว้น",
+  "edit_faces": "แก้ไขหน้า",
+  "edit_import_path": "แก้ไขพาธนําเข้า",
+  "edit_import_paths": "แก้ไขพาธนําเข้า",
+  "edit_key": "แก้ไขกุญแจ",
   "edit_link": "แก้ไขลิงก์",
   "edit_location": "แก้ไขตำแหน่ง",
   "edit_name": "แก้ไขชื่อ",
-  "edit_people": "",
-  "edit_title": "",
-  "edit_user": "",
+  "edit_people": "แก้ไขผู้คน",
+  "edit_title": "แก้ไขชื่อ",
+  "edit_user": "แก้ไขผู้ใช้",
   "edited": "แก้ไขแล้ว",
   "editor": "ผู้แก้ไข",
   "email": "อีเมล",
-  "empty": "",
-  "empty_album": "",
   "empty_trash": "ทิ้งจากถังขยะ",
   "enable": "เปิดใช้งาน",
   "enabled": "เปิดใช้งาน",
-  "end_date": "",
+  "end_date": "วันสิ้นสุด",
   "error": "เกิดข้อผิดพลาด",
-  "error_loading_image": "",
+  "error_loading_image": "เกิดข้อผิดพลาดระหว่างโหลดภาพ",
   "errors": {
     "import_path_already_exists": "พาธนำเข้านี้มีอยู่แล้ว",
-    "unable_to_add_album_users": "",
-    "unable_to_add_comment": "",
-    "unable_to_add_partners": "",
-    "unable_to_change_album_user_role": "",
-    "unable_to_change_date": "",
-    "unable_to_change_location": "",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
+    "unable_to_add_album_users": "ไม่สามารถเพิ่มผู้ใช้ไปยังอัลบั้มได้",
+    "unable_to_add_comment": "ไม่สามารถเพิ่มความเห็นได้",
+    "unable_to_add_partners": "ไม่สามารถเพิ่มคู่หูได้",
+    "unable_to_change_album_user_role": "ไม่สามารถเปลี่ยนบทบาทผู้ใช้ในอัลบั้มได้",
+    "unable_to_change_date": "ไม่สามารถเปลี่ยนวันที่ได้",
+    "unable_to_change_location": "ไม่สามารถเปลี่ยนตําแหน่งได้",
     "unable_to_create_admin_account": "",
-    "unable_to_create_library": "",
-    "unable_to_create_user": "",
-    "unable_to_delete_album": "ไม่สามารถลบอัลบั้ม",
-    "unable_to_delete_asset": "",
-    "unable_to_delete_user": "",
-    "unable_to_empty_trash": "",
-    "unable_to_enter_fullscreen": "",
-    "unable_to_exit_fullscreen": "",
-    "unable_to_hide_person": "",
-    "unable_to_load_album": "",
-    "unable_to_load_asset_activity": "",
-    "unable_to_load_items": "",
-    "unable_to_load_liked_status": "",
-    "unable_to_play_video": "",
-    "unable_to_refresh_user": "",
-    "unable_to_remove_album_users": "",
-    "unable_to_remove_comment": "",
-    "unable_to_remove_library": "",
-    "unable_to_remove_partner": "",
-    "unable_to_remove_reaction": "",
-    "unable_to_remove_user": "",
-    "unable_to_repair_items": "",
-    "unable_to_reset_password": "",
-    "unable_to_resolve_duplicate": "",
-    "unable_to_restore_assets": "",
-    "unable_to_restore_trash": "",
-    "unable_to_restore_user": "",
-    "unable_to_save_album": "",
-    "unable_to_save_name": "",
-    "unable_to_save_profile": "",
-    "unable_to_save_settings": "",
-    "unable_to_scan_libraries": "",
-    "unable_to_scan_library": "",
-    "unable_to_set_profile_picture": "",
-    "unable_to_submit_job": "",
-    "unable_to_trash_asset": "",
-    "unable_to_unlink_account": "",
-    "unable_to_update_library": "",
-    "unable_to_update_location": "",
-    "unable_to_update_settings": "",
-    "unable_to_update_user": ""
+    "unable_to_create_library": "ไม่สามารถสร้างคลังภาพได้",
+    "unable_to_create_user": "ไม่สามารถสร้างผู้ใช้ได้",
+    "unable_to_delete_album": "ไม่สามารถลบอัลบั้มได้",
+    "unable_to_delete_asset": "ไม่สามารถลบสื่อได้",
+    "unable_to_delete_user": "ไม่สามารถลบผู้ใช้ได้",
+    "unable_to_empty_trash": "ไม่สามารถลบถังขยะได้",
+    "unable_to_enter_fullscreen": "ไม่สามารถเปิดเต็มจอได้",
+    "unable_to_exit_fullscreen": "ไม่สามารถออกโหมดเต็มจอได้",
+    "unable_to_hide_person": "ไม่สามารถซ่อนบุคคลได้",
+    "unable_to_load_album": "ไม่สามารถโหลดอัลบั้มได้",
+    "unable_to_load_asset_activity": "ไม่สามารถโหลดข้อมูลของสื่อได้",
+    "unable_to_load_items": "ไม่สามารถโหลดรายการได้",
+    "unable_to_load_liked_status": "ไม่สามารถโหลดสถานะ like ได้",
+    "unable_to_play_video": "ไม่สามารถเล่นวิดีโอได้",
+    "unable_to_refresh_user": "ไม่สามารถรีเฟรชผู้ใช้ได้",
+    "unable_to_remove_album_users": "ไม่สามารถลบผู้ใช้ออกจากอัลบั้มได้",
+    "unable_to_remove_library": "ไม่สามารถลบคลังภาพได้",
+    "unable_to_remove_partner": "ไม่สามารถลบคู่หูได้",
+    "unable_to_remove_reaction": "ไม่สามารถลบ reaction ได้",
+    "unable_to_repair_items": "ไม่สามารถซ่อมแซมรายการได้",
+    "unable_to_reset_password": "ไม่สามารถตั้งรหัสผ่านใหม่ได้",
+    "unable_to_resolve_duplicate": "ไม่สามารถแก้ไขของซ้ำได้",
+    "unable_to_restore_assets": "ไม่สามารถเรียกคืนสื่อได้",
+    "unable_to_restore_trash": "ไม่สามารถเรียกคืนถังขยะได้",
+    "unable_to_restore_user": "ไม่สามารถเรียกคืนผู้ใช้ได้",
+    "unable_to_save_album": "ไม่สามารถบันทึกอัลบั้มได้",
+    "unable_to_save_name": "ไม่สามารถบันทึกชื่อได้",
+    "unable_to_save_profile": "ไม่สามารถบันทึกโปรไฟล์ได้",
+    "unable_to_save_settings": "ไม่สามารถบันทึกการตั้งค่าได้",
+    "unable_to_scan_libraries": "ไม่สามารถสแกนคลังภาพได้",
+    "unable_to_scan_library": "ไม่สามารถสแกนคลังภาพได้",
+    "unable_to_set_profile_picture": "ไม่สามารถตั้งภาพโปรไฟล์ได้",
+    "unable_to_submit_job": "ไม่สามารถส่งงานได้",
+    "unable_to_trash_asset": "ไม่สามารถทิ้งสื่อได้",
+    "unable_to_unlink_account": "ไม่สามารถยกเลิกการเชื่อมโยงบัญชีผู้ใช้ได้",
+    "unable_to_update_library": "ไม่สามารถอัพเดทคลังภาพได้",
+    "unable_to_update_location": "ไม่สามารถอัพเดทตําแหน่งได้",
+    "unable_to_update_settings": "ไม่สามารถอัพเดทการตั้งค่าได้",
+    "unable_to_update_user": "ไม่สามารถอัพเดทผู้ใช้ได้"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exit_slideshow": "",
   "expand_all": "",
   "expire_after": "หมดอายุหลังจาก",
   "expired": "หมดอายุแล้ว",
   "explore": "สํารวจ",
   "extension": "ส่วนต่อขยาย",
-  "external_libraries": "",
-  "failed_to_get_people": "",
+  "external_libraries": "ภายนอกคลังภาพ",
   "favorite": "รายการโปรด",
-  "favorite_or_unfavorite_photo": "",
+  "favorite_or_unfavorite_photo": "โปรดหรือไม่โปรดภาพ",
   "favorites": "รายการโปรด",
-  "feature": "",
-  "feature_photo_updated": "",
-  "featurecollection": "",
+  "feature_photo_updated": "อัพเดทภาพเด่นแล้ว",
   "file_name": "",
   "file_name_or_extension": "",
   "filename": "ชื่อไฟล์",
-  "files": "",
   "filetype": "ชนิดไฟล์",
-  "filter_people": "",
+  "filter_people": "กรองผู้คน",
   "fix_incorrect_match": "",
-  "force_re-scan_library_files": "",
   "forward": "ไปข้างหน้า",
   "general": "ทั่วไป",
   "get_help": "",
   "getting_started": "",
   "go_back": "",
   "go_to_search": "",
-  "go_to_share_page": "",
   "group_albums_by": "",
   "has_quota": "",
-  "hide_gallery": "",
+  "hide_gallery": "ซ่อนคลังภาพ",
   "hide_password": "",
-  "hide_person": "",
+  "hide_person": "ซ่อนบุคคล",
   "host": "โฮสต์",
   "hour": "ชั่วโมง",
   "image": "รูปภาพ",
-  "img": "",
   "immich_logo": "",
-  "import_path": "",
-  "in_archive": "",
-  "include_archived": "รวมเก็บถาวร",
-  "include_shared_albums": "",
-  "include_shared_partner_assets": "",
-  "individual_share": "",
+  "import_path": "นำเข้าพาธ",
+  "in_archive": "ในที่เก็บถาวร",
+  "include_archived": "รวมไฟล์เก็บถาวร",
+  "include_shared_albums": "รวมอัลบั้มที่แชร์กัน",
+  "include_shared_partner_assets": "รวมสื่อที่แชร์กับคู่หู",
+  "individual_share": "แชร์ส่วนตัว",
   "info": "ข้อมูล",
   "interval": {
-    "day_at_onepm": "",
+    "day_at_onepm": "ทุกวันเวลาบ่ายโมง",
     "hours": "",
-    "night_at_midnight": "",
-    "night_at_twoam": ""
+    "night_at_midnight": "ทุกเที่ยงคืน",
+    "night_at_twoam": "ทุกวันเวลาตี 2"
   },
-  "invite_people": "",
+  "invite_people": "เชิญผู้คน",
   "invite_to_album": "เชิญเข้าอัลบั้ม",
-  "job_settings_description": "",
   "jobs": "งาน",
   "keep": "เก็บ",
-  "keyboard_shortcuts": "",
+  "keyboard_shortcuts": "ปุ่มพิมพ์ลัด",
   "language": "ภาษา",
-  "language_setting_description": "",
-  "last_seen": "",
+  "language_setting_description": "เลือกภาษาที่ต้องการ",
+  "last_seen": "เห็นล่าสุด",
+  "latest_version": "เวอร์ชันล่าสุด",
   "leave": "ทิ้ง",
   "let_others_respond": "ให้คนอื่นตอบ",
   "level": "ระดับ",
-  "library": "คลัง",
-  "library_options": "",
+  "library": "คลังภาพ",
+  "library_options": "ตัวเลือกคลังภาพ",
   "light": "สว่าง",
-  "link_options": "",
-  "link_to_oauth": "",
-  "linked_oauth_account": "",
+  "link_options": "ตัวเลือกลิงก์",
+  "link_to_oauth": "ลิงก์ไปยัง OAuth",
+  "linked_oauth_account": "ลิงก์บัญชีผู้ใช้ OAuth",
   "list": "รายการ",
   "loading": "กำลังโหลด",
-  "loading_search_results_failed": "",
+  "loading_search_results_failed": "โหลดผลการค้นหาล้มเหลว",
   "log_out": "ออกจากระบบ",
-  "log_out_all_devices": "",
-  "login_has_been_disabled": "",
-  "look": "",
-  "loop_videos": "",
+  "log_out_all_devices": "ให้ทุกอุปกรณ์ออกจากระบบทั้งหมด",
+  "login_has_been_disabled": "ปิดการใช้งานการเข้าสู่ระบบแล้ว",
+  "look": "ดู",
+  "loop_videos": "วนวิดีโอ",
   "loop_videos_description": "เปิดเพื่อให้วิดีโอวนลูปในที่ดูรายละเอียด",
-  "make": "",
-  "manage_shared_links": "บริหารลิงก์",
-  "manage_sharing_with_partners": "",
-  "manage_the_app_settings": "",
-  "manage_your_account": "",
-  "manage_your_api_keys": "",
-  "manage_your_devices": "",
-  "manage_your_oauth_connection": "",
+  "make": "สร้าง",
+  "manage_shared_links": "จัดการลิงก์ที่แชร์",
+  "manage_sharing_with_partners": "จัดการการแชร์กับคู่หู",
+  "manage_the_app_settings": "จัดการการตั้งค่าแอป",
+  "manage_your_account": "จัดการบัญชีของคุณ",
+  "manage_your_api_keys": "จัดการกุญแจ API ของคุณ",
+  "manage_your_devices": "จัดการอุปกรณ์ของคุณ",
+  "manage_your_oauth_connection": "จัดการการเชื่อมต่อ OAuth ของคุณ",
   "map": "แผนที่",
-  "map_marker_with_image": "",
-  "map_settings": "ตั้งค่าแผนที่",
-  "media_type": "",
+  "map_marker_for_images": "หมุดแผนที่สำหรับรูปถ่ายที่ {city}, {country}",
+  "map_marker_with_image": "หมุดแผนที่กับรูปถ่าย",
+  "map_settings": "การตั้งค่าแผนที่",
+  "matches": "ตรงกัน",
+  "media_type": "ชนิดสื่อ",
   "memories": "ความทรงจำ",
-  "memories_setting_description": "",
+  "memories_setting_description": "จัดการสิ่งที่คุณเห็นในความทรงจําของคุณ",
+  "memory": "ความทรงจำ",
+  "memory_lane_title": "ความทรงจำ {title}",
   "menu": "เมนู",
-  "merge": "",
-  "merge_people": "",
-  "merge_people_successfully": "",
+  "merge": "รวม",
+  "merge_people": "รวมผู้คน",
+  "merge_people_limit": "คุณรวมใบหน้าได้มากถึง 5 รูปต่อครั้ง",
+  "merge_people_prompt": "คุณต้องการรวมคนพวกนี้หรือไม่ การกระทำนี้ไม่สามารถย้อนกลับได้",
+  "merge_people_successfully": "รวมผู้คนเรียบร้อยแล้ว",
   "minimize": "ย่อลง",
   "minute": "นาที",
   "missing": "ขาดหาย",
   "model": "โมเดล",
   "month": "เดือน",
   "more": "เพิ่มเติม",
-  "moved_to_trash": "",
-  "my_albums": "",
+  "moved_to_trash": "ทิ้งลงถังขยะแล้ว",
+  "my_albums": "อัลบั้มของฉัน",
   "name": "ชื่อ",
-  "name_or_nickname": "",
+  "name_or_nickname": "ชื่อหรือชื่อเล่น",
   "never": "ไม่เคย",
-  "new_api_key": "",
+  "new_album": "อัลบั้มใหม่",
+  "new_api_key": "กุญแจ API ใหม่",
   "new_password": "รหัสผ่านใหม่",
-  "new_person": "",
-  "new_user_created": "",
-  "newest_first": "",
+  "new_person": "คนใหม่",
+  "new_user_created": "สร้างผู้ใช้ใหม่แล้ว",
+  "new_version_available": "มีเวอร์ชันใหม่ให้ใช้งาน",
+  "newest_first": "ใหม่สุดก่อน",
   "next": "ต่อไป",
-  "next_memory": "",
+  "next_memory": "ความทรงจำต่อไป",
   "no": "ไม่",
-  "no_albums_message": "",
-  "no_archived_assets_message": "",
-  "no_assets_message": "",
-  "no_exif_info_available": "",
+  "no_albums_message": "สร้างอัลบั้มเพื่อจัดการรูปภาพและวิดีโอของคุณ",
+  "no_albums_with_name_yet": "ดูเหมือนว่าไม่มีอัลบั้มไหนที่ใช้ชื่อนี้",
+  "no_archived_assets_message": "จัดเก็บรูปภาพและวีดิโอถาวรเพื่อซ่อนจากมุมมองคุณ",
+  "no_assets_message": "กดเพื่อใส่ภาพคุณภาพแรก",
+  "no_duplicates_found": "ไม่พบรายการที่ซ้ำกัน",
+  "no_exif_info_available": "ไม่มีข้อมูล exif",
   "no_explore_results_message": "",
-  "no_favorites_message": "",
-  "no_libraries_message": "",
-  "no_name": "",
-  "no_places": "",
-  "no_results": "",
-  "no_shared_albums_message": "",
-  "not_in_any_album": "",
+  "no_favorites_message": "เพิ่มรายการโปรดเพื่อค้นหาภาพและวิดีโอที่ดีที่สุดของคุณอย่างรวดเร็ว",
+  "no_libraries_message": "สร้างคลังภาพภายนอกเพื่อดูภาพถ่ายและวิดีโอต่าง ๆ ของคุณ",
+  "no_name": "ไม่มีชื่อ",
+  "no_places": "ไม่มีสถานที่",
+  "no_results": "ไม่มีผลลัพธ์",
+  "no_shared_albums_message": "สร้างอัลบั้มเพื่อแชร์รูปภาพและวิดีโอกับคนในเครือข่ายของคุณ",
+  "not_in_any_album": "ไม่อยู่ในอัลบั้มใด ๆ",
+  "note_unlimited_quota": "หมายเหตุ: กรอก 0 สำหรับโควตาแบบไม่จำกัด",
   "notes": "หมายเหตุ",
-  "notification_toggle_setting_description": "",
+  "notification_toggle_setting_description": "เปิด/ปิด การแจ้งเตือนอีเมล",
   "notifications": "การแจ้งเตือน",
-  "notifications_setting_description": "",
+  "notifications_setting_description": "จัดการการแจ้งเตือน",
   "oauth": "OAuth",
+  "official_immich_resources": "แหล่งข้อมูล Immich อย่างเป็นทางการ",
   "offline": "ออฟไลน์",
   "ok": "โอเค",
-  "oldest_first": "",
+  "oldest_first": "เก่าสุดก่อน",
+  "onboarding_welcome_user": "ยินดีต้อนรับ {user}",
   "online": "ออนไลน์",
-  "only_favorites": "",
-  "only_refreshes_modified_files": "",
-  "open_the_search_filters": "",
+  "only_favorites": "รายการโปรดเท่านั้น",
+  "open_in_openstreetmap": "เปิดใน OpenStreetMap",
+  "open_the_search_filters": "เปิดตัวกรองการค้นหา",
   "options": "ตัวเลือก",
-  "organize_your_library": "",
+  "or": "หรือ",
+  "organize_your_library": "จัดเรียงคลังภาพของคุณ",
+  "original": "ต้นฉบับ",
   "other": "อื่น ๆ",
-  "other_devices": "",
-  "other_variables": "",
+  "other_devices": "เครื่องอื่น",
+  "other_variables": "ตัวแปรอื่น",
   "owned": "เป็นเจ้าของ",
   "owner": "เจ้าของ",
-  "partner_sharing": "",
-  "partners": "",
+  "partner": "คู่หู",
+  "partner_can_access": "{partner} สามารถเข้าถึง",
+  "partner_can_access_assets": "รูปภาพและวิดีโอทั้งหมดยกเว้นที่อยู่ในเก็บถาวรและถูกลบทิ้ง",
+  "partner_can_access_location": "ตำแหน่งที่รูปถูกถ่าย",
+  "partner_sharing": "การแชร์แบบคู่หู",
+  "partners": "คู่หู",
   "password": "รหัสผ่าน",
-  "password_does_not_match": "",
-  "password_required": "",
-  "password_reset_success": "",
+  "password_does_not_match": "รหัสผ่านไม่ตรงกัน",
+  "password_required": "จำเป็นต้องมีรหัสผ่าน",
+  "password_reset_success": "รีเซ็ตรหัสผ่านสำเร็จ",
   "past_durations": {
-    "days": "",
-    "hours": "",
-    "years": ""
+    "days": "{days, plural, one {วัน} other {# วัน}}ที่ผ่านมา",
+    "hours": "{hours, plural, one {ชั่วโมง} other {# ชั่วโมง}}ที่ผ่านมา",
+    "years": "{years, plural, one {ปี} other {# ปี}}ที่ผ่านมา"
   },
   "path": "",
   "pattern": "",
@@ -687,66 +687,59 @@
   "paused": "หยุด",
   "pending": "กำลังรอ",
   "people": "ผู้คน",
-  "people_sidebar_description": "",
-  "perform_library_tasks": "",
-  "permanent_deletion_warning": "",
-  "permanent_deletion_warning_setting_description": "",
-  "permanently_delete": "",
-  "permanently_deleted_asset": "",
+  "people_sidebar_description": "แสดงลิงก์ไปยังผู้คนในแถบด้านข้าง",
+  "permanent_deletion_warning": "แจ้งเตือนการลบถาวร",
+  "permanent_deletion_warning_setting_description": "เตือนเมื่อจะลบสื่อถาวร",
+  "permanently_delete": "ลบถาวร",
+  "permanently_deleted_asset": "ลบสื่อถาวรแล้ว",
   "photos": "รูปภาพ",
-  "photos_from_previous_years": "",
-  "pick_a_location": "",
+  "photos_from_previous_years": "ภาพถ่ายจากปีก่อน",
+  "pick_a_location": "เลือกตําแหน่ง",
   "place": "สถานที่",
   "places": "สถานที่",
   "play": "เล่น",
-  "play_memories": "",
-  "play_motion_photo": "",
-  "play_or_pause_video": "",
-  "point": "",
+  "play_memories": "เล่นความทรงจํา",
+  "play_motion_photo": "เล่นภาพวัตถุเคลื่อนไหว",
+  "play_or_pause_video": "เล่นหรือหยุดวิดีโอ",
   "port": "พอร์ต",
   "preset": "",
   "preview": "ตัวอย่าง",
   "previous": "ก่อนหน้า",
-  "previous_memory": "",
-  "previous_or_next_photo": "",
+  "previous_memory": "ความทรงจําก่อนหน้า",
+  "previous_or_next_photo": "ภาพก่อนหน้าหรือภาพถัดไป",
   "primary": "หลัก",
-  "profile_picture_set": "",
-  "public_share": "",
-  "range": "",
-  "raw": "",
-  "reaction_options": "",
-  "read_changelog": "",
+  "profile_picture_set": "ตั้งภาพโปรไฟล์แล้ว",
+  "public_share": "แชร์แบบสาธารณะ",
+  "reaction_options": "ตัวเลือก reaction",
+  "read_changelog": "อ่านบันทึกการเปลี่ยนแปลง",
   "recent": "ล่าสุด",
-  "recent_searches": "",
+  "recent_searches": "การค้นหาล่าสุด",
   "refresh": "รีเฟรช",
-  "refreshed": "ถูกรีเฟรช",
-  "refreshes_every_file": "",
-  "remove": "เอาออก",
+  "refreshed": "รีเฟรช",
+  "refreshes_every_file": "รีเฟรชทุกไฟล์",
+  "remove": "ลบ",
+  "remove_deleted_assets": "",
   "remove_from_album": "ลบออกจากอัลบั้ม",
-  "remove_from_favorites": "",
-  "remove_from_shared_link": "",
-  "remove_offline_files": "",
+  "remove_from_favorites": "เอาออกจากรายการโปรด",
+  "remove_from_shared_link": "ลบออกจากลิงก์ที่แชร์",
   "repair": "ซ่อม",
   "repair_no_results_message": "",
   "replace_with_upload": "",
-  "require_password": "",
+  "require_password": "ต้องการรหัสผ่าน",
   "reset": "รีเซ็ต",
-  "reset_password": "",
-  "reset_people_visibility": "",
-  "reset_settings_to_default": "",
-  "restore": "กู้คืน",
-  "restore_user": "",
-  "retry_upload": "",
+  "reset_password": "ตั้งค่ารหัสผ่านใหม่",
+  "reset_people_visibility": "ปรับการมองเห็นใหม่",
+  "restore": "เรียกคืน",
+  "restore_user": "เรียกคืนผู้ใช้",
+  "retry_upload": "ลองอัพโหลดใหม่",
   "review_duplicates": "",
   "role": "บทบาท",
   "save": "บันทึก",
-  "saved_profile": "",
-  "saved_settings": "",
+  "saved_profile": "โพรไฟล์ที่บันทึกไว้",
+  "saved_settings": "การตั้งค่าที่บันทึกไว้",
   "say_something": "พูดอะไรสักอย่าง",
-  "scan_all_libraries": "",
-  "scan_all_library_files": "",
-  "scan_new_library_files": "",
-  "scan_settings": "",
+  "scan_all_libraries": "สแกนคลังภาพทั้งหมด",
+  "scan_settings": "ตั้งค่าการสแกน",
   "search": "ค้นหา",
   "search_albums": "",
   "search_by_context": "",
@@ -755,7 +748,7 @@
   "search_city": "",
   "search_country": "",
   "search_for_existing_person": "",
-  "search_people": "",
+  "search_people": "ค้นหาผู้คน",
   "search_places": "",
   "search_state": "",
   "search_timezone": "",
@@ -767,13 +760,12 @@
   "select_all": "",
   "select_avatar_color": "",
   "select_face": "",
-  "select_featured_photo": "",
-  "select_library_owner": "",
+  "select_featured_photo": "เลือกภาพเด่น",
+  "select_library_owner": "เลือกเจ้าของคลังภาพ",
   "select_new_face": "",
   "select_photos": "เลือกรูปภาพ",
-  "selected": "ถูกเลือก",
+  "selected": "เลือก",
   "send_message": "",
-  "server": "เซิร์ฟเวอร์",
   "server_stats": "",
   "set": "",
   "set_as_album_cover": "",
@@ -782,76 +774,73 @@
   "set_profile_picture": "",
   "set_slideshow_to_fullscreen": "",
   "settings": "ตั้งค่า",
-  "settings_saved": "",
+  "settings_saved": "บันทึกการตั้งค่าแล้ว",
   "share": "แชร์",
   "shared": "แชร์",
-  "shared_by": "",
-  "shared_by_you": "",
+  "shared_by": "แชร์โดย",
+  "shared_by_you": "แชร์โดยคุณ",
   "shared_links": "ลิงก์ที่แชร์",
   "sharing": "การแชร์",
   "sharing_sidebar_description": "",
-  "show_album_options": "",
-  "show_file_location": "",
-  "show_gallery": "",
-  "show_hidden_people": "",
-  "show_in_timeline": "",
-  "show_in_timeline_setting_description": "",
-  "show_keyboard_shortcuts": "",
+  "show_album_options": "แสดงตัวเลือกอัลบั้ม",
+  "show_file_location": "แสดงตําแหน่งของไฟล์",
+  "show_gallery": "แสดงคลังภาพ",
+  "show_hidden_people": "แสดงคนที่ซ่อนไว้",
+  "show_in_timeline": "แสดงในไทม์ไลน์",
+  "show_in_timeline_setting_description": "แสดงรูปภาพและวิดีโอของผู้ใช้นี้ในไทม์ไลน์ของคุณ",
+  "show_keyboard_shortcuts": "แสดงปุ่มลัดแป้นพิมพ์",
   "show_metadata": "แสดง metadata",
-  "show_or_hide_info": "",
-  "show_password": "",
-  "show_person_options": "",
-  "show_progress_bar": "",
-  "show_search_options": "",
+  "show_or_hide_info": "แสดงหรือซ่อนข้อมูล",
+  "show_password": "แสดงรหัสผ่าน",
+  "show_person_options": "แสดงตัวเลือกของตัวบุคคล",
+  "show_progress_bar": "แสดงความคืบหน้า แถบ",
+  "show_search_options": "แสดงตัวเลือกการค้นหา",
   "shuffle": "สับเปลี่ยน",
-  "sign_up": "",
+  "sign_up": "ลงทะเบียน",
   "size": "ขนาด",
-  "skip_to_content": "",
+  "skip_to_content": "ข้ามไปยังเนื้อหา",
   "slideshow": "สไลด์",
-  "slideshow_settings": "",
-  "sort_albums_by": "",
+  "slideshow_settings": "ตั้งค่าสไลด์",
+  "sort_albums_by": "เรียงอัลบั้มโดย...",
   "stack": "ซ้อน",
   "stack_selected_photos": "",
   "stacktrace": "",
-  "start_date": "",
+  "start_date": "วันที่เริ่ม",
   "state": "รัฐ",
   "status": "สถานะ",
-  "stop_motion_photo": "",
+  "stop_motion_photo": "ภาพวัตถุเคลื่อนไหว",
   "stop_photo_sharing": "หยุดแชร์รูปภาพ?",
   "storage": "ที่จัดเก็บ",
-  "storage_label": "",
+  "storage_label": "ฉลากจัดเก็บ",
   "submit": "ส่ง",
   "suggestions": "ข้อเสนอแนะ",
-  "sunrise_on_the_beach": "",
-  "swap_merge_direction": "",
+  "sunrise_on_the_beach": "พระอาทิตย์ขึ้นบนชายหาด",
+  "swap_merge_direction": "สลับด้านรวม",
   "sync": "ซิงค์",
   "template": "แม่แบบ",
   "theme": "ธีม",
-  "theme_selection": "",
-  "theme_selection_description": "",
-  "time_based_memories": "",
+  "theme_selection": "การเลือกธีม",
+  "theme_selection_description": "ตั้งค่าธีมให้สว่างหรือมืดโดยอัตโนมัติ อิงจากค่าของเบราว์เซอร์ของคุณ",
+  "time_based_memories": "ความทรงจําตามเวลา",
   "timezone": "เขตเวลา",
-  "toggle_settings": "",
-  "toggle_theme": "",
-  "toggle_visibility": "",
-  "total_usage": "",
+  "toggle_settings": "สลับการตั้งค่า",
+  "toggle_theme": "สลับธีม",
+  "total_usage": "การใช้งานรวม",
   "trash": "ขยะ",
-  "trash_all": "",
-  "trash_no_results_message": "",
+  "trash_all": "ทิ้งทั้งหมด",
+  "trash_no_results_message": "รูปและวีดีโอที่ถูกทิ้งจะมาโผล่ที่นี่",
   "type": "ประเภท",
   "unarchive": "นำออกจากที่เก็บถาวร",
-  "unarchived": "",
   "unfavorite": "นำออกจากรายการโปรด",
-  "unhide_person": "",
+  "unhide_person": "ยกเลิกซ่อนบุคคล",
   "unknown": "ไม่ทราบ",
-  "unknown_album": "",
-  "unknown_year": "",
+  "unknown_year": "ไม่ทราบปี",
   "unlink_oauth": "",
   "unlinked_oauth_account": "",
-  "unselect_all": "",
+  "unselect_all": "ยกเลิกการเลือกทั้งหมด",
   "unstack": "หยุดซ้อน",
-  "up_next": "",
-  "updated_password": "",
+  "up_next": "ต่อไป",
+  "updated_password": "รหัสผ่านเปลี่ยนแล้ว",
   "upload": "อัพโหลด",
   "upload_concurrency": "อัพโหลดพร้อมกัน",
   "url": "URL",
@@ -859,12 +848,14 @@
   "user": "ผู้ใช้",
   "user_id": "ไอดีผู้ใช้",
   "user_usage_detail": "รายละเอียดการใช้งานของผู้ใช้",
+  "user_usage_stats": "สถิติการใช้งานบัญชี",
+  "user_usage_stats_description": "ดูสถิติการใช้งานบัญชี",
   "username": "ชื่อผู้ใช้",
   "users": "ผู้ใช้",
   "utilities": "",
   "validate": "ตรวจสอบ",
   "variables": "ตัวแปร",
-  "version": "เวอร์ชัน",
+  "version": "รุ่น",
   "video": "วิดีโอ",
   "video_hover_setting": "เล่นวิดีโอตัวอย่างเมื่อจ่อ",
   "video_hover_setting_description": "เล่นวิดีโอตัวอย่างเมื่อเมาส์จ่อข้างบน เมื่อปิดใช้งาน วิดีโอตัวอย่างยังสามารถเล่นได้โดยกดปุ่มเล่น",
@@ -874,13 +865,12 @@
   "view_links": "ดูลิงก์",
   "view_next_asset": "ดูสื่อถัดไป",
   "view_previous_asset": "ดูสื่อก่อนหน้า",
-  "viewer": "",
   "waiting": "กำลังรอ",
   "week": "สัปดาห์",
   "welcome": "ยินดีต้อนรับ",
   "welcome_to_immich": "ยินดีต้อนรับสู่ immich",
   "year": "ปี",
   "yes": "ใช่",
-  "you_dont_have_any_shared_links": "คุณไม่มีลิงก์ที่ใช้ร่วมกัน",
+  "you_dont_have_any_shared_links": "คุณไม่ได้มีลิงก์ที่แชร์",
   "zoom_image": "ซูมรูปภาพ"
 }
diff --git a/web/src/lib/i18n/tr.json b/i18n/tr.json
similarity index 51%
rename from web/src/lib/i18n/tr.json
rename to i18n/tr.json
index 4fefbf2f21..b1e918afef 100644
--- a/web/src/lib/i18n/tr.json
+++ b/i18n/tr.json
@@ -1,5 +1,5 @@
 {
-  "about": "Hakkında",
+  "about": "Yenile",
   "account": "Hesap",
   "account_settings": "Hesap Ayarları",
   "acknowledge": "Onayla",
@@ -10,11 +10,11 @@
   "activity_changed": "Etkinlik {enabled, select, true {etkin} other {devre dışı}}",
   "add": "Ekle",
   "add_a_description": "Açıklama ekle",
-  "add_a_location": "Konum ekle",
+  "add_a_location": "Lokasyon ekle",
   "add_a_name": "İsim ekle",
   "add_a_title": "Başlık ekle",
-  "add_exclusion_pattern": "Dışlama deseni ekle",
-  "add_import_path": "İçeri aktarma yolu ekle",
+  "add_exclusion_pattern": "Hariç tutma deseni ekle",
+  "add_import_path": "İçe aktarma yolu ekle",
   "add_location": "Lokasyon ekle",
   "add_more_users": "Daha fazla kullanıcı ekle",
   "add_partner": "Partner ekle",
@@ -23,16 +23,23 @@
   "add_to": "Şuraya ekle...",
   "add_to_album": "Albüme ekle",
   "add_to_shared_album": "Paylaşılan albüme ekle",
+  "add_url": "URL ekle",
   "added_to_archive": "Arşive eklendi",
   "added_to_favorites": "Favorilere eklendi",
   "added_to_favorites_count": "{count, number} fotoğraf favorilere eklendi",
   "admin": {
     "add_exclusion_pattern_description": "Dışlama desenleri ekleyin. *, ** ve ? kullanılarak Globbing (temsili yer doldurucu karakter) desteklenir. Farzedelim \"Raw\" adlı bir dizininiz var, içinde ki tüm dosyaları yoksaymak için \"**/Raw/**\" şeklinde yazabilirsiniz. \".tif\" ile biten tüm dosyaları yoksaymak için \"**/*.tif\" yazabilirsiniz. Mutlak yolu yoksaymak için \"/yoksayılacak/olan/yol/**\" şeklinde yazabilirsiniz.",
+    "asset_offline_description": "Bu harici kütüphane varlığı artık diskte bulunmuyor ve çöp kutusuna taşındı. Dosya kütüphane içinde taşındıysa, yeni karşılık gelen varlık için zaman çizelgenizi kontrol edin. Bu varlığı geri yüklemek için lütfen aşağıdaki dosya yolunun Immich tarafından erişilebilir olduğundan emin olun ve kütüphaneyi tarayın.",
     "authentication_settings": "Yetkilendirme Ayarları",
     "authentication_settings_description": "Şifre, OAuth, ve diğer yetkilendirme ayarlarını yönet",
     "authentication_settings_disable_all": "Tüm giriş yöntemlerini devre dışı bırakmak istediğinize emin misiniz? Giriş yapma fonksiyonu tamamen devre dışı bırakılacak.",
     "authentication_settings_reenable": "Yeniden aktif etmek için <link>Sunucu Komutu</link>'nu kullanın.",
     "background_task_job": "Arka Plan Görevleri",
+    "backup_database": "Yedek Veritabanı",
+    "backup_database_enable_description": "Veritabanı Yedeklerinş Etkinleştir",
+    "backup_keep_last_amount": "Önceki yedeklemeden saklanacak miktar",
+    "backup_settings": "Yedekleme Ayarları",
+    "backup_settings_description": "Veritabanı Yedekleme Ayarlarını Yönet",
     "check_all": "Hepsini Kontrol Et",
     "cleared_jobs": "{job} için işler temizlendi",
     "config_set_by_file": "Ayarlar şuanda config dosyası tarafından ayarlanmıştır",
@@ -41,53 +48,56 @@
     "confirm_email_below": "Onaylamak için aşağıya {email} yazın",
     "confirm_reprocess_all_faces": "Tüm yüzleri tekrardan işlemek istediğinize emin misiniz? Bu işlem isimlendirilmiş insanları da silecek.",
     "confirm_user_password_reset": "{user} adlı kullanıcının şifresini sıfırlamak istediğinize emin misiniz?",
-    "crontab_guru": "",
+    "create_job": "Görev oluştur",
+    "cron_expression": "Cron İfadesi",
+    "cron_expression_description": "Cron formatını kullanarak tarama aralığını belirle. Daha fazla bilgi için örneğin <link>Crontab Guru</link>’ya bakın",
+    "cron_expression_presets": "Cron İfadesi Önayarları",
     "disable_login": "Girişi devre dışı bırak",
     "duplicate_detection_job_description": "Benzer fotoğrafları bulmak için makine öğrenmesini çalıştır. Bu işlem Akıllı Arama'ya bağlıdır",
     "exclusion_pattern_description": "Kütüphaneyi tararken dosya ve klasörleri görmezden gelmek için dışlama desenlerini kullanabilirsiniz. RAW dosyaları gibi bazı dosya ve klasörleri içe aktarmak istemediğinizde bu seçeneği kullanabilirsiniz.",
     "external_library_created_at": "Dış kütüphane ({date} tarihinde oluşturuldu.)",
     "external_library_management": "Dış Kütüphane Yönetimi",
     "face_detection": "Yüz tarama",
-    "face_detection_description": "Makine öğrenmesini kullanarak içeriklerinizde ki yüzleri bulun. Videolar için sadece önizleme görüntüleri kullanılacak. \"Hepsi\" seçeneği tüm medyaları tekrardan işler. \"İşlenmemiş\" daha önceden işlenmemiş içerikleri işlenmeleri için sıraya koyar. Tespit edilen yüzler yüz tarama işlemi tamamlandıktan sonra Yüz Tanıma için sıraya koyulacak ve kişiler olarak gruplandırılacak.",
-    "facial_recognition_job_description": "Tespit edilen yüzleri gruplandır. Bu işlem, yüz tanıma işlemi tamamlandıktan sonra çalışır. \"Hepsi\" tüm yüzleri gruplandırır. \"İşlenmemiş\" ise tespit edilen fakat kişi atanmamış olan yüzleri sıraya koyar.",
-    "failed_job_command": "{job} işi için {command} komutu başarısız",
-    "force_delete_user_warning": "UYARI: Bu işlem kullanıcıyı ve bütün verilerini silecek. Bu işlem geri alınamaz ve silinen veriler geri kurtarılamaz.",
-    "forcing_refresh_library_files": "Tüm kütüphane dosyaları yenileniyor",
+    "face_detection_description": "Makine öğrenimi kullanarak varlıklardaki yüzleri tespit et. Videolar için sadece küçük resim (thumbnail) dikkate alınır. 'Yenile' tüm varlıkları yeniden işler. 'Sıfırla', mevcut tüm yüz verilerini temizleyerek işlemi yeniden başlatır. 'Eksik' henüz işlenmemiş varlıkları sıraya alır. Tespit edilen yüzler, Yüz Tanıma işlemi tamamlandıktan sonra mevcut ya da yeni kişilere gruplanmak üzere Yüz Tanıma için sıraya alınacaktır.",
+    "facial_recognition_job_description": "Algılanan yüzleri kişilere grupla. Bu adım, Yüz Tespit işlemi tamamlandıktan sonra çalışır. \"Sıfırla\", tüm yüzleri yeniden gruplandırır. \"Eksik\" ise henüz bir kişiye atanmamış yüzleri sıraya alır.",
+    "failed_job_command": "{job} görevi için {command} komutu başarısız",
+    "force_delete_user_warning": "UYARI: Bu işlem kullanıcıyı ve tüm varlıkları anında kaldıracaktır. Bu geri alınamaz ve dosyalar geri getirilemez.",
+    "forcing_refresh_library_files": "Tüm kütüphane dosyalarının zorunlu olarak yenilenmesi sağlanıyor",
+    "image_format": "Biçim",
     "image_format_description": "WebP, JPEG'e göre daha küçük dosya boyutu sunar fakat işlemesi daha uzun sürer.",
     "image_prefer_embedded_preview": "Gömülü önizlemeyi tercih et",
     "image_prefer_embedded_preview_setting_description": "RAW fotoğrafları için mümkün olduğunda gömülü önizlemeyi kullan. Bu, bazı fotoğraflarda daha gerçekçi renkler üretebilir, fakat önizlemenin kalitesi kullanılan kameraya bağlıdır ve fotoğrafta normalden daha fazla görüntü bozukluklarına sebep olabilir.",
     "image_prefer_wide_gamut": "Geniş renk aralığını tercih et",
     "image_prefer_wide_gamut_setting_description": "Önizleme görseli için P3 renk paletini tercih et. Bu, geniş renk paletli fotoğraflarda renk canlılığını daha iyi korur, fakat fotoğraflar eski tarayıcılarda ve eski cihazlarda daha farklı görünebilir. sRGB fotoğraflar renk paletini korumak için sRGB olarak tutulur.",
-    "image_preview_format": "Biçimi önizle",
-    "image_preview_resolution": "Çözünürlüğü önizle",
-    "image_preview_resolution_description": "Makine öğrenmesi ve tekil fotoğrafları görüntülerken kullanılır. Yüksek çözünürlük daha fazla detayı korur fakat işlemesi daha uzun sürer, daha fazla boyuta sahip olur ve uygulamanın performansını düşürebilir.",
+    "image_preview_description": "Orta boyutlu görüntü, meta verisi çıkarılmış, tekil bir varlık görüntülenirken ve makine öğrenimi için kullanılır",
+    "image_preview_quality_description": "Ön izleme kalitesi 1-100 arasıdır. Yüksek değerler daha iyi kalite sağlar, ancak daha büyük dosyalar üretir ve uygulama yanıt verme hızını düşürebilir. Düşük bir değer belirlemek, makine öğrenimi kalitesini etkileyebilir.",
+    "image_preview_title": "Ön izleme Ayarları",
     "image_quality": "Kalite",
-    "image_quality_description": "Fotoğraf kalitesi 1-100. Yüksek değer, daha yüksek kalite demektir fakat boyutu arttırır. Bu özellik Önizlemeyi etkiler.",
+    "image_resolution": "Çözünürlük",
+    "image_resolution_description": "Daha yüksek çözünürlükle, daha fazla detayı koruyabilir ancak kodlanması daha uzun sürer, daha büyük dosya boyutlarına sahip olur ve uygulamanın yanıt verme hızını azaltabilir.",
     "image_settings": "Fotoğraf ayarları",
     "image_settings_description": "Oluşturulan fotoğrafların kalite ve çözünürlüklerini yönet",
-    "image_thumbnail_format": "Önizleme biçimi",
-    "image_thumbnail_resolution": "Önizleme çözünürlüğü",
-    "image_thumbnail_resolution_description": "Fotoğrafların grup hâlinde gösteriminde kullanılır (ana zaman akışı, albümler, vb.). Daha yüksek çözünürlükler daha fazla detayı koruyabilir ama kodlamaları daha uzun sürer, daha fazla yer kaplarlar, ve uygulamanın akıcılığını azaltabilirler.",
-    "job_concurrency": "{job} eşzamanlılık",
+    "image_thumbnail_description": "Meta verisi çıkarılmış küçük boyutlu küçük resim, ana zaman çizelgesi gibi fotoğraf gruplarını görüntülerken kullanılır",
+    "image_thumbnail_quality_description": "Küçük resim kalitesi 1-100 arasında. Daha yüksek değerler daha iyidir, ancak daha büyük dosyalar üretir ve uygulamanın yanıt hızını azaltabilir.",
+    "image_thumbnail_title": "Küçük Fotoğraf Ayarları",
+    "job_concurrency": "{job} eş zamanlılık",
+    "job_created": "Görev oluşturuldu",
     "job_not_concurrency_safe": "Bu işlem eşzamanlama için uygun değil.",
-    "job_settings": "İş ayarları",
-    "job_settings_description": "Aynı anda çalışacak işleri yönet",
-    "job_status": "İş durumu",
+    "job_settings": "Görev Ayarları",
+    "job_settings_description": "Aynı anda çalışacak görevleri yönet",
+    "job_status": "Görev Statüleri",
     "jobs_delayed": "{jobCount, plural, other {# gecikmeli}}",
     "jobs_failed": "{jobCount, plural, other {# Başarısız}}",
     "library_created": "{library} kütüphanesi oluşturuldu",
-    "library_cron_expression": "Cron formatı",
-    "library_cron_expression_description": "Cron formatını kullanarak tarama aralığını belirleyin. Daha fazla bilgi için <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Cron formatı önayarları",
     "library_deleted": "Kütüphane silindi",
-    "library_import_path_description": "İçe aktarmak için bir klasör seçin. Bu klasör, alt klasörleriyle birlikte fotoğraf ve videolar için taranacak.",
-    "library_scanning": "Periyodik tarama",
+    "library_import_path_description": "Belirtilecek klasörü içe aktarın. Bu klasör, alt klasörler dahil olmak üzere, görüntüler ve videolar için taranacaktır.",
+    "library_scanning": "Periyodik Tarama",
     "library_scanning_description": "Periyodik kütüphane taramasını yönet",
     "library_scanning_enable_description": "Periyodik kütüphane taramasını etkinleştir",
-    "library_settings": "Dış kütüphane",
-    "library_settings_description": "Dış kütüphane ayarlarını yönet",
+    "library_settings": "Harici kütüphane",
+    "library_settings_description": "Harici kütüphane ayarlarını yönet",
     "library_tasks_description": "Kütüphane görevleri gerçekleştir",
-    "library_watching_enable_description": "Dış kütüphaneleri dosya değişimi için izle",
+    "library_watching_enable_description": "Harici kütüphanelerdeki dosya değişikliklerini izle",
     "library_watching_settings": "Kütüphane izleme (DENEYSEL)",
     "library_watching_settings_description": "Değişen dosyalar için otomatik olarak izle",
     "logging_enable_description": "Günlüğü aktifleştir",
@@ -137,9 +147,9 @@
     "map_settings": "Harita",
     "map_settings_description": "Harita ayarlarını yönet",
     "map_style_description": "style.json Harita ayarlarının URL'si",
-    "metadata_extraction_job": "Metadata'yı çıkart",
-    "metadata_extraction_job_description": "Tüm varlıklardan GPS, çözünürlük gibi metadatayı çıkart",
-    "metadata_faces_import_setting": "Yüzleri alma aktif",
+    "metadata_extraction_job": "Meta verilerinden Ayıkla",
+    "metadata_extraction_job_description": "GPS ve çözünürlük gibi ger bir varlığın meta veri bilgilerini ayıklayın",
+    "metadata_faces_import_setting": "Yüz içe aktarmayı etkinleştir",
     "metadata_faces_import_setting_description": "Yüzleri, EXIF verileri ve sidecar dosyalardan getir",
     "metadata_settings": "Metaveri Ayarları",
     "metadata_settings_description": "Metaveri ayarlarını yönet",
@@ -147,11 +157,11 @@
     "migration_job_description": "Varlıklar ve yüzler için resim çerçeve önizlemelerini en yeni klasör yapısına aktar",
     "no_paths_added": "Yol eklenmedi",
     "no_pattern_added": "Desen eklenmedi",
-    "note_apply_storage_label_previous_assets": "Not: Depolama adresini daha önce yüklenmiş dosyalara uygulamak için",
+    "note_apply_storage_label_previous_assets": "Not: Daha önce yüklenen varlıklara Depolama Etiketi uygulamak için şu komutu çalıştırın",
     "note_cannot_be_changed_later": "NOT: Bu daha sonra değiştirilemez!",
     "note_unlimited_quota": "NOT: Sınırsız kota için 0 yazın",
     "notification_email_from_address": "Şu adresten",
-    "notification_email_from_address_description": "Göndericinin email adresi, örnek: \"Immich Fotoğraf Sunucusu <noreply@immich.app>\"",
+    "notification_email_from_address_description": "Göndericinin email adresi, örnek: \"Immich Fotoğraf Sunucusu <noreply@example.com>\"",
     "notification_email_host_description": "E-posta sunucusunun ana bilgisayarı (örneğin, smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Sertifika hatalarını görmezden gel",
     "notification_email_ignore_certificate_errors_description": "TLS sertifika doğrulama ayarlarını görmezden gel (Önerilmez)",
@@ -171,13 +181,13 @@
     "oauth_auto_register": "Otomatik kayıt",
     "oauth_auto_register_description": "OAuth ile giriş yapan yeni kullanıcıları otomatik kaydet",
     "oauth_button_text": "Buton yazısı",
-    "oauth_client_id": "Kullanıcı ID",
+    "oauth_client_id": "İstemci ID",
     "oauth_client_secret": "Gizli İstemci Anahtarı",
     "oauth_enable_description": "OAuth ile giriş yap",
     "oauth_issuer_url": "Yayınlayıcı URL",
     "oauth_mobile_redirect_uri": "Mobil yönlendirme URL'si",
     "oauth_mobile_redirect_uri_override": "Mobilde zorla kullanılacak Yönlendirme Adresi",
-    "oauth_mobile_redirect_uri_override_description": "OAuth sağlayıcısı '{callback}'gibi bir mobil URI'ye izin vermediğinde etkinleştir.",
+    "oauth_mobile_redirect_uri_override_description": "Mobil URI'ye izin vermeyen OAuth sağlayıcısı olduğunda etkinleştir, örneğin '{callback}'",
     "oauth_profile_signing_algorithm": "Profil imzalama algoritması",
     "oauth_profile_signing_algorithm_description": "Kullanıcının profilini imzalarken kullanılacak güvenlik algoritması.",
     "oauth_scope": "Kapsam",
@@ -197,25 +207,27 @@
     "password_settings": "Şifre giriş",
     "password_settings_description": "Şifre giriş ayarlarını yönet",
     "paths_validated_successfully": "Tüm yollar başarıyla doğrulandı",
+    "person_cleanup_job": "Kişi temizleme",
     "quota_size_gib": "Kota boyutu (GiB)",
     "refreshing_all_libraries": "Tüm kütüphaneler yenileniyor",
     "registration": "Yönetici kaydı",
     "registration_description": "Sistemdeki ilk kullanıcı olduğunuz için hesabınız Yönetici olarak ayarlandı. Yeni oluşturulan üyeliklerin, ve yönetici görevlerinin sorumlusu olarak atandınız.",
-    "removing_offline_files": "Çevrimdışı dosyalar kaldırılıyor",
     "repair_all": "Tümünü onar",
-    "repair_matched_items": "Eşleşen {sayı, çoğul, bir {# öğe} diğer {# öğeler}}",
+    "repair_matched_items": "{count, plural, one {# öğe eşleşti} other {# öğeler eşleşti}}",
     "repaired_items": "{count, plural, one {# item} other {# items}} tamir edildi",
     "require_password_change_on_login": "Kullanıcının ilk girişinde şifre değiştirmesini zorunlu kıl",
     "reset_settings_to_default": "Ayarları varsayılana sıfırla",
     "reset_settings_to_recent_saved": "Ayarları kaydedilmiş önceki ayarlara döndür",
-    "scanning_library_for_changed_files": "Değişen dosyalar için kütüphane taranıyor",
-    "scanning_library_for_new_files": "Yeni dosyalar için kütüphane taranıyor",
-    "send_welcome_email": "Hoşgeldin emaili yolla",
+    "scanning_library": "Kütüphaneyi tarama",
+    "search_jobs": "Görevleri Ara...",
+    "send_welcome_email": "Hoş geldin e-postası gönder",
     "server_external_domain_settings": "Dış domain",
     "server_external_domain_settings_description": "Paylaşılan fotoğraflar için domain, http(s):// dahil",
+    "server_public_users": "Harici Kullanıcılar",
+    "server_public_users_description": "Paylaşılan albümlere bir kullanıcı eklenirken tüm kullanıcılar (ad ve e-posta) listelenir. Devre dışı bırakıldığında, kullanıcı listesi yalnızca yönetici kullanıcılar tarafından kullanılabilir.",
     "server_settings": "Sunucu ayarları",
     "server_settings_description": "Sunucu ayarlarını yönet",
-    "server_welcome_message": "Hoşgeldin mesajı",
+    "server_welcome_message": "Hoş geldin mesajı",
     "server_welcome_message_description": "Giriş sayfasında gösterilen mesaj.",
     "sidecar_job": "Ek dosya ile taşınan metadata",
     "sidecar_job_description": "Ek dosyalardaki metadataları bul ve güncelle",
@@ -230,13 +242,22 @@
     "storage_template_migration_description": "Geçerli <link>{template}</link> ayarlarını daha önce yüklenmiş olan varlıklara uygula",
     "storage_template_migration_info": "Şablon ayarlarındaki değişiklikler sadece yeni varlıklara uygulanacak. Şablon ayarlarını daha önce yüklenmiş olan varlıklara uygulamak için <link>{job}</link> çalıştırın.",
     "storage_template_migration_job": "Depolama Adreslerini Değiştirme Görevi",
-    "storage_template_more_details": "Bu özellik hakkında daha fazla bilgi edinmek için <template-link>Depolama Şablonu</template-link> linkini, bunun neticesi için ise <template-link>Netice</template-link> linkini ziyaret edin",
+    "storage_template_more_details": "Bu özellik hakkında daha fazla bilgi için, <template-link>Depolama Şablonu</template-link> ve onun <implications-link>etkileri</implications-link> kısmına bakın",
     "storage_template_onboarding_description": "Bu özellik açıldığında, dosyaları kullanıcı için belirlenen depolama adresi taslağına göre otomatik olarak düzenler. Bu özellik bazen sorun çıkarabildiğini için kapalı gelmektedir. Daha fazla bilgi için <link>dokümantasyona</link> bakabilirsiniz.",
     "storage_template_path_length": "Tahmini dosya adresi uzunluğu: <b>{length, number}</b>/{limit, number}",
     "storage_template_settings": "Depolama Şablonu",
     "storage_template_settings_description": "Yüklenen dosyanın ismini ve klasör yapısını düzenle",
     "storage_template_user_label": "<code>{label}</code> kullanıcını dosyaları için kullanılan alt klasördür",
     "system_settings": "Sistem Ayarları",
+    "tag_cleanup_job": "Etiket temizleme",
+    "template_email_available_tags": "Şablonunuzda şu değişkenler kullanılabilir: {tags}",
+    "template_email_if_empty": "Şablon boş ise, varsayılan e-posta kullanılacak.",
+    "template_email_preview": "Ön izleme",
+    "template_email_settings": "Eposta Taslakları",
+    "template_email_settings_description": "Özel e-posta bildirim şablonlarını yönet",
+    "template_email_update_album": "Albüm Şablonunu Güncelle",
+    "template_settings": "Bildirim Şablonları",
+    "template_settings_description": "Bildirim şablonlarını yönet.",
     "theme_custom_css_settings": "Özel CSS",
     "theme_custom_css_settings_description": "CSS (Cascading Style Sheets) kullanılarak Immich'in tasarımı değiştirilebilir.",
     "theme_settings": "Tema ayarları",
@@ -269,7 +290,7 @@
     "transcoding_hardware_acceleration": "Donanım Hızlandırma",
     "transcoding_hardware_acceleration_description": "Deneysel; daha hızlı, fakat aynı bitrate ayarlarında daha düşük kaliteye sahip",
     "transcoding_hardware_decoding": "Donanım çözücü",
-    "transcoding_hardware_decoding_setting_description": "Sadece NVENC, QSV ve RKMPP için geçerli. Sadece işlemeyi hızlandırmak yerine uçtan uca hızlandırmayı etkinleştirir. Tüm videolarda çalışmayabilir.",
+    "transcoding_hardware_decoding_setting_description": "Uçtan uca hızlandırmayı, sadece kodlamayı hızlandırmanın yerine etkinleştirir. Tüm videolarda çalışmayabilir.",
     "transcoding_hevc_codec": "HEVC kodek",
     "transcoding_max_b_frames": "Maksimum B-kareler",
     "transcoding_max_b_frames_description": "Daha yüksek değerler sıkıştırma verimliliğini artırır, ancak kodlamayı yavaşlatır. Eski cihazlarda donanım hızlandırma ile uyumlu olmayabilir. 0, B-çerçevelerini devre dışı bırakır, -1 ise bu değeri otomatik olarak ayarlar.",
@@ -280,7 +301,7 @@
     "transcoding_optimal_description": "Hedef çözünürlükten yüksek veya kabul edilen formatta olmayan videolar",
     "transcoding_preferred_hardware_device": "Tercih edilen donanım cihazı",
     "transcoding_preferred_hardware_device_description": "Sadece VAAPI ve QSV için uygulanır. Donanım kod çevrimi için DRI Node ayarlar.",
-    "transcoding_preset_preset": "",
+    "transcoding_preset_preset": "Ön ayar (-ön)",
     "transcoding_preset_preset_description": "Sıkıştırma hızı. Daha yavaş olan ayarlar belirli bitrate ayarları için daha küçük ve daha kaliteli dosya üretir. VP9 ayarı 'daha hızlı' ayarının üstündeki ayarları görmezden gelir.",
     "transcoding_reference_frames": "Referans kareler",
     "transcoding_reference_frames_description": "Belirli bir kareyi sıkıştırırken referans alınacak kare sayısı. Daha yüksek değerler sıkıştırma verimliliğini artırır, ancak kodlamayı yavaşlatır. 0 bu değeri otomatik olarak ayarlar.",
@@ -289,15 +310,13 @@
     "transcoding_settings_description": "Video dosyalarının çözünürlük ve kodlama bilgilerini yönetir",
     "transcoding_target_resolution": "Hedef çözünürlük",
     "transcoding_target_resolution_description": "Daha yüksek çözünürlükler daha fazla detayı koruyabilir fakat işlemesi daha uzun sürer, dosya boyutu daha yüksek olur ve uygulamanın akıcılığını etkileyebilir.",
-    "transcoding_temporal_aq": "",
+    "transcoding_temporal_aq": "Zamansal AQ",
     "transcoding_temporal_aq_description": "Sadece NVENC için geçerlidir. Yüksek-detayların ve düşük-hareket sahnelerin kalitesini arttır. Eski cihazlarla uyumlu olmayabilir.",
     "transcoding_threads": "İş Parçacıkları",
     "transcoding_threads_description": "Daha yüksek değerler daha hızlı kodlamaya yol açar, ancak sunucunun etkin durumdayken diğer görevleri işlemesi için daha az alan bırakır. Bu değer İşlemci çekirdeği sayısından fazla olmamalıdır. 0'a ayarlanırsa kullanımı en üst düzeye çıkarır.",
     "transcoding_tone_mapping": "Ton-haritalama",
     "transcoding_tone_mapping_description": "HDR videoların SDR'ye dönüştürülürken görünümünü korumayı amaçlar. Her algoritma renk, detay ve parlaklık için farklı dengeleme yapar. Hable detayları korur, Mobius renkleri korur ve Reinhard parlaklığı korur.",
-    "transcoding_tone_mapping_npl": "",
-    "transcoding_tone_mapping_npl_description": "Renkler, bu parlaklıkta bir ekran için normal görünecek şekilde ayarlanacaktır. Karşıt olarak, daha düşük değerler videonun parlaklığını artırır ve tersi de geçerlidir çünkü ekranın parlaklığını telafi eder. 0 bu değeri otomatik olarak ayarlar.",
-    "transcoding_transcode_policy": "Dönüştürme(çevirme) politikası",
+    "transcoding_transcode_policy": "Dönüştürme (çevirme) politikası",
     "transcoding_transcode_policy_description": "Bir videonun ne zaman kod dönüştürülmesi gerektiğine ilişkin ilke. Dönüştürme devre dışı bırakılmadığı sürece HDR videolar her zaman dönüştürülür.",
     "transcoding_two_pass_encoding": "İki geçişli kodlama",
     "transcoding_two_pass_encoding_setting_description": "Daha iyi kodlanmış videolar üretmek için iki geçişte kod dönüştürün. Maksimum bit hızı etkinleştirildiğinde (H.264 ve HEVC ile çalışması için gereklidir), bu mod maksimum bit hızına dayalı bir bit hızı aralığı kullanır ve CRF'yi yok sayar. VP9 için, maksimum bit hızı devre dışı bırakılırsa CRF kullanılabilir.",
@@ -310,29 +329,34 @@
     "trash_settings_description": "Çöp ayarlarını yönet",
     "untracked_files": "İzlenmeyen dosyalar",
     "untracked_files_description": "Bu dosyalar uygulama tarafından izlenmiyor. Yarıda kesilen yüklemeler veya uygulama hatası bunlara sebep olmuş olabilir",
+    "user_cleanup_job": "Kullanıcı temizleme",
     "user_delete_delay": "<b>{user}</b> hesabı ve varlıkları {delay, plural, one {# day} other {# days}} gün içinde kalıcı olarak silinmek için planlandı.",
     "user_delete_delay_settings": "Silme gecikmesi",
     "user_delete_delay_settings_description": "Bir kullanıcının hesabını ve varlıklarını kalıcı olarak silmek için kaldırıldıktan sonra gereken gün sayısı. Kullanıcı silme işi, silinmeye hazır kullanıcıları kontrol etmek için gece yarısı çalışır. Bu ayardaki değişiklikler bir sonraki yürütmede değerlendirilecektir.",
-    "user_delete_immediately": "<b>{Kullanıcı}</b>'nın hesabı ve varlıkları <b>hemen</b> kalıcı olarak silinmek üzere sıraya alınacak.",
-    "user_delete_immediately_checkbox": "Kullanıcıyı ve tüm varlıklarını kalıcı olarak silmek için sıraya koy",
+    "user_delete_immediately": "<b>{user}</b>'in hesabı ve varlıkları <b>hemen</b> kalıcı olarak silinmek üzere sıraya alınacak.",
+    "user_delete_immediately_checkbox": "Kullanıcı ve varlıkları hemen silinmek üzere sıraya al",
     "user_management": "Kullanıcı Yönetimi",
     "user_password_has_been_reset": "Kullanıcının şifresi sıfırlandı:",
     "user_password_reset_description": "Lütfen kullanıcıya geçici şifreyi sağlayın ve bir sonraki oturum açışında şifreyi değiştirmesi gerektiğini bildirin.",
     "user_restore_description": "<b>{user}</b> kullanıcısı geri yüklenecek.",
-    "user_restore_scheduled_removal": "Kullanıcıyı geri yükle - {tarih, tarih, uzun} tarihinde zamanlanmış kaldırma",
+    "user_restore_scheduled_removal": "Kullanıcıyı geri yükle - {date, date, long} tarihinde planlanan kaldırma",
     "user_settings": "Kullanıcı Ayarları",
     "user_settings_description": "Kullanıcı Ayarlarını Yönet",
     "user_successfully_removed": "Kullanıcı {email} başarıyla kaldırıldı.",
     "version_check_enabled_description": "Sürüm kontrolü etkin",
+    "version_check_implications": "Sürüm kontrol özelliği, github.com ile periyodik iletişime dayanır",
     "version_check_settings": "Versiyon kontrolü",
     "version_check_settings_description": "Yeni sürüm bildirimini etkinleştir/devre dışı bırak",
-    "video_conversion_job": "",
+    "video_conversion_job": "Videoları dönüştür",
     "video_conversion_job_description": "Tarayıcılar ve cihazlarla daha geniş uyumluluk için videoları dönüştür"
   },
   "admin_email": "Yönetici Emaili",
   "admin_password": "Yönetici Şifresi",
   "administration": "Yönetim",
   "advanced": "Gelişmiş",
+  "age_months": "Yaş {months, plural, one {# ay} other {# ay}}",
+  "age_year_months": "1 yaş, {months, plural, one {# ay} other {# ay}}",
+  "age_years": "{years, plural, other {Yaş #}}",
   "album_added": "Albüm eklendi",
   "album_added_notification_setting_description": "Paylaşılan bir albüme eklendiğinizde email bildirimi alın",
   "album_cover_updated": "Albüm Kapağı güncellendi",
@@ -352,7 +376,7 @@
   "album_user_removed": "{user} kaldırıldı",
   "album_with_link_access": "Link'e sahip olan herhangi bir kişinin bu albümdeki fotoğrafları ve kişileri görmesine izin ver.",
   "albums": "Albümler",
-  "albums_count": "",
+  "albums_count": "{count, plural, one {{count, number} Albüm} other {{count, number} Albüm}}",
   "all": "Tümü",
   "all_albums": "Tüm Albümler",
   "all_people": "Tüm Kişiler",
@@ -367,12 +391,12 @@
   "api_key_empty": "Apı Anahtarı isminiz boş olmamalı",
   "api_keys": "API Anahtarları",
   "app_settings": "Uygulama Ayarları",
-  "appears_in": "",
+  "appears_in": "Şurada görünür",
   "archive": "Arşiv",
   "archive_or_unarchive_photo": "Fotoğrafı arşivle/arşivden çıkar",
   "archive_size": "Arşiv boyutu",
   "archive_size_description": "İndirmeler için arşiv boyutunu yapılandırın (GiB cinsinden)",
-  "archived": "",
+  "archived_count": "{count, plural, other {# arşivlendi}}",
   "are_these_the_same_person": "Bunlar aynı kişi mi?",
   "are_you_sure_to_do_this": "Bunu yapmak istediğinize emin misiniz?",
   "asset_added_to_album": "Albüme eklendi",
@@ -380,23 +404,38 @@
   "asset_description_updated": "Varlık açıklaması güncellendi",
   "asset_filename_is_offline": "Varlık {filename} çevrimdışı",
   "asset_has_unassigned_faces": "Varlık, atanmamış yüzler içeriyor",
-  "asset_offline": "Varlık çevrimdışı",
-  "asset_offline_description": "Bu varlık çevrimdışı. Immich dosya konumuna erişemiyor. Lütfen varlığın kullanılabilir olduğundan emin olun ve ardından kitaplığı yeniden tarayın.",
+  "asset_hashing": "Karma (hashleme) oluşturuluyor...",
+  "asset_offline": "Varlık Çevrim Dışı",
+  "asset_offline_description": "Bu harici varlık artık diskte bulunmuyor. Yardım için lütfen Immich yöneticinizle iletişime geçin.",
   "asset_skipped": "Atlandı",
+  "asset_skipped_in_trash": "Çöpte",
   "asset_uploaded": "Yüklendi",
   "asset_uploading": "Yükleniyor...",
   "assets": "Varlıklar",
-  "assets_restore_confirmation": "Çöpteki bütün varlıkları geri yüklemek istediğinize emin misiniz? Bu işlem geri alınamaz!",
+  "assets_added_count": "{count, plural, one {# varlık eklendi} other {# varlık eklendi}}",
+  "assets_added_to_album_count": "{count, plural, one {# varlık} other {# varlık}} albüme eklendi",
+  "assets_added_to_name_count": "{count, plural, one {# varlık} other {# varlık}} {hasName, select, true {<b>{name}</b>} other {yeni albüm}} içine eklendi",
+  "assets_count": "{count, plural, one {# varlık} other {# varlıklar}}",
+  "assets_moved_to_trash_count": "{count, plural, one {# varlık} other {# varlık}} çöpe taşındı",
+  "assets_permanently_deleted_count": "Kalıcı olarak silindi {count, plural, one {# varlık} other {# varlıklar}}",
+  "assets_removed_count": "Kaldırıldı {count, plural, one {# varlık} other {# varlıklar}}",
+  "assets_restore_confirmation": "Tüm çöp kutusundaki varlıklarınızı geri yüklemek istediğinizden emin misiniz? Bu işlemi geri alamazsınız! Ayrıca, çevrim dışı olan varlıkların bu şekilde geri yüklenemeyeceğini unutmayın.",
+  "assets_restored_count": "{count, plural, one {# varlık} other {# varlıklar}} geri yüklendi",
+  "assets_trashed_count": "{count, plural, one {# varlık} other {# varlıklar}} çöp kutusuna taşındı",
+  "assets_were_part_of_album_count": "{count, plural, one {Varlık zaten} other {Varlıklar zaten}} albümün parçasıydı",
   "authorized_devices": "Yetki Verilmiş Cihazlar",
   "back": "Geri",
-  "back_close_deselect": "Geri, kapat, veya seçimi kaldır",
-  "backward": "",
+  "back_close_deselect": "Geri, kapat veya seçimi kaldır",
+  "backward": "Geriye doğru",
   "birthdate_saved": "Doğum günü başarılı bir şekilde kaydedildi",
   "birthdate_set_description": "Doğum günü, fotoğraftaki insanın fotoğraf çekildiği zamandaki yaşının hesaplanması için kullanılır.",
   "blurred_background": "Bulanık arkaplan",
-  "bulk_delete_duplicates_confirmation": "Toplu olarak {sayım, çoğul, bir {# yinelenen varlık} diğer {# yinelenen varlıklar} 'ı silmek istediğinizden emin misiniz? Bu, her grubun en büyük varlığını tutacak ve diğer tüm kopyaları kalıcı olarak silecektir. Bu işlemi geri alamazsın!",
-  "bulk_keep_duplicates_confirmation": "{sayım, çoğul, bir {# yinelenen varlık} diğer {# yinelenen varlıklar}}ı tutmak istediğinizden emin misiniz? Bu, hiçbir şeyi silmeden tüm yinelenen grupları çözecektir.",
-  "bulk_trash_duplicates_confirmation": "Toplu olarak {say, çoğul, bir {# yinelenen varlık} diğer {# yinelenen varlıklar} öğesini çöpe atmak istediğinizden emin misiniz? Bu, her grubun en büyük varlığını tutacak ve diğer tüm kopyaları çöpe atacaktır.",
+  "bugs_and_feature_requests": "Hatalar ve Özellik Talepleri",
+  "build": "Yapı",
+  "build_image": "Görüntü Oluştur",
+  "bulk_delete_duplicates_confirmation": "Toplu olarak {count, plural, one {# kopya öğeyi} other {# kopya öğeleri}} silmek istediğinizden emin misiniz? Bu işlem, her gruptaki en büyük öğeyi tutacak ve diğer tüm kopyaları kalıcı olarak silecektir. Bu işlemi geri alamazsınız!",
+  "bulk_keep_duplicates_confirmation": "{count, plural, one {# kopya öğeyi} other {# kopya öğeleri}} tutmak istediğinizden emin misiniz? Bu işlem, hiçbir şeyi silmeden tüm kopya gruplarını çözecektir.",
+  "bulk_trash_duplicates_confirmation": "{count, plural, one {# kopya öğeyi} other {# kopya öğeleri}} toplu olarak çöp kutusuna taşımak istediğinizden emin misiniz? Bu işlem, her grubun en büyük öğesini tutacak ve diğer tüm kopyaları çöp kutusuna taşıyacaktır.",
   "buy": "Immich'i Satın Alın",
   "camera": "Kamera",
   "camera_brand": "Kamera markası",
@@ -406,12 +445,8 @@
   "cannot_merge_people": "Kişiler birleştirilemiyor",
   "cannot_undo_this_action": "Bu işlem geri alınamaz!",
   "cannot_update_the_description": "Açıklama güncellenemiyor",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
   "change_date": "Tarihi değiştir",
-  "change_expiration_time": "",
+  "change_expiration_time": "Son kullanma süresini değiştir",
   "change_location": "Konumu değiştir",
   "change_name": "İsim değiştir",
   "change_name_successfully": "İsim başarıyla değiştirildi",
@@ -419,8 +454,8 @@
   "change_password_description": "Bu ya sistemdeki ilk oturum açışınız ya da şifre değişikliği için bir talepte bulunuldu. Lütfen yeni şifreyi aşağıya yazınız.",
   "change_your_password": "Şifreni değiştir",
   "changed_visibility_successfully": "Görünürlük başarıyla değiştirildi",
-  "check_all": "",
-  "check_logs": "Logları Konrol et",
+  "check_all": "Tümünü Seç",
+  "check_logs": "Günlükleri Kontrol Et",
   "choose_matching_people_to_merge": "Birleştirmek için eşleşen kişileri seçiniz",
   "city": "Şehir",
   "clear": "Temiz",
@@ -430,7 +465,8 @@
   "clear_value": "Değeri Temizle",
   "clockwise": "Saat yönü",
   "close": "Kapat",
-  "collapse_all": "",
+  "collapse": "Daralt",
+  "collapse_all": "Tümünü Daralt",
   "color": "Renk",
   "color_theme": "Renk teması",
   "comment_deleted": "Yorum silindi",
@@ -440,9 +476,10 @@
   "confirm": "Onayla",
   "confirm_admin_password": "Yönetici Şifresini Onayla",
   "confirm_delete_shared_link": "Bu paylaşılan bağlantıyı silmek istediğinizden emin misiniz?",
+  "confirm_keep_this_delete_others": "Yığındaki diğer tüm öğeler bu varlık haricinde silinecektir. Devam etmek istediğinizden emin misiniz?",
   "confirm_password": "Şifreyi onayla",
-  "contain": "",
-  "context": "",
+  "contain": "İçermek",
+  "context": "Bağlam",
   "continue": "Devam et",
   "copied_image_to_clipboard": "Resim, panoya kopyalandı.",
   "copied_to_clipboard": "Panoya kopyalandı!",
@@ -454,618 +491,848 @@
   "copy_password": "Parolayı kopyala",
   "copy_to_clipboard": "Panoya Kopyala",
   "country": "Ülke",
-  "cover": "",
-  "covers": "",
+  "cover": "Kapla",
+  "covers": "Kaplar",
   "create": "Oluştur",
   "create_album": "Albüm oluştur",
   "create_library": "Kütüphane Oluştur",
   "create_link": "Link oluştur",
   "create_link_to_share": "Paylaşmak için link oluştur",
+  "create_link_to_share_description": "Bağlantıya sahip olan herkesin seçilen fotoğrafları görmesine izin ver",
   "create_new_person": "Yeni kişi oluştur",
   "create_new_person_hint": "Seçili varlıkları yeni bir kişiye atayın",
   "create_new_user": "Yeni kullanıcı oluştur",
   "create_tag": "Etiket oluştur",
+  "create_tag_description": "Yeni bir etiket oluşturun. İç içe geçmiş etiketler için, etiketi tam yolu ve eğik çizgileri de dahil ederek giriniz.",
   "create_user": "Kullanıcı oluştur",
   "created": "Oluşturuldu",
-  "current_device": "",
+  "current_device": "Mevcut cihaz",
   "custom_locale": "Özel Yerel Ayar",
   "custom_locale_description": "Tarihleri ve sayıları dile ve bölgeye göre biçimlendirin",
   "dark": "Koyu",
-  "date_after": "",
+  "date_after": "Sonraki tarih",
   "date_and_time": "Tarih ve Zaman",
-  "date_before": "",
+  "date_before": "Önceki tarih",
   "date_of_birth_saved": "Doğum günü başarı ile kaydedildi",
   "date_range": "Tarih aralığı",
   "day": "Gün",
-  "default_locale": "",
+  "deduplicate_all": "Tüm kopyaları kaldır",
+  "default_locale": "Varsayılan Yerel Ayar",
   "default_locale_description": "Tarihleri ve sayıları tarayıcınızın yerel ayarına göre biçimlendirin",
   "delete": "Sil",
   "delete_album": "Albümü sil",
   "delete_api_key_prompt": "Bu API anahtarını silmek istediğinizden emin misiniz?",
+  "delete_duplicates_confirmation": "Bu kopyaları kalıcı olarak silmek istediğinizden emin misiniz?",
   "delete_key": "Anahtarı sil",
   "delete_library": "Kütüphaneyi sil",
   "delete_link": "Bağlantıyı sil",
+  "delete_others": "Diğerlerini sil",
   "delete_shared_link": "Paylaşılmış linki sil",
   "delete_tag": "Etiketi sil",
   "delete_tag_confirmation_prompt": "{tagName} etiketini silmek istediğinizden emin misiniz?",
   "delete_user": "Kullanıcıyı sil",
-  "deleted_shared_link": "",
+  "deleted_shared_link": "Paylaşılan bağlantı silindi",
+  "deletes_missing_assets": "Diskte eksik olan varlıkları siler",
   "description": "Açıklama",
   "details": "Detaylar",
   "direction": "Yön",
-  "disabled": "",
-  "disallow_edits": "",
+  "disabled": "Devre dışı bırakıldı",
+  "disallow_edits": "Değişikliklere izin verme",
+  "discord": "Discord",
   "discover": "Keşfet",
   "dismiss_all_errors": "Tüm hataları yoksay",
   "dismiss_error": "Hatayı yoksay",
   "display_options": "Görüntüleme seçenekleri",
-  "display_order": "",
+  "display_order": "Gösterim sıralaması",
   "display_original_photos": "Orijinal fotoğrafları göster",
-  "display_original_photos_setting_description": "",
+  "display_original_photos_setting_description": "Orijinal varlık web uyumlu olduğunda, bir varlığı görüntülerken küçük resimler yerine orijinal fotoğrafı görüntülemeyi tercih edin. Bu, fotoğraf görüntüleme hızlarının yavaşlamasına neden olabilir.",
   "do_not_show_again": "Bu mesajı bir daha gösterme",
-  "done": "",
+  "documentation": "Dokümantasyon",
+  "done": "Bitti",
   "download": "İndir",
+  "download_include_embedded_motion_videos": "Gömülü videolar",
+  "download_include_embedded_motion_videos_description": "Görsel hareketli fotoğraflarda yer alan gömülü videoları ayrı bir dosya olarak dahil et",
   "download_settings": "İndir",
-  "downloading": "",
-  "duplicates": "",
+  "download_settings_description": "Varlık indirme ile ilgili ayarları yönetin",
+  "downloading": "İndiriliyor",
+  "downloading_asset_filename": "Varlık indiriliyor {filename}",
+  "drop_files_to_upload": "Dosyaları yüklemek için herhangi bir yere bırakın",
+  "duplicates": "Kopyalar",
+  "duplicates_description": "Her grubu çözmek için, varsa hangilerinin kopya olduğunu belirtin",
   "duration": "Süre",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
   "edit": "Düzenle",
   "edit_album": "Albümü düzenle",
-  "edit_avatar": "",
-  "edit_date": "",
-  "edit_date_and_time": "",
-  "edit_exclusion_pattern": "",
-  "edit_faces": "",
-  "edit_import_path": "",
-  "edit_import_paths": "",
+  "edit_avatar": "Avatarı Düzenle",
+  "edit_date": "Tarihi Düzenle",
+  "edit_date_and_time": "Tarih ve zamanı düzenleyin",
+  "edit_exclusion_pattern": "Hariç tutma desenini düzenle",
+  "edit_faces": "Yüzleri Düzenleyin",
+  "edit_import_path": "İçe aktarma yolunu düzenleyin",
+  "edit_import_paths": "İçe Aktarma Yollarını Düzenle",
   "edit_key": "Anahtarı düzenle",
   "edit_link": "Bağlantıyı düzenle",
-  "edit_location": "",
-  "edit_name": "",
+  "edit_location": "Lokasyonu düzenleyin",
+  "edit_name": "İsmi düzenleyin",
   "edit_people": "Kişileri düzenle",
   "edit_tag": "Etiketi düzenle",
   "edit_title": "Başlığı düzenle",
   "edit_user": "Kullanıcıyı düzenle",
-  "edited": "",
-  "editor": "",
+  "edited": "Düzenlendi",
+  "editor": "Editör",
   "editor_close_without_save_prompt": "Değişiklikler kaydedilmeyecek",
   "editor_close_without_save_title": "Düzenleyici kapatılsın mı?",
+  "editor_crop_tool_h2_aspect_ratios": "En boy oranları",
+  "editor_crop_tool_h2_rotation": "Rotasyon",
   "email": "E-posta",
-  "empty_album": "",
   "empty_trash": "Çöpü boşalt",
+  "empty_trash_confirmation": "Çöp kutusunu boşaltmak istediğinizden emin misiniz? Bu işlem, Immich'teki çöp kutusundaki tüm varlıkları kalıcı olarak silecektir.\nBu işlemi geri alamazsınız!",
   "enable": "Etkinleştir",
   "enabled": "Etkinleştirildi",
-  "end_date": "",
+  "end_date": "Bitiş tarihi",
   "error": "Hata",
-  "error_loading_image": "",
+  "error_loading_image": "Resim yüklenirken hata oluştu",
+  "error_title": "Bir Hata Oluştu - Bir şeyler ters gitti",
   "errors": {
+    "cannot_navigate_next_asset": "Sonraki varlığa geçiş yapılamıyor",
+    "cannot_navigate_previous_asset": "Önceki varlığa geçiş yapılamıyor",
     "cant_apply_changes": "Değişiklikler uygulanamıyor",
+    "cant_change_activity": "Etkinliği {enabled, select, true {devre dışı bırakamıyor} other {etkinleştiremiyor}}",
+    "cant_change_asset_favorite": "Varlığın favori durumunu değiştiremiyor",
+    "cant_change_metadata_assets_count": "{count, plural, one {# varlığın} other {# varlıkların}} meta verisi değiştirilemiyor",
+    "cant_get_faces": "Yüzler alınamadı",
+    "cant_get_number_of_comments": "Yorumların sayısı alınamadı",
     "cant_search_people": "Kişiler aranamıyor",
-    "cleared_jobs": "",
-    "exclusion_pattern_already_exists": "",
-    "failed_job_command": "",
+    "cant_search_places": "Mekanlar aranamıyor",
+    "cleared_jobs": "İşler temizlendi: {job}",
+    "error_adding_assets_to_album": "Albüme varlık ekleme hatası",
+    "error_adding_users_to_album": "Albüme kullanıcı ekleme hatası",
+    "error_deleting_shared_user": "Paylaşılan kullanıcı silme hatası",
+    "error_downloading": "{filename} indirme hatası",
+    "error_hiding_buy_button": "Satın alma butonu gizleme hatası",
+    "error_removing_assets_from_album": "Varlığı albümden silme hatası, daha fazla detay için konsolu kontrol et",
+    "error_selecting_all_assets": "Bütün varlıkları seçme hatası",
+    "exclusion_pattern_already_exists": "Bu dışlama modeli halihazırda mevcut.",
+    "failed_job_command": "{command} komutu iş: {job} için tamamlanamadı",
     "failed_to_create_album": "Albüm oluşturulamadı",
     "failed_to_create_shared_link": "Paylaşılan bağlantı oluşturulamadı",
     "failed_to_edit_shared_link": "Paylaşılan bağlantı düzenlenemedi",
+    "failed_to_get_people": "Kişiler alınamadı",
+    "failed_to_keep_this_delete_others": "Bu öğenin tutulması ve diğer öğenin silinmesi başarısız oldu",
+    "failed_to_load_asset": "Varlık yüklenemedi",
+    "failed_to_load_assets": "Varlıklar yüklenemedi",
+    "failed_to_load_people": "Kişiler yüklenemedi",
     "failed_to_remove_product_key": "Ürün anahtarı kaldırılamadı",
-    "import_path_already_exists": "",
+    "failed_to_stack_assets": "Varlıklar yığınlanamadı",
+    "failed_to_unstack_assets": "Varlıkların yığını kaldırılamadı",
+    "import_path_already_exists": "Bu içe aktarma yolu halihazırda mevcut.",
     "incorrect_email_or_password": "Yanlış e-posta veya şifre",
-    "paths_validation_failed": "",
+    "paths_validation_failed": "{paths, plural, one {# Yol} other {# Yollar}} doğrulanamadı",
     "profile_picture_transparent_pixels": "Profil resimleri şeffaf piksele sahip olamaz. Lütfen resme yakınlaştırın ve/veya resmi hareket ettirin.",
-    "quota_higher_than_disk_size": "",
-    "repair_unable_to_check_items": "",
+    "quota_higher_than_disk_size": "Disk boyutundan daha yüksek bir kota belirlediniz",
+    "repair_unable_to_check_items": "{count, select, one {Öğe} other {Öğeler}} kontrol edilemedi",
     "unable_to_add_album_users": "Kullanıcılar albüme eklenemiyor",
+    "unable_to_add_assets_to_shared_link": "Varlıklar paylaşılan bağlantıya eklenemiyor",
     "unable_to_add_comment": "Yorum eklenemiyor",
-    "unable_to_add_exclusion_pattern": "",
-    "unable_to_add_import_path": "",
-    "unable_to_add_partners": "",
-    "unable_to_change_album_user_role": "",
+    "unable_to_add_exclusion_pattern": "Hariç tutma modeli eklenemiyor",
+    "unable_to_add_import_path": "İçe aktarma yolu eklenemiyor",
+    "unable_to_add_partners": "Ortaklar eklenemiyor",
+    "unable_to_add_remove_archive": "Arşive {archived, select, true {dosyayı kaldır} other {dosya ekle}} işlemi yapılamıyor",
+    "unable_to_add_remove_favorites": "Favorilere {favorite, select, true {dosya ekle} other {dosyayı kaldır}} işlemi yapılamıyor",
+    "unable_to_archive_unarchive": "{archived, select, true {Arşivleme} other {Arşivden çıkarma}} işlemi yapılamıyor",
+    "unable_to_change_album_user_role": "Albüm kullanıcı rolü değiştirilemiyor",
     "unable_to_change_date": "Tarih değiştirilemiyor",
-    "unable_to_change_location": "",
-    "unable_to_change_password": "",
+    "unable_to_change_favorite": "Favori durumu değiştirilemiyor",
+    "unable_to_change_location": "Konum değiştirilemiyor",
+    "unable_to_change_password": "Şifre değiştirilemiyor",
+    "unable_to_change_visibility": "{count, plural, one {# kişi} other {# kişi}} için görünürlük değiştirilemedi",
+    "unable_to_complete_oauth_login": "OAuth giriş işlemi tamamlanamadı",
     "unable_to_connect": "Bağlanılamıyor",
     "unable_to_connect_to_server": "Sunucuya bağlanılamıyor",
     "unable_to_copy_to_clipboard": "Panoya kopyalanamıyor, sayfaya https üzerinden eriştiğinizden emin olun",
     "unable_to_create_admin_account": "Yönetici hesabı oluşturulamıyor",
     "unable_to_create_api_key": "Yeni API anahtarı oluşturulamıyor",
-    "unable_to_create_library": "",
+    "unable_to_create_library": "Kütüphane oluşturulamıyor",
     "unable_to_create_user": "Kullanıcı oluşturulamıyor",
     "unable_to_delete_album": "Albüm silinemiyor",
-    "unable_to_delete_asset": "",
-    "unable_to_delete_exclusion_pattern": "",
-    "unable_to_delete_import_path": "",
+    "unable_to_delete_asset": "Varlık silinemiyor",
+    "unable_to_delete_assets": "Varlıklar silinemiyor",
+    "unable_to_delete_exclusion_pattern": "Hariç tutma deseni silinemiyor",
+    "unable_to_delete_import_path": "İçe aktarma yolu silinemiyor",
     "unable_to_delete_shared_link": "Paylaşılan bağlantı silinemiyor",
     "unable_to_delete_user": "Kullanıcı silinemiyor",
     "unable_to_download_files": "Dosyalar indirilemiyor",
-    "unable_to_edit_exclusion_pattern": "",
-    "unable_to_edit_import_path": "",
+    "unable_to_edit_exclusion_pattern": "Hariç tutma deseni düzenlenemiyor",
+    "unable_to_edit_import_path": "İçe aktarma yolu düzenlenemiyor",
     "unable_to_empty_trash": "Çöp boşaltılamıyor",
     "unable_to_enter_fullscreen": "Tam ekran yapılamıyor",
     "unable_to_exit_fullscreen": "Tam ekrandan çıkılamıyor",
     "unable_to_get_comments_number": "Yorum sayısı alınamıyor",
     "unable_to_get_shared_link": "Paylaşılan bağlantı alınamadı",
     "unable_to_hide_person": "Kişi gizlenemiyor",
-    "unable_to_link_oauth_account": "",
-    "unable_to_load_album": "",
-    "unable_to_load_asset_activity": "",
-    "unable_to_load_items": "",
-    "unable_to_load_liked_status": "",
-    "unable_to_play_video": "",
-    "unable_to_refresh_user": "",
-    "unable_to_remove_album_users": "",
-    "unable_to_remove_api_key": "",
+    "unable_to_link_motion_video": "Hareket videosu bağlanamıyor",
+    "unable_to_link_oauth_account": "OAuth hesabı bağlanamıyor",
+    "unable_to_load_album": "Albüm yüklenemiyor",
+    "unable_to_load_asset_activity": "Varlık aktivitesi yüklenemiyor",
+    "unable_to_load_items": "Öğeler yüklenemiyor",
+    "unable_to_load_liked_status": "Beğenilen durum yüklenemiyor",
+    "unable_to_log_out_all_devices": "Tüm cihazlardan çıkış yapılamıyor",
+    "unable_to_log_out_device": "Cihazdan çıkış yapılamıyor",
+    "unable_to_login_with_oauth": "OAuth ile giriş yapılamıyor",
+    "unable_to_play_video": "Video oynatılamıyor",
+    "unable_to_reassign_assets_existing_person": "Varlıklar {name, select, null {mevcut bir kişiye} other {{name}}} yeniden atanamıyor",
+    "unable_to_reassign_assets_new_person": "Varlıklar yeni bir kişiye yeniden atanamıyor",
+    "unable_to_refresh_user": "Kullanıcı yenilenemiyor",
+    "unable_to_remove_album_users": "Albüm kullanıcıları kaldırılamıyor",
+    "unable_to_remove_api_key": "API anahtarı kaldırılamıyor",
+    "unable_to_remove_assets_from_shared_link": "Varlıklar paylaşılan bağlantıdan kaldırılamıyor",
+    "unable_to_remove_deleted_assets": "Silinmiş varlıklar kaldırılamıyor",
     "unable_to_remove_library": "Kütüphane kaldırılamadı",
-    "unable_to_remove_offline_files": "",
-    "unable_to_remove_partner": "",
-    "unable_to_remove_reaction": "",
+    "unable_to_remove_partner": "Ortak kaldırılamıyor",
+    "unable_to_remove_reaction": "Reaksiyon kaldırılamıyor",
     "unable_to_repair_items": "Ögeler onarılamadı",
-    "unable_to_reset_password": "",
-    "unable_to_resolve_duplicate": "",
-    "unable_to_restore_assets": "",
-    "unable_to_restore_trash": "",
-    "unable_to_restore_user": "",
+    "unable_to_reset_password": "Şifre sıfırlanamıyor",
+    "unable_to_resolve_duplicate": "Çiftler çözümlenemiyor",
+    "unable_to_restore_assets": "Varlıklar geri yüklenemiyor",
+    "unable_to_restore_trash": "Çöp geri yüklenemiyor",
+    "unable_to_restore_user": "Kullanıcı geri yüklenemiyor",
     "unable_to_save_album": "Albüm kaydedilemiyor",
-    "unable_to_save_api_key": "",
+    "unable_to_save_api_key": "API anahtarı kaydedilemiyor",
     "unable_to_save_date_of_birth": "Doğum günü kaydedilemiyor",
     "unable_to_save_name": "İsim kaydedilemyor",
     "unable_to_save_profile": "Profil kaydedilemiyor",
     "unable_to_save_settings": "Ayarlar kaydedilemiyor",
     "unable_to_scan_libraries": "Kütüphaneler taranamıyor",
     "unable_to_scan_library": "Kütüphane taranamıyor",
+    "unable_to_set_feature_photo": "Özellikli fotoğraf ayarlanamıyor",
     "unable_to_set_profile_picture": "Profil resmi ayarlanamıyor",
-    "unable_to_submit_job": "",
-    "unable_to_trash_asset": "",
-    "unable_to_unlink_account": "",
+    "unable_to_submit_job": "Görev gönderilemiyor",
+    "unable_to_trash_asset": "Varlık çöp kutusuna taşınamıyor",
+    "unable_to_unlink_account": "Hesap bağlantısı kaldırılamıyor",
+    "unable_to_unlink_motion_video": "Hareket videosunun bağlantısı kaldırılamıyor",
     "unable_to_update_album_cover": "Albüm resmi güncellenemiyor",
     "unable_to_update_album_info": "Albüm açıklaması güncellenemiyor",
     "unable_to_update_library": "Kütüphane güncellenemiyor",
     "unable_to_update_location": "Konum güncellenemiyor",
     "unable_to_update_settings": "Ayarlar güncellenemiyor",
-    "unable_to_update_timeline_display_status": "",
-    "unable_to_update_user": "",
+    "unable_to_update_timeline_display_status": "Zaman çizelgesi görüntüleme durumu güncellenemiyor",
+    "unable_to_update_user": "Kullanıcı güncellenemiyor",
     "unable_to_upload_file": "Dosya yüklenemiyor"
   },
+  "exif": "EXIF",
   "exit_slideshow": "Slayt gösterisinden çık",
-  "expand_all": "",
-  "expire_after": "",
-  "expired": "",
+  "expand_all": "Hepsini genişlet",
+  "expire_after": "Sonlanma süresi",
+  "expired": "Süresi dolmuş",
+  "expires_date": "{date} tarihinde sona eriyor",
   "explore": "Keşfet",
+  "explorer": "Geçmiş",
   "export": "Dışa Aktar",
   "export_as_json": "JSON olarak Dışa Aktar",
   "extension": "Uzantı",
-  "external": "",
-  "external_libraries": "",
-  "failed_to_get_people": "",
-  "favorite": "",
-  "favorite_or_unfavorite_photo": "",
+  "external": "Harici",
+  "external_libraries": "Harici kütüphaneler",
+  "face_unassigned": "Yüz atanmadı",
+  "failed_to_load_assets": "Varlıklar yüklenemedi",
+  "favorite": "Favori",
+  "favorite_or_unfavorite_photo": "Favoriye ekle veya çıkar",
   "favorites": "Favoriler",
-  "feature_photo_updated": "",
+  "feature_photo_updated": "Özellikli fotoğraf güncellendi",
+  "features": "Özellikler",
+  "features_setting_description": "Uygulamanın özelliklerini yönet",
   "file_name": "Dosya adı",
   "file_name_or_extension": "Dosya adı veya uzantı",
-  "filename": "",
-  "filetype": "",
-  "filter_people": "",
-  "find_them_fast": "",
+  "filename": "Dosya adı",
+  "filetype": "Dosya tipi",
+  "filter_people": "Kişileri filtrele",
+  "find_them_fast": "Adlarına göre hızlıca bul",
   "fix_incorrect_match": "Yanlış eşleştirmeyi düzelt",
-  "force_re-scan_library_files": "Tüm Kütüphane Dosyalarını Yeniden Taramaya Zorla",
-  "forward": "",
+  "folders": "Klasörler",
+  "folders_feature_description": "Dosya sistemindeki fotoğraf ve videoları klasör görünümüyle keşfedin",
+  "forward": "İleri",
   "general": "Genel",
   "get_help": "Yardım Al",
-  "getting_started": "",
+  "getting_started": "Başlarken",
   "go_back": "Geri git",
-  "go_to_search": "",
-  "go_to_share_page": "Paylaşma ekranına git",
-  "group_albums_by": "",
+  "go_to_search": "Aramaya git",
+  "group_albums_by": "Albümleri gruplandır...",
+  "group_no": "Gruplama yok",
+  "group_owner": "Sahibe göre gruplandır",
   "group_year": "Yıla göre grupla",
-  "has_quota": "",
+  "has_quota": "Kota var",
   "hi_user": "Merhaba {name} {email}",
   "hide_all_people": "Tüm kişileri gizle",
-  "hide_gallery": "",
+  "hide_gallery": "Galeriyi gizle",
+  "hide_named_person": "{name} adlı kişiyi gizle",
   "hide_password": "Şifreyi gizle",
-  "hide_person": "",
+  "hide_person": "Kişiyi gizle",
   "hide_unnamed_people": "İsimsiz kişileri gizle",
-  "host": "",
+  "host": "Host",
   "hour": "Saat",
-  "image": "",
-  "immich_logo": "",
-  "immich_web_interface": "",
+  "image": "Resim",
+  "image_alt_text_date": "{isVideo, select, true {Video} other {Fotoğraf}} {date} tarihinde çekildi",
+  "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Fotoğraf}} {person1} ile {date} tarihinde çekildi",
+  "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Fotoğraf}} {person1} ve {person2} ile {date} tarihinde çekildi",
+  "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Fotoğraf}} {person1}, {person2} ve {person3} ile {date} tarihinde çekildi",
+  "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Fotoğraf}} {person1}, {person2} ve diğer {additionalCount, number} kişi ile {date} tarihinde çekildi",
+  "image_alt_text_date_place": "{isVideo, select, true {Video} other {Fotoğraf}} {city}, {country} şehrinde {date} tarihinde çekildi",
+  "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Fotoğraf}} {city}, {country} şehrinde {person1} ile {date} tarihinde çekildi",
+  "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Fotoğraf}} {city}, {country} şehrinde {person1} ve {person2} ile {date} tarihinde çekildi",
+  "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Fotoğraf}} {city}, {country} şehrinde {person1}, {person2} ve {person3} ile {date} tarihinde çekildi",
+  "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Fotoğraf}} {city}, {country} şehrinde {person1}, {person2} ve diğer {additionalCount, number} kişi ile {date} tarihinde çekildi",
+  "immich_logo": "Immich Logosu",
+  "immich_web_interface": "Immich Web Arayüzü",
   "import_from_json": "JSON'dan İçe Aktar",
-  "import_path": "",
+  "import_path": "İçe aktarma yolu",
+  "in_albums": "{count, plural, one {# Albüm} other {# Albümde}}",
   "in_archive": "Arşivde",
   "include_archived": "Arşivlenenleri dahil et",
   "include_shared_albums": "Paylaşılmış albümleri dahil et",
-  "include_shared_partner_assets": "",
-  "individual_share": "",
+  "include_shared_partner_assets": "Paylaşılan ortak varlıkları dahil et",
+  "individual_share": "Bireysel paylaşım",
   "info": "Bilgi",
   "interval": {
-    "day_at_onepm": "",
-    "hours": "",
+    "day_at_onepm": "Her gün saat 13:00'te",
+    "hours": "{hours, plural, one {Her saat} other {Her {hours, number} saatte}}",
     "night_at_midnight": "Her akşam geceyarısında",
-    "night_at_twoam": ""
+    "night_at_twoam": "Her gün gece 2:00'de"
   },
   "invite_people": "Kişileri Davet Et",
   "invite_to_album": "Albüme davet et",
-  "jobs": "",
-  "keep": "",
+  "items_count": "{count, plural, one {# Öğe} other {# Öğe}}",
+  "jobs": "Görevler",
+  "keep": "Koru",
+  "keep_all": "Hepsini koru",
+  "keep_this_delete_others": "Bunu sakla, diğerlerini sil",
+  "kept_this_deleted_others": "Bu varlık tutuldu ve {count, plural, one {# varlık} other {# varlık}} silindi",
   "keyboard_shortcuts": "Klavye kısayolları",
   "language": "Dil",
   "language_setting_description": "Tercih ettiğiniz dili seçiniz",
   "last_seen": "Son görülme",
   "latest_version": "En son versiyon",
-  "leave": "",
-  "let_others_respond": "",
-  "level": "",
+  "latitude": "Enlem",
+  "leave": "Ayrıl",
+  "let_others_respond": "Diğerlerinin yanıt vermesine izin ver",
+  "level": "Seviye",
   "library": "Kütüphane",
   "library_options": "Kütüphane ayarları",
-  "light": "",
-  "link_options": "",
-  "link_to_oauth": "",
-  "linked_oauth_account": "",
-  "list": "",
-  "loading": "",
-  "loading_search_results_failed": "",
+  "light": "Açık",
+  "like_deleted": "Beğeni silindi",
+  "link_motion_video": "Hareket videosunu bağla",
+  "link_options": "Bağlantı seçenekleri",
+  "link_to_oauth": "OAuth'a bağla",
+  "linked_oauth_account": "Bağlı OAuth hesabı",
+  "list": "Liste",
+  "loading": "Yükleniyor",
+  "loading_search_results_failed": "Arama sonuçları yüklenemedi",
   "log_out": "Oturumu kapat",
   "log_out_all_devices": "Tüm Cihazlarda Oturumu Kapat",
   "logged_out_all_devices": "Tüm cihazlarda oturum kapatıldı",
   "logged_out_device": "Oturum kapatılmış cihaz",
-  "login_has_been_disabled": "",
+  "login": "Giriş yap",
+  "login_has_been_disabled": "Giriş devre dışı bırakıldı.",
   "logout_all_device_confirmation": "Tüm cihazlarda oturum kapatmak istediğinizden emin misiniz?",
   "logout_this_device_confirmation": "Bu cihazda oturum kapatmak istediğinizden emin misiniz?",
-  "look": "",
-  "loop_videos": "",
-  "loop_videos_description": "",
-  "make": "",
-  "manage_shared_links": "",
-  "manage_sharing_with_partners": "",
-  "manage_the_app_settings": "",
+  "longitude": "Boylam",
+  "look": "Görünüm",
+  "loop_videos": "Videoları döngüye al",
+  "loop_videos_description": "Ayrıntı görünümünde videoların otomatik döngüye alınmasını etkinleştir.",
+  "main_branch_warning": "Geliştirme sürümü kullanıyorsunuz. Yayınlanan bir sürüm kullanmanızı önemle tavsiye ederiz!",
+  "make": "Marka",
+  "manage_shared_links": "Paylaşılan bağlantıları yönet",
+  "manage_sharing_with_partners": "Ortaklarla paylaşımı yönet",
+  "manage_the_app_settings": "Uygulama ayarlarını yönet",
   "manage_your_account": "Hesabınızı yönetin",
-  "manage_your_api_keys": "",
-  "manage_your_devices": "",
-  "manage_your_oauth_connection": "",
+  "manage_your_api_keys": "API anahtarlarınızı yönetin",
+  "manage_your_devices": "Cihazlarınızı yönetin",
+  "manage_your_oauth_connection": "OAuth bağlantınızı yönetin",
   "map": "Harita",
-  "map_marker_with_image": "",
+  "map_marker_for_images": "{city}, {country} şehrinde çekilen fotoğraflar için harita işaretleyicisi",
+  "map_marker_with_image": "Resimli harita işaretleyicisi",
   "map_settings": "Harita ayarları",
   "matches": "Eşleşenler",
-  "media_type": "",
+  "media_type": "Medya türü",
   "memories": "Anılar",
-  "memories_setting_description": "",
+  "memories_setting_description": "Anılarınızda görmek istediklerinizi yönetin",
   "memory": "Anı",
   "memory_lane_title": "Anılara Yolculuk {title}",
   "menu": "Menü",
   "merge": "Birleştir",
-  "merge_people": "",
+  "merge_people": "Kişileri birleştir",
   "merge_people_limit": "Aynı anda 5 yüzü birleştirebilirsiniz",
+  "merge_people_prompt": "Bu kişileri birleştirmek istiyor musunuz? Bu işlem geri alınamaz.",
   "merge_people_successfully": "Kişiler başarılı bir şekilde birleştirildi",
+  "merged_people_count": "{count, plural, one {# kişi} other {# kişi}} birleştirildi",
   "minimize": "Küçült",
   "minute": "Dakika",
   "missing": "Eksik",
   "model": "Model",
   "month": "Ay",
-  "more": "",
-  "moved_to_trash": "",
+  "more": "Daha fazla",
+  "moved_to_trash": "Çöp kutusuna taşındı",
   "my_albums": "Albümlerim",
   "name": "İsim",
   "name_or_nickname": "İsim veya takma isim",
   "never": "Asla",
+  "new_album": "Yeni albüm",
   "new_api_key": "Yeni API Anahtarı",
   "new_password": "Yeni şifre",
   "new_person": "Yeni kişi",
   "new_user_created": "Yeni kullanıcı oluşturuldu",
   "new_version_available": "YENİ VERSİYON MEVCUT",
-  "newest_first": "",
+  "newest_first": "Önce en yeniler",
   "next": "Sonraki",
-  "next_memory": "",
+  "next_memory": "Sonraki anı",
   "no": "Hayır",
   "no_albums_message": "Fotoğraf ve videolarınızı düzenlemek için yeni bir albüm oluşturun",
   "no_albums_with_name_yet": "Henüz bu isimde bir albümünüz bulunmuyor.",
-  "no_archived_assets_message": "",
+  "no_albums_yet": "Henüz albüm oluşturmadınız.",
+  "no_archived_assets_message": "Fotoğraf görünümünüzden kaldırmak için fotoğrafları ve videoları arşivleyin",
   "no_assets_message": "İLK FOTOĞRAFINIZI YÜKLEMEK İÇİN TIKLAYIN",
-  "no_duplicates_found": "",
-  "no_exif_info_available": "",
-  "no_explore_results_message": "",
-  "no_favorites_message": "",
-  "no_libraries_message": "",
-  "no_name": "",
-  "no_places": "",
-  "no_results": "",
+  "no_duplicates_found": "Çift bulunamadı.",
+  "no_exif_info_available": "EXIF bilgisi mevcut değil",
+  "no_explore_results_message": "Koleksiyonunuzu keşfetmek için daha fazla fotoğraf yükleyin.",
+  "no_favorites_message": "En sevdiğiniz fotoğraf ve videoları hızlıca bulmak için favoriler ekleyin",
+  "no_libraries_message": "Fotoğraf ve videolarınızı görmek için bir harici kütüphane oluşturun",
+  "no_name": "İsim yok",
+  "no_places": "Yer yok",
+  "no_results": "Sonuç bulunamadı",
   "no_results_description": "Eş anlamlı ya da daha genel anlamlı bir kelime deneyin",
-  "no_shared_albums_message": "",
-  "not_in_any_album": "",
-  "note_apply_storage_label_to_previously_uploaded assets": "",
+  "no_shared_albums_message": "Fotoğrafları ve videoları ağınızdaki kişilerle paylaşmak için bir albüm oluşturun",
+  "not_in_any_album": "Hiçbir albümde değil",
+  "note_apply_storage_label_to_previously_uploaded assets": "Not: Daha önce yüklenen varlıklar için bir depolama yolu etiketi uygulamak üzere şunu başlatın",
   "note_unlimited_quota": "Not: Sınırsız kota için 0 yazın",
   "notes": "Notlar",
   "notification_toggle_setting_description": "E-posta bildirimlerine izin ver",
   "notifications": "Bildirimler",
   "notifications_setting_description": "Bildirimleri yönetin",
-  "oauth": "",
-  "offline": "Çevrimdışı",
-  "offline_paths": "",
-  "offline_paths_description": "",
+  "oauth": "OAuth",
+  "official_immich_resources": "Resmi Immich Kaynakları",
+  "offline": "Çevrim dışı",
+  "offline_paths": "Çevrim dışı yollar",
+  "offline_paths_description": "Bu sonuçlar, harici bir kütüphaneye ait olmayan dosyaların elle silinmesinden kaynaklanıyor olabilir.",
   "ok": "Tamam",
-  "oldest_first": "",
-  "onboarding_welcome_user": "Hoşgeldin, {user}",
+  "oldest_first": "Eski olan önce",
+  "onboarding": "Uyum Süreci",
+  "onboarding_privacy_description": "Şu (isteğe bağlı) özellikler harici hizmetlere dayanır ve yönetim ayarlarından herhangi bir zamanda devre dışı bırakılabilir.",
+  "onboarding_theme_description": "İnstance’ınız için bir renk teması seçin. Bunu daha sonra ayarlarınızdan değiştirebilirsiniz.",
+  "onboarding_welcome_description": "Şimdi, instance’ınızı bazı yaygın ayarlarla kurmaya başlayalım.",
+  "onboarding_welcome_user": "Hoş geldin, {user}",
   "online": "Çevrimiçi",
   "only_favorites": "Sadece favoriler",
-  "only_refreshes_modified_files": "",
+  "open_in_map_view": "Harita görünümünde aç",
   "open_in_openstreetmap": "OpenStreetMap'te Aç",
   "open_the_search_filters": "Arama filtrelerini aç",
-  "options": "Ayarlar",
+  "options": "Seçenekler",
   "or": "veya",
   "organize_your_library": "Kütüphanenizi düzenleyin",
   "original": "orijinal",
   "other": "Diğer",
   "other_devices": "Diğer cihazlar",
   "other_variables": "Diğer değişkenler",
-  "owned": "",
-  "owner": "",
+  "owned": "Sahip olunan",
+  "owner": "Sahip",
+  "partner": "Ortak",
   "partner_can_access": "{partner} erişebilir",
   "partner_can_access_assets": "Arşivlenenler ve Silinenler dışındaki tüm fotoğraf ve videolarınız",
   "partner_can_access_location": "Fotoğraf ve videolarınızın çekildiği konum",
-  "partner_sharing": "",
-  "partners": "",
+  "partner_sharing": "Ortak paylaşımı",
+  "partners": "Ortaklar",
   "password": "Şifre",
-  "password_does_not_match": "",
-  "password_required": "",
-  "password_reset_success": "",
+  "password_does_not_match": "Şifreler eşleşmiyor",
+  "password_required": "Şifre gereklidir",
+  "password_reset_success": "Şifre başarıyla sıfırlandı",
   "past_durations": {
-    "days": "",
-    "hours": "",
-    "years": ""
+    "days": "{days, plural, one {Dün} other {Son # gün}}",
+    "hours": "Son {hours, plural, one {saat} other {# saat}}",
+    "years": "{years, plural, one {Geçen yıl} other {Son # yıl}}"
   },
-  "path": "",
-  "pattern": "",
-  "pause": "",
-  "pause_memories": "",
+  "path": "Yol",
+  "pattern": "Desen",
+  "pause": "Duraklat",
+  "pause_memories": "Anıları duraklat",
   "paused": "Durduruldu",
-  "pending": "",
+  "pending": "Beklemede",
   "people": "Kişiler",
-  "people_sidebar_description": "",
-  "permanent_deletion_warning": "",
-  "permanent_deletion_warning_setting_description": "",
+  "people_edits_count": "{count, plural, one {# kişi} other {# kişi}} düzenlendi",
+  "people_feature_description": "Kişilere göre gruplanmış fotoğrafları ve videoları inceleyin",
+  "people_sidebar_description": "Yan panelde kişilere hızlı erişim bağlantısı göster",
+  "permanent_deletion_warning": "Kalıcı silme uyarısı",
+  "permanent_deletion_warning_setting_description": "Nesneleri kalıcı olarak silerken uyarı göster",
   "permanently_delete": "Kalıcı olarak sil",
+  "permanently_delete_assets_count": "{count, plural, one {Dosya} other {Dosyalar}} kalıcı olarak silindi",
+  "permanently_delete_assets_prompt": "Bu {count, plural, one {dosyayı} other {<b>#</b> dosyaları}} kalıcı olarak silmek istediğinizden emin misiniz? Bu işlem {count, plural, one {bu dosyayı} other {bu dosyaları}} albümlerinizden de kaldırır.",
   "permanently_deleted_asset": "Kalıcı olarak silinmiş ögeler",
+  "permanently_deleted_assets_count": "{count, plural, one {# dosya} other {# dosya}} kalıcı olarak silindi",
   "person": "Kişi",
+  "person_hidden": "{name}{hidden, select, true { (gizli)} other {}}",
+  "photo_shared_all_users": "Fotoğraflarınızı tüm kullanıcılarla paylaştınız gibi görünüyor veya paylaşacak kullanıcı bulunmuyor.",
   "photos": "Fotoğraflar",
   "photos_and_videos": "Fotoğraflar & Videolar",
-  "photos_count": "",
+  "photos_count": "{count, plural, one {{count, number} fotoğraf} other {{count, number} fotoğraf}}",
   "photos_from_previous_years": "Önceki yıllardan fotoğraflar",
   "pick_a_location": "Bir konum seçin",
   "place": "Konum",
   "places": "Konumlar",
   "play": "Oynat",
-  "play_memories": "",
-  "play_motion_photo": "",
+  "play_memories": "Anıları oynat",
+  "play_motion_photo": "Hareketli fotoğrafı oynat",
   "play_or_pause_video": "Videoyu oynat ya da durdur",
-  "port": "",
-  "preset": "",
-  "preview": "",
+  "port": "Port",
+  "preset": "Ön ayar",
+  "preview": "Önizleme",
   "previous": "Önceki",
   "previous_memory": "Önceki anı",
   "previous_or_next_photo": "Önceki ya da sonraki fotoğraf",
-  "primary": "",
-  "profile_picture_set": "",
-  "public_share": "",
-  "reaction_options": "",
-  "read_changelog": "",
-  "recent": "",
+  "primary": "Birincil",
+  "privacy": "Gizlilik",
+  "profile_image_of_user": "{user} kullanıcısının profil resmi",
+  "profile_picture_set": "Profil resmi ayarlandı.",
+  "public_album": "Herkese açık albüm",
+  "public_share": "Genel paylaşım",
+  "purchase_account_info": "Destekçi",
+  "purchase_activated_subtitle": "Immich ve açık kaynak yazılıma destek olduğunuz için teşekkür ederiz",
+  "purchase_activated_time": "{date, date} tarihinde etkinleştirildi",
+  "purchase_activated_title": "Anahtarınız başarıyla etkinleştirildi",
+  "purchase_button_activate": "Aktifleştir",
+  "purchase_button_buy": "Satın al",
+  "purchase_button_buy_immich": "Immich satın al",
+  "purchase_button_never_show_again": "Bir daha gösterme",
+  "purchase_button_reminder": "30 gün içinde bana hatırlat",
+  "purchase_button_remove_key": "Anahtarı kaldır",
+  "purchase_button_select": "Seç",
+  "purchase_failed_activation": "Etkinleştirme başarısız oldu! Lütfen e-postadaki ürün anahtarını kontrol edin!",
+  "purchase_individual_description_1": "Bireysel kullanım için",
+  "purchase_individual_description_2": "Destekçi statüsü",
+  "purchase_individual_title": "Bireysel",
+  "purchase_input_suggestion": "Zaten bir ürün anahtarınız var mı? Lütfen aşağıya girin",
+  "purchase_license_subtitle": "Immich'i satın alarak devam eden gelişimini destekleyin",
+  "purchase_lifetime_description": "Ömür boyu geçerli",
+  "purchase_option_title": "SATIN ALMA SEÇENEKLERİ",
+  "purchase_panel_info_1": "Immich'in gelişimi zaman ve çaba gerektiriyor ve tam zamanlı geliştiricilerimiz var. Amacımız, açık kaynak yazılımı sürdürülebilir bir gelir kaynağı haline getirmek.",
+  "purchase_panel_info_2": "Bu satın alma işlemi Immich'te ek işlevsellik açmayacak. Immich'in gelişimini desteklemek için size güveniyoruz.",
+  "purchase_panel_title": "Projeyi destekleyin",
+  "purchase_per_server": "Sunucu başına",
+  "purchase_per_user": "Kullanıcı başına",
+  "purchase_remove_product_key": "Ürün anahtarını kaldır",
+  "purchase_remove_product_key_prompt": "Ürün anahtarını kaldırmak istediğinize emin misiniz?",
+  "purchase_remove_server_product_key": "Sunucu ürün anahtarını kaldır",
+  "purchase_remove_server_product_key_prompt": "Sunucu ürün anahtarını kaldırmak istediğinize emin misiniz?",
+  "purchase_server_description_1": "Tüm sunucu için",
+  "purchase_server_description_2": "Destekçi statüsü",
+  "purchase_server_title": "Sunucu",
+  "purchase_settings_server_activated": "Sunucu ürün anahtarı, yönetici tarafından yönetilir",
+  "rating": "Derecelendirme",
+  "rating_clear": "Derecelendirmeyi temizle",
+  "rating_count": "{count, plural, one {# yıldız} other {# yıldız}}",
+  "rating_description": "EXIF derecelendirmesini bilgi panelinde göster",
+  "reaction_options": "Tepki seçenekleri",
+  "read_changelog": "Değişiklik günlüğünü oku",
+  "reassign": "Yeniden ata",
+  "reassigned_assets_to_existing_person": "{count, plural, one {# dosya} other {# dosya}} {name, select, null {mevcut bir kişiye} other {{name}}} atandı",
+  "reassigned_assets_to_new_person": "{count, plural, one {# dosya} other {# dosya}} yeni bir kişiye atandı",
+  "reassing_hint": "Seçili dosyaları mevcut bir kişiye atayın",
+  "recent": "Son",
+  "recent-albums": "Son kaydedilen albümler",
   "recent_searches": "Son aramalar",
   "refresh": "Yenile",
-  "refreshed": "",
-  "refreshes_every_file": "",
+  "refresh_encoded_videos": "Kodlanmış videoları yenile",
+  "refresh_faces": "Yüzleri yenile",
+  "refresh_metadata": "Meta verileri yenile",
+  "refresh_thumbnails": "Küçük resimleri yenile",
+  "refreshed": "Yenilendi",
+  "refreshes_every_file": "Tüm mevcut ve yeni dosyaları tekrar yükler",
+  "refreshing_encoded_video": "Kodlanmış videolar yenileniyor",
+  "refreshing_faces": "Yüzler yenileniyor",
+  "refreshing_metadata": "Meta veriler yenileniyor",
+  "regenerating_thumbnails": "Küçük resimler yeniden oluşturuluyor",
   "remove": "Kaldır",
-  "remove_from_album": "",
-  "remove_from_favorites": "",
-  "remove_from_shared_link": "",
-  "remove_offline_files": "",
-  "removed_api_key": "",
+  "remove_assets_album_confirmation": "{count, plural, one {# dosyayı} other {# dosyayı}} albümden çıkarmak istediğinizden emin misiniz?",
+  "remove_assets_shared_link_confirmation": "{count, plural, one {# dosyayı} other {# dosyayı}} bu paylaşılan bağlantıdan çıkarmak istediğinizden emin misiniz?",
+  "remove_assets_title": "Dosyaları çıkar?",
+  "remove_custom_date_range": "Özel tarih aralığını kaldır",
+  "remove_deleted_assets": "Çevrimdışı dosyaları kaldır",
+  "remove_from_album": "Albümden çıkar",
+  "remove_from_favorites": "Favorilerden çıkar",
+  "remove_from_shared_link": "Paylaşılan bağlantıdan çıkar",
+  "remove_url": "Bağlantıyı kaldır",
+  "remove_user": "Kullanıcıyı çıkar",
+  "removed_api_key": "API anahtarı {name} kaldırıldı",
+  "removed_from_archive": "Arşivden çıkarıldı",
   "removed_from_favorites": "Favorilerden kaldırıldı",
+  "removed_from_favorites_count": "{count, plural, other {#}} favorilerden çıkarıldı",
+  "removed_tagged_assets": "{count, plural, one {# dosya} other {# dosya}} etiketleri kaldırıldı",
   "rename": "Yeniden adlandır",
   "repair": "Onar",
-  "repair_no_results_message": "",
-  "replace_with_upload": "",
-  "require_password": "",
-  "require_user_to_change_password_on_first_login": "",
-  "reset": "",
-  "reset_password": "",
-  "reset_people_visibility": "",
-  "restore": "",
-  "restore_all": "",
-  "restore_user": "",
+  "repair_no_results_message": "Bulunamayan ve eksik dosyalar burada listelenecektir",
+  "replace_with_upload": "Yükleme ile değiştir",
+  "repository": "Depo",
+  "require_password": "Şifre gerekli",
+  "require_user_to_change_password_on_first_login": "Kullanıcı ilk girişte şifreyi değiştirmeli",
+  "reset": "Sıfırla",
+  "reset_password": "Şifreyi sıfırla",
+  "reset_people_visibility": "Kişilerin görünürlüğünü sıfırla",
+  "reset_to_default": "Varsayılana sıfırla",
+  "resolve_duplicates": "Çiftleri çöz",
+  "resolved_all_duplicates": "Tüm çiftler çözüldü",
+  "restore": "Geri yükle",
+  "restore_all": "Tümünü geri yükle",
+  "restore_user": "Kullanıcıyı geri yükle",
+  "restored_asset": "Dosya geri yüklendi",
   "resume": "Devam et",
   "retry_upload": "Yeniden yüklemeyi dene",
-  "review_duplicates": "",
-  "role": "",
+  "review_duplicates": "Çiftleri gözden geçir",
+  "role": "Rol",
+  "role_editor": "Düzenleyici",
+  "role_viewer": "Görüntüleyici",
   "save": "Kaydet",
-  "saved_api_key": "",
-  "saved_profile": "",
-  "saved_settings": "",
+  "saved_api_key": "API anahtarı kaydedildi",
+  "saved_profile": "Profil kaydedildi",
+  "saved_settings": "Kaydedilen ayarlar",
   "say_something": "Bir şey söyle",
   "scan_all_libraries": "Tüm Kütüphaneleri Tara",
-  "scan_all_library_files": "Tüm Kütüphaneleri Yeniden Tara",
-  "scan_new_library_files": "",
+  "scan_library": "Kütüphaneyi tara",
   "scan_settings": "Ayarları Tara",
   "scanning_for_album": "Albüm için taranıyor...",
   "search": "Ara",
-  "search_albums": "",
-  "search_by_context": "",
-  "search_camera_make": "",
-  "search_camera_model": "",
-  "search_city": "",
-  "search_country": "",
-  "search_for_existing_person": "",
+  "search_albums": "Albüm ara",
+  "search_by_context": "Bağlama göre ara",
+  "search_by_filename": "Dosya adına göre ara",
+  "search_by_filename_example": "Örn. IMG_1234.JPG veya PNG",
+  "search_camera_make": "Kamera markasına göre ara...",
+  "search_camera_model": "Kamera modeline göre ara...",
+  "search_city": "Şehre göre ara...",
+  "search_country": "Ülkeye göre ara...",
+  "search_for_existing_person": "Mevcut bir kişiyi ara",
+  "search_no_people": "Kişi yok",
   "search_no_people_named": "\"{name}\" isimli bir kişi yok",
-  "search_people": "",
-  "search_places": "",
-  "search_state": "",
-  "search_timezone": "",
+  "search_options": "Arama seçenekleri",
+  "search_people": "Kişilere göre ara",
+  "search_places": "Yerleri ara",
+  "search_settings": "Ayarları ara",
+  "search_state": "Eyalet/İl ara...",
+  "search_tags": "Etiketleri ara...",
+  "search_timezone": "Saat dilimi ara...",
   "search_type": "Arama türü",
   "search_your_photos": "Fotoğraflarınızı arayın",
-  "searching_locales": "",
-  "second": "",
+  "searching_locales": "Yerleri arıyor...",
+  "second": "Saniye",
   "see_all_people": "Tüm kişileri gör",
-  "select_album_cover": "",
-  "select_all": "",
-  "select_avatar_color": "",
-  "select_face": "",
-  "select_featured_photo": "",
+  "select_album_cover": "Albüm kapağı seç",
+  "select_all": "Tümünü seç",
+  "select_all_duplicates": "Tüm çiftleri seç",
+  "select_avatar_color": "Avatar rengini seç",
+  "select_face": "Yüzü seç",
+  "select_featured_photo": "Öne çıkan fotoğrafı seç",
   "select_from_computer": "Bilgisayardan seç",
-  "select_keep_all": "",
+  "select_keep_all": "Hepsini sakla",
   "select_library_owner": "Kütüphane sahibini seç",
-  "select_new_face": "",
-  "select_photos": "",
-  "select_trash_all": "",
-  "selected": "",
-  "send_message": "",
-  "send_welcome_email": "",
-  "server": "",
-  "server_stats": "",
-  "set": "",
+  "select_new_face": "Yeni yüz seç",
+  "select_photos": "Fotoğrafları seç",
+  "select_trash_all": "Hepsini çöpe at",
+  "selected": "Seçildi",
+  "selected_count": "{count, plural, other {# seçildi}}",
+  "send_message": "Mesaj gönder",
+  "send_welcome_email": "Hoş geldin e-postası gönder",
+  "server_offline": "Sunucu çevrimdışı",
+  "server_online": "Sunucu çevrimiçi",
+  "server_stats": "Sunucu istatistikleri",
+  "server_version": "Sunucu versiyonu",
+  "set": "Ayarla",
   "set_as_album_cover": "Albüm resmi olarak ayarla",
   "set_as_profile_picture": "Profil resmi olarak ayarla",
-  "set_date_of_birth": "",
-  "set_profile_picture": "",
-  "set_slideshow_to_fullscreen": "",
+  "set_date_of_birth": "Doğum tarihini ayarla",
+  "set_profile_picture": "Profil resmini ayarla",
+  "set_slideshow_to_fullscreen": "Slayt gösterisini tam ekran yap",
   "settings": "Ayarlar",
   "settings_saved": "Ayarlar kaydedildi",
   "share": "Paylaş",
-  "shared": "",
-  "shared_by": "",
+  "shared": "Paylaşılan",
+  "shared_by": "Tarafından paylaşılan",
   "shared_by_user": "{user} tarafından paylaşıldı",
-  "shared_by_you": "",
-  "shared_from_partner": "",
-  "shared_links": "",
-  "shared_photos_and_videos_count": "",
+  "shared_by_you": "Senin tarafından paylaşıldı",
+  "shared_from_partner": "{partner} tarafından paylaşılan fotoğraflar",
+  "shared_link_options": "Paylaşılan bağlantı seçenekleri",
+  "shared_links": "Paylaşılan bağlantılar",
+  "shared_photos_and_videos_count": "{assetCount, plural, one {# paylaşılan fotoğraf veya video.} other {# paylaşılan fotoğraf & video.}}",
   "shared_with_partner": "{partner} ile paylaşıldı",
   "sharing": "Paylaşılıyor",
   "sharing_enter_password": "Bu sayfayı görebilmek için lütfen şifreyi giriniz.",
-  "sharing_sidebar_description": "",
+  "sharing_sidebar_description": "Yan panelde paylaşılanlara kısa yol göster",
+  "shift_to_permanent_delete": "Dosyayı kalıcı olarak silmek için ⇧ tuşuna basın",
   "show_album_options": "Albüm ayarlarını göster",
+  "show_albums": "Albümleri göster",
   "show_all_people": "Tüm kişileri göster",
-  "show_and_hide_people": "",
-  "show_file_location": "",
-  "show_gallery": "",
-  "show_hidden_people": "",
-  "show_in_timeline": "",
-  "show_in_timeline_setting_description": "",
+  "show_and_hide_people": "Kişileri göster ve gizle",
+  "show_file_location": "Dosya konumunu göster",
+  "show_gallery": "Galeriyi göster",
+  "show_hidden_people": "Gizli kişileri göster",
+  "show_in_timeline": "Zaman çizelgesinde göster",
+  "show_in_timeline_setting_description": "Bu kullanıcının fotoğraf ve videolarını zaman çizelgenizde göster",
   "show_keyboard_shortcuts": "Klavye kısayollarını göster",
-  "show_metadata": "",
-  "show_or_hide_info": "",
+  "show_metadata": "Meta verileri göster",
+  "show_or_hide_info": "Bilgiyi göster veya gizle",
   "show_password": "Şifreyi göster",
   "show_person_options": "Kişi ayarlarını göster",
-  "show_progress_bar": "",
+  "show_progress_bar": "İlerleme çubuğunu göster",
   "show_search_options": "Arama ayarlarını göster",
+  "show_slideshow_transition": "Slayt geçişini göster",
+  "show_supporter_badge": "Destekçi rozeti",
+  "show_supporter_badge_description": "Destekçi rozetini göster",
   "shuffle": "Karıştır",
+  "sidebar": "Yan panel",
+  "sidebar_display_description": "Yan panelde görünüme kısa yol göster",
   "sign_out": "Oturumu Kapat",
   "sign_up": "Kaydol",
-  "size": "",
-  "skip_to_content": "",
+  "size": "Boyut",
+  "skip_to_content": "İçeriğe atla",
+  "skip_to_folders": "Klasörlere atla",
+  "skip_to_tags": "Etiketlere atla",
   "slideshow": "Slayt gösteriisi",
   "slideshow_settings": "Slayt gösterisi ayarları",
-  "sort_albums_by": "",
+  "sort_albums_by": "Albümleri sırala...",
   "sort_created": "Oluşturulma tarihi",
+  "sort_items": "Öğe sayısı",
+  "sort_modified": "Değişiklik tarihi",
+  "sort_oldest": "En eski fotoğraf",
+  "sort_recent": "En yeni fotoğraf",
   "sort_title": "Başlık",
   "source": "Kaynak",
-  "stack": "",
-  "stack_selected_photos": "",
-  "stacktrace": "",
-  "start": "",
-  "start_date": "",
-  "state": "",
+  "stack": "Yığın",
+  "stack_duplicates": "Çiftleri yığınla",
+  "stack_select_one_photo": "Yığın için ana fotoğrafı seç",
+  "stack_selected_photos": "Seçili fotoğrafları yığınla",
+  "stacked_assets_count": "{count, plural, one {# dosya} other {# dosya}} yığınlandı",
+  "stacktrace": "Yığın izi",
+  "start": "Başlat",
+  "start_date": "Başlangıç tarihi",
+  "state": "Eyalet/İl",
   "status": "Durum",
-  "stop_motion_photo": "",
-  "stop_photo_sharing": "",
-  "stop_photo_sharing_description": "",
+  "stop_motion_photo": "Hareketli fotoğrafı durdur",
+  "stop_photo_sharing": "Fotoğraflarınızı paylaşmayı durdurmak mı istiyorsunuz?",
+  "stop_photo_sharing_description": "{partner} artık fotoğraflarınıza erişemeyecek.",
   "stop_sharing_photos_with_user": "Bu kullanıcı ile fotoğraflarınızı paylaşmayı durdurun",
   "storage": "Depolama alanı",
-  "storage_label": "",
-  "storage_usage": "",
-  "submit": "",
+  "storage_label": "Depolama yolu",
+  "storage_usage": "{used} / {available} kullanıldı",
+  "submit": "Gönder",
   "suggestions": "Öneriler",
   "sunrise_on_the_beach": "Plajda gün doğumu",
-  "swap_merge_direction": "",
-  "sync": "",
+  "support": "Destek",
+  "support_and_feedback": "Destek & Geri Bildirim",
+  "support_third_party_description": "Immich kurulumu üçüncü bir tarafça yapıldı. Yaşadığınız sorunlar bu paketle ilgili olabilir. Lütfen öncelikli olarak aşağıdaki bağlantıları kullanarak bu sağlayıcıyla iletişime geçin.",
+  "swap_merge_direction": "Birleştirme yönünü değiştir",
+  "sync": "Senkronize et",
+  "tag": "Etiket",
+  "tag_assets": "Dosyaları etiketle",
+  "tag_created": "Etiket oluşturuldu: {tag}",
+  "tag_feature_description": "Etiket temalarına göre gruplandırılmış fotoğraf ve videoları keşfedin",
+  "tag_not_found_question": "Etiket bulunamadı mı? <link>Yeni bir etiket oluşturun.</link>",
+  "tag_updated": "Etiket güncellendi: {tag}",
+  "tagged_assets": "{count, plural, one {# dosya} other {# dosya}} etiketlendi",
+  "tags": "Etiketler",
   "template": "Şablon",
   "theme": "Tema",
   "theme_selection": "Tema seçimi",
   "theme_selection_description": "Temayı otomatik olarak tarayıcınızın sistem tercihine göre açık veya koyu ayarlayın",
-  "time_based_memories": "",
+  "they_will_be_merged_together": "Birlikte birleştirilecekler",
+  "third_party_resources": "Üçüncü taraf kaynaklar",
+  "time_based_memories": "Zaman bazlı anılar",
+  "timeline": "Zaman Çizelgesi",
   "timezone": "Zaman dilimi",
   "to_archive": "Arşivle",
   "to_change_password": "Şifreyi değiştir",
-  "to_favorite": "",
+  "to_favorite": "Favorilere ekle",
   "to_login": "Oturum aç",
-  "to_trash": "",
-  "toggle_settings": "",
-  "toggle_theme": "",
-  "toggle_visibility": "",
-  "total_usage": "",
+  "to_parent": "Üst öğeye git",
+  "to_trash": "Çöpe taşı",
+  "toggle_settings": "Ayarları değiştir",
+  "toggle_theme": "Tema değiştir",
+  "total": "Toplam",
+  "total_usage": "Toplam kullanım",
   "trash": "Çöp",
-  "trash_all": "",
+  "trash_all": "Hepsini sil",
+  "trash_count": "Çöp kutusu {count, number}",
   "trash_delete_asset": "Ögeyi Sil/Çöpe gönder",
-  "trash_no_results_message": "",
-  "trashed_items_will_be_permanently_deleted_after": "",
-  "type": "",
+  "trash_no_results_message": "Silinen fotoğraf ve videolar burada listelenecektir.",
+  "trashed_items_will_be_permanently_deleted_after": "Silinen öğeler {days, plural, one {# gün} other {# gün}} sonra kalıcı olarak silinecek.",
+  "type": "Tür",
   "unarchive": "Arşivden çıkar",
-  "unarchived": "",
-  "unfavorite": "",
-  "unhide_person": "",
+  "unarchived_count": "{count, plural, other {# arşivden çıkarıldı}}",
+  "unfavorite": "Favorilerden kaldır",
+  "unhide_person": "Kişiyi göster",
   "unknown": "Bilinmeyen",
-  "unknown_album": "",
   "unknown_year": "Bilinmeyen YIl",
   "unlimited": "Sınırsız",
-  "unlink_oauth": "",
-  "unlinked_oauth_account": "",
+  "unlink_motion_video": "Hareketli video bağlantısını kaldır",
+  "unlink_oauth": "OAuth bağlantısını kaldır",
+  "unlinked_oauth_account": "Bağlantısı kaldırılmış OAuth hesabı",
   "unnamed_album": "İsimsiz Albüm",
-  "unselect_all": "",
-  "unstack": "",
-  "untracked_files": "",
-  "untracked_files_decription": "",
-  "up_next": "",
+  "unnamed_album_delete_confirmation": "Bu albümü silmek istediğinizden emin misiniz?",
+  "unnamed_share": "İsimsiz paylaşım",
+  "unsaved_change": "Kaydedilmemiş değişiklik",
+  "unselect_all": "Tümünü seçimini kaldır",
+  "unselect_all_duplicates": "Tüm çiftlerin seçimini kaldır",
+  "unstack": "Yığını kaldır",
+  "unstacked_assets_count": "{count, plural, one {# dosya} other {# dosya}} yığını kaldırıldı",
+  "untracked_files": "İzlenmeyen dosyalar",
+  "untracked_files_decription": "Bu dosyalar uygulama tarafından izlenmiyor. Başarısız taşıma işlemlerinin, kesintiye uğrayan yüklemelerin sonuçları olabilir veya bir hata nedeniyle geride kalmış olabilirler",
+  "up_next": "Sıradaki",
   "updated_password": "Şifreyi güncelle",
   "upload": "Yükle",
-  "upload_concurrency": "",
+  "upload_concurrency": "Yükleme eşzamanlılığı",
+  "upload_errors": "{count, plural, one {# hata} other {# hatayla}} yükleme tamamlandı, yeni yüklenen dosyaları görmek için sayfayı güncelleyin.",
+  "upload_progress": "{remaining, number} kalan - {processed, number}/{total, number} işlendi",
+  "upload_skipped_duplicates": "{count, plural, one {# çift dosya} other {# çift dosya}} atlandı",
+  "upload_status_duplicates": "Çiftler",
   "upload_status_errors": "Hatalar",
+  "upload_status_uploaded": "Yüklendi",
   "upload_success": "Yükleme başarılı, yüklenen yeni ögeleri görebilmek için sayfayı yenileyin.",
-  "url": "",
-  "usage": "",
+  "url": "URL",
+  "usage": "Kullanım",
+  "use_custom_date_range": "Bunun yerine özel tarih aralığını kullan",
   "user": "Kullanıcı",
-  "user_id": "",
-  "user_license_settings": "Lisans",
+  "user_id": "Kullanıcı ID",
+  "user_liked": "{type, select, photo {Bu fotoğraf} video {Bu video} asset {Bu dosya} other {Bu}} {user} tarafından beğenildi",
+  "user_purchase_settings": "Satın Alma",
+  "user_purchase_settings_description": "Satın alma işlemlerini yönet",
+  "user_role_set": "{user}, {role} olarak ayarlandı",
   "user_usage_detail": "Kullanıcı kullanım detayı",
+  "user_usage_stats": "Hesap kullanım istatistikleri",
+  "user_usage_stats_description": "hesap kullanım istatistiklerini göster",
   "username": "Kullanıcı adı",
   "users": "Kullanıcılar",
-  "utilities": "",
-  "validate": "",
-  "variables": "",
+  "utilities": "Yardımcılar",
+  "validate": "Doğrula",
+  "variables": "Değişkenler",
   "version": "Versiyon",
   "version_announcement_closing": "Arkadaşınız, Alex",
+  "version_announcement_message": "Merhaba! Immich'in yeni bir sürümü mevcut. Lütfen yapılandırmanızın güncel olduğundan emin olmak için <link>sürüm notlarını</link> okumak için biraz zaman ayırın, özellikle WatchTower veya Immich kurulumunuzu otomatik olarak güncelleyen bir mekanizma kullanıyorsanız yanlış yapılandırmaların önüne geçmek adına bu önemlidir.",
+  "version_history": "Versiyon geçmişi",
+  "version_history_item": "{version}, {date} tarihinde kuruldu",
   "video": "Video",
-  "video_hover_setting": "",
-  "video_hover_setting_description": "",
+  "video_hover_setting": "Üzerinde durulduğunda video önizlemesi oynat",
+  "video_hover_setting_description": "Öğe üzerinde fareyle durulduğunda video küçük resmini oynatır. Bu özellik devre dışıyken, oynatma simgesine fareyle gidilerek oynatma başlatılabilir.",
   "videos": "Videolar",
-  "videos_count": "",
+  "videos_count": "{count, plural, one {# video} other {# video}}",
   "view": "Görüntüle",
   "view_album": "Albümü görüntüle",
-  "view_all": "",
+  "view_all": "Tümünü gör",
   "view_all_users": "Tüm kullanıcıları görüntüle",
-  "view_links": "",
-  "view_next_asset": "",
-  "view_previous_asset": "",
-  "viewer": "",
+  "view_in_timeline": "Zaman çizelgesinde görüntüle",
+  "view_links": "Bağlantıları göster",
+  "view_name": "Göster",
+  "view_next_asset": "Sonraki dosyayı görüntüle",
+  "view_previous_asset": "Önceki dosyayı görüntüle",
+  "view_stack": "Yığını görüntüle",
+  "visibility_changed": "Görünürlük {count, plural, one {# kişi} other {# kişi}} için değiştirildi",
   "waiting": "Bekleniyor",
   "warning": "Uyarı",
   "week": "Hafta",
-  "welcome": "Hoşgeldiniz",
-  "welcome_to_immich": "Immich'e hoşgeldiniz",
+  "welcome": "Hoş geldiniz",
+  "welcome_to_immich": "Immich'e hoş geldiniz",
   "year": "Yıl",
+  "years_ago": "{years, plural, one {bir yıl} other {# yıl}} önce",
   "yes": "Evet",
-  "you_dont_have_any_shared_links": "",
-  "zoom_image": ""
+  "you_dont_have_any_shared_links": "Herhangi bir paylaşılan bağlantınız yok",
+  "zoom_image": "Görüntüyü yakınlaştır"
 }
diff --git a/web/src/lib/i18n/uk.json b/i18n/uk.json
similarity index 90%
rename from web/src/lib/i18n/uk.json
rename to i18n/uk.json
index 64411ef758..806ae394f8 100644
--- a/web/src/lib/i18n/uk.json
+++ b/i18n/uk.json
@@ -23,16 +23,23 @@
   "add_to": "Додати у...",
   "add_to_album": "Додати у альбом",
   "add_to_shared_album": "Додати у спільний альбом",
+  "add_url": "Додати URL",
   "added_to_archive": "Додано до архіву",
   "added_to_favorites": "Додано до обраного",
   "added_to_favorites_count": "Додано {count, number} до обраного",
   "admin": {
     "add_exclusion_pattern_description": "Додайте шаблони виключень. Підстановка з використанням *, ** та ? підтримується. Для ігнорування всіх файлів у будь-якому каталозі з ім'ям «Raw», використовуйте \"**/Raw/**\". Для ігнорування всіх файлів, що закінчуються на \".tif\", використовуйте \"**/*.tif\". Для ігнорування абсолютного шляху використовуйте \"/path/to/ignore/**\".",
+    "asset_offline_description": "Цей зовнішній бібліотечний актив більше не знайдено на диску і був переміщений до кошика. Якщо файл був переміщений у межах бібліотеки, перевірте свій таймлайн на наявність нового відповідного активу. Щоб відновити цей актив, переконайтеся, що шлях файлу нижче доступний для Immich, і проскануйте бібліотеку.",
     "authentication_settings": "Налаштування аутентифікації",
     "authentication_settings_description": "Управління паролями, OAuth та іншими налаштуваннями аутентифікації",
     "authentication_settings_disable_all": "Ви впевнені, що хочете вимкнути всі методи входу? Вхід буде повністю вимкнений.",
     "authentication_settings_reenable": "Для повторного ввімкнення використовуйте <link>Команду сервера</link>.",
     "background_task_job": "Фонові Завдання",
+    "backup_database": "Резервна копія бази даних",
+    "backup_database_enable_description": "Увімкнути резервне копіювання бази даних",
+    "backup_keep_last_amount": "Кількість резервних копій для зберігання",
+    "backup_settings": "Налаштування резервного копіювання",
+    "backup_settings_description": "Керування налаштуваннями резервного копіювання бази даних",
     "check_all": "Перевірити все",
     "cleared_jobs": "Очищені завдання для: {job}",
     "config_set_by_file": "Налаштовано за допомогою конфіг-файлу",
@@ -41,35 +48,40 @@
     "confirm_email_below": "Для підтвердження введіть \"{email}\" нижче",
     "confirm_reprocess_all_faces": "Ви впевнені, що хочете повторно визначити всі обличчя? Це також призведе до видалення імен з усіх облич.",
     "confirm_user_password_reset": "Ви впевнені, що хочете скинути пароль користувача {user}?",
-    "crontab_guru": "",
+    "create_job": "Створити завдання",
+    "cron_expression": "Cron вираз",
+    "cron_expression_description": "Встановіть інтервал сканування, використовуючи формат cron. Для отримання додаткової інформації зверніться до напр. <link>Crontab Guru</link>",
+    "cron_expression_presets": "Попередні налаштування cron виразів",
     "disable_login": "Вимкнути вхід",
-    "disabled": "",
     "duplicate_detection_job_description": "Запустити машинне навчання на активах для виявлення схожих зображень. Залежить від інтелектуального пошуку",
     "exclusion_pattern_description": "Шаблони виключень дозволяють ігнорувати файли та папки під час сканування вашої бібліотеки. Це корисно, якщо у вас є папки, які містять файли, які ви не хочете імпортувати, наприклад, RAW-файли.",
     "external_library_created_at": "Зовнішня бібліотека (створена {date})",
     "external_library_management": "Керування зовнішніми бібліотеками",
     "face_detection": "Виявлення обличчя",
-    "face_detection_description": "Виявлення обличчя на активах з використанням машинного навчання. Для відео розглядається лише ескіз. Опція \"Усі\" повторно обробляє всі активи. Опція \"Відсутні\" ставить в чергу активи, які ще не були оброблені. Виявлені обличчя будуть поставлені в чергу для визначення обличчя після завершення виявлення обличчя, групуючи їх в існуючих або нових людей.",
-    "facial_recognition_job_description": "Групувати виявлені обличчя у людей. Цей крок виконується після завершення виявлення обличчя. Опція \"Усі\" перегруповує всі обличчя. Опція \"Відсутні\" ставить в чергу обличчя, які ще не мають призначеної особи.",
+    "face_detection_description": "Виявлення облич на медіафайлах за допомогою машинного навчання. Для відео обробляється лише ескіз. \"Оновити\" повторно обробляє всі файли. \"Скинути\" додатково очищає всі поточні дані про обличчя. \"Відсутні\" ставить у чергу файли, які ще не були оброблені. Виявлені обличчя будуть поставлені в чергу для розпізнавання після завершення виявлення, групуючи їх у вже існуючих або нових людей.",
+    "facial_recognition_job_description": "Групування виявлених облич у людей. Цей крок виконується після завершення виявлення облич. \"Скинути\" повторно кластеризує всі обличчя. \"Відсутні\" ставить у чергу обличчя, яким ще не призначено людину.",
     "failed_job_command": "Команда {command} не виконалася для завдання: {job}",
     "force_delete_user_warning": "ПОПЕРЕДЖЕННЯ: Це негайно призведе до видалення користувача і всіх активів. Цю дію не можна скасувати, і файли не можна буде відновити.",
     "forcing_refresh_library_files": "Примусове оновлення всіх файлів бібліотеки",
+    "image_format": "Формат",
     "image_format_description": "Формат WebP виробляє меньші файлів, ніж JPEG, але його кодування вимагає більше часу.",
     "image_prefer_embedded_preview": "Надати перевагу вбудованому перегляду",
     "image_prefer_embedded_preview_setting_description": "Використовуйте вбудовані попередні перегляди у RAW фотографіях як вхідні дані для обробки зображень, коли це можливо. Це може забезпечити більш точні кольори для деяких зображень, але якість попереднього перегляду залежить від камери та зображення можуть мати більше артефактів стиснення.",
     "image_prefer_wide_gamut": "Віддають перевагу широкій гамі",
-    "image_prefer_wide_gamut_setting_description": "Для ескізів використовуйте дисплей P3. Це краще зберігає яскравість зображень з широким колірним простором, але на старих пристроях зі старою версією браузера зображення можуть виглядати інакше. sRGB-зображення зберігаються у форматі sRGB, щоб уникнути зсуву кольорів.",
-    "image_preview_format": "Формат прев'ю",
-    "image_preview_resolution": "Роздільність прев'ю",
-    "image_preview_resolution_description": "Використовується при перегляді окремої фотографії та для машинного навчання. Вища роздільність може зберігати більше деталей, але потребує більше часу на кодування, має більший розмір файлу і може знижувати реакцію програми.",
+    "image_prefer_wide_gamut_setting_description": "Для мініатюр використовуйте дисплей P3. Це краще зберігає яскравість зображень з широким колірним простором, але на старих пристроях зі старою версією браузера зображення можуть виглядати інакше. sRGB-зображення зберігаються у форматі sRGB, щоб уникнути зсуву кольорів.",
+    "image_preview_description": "Зображення середнього розміру з видаленими метаданими, яке використовується при перегляді одного об'єкта та для машинного навчання",
+    "image_preview_quality_description": "Якість попереднього перегляду від 1 до 100. Вища оцінка означає кращу якість, але створює більші файли та може зменшити швидкість роботи програми. Встановлення низького значення може вплинути на якість машинного навчання.",
+    "image_preview_title": "Налаштування попереднього перегляду",
     "image_quality": "Якість",
-    "image_quality_description": "Якість зображення від 1 до 100. Чим вище, тим краще якість, але створюються більші файли, цей параметр впливає на прев'ю і мініатюри зображень.",
+    "image_resolution": "Роздільність",
+    "image_resolution_description": "Вища роздільність може зберігати більше деталей, але займає більше часу для кодування, має більші розміри файлів і може зменшити швидкість роботи програми.",
     "image_settings": "Налаштування зображення",
     "image_settings_description": "Керуйте якістю та роздільною здатністю згенерованих зображень",
-    "image_thumbnail_format": "Формат ескізу",
-    "image_thumbnail_resolution": "Розмір ескізу",
-    "image_thumbnail_resolution_description": "Використовується при перегляді груп фотографій (основна стрічка, перегляд альбому тощо). Вища роздільна здатність може зберегти більше деталей, але вимагає більше часу для кодування, має більший розмір файлів і може знижувати чутливість додатку.",
+    "image_thumbnail_description": "Маленька мініатюра із видаленими метаданими, що використовується для перегляду груп фотографій, наприклад, на основній лінії часу",
+    "image_thumbnail_quality_description": "Якість мініатюри від 1 до 100. Вища оцінка означає кращу якість, але створює більші файли та може зменшити швидкість роботи програми.",
+    "image_thumbnail_title": "Налаштування мініатюр",
     "job_concurrency": "{job} одночасно",
+    "job_created": "Завдання створено",
     "job_not_concurrency_safe": "Це завдання не є безпечним для одночасного виконання.",
     "job_settings": "Налаштування завдань",
     "job_settings_description": "Управління паралельністю завдань",
@@ -77,9 +89,6 @@
     "jobs_delayed": "{jobCount, plural, other {# відкладено}}",
     "jobs_failed": "{jobCount, plural, other {# не вдалося}}",
     "library_created": "Створена бібліотека: {library}",
-    "library_cron_expression": "Вираз Cron",
-    "library_cron_expression_description": "Встановіть інтервал сканування за допомогою формату Cron. Для отримання додаткової інформації дивіться, наприклад, на <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Шаблони виразів Cron",
     "library_deleted": "Бібліотеку видалено",
     "library_import_path_description": "Вкажіть папку для імпорту. Цю папку, включно з підпапками, буде проскановано на наявність зображень і відео.",
     "library_scanning": "Періодичне сканування",
@@ -122,7 +131,7 @@
     "machine_learning_smart_search_description": "Пошук зображень за допомогою семантичних вбудовувань CLIP",
     "machine_learning_smart_search_enabled": "Увімкнути розумний пошук",
     "machine_learning_smart_search_enabled_description": "Якщо ця функція вимкнена, зображення не будуть кодуватися для розумного пошуку.",
-    "machine_learning_url_description": "URL сервера машинного навчання",
+    "machine_learning_url_description": "URL сервера машинного навчання. Якщо надано більше одного URL, сервери будуть опитуватися по черзі, поки один з них не відповість успішно, у порядку від першого до останнього.",
     "manage_concurrency": "Керування паралельністю завдань",
     "manage_log_settings": "Керування налаштуваннями журналу",
     "map_dark_style": "Темний стиль",
@@ -152,7 +161,7 @@
     "note_cannot_be_changed_later": "ПРИМІТКА: Це не можна змінити пізніше!",
     "note_unlimited_quota": "Примітка: Введіть 0 для необмеженого обсягу квоти",
     "notification_email_from_address": "З адреси",
-    "notification_email_from_address_description": "Адреса електронної пошти відправника, наприклад: \"Immich Photo Server <noreply@immich.app>\"",
+    "notification_email_from_address_description": "Адреса електронної пошти відправника, наприклад: \"Immich Photo Server <noreply@example.com>\"",
     "notification_email_host_description": "Хост поштового сервера (наприклад, smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Ігнорувати помилки сертифіката",
     "notification_email_ignore_certificate_errors_description": "Ігнорувати помилки перевірки сертифікатів TLS (не рекомендується)",
@@ -198,22 +207,24 @@
     "password_settings": "Налаштування входу з паролем",
     "password_settings_description": "Керування налаштуваннями входу за паролем",
     "paths_validated_successfully": "Усі шляхи успішно перевірено",
+    "person_cleanup_job": "Очищення особи",
     "quota_size_gib": "Розмір квоти (GiB)",
     "refreshing_all_libraries": "Оновлення всіх бібліотек",
     "registration": "Реєстрація адміністратора",
     "registration_description": "Оскільки ви перший користувач в системі, ви будете призначені Адміністратором і відповідатимете за адміністративні завдання, а додаткові користувачі будуть створені вами.",
-    "removing_offline_files": "Видалення недоступних файлів",
     "repair_all": "Відремонтуйте все",
     "repair_matched_items": "Відповідає {count, plural, one {# елемент} few {# елементи} many {# елементів} other {# елементів}}",
     "repaired_items": "Відновлено {count, plural, one {# елемент} few {# елементи} many {# елементів} other {# елементів}}",
     "require_password_change_on_login": "Вимагати зміни пароля користувача при першому вході",
     "reset_settings_to_default": "Скинути налаштування до заводських значень",
     "reset_settings_to_recent_saved": "Скинути налаштування до недавно збережених налаштувань",
-    "scanning_library_for_changed_files": "Сканування бібліотеки на наявність змінених файлів",
-    "scanning_library_for_new_files": "Сканування бібліотеки на наявність нових файлів",
+    "scanning_library": "Сканування бібліотеки",
+    "search_jobs": "Пошук завдань...",
     "send_welcome_email": "Надіслати лист з вітанням",
     "server_external_domain_settings": "Зовнішній домен",
     "server_external_domain_settings_description": "Домен для публічних загальнодоступних посилань, включаючи http(s)://",
+    "server_public_users": "Публічні користувачі",
+    "server_public_users_description": "Усі користувачі (ім'я та електронна пошта) відображаються під час додавання користувача до спільних альбомів. Якщо вимкнено, список користувачів буде доступний лише адміністраторам.",
     "server_settings": "Налаштування сервера",
     "server_settings_description": "Керування налаштуваннями сервера",
     "server_welcome_message": "Вітальне повідомлення",
@@ -238,14 +249,24 @@
     "storage_template_settings_description": "Керуйте структурою тек та іменем завантаженого файлу",
     "storage_template_user_label": "<code>{label}</code> - це мітка зберігання користувача",
     "system_settings": "Системні налаштування",
+    "tag_cleanup_job": "Очистити тег",
+    "template_email_available_tags": "Ви можете використовувати наступні змінні у вашому шаблоні: {tags}",
+    "template_email_if_empty": "Якщо шаблон порожній, буде використано стандартний ел. лист.",
+    "template_email_invite_album": "Шаблон запрошення до альбому",
+    "template_email_preview": "Попередній перегляд",
+    "template_email_settings": "Шаблони ел. листів",
+    "template_email_settings_description": "Керувати шаблонами сповіщень ел. пошти",
+    "template_email_update_album": "Оновити шаблон альбому",
+    "template_email_welcome": "Шаблон вітального ел. листа",
+    "template_settings": "Шаблони сповіщень",
+    "template_settings_description": "Керувати шаблонами для сповіщень.",
     "theme_custom_css_settings": "Власний CSS",
     "theme_custom_css_settings_description": "Каскадні таблиці стилів дозволяють настроювати дизайн Immich.",
     "theme_settings": "Налаштування теми",
     "theme_settings_description": "Налаштування персоналізації веб-інтерфейсу Immich",
     "these_files_matched_by_checksum": "Ці файли відповідають своїм контрольним сумам",
     "thumbnail_generation_job": "Створення мініатюр",
-    "thumbnail_generation_job_description": "Створити великі, малі та розмиті ескізи для кожного ресурсу, а також ескізи для кожної особи",
-    "transcode_policy_description": "",
+    "thumbnail_generation_job_description": "Створити великі, малі та розмиті мініатюри для кожного ресурсу, а також мініатюри для кожної особи",
     "transcoding_acceleration_api": "API прискорення",
     "transcoding_acceleration_api_description": "API, яка буде взаємодіяти з вашим пристроєм для прискорення транскодування. Ця настройка працює у \"найкращих умовах\" і, в разі невдачі, перейде на програмне транскодування. Підтримка VP9 може або не може працювати, залежно від вашого обладнання.",
     "transcoding_acceleration_nvenc": "NVENC (вимагає графічного процесора NVIDIA)",
@@ -271,7 +292,7 @@
     "transcoding_hardware_acceleration": "Апаратне прискорення",
     "transcoding_hardware_acceleration_description": "Експериментальний режим: значно швидший, але при однаковому бітрейті може мати меншу якість",
     "transcoding_hardware_decoding": "Апаратне декодування",
-    "transcoding_hardware_decoding_setting_description": "Застосовується тільки до NVENC, QSV і RKMPP. Вмикає наскрізне прискорення на заміну лише прискорення кодування. Може працювати не з усіма відео.",
+    "transcoding_hardware_decoding_setting_description": "Увімкнення наскрізного прискорення замість прискорення лише кодування. Може не працювати для всіх відео.",
     "transcoding_hevc_codec": "Кодек HEVC",
     "transcoding_max_b_frames": "Максимальна кількість проміжних кадрів",
     "transcoding_max_b_frames_description": "Вищі значення покращують ефективність стиснення, але збільшують час кодування. Можуть бути несумісні з апаратним прискоренням на старих пристроях. Значення 0 вимикає B-фрейми, а -1 автоматично налаштовує це значення.",
@@ -297,8 +318,6 @@
     "transcoding_threads_description": "Вищі значення прискорюють кодування, але залишають менше місця для обробки інших завдань сервером під час активності. Це значення не повинно бути більше кількості ядер процесора. Максимізує використання, якщо встановлено на 0.",
     "transcoding_tone_mapping": "Тонова картографія",
     "transcoding_tone_mapping_description": "Намагається зберегти вигляд HDR-відео при конвертації в SDR. Кожен алгоритм робить різні компроміси щодо кольору, деталізації та яскравості. Алгоритм Hable зберігає деталі, Mobius - кольори, Reinhard - яскравість.",
-    "transcoding_tone_mapping_npl": "Тонова картографія NPL",
-    "transcoding_tone_mapping_npl_description": "Кольори будуть налаштовані для нормального вигляду на дисплеї цього яскравості. Протилежно до інтуїтивного, менші значення збільшують яскравість відео, а вищі - навпаки, оскільки вони компенсують яскравість дисплея. Значення 0 автоматично налаштовує це значення.",
     "transcoding_transcode_policy": "Політика перекодування",
     "transcoding_transcode_policy_description": "Політика транскодування для відео. HDR відео завжди буде транскодуватись (крім випадків, коли транскодування вимкнено).",
     "transcoding_two_pass_encoding": "Кодування з двома проходами",
@@ -312,8 +331,9 @@
     "trash_settings_description": "Керування налаштуваннями кошика",
     "untracked_files": "Невідстежувані файли",
     "untracked_files_description": "Ці файли не відстежуються програмою. Вони можуть бути результатом невдалого переміщення, перерваного завантаження або залишитися через помилку програми",
+    "user_cleanup_job": "Очищення користувача",
     "user_delete_delay": "Акаунт <b>{user}</b> і його ресурси будуть заплановані для остаточного видалення через {delay, plural, one {# день} few {# дні} many {# днів} other {# днів}}.",
-    "user_delete_delay_settings": "Видалити затримку",
+    "user_delete_delay_settings": "Відкладене видалення",
     "user_delete_delay_settings_description": "Кількість днів після видалення для остаточного видалення акаунта користувача та його ресурсів. Задача видалення користувача запускається опівночі для перевірки користувачів, готових до видалення. Зміни цього налаштування будуть оцінені під час наступного виконання.",
     "user_delete_immediately": "Акаунт та ресурси користувача <b>{user}</b> будуть <b>негайно</b> поставлені в чергу на остаточне видалення.",
     "user_delete_immediately_checkbox": "Поставити користувача та ресурси в чергу для негайного видалення",
@@ -378,7 +398,6 @@
   "archive_or_unarchive_photo": "Архівувати або розархівувати фото",
   "archive_size": "Розмір архіву",
   "archive_size_description": "Налаштувати розмір архіву для завантаження (у GiB)",
-  "archived": "",
   "archived_count": "{count, plural, other {Архівовано #}}",
   "are_these_the_same_person": "Це та сама людина?",
   "are_you_sure_to_do_this": "Ви впевнені, що хочете це зробити?",
@@ -389,7 +408,7 @@
   "asset_has_unassigned_faces": "Є нерозпізнані обличчя",
   "asset_hashing": "Хешування...",
   "asset_offline": "Актив вимкнено",
-  "asset_offline_description": "Цей ресурс відключений. Immich не може отримати доступ до його місцезнаходження файлів. Будь ласка, переконайтеся, що ресурс доступний, а потім знову проскануйте бібліотеку.",
+  "asset_offline_description": "Цей зовнішній актив більше не знайдено на диску. Будь ласка, зверніться до адміністратора Immich за допомогою.",
   "asset_skipped": "Пропущено",
   "asset_skipped_in_trash": "У смітнику",
   "asset_uploaded": "Завантажено",
@@ -402,7 +421,7 @@
   "assets_moved_to_trash_count": "Переміщено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}} у кошик",
   "assets_permanently_deleted_count": "Остаточно видалено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}",
   "assets_removed_count": "Вилучено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}",
-  "assets_restore_confirmation": "Ви впевнені, що хочете відновити всі видалені ресурси? Цю дію не можна скасувати!",
+  "assets_restore_confirmation": "Ви впевнені, що хочете відновити всі свої активи з кошика? Цю дію не можна скасувати! Зверніть увагу, що будь-які офлайн-активи не можуть бути відновлені таким чином.",
   "assets_restored_count": "Відновлено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}",
   "assets_trashed_count": "Поміщено в кошик {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}",
   "assets_were_part_of_album_count": "{count, plural, one {Ресурс був} few {Ресурси були} other {Ресурси були}} вже частиною альбому",
@@ -413,6 +432,7 @@
   "birthdate_saved": "Дата народження успішно збережена",
   "birthdate_set_description": "Дата народження використовується для обчислення віку цієї особи на момент фотографії.",
   "blurred_background": "Розмитий фон",
+  "bugs_and_feature_requests": "Помилки та Запити",
   "build": "Збірка",
   "build_image": "Створити зображення",
   "bulk_delete_duplicates_confirmation": "Ви впевнені, що хочете масово видалити {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}}? Це дія залишить найбільший ресурс у кожній групі і остаточно видалить всі інші дублікати. Цю дію неможливо скасувати!",
@@ -427,10 +447,6 @@
   "cannot_merge_people": "Неможливо об'єднати людей",
   "cannot_undo_this_action": "Ви не можете скасувати цю дію!",
   "cannot_update_the_description": "Неможливо оновити опис",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
   "change_date": "Змінити дату",
   "change_expiration_time": "Змінити термін дії",
   "change_location": "Змінити місцезнаходження",
@@ -462,6 +478,7 @@
   "confirm": "Підтвердіть",
   "confirm_admin_password": "Підтвердити пароль адміністратора",
   "confirm_delete_shared_link": "Ви впевнені, що хочете видалити це спільне посилання?",
+  "confirm_keep_this_delete_others": "Усі інші ресурси в стеку буде видалено, окрім цього ресурсу. Ви впевнені, що хочете продовжити?",
   "confirm_password": "Підтвердити пароль",
   "contain": "Містити",
   "context": "Контекст",
@@ -511,16 +528,19 @@
   "delete_key": "Видалити ключ",
   "delete_library": "Видалити бібліотеку",
   "delete_link": "Видалити посилання",
+  "delete_others": "Видалити інші",
   "delete_shared_link": "Видалити спільне посилання",
   "delete_tag": "Видалити тег",
   "delete_tag_confirmation_prompt": "Ви впевнені, що хочете видалити тег {tagName}?",
   "delete_user": "Видалити користувача",
   "deleted_shared_link": "Видалено загальне посилання",
+  "deletes_missing_assets": "Видаляє активи, які відсутні на диску",
   "description": "Опис",
   "details": "ПОДРОБИЦІ",
   "direction": "Напрям",
   "disabled": "Вимкнено",
   "disallow_edits": "Заборонити редагування",
+  "discord": "Discord",
   "discover": "Виявити",
   "dismiss_all_errors": "Пропустити всі помилки",
   "dismiss_error": "Пропустити помилку",
@@ -529,6 +549,7 @@
   "display_original_photos": "Відображення оригінальних фотографій",
   "display_original_photos_setting_description": "Перевага відображення оригінального фото при перегляді ресурсу, якщо оригінальний ресурс сумісний з вебом. Це може призвести до повільнішого відображення фотографій.",
   "do_not_show_again": "Не показувати це повідомлення знову",
+  "documentation": "Документація",
   "done": "Готово",
   "download": "Скачати",
   "download_include_embedded_motion_videos": "Вбудовані відео",
@@ -541,13 +562,6 @@
   "duplicates": "Дублікати",
   "duplicates_description": "Визначити, які групи є дублікатами",
   "duration": "Тривалість",
-  "durations": {
-    "days": "{days, plural, one {день} few {{days, number} дні} many {{days, number} днів} other {{days, number} днів}}",
-    "hours": "{hours, plural, one {година} few {{hours, number} години} many {{hours, number} годин} other {{hours, number} години}}",
-    "minutes": "{minutes, plural, one {хвилина} few {{minutes, number} хвилини} many {{minutes, number} хвилин} other {{minutes, number} хвилин}}",
-    "months": "{months, plural, one {місяць} few {{months, number} місяці} many {{months, number} місяців} other {{months, number} місяців}}",
-    "years": "{years, plural, one {рік} few {{years, number} роки} many {{years, number} років} other {{years, number} років}}"
-  },
   "edit": "Редагувати",
   "edit_album": "Редагувати альбом",
   "edit_avatar": "Редагувати аватар",
@@ -572,8 +586,6 @@
   "editor_crop_tool_h2_aspect_ratios": "Пропорції зображення",
   "editor_crop_tool_h2_rotation": "Орієнтація",
   "email": "Електронна пошта",
-  "empty": "",
-  "empty_album": "",
   "empty_trash": "Очистити кошик",
   "empty_trash_confirmation": "Ви впевнені, що хочете очистити кошик? Це остаточно видалить всі ресурси в кошику з Immich.\nЦю дію не можна скасувати!",
   "enable": "Увімкнути",
@@ -607,6 +619,7 @@
     "failed_to_create_shared_link": "Не вдалося створити спільне посилання",
     "failed_to_edit_shared_link": "Не вдалося відредагувати спільне посилання",
     "failed_to_get_people": "Не вдалося отримати інформацію про людей",
+    "failed_to_keep_this_delete_others": "Не вдалося зберегти цей ресурс і видалити інші ресурси",
     "failed_to_load_asset": "Не вдалося завантажити ресурс",
     "failed_to_load_assets": "Не вдалося завантажити ресурси",
     "failed_to_load_people": "Не вдалося завантажити людей",
@@ -634,8 +647,6 @@
     "unable_to_change_location": "Неможливо змінити місцезнаходження",
     "unable_to_change_password": "Не вдається змінити пароль",
     "unable_to_change_visibility": "Неможливо змінити видимість для {count, plural, one {# особи} few {# осіб} other {# людей}}",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_complete_oauth_login": "Неможливо завершити вхід через OAuth",
     "unable_to_connect": "Не вдається підключитися",
     "unable_to_connect_to_server": "Не вдається підключитися до сервера",
@@ -660,6 +671,7 @@
     "unable_to_get_comments_number": "Не вдалося отримати кількість коментарів",
     "unable_to_get_shared_link": "Не вдалося отримати спільне посилання",
     "unable_to_hide_person": "Неможливо приховати людину",
+    "unable_to_link_motion_video": "Не вдається зв'язати рухоме відео",
     "unable_to_link_oauth_account": "Не вдається прив'язати обліковий запис OAuth",
     "unable_to_load_album": "Неможливо завантажити альбом",
     "unable_to_load_asset_activity": "Неможливо завантажити активність активу",
@@ -675,12 +687,10 @@
     "unable_to_remove_album_users": "Неможливо видалити користувачів з альбому",
     "unable_to_remove_api_key": "Не вдається видалити ключ API",
     "unable_to_remove_assets_from_shared_link": "Не вдається видалити ресурси зі спільного посилання",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_deleted_assets": "Неможливо видалити автономні файли",
     "unable_to_remove_library": "Не вдається видалити бібліотеку",
-    "unable_to_remove_offline_files": "Неможливо видалити автономні файли",
     "unable_to_remove_partner": "Не вдається видалити партнера",
     "unable_to_remove_reaction": "Не вдалося видалити реакцію",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "Не вдалося відновити елементи",
     "unable_to_reset_password": "Не вдається скинути пароль",
     "unable_to_resolve_duplicate": "Не вдається вирішити дублікат",
@@ -700,6 +710,7 @@
     "unable_to_submit_job": "Не вдалося відправити завдання",
     "unable_to_trash_asset": "Неможливо вилучити актив",
     "unable_to_unlink_account": "Не вдається відв'язати обліковий запис",
+    "unable_to_unlink_motion_video": "Не вдається від'єднати рухоме відео",
     "unable_to_update_album_cover": "Неможливо оновити обкладинку альбому",
     "unable_to_update_album_info": "Неможливо оновити інформацію про альбом",
     "unable_to_update_library": "Не вдалося оновити бібліотеку",
@@ -709,10 +720,6 @@
     "unable_to_update_user": "Неможливо оновити дані користувача",
     "unable_to_upload_file": "Не вдалося завантажити файл"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exif": "Exif",
   "exit_slideshow": "Вийти зі слайд-шоу",
   "expand_all": "Розгорнути все",
@@ -727,37 +734,32 @@
   "external": "Зовнішні",
   "external_libraries": "Зовнішні бібліотеки",
   "face_unassigned": "Не призначено",
-  "failed_to_get_people": "",
+  "failed_to_load_assets": "Не вдалося завантажити ресурси",
   "favorite": "До улюблених",
   "favorite_or_unfavorite_photo": "Додати до обраних або видалити з обраних фото",
   "favorites": "Улюблені",
-  "feature": "",
   "feature_photo_updated": "Вибране фото оновлено",
-  "featurecollection": "",
   "features": "Додаткові можливості",
   "features_setting_description": "Керування додатковими можливостями додатка",
   "file_name": "Ім'я файлу",
   "file_name_or_extension": "Ім'я файлу або розширення",
   "filename": "Ім'я файлу",
-  "files": "",
   "filetype": "Тип файлу",
   "filter_people": "Фільтр по людях",
   "find_them_fast": "Швидко знаходьте їх за назвою за допомогою пошуку",
   "fix_incorrect_match": "Виправити неправильний збіг",
   "folders": "Папки",
   "folders_feature_description": "Перегляд перегляду папок для фотографій і відео у файловій системі",
-  "force_re-scan_library_files": "Примусово пересканувати всі файли бібліотеки",
   "forward": "Переслати",
   "general": "Загальні",
   "get_help": "Отримати допомогу",
   "getting_started": "Початок",
   "go_back": "Повернутися назад",
   "go_to_search": "Перейти до пошуку",
-  "go_to_share_page": "Перейдіть на сторінку спільного доступу",
   "group_albums_by": "Групувати альбоми за...",
   "group_no": "Без групування",
-  "group_owner": "Групування за власником",
-  "group_year": "Групувати за роками",
+  "group_owner": "За власником",
+  "group_year": "За роком",
   "has_quota": "Квота",
   "hi_user": "Привіт {name} ({email})",
   "hide_all_people": "Сховати всіх",
@@ -779,10 +781,6 @@
   "image_alt_text_date_place_2_people": "{isVideo, select, true {Відео} other {Зображення}} зроблено в {city}, {country} з {person1} та {person2} {date}",
   "image_alt_text_date_place_3_people": "{isVideo, select, true {Відео} other {Зображення}} зроблено в {city}, {country} з {person1}, {person2} та {person3} {date}",
   "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Відео} other {Зображення}} зроблено в {city}, {country} з {person1}, {person2} та ще {additionalCount, number} особами {date}",
-  "image_alt_text_people": "{count, plural, =1 {з {person1}} =2 {з {person1} та {person2}} =3 {з {person1}, {person2}, та {person3}} other {з {person1}, {person2}, та {others, number} ін.}}",
-  "image_alt_text_place": "у {city}, {country}",
-  "image_taken": "{isVideo, select, true {Зняте відео} other {Зроблений знімок}}",
-  "img": "",
   "immich_logo": "Логотип Immich",
   "immich_web_interface": "Веб інтерфейс Immich",
   "import_from_json": "Імпорт з JSON",
@@ -803,10 +801,11 @@
   "invite_people": "Запросити",
   "invite_to_album": "Запросити в альбом",
   "items_count": "{count, plural, one {# елемент} few {# елементи} many {# елементів} other {# елемента}}",
-  "job_settings_description": "",
   "jobs": "Завдання",
   "keep": "Залишити",
   "keep_all": "Зберегти все",
+  "keep_this_delete_others": "Залишити цей ресурс, видалити інші",
+  "kept_this_deleted_others": "Збережено цей ресурс і видалено {count, plural, one {# ресурс} few {# ресурси} many {# ресурсів} other {# ресурсу}}",
   "keyboard_shortcuts": "Сполучення клавіш",
   "language": "Мова",
   "language_setting_description": "Виберіть мову, якій ви надаєте перевагу",
@@ -818,33 +817,9 @@
   "level": "Рівень",
   "library": "Бібліотека",
   "library_options": "Параметри бібліотеки",
-  "license_account_info": "Ваш обліковий запис має ліцензію",
-  "license_activated_subtitle": "Дякуємо за підтримку Immich та програмного забезпечення з відкритим кодом",
-  "license_activated_title": "Ваша ліцензія успішно активована",
-  "license_button_activate": "Активувати",
-  "license_button_buy": "Купити",
-  "license_button_buy_license": "Купити ліцензію",
-  "license_button_select": "Оберіть",
-  "license_failed_activation": "Не вдалося активувати ліцензію. Будь ласка, перевірте свою електронну пошту, щоб отримати правильний ліцензійний ключ!",
-  "license_individual_description_1": "1 ліцензія на користувача на будь-якому сервері",
-  "license_individual_title": "Індивідуальна ліцензія",
-  "license_info_licensed": "Ліцензовано",
-  "license_info_unlicensed": "Без ліцензії",
-  "license_input_suggestion": "Маєте ліцензію? Введіть ключ нижче",
-  "license_license_subtitle": "Придбайте ліцензію на підтримку Immich",
-  "license_license_title": "ЛІЦЕНЗІЯ",
-  "license_lifetime_description": "Довічна ліцензія",
-  "license_per_server": "На один сервер",
-  "license_per_user": "На одного користувача",
-  "license_server_description_1": "1 ліцензія на сервер",
-  "license_server_description_2": "Ліцензія для всіх користувачів на сервері",
-  "license_server_title": "Ліцензія на сервер",
-  "license_trial_info_1": "Ви використовуєте неліцензійну версію Immich",
-  "license_trial_info_2": "Ви використовуєте Immich приблизно",
-  "license_trial_info_3": "{accountAge, plural, one {# день} few {# дні} many {# днів} other {# дня}}",
-  "license_trial_info_4": "Будь ласка, розгляньте можливість придбання ліцензії для підтримки подальшого розвитку сервісу",
   "light": "Світла",
   "like_deleted": "Лайк видалено",
+  "link_motion_video": "Посилання на рухоме відео",
   "link_options": "Налаштування посилання",
   "link_to_oauth": "Приєднання до OAuth",
   "linked_oauth_account": "Приєднаний акаунт OAuth",
@@ -863,6 +838,7 @@
   "look": "Дивитися",
   "loop_videos": "Циклічні відео",
   "loop_videos_description": "Увімкнути циклічне відтворення відео.",
+  "main_branch_warning": "Ви використовуєте версію для розробників; ми настійно рекомендуємо використовувати релізну версію!",
   "make": "Виробник",
   "manage_shared_links": "Керування спільними посиланнями",
   "manage_sharing_with_partners": "Керуйте спільним використанням з партнерами",
@@ -932,6 +908,7 @@
   "notifications": "Сповіщення",
   "notifications_setting_description": "Керування сповіщеннями",
   "oauth": "OAuth",
+  "official_immich_resources": "Офіційні ресурси Immich",
   "offline": "Офлайн",
   "offline_paths": "Недоступні шляхи",
   "offline_paths_description": "Ці результати можуть бути пов'язані з ручним видаленням файлів, які не є частиною зовнішньої бібліотеки.",
@@ -944,7 +921,6 @@
   "onboarding_welcome_user": "Ласкаво просимо, {user}",
   "online": "Доступний",
   "only_favorites": "Лише обрані",
-  "only_refreshes_modified_files": "Оновлює лише змінені файли",
   "open_in_map_view": "Відкрити у перегляді мапи",
   "open_in_openstreetmap": "Відкрити в OpenStreetMap",
   "open_the_search_filters": "Відкрийте фільтри пошуку",
@@ -982,13 +958,12 @@
   "people_edits_count": "Відредаговано {count, plural, one {# особу} few {# особи} many {# осіб} other {# людей}}",
   "people_feature_description": "Перегляд фотографій і відео, згрупованих за людьми",
   "people_sidebar_description": "Відображення посилання на людей у бічній панелі",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "Попередження про видалення",
   "permanent_deletion_warning_setting_description": "Показувати попередження при остаточному видаленні ресурсів",
   "permanently_delete": "Видалити назавжди",
   "permanently_delete_assets_count": "Остаточно видалити {count, plural, one {ресурс} other {ресурси}}",
   "permanently_delete_assets_prompt": "Ви впевнені, що хочете назавжди видалити {count, plural, one {цей ресурс?} other {ці <b>#</b> ресурси?}} Це також видалить {count, plural, one {його з його} other {їх з їхніх}} альбому(ів).",
-  "permanently_deleted_asset": "Видалити об'єкт назавжди",
+  "permanently_deleted_asset": "Видалити назавжди",
   "permanently_deleted_assets_count": "Видалено остаточно {count, plural, one {# ресурс} few {# ресурси} many {# ресурсів} other {# ресурсів}}",
   "person": "Людина",
   "person_hidden": "{name}{hidden, select, true { (приховано)} other {}}",
@@ -1004,7 +979,6 @@
   "play_memories": "Відтворити спогади",
   "play_motion_photo": "Відтворювати рухомі фото",
   "play_or_pause_video": "Відтворення або призупинення відео",
-  "point": "",
   "port": "Порт",
   "preset": "Передвстановлення",
   "preview": "Прев'ю",
@@ -1049,12 +1023,10 @@
   "purchase_server_description_2": "Статус підтримки",
   "purchase_server_title": "Сервер",
   "purchase_settings_server_activated": "Ключ продукту сервера керується адміністратором",
-  "range": "",
   "rating": "Зоряний рейтинг",
   "rating_clear": "Очистити рейтинг",
   "rating_count": "{count, plural, one {# зірка} few {# зірки} many {# зірок} other {# зірок}}",
   "rating_description": "Показувати рейтинг EXIF на інформаційній панелі",
-  "raw": "",
   "reaction_options": "Опції реакції",
   "read_changelog": "Прочитати зміни в оновленні",
   "reassign": "Перепризначити",
@@ -1062,14 +1034,17 @@
   "reassigned_assets_to_new_person": "Перепризначено {count, plural, one {# ресурс} other {# ресурси}} новій особі",
   "reassing_hint": "Призначити обрані ресурси існуючій особі",
   "recent": "Нещодавно",
+  "recent-albums": "Останні альбоми",
   "recent_searches": "Нещодавні пошукові запити",
   "refresh": "Оновити",
   "refresh_encoded_videos": "Оновити закодовані відео",
+  "refresh_faces": "Оновити обличчя",
   "refresh_metadata": "Оновити метадані",
   "refresh_thumbnails": "Оновити мініатюри",
   "refreshed": "Оновлений",
-  "refreshes_every_file": "Оновлює кожен файл",
+  "refreshes_every_file": "Повторно читає всі існуючі та нові файли",
   "refreshing_encoded_video": "Оновлення закодованого відео",
+  "refreshing_faces": "Оновлення облич",
   "refreshing_metadata": "Оновлення метаданих",
   "regenerating_thumbnails": "Відновлення мініатюр",
   "remove": "Вилучити",
@@ -1077,10 +1052,11 @@
   "remove_assets_shared_link_confirmation": "Ви впевнені, що хочете видалити {count, plural, one {# ресурс} few {# ресурси} many {# ресурсів} other {# ресурсів}} з цього спільного посилання?",
   "remove_assets_title": "Видалити об'єкти?",
   "remove_custom_date_range": "Видалити користувацький діапазон дат",
+  "remove_deleted_assets": "Видалення автономних файлів",
   "remove_from_album": "Видалити з альбому",
   "remove_from_favorites": "Видалити з обраного",
   "remove_from_shared_link": "Видалити зі спільного посилання",
-  "remove_offline_files": "Видалення автономних файлів",
+  "remove_url": "Видалити URL",
   "remove_user": "Видалити користувача",
   "removed_api_key": "Видалено ключ API: {name}",
   "removed_from_archive": "Видалено з архіву",
@@ -1097,7 +1073,6 @@
   "reset": "Скидання",
   "reset_password": "Скинути пароль",
   "reset_people_visibility": "Відновити видимість людей",
-  "reset_settings_to_default": "",
   "reset_to_default": "Скидання до налаштувань за замовчуванням",
   "resolve_duplicates": "Усунути дублікати",
   "resolved_all_duplicates": "Усі дублікати усунуто",
@@ -1117,8 +1092,7 @@
   "saved_settings": "Налаштування збережено",
   "say_something": "Скажіть що-небудь",
   "scan_all_libraries": "Сканувати всі бібліотеки",
-  "scan_all_library_files": "Повторне сканування всіх файлів бібліотеки",
-  "scan_new_library_files": "Сканування нових файлів бібліотеки",
+  "scan_library": "Сканувати",
   "scan_settings": "Налаштування сканування",
   "scanning_for_album": "Сканування альбому...",
   "search": "Пошук",
@@ -1136,6 +1110,7 @@
   "search_options": "Опції пошуку",
   "search_people": "Шукати людей",
   "search_places": "Пошук місць",
+  "search_settings": "Налаштування пошуку",
   "search_state": "Пошук регіону...",
   "search_tags": "Пошук тегів...",
   "search_timezone": "Пошук часового поясу...",
@@ -1160,7 +1135,6 @@
   "selected_count": "{count, plural, one {# обраний} other {# обраних}}",
   "send_message": "Надіслати повідомлення",
   "send_welcome_email": "Надішліть вітальний лист",
-  "server": "Сервер",
   "server_offline": "Сервер офлайн",
   "server_online": "Сервер онлайн",
   "server_stats": "Статистика сервера",
@@ -1203,6 +1177,7 @@
   "show_person_options": "Показати параметри людини",
   "show_progress_bar": "Показати індикатор прогресу",
   "show_search_options": "Показати параметри пошуку",
+  "show_slideshow_transition": "Показати перехід слайд-шоу",
   "show_supporter_badge": "Значок підтримки",
   "show_supporter_badge_description": "Показати значок підтримки",
   "shuffle": "Перемішати",
@@ -1223,8 +1198,8 @@
   "sort_oldest": "Старі фото",
   "sort_recent": "Нещодавні",
   "sort_title": "Заголовок",
-  "source": "Джерело",
-  "stack": "Стек",
+  "source": "Вихідний код",
+  "stack": "У стопку",
   "stack_duplicates": "Групувати дублікати",
   "stack_select_one_photo": "Вибрати одне основне фото для групи",
   "stack_selected_photos": "Сгрупувати обрані фотографії",
@@ -1244,13 +1219,16 @@
   "submit": "Підтвердити",
   "suggestions": "Пропозиції",
   "sunrise_on_the_beach": "Світанок на пляжі",
+  "support": "Підтримка",
+  "support_and_feedback": "Підтримка та зворотний зв'язок",
+  "support_third_party_description": "Вашу установку Immich було упаковано третьою стороною. Проблеми, з якими ви стикаєтесь, можуть бути викликані цим пакетом, тому спочатку зверніться до них за допомогою, використовуючи наведені нижче посилання.",
   "swap_merge_direction": "Змінити напрямок об'єднання",
   "sync": "Синхронізувати",
   "tag": "Тег",
   "tag_assets": "Додати теги",
   "tag_created": "Створено тег: {tag}",
   "tag_feature_description": "Перегляд фотографій та відео, згрупованих за логічними темами тегів",
-  "tag_not_found_question": "Не знайшли тег? Створіть його <link>тут</link>",
+  "tag_not_found_question": "Не вдається знайти тег? <link>Створити новий тег.</link>",
   "tag_updated": "Оновлено тег: {tag}",
   "tagged_assets": "Позначено тегом {count, plural, one {# актив} other {# активи}}",
   "tags": "Теги",
@@ -1259,18 +1237,19 @@
   "theme_selection": "Вибір теми",
   "theme_selection_description": "Автоматично встановлювати тему на світлу або темну залежно від системних налаштувань вашого браузера",
   "they_will_be_merged_together": "Вони будуть об'єднані разом",
+  "third_party_resources": "Ресурси третіх сторін",
   "time_based_memories": "Спогади, що базуються на часі",
+  "timeline": "Хронологія",
   "timezone": "Часовий пояс",
   "to_archive": "Архів",
   "to_change_password": "Змінити пароль",
   "to_favorite": "Обране",
   "to_login": "Вхід",
   "to_parent": "Повернутись назад",
-  "to_root": "На початок",
   "to_trash": "Смітник",
   "toggle_settings": "Перемикання налаштувань",
   "toggle_theme": "Перемикання теми",
-  "toggle_visibility": "Перемикання видимості",
+  "total": "Усього",
   "total_usage": "Загальне використання",
   "trash": "Кошик",
   "trash_all": "Видалити все",
@@ -1280,14 +1259,13 @@
   "trashed_items_will_be_permanently_deleted_after": "Видалені елементи будуть остаточно видалені через {days, plural, one {# день} few {# дні} many {# днів} other {# днів}}.",
   "type": "Тип",
   "unarchive": "Розархівувати",
-  "unarchived": "",
   "unarchived_count": "{count, plural, other {Повернуто з архіву #}}",
   "unfavorite": "Видалити з улюблених",
   "unhide_person": "Розкрити особу",
   "unknown": "Невідомо",
-  "unknown_album": "",
   "unknown_year": "Невідомий рік",
   "unlimited": "Без обмежень",
+  "unlink_motion_video": "Від'єднати рухоме відео",
   "unlink_oauth": "Від'єднайте OAuth",
   "unlinked_oauth_account": "Відключити акаунт OAuth",
   "unnamed_album": "Альбом без назви",
@@ -1316,13 +1294,13 @@
   "use_custom_date_range": "Використовувати користувацький діапазон дат",
   "user": "Користувач",
   "user_id": "ID Користувача",
-  "user_license_settings": "Ліцензія",
-  "user_license_settings_description": "Керування ліцензією",
   "user_liked": "{user} вподобав {type, select, photo {це фото} video {це відео} asset {цей ресурс} other {це}}",
   "user_purchase_settings": "Придбати",
   "user_purchase_settings_description": "Керувати вашою покупкою",
   "user_role_set": "Призначити {user} на роль {role}",
   "user_usage_detail": "Деталі використання користувача",
+  "user_usage_stats": "Статистика використання акаунта",
+  "user_usage_stats_description": "Переглянути статистику використання акаунта",
   "username": "Ім'я користувача",
   "users": "Користувачі",
   "utilities": "Утиліти",
@@ -1330,7 +1308,9 @@
   "variables": "Змінні",
   "version": "Версія",
   "version_announcement_closing": "Твій друг, Алекс",
-  "version_announcement_message": "Привіт, друг! В нас є нова версія додатку. Будь ласка, відвідайте <link>релізні нотатки</link> і переконайтеся, що ваші файли <code>docker-compose.yml</code> та <code>.env</code> актуальні, щоб уникнути будь-яких помилок конфігурації, особливо якщо ви використовуєте WatchTower або інші механізми автоматичного оновлення додатку.",
+  "version_announcement_message": "Привіт! Доступна нова версія Immich. Будь ласка, приділіть трохи часу для ознайомлення з <link>примітками до випуску</link>, щоб переконатися, що ваша установка оновлена і уникнути можливих помилок у налаштуваннях, особливо якщо ви використовуєте WatchTower або будь-який інший механізм, який автоматично оновлює ваш екземпляр Immich.",
+  "version_history": "Історія версій",
+  "version_history_item": "Встановлено {version} {date}",
   "video": "Відео",
   "video_hover_setting": "Відтворення мініатюри відео під час наведення курсору миші",
   "video_hover_setting_description": "Відтворювати зображення відео при наведенні курсора на елемент. Навіть якщо вимкнено, відтворення може бути запущено, навівши курсор на піктограму відтворення.",
@@ -1342,16 +1322,16 @@
   "view_all_users": "Переглянути всіх користувачів",
   "view_in_timeline": "Переглянути в хронології",
   "view_links": "Переглянути посилання",
+  "view_name": "Переглянути",
   "view_next_asset": "Переглянути наступний ресурс",
   "view_previous_asset": "Переглянути попередній ресурс",
   "view_stack": "Перегляд стеку",
-  "viewer": "",
   "visibility_changed": "Видимість змінено для {count, plural, one {# особи} few {# осіб} many {# осіб} other {# осіб}}",
   "waiting": "Очікують",
   "warning": "Попередження",
   "week": "Тиждень",
   "welcome": "Ласкаво просимо",
-  "welcome_to_immich": "Ласкаво просимо до immich",
+  "welcome_to_immich": "Ласкаво просимо до Immich",
   "year": "Рік",
   "years_ago": "{years, plural, one {# рік} few {# роки} many {# років} other {# років}} тому",
   "yes": "Так",
diff --git a/i18n/ur.json b/i18n/ur.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/i18n/ur.json
@@ -0,0 +1 @@
+{}
diff --git a/web/src/lib/i18n/vi.json b/i18n/vi.json
similarity index 91%
rename from web/src/lib/i18n/vi.json
rename to i18n/vi.json
index c4f23ec273..9c30ea2935 100644
--- a/web/src/lib/i18n/vi.json
+++ b/i18n/vi.json
@@ -1,5 +1,5 @@
 {
-  "about": "Giới thiệu",
+  "about": "Làm mới",
   "account": "Tài khoản",
   "account_settings": "Cài đặt tài khoản",
   "acknowledge": "Ghi nhận",
@@ -28,11 +28,17 @@
   "added_to_favorites_count": "Đã thêm {count, number} vào Mục yêu thích",
   "admin": {
     "add_exclusion_pattern_description": "Thêm quy tắc loại trừ. Hỗ trợ sử dụng ký tự *, **, và ?. Để bỏ qua tất cả các tập tin bất kỳ trong thư mục tên \"Raw\", hãy dùng \"**/Raw/**\". Để bỏ qua các tập tin có đuôi \".tif\", hãy dùng \"**/*.tif\". Để bỏ qua một đường dẫn cố định, hãy dùng \"/path/to/ignore/**\".",
+    "asset_offline_description": "Ảnh thư viện ngoài này không còn trên ổ đĩa và đã bị chuyển vào thùng rác. Nếu ảnh đã bị di chuyển trong thư viện, kiểm tra dòng thời gian của bạn để tìm ảnh mới tương ứng. Để khôi phục, hãy đảm bảo Immich có thể truy cập đường dẫn ảnh bên dưới và quét lại thư viện.",
     "authentication_settings": "Đăng nhập",
     "authentication_settings_description": "Quản lý mật khẩu, OAuth và các cài đặt xác thực khác",
     "authentication_settings_disable_all": "Bạn có chắc chắn muốn vô hiệu hoá tất cả các phương thức đăng nhập? Đăng nhập sẽ bị vô hiệu hóa hoàn toàn.",
     "authentication_settings_reenable": "Để bật lại, dùng <link>Lệnh Máy chủ</link>.",
     "background_task_job": "Các tác vụ nền",
+    "backup_database": "Sao lưu dữ liệu",
+    "backup_database_enable_description": "Kích hoạt Sao lưu dữ liệu",
+    "backup_keep_last_amount": "Số lượng các bản Sao lưu được giữ lại",
+    "backup_settings": "Cài đặt sao lưu",
+    "backup_settings_description": "Quản lý các thông số cài đặt của Sao lưu dữ liệu",
     "check_all": "Chọn tất cả",
     "cleared_jobs": "Đã xoá các tác vụ: {job}",
     "config_set_by_file": "Cấu hình hiện tại đang được đặt bởi một tập tin cấu hình",
@@ -41,35 +47,40 @@
     "confirm_email_below": "Để xác nhận, nhập \"{email}\" bên dưới",
     "confirm_reprocess_all_faces": "Bạn có chắc chắn muốn xử lý lại tất cả các khuôn mặt? Thao tác này sẽ xoá tên người đã được gán.",
     "confirm_user_password_reset": "Bạn có chắc chắn muốn đặt lại mật khẩu của {user}?",
-    "crontab_guru": "Crontab Guru",
+    "create_job": "Tạo tác vụ",
+    "cron_expression": "Biểu thức Cron",
+    "cron_expression_description": "Thiết lập khoảng thời gian để quét bằng biểu thức cron. Tham khảo <link>Crontab Guru</link> để biết thêm thông tin.",
+    "cron_expression_presets": "Mẫu biểu thức Cron",
     "disable_login": "Vô hiệu hoá đăng nhập",
-    "disabled": "",
-    "duplicate_detection_job_description": "Sử dụng machine learning để phát hiện các hình ảnh giống nhau. Dựa vào Tìm kiếm Thông Minh",
+    "duplicate_detection_job_description": "Sử dụng Học máy để phát hiện các hình ảnh giống nhau. Dựa vào Tìm kiếm Thông Minh",
     "exclusion_pattern_description": "Quy tắc loại trừ cho bạn bỏ qua các tập tin và thư mục khi quét thư viện của bạn. Điều này hữu ích nếu bạn có các thư mục chứa tập tin bạn không muốn nhập, chẳng hạn như các tập tin RAW.",
     "external_library_created_at": "Thư viện bên ngoài (được tạo vào {date})",
     "external_library_management": "Quản lý thư viện bên ngoài",
     "face_detection": "Phát hiện khuôn mặt",
-    "face_detection_description": "Sử dụng machine learning để phát hiện các khuôn mặt trong ảnh. Với video, chỉ thực hiện trên ảnh thu nhỏ. Xử lý lại tất cả các hình ảnh. Các hỉnh ảnh trong hàng đợi bị bỏ lỡ chưa được xử lý. Các khuôn mặt được phát hiện sẽ được xếp vào hàng đợi cho quá trình Nhận dạng khuôn mặt sau khi quá trình Phát hiện khuôn mặt hoàn tất, nhóm chúng vào người hiện có hoặc tạo người mới.",
-    "facial_recognition_job_description": "Nhóm các khuôn mặt đã phát hiện thành người. Bước này được thực hiện sau khi Phát hiện khuôn mặt hoàn tất. Xử lý lại việc nhóm cho toàn bộ khuôn mặt. Các khuôn mặt trong hàng đợi bị bỏ lỡ chưa được gán cho người nào.",
+    "face_detection_description": "Sử dụng Học máy để nhận dạng khuôn mặt trong ảnh. Đối với video, sẽ sử dụng hình ảnh thu nhỏ. \"Làm Mới\" sẽ xử lý lại tất cả các hình. \"Xử Lý Lại\" sẽ xoá hết tất cả dữ liệu khuôn mặt và nhận dạng lại. \"Còn Thiếu\" sẽ xử lý các ảnh còn thiếu. Các khuôn mặt được phát hiện sẽ được xử lý bởi tác vụ Nhận Dạng Khuôn Mặt để nhóm chúng vào những người đã có hoặc người mới.",
+    "facial_recognition_job_description": "Nhóm các khuôn mặt đã phát hiện thành người. Bước này được thực hiện sau khi tác vụ Phát hiện Khuôn mặt hoàn tất. \"Xử Lý Lại\" sẽ nhóm lại tất cả các khuôn mặt. \"Còn Thiếu\" sẽ xử lý các khuôn mặt chưa có gán với người nào.",
     "failed_job_command": "Lệnh {command} không thực hiện được tác vụ: {job}",
     "force_delete_user_warning": "CẢNH BÁO: Thao tác này sẽ ngay lập tức xoá người dùng và tất cả ảnh. Hành động này không thể hoàn tác và các tập tin không thể khôi phục.",
     "forcing_refresh_library_files": "Làm mới toàn bộ thư viện ảnh",
+    "image_format": "Định dạng",
     "image_format_description": "Định dạng WebP dung lượng nhỏ hơn JPEG, nhưng mã hóa chậm hơn.",
     "image_prefer_embedded_preview": "Ưu tiên ảnh xem trước đi kèm",
     "image_prefer_embedded_preview_setting_description": "Ứng dụng sẽ sử dụng ảnh xem trước trong ảnh RAW khi có sẵn để xử lý hình ảnh. Điều này có thể giúp tái tạo màu sắc chính xác hơn cho một số hình ảnh, nhưng chất lượng của ảnh xem trước phụ thuộc vào máy ảnh và có thể bị nén.",
     "image_prefer_wide_gamut": "Ưu tiên gam màu mở rộng",
-    "image_prefer_wide_gamut_setting_description": "Hiển thị ảnh thu nhỏ ở gam màu Display P3. Điều này giúp giữ màu sắc rực rỡ của những hình ảnh có gam màu rộng, nhưng hình ảnh có thể trông khác trên các thiết bị cũ và trình duyệt cũ. Hình ảnh sRGB được giữ nguyên để tránh thay đổi màu sắc.",
-    "image_preview_format": "Định dạng xem trước",
-    "image_preview_resolution": "Độ phân giải xem trước",
-    "image_preview_resolution_description": "Được sử dụng khi xem một bức ảnh và cho machine learning. Độ phân giải cao hơn có thể giữ lại nhiều chi tiết hơn nhưng mất nhiều thời gian mã hóa, có kích thước lớn hơn và có thể làm giảm khả năng phản hồi của ứng dụng.",
+    "image_prefer_wide_gamut_setting_description": "Hiển thị hình thu nhỏ ở gam màu Display P3. Điều này giúp giữ màu sắc rực rỡ của những hình ảnh có gam màu rộng, nhưng hình ảnh có thể trông khác trên các thiết bị cũ và trình duyệt cũ. Hình ảnh sRGB được giữ nguyên để tránh thay đổi màu sắc.",
+    "image_preview_description": "Hình ảnh kích thước trung bình đã loại bỏ metadata, được sử dụng khi xem một hình duy nhất và cho Học máy",
+    "image_preview_quality_description": "Chất lượng xem trước từ 1-100. Càng cao càng tốt, nhưng sẽ tạo ra các tập tin lớn hơn có thể làm giảm khả năng phản hồi của ứng dụng. Sử dụng giá trị thấp có thể ảnh hưởng đến chất lượng tác vụ Học máy.",
+    "image_preview_title": "Cài đặt Xem trước",
     "image_quality": "Chất lượng",
-    "image_quality_description": "Chất lượng hình ảnh từ 1 - 100. Giá trị càng cao hình ảnh đẹp hơn nhưng kích thước tập tin sẽ lớn, lựa chọn này ảnh hưởng tới ảnh xem trước và ảnh thu nhỏ.",
+    "image_resolution": "Độ phân giải",
+    "image_resolution_description": "Độ phân giải cao hơn sẽ rõ nét hơn nhưng tốn nhiều thời gian hơn để mã hóa, kích thước tập tin lớn hơn và có thể làm giảm khả năng phản hồi của ứng dụng.",
     "image_settings": "Hình ảnh",
     "image_settings_description": "Quản lý chất lượng và độ phân giải của hình ảnh được tạo",
-    "image_thumbnail_format": "Định dạng ảnh thu nhỏ",
-    "image_thumbnail_resolution": "Độ phân giải ảnh thu nhỏ",
-    "image_thumbnail_resolution_description": "Dùng khi xem một nhóm các ảnh (dòng thời gian chính, xem album, v.v.). Độ phân giải cao hơn có thể giữ lại nhiều chi tiết hơn nhưng mất nhiều thời gian mã hóa, có kích thước lớn hơn và có thể làm giảm khả năng phản hồi của ứng dụng.",
+    "image_thumbnail_description": "Hình thu nhỏ kích thước nhỏ đã loại bỏ metadata, dùng khi xem nhiều ảnh cùng lúc, ví dụ như xem Dòng Thời gian chính",
+    "image_thumbnail_quality_description": "Chất lượng hình thu nhỏ từ 1-100. Càng cao càng tốt, nhưng sẽ tạo ra các tập tin lớn hơn có thể làm giảm khả năng phản hồi của ứng dụng.",
+    "image_thumbnail_title": "Cài đặt hình thu nhỏ",
     "job_concurrency": "{job} thực hiện đồng thời",
+    "job_created": "Tác vụ đã được tạo",
     "job_not_concurrency_safe": "Tác vụ này không an toàn để thực hiện đồng thời.",
     "job_settings": "Tác vụ",
     "job_settings_description": "Quản lý mức độ thực hiện đồng thời của tác vụ",
@@ -77,9 +88,6 @@
     "jobs_delayed": "{jobCount, plural, other {# tác vụ bị hoãn lại}}",
     "jobs_failed": "{jobCount, plural, other {# tác vụ bị thất bại}}",
     "library_created": "Đã tạo thư viện: {library}",
-    "library_cron_expression": "Cú pháp Cron",
-    "library_cron_expression_description": "Đặt lịch quét bằng định dạng cron. Để biết thêm thông tin, vui lòng tham khảo ví dụ. <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Các mẫu biểu thức Cron",
     "library_deleted": "Thư viện đã bị xoá",
     "library_import_path_description": "Chọn thư mục để nhập. Ứng dụng sẽ quét tất cả hình ảnh và video trong thư mục này bao gồm các thư mục con.",
     "library_scanning": "Quét định kỳ",
@@ -96,13 +104,13 @@
     "logging_settings": "Ghi nhật ký",
     "machine_learning_clip_model": "Mô hình CLIP",
     "machine_learning_clip_model_description": "Tên của mô hình CLIP được liệt kê <link>tại đây</link>. Bạn cần chạy lại tác vụ \"Tìm kiếm thông minh\" cho tất cả hình ảnh sau khi thay đổi mô hình.",
-    "machine_learning_duplicate_detection": "Phát hiện ảnh trùng lặp",
+    "machine_learning_duplicate_detection": "Phát hiện Trùng lặp",
     "machine_learning_duplicate_detection_enabled": "Bật phát hiện ảnh trùng lặp",
     "machine_learning_duplicate_detection_enabled_description": "Nếu bị tắt, các ảnh trùng lặp giống hệt nhau vẫn sẽ bị loại bỏ.",
     "machine_learning_duplicate_detection_setting_description": "Sử dụng vector nhúng CLIP để tìm kiếm ảnh trùng lặp",
-    "machine_learning_enabled": "Bật machine learning",
-    "machine_learning_enabled_description": "Nếu bị tắt, tất cả các tính năng ML sẽ bị vô hiệu hoá kể các cài đặt bên dưới.",
-    "machine_learning_facial_recognition": "Nhận dạng khuôn mặt",
+    "machine_learning_enabled": "Bật Học máy",
+    "machine_learning_enabled_description": "Nếu bị tắt, tất cả các tính năng và cài đặt Học máy sẽ bị loại bỏ.",
+    "machine_learning_facial_recognition": "Nhận dạng Khuôn mặt",
     "machine_learning_facial_recognition_description": "Phát hiện, nhận dạng và nhóm các khuôn mặt trong ảnh",
     "machine_learning_facial_recognition_model": "Mô hình nhận dạng khuôn mặt",
     "machine_learning_facial_recognition_model_description": "Các mô hình được liệt kê theo thứ tự kích thước giảm dần. Mô hình càng lớn, kết quả càng chính xác nhưng sẽ chạy chậm và tốn nhiều bộ nhớ hơn. Lưu ý rằng sau khi thay đổi mô hình, bạn cần chạy lại tác vụ \"Phát hiện Khuôn mặt\" cho tất cả hình ảnh.",
@@ -116,14 +124,14 @@
     "machine_learning_min_detection_score_description": "Mức điểm tin cậy tối thiểu để phát hiện khuôn mặt, từ 0 đến 1. Giá trị càng thấp, nhiều khuôn mặt sẽ được phát hiện nhưng có thể tăng khả năng phát hiện sai.",
     "machine_learning_min_recognized_faces": "Số khuôn mặt tối thiểu để nhận dạng",
     "machine_learning_min_recognized_faces_description": "Số khuôn mặt tối thiểu cần nhận dạng để tạo thành một người. Tăng số lượng này sẽ làm cho Nhận dạng khuôn mặt chính xác hơn, nhưng sẽ tăng khả năng một khuôn mặt không được gán cho người phù hợp.",
-    "machine_learning_settings": "Machine Learning",
-    "machine_learning_settings_description": "Quản lý các tính năng và cài đặt của machine learning",
-    "machine_learning_smart_search": "Tìm kiếm thông minh",
+    "machine_learning_settings": "Cài đặt Học máy",
+    "machine_learning_settings_description": "Quản lý các tính năng và cài đặt Học máy",
+    "machine_learning_smart_search": "Tìm kiếm Thông minh",
     "machine_learning_smart_search_description": "Tìm kiếm hình ảnh theo ngữ cảnh với CLIP",
-    "machine_learning_smart_search_enabled": "Bật tìm kiếm thông minh",
+    "machine_learning_smart_search_enabled": "Bật Tìm kiếm Thông minh",
     "machine_learning_smart_search_enabled_description": "Nếu tắt, hình ảnh sẽ không được mã hoá để tìm kiếm thông minh.",
-    "machine_learning_url_description": "Địa chỉ máy chủ machine learning",
-    "manage_concurrency": "Quản lý tác vụ",
+    "machine_learning_url_description": "Địa chỉ máy chủ Học máy",
+    "manage_concurrency": "Quản lý Tác vụ",
     "manage_log_settings": "Quản lý cài đặt nhật ký",
     "map_dark_style": "Giao diện tối",
     "map_enable_description": "Bật tính năng bản đồ",
@@ -132,18 +140,18 @@
     "map_implications": "Tính năng bản đồ phụ thuộc vào dịch vụ thẻ bản đồ bên ngoài (tiles.immich.cloud)",
     "map_light_style": "Giao diện sáng",
     "map_manage_reverse_geocoding_settings": "Quản lý cài đặt <link>Mã hóa địa lý ngược</link>",
-    "map_reverse_geocoding": "Mã hoá địa lý ngược (Reverse Geocoding)",
+    "map_reverse_geocoding": "Mã hoá Địa lý Ngược (Reverse Geocoding)",
     "map_reverse_geocoding_enable_description": "Bật mã hoá địa lý ngược",
-    "map_reverse_geocoding_settings": "Mã hoá địa lý ngược (Reverse Geocoding)",
+    "map_reverse_geocoding_settings": "Cài đặt Mã hoá Địa lý Ngược (Reverse Geocoding)",
     "map_settings": "Bản đồ",
     "map_settings_description": "Quản lý cài đặt bản đồ",
     "map_style_description": "Đường dẫn URL đến tập tin tuỳ biến bản đồ style.json",
-    "metadata_extraction_job": "Trích xuất siêu dữ liệu",
-    "metadata_extraction_job_description": "Trích xuất siêu dữ liệu từ mỗi ảnh, chẳng hạn như GPS, khuôn mặt và độ phân giải",
+    "metadata_extraction_job": "Trích xuất Metadata",
+    "metadata_extraction_job_description": "Trích xuất Metadata từ mỗi ảnh, chẳng hạn như GPS, khuôn mặt và độ phân giải",
     "metadata_faces_import_setting": "Bật tính năng nhập khuôn mặt",
     "metadata_faces_import_setting_description": "Nhập khuôn mặt từ dữ liệu EXIF hình ảnh và tập tin đi kèm",
-    "metadata_settings": "Siêu dữ liệu",
-    "metadata_settings_description": "Quản lý cài đặt siêu dữ liệu",
+    "metadata_settings": "Cài đặt Metadata",
+    "metadata_settings_description": "Quản lý cài đặt Metadata",
     "migration_job": "Di chuyển dữ liệu",
     "migration_job_description": "Di chuyển hình thu nhỏ của các ảnh và khuôn mặt sang cấu trúc thư mục mới",
     "no_paths_added": "Không có đường dẫn nào được thêm vào",
@@ -152,7 +160,7 @@
     "note_cannot_be_changed_later": "LƯU Ý: Cài đặt này không thể thay đổi được sau khi lưu!",
     "note_unlimited_quota": "Lưu ý: Nhập 0 để hạn mức không giới hạn",
     "notification_email_from_address": "Địa chỉ email người gửi",
-    "notification_email_from_address_description": "Địa chỉ email của người gửi, ví dụ: \"Immich Photo Server <noreply@immich.app>\"",
+    "notification_email_from_address_description": "Địa chỉ email của người gửi, ví dụ: \"Immich Photo Server <noreply@example.com>\"",
     "notification_email_host_description": "Địa chỉ máy chủ email (ví dụ: smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "Bỏ qua các lỗi chứng chỉ",
     "notification_email_ignore_certificate_errors_description": "Bỏ qua lỗi xác thực chứng chỉ TLS (không khuyến nghị)",
@@ -198,19 +206,19 @@
     "password_settings": "Mật khẩu đăng nhập",
     "password_settings_description": "Quản lý cài đặt mật khẩu đăng nhập",
     "paths_validated_successfully": "Tất cả các đường dẫn được xác minh thành công",
+    "person_cleanup_job": "Dọn dẹp người",
     "quota_size_gib": "Hạn mức (GiB)",
     "refreshing_all_libraries": "Làm mới tất cả các thư viện",
     "registration": "Đăng ký Quản trị viên",
     "registration_description": "Vì bạn là người dùng đầu tiên, bạn sẽ trở thành Quản trị viên và chịu trách nhiệm cho việc quản lý hệ thống. Ngoài ra, bạn có thể thêm các người dùng khác.",
-    "removing_offline_files": "Đang xoá các tập tin ngoại tuyến",
     "repair_all": "Sửa chữa tất cả",
     "repair_matched_items": "Đã tìm thấy {count, plural, one {# mục} other {# mục}} trùng khớp",
     "repaired_items": "Đã sửa chữa {count, plural, one{# mục} other {# mục}}",
     "require_password_change_on_login": "Yêu cầu người dùng thay đổi mật khẩu trong lần đăng nhập đầu tiên",
     "reset_settings_to_default": "Đặt lại cài đặt về mặc định",
     "reset_settings_to_recent_saved": "Đặt lại cài đặt về cài đặt trước đó",
-    "scanning_library_for_changed_files": "Đang quét thư viện để tìm các tập tin đã thay đổi",
-    "scanning_library_for_new_files": "Đang quét thư viện để tìm các tập tin mới",
+    "scanning_library": "Quét thư viện",
+    "search_jobs": "Tìm kiếm tác vụ...",
     "send_welcome_email": "Gửi email chào mừng",
     "server_external_domain_settings": "Tên miền công khai",
     "server_external_domain_settings_description": "Tên miền dành cho các liên kết chia sẻ công khai, bao gồm http(s)://",
@@ -238,6 +246,7 @@
     "storage_template_settings_description": "Quản lý cấu trúc thư mục và tên tập tin của ảnh tải lên",
     "storage_template_user_label": "Cụm từ <code>{label}</code> là Nhãn lưu trữ của người dùng",
     "system_settings": "Cài đặt hệ thống",
+    "tag_cleanup_job": "Dọn dẹp thẻ",
     "theme_custom_css_settings": "CSS tùy chỉnh",
     "theme_custom_css_settings_description": "Cascading Style Sheets cho phép tùy chỉnh thiết kế của Immich.",
     "theme_settings": "Chủ đề",
@@ -245,7 +254,6 @@
     "these_files_matched_by_checksum": "Các tập tin này khớp với các giá trị băm của chúng",
     "thumbnail_generation_job": "Tạo hình thu nhỏ",
     "thumbnail_generation_job_description": "Tạo hình thu nhỏ lớn, nhỏ và mờ cho mỗi ảnh, cũng như hình thu nhỏ cho mỗi người",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "API Tăng tốc",
     "transcoding_acceleration_api_description": "API này sẽ tương tác với thiết bị của bạn để tăng tốc quá trình chuyển mã. Cài đặt này hoạt động theo nguyên tắc 'cố gắng hết sức'': nó sẽ quay lại chuyển mã phần mềm nếu gặp lỗi. VP9 có thể hoạt động hoặc không tùy thuộc vào phần cứng của bạn.",
     "transcoding_acceleration_nvenc": "NVENC (yêu cầu GPU NVIDIA)",
@@ -271,7 +279,7 @@
     "transcoding_hardware_acceleration": "Tăng tốc phần cứng",
     "transcoding_hardware_acceleration_description": "(Thử nghiệm) nhanh hơn nhiều nhưng sẽ có chất lượng thấp hơn ở cùng bitrate",
     "transcoding_hardware_decoding": "Giải mã phần cứng",
-    "transcoding_hardware_decoding_setting_description": "Chỉ áp dụng cho NVENC, QSV và RKMPP. Kích hoạt tăng tốc toàn bộ quá trình xử lý video chứ không chỉ là mã hóa. Điều này có thể không áp dụng được cho mọi video.",
+    "transcoding_hardware_decoding_setting_description": "Cho phép tăng tốc đầu cuối thay vì chỉ tăng tốc mã hóa. Có thể không hoạt động trên tất cả video.",
     "transcoding_hevc_codec": "Codec HEVC",
     "transcoding_max_b_frames": "Số B-frame tối đa",
     "transcoding_max_b_frames_description": "Giá trị cao hơn cải thiện hiệu quả nén, nhưng làm chậm mã hóa. Có thể không tương thích với tăng tốc phần cứng trên các thiết bị cũ. Giá trị 0 để tắt B-frames, trong khi giá trị -1 để tự động thiết lập giá trị này.",
@@ -297,8 +305,6 @@
     "transcoding_threads_description": "Giá trị cao hơn dẫn đến mã hóa nhanh hơn nhưng để lại ít không gian hơn cho máy chủ xử lý các tác vụ khác khi đang hoạt động. Giá trị này không nên vượt quá số lượng lõi CPU. Tối đa hóa sử dụng nếu đặt thành 0.",
     "transcoding_tone_mapping": "Ánh Xạ Sắc Thái (Tone-mapping)",
     "transcoding_tone_mapping_description": "Cố gắng duy trì chất lượng video tốt nhất khi chuyển đổi từ HDR sang SDR. Mỗi thuật toán có sự đánh đổi khác nhau về màu sắc, chi tiết và độ sáng. Hable giữ chi tiết, Mobius giữ màu sắc và Reinhard giữ độ sáng.",
-    "transcoding_tone_mapping_npl": "Ánh Xạ Sắc Thái NPL (Tone-mapping NPL)",
-    "transcoding_tone_mapping_npl_description": "Màu sắc sẽ được điều chỉnh để trông bình thường với độ sáng của màn hình này. Theo cách trái ngược, giá trị thấp hơn sẽ tăng độ sáng của video và ngược lại vì nó bù đắp cho độ sáng của màn hình. Giá trị 0 để tự động thiết lập giá trị này.",
     "transcoding_transcode_policy": "Quy tắc chuyển mã",
     "transcoding_transcode_policy_description": "Quy tắc khi nào video nên được chuyển mã. Các video HDR luôn được chuyển mã (ngoại trừ khi tính năng chuyển mã bị tắt).",
     "transcoding_two_pass_encoding": "Mã hóa hai lần",
@@ -312,6 +318,7 @@
     "trash_settings_description": "Quản lý cài đặt thùng rác",
     "untracked_files": "Các tập tin không được theo dõi",
     "untracked_files_description": "Những tập tin này không được ứng dụng theo dõi. Chúng có thể là kết quả của các thao tác di chuyển thất bại, tải lên bị gián đoạn, hoặc bị bỏ lại do lỗi",
+    "user_cleanup_job": "Dọn dẹp người dùng",
     "user_delete_delay": "Tài khoản và các ảnh của <b>{user}</b> sẽ được lên lịch xóa vĩnh viễn sau {delay, plural, one {# ngày} other {# ngày}}.",
     "user_delete_delay_settings": "Thời gian xóa",
     "user_delete_delay_settings_description": "Số ngày chờ xóa để xóa vĩnh viễn tài khoản và các ảnh của người dùng. Tác vụ xóa người dùng chạy vào giữa đêm để kiểm tra các người dùng sẵn sàng bị xóa. Thay đổi cài đặt này sẽ được đánh giá vào lần thực hiện tiếp theo.",
@@ -378,7 +385,6 @@
   "archive_or_unarchive_photo": "Lưu trữ hoặc huỷ lưu trữ ảnh",
   "archive_size": "Kích thước gói nén",
   "archive_size_description": "Cấu hình kích thước nén cho các tập tin tải xuống (đơn vị GiB)",
-  "archived": "",
   "archived_count": "{count, plural, other {Đã lưu trữ # mục}}",
   "are_these_the_same_person": "Đây có phải cùng một người không?",
   "are_you_sure_to_do_this": "Bạn có chắc chắn muốn thực hiện điều này không?",
@@ -388,8 +394,8 @@
   "asset_filename_is_offline": "Ảnh {filename} đang ngoại tuyến",
   "asset_has_unassigned_faces": "Ảnh chưa được gán khuôn mặt",
   "asset_hashing": "Đang băm...",
-  "asset_offline": "Ảnh ngoại tuyến",
-  "asset_offline_description": "Tập tin này đang ngoại tuyến. Immich không thể truy cập vị trí tập tin của nó. Vui lòng đảm bảo tập tin có sẵn và sau đó quét lại thư viện.",
+  "asset_offline": "Ảnh Ngoại tuyến",
+  "asset_offline_description": "Tập tin bên ngoài này không còn trên ổ đĩa. Vui lòng liên hệ quản trị viên Immich của bạn để được trợ giúp.",
   "asset_skipped": "Đã bỏ qua",
   "asset_skipped_in_trash": "Trong thùng rác",
   "asset_uploaded": "Đã tải lên",
@@ -402,7 +408,7 @@
   "assets_moved_to_trash_count": "Đã chuyển {count, plural, one {# mục} other {# mục}} vào thùng rác",
   "assets_permanently_deleted_count": "Đã xóa vĩnh viễn {count, plural, one {# mục} other {# mục}}",
   "assets_removed_count": "Đã xóa {count, plural, one {# mục} other {# mục}}",
-  "assets_restore_confirmation": "Bạn có chắc chắn muốn khôi phục tất cả các mục đã xóa của mình không? Bạn không thể hoàn tác hành động này!",
+  "assets_restore_confirmation": "Bạn có chắc chắn muốn khôi phục tất cả các mục đã xóa của mình không? Bạn không thể hoàn tác hành động này! Lưu ý rằng không thể khôi phục các ảnh ngoại tuyến theo cách này.",
   "assets_restored_count": "Đã khôi phục {count, plural, one {# mục} other {# mục}}",
   "assets_trashed_count": "Đã chuyển {count, plural, one {# mục} other {# mục}} vào thùng rác",
   "assets_were_part_of_album_count": "{count, plural, one {Mục đã} other {Các mục đã}} có trong album",
@@ -413,6 +419,7 @@
   "birthdate_saved": "Ngày sinh đã được lưu thành công",
   "birthdate_set_description": "Ngày sinh được sử dụng để tính tuổi của người này tại thời điểm chụp ảnh.",
   "blurred_background": "Nền mờ",
+  "bugs_and_feature_requests": "Lỗi & Yêu cầu tính năng",
   "build": "Dựng",
   "build_image": "Bản dựng",
   "bulk_delete_duplicates_confirmation": "Bạn có chắc chắn muốn xóa hàng loạt {count, plural, one {# mục trùng lặp} other {# mục trùng lặp}} không? Điều này sẽ giữ lại ảnh chất lượng nhất của mỗi nhóm và xóa vĩnh viễn tất cả các bản trùng lặp khác. Bạn không thể hoàn tác hành động này!",
@@ -427,10 +434,6 @@
   "cannot_merge_people": "Không thể hợp nhất người",
   "cannot_undo_this_action": "Bạn không thể hoàn tác hành động này!",
   "cannot_update_the_description": "Không thể cập nhật mô tả",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
   "change_date": "Thay đổi ngày",
   "change_expiration_time": "Thay đổi thời gian hết hạn",
   "change_location": "Thay đổi vị trí",
@@ -462,6 +465,7 @@
   "confirm": "Xác nhận",
   "confirm_admin_password": "Xác nhận mật khẩu quản trị viên",
   "confirm_delete_shared_link": "Bạn có chắc chắn muốn xóa liên kết chia sẻ này không?",
+  "confirm_keep_this_delete_others": "Các hình còn lại trong stack này sẽ bị xoá ngoại trừ hình này. Bạn có chắc chắn tiếp tục không?",
   "confirm_password": "Xác nhận mật khẩu",
   "contain": "Chứa",
   "context": "Ngữ cảnh",
@@ -509,18 +513,21 @@
   "delete_api_key_prompt": "Bạn có chắc chắn muốn xóa khóa API này không?",
   "delete_duplicates_confirmation": "Bạn có chắc chắn muốn xóa vĩnh viễn các bản trùng lặp này không?",
   "delete_key": "Xóa khóa",
-  "delete_library": "Xóa thư viện",
+  "delete_library": "Xóa Thư viện",
   "delete_link": "Xóa liên kết",
+  "delete_others": "Xoá các hình còn lại",
   "delete_shared_link": "Xóa liên kết chia sẻ",
   "delete_tag": "Xóa thẻ",
   "delete_tag_confirmation_prompt": "Bạn có chắc chắn muốn xóa thẻ {tagName} không?",
   "delete_user": "Xóa người dùng",
   "deleted_shared_link": "Đã xóa liên kết chia sẻ",
+  "deletes_missing_assets": "Xóa các ảnh không còn tồn tại trên ổ đĩa",
   "description": "Mô tả",
   "details": "Chi tiết",
   "direction": "Hướng",
   "disabled": "Tắt",
   "disallow_edits": "Không cho phép chỉnh sửa",
+  "discord": "Discord",
   "discover": "Tìm",
   "dismiss_all_errors": "Bỏ qua tất cả lỗi",
   "dismiss_error": "Bỏ qua lỗi",
@@ -529,6 +536,7 @@
   "display_original_photos": "Hiển thị ảnh gốc",
   "display_original_photos_setting_description": "Ưu tiên hiển thị ảnh gốc khi xem ảnh thay vì hình thu nhỏ khi ảnh gốc tương thích với web. Điều này có thể dẫn đến tốc độ hiển thị ảnh chậm hơn.",
   "do_not_show_again": "Không hiển thị thông báo này nữa",
+  "documentation": "Tài liệu",
   "done": "Xong",
   "download": "Tải xuống",
   "download_include_embedded_motion_videos": "Các video nhúng",
@@ -541,13 +549,6 @@
   "duplicates": "Mục trùng lặp",
   "duplicates_description": "Xem lại các nhóm ảnh bị nghi ngờ trùng lặp và chọn những mục bạn muốn giữ hoặc xóa",
   "duration": "Thời gian",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
   "edit": "Chỉnh sửa",
   "edit_album": "Chỉnh sửa album",
   "edit_avatar": "Chỉnh sửa ảnh đại diện",
@@ -572,8 +573,6 @@
   "editor_crop_tool_h2_aspect_ratios": "Tỷ lệ khung hình",
   "editor_crop_tool_h2_rotation": "Xoay",
   "email": "Email",
-  "empty": "",
-  "empty_album": "",
   "empty_trash": "Dọn sạch thùng rác",
   "empty_trash_confirmation": "Bạn có chắc chắn muốn dọn sạch thùng rác không? Điều này sẽ xóa vĩnh viễn tất cả các mục trong thùng rác khỏi Immich.\nBạn không thể hoàn tác hành động này!",
   "enable": "Bật",
@@ -607,6 +606,7 @@
     "failed_to_create_shared_link": "Không thể tạo liên kết chia sẻ",
     "failed_to_edit_shared_link": "Không thể chỉnh sửa liên kết chia sẻ",
     "failed_to_get_people": "Không thể tải người",
+    "failed_to_keep_this_delete_others": "Có lỗi trong quá trình xoá các hình",
     "failed_to_load_asset": "Không thể tải ảnh",
     "failed_to_load_assets": "Không thể tải các ảnh",
     "failed_to_load_people": "Không thể tải người",
@@ -634,8 +634,6 @@
     "unable_to_change_location": "Không thể thay đổi vị trí",
     "unable_to_change_password": "Không thể thay đổi mật khẩu",
     "unable_to_change_visibility": "Không thể thay đổi trạng thái hiển thị cho {count, plural, one {# người} other {# người}}",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_complete_oauth_login": "Không thể hoàn tất đăng nhập OAuth",
     "unable_to_connect": "Không thể kết nối",
     "unable_to_connect_to_server": "Không thể kết nối đến máy chủ",
@@ -660,6 +658,7 @@
     "unable_to_get_comments_number": "Không thể lấy số lượng bình luận",
     "unable_to_get_shared_link": "Không thể lấy liên kết chia sẻ",
     "unable_to_hide_person": "Không thể ẩn người",
+    "unable_to_link_motion_video": "Không thể liên kết video chuyển động",
     "unable_to_link_oauth_account": "Không thể liên kết tài khoản OAuth",
     "unable_to_load_album": "Không thể tải album",
     "unable_to_load_asset_activity": "Không thể tải hoạt động của ảnh",
@@ -675,12 +674,10 @@
     "unable_to_remove_album_users": "Không thể xóa người dùng khỏi album",
     "unable_to_remove_api_key": "Không thể xóa khóa API",
     "unable_to_remove_assets_from_shared_link": "Không thể xóa các mục đã chọn khỏi liên kết chia sẻ",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_deleted_assets": "Không thể xóa tập tin ngoại tuyến",
     "unable_to_remove_library": "Không thể xóa thư viện",
-    "unable_to_remove_offline_files": "Không thể xóa tập tin ngoại tuyến",
     "unable_to_remove_partner": "Không thể xóa người thân",
     "unable_to_remove_reaction": "Không thể xóa phản ứng",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "Không thể sửa chữa các mục",
     "unable_to_reset_password": "Không thể đặt lại mật khẩu",
     "unable_to_resolve_duplicate": "Không thể xử lý trùng lặp",
@@ -700,6 +697,7 @@
     "unable_to_submit_job": "Không thể gửi tác vụ",
     "unable_to_trash_asset": "Không thể chuyển ảnh vào thùng rác",
     "unable_to_unlink_account": "Không thể hủy liên kết tài khoản",
+    "unable_to_unlink_motion_video": "Không thể hủy liên kết video chuyển động",
     "unable_to_update_album_cover": "Không thể cập nhật ảnh bìa album",
     "unable_to_update_album_info": "Không thể cập nhật thông tin album",
     "unable_to_update_library": "Không thể cập nhật thư viện",
@@ -709,10 +707,6 @@
     "unable_to_update_user": "Không thể cập nhật người dùng",
     "unable_to_upload_file": "Không thể tải tập tin lên"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
   "exif": "Exif",
   "exit_slideshow": "Thoát trình chiếu",
   "expand_all": "Mở rộng tất cả",
@@ -727,33 +721,27 @@
   "external": "Bên ngoài",
   "external_libraries": "Thư viện bên ngoài",
   "face_unassigned": "Chưa được gán",
-  "failed_to_get_people": "",
   "favorite": "Yêu thích",
   "favorite_or_unfavorite_photo": "Yêu thích hoặc bỏ yêu thích ảnh",
   "favorites": "Ảnh yêu thích",
-  "feature": "",
   "feature_photo_updated": "Đã cập nhật ảnh nổi bật",
-  "featurecollection": "",
   "features": "Tính năng",
   "features_setting_description": "Quản lý các tính năng ứng dụng",
   "file_name": "Tên tập tin",
   "file_name_or_extension": "Tên hoặc phần mở rộng tập tin",
   "filename": "Tên tập tin",
-  "files": "",
   "filetype": "Loại tập tin",
   "filter_people": "Lọc người",
   "find_them_fast": "Tìm nhanh bằng tên với tìm kiếm",
   "fix_incorrect_match": "Sửa lỗi trùng khớp không chính xác",
   "folders": "Thư mục",
   "folders_feature_description": "Duyệt ảnh và video theo thư mục trên hệ thống tập tin",
-  "force_re-scan_library_files": "Yêu cầu quét lại tất cả các tập tin thư viện",
   "forward": "Tiến về phía trước",
   "general": "Chung",
   "get_help": "Nhận trợ giúp",
   "getting_started": "Bắt đầu",
   "go_back": "Quay lại",
   "go_to_search": "Đi đến tìm kiếm",
-  "go_to_share_page": "Đi đến trang chia sẻ",
   "group_albums_by": "Nhóm album theo...",
   "group_no": "Không nhóm",
   "group_owner": "Nhóm theo chủ sở hữu",
@@ -779,7 +767,6 @@
   "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp tại {city}, {country} với {person1} và {person2} vào {date}",
   "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp tại {city}, {country} với {person1}, {person2}, và {person3} vào {date}",
   "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp tại {city}, {country} với {person1}, {person2}, và {additionalCount, number} người khác vào {date}",
-  "img": "",
   "immich_logo": "Logo Immich",
   "immich_web_interface": "Giao diện web Immich",
   "import_from_json": "Nhập từ JSON",
@@ -800,10 +787,10 @@
   "invite_people": "Mời mọi người",
   "invite_to_album": "Mời vào album",
   "items_count": "{count, plural, one {# mục} other {# mục}}",
-  "job_settings_description": "",
   "jobs": "Tác vụ",
   "keep": "Giữ",
   "keep_all": "Giữ tất cả",
+  "keep_this_delete_others": "Giữ tấm này và xoá tất cả còn lại",
   "keyboard_shortcuts": "Phím tắt",
   "language": "Ngôn ngữ",
   "language_setting_description": "Chọn ngôn ngữ ưa thích của bạn",
@@ -836,6 +823,7 @@
   "look": "Xem",
   "loop_videos": "Lặp video",
   "loop_videos_description": "Bật để video tự động lặp lại trong trình xem chi tiết.",
+  "main_branch_warning": "Bạn đang dùng phiên bản đang phát triển; chúng tôi khuyên bạn nên dùng phiên bản phát hành!",
   "make": "Thương hiệu",
   "manage_shared_links": "Quản lý liên kết chia sẻ",
   "manage_sharing_with_partners": "Quản lý chia sẻ với người thân",
@@ -905,6 +893,7 @@
   "notifications": "Thông báo",
   "notifications_setting_description": "Quản lý thông báo",
   "oauth": "OAuth",
+  "official_immich_resources": "Tài nguyên chính thức của Immich",
   "offline": "Ngoại tuyến",
   "offline_paths": "Đường dẫn ngoại tuyến",
   "offline_paths_description": "Những kết quả này có thể do việc xóa thủ công các tập tin không phải là một phần của thư viện bên ngoài.",
@@ -917,7 +906,6 @@
   "onboarding_welcome_user": "Chào mừng, {user}",
   "online": "Trực tuyến",
   "only_favorites": "Chỉ yêu thích",
-  "only_refreshes_modified_files": "Chỉ làm mới các tập tin đã thay đổi",
   "open_in_map_view": "Mở trong bản đồ",
   "open_in_openstreetmap": "Mở trong OpenStreetMap",
   "open_the_search_filters": "Mở bộ lọc tìm kiếm",
@@ -955,7 +943,6 @@
   "people_edits_count": "Đã chỉnh sửa {count, plural, one {# người} other {# người}}",
   "people_feature_description": "Duyệt ảnh và video được nhóm theo người",
   "people_sidebar_description": "Hiển thị mục Mọi người trong thanh bên",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "Cảnh báo xóa vĩnh viễn",
   "permanent_deletion_warning_setting_description": "Hiển thị cảnh báo khi xóa vĩnh viễn ảnh",
   "permanently_delete": "Xóa vĩnh viễn",
@@ -977,7 +964,6 @@
   "play_memories": "Phát kỷ niệm",
   "play_motion_photo": "Phát ảnh chuyển động",
   "play_or_pause_video": "Phát hoặc tạm dừng video",
-  "point": "",
   "port": "Cổng",
   "preset": "Mẫu có sẵn",
   "preview": "Xem trước",
@@ -1022,12 +1008,10 @@
   "purchase_server_description_2": "Trạng thái người hỗ trợ",
   "purchase_server_title": "Máy chủ",
   "purchase_settings_server_activated": "Khóa sản phẩm máy chủ được quản lý bởi quản trị viên",
-  "range": "",
   "rating": "Xếp hạng sao",
   "rating_clear": "Xóa đánh giá",
   "rating_count": "{count, plural, one {# sao} other {# sao}}",
   "rating_description": "Hiển thị xếp hạng EXIF trong bảng thông tin",
-  "raw": "",
   "reaction_options": "Tùy chọn phản ứng",
   "read_changelog": "Đọc nhật ký thay đổi",
   "reassign": "Gán lại",
@@ -1038,11 +1022,13 @@
   "recent_searches": "Tìm kiếm gần đây",
   "refresh": "Làm mới",
   "refresh_encoded_videos": "Làm mới video đã mã hóa",
+  "refresh_faces": "Làm mới khuôn mặt",
   "refresh_metadata": "Làm mới siêu dữ liệu",
   "refresh_thumbnails": "Làm mới hình thu nhỏ",
   "refreshed": "Đã làm mới",
-  "refreshes_every_file": "Làm mới mọi tập tin",
+  "refreshes_every_file": "Đọc lại tất cả tập tin mới và hiện có",
   "refreshing_encoded_video": "Đang làm mới video đã mã hóa",
+  "refreshing_faces": "Đang làm mới khuôn mặt",
   "refreshing_metadata": "Đang làm mới siêu dữ liệu",
   "regenerating_thumbnails": "Đang tạo lại hình thu nhỏ",
   "remove": "Xóa",
@@ -1050,10 +1036,10 @@
   "remove_assets_shared_link_confirmation": "Bạn có chắc chắn muốn xoá {count, plural, one {# mục} other {# mục}} khỏi liên kết chia sẻ này?",
   "remove_assets_title": "Xóa mục?",
   "remove_custom_date_range": "Bỏ chọn khoảng ngày tùy chỉnh",
+  "remove_deleted_assets": "Loại bỏ tập tin ngoại tuyến",
   "remove_from_album": "Xóa khỏi album",
   "remove_from_favorites": "Xóa khỏi Mục yêu thích",
   "remove_from_shared_link": "Xóa khỏi liên kết chia sẻ",
-  "remove_offline_files": "Loại bỏ tập tin ngoại tuyến",
   "remove_user": "Xóa người dùng",
   "removed_api_key": "Khóa API đã xóa: {name}",
   "removed_from_archive": "Đã xoá khỏi Kho lưu trữ",
@@ -1070,7 +1056,6 @@
   "reset": "Đặt lại",
   "reset_password": "Đặt lại mật khẩu",
   "reset_people_visibility": "Đặt lại trạng thái hiển thị của mọi người",
-  "reset_settings_to_default": "",
   "reset_to_default": "Đặt lại về mặc định",
   "resolve_duplicates": "Xử lý các bản trùng lặp",
   "resolved_all_duplicates": "Đã xử lý tất cả các bản trùng lặp",
@@ -1090,8 +1075,7 @@
   "saved_settings": "Cài đặt đã lưu",
   "say_something": "Nói điều gì đó",
   "scan_all_libraries": "Quét tất cả thư viện",
-  "scan_all_library_files": "Quét lại tất cả các tập tin thư viện",
-  "scan_new_library_files": "Quét các tập tin thư viện mới",
+  "scan_library": "Quét",
   "scan_settings": "Cài đặt quét",
   "scanning_for_album": "Đang quét album...",
   "search": "Tìm kiếm",
@@ -1109,6 +1093,7 @@
   "search_options": "Tùy chọn tìm kiếm",
   "search_people": "Tìm kiếm người",
   "search_places": "Tìm kiếm địa điểm",
+  "search_settings": "Cài đặt tìm kiếm",
   "search_state": "Tìm kiếm tỉnh...",
   "search_tags": "Tìm kiếm thẻ...",
   "search_timezone": "Tìm kiếm múi giờ...",
@@ -1133,7 +1118,6 @@
   "selected_count": "{count, plural, other {Đã chọn # mục}}",
   "send_message": "Gửi tin nhắn",
   "send_welcome_email": "Gửi email chào mừng",
-  "server": "",
   "server_offline": "Máy chủ ngoại tuyến",
   "server_online": "Máy chủ trực tuyến",
   "server_stats": "Thống kê máy chủ",
@@ -1176,6 +1160,7 @@
   "show_person_options": "Hiển thị tùy chọn người",
   "show_progress_bar": "Hiển thị thanh tiến trình",
   "show_search_options": "Hiển thị tùy chọn tìm kiếm",
+  "show_slideshow_transition": "Hiển thị hiệu ứng chuyển tiếp",
   "show_supporter_badge": "Huy hiệu người ủng hộ",
   "show_supporter_badge_description": "Hiển thị huy hiệu người ủng hộ",
   "shuffle": "Xáo trộn",
@@ -1217,13 +1202,16 @@
   "submit": "Gửi",
   "suggestions": "Gợi ý",
   "sunrise_on_the_beach": "Bình minh trên bãi biển",
+  "support": "Hỗ trợ",
+  "support_and_feedback": "Hỗ trợ & Góp ý",
+  "support_third_party_description": "Bản cài đặt Immich của bạn được đóng gói bởi một bên thứ ba. Các sự cố bạn gặp phải có thể do gói đó gây ra, vì vậy vui lòng báo cáo sự cố với họ trước bằng cách sử dụng các liên kết bên dưới.",
   "swap_merge_direction": "Đổi hướng hợp nhất",
   "sync": "Đồng bộ",
   "tag": "Thẻ",
   "tag_assets": "Gắn thẻ",
   "tag_created": "Đã tạo thẻ: {tag}",
   "tag_feature_description": "Duyệt ảnh và video được nhóm theo chủ đề thẻ hợp lý",
-  "tag_not_found_question": "Không tìm thấy thẻ? Tạo một cái <link>tại đây</link>",
+  "tag_not_found_question": "Không tìm thấy thẻ? <link>Tạo một thẻ mới</link>",
   "tag_updated": "Đã cập nhật thẻ: {tag}",
   "tagged_assets": "Đã gắn thẻ {count, plural, one {# mục} other {# mục}}",
   "tags": "Thẻ",
@@ -1232,6 +1220,7 @@
   "theme_selection": "Chủ đề tổng thể",
   "theme_selection_description": "Tự động đặt chủ đề sáng hoặc tối dựa trên tùy chọn hệ thống của trình duyệt của bạn",
   "they_will_be_merged_together": "Chúng sẽ được hợp nhất với nhau",
+  "third_party_resources": "Tài nguyên bên thứ ba",
   "time_based_memories": "Kỷ niệm dựa trên thời gian",
   "timezone": "Múi giờ",
   "to_archive": "Lưu trữ",
@@ -1239,11 +1228,9 @@
   "to_favorite": "Yêu thích",
   "to_login": "Đăng nhập",
   "to_parent": "Đến thư mục cha",
-  "to_root": "Tới thư mục gốc",
   "to_trash": "Xóa",
   "toggle_settings": "Chuyển đổi cài đặt",
   "toggle_theme": "Chuyển đổi chủ đề tối",
-  "toggle_visibility": "",
   "total_usage": "Tổng dung lượng đã sử dụng",
   "trash": "Thùng rác",
   "trash_all": "Xóa hết",
@@ -1253,14 +1240,13 @@
   "trashed_items_will_be_permanently_deleted_after": "Các mục đã xóa sẽ bị xóa vĩnh viễn sau {days, plural, one {# ngày} other {# ngày}}.",
   "type": "Loại",
   "unarchive": "Huỷ lưu trữ",
-  "unarchived": "",
   "unarchived_count": "{count, plural, other {Đã huỷ lưu trữ # mục}}",
   "unfavorite": "Bỏ yêu thích",
   "unhide_person": "Hiện người",
   "unknown": "Không xác định",
-  "unknown_album": "",
   "unknown_year": "Năm không xác định",
   "unlimited": "Không giới hạn",
+  "unlink_motion_video": "Hủy liên kết video chuyển động",
   "unlink_oauth": "Huỷ liên kết OAuth",
   "unlinked_oauth_account": "Đã huỷ liên kết tài khoản OAuth",
   "unnamed_album": "Album chưa đặt tên",
@@ -1301,7 +1287,9 @@
   "variables": "Các tham số",
   "version": "Phiên bản",
   "version_announcement_closing": "Bạn của bạn, Alex",
-  "version_announcement_message": "Chào bạn, có một phiên bản mới của ứng dụng. Vui lòng dành thời gian để xem <link>ghi chú phát hành</link> và đảm bảo rằng cấu hình <code>docker-compose.yml</code> và <code>.env</code> của bạn được cập nhật để tránh bất kỳ cấu hình sai nào, đặc biệt nếu bạn sử dụng WatchTower hoặc bất kỳ cơ chế nào tự động cập nhật ứng dụng của bạn.",
+  "version_announcement_message": "Chào bạn! Một phiên bản mới của Immich đã phát hành. Vui lòng dành thời gian để xem <link>danh sách thay đổi</link>  để đảm bảo cấu hình của bạn được cập nhật để tránh lỗi cấu hình sai, đặc biệt nếu bạn sử dụng WatchTower hoặc bất kỳ cơ chế tự động cập nhật Immich của bạn.",
+  "version_history": "Lịch sử phiên bản",
+  "version_history_item": "Đã cài đặt {version} vào {date}",
   "video": "Video",
   "video_hover_setting": "Phát đoạn video xem trước khi di chuột",
   "video_hover_setting_description": "Phát đoạn video xem trước khi di chuột qua mục. Ngay cả khi tắt chức năng này, vẫn có thể bắt đầu phát video bằng cách di chuột qua biểu tượng phát.",
@@ -1316,13 +1304,12 @@
   "view_next_asset": "Xem ảnh tiếp theo",
   "view_previous_asset": "Xem ảnh trước đó",
   "view_stack": "Xem nhóm ảnh",
-  "viewer": "",
   "visibility_changed": "Đã thay đổi trạng thái hiển thị cho {count, plural, one {# người} other {# người}}",
   "waiting": "Đang chờ",
   "warning": "Cảnh báo",
   "week": "Tuần",
   "welcome": "Chào mừng",
-  "welcome_to_immich": "Chào mừng đến với immich",
+  "welcome_to_immich": "Chào mừng đến với Immich",
   "year": "Năm",
   "years_ago": "{years, plural, one {# năm} other {# năm}} trước",
   "yes": "Có",
diff --git a/web/src/lib/i18n/zh_Hant.json b/i18n/zh_Hant.json
similarity index 85%
rename from web/src/lib/i18n/zh_Hant.json
rename to i18n/zh_Hant.json
index 30e32f60c9..d343f89544 100644
--- a/web/src/lib/i18n/zh_Hant.json
+++ b/i18n/zh_Hant.json
@@ -1,86 +1,95 @@
 {
-  "about": "關於",
+  "about": "重新整理",
   "account": "帳號",
   "account_settings": "帳號設定",
   "acknowledge": "收到",
-  "action": "行爲",
-  "actions": "行爲",
+  "action": "操作",
+  "actions": "操作",
   "active": "處理中",
-  "activity": "活動",
-  "activity_changed": "活動已{enabled, select, true {啟用} other {停用}}",
-  "add": "新增",
-  "add_a_description": "新增敘述",
-  "add_a_location": "新增位置",
-  "add_a_name": "新增名稱",
+  "activity": "動態",
+  "activity_changed": "動態已{enabled, select, true {啟用} other {停用}}",
+  "add": "加入",
+  "add_a_description": "加入文字說明",
+  "add_a_location": "新增地點",
+  "add_a_name": "加入姓名",
   "add_a_title": "新增標題",
-  "add_exclusion_pattern": "新增排除規則",
+  "add_exclusion_pattern": "加入篩選條件",
   "add_import_path": "新增匯入路徑",
   "add_location": "新增地點",
-  "add_more_users": "新增更多使用者",
-  "add_partner": "新增同伴",
+  "add_more_users": "新增其他使用者",
+  "add_partner": "新增親朋好友",
   "add_path": "新增路徑",
   "add_photos": "加入照片",
-  "add_to": "新增至…",
-  "add_to_album": "加入相簿",
-  "add_to_shared_album": "加入共享相簿",
-  "added_to_archive": "已加入封存",
-  "added_to_favorites": "已加入收藏",
-  "added_to_favorites_count": "已把 {count, number} 個項目加入收藏",
+  "add_to": "加入到…",
+  "add_to_album": "加入到相簿",
+  "add_to_shared_album": "加到共享相簿",
+  "add_url": "新增URL",
+  "added_to_archive": "封存",
+  "added_to_favorites": "加入收藏",
+  "added_to_favorites_count": "將 {count, number} 個項目加入收藏",
   "admin": {
-    "add_exclusion_pattern_description": "新增排除規則。支援使用「*」、「 **」、「?」來匹配字串。如果要排除所有名稱為「Raw」的檔案或目錄,請使用「**/Raw/**」。如果要排除所有「.tif」結尾的檔案,請使用「**/*.tif」。如果要排除某個絕對路徑,請使用「/path/to/ignore/**」。",
+    "add_exclusion_pattern_description": "新增排除條件。支援使用「*」、「 **」、「?」來找尋符合規則的字串。如果要在任何名為「Raw」的目錄內排除所有符合條件的檔案,請使用「**/Raw/**」。如果要排除所有「.tif」結尾的檔案,請使用「**/*.tif」。如果要排除某個絕對路徑,請使用「/path/to/ignore/**」。",
+    "asset_offline_description": "磁碟上找不到此外部相簿檔案,且已移至垃圾桶。如果檔案在相簿內被移動,請檢查時間軸中是否有新的相應的檔案。若要還原這份檔案,請確保 Immich 可以寫入下列檔案路徑,並讀取掃描相簿內容。",
     "authentication_settings": "驗證設定",
     "authentication_settings_description": "管理密碼、OAuth 與其他驗證設定",
     "authentication_settings_disable_all": "確定要停用所有登入方式嗎?這樣會完全無法登入。",
     "authentication_settings_reenable": "如需重新啟用,請使用 <link>伺服器指令</link>。",
-    "background_task_job": "背景任務",
+    "background_task_job": "背景執行",
+    "backup_database": "備份資料庫",
+    "backup_database_enable_description": "啟用資料庫備份",
+    "backup_keep_last_amount": "保留先前備份的數量",
+    "backup_settings": "備份設定",
+    "backup_settings_description": "管理資料庫備份設定",
     "check_all": "全選",
-    "cleared_jobs": "已清除 {job} 的任務",
-    "config_set_by_file": "目前的設定已透過設定檔案設置",
-    "confirm_delete_library": "確定要刪除「{library}」(圖庫)嗎?",
-    "confirm_delete_library_assets": "您確定要刪除此圖庫嗎?這將從 Immich 中刪除{count, plural, one {個項目} other {個項目}},且無法復原。檔案仍會保留在硬碟中。",
+    "cleared_jobs": "已刪除「{job}」任務",
+    "config_set_by_file": "已透過設定檔更新設定",
+    "confirm_delete_library": "確定要刪除 {library} 相簿嗎?",
+    "confirm_delete_library_assets": "您確定要刪除此相簿嗎?這將從 Immich 中刪除 {count, plural, one {個項目} other {個項目}} ,且無法復原。檔案仍會保留在硬碟中。",
     "confirm_email_below": "請在底下輸入 {email} 來確認",
     "confirm_reprocess_all_faces": "確定要重新處理所有臉孔嗎?這會清除已命名的人物。",
     "confirm_user_password_reset": "您確定要重設 {user} 的密碼嗎?",
-    "crontab_guru": "",
+    "create_job": "建立作業",
+    "cron_expression": "Cron 運算式",
+    "cron_expression_description": "以 Cron 格式設定掃描時段。詳細資訊請參閱 <link>Crontab Guru</link>",
+    "cron_expression_presets": "現成的 Cron 運算式",
     "disable_login": "停用登入",
-    "disabled": "已禁用",
     "duplicate_detection_job_description": "對檔案執行機器學習來偵測相似圖片。(此功能仰賴智慧搜尋)",
     "exclusion_pattern_description": "排除規則讓您在掃描資料庫時忽略特定文件和文件夾。用於當您有不想導入的文件(例如 RAW 文件)或文件夾。",
-    "external_library_created_at": "外部圖庫(於 {date} 建立)",
-    "external_library_management": "外部圖庫管理",
+    "external_library_created_at": "外部相簿(於 {date} 建立)",
+    "external_library_management": "外部相簿管理",
     "face_detection": "臉孔偵測",
-    "face_detection_description": "使用機器學習檢測資料中的人臉。影片檔只會偵測縮圖。選擇「全部」將重新處理所有資料。選擇「缺失」將把尚未處理的資料加入處理佇列中。被檢測到的人臉將在所有人臉檢測完成後,排入人臉識別佇列中,並將它們分配到現有或新的人物中。",
-    "facial_recognition_job_description": "將檢測到的人臉分組到人物中。此步驟將在人臉檢測完成後運行。選擇「全部」將重新分類所有人臉。選擇「缺失」將把沒有分配人物的人臉排入佇列。",
+    "face_detection_description": "使用機器學習偵測檔案中的臉孔(影片只會偵測縮圖中的臉孔)。選擇「重新整理」會重新處理所有檔案。選擇「重設」會清除目前所有的臉孔資料。選擇「遺失的」會把尚未處理的檔案排入處理佇列。臉孔偵測完成後,會把偵測到的臉孔排入臉部辨識佇列,將其分組到現有的或新的人物中。",
+    "facial_recognition_job_description": "將偵測到的臉孔依照人物分組。此步驟會在臉孔偵測完成後執行。選擇「重設」會重新分組所有臉孔。選擇「遺失的」會把尚未指定人物的臉孔排入佇列。",
     "failed_job_command": "{job} 任務的 {command} 指令執行失敗",
-    "force_delete_user_warning": "警告:這將立即移除使用者及其資料。操作後無法反悔且移除的檔案無法恢復。",
+    "force_delete_user_warning": "警告:這將立即刪除使用者及其資料。操作後無法反悔且刪除的檔案無法恢復。",
     "forcing_refresh_library_files": "強制重新整理所有圖庫檔案",
+    "image_format": "格式",
     "image_format_description": "WebP 能產生相對於 JPEG 更小的檔案,但編碼速度較慢。",
     "image_prefer_embedded_preview": "偏好嵌入的預覽",
     "image_prefer_embedded_preview_setting_description": "優先使用 RAW 的嵌入預覧作影像處理。可以提升某些影像的顏色精確度,但嵌入預覧的影像品質依相機而異,且可能壓縮較多。",
     "image_prefer_wide_gamut": "偏好廣色域",
     "image_prefer_wide_gamut_setting_description": "使用 Display P3 來製作縮圖。這可以更好地保留廣色域圖片的鮮豔度,但在舊版瀏覽器或舊設備上,圖片可能會顯示不同。sRGB 圖片會維持 sRGB 以避免顏色變化。",
-    "image_preview_format": "預覽格式",
-    "image_preview_resolution": "預覽解析度",
-    "image_preview_resolution_description": "觀賞單張照片及機器學習時用。較高的解析度可以保留更多細節,但編碼時間較長,檔案也較大,且可能降低應用程式的響應速度。",
+    "image_preview_description": "刪除中等尺寸圖片的詳細資料,當選擇看指定項目和機器學習時使用",
+    "image_preview_quality_description": "預覽品質爲 1 ~ 100。數值越大品質越高,但會產生較大的檔案,且可能降低應用程式的響應速度。而數值較小可能會影響機器學習品質。",
+    "image_preview_title": "預覽設定",
     "image_quality": "品質",
-    "image_quality_description": "圖片品質從1到100,數值越高代表品質越好但檔案也越大,此選項影響預覽和縮圖圖片。",
+    "image_resolution": "解析度",
+    "image_resolution_description": "較高的解析度可以保留更多細節,但編碼時間較長,檔案較大且可能降低應用程式的響應速度。",
     "image_settings": "圖片設定",
     "image_settings_description": "管理產生圖片的品質和解析度",
-    "image_thumbnail_format": "縮圖格式",
-    "image_thumbnail_resolution": "縮圖解析度",
-    "image_thumbnail_resolution_description": "觀賞多張照片時(時間軸、相簿等)用。較高的解析度可以保留更多細節,但編碼時間較長,檔案也較大,且可能降低應用程式的響應速度。",
+    "image_thumbnail_description": "刪除縮圖的詳細資料,在快速瀏覽重要時間軸時或大量照片時使用",
+    "image_thumbnail_quality_description": "縮圖品質爲 1 ~ 100。數值越大品質越高,但會產生較大的檔案,且可能降低應用程式的響應速度。",
+    "image_thumbnail_title": "縮圖設定",
     "job_concurrency": "{job}並行",
+    "job_created": "已建立作業",
     "job_not_concurrency_safe": "這個任務並行並不安全。",
-    "job_settings": "任務設定",
-    "job_settings_description": "管理任務並行",
-    "job_status": "任務狀態",
-    "jobs_delayed": "{jobCount, plural, other {# 項任務延遲}}",
-    "jobs_failed": "{jobCount, plural, other {# 項}}任務失敗",
+    "job_settings": "作業設定",
+    "job_settings_description": "管理作業並行",
+    "job_status": "作業狀態",
+    "jobs_delayed": "已延後 {jobCount, plural, other {# 項作業}}",
+    "jobs_failed": "{jobCount, plural, other {# 項}}作業失敗",
     "library_created": "已建立圖庫:{library}",
-    "library_cron_expression": "Cron 運算式",
-    "library_cron_expression_description": "以 Cron 格式設定掃描時段。詳細資訊請參閱 <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "現成的 Cron 運算式",
-    "library_deleted": "圖庫已刪除",
+    "library_deleted": "相簿已刪除",
     "library_import_path_description": "選取要載入的資料夾。以掃描資料夾(含子資料夾)內的影像和影片。",
     "library_scanning": "定期掃描",
     "library_scanning_description": "定期圖庫掃描設定",
@@ -95,7 +104,7 @@
     "logging_level_description": "啟用時的記錄層級。",
     "logging_settings": "記錄檔",
     "machine_learning_clip_model": "CLIP 模型",
-    "machine_learning_clip_model_description": "CLIP 模型 <link>名稱列表</link>。更換模型後須對所有影像重新執行「智慧搜尋」。",
+    "machine_learning_clip_model_description": "<link>這裏</link>有份 CLIP 模型名單。註:更換模型後須對所有圖片重新執行「智慧搜尋」作業。",
     "machine_learning_duplicate_detection": "重複項目偵測",
     "machine_learning_duplicate_detection_enabled": "啟用重複項目偵測",
     "machine_learning_duplicate_detection_enabled_description": "即使停用,完全一樣的素材仍會被忽略。",
@@ -122,7 +131,7 @@
     "machine_learning_smart_search_description": "使用 CLIP 嵌入進行語義圖像搜尋",
     "machine_learning_smart_search_enabled": "啟用智慧搜尋",
     "machine_learning_smart_search_enabled_description": "如果停用,圖片將不會被編碼以進行智能搜尋。",
-    "machine_learning_url_description": "機器學習伺服器的網址",
+    "machine_learning_url_description": "機器學習伺服器的網址,如果提供多個 URL,則將按依序嘗試連接每個伺服器,直到有一個伺服器成功回應為止。",
     "manage_concurrency": "管理並行",
     "manage_log_settings": "管理日誌設定",
     "map_dark_style": "深色樣式",
@@ -139,11 +148,11 @@
     "map_settings_description": "管理地圖設定",
     "map_style_description": "地圖主題(style.json)的網址",
     "metadata_extraction_job": "擷取元資料",
-    "metadata_extraction_job_description": "擷取每個檔案的 GPS、臉孔、解析度等元資料資訊",
+    "metadata_extraction_job_description": "擷取所有檔案的 GPS、臉孔、解析度等原始詳細資料",
     "metadata_faces_import_setting": "啟用臉孔匯入",
     "metadata_faces_import_setting_description": "從圖片的 EXIF 資料和側接檔案匯入臉孔",
-    "metadata_settings": "元資料設定",
-    "metadata_settings_description": "管理元資料設定",
+    "metadata_settings": "詳細資料設定",
+    "metadata_settings_description": "管理詮釋資料設定",
     "migration_job": "遷移",
     "migration_job_description": "將照片和人臉的縮圖遷移到最新的文件夾結構",
     "no_paths_added": "未添加路徑",
@@ -152,7 +161,7 @@
     "note_cannot_be_changed_later": "註:之後就無法更改嘍!",
     "note_unlimited_quota": "註:輸入 0 表示不限制配額",
     "notification_email_from_address": "寄件地址",
-    "notification_email_from_address_description": "寄件者電子郵件地址(例:Immich Photo Server <noreply@immich.app>)",
+    "notification_email_from_address_description": "寄件者電子郵件地址(例:Immich Photo Server <noreply@example.com>)",
     "notification_email_host_description": "電子郵件伺服器主機(例:smtp.immich.app)",
     "notification_email_ignore_certificate_errors": "忽略憑證錯誤",
     "notification_email_ignore_certificate_errors_description": "忽略 TLS 憑證驗證錯誤(不建議)",
@@ -198,28 +207,30 @@
     "password_settings": "密碼登入",
     "password_settings_description": "管理密碼登入設定",
     "paths_validated_successfully": "所有路徑驗證成功",
+    "person_cleanup_job": "清理人物",
     "quota_size_gib": "配額(GiB)",
     "refreshing_all_libraries": "正在重新整理所有圖庫",
     "registration": "管理者註冊",
     "registration_description": "由於您是本系統的首位使用者,因此將您指派爲負責管理本系統的管理者,其他使用者須由您協助建立帳號。",
-    "removing_offline_files": "移除離線檔案中",
     "repair_all": "全部糾正",
     "repair_matched_items": "有 {count, plural, other {# 個項目相符}}",
     "repaired_items": "已糾正 {count, plural, other {# 個項目}}",
     "require_password_change_on_login": "要求使用者在首次登入時更改密碼",
     "reset_settings_to_default": "將設定重設回預設",
     "reset_settings_to_recent_saved": "已設回最後儲存的設定",
-    "scanning_library_for_changed_files": "正在掃描資料庫以檢查文件變更",
-    "scanning_library_for_new_files": "正在掃描資料庫以檢查新文件",
+    "scanning_library": "掃描圖庫",
+    "search_jobs": "搜尋作業…",
     "send_welcome_email": "傳送歡迎電子郵件",
     "server_external_domain_settings": "外部網域",
-    "server_external_domain_settings_description": "公開分享鏈結的網域(包含「http(s)://」)",
+    "server_external_domain_settings_description": "公開網址,,包含 http(s)://",
+    "server_public_users": "訪客使用者",
+    "server_public_users_description": "將使用者新增至共用相簿時,會列出所有使用者(姓名、email)。關閉時,使用者列表僅對管理者生效。",
     "server_settings": "伺服器",
     "server_settings_description": "管理伺服器設定",
     "server_welcome_message": "歡迎訊息",
     "server_welcome_message_description": "在登入頁面顯示的訊息。",
-    "sidecar_job": "側接元資料",
-    "sidecar_job_description": "從檔案系統探索或同步側接(Sidecar)元資料",
+    "sidecar_job": "邊車模式詮釋資料",
+    "sidecar_job_description": "從檔案系統搜索或同步邊車模式詮釋資料",
     "slideshow_duration_description": "每張圖片放映的秒數",
     "smart_search_job_description": "對檔案執行機器學習,以利智慧搜尋",
     "storage_template_date_time_description": "檔案的創建時戳會用於判斷時間資訊",
@@ -238,6 +249,14 @@
     "storage_template_settings_description": "管理上傳檔案的資料夾結構和檔名",
     "storage_template_user_label": "<code>{label}</code> 是使用者的儲存標籤",
     "system_settings": "系統設定",
+    "tag_cleanup_job": "清理標記",
+    "template_email_invite_album": "邀請項目範本",
+    "template_email_preview": "預覽",
+    "template_email_settings": "Email範本",
+    "template_email_settings_description": "管理自定義Email通知模板",
+    "template_email_update_album": "更新向本範本",
+    "template_email_welcome": "歡迎Email範本",
+    "template_settings": "通知範本",
     "theme_custom_css_settings": "自訂 CSS",
     "theme_custom_css_settings_description": "可以用層疊樣式表(CSS)來自訂 Immich 的設計。",
     "theme_settings": "主題",
@@ -245,7 +264,6 @@
     "these_files_matched_by_checksum": "這些檔案的核對和(Checksum)是相符的",
     "thumbnail_generation_job": "產生縮圖",
     "thumbnail_generation_job_description": "爲每個檔案產生大、小及模糊縮圖,也爲每位人物產生縮圖",
-    "transcode_policy_description": "",
     "transcoding_acceleration_api": "加速 API",
     "transcoding_acceleration_api_description": "該 API 將用您的設備加速轉碼。設置是“盡力而為”:如果失敗,它將退回到軟件轉碼。VP9 轉碼是否可行取決於您的硬件。",
     "transcoding_acceleration_nvenc": "NVENC(需要 NVIDIA GPU)",
@@ -256,12 +274,12 @@
     "transcoding_accepted_audio_codecs_description": "選擇不需要轉碼的音頻編解碼器。僅用於某些轉碼策略。",
     "transcoding_accepted_containers": "接受的容器格式",
     "transcoding_accepted_containers_description": "選擇不需要重新封裝為 MP4 的容器格式。僅用於某些轉碼策略。",
-    "transcoding_accepted_video_codecs": "接受的視頻編碼器",
+    "transcoding_accepted_video_codecs": "支援的影片編碼器",
     "transcoding_accepted_video_codecs_description": "選擇不需要轉碼的視頻編解碼器。僅用於某些轉碼策略。",
     "transcoding_advanced_options_description": "大多數使用者不需要更改的選項",
     "transcoding_audio_codec": "音頻編解碼器",
     "transcoding_audio_codec_description": "Opus 是音質最高的選擇,但會與舊設備或軟件有較低的兼容性。",
-    "transcoding_bitrate_description": "比特率高於最大比特率或格式不被接受的視頻",
+    "transcoding_bitrate_description": "高於最大位元速率或格式不被支援的影片",
     "transcoding_codecs_learn_more": "欲瞭解此處使用的術語,請參閱 FFmpeg 說明書中的 <h264-link>H.264 編解碼器</h264-link>、<hevc-link>HEVC 編解碼器</hevc-link>和 <vp9-link>VP9 編解碼器</vp9-link>。",
     "transcoding_constant_quality_mode": "恆定質量模式",
     "transcoding_constant_quality_mode_description": "ICQ 比 CQP 更好,但某些硬件加速設備不支持此模式。設置此選項時,會在使用基於質量的編碼時偏好指定的模式。由於 NVENC 不支持 ICQ,此選項對其無效。",
@@ -271,7 +289,7 @@
     "transcoding_hardware_acceleration": "硬體加速",
     "transcoding_hardware_acceleration_description": "實驗性功能;速度更快,但在相同比特率下質量較低",
     "transcoding_hardware_decoding": "硬體解碼",
-    "transcoding_hardware_decoding_setting_description": "僅適用於 NVENC、QSV 和 RKMPP。啟用端到端加速,而不僅僅是加速編碼。可能並非所有視頻都適用。",
+    "transcoding_hardware_decoding_setting_description": "不只加速編碼,還啟用端對端加速。可能不支援某些影片。",
     "transcoding_hevc_codec": "HEVC 編解碼器",
     "transcoding_max_b_frames": "最大 B 幀數",
     "transcoding_max_b_frames_description": "更高的值可以提高壓縮效率,但會降低編碼速度。在舊設備上可能不兼容硬件加速。0 表示禁用 B 幀,而 -1 則會自動設置此值。",
@@ -286,7 +304,7 @@
     "transcoding_preset_preset_description": "壓縮速度。在針對特定位元速率時,較慢的預設值會減少檔案大小並提高品質。VP9 會忽略高於「faster」的速度。",
     "transcoding_reference_frames": "參考幀數",
     "transcoding_reference_frames_description": "壓縮給定幀時參考的幀數。較高的值可以提高壓縮效率,但會降低編碼速度。0 會自動設置此值。",
-    "transcoding_required_description": "僅限於格式不被接受的視頻",
+    "transcoding_required_description": "僅限格式不被支援的影片",
     "transcoding_settings": "影片轉碼",
     "transcoding_settings_description": "管理影片的解析度和編碼資訊",
     "transcoding_target_resolution": "目標解析度",
@@ -297,8 +315,6 @@
     "transcoding_threads_description": "較高的值會加快編碼速度,但會減少伺服器在運行過程中處理其他任務的空間。此值不應超過 CPU 核心數。設置為 0 可以最大化利用率。",
     "transcoding_tone_mapping": "色調映射",
     "transcoding_tone_mapping_description": "在將 HDR 視頻轉換為 SDR 時,嘗試保留其外觀。每種算法在顏色、細節和亮度方面都有不同的權衡。Hable 保留細節,Mobius 保留顏色,Reinhard 保留亮度。",
-    "transcoding_tone_mapping_npl": "色調映射 NPL",
-    "transcoding_tone_mapping_npl_description": "顏色將調整為在此亮度顯示器上看起來正常。反直觀地,較低的值會增加視頻的亮度,反之亦然,因為它會補償顯示器的亮度。0 會自動設置此值。",
     "transcoding_transcode_policy": "轉碼策略",
     "transcoding_transcode_policy_description": "視頻何時應進行轉碼的策略。HDR 視頻將始終進行轉碼(除非禁用轉碼)。",
     "transcoding_two_pass_encoding": "雙通道編碼",
@@ -312,9 +328,10 @@
     "trash_settings_description": "管理垃圾桶設定",
     "untracked_files": "未被追蹤的檔案",
     "untracked_files_description": "這些檔案不會被追蹤。它們可能是移動失誤、上傳中斷或遇到漏洞而遺留的產物",
-    "user_delete_delay": "<b>{user}</b> 的帳戶和資產將安排在 {delay, plural, one {# 天} other {# 天}} 後進行永久刪除。",
-    "user_delete_delay_settings": "刪除延遲",
-    "user_delete_delay_settings_description": "移除後永久刪除用戶帳戶和資產的天數。用戶刪除任務會在午夜運行,以檢查是否有準備好刪除的用戶。對此設置的更改將在下一次執行時進行評估。",
+    "user_cleanup_job": "清理使用者",
+    "user_delete_delay": "<b>{user}</b> 的帳號和項目將於 {delay, plural, other {# 天}}後永久刪除。",
+    "user_delete_delay_settings": "延後刪除",
+    "user_delete_delay_settings_description": "移除後,永久刪除使用者帳號和檔案的天數。使用者刪除作業會在午夜檢查是否有可以刪除的使用者。變更這項設定後,會在下次執行時檢查。",
     "user_delete_immediately": "<b>{user}</b> 的帳戶和資產將被<b>立即</b>排隊進行永久刪除。",
     "user_delete_immediately_checkbox": "將用戶和資產排隊進行立即刪除",
     "user_management": "使用者管理",
@@ -356,7 +373,7 @@
   "album_updated_setting_description": "當共享相簿有新檔案時,用電子郵件通知我",
   "album_user_left": "已離開 {album}",
   "album_user_removed": "已移除 {user}",
-  "album_with_link_access": "讓知道鏈結的任何人都可以看到此相簿中的照片及人物。",
+  "album_with_link_access": "知道連結的使用者都可以查看這本相簿中的相片和使用者。",
   "albums": "相簿",
   "albums_count": "{count, plural, one {{count, number} 本相簿} other {{count, number} 本相簿}}",
   "all": "全部",
@@ -378,7 +395,6 @@
   "archive_or_unarchive_photo": "封存或取消封存照片",
   "archive_size": "封存量",
   "archive_size_description": "設定要下載的封存量(單位:GiB)",
-  "archived": "",
   "archived_count": "{count, plural, other {已封存 # 個項目}}",
   "are_these_the_same_person": "這也是同一個人嗎?",
   "are_you_sure_to_do_this": "您確定要這麼做嗎?",
@@ -386,10 +402,10 @@
   "asset_adding_to_album": "加入相簿中…",
   "asset_description_updated": "檔案描述已更新",
   "asset_filename_is_offline": "檔案 {filename} 離線了",
-  "asset_has_unassigned_faces": "檔案有未分配的面孔",
+  "asset_has_unassigned_faces": "檔案中有未指定的臉孔",
   "asset_hashing": "Hashing中...",
   "asset_offline": "檔案離線",
-  "asset_offline_description": "此檔案己離線。Immich 無法訪問其文件位置。請確保資產可用,然後重新掃描資料庫。",
+  "asset_offline_description": "磁碟中找不到此外部檔案。請向您的 Immich 管理員尋求協助。",
   "asset_skipped": "已略過",
   "asset_skipped_in_trash": "已丟掉",
   "asset_uploaded": "已上傳",
@@ -402,7 +418,7 @@
   "assets_moved_to_trash_count": "已將 {count, plural, other {# 個檔案}}丟進垃圾桶",
   "assets_permanently_deleted_count": "已永久刪除 {count, plural, one {# 個檔案} other {# 個檔案}}",
   "assets_removed_count": "已移除 {count, plural, one {# 個檔案} other {# 個檔案}}",
-  "assets_restore_confirmation": "確定要還原所有丟掉的檔案嗎?此步驟無法取消喔!",
+  "assets_restore_confirmation": "確定要還原所有丟掉的檔案嗎?此步驟無法取消喔!註:這無法還原任何離線檔案。",
   "assets_restored_count": "已還原 {count, plural, other {# 個檔案}}",
   "assets_trashed_count": "已丟掉 {count, plural, other {# 個檔案}}",
   "assets_were_part_of_album_count": "{count, plural, one {檔案已} other {檔案已}} 是相冊的一部分",
@@ -413,6 +429,7 @@
   "birthdate_saved": "出生日期儲存成功",
   "birthdate_set_description": "出生日期會用來計算此人拍照時的歲數。",
   "blurred_background": "模糊背景",
+  "bugs_and_feature_requests": "錯誤及功能請求",
   "build": "建置編號",
   "build_image": "建置映像",
   "bulk_delete_duplicates_confirmation": "您確定要批量刪除 {count, plural, one {# 個重複檔案} other {# 個重複檔案}} 嗎?這將保留每組中的最大檔案,並永久刪除所有其他重複項。此操作無法撤銷!",
@@ -427,10 +444,6 @@
   "cannot_merge_people": "無法合併人物",
   "cannot_undo_this_action": "此步驟無法取消喔!",
   "cannot_update_the_description": "無法更新描述",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
   "change_date": "更改日期",
   "change_expiration_time": "更改失效期限",
   "change_location": "更改位置",
@@ -461,7 +474,8 @@
   "comments_are_disabled": "評論已禁用",
   "confirm": "確認",
   "confirm_admin_password": "確認管理者密碼",
-  "confirm_delete_shared_link": "確定要刪除這條分享鏈結嗎?",
+  "confirm_delete_shared_link": "確定刪除連結嗎?",
+  "confirm_keep_this_delete_others": "所有的其他堆疊項目將被刪除。確定繼續嗎?",
   "confirm_password": "確認密碼",
   "contain": "包含",
   "context": "情境",
@@ -471,8 +485,8 @@
   "copy_error": "複製錯誤",
   "copy_file_path": "複製檔案路徑",
   "copy_image": "複製圖片",
-  "copy_link": "複製鏈結",
-  "copy_link_to_clipboard": "將鏈結複製到剪貼簿",
+  "copy_link": "複製連結",
+  "copy_link_to_clipboard": "複製連結到剪貼簿",
   "copy_password": "複製密碼",
   "copy_to_clipboard": "複製到剪貼簿",
   "country": "國家",
@@ -481,14 +495,14 @@
   "create": "建立",
   "create_album": "建立相簿",
   "create_library": "建立圖庫",
-  "create_link": "建立鏈結",
-  "create_link_to_share": "建立分享鏈結",
-  "create_link_to_share_description": "允許任何擁有鏈接的人查看所選的照片",
+  "create_link": "建立連結",
+  "create_link_to_share": "建立共享連結",
+  "create_link_to_share_description": "知道連結的使用者都可以查看這本相簿中的相片",
   "create_new_person": "創建新人物",
   "create_new_person_hint": "將選定的檔案分配給新人物",
   "create_new_user": "建立新使用者",
   "create_tag": "建立標記",
-  "create_tag_description": "建立新的標記。若要建立巢狀標記,請輸入完整的標記路徑(包括正斜線 / )。",
+  "create_tag_description": "建立新的標籤。若要建立不同群組分類標籤,請輸入完整的標籤路徑(包括正斜線 / )。",
   "create_user": "建立使用者",
   "created": "建立於",
   "current_device": "此裝置",
@@ -504,23 +518,26 @@
   "deduplicate_all": "刪除所有重複項目",
   "default_locale": "預設區域",
   "default_locale_description": "依瀏覽器區域設定日期和數字格式",
-  "delete": "删除",
+  "delete": "刪除",
   "delete_album": "刪除相簿",
   "delete_api_key_prompt": "您確定要刪除這個 API Key嗎?",
   "delete_duplicates_confirmation": "您確定要永久刪除這些重複項嗎?",
   "delete_key": "刪除密鑰",
   "delete_library": "刪除圖庫",
   "delete_link": "刪除鏈結",
-  "delete_shared_link": "刪除分享鏈結",
+  "delete_others": "刪除其他",
+  "delete_shared_link": "刪除共享鏈結",
   "delete_tag": "刪除標記",
   "delete_tag_confirmation_prompt": "確定要刪除「{tagName}」(標記)嗎?",
   "delete_user": "刪除使用者",
-  "deleted_shared_link": "已刪除分享鏈結",
+  "deleted_shared_link": "已刪除共享鏈結",
+  "deletes_missing_assets": "刪除磁碟中遺失的檔案",
   "description": "描述",
   "details": "詳情",
   "direction": "方向",
   "disabled": "禁用",
   "disallow_edits": "不允許編輯",
+  "discord": "Discord",
   "discover": "探索",
   "dismiss_all_errors": "忽略所有錯誤",
   "dismiss_error": "忽略錯誤",
@@ -529,6 +546,7 @@
   "display_original_photos": "顯示原始照片",
   "display_original_photos_setting_description": "在網頁與原始檔案相容的情況下,查看檔案時優先顯示原始檔案而非縮圖。這可能會讓照片顯示速度變慢。",
   "do_not_show_again": "不再顯示此訊息",
+  "documentation": "說明書",
   "done": "完成",
   "download": "下載",
   "download_include_embedded_motion_videos": "嵌入影片",
@@ -541,13 +559,6 @@
   "duplicates": "重複項目",
   "duplicates_description": "通過指示每一組重複的檔案(如果有)來解決問題",
   "duration": "時長",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
   "edit": "編輯",
   "edit_album": "編輯相簿",
   "edit_avatar": "編輯形象",
@@ -572,8 +583,6 @@
   "editor_crop_tool_h2_aspect_ratios": "長寬比",
   "editor_crop_tool_h2_rotation": "旋轉",
   "email": "電子郵件",
-  "empty": "",
-  "empty_album": "",
   "empty_trash": "清空垃圾桶",
   "empty_trash_confirmation": "確定要清空垃圾桶嗎?這會永久刪除 Immich 垃圾桶中所有的檔案。\n此步驟無法取消喔!",
   "enable": "啟用",
@@ -588,12 +597,12 @@
     "cant_apply_changes": "無法套用更改",
     "cant_change_activity": "無法{enabled, select, true {禁用} other {啟用}}活動",
     "cant_change_asset_favorite": "無法更改檔案的收藏狀態",
-    "cant_change_metadata_assets_count": "無法更改 {count, plural, other {# 個檔案}}的元資料",
+    "cant_change_metadata_assets_count": "無法更改 {count, plural, other {# 個檔案}}的詳細資料",
     "cant_get_faces": "無法取得臉孔",
     "cant_get_number_of_comments": "無法獲取評論數量",
     "cant_search_people": "無法搜尋人",
     "cant_search_places": "無法搜尋地點",
-    "cleared_jobs": "已清除以下工作的任務: {job}",
+    "cleared_jobs": "已清除的作業:{job}",
     "error_adding_assets_to_album": "將檔案加入相簿時出錯",
     "error_adding_users_to_album": "將使用者加入相簿時出錯",
     "error_deleting_shared_user": "刪除共享使用者時出錯",
@@ -604,9 +613,10 @@
     "exclusion_pattern_already_exists": "此排除模式已存在。",
     "failed_job_command": "命令 {command} 執行失敗,作業:{job}",
     "failed_to_create_album": "相簿建立失敗",
-    "failed_to_create_shared_link": "建立分享鏈結失敗",
-    "failed_to_edit_shared_link": "編輯分享鏈結失敗",
+    "failed_to_create_shared_link": "建立共享連結失敗",
+    "failed_to_edit_shared_link": "編輯共享連結失敗",
     "failed_to_get_people": "無法獲取人物",
+    "failed_to_keep_this_delete_others": "無法保留此項目並刪除其他項目",
     "failed_to_load_asset": "檔案載入失敗",
     "failed_to_load_assets": "檔案載入失敗",
     "failed_to_load_people": "無法載入人物",
@@ -620,7 +630,7 @@
     "quota_higher_than_disk_size": "您定的配額高於磁碟容量",
     "repair_unable_to_check_items": "無法檢查 {count, select, other { 個項目}}",
     "unable_to_add_album_users": "無法將使用者加入相簿",
-    "unable_to_add_assets_to_shared_link": "無法將檔案加上分享鏈結",
+    "unable_to_add_assets_to_shared_link": "無法加入項目到共享連結",
     "unable_to_add_comment": "無法添加評論",
     "unable_to_add_exclusion_pattern": "無法添加排除模式",
     "unable_to_add_import_path": "無法添加匯入路徑",
@@ -634,8 +644,6 @@
     "unable_to_change_location": "無法更改位置",
     "unable_to_change_password": "無法更改密碼",
     "unable_to_change_visibility": "無法更改 {count, plural, one {# 位人士} other {# 位人士}} 的可見性",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
     "unable_to_complete_oauth_login": "無法完成 OAuth 登入",
     "unable_to_connect": "無法連接",
     "unable_to_connect_to_server": "無法連接到伺服器",
@@ -649,7 +657,7 @@
     "unable_to_delete_assets": "刪除檔案時發生錯誤",
     "unable_to_delete_exclusion_pattern": "無法刪除排除模式",
     "unable_to_delete_import_path": "無法刪除匯入路徑",
-    "unable_to_delete_shared_link": "無法刪除分享鏈結",
+    "unable_to_delete_shared_link": "刪除共享連結失敗",
     "unable_to_delete_user": "無法刪除使用者",
     "unable_to_download_files": "無法下載檔案",
     "unable_to_edit_exclusion_pattern": "無法編輯排除模式",
@@ -658,10 +666,10 @@
     "unable_to_enter_fullscreen": "無法進入全螢幕",
     "unable_to_exit_fullscreen": "無法退出全螢幕",
     "unable_to_get_comments_number": "無法獲取評論數量",
-    "unable_to_get_shared_link": "取得分享鏈結失敗",
+    "unable_to_get_shared_link": "取得共享連結失敗",
     "unable_to_hide_person": "無法隱藏人物",
     "unable_to_link_motion_video": "無法鏈結動態影片",
-    "unable_to_link_oauth_account": "無法連結 OAuth 帳戶",
+    "unable_to_link_oauth_account": "取得 OAuth 授權失敗",
     "unable_to_load_album": "無法載入相簿",
     "unable_to_load_asset_activity": "無法載入檔案活動",
     "unable_to_load_items": "無法載入項目",
@@ -675,13 +683,11 @@
     "unable_to_refresh_user": "無法重新整理使用者",
     "unable_to_remove_album_users": "無法從相簿中移除使用者",
     "unable_to_remove_api_key": "無法移除 API 金鑰",
-    "unable_to_remove_assets_from_shared_link": "無法從分享鏈結中刪除檔案",
-    "unable_to_remove_comment": "",
+    "unable_to_remove_assets_from_shared_link": "刪除共享連結中檔案失敗",
+    "unable_to_remove_deleted_assets": "無法移除離線檔案",
     "unable_to_remove_library": "無法移除資料庫",
-    "unable_to_remove_offline_files": "無法移除離線檔案",
     "unable_to_remove_partner": "無法移除夥伴",
     "unable_to_remove_reaction": "無法移除反應",
-    "unable_to_remove_user": "",
     "unable_to_repair_items": "無法糾正項目",
     "unable_to_reset_password": "無法重設密碼",
     "unable_to_resolve_duplicate": "無法解決重複項",
@@ -694,8 +700,8 @@
     "unable_to_save_name": "無法儲存名稱",
     "unable_to_save_profile": "無法儲存個人資料",
     "unable_to_save_settings": "無法儲存設定",
-    "unable_to_scan_libraries": "無法掃描資料庫",
-    "unable_to_scan_library": "無法掃描資料庫",
+    "unable_to_scan_libraries": "無法掃描圖庫",
+    "unable_to_scan_library": "無法掃描圖庫",
     "unable_to_set_feature_photo": "無法設置特色照片",
     "unable_to_set_profile_picture": "無法設置個人頭像",
     "unable_to_submit_job": "無法提交作業",
@@ -711,31 +717,24 @@
     "unable_to_update_user": "無法更新使用者",
     "unable_to_upload_file": "無法上傳檔案"
   },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
-  "exif": "Exif",
+  "exif": "EXIF",
   "exit_slideshow": "退出幻燈片",
   "expand_all": "展開全部",
   "expire_after": "失效時間",
   "expired": "已過期",
   "expires_date": "失效期限:{date}",
   "explore": "探索",
-  "explorer": "探測器",
+  "explorer": "總攬",
   "export": "匯出",
   "export_as_json": "匯出 JSON",
   "extension": "副檔名",
   "external": "外部",
   "external_libraries": "外部圖庫",
-  "face_unassigned": "未指派",
-  "failed_to_get_people": "",
+  "face_unassigned": "未指定",
   "favorite": "收藏",
   "favorite_or_unfavorite_photo": "收藏或取消收藏照片",
   "favorites": "收藏",
-  "feature": "",
   "feature_photo_updated": "特色照片已更新",
-  "featurecollection": "",
   "features": "功能",
   "features_setting_description": "管理應用程式功能",
   "file_name": "檔名",
@@ -747,15 +746,13 @@
   "fix_incorrect_match": "修復不相符的",
   "folders": "資料夾",
   "folders_feature_description": "以資料夾瀏覽檔案系統中的照片和影片",
-  "force_re-scan_library_files": "強制重新掃描所有資料庫檔案",
   "forward": "順序",
   "general": "一般",
   "get_help": "線上求助",
   "getting_started": "開始使用",
   "go_back": "返回",
   "go_to_search": "前往搜尋",
-  "go_to_share_page": "前往分享頁面",
-  "group_albums_by": "相簿分組方式",
+  "group_albums_by": "分類群組的方式...",
   "group_no": "無分組",
   "group_owner": "按擁有者分組",
   "group_year": "按年份分組",
@@ -780,7 +777,6 @@
   "image_alt_text_date_place_2_people": "{isVideo, select, true {影片} other {圖片}} 在 {city}、{country},與 {person1} 和 {person2} 一同於 {date} 拍攝",
   "image_alt_text_date_place_3_people": "{isVideo, select, true {影片} other {圖片}} 在 {city}、{country},與 {person1}、{person2} 和 {person3} 一同於 {date} 拍攝",
   "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {影片} other {圖片}} 在 {city}、{country},與 {person1}、{person2} 和其他 {additionalCount, number} 人於 {date} 拍攝",
-  "img": "",
   "immich_logo": "Immich 標誌",
   "immich_web_interface": "Immich 網頁介面",
   "import_from_json": "匯入 JSON",
@@ -801,10 +797,11 @@
   "invite_people": "邀請人員",
   "invite_to_album": "邀請至相簿",
   "items_count": "{count, plural, other {# 個項目}}",
-  "job_settings_description": "",
-  "jobs": "工作",
+  "jobs": "作業",
   "keep": "保留",
   "keep_all": "全部保留",
+  "keep_this_delete_others": "保留這個,刪除其他",
+  "kept_this_deleted_others": "保留這個項目並刪除{count, plural, one {# asset} other {# assets}}",
   "keyboard_shortcuts": "鍵盤快捷鍵",
   "language": "語言",
   "language_setting_description": "選擇您的首選語言",
@@ -812,7 +809,7 @@
   "latest_version": "最新版本",
   "latitude": "緯度",
   "leave": "離開",
-  "let_others_respond": "允许他人回复",
+  "let_others_respond": "允許他人回覆",
   "level": "等級",
   "library": "圖庫",
   "library_options": "資料庫選項",
@@ -837,8 +834,9 @@
   "look": "樣貌",
   "loop_videos": "重播影片",
   "loop_videos_description": "啟用後,影片結束會自動重播。",
+  "main_branch_warning": "現在使用的是開發版本;我們強烈建議使用正式發行版!",
   "make": "製造商",
-  "manage_shared_links": "管理分享鏈結",
+  "manage_shared_links": "管理共享連結",
   "manage_sharing_with_partners": "管理與夥伴的分享",
   "manage_the_app_settings": "管理應用程式設定",
   "manage_your_account": "管理您的帳號",
@@ -906,6 +904,7 @@
   "notifications": "通知",
   "notifications_setting_description": "管理通知",
   "oauth": "OAuth",
+  "official_immich_resources": "官方 Immich 資源",
   "offline": "離線",
   "offline_paths": "失效路徑",
   "offline_paths_description": "這些可能是手動刪除非外部圖庫的檔案時所遺留的。",
@@ -918,7 +917,6 @@
   "onboarding_welcome_user": "歡迎,{user}",
   "online": "在線",
   "only_favorites": "僅顯示己收藏",
-  "only_refreshes_modified_files": "只重新整理修改過的檔案",
   "open_in_map_view": "開啟地圖檢視",
   "open_in_openstreetmap": "用 OpenStreetMap 開啟",
   "open_the_search_filters": "開啟搜尋篩選器",
@@ -977,7 +975,6 @@
   "play_memories": "播放回憶",
   "play_motion_photo": "播放動態照片",
   "play_or_pause_video": "播放或暫停影片",
-  "point": "",
   "port": "埠口",
   "preset": "預設",
   "preview": "預覽",
@@ -1001,7 +998,7 @@
   "purchase_button_reminder": "過 30 天再提醒我",
   "purchase_button_remove_key": "移除金鑰",
   "purchase_button_select": "選這個",
-  "purchase_failed_activation": "啟用失敗!請檢查您的電子郵件以取得正確的產品金鑰!",
+  "purchase_failed_activation": "啟用失敗!請檢查您的電子郵件,取得正確的產品金鑰!",
   "purchase_individual_description_1": "針對個人",
   "purchase_individual_description_2": "擁護者狀態",
   "purchase_individual_title": "個人",
@@ -1022,38 +1019,38 @@
   "purchase_server_description_2": "擁護者狀態",
   "purchase_server_title": "伺服器",
   "purchase_settings_server_activated": "伺服器產品金鑰是由管理者管理的",
-  "range": "",
   "rating": "評星",
   "rating_clear": "清除評等",
   "rating_count": "{count, plural, other {# 星}}",
   "rating_description": "在資訊面板中顯示 EXIF 評等",
-  "raw": "",
   "reaction_options": "反應選項",
   "read_changelog": "閱覽變更日誌",
-  "reassign": "重新指派",
-  "reassigned_assets_to_existing_person": "已將 {count, plural, one {# 個檔案} other {# 個檔案}} 重新分配給 {name, select, null {現有的人} other {{name}}}",
-  "reassigned_assets_to_new_person": "已將 {count, plural, one {# 個檔案} other {# 個檔案}} 重新分配給一位新的使用者",
+  "reassign": "重新指定",
+  "reassigned_assets_to_existing_person": "已將 {count, plural, other {# 個檔案}}重新指定給{name, select, null {現有的人} other {{name}}}",
+  "reassigned_assets_to_new_person": "已將 {count, plural, other {# 個檔案}}重新指定給一位新人物",
   "reassing_hint": "將選定的檔案分配給己存在的人物",
   "recent": "最近",
   "recent_searches": "最近搜尋項目",
   "refresh": "重新整理",
   "refresh_encoded_videos": "重新整理已編碼的影片",
-  "refresh_metadata": "重新整理元資料",
+  "refresh_faces": "重整面部資料",
+  "refresh_metadata": "重新整理詳細資料",
   "refresh_thumbnails": "重新整理縮圖",
   "refreshed": "重新整理完畢",
-  "refreshes_every_file": "重新整理所有檔案",
+  "refreshes_every_file": "重新讀取現有的所有檔案和新檔案",
   "refreshing_encoded_video": "正在重新整理已編碼的影片",
-  "refreshing_metadata": "正在重新整理元資料",
+  "refreshing_faces": "重整面部資料中",
+  "refreshing_metadata": "正在重新整理詳細資料",
   "regenerating_thumbnails": "重新產生縮圖中",
   "remove": "移除",
   "remove_assets_album_confirmation": "確定要從相簿中移除 {count, plural, other {# 個檔案}}嗎?",
-  "remove_assets_shared_link_confirmation": "確定要從此分享鏈結中移除{count, plural, other {# 個檔案}}嗎?",
+  "remove_assets_shared_link_confirmation": "確定刪除共享連結中{count, plural, other {# 個項目}}嗎?",
   "remove_assets_title": "移除檔案?",
   "remove_custom_date_range": "移除自訂日期範圍",
+  "remove_deleted_assets": "移除離線檔案",
   "remove_from_album": "從相簿中移除",
   "remove_from_favorites": "從收藏中移除",
-  "remove_from_shared_link": "從分享鏈結中移除",
-  "remove_offline_files": "移除離線檔案",
+  "remove_from_shared_link": "從共享連結中移除",
   "remove_user": "移除用戶",
   "removed_api_key": "已移除 API 金鑰:{name}",
   "removed_from_archive": "從封存中移除",
@@ -1070,7 +1067,6 @@
   "reset": "重設",
   "reset_password": "重設密碼",
   "reset_people_visibility": "重設人物可見性",
-  "reset_settings_to_default": "",
   "reset_to_default": "重設回預設",
   "resolve_duplicates": "解決重複項",
   "resolved_all_duplicates": "已解決所有重複項目",
@@ -1090,8 +1086,7 @@
   "saved_settings": "已儲存設定",
   "say_something": "说些什么",
   "scan_all_libraries": "掃描所有圖庫",
-  "scan_all_library_files": "重新掃描所有圖庫文件",
-  "scan_new_library_files": "掃描新圖庫",
+  "scan_library": "掃描",
   "scan_settings": "掃描設定",
   "scanning_for_album": "掃描相簿中……",
   "search": "搜尋",
@@ -1109,8 +1104,9 @@
   "search_options": "搜尋選項",
   "search_people": "搜尋人物",
   "search_places": "搜尋地點",
+  "search_settings": "搜尋設定",
   "search_state": "搜尋地區…",
-  "search_tags": "搜尋標記…",
+  "search_tags": "搜尋標籤...",
   "search_timezone": "搜尋時區…",
   "search_type": "搜尋類型",
   "search_your_photos": "搜尋照片",
@@ -1133,7 +1129,6 @@
   "selected_count": "{count, plural, other {選了 # 項}}",
   "send_message": "傳訊息",
   "send_welcome_email": "傳送歡迎電子郵件",
-  "server": "",
   "server_offline": "伺服器離線",
   "server_online": "伺服器在線",
   "server_stats": "伺服器統計",
@@ -1152,8 +1147,8 @@
   "shared_by_user": "由 {user} 分享",
   "shared_by_you": "由你分享",
   "shared_from_partner": "來自 {partner} 的照片",
-  "shared_link_options": "分享鏈結選項",
-  "shared_links": "分享鏈結",
+  "shared_link_options": "共享連結選項",
+  "shared_links": "共享連結",
   "shared_photos_and_videos_count": "{assetCount, plural, other {已分享 # 張照片及影片。}}",
   "shared_with_partner": "與 {partner} 共享",
   "sharing": "共享",
@@ -1170,12 +1165,13 @@
   "show_in_timeline": "在時間軸中顯示",
   "show_in_timeline_setting_description": "在您的時間軸中顯示這位使用者的照片和影片",
   "show_keyboard_shortcuts": "顯示鍵盤快捷鍵",
-  "show_metadata": "顯示元資料",
+  "show_metadata": "顯示詳細資料",
   "show_or_hide_info": "顯示或隱藏資訊",
   "show_password": "顯示密碼",
   "show_person_options": "顯示人物選項",
   "show_progress_bar": "顯示進度條",
   "show_search_options": "顯示搜尋選項",
+  "show_slideshow_transition": "顯示幻燈片轉場",
   "show_supporter_badge": "擁護者徽章",
   "show_supporter_badge_description": "顯示擁護者徽章",
   "shuffle": "隨機排序",
@@ -1186,10 +1182,10 @@
   "size": "用量",
   "skip_to_content": "跳至內容",
   "skip_to_folders": "跳到資料夾",
-  "skip_to_tags": "跳到標記",
+  "skip_to_tags": "跳轉到標籤",
   "slideshow": "幻燈片",
   "slideshow_settings": "幻燈片設定",
-  "sort_albums_by": "相簿排序方式",
+  "sort_albums_by": "排序相簿",
   "sort_created": "建立日期",
   "sort_items": "項目數量",
   "sort_modified": "日期已修改",
@@ -1217,33 +1213,37 @@
   "submit": "提交",
   "suggestions": "建議",
   "sunrise_on_the_beach": "日出的海灘",
+  "support": "支援",
+  "support_and_feedback": "支持與回饋",
+  "support_third_party_description": "您安裝的 immich 是由第三方打包的。您遇到的問題可能是該軟體包造成的,所以請先使用下面的鏈結向他們提出問題。",
   "swap_merge_direction": "交換合併方向",
   "sync": "同步",
   "tag": "標記",
   "tag_assets": "標記檔案",
   "tag_created": "已建立標記:{tag}",
   "tag_feature_description": "以邏輯標記要旨分組瀏覽照片和影片",
-  "tag_not_found_question": "找不到標記?可以到<link>這裏</link>建立",
+  "tag_not_found_question": "找不到標記?<link>建立新標記吧。</link>",
   "tag_updated": "已更新標記:{tag}",
   "tagged_assets": "已標記 {count, plural, other {# 個檔案}}",
-  "tags": "標記",
+  "tags": "標籤",
   "template": "模板",
   "theme": "主題",
   "theme_selection": "主題選項",
   "theme_selection_description": "依瀏覽器系統偏好自動設定深、淺色主題",
   "they_will_be_merged_together": "它們將會被合併在一起",
+  "third_party_resources": "第三方資源",
   "time_based_memories": "依時間回憶",
+  "timeline": "時間軸",
   "timezone": "時區",
   "to_archive": "封存",
   "to_change_password": "更改密碼",
   "to_favorite": "收藏",
   "to_login": "登入",
   "to_parent": "到上一級",
-  "to_root": "到根",
   "to_trash": "垃圾桶",
   "toggle_settings": "切換設定",
   "toggle_theme": "切換深色主題",
-  "toggle_visibility": "",
+  "total": "統計",
   "total_usage": "總用量",
   "trash": "垃圾桶",
   "trash_all": "全部丟掉",
@@ -1253,12 +1253,10 @@
   "trashed_items_will_be_permanently_deleted_after": "垃圾桶中的項目會在 {days, plural, other {# 天}}後永久刪除。",
   "type": "類型",
   "unarchive": "取消封存",
-  "unarchived": "",
   "unarchived_count": "{count, plural, other {已取消封存 # 個項目}}",
   "unfavorite": "取消收藏",
   "unhide_person": "取消隱藏人物",
   "unknown": "未知",
-  "unknown_album": "",
   "unknown_year": "不知年份",
   "unlimited": "不限制",
   "unlink_motion_video": "取消鏈結動態影片",
@@ -1295,6 +1293,8 @@
   "user_purchase_settings_description": "管理你的購買",
   "user_role_set": "設 {user} 爲{role}",
   "user_usage_detail": "使用者用量詳情",
+  "user_usage_stats": "帳號使用量統計",
+  "user_usage_stats_description": "查看帳號使用量",
   "username": "使用者名稱",
   "users": "使用者",
   "utilities": "工具",
@@ -1302,7 +1302,9 @@
   "variables": "變數",
   "version": "版本",
   "version_announcement_closing": "敬祝順心,Alex",
-  "version_announcement_message": "嗨~本應用程式可以更新了,爲防止配置出錯,請花點時間閱讀<link>發行說明</link>,並確保 <code>docker-compose.yml</code> 和 <code>.env</code> 設置是最新的,特別是使用 WatchTower 等自動更新工具時。",
+  "version_announcement_message": "嗨~新版本的 Immich 推出了。爲防止配置出錯,請花點時間閱讀<link>發行說明</link>,並確保設定是最新的,特別是使用 WatchTower 等自動更新工具時。",
+  "version_history": "版本紀錄",
+  "version_history_item": "{date} 安裝了 {version}",
   "video": "影片",
   "video_hover_setting": "游標停留時播放影片縮圖",
   "video_hover_setting_description": "當滑鼠停在項目上時播放影片縮圖。即使停用,將滑鼠停在播放圖示上也可以播放。",
@@ -1314,10 +1316,10 @@
   "view_all_users": "查看所有使用者",
   "view_in_timeline": "在時間軸中查看",
   "view_links": "檢視鏈結",
+  "view_name": "查看",
   "view_next_asset": "查看下一項",
   "view_previous_asset": "查看上一項",
   "view_stack": "查看堆疊",
-  "viewer": "",
   "visibility_changed": "已更改 {count, plural, other {# 位人物}}的可見性",
   "waiting": "待處理",
   "warning": "警告",
@@ -1327,6 +1329,6 @@
   "year": "年",
   "years_ago": "{years, plural, other {# 年}}前",
   "yes": "是",
-  "you_dont_have_any_shared_links": "您沒有分享鏈結",
+  "you_dont_have_any_shared_links": "您沒有任何共享連結",
   "zoom_image": "縮放圖片"
 }
diff --git a/web/src/lib/i18n/zh_SIMPLIFIED.json b/i18n/zh_SIMPLIFIED.json
similarity index 85%
rename from web/src/lib/i18n/zh_SIMPLIFIED.json
rename to i18n/zh_SIMPLIFIED.json
index b56c2d29eb..d2f7a3b90c 100644
--- a/web/src/lib/i18n/zh_SIMPLIFIED.json
+++ b/i18n/zh_SIMPLIFIED.json
@@ -23,63 +23,72 @@
   "add_to": "添加到...",
   "add_to_album": "添加到相册",
   "add_to_shared_album": "添加到共享相册",
+  "add_url": "添加URL",
   "added_to_archive": "添加到归档",
   "added_to_favorites": "添加到收藏",
   "added_to_favorites_count": "添加{count, number}项到收藏",
   "admin": {
-    "add_exclusion_pattern_description": "添加排除规则。支持使用 *、** 和 ? 通配符。比如要忽略名为 “Raw” 的任何目录中的所有文件,请使用 “**/Raw/**”;要忽略所有以 “.tif” 结尾的文件,请使用 “**/*.tif”;要忽略绝对路径,请使用 “/path/to/ignore/**”。",
+    "add_exclusion_pattern_description": "添加排除规则。支持使用 *、** 和 ? 通配符。比如要忽略任何名为 “Raw” 的文件夹中的所有文件,请使用 “**/Raw/**”;要忽略所有以 “.tif” 结尾的文件,请使用 “**/*.tif”;要忽略绝对路径,请使用 “/path/to/ignore/**”。",
+    "asset_offline_description": "磁盘上已找不到此外部库项目,已将其移至回收站。如果文件已在库中移动,请检查时间线中是否有对应项目。要恢复此项目,请确保 Immich 可以访问以下文件路径并执行“扫描库”任务。",
     "authentication_settings": "认证设置",
     "authentication_settings_description": "管理密码、OAuth 和其它认证设置",
-    "authentication_settings_disable_all": "确定要禁用所有的登录方式?此操作将完全禁止登录。",
+    "authentication_settings_disable_all": "确定要禁用所有的登录方式?该操作将完全禁止登录。",
     "authentication_settings_reenable": "如需再次启用,使用 <link>服务器指令</link>。",
     "background_task_job": "后台任务",
+    "backup_database": "备份数据库",
+    "backup_database_enable_description": "启用数据库备份",
+    "backup_keep_last_amount": "要保留的先前备份数量",
+    "backup_settings": "备份设置",
+    "backup_settings_description": "管理数据库备份设置",
     "check_all": "检查全部",
-    "cleared_jobs": "已清理作业:{job}",
+    "cleared_jobs": "已清理任务:{job}",
     "config_set_by_file": "当前配置已通过配置文件设置",
     "confirm_delete_library": "确定要删除图库“{library}”吗?",
     "confirm_delete_library_assets": "确定要删除该图库吗?这将删除所有包含在Immich中的{count, plural, one {#个项目} other {#个项目}},且无法撤销。但文件仍将保留在磁盘中。",
     "confirm_email_below": "输入“{email}”来确认",
     "confirm_reprocess_all_faces": "确定要对全部照片重新进行面部识别吗?这将同时清除所有已命名人物。",
-    "confirm_user_password_reset": "确定要重置用户{user}的密码吗?",
-    "crontab_guru": "Crontab Guru",
+    "confirm_user_password_reset": "确定要重置用户“{user}”的密码吗?",
+    "create_job": "创建任务",
+    "cron_expression": "Cron表达式",
+    "cron_expression_description": "使用 cron 格式设置扫描间隔。更多详细信息请参阅 <link>Crontab Guru</link>",
+    "cron_expression_presets": "Cron 表达式预设",
     "disable_login": "禁用登录",
-    "disabled": "已禁用",
     "duplicate_detection_job_description": "对照片进行机器学习处理来检测相似项目,依赖于智能搜索",
     "exclusion_pattern_description": "排除规则允许在扫描图库时忽略文件和文件夹。如果有包含不想导入的文件的文件夹,例如RAW文件,排除规则将非常有用。",
     "external_library_created_at": "外部图库(创建于{date})",
     "external_library_management": "外部图库管理",
     "face_detection": "人脸检测",
-    "face_detection_description": "使用机器学习检测项目中的人脸(视频只检测其缩略图中的人脸)。选择“全部”项将会(重新)处理所有项目。选择“缺失”项将尚未处理的项目置于队列中。人脸检测完成后,检测到的人脸将排队进行面部识别,将它们分组到现有的或新的人物中。",
-    "facial_recognition_job_description": "将检测到的人脸按照人物分组。这一步将在人脸检测完成后执行。选择“全部”项将会(重新)分组所有面孔。选择“缺失”项将尚未分配的人脸置于队列中。",
-    "failed_job_command": "{command}命令执行失败的作业:{job}",
-    "force_delete_user_warning": "警告:这将立即移除用户以及所有项目。该操作无法撤回且文件无法恢复。",
+    "face_detection_description": "使用机器学习检测项目中的人脸(视频只检测其缩略图中的人脸)。选择“刷新”将会(重新)处理所有项目。选择“重置”还会清除所有当前面部数据。选择“缺失”将尚未处理的项目进行排队处理。人脸检测完成后,检测到的人脸将排队进行面部识别,将它们分组到现有的或新的人物中。",
+    "facial_recognition_job_description": "将检测到的人脸按照人物分组。这一步将在人脸检测完成后执行。选择“重置”将会(重新)分组所有面孔。选择“缺失”将尚未分配的人脸置于队列中。",
+    "failed_job_command": "{command}命令执行失败的任务:{job}",
+    "force_delete_user_warning": "警告:这将立即移除用户以及其所有项目。该操作无法撤销且文件无法恢复。",
     "forcing_refresh_library_files": "强制刷新所有图库文件",
+    "image_format": "格式",
     "image_format_description": "WebP 文件比 JPEG 文件小,但编码速度较慢。",
     "image_prefer_embedded_preview": "嵌入式预览",
     "image_prefer_embedded_preview_setting_description": "在可用时,使用 RAW 照片的嵌入式预览作为图像处理的输入。这可能为某些图像产生更准确的颜色,但预览的质量取决于相机,图像可能具有更多的压缩失真。",
     "image_prefer_wide_gamut": "广色域",
     "image_prefer_wide_gamut_setting_description": "对缩略图使用 Display P3。这可以更好地保留宽色域图像的鲜艳度,但在旧设备和旧版浏览器上图像可能会显得不同。sRGB 图像保持为 sRGB 以避免颜色偏移。",
-    "image_preview_format": "预览格式",
-    "image_preview_resolution": "预览分辨率",
-    "image_preview_resolution_description": "在查看单张照片和进行机器学习时使用。更高的分辨率可以保留更多细节,但编码时间更长,文件体积更大,且可能降低应用程序的响应速度。",
+    "image_preview_description": "去除元数据的中尺寸图像,用于单一项目查看和机器学习",
+    "image_preview_quality_description": "预览质量从 1 到 100。越高越好,但会产生更大的文件,并且会降低系统的响应能力。设置较低的值可能会影响机器学习的质量。",
+    "image_preview_title": "预览设置",
     "image_quality": "质量",
-    "image_quality_description": "图像质量从1到100。数字越高,质量越好,但生成的文件也越大,此选项会同时影响预览和缩略图。",
+    "image_resolution": "分辨率",
+    "image_resolution_description": "更高的分辨率可以保留更多细节,但编码时间更长,文件体积更大,而且会降低系统的响应速度。",
     "image_settings": "图片设置",
     "image_settings_description": "管理生成图像的质量和分辨率",
-    "image_thumbnail_format": "缩略图格式",
-    "image_thumbnail_resolution": "缩略图分辨率",
-    "image_thumbnail_resolution_description": "用于查看照片组(主时间轴、相册视图等)。更高的分辨率可以保留更多的细节,但编码时间更长,文件体积更大,并会降低应用程序的响应速度。",
+    "image_thumbnail_description": "去除元数据的小缩略图,用于浏览主时间线等照片组",
+    "image_thumbnail_quality_description": "缩略图质量从 1 到 100。越高越好,但会产生更大的文件,并且会降低系统的响应能力。",
+    "image_thumbnail_title": "缩略图设置",
     "job_concurrency": "{job}并发",
+    "job_created": "任务已创建",
     "job_not_concurrency_safe": "此任务并发并不安全。",
     "job_settings": "任务设置",
     "job_settings_description": "管理任务并发",
     "job_status": "任务状态",
-    "jobs_delayed": "{jobCount, plural, other {#项作业已推迟}}",
+    "jobs_delayed": "{jobCount, plural, other {#项任务已推迟}}",
     "jobs_failed": "{jobCount, plural, other {#项失败}}",
     "library_created": "已创建图库:{library}",
-    "library_cron_expression": "Cron 表达式",
-    "library_cron_expression_description": "使用 cron 格式设置扫描间隔。关于 cron 格式请参阅<link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Cron 表达式预设",
     "library_deleted": "图库已删除",
     "library_import_path_description": "指定一个要导入的文件夹。将扫描此文件夹(包括子文件夹)中的图像和视频。",
     "library_scanning": "定期扫描",
@@ -95,7 +104,7 @@
     "logging_level_description": "启用的日志级别。",
     "logging_settings": "日志",
     "machine_learning_clip_model": "CLIP模型",
-    "machine_learning_clip_model_description": "支持的CLIP模型名称见 <link>此处</link>。注意,更换模型后需要对所有图片重新运行“智能检索”作业。",
+    "machine_learning_clip_model_description": "支持的CLIP模型名称见 <link>此处</link>。注意,更换模型后需要对所有图片重新运行“智能检索”任务。",
     "machine_learning_duplicate_detection": "重复项检测",
     "machine_learning_duplicate_detection_enabled": "启用重复检测",
     "machine_learning_duplicate_detection_enabled_description": "如果禁用此功能,完全相同的项目仍将被去重。",
@@ -119,10 +128,10 @@
     "machine_learning_settings": "机器学习设置",
     "machine_learning_settings_description": "管理机器学习功能和设置",
     "machine_learning_smart_search": "智能搜索",
-    "machine_learning_smart_search_description": "使用CLIP相似度进行图像语义搜索",
+    "machine_learning_smart_search_description": "使用CLIP以文搜图、智能搜图",
     "machine_learning_smart_search_enabled": "启用智能搜索",
     "machine_learning_smart_search_enabled_description": "如果禁用,则不会对图像编码以用于智能搜索。",
-    "machine_learning_url_description": "机器学习服务器的URL",
+    "machine_learning_url_description": "机器学习服务器的 URL。如果提供多个 URL,则将按依次尝试连接每个服务器,直到有一个服务器成功响应为止。",
     "manage_concurrency": "管理任务并发",
     "manage_log_settings": "管理日志设置",
     "map_dark_style": "深色模式",
@@ -148,12 +157,12 @@
     "migration_job_description": "将项目和人脸识别的缩略图迁移到最新的文件夹结构",
     "no_paths_added": "无已添加路径",
     "no_pattern_added": "无已添加规则",
-    "note_apply_storage_label_previous_assets": "提示:要将存储标签应用于之前上传的项目,运行以下命令",
+    "note_apply_storage_label_previous_assets": "提示:要将存储标签应用于之前上传的项目,需要运行",
     "note_cannot_be_changed_later": "注意:此项一旦设定,以后无法更改!",
     "note_unlimited_quota": "提示:输入0表示无限制",
     "notification_email_from_address": "发件人地址",
-    "notification_email_from_address_description": "发件人邮箱地址,例如“Immich 服务器 <noreply@immich.app>”",
-    "notification_email_host_description": "邮件服务器主机(例如 smtp.immich.app)",
+    "notification_email_from_address_description": "发件人邮箱,例如:“张三 <12345@qq.com>”",
+    "notification_email_host_description": "服务器地址(例如:smtp.qq.com)",
     "notification_email_ignore_certificate_errors": "忽略证书错误",
     "notification_email_ignore_certificate_errors_description": "忽略TLS证书验证错误(不建议)",
     "notification_email_password_description": "与邮件服务器进行身份验证时使用的密码",
@@ -171,7 +180,7 @@
     "oauth_auto_launch_description": "在登录页面自动启动OAuth登录",
     "oauth_auto_register": "自动注册",
     "oauth_auto_register_description": "使用OAuth登录后自动注册新用户",
-    "oauth_button_text": "按钮文本",
+    "oauth_button_text": "按钮名称",
     "oauth_client_id": "客户端ID",
     "oauth_client_secret": "客户端密钥",
     "oauth_enable_description": "使用OAuth登录",
@@ -184,7 +193,7 @@
     "oauth_scope": "范围",
     "oauth_settings": "OAuth",
     "oauth_settings_description": "管理OAuth登录设置",
-    "oauth_settings_more_details": "关于本功能的更多详细信息,请查看<link>相关文档</link>。",
+    "oauth_settings_more_details": "关于此功能的更多详细信息,请查看<link>相关文档</link>。",
     "oauth_signing_algorithm": "签名算法",
     "oauth_storage_label_claim": "存储标签声明",
     "oauth_storage_label_claim_description": "自动将用户的存储标签设置为此项的值。",
@@ -198,22 +207,24 @@
     "password_settings": "密码登录",
     "password_settings_description": "管理密码登录设置",
     "paths_validated_successfully": "所有路径验证成功",
+    "person_cleanup_job": "清理人物",
     "quota_size_gib": "配额大小(GB)",
     "refreshing_all_libraries": "刷新所有图库",
     "registration": "注册管理员",
     "registration_description": "由于您是系统上的第一个用户,您将被指定为管理员并负责管理任务,由您来创建新的用户。",
-    "removing_offline_files": "移除离线文件",
     "repair_all": "修复所有",
     "repair_matched_items": "匹配到 {count, plural, one {#个项目} other {#个项目}}",
     "repaired_items": "已修复{count, plural, one {#个项目} other {#个项目}}",
     "require_password_change_on_login": "要求用户首次登录时更改密码",
     "reset_settings_to_default": "恢复默认设置",
     "reset_settings_to_recent_saved": "恢复到最近保存的设置",
-    "scanning_library_for_changed_files": "扫描图库变更的文件",
-    "scanning_library_for_new_files": "扫描图库新增的文件",
+    "scanning_library": "扫描图库",
+    "search_jobs": "搜索任务...",
     "send_welcome_email": "发送欢迎邮件",
     "server_external_domain_settings": "外部域名",
     "server_external_domain_settings_description": "共享链接域名,包括 http(s)://",
+    "server_public_users": "公共用户",
+    "server_public_users_description": "将用户添加到共享相册时,会列出所有用户(姓名和邮箱)。禁用后,用户列表将仅对管理员用户可用。",
     "server_settings": "服务器设置",
     "server_settings_description": "管理服务器设置",
     "server_welcome_message": "欢迎消息",
@@ -230,7 +241,7 @@
     "storage_template_migration": "存储模板转换",
     "storage_template_migration_description": "应用当前的<link>{template}</link>到之前上传的项目",
     "storage_template_migration_info": "模板修改将只作用于新的项目。如也需应用此模板到之前上传的项目,请运行<link>{job}</link>。",
-    "storage_template_migration_job": "存储模板迁移任务",
+    "storage_template_migration_job": "存储模板转换任务",
     "storage_template_more_details": "关于本功能的更多细节,请参见<template-link>存储模板</template-link>及其<implications-link>实现方式</implications-link>",
     "storage_template_onboarding_description": "启用后,本功能将根据用户定义的模板自动整理文件。出于稳定性考虑,本功能默认是禁用的。更多详细信息请参见 <link>文档</link>。",
     "storage_template_path_length": "路径的字符长度及限制:<b>{length, number}</b>/{limit, number}",
@@ -238,18 +249,28 @@
     "storage_template_settings_description": "管理上传项目文件夹结构和文件名",
     "storage_template_user_label": "<code>{label}</code>是用户的存储标签",
     "system_settings": "系统设置",
+    "tag_cleanup_job": "清理标签",
+    "template_email_available_tags": "可以在模板中使用以下变量:{tags}",
+    "template_email_if_empty": "如果模板为空,则使用默认模板。",
+    "template_email_invite_album": "相册邀请模板",
+    "template_email_preview": "预览",
+    "template_email_settings": "邮件模板",
+    "template_email_settings_description": "管理自定义邮件通知模板",
+    "template_email_update_album": "相册更新模板",
+    "template_email_welcome": "欢迎邮件模板",
+    "template_settings": "通知模板",
+    "template_settings_description": "管理自定义通知模板。",
     "theme_custom_css_settings": "自定义CSS",
     "theme_custom_css_settings_description": "可以通过CSS自定义Immich外观。",
     "theme_settings": "主题设置",
-    "theme_settings_description": "管理Immich web界面定制",
+    "theme_settings_description": "管理Immich web界面的定制",
     "these_files_matched_by_checksum": "这些文件与校验匹配",
     "thumbnail_generation_job": "生成缩略图",
     "thumbnail_generation_job_description": "为每个项目生成不同尺寸的缩略图,并为每个人物生成缩略图",
-    "transcode_policy_description": "视频转码的策略。HDR视频将始终进行转码(除非禁用了转码功能)。",
     "transcoding_acceleration_api": "加速器API",
     "transcoding_acceleration_api_description": "这个API将与您的设备交互,以加速转码过程。此设置为“尽力而为”——如果转码失败,将回到软件转码。VP9是否工作取决于您的硬件配置。",
     "transcoding_acceleration_nvenc": "NVENC(需要 NVIDIA GPU)",
-    "transcoding_acceleration_qsv": "快速同步(需要Intel 7代及以上的 CPU)",
+    "transcoding_acceleration_qsv": "Quick Sync(需要Intel 7代及以上的 CPU)",
     "transcoding_acceleration_rkmpp": "RKMPP(仅适用于 Rockchip SOCs)",
     "transcoding_acceleration_vaapi": "VAAPI",
     "transcoding_accepted_audio_codecs": "支持的音频编解码器",
@@ -271,7 +292,7 @@
     "transcoding_hardware_acceleration": "硬件加速",
     "transcoding_hardware_acceleration_description": "(实验性功能)速度更快,但在相同码率下质量会降低",
     "transcoding_hardware_decoding": "硬件解码",
-    "transcoding_hardware_decoding_setting_description": "仅适用于NVENC、QSV和RKMPP。启用端到端加速,而不仅仅是加速编码。可能并不适用于所有视频。",
+    "transcoding_hardware_decoding_setting_description": "启用端到端加速,而不仅仅是加速编码。可能并不适用于所有视频。",
     "transcoding_hevc_codec": "HEVC 编解码器",
     "transcoding_max_b_frames": "最大B帧数",
     "transcoding_max_b_frames_description": "较高的值可以提高压缩效率,但会减慢编码速度。可能与旧设备上的硬件加速不兼容。0表示将禁用B帧,-1表示将自动设置此参数。",
@@ -291,14 +312,12 @@
     "transcoding_settings_description": "管理视频文件的分辨率和编码信息",
     "transcoding_target_resolution": "目标分辨率",
     "transcoding_target_resolution_description": "更高的分辨率可以保留更多细节,但编码时间更长,文件体积更大,且可能降低应用程序的响应速度。",
-    "transcoding_temporal_aq": "Temporal AQ",
+    "transcoding_temporal_aq": "时间自适应量化",
     "transcoding_temporal_aq_description": "仅适用于NVENC。提高高细节、低动态场景的质量。可能与旧设备不兼容。",
     "transcoding_threads": "线程数",
     "transcoding_threads_description": "设定值越高,编码速度越快,留给其它任务(Docker外宿主机的任务等)的计算能力越少。此值不应大于CPU核心的数量。0表示最大限度地提高利用率。",
     "transcoding_tone_mapping": "色调映射",
     "transcoding_tone_mapping_description": "在将HDR视频转换为SDR时,尝试保持其外观。每种算法在颜色、细节和亮度方面做出了不同的权衡。Hable算法保留细节,Mobius算法保留颜色,而Reinhard算法保留亮度。",
-    "transcoding_tone_mapping_npl": "NPL色调映射",
-    "transcoding_tone_mapping_npl_description": "对于这种亮度的显示器,颜色将被调整到显示正常。与直觉相反,较低的值会增加视频的亮度,反之亦然,因为它会补偿显示器的亮度。0表示将自动设置此值。",
     "transcoding_transcode_policy": "转码策略",
     "transcoding_transcode_policy_description": "视频转码策略。HDR视频将始终进行转码(除非禁用了转码功能)。",
     "transcoding_two_pass_encoding": "二次编码",
@@ -310,8 +329,9 @@
     "trash_number_of_days_description": "回收站中项目永久删除的天数",
     "trash_settings": "回收站设置",
     "trash_settings_description": "管理回收站设置",
-    "untracked_files": "未被追踪的文件",
-    "untracked_files_description": "这些文件未被系统追踪。 这可能是移动失败、上传中断或因bug而落下",
+    "untracked_files": "未被扫描的文件",
+    "untracked_files_description": "这些文件未被系统扫描。 这可能是移动失败、上传中断或因bug而落下",
+    "user_cleanup_job": "清理用户",
     "user_delete_delay": "<b>{user}</b>的账户及项目将在{delay, plural, one {#天} other {#天}}后自动永久删除。",
     "user_delete_delay_settings": "延期删除",
     "user_delete_delay_settings_description": "删除用户后永久删除账户及其所有项目的天数。用户删除作业在午夜运行,检查是否有用户可以删除。对该设置的更改将在下次执行时开始计算。",
@@ -320,11 +340,11 @@
     "user_management": "用户管理",
     "user_password_has_been_reset": "该用户的密码被重置:",
     "user_password_reset_description": "请向用户提供临时密码,并告知他们下次登录时需要更改密码。",
-    "user_restore_description": "<b>{user}</b>的账户将被恢复。",
+    "user_restore_description": "账户“<b>{user}</b>”将被恢复。",
     "user_restore_scheduled_removal": "恢复用户 - 计划于{date, date, long}删除",
     "user_settings": "用户设置",
     "user_settings_description": "管理用户设置",
-    "user_successfully_removed": "用户{email}已被成功删除。",
+    "user_successfully_removed": "用户“{email}”已被成功删除。",
     "version_check_enabled_description": "启用版本检测",
     "version_check_implications": "版本检查功能依赖于与 github.com 的定期通信",
     "version_check_settings": "版本检查",
@@ -340,22 +360,22 @@
   "age_year_months": "1岁{months, plural, one {#个月} other {#个月}}",
   "age_years": "{years, plural, other {#岁}}",
   "album_added": "相册已添加",
-  "album_added_notification_setting_description": "当您被添加到共享相册时,接收电子邮件通知",
+  "album_added_notification_setting_description": "当您被添加到共享相册时,接收邮箱通知",
   "album_cover_updated": "相册封面已更新",
   "album_delete_confirmation": "确定要删除相册“{album}”吗?",
   "album_delete_confirmation_description": "如果该相册是共享的,其他用户将无法再访问它。",
   "album_info_updated": "相册信息已更新",
   "album_leave": "退出相册?",
-  "album_leave_confirmation": "确定要退出相册{album}吗?",
+  "album_leave_confirmation": "确定要退出相册“{album}”吗?",
   "album_name": "相册名称",
   "album_options": "相册设置",
   "album_remove_user": "移除用户?",
-  "album_remove_user_confirmation": "你确定要移除{user}吗?",
+  "album_remove_user_confirmation": "你确定要移除“{user}”吗?",
   "album_share_no_users": "看起来您已与所有用户共享了此相册,或者您根本没有任何用户可共享。",
   "album_updated": "相册已更新",
   "album_updated_setting_description": "当共享相册有新项目时接收邮件通知",
-  "album_user_left": "离开{album}",
-  "album_user_removed": "已移除{user}",
+  "album_user_left": "离开“{album}”",
+  "album_user_removed": "已移除“{user}”",
   "album_with_link_access": "拥有此链接的任何人均可查看本相册中的照片和人物。",
   "albums": "相册",
   "albums_count": "{count, plural, one {{count, number} 个相册} other {{count, number} 个相册}}",
@@ -368,28 +388,27 @@
   "allow_public_user_to_download": "允许所有用户下载",
   "allow_public_user_to_upload": "允许所有用户上传",
   "anti_clockwise": "逆时针",
-  "api_key": "API Key",
-  "api_key_description": "该应用密钥只会展示一次。请确保在关闭窗口前复制下来。",
+  "api_key": "API 密钥",
+  "api_key_description": "该应用密钥只会显示一次。请确保在关闭窗口前复制下来。",
   "api_key_empty": "API Key的名称不可以为空",
-  "api_keys": "API Keys",
+  "api_keys": "API 密钥",
   "app_settings": "应用设置",
   "appears_in": "出现于",
   "archive": "归档",
   "archive_or_unarchive_photo": "归档或取消归档照片",
   "archive_size": "归档大小",
   "archive_size_description": "配置下载归档大小(GB)",
-  "archived": "已归档",
   "archived_count": "{count, plural, other {已归档 # 项}}",
   "are_these_the_same_person": "是同一个人吗?",
   "are_you_sure_to_do_this": "确定要这样做吗?",
   "asset_added_to_album": "已添加至相册",
   "asset_adding_to_album": "正在添加至相册...",
   "asset_description_updated": "项目描述已更新",
-  "asset_filename_is_offline": "项目{filename}已离线",
+  "asset_filename_is_offline": "项目“{filename}”已离线",
   "asset_has_unassigned_faces": "项目中有未分配的人脸",
   "asset_hashing": "哈希校验中...",
   "asset_offline": "项目离线",
-  "asset_offline_description": "项目已离线。Immich无法访问该文件。请确保项目可读并重新扫描项目库。",
+  "asset_offline_description": "磁盘上已找不到该外部项目。请联系您的 Immich 管理员寻求帮助。",
   "asset_skipped": "已跳过",
   "asset_skipped_in_trash": "已回收",
   "asset_uploaded": "已上传",
@@ -399,11 +418,10 @@
   "assets_added_to_album_count": "已添加{count, plural, one {#个项目} other {#个项目}}到相册",
   "assets_added_to_name_count": "已添加{count, plural, one {#个项目} other {#个项目}}到{hasName, select, true {<b>{name}</b>} other {新相册}}",
   "assets_count": "{count, plural, one {#个项目} other {#个项目}}",
-  "assets_moved_to_trash": "将{count, plural, one {# 个项目} other {# 个项目}}移动到回收站",
   "assets_moved_to_trash_count": "已移动{count, plural, one {#个项目} other {#个项目}}到回收站",
   "assets_permanently_deleted_count": "已永久删除{count, plural, one {#个项目} other {#个项目}}",
   "assets_removed_count": "已移除{count, plural, one {#个项目} other {#个项目}}",
-  "assets_restore_confirmation": "确定要恢复回收站中的所有项目吗?该操作无法撤消!",
+  "assets_restore_confirmation": "确定要恢复回收站中的所有项目吗?该操作无法撤消!请注意,脱机项目无法通过这种方式恢复。",
   "assets_restored_count": "已恢复{count, plural, one {#个项目} other {#个项目}}",
   "assets_trashed_count": "{count, plural, one {#个项目} other {#个项目}}已放入回收站",
   "assets_were_part_of_album_count": "{count, plural, one {项目} other {项目}}已经在相册中",
@@ -412,11 +430,12 @@
   "back_close_deselect": "返回、关闭或反选",
   "backward": "后退",
   "birthdate_saved": "出生日期保存成功",
-  "birthdate_set_description": "出生日期用于计算照片中人物在拍照时的年龄。",
+  "birthdate_set_description": "出生日期用于计算照片中该人物在拍照时的年龄。",
   "blurred_background": "背景模糊",
+  "bugs_and_feature_requests": "Bug和特性要求",
   "build": "构建版本",
   "build_image": "镜像版本",
-  "bulk_delete_duplicates_confirmation": "您确定要批量删除{count, plural, one {#个重复项目} other {#个重复项目}}吗?这将保留每个组中最大的项目并永久删除所有其它重复项目。此操作无法撤消!",
+  "bulk_delete_duplicates_confirmation": "您确定要批量删除{count, plural, one {#个重复项目} other {#个重复项目}}吗?这将保留每个组中最大的项目并永久删除所有其它重复项目。注意:该操作无法被撤消!",
   "bulk_keep_duplicates_confirmation": "您确定要保留{count, plural, one {#个重复项目} other {#个重复项目}}吗?这将清空所有重复记录,但不会删除任何内容。",
   "bulk_trash_duplicates_confirmation": "您确定要批量删除{count, plural, one {#个重复项目} other {#个重复项目}}吗?这将保留每组中最大的项目并删除所有其它重复项目。",
   "buy": "购买Immich",
@@ -426,12 +445,8 @@
   "cancel": "取消",
   "cancel_search": "取消搜索",
   "cannot_merge_people": "无法合并人物",
-  "cannot_undo_this_action": "无法撤消此操作!",
+  "cannot_undo_this_action": "注意:该操作无法被撤消!",
   "cannot_update_the_description": "无法更新描述",
-  "cant_apply_changes": "无法应用更改",
-  "cant_get_faces": "找不到人脸",
-  "cant_search_people": "找不到人物",
-  "cant_search_places": "找不到地点",
   "change_date": "更改日期",
   "change_expiration_time": "更改过期时间",
   "change_location": "更改位置",
@@ -449,7 +464,7 @@
   "clear_all": "清空全部",
   "clear_all_recent_searches": "清除所有最近搜索",
   "clear_message": "清空消息",
-  "clear_value": "清空值",
+  "clear_value": "删除内容",
   "clockwise": "顺时针",
   "close": "关闭",
   "collapse": "折叠",
@@ -463,9 +478,10 @@
   "confirm": "确认",
   "confirm_admin_password": "确认管理员密码",
   "confirm_delete_shared_link": "您确定要删除此共享链接吗?",
+  "confirm_keep_this_delete_others": "除此项目外,堆叠中的所有其它项目都将被删除。您确定要继续吗?",
   "confirm_password": "确认密码",
   "contain": "包含",
-  "context": "图像语义搜索",
+  "context": "以文搜图",
   "continue": "继续",
   "copied_image_to_clipboard": "已复制图片至剪贴板。",
   "copied_to_clipboard": "已复制到剪切板!",
@@ -496,12 +512,12 @@
   "custom_locale": "自定义地区",
   "custom_locale_description": "日期和数字显示格式跟随语言和地区",
   "dark": "深色",
-  "date_after": "日期之后",
+  "date_after": "开始日期",
   "date_and_time": "日期与时间",
-  "date_before": "日期之前",
+  "date_before": "结束日期",
   "date_of_birth_saved": "出生日期保存成功",
   "date_range": "日期范围",
-  "day": "天",
+  "day": "日",
   "deduplicate_all": "删除所有重复项",
   "default_locale": "默认地区",
   "default_locale_description": "根据您的浏览器地区设置日期和数字显示格式",
@@ -512,16 +528,19 @@
   "delete_key": "删除密钥",
   "delete_library": "删除图库",
   "delete_link": "删除链接",
+  "delete_others": "删除其它",
   "delete_shared_link": "删除共享链接",
   "delete_tag": "删除标签",
   "delete_tag_confirmation_prompt": "您确定要删除“{tagName}”标签吗?",
   "delete_user": "删除用户",
   "deleted_shared_link": "共享链接已删除",
+  "deletes_missing_assets": "删除磁盘中丢失的项目",
   "description": "描述",
   "details": "详情",
   "direction": "方向",
   "disabled": "已禁用",
   "disallow_edits": "不允许编辑",
+  "discord": "Discord聊天",
   "discover": "发现",
   "dismiss_all_errors": "忽略所有错误",
   "dismiss_error": "忽略错误",
@@ -530,6 +549,7 @@
   "display_original_photos": "显示原始照片",
   "display_original_photos_setting_description": "在网络与原始格式兼容的情况下,查看图片或视频时优先显示原始文件而不是缩略图。这可能导致照片显示速度变慢。",
   "do_not_show_again": "不再显示该信息",
+  "documentation": "文档",
   "done": "完成",
   "download": "下载",
   "download_include_embedded_motion_videos": "内嵌视频",
@@ -537,18 +557,11 @@
   "download_settings": "下载",
   "download_settings_description": "管理项目下载相关设置",
   "downloading": "下载中",
-  "downloading_asset_filename": "下载项目{filename}",
+  "downloading_asset_filename": "下载项目“{filename}”",
   "drop_files_to_upload": "拖放文件以上传",
   "duplicates": "重复项",
   "duplicates_description": "审查每组疑似重复项并标记哪些是重复的(如果有的话)",
   "duration": "时长",
-  "durations": {
-    "days": "{days, plural, one {天} other {{days, number} 天}}",
-    "hours": "{hours, plural, one {小时} other {{hours, number} 小时}}",
-    "minutes": "{minutes, plural, one {分钟} other {{minutes, number} 分钟}}",
-    "months": "{months, plural, one {月} other {{months, number} 月}}",
-    "years": "{years, plural, one {年} other {{years, number} 年}}"
-  },
   "edit": "编辑",
   "edit_album": "编辑相册",
   "edit_avatar": "编辑头像",
@@ -573,8 +586,6 @@
   "editor_crop_tool_h2_aspect_ratios": "长宽比",
   "editor_crop_tool_h2_rotation": "旋转",
   "email": "邮箱",
-  "empty": "空",
-  "empty_album": "清空相册",
   "empty_trash": "清空回收站",
   "empty_trash_confirmation": "确定要清空回收站?这将永久删除回收站中的所有项目。\n注意:该操作无法撤消!",
   "enable": "启用",
@@ -582,7 +593,7 @@
   "end_date": "结束日期",
   "error": "错误",
   "error_loading_image": "加载图片时出错",
-  "error_title": "错误 - 出了点问题",
+  "error_title": "错误 - 好像出了问题",
   "errors": {
     "cannot_navigate_next_asset": "无法导航到下一个项目",
     "cannot_navigate_previous_asset": "无法导航到上一个项目",
@@ -594,7 +605,7 @@
     "cant_get_number_of_comments": "无法获取评论数量",
     "cant_search_people": "无法检索人物",
     "cant_search_places": "无法检索地点",
-    "cleared_jobs": "已删除作业:{job}",
+    "cleared_jobs": "已删除任务:{job}",
     "error_adding_assets_to_album": "添加项目到相册时出错",
     "error_adding_users_to_album": "添加用户到相册时出错",
     "error_deleting_shared_user": "删除共享用户时出错",
@@ -608,6 +619,7 @@
     "failed_to_create_shared_link": "创建共享链接失败",
     "failed_to_edit_shared_link": "编辑共享链接失败",
     "failed_to_get_people": "无法获取人物",
+    "failed_to_keep_this_delete_others": "无法保留该项目并删除其它项目",
     "failed_to_load_asset": "加载项目失败",
     "failed_to_load_assets": "加载项目失败",
     "failed_to_load_people": "加载人物失败",
@@ -635,8 +647,6 @@
     "unable_to_change_location": "无法更改位置",
     "unable_to_change_password": "无法修改密码",
     "unable_to_change_visibility": "无法修改{count, plural, one {#个人} other {#个人}}的可见性",
-    "unable_to_check_item": "无法选中项目",
-    "unable_to_check_items": "无法选中项目",
     "unable_to_complete_oauth_login": "无法完成OAuth登录",
     "unable_to_connect": "无法连接",
     "unable_to_connect_to_server": "无法连接至服务器",
@@ -677,12 +687,10 @@
     "unable_to_remove_album_users": "无法从相册中移除用户",
     "unable_to_remove_api_key": "无法移除API Key",
     "unable_to_remove_assets_from_shared_link": "无法从共享链接中移除项目",
-    "unable_to_remove_comment": "无法移除评论",
+    "unable_to_remove_deleted_assets": "无法移除离线文件",
     "unable_to_remove_library": "无法移除图库",
-    "unable_to_remove_offline_files": "无法移除离线文件",
     "unable_to_remove_partner": "无法移除同伴",
-    "unable_to_remove_reaction": "无法移除反应",
-    "unable_to_remove_user": "无法移除用户",
+    "unable_to_remove_reaction": "无法移除回应",
     "unable_to_repair_items": "无法修复项目",
     "unable_to_reset_password": "无法重置密码",
     "unable_to_resolve_duplicate": "无法解决重复项",
@@ -712,10 +720,6 @@
     "unable_to_update_user": "无法更新用户",
     "unable_to_upload_file": "无法上传文件"
   },
-  "every_day_at_onepm": "每天下午一点",
-  "every_night_at_midnight": "每天午夜",
-  "every_night_at_twoam": "每天凌晨两点",
-  "every_six_hours": "每6小时",
   "exif": "Exif信息",
   "exit_slideshow": "退出幻灯片放映",
   "expand_all": "全部展开",
@@ -730,38 +734,33 @@
   "external": "外部的",
   "external_libraries": "外部图库",
   "face_unassigned": "未指派",
-  "failed_to_get_people": "无法获取人物",
+  "failed_to_load_assets": "加载项目失败",
   "favorite": "收藏",
   "favorite_or_unfavorite_photo": "收藏或取消收藏照片",
   "favorites": "收藏夹",
-  "feature": "功能",
   "feature_photo_updated": "人物头像已更新",
-  "featurecollection": "功能合集",
   "features": "功能",
   "features_setting_description": "管理App功能",
   "file_name": "文件名",
-  "file_name_or_extension": "文件名或扩展名",
+  "file_name_or_extension": "文件名",
   "filename": "文件名",
-  "files": "",
   "filetype": "文件类型",
   "filter_people": "过滤人物",
   "find_them_fast": "按名称快速搜索",
   "fix_incorrect_match": "修复不正确的匹配",
   "folders": "文件夹",
   "folders_feature_description": "在文件夹视图中浏览文件系统上的照片和视频",
-  "force_re-scan_library_files": "强制重新扫描所有图库文件",
   "forward": "向前",
   "general": "通用",
   "get_help": "获取帮助",
   "getting_started": "入门",
   "go_back": "返回",
   "go_to_search": "前往搜索",
-  "go_to_share_page": "转到共享页面",
   "group_albums_by": "相册分组依据...",
   "group_no": "未分组",
   "group_owner": "按所有者分组",
   "group_year": "按年分组",
-  "has_quota": "有限额",
+  "has_quota": "配额大小",
   "hi_user": "你好,{name}({email})",
   "hide_all_people": "隐藏所有人物",
   "hide_gallery": "隐藏相册",
@@ -769,7 +768,7 @@
   "hide_password": "隐藏密码",
   "hide_person": "隐藏人物",
   "hide_unnamed_people": "隐藏未命名的人物",
-  "host": "主机",
+  "host": "服务器",
   "hour": "时",
   "image": "图片",
   "image_alt_text_date": "在{date}拍摄的{isVideo, select, true {视频} other {照片}}",
@@ -782,34 +781,31 @@
   "image_alt_text_date_place_2_people": "{date}在{country}{city}拍摄的包含{person1}和{person2}的{isVideo, select, true {视频} other {照片}}",
   "image_alt_text_date_place_3_people": "{date}在{country}{city}拍摄的包含{person1}、{person2}和{person3}的{isVideo, select, true {视频} other {照片}}",
   "image_alt_text_date_place_4_or_more_people": "{date}在{country}{city}拍摄的包含{person1}、{person2}及其他{additionalCount, number}个人物的{isVideo, select, true {视频} other {照片}}",
-  "image_alt_text_people": "{count, plural, =1 {和{person1}在一起} =2 {和{person1}及{person2}在一起} =3 {和{person1}、{person2}及{person3}在一起} other {和{person1}、{person2}及其他{others, number}个人在一起}}",
-  "image_alt_text_place": "在{country} {city}",
-  "image_taken": "{isVideo, select, true {选择视频} other {选择图片}}",
-  "img": "图片",
   "immich_logo": "Immich Logo",
-  "immich_web_interface": "Immich Web接口",
+  "immich_web_interface": "Immich Web界面",
   "import_from_json": "从JSON导入",
   "import_path": "导入路径",
   "in_albums": "在{count, plural, one {#个相册} other {#个相册}}中",
   "in_archive": "在归档中",
   "include_archived": "包括已归档",
-  "include_shared_albums": "包含共享相册",
+  "include_shared_albums": "包括共享相册",
   "include_shared_partner_assets": "包括同伴共享项目",
   "individual_share": "个人分享",
   "info": "信息",
   "interval": {
     "day_at_onepm": "每天下午1点",
     "hours": "每 {hours, plural, one {小时} other {{hours, number} 小时}}",
-    "night_at_midnight": "每晚24点",
+    "night_at_midnight": "每晚0点",
     "night_at_twoam": "每晚凌晨2点"
   },
   "invite_people": "邀请人员",
   "invite_to_album": "邀请加入相册",
   "items_count": "{count, plural, one {#个项目} other {#个项目}}",
-  "job_settings_description": "管理任务并发",
   "jobs": "任务",
   "keep": "保留",
   "keep_all": "保留所有",
+  "keep_this_delete_others": "保留这个,删除其它",
+  "kept_this_deleted_others": "保留该项目并删除 {count, plural, one {# 个项目} other {# 个项目}}",
   "keyboard_shortcuts": "键盘快捷键",
   "language": "语言",
   "language_setting_description": "选择您的语言偏好",
@@ -821,31 +817,6 @@
   "level": "等级",
   "library": "图库",
   "library_options": "图库选项",
-  "license_account_info": "您的帐户已授权",
-  "license_activated_subtitle": "感谢您对Immich和开源软件的支持",
-  "license_activated_title": "您的授权已激活成功",
-  "license_button_activate": "激活",
-  "license_button_buy": "购买",
-  "license_button_buy_license": "购买授权",
-  "license_button_select": "选择",
-  "license_failed_activation": "授权激活失败。请检查邮件获取正确的授权码!",
-  "license_individual_description_1": "1个用于任意服务的授权用户",
-  "license_individual_title": "个人授权",
-  "license_info_licensed": "已授权",
-  "license_info_unlicensed": "未授权",
-  "license_input_suggestion": "已有授权?请在下方输入授权码",
-  "license_license_subtitle": "购买授权来支持Immich",
-  "license_license_title": "授权",
-  "license_lifetime_description": "永久授权",
-  "license_per_server": "每台服务器",
-  "license_per_user": "每个用户",
-  "license_server_description_1": "1台授权服务器",
-  "license_server_description_2": "授权给本服务器中的所有用户",
-  "license_server_title": "服务器授权",
-  "license_trial_info_1": "您运行的是未授权的Immich版本",
-  "license_trial_info_2": "您已经使用Immich大概",
-  "license_trial_info_3": "{accountAge, plural, one {#天} other {#天}}",
-  "license_trial_info_4": "请考虑购买授权来支持此服务的持续开发",
   "light": "浅色",
   "like_deleted": "已删除的收藏",
   "link_motion_video": "链接动态视频",
@@ -867,6 +838,7 @@
   "look": "样式",
   "loop_videos": "循环视频",
   "loop_videos_description": "启用在详细信息中自动循环播放视频。",
+  "main_branch_warning": "您当前使用的是开发版;我们强烈建议您使用正式发行版(release版)!",
   "make": "品牌",
   "manage_shared_links": "管理共享链接",
   "manage_sharing_with_partners": "管理与同伴的共享",
@@ -889,7 +861,7 @@
   "merge": "合并",
   "merge_people": "合并人物",
   "merge_people_limit": "每次最多只能合并 5 个人",
-  "merge_people_prompt": "你想合并这些人吗?此操作不可逆。",
+  "merge_people_prompt": "你想合并这些人吗?该操作不可逆(无法被撤销)。",
   "merge_people_successfully": "合并人物成功",
   "merged_people_count": "已合并{count, plural, one {#个人} other {#个人}}",
   "minimize": "最小化",
@@ -919,7 +891,7 @@
   "no_archived_assets_message": "归档照片和视频以便在照片视图中隐藏它们",
   "no_assets_message": "点击上传您的第一张照片",
   "no_duplicates_found": "未发现重复项。",
-  "no_exif_info_available": "没有可用的exif信息",
+  "no_exif_info_available": "没有可用的EXIF信息",
   "no_explore_results_message": "上传更多照片来探索。",
   "no_favorites_message": "添加到收藏夹,快速查找最佳图片和视频",
   "no_libraries_message": "创建外部图库来查看你的照片和视频",
@@ -929,13 +901,14 @@
   "no_results_description": "尝试使用同义词或更通用的关键词",
   "no_shared_albums_message": "创建相册以共享照片和视频",
   "not_in_any_album": "不在任何相册中",
-  "note_apply_storage_label_to_previously_uploaded assets": "提示:要将存储标签应用于之前上传的项目,运行以下命令",
-  "note_unlimited_quota": "注:输入 0 表示无限制配额",
+  "note_apply_storage_label_to_previously_uploaded assets": "提示:要将存储标签应用于之前上传的项目,需要运行",
+  "note_unlimited_quota": "注:输入 0 表示无限配额",
   "notes": "提示",
   "notification_toggle_setting_description": "启用邮件通知",
   "notifications": "通知",
   "notifications_setting_description": "管理通知",
   "oauth": "OAuth",
+  "official_immich_resources": "Immich 官方资源",
   "offline": "离线",
   "offline_paths": "离线文件",
   "offline_paths_description": "这些结果可能是由于手动删除了不属于外部图库的文件造成的。",
@@ -948,7 +921,6 @@
   "onboarding_welcome_user": "欢迎你,{user}",
   "online": "在线",
   "only_favorites": "仅显示已收藏",
-  "only_refreshes_modified_files": "仅刷新修改的文件",
   "open_in_map_view": "在地图视图中打开",
   "open_in_openstreetmap": "在OpenStreetMap中打开",
   "open_the_search_filters": "打开搜索过滤器",
@@ -986,14 +958,12 @@
   "people_edits_count": "{count, plural, one {#个人物} other {#个人物}}已编辑",
   "people_feature_description": "按人物分组进行浏览照片和视频",
   "people_sidebar_description": "在侧边栏中显示“人物”链接",
-  "perform_library_tasks": "",
   "permanent_deletion_warning": "永久删除警告",
   "permanent_deletion_warning_setting_description": "当永久删除项目时显示警告",
   "permanently_delete": "永久删除",
-  "permanently_delete_assets_count": "{count, plural, one {个项目} other {个项目}}已删除",
-  "permanently_delete_assets_prompt": "确定要永久删除{count, plural, one {此项目} other {这<b>#</b>个项目}}?该操作会同时将{count, plural, one {它} other {它们}}从其所在相册中移除。",
+  "permanently_delete_assets_count": "永久删除{count, plural, one {项目} other {项目}}",
+  "permanently_delete_assets_prompt": "确定要永久删除 {count, plural, one {此项目?} other {这<b>#</b>个项目?}} 该操作会同时将 {count, plural, one {它} other {它们}} 从其所在相册中移除。",
   "permanently_deleted_asset": "永久删除的项目",
-  "permanently_deleted_assets": "永久删除{count, plural, one {# 个项目} other {# 个项目}}",
   "permanently_deleted_assets_count": "{count, plural, one {#个项目} other {#个项目}}已删除",
   "person": "人物",
   "person_hidden": "{name}{hidden, select, true {(已隐藏)} other {}}",
@@ -1009,7 +979,6 @@
   "play_memories": "播放回忆",
   "play_motion_photo": "播放动态图片",
   "play_or_pause_video": "播放或暂停视频",
-  "point": "点",
   "port": "端口",
   "preset": "预设",
   "preview": "预览",
@@ -1038,7 +1007,7 @@
   "purchase_individual_description_2": "支持者状态",
   "purchase_individual_title": "个人",
   "purchase_input_suggestion": "已有一个产品密钥?请在下方输入密钥",
-  "purchase_license_subtitle": "购买 Immich 以支持此项目的持续发展",
+  "purchase_license_subtitle": "购买 Immich 以支持此服务的持续发展",
   "purchase_lifetime_description": "终身许可",
   "purchase_option_title": "购买选项",
   "purchase_panel_info_1": "开发 Immich 需要大量的时间和精力,我们有全职工程师在努力将其做到最好。我们的使命是通过开源软件和道德商业实践,为开发者提供可持续的收入来源,并创建一个尊重隐私的生态系统,提供一个可以真正替代现有剥削性云服务的选择。",
@@ -1054,38 +1023,40 @@
   "purchase_server_description_2": "支持者状态",
   "purchase_server_title": "服务器",
   "purchase_settings_server_activated": "服务器产品密钥正在由管理员管理",
-  "range": "范围",
   "rating": "星级",
   "rating_clear": "删除星级",
   "rating_count": "{count, plural, one {#星} other {#星}}",
   "rating_description": "在信息面板中展示EXIF星级",
-  "raw": "Raw",
-  "reaction_options": "反应选项",
+  "reaction_options": "回应选项",
   "read_changelog": "阅读更新日志",
   "reassign": "重新指派",
   "reassigned_assets_to_existing_person": "重新指派{count, plural, one {#个项目} other {#个项目}}到{name, select, null {已存在的人物} other {{name}}}",
   "reassigned_assets_to_new_person": "重新指派{count, plural, one {#个项目} other {#个项目}}到新的人物",
   "reassing_hint": "指派选择的项目到已存在的人物",
   "recent": "最近",
+  "recent-albums": "最近的相册",
   "recent_searches": "最近搜索",
   "refresh": "刷新",
   "refresh_encoded_videos": "刷新已编码的视频",
+  "refresh_faces": "刷新人脸",
   "refresh_metadata": "刷新元数据",
   "refresh_thumbnails": "刷新缩略图",
   "refreshed": "已刷新",
-  "refreshes_every_file": "刷新全部文件",
+  "refreshes_every_file": "重新扫描所有现有文件和新文件",
   "refreshing_encoded_video": "正在刷新已编码视频",
+  "refreshing_faces": "正在刷新人脸",
   "refreshing_metadata": "正在刷新元数据",
   "regenerating_thumbnails": "正在重新生成缩略图",
   "remove": "移除",
   "remove_assets_album_confirmation": "确定要从项目中移除{count, plural, one {#个项目} other {#个项目}}?",
   "remove_assets_shared_link_confirmation": "确定要从共享链接中移除{count, plural, one {#个项目} other {#个项目}}?",
   "remove_assets_title": "移除项目?",
-  "remove_custom_date_range": "根据自定义日期范围移除",
+  "remove_custom_date_range": "取消自定义日期范围",
+  "remove_deleted_assets": "删除离线文件",
   "remove_from_album": "从相册中移除",
   "remove_from_favorites": "移出收藏",
   "remove_from_shared_link": "从共享链接中移除",
-  "remove_offline_files": "删除离线文件",
+  "remove_url": "移除URL",
   "remove_user": "移除用户",
   "removed_api_key": "移除的API Key:{name}",
   "removed_from_archive": "从归档中移除",
@@ -1102,7 +1073,6 @@
   "reset": "重置",
   "reset_password": "重置密码",
   "reset_people_visibility": "重置人物可见性",
-  "reset_settings_to_default": "恢复到默认设置",
   "reset_to_default": "恢复默认值",
   "resolve_duplicates": "处理重复项",
   "resolved_all_duplicates": "解决所有重复问题",
@@ -1122,14 +1092,13 @@
   "saved_settings": "已保存设置",
   "say_something": "说点什么",
   "scan_all_libraries": "扫描所有图库",
-  "scan_all_library_files": "重新扫描所有图库文件",
-  "scan_new_library_files": "扫描新的图库文件",
+  "scan_library": "扫描",
   "scan_settings": "扫描设置",
   "scanning_for_album": "扫描相册中...",
   "search": "搜索",
   "search_albums": "搜索相册",
   "search_by_context": "搜索内容",
-  "search_by_filename": "通过文件名或扩展名搜索",
+  "search_by_filename": "通过文件名搜索",
   "search_by_filename_example": "如 IMG_1234.JPG 或 PNG",
   "search_camera_make": "搜索相机品牌...",
   "search_camera_model": "搜索相机型号...",
@@ -1141,11 +1110,12 @@
   "search_options": "搜索选项",
   "search_people": "搜索人物",
   "search_places": "搜索地点",
+  "search_settings": "搜索设置",
   "search_state": "搜索省份...",
   "search_tags": "搜索标签…",
   "search_timezone": "搜索时区...",
   "search_type": "搜索类型",
-  "search_your_photos": "搜索你的照片",
+  "search_your_photos": "搜索您的照片",
   "searching_locales": "搜索地区...",
   "second": "秒",
   "see_all_people": "查看所有人物",
@@ -1165,7 +1135,6 @@
   "selected_count": "{count, plural, other {#项已选择}}",
   "send_message": "发送消息",
   "send_welcome_email": "发送欢迎邮件",
-  "server": "服务器",
   "server_offline": "服务器离线",
   "server_online": "服务器在线",
   "server_stats": "服务统计",
@@ -1181,16 +1150,16 @@
   "share": "共享",
   "shared": "共享",
   "shared_by": "共享自",
-  "shared_by_user": "由{user}共享",
+  "shared_by_user": "由“{user}”共享",
   "shared_by_you": "你的共享",
-  "shared_from_partner": "来自{partner}的照片",
+  "shared_from_partner": "来自“{partner}”的照片",
   "shared_link_options": "共享链接选项",
   "shared_links": "共享链接",
   "shared_photos_and_videos_count": "{assetCount, plural, other {#项已共享照片&视频。}}",
-  "shared_with_partner": "与{partner}共享",
+  "shared_with_partner": "与“{partner}”共享",
   "sharing": "共享",
   "sharing_enter_password": "请输入密码后查看此页面。",
-  "sharing_sidebar_description": "在侧边栏中显示共享链接",
+  "sharing_sidebar_description": "在侧边栏中显示“共享”链接",
   "shift_to_permanent_delete": "按住Shift键永久删除项目",
   "show_album_options": "显示相册选项",
   "show_albums": "显示相册",
@@ -1208,6 +1177,7 @@
   "show_person_options": "显示人物选项",
   "show_progress_bar": "显示进度条",
   "show_search_options": "显示搜索选项",
+  "show_slideshow_transition": "显示幻灯片过渡效果",
   "show_supporter_badge": "支持者徽章",
   "show_supporter_badge_description": "展示支持者徽章",
   "shuffle": "随机",
@@ -1216,9 +1186,9 @@
   "sign_out": "注销",
   "sign_up": "注册",
   "size": "大小",
-  "skip_to_content": "跳到内容",
-  "skip_to_folders": "跳到文件夹",
-  "skip_to_tags": "跳到标签",
+  "skip_to_content": "跳转到内容",
+  "skip_to_folders": "跳转到文件夹",
+  "skip_to_tags": "跳转到标签",
   "slideshow": "幻灯片放映",
   "slideshow_settings": "放映设置",
   "sort_albums_by": "相册排序依据...",
@@ -1226,7 +1196,7 @@
   "sort_items": "项目数量",
   "sort_modified": "修改日期",
   "sort_oldest": "最早的照片",
-  "sort_recent": "最近的照片",
+  "sort_recent": "最新的照片",
   "sort_title": "标题",
   "source": "源",
   "stack": "堆叠",
@@ -1241,41 +1211,45 @@
   "status": "状态",
   "stop_motion_photo": "定格照片",
   "stop_photo_sharing": "停止共享照片?",
-  "stop_photo_sharing_description": "{partner}将不能访问你的照片。",
+  "stop_photo_sharing_description": "“{partner}”将不能访问你的照片。",
   "stop_sharing_photos_with_user": "停止与此用户共享照片",
   "storage": "存储空间",
   "storage_label": "存储标签",
-  "storage_usage": "总量:{available},已用{used}",
+  "storage_usage": "已用:{used}/{available}",
   "submit": "提交",
   "suggestions": "建议",
   "sunrise_on_the_beach": "海滩上的日出",
-  "swap_merge_direction": "交换合并方向",
+  "support": "支持",
+  "support_and_feedback": "支持和反馈",
+  "support_third_party_description": "您的 Immich 安装程序是由第三方打包的。您遇到的问题可能是由软件包引起的,所以首先请使用下面的链接提出问题或BUG。",
+  "swap_merge_direction": "互换合并方向",
   "sync": "同步",
   "tag": "标签",
   "tag_assets": "标记项目",
   "tag_created": "已创建标签:{tag}",
   "tag_feature_description": "按逻辑标签主题分组进行浏览照片和视频",
-  "tag_not_found_question": "找不到标签吗?<link>点击这里</link>创建一个",
+  "tag_not_found_question": "找不到标签吗?<link>创建新标签</link>",
   "tag_updated": "已更新标签:{tag}",
   "tagged_assets": "{count, plural, one {# 个项目} other {# 个项目}}被加上标签",
   "tags": "标签",
   "template": "模版",
   "theme": "主题",
   "theme_selection": "主题选项",
-  "theme_selection_description": "根据浏览器的系统首选项自动设置主题色",
+  "theme_selection_description": "跟随浏览器自动设置主题颜色",
   "they_will_be_merged_together": "项目将会合并到一起",
+  "third_party_resources": "第三方资源",
   "time_based_memories": "基于时间的回忆",
+  "timeline": "时间线",
   "timezone": "时区",
   "to_archive": "归档",
   "to_change_password": "修改密码",
   "to_favorite": "收藏",
   "to_login": "登录",
   "to_parent": "返回上一级",
-  "to_root": "返回到根目录",
   "to_trash": "放入回收站",
   "toggle_settings": "切换设置",
   "toggle_theme": "切换深色主题",
-  "toggle_visibility": "切换可见性",
+  "total": "总计",
   "total_usage": "总用量",
   "trash": "回收站",
   "trash_all": "全部删除",
@@ -1285,12 +1259,10 @@
   "trashed_items_will_be_permanently_deleted_after": "回收站中的项目将在{days, plural, one {#天} other {#天}}后被永久删除。",
   "type": "种类",
   "unarchive": "取消归档",
-  "unarchived": "已取消归档",
   "unarchived_count": "{count, plural, other {取消归档 # 项}}",
   "unfavorite": "取消收藏",
   "unhide_person": "显示人物",
   "unknown": "未知",
-  "unknown_album": "未知相册",
   "unknown_year": "未知年份",
   "unlimited": "无限制",
   "unlink_motion_video": "取消链接动态视频",
@@ -1299,7 +1271,7 @@
   "unnamed_album": "未命名相册",
   "unnamed_album_delete_confirmation": "您确定要删除该相册吗?",
   "unnamed_share": "未命名共享",
-  "unsaved_change": "未保存的修改",
+  "unsaved_change": "修改未保存",
   "unselect_all": "取消全选",
   "unselect_all_duplicates": "取消选择所有重复项",
   "unstack": "取消堆叠",
@@ -1319,16 +1291,16 @@
   "upload_success": "上传成功,刷新页面查看新上传的项目。",
   "url": "URL",
   "usage": "用量",
-  "use_custom_date_range": "使用自定义日期范围",
+  "use_custom_date_range": "自定义日期范围",
   "user": "用户",
   "user_id": "用户ID",
-  "user_license_settings": "授权",
-  "user_license_settings_description": "管理你的授权",
-  "user_liked": "{user}点赞了{type, select, photo {该照片} video {该视频} asset {该项目} other {它}}",
+  "user_liked": "“{user}”点赞了{type, select, photo {该照片} video {该视频} asset {该项目} other {它}}",
   "user_purchase_settings": "购买",
   "user_purchase_settings_description": "管理购买订单",
-  "user_role_set": "设置{user}为{role}",
+  "user_role_set": "设置“{user}”为“{role}”",
   "user_usage_detail": "用户用量详情",
+  "user_usage_stats": "帐户使用统计",
+  "user_usage_stats_description": "查看帐户使用统计信息",
   "username": "用户名",
   "users": "用户",
   "utilities": "实用工具",
@@ -1336,7 +1308,9 @@
   "variables": "变量",
   "version": "版本",
   "version_announcement_closing": "你的朋友,Alex",
-  "version_announcement_message": "嗨,伙计,当前应用出新版本了,请抽空阅读一下<link>发行说明</link>,并及时更新你的<code>docker-compose.yml</code>和<code>.env</code>文件,避免存在错误配置,特别是当你是使用WatchTower或其它类似的自动升级工具时。",
+  "version_announcement_message": "你好!已经检测到Immich有新版本。请抽空阅读一下<link>发行说明</link>,以确保您的配置文件是最新的,避免存在配置错误,特别是当你是使用WatchTower或其它类似的自动升级工具时。",
+  "version_history": "版本更新历史记录",
+  "version_history_item": "在 {date} 安装 {version} 版本",
   "video": "视频",
   "video_hover_setting": "鼠标悬停时播放视频缩略图",
   "video_hover_setting_description": "当鼠标悬停在项目上时播放视频缩略图。即使禁用了这个功能,也可以通过将鼠标悬停在播放图标上来开始播放。",
@@ -1348,16 +1322,16 @@
   "view_all_users": "查看全部用户",
   "view_in_timeline": "在时间轴中查看",
   "view_links": "查看链接",
+  "view_name": "查看",
   "view_next_asset": "查看下一项",
   "view_previous_asset": "查看上一项",
   "view_stack": "查看堆叠项目",
-  "viewer": "预览",
   "visibility_changed": "{count, plural, one {#个人物} other {#个人物}}的可见性已修改",
-  "waiting": "队列中",
+  "waiting": "准备处理",
   "warning": "警告",
   "week": "周",
   "welcome": "欢迎",
-  "welcome_to_immich": "欢迎使用immich",
+  "welcome_to_immich": "欢迎使用 Immich",
   "year": "年",
   "years_ago": "{years, plural, one {#年} other {#年}}前",
   "yes": "是",
diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile
index e49fde1464..bca9244fa1 100644
--- a/machine-learning/Dockerfile
+++ b/machine-learning/Dockerfile
@@ -1,6 +1,6 @@
 ARG DEVICE=cpu
 
-FROM python:3.11-bookworm@sha256:3cd9b520be95c671135ea1318f32be6912876024ee16d0f472669d3878801651 AS builder-cpu
+FROM python:3.11-bookworm@sha256:2c80c66d876952e04fa74113864903198b7cfb36b839acb7a8fef82e94ed067c AS builder-cpu
 
 FROM builder-cpu AS builder-openvino
 
@@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv
 COPY poetry.lock pyproject.toml ./
 RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
 
-FROM python:3.11-slim-bookworm@sha256:50ec89bdac0a845ec1751f91cb6187a3d8adb2b919d6e82d17acf48d1a9743fc AS prod-cpu
+FROM python:3.11-slim-bookworm@sha256:370c586a6ffc8c619e6d652f81c094b34b14b8f2fb9251f092de23f16e299b78 AS prod-cpu
 
 FROM prod-cpu AS prod-openvino
 
@@ -104,7 +104,7 @@ RUN echo "hard core 0" >> /etc/security/limits.conf && \
 
 COPY --from=builder /opt/venv /opt/venv
 COPY ann/ann.py /usr/src/ann/ann.py
-COPY start.sh log_conf.json ./
+COPY start.sh log_conf.json gunicorn_conf.py ./
 COPY app .
 ENTRYPOINT ["tini", "--"]
 CMD ["./start.sh"]
diff --git a/machine-learning/app/config.py b/machine-learning/app/config.py
index af2d0aa4b9..92799ac692 100644
--- a/machine-learning/app/config.py
+++ b/machine-learning/app/config.py
@@ -6,7 +6,8 @@ from pathlib import Path
 from socket import socket
 
 from gunicorn.arbiter import Arbiter
-from pydantic import BaseModel, BaseSettings
+from pydantic import BaseModel
+from pydantic_settings import BaseSettings, SettingsConfigDict
 from rich.console import Console
 from rich.logging import RichHandler
 from uvicorn import Server
@@ -14,11 +15,22 @@ from uvicorn.workers import UvicornWorker
 
 
 class PreloadModelData(BaseModel):
-    clip: str | None
-    facial_recognition: str | None
+    clip: str | None = None
+    facial_recognition: str | None = None
+
+
+class MaxBatchSize(BaseModel):
+    facial_recognition: int | None = None
 
 
 class Settings(BaseSettings):
+    model_config = SettingsConfigDict(
+        env_prefix="MACHINE_LEARNING_",
+        case_sensitive=False,
+        env_nested_delimiter="__",
+        protected_namespaces=("settings_",),
+    )
+
     cache_folder: Path = Path("/cache")
     model_ttl: int = 300
     model_ttl_poll_s: int = 10
@@ -33,20 +45,19 @@ class Settings(BaseSettings):
     ann_fp16_turbo: bool = False
     ann_tuning_level: int = 2
     preload: PreloadModelData | None = None
+    max_batch_size: MaxBatchSize | None = None
 
-    class Config:
-        env_prefix = "MACHINE_LEARNING_"
-        case_sensitive = False
-        env_nested_delimiter = "__"
+    @property
+    def device_id(self) -> str:
+        return os.environ.get("MACHINE_LEARNING_DEVICE_ID", "0")
 
 
 class LogSettings(BaseSettings):
+    model_config = SettingsConfigDict(case_sensitive=False)
+
     immich_log_level: str = "info"
     no_color: bool = False
 
-    class Config:
-        case_sensitive = False
-
 
 _clean_name = str.maketrans(":\\/", "___", ".")
 
diff --git a/machine-learning/app/main.py b/machine-learning/app/main.py
index 000119937e..684001b875 100644
--- a/machine-learning/app/main.py
+++ b/machine-learning/app/main.py
@@ -12,7 +12,7 @@ from zipfile import BadZipFile
 
 import orjson
 from fastapi import Depends, FastAPI, File, Form, HTTPException
-from fastapi.responses import ORJSONResponse
+from fastapi.responses import ORJSONResponse, PlainTextResponse
 from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile
 from PIL.Image import Image
 from pydantic import ValidationError
@@ -28,14 +28,12 @@ from .schemas import (
     InferenceEntries,
     InferenceEntry,
     InferenceResponse,
-    MessageResponse,
     ModelFormat,
     ModelIdentity,
     ModelTask,
     ModelType,
     PipelineRequest,
     T,
-    TextResponse,
 )
 
 MultiPartParser.max_file_size = 2**26  # spools to disk if payload is 64 MiB or larger
@@ -127,14 +125,14 @@ def get_entries(entries: str = Form()) -> InferenceEntries:
 app = FastAPI(lifespan=lifespan)
 
 
-@app.get("/", response_model=MessageResponse)
-async def root() -> dict[str, str]:
-    return {"message": "Immich ML"}
+@app.get("/")
+async def root() -> ORJSONResponse:
+    return ORJSONResponse({"message": "Immich ML"})
 
 
-@app.get("/ping", response_model=TextResponse)
-def ping() -> str:
-    return "pong"
+@app.get("/ping")
+def ping() -> PlainTextResponse:
+    return PlainTextResponse("pong")
 
 
 @app.post("/predict", dependencies=[Depends(update_state)])
diff --git a/machine-learning/app/models/facial_recognition/recognition.py b/machine-learning/app/models/facial_recognition/recognition.py
index c060bdd616..dcfb6b530e 100644
--- a/machine-learning/app/models/facial_recognition/recognition.py
+++ b/machine-learning/app/models/facial_recognition/recognition.py
@@ -3,13 +3,14 @@ from typing import Any
 
 import numpy as np
 import onnx
+import onnxruntime as ort
 from insightface.model_zoo import ArcFaceONNX
 from insightface.utils.face_align import norm_crop
 from numpy.typing import NDArray
 from onnx.tools.update_model_dims import update_inputs_outputs_dims
 from PIL import Image
 
-from app.config import log
+from app.config import log, settings
 from app.models.base import InferenceModel
 from app.models.transforms import decode_cv2
 from app.schemas import FaceDetectionOutput, FacialRecognitionOutput, ModelFormat, ModelSession, ModelTask, ModelType
@@ -22,11 +23,12 @@ class FaceRecognizer(InferenceModel):
     def __init__(self, model_name: str, min_score: float = 0.7, **model_kwargs: Any) -> None:
         super().__init__(model_name, **model_kwargs)
         self.min_score = model_kwargs.pop("minScore", min_score)
-        self.batch = self.model_format == ModelFormat.ONNX
+        max_batch_size = settings.max_batch_size.facial_recognition if settings.max_batch_size else None
+        self.batch_size = max_batch_size if max_batch_size else self._batch_size_default
 
     def _load(self) -> ModelSession:
         session = self._make_session(self.model_path)
-        if self.batch and str(session.get_inputs()[0].shape[0]) != "batch":
+        if (not self.batch_size or self.batch_size > 1) and str(session.get_inputs()[0].shape[0]) != "batch":
             self._add_batch_axis(self.model_path)
             session = self._make_session(self.model_path)
         self.model = ArcFaceONNX(
@@ -42,18 +44,18 @@ class FaceRecognizer(InferenceModel):
             return []
         inputs = decode_cv2(inputs)
         cropped_faces = self._crop(inputs, faces)
-        embeddings = self._predict_batch(cropped_faces) if self.batch else self._predict_single(cropped_faces)
+        embeddings = self._predict_batch(cropped_faces)
         return self.postprocess(faces, embeddings)
 
     def _predict_batch(self, cropped_faces: list[NDArray[np.uint8]]) -> NDArray[np.float32]:
-        embeddings: NDArray[np.float32] = self.model.get_feat(cropped_faces)
-        return embeddings
+        if not self.batch_size or len(cropped_faces) <= self.batch_size:
+            embeddings: NDArray[np.float32] = self.model.get_feat(cropped_faces)
+            return embeddings
 
-    def _predict_single(self, cropped_faces: list[NDArray[np.uint8]]) -> NDArray[np.float32]:
-        embeddings: list[NDArray[np.float32]] = []
-        for face in cropped_faces:
-            embeddings.append(self.model.get_feat(face))
-        return np.concatenate(embeddings, axis=0)
+        batch_embeddings: list[NDArray[np.float32]] = []
+        for i in range(0, len(cropped_faces), self.batch_size):
+            batch_embeddings.append(self.model.get_feat(cropped_faces[i : i + self.batch_size]))
+        return np.concatenate(batch_embeddings, axis=0)
 
     def postprocess(self, faces: FaceDetectionOutput, embeddings: NDArray[np.float32]) -> FacialRecognitionOutput:
         return [
@@ -77,3 +79,8 @@ class FaceRecognizer(InferenceModel):
         output_dims = {proto.graph.output[0].name: ["batch"] + static_output_dims}
         updated_proto = update_inputs_outputs_dims(proto, input_dims, output_dims)
         onnx.save(updated_proto, model_path)
+
+    @property
+    def _batch_size_default(self) -> int | None:
+        providers = ort.get_available_providers()
+        return None if self.model_format == ModelFormat.ONNX and "OpenVINOExecutionProvider" not in providers else 1
diff --git a/machine-learning/app/schemas.py b/machine-learning/app/schemas.py
index f051db12c3..a7ce2ee60d 100644
--- a/machine-learning/app/schemas.py
+++ b/machine-learning/app/schemas.py
@@ -1,9 +1,9 @@
 from enum import Enum
-from typing import Any, Literal, Protocol, TypedDict, TypeGuard, TypeVar
+from typing import Any, Literal, Protocol, TypeGuard, TypeVar
 
 import numpy as np
 import numpy.typing as npt
-from pydantic import BaseModel
+from typing_extensions import TypedDict
 
 
 class StrEnum(str, Enum):
@@ -13,14 +13,6 @@ class StrEnum(str, Enum):
         return self.value
 
 
-class TextResponse(BaseModel):
-    __root__: str
-
-
-class MessageResponse(BaseModel):
-    message: str
-
-
 class BoundingBox(TypedDict):
     x1: int
     y1: int
diff --git a/machine-learning/app/sessions/ort.py b/machine-learning/app/sessions/ort.py
index 1a244b7c57..00c7ad50a9 100644
--- a/machine-learning/app/sessions/ort.py
+++ b/machine-learning/app/sessions/ort.py
@@ -86,11 +86,13 @@ class OrtSession:
         provider_options = []
         for provider in self.providers:
             match provider:
-                case "CPUExecutionProvider" | "CUDAExecutionProvider":
+                case "CPUExecutionProvider":
                     options = {"arena_extend_strategy": "kSameAsRequested"}
+                case "CUDAExecutionProvider":
+                    options = {"arena_extend_strategy": "kSameAsRequested", "device_id": settings.device_id}
                 case "OpenVINOExecutionProvider":
                     options = {
-                        "device_type": "GPU",
+                        "device_type": f"GPU.{settings.device_id}",
                         "precision": "FP32",
                         "cache_dir": (self.model_path.parent / "openvino").as_posix(),
                     }
diff --git a/machine-learning/app/test_main.py b/machine-learning/app/test_main.py
index 5f8e5b9e9c..e5cb63997c 100644
--- a/machine-learning/app/test_main.py
+++ b/machine-learning/app/test_main.py
@@ -210,10 +210,24 @@ class TestOrtSession:
         session = OrtSession(model_path, providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"])
 
         assert session.provider_options == [
-            {"device_type": "GPU", "precision": "FP32", "cache_dir": "/cache/ViT-B-32__openai/openvino"},
+            {"device_type": "GPU.0", "precision": "FP32", "cache_dir": "/cache/ViT-B-32__openai/openvino"},
             {"arena_extend_strategy": "kSameAsRequested"},
         ]
 
+    def test_sets_device_id_for_openvino(self) -> None:
+        os.environ["MACHINE_LEARNING_DEVICE_ID"] = "1"
+
+        session = OrtSession("ViT-B-32__openai", providers=["OpenVINOExecutionProvider"])
+
+        assert session.provider_options[0]["device_type"] == "GPU.1"
+
+    def test_sets_device_id_for_cuda(self) -> None:
+        os.environ["MACHINE_LEARNING_DEVICE_ID"] = "1"
+
+        session = OrtSession("ViT-B-32__openai", providers=["CUDAExecutionProvider"])
+
+        assert session.provider_options[0]["device_id"] == "1"
+
     def test_sets_provider_options_kwarg(self) -> None:
         session = OrtSession(
             "ViT-B-32__openai",
@@ -535,7 +549,7 @@ class TestFaceRecognition:
         face_recognizer = FaceRecognizer("buffalo_s", cache_dir=path)
         face_recognizer.load()
 
-        assert face_recognizer.batch is True
+        assert face_recognizer.batch_size is None
         update_dims.assert_called_once_with(proto, {"input.1": ["batch", 3, 224, 224]}, {"output.1": ["batch", 800]})
         onnx.save.assert_called_once_with(update_dims.return_value, face_recognizer.model_path)
 
@@ -558,7 +572,7 @@ class TestFaceRecognition:
         face_recognizer = FaceRecognizer("buffalo_s", cache_dir=path)
         face_recognizer.load()
 
-        assert face_recognizer.batch is True
+        assert face_recognizer.batch_size is None
         update_dims.assert_not_called()
         onnx.load.assert_not_called()
         onnx.save.assert_not_called()
@@ -582,7 +596,33 @@ class TestFaceRecognition:
         face_recognizer = FaceRecognizer("buffalo_s", model_format=ModelFormat.ARMNN, cache_dir=path)
         face_recognizer.load()
 
-        assert face_recognizer.batch is False
+        assert face_recognizer.batch_size == 1
+        update_dims.assert_not_called()
+        onnx.load.assert_not_called()
+        onnx.save.assert_not_called()
+
+    def test_recognition_does_not_add_batch_axis_for_openvino(
+        self, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture
+    ) -> None:
+        onnx = mocker.patch("app.models.facial_recognition.recognition.onnx", autospec=True)
+        update_dims = mocker.patch(
+            "app.models.facial_recognition.recognition.update_inputs_outputs_dims", autospec=True
+        )
+        mocker.patch("app.models.base.InferenceModel.download")
+        mocker.patch("app.models.facial_recognition.recognition.ArcFaceONNX")
+        path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = ".onnx"
+
+        inputs = [SimpleNamespace(name="input.1", shape=("batch", 3, 224, 224))]
+        outputs = [SimpleNamespace(name="output.1", shape=("batch", 800))]
+        ort_session.return_value.get_inputs.return_value = inputs
+        ort_session.return_value.get_outputs.return_value = outputs
+
+        face_recognizer = FaceRecognizer(
+            "buffalo_s", model_format=ModelFormat.ARMNN, cache_dir=path, providers=["OpenVINOExecutionProvider"]
+        )
+        face_recognizer.load()
+
+        assert face_recognizer.batch_size == 1
         update_dims.assert_not_called()
         onnx.load.assert_not_called()
         onnx.save.assert_not_called()
@@ -796,11 +836,26 @@ class TestLoad:
         mock_model.model_format = ModelFormat.ONNX
 
 
+def test_root_endpoint(deployed_app: TestClient) -> None:
+    response = deployed_app.get("http://localhost:3003")
+
+    body = response.json()
+    assert response.status_code == 200
+    assert body == {"message": "Immich ML"}
+
+
+def test_ping_endpoint(deployed_app: TestClient) -> None:
+    response = deployed_app.get("http://localhost:3003/ping")
+
+    assert response.status_code == 200
+    assert response.text == "pong"
+
+
 @pytest.mark.skipif(
     not settings.test_full,
     reason="More time-consuming since it deploys the app and loads models.",
 )
-class TestEndpoints:
+class TestPredictionEndpoints:
     def test_clip_image_endpoint(
         self, pil_image: Image.Image, responses: dict[str, Any], deployed_app: TestClient
     ) -> None:
diff --git a/machine-learning/export/Dockerfile b/machine-learning/export/Dockerfile
index 85be083c3c..195e64ab35 100644
--- a/machine-learning/export/Dockerfile
+++ b/machine-learning/export/Dockerfile
@@ -1,4 +1,4 @@
-FROM mambaorg/micromamba:bookworm-slim@sha256:b10f75974a30a6889b03519ac48d3e1510fd13d0689468c2c443033a15d84f1b AS builder
+FROM mambaorg/micromamba:bookworm-slim@sha256:e3797091302382ea841498bc93a7b0a50f7c1448333d5e946d2d1608d0c5f43d AS builder
 
 ENV TRANSFORMERS_CACHE=/cache \
   PYTHONDONTWRITEBYTECODE=1 \
diff --git a/machine-learning/gunicorn_conf.py b/machine-learning/gunicorn_conf.py
new file mode 100644
index 0000000000..efec3a95aa
--- /dev/null
+++ b/machine-learning/gunicorn_conf.py
@@ -0,0 +1,12 @@
+import os
+
+from gunicorn.arbiter import Arbiter
+from gunicorn.workers.base import Worker
+
+device_ids = os.environ.get("MACHINE_LEARNING_DEVICE_IDS", "0").replace(" ", "").split(",")
+env = os.environ
+
+
+# Round-robin device assignment for each worker
+def pre_fork(arbiter: Arbiter, _: Worker) -> None:
+    env["MACHINE_LEARNING_DEVICE_ID"] = device_ids[len(arbiter.WORKERS) % len(device_ids)]
diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock
index 20ffc6466a..eb8fe31dff 100644
--- a/machine-learning/poetry.lock
+++ b/machine-learning/poetry.lock
@@ -1,14 +1,14 @@
-# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
 
 [[package]]
 name = "aiocache"
-version = "0.12.2"
+version = "0.12.3"
 description = "multi backend asyncio cache"
 optional = false
 python-versions = "*"
 files = [
-    {file = "aiocache-0.12.2-py2.py3-none-any.whl", hash = "sha256:9b6fa30634ab0bfc3ecc44928a91ff07c6ea16d27d55469636b296ebc6eb5918"},
-    {file = "aiocache-0.12.2.tar.gz", hash = "sha256:b41c9a145b050a5dcbae1599f847db6dd445193b1f3bd172d8e0fe0cb9e96684"},
+    {file = "aiocache-0.12.3-py2.py3-none-any.whl", hash = "sha256:889086fc24710f431937b87ad3720a289f7fc31c4fd8b68e9f918b9bacd8270d"},
+    {file = "aiocache-0.12.3.tar.gz", hash = "sha256:f528b27bf4d436b497a1d0d1a8f59a542c153ab1e37c3621713cb376d44c4713"},
 ]
 
 [package.extras]
@@ -40,6 +40,17 @@ develop = ["imgaug (>=0.4.0)", "pytest"]
 imgaug = ["imgaug (>=0.4.0)"]
 tests = ["pytest"]
 
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+description = "Reusable constraint types to use with typing.Annotated"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
+    {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
+]
+
 [[package]]
 name = "anyio"
 version = "4.2.0"
@@ -64,33 +75,33 @@ trio = ["trio (>=0.23)"]
 
 [[package]]
 name = "black"
-version = "24.8.0"
+version = "24.10.0"
 description = "The uncompromising code formatter."
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"},
-    {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"},
-    {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"},
-    {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"},
-    {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"},
-    {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"},
-    {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"},
-    {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"},
-    {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"},
-    {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"},
-    {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"},
-    {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"},
-    {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"},
-    {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"},
-    {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"},
-    {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"},
-    {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"},
-    {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"},
-    {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"},
-    {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"},
-    {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"},
-    {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"},
+    {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"},
+    {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"},
+    {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"},
+    {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"},
+    {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"},
+    {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"},
+    {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"},
+    {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"},
+    {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"},
+    {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"},
+    {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"},
+    {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"},
+    {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"},
+    {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"},
+    {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"},
+    {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"},
+    {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"},
+    {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"},
+    {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"},
+    {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"},
+    {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"},
+    {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"},
 ]
 
 [package.dependencies]
@@ -104,7 +115,7 @@ typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
 
 [package.extras]
 colorama = ["colorama (>=0.4.3)"]
-d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
+d = ["aiohttp (>=3.10)"]
 jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
 uvloop = ["uvloop (>=0.15.2)"]
 
@@ -136,6 +147,10 @@ files = [
     {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"},
     {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"},
     {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"},
+    {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c"},
+    {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1"},
+    {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2"},
+    {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec"},
     {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"},
     {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"},
     {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"},
@@ -148,8 +163,14 @@ files = [
     {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"},
     {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"},
     {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"},
+    {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f"},
+    {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757"},
+    {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0"},
+    {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b"},
     {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"},
     {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"},
+    {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28"},
+    {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f"},
     {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"},
     {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"},
     {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"},
@@ -160,8 +181,24 @@ files = [
     {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"},
     {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"},
     {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"},
+    {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9"},
+    {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb"},
+    {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111"},
+    {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839"},
     {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"},
     {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"},
+    {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5"},
+    {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8"},
+    {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f"},
+    {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648"},
+    {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0"},
+    {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089"},
+    {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368"},
+    {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c"},
+    {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284"},
+    {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7"},
+    {file = "Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0"},
+    {file = "Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b"},
     {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"},
     {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"},
     {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"},
@@ -171,6 +208,10 @@ files = [
     {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"},
     {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"},
     {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"},
+    {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75"},
+    {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c"},
+    {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2"},
+    {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52"},
     {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"},
     {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"},
     {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"},
@@ -182,6 +223,10 @@ files = [
     {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"},
     {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"},
     {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"},
+    {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01"},
+    {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547"},
+    {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38"},
+    {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c"},
     {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"},
     {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"},
     {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"},
@@ -194,6 +239,10 @@ files = [
     {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"},
     {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"},
     {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"},
+    {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7"},
+    {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5"},
+    {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943"},
+    {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a"},
     {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"},
     {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"},
     {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"},
@@ -206,6 +255,10 @@ files = [
     {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"},
     {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"},
     {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"},
+    {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419"},
+    {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2"},
+    {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f"},
+    {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb"},
     {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"},
     {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"},
     {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"},
@@ -224,63 +277,78 @@ files = [
 
 [[package]]
 name = "cffi"
-version = "1.16.0"
+version = "1.17.1"
 description = "Foreign Function Interface for Python calling C code."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"},
-    {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"},
-    {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"},
-    {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"},
-    {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"},
-    {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"},
-    {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"},
-    {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"},
-    {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"},
-    {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"},
-    {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"},
-    {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"},
-    {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"},
-    {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"},
-    {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"},
-    {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"},
-    {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"},
-    {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"},
-    {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"},
-    {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"},
-    {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"},
-    {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"},
-    {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"},
-    {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"},
-    {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"},
-    {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"},
-    {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"},
-    {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"},
-    {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"},
-    {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"},
-    {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"},
-    {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"},
-    {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"},
-    {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"},
-    {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"},
-    {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"},
-    {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"},
-    {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"},
-    {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"},
-    {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"},
-    {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"},
-    {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"},
-    {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"},
-    {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"},
-    {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"},
-    {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"},
-    {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"},
-    {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"},
-    {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"},
-    {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"},
-    {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"},
-    {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"},
+    {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
+    {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
+    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"},
+    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"},
+    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"},
+    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"},
+    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"},
+    {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"},
+    {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"},
+    {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"},
+    {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"},
+    {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"},
+    {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
+    {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
+    {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
+    {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
+    {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
+    {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
+    {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
+    {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
+    {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
+    {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
+    {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
+    {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
+    {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
+    {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
+    {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
+    {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
+    {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
+    {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
+    {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
+    {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"},
+    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"},
+    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"},
+    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"},
+    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"},
+    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"},
+    {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"},
+    {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"},
+    {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"},
+    {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"},
+    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"},
+    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"},
+    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"},
+    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"},
+    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"},
+    {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"},
+    {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"},
+    {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"},
+    {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"},
+    {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
+    {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
 ]
 
 [package.dependencies]
@@ -507,63 +575,73 @@ test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"]
 
 [[package]]
 name = "coverage"
-version = "7.4.0"
+version = "7.6.4"
 description = "Code coverage measurement for Python"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "coverage-7.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a"},
-    {file = "coverage-7.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471"},
-    {file = "coverage-7.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9"},
-    {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516"},
-    {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5"},
-    {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566"},
-    {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae"},
-    {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43"},
-    {file = "coverage-7.4.0-cp310-cp310-win32.whl", hash = "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451"},
-    {file = "coverage-7.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137"},
-    {file = "coverage-7.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca"},
-    {file = "coverage-7.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06"},
-    {file = "coverage-7.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505"},
-    {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc"},
-    {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25"},
-    {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70"},
-    {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09"},
-    {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26"},
-    {file = "coverage-7.4.0-cp311-cp311-win32.whl", hash = "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614"},
-    {file = "coverage-7.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590"},
-    {file = "coverage-7.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143"},
-    {file = "coverage-7.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2"},
-    {file = "coverage-7.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a"},
-    {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446"},
-    {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9"},
-    {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd"},
-    {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a"},
-    {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa"},
-    {file = "coverage-7.4.0-cp312-cp312-win32.whl", hash = "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450"},
-    {file = "coverage-7.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0"},
-    {file = "coverage-7.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e"},
-    {file = "coverage-7.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85"},
-    {file = "coverage-7.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac"},
-    {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1"},
-    {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba"},
-    {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952"},
-    {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e"},
-    {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105"},
-    {file = "coverage-7.4.0-cp38-cp38-win32.whl", hash = "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2"},
-    {file = "coverage-7.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555"},
-    {file = "coverage-7.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42"},
-    {file = "coverage-7.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7"},
-    {file = "coverage-7.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9"},
-    {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed"},
-    {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c"},
-    {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870"},
-    {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058"},
-    {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f"},
-    {file = "coverage-7.4.0-cp39-cp39-win32.whl", hash = "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932"},
-    {file = "coverage-7.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e"},
-    {file = "coverage-7.4.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6"},
-    {file = "coverage-7.4.0.tar.gz", hash = "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e"},
+    {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"},
+    {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"},
+    {file = "coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72"},
+    {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51"},
+    {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491"},
+    {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b"},
+    {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea"},
+    {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a"},
+    {file = "coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa"},
+    {file = "coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172"},
+    {file = "coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b"},
+    {file = "coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25"},
+    {file = "coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546"},
+    {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b"},
+    {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e"},
+    {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718"},
+    {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db"},
+    {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522"},
+    {file = "coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf"},
+    {file = "coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19"},
+    {file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"},
+    {file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"},
+    {file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"},
+    {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"},
+    {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"},
+    {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"},
+    {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"},
+    {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"},
+    {file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"},
+    {file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"},
+    {file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"},
+    {file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"},
+    {file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"},
+    {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"},
+    {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"},
+    {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"},
+    {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"},
+    {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"},
+    {file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"},
+    {file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"},
+    {file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"},
+    {file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"},
+    {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"},
+    {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"},
+    {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"},
+    {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"},
+    {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"},
+    {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"},
+    {file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"},
+    {file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"},
+    {file = "coverage-7.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3"},
+    {file = "coverage-7.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c"},
+    {file = "coverage-7.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076"},
+    {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376"},
+    {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0"},
+    {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858"},
+    {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111"},
+    {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901"},
+    {file = "coverage-7.6.4-cp39-cp39-win32.whl", hash = "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09"},
+    {file = "coverage-7.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f"},
+    {file = "coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e"},
+    {file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"},
 ]
 
 [package.dependencies]
@@ -679,19 +757,19 @@ files = [
 test = ["pytest (>=6)"]
 
 [[package]]
-name = "fastapi-slim"
-version = "0.114.0"
+name = "fastapi"
+version = "0.115.6"
 description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "fastapi_slim-0.114.0-py3-none-any.whl", hash = "sha256:83c8e95301c75c6575f7f6c4b885bf42a4c0b4a85e936e2faca25055470d0afe"},
-    {file = "fastapi_slim-0.114.0.tar.gz", hash = "sha256:2299d5e0b8818f264725bd13dd91c80b904589be06c98c3d8115132576e5e2dd"},
+    {file = "fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305"},
+    {file = "fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654"},
 ]
 
 [package.dependencies]
 pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
-starlette = ">=0.37.2,<0.39.0"
+starlette = ">=0.40.0,<0.42.0"
 typing-extensions = ">=4.8.0"
 
 [package.extras]
@@ -878,73 +956,68 @@ tqdm = ["tqdm"]
 
 [[package]]
 name = "ftfy"
-version = "6.2.3"
+version = "6.3.1"
 description = "Fixes mojibake and other problems with Unicode, after the fact"
 optional = false
-python-versions = "<4,>=3.8.1"
+python-versions = ">=3.9"
 files = [
-    {file = "ftfy-6.2.3-py3-none-any.whl", hash = "sha256:f15761b023f3061a66207d33f0c0149ad40a8319fd16da91796363e2c049fdf8"},
-    {file = "ftfy-6.2.3.tar.gz", hash = "sha256:79b505988f29d577a58a9069afe75553a02a46e42de6091c0660cdc67812badc"},
+    {file = "ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083"},
+    {file = "ftfy-6.3.1.tar.gz", hash = "sha256:9b3c3d90f84fb267fe64d375a07b7f8912d817cf86009ae134aa03e1819506ec"},
 ]
 
 [package.dependencies]
-wcwidth = ">=0.2.12,<0.3.0"
+wcwidth = "*"
 
 [[package]]
 name = "gevent"
-version = "23.9.1"
+version = "24.10.3"
 description = "Coroutine-based network library"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "gevent-23.9.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:a3c5e9b1f766a7a64833334a18539a362fb563f6c4682f9634dea72cbe24f771"},
-    {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b101086f109168b23fa3586fccd1133494bdb97f86920a24dc0b23984dc30b69"},
-    {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36a549d632c14684bcbbd3014a6ce2666c5f2a500f34d58d32df6c9ea38b6535"},
-    {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:272cffdf535978d59c38ed837916dfd2b5d193be1e9e5dcc60a5f4d5025dd98a"},
-    {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcb8612787a7f4626aa881ff15ff25439561a429f5b303048f0fca8a1c781c39"},
-    {file = "gevent-23.9.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d57737860bfc332b9b5aa438963986afe90f49645f6e053140cfa0fa1bdae1ae"},
-    {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5f3c781c84794926d853d6fb58554dc0dcc800ba25c41d42f6959c344b4db5a6"},
-    {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:dbb22a9bbd6a13e925815ce70b940d1578dbe5d4013f20d23e8a11eddf8d14a7"},
-    {file = "gevent-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:707904027d7130ff3e59ea387dddceedb133cc742b00b3ffe696d567147a9c9e"},
-    {file = "gevent-23.9.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:45792c45d60f6ce3d19651d7fde0bc13e01b56bb4db60d3f32ab7d9ec467374c"},
-    {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e24c2af9638d6c989caffc691a039d7c7022a31c0363da367c0d32ceb4a0648"},
-    {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e1ead6863e596a8cc2a03e26a7a0981f84b6b3e956101135ff6d02df4d9a6b07"},
-    {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65883ac026731ac112184680d1f0f1e39fa6f4389fd1fc0bf46cc1388e2599f9"},
-    {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7af500da05363e66f122896012acb6e101a552682f2352b618e541c941a011"},
-    {file = "gevent-23.9.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c3e5d2fa532e4d3450595244de8ccf51f5721a05088813c1abd93ad274fe15e7"},
-    {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c84d34256c243b0a53d4335ef0bc76c735873986d478c53073861a92566a8d71"},
-    {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ada07076b380918829250201df1d016bdafb3acf352f35e5693b59dceee8dd2e"},
-    {file = "gevent-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:921dda1c0b84e3d3b1778efa362d61ed29e2b215b90f81d498eb4d8eafcd0b7a"},
-    {file = "gevent-23.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ed7a048d3e526a5c1d55c44cb3bc06cfdc1947d06d45006cc4cf60dedc628904"},
-    {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c1abc6f25f475adc33e5fc2dbcc26a732608ac5375d0d306228738a9ae14d3b"},
-    {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4368f341a5f51611411ec3fc62426f52ac3d6d42eaee9ed0f9eebe715c80184e"},
-    {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:52b4abf28e837f1865a9bdeef58ff6afd07d1d888b70b6804557e7908032e599"},
-    {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52e9f12cd1cda96603ce6b113d934f1aafb873e2c13182cf8e86d2c5c41982ea"},
-    {file = "gevent-23.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:de350fde10efa87ea60d742901e1053eb2127ebd8b59a7d3b90597eb4e586599"},
-    {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fde6402c5432b835fbb7698f1c7f2809c8d6b2bd9d047ac1f5a7c1d5aa569303"},
-    {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:dd6c32ab977ecf7c7b8c2611ed95fa4aaebd69b74bf08f4b4960ad516861517d"},
-    {file = "gevent-23.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:455e5ee8103f722b503fa45dedb04f3ffdec978c1524647f8ba72b4f08490af1"},
-    {file = "gevent-23.9.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:7ccf0fd378257cb77d91c116e15c99e533374a8153632c48a3ecae7f7f4f09fe"},
-    {file = "gevent-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d163d59f1be5a4c4efcdd13c2177baaf24aadf721fdf2e1af9ee54a998d160f5"},
-    {file = "gevent-23.9.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7532c17bc6c1cbac265e751b95000961715adef35a25d2b0b1813aa7263fb397"},
-    {file = "gevent-23.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:78eebaf5e73ff91d34df48f4e35581ab4c84e22dd5338ef32714264063c57507"},
-    {file = "gevent-23.9.1-cp38-cp38-win32.whl", hash = "sha256:f632487c87866094546a74eefbca2c74c1d03638b715b6feb12e80120960185a"},
-    {file = "gevent-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:62d121344f7465e3739989ad6b91f53a6ca9110518231553fe5846dbe1b4518f"},
-    {file = "gevent-23.9.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:bf456bd6b992eb0e1e869e2fd0caf817f0253e55ca7977fd0e72d0336a8c1c6a"},
-    {file = "gevent-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43daf68496c03a35287b8b617f9f91e0e7c0d042aebcc060cadc3f049aadd653"},
-    {file = "gevent-23.9.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7c28e38dcde327c217fdafb9d5d17d3e772f636f35df15ffae2d933a5587addd"},
-    {file = "gevent-23.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fae8d5b5b8fa2a8f63b39f5447168b02db10c888a3e387ed7af2bd1b8612e543"},
-    {file = "gevent-23.9.1-cp39-cp39-win32.whl", hash = "sha256:2c7b5c9912378e5f5ccf180d1fdb1e83f42b71823483066eddbe10ef1a2fcaa2"},
-    {file = "gevent-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:a2898b7048771917d85a1d548fd378e8a7b2ca963db8e17c6d90c76b495e0e2b"},
-    {file = "gevent-23.9.1.tar.gz", hash = "sha256:72c002235390d46f94938a96920d8856d4ffd9ddf62a303a0d7c118894097e34"},
+    {file = "gevent-24.10.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d7a1ad0f2da582f5bd238bca067e1c6c482c30c15a6e4d14aaa3215cbb2232f3"},
+    {file = "gevent-24.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4e526fdc279c655c1e809b0c34b45844182c2a6b219802da5e411bd2cf5a8ad"},
+    {file = "gevent-24.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57a5c4e0bdac482c5f02f240d0354e61362df73501ef6ebafce8ef635cad7527"},
+    {file = "gevent-24.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d67daed8383326dc8b5e58d88e148d29b6b52274a489e383530b0969ae7b9cb9"},
+    {file = "gevent-24.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e24ffea72e27987979c009536fd0868e52239b44afe6cf7135ce8aafd0f108e"},
+    {file = "gevent-24.10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c1d80090485da1ea3d99205fe97908b31188c1f4857f08b333ffaf2de2e89d18"},
+    {file = "gevent-24.10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f0c129f81d60cda614acb4b0c5731997ca05b031fb406fcb58ad53a7ade53b13"},
+    {file = "gevent-24.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:26ca7a6b42d35129617025ac801135118333cad75856ffc3217b38e707383eba"},
+    {file = "gevent-24.10.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:68c3a0d8402755eba7f69022e42e8021192a721ca8341908acc222ea597029b6"},
+    {file = "gevent-24.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d850a453d66336272be4f1d3a8126777f3efdaea62d053b4829857f91e09755"},
+    {file = "gevent-24.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8e58ee3723f1fbe07d66892f1caa7481c306f653a6829b6fd16cb23d618a5915"},
+    {file = "gevent-24.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b52382124eca13135a3abe4f65c6bd428656975980a48e51b17aeab68bdb14db"},
+    {file = "gevent-24.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ca2266e08f43c0e22c028801dff7d92a0b102ef20e4caeb6a46abfb95f6a328"},
+    {file = "gevent-24.10.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d758f0d4dbf32502ec87bb9b536ca8055090a16f8305f0ada3ce6f34e70f2fd7"},
+    {file = "gevent-24.10.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0de6eb3d55c03138fda567d9bfed28487ce5d0928c5107549767a93efdf2be26"},
+    {file = "gevent-24.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:385710355eadecdb70428a5ae3e7e5a45dcf888baa1426884588be9d25ac4290"},
+    {file = "gevent-24.10.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3ad8fb70aa0ebc935729c9699ac31b210a49b689a7b27b7ac9f91676475f3f53"},
+    {file = "gevent-24.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f18689f7a70d2ed0e75bad5036ec3c89690a493d4cfac8d7cdb258ac04b132bd"},
+    {file = "gevent-24.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f4f171d4d2018170454d84c934842e1b5f6ce7468ba298f6e7f7cff15000a3"},
+    {file = "gevent-24.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7021e26d70189b33c27173d4173f27bf4685d6b6f1c0ea50e5335f8491cb110c"},
+    {file = "gevent-24.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34aea15f9c79f27a8faeaa361bc1e72c773a9b54a1996a2ec4eefc8bcd59a824"},
+    {file = "gevent-24.10.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8af65a4d4feaec6042c666d22c322a310fba3b47e841ad52f724b9c3ce5da48e"},
+    {file = "gevent-24.10.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:89c4115e3f5ada55f92b61701a46043fe42f702b5af863b029e4c1a76f6cc2d4"},
+    {file = "gevent-24.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:1ce6dab94c0b0d24425ba55712de2f8c9cb21267150ca63f5bb3a0e1f165da99"},
+    {file = "gevent-24.10.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:f147e38423fbe96e8731f60a63475b3d2cab2f3d10578d8ee9d10c507c58a2ff"},
+    {file = "gevent-24.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e6984ec96fc95fd67488555c38ece3015be1f38b1bcceb27b7d6c36b343008"},
+    {file = "gevent-24.10.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:051b22e2758accfddb0457728bfc9abf8c3f2ce6bca43f1ff6e07b5ed9e49bf4"},
+    {file = "gevent-24.10.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb5edb6433764119a664bbb148d2aea9990950aa89cc3498f475c2408d523ea3"},
+    {file = "gevent-24.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce417bcaaab496bc9c77f75566531e9d93816262037b8b2dbb88b0fdcd66587c"},
+    {file = "gevent-24.10.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:1c3a828b033fb02b7c31da4d75014a1f82e6c072fc0523456569a57f8b025861"},
+    {file = "gevent-24.10.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f2ae3efbbd120cdf4a68b7abc27a37e61e6f443c5a06ec2c6ad94c37cd8471ec"},
+    {file = "gevent-24.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:9e1210334a9bc9f76c3d008e0785ca62214f8a54e1325f6c2ecab3b6a572a015"},
+    {file = "gevent-24.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70e9ed7ecb70e0df7dc97c3bc420de9a45a7c76bd5861c6cfec8c549700e681e"},
+    {file = "gevent-24.10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3ac83b74304487afa211a01909c7dd257e574db0cd429d866c298e21df7aeedf"},
+    {file = "gevent-24.10.3-cp39-cp39-win32.whl", hash = "sha256:a9a89d6e396ef6f1e3968521bf56e8c4bee25b193bbf5d428b7782d582410822"},
+    {file = "gevent-24.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:40ea3e40e8bb4fdb143c2a8edf2ccfdebd56016c7317c341ce8094c7bee08818"},
+    {file = "gevent-24.10.3-pp310-pypy310_pp73-macosx_11_0_universal2.whl", hash = "sha256:e534e6a968d74463b11de6c9c67f4b4bf61775fb00f2e6e0f7fcdd412ceade18"},
+    {file = "gevent-24.10.3.tar.gz", hash = "sha256:aa7ee1bd5cabb2b7ef35105f863b386c8d5e332f754b60cfc354148bd70d35d1"},
 ]
 
 [package.dependencies]
-cffi = {version = ">=1.12.2", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
-greenlet = [
-    {version = ">=3.0rc3", markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\""},
-    {version = ">=2.0.0", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""},
-]
+cffi = {version = ">=1.17.1", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
+greenlet = {version = ">=3.1.1", markers = "platform_python_implementation == \"CPython\""}
 "zope.event" = "*"
 "zope.interface" = "*"
 
@@ -952,8 +1025,8 @@ greenlet = [
 dnspython = ["dnspython (>=1.16.0,<2.0)", "idna"]
 docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"]
 monitor = ["psutil (>=5.7.0)"]
-recommended = ["cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "psutil (>=5.7.0)"]
-test = ["cffi (>=1.12.2)", "coverage (>=5.0)", "dnspython (>=1.16.0,<2.0)", "idna", "objgraph", "psutil (>=5.7.0)", "requests", "setuptools"]
+recommended = ["cffi (>=1.17.1)", "dnspython (>=1.16.0,<2.0)", "idna", "psutil (>=5.7.0)"]
+test = ["cffi (>=1.17.1)", "coverage (>=5.0)", "dnspython (>=1.16.0,<2.0)", "idna", "objgraph", "psutil (>=5.7.0)", "requests"]
 
 [[package]]
 name = "geventhttpclient"
@@ -1040,69 +1113,84 @@ examples = ["oauth2"]
 
 [[package]]
 name = "greenlet"
-version = "3.0.3"
+version = "3.1.1"
 description = "Lightweight in-process concurrent programming"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"},
-    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"},
-    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"},
-    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"},
-    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"},
-    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"},
-    {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"},
-    {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"},
-    {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"},
-    {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"},
-    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"},
-    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"},
-    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"},
-    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"},
-    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"},
-    {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"},
-    {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"},
-    {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"},
-    {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"},
-    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"},
-    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"},
-    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"},
-    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"},
-    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"},
-    {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"},
-    {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"},
-    {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"},
-    {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"},
-    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"},
-    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"},
-    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"},
-    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"},
-    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"},
-    {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"},
-    {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"},
-    {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"},
-    {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"},
-    {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"},
-    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"},
-    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"},
-    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"},
-    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"},
-    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"},
-    {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"},
-    {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"},
-    {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"},
-    {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"},
-    {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"},
-    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"},
-    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"},
-    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"},
-    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"},
-    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"},
-    {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"},
-    {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"},
-    {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"},
-    {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"},
-    {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"},
+    {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"},
+    {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"},
+    {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"},
+    {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"},
+    {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"},
+    {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"},
+    {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"},
+    {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"},
+    {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"},
+    {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"},
+    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"},
+    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"},
+    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"},
+    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"},
+    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"},
+    {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"},
+    {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"},
+    {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"},
+    {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"},
+    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"},
+    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"},
+    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"},
+    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"},
+    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"},
+    {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"},
+    {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"},
+    {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"},
+    {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"},
+    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"},
+    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"},
+    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"},
+    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"},
+    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"},
+    {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"},
+    {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"},
+    {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"},
+    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"},
+    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"},
+    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"},
+    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"},
+    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"},
+    {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"},
+    {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"},
+    {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"},
+    {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"},
+    {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"},
+    {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"},
+    {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"},
+    {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"},
+    {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"},
+    {file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"},
+    {file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"},
+    {file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"},
+    {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"},
+    {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"},
+    {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"},
+    {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"},
+    {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"},
+    {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"},
+    {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"},
+    {file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"},
+    {file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"},
+    {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"},
+    {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"},
+    {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"},
+    {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"},
+    {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"},
+    {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"},
+    {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"},
+    {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"},
+    {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"},
+    {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"},
+    {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"},
 ]
 
 [package.extras]
@@ -1164,61 +1252,68 @@ trio = ["trio (>=0.22.0,<0.23.0)"]
 
 [[package]]
 name = "httptools"
-version = "0.6.1"
+version = "0.6.4"
 description = "A collection of framework independent HTTP protocol utils."
 optional = false
 python-versions = ">=3.8.0"
 files = [
-    {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"},
-    {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"},
-    {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"},
-    {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"},
-    {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"},
-    {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"},
-    {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"},
-    {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"},
-    {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"},
-    {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"},
-    {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"},
-    {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"},
-    {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"},
-    {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"},
-    {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"},
-    {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"},
-    {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"},
-    {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"},
-    {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"},
-    {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"},
-    {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"},
-    {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"},
-    {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"},
-    {file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"},
-    {file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"},
-    {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"},
-    {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"},
-    {file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"},
-    {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"},
-    {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"},
-    {file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"},
-    {file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"},
-    {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"},
-    {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"},
-    {file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"},
-    {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"},
+    {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"},
+    {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"},
+    {file = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"},
+    {file = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"},
+    {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"},
+    {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"},
+    {file = "httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"},
+    {file = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"},
+    {file = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"},
+    {file = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"},
+    {file = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"},
+    {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"},
+    {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"},
+    {file = "httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"},
+    {file = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"},
+    {file = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"},
+    {file = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"},
+    {file = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"},
+    {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"},
+    {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"},
+    {file = "httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"},
+    {file = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"},
+    {file = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"},
+    {file = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"},
+    {file = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"},
+    {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"},
+    {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"},
+    {file = "httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"},
+    {file = "httptools-0.6.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba"},
+    {file = "httptools-0.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc"},
+    {file = "httptools-0.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff"},
+    {file = "httptools-0.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490"},
+    {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43"},
+    {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440"},
+    {file = "httptools-0.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f"},
+    {file = "httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003"},
+    {file = "httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab"},
+    {file = "httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547"},
+    {file = "httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9"},
+    {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076"},
+    {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd"},
+    {file = "httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6"},
+    {file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"},
 ]
 
 [package.extras]
-test = ["Cython (>=0.29.24,<0.30.0)"]
+test = ["Cython (>=0.29.24)"]
 
 [[package]]
 name = "httpx"
-version = "0.27.2"
+version = "0.28.1"
 description = "The next generation HTTP client."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"},
-    {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
+    {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
+    {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
 ]
 
 [package.dependencies]
@@ -1226,7 +1321,6 @@ anyio = "*"
 certifi = "*"
 httpcore = "==1.*"
 idna = "*"
-sniffio = "*"
 
 [package.extras]
 brotli = ["brotli", "brotlicffi"]
@@ -1237,13 +1331,13 @@ zstd = ["zstandard (>=0.18.0)"]
 
 [[package]]
 name = "huggingface-hub"
-version = "0.24.6"
+version = "0.27.0"
 description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
 optional = false
 python-versions = ">=3.8.0"
 files = [
-    {file = "huggingface_hub-0.24.6-py3-none-any.whl", hash = "sha256:a990f3232aa985fe749bc9474060cbad75e8b2f115f6665a9fda5b9c97818970"},
-    {file = "huggingface_hub-0.24.6.tar.gz", hash = "sha256:cc2579e761d070713eaa9c323e3debe39d5b464ae3a7261c39a9195b27bb8000"},
+    {file = "huggingface_hub-0.27.0-py3-none-any.whl", hash = "sha256:8f2e834517f1f1ddf1ecc716f91b120d7333011b7485f665a9a412eacb1a2a81"},
+    {file = "huggingface_hub-0.27.0.tar.gz", hash = "sha256:902cce1a1be5739f5589e560198a65a8edcfd3b830b1666f36e4b961f0454fac"},
 ]
 
 [package.dependencies]
@@ -1256,16 +1350,16 @@ tqdm = ">=4.42.1"
 typing-extensions = ">=3.7.4.3"
 
 [package.extras]
-all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.5.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
+all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "libcst (==1.4.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.5.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
 cli = ["InquirerPy (==0.3.4)"]
-dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.5.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
+dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "libcst (==1.4.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.5.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
 fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"]
 hf-transfer = ["hf-transfer (>=0.1.4)"]
-inference = ["aiohttp", "minijinja (>=1.0)"]
-quality = ["mypy (==1.5.1)", "ruff (>=0.5.0)"]
+inference = ["aiohttp"]
+quality = ["libcst (==1.4.0)", "mypy (==1.5.1)", "ruff (>=0.5.0)"]
 tensorflow = ["graphviz", "pydot", "tensorflow"]
 tensorflow-testing = ["keras (<3.0)", "tensorflow"]
-testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"]
+testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"]
 torch = ["safetensors[torch]", "torch"]
 typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"]
 
@@ -1531,13 +1625,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
 
 [[package]]
 name = "locust"
-version = "2.31.5"
+version = "2.32.4"
 description = "Developer-friendly load testing framework"
 optional = false
 python-versions = ">=3.9"
 files = [
-    {file = "locust-2.31.5-py3-none-any.whl", hash = "sha256:2904ff6307d54d3202c9ebd776f9170214f6dfbe4059504dad9e3ffaca03f600"},
-    {file = "locust-2.31.5.tar.gz", hash = "sha256:14b2fa6f95bf248668e6dc92d100a44f06c5dcb1c26f88a5442bcaaee18faceb"},
+    {file = "locust-2.32.4-py3-none-any.whl", hash = "sha256:7c5b8767c0d771b5167d5d6b82878622faead74f394eb9cafe8891d89eb36b97"},
+    {file = "locust-2.32.4.tar.gz", hash = "sha256:fd650cbc40842e721668a8d0f7f8224775432b40c63d0a378546b9a9f54b7559"},
 ]
 
 [package.dependencies]
@@ -1545,7 +1639,10 @@ ConfigArgParse = ">=1.5.5"
 flask = ">=2.0.0"
 Flask-Cors = ">=3.0.10"
 Flask-Login = ">=0.6.3"
-gevent = ">=22.10.2"
+gevent = [
+    {version = ">=22.10.2", markers = "python_full_version <= \"3.12.0\""},
+    {version = ">=24.10.1", markers = "python_full_version > \"3.13.0\""},
+]
 geventhttpclient = ">=2.3.1"
 msgpack = ">=1.0.0"
 psutil = ">=5.9.1"
@@ -1555,6 +1652,7 @@ requests = [
     {version = ">=2.26.0", markers = "python_full_version <= \"3.11.0\""},
     {version = ">=2.32.2", markers = "python_full_version > \"3.11.0\""},
 ]
+setuptools = ">=70.0.0"
 tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
 typing_extensions = {version = ">=4.6.0", markers = "python_version < \"3.11\""}
 Werkzeug = ">=2.0.0"
@@ -1795,38 +1893,43 @@ files = [
 
 [[package]]
 name = "mypy"
-version = "1.11.2"
+version = "1.13.0"
 description = "Optional static typing for Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"},
-    {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"},
-    {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"},
-    {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"},
-    {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"},
-    {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"},
-    {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"},
-    {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"},
-    {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"},
-    {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"},
-    {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"},
-    {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"},
-    {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"},
-    {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"},
-    {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"},
-    {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"},
-    {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"},
-    {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"},
-    {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"},
-    {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"},
-    {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"},
-    {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"},
-    {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"},
-    {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"},
-    {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"},
-    {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"},
-    {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"},
+    {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"},
+    {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"},
+    {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"},
+    {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"},
+    {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"},
+    {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"},
+    {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"},
+    {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"},
+    {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"},
+    {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"},
+    {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"},
+    {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"},
+    {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"},
+    {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"},
+    {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"},
+    {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"},
+    {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"},
+    {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"},
+    {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"},
+    {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"},
+    {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"},
+    {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"},
+    {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"},
+    {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"},
+    {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"},
+    {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"},
+    {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"},
+    {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"},
+    {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"},
+    {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"},
+    {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"},
+    {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"},
 ]
 
 [package.dependencies]
@@ -1836,6 +1939,7 @@ typing-extensions = ">=4.6.0"
 
 [package.extras]
 dmypy = ["psutil (>=4.0)"]
+faster-cache = ["orjson"]
 install-types = ["pip"]
 mypyc = ["setuptools (>=50)"]
 reports = ["lxml"]
@@ -1963,36 +2067,32 @@ reference = ["Pillow", "google-re2"]
 
 [[package]]
 name = "onnxruntime"
-version = "1.19.2"
+version = "1.20.1"
 description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
 optional = false
 python-versions = "*"
 files = [
-    {file = "onnxruntime-1.19.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:84fa57369c06cadd3c2a538ae2a26d76d583e7c34bdecd5769d71ca5c0fc750e"},
-    {file = "onnxruntime-1.19.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdc471a66df0c1cdef774accef69e9f2ca168c851ab5e4f2f3341512c7ef4666"},
-    {file = "onnxruntime-1.19.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e3a4ce906105d99ebbe817f536d50a91ed8a4d1592553f49b3c23c4be2560ae6"},
-    {file = "onnxruntime-1.19.2-cp310-cp310-win32.whl", hash = "sha256:4b3d723cc154c8ddeb9f6d0a8c0d6243774c6b5930847cc83170bfe4678fafb3"},
-    {file = "onnxruntime-1.19.2-cp310-cp310-win_amd64.whl", hash = "sha256:17ed7382d2c58d4b7354fb2b301ff30b9bf308a1c7eac9546449cd122d21cae5"},
-    {file = "onnxruntime-1.19.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:d863e8acdc7232d705d49e41087e10b274c42f09e259016a46f32c34e06dc4fd"},
-    {file = "onnxruntime-1.19.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dfe4f660a71b31caa81fc298a25f9612815215a47b286236e61d540350d7b6"},
-    {file = "onnxruntime-1.19.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a36511dc07c5c964b916697e42e366fa43c48cdb3d3503578d78cef30417cb84"},
-    {file = "onnxruntime-1.19.2-cp311-cp311-win32.whl", hash = "sha256:50cbb8dc69d6befad4746a69760e5b00cc3ff0a59c6c3fb27f8afa20e2cab7e7"},
-    {file = "onnxruntime-1.19.2-cp311-cp311-win_amd64.whl", hash = "sha256:1c3e5d415b78337fa0b1b75291e9ea9fb2a4c1f148eb5811e7212fed02cfffa8"},
-    {file = "onnxruntime-1.19.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:68e7051bef9cfefcbb858d2d2646536829894d72a4130c24019219442b1dd2ed"},
-    {file = "onnxruntime-1.19.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2d366fbcc205ce68a8a3bde2185fd15c604d9645888703785b61ef174265168"},
-    {file = "onnxruntime-1.19.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:477b93df4db467e9cbf34051662a4b27c18e131fa1836e05974eae0d6e4cf29b"},
-    {file = "onnxruntime-1.19.2-cp312-cp312-win32.whl", hash = "sha256:9a174073dc5608fad05f7cf7f320b52e8035e73d80b0a23c80f840e5a97c0147"},
-    {file = "onnxruntime-1.19.2-cp312-cp312-win_amd64.whl", hash = "sha256:190103273ea4507638ffc31d66a980594b237874b65379e273125150eb044857"},
-    {file = "onnxruntime-1.19.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:636bc1d4cc051d40bc52e1f9da87fbb9c57d9d47164695dfb1c41646ea51ea66"},
-    {file = "onnxruntime-1.19.2-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5bd8b875757ea941cbcfe01582970cc299893d1b65bd56731e326a8333f638a3"},
-    {file = "onnxruntime-1.19.2-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b2046fc9560f97947bbc1acbe4c6d48585ef0f12742744307d3364b131ac5778"},
-    {file = "onnxruntime-1.19.2-cp38-cp38-win32.whl", hash = "sha256:31c12840b1cde4ac1f7d27d540c44e13e34f2345cf3642762d2a3333621abb6a"},
-    {file = "onnxruntime-1.19.2-cp38-cp38-win_amd64.whl", hash = "sha256:016229660adea180e9a32ce218b95f8f84860a200f0f13b50070d7d90e92956c"},
-    {file = "onnxruntime-1.19.2-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:006c8d326835c017a9e9f74c9c77ebb570a71174a1e89fe078b29a557d9c3848"},
-    {file = "onnxruntime-1.19.2-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df2a94179a42d530b936f154615b54748239c2908ee44f0d722cb4df10670f68"},
-    {file = "onnxruntime-1.19.2-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fae4b4de45894b9ce7ae418c5484cbf0341db6813effec01bb2216091c52f7fb"},
-    {file = "onnxruntime-1.19.2-cp39-cp39-win32.whl", hash = "sha256:dc5430f473e8706fff837ae01323be9dcfddd3ea471c900a91fa7c9b807ec5d3"},
-    {file = "onnxruntime-1.19.2-cp39-cp39-win_amd64.whl", hash = "sha256:38475e29a95c5f6c62c2c603d69fc7d4c6ccbf4df602bd567b86ae1138881c49"},
+    {file = "onnxruntime-1.20.1-cp310-cp310-macosx_13_0_universal2.whl", hash = "sha256:e50ba5ff7fed4f7d9253a6baf801ca2883cc08491f9d32d78a80da57256a5439"},
+    {file = "onnxruntime-1.20.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b2908b50101a19e99c4d4e97ebb9905561daf61829403061c1adc1b588bc0de"},
+    {file = "onnxruntime-1.20.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d82daaec24045a2e87598b8ac2b417b1cce623244e80e663882e9fe1aae86410"},
+    {file = "onnxruntime-1.20.1-cp310-cp310-win32.whl", hash = "sha256:4c4b251a725a3b8cf2aab284f7d940c26094ecd9d442f07dd81ab5470e99b83f"},
+    {file = "onnxruntime-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:d3b616bb53a77a9463707bb313637223380fc327f5064c9a782e8ec69c22e6a2"},
+    {file = "onnxruntime-1.20.1-cp311-cp311-macosx_13_0_universal2.whl", hash = "sha256:06bfbf02ca9ab5f28946e0f912a562a5f005301d0c419283dc57b3ed7969bb7b"},
+    {file = "onnxruntime-1.20.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6243e34d74423bdd1edf0ae9596dd61023b260f546ee17d701723915f06a9f7"},
+    {file = "onnxruntime-1.20.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5eec64c0269dcdb8d9a9a53dc4d64f87b9e0c19801d9321246a53b7eb5a7d1bc"},
+    {file = "onnxruntime-1.20.1-cp311-cp311-win32.whl", hash = "sha256:a19bc6e8c70e2485a1725b3d517a2319603acc14c1f1a017dda0afe6d4665b41"},
+    {file = "onnxruntime-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:8508887eb1c5f9537a4071768723ec7c30c28eb2518a00d0adcd32c89dea3221"},
+    {file = "onnxruntime-1.20.1-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:22b0655e2bf4f2161d52706e31f517a0e54939dc393e92577df51808a7edc8c9"},
+    {file = "onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f56e898815963d6dc4ee1c35fc6c36506466eff6d16f3cb9848cea4e8c8172"},
+    {file = "onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb71a814f66517a65628c9e4a2bb530a6edd2cd5d87ffa0af0f6f773a027d99e"},
+    {file = "onnxruntime-1.20.1-cp312-cp312-win32.whl", hash = "sha256:bd386cc9ee5f686ee8a75ba74037750aca55183085bf1941da8efcfe12d5b120"},
+    {file = "onnxruntime-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:19c2d843eb074f385e8bbb753a40df780511061a63f9def1b216bf53860223fb"},
+    {file = "onnxruntime-1.20.1-cp313-cp313-macosx_13_0_universal2.whl", hash = "sha256:cc01437a32d0042b606f462245c8bbae269e5442797f6213e36ce61d5abdd8cc"},
+    {file = "onnxruntime-1.20.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb44b08e017a648924dbe91b82d89b0c105b1adcfe31e90d1dc06b8677ad37be"},
+    {file = "onnxruntime-1.20.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda6aebdf7917c1d811f21d41633df00c58aff2bef2f598f69289c1f1dabc4b3"},
+    {file = "onnxruntime-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:d30367df7e70f1d9fc5a6a68106f5961686d39b54d3221f760085524e8d38e16"},
+    {file = "onnxruntime-1.20.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9158465745423b2b5d97ed25aa7740c7d38d2993ee2e5c3bfacb0c4145c49d8"},
+    {file = "onnxruntime-1.20.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0df6f2df83d61f46e842dbcde610ede27218947c33e994545a22333491e72a3b"},
 ]
 
 [package.dependencies]
@@ -2005,27 +2105,27 @@ sympy = "*"
 
 [[package]]
 name = "onnxruntime-gpu"
-version = "1.18.1"
+version = "1.19.2"
 description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
 optional = false
 python-versions = "*"
 files = [
-    {file = "onnxruntime_gpu-1.18.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e9a52f5d43a84fe29e135da6bf10daa18836c81bed9060a5924efd6afc0d259"},
-    {file = "onnxruntime_gpu-1.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:e7c1c665e8a11a5cf15369948b04288dc0a6812ad2e6beaff93a3d157c864d9a"},
-    {file = "onnxruntime_gpu-1.18.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1334f802cb1e4e2eb6ceebc4ef71ba44f3ef444d34216baafb940368a7a5d2f5"},
-    {file = "onnxruntime_gpu-1.18.1-cp311-cp311-win_amd64.whl", hash = "sha256:0ffcc711e89b80c935d5172544f8a605b11525fc1e6f0e78ee79e2c28956e2d9"},
-    {file = "onnxruntime_gpu-1.18.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbb1a6c986b2392eebaebc43e198a1614e3f7d2c191725002dbfa0dceb24454b"},
-    {file = "onnxruntime_gpu-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:bee352929e6eec2ff4e11e323a025ed8bd5eac24795005bc502ac740971fa7bd"},
-    {file = "onnxruntime_gpu-1.18.1-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:76d307a849a863d0457869febe4b2fd2fc07c7f26385c7339d17066312fa6be0"},
-    {file = "onnxruntime_gpu-1.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:b7498d6c64a03558308ce6d7d14dab306ea90d1204b563890c4d2d26c1b520f0"},
-    {file = "onnxruntime_gpu-1.18.1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a1d8113cb4b8a51b195fae91cfeb6849728462a4b46aaf51b6764c44e54f81f"},
-    {file = "onnxruntime_gpu-1.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:fc1d2544a39f5db64c5b8a0c24d0b934d7d64682e6d70763eb2cc726b1fd6c3f"},
+    {file = "onnxruntime_gpu-1.19.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a49740e079e7c5215830d30cde3df792e903df007aa0b0fd7aa797937061b27a"},
+    {file = "onnxruntime_gpu-1.19.2-cp310-cp310-win_amd64.whl", hash = "sha256:b895920bb5e4241299f68874e0becdc2635ea0142939c11e7ff5ae5b28993613"},
+    {file = "onnxruntime_gpu-1.19.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:562fc7c755393eaad9751e56149339dd201ffbfdb3ef5f43ff21d0619ba9045f"},
+    {file = "onnxruntime_gpu-1.19.2-cp311-cp311-win_amd64.whl", hash = "sha256:522f7495918176cb8c1a3c78bde7152d984f7096acc786c73a27643af8af87c9"},
+    {file = "onnxruntime_gpu-1.19.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:554a02a3fac0119707eb87327908afd21c4e6f0fa5bf9a034398f098adc316c5"},
+    {file = "onnxruntime_gpu-1.19.2-cp312-cp312-win_amd64.whl", hash = "sha256:e7c6165a405027e3c0f11d189ae7013b5d66919b3381f9bfb3405c0c0cf07968"},
+    {file = "onnxruntime_gpu-1.19.2-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4a8562e1e6f1912870c60bfaf8233c82b86e5b93ae39f211b650ac0f2015430"},
+    {file = "onnxruntime_gpu-1.19.2-cp38-cp38-win_amd64.whl", hash = "sha256:55505c99e18688a7c68fdc811ed6e7a315aa36f543b33920c77d03a627d2c3f5"},
+    {file = "onnxruntime_gpu-1.19.2-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9e369f01f55ea726ae5d28f18236426e52e97c433f0b7682054e61c478a06c9"},
+    {file = "onnxruntime_gpu-1.19.2-cp39-cp39-win_amd64.whl", hash = "sha256:c8b8128174b0470537e9f4983aeecc002a435d13914970c2af2f41d244ef2781"},
 ]
 
 [package.dependencies]
 coloredlogs = "*"
 flatbuffers = "*"
-numpy = ">=1.21.6,<2.0"
+numpy = ">=1.21.6"
 packaging = "*"
 protobuf = "*"
 sympy = "*"
@@ -2083,68 +2183,86 @@ numpy = [
 
 [[package]]
 name = "orjson"
-version = "3.10.7"
+version = "3.10.12"
 description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "orjson-3.10.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:74f4544f5a6405b90da8ea724d15ac9c36da4d72a738c64685003337401f5c12"},
-    {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34a566f22c28222b08875b18b0dfbf8a947e69df21a9ed5c51a6bf91cfb944ac"},
-    {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf6ba8ebc8ef5792e2337fb0419f8009729335bb400ece005606336b7fd7bab7"},
-    {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac7cf6222b29fbda9e3a472b41e6a5538b48f2c8f99261eecd60aafbdb60690c"},
-    {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de817e2f5fc75a9e7dd350c4b0f54617b280e26d1631811a43e7e968fa71e3e9"},
-    {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:348bdd16b32556cf8d7257b17cf2bdb7ab7976af4af41ebe79f9796c218f7e91"},
-    {file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:479fd0844ddc3ca77e0fd99644c7fe2de8e8be1efcd57705b5c92e5186e8a250"},
-    {file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fdf5197a21dd660cf19dfd2a3ce79574588f8f5e2dbf21bda9ee2d2b46924d84"},
-    {file = "orjson-3.10.7-cp310-none-win32.whl", hash = "sha256:d374d36726746c81a49f3ff8daa2898dccab6596864ebe43d50733275c629175"},
-    {file = "orjson-3.10.7-cp310-none-win_amd64.whl", hash = "sha256:cb61938aec8b0ffb6eef484d480188a1777e67b05d58e41b435c74b9d84e0b9c"},
-    {file = "orjson-3.10.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7db8539039698ddfb9a524b4dd19508256107568cdad24f3682d5773e60504a2"},
-    {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:480f455222cb7a1dea35c57a67578848537d2602b46c464472c995297117fa09"},
-    {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a9c9b168b3a19e37fe2778c0003359f07822c90fdff8f98d9d2a91b3144d8e0"},
-    {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8de062de550f63185e4c1c54151bdddfc5625e37daf0aa1e75d2a1293e3b7d9a"},
-    {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b0dd04483499d1de9c8f6203f8975caf17a6000b9c0c54630cef02e44ee624e"},
-    {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b58d3795dafa334fc8fd46f7c5dc013e6ad06fd5b9a4cc98cb1456e7d3558bd6"},
-    {file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:33cfb96c24034a878d83d1a9415799a73dc77480e6c40417e5dda0710d559ee6"},
-    {file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e724cebe1fadc2b23c6f7415bad5ee6239e00a69f30ee423f319c6af70e2a5c0"},
-    {file = "orjson-3.10.7-cp311-none-win32.whl", hash = "sha256:82763b46053727a7168d29c772ed5c870fdae2f61aa8a25994c7984a19b1021f"},
-    {file = "orjson-3.10.7-cp311-none-win_amd64.whl", hash = "sha256:eb8d384a24778abf29afb8e41d68fdd9a156cf6e5390c04cc07bbc24b89e98b5"},
-    {file = "orjson-3.10.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44a96f2d4c3af51bfac6bc4ef7b182aa33f2f054fd7f34cc0ee9a320d051d41f"},
-    {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76ac14cd57df0572453543f8f2575e2d01ae9e790c21f57627803f5e79b0d3c3"},
-    {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bdbb61dcc365dd9be94e8f7df91975edc9364d6a78c8f7adb69c1cdff318ec93"},
-    {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b48b3db6bb6e0a08fa8c83b47bc169623f801e5cc4f24442ab2b6617da3b5313"},
-    {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23820a1563a1d386414fef15c249040042b8e5d07b40ab3fe3efbfbbcbcb8864"},
-    {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0c6a008e91d10a2564edbb6ee5069a9e66df3fbe11c9a005cb411f441fd2c09"},
-    {file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d352ee8ac1926d6193f602cbe36b1643bbd1bbcb25e3c1a657a4390f3000c9a5"},
-    {file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d2d9f990623f15c0ae7ac608103c33dfe1486d2ed974ac3f40b693bad1a22a7b"},
-    {file = "orjson-3.10.7-cp312-none-win32.whl", hash = "sha256:7c4c17f8157bd520cdb7195f75ddbd31671997cbe10aee559c2d613592e7d7eb"},
-    {file = "orjson-3.10.7-cp312-none-win_amd64.whl", hash = "sha256:1d9c0e733e02ada3ed6098a10a8ee0052dd55774de3d9110d29868d24b17faa1"},
-    {file = "orjson-3.10.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:77d325ed866876c0fa6492598ec01fe30e803272a6e8b10e992288b009cbe149"},
-    {file = "orjson-3.10.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ea2c232deedcb605e853ae1db2cc94f7390ac776743b699b50b071b02bea6fe"},
-    {file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3dcfbede6737fdbef3ce9c37af3fb6142e8e1ebc10336daa05872bfb1d87839c"},
-    {file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11748c135f281203f4ee695b7f80bb1358a82a63905f9f0b794769483ea854ad"},
-    {file = "orjson-3.10.7-cp313-none-win32.whl", hash = "sha256:a7e19150d215c7a13f39eb787d84db274298d3f83d85463e61d277bbd7f401d2"},
-    {file = "orjson-3.10.7-cp313-none-win_amd64.whl", hash = "sha256:eef44224729e9525d5261cc8d28d6b11cafc90e6bd0be2157bde69a52ec83024"},
-    {file = "orjson-3.10.7-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6ea2b2258eff652c82652d5e0f02bd5e0463a6a52abb78e49ac288827aaa1469"},
-    {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:430ee4d85841e1483d487e7b81401785a5dfd69db5de01314538f31f8fbf7ee1"},
-    {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4b6146e439af4c2472c56f8540d799a67a81226e11992008cb47e1267a9b3225"},
-    {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:084e537806b458911137f76097e53ce7bf5806dda33ddf6aaa66a028f8d43a23"},
-    {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4829cf2195838e3f93b70fd3b4292156fc5e097aac3739859ac0dcc722b27ac0"},
-    {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1193b2416cbad1a769f868b1749535d5da47626ac29445803dae7cc64b3f5c98"},
-    {file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4e6c3da13e5a57e4b3dca2de059f243ebec705857522f188f0180ae88badd354"},
-    {file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c31008598424dfbe52ce8c5b47e0752dca918a4fdc4a2a32004efd9fab41d866"},
-    {file = "orjson-3.10.7-cp38-none-win32.whl", hash = "sha256:7122a99831f9e7fe977dc45784d3b2edc821c172d545e6420c375e5a935f5a1c"},
-    {file = "orjson-3.10.7-cp38-none-win_amd64.whl", hash = "sha256:a763bc0e58504cc803739e7df040685816145a6f3c8a589787084b54ebc9f16e"},
-    {file = "orjson-3.10.7-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e76be12658a6fa376fcd331b1ea4e58f5a06fd0220653450f0d415b8fd0fbe20"},
-    {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed350d6978d28b92939bfeb1a0570c523f6170efc3f0a0ef1f1df287cd4f4960"},
-    {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:144888c76f8520e39bfa121b31fd637e18d4cc2f115727865fdf9fa325b10412"},
-    {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09b2d92fd95ad2402188cf51573acde57eb269eddabaa60f69ea0d733e789fe9"},
-    {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b24a579123fa884f3a3caadaed7b75eb5715ee2b17ab5c66ac97d29b18fe57f"},
-    {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591bcfe7512353bd609875ab38050efe3d55e18934e2f18950c108334b4ff"},
-    {file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f4db56635b58cd1a200b0a23744ff44206ee6aa428185e2b6c4a65b3197abdcd"},
-    {file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0fa5886854673222618638c6df7718ea7fe2f3f2384c452c9ccedc70b4a510a5"},
-    {file = "orjson-3.10.7-cp39-none-win32.whl", hash = "sha256:8272527d08450ab16eb405f47e0f4ef0e5ff5981c3d82afe0efd25dcbef2bcd2"},
-    {file = "orjson-3.10.7-cp39-none-win_amd64.whl", hash = "sha256:974683d4618c0c7dbf4f69c95a979734bf183d0658611760017f6e70a145af58"},
-    {file = "orjson-3.10.7.tar.gz", hash = "sha256:75ef0640403f945f3a1f9f6400686560dbfb0fb5b16589ad62cd477043c4eee3"},
+    {file = "orjson-3.10.12-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ece01a7ec71d9940cc654c482907a6b65df27251255097629d0dea781f255c6d"},
+    {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c34ec9aebc04f11f4b978dd6caf697a2df2dd9b47d35aa4cc606cabcb9df69d7"},
+    {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd6ec8658da3480939c79b9e9e27e0db31dffcd4ba69c334e98c9976ac29140e"},
+    {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17e6baf4cf01534c9de8a16c0c611f3d94925d1701bf5f4aff17003677d8ced"},
+    {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6402ebb74a14ef96f94a868569f5dccf70d791de49feb73180eb3c6fda2ade56"},
+    {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0000758ae7c7853e0a4a6063f534c61656ebff644391e1f81698c1b2d2fc8cd2"},
+    {file = "orjson-3.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:888442dcee99fd1e5bd37a4abb94930915ca6af4db50e23e746cdf4d1e63db13"},
+    {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c1f7a3ce79246aa0e92f5458d86c54f257fb5dfdc14a192651ba7ec2c00f8a05"},
+    {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:802a3935f45605c66fb4a586488a38af63cb37aaad1c1d94c982c40dcc452e85"},
+    {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1da1ef0113a2be19bb6c557fb0ec2d79c92ebd2fed4cfb1b26bab93f021fb885"},
+    {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a3273e99f367f137d5b3fecb5e9f45bcdbfac2a8b2f32fbc72129bbd48789c2"},
+    {file = "orjson-3.10.12-cp310-none-win32.whl", hash = "sha256:475661bf249fd7907d9b0a2a2421b4e684355a77ceef85b8352439a9163418c3"},
+    {file = "orjson-3.10.12-cp310-none-win_amd64.whl", hash = "sha256:87251dc1fb2b9e5ab91ce65d8f4caf21910d99ba8fb24b49fd0c118b2362d509"},
+    {file = "orjson-3.10.12-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a734c62efa42e7df94926d70fe7d37621c783dea9f707a98cdea796964d4cf74"},
+    {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:750f8b27259d3409eda8350c2919a58b0cfcd2054ddc1bd317a643afc646ef23"},
+    {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb52c22bfffe2857e7aa13b4622afd0dd9d16ea7cc65fd2bf318d3223b1b6252"},
+    {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:440d9a337ac8c199ff8251e100c62e9488924c92852362cd27af0e67308c16ef"},
+    {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9e15c06491c69997dfa067369baab3bf094ecb74be9912bdc4339972323f252"},
+    {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:362d204ad4b0b8724cf370d0cd917bb2dc913c394030da748a3bb632445ce7c4"},
+    {file = "orjson-3.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b57cbb4031153db37b41622eac67329c7810e5f480fda4cfd30542186f006ae"},
+    {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:165c89b53ef03ce0d7c59ca5c82fa65fe13ddf52eeb22e859e58c237d4e33b9b"},
+    {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5dee91b8dfd54557c1a1596eb90bcd47dbcd26b0baaed919e6861f076583e9da"},
+    {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a4e1cfb72de6f905bdff061172adfb3caf7a4578ebf481d8f0530879476c07"},
+    {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:038d42c7bc0606443459b8fe2d1f121db474c49067d8d14c6a075bbea8bf14dd"},
+    {file = "orjson-3.10.12-cp311-none-win32.whl", hash = "sha256:03b553c02ab39bed249bedd4abe37b2118324d1674e639b33fab3d1dafdf4d79"},
+    {file = "orjson-3.10.12-cp311-none-win_amd64.whl", hash = "sha256:8b8713b9e46a45b2af6b96f559bfb13b1e02006f4242c156cbadef27800a55a8"},
+    {file = "orjson-3.10.12-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:53206d72eb656ca5ac7d3a7141e83c5bbd3ac30d5eccfe019409177a57634b0d"},
+    {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac8010afc2150d417ebda810e8df08dd3f544e0dd2acab5370cfa6bcc0662f8f"},
+    {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed459b46012ae950dd2e17150e838ab08215421487371fa79d0eced8d1461d70"},
+    {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dcb9673f108a93c1b52bfc51b0af422c2d08d4fc710ce9c839faad25020bb69"},
+    {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22a51ae77680c5c4652ebc63a83d5255ac7d65582891d9424b566fb3b5375ee9"},
+    {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910fdf2ac0637b9a77d1aad65f803bac414f0b06f720073438a7bd8906298192"},
+    {file = "orjson-3.10.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:24ce85f7100160936bc2116c09d1a8492639418633119a2224114f67f63a4559"},
+    {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a76ba5fc8dd9c913640292df27bff80a685bed3a3c990d59aa6ce24c352f8fc"},
+    {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ff70ef093895fd53f4055ca75f93f047e088d1430888ca1229393a7c0521100f"},
+    {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f4244b7018b5753ecd10a6d324ec1f347da130c953a9c88432c7fbc8875d13be"},
+    {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16135ccca03445f37921fa4b585cff9a58aa8d81ebcb27622e69bfadd220b32c"},
+    {file = "orjson-3.10.12-cp312-none-win32.whl", hash = "sha256:2d879c81172d583e34153d524fcba5d4adafbab8349a7b9f16ae511c2cee8708"},
+    {file = "orjson-3.10.12-cp312-none-win_amd64.whl", hash = "sha256:fc23f691fa0f5c140576b8c365bc942d577d861a9ee1142e4db468e4e17094fb"},
+    {file = "orjson-3.10.12-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:47962841b2a8aa9a258b377f5188db31ba49af47d4003a32f55d6f8b19006543"},
+    {file = "orjson-3.10.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6334730e2532e77b6054e87ca84f3072bee308a45a452ea0bffbbbc40a67e296"},
+    {file = "orjson-3.10.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:accfe93f42713c899fdac2747e8d0d5c659592df2792888c6c5f829472e4f85e"},
+    {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7974c490c014c48810d1dede6c754c3cc46598da758c25ca3b4001ac45b703f"},
+    {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3f250ce7727b0b2682f834a3facff88e310f52f07a5dcfd852d99637d386e79e"},
+    {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f31422ff9486ae484f10ffc51b5ab2a60359e92d0716fcce1b3593d7bb8a9af6"},
+    {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5f29c5d282bb2d577c2a6bbde88d8fdcc4919c593f806aac50133f01b733846e"},
+    {file = "orjson-3.10.12-cp313-none-win32.whl", hash = "sha256:f45653775f38f63dc0e6cd4f14323984c3149c05d6007b58cb154dd080ddc0dc"},
+    {file = "orjson-3.10.12-cp313-none-win_amd64.whl", hash = "sha256:229994d0c376d5bdc91d92b3c9e6be2f1fbabd4cc1b59daae1443a46ee5e9825"},
+    {file = "orjson-3.10.12-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7d69af5b54617a5fac5c8e5ed0859eb798e2ce8913262eb522590239db6c6763"},
+    {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ed119ea7d2953365724a7059231a44830eb6bbb0cfead33fcbc562f5fd8f935"},
+    {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5fc1238ef197e7cad5c91415f524aaa51e004be5a9b35a1b8a84ade196f73f"},
+    {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43509843990439b05f848539d6f6198d4ac86ff01dd024b2f9a795c0daeeab60"},
+    {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f72e27a62041cfb37a3de512247ece9f240a561e6c8662276beaf4d53d406db4"},
+    {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a904f9572092bb6742ab7c16c623f0cdccbad9eeb2d14d4aa06284867bddd31"},
+    {file = "orjson-3.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:855c0833999ed5dc62f64552db26f9be767434917d8348d77bacaab84f787d7b"},
+    {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:897830244e2320f6184699f598df7fb9db9f5087d6f3f03666ae89d607e4f8ed"},
+    {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:0b32652eaa4a7539f6f04abc6243619c56f8530c53bf9b023e1269df5f7816dd"},
+    {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:36b4aa31e0f6a1aeeb6f8377769ca5d125db000f05c20e54163aef1d3fe8e833"},
+    {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5535163054d6cbf2796f93e4f0dbc800f61914c0e3c4ed8499cf6ece22b4a3da"},
+    {file = "orjson-3.10.12-cp38-none-win32.whl", hash = "sha256:90a5551f6f5a5fa07010bf3d0b4ca2de21adafbbc0af6cb700b63cd767266cb9"},
+    {file = "orjson-3.10.12-cp38-none-win_amd64.whl", hash = "sha256:703a2fb35a06cdd45adf5d733cf613cbc0cb3ae57643472b16bc22d325b5fb6c"},
+    {file = "orjson-3.10.12-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f29de3ef71a42a5822765def1febfb36e0859d33abf5c2ad240acad5c6a1b78d"},
+    {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de365a42acc65d74953f05e4772c974dad6c51cfc13c3240899f534d611be967"},
+    {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a5a0158648a67ff0004cb0df5df7dcc55bfc9ca154d9c01597a23ad54c8d0c"},
+    {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c47ce6b8d90fe9646a25b6fb52284a14ff215c9595914af63a5933a49972ce36"},
+    {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0eee4c2c5bfb5c1b47a5db80d2ac7aaa7e938956ae88089f098aff2c0f35d5d8"},
+    {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d3081bbe8b86587eb5c98a73b97f13d8f9fea685cf91a579beddacc0d10566"},
+    {file = "orjson-3.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c23a6e90383884068bc2dba83d5222c9fcc3b99a0ed2411d38150734236755"},
+    {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5472be7dc3269b4b52acba1433dac239215366f89dc1d8d0e64029abac4e714e"},
+    {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:7319cda750fca96ae5973efb31b17d97a5c5225ae0bc79bf5bf84df9e1ec2ab6"},
+    {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:74d5ca5a255bf20b8def6a2b96b1e18ad37b4a122d59b154c458ee9494377f80"},
+    {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ff31d22ecc5fb85ef62c7d4afe8301d10c558d00dd24274d4bbe464380d3cd69"},
+    {file = "orjson-3.10.12-cp39-none-win32.whl", hash = "sha256:c22c3ea6fba91d84fcb4cda30e64aff548fcf0c44c876e681f47d61d24b12e6b"},
+    {file = "orjson-3.10.12-cp39-none-win_amd64.whl", hash = "sha256:be604f60d45ace6b0b33dd990a66b4526f1a7a186ac411c942674625456ca548"},
+    {file = "orjson-3.10.12.tar.gz", hash = "sha256:0a78bbda3aea0f9f079057ee1ee8a1ecf790d4f1af88dd67493c6b8ee52506ff"},
 ]
 
 [[package]]
@@ -2374,62 +2492,155 @@ files = [
 
 [[package]]
 name = "pydantic"
-version = "1.10.18"
-description = "Data validation and settings management using python type hints"
+version = "2.10.4"
+description = "Data validation using Python type hints"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "pydantic-1.10.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02"},
-    {file = "pydantic-1.10.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80"},
-    {file = "pydantic-1.10.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62"},
-    {file = "pydantic-1.10.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab"},
-    {file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0"},
-    {file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861"},
-    {file = "pydantic-1.10.18-cp310-cp310-win_amd64.whl", hash = "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70"},
-    {file = "pydantic-1.10.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec"},
-    {file = "pydantic-1.10.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5"},
-    {file = "pydantic-1.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481"},
-    {file = "pydantic-1.10.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3"},
-    {file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588"},
-    {file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f"},
-    {file = "pydantic-1.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33"},
-    {file = "pydantic-1.10.18-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2"},
-    {file = "pydantic-1.10.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048"},
-    {file = "pydantic-1.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7"},
-    {file = "pydantic-1.10.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890"},
-    {file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f"},
-    {file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a"},
-    {file = "pydantic-1.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357"},
-    {file = "pydantic-1.10.18-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3d5492dbf953d7d849751917e3b2433fb26010d977aa7a0765c37425a4026ff1"},
-    {file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe734914977eed33033b70bfc097e1baaffb589517863955430bf2e0846ac30f"},
-    {file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15fdbe568beaca9aacfccd5ceadfb5f1a235087a127e8af5e48df9d8a45ae85c"},
-    {file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c3e742f62198c9eb9201781fbebe64533a3bbf6a76a91b8d438d62b813079dbc"},
-    {file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:19a3bd00b9dafc2cd7250d94d5b578edf7a0bd7daf102617153ff9a8fa37871c"},
-    {file = "pydantic-1.10.18-cp37-cp37m-win_amd64.whl", hash = "sha256:2ce3fcf75b2bae99aa31bd4968de0474ebe8c8258a0110903478bd83dfee4e3b"},
-    {file = "pydantic-1.10.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03"},
-    {file = "pydantic-1.10.18-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a"},
-    {file = "pydantic-1.10.18-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b"},
-    {file = "pydantic-1.10.18-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682"},
-    {file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe"},
-    {file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3"},
-    {file = "pydantic-1.10.18-cp38-cp38-win_amd64.whl", hash = "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620"},
-    {file = "pydantic-1.10.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf"},
-    {file = "pydantic-1.10.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9"},
-    {file = "pydantic-1.10.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d"},
-    {file = "pydantic-1.10.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485"},
-    {file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f"},
-    {file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86"},
-    {file = "pydantic-1.10.18-cp39-cp39-win_amd64.whl", hash = "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518"},
-    {file = "pydantic-1.10.18-py3-none-any.whl", hash = "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82"},
-    {file = "pydantic-1.10.18.tar.gz", hash = "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a"},
+    {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"},
+    {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"},
 ]
 
 [package.dependencies]
-typing-extensions = ">=4.2.0"
+annotated-types = ">=0.6.0"
+pydantic-core = "2.27.2"
+typing-extensions = ">=4.12.2"
 
 [package.extras]
-dotenv = ["python-dotenv (>=0.10.4)"]
-email = ["email-validator (>=1.0.3)"]
+email = ["email-validator (>=2.0.0)"]
+timezone = ["tzdata"]
+
+[[package]]
+name = "pydantic-core"
+version = "2.27.2"
+description = "Core functionality for Pydantic validation and serialization"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"},
+    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"},
+    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"},
+    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"},
+    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"},
+    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"},
+    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"},
+    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"},
+    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"},
+    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"},
+    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"},
+    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"},
+    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"},
+    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"},
+    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"},
+    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"},
+    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"},
+    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"},
+    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"},
+    {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
+
+[[package]]
+name = "pydantic-settings"
+version = "2.7.0"
+description = "Settings management using Pydantic"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pydantic_settings-2.7.0-py3-none-any.whl", hash = "sha256:e00c05d5fa6cbbb227c84bd7487c5c1065084119b750df7c8c1a554aed236eb5"},
+    {file = "pydantic_settings-2.7.0.tar.gz", hash = "sha256:ac4bfd4a36831a48dbf8b2d9325425b549a0a6f18cea118436d728eb4f1c4d66"},
+]
+
+[package.dependencies]
+pydantic = ">=2.7.0"
+python-dotenv = ">=0.21.0"
+
+[package.extras]
+azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"]
+toml = ["tomli (>=2.0.1)"]
+yaml = ["pyyaml (>=6.0.1)"]
 
 [[package]]
 name = "pygments"
@@ -2473,13 +2684,13 @@ files = [
 
 [[package]]
 name = "pytest"
-version = "8.3.2"
+version = "8.3.4"
 description = "pytest: simple powerful testing with Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"},
-    {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"},
+    {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
+    {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
 ]
 
 [package.dependencies]
@@ -2495,35 +2706,35 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments
 
 [[package]]
 name = "pytest-asyncio"
-version = "0.24.0"
+version = "0.25.0"
 description = "Pytest support for asyncio"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"},
-    {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"},
+    {file = "pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3"},
+    {file = "pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609"},
 ]
 
 [package.dependencies]
 pytest = ">=8.2,<9"
 
 [package.extras]
-docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
+docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"]
 testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
 
 [[package]]
 name = "pytest-cov"
-version = "5.0.0"
+version = "6.0.0"
 description = "Pytest plugin for measuring coverage."
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"},
-    {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"},
+    {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"},
+    {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"},
 ]
 
 [package.dependencies]
-coverage = {version = ">=5.2.1", extras = ["toml"]}
+coverage = {version = ">=7.5", extras = ["toml"]}
 pytest = ">=4.6"
 
 [package.extras]
@@ -2576,18 +2787,15 @@ cli = ["click (>=5.0)"]
 
 [[package]]
 name = "python-multipart"
-version = "0.0.9"
+version = "0.0.20"
 description = "A streaming multipart parser for Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"},
-    {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"},
+    {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"},
+    {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"},
 ]
 
-[package.extras]
-dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"]
-
 [[package]]
 name = "pywin32"
 version = "306"
@@ -2816,47 +3024,48 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 
 [[package]]
 name = "rich"
-version = "13.8.0"
+version = "13.9.4"
 description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
 optional = false
-python-versions = ">=3.7.0"
+python-versions = ">=3.8.0"
 files = [
-    {file = "rich-13.8.0-py3-none-any.whl", hash = "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc"},
-    {file = "rich-13.8.0.tar.gz", hash = "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4"},
+    {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"},
+    {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"},
 ]
 
 [package.dependencies]
 markdown-it-py = ">=2.2.0"
 pygments = ">=2.13.0,<3.0.0"
+typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""}
 
 [package.extras]
 jupyter = ["ipywidgets (>=7.5.1,<9)"]
 
 [[package]]
 name = "ruff"
-version = "0.6.4"
+version = "0.8.3"
 description = "An extremely fast Python linter and code formatter, written in Rust."
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "ruff-0.6.4-py3-none-linux_armv6l.whl", hash = "sha256:c4b153fc152af51855458e79e835fb6b933032921756cec9af7d0ba2aa01a258"},
-    {file = "ruff-0.6.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bedff9e4f004dad5f7f76a9d39c4ca98af526c9b1695068198b3bda8c085ef60"},
-    {file = "ruff-0.6.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d02a4127a86de23002e694d7ff19f905c51e338c72d8e09b56bfb60e1681724f"},
-    {file = "ruff-0.6.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7862f42fc1a4aca1ea3ffe8a11f67819d183a5693b228f0bb3a531f5e40336fc"},
-    {file = "ruff-0.6.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eebe4ff1967c838a1a9618a5a59a3b0a00406f8d7eefee97c70411fefc353617"},
-    {file = "ruff-0.6.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:932063a03bac394866683e15710c25b8690ccdca1cf192b9a98260332ca93408"},
-    {file = "ruff-0.6.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:50e30b437cebef547bd5c3edf9ce81343e5dd7c737cb36ccb4fe83573f3d392e"},
-    {file = "ruff-0.6.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c44536df7b93a587de690e124b89bd47306fddd59398a0fb12afd6133c7b3818"},
-    {file = "ruff-0.6.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ea086601b22dc5e7693a78f3fcfc460cceabfdf3bdc36dc898792aba48fbad6"},
-    {file = "ruff-0.6.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b52387d3289ccd227b62102c24714ed75fbba0b16ecc69a923a37e3b5e0aaaa"},
-    {file = "ruff-0.6.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0308610470fcc82969082fc83c76c0d362f562e2f0cdab0586516f03a4e06ec6"},
-    {file = "ruff-0.6.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:803b96dea21795a6c9d5bfa9e96127cc9c31a1987802ca68f35e5c95aed3fc0d"},
-    {file = "ruff-0.6.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:66dbfea86b663baab8fcae56c59f190caba9398df1488164e2df53e216248baa"},
-    {file = "ruff-0.6.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:34d5efad480193c046c86608dbba2bccdc1c5fd11950fb271f8086e0c763a5d1"},
-    {file = "ruff-0.6.4-py3-none-win32.whl", hash = "sha256:f0f8968feea5ce3777c0d8365653d5e91c40c31a81d95824ba61d871a11b8523"},
-    {file = "ruff-0.6.4-py3-none-win_amd64.whl", hash = "sha256:549daccee5227282289390b0222d0fbee0275d1db6d514550d65420053021a58"},
-    {file = "ruff-0.6.4-py3-none-win_arm64.whl", hash = "sha256:ac4b75e898ed189b3708c9ab3fc70b79a433219e1e87193b4f2b77251d058d14"},
-    {file = "ruff-0.6.4.tar.gz", hash = "sha256:ac3b5bfbee99973f80aa1b7cbd1c9cbce200883bdd067300c22a6cc1c7fba212"},
+    {file = "ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"},
+    {file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"},
+    {file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"},
+    {file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"},
+    {file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"},
+    {file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"},
+    {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"},
+    {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"},
+    {file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"},
+    {file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"},
+    {file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"},
+    {file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"},
+    {file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"},
+    {file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"},
+    {file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"},
+    {file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"},
+    {file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"},
+    {file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"},
 ]
 
 [[package]]
@@ -3035,13 +3244,13 @@ files = [
 
 [[package]]
 name = "starlette"
-version = "0.37.2"
+version = "0.41.2"
 description = "The little ASGI library that shines."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"},
-    {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"},
+    {file = "starlette-0.41.2-py3-none-any.whl", hash = "sha256:fbc189474b4731cf30fcef52f18a8d070e3f3b46c6a04c97579e85e6ffca942d"},
+    {file = "starlette-0.41.2.tar.gz", hash = "sha256:9834fd799d1a87fd346deb76158668cfa0b0d56f85caefe8268e2d97c3468b62"},
 ]
 
 [package.dependencies]
@@ -3094,111 +3303,26 @@ all = ["defusedxml", "fsspec", "imagecodecs (>=2023.8.12)", "lxml", "matplotlib"
 
 [[package]]
 name = "tokenizers"
-version = "0.20.0"
+version = "0.21.0"
 description = ""
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "tokenizers-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6cff5c5e37c41bc5faa519d6f3df0679e4b37da54ea1f42121719c5e2b4905c0"},
-    {file = "tokenizers-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:62a56bf75c27443432456f4ca5ca055befa95e25be8a28141cc495cac8ae4d6d"},
-    {file = "tokenizers-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68cc7de6a63f09c4a86909c2597b995aa66e19df852a23aea894929c74369929"},
-    {file = "tokenizers-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:053c37ecee482cc958fdee53af3c6534286a86f5d35aac476f7c246830e53ae5"},
-    {file = "tokenizers-0.20.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d7074aaabc151a6363fa03db5493fc95b423b2a1874456783989e96d541c7b6"},
-    {file = "tokenizers-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a11435780f2acd89e8fefe5e81cecf01776f6edb9b3ac95bcb76baee76b30b90"},
-    {file = "tokenizers-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a81cd2712973b007d84268d45fc3f6f90a79c31dfe7f1925e6732f8d2959987"},
-    {file = "tokenizers-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7dfd796ab9d909f76fb93080e1c7c8309f196ecb316eb130718cd5e34231c69"},
-    {file = "tokenizers-0.20.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8029ad2aa8cb00605c9374566034c1cc1b15130713e0eb5afcef6cface8255c9"},
-    {file = "tokenizers-0.20.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ca4d54260ebe97d59dfa9a30baa20d0c4dd9137d99a8801700055c561145c24e"},
-    {file = "tokenizers-0.20.0-cp310-none-win32.whl", hash = "sha256:95ee16b57cec11b86a7940174ec5197d506439b0f415ab3859f254b1dffe9df0"},
-    {file = "tokenizers-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:0a61a11e93eeadbf02aea082ffc75241c4198e0608bbbac4f65a9026851dcf37"},
-    {file = "tokenizers-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6636b798b3c4d6c9b1af1a918bd07c867808e5a21c64324e95318a237e6366c3"},
-    {file = "tokenizers-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ec603e42eaf499ffd58b9258162add948717cf21372458132f14e13a6bc7172"},
-    {file = "tokenizers-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cce124264903a8ea6f8f48e1cc7669e5ef638c18bd4ab0a88769d5f92debdf7f"},
-    {file = "tokenizers-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07bbeba0231cf8de07aa6b9e33e9779ff103d47042eeeb859a8c432e3292fb98"},
-    {file = "tokenizers-0.20.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:06c0ca8397b35d38b83a44a9c6929790c1692957d88541df061cb34d82ebbf08"},
-    {file = "tokenizers-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca6557ac3b83d912dfbb1f70ab56bd4b0594043916688e906ede09f42e192401"},
-    {file = "tokenizers-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a5ad94c9e80ac6098328bee2e3264dbced4c6faa34429994d473f795ec58ef4"},
-    {file = "tokenizers-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b5c7f906ee6bec30a9dc20268a8b80f3b9584de1c9f051671cb057dc6ce28f6"},
-    {file = "tokenizers-0.20.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:31e087e9ee1b8f075b002bfee257e858dc695f955b43903e1bb4aa9f170e37fe"},
-    {file = "tokenizers-0.20.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c3124fb6f3346cb3d8d775375d3b429bf4dcfc24f739822702009d20a4297990"},
-    {file = "tokenizers-0.20.0-cp311-none-win32.whl", hash = "sha256:a4bb8b40ba9eefa621fdcabf04a74aa6038ae3be0c614c6458bd91a4697a452f"},
-    {file = "tokenizers-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:2b709d371f1fe60a28ef0c5c67815952d455ca7f34dbe7197eaaed3cc54b658e"},
-    {file = "tokenizers-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:15c81a17d0d66f4987c6ca16f4bea7ec253b8c7ed1bb00fdc5d038b1bb56e714"},
-    {file = "tokenizers-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a531cdf1fb6dc41c984c785a3b299cb0586de0b35683842a3afbb1e5207f910"},
-    {file = "tokenizers-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06caabeb4587f8404e0cd9d40f458e9cba3e815c8155a38e579a74ff3e2a4301"},
-    {file = "tokenizers-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8768f964f23f5b9f50546c0369c75ab3262de926983888bbe8b98be05392a79c"},
-    {file = "tokenizers-0.20.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:626403860152c816f97b649fd279bd622c3d417678c93b4b1a8909b6380b69a8"},
-    {file = "tokenizers-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c1b88fa9e5ff062326f4bf82681da5a96fca7104d921a6bd7b1e6fcf224af26"},
-    {file = "tokenizers-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d7e559436a07dc547f22ce1101f26d8b2fad387e28ec8e7e1e3b11695d681d8"},
-    {file = "tokenizers-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48afb75e50449848964e4a67b0da01261dd3aa8df8daecf10db8fd7f5b076eb"},
-    {file = "tokenizers-0.20.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:baf5d0e1ff44710a95eefc196dd87666ffc609fd447c5e5b68272a7c3d342a1d"},
-    {file = "tokenizers-0.20.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e5e56df0e8ed23ba60ae3848c3f069a0710c4b197218fe4f89e27eba38510768"},
-    {file = "tokenizers-0.20.0-cp312-none-win32.whl", hash = "sha256:ec53e5ecc142a82432f9c6c677dbbe5a2bfee92b8abf409a9ecb0d425ee0ce75"},
-    {file = "tokenizers-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:f18661ece72e39c0dfaa174d6223248a15b457dbd4b0fc07809b8e6d3ca1a234"},
-    {file = "tokenizers-0.20.0-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:f7065b1084d8d1a03dc89d9aad69bcbc8415d4bc123c367063eb32958cd85054"},
-    {file = "tokenizers-0.20.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e5d4069e4714e3f7ba0a4d3d44f9d84a432cd4e4aa85c3d7dd1f51440f12e4a1"},
-    {file = "tokenizers-0.20.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:799b808529e54b7e1a36350bda2aeb470e8390e484d3e98c10395cee61d4e3c6"},
-    {file = "tokenizers-0.20.0-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f9baa027cc8a281ad5f7725a93c204d7a46986f88edbe8ef7357f40a23fb9c7"},
-    {file = "tokenizers-0.20.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:010ec7f3f7a96adc4c2a34a3ada41fa14b4b936b5628b4ff7b33791258646c6b"},
-    {file = "tokenizers-0.20.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98d88f06155335b14fd78e32ee28ca5b2eb30fced4614e06eb14ae5f7fba24ed"},
-    {file = "tokenizers-0.20.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e13eb000ef540c2280758d1b9cfa5fe424b0424ae4458f440e6340a4f18b2638"},
-    {file = "tokenizers-0.20.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fab3cf066ff426f7e6d70435dc28a9ff01b2747be83810e397cba106f39430b0"},
-    {file = "tokenizers-0.20.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:39fa3761b30a89368f322e5daf4130dce8495b79ad831f370449cdacfb0c0d37"},
-    {file = "tokenizers-0.20.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c8da0fba4d179ddf2607821575998df3c294aa59aa8df5a6646dc64bc7352bce"},
-    {file = "tokenizers-0.20.0-cp37-none-win32.whl", hash = "sha256:fada996d6da8cf213f6e3c91c12297ad4f6cdf7a85c2fadcd05ec32fa6846fcd"},
-    {file = "tokenizers-0.20.0-cp37-none-win_amd64.whl", hash = "sha256:7d29aad702279e0760c265fcae832e89349078e3418dd329732d4503259fd6bd"},
-    {file = "tokenizers-0.20.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:099c68207f3ef0227ecb6f80ab98ea74de559f7b124adc7b17778af0250ee90a"},
-    {file = "tokenizers-0.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:68012d8a8cddb2eab3880870d7e2086cb359c7f7a2b03f5795044f5abff4e850"},
-    {file = "tokenizers-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9253bdd209c6aee168deca7d0e780581bf303e0058f268f9bb06859379de19b6"},
-    {file = "tokenizers-0.20.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f868600ddbcb0545905ed075eb7218a0756bf6c09dae7528ea2f8436ebd2c93"},
-    {file = "tokenizers-0.20.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9643d9c8c5f99b6aba43fd10034f77cc6c22c31f496d2f0ee183047d948fa0"},
-    {file = "tokenizers-0.20.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c375c6a889aeab44734028bc65cc070acf93ccb0f9368be42b67a98e1063d3f6"},
-    {file = "tokenizers-0.20.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e359f852328e254f070bbd09a19a568421d23388f04aad9f2fb7da7704c7228d"},
-    {file = "tokenizers-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d98b01a309d4387f3b1c1dd68a8b8136af50376cf146c1b7e8d8ead217a5be4b"},
-    {file = "tokenizers-0.20.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:459f7537119554c2899067dec1ac74a00d02beef6558f4ee2e99513bf6d568af"},
-    {file = "tokenizers-0.20.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:392b87ec89452628c045c9f2a88bc2a827f4c79e7d84bc3b72752b74c2581f70"},
-    {file = "tokenizers-0.20.0-cp38-none-win32.whl", hash = "sha256:55a393f893d2ed4dd95a1553c2e42d4d4086878266f437b03590d3f81984c4fe"},
-    {file = "tokenizers-0.20.0-cp38-none-win_amd64.whl", hash = "sha256:30ffe33c5c2f2aab8e9a3340d0110dd9f7ace7eec7362e20a697802306bd8068"},
-    {file = "tokenizers-0.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:aa2d4a6fed2a7e3f860c7fc9d48764bb30f2649d83915d66150d6340e06742b8"},
-    {file = "tokenizers-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b5ef0f814084a897e9071fc4a868595f018c5c92889197bdc4bf19018769b148"},
-    {file = "tokenizers-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc1e1b791e8c3bf4c4f265f180dadaff1c957bf27129e16fdd5e5d43c2d3762c"},
-    {file = "tokenizers-0.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b69e55e481459c07885263743a0d3c18d52db19bae8226a19bcca4aaa213fff"},
-    {file = "tokenizers-0.20.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4806b4d82e27a2512bc23057b2986bc8b85824914286975b84d8105ff40d03d9"},
-    {file = "tokenizers-0.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9859e9ef13adf5a473ccab39d31bff9c550606ae3c784bf772b40f615742a24f"},
-    {file = "tokenizers-0.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef703efedf4c20488a8eb17637b55973745b27997ff87bad88ed499b397d1144"},
-    {file = "tokenizers-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eec0061bab94b1841ab87d10831fdf1b48ebaed60e6d66d66dbe1d873f92bf5"},
-    {file = "tokenizers-0.20.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:980f3d0d7e73f845b69087f29a63c11c7eb924c4ad6b358da60f3db4cf24bdb4"},
-    {file = "tokenizers-0.20.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7c157550a2f3851b29d7fdc9dc059fcf81ff0c0fc49a1e5173a89d533ed043fa"},
-    {file = "tokenizers-0.20.0-cp39-none-win32.whl", hash = "sha256:8a3d2f4d08608ec4f9895ec25b4b36a97f05812543190a5f2c3cd19e8f041e5a"},
-    {file = "tokenizers-0.20.0-cp39-none-win_amd64.whl", hash = "sha256:d90188d12afd0c75e537f9a1d92f9c7375650188ee4f48fdc76f9e38afbd2251"},
-    {file = "tokenizers-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d68e15f1815357b059ec266062340c343ea7f98f7f330602df81ffa3474b6122"},
-    {file = "tokenizers-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:23f9ecec637b9bc80da5f703808d29ed5329e56b5aa8d791d1088014f48afadc"},
-    {file = "tokenizers-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f830b318ee599e3d0665b3e325f85bc75ee2d2ca6285f52e439dc22b64691580"},
-    {file = "tokenizers-0.20.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3dc750def789cb1de1b5a37657919545e1d9ffa667658b3fa9cb7862407a1b8"},
-    {file = "tokenizers-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e26e6c755ae884c2ea6135cd215bdd0fccafe4ee62405014b8c3cd19954e3ab9"},
-    {file = "tokenizers-0.20.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a1158c7174f427182e08baa2a8ded2940f2b4a3e94969a85cc9cfd16004cbcea"},
-    {file = "tokenizers-0.20.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:6324826287a3fc198898d3dcf758fe4a8479e42d6039f4c59e2cedd3cf92f64e"},
-    {file = "tokenizers-0.20.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7d8653149405bb0c16feaf9cfee327fdb6aaef9dc2998349fec686f35e81c4e2"},
-    {file = "tokenizers-0.20.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8a2dc1e402a155e97309287ca085c80eb1b7fab8ae91527d3b729181639fa51"},
-    {file = "tokenizers-0.20.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07bef67b20aa6e5f7868c42c7c5eae4d24f856274a464ae62e47a0f2cccec3da"},
-    {file = "tokenizers-0.20.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da06e397182ff53789c506c7833220c192952c57e1581a53f503d8d953e2d67e"},
-    {file = "tokenizers-0.20.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:302f7e11a14814028b7fc88c45a41f1bbe9b5b35fd76d6869558d1d1809baa43"},
-    {file = "tokenizers-0.20.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:055ec46e807b875589dfbe3d9259f9a6ee43394fb553b03b3d1e9541662dbf25"},
-    {file = "tokenizers-0.20.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e3144b8acebfa6ae062e8f45f7ed52e4b50fb6c62f93afc8871b525ab9fdcab3"},
-    {file = "tokenizers-0.20.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b52aa3fd14b2a07588c00a19f66511cff5cca8f7266ca3edcdd17f3512ad159f"},
-    {file = "tokenizers-0.20.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b8cf52779ffc5d4d63a0170fbeb512372bad0dd014ce92bbb9149756c831124"},
-    {file = "tokenizers-0.20.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:983a45dd11a876124378dae71d6d9761822199b68a4c73f32873d8cdaf326a5b"},
-    {file = "tokenizers-0.20.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6b819c9a19831ebec581e71a7686a54ab45d90faf3842269a10c11d746de0c"},
-    {file = "tokenizers-0.20.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e738cfd80795fcafcef89c5731c84b05638a4ab3f412f97d5ed7765466576eb1"},
-    {file = "tokenizers-0.20.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c8842c7be2fadb9c9edcee233b1b7fe7ade406c99b0973f07439985c1c1d0683"},
-    {file = "tokenizers-0.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e47a82355511c373a4a430c4909dc1e518e00031207b1fec536c49127388886b"},
-    {file = "tokenizers-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9afbf359004551179a5db19424180c81276682773cff2c5d002f6eaaffe17230"},
-    {file = "tokenizers-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07eaa8799a92e6af6f472c21a75bf71575de2af3c0284120b7a09297c0de2f3"},
-    {file = "tokenizers-0.20.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0994b2e5fc53a301071806bc4303e4bc3bdc3f490e92a21338146a36746b0872"},
-    {file = "tokenizers-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b6466e0355b603d10e3cc3d282d350b646341b601e50969464a54939f9848d0"},
-    {file = "tokenizers-0.20.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1e86594c2a433cb1ea09cfbe596454448c566e57ee8905bd557e489d93e89986"},
-    {file = "tokenizers-0.20.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3e14cdef1efa96ecead6ea64a891828432c3ebba128bdc0596e3059fea104ef3"},
-    {file = "tokenizers-0.20.0.tar.gz", hash = "sha256:39d7acc43f564c274085cafcd1dae9d36f332456de1a31970296a6b8da4eac8d"},
+    {file = "tokenizers-0.21.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:3c4c93eae637e7d2aaae3d376f06085164e1660f89304c0ab2b1d08a406636b2"},
+    {file = "tokenizers-0.21.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:f53ea537c925422a2e0e92a24cce96f6bc5046bbef24a1652a5edc8ba975f62e"},
+    {file = "tokenizers-0.21.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b177fb54c4702ef611de0c069d9169f0004233890e0c4c5bd5508ae05abf193"},
+    {file = "tokenizers-0.21.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6b43779a269f4629bebb114e19c3fca0223296ae9fea8bb9a7a6c6fb0657ff8e"},
+    {file = "tokenizers-0.21.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aeb255802be90acfd363626753fda0064a8df06031012fe7d52fd9a905eb00e"},
+    {file = "tokenizers-0.21.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8b09dbeb7a8d73ee204a70f94fc06ea0f17dcf0844f16102b9f414f0b7463ba"},
+    {file = "tokenizers-0.21.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:400832c0904f77ce87c40f1a8a27493071282f785724ae62144324f171377273"},
+    {file = "tokenizers-0.21.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84ca973b3a96894d1707e189c14a774b701596d579ffc7e69debfc036a61a04"},
+    {file = "tokenizers-0.21.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:eb7202d231b273c34ec67767378cd04c767e967fda12d4a9e36208a34e2f137e"},
+    {file = "tokenizers-0.21.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:089d56db6782a73a27fd8abf3ba21779f5b85d4a9f35e3b493c7bbcbbf0d539b"},
+    {file = "tokenizers-0.21.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:c87ca3dc48b9b1222d984b6b7490355a6fdb411a2d810f6f05977258400ddb74"},
+    {file = "tokenizers-0.21.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4145505a973116f91bc3ac45988a92e618a6f83eb458f49ea0790df94ee243ff"},
+    {file = "tokenizers-0.21.0-cp39-abi3-win32.whl", hash = "sha256:eb1702c2f27d25d9dd5b389cc1f2f51813e99f8ca30d9e25348db6585a97e24a"},
+    {file = "tokenizers-0.21.0-cp39-abi3-win_amd64.whl", hash = "sha256:87841da5a25a3a5f70c102de371db120f41873b854ba65e52bccd57df5a3780c"},
+    {file = "tokenizers-0.21.0.tar.gz", hash = "sha256:ee0894bf311b75b0c03079f33859ae4b2334d675d4e93f5a4132e1eae2834fe4"},
 ]
 
 [package.dependencies]
@@ -3269,20 +3393,20 @@ zstd = ["zstandard (>=0.18.0)"]
 
 [[package]]
 name = "uvicorn"
-version = "0.30.6"
+version = "0.34.0"
 description = "The lightning-fast ASGI server."
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"},
-    {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"},
+    {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"},
+    {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"},
 ]
 
 [package.dependencies]
 click = ">=7.0"
 colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""}
 h11 = ">=0.8"
-httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""}
+httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standard\""}
 python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
 pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""}
 typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
@@ -3291,7 +3415,7 @@ watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standar
 websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""}
 
 [package.extras]
-standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
+standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
 
 [[package]]
 name = "uvloop"
@@ -3607,4 +3731,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
 [metadata]
 lock-version = "2.0"
 python-versions = ">=3.10,<4.0"
-content-hash = "b2b053886ca1dd3a3305c63caf155b1976dfc4066f72f5d1ecfc42099db34aab"
+content-hash = "b690d5fbd141da3947f4f1dc029aba1b95e7faafd723166f2c4bdc47a66c095e"
diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml
index cc7f74dfa9..785c7ba8ac 100644
--- a/machine-learning/pyproject.toml
+++ b/machine-learning/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "machine-learning"
-version = "1.115.0"
+version = "1.123.0"
 description = ""
 authors = ["Hau Tran <alex.tran1502@gmail.com>"]
 readme = "README.md"
@@ -11,13 +11,13 @@ python = ">=3.10,<4.0"
 insightface = ">=0.7.3,<1.0"
 opencv-python-headless = ">=4.7.0.72,<5.0"
 pillow = ">=9.5.0,<11.0"
-fastapi-slim = ">=0.95.2,<1.0"
+fastapi = ">=0.95.2,<1.0"
 uvicorn = {extras = ["standard"], version = ">=0.22.0,<1.0"}
-pydantic = "^1.10.8"
+pydantic = "^2.0.0"
+pydantic-settings = "^2.5.2"
 aiocache = ">=0.12.1,<1.0"
 rich = ">=13.4.2"
 ftfy = ">=6.1.1"
-setuptools = "^70.0.0"
 python-multipart = ">=0.0.6,<1.0"
 orjson = ">=3.9.5"
 gunicorn = ">=21.1.0"
@@ -51,7 +51,7 @@ onnxruntime-gpu = {version = "^1.17.0", source = "cuda12"}
 optional = true
 
 [tool.poetry.group.openvino.dependencies]
-onnxruntime-openvino = "^1.17.1"
+onnxruntime-openvino = ">=1.17.1,<1.19.0"
 
 [tool.poetry.group.armnn]
 optional = true
diff --git a/machine-learning/start.sh b/machine-learning/start.sh
index c3fda523df..552cca1f5e 100755
--- a/machine-learning/start.sh
+++ b/machine-learning/start.sh
@@ -17,6 +17,7 @@ fi
 
 gunicorn app.main:app \
 	-k app.config.CustomUvicornWorker \
+	-c gunicorn_conf.py \
 	-b "$IMMICH_HOST":"$IMMICH_PORT" \
 	-w "$MACHINE_LEARNING_WORKERS" \
 	-t "$MACHINE_LEARNING_WORKER_TIMEOUT" \
diff --git a/mobile/.fvmrc b/mobile/.fvmrc
index 971587f297..691c22dd17 100644
--- a/mobile/.fvmrc
+++ b/mobile/.fvmrc
@@ -1,3 +1,3 @@
 {
-  "flutter": "3.24.0"
+  "flutter": "3.24.5"
 }
diff --git a/mobile/.vscode/settings.json b/mobile/.vscode/settings.json
index aa43dab3fb..ceaf9a6ab8 100644
--- a/mobile/.vscode/settings.json
+++ b/mobile/.vscode/settings.json
@@ -1,5 +1,5 @@
 {
-  "dart.flutterSdkPath": ".fvm/versions/3.24.0",
+  "dart.flutterSdkPath": ".fvm/versions/3.24.3",
   "search.exclude": {
     "**/.fvm": true
   },
diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml
index fe5729fc60..9327780f1d 100644
--- a/mobile/analysis_options.yaml
+++ b/mobile/analysis_options.yaml
@@ -28,6 +28,7 @@ linter:
     use_build_context_synchronously: false
     require_trailing_commas: true
     unrelated_type_equality_checks: true
+    prefer_const_constructors: true
 
 # Additional information about this file can be found at
 # https://dart.dev/guides/language/analysis-options
@@ -36,8 +37,75 @@ analyzer:
     - openapi/**
     - lib/generated_plugin_registrant.dart
 
-plugins:
-  - custom_lint
+  plugins:
+    - custom_lint
+
+custom_lint:
+  debug: true
+  rules:
+    - avoid_build_context_in_providers: false
+    - avoid_public_notifier_properties: false
+    - avoid_manual_providers_as_generated_provider_dependency: false
+    - unsupported_provider_value: false
+    - import_rule_photo_manager:
+      message: photo_manager must only be used in MediaRepositories
+      restrict: package:photo_manager
+      allowed:
+        # required / wanted
+        - 'lib/repositories/{album,asset,file}_media.repository.dart'
+        # acceptable exceptions for the time being
+        - lib/entities/asset.entity.dart # to provide local AssetEntity for now
+        - lib/providers/image/immich_local_{image,thumbnail}_provider.dart # accesses thumbnails via PhotoManager
+        # refactor to make the providers and services testable
+        - lib/providers/backup/{backup,manual_upload}.provider.dart # uses only PMProgressHandler
+        - lib/services/{background,backup}.service.dart # uses only PMProgressHandler
+    - import_rule_isar:
+      message: isar must only be used in entities and repositories
+      restrict: package:isar
+      allowed:
+        # required / wanted
+        - lib/entities/*.entity.dart
+        - lib/repositories/{album,asset,backup,database,etag,exif_info,user}.repository.dart
+        # acceptable exceptions for the time being (until Isar is fully replaced)
+        - integration_test/test_utils/general_helper.dart
+        - lib/main.dart
+        - lib/pages/album/album_asset_selection.page.dart
+        - lib/routing/router.dart
+        - lib/services/immich_logger.service.dart # not really a service... more a util
+        - lib/utils/{db,migration,renderlist_generator}.dart
+        - lib/widgets/asset_grid/asset_grid_data_structure.dart
+        - test/**.dart
+        # refactor the remaining providers
+        - lib/providers/{archive,asset,authentication,db,favorite,partner,trash,user}.provider.dart
+        - lib/providers/{album/album,album/shared_album,asset_viewer/asset_stack,asset_viewer/render_list,backup/backup,search/all_motion_photos,search/recently_added_asset}.provider.dart
+
+    - import_rule_openapi:
+      message: openapi must only be used through ApiRepositories
+      restrict: package:openapi
+      allowed:
+        # requried / wanted
+        - lib/repositories/*_api.repository.dart
+        # acceptable exceptions for the time being
+        - lib/entities/{album,asset,exif_info,user}.entity.dart # to convert DTOs to entities
+        - lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine
+        - test/modules/utils/openapi_patching_test.dart # filename is self-explanatory...
+        # refactor
+        - lib/models/map/map_marker.model.dart
+        - lib/models/server_info/server_{config,disk_info,features,version}.model.dart
+        - lib/models/shared_link/shared_link.model.dart
+        - lib/providers/asset_viewer/asset_people.provider.dart
+        - lib/providers/auth.provider.dart
+        - lib/providers/image/immich_remote_{image,thumbnail}_provider.dart
+        - lib/providers/map/map_state.provider.dart
+        - lib/providers/search/{search,search_filter}.provider.dart
+        - lib/providers/websocket.provider.dart
+        - lib/routing/auth_guard.dart
+        - lib/services/{api,asset,backup,memory,oauth,search,shared_link,stack,trash}.service.dart
+        - lib/widgets/album/album_thumbnail_listtile.dart
+        - lib/widgets/forms/login/login_form.dart
+        - lib/widgets/search/search_filter/{camera_picker,location_picker,people_picker}.dart
+        - lib/services/auth.service.dart # on ApiException
+        - test/services/auth.service_test.dart # on ApiException
 
 dart_code_metrics:
   metrics:
diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle
index 52750232cc..0ec511d9f1 100644
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -2,7 +2,7 @@ plugins {
     id "com.android.application"
     id "kotlin-android"
     id "dev.flutter.flutter-gradle-plugin"
-    id "kotlin-kapt"
+    id 'com.google.devtools.ksp'
 }
 
 def localProperties = new Properties()
@@ -28,15 +28,16 @@ if (keystorePropertiesFile.exists()) {
 }
 
 android {
-    compileSdkVersion 34
+    compileSdkVersion 35
 
     compileOptions {
-        sourceCompatibility JavaVersion.VERSION_1_8
-        targetCompatibility JavaVersion.VERSION_1_8
+        sourceCompatibility JavaVersion.VERSION_17
+        targetCompatibility JavaVersion.VERSION_17
+        coreLibraryDesugaringEnabled true
     }
 
     kotlinOptions {
-        jvmTarget = '1.8'
+        jvmTarget = '17'
     }
 
     sourceSets {
@@ -46,7 +47,7 @@ android {
     defaultConfig {
         applicationId "app.alextran.immich"
         minSdkVersion 26
-        targetSdkVersion 34
+        targetSdkVersion 35
         versionCode flutterVersionCode.toInteger()
         versionName flutterVersionName
     }
@@ -74,6 +75,7 @@ android {
             signingConfig signingConfigs.release
         }
     }
+    namespace 'app.alextran.immich'
 }
 
 flutter {
@@ -81,11 +83,11 @@ flutter {
 }
 
 dependencies {
-    def kotlin_version = '1.9.24'
-    def kotlin_coroutines_version = '1.8.1'
-    def work_version = '2.9.0'
-    def concurrent_version = '1.1.0'
-    def guava_version = '33.2.0-android'
+    def kotlin_version = '2.0.20'
+    def kotlin_coroutines_version = '1.9.0'
+    def work_version = '2.9.1'
+    def concurrent_version = '1.2.0'
+    def guava_version = '33.3.1-android'
     def glide_version = '4.16.0'
 
     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
@@ -94,7 +96,8 @@ dependencies {
     implementation "androidx.concurrent:concurrent-futures:$concurrent_version"
     implementation "com.google.guava:guava:$guava_version"
     implementation "com.github.bumptech.glide:glide:$glide_version"
-    kapt "com.github.bumptech.glide:compiler:$glide_version"
+    ksp "com.github.bumptech.glide:ksp:$glide_version"
+    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2'
 }
 
 // This is uncommented in F-Droid build script
diff --git a/mobile/android/app/proguard-rules.pro b/mobile/android/app/proguard-rules.pro
new file mode 100644
index 0000000000..ea6dd795b5
--- /dev/null
+++ b/mobile/android/app/proguard-rules.pro
@@ -0,0 +1,32 @@
+##---------------Begin: proguard configuration for Gson  ----------
+# Gson uses generic type information stored in a class file when working with fields. Proguard
+# removes such information by default, so configure it to keep all of it.
+-keepattributes Signature
+
+# For using GSON @Expose annotation
+-keepattributes *Annotation*
+
+# Gson specific classes
+-dontwarn sun.misc.**
+#-keep class com.google.gson.stream.** { *; }
+
+# Application classes that will be serialized/deserialized over Gson
+-keep class com.google.gson.examples.android.model.** { <fields>; }
+
+# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
+# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
+-keep class * extends com.google.gson.TypeAdapter
+-keep class * implements com.google.gson.TypeAdapterFactory
+-keep class * implements com.google.gson.JsonSerializer
+-keep class * implements com.google.gson.JsonDeserializer
+
+# Prevent R8 from leaving Data object members always null
+-keepclassmembers,allowobfuscation class * {
+  @com.google.gson.annotations.SerializedName <fields>;
+}
+
+# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
+-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
+-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
+
+##---------------End: proguard configuration for Gson  ----------
\ No newline at end of file
diff --git a/mobile/android/app/src/debug/AndroidManifest.xml b/mobile/android/app/src/debug/AndroidManifest.xml
index e33c470b4d..ac7c0c7e53 100644
--- a/mobile/android/app/src/debug/AndroidManifest.xml
+++ b/mobile/android/app/src/debug/AndroidManifest.xml
@@ -1,6 +1,6 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
   <!-- Flutter needs it to communicate with the running application
          to allow setting breakpoints, to provide hot reload, etc.
     -->
   <uses-permission android:name="android.permission.INTERNET" />
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml
index 17c2830b48..e49cf5b8da 100644
--- a/mobile/android/app/src/main/AndroidManifest.xml
+++ b/mobile/android/app/src/main/AndroidManifest.xml
@@ -1,4 +1,4 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich"
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools">
 
   <uses-permission android:name="android.permission.INTERNET" />
@@ -16,6 +16,8 @@
   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
   <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
   <uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
+  <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+  <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
 
   <!-- Foreground service permission -->
   <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
diff --git a/mobile/android/app/src/profile/AndroidManifest.xml b/mobile/android/app/src/profile/AndroidManifest.xml
index e33c470b4d..ac7c0c7e53 100644
--- a/mobile/android/app/src/profile/AndroidManifest.xml
+++ b/mobile/android/app/src/profile/AndroidManifest.xml
@@ -1,6 +1,6 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
   <!-- Flutter needs it to communicate with the running application
          to allow setting breakpoints, to provide hot reload, etc.
     -->
   <uses-permission android:name="android.permission.INTERNET" />
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/mobile/android/build.gradle b/mobile/android/build.gradle
index 87cc79281d..bcf3daa1c8 100644
--- a/mobile/android/build.gradle
+++ b/mobile/android/build.gradle
@@ -1,5 +1,5 @@
 allprojects {
-    ext.kotlin_version = '1.9.24' 
+    ext.kotlin_version = '2.0.20' 
 
     repositories {
         google()
@@ -16,8 +16,8 @@ subprojects {
         if (project.plugins.hasPlugin("com.android.application") ||
                 project.plugins.hasPlugin("com.android.library")) {
             project.android {
-                compileSdkVersion 34
-                buildToolsVersion "34.0.0"
+                compileSdkVersion 35
+                buildToolsVersion "35.0.0"
             }
         }
     }
diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile
index 18243f5502..45212a76a8 100644
--- a/mobile/android/fastlane/Fastfile
+++ b/mobile/android/fastlane/Fastfile
@@ -35,8 +35,8 @@ platform :android do
       task: 'bundle', 
       build_type: 'Release',
       properties: {
-        "android.injected.version.code" => 159,
-        "android.injected.version.name" => "1.115.0",
+        "android.injected.version.code" => 172,
+        "android.injected.version.name" => "1.123.0",
       }
     )
     upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
diff --git a/mobile/android/gradle.properties b/mobile/android/gradle.properties
index 4d3226abc2..78c37cc2a3 100644
--- a/mobile/android/gradle.properties
+++ b/mobile/android/gradle.properties
@@ -1,3 +1,5 @@
-org.gradle.jvmargs=-Xmx1536M
+org.gradle.jvmargs=-Xmx4096M
 android.useAndroidX=true
-android.enableJetifier=true
\ No newline at end of file
+android.enableJetifier=true
+android.nonTransitiveRClass=false
+android.nonFinalResIds=false
\ No newline at end of file
diff --git a/mobile/android/gradle/wrapper/gradle-wrapper.properties b/mobile/android/gradle/wrapper/gradle-wrapper.properties
index 6357330c9e..dedd5d1e69 100644
--- a/mobile/android/gradle/wrapper/gradle-wrapper.properties
+++ b/mobile/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,7 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip
+networkTimeout=10000
 validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip
-distributionSha256Sum=fe696c020f241a5f69c30f763c5a7f38eec54b490db19cd2b0962dda420d7d12
\ No newline at end of file
diff --git a/mobile/android/settings.gradle b/mobile/android/settings.gradle
index e809a0abaa..74f8904a10 100644
--- a/mobile/android/settings.gradle
+++ b/mobile/android/settings.gradle
@@ -18,9 +18,9 @@ pluginManagement {
 
 plugins {
     id "dev.flutter.flutter-plugin-loader" version "1.0.0"
-    id "com.android.application" version "7.4.2" apply false
-    id "org.jetbrains.kotlin.android" version "1.9.0" apply false
-    id "org.jetbrains.kotlin.kapt" version "1.9.0" apply false
+    id "com.android.application" version '8.7.2' apply false
+    id "org.jetbrains.kotlin.android" version "2.0.20" apply false
+    id 'com.google.devtools.ksp' version '2.0.20-1.0.24' apply false
 }
 
 include ":app"
diff --git a/mobile/assets/i18n/ar-JO.json b/mobile/assets/i18n/ar-JO.json
index fdc54da2b7..ec65d9ac9e 100644
--- a/mobile/assets/i18n/ar-JO.json
+++ b/mobile/assets/i18n/ar-JO.json
@@ -6,6 +6,8 @@
   "action_common_save": "Save",
   "action_common_select": "Select",
   "action_common_update": "تحديث",
+  "add_a_name": "Add a name",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "تمت الاضافة{album}",
   "add_to_album_bottom_sheet_already_exists": "موجودة مسبقا {album}",
   "advanced_settings_log_level_title": "Log level: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "استكشاف الأخطاء وإصلاحها",
   "album_info_card_backup_album_excluded": "مستبعد",
   "album_info_card_backup_album_included": "متضمنة",
+  "albums": "Albums",
   "album_thumbnail_card_item": "عنصر واحد",
   "album_thumbnail_card_items": "{} items",
   "album_thumbnail_card_shared": " · . مشترك",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "إزالة من الألبوم",
   "album_viewer_appbar_share_to": "حصة ل",
   "album_viewer_page_share_add_users": "اضافة مستخدمين",
+  "all": "All",
   "all_people_page_title": "الناس",
   "all_videos_page_title": "أشرطة فيديو",
   "app_bar_signout_dialog_content": "هل أنت متأكد أنك تريد الخروج",
   "app_bar_signout_dialog_ok": "نعم",
   "app_bar_signout_dialog_title": "خروج",
+  "archived": "Archived",
   "archive_page_no_archived_assets": "لم يتم العثور على الأصول المؤرشفة",
   "archive_page_title": "Archive ({})",
   "asset_action_delete_err_read_only": "لا يمكن حذف الأصول ذات للقراءة فقط، وسوف يتم التخطي",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} asset(s) restored successfully",
   "assets_trashed": "{} asset(s) trashed",
   "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "عارض الأصول",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Albums on device ({})",
   "backup_album_selection_page_albums_tap": "انقر للتضمين، وانقر نقرًا مزدوجًا للاستثناء",
   "backup_album_selection_page_assets_scatter": "يمكن أن تنتشر الأصول عبر ألبومات متعددة. وبالتالي، يمكن تضمين الألبومات أو استبعادها أثناء عملية النسخ الاحتياطي.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "نجاح",
   "backup_manual_title": "حالة التحميل",
   "backup_options_page_title": "خيارات النسخ الاحتياطي",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
   "cache_settings_clear_cache_button": "مسح ذاكرة التخزين المؤقت",
   "cache_settings_clear_cache_button_title": "يقوم بمسح ذاكرة التخزين المؤقت للتطبيق.سيؤثر هذا بشكل كبير على أداء التطبيق حتى إعادة بناء ذاكرة التخزين المؤقت.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "التحكم في سلوك التخزين المحلي",
   "cache_settings_tile_title": "التخزين المحلي",
   "cache_settings_title": "إعدادات التخزين المؤقت",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "تأكيد كلمة المرور",
   "change_password_form_description": "مرحبًا ،هذه هي المرة الأولى التي تقوم فيها بالتسجيل في النظام أو تم تقديم طلب لتغيير كلمة المرور الخاصة بك.الرجاء إدخال كلمة المرور الجديدة أدناه",
   "change_password_form_new_password": "كلمة المرور الجديدة",
   "change_password_form_password_mismatch": "كلمة المرور غير مطابقة",
   "change_password_form_reenter_new_password": "أعد إدخال كلمة مرور جديدة",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Enter Password",
   "client_cert_import": "Import",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "غير محفوظ في الارشيف",
   "control_bottom_app_bar_unfavorite": "غير مفضل",
   "control_bottom_app_bar_upload": "رفع وتحميل",
+  "create_album": "Create album",
   "create_album_page_untitled": "بدون اسم",
+  "create_new": "CREATE NEW",
   "create_shared_album_page_create": "انشاء",
   "create_shared_album_page_share": "يشارك",
   "create_shared_album_page_share_add_assets": "إضافة الأصول",
@@ -193,6 +211,7 @@
   "crop": "Crop",
   "curated_location_page_title": "أماكن",
   "curated_object_page_title": "أشياء",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E ، MMM DD",
   "daily_title_text_date_year": "E ، MMM DD ، yyyy",
   "date_format": "E ، Lll D ، Y • H: MM A",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "حذف الرابط المشترك",
   "description_input_hint_text": "اضف وصفا...",
   "description_input_submit_error": "خطأ تحديث الوصف ، تحقق من السجل لمزيد من التفاصيل",
+  "download_canceled": "Download canceled",
+  "download_complete": "Download complete",
+  "download_enqueue": "Download enqueued",
   "download_error": "Download Error",
+  "download_failed": "Download failed",
+  "download_filename": "file: {}",
+  "download_finished": "Download finished",
+  "downloading": "Downloading...",
+  "downloading_media": "Downloading media",
+  "download_notfound": "Download not found",
+  "download_paused": "Download paused",
   "download_started": "Download started",
   "download_sucess": "Download success",
   "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_waiting_to_retry": "Waiting to retry",
   "edit_date_time_dialog_date_time": "التاريخ و الوقت",
   "edit_date_time_dialog_timezone": "وحدة زمنية",
   "edit_image_title": "Edit",
   "edit_location_dialog_title": "موقع",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Error: {}",
   "exif_bottom_sheet_description": "اضف وصفا...",
   "exif_bottom_sheet_details": "تفاصيل",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "تمكين شبكة الصور التجريبية",
   "experimental_settings_subtitle": "استخدام على مسؤوليتك الخاصة!",
   "experimental_settings_title": "تجريبي",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favorites",
   "favorites_page_no_favorites": "لم يتم العثور على الأصول المفضلة",
   "favorites_page_title": "المفضلة",
   "filename_search": "File name or extension",
+  "filter": "Filter",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "تمكين ردود الفعل اللمسية",
   "haptic_feedback_title": "ردود فعل لمسية",
   "header_settings_add_header_tip": "Add Header",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "إذا كانت هذه هي المرة الأولى التي تستخدم فيها التطبيق، فيرجى التأكد من اختيار ألبوم (ألبومات) احتياطية حتى يتمكن المخطط الزمني من ملء الصور ومقاطع الفيديو في الألبوم (الألبومات).",
   "home_page_share_err_local": "لا يمكن مشاركة الأصول المحلية عبر الرابط ، سوف يتخطى",
   "home_page_upload_err_limit": "لا يمكن إلا تحميل 30 أحد الأصول في وقت واحد ، سوف يتخطى",
+  "ignore_icloud_photos": "Ignore iCloud photos",
+  "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
   "image_saved_successfully": "Image saved",
   "image_viewer_page_state_provider_download_error": "خطا في التحميل",
   "image_viewer_page_state_provider_download_started": "بدأ التنزيل",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "خطأ في المشاركة",
   "invalid_date": "Invalid date",
   "invalid_date_format": "Invalid date format",
+  "library": "Library",
   "library_page_albums": "ألبومات",
   "library_page_archive": "أرشيف",
   "library_page_device_albums": "ألبومات على الجهاز",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "أقدم صورة",
   "library_page_sort_most_recent_photo": "أحدث الصور",
   "library_page_sort_title": "عنوان الألبوم",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "اختر على الخريطة",
   "location_picker_latitude": "خط العرض",
   "location_picker_latitude_error": "أدخل خط عرض صالح",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "الصور المتحركة",
   "multiselect_grid_edit_date_time_err_read_only": "لا يمكن تعديل تاريخ الأصول (المواد) للقراءة فقط، سوف يتخطى",
   "multiselect_grid_edit_gps_err_read_only": "لا يمكن تعديل موقع الأصول (المواد) للقراءة فقط، سوف يتخطى",
+  "my_albums": "My albums",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "لا توجد أصول لعرضها",
   "no_name": "No name",
   "notification_permission_dialog_cancel": "يلغي",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "منح إذن لتمكين الإخطارات.",
   "notification_permission_list_tile_enable_button": "تمكين الإخطارات",
   "notification_permission_list_tile_title": "إذن الإخطار",
+  "on_this_device": "On this device",
   "partner_list_user_photos": "{user}'s photos",
   "partner_list_view_all": "عرض الكل",
   "partner_page_add_partner": "أضف شريكًا",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
   "partner_page_stop_sharing_title": "توقف عن مشاركة صورك؟",
   "partner_page_title": "شريك",
+  "partners": "Partners",
+  "people": "People",
   "permission_onboarding_back": "خلف",
   "permission_onboarding_continue_anyway": "تواصل على أي حال",
   "permission_onboarding_get_started": "البدء",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "تم تأمين التصريح! وضعك تمام.",
   "permission_onboarding_permission_limited": "إذن محدود. للسماح بالنسخ الاحتياطي للتطبيق وإدارة مجموعة المعرض بالكامل، امنح أذونات الصور والفيديو في الإعدادات.",
   "permission_onboarding_request": "يتطلب التطبيق إذنًا لعرض الصور ومقاطع الفيديو الخاصة بك",
+  "places": "Places",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "التفضيلات",
   "profile_drawer_app_logs": "السجلات",
   "profile_drawer_client_out_of_date_major": "تطبيق الهاتف المحمول قديم.يرجى التحديث إلى أحدث إصدار رئيسي.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "إعدادات",
   "profile_drawer_sign_out": "خروج",
   "profile_drawer_trash": "نفايات",
+  "recently_added": "Recently added",
   "recently_added_page_title": "أضيف مؤخرا",
+  "save": "Save",
   "save_to_gallery": "Save to gallery",
   "scaffold_body_error_occurred": "حدث خطأ",
+  "search_albums": "Search albums",
   "search_bar_hint": "ابحث عن صورك",
   "search_filter_apply": "اختار الفلتر ",
   "search_filter_camera": "Camera",
@@ -428,6 +484,7 @@
   "search_page_places": "أماكن",
   "search_page_recently_added": "أضيف مؤخرا",
   "search_page_screenshots": "لقطات الشاشة",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": " صور ذاتيه",
   "search_page_things": "أشياء",
   "search_page_videos": "أشرطة فيديو",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "اقتراحات",
   "select_user_for_sharing_page_err_album": "فشل في إنشاء ألبوم",
   "select_user_for_sharing_page_share_suggestions": "اقتراحات",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "نسخة التطبيق",
   "server_info_box_latest_release": "احدث اصدار",
   "server_info_box_server_url": "عنوان URL الخادم",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "تحميل صورة معاينة",
   "setting_image_viewer_title": "الصور",
   "setting_languages_apply": "تغيير الإعدادات",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "اللغات",
   "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
   "setting_notifications_notify_hours": "{} hours",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "رفع",
   "shared_link_manage_links": "إدارة الروابط المشتركة",
   "shared_link_public_album": "الألبوم العام",
+  "shared_links": "Shared links",
   "share_done": "منتهي",
+  "shared_with_me": "Shared with me",
   "share_invite": "دعوة إلى الألبوم",
   "sharing_page_album": "ألبومات مشتركة",
   "sharing_page_description": "قم بإنشاء ألبومات مشتركة لمشاركة الصور ومقاطع الفيديو مع أشخاص في شبكتك.",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "قد يزيد التحميل من ثلاث مراحل من أداء التحميل ولكنه يسبب تحميل شبكة أعلى بكثير",
   "theme_setting_three_stage_loading_title": "تمكين تحميل ثلاث مراحل",
   "translated_text_options": "خيارات",
+  "trash": "Trash",
   "trash_emptied": "Emptied trash",
   "trash_page_delete": "مسح",
   "trash_page_delete_all": "حذف الكل",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "هل تريد النسخ الاحتياطي للأصول (الأصول) المحددة إلى الخادم؟",
   "upload_dialog_ok": "رفع",
   "upload_dialog_title": "تحميل الأصول",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "يُقرّ",
   "version_announcement_overlay_release_notes": "ملاحظات الإصدار",
   "version_announcement_overlay_text_1": "مرحبًا يا صديقي ، هناك إصدار جديد",
   "version_announcement_overlay_text_2": "من فضلك خذ وقتك لزيارة",
   "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
   "version_announcement_overlay_title": "نسخه جديده متاحه للخادم ",
+  "videos": "Videos",
   "viewer_remove_from_stack": "حذف من الكومه أو المجموعة",
   "viewer_stack_use_as_main_asset": "استخدم كأصل رئيسي",
-  "viewer_unstack": "فك الكومه"
+  "viewer_unstack": "فك الكومه",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/cs-CZ.json b/mobile/assets/i18n/cs-CZ.json
index 4a81de7596..1c2eda1de5 100644
--- a/mobile/assets/i18n/cs-CZ.json
+++ b/mobile/assets/i18n/cs-CZ.json
@@ -6,6 +6,8 @@
   "action_common_save": "Uložit",
   "action_common_select": "Vybrat",
   "action_common_update": "Aktualizovat",
+  "add_a_name": "Přidat název",
+  "add_endpoint": "Přidat koncový bod",
   "add_to_album_bottom_sheet_added": "Přidáno do {album}",
   "add_to_album_bottom_sheet_already_exists": "Je již v {album}",
   "advanced_settings_log_level_title": "Úroveň protokolování: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Řešení problémů",
   "album_info_card_backup_album_excluded": "VYLOUČENO",
   "album_info_card_backup_album_included": "ZAHRNUTO",
+  "albums": "Alba",
   "album_thumbnail_card_item": "1 položka",
   "album_thumbnail_card_items": "{} položek",
   "album_thumbnail_card_shared": " · Sdíleno",
@@ -36,13 +39,15 @@
   "album_viewer_appbar_share_remove": "Odstranit z alba",
   "album_viewer_appbar_share_to": "Sdílet na",
   "album_viewer_page_share_add_users": "Přidat uživatele",
+  "all": "Vše",
   "all_people_page_title": "Lidé",
   "all_videos_page_title": "Videa",
   "app_bar_signout_dialog_content": "Určitě se chcete odhlásit?",
   "app_bar_signout_dialog_ok": "Ano",
   "app_bar_signout_dialog_title": "Odhlásit se",
+  "archived": "Archiv",
   "archive_page_no_archived_assets": "Nebyla nalezena žádná archivovaná média",
-  "archive_page_title": "Archív ({})",
+  "archive_page_title": "Archiv ({})",
   "asset_action_delete_err_read_only": "Nelze odstranit položky pouze pro čtení, přeskakuji",
   "asset_action_share_err_offline": "Nelze načíst offline položky, přeskakuji",
   "asset_list_group_by_sub_title": "Seskupit podle",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} položek úspěšně obnoveno",
   "assets_trashed": "{} položek vyhozeno do koše",
   "assets_trashed_from_server": "{} položek vyhozeno do koše na Immich serveru",
+  "asset_viewer_settings_subtitle": "Správa nastavení prohlížeče galerie",
   "asset_viewer_settings_title": "Prohlížeč",
+  "automatic_endpoint_switching_subtitle": "Připojit se místně přes určenou Wi-Fi, pokud je k dispozici, a používat alternativní připojení jinde",
+  "automatic_endpoint_switching_title": "Automatické přepínání URL",
+  "background_location_permission": "Povolení polohy na pozadí",
+  "background_location_permission_content": "Aby bylo možné přepínat sítě při běhu na pozadí, musí mít Immich *vždy* přístup k přesné poloze, aby mohl zjistit název Wi-Fi sítě",
   "backup_album_selection_page_albums_device": "Alba v zařízení ({})",
   "backup_album_selection_page_albums_tap": "Klepnutím na položku ji zahrnete, opětovným klepnutím ji vyloučíte",
   "backup_album_selection_page_assets_scatter": "Položky mohou být roztroušeny ve více albech. To umožňuje zahrnout nebo vyloučit alba během procesu zálohování.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Úspěch",
   "backup_manual_title": "Stav nahrávání",
   "backup_options_page_title": "Nastavení záloh",
+  "backup_setting_subtitle": "Správa nastavení zálohování na pozadí a na popředí",
   "cache_settings_album_thumbnails": "Náhledy stránek knihovny (položek {})",
   "cache_settings_clear_cache_button": "Vymazat vyrovnávací paměť",
   "cache_settings_clear_cache_button_title": "Vymaže vyrovnávací paměť aplikace. To výrazně ovlivní výkon aplikace, dokud se vyrovnávací paměť neobnoví.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Ovládání chování místního úložiště",
   "cache_settings_tile_title": "Místní úložiště",
   "cache_settings_title": "Nastavení vyrovnávací paměti",
+  "cancel": "Zrušit",
+  "change_display_order": "Změnit pořadí zobrazení",
   "change_password_form_confirm_password": "Potvrďte heslo",
   "change_password_form_description": "Dobrý den, {name}\n\nje to buď poprvé, co se přihlašujete do systému, nebo byl vytvořen požadavek na změnu hesla. Níže zadejte nové heslo.",
   "change_password_form_new_password": "Nové heslo",
   "change_password_form_password_mismatch": "Hesla se neshodují",
   "change_password_form_reenter_new_password": "Znovu zadejte nové heslo",
+  "check_corrupt_asset_backup": "Kontrola poškozených záloh položek",
+  "check_corrupt_asset_backup_button": "Provést kontrolu",
+  "check_corrupt_asset_backup_description": "Tuto kontrolu provádějte pouze přes Wi-Fi a po zálohování všech prostředků. Takto operace může trvat několik minut.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Zadejte heslo",
   "client_cert_import": "Importovat",
@@ -168,7 +184,7 @@
   "control_bottom_app_bar_add_to_album": "Přidat do alba",
   "control_bottom_app_bar_album_info": "{} položek",
   "control_bottom_app_bar_album_info_shared": "{} položky – sdílené",
-  "control_bottom_app_bar_archive": "Archív",
+  "control_bottom_app_bar_archive": "Archivovat",
   "control_bottom_app_bar_create_new_album": "Vytvořit nové album",
   "control_bottom_app_bar_delete": "Smazat",
   "control_bottom_app_bar_delete_from_immich": "Smazat ze serveru Immich",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Odarchivovat",
   "control_bottom_app_bar_unfavorite": "Zrušit oblíbení",
   "control_bottom_app_bar_upload": "Nahrát",
+  "create_album": "Vytvořit album",
   "create_album_page_untitled": "Bez názvu",
+  "create_new": "VYTVOŘIT NOVÉ",
   "create_shared_album_page_create": "Vytvořit",
   "create_shared_album_page_share": "Sdílet",
   "create_shared_album_page_share_add_assets": "PŘIDAT POLOŽKY",
@@ -193,6 +211,7 @@
   "crop": "Oříznout",
   "curated_location_page_title": "Místa",
   "curated_object_page_title": "Věci",
+  "current_server_address": "Aktuální adresa serveru",
   "daily_title_text_date": "EEEE, d. MMMM",
   "daily_title_text_date_year": "EEEE, d. MMMM y",
   "date_format": "EEEE, d. MMMM y • H:mm",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "Odstranit sdílený odkaz",
   "description_input_hint_text": "Přidat popis...",
   "description_input_submit_error": "Chyba aktualizace popisu, další podrobnosti najdete v logu",
-  "download_error": "Download Error",
-  "download_started": "Download started",
-  "download_sucess": "Download success",
-  "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_canceled": "Stahování zrušeno",
+  "download_complete": "Stahování kompletní",
+  "download_enqueue": "Stahování ve frontě",
+  "download_error": "Chyba při stahování",
+  "download_failed": "Stahování selhalo",
+  "download_filename": "soubor: {}",
+  "download_finished": "Stahování dokončeno",
+  "downloading": "Stahování...",
+  "downloading_media": "Stahování média",
+  "download_notfound": "Stahování nebylo nalezeno",
+  "download_paused": "Stahování pozastaveno",
+  "download_started": "Stahování zahájeno",
+  "download_sucess": "Stažení úspěšné",
+  "download_sucess_android": "Média byla stažena do DCIM/Immich",
+  "download_waiting_to_retry": "Čekání na opakovaný pokus",
   "edit_date_time_dialog_date_time": "Datum a čas",
   "edit_date_time_dialog_timezone": "Časové pásmo",
   "edit_image_title": "Upravit",
   "edit_location_dialog_title": "Poloha",
+  "enter_wifi_name": "Zadejte název WiFi",
+  "error_change_sort_album": "Nepodařilo se změnit pořadí alba",
   "error_saving_image": "Chyba: {}",
   "exif_bottom_sheet_description": "Přidat popis...",
   "exif_bottom_sheet_details": "PODROBNOSTI",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Povolení experimentální mřížky fotografií",
   "experimental_settings_subtitle": "Používejte na vlastní riziko!",
   "experimental_settings_title": "Experimentální",
+  "external_network": "Externí síť",
+  "external_network_sheet_info": "Pokud nejste v preferované síti WiFi, aplikace se připojí k serveru prostřednictvím první z níže uvedených adres URL, které může dosáhnout, počínaje shora dolů",
+  "favorites": "Oblíbené",
   "favorites_page_no_favorites": "Nebyla nalezena žádná oblíbená média",
   "favorites_page_title": "Oblíbené",
   "filename_search": "Název nebo přípona souboru",
+  "filter": "Filtr",
+  "get_wifiname_error": "Nepodařilo se získat název Wi-Fi. Zkontrolujte, zda jste udělili potřebná oprávnění a zda jste připojeni k Wi-Fi síti",
+  "grant_permission": "Udělit oprávnění",
   "haptic_feedback_switch": "Povolit dotykovou zpětnou vazbu",
   "haptic_feedback_title": "Dotyková zpětná vazba",
   "header_settings_add_header_tip": "Přidat hlavičku",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "Pokud aplikaci používáte poprvé, nezapomeňte si vybrat zálohovaná alba, aby se na časové ose mohly nacházet fotografie a videa z vybraných alb.",
   "home_page_share_err_local": "Nelze sdílet místní položky prostřednictvím odkazu, přeskakuji",
   "home_page_upload_err_limit": "Lze nahrát nejvýše 30 položek najednou, přeskakuji",
+  "ignore_icloud_photos": "Ignorovat fotografie na iCloudu",
+  "ignore_icloud_photos_description": "Fotografie uložené na iCloudu se nebudou nahrávat na Immich server",
   "image_saved_successfully": "Obrázek uložen",
   "image_viewer_page_state_provider_download_error": "Chyba stahování",
   "image_viewer_page_state_provider_download_started": "Stahování zahájeno",
@@ -262,8 +302,9 @@
   "image_viewer_page_state_provider_share_error": "Chyba sdílení",
   "invalid_date": "Chybné datum",
   "invalid_date_format": "Chybný formát data",
+  "library": "Knihovna",
   "library_page_albums": "Alba",
-  "library_page_archive": "Archív",
+  "library_page_archive": "Archivovat",
   "library_page_device_albums": "Alba v zařízení",
   "library_page_favorites": "Oblíbené",
   "library_page_new_album": "Nové album",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Nejstarší fotografie",
   "library_page_sort_most_recent_photo": "Nejnovější fotografie",
   "library_page_sort_title": "Podle názvu alba",
+  "local_network": "Místní síť",
+  "local_network_sheet_info": "Aplikace se při použití zadané sítě Wi-Fi připojí k serveru prostřednictvím tohoto URL",
+  "location_permission": "Oprávnění polohy",
+  "location_permission_content": "Aby bylo možné používat funkci automatického přepínání, potřebuje Immich oprávnění k přesné poloze, aby mohl přečíst název aktuální WiFi sítě",
   "location_picker_choose_on_map": "Vyberte na mapě",
   "location_picker_latitude": "Zeměpisná šířka",
   "location_picker_latitude_error": "Zadejte platnou zeměpisnou šířku",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Pohyblivé fotky",
   "multiselect_grid_edit_date_time_err_read_only": "Nelze upravit datum položek pouze pro čtení, přeskakuji",
   "multiselect_grid_edit_gps_err_read_only": "Nelze upravit polohu položek pouze pro čtení, přeskakuji",
+  "my_albums": "Moje alba",
+  "networking_settings": "Síť",
+  "networking_subtitle": "Správa nastavení koncového bodu serveru",
   "no_assets_to_show": "Žádné položky k zobrazení",
   "no_name": "Bez jména",
   "notification_permission_dialog_cancel": "Zrušit",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Udělte oprávnění k aktivaci oznámení.",
   "notification_permission_list_tile_enable_button": "Povolit oznámení",
   "notification_permission_list_tile_title": "Povolení oznámení",
+  "on_this_device": "V tomto zařízení",
   "partner_list_user_photos": "Fotografie uživatele {user}",
   "partner_list_view_all": "Zobrazit všechny",
   "partner_page_add_partner": "Přidat partnera",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} již nebude mít přístup k vašim fotografiím.",
   "partner_page_stop_sharing_title": "Přestat sdílet vaše fotografie?",
   "partner_page_title": "Partner",
+  "partners": "Partneři",
+  "people": "Lidé",
   "permission_onboarding_back": "Zpět",
   "permission_onboarding_continue_anyway": "Přesto pokračovat",
   "permission_onboarding_get_started": "Začít",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Přístup povolen! Vše je připraveno.",
   "permission_onboarding_permission_limited": "Přístup omezen. Chcete-li používat Immich k zálohování a správě celé vaší kolekce galerií, povolte v nastavení přístup k fotkám a videím.",
   "permission_onboarding_request": "Immich potřebuje přístup k zobrazení vašich fotek a videí.",
+  "places": "Místa",
+  "preferences_settings_subtitle": "Správa předvoleb aplikace",
   "preferences_settings_title": "Předvolby",
   "profile_drawer_app_logs": "Logy",
   "profile_drawer_client_out_of_date_major": "Mobilní aplikace je zastaralá. Aktualizujte ji na nejnovější hlavní verzi.",
@@ -383,10 +436,13 @@
   "profile_drawer_settings": "Nastavení",
   "profile_drawer_sign_out": "Odhlásit se",
   "profile_drawer_trash": "Vyhodit",
+  "recently_added": "Nedávno přidané",
   "recently_added_page_title": "Nedávno přidané",
+  "save": "Uložit",
   "save_to_gallery": "Uložit do galerie",
   "scaffold_body_error_occurred": "Došlo k chybě",
-  "search_bar_hint": "Prohledejte své fotky",
+  "search_albums": "Vyhledávejte alba",
+  "search_bar_hint": "Vyhledávejte svoje fotky",
   "search_filter_apply": "Použít filtr",
   "search_filter_camera": "Fotoaparát",
   "search_filter_camera_make": "Výrobce",
@@ -428,6 +484,7 @@
   "search_page_places": "Místa",
   "search_page_recently_added": "Nedávno přidané",
   "search_page_screenshots": "Snímky obrazovky",
+  "search_page_search_photos_videos": "Vyhledávejte svoje fotky a videa",
   "search_page_selfies": "Autoportréty",
   "search_page_things": "Věci",
   "search_page_videos": "Videa",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Návrhy",
   "select_user_for_sharing_page_err_album": "Nepodařilo se vytvořit album",
   "select_user_for_sharing_page_share_suggestions": "Návrhy",
+  "server_endpoint": "Koncový bod serveru",
   "server_info_box_app_version": "Verze aplikace",
   "server_info_box_latest_release": "Nejnovější verze",
   "server_info_box_server_url": "URL serveru",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Načíst náhled obrázku",
   "setting_image_viewer_title": "Obrázky",
   "setting_languages_apply": "Použít",
+  "setting_languages_subtitle": "Změna jazyka aplikace",
   "setting_languages_title": "Jazyk",
   "setting_notifications_notify_failures_grace_period": "Oznámení o selhání zálohování na pozadí: {}",
   "setting_notifications_notify_hours": "{} hodin",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Nahrát",
   "shared_link_manage_links": "Spravovat sdílené odkazy",
   "shared_link_public_album": "Veřejné album",
+  "shared_links": "Sdílené odkazy",
   "share_done": "Hotovo",
+  "shared_with_me": "Sdílené se mnou",
   "share_invite": "Pozvat do alba",
   "sharing_page_album": "Sdílená alba",
   "sharing_page_description": "Vytvářejte sdílená alba a sdílejte fotografie a videa s lidmi ve vaší síti.",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Třístupňové načítání může zvýšit výkonnost načítání, ale vede k výrazně vyššímu zatížení sítě.",
   "theme_setting_three_stage_loading_title": "Povolení třístupňového načítání",
   "translated_text_options": "Možnosti",
+  "trash": "Koš",
   "trash_emptied": "Koš vyprázdněn",
   "trash_page_delete": "Smazat",
   "trash_page_delete_all": "Smazat všechny",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Chcete zálohovat vybrané položky na server?",
   "upload_dialog_ok": "Nahrát",
   "upload_dialog_title": "Nahrát položku",
+  "use_current_connection": "použít aktuální připojení",
+  "validate_endpoint_error": "Zadejte platné URL",
   "version_announcement_overlay_ack": "Potvrdit",
   "version_announcement_overlay_release_notes": "poznámky k vydání",
   "version_announcement_overlay_text_1": "Ahoj, k dispozici je nová verze",
   "version_announcement_overlay_text_2": "najděte si čas na návštěvu ",
   "version_announcement_overlay_text_3": " a ujistěte se, že vaše konfigurace docker-compose a .env je aktuální, abyste předešli nesprávné konfiguraci, zvláště pokud používáte WatchTower nebo jakýkoli mechanismus, který podporuje automatické aktualizace serverových aplikací.",
   "version_announcement_overlay_title": "K dispozici je nová verze serveru \uD83C\uDF89",
+  "videos": "Videa",
   "viewer_remove_from_stack": "Odstranit ze zásobníku",
   "viewer_stack_use_as_main_asset": "Použít jako hlavní položku",
-  "viewer_unstack": "Rozbalit zásobník"
+  "viewer_unstack": "Rozbalit zásobník",
+  "wifi_name": "Název WiFi",
+  "your_wifi_name": "Váš název WiFi"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/da-DK.json b/mobile/assets/i18n/da-DK.json
index 20c3c43b09..ce2f284e51 100644
--- a/mobile/assets/i18n/da-DK.json
+++ b/mobile/assets/i18n/da-DK.json
@@ -6,6 +6,8 @@
   "action_common_save": "Save",
   "action_common_select": "Select",
   "action_common_update": "Opdater",
+  "add_a_name": "Tilføj navn",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Tilføjet til {album}",
   "add_to_album_bottom_sheet_already_exists": "Allerede i {album}",
   "advanced_settings_log_level_title": "Logniveau: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Fejlsøgning",
   "album_info_card_backup_album_excluded": "EKSKLUDERET",
   "album_info_card_backup_album_included": "INKLUDERET",
+  "albums": "Albummer",
   "album_thumbnail_card_item": "1 genstand",
   "album_thumbnail_card_items": "{} genstande",
   "album_thumbnail_card_shared": ". Delt",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Fjern fra album",
   "album_viewer_appbar_share_to": "Del til",
   "album_viewer_page_share_add_users": "Tilføj brugere",
+  "all": "Alt",
   "all_people_page_title": "Personer",
   "all_videos_page_title": "Videoer",
   "app_bar_signout_dialog_content": "Er du sikker på, du vil logge ud?",
   "app_bar_signout_dialog_ok": "Ja",
   "app_bar_signout_dialog_title": "Log ud",
+  "archived": "Arkiveret",
   "archive_page_no_archived_assets": "Ingen arkiverede elementer blev fundet",
   "archive_page_title": "Arkivér ({})",
   "asset_action_delete_err_read_only": "Kan ikke slette kun læselige elementer. Springer over",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} element(er) blev gendannet succesfuldt",
   "assets_trashed": "{} element(er) blev smidt i papirkurven",
   "assets_trashed_from_server": "{} element(er) blev smidt i serverens papirkurv",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "Billedviser",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Albummer på enhed ({})",
   "backup_album_selection_page_albums_tap": "Tryk en gang for at inkludere, tryk to gange for at ekskludere",
   "backup_album_selection_page_assets_scatter": "Elementer kan være spredt på tværs af flere albummer. Albummer kan således inkluderes eller udelukkes under sikkerhedskopieringsprocessen.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Succes",
   "backup_manual_title": "Uploadstatus",
   "backup_options_page_title": "Backupindstillinger",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Biblioteksminiaturebilleder ({} elementer)",
   "cache_settings_clear_cache_button": "Fjern cache",
   "cache_settings_clear_cache_button_title": "Fjern appens cache. Dette vil i stor grad påvirke appens ydeevne indtil cachen er genopbygget.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Kontroller den lokale lagerplads",
   "cache_settings_tile_title": "Lokal lagerplads",
   "cache_settings_title": "Cache-indstillinger",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Bekræft kodeord",
   "change_password_form_description": "Hej {name},\n\nDette er enten første gang du logger ind eller også er der lavet en anmodning om at ændre dit kodeord. Indtast venligst et nyt kodeord nedenfor.",
   "change_password_form_new_password": "Nyt kodeord",
   "change_password_form_password_mismatch": "Kodeord er ikke ens",
   "change_password_form_reenter_new_password": "Gentag nyt kodeord",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Enter Password",
   "client_cert_import": "Import",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Afakivér",
   "control_bottom_app_bar_unfavorite": "Fjern favorit",
   "control_bottom_app_bar_upload": "Upload",
+  "create_album": "Opret album",
   "create_album_page_untitled": "Uden titel",
+  "create_new": "OPRET NY",
   "create_shared_album_page_create": "Opret",
   "create_shared_album_page_share": "Del",
   "create_shared_album_page_share_add_assets": "TILFØJ ELEMENT",
@@ -193,6 +211,7 @@
   "crop": "Beskær",
   "curated_location_page_title": "Steder",
   "curated_object_page_title": "Ting",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, dd MMM",
   "daily_title_text_date_year": "E, dd MMM, yyyy",
   "date_format": "E d. LLL y • hh:mm",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "Slet delt link",
   "description_input_hint_text": "Tilføj en beskrivelse...",
   "description_input_submit_error": "Fejl med at opdatere beskrivelsen. Tjek loggen for flere detaljer",
-  "download_error": "Download Error",
-  "download_started": "Download started",
-  "download_sucess": "Download success",
-  "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_canceled": "Download annulleret",
+  "download_complete": "Download fuldført",
+  "download_enqueue": "Donload sat i kø",
+  "download_error": "Fejl med download",
+  "download_failed": "Download mislykkes",
+  "download_filename": "fil: {}",
+  "download_finished": "Download afsluttet",
+  "downloading": "Downloader...",
+  "downloading_media": "Download medier",
+  "download_notfound": "Download ikke fundet",
+  "download_paused": "Download pauset",
+  "download_started": "Download startet",
+  "download_sucess": "Download færdig",
+  "download_sucess_android": "Mediet er blevet downloadet til DCIM/Immich",
+  "download_waiting_to_retry": "Afventer at prøve igen",
   "edit_date_time_dialog_date_time": "Dato og klokkeslæt",
   "edit_date_time_dialog_timezone": "Tidszone",
   "edit_image_title": "Rediger",
   "edit_location_dialog_title": "Placering",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Fejl: {}",
   "exif_bottom_sheet_description": "Tilføj beskrivelse...",
   "exif_bottom_sheet_details": "DETALJER",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Aktiver eksperimentelt fotogitter",
   "experimental_settings_subtitle": "Brug på eget ansvar!",
   "experimental_settings_title": "Eksperimentelle",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favoritter",
   "favorites_page_no_favorites": "Ingen favoritter blev fundet",
   "favorites_page_title": "Favoritter",
   "filename_search": "File name or extension",
+  "filter": "Filter",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Slå haptisk feedback til",
   "haptic_feedback_title": "Haptisk feedback",
   "header_settings_add_header_tip": "Add Header",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "Hvis det er din første gang i appen, bedes du vælge en sikkerhedskopi af albummer så tidlinjen kan blive fyldt med billeder og videoer fra albummerne.",
   "home_page_share_err_local": "Kan ikke dele lokale elementer via link, springer over",
   "home_page_upload_err_limit": "Det er kun muligt at lave sikkerhedskopi af 30 elementer ad gangen. Springer over",
+  "ignore_icloud_photos": "Ignorer iCloud-billeder",
+  "ignore_icloud_photos_description": "Billeder der er gemt på iCloud vil ikke blive uploadet til Immich-serveren",
   "image_saved_successfully": "Billede gemt",
   "image_viewer_page_state_provider_download_error": "Fejl ved download",
   "image_viewer_page_state_provider_download_started": "Download startet",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "Delingsfejl",
   "invalid_date": "Invalid date",
   "invalid_date_format": "Invalid date format",
+  "library": "Bibliotek",
   "library_page_albums": "Albummer",
   "library_page_archive": "Arkiv",
   "library_page_device_albums": "Albummer på enhed",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Ældste billede",
   "library_page_sort_most_recent_photo": "Seneste billede",
   "library_page_sort_title": "Albumtitel",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Vælg på kort",
   "location_picker_latitude": "Breddegrad",
   "location_picker_latitude_error": "Indtast en gyldig breddegrad",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Bevægelsesbilleder",
   "multiselect_grid_edit_date_time_err_read_only": "Kan ikke redigere datoen på kun læselige elementer. Springer over",
   "multiselect_grid_edit_gps_err_read_only": "Kan ikke redigere lokation af kun læselige elementer. Springer over",
+  "my_albums": "Mine albummer",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "Ingen elementer at vise",
   "no_name": "Intet navn",
   "notification_permission_dialog_cancel": "Annuller",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Tillad at bruge notifikationer.",
   "notification_permission_list_tile_enable_button": "Slå notifikationer til",
   "notification_permission_list_tile_title": "Notifikationstilladelser",
+  "on_this_device": "På denne enhed",
   "partner_list_user_photos": "{user}s billeder",
   "partner_list_view_all": "Se alle",
   "partner_page_add_partner": "Tilføj partner",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} vil ikke længere have adgang til dine billeder.",
   "partner_page_stop_sharing_title": "Stop med at dele dine billeder?",
   "partner_page_title": "Partner",
+  "partners": "Partnere",
+  "people": "Personer",
   "permission_onboarding_back": "Tilbage",
   "permission_onboarding_continue_anyway": "Fortsæt alligevel",
   "permission_onboarding_get_started": "Kom i gang",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Tilladelse givet! Du er nu klar.",
   "permission_onboarding_permission_limited": "Tilladelse begrænset. For at lade Immich lave sikkerhedskopi og styre hele dit galleri, skal der gives tilladelse til billeder og videoer i indstillinger.",
   "permission_onboarding_request": "Immich kræver tilliadelse til at se dine billeder og videoer.",
+  "places": "Placeringer",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Præferencer",
   "profile_drawer_app_logs": "Log",
   "profile_drawer_client_out_of_date_major": "Mobilapp er forældet. Opdater venligst til den nyeste større version",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Indstillinger",
   "profile_drawer_sign_out": "Log ud",
   "profile_drawer_trash": "Papirkurv",
+  "recently_added": "Senest tilføjet",
   "recently_added_page_title": "Nyligt tilføjet",
+  "save": "Save",
   "save_to_gallery": "Gem til galleri",
   "scaffold_body_error_occurred": "Der opstod en fejl",
+  "search_albums": "Søg i albummer",
   "search_bar_hint": "Søg i dine billeder",
   "search_filter_apply": "Tilføj filter",
   "search_filter_camera": "Kamera",
@@ -428,6 +484,7 @@
   "search_page_places": "Steder",
   "search_page_recently_added": "Nyligt tilføjet",
   "search_page_screenshots": "Skærmbilleder",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Selfier",
   "search_page_things": "Ting",
   "search_page_videos": "Videoer",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Anbefalinger",
   "select_user_for_sharing_page_err_album": "Fejlede i at oprette et nyt album",
   "select_user_for_sharing_page_share_suggestions": "Anbefalinger",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "Applikationsversion",
   "server_info_box_latest_release": "Seneste version",
   "server_info_box_server_url": "Server URL",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Indlæs forhåndsvisning af billedet",
   "setting_image_viewer_title": "Images",
   "setting_languages_apply": "Anvend",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Sprog",
   "setting_notifications_notify_failures_grace_period": "Giv besked om fejl med sikkerhedskopiering i baggrunden: {}",
   "setting_notifications_notify_hours": "{} timer",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Upload",
   "shared_link_manage_links": "Håndter delte links",
   "shared_link_public_album": "Offentligt album",
+  "shared_links": "Delte links",
   "share_done": "Færdig",
+  "shared_with_me": "Delt med mig",
   "share_invite": "Inviter til album",
   "sharing_page_album": "Delt albums",
   "sharing_page_description": "Opret delte albummer for at dele billeder og video med personer på dit netværk.",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Tre-trins indlæsning kan øge ydeevnen, men kan ligeledes føre til højere netværksbelastning",
   "theme_setting_three_stage_loading_title": "Slå tre-trins indlæsning til",
   "translated_text_options": "Handlinger",
+  "trash": "Papirkurv",
   "trash_emptied": "Tømte papirkurven",
   "trash_page_delete": "Slet",
   "trash_page_delete_all": "Slet alt",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Vil du sikkerhedskopiere de(t) valgte element(er) til serveren?",
   "upload_dialog_ok": "Upload",
   "upload_dialog_title": "Upload element",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Accepter",
   "version_announcement_overlay_release_notes": "udgivelsesnoterne",
   "version_announcement_overlay_text_1": "Hej ven, der er en ny version af",
   "version_announcement_overlay_text_2": ". Besøg venligst ",
   "version_announcement_overlay_text_3": " for at sikre dig, at din dockercompose- og .env-fil er opdateret, så der undgås fejlkonfiguration, specielt hvis du bruger WatchTower eller lignede.",
   "version_announcement_overlay_title": "Ny serverversion er tilgængelig \uD83C\uDF89",
+  "videos": "Videoer",
   "viewer_remove_from_stack": "Fjern fra stak",
   "viewer_stack_use_as_main_asset": "Brug som hovedelement",
-  "viewer_unstack": "Fjern fra stak"
+  "viewer_unstack": "Fjern fra stak",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/de-DE.json b/mobile/assets/i18n/de-DE.json
index bb2ed31f8a..20c4baa5c0 100644
--- a/mobile/assets/i18n/de-DE.json
+++ b/mobile/assets/i18n/de-DE.json
@@ -6,6 +6,8 @@
   "action_common_save": "Speichern",
   "action_common_select": "Auswählen ",
   "action_common_update": "Aktualisieren",
+  "add_a_name": "Name hinzufügen",
+  "add_endpoint": "Endpunkt hinzufügen",
   "add_to_album_bottom_sheet_added": "Zu {album} hinzugefügt",
   "add_to_album_bottom_sheet_already_exists": "Bereits in {album}",
   "advanced_settings_log_level_title": "Log-Level: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Fehlersuche",
   "album_info_card_backup_album_excluded": "AUSGESCHLOSSEN",
   "album_info_card_backup_album_included": "EINGESCHLOSSEN",
+  "albums": "Alben",
   "album_thumbnail_card_item": "1 Element",
   "album_thumbnail_card_items": "{} Elemente",
   "album_thumbnail_card_shared": " · Geteilt",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Vom Album entfernen",
   "album_viewer_appbar_share_to": "Teile über",
   "album_viewer_page_share_add_users": "Nutzer hinzufügen",
+  "all": "Alle",
   "all_people_page_title": "Personen",
   "all_videos_page_title": "Videos",
   "app_bar_signout_dialog_content": "Bist du dir sicher, dass du dich abmelden möchtest?",
   "app_bar_signout_dialog_ok": "Ja",
   "app_bar_signout_dialog_title": "Abmelden",
+  "archived": "Archiviert",
   "archive_page_no_archived_assets": "Keine archivierten Inhalte gefunden",
   "archive_page_title": "Archiv ({})",
   "asset_action_delete_err_read_only": "Schreibgeschützte Inhalte können nicht gelöscht werden, überspringen...",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} Datei/en erfolgreich wiederhergestellt",
   "assets_trashed": "{} Datei/en gelöscht",
   "assets_trashed_from_server": "{} Datei/en vom Immich-Server gelöscht",
+  "asset_viewer_settings_subtitle": "Verwaltung der Einstellungen für den Galerie-Viewer",
   "asset_viewer_settings_title": "Fotoanzeige",
+  "automatic_endpoint_switching_subtitle": "Verbinden Sie sich lokal über ein bestimmtes WLAN, wenn es verfügbar ist, und verwenden Sie andere Verbindungsmöglichkeiten anderswo.",
+  "automatic_endpoint_switching_title": "Automatische URL-Umschaltung",
+  "background_location_permission": "Hintergrund Standortfreigabe",
+  "background_location_permission_content": "Um im Hintergrund zwischen den Netzwerken wechseln zu können, muss Immich *immer* Zugriff auf den genauen Standort haben, damit die App den Namen des WLAN-Netzwerks ermitteln kann",
   "backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})",
   "backup_album_selection_page_albums_tap": "Einmalig das Album antippen um es zu sichern, doppelt antippen um es nicht mehr zu sichern.",
   "backup_album_selection_page_assets_scatter": "Elemente (Fotos / Videos) können sich über mehrere Alben verteilen. Daher können diese vor der Sicherung eingeschlossen oder ausgeschlossen werden.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Erfolgreich",
   "backup_manual_title": "Sicherungsstatus",
   "backup_options_page_title": "Sicherungsoptionen",
+  "backup_setting_subtitle": "Verwaltung der Upload-Einstellungen im Hintergrund und im Vordergrund",
   "cache_settings_album_thumbnails": "Vorschaubilder der Bibliothek ({} Elemente)",
   "cache_settings_clear_cache_button": "Zwischenspeicher löschen",
   "cache_settings_clear_cache_button_title": "Löscht den Zwischenspeicher der App. Dies wird die Leistungsfähigkeit der App deutlich einschränken, bis der Zwischenspeicher wieder aufgebaut wurde.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Lokalen Speicher verwalten",
   "cache_settings_tile_title": "Lokaler Speicher",
   "cache_settings_title": "Zwischenspeicher Einstellungen",
+  "cancel": "Abbrechen",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Passwort bestätigen",
   "change_password_form_description": "Hallo {name}\n\nDas ist entweder das erste Mal dass du dich einloggst oder es wurde eine Anfrage zur Änderung deines Passwortes gestellt. Bitte gib das neue Passwort ein.",
   "change_password_form_new_password": "Neues Passwort",
   "change_password_form_password_mismatch": "Passwörter stimmen nicht überein",
   "change_password_form_reenter_new_password": "Passwort erneut eingeben",
+  "check_corrupt_asset_backup": "Auf beschädigte Asset-Backups überprüfen",
+  "check_corrupt_asset_backup_button": "Überprüfung durchführen",
+  "check_corrupt_asset_backup_description": "Führe diese Prüfung nur mit aktivierten WLAN durch, nachdem alle Dateien gesichert worden sind. Dieser Vorgang kann ein paar Minuten dauern.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Passwort eingeben",
   "client_cert_import": "Importieren",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Dearchivieren",
   "control_bottom_app_bar_unfavorite": "Aus Favoriten entfernen",
   "control_bottom_app_bar_upload": "Hochladen",
+  "create_album": "Album erstellen",
   "create_album_page_untitled": "Unbenannt",
+  "create_new": "NEUES ERSTELLEN",
   "create_shared_album_page_create": "Erstellen",
   "create_shared_album_page_share": "Teilen",
   "create_shared_album_page_share_add_assets": "INHALTE HINZUFÜGEN",
@@ -193,6 +211,7 @@
   "crop": "Zuschneiden",
   "curated_location_page_title": "Orte",
   "curated_object_page_title": "Dinge",
+  "current_server_address": "Aktuelle Server-Adresse",
   "daily_title_text_date": "E, dd MMM",
   "daily_title_text_date_year": "E, dd MMM, yyyy",
   "date_format": "E d. LLL y • hh:mm",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "Geteilten Link löschen",
   "description_input_hint_text": "Beschreibung hinzufügen...",
   "description_input_submit_error": "Beschreibung konnte nicht geändert werden, bitte im Log für mehr Details nachsehen.",
-  "download_error": "Download Error",
-  "download_started": "Download started",
-  "download_sucess": "Download success",
-  "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_canceled": "Download abgebrochen!",
+  "download_complete": "Download vollständig!",
+  "download_enqueue": "Download in die Warteschlange gesetzt!",
+  "download_error": "Download fehlerhaft",
+  "download_failed": "Download fehlerhaft!",
+  "download_filename": "Datei: {}",
+  "download_finished": "Download abgeschlossen",
+  "downloading": "Wird heruntergeladen...",
+  "downloading_media": "Medien werden heruntergeladen",
+  "download_notfound": "Download nicht gefunden!",
+  "download_paused": "Download pausiert!",
+  "download_started": "Download gestartet",
+  "download_sucess": "Download erfolgreich",
+  "download_sucess_android": "Die Datei wurde nach DCIM/Immich heruntergeladen",
+  "download_waiting_to_retry": "Warte auf erneuten Versuch...",
   "edit_date_time_dialog_date_time": "Datum und Uhrzeit",
   "edit_date_time_dialog_timezone": "Zeitzone",
   "edit_image_title": "Bearbeiten",
   "edit_location_dialog_title": "Ort bearbeiten",
+  "enter_wifi_name": "WLAN-Name eingeben",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Fehler: {}",
   "exif_bottom_sheet_description": "Beschreibung hinzufügen...",
   "exif_bottom_sheet_details": "DETAILS",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Experimentelles Fotogitter aktivieren",
   "experimental_settings_subtitle": "Benutzung auf eigene Gefahr!",
   "experimental_settings_title": "Experimentell",
+  "external_network": "Externes Netzwerk",
+  "external_network_sheet_info": "Wenn sich die App nicht im bevorzugten WLAN-Netzwerk befindet, verbindet sie sich mit dem Server über die erste der folgenden URLs, die sie erreichen kann (von oben nach unten)",
+  "favorites": "Favoriten",
   "favorites_page_no_favorites": "Keine favorisierten Inhalte gefunden",
   "favorites_page_title": "Favoriten",
   "filename_search": "Dateiname oder Dateityp",
+  "filter": "Filter",
+  "get_wifiname_error": "WLAN-Name konnte nicht ermittelt werden. Vergewissere dich, dass die erforderlichen Berechtigungen erteilt wurden und du mit einem WLAN-Netzwerk verbunden bist.\n",
+  "grant_permission": "Erlaubnis gewähren",
   "haptic_feedback_switch": "Haptisches Feedback aktivieren",
   "haptic_feedback_title": "Haptisches Feedback",
   "header_settings_add_header_tip": "Header hinzufügen",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "Wenn dies das erste Mal ist dass Du Immich nutzt, stelle bitte sicher, dass mindestens ein Album zur Sicherung ausgewählt ist, sodass die Zeitachse mit Fotos und Videos gefüllt werden kann.",
   "home_page_share_err_local": "Lokale Inhalte können nicht per Link geteilt werden, überspringe",
   "home_page_upload_err_limit": "Es können max. 30 Elemente gleichzeitig hochgeladen werden, überspringen...",
+  "ignore_icloud_photos": "iCloud Fotos ignorieren",
+  "ignore_icloud_photos_description": "Fotos, die in der iCloud gespeichert sind, werden nicht auf den immich Server  hochgeladen",
   "image_saved_successfully": "Bild gespeichert",
   "image_viewer_page_state_provider_download_error": "Fehler beim Herunterladen",
   "image_viewer_page_state_provider_download_started": "Download gestartet",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "Fehler beim Teilen",
   "invalid_date": "Ungültiges Datum ",
   "invalid_date_format": "Ungültiges Datumsformat",
+  "library": "Bibliothek",
   "library_page_albums": "Alben",
   "library_page_archive": "Archiv",
   "library_page_device_albums": "Alben auf dem Gerät",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Ältestes Foto",
   "library_page_sort_most_recent_photo": "Neuestes Foto",
   "library_page_sort_title": "Titel des Albums",
+  "local_network": "Lokales Netzwerk",
+  "local_network_sheet_info": "Die App stellt über diese URL eine Verbindung zum Server her, wenn sie das angegebene WLAN-Netzwerk verwendet",
+  "location_permission": "Standort Genehmigung",
+  "location_permission_content": "Um die automatische Umschaltfunktion nutzen zu können, benötigt Immich eine genaue Standortberechtigung, damit es den Namen des aktuellen WLAN-Netzwerks ermitteln kann",
   "location_picker_choose_on_map": "Auf der Karte auswählen",
   "location_picker_latitude": "Breitengrad",
   "location_picker_latitude_error": "Gültigen Breitengrad eingeben",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Live-Fotos",
   "multiselect_grid_edit_date_time_err_read_only": "Das Datum und die Uhrzeit von schreibgeschützten Inhalten kann nicht verändert werden, überspringen...",
   "multiselect_grid_edit_gps_err_read_only": "Der Aufnahmeort von schreibgeschützten Inhalten kann nicht verändert werden, überspringen...",
+  "my_albums": "Meine Alben",
+  "networking_settings": "Netzwerk",
+  "networking_subtitle": "Verwaltung von Server-Endpunkt-Einstellungen",
   "no_assets_to_show": "Keine Vorschau vorhanden",
   "no_name": "Kein Name",
   "notification_permission_dialog_cancel": "Abbrechen",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Erlaube Berechtigung für Benachrichtigungen",
   "notification_permission_list_tile_enable_button": "Aktiviere Benachrichtigungen",
   "notification_permission_list_tile_title": "Benachrichtigungs-Berechtigung",
+  "on_this_device": "Auf diesem Gerät",
   "partner_list_user_photos": "{user}s Fotos",
   "partner_list_view_all": "Alle anzeigen",
   "partner_page_add_partner": "Partner hinzufügen",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} wird nicht mehr auf deine Fotos zugreifen können.",
   "partner_page_stop_sharing_title": "Deine Fotos nicht mehr teilen?",
   "partner_page_title": "Partner",
+  "partners": "Partner",
+  "people": "Personen",
   "permission_onboarding_back": "Zurück",
   "permission_onboarding_continue_anyway": "Trotzdem fortfahren",
   "permission_onboarding_get_started": "Jetzt starten",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Berechtigung erteilt! Du bist startklar.",
   "permission_onboarding_permission_limited": "Berechtigungen unzureichend. Um Immich das Sichern von ganzen Sammlungen zu ermöglichen, muss der Zugriff auf alle Fotos und Videos in den Einstellungen erlaubt werden.",
   "permission_onboarding_request": "Immich benötigt Berechtigung um auf deine Fotos und Videos zuzugreifen.",
+  "places": "Orte",
+  "preferences_settings_subtitle": "App-Einstellungen verwalten",
   "preferences_settings_title": "Voreinstellungen",
   "profile_drawer_app_logs": "Logs",
   "profile_drawer_client_out_of_date_major": "Mobile-App ist veraltet. Bitte aktualisiere auf die neueste Major-Version.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Einstellungen",
   "profile_drawer_sign_out": "Abmelden",
   "profile_drawer_trash": "Papierkorb",
+  "recently_added": "Kürzlich hinzugefügt",
   "recently_added_page_title": "Zuletzt hinzugefügt",
+  "save": "Speichern",
   "save_to_gallery": "In Galerie speichern",
   "scaffold_body_error_occurred": "Ein Fehler ist aufgetreten",
+  "search_albums": "nach Album suchen",
   "search_bar_hint": "Durchsuche deine Fotos",
   "search_filter_apply": "Filter anwenden",
   "search_filter_camera": "Kamera",
@@ -428,6 +484,7 @@
   "search_page_places": "Orte",
   "search_page_recently_added": "Zuletzt hinzugefügt",
   "search_page_screenshots": "Bildschirmfotos",
+  "search_page_search_photos_videos": "Nach deinen Fotos und Videos suchen",
   "search_page_selfies": "Selfies",
   "search_page_things": "Gegenstände und Tiere",
   "search_page_videos": "Videos",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Vorschläge",
   "select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden",
   "select_user_for_sharing_page_share_suggestions": "Empfehlungen",
+  "server_endpoint": "Server-Endpunkt",
   "server_info_box_app_version": "App-Version",
   "server_info_box_latest_release": "Neueste Version",
   "server_info_box_server_url": "Server-URL",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Vorschaubild laden",
   "setting_image_viewer_title": "Bilder",
   "setting_languages_apply": "Anwenden",
+  "setting_languages_subtitle": "App-Sprache ändern",
   "setting_languages_title": "Sprachen",
   "setting_notifications_notify_failures_grace_period": "Benachrichtigung bei Fehler/n in der Hintergrundsicherung: {}",
   "setting_notifications_notify_hours": "{} Stunden",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Hochladen",
   "shared_link_manage_links": "Geteilte Links verwalten",
   "shared_link_public_album": "Öffentliches Album",
+  "shared_links": "Geteilte Links",
   "share_done": "Fertig",
+  "shared_with_me": "Mit mir geteilt",
   "share_invite": "Zum Album einladen",
   "sharing_page_album": "Geteilte Alben",
   "sharing_page_description": "Erstelle ein geteiltes Album um Fotos und Videos mit Personen in deinem Netzwerk zu teilen.",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Das dreistufige Ladeverfahren kann die Performance beim Laden verbessern, erhöht allerdings den Datenverbrauch deutlich",
   "theme_setting_three_stage_loading_title": "Dreistufiges Laden aktivieren",
   "translated_text_options": "Optionen",
+  "trash": "Papierkorb",
   "trash_emptied": "Geleerter Papierkorb",
   "trash_page_delete": "Löschen",
   "trash_page_delete_all": "Alle löschen",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Willst du die ausgewählten Elemente auf dem Server sichern?",
   "upload_dialog_ok": "Hochladen",
   "upload_dialog_title": "Element hochladen",
+  "use_current_connection": "aktuelle Verbindung verwenden",
+  "validate_endpoint_error": "Bitte gib eine gültige URL ein",
   "version_announcement_overlay_ack": "Ich habe verstanden",
   "version_announcement_overlay_release_notes": "Änderungsprotokoll",
   "version_announcement_overlay_text_1": "Hallo mein Freund! Es gibt eine neue Version von",
   "version_announcement_overlay_text_2": "Bitte nehme dir die Zeit und lies das ",
   "version_announcement_overlay_text_3": " und achte darauf, dass deine docker-compose und .env Dateien aktuell sind, vor allem wenn du ein System für automatische Updates benutzt (z.B. Watchtower).",
   "version_announcement_overlay_title": "Neue Server-Version verfügbar \uD83C\uDF89",
+  "videos": "Videos",
   "viewer_remove_from_stack": "Aus Stapel entfernen",
   "viewer_stack_use_as_main_asset": "An Stapelanfang",
-  "viewer_unstack": "Stapel aufheben"
+  "viewer_unstack": "Stapel aufheben",
+  "wifi_name": "WLAN-Name",
+  "your_wifi_name": "Dein WLAN-Name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/el-GR.json b/mobile/assets/i18n/el-GR.json
index 88426a6076..47e945c1a9 100644
--- a/mobile/assets/i18n/el-GR.json
+++ b/mobile/assets/i18n/el-GR.json
@@ -1,18 +1,20 @@
 {
-  "action_common_back": "Back",
+  "action_common_back": "Πίσω",
   "action_common_cancel": "Ακύρωση",
-  "action_common_clear": "Clear",
-  "action_common_confirm": "Confirm",
-  "action_common_save": "Save",
-  "action_common_select": "Select",
+  "action_common_clear": "Εκκαθάριση",
+  "action_common_confirm": "Επιβεβαίωση",
+  "action_common_save": "Αποθήκευση",
+  "action_common_select": "Επιλογή",
   "action_common_update": "Ενημέρωση",
+  "add_a_name": "Πρόσθεση ονόματος",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Προστέθηκε στο {album}",
   "add_to_album_bottom_sheet_already_exists": "Ήδη στο {album}",
   "advanced_settings_log_level_title": "Επίπεδο καταγραφής: {}",
   "advanced_settings_prefer_remote_subtitle": "Μερικές συσκευές αργούν πολύ να φορτώσουν μικρογραφίες από αρχεία στη συσκευή. Ενεργοποιήστε αυτήν τη ρύθμιση για να φορτώνονται αντί αυτού απομακρυσμένες εικόνες.",
   "advanced_settings_prefer_remote_title": "Προτίμηση απομακρυσμένων εικόνων.",
-  "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
-  "advanced_settings_proxy_headers_title": "Proxy Headers",
+  "advanced_settings_proxy_headers_subtitle": "Καθορισμός κεφαλίδων διακομιστή μεσολάβησης που το Immich πρέπει να στέλνει με κάθε αίτημα δικτύου",
+  "advanced_settings_proxy_headers_title": "Κεφαλίδες διακομιστή μεσολάβησης",
   "advanced_settings_self_signed_ssl_subtitle": "Παρακάμπτει τον έλεγχο πιστοποιητικού SSL του διακομιστή. Απαραίτητο για αυτο-υπογεγραμμένα πιστοποιητικά.",
   "advanced_settings_self_signed_ssl_title": "Να επιτρέπονται αυτο-υπογεγραμμένα πιστοποιητικά SSL",
   "advanced_settings_tile_subtitle": "Ρυθμίσεις προχωρημένου χρήστη",
@@ -21,12 +23,13 @@
   "advanced_settings_troubleshooting_title": "Αντιμετώπιση προβλημάτων",
   "album_info_card_backup_album_excluded": "ΕΞΑΙΡΟΥΜΕΝΟ",
   "album_info_card_backup_album_included": "ΣΥΜΠΕΡΙΛΑΜΒΑΝΟΜΕΝΟ",
+  "albums": "Άλμπουμ",
   "album_thumbnail_card_item": "1 αντικείμενο",
   "album_thumbnail_card_items": "{} αντικείμενα",
   "album_thumbnail_card_shared": "· Κοινόχρηστο",
   "album_thumbnail_owned": "Δικό μου",
   "album_thumbnail_shared_by": "Κοινοποιημένο από {}",
-  "album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?",
+  "album_viewer_appbar_delete_confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το άλμπουμ από τον λογαριασμό σας;",
   "album_viewer_appbar_share_delete": "Διαγραφή άλμπουμ",
   "album_viewer_appbar_share_err_delete": "Αποτυχία διαγραφής άλμπουμ",
   "album_viewer_appbar_share_err_leave": "Αποτυχία αποχώρησης από άλμπουμ",
@@ -36,32 +39,39 @@
   "album_viewer_appbar_share_remove": "Αφαίρεση από άλμπουμ",
   "album_viewer_appbar_share_to": "Κοινοποίηση σε",
   "album_viewer_page_share_add_users": "Προσθήκη χρηστών",
+  "all": "Όλα",
   "all_people_page_title": "Άτομα",
   "all_videos_page_title": "Βίντεο",
   "app_bar_signout_dialog_content": "Είστε βέβαιοι ότι θέλετε να αποσυνδεθείτε;",
   "app_bar_signout_dialog_ok": "Ναι",
   "app_bar_signout_dialog_title": "Αποσύνδεση",
+  "archived": "Αρχείο",
   "archive_page_no_archived_assets": "Δε βρέθηκαν αρχειοθετημένα στοιχεία",
-  "archive_page_title": "Αρχειοθέτηση ({})",
-  "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
-  "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
-  "asset_list_group_by_sub_title": "Group by",
+  "archive_page_title": "Αρχείο ({})",
+  "asset_action_delete_err_read_only": "Δεν είναι δυνατή η διαγραφή στοιχείων μόνο για ανάγνωση, παραλείπεται",
+  "asset_action_share_err_offline": "Δεν είναι δυνατή η ανάκτηση στοιχείων εκτός σύνδεσης, παραλείπεται",
+  "asset_list_group_by_sub_title": "Ομαδοποίηση κατά",
   "asset_list_layout_settings_dynamic_layout_title": "Δυναμική διάταξη",
   "asset_list_layout_settings_group_automatically": "Αυτόματα",
   "asset_list_layout_settings_group_by": "Ομαδοποίηση στοιχείων ανά",
   "asset_list_layout_settings_group_by_month": "Μήνας",
   "asset_list_layout_settings_group_by_month_day": "Μήνας + ημέρα",
-  "asset_list_layout_sub_title": "Layout",
+  "asset_list_layout_sub_title": "Διάταξη",
   "asset_list_settings_subtitle": "Ρυθμίσεις διάταξης πλέγματος φωτογραφιών",
   "asset_list_settings_title": "Πλέγμα φωτογραφιών",
-  "asset_restored_successfully": "Asset restored successfully",
-  "assets_deleted_permanently": "{} asset(s) deleted permanently",
-  "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server",
-  "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device",
-  "assets_restored_successfully": "{} asset(s) restored successfully",
-  "assets_trashed": "{} asset(s) trashed",
-  "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
-  "asset_viewer_settings_title": "Asset Viewer",
+  "asset_restored_successfully": "Το στοιχείο αποκαταστάθηκε με επιτυχία",
+  "assets_deleted_permanently": "{} στοιχείο(α) διαγράφηκαν οριστικά",
+  "assets_deleted_permanently_from_server": "{} στοιχείο(α) διαγράφηκαν οριστικά από τον διακομιστή Immich",
+  "assets_removed_permanently_from_device": "{} στοιχεία καταργήθηκαν οριστικά από τη συσκευή σας",
+  "assets_restored_successfully": "{} στοιχεία αποκαταστάθηκαν με επιτυχία",
+  "assets_trashed": "{} στοιχεία μεταφέρθηκαν στον κάδο απορριμμάτων",
+  "assets_trashed_from_server": "{} στοιχεία μεταφέρθηκαν στον κάδο απορριμμάτων από τον διακομιστή Immich",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
+  "asset_viewer_settings_title": "Προβολή Στοιχείων",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Άλμπουμ στη συσκευή ({})",
   "backup_album_selection_page_albums_tap": "Πάτημα για συμπερίληψη, διπλό πάτημα για εξαίρεση",
   "backup_album_selection_page_assets_scatter": "Τα  στοιχεία μπορεί να διασκορπιστούν σε πολλά άλμπουμ. Έτσι, τα άλμπουμ μπορούν να περιληφθούν ή να εξαιρεθούν κατά τη διαδικασία δημιουργίας αντιγράφων ασφαλείας.",
@@ -99,7 +109,7 @@
   "backup_controller_page_cancel": "Ακύρωση",
   "backup_controller_page_created": "Δημιουργήθηκε στις: {}",
   "backup_controller_page_desc_backup": "Ενεργοποιήστε την δημιουργία αντιγράφων ασφαλείας στο προσκήνιο για αυτόματη μεταφόρτωση νέων στοιχείων στον διακομιστή όταν ανοίγετε την εφαρμογή.",
-  "backup_controller_page_excluded": "Εξαιρεμένα:",
+  "backup_controller_page_excluded": "Εξαιρούμενα:",
   "backup_controller_page_failed": "Αποτυχημένα ({})",
   "backup_controller_page_filename": "Όνομα αρχείου: {} [{}]",
   "backup_controller_page_id": "ID: {}",
@@ -126,7 +136,8 @@
   "backup_manual_in_progress": "Μεταφόρτωση σε εξέλιξη. Δοκιμάστε αργότερα",
   "backup_manual_success": "Επιτυχία",
   "backup_manual_title": "Κατάσταση μεταφόρτωσης",
-  "backup_options_page_title": "Backup options",
+  "backup_options_page_title": "Επιλογές αντιγράφων ασφαλείας",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Μικρογραφίες σελίδας βιβλιοθήκης ({} στοιχεία)",
   "cache_settings_clear_cache_button": "Εκκαθάριση προσωρινής μνήμης",
   "cache_settings_clear_cache_button_title": "Καθαρίζει τη προσωρινή μνήμη της εφαρμογής. Αυτό θα επηρεάσει σημαντικά την απόδοση της εφαρμογής μέχρι να αναδημιουργηθεί η προσωρινή μνήμη.",
@@ -140,41 +151,46 @@
   "cache_settings_statistics_shared": "Μικρογραφίες κοινοποιημένου άλμπουμ",
   "cache_settings_statistics_thumbnail": "Μικρογραφίες",
   "cache_settings_statistics_title": "Χρήση προσωρινής μνήμης",
-  "cache_settings_subtitle": "Χειριστείτε τη συμπεριφορά της προσωρινής μνήμης της εφαρμογής Immich για κινητά τηλέφωνα",
+  "cache_settings_subtitle": "Διαχείρηση συμπεριφοράς της προσωρινής μνήμης",
   "cache_settings_thumbnail_size": "Μέγεθος προσωρινής μνήμης μικρογραφιών ({} στοιχεία)",
   "cache_settings_tile_subtitle": "Χειριστείτε τη συμπεριφορά της τοπικής αποθήκευσης",
   "cache_settings_tile_title": "Τοπική Αποθήκευση",
   "cache_settings_title": "Ρυθμίσεις Προσωρινής Μνήμης",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Επιβεβαίωση Κωδικού",
   "change_password_form_description": "Γεια σας {name},\n\nΕίτε είναι η πρώτη φορά που συνδέεστε στο σύστημα είτε έχει γίνει αίτηση για αλλαγή του κωδικού σας. Παρακαλώ εισάγετε τον νέο κωδικό.",
   "change_password_form_new_password": "Νέος Κωδικός",
   "change_password_form_password_mismatch": "Οι κωδικοί δεν ταιριάζουν",
   "change_password_form_reenter_new_password": "Επανεισαγωγή Νέου Κωδικού",
-  "client_cert_dialog_msg_confirm": "OK",
-  "client_cert_enter_password": "Enter Password",
-  "client_cert_import": "Import",
-  "client_cert_import_success_msg": "Client certificate is imported",
-  "client_cert_invalid_msg": "Invalid certificate file or wrong password",
-  "client_cert_remove": "Remove",
-  "client_cert_remove_msg": "Client certificate is removed",
-  "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login",
-  "client_cert_title": "SSL Client Certificate",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
+  "client_cert_dialog_msg_confirm": "ΟΚ",
+  "client_cert_enter_password": "Εισαγάγετε κωδικό πρόσβασης",
+  "client_cert_import": "Εισαγωγή",
+  "client_cert_import_success_msg": "Το πιστοποιητικό πελάτη εισάγεται",
+  "client_cert_invalid_msg": "Μη έγκυρο αρχείο πιστοποιητικού ή λάθος κωδικός πρόσβασης",
+  "client_cert_remove": "Αφαίρεση",
+  "client_cert_remove_msg": "Το πιστοποιητικό πελάτη καταργήθηκε",
+  "client_cert_subtitle": "Υποστηρίζει μόνο τη μορφή PKCS12 (.p12, .pfx). Η Εισαγωγή/Αφαίρεση πιστοποιητικού είναι διαθέσιμη μόνο πριν από τη σύνδεση",
+  "client_cert_title": "Πιστοποιητικό πελάτη SSL",
   "common_add_to_album": "Προσθήκη στο άλμπουμ",
   "common_change_password": "Αλλαγή Κωδικού",
   "common_create_new_album": "Δημιουργία νέου άλμπουμ",
   "common_server_error": "Ελέγξτε τη σύνδεσή σας, βεβαιωθείτε ότι ο διακομιστής είναι προσβάσιμος και ότι οι εκδόσεις της εφαρμογής/διακομιστή είναι συμβατές.",
   "common_shared": "Κοινόχρηστο",
-  "contextual_search": "Sunrise on the beach",
+  "contextual_search": "Ανατολή στην παραλία",
   "control_bottom_app_bar_add_to_album": "Προσθήκη στο άλμπουμ",
   "control_bottom_app_bar_album_info": "{} αντικείμενα",
   "control_bottom_app_bar_album_info_shared": "{} αντικείμενα · Κοινόχρηστα",
-  "control_bottom_app_bar_archive": "Αρχειοθέτηση",
+  "control_bottom_app_bar_archive": "Αρχείο",
   "control_bottom_app_bar_create_new_album": "Δημιουργία νέου άλμπουμ",
   "control_bottom_app_bar_delete": "Διαγραφή",
   "control_bottom_app_bar_delete_from_immich": "Διαγραφή από το Immich",
   "control_bottom_app_bar_delete_from_local": "Διαγραφή από τη συσκευή",
-  "control_bottom_app_bar_download": "Download",
-  "control_bottom_app_bar_edit": "Edit",
+  "control_bottom_app_bar_download": "Λήψη",
+  "control_bottom_app_bar_edit": "Επεξεργασία",
   "control_bottom_app_bar_edit_location": "Επεξεργασία Τοποθεσίας",
   "control_bottom_app_bar_edit_time": "Επεξεργασία Ημερομηνίας & Ώρας",
   "control_bottom_app_bar_favorite": "Προσθήκη στα αγαπημένα",
@@ -185,14 +201,17 @@
   "control_bottom_app_bar_unarchive": "Αναίρεση αρχειοθέτησης",
   "control_bottom_app_bar_unfavorite": "Κατάργηση από τα αγαπημένα",
   "control_bottom_app_bar_upload": "Μεταφόρτωση",
+  "create_album": "Δημιουργία άλμπουμ",
   "create_album_page_untitled": "Χωρίς τίτλο",
+  "create_new": "ΔΗΜΙΟΥΡΓΙΑ ΝΕΟΥ",
   "create_shared_album_page_create": "Δημιουργία",
   "create_shared_album_page_share": "Κοινοποίηση",
   "create_shared_album_page_share_add_assets": "ΠΡΟΣΘΗΚΗ ΣΤΟΙΧΕΙΩΝ",
   "create_shared_album_page_share_select_photos": "Επιλέξτε Φωτογραφίες",
-  "crop": "Crop",
+  "crop": "Αποκοπή",
   "curated_location_page_title": "Τοποθεσίες",
   "curated_object_page_title": "Πράγματα",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "Ε, MMM dd",
   "daily_title_text_date_year": "Ε, MMM dd, yyyy",
   "date_format": "Ε, LLL d, y • h:mm a",
@@ -202,156 +221,186 @@
   "delete_dialog_alert_remote": "Αυτά τα αντικείμενα θα διαγραφούν οριστικά από τον διακομιστή Immich",
   "delete_dialog_cancel": "Ακύρωση",
   "delete_dialog_ok": "Διαγραφή",
-  "delete_dialog_ok_force": "Delete Anyway",
+  "delete_dialog_ok_force": "Διαγραφή όπως και να έχει",
   "delete_dialog_title": "Οριστική Διαγραφή",
-  "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
-  "delete_local_dialog_ok_force": "Delete Anyway",
+  "delete_local_dialog_ok_backed_up_only": "Διαγραφή μόνο των αντιγράφων ασφαλείας",
+  "delete_local_dialog_ok_force": "Διαγραφή όπως και να έχει",
   "delete_shared_link_dialog_content": "Σίγουρα θέλετε να διαγράψετε αυτόν τον κοινοποιημένο σύνδεσμο;",
   "delete_shared_link_dialog_title": "Διαγραφή Κοινοποιημένου Συνδέσμου",
   "description_input_hint_text": "Προσθήκη περιγραφής...",
   "description_input_submit_error": "Σφάλμα κατά την ενημέρωση της περιγραφής, ελέγξτε το αρχείο καταγραφής για περισσότερες λεπτομέρειες",
-  "download_error": "Download Error",
-  "download_started": "Download started",
-  "download_sucess": "Download success",
-  "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_canceled": "Η λήψη ακυρώθηκε",
+  "download_complete": "Η λήψη ολοκληρώθηκε",
+  "download_enqueue": "Η λήψη τέθηκε σε ουρά",
+  "download_error": "Σφάλμα λήψης",
+  "download_failed": "Η λήψη απέτυχε",
+  "download_filename": "αρχείο: {}",
+  "download_finished": "Η λήψη ολοκληρώθηκε",
+  "downloading": "Λήψη...",
+  "downloading_media": "Λήψη πολυμέσων",
+  "download_notfound": "Το αρχείο δεν βρέθηκε",
+  "download_paused": "Η λήψη διακόπηκε",
+  "download_started": "Η λήψη ξεκίνησε",
+  "download_sucess": "Επιτυχία λήψης",
+  "download_sucess_android": "Το μέσο έχει ληφθεί στο DCIM/Immich",
+  "download_waiting_to_retry": "Αναμονή για επανάληψη",
   "edit_date_time_dialog_date_time": "Ημερομηνία και Ώρα",
   "edit_date_time_dialog_timezone": "Ζώνη ώρας",
-  "edit_image_title": "Edit",
+  "edit_image_title": "Επεξεργασία",
   "edit_location_dialog_title": "Τοποθεσία",
-  "error_saving_image": "Error: {}",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
+  "error_saving_image": "Σφάλμα: {}",
   "exif_bottom_sheet_description": "Προσθήκη Περιγραφής...",
   "exif_bottom_sheet_details": "ΛΕΠΤΟΜΕΡΕΙΕΣ",
   "exif_bottom_sheet_location": "ΤΟΠΟΘΕΣΙΑ",
   "exif_bottom_sheet_location_add": "Προσθήκη τοποθεσίας",
-  "exif_bottom_sheet_people": "PEOPLE",
-  "exif_bottom_sheet_person_add_person": "Add name",
+  "exif_bottom_sheet_people": "ΑΝΘΡΩΠΟΙ",
+  "exif_bottom_sheet_person_add_person": "Προσθήκη ονόματος",
   "experimental_settings_new_asset_list_subtitle": "Σε εξέλιξη",
   "experimental_settings_new_asset_list_title": "Ενεργοποίηση πειραματικού πλέγματος φωτογραφιών",
   "experimental_settings_subtitle": "Χρησιμοποιείτε με δική σας ευθύνη!",
   "experimental_settings_title": "Πειραματικό",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Αγαπημένα",
   "favorites_page_no_favorites": "Δεν βρέθηκαν αγαπημένα στοιχεία",
   "favorites_page_title": "Αγαπημένα",
-  "filename_search": "File name or extension",
-  "haptic_feedback_switch": "Enable haptic feedback",
-  "haptic_feedback_title": "Haptic Feedback",
-  "header_settings_add_header_tip": "Add Header",
-  "header_settings_field_validator_msg": "Value cannot be empty",
-  "header_settings_header_name_input": "Header name",
-  "header_settings_header_value_input": "Header value",
-  "header_settings_page_title": "Proxy Headers",
-  "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
-  "headers_settings_tile_title": "Custom proxy headers",
+  "filename_search": "Όνομα αρχείου ή επέκταση",
+  "filter": "Φίλτρο",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
+  "haptic_feedback_switch": "Ενεργοποίηση απτικής ανάδρασης",
+  "haptic_feedback_title": "Απτική Ανάδραση",
+  "header_settings_add_header_tip": "Προσθήκη Κεφαλίδας",
+  "header_settings_field_validator_msg": "Η τιμή δεν μπορεί να είναι κενή",
+  "header_settings_header_name_input": "Όνομα κεφαλίδας",
+  "header_settings_header_value_input": "Τιμή κεφαλίδας",
+  "header_settings_page_title": "Κεφαλίδες διακομιστή μεσολάβησης",
+  "headers_settings_tile_subtitle": "Καθορίστε τις κεφαλίδες διακομιστή μεσολάβησης που θα πρέπει να στέλνει η εφαρμογή με κάθε αίτημα δικτύου",
+  "headers_settings_tile_title": "Προσαρμοσμένες κεφαλίδες διακομιστή μεσολάβησης",
   "home_page_add_to_album_conflicts": "Προστέθηκαν {added} στοιχεία στο άλμπουμ {album}. {failed} στοιχεία υπάρχουν ήδη στο άλμπουμ.",
   "home_page_add_to_album_err_local": "Δεν είναι ακόμη δυνατή η προσθήκη τοπικών στοιχείων σε άλμπουμ, παράβλεψη",
   "home_page_add_to_album_success": "Προστέθηκαν {added} στοιχεία στο άλμπουμ {album}.",
-  "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping",
-  "home_page_archive_err_local": "Can not archive local assets yet, skipping",
-  "home_page_archive_err_partner": "Can not archive partner assets, skipping",
-  "home_page_building_timeline": "Building the timeline",
-  "home_page_delete_err_partner": "Can not delete partner assets, skipping",
-  "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
-  "home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
-  "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping",
-  "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
-  "home_page_share_err_local": "Can not share local assets via link, skipping",
-  "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
-  "image_saved_successfully": "Image saved",
-  "image_viewer_page_state_provider_download_error": "Download Error",
-  "image_viewer_page_state_provider_download_started": "Download Started",
-  "image_viewer_page_state_provider_download_success": "Download Success",
-  "image_viewer_page_state_provider_share_error": "Share Error",
-  "invalid_date": "Invalid date",
-  "invalid_date_format": "Invalid date format",
-  "library_page_albums": "Albums",
-  "library_page_archive": "Archive",
-  "library_page_device_albums": "Albums on Device",
-  "library_page_favorites": "Favorites",
-  "library_page_new_album": "New album",
-  "library_page_sharing": "Sharing",
-  "library_page_sort_asset_count": "Number of assets",
-  "library_page_sort_created": "Created date",
-  "library_page_sort_last_modified": "Last modified",
-  "library_page_sort_most_oldest_photo": "Oldest photo",
-  "library_page_sort_most_recent_photo": "Most recent photo",
-  "library_page_sort_title": "Album title",
-  "location_picker_choose_on_map": "Choose on map",
-  "location_picker_latitude": "Latitude",
-  "location_picker_latitude_error": "Enter a valid latitude",
-  "location_picker_latitude_hint": "Enter your latitude here",
-  "location_picker_longitude": "Longitude",
-  "location_picker_longitude_error": "Enter a valid longitude",
-  "location_picker_longitude_hint": "Enter your longitude here",
+  "home_page_album_err_partner": "Δεν είναι δυνατή η προσθήκη στοιχείων συντρόφου σε ένα άλμπουμ ακόμα, παραλείπεται",
+  "home_page_archive_err_local": "Δεν είναι δυνατή η αρχειοθέτηση τοπικών στοιχείων ακόμα, παραλείπεται",
+  "home_page_archive_err_partner": "Δεν είναι δυνατή η αρχειοθέτηση στοιχείων συντρόφου, παραλείπεται",
+  "home_page_building_timeline": "Χτίζεται το χρονοδιάγραμμα",
+  "home_page_delete_err_partner": "Δεν είναι δυνατή η διαγραφή στοιχείων συντρόφου, παραλείπεται",
+  "home_page_delete_remote_err_local": "Τοπικά στοιχεία στη διαγραφή απομακρυσμένης επιλογής, παραλείπεται",
+  "home_page_favorite_err_local": "Δεν μπορώ ακόμα να αγαπήσω τα τοπικά στοιχεία, παραλείπεται",
+  "home_page_favorite_err_partner": "Δεν είναι ακόμα δυνατή η πρόσθεση στοιχείων συντρόφου στα αγαπημένα, παραλείπεται",
+  "home_page_first_time_notice": "Εάν αυτή είναι η πρώτη φορά που χρησιμοποιείτε την εφαρμογή, βεβαιωθείτε ότι έχετε επιλέξει ένα άλμπουμ αντίγραφου ασφαλείας, ώστε το χρονοδιάγραμμα να μπορεί να συμπληρώσει φωτογραφίες και βίντεο στα άλμπουμ.",
+  "home_page_share_err_local": "Δεν είναι δυνατή η κοινή χρήση τοπικών στοιχείων μέσω συνδέσμου, παραλείπεται",
+  "home_page_upload_err_limit": "Μπορείτε να ανεβάσετε μόνο 30 στοιχεία κάθε φορά, παραλείπεται",
+  "ignore_icloud_photos": "Αγνοήστε τις φωτογραφίες iCloud",
+  "ignore_icloud_photos_description": "Οι φωτογραφίες που είναι αποθηκευμένες στο iCloud δεν θα μεταφορτωθούν στον διακομιστή Immich",
+  "image_saved_successfully": "Η εικόνα αποθηκεύτηκε",
+  "image_viewer_page_state_provider_download_error": "Σφάλμα Λήψης",
+  "image_viewer_page_state_provider_download_started": "Ξεκίνησε Λήψη",
+  "image_viewer_page_state_provider_download_success": "Επιτυχία Λήψης",
+  "image_viewer_page_state_provider_share_error": "Σφάλμα Κοινής Χρήσης",
+  "invalid_date": "Μη έγκυρη ημερομηνία",
+  "invalid_date_format": "Μη έγκυρη μορφή ημερομηνίας",
+  "library": "Βιβλιοθήκη",
+  "library_page_albums": "Άλμπουμ",
+  "library_page_archive": "Αρχείο",
+  "library_page_device_albums": "Άλμπουμ στη Συσκευή",
+  "library_page_favorites": "Αγαπημένα",
+  "library_page_new_album": "Νέο άλμπουμ",
+  "library_page_sharing": "Κοινή Χρήση",
+  "library_page_sort_asset_count": "Αριθμός στοιχείων",
+  "library_page_sort_created": "Ημερομηνία δημιουργίας",
+  "library_page_sort_last_modified": "Τελευταία τροποποίηση",
+  "library_page_sort_most_oldest_photo": "Πιο παλιά φωτογραφία",
+  "library_page_sort_most_recent_photo": "Πιο πρόσφατη φωτογραφία",
+  "library_page_sort_title": "Τίτλος άλμπουμ",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
+  "location_picker_choose_on_map": "Επιλέξτε στο χάρτη",
+  "location_picker_latitude": "Γεωγραφικό πλάτος",
+  "location_picker_latitude_error": "Εισαγάγετε ένα έγκυρο γεωγραφικό πλάτος",
+  "location_picker_latitude_hint": "Εισαγάγετε το γεωγραφικό πλάτος σας εδώ",
+  "location_picker_longitude": "Γεωγραφικό μήκος",
+  "location_picker_longitude_error": "Εισαγάγετε ένα έγκυρο γεωγραφικό μήκος",
+  "location_picker_longitude_hint": "Εισαγάγετε εδώ το γεωγραφικό σας μήκος",
   "login_disabled": "Η σύνδεση έχει απενεργοποιηθεί",
-  "login_form_api_exception": "API exception. Please check the server URL and try again.",
-  "login_form_back_button_text": "Back",
-  "login_form_button_text": "Login",
-  "login_form_email_hint": "youremail@email.com",
-  "login_form_endpoint_hint": "http://your-server-ip:port/api",
-  "login_form_endpoint_url": "Server Endpoint URL",
-  "login_form_err_http": "Please specify http:// or https://",
-  "login_form_err_invalid_email": "Invalid Email",
-  "login_form_err_invalid_url": "Invalid URL",
-  "login_form_err_leading_whitespace": "Leading whitespace",
-  "login_form_err_trailing_whitespace": "Trailing whitespace",
-  "login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL",
-  "login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server",
-  "login_form_failed_login": "Error logging you in, check server URL, email and password",
-  "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.",
+  "login_form_api_exception": "Εξαίρεση API. Ελέγξτε τη διεύθυνση URL του διακομιστή και δοκιμάστε ξανά.",
+  "login_form_back_button_text": "Πίσω",
+  "login_form_button_text": "Σύνδεση",
+  "login_form_email_hint": "to-email-sou@email.com",
+  "login_form_endpoint_hint": "http://ip-tou-server-sou:porta/api",
+  "login_form_endpoint_url": "URL τελικού σημείου διακομιστή",
+  "login_form_err_http": "Προσδιορίστε http:// ή https://",
+  "login_form_err_invalid_email": "Μη έγκυρο email",
+  "login_form_err_invalid_url": "Μη έγκυρη διεύθυνση URL",
+  "login_form_err_leading_whitespace": "Κενό διάστημα στην αρχή",
+  "login_form_err_trailing_whitespace": "Κενό διάστημα στο τέλος",
+  "login_form_failed_get_oauth_server_config": "Σφάλμα καταγραφής χρησιμοποιώντας το OAuth, ελέγξτε τη διεύθυνση URL του διακομιστή",
+  "login_form_failed_get_oauth_server_disable": "Η δυνατότητα OAuth δεν είναι διαθέσιμη σε αυτόν τον διακομιστή",
+  "login_form_failed_login": "Σφάλμα κατά τη σύνδεσή σας, ελέγξτε τη διεύθυνση URL του διακομιστή, το email και τον κωδικό πρόσβασης",
+  "login_form_handshake_exception": "Υπήρξε σφάλμα χειραψίας με τον διακομιστή. Ενεργοποιήστε την υποστήριξη αυτο-υπογεγραμμένου πιστοποιητικού στις ρυθμίσεις εάν χρησιμοποιείτε αυτο-υπογεγραμμένο πιστοποιητικό.",
   "login_form_label_email": "Email",
-  "login_form_label_password": "Password",
-  "login_form_next_button": "Next",
-  "login_form_password_hint": "password",
-  "login_form_save_login": "Stay logged in",
-  "login_form_server_empty": "Enter a server URL.",
-  "login_form_server_error": "Could not connect to server.",
-  "login_password_changed_error": "There was an error updating your password",
-  "login_password_changed_success": "Password updated successfully",
-  "map_assets_in_bound": "{} photo",
-  "map_assets_in_bounds": "{} photos",
-  "map_cannot_get_user_location": "Cannot get user's location",
-  "map_location_dialog_cancel": "Cancel",
-  "map_location_dialog_yes": "Yes",
-  "map_location_picker_page_use_location": "Use this location",
-  "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?",
-  "map_location_service_disabled_title": "Location Service disabled",
-  "map_no_assets_in_bounds": "No photos in this area",
-  "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
-  "map_no_location_permission_title": "Location Permission denied",
-  "map_settings_dark_mode": "Dark mode",
-  "map_settings_date_range_option_all": "All",
-  "map_settings_date_range_option_day": "Past 24 hours",
-  "map_settings_date_range_option_days": "Past {} days",
-  "map_settings_date_range_option_year": "Past year",
-  "map_settings_date_range_option_years": "Past {} years",
-  "map_settings_dialog_cancel": "Cancel",
-  "map_settings_dialog_save": "Save",
-  "map_settings_dialog_title": "Map Settings",
-  "map_settings_include_show_archived": "Include Archived",
-  "map_settings_include_show_partners": "Include Partners",
-  "map_settings_only_relative_range": "Date range",
-  "map_settings_only_show_favorites": "Show Favorite Only",
-  "map_settings_theme_settings": "Map Theme",
-  "map_zoom_to_see_photos": "Zoom out to see photos",
-  "memories_all_caught_up": "All caught up",
-  "memories_check_back_tomorrow": "Check back tomorrow for more memories",
-  "memories_start_over": "Start Over",
-  "memories_swipe_to_close": "Swipe up to close",
-  "memories_year_ago": "A year ago",
-  "memories_years_ago": "{} years ago",
-  "monthly_title_text_date_format": "MMMM y",
-  "motion_photos_page_title": "Motion Photos",
-  "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
-  "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
-  "no_assets_to_show": "No assets to show",
-  "no_name": "No name",
-  "notification_permission_dialog_cancel": "Cancel",
-  "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
-  "notification_permission_dialog_settings": "Settings",
-  "notification_permission_list_tile_content": "Grant permission to enable notifications.",
-  "notification_permission_list_tile_enable_button": "Enable Notifications",
-  "notification_permission_list_tile_title": "Notification Permission",
-  "partner_list_user_photos": "{user}'s photos",
-  "partner_list_view_all": "View all",
+  "login_form_label_password": "Κωδικός Πρόσβασης",
+  "login_form_next_button": "Επόμενος",
+  "login_form_password_hint": "κωδικός πρόσβασης",
+  "login_form_save_login": "Μείνετε συνδεδεμένοι",
+  "login_form_server_empty": "Εισαγάγετε μια διεύθυνση URL διακομιστή.",
+  "login_form_server_error": "Δεν ήταν δυνατή η σύνδεση με τον διακομιστή.",
+  "login_password_changed_error": "Παρουσιάστηκε σφάλμα κατά την ενημέρωση του κωδικού πρόσβασής σας",
+  "login_password_changed_success": "Ο κωδικός πρόσβασης ενημερώθηκε με επιτυχία",
+  "map_assets_in_bound": "{} φωτογραφία",
+  "map_assets_in_bounds": "{} φωτογραφίες",
+  "map_cannot_get_user_location": "Δεν είναι δυνατή η λήψη της τοποθεσίας του χρήστη",
+  "map_location_dialog_cancel": "Ακύρωση",
+  "map_location_dialog_yes": "Ναι",
+  "map_location_picker_page_use_location": "Χρησιμοποιήστε αυτήν την τοποθεσία",
+  "map_location_service_disabled_content": "Η υπηρεσία τοποθεσίας πρέπει να είναι ενεργοποιημένη για την εμφάνιση στοιχείων από την τρέχουσα τοποθεσία σας. Θέλετε να το ενεργοποιήσετε τώρα;",
+  "map_location_service_disabled_title": "Η υπηρεσία τοποθεσίας απενεργοποιήθηκε",
+  "map_no_assets_in_bounds": "Δεν υπάρχουν φωτογραφίες σε αυτήν την περιοχή",
+  "map_no_location_permission_content": "Απαιτείται άδεια τοποθεσίας για την εμφάνιση στοιχείων από την τρέχουσα τοποθεσία σας. Θέλετε να το επιτρέψετε τώρα;",
+  "map_no_location_permission_title": "Η άδεια τοποθεσίας απορρίφθηκε",
+  "map_settings_dark_mode": "Σκοτεινή λειτουργία",
+  "map_settings_date_range_option_all": "Όλοι",
+  "map_settings_date_range_option_day": "Προηγούμενες 24 ώρες",
+  "map_settings_date_range_option_days": "Προηγούμενες {} ημέρες",
+  "map_settings_date_range_option_year": "Προηγούμενο έτος",
+  "map_settings_date_range_option_years": "Προηγούμενα {} έτη",
+  "map_settings_dialog_cancel": "Ακύρωση",
+  "map_settings_dialog_save": "Αποθήκευση",
+  "map_settings_dialog_title": "Ρυθμίσεις Χάρτη",
+  "map_settings_include_show_archived": "Συμπεριλάβετε Αρχειοθετημένα",
+  "map_settings_include_show_partners": "Συμπεριλάβετε Συντρόφους",
+  "map_settings_only_relative_range": "Εύρος ημερομηνιών",
+  "map_settings_only_show_favorites": "Εμφάνιση μόνο αγαπημένων",
+  "map_settings_theme_settings": "Θέμα χάρτη",
+  "map_zoom_to_see_photos": "Σμικρύνετε για να δείτε φωτογραφίες",
+  "memories_all_caught_up": "Συγχρονισμένα",
+  "memories_check_back_tomorrow": "Ελέγξτε ξανά αύριο για περισσότερες αναμνήσεις",
+  "memories_start_over": "Ξεκινήστε από την αρχή",
+  "memories_swipe_to_close": "Σύρετε προς τα πάνω για να κλείσετε",
+  "memories_year_ago": "Πριν ένα χρόνο",
+  "memories_years_ago": "Πριν από {} χρόνια",
+  "monthly_title_text_date_format": "ΜΜΜΜ y",
+  "motion_photos_page_title": "Κινούμενες Φωτογραφίες",
+  "multiselect_grid_edit_date_time_err_read_only": "Δεν είναι δυνατή η επεξεργασία της ημερομηνίας των στοιχείων μόνο για ανάγνωση, παραλείπεται",
+  "multiselect_grid_edit_gps_err_read_only": "Δεν είναι δυνατή η επεξεργασία της τοποθεσίας των στοιχείων μόνο για ανάγνωση, παραλείπεται",
+  "my_albums": "Τα άλμπουμ μου",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
+  "no_assets_to_show": "Δεν υπάρχουν στοιχεία προς εμφάνιση",
+  "no_name": "Κανένα όνομα",
+  "notification_permission_dialog_cancel": "Ακύρωση",
+  "notification_permission_dialog_content": "Για να ενεργοποιήσετε τις ειδοποιήσεις, μεταβείτε στις Ρυθμίσεις και επιλέξτε να επιτρέπεται.",
+  "notification_permission_dialog_settings": "Ρυθμίσεις",
+  "notification_permission_list_tile_content": "Παραχωρήστε άδεια για ενεργοποίηση ειδοποιήσεων.",
+  "notification_permission_list_tile_enable_button": "Ενεργοποίηση Ειδοποιήσεων",
+  "notification_permission_list_tile_title": "Άδεια Ειδοποίησης",
+  "on_this_device": "Σε αυτή τη συσκευή",
+  "partner_list_user_photos": "Φωτογραφίες του/της {user}",
+  "partner_list_view_all": "Προβολή όλων",
   "partner_page_add_partner": "Προσθήκη συντρόφου",
   "partner_page_empty_message": "Οι φωτογραφίες σας δεν διαμοιράζονται ακόμα με κανέναν.",
   "partner_page_no_more_users": "Δεν υπάρχουν άλλοι χρήστες για προσθήκη",
@@ -361,232 +410,250 @@
   "partner_page_stop_sharing_content": "Ο/Η {} δεν θα μπορεί πλέον να δει τις φωτογραφίες σας.",
   "partner_page_stop_sharing_title": "Θέλετε να σταματήσετε να μοιράζεστε τις φωτογραφίες σας;",
   "partner_page_title": "Σύντροφος",
+  "partners": "Σύντροφοι",
+  "people": "Ανθρωποι",
   "permission_onboarding_back": "Πίσω",
-  "permission_onboarding_continue_anyway": "Continue anyway",
-  "permission_onboarding_get_started": "Get started",
-  "permission_onboarding_go_to_settings": "Go to settings",
-  "permission_onboarding_grant_permission": "Grant permission",
-  "permission_onboarding_log_out": "Log out",
-  "permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.",
-  "permission_onboarding_permission_granted": "Permission granted! You are all set.",
-  "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
-  "permission_onboarding_request": "Immich requires permission to view your photos and videos.",
-  "preferences_settings_title": "Preferences",
-  "profile_drawer_app_logs": "Logs",
-  "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
-  "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
-  "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
-  "profile_drawer_documentation": "Documentation",
+  "permission_onboarding_continue_anyway": "Συνέχεια",
+  "permission_onboarding_get_started": "Ξεκινήστε",
+  "permission_onboarding_go_to_settings": "Μεταβείτε στις ρυθμίσεις",
+  "permission_onboarding_grant_permission": "Χορήγηση άδειας",
+  "permission_onboarding_log_out": "Αποσυνδεθείτε",
+  "permission_onboarding_permission_denied": "Η άδεια απορρίφθηκε. Για να χρησιμοποιήσετε το Immich, παραχωρήστε δικαιώματα φωτογραφίας και βίντεο στις Ρυθμίσεις.",
+  "permission_onboarding_permission_granted": "Δόθηκε άδεια! Είστε έτοιμοι.",
+  "permission_onboarding_permission_limited": "Περιορισμένη άδεια. Για να επιτρέψετε στο Immich να δημιουργεί αντίγραφα ασφαλείας και να διαχειρίζεται ολόκληρη τη συλλογή σας, παραχωρήστε άδειες φωτογραφιών και βίντεο στις Ρυθμίσεις.",
+  "permission_onboarding_request": "Το Immich απαιτεί άδεια πρόσβασεις στις φωτογραφίες και τα βίντεό σας.",
+  "places": "Μέρη",
+  "preferences_settings_subtitle": "Manage the app's preferences",
+  "preferences_settings_title": "Προτιμήσεις",
+  "profile_drawer_app_logs": "Καταγραφές",
+  "profile_drawer_client_out_of_date_major": "Παρακαλώ ενημερώστε την εφαρμογή στην πιο πρόσφατη κύρια έκδοση.",
+  "profile_drawer_client_out_of_date_minor": "Παρακαλώ ενημερώστε την εφαρμογή στην πιο πρόσφατη δευτερεύουσα έκδοση.",
+  "profile_drawer_client_server_up_to_date": "Ο πελάτης και ο διακομιστής είναι ενημερωμένοι",
+  "profile_drawer_documentation": "Οδηγίες Χρήσης",
   "profile_drawer_github": "GitHub",
-  "profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.",
-  "profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.",
-  "profile_drawer_settings": "Settings",
-  "profile_drawer_sign_out": "Sign Out",
-  "profile_drawer_trash": "Trash",
-  "recently_added_page_title": "Recently Added",
-  "save_to_gallery": "Save to gallery",
-  "scaffold_body_error_occurred": "Error occurred",
-  "search_bar_hint": "Search your photos",
-  "search_filter_apply": "Apply filter",
-  "search_filter_camera": "Camera",
-  "search_filter_camera_make": "Make",
-  "search_filter_camera_model": "Model",
-  "search_filter_camera_title": "Select camera type",
-  "search_filter_date": "Date",
-  "search_filter_date_interval": "{start} to {end}",
-  "search_filter_date_title": "Select a date range",
-  "search_filter_display_option_archive": "Archive",
-  "search_filter_display_option_favorite": "Favorite",
-  "search_filter_display_option_not_in_album": "Not in album",
-  "search_filter_display_options": "Display Options",
-  "search_filter_display_options_title": "Display options",
-  "search_filter_location": "Location",
-  "search_filter_location_city": "City",
-  "search_filter_location_country": "Country",
-  "search_filter_location_state": "State",
-  "search_filter_location_title": "Select location",
-  "search_filter_media_type": "Media Type",
-  "search_filter_media_type_all": "All",
-  "search_filter_media_type_image": "Image",
-  "search_filter_media_type_title": "Select media type",
-  "search_filter_media_type_video": "Video",
-  "search_filter_people": "People",
-  "search_filter_people_title": "Select people",
-  "search_page_categories": "Categories",
-  "search_page_favorites": "Favorites",
-  "search_page_motion_photos": "Motion Photos",
-  "search_page_no_objects": "No Objects Info Available",
-  "search_page_no_places": "No Places Info Available",
+  "profile_drawer_server_out_of_date_major": "Παρακαλώ ενημερώστε τον διακομιστή στην πιο πρόσφατη κύρια έκδοση.",
+  "profile_drawer_server_out_of_date_minor": "Παρακαλώ ενημερώστε τον διακομιστή στην πιο πρόσφατη δευτερεύουσα έκδοση.",
+  "profile_drawer_settings": "Ρυθμίσεις",
+  "profile_drawer_sign_out": "Αποσύνδεση",
+  "profile_drawer_trash": "Σκουπίδια",
+  "recently_added": "Προστέθηκαν πρόσφατα",
+  "recently_added_page_title": "Προστέθηκαν Πρόσφατα",
+  "save": "Save",
+  "save_to_gallery": "Αποθήκευση στη συλλογή",
+  "scaffold_body_error_occurred": "Παρουσιάστηκε σφάλμα",
+  "search_albums": "Αναζήτηση άλμπουμ",
+  "search_bar_hint": "Αναζητήστε τις φωτογραφίες σας",
+  "search_filter_apply": "Εφαρμογή φίλτρου",
+  "search_filter_camera": "Κάμερα",
+  "search_filter_camera_make": "Μάρκα",
+  "search_filter_camera_model": "Μοντέλο",
+  "search_filter_camera_title": "Επιλέξτε τύπο κάμερας",
+  "search_filter_date": "Ημερομηνία",
+  "search_filter_date_interval": "{start} έως {end}",
+  "search_filter_date_title": "Επιλέξτε εύρος ημερομηνιών",
+  "search_filter_display_option_archive": "Αρχείο",
+  "search_filter_display_option_favorite": "Αγαπημένο",
+  "search_filter_display_option_not_in_album": "Όχι στο άλμπουμ",
+  "search_filter_display_options": "Επιλογές εμφάνισης",
+  "search_filter_display_options_title": "Επιλογές εμφάνισης",
+  "search_filter_location": "Τοποθεσία",
+  "search_filter_location_city": "Πόλη",
+  "search_filter_location_country": "Χώρα",
+  "search_filter_location_state": "Πολιτεία",
+  "search_filter_location_title": "Επιλέξτε τοποθεσία",
+  "search_filter_media_type": "Τύπος Μέσου",
+  "search_filter_media_type_all": "Όλα",
+  "search_filter_media_type_image": "Εικόνα",
+  "search_filter_media_type_title": "Επιλέξτε τύπο μέσου",
+  "search_filter_media_type_video": "Βίντεο",
+  "search_filter_people": "Ανθρωποι",
+  "search_filter_people_title": "Επιλέξτε άτομα",
+  "search_page_categories": "Κατηγορίες",
+  "search_page_favorites": "Αγαπημένα",
+  "search_page_motion_photos": "Κινούμενες Φωτογραφίες",
+  "search_page_no_objects": "Μη διαθέσιμες πληροφορίες αντικειμένων",
+  "search_page_no_places": "Μη διαθέσιμες πληροφορίες για μέρη",
   "search_page_people": "Άτομα",
-  "search_page_person_add_name_dialog_cancel": "Cancel",
-  "search_page_person_add_name_dialog_hint": "Name",
-  "search_page_person_add_name_dialog_save": "Save",
-  "search_page_person_add_name_dialog_title": "Add a name",
-  "search_page_person_add_name_subtitle": "Find them fast by name with search",
-  "search_page_person_add_name_title": "Add a name",
-  "search_page_person_edit_name": "Edit name",
-  "search_page_places": "Places",
-  "search_page_recently_added": "Recently added",
-  "search_page_screenshots": "Screenshots",
-  "search_page_selfies": "Selfies",
-  "search_page_things": "Things",
-  "search_page_videos": "Videos",
-  "search_page_view_all_button": "View all",
-  "search_page_your_activity": "Your activity",
-  "search_page_your_map": "Your Map",
-  "search_result_page_new_search_hint": "New Search",
-  "search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ",
-  "search_suggestion_list_smart_search_hint_2": "m:your-search-term",
-  "select_additional_user_for_sharing_page_suggestions": "Suggestions",
-  "select_user_for_sharing_page_err_album": "Failed to create album",
-  "select_user_for_sharing_page_share_suggestions": "Suggestions",
-  "server_info_box_app_version": "App Version",
+  "search_page_person_add_name_dialog_cancel": "Ακύρωση",
+  "search_page_person_add_name_dialog_hint": "Όνομα",
+  "search_page_person_add_name_dialog_save": "Αποθήκευση",
+  "search_page_person_add_name_dialog_title": "Προσθέστε όνομα",
+  "search_page_person_add_name_subtitle": "Βρείτε τα γρήγορα ονομαστικά με αναζήτηση",
+  "search_page_person_add_name_title": "Προσθέστε ένα όνομα",
+  "search_page_person_edit_name": "Επεξεργασία ονόματος",
+  "search_page_places": "Μέρη",
+  "search_page_recently_added": "Προστέθηκε πρόσφατα",
+  "search_page_screenshots": "Στιγμιότυπα οθόνης",
+  "search_page_search_photos_videos": "Search for your photos and videos",
+  "search_page_selfies": "Σέλφι",
+  "search_page_things": "Πράγματα",
+  "search_page_videos": "Βίντεο",
+  "search_page_view_all_button": "Προβολή όλων",
+  "search_page_your_activity": "Η δραστηριότητά σας",
+  "search_page_your_map": "Ο χάρτης σας",
+  "search_result_page_new_search_hint": "Νέα Αναζήτηση",
+  "search_suggestion_list_smart_search_hint_1": "Η έξυπνη αναζήτηση είναι ενεργοποιημένη από προεπιλογή, για αναζήτηση μεταδεδομένων χρησιμοποιήστε το συντακτικό",
+  "search_suggestion_list_smart_search_hint_2": "m:όρος-αναζήτησης",
+  "select_additional_user_for_sharing_page_suggestions": "Προτάσεις",
+  "select_user_for_sharing_page_err_album": "Αποτυχία δημιουργίας άλπουμ",
+  "select_user_for_sharing_page_share_suggestions": "Προτάσεις",
+  "server_endpoint": "Server Endpoint",
+  "server_info_box_app_version": "Έκδοση εφαρμογής",
   "server_info_box_latest_release": "Τελευταία Έκδοση",
-  "server_info_box_server_url": "Server URL",
-  "server_info_box_server_version": "Server Version",
-  "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
-  "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).",
-  "setting_image_viewer_original_title": "Load original image",
-  "setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.",
-  "setting_image_viewer_preview_title": "Load preview image",
-  "setting_image_viewer_title": "Images",
-  "setting_languages_apply": "Apply",
-  "setting_languages_title": "Languages",
-  "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
-  "setting_notifications_notify_hours": "{} hours",
-  "setting_notifications_notify_immediately": "immediately",
-  "setting_notifications_notify_minutes": "{} minutes",
-  "setting_notifications_notify_never": "never",
-  "setting_notifications_notify_seconds": "{} seconds",
-  "setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset",
-  "setting_notifications_single_progress_title": "Show background backup detail progress",
-  "setting_notifications_subtitle": "Adjust your notification preferences",
-  "setting_notifications_title": "Notifications",
-  "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
-  "setting_notifications_total_progress_title": "Show background backup total progress",
-  "setting_pages_app_bar_settings": "Settings",
-  "settings_require_restart": "Please restart Immich to apply this setting",
-  "setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
-  "setting_video_viewer_looping_title": "Looping",
-  "setting_video_viewer_title": "Videos",
-  "share_add": "Add",
-  "share_add_photos": "Add photos",
-  "share_add_title": "Add a title",
-  "share_assets_selected": "{} selected",
-  "share_create_album": "Create album",
+  "server_info_box_server_url": "URL διακομιστή",
+  "server_info_box_server_version": "Έκδοση Διακομιστή",
+  "setting_image_viewer_help": "Το πρόγραμμα προβολής λεπτομερειών φορτώνει πρώτα τη μικρογραφία, στη συνέχεια φορτώνει την προεπισκόπηση μεσαίου μεγέθους (αν είναι ενεργοποιημένη), τέλος φορτώνει το πρωτότυπο (αν είναι ενεργοποιημένο).",
+  "setting_image_viewer_original_subtitle": "Ενεργοποιήστε τη φόρτωση της πρωτότυπης εικόνας πλήρους ανάλυσης (μεγάλη!). Απενεργοποιήστε για να μειώσετε τη χρήση δεδομένων (τόσο στο δίκτυο όσο και στην κρυφή μνήμη της συσκευής).",
+  "setting_image_viewer_original_title": "Φόρτωση πρωτότυπης εικόνας",
+  "setting_image_viewer_preview_subtitle": "Ενεργοποιήστε τη φόρτωση μιας εικόνας μέσης ανάλυσης. Απενεργοποιήστε είτε για να φορτώνεται απευθείας το πρωτότυπο είτε για να χρησιμοποιείται μόνο η μικρογραφία.",
+  "setting_image_viewer_preview_title": "Φόρτωση εικόνας προεπισκόπησης",
+  "setting_image_viewer_title": "Εικόνες",
+  "setting_languages_apply": "Εφαρμογή",
+  "setting_languages_subtitle": "Change the app's language",
+  "setting_languages_title": "Γλώσσες",
+  "setting_notifications_notify_failures_grace_period": "Ειδοποίηση αποτυχιών δημιουργίας αντιγράφων ασφαλείας στο παρασκήνιο: {}",
+  "setting_notifications_notify_hours": "{} ώρες",
+  "setting_notifications_notify_immediately": "αμέσως",
+  "setting_notifications_notify_minutes": "{} λεπτά",
+  "setting_notifications_notify_never": "ποτέ",
+  "setting_notifications_notify_seconds": "{} δευτερόλεπτα",
+  "setting_notifications_single_progress_subtitle": "Λεπτομερείς πληροφορίες προόδου μεταφόρτωσης ανά στοιχείο",
+  "setting_notifications_single_progress_title": "Εμφάνιση προόδου λεπτομερειών δημιουργίας αντιγράφων ασφαλείας παρασκηνίου",
+  "setting_notifications_subtitle": "Προσαρμόστε τις προτιμήσεις ειδοποίησης",
+  "setting_notifications_title": "Ειδοποιήσεις",
+  "setting_notifications_total_progress_subtitle": "Συνολική πρόοδος μεταφόρτωσης (ολοκληρώθηκε/σύνολο στοιχείων)",
+  "setting_notifications_total_progress_title": "Εμφάνιση συνολικής προόδου δημιουργίας αντιγράφων ασφαλείας παρασκηνίου",
+  "setting_pages_app_bar_settings": "Ρυθμίσεις",
+  "settings_require_restart": "Επανεκκινήστε το Immich για να εφαρμόσετε αυτήν τη ρύθμιση",
+  "setting_video_viewer_looping_subtitle": "Ενεργοποιήστε για το αυτόματη συνεχής επανάληψη βίντεο στο πρόγραμμα προβολής λεπτομερειών.",
+  "setting_video_viewer_looping_title": "Συνεχής Επανάληψη",
+  "setting_video_viewer_title": "Βίντεο",
+  "share_add": "Πρόσθεση",
+  "share_add_photos": "Προσθήκη φωτογραφιών",
+  "share_add_title": "Προσθέστε έναν τίτλο",
+  "share_assets_selected": "{} επιλεγμένα",
+  "share_create_album": "Δημιουργία άλμπουμ",
   "shared_album_activities_input_disable": "Το σχόλιο είναι απενεργοποιημένο",
-  "shared_album_activities_input_hint": "Say something",
-  "shared_album_activity_remove_content": "Do you want to delete this activity?",
-  "shared_album_activity_remove_title": "Delete Activity",
+  "shared_album_activities_input_hint": "Πες κάτι",
+  "shared_album_activity_remove_content": "Θέλετε να διαγράψετε αυτήν τη δραστηριότητα;",
+  "shared_album_activity_remove_title": "Διαγραφή Δραστηριότητας",
   "shared_album_activity_setting_subtitle": "Επέτρεψε σε άλλους να απαντάνε",
-  "shared_album_activity_setting_title": "Comments & likes",
-  "shared_album_section_people_action_error": "Error leaving/removing from album",
-  "shared_album_section_people_action_leave": "Remove user from album",
-  "shared_album_section_people_action_remove_user": "Remove user from album",
-  "shared_album_section_people_owner_label": "Owner",
-  "shared_album_section_people_title": "PEOPLE",
-  "share_dialog_preparing": "Preparing...",
-  "shared_link_app_bar_title": "Shared Links",
-  "shared_link_clipboard_copied_massage": "Copied to clipboard",
-  "shared_link_clipboard_text": "Link: {}\nPassword: {}",
-  "shared_link_create_app_bar_title": "Create link to share",
-  "shared_link_create_error": "Error while creating shared link",
-  "shared_link_create_info": "Let anyone with the link see the selected photo(s)",
-  "shared_link_create_submit_button": "Create link",
-  "shared_link_edit_allow_download": "Allow public user to download",
-  "shared_link_edit_allow_upload": "Allow public user to upload",
-  "shared_link_edit_app_bar_title": "Edit link",
-  "shared_link_edit_change_expiry": "Change expiration time",
-  "shared_link_edit_description": "Description",
-  "shared_link_edit_description_hint": "Enter the share description",
+  "shared_album_activity_setting_title": "Σχόλια & likes",
+  "shared_album_section_people_action_error": "Σφάλμα αποχώρησης/κατάργησης από το άλμπουμ",
+  "shared_album_section_people_action_leave": "Αποχώρηση χρήστη από το άλμπουμ",
+  "shared_album_section_people_action_remove_user": "Κατάργηση χρήστη από το άλμπουμ",
+  "shared_album_section_people_owner_label": "Ιδιοκτήτης",
+  "shared_album_section_people_title": "ΑΝΘΡΩΠΟΙ",
+  "share_dialog_preparing": "Προετοιμασία...",
+  "shared_link_app_bar_title": "Κοινόχρηστοι Σύνδεσμοι",
+  "shared_link_clipboard_copied_massage": "Αντιγράφηκε στο πρόχειρο",
+  "shared_link_clipboard_text": "Σύνδεσμος: {}\nΚωδικός πρόσβασης: {}",
+  "shared_link_create_app_bar_title": "Δημιουργία συνδέσμου για κοινή χρήση",
+  "shared_link_create_error": "Σφάλμα κατά τη δημιουργία κοινόχρηστου συνδέσμου",
+  "shared_link_create_info": "Να επιτρέπεται σε οποιονδήποτε έχει τον σύνδεσμο να δει τις επιλεγμένες φωτογραφίες",
+  "shared_link_create_submit_button": "Δημιουργία συνδέσμου",
+  "shared_link_edit_allow_download": "Να επιτρέπεται η λήψη απο δημόσιο χρήστη",
+  "shared_link_edit_allow_upload": "Να επιτρέπεται η μεταφόρτωση απο δημόσιο χρήστη",
+  "shared_link_edit_app_bar_title": "Επεξεργασία συνδέσμου",
+  "shared_link_edit_change_expiry": "Αλλαγή χρόνου λήξης",
+  "shared_link_edit_description": "Περιγραφή",
+  "shared_link_edit_description_hint": "Εισαγάγετε την περιγραφή της κοινής χρήσης",
   "shared_link_edit_expire_after": "Λήξη μετά από",
-  "shared_link_edit_expire_after_option_day": "1 day",
-  "shared_link_edit_expire_after_option_days": "{} days",
-  "shared_link_edit_expire_after_option_hour": "1 hour",
-  "shared_link_edit_expire_after_option_hours": "{} hours",
-  "shared_link_edit_expire_after_option_minute": "1 minute",
-  "shared_link_edit_expire_after_option_minutes": "{} minutes",
-  "shared_link_edit_expire_after_option_months": "{} months",
-  "shared_link_edit_expire_after_option_never": "Never",
-  "shared_link_edit_expire_after_option_year": "{} year",
-  "shared_link_edit_password": "Password",
-  "shared_link_edit_password_hint": "Enter the share password",
-  "shared_link_edit_show_meta": "Show metadata",
-  "shared_link_edit_submit_button": "Update link",
-  "shared_link_empty": "You don't have any shared links",
-  "shared_link_error_server_url_fetch": "Cannot fetch the server url",
-  "shared_link_expired": "Expired",
-  "shared_link_expires_day": "Expires in {} day",
-  "shared_link_expires_days": "Expires in {} days",
-  "shared_link_expires_hour": "Expires in {} hour",
-  "shared_link_expires_hours": "Expires in {} hours",
-  "shared_link_expires_minute": "Expires in {} minute",
-  "shared_link_expires_minutes": "Expires in {} minutes",
-  "shared_link_expires_never": "Expires ∞",
-  "shared_link_expires_second": "Expires in {} second",
-  "shared_link_expires_seconds": "Expires in {} seconds",
-  "shared_link_individual_shared": "Individual shared",
-  "shared_link_info_chip_download": "Download",
+  "shared_link_edit_expire_after_option_day": "1 ημέρα",
+  "shared_link_edit_expire_after_option_days": "{} ημέρες",
+  "shared_link_edit_expire_after_option_hour": "1 ώρα",
+  "shared_link_edit_expire_after_option_hours": "{} ώρες",
+  "shared_link_edit_expire_after_option_minute": "1 λεπτό",
+  "shared_link_edit_expire_after_option_minutes": "{} λεπτά",
+  "shared_link_edit_expire_after_option_months": "{} μήνες",
+  "shared_link_edit_expire_after_option_never": "Ποτέ",
+  "shared_link_edit_expire_after_option_year": "{} έτος",
+  "shared_link_edit_password": "Κωδικός πρόσβασης",
+  "shared_link_edit_password_hint": "Εισαγάγετε τον κωδικό πρόσβασης κοινής χρήσης",
+  "shared_link_edit_show_meta": "Εμφάνιση μεταδεδομένων",
+  "shared_link_edit_submit_button": "Ενημέρωση συνδέσμου",
+  "shared_link_empty": "Δεν έχετε κοινόχρηστους συνδέσμους",
+  "shared_link_error_server_url_fetch": "Δεν είναι δυνατή η ανάκτηση του URL του διακομιστή",
+  "shared_link_expired": "Έληξε",
+  "shared_link_expires_day": "Λήγει σε {} ημέρα",
+  "shared_link_expires_days": "Λήγει σε {} ημέρες",
+  "shared_link_expires_hour": "Λήγει σε {} ώρα",
+  "shared_link_expires_hours": "Λήγει σε {} ώρες",
+  "shared_link_expires_minute": "Λήγει σε {} λεπτό",
+  "shared_link_expires_minutes": "Λήγει σε {} λεπτά",
+  "shared_link_expires_never": "Λήγει ∞",
+  "shared_link_expires_second": "Λήγει σε {} δευτερόλεπτο",
+  "shared_link_expires_seconds": "Λήγει σε {} δευτερόλεπτα",
+  "shared_link_individual_shared": "Μεμονωμένο κοινό",
+  "shared_link_info_chip_download": "Λήψη",
   "shared_link_info_chip_metadata": "EXIF",
-  "shared_link_info_chip_upload": "Upload",
-  "shared_link_manage_links": "Manage Shared links",
-  "shared_link_public_album": "Public album",
-  "share_done": "Done",
-  "share_invite": "Invite to album",
-  "sharing_page_album": "Shared albums",
-  "sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
-  "sharing_page_empty_list": "EMPTY LIST",
-  "sharing_silver_appbar_create_shared_album": "New shared album",
-  "sharing_silver_appbar_shared_links": "Shared links",
-  "sharing_silver_appbar_share_partner": "Share with partner",
-  "sync": "Sync",
-  "sync_albums": "Sync albums",
-  "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
-  "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
-  "tab_controller_nav_library": "Library",
-  "tab_controller_nav_photos": "Photos",
-  "tab_controller_nav_search": "Search",
-  "tab_controller_nav_sharing": "Sharing",
-  "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles",
-  "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})",
-  "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.",
-  "theme_setting_colorful_interface_title": "Colorful interface",
-  "theme_setting_dark_mode_switch": "Dark mode",
-  "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer",
-  "theme_setting_image_viewer_quality_title": "Image viewer quality",
-  "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.",
-  "theme_setting_primary_color_title": "Primary color",
-  "theme_setting_system_primary_color_title": "Use system color",
-  "theme_setting_system_theme_switch": "Automatic (Follow system setting)",
-  "theme_setting_theme_subtitle": "Choose the app's theme setting",
-  "theme_setting_theme_title": "Theme",
-  "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
-  "theme_setting_three_stage_loading_title": "Enable three-stage loading",
-  "translated_text_options": "Options",
-  "trash_emptied": "Emptied trash",
-  "trash_page_delete": "Delete",
-  "trash_page_delete_all": "Delete All",
-  "trash_page_empty_trash_btn": "Empty trash",
-  "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich",
-  "trash_page_empty_trash_dialog_ok": "Ok",
-  "trash_page_info": "Trashed items will be permanently deleted after {} days",
-  "trash_page_no_assets": "No trashed assets",
-  "trash_page_restore": "Restore",
-  "trash_page_restore_all": "Restore All",
-  "trash_page_select_assets_btn": "Select assets",
-  "trash_page_select_btn": "Select",
-  "trash_page_title": "Trash ({})",
+  "shared_link_info_chip_upload": "Μεταφόρτωση",
+  "shared_link_manage_links": "Διαχείριση Κοινόχρηστων Συνδέσμων",
+  "shared_link_public_album": "Δημόσιο άλμπουμ",
+  "shared_links": "Κοινόχρηστοι σύνδεσμοι",
+  "share_done": "Τέλος",
+  "shared_with_me": "Μοιρασμένα μαζί μου",
+  "share_invite": "Πρόσκληση σε άλμπουμ",
+  "sharing_page_album": "Κοινόχρηστα άλμπουμ",
+  "sharing_page_description": "Δημιουργήστε κοινόχρηστα άλμπουμ για να μοιράζεστε φωτογραφίες και βίντεο με άτομα στο δίκτυό σας.",
+  "sharing_page_empty_list": "ΚΕΝΗ ΛΙΣΤΑ",
+  "sharing_silver_appbar_create_shared_album": "Νέο κοινόχρηστο άλμπουμ",
+  "sharing_silver_appbar_shared_links": "Κοινόχρηστοι σύνδεσμοι",
+  "sharing_silver_appbar_share_partner": "Μοιραστείτε με τον συνεργάτη",
+  "sync": "Συγχρονισμός",
+  "sync_albums": "Συγχρονισμός άλμπουμ",
+  "sync_albums_manual_subtitle": "Συγχρονίστε όλα τα μεταφορτωμένα βίντεο και φωτογραφίες με τα επιλεγμένα εφεδρικά άλμπουμ",
+  "sync_upload_album_setting_subtitle": "Δημιουργήστε και ανεβάστε τις φωτογραφίες και τα βίντεό σας στα επιλεγμένα άλμπουμ στο Immich",
+  "tab_controller_nav_library": "Βιβλιοθήκη",
+  "tab_controller_nav_photos": "Φωτογραφίες",
+  "tab_controller_nav_search": "Αναζήτηση",
+  "tab_controller_nav_sharing": "Κοινή Χρήση",
+  "theme_setting_asset_list_storage_indicator_title": "Εμφάνιση ένδειξης αποθήκευσης σε πλακίδια στοιχείων",
+  "theme_setting_asset_list_tiles_per_row_title": "Αριθμός στοιχείων ανά σειρά ({})",
+  "theme_setting_colorful_interface_subtitle": "Εφαρμόστε βασικό χρώμα σε επιφάνειες φόντου.",
+  "theme_setting_colorful_interface_title": "Πολύχρωμη διεπαφή",
+  "theme_setting_dark_mode_switch": "Σκοτεινή λειτουργία",
+  "theme_setting_image_viewer_quality_subtitle": "Προσαρμόστε την ποιότητα του προγράμματος προβολής εικόνας λεπτομερειών",
+  "theme_setting_image_viewer_quality_title": "Ποιότητα προβολής εικόνων",
+  "theme_setting_primary_color_subtitle": "Επιλέξτε ένα χρώμα για κύριες ενέργειες και τόνους.",
+  "theme_setting_primary_color_title": "Πρωταρχικό χρώμα",
+  "theme_setting_system_primary_color_title": "Χρησιμοποιήστε το χρώμα συστήματος",
+  "theme_setting_system_theme_switch": "Αυτόματο (Ακολουθήστε τη ρύθμιση συστήματος)",
+  "theme_setting_theme_subtitle": "Επιλέξτε τη ρύθμιση θέματος της εφαρμογής",
+  "theme_setting_theme_title": "Θέμα",
+  "theme_setting_three_stage_loading_subtitle": "Η φόρτωση τριών σταδίων μπορεί να αυξήσει την απόδοση φόρτωσης, αλλά προκαλεί σημαντικά υψηλότερο φόρτο δικτύου",
+  "theme_setting_three_stage_loading_title": "Ενεργοποιήστε τη φόρτωση τριών σταδίων",
+  "translated_text_options": "Επιλογές",
+  "trash": "Σκουπίδια",
+  "trash_emptied": "Αδειάστηκαν τα σκουπίδια",
+  "trash_page_delete": "Διαγραφή",
+  "trash_page_delete_all": "Διαγραφή όλων",
+  "trash_page_empty_trash_btn": "Αδειάστε τα σκουπίδια",
+  "trash_page_empty_trash_dialog_content": "Θέλετε να αδειάσετε τα περιουσιακά σας στοιχεία στον κάδο απορριμμάτων; Αυτά τα στοιχεία θα καταργηθούν οριστικά από το Immich",
+  "trash_page_empty_trash_dialog_ok": "Εντάξει",
+  "trash_page_info": "Τα στοιχεία που έχουν απορριφθεί θα διαγραφούν οριστικά μετά από {} ημέρες",
+  "trash_page_no_assets": "Δεν υπάρχουν περιουσιακά στοιχεία που έχουν απορριφθεί",
+  "trash_page_restore": "Επαναφορά",
+  "trash_page_restore_all": "Επαναφορά Όλων",
+  "trash_page_select_assets_btn": "Επιλέξτε στοιχεία",
+  "trash_page_select_btn": "Επιλογή",
+  "trash_page_title": "Κάδος Απορριμμάτων ({})",
   "upload_dialog_cancel": "Ακύρωση",
   "upload_dialog_info": "Θέλετε να αντιγράψετε (κάνετε backup) τα επιλεγμένo(α) στοιχείο(α) στο διακομιστή;",
   "upload_dialog_ok": "Ανέβασμα",
   "upload_dialog_title": "Ανέβασμα στοιχείου",
-  "version_announcement_overlay_ack": "Acknowledge",
-  "version_announcement_overlay_release_notes": "release notes",
-  "version_announcement_overlay_text_1": "Hi friend, there is a new release of",
-  "version_announcement_overlay_text_2": "please take your time to visit the ",
-  "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
-  "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
-  "viewer_remove_from_stack": "Remove from Stack",
-  "viewer_stack_use_as_main_asset": "Use as Main Asset",
-  "viewer_unstack": "Un-Stack"
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
+  "version_announcement_overlay_ack": "Κατάλαβα",
+  "version_announcement_overlay_release_notes": "σημειώσεις έκδοσης",
+  "version_announcement_overlay_text_1": "Γειά σας, υπάρχει μια νέα έκδοση του",
+  "version_announcement_overlay_text_2": "παρακαλώ αφιερώστε χρόνο να επισκεφθείτε το",
+  "version_announcement_overlay_text_3": " και βεβαιωθείτε ότι το docker-compose και το .env σας είναι ενημερωμένη για την αποφυγή τυχόν εσφαλμένων διαμορφώσεων, ειδικά εάν χρησιμοποιείτε το WatchTower ή οποιονδήποτε μηχανισμό που χειρίζεται την αυτόματη ενημέρωση του διακομιστή σας.",
+  "version_announcement_overlay_title": "Διαθέσιμη νέα έκδοση διακομιστή \uD83C\uDF89",
+  "videos": "Βίντεο",
+  "viewer_remove_from_stack": "Κατάργηση από τη Στοίβα",
+  "viewer_stack_use_as_main_asset": "Χρήση ως Κύριο Στοιχείο",
+  "viewer_unstack": "Αποστοίβαξε",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json
index 324c9069fd..a7f31f8440 100644
--- a/mobile/assets/i18n/en-US.json
+++ b/mobile/assets/i18n/en-US.json
@@ -6,6 +6,8 @@
   "action_common_save": "Save",
   "action_common_select": "Select",
   "action_common_update": "Update",
+  "add_a_name": "Add a name",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Added to {album}",
   "add_to_album_bottom_sheet_already_exists": "Already in {album}",
   "advanced_settings_log_level_title": "Log level: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Troubleshooting",
   "album_info_card_backup_album_excluded": "EXCLUDED",
   "album_info_card_backup_album_included": "INCLUDED",
+  "albums": "Albums",
   "album_thumbnail_card_item": "1 item",
   "album_thumbnail_card_items": "{} items",
   "album_thumbnail_card_shared": " · Shared",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Remove from album",
   "album_viewer_appbar_share_to": "Share To",
   "album_viewer_page_share_add_users": "Add users",
+  "all": "All",
   "all_people_page_title": "People",
   "all_videos_page_title": "Videos",
   "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
   "app_bar_signout_dialog_ok": "Yes",
   "app_bar_signout_dialog_title": "Sign out",
+  "archived": "Archived",
   "archive_page_no_archived_assets": "No archived assets found",
   "archive_page_title": "Archive ({})",
   "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} asset(s) restored successfully",
   "assets_trashed": "{} asset(s) trashed",
   "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "Asset Viewer",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Albums on device ({})",
   "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
   "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Success",
   "backup_manual_title": "Upload status",
   "backup_options_page_title": "Backup options",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
   "cache_settings_clear_cache_button": "Clear cache",
   "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Control the local storage behaviour",
   "cache_settings_tile_title": "Local Storage",
   "cache_settings_title": "Caching Settings",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Confirm Password",
   "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
   "change_password_form_new_password": "New Password",
   "change_password_form_password_mismatch": "Passwords do not match",
   "change_password_form_reenter_new_password": "Re-enter New Password",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Enter Password",
   "client_cert_import": "Import",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Unarchive",
   "control_bottom_app_bar_unfavorite": "Unfavorite",
   "control_bottom_app_bar_upload": "Upload",
+  "create_album": "Create album",
   "create_album_page_untitled": "Untitled",
+  "create_new": "CREATE NEW",
   "create_shared_album_page_create": "Create",
   "create_shared_album_page_share": "Share",
   "create_shared_album_page_share_add_assets": "ADD ASSETS",
@@ -193,6 +211,7 @@
   "crop": "Crop",
   "curated_location_page_title": "Places",
   "curated_object_page_title": "Things",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "Delete Shared Link",
   "description_input_hint_text": "Add description...",
   "description_input_submit_error": "Error updating description, check the log for more details",
+  "download_canceled": "Download canceled",
+  "download_complete": "Download complete",
+  "download_enqueue": "Download enqueued",
   "download_error": "Download Error",
+  "download_failed": "Download failed",
+  "download_filename": "file: {}",
+  "download_finished": "Download finished",
+  "downloading": "Downloading...",
+  "downloading_media": "Downloading media",
+  "download_notfound": "Download not found",
+  "download_paused": "Download paused",
   "download_started": "Download started",
   "download_sucess": "Download success",
   "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_waiting_to_retry": "Waiting to retry",
   "edit_date_time_dialog_date_time": "Date and Time",
   "edit_date_time_dialog_timezone": "Timezone",
   "edit_image_title": "Edit",
   "edit_location_dialog_title": "Location",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Error: {}",
   "exif_bottom_sheet_description": "Add Description...",
   "exif_bottom_sheet_details": "DETAILS",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Enable experimental photo grid",
   "experimental_settings_subtitle": "Use at your own risk!",
   "experimental_settings_title": "Experimental",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favorites",
   "favorites_page_no_favorites": "No favorite assets found",
   "favorites_page_title": "Favorites",
   "filename_search": "File name or extension",
+  "filter": "Filter",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Enable haptic feedback",
   "haptic_feedback_title": "Haptic Feedback",
   "header_settings_add_header_tip": "Add Header",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
   "home_page_share_err_local": "Can not share local assets via link, skipping",
   "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
+  "ignore_icloud_photos": "Ignore iCloud photos",
+  "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
   "image_saved_successfully": "Image saved",
   "image_viewer_page_state_provider_download_error": "Download Error",
   "image_viewer_page_state_provider_download_started": "Download Started",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "Share Error",
   "invalid_date": "Invalid date",
   "invalid_date_format": "Invalid date format",
+  "library": "Library",
   "library_page_albums": "Albums",
   "library_page_archive": "Archive",
   "library_page_device_albums": "Albums on Device",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Oldest photo",
   "library_page_sort_most_recent_photo": "Most recent photo",
   "library_page_sort_title": "Album title",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Choose on map",
   "location_picker_latitude": "Latitude",
   "location_picker_latitude_error": "Enter a valid latitude",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Motion Photos",
   "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
   "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
+  "my_albums": "My albums",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "No assets to show",
   "no_name": "No name",
   "notification_permission_dialog_cancel": "Cancel",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Grant permission to enable notifications.",
   "notification_permission_list_tile_enable_button": "Enable Notifications",
   "notification_permission_list_tile_title": "Notification Permission",
+  "on_this_device": "On this device",
   "partner_list_user_photos": "{user}'s photos",
   "partner_list_view_all": "View all",
   "partner_page_add_partner": "Add partner",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
   "partner_page_stop_sharing_title": "Stop sharing your photos?",
   "partner_page_title": "Partner",
+  "partners": "Partners",
+  "people": "People",
   "permission_onboarding_back": "Back",
   "permission_onboarding_continue_anyway": "Continue anyway",
   "permission_onboarding_get_started": "Get started",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Permission granted! You are all set.",
   "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
   "permission_onboarding_request": "Immich requires permission to view your photos and videos.",
+  "places": "Places",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Preferences",
   "profile_drawer_app_logs": "Logs",
   "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Settings",
   "profile_drawer_sign_out": "Sign Out",
   "profile_drawer_trash": "Trash",
+  "recently_added": "Recently added",
   "recently_added_page_title": "Recently Added",
+  "save": "Save",
   "save_to_gallery": "Save to gallery",
   "scaffold_body_error_occurred": "Error occurred",
+  "search_albums": "Search albums",
   "search_bar_hint": "Search your photos",
   "search_filter_apply": "Apply filter",
   "search_filter_camera": "Camera",
@@ -428,6 +484,7 @@
   "search_page_places": "Places",
   "search_page_recently_added": "Recently added",
   "search_page_screenshots": "Screenshots",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Selfies",
   "search_page_things": "Things",
   "search_page_videos": "Videos",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Suggestions",
   "select_user_for_sharing_page_err_album": "Failed to create album",
   "select_user_for_sharing_page_share_suggestions": "Suggestions",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "App Version",
   "server_info_box_latest_release": "Latest Version",
   "server_info_box_server_url": "Server URL",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Load preview image",
   "setting_image_viewer_title": "Images",
   "setting_languages_apply": "Apply",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Languages",
   "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
   "setting_notifications_notify_hours": "{} hours",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Upload",
   "shared_link_manage_links": "Manage Shared links",
   "shared_link_public_album": "Public album",
+  "shared_links": "Shared links",
   "share_done": "Done",
+  "shared_with_me": "Shared with me",
   "share_invite": "Invite to album",
   "sharing_page_album": "Shared albums",
   "sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
   "theme_setting_three_stage_loading_title": "Enable three-stage loading",
   "translated_text_options": "Options",
+  "trash": "Trash",
   "trash_emptied": "Emptied trash",
   "trash_page_delete": "Delete",
   "trash_page_delete_all": "Delete All",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
   "upload_dialog_ok": "Upload",
   "upload_dialog_title": "Upload Asset",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Acknowledge",
   "version_announcement_overlay_release_notes": "release notes",
   "version_announcement_overlay_text_1": "Hi friend, there is a new release of",
   "version_announcement_overlay_text_2": "please take your time to visit the ",
   "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
   "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
+  "videos": "Videos",
   "viewer_remove_from_stack": "Remove from Stack",
   "viewer_stack_use_as_main_asset": "Use as Main Asset",
-  "viewer_unstack": "Un-Stack"
+  "viewer_unstack": "Un-Stack",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/es-ES.json b/mobile/assets/i18n/es-ES.json
index 1943116b4f..d7ddb03fd2 100644
--- a/mobile/assets/i18n/es-ES.json
+++ b/mobile/assets/i18n/es-ES.json
@@ -3,15 +3,17 @@
   "action_common_cancel": "Cancelar",
   "action_common_clear": "Limpiar",
   "action_common_confirm": "Confirmar",
-  "action_common_save": "Save",
-  "action_common_select": "Select",
+  "action_common_save": "Guardar",
+  "action_common_select": "Seleccionar",
   "action_common_update": "Actualizar",
+  "add_a_name": "Añadir nombre",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Agregado a {album}",
   "add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}",
   "advanced_settings_log_level_title": "Nivel de registro: {}",
   "advanced_settings_prefer_remote_subtitle": "Algunos dispositivos tardan mucho en cargar las miniaturas de los elementos encontrados en el dispositivo. Activa esta opción para cargar imágenes remotas en su lugar.",
   "advanced_settings_prefer_remote_title": "Preferir imágenes remotas",
-  "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
+  "advanced_settings_proxy_headers_subtitle": "Configura headers HTTP que Immich incluirá en cada petición de red",
   "advanced_settings_proxy_headers_title": "Proxy Headers",
   "advanced_settings_self_signed_ssl_subtitle": "Omitir verificación del certificado SSL del servidor. Requerido para certificados autofirmados",
   "advanced_settings_self_signed_ssl_title": "Permitir certificados autofirmados",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Solución de problemas",
   "album_info_card_backup_album_excluded": "EXCLUIDOS",
   "album_info_card_backup_album_included": "INCLUIDOS",
+  "albums": "Álbumes",
   "album_thumbnail_card_item": "1 elemento",
   "album_thumbnail_card_items": "{} elementos",
   "album_thumbnail_card_shared": "Compartido",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Eliminar del álbum ",
   "album_viewer_appbar_share_to": "Compartir Con",
   "album_viewer_page_share_add_users": "Agregar usuarios",
+  "all": "Todos",
   "all_people_page_title": "Personas",
   "all_videos_page_title": "Videos",
   "app_bar_signout_dialog_content": "¿Estás seguro que quieres cerrar sesión?",
   "app_bar_signout_dialog_ok": "Sí",
   "app_bar_signout_dialog_title": "Cerrar sesión",
+  "archived": "Archivado",
   "archive_page_no_archived_assets": "No se encontraron elementos archivados",
   "archive_page_title": "Archivo ({})",
   "asset_action_delete_err_read_only": "No se pueden borrar el archivo(s) de solo lectura, omitiendo",
@@ -54,14 +59,19 @@
   "asset_list_layout_sub_title": "Disposición",
   "asset_list_settings_subtitle": "Configuraciones del diseño de la cuadrícula de fotos",
   "asset_list_settings_title": "Cuadrícula de fotos",
-  "asset_restored_successfully": "Asset restored successfully",
-  "assets_deleted_permanently": "{} asset(s) deleted permanently",
+  "asset_restored_successfully": "Elementos restaurados exitosamente",
+  "assets_deleted_permanently": "\n{} elementos(s) eliminado(s) permanentemente",
   "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server",
-  "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device",
-  "assets_restored_successfully": "{} asset(s) restored successfully",
-  "assets_trashed": "{} asset(s) trashed",
-  "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "assets_removed_permanently_from_device": "{} elemento(s) eliminado(s) permanentemente de su dispositivo",
+  "assets_restored_successfully": "{} elemento(s) restaurado(s) exitosamente",
+  "assets_trashed": "{} elemento(s) eliminado(s)",
+  "assets_trashed_from_server": "{} elemento(s) movido a la papelera en Immich",
+  "asset_viewer_settings_subtitle": "Administra las configuracioens de tu visor de fotos",
   "asset_viewer_settings_title": "Visor de Archivos",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})",
   "backup_album_selection_page_albums_tap": "Toque para incluir, doble toque para excluir",
   "backup_album_selection_page_assets_scatter": "Los elementos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Éxito",
   "backup_manual_title": "Estado de la subida",
   "backup_options_page_title": "Opciones de Copia de Seguridad",
+  "backup_setting_subtitle": "Administra las configuraciones de respaldo en segundo y primer plano",
   "cache_settings_album_thumbnails": "Miniaturas de la página de la biblioteca ({} elementos)",
   "cache_settings_clear_cache_button": "Borrar caché",
   "cache_settings_clear_cache_button_title": "Borra la caché de la aplicación. Esto afectará significativamente el rendimiento de la aplicación hasta que se reconstruya la caché.",
@@ -145,17 +156,22 @@
   "cache_settings_tile_subtitle": "Controla el comportamiento del almacenamiento local",
   "cache_settings_tile_title": "Almacenamiento local",
   "cache_settings_title": "Configuración de la caché",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Confirmar Contraseña",
   "change_password_form_description": "Hola {name},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.",
   "change_password_form_new_password": "Nueva Contraseña",
   "change_password_form_password_mismatch": "Las contraseñas no coinciden",
   "change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseña",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
-  "client_cert_enter_password": "Enter Password",
-  "client_cert_import": "Import",
+  "client_cert_enter_password": "Introduzca contraseña",
+  "client_cert_import": "Importar",
   "client_cert_import_success_msg": "Client certificate is imported",
   "client_cert_invalid_msg": "Invalid certificate file or wrong password",
-  "client_cert_remove": "Remove",
+  "client_cert_remove": "Eliminar",
   "client_cert_remove_msg": "Client certificate is removed",
   "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login",
   "client_cert_title": "SSL Client Certificate",
@@ -164,7 +180,7 @@
   "common_create_new_album": "Crear nuevo álbum",
   "common_server_error": "Por favor, verifica tu conexión de red, asegúrate de que el servidor esté accesible y las versiones de la aplicación y del servidor sean compatibles.",
   "common_shared": "Compartido",
-  "contextual_search": "Sunrise on the beach",
+  "contextual_search": "Amanecer en la playa",
   "control_bottom_app_bar_add_to_album": "Agregar al álbum",
   "control_bottom_app_bar_album_info": "{} elementos",
   "control_bottom_app_bar_album_info_shared": "{} elementos · Compartidos",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Desarchivar",
   "control_bottom_app_bar_unfavorite": "Retirar favorito",
   "control_bottom_app_bar_upload": "Subir",
+  "create_album": "Crear álbum",
   "create_album_page_untitled": "Sin título",
+  "create_new": "Crear nuevo",
   "create_shared_album_page_create": "Crear",
   "create_shared_album_page_share": "Compartir",
   "create_shared_album_page_share_add_assets": "AGREGAR ELEMENTOS",
@@ -193,6 +211,7 @@
   "crop": "Recortar",
   "curated_location_page_title": "Lugares",
   "curated_object_page_title": "Objetos",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E dd, MMM",
   "daily_title_text_date_year": "E dd de MMM, yyyy",
   "date_format": "E d, LLL y • h:mm a",
@@ -206,18 +225,31 @@
   "delete_dialog_title": "Eliminar Permanentemente",
   "delete_local_dialog_ok_backed_up_only": "Borrar solo las que tengan copia de seguridad",
   "delete_local_dialog_ok_force": "Borrar de todos modos",
-  "delete_shared_link_dialog_content": "Estás seguro que quieres eliminar este enlace compartido",
+  "delete_shared_link_dialog_content": "¿Estás seguro que quieres eliminar este enlace compartido?",
   "delete_shared_link_dialog_title": "Eliminar enlace compartido",
   "description_input_hint_text": "Agregar descripción...",
   "description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles",
-  "download_error": "Download Error",
-  "download_started": "Download started",
-  "download_sucess": "Download success",
-  "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_canceled": "Descarga cancelada",
+  "download_complete": "Descarga completada",
+  "download_enqueue": "Descarga en cola",
+  "download_error": "Error al descargar",
+  "download_failed": "Descarga fallida",
+  "download_filename": "Archivo: {}",
+  "download_finished": "Descarga completada",
+  "downloading": "Descargando...",
+  "downloading_media": "Descargando medios",
+  "download_notfound": "Descarga no encontrada",
+  "download_paused": "Descarga en pausa",
+  "download_started": "Descarga iniciada",
+  "download_sucess": "Descarga Exitosa",
+  "download_sucess_android": "Los archivos se han descargado en DCIM/Immich",
+  "download_waiting_to_retry": "Esperando para reintentar",
   "edit_date_time_dialog_date_time": "Fecha y Hora",
   "edit_date_time_dialog_timezone": "Zona horaria",
   "edit_image_title": "Editar",
   "edit_location_dialog_title": "Ubicación",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Error: {}",
   "exif_bottom_sheet_description": "Agregar Descripción...",
   "exif_bottom_sheet_details": "DETALLES",
@@ -229,18 +261,24 @@
   "experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental",
   "experimental_settings_subtitle": "Úsalo bajo tu responsabilidad",
   "experimental_settings_title": "Experimental",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favoritos",
   "favorites_page_no_favorites": "No se encontraron elementos marcados como favoritos",
   "favorites_page_title": "Favoritos",
-  "filename_search": "File name or extension",
+  "filename_search": "Nombre o extensión",
+  "filter": "Filtrar",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Activar respuesta háptica",
   "haptic_feedback_title": "Respuesta Háptica",
   "header_settings_add_header_tip": "Añadir cabecera",
-  "header_settings_field_validator_msg": "Value cannot be empty",
-  "header_settings_header_name_input": "Header name",
-  "header_settings_header_value_input": "Header value",
+  "header_settings_field_validator_msg": "El valor no puede estar vacío",
+  "header_settings_header_name_input": "Nombre de la cabecera",
+  "header_settings_header_value_input": "Valor de la cabecera",
   "header_settings_page_title": "Proxy Headers",
-  "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
-  "headers_settings_tile_title": "Custom proxy headers",
+  "headers_settings_tile_subtitle": "Configura headers HTTP que la aplicación incluirá en cada petición de red",
+  "headers_settings_tile_title": "Cabeceras de proxy personalizadas",
   "home_page_add_to_album_conflicts": "{added} elementos agregados al álbum {album}.{failed} elementos ya existen en el álbum.",
   "home_page_add_to_album_err_local": "Aún no se pueden agregar elementos locales a álbumes, omitiendo",
   "home_page_add_to_album_success": "{added} elementos agregados al álbum {album}. ",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "Si esta es la primera vez que usas la app, por favor, asegúrate de elegir un álbum de respaldo para que la línea de tiempo pueda cargar fotos y videos en los álbumes.",
   "home_page_share_err_local": "No se pueden compartir elementos locales a través de un enlace, omitiendo",
   "home_page_upload_err_limit": "Solo se pueden subir 30 elementos simultáneamente, omitiendo",
+  "ignore_icloud_photos": "Ignorar fotos de iCloud",
+  "ignore_icloud_photos_description": "Las fotos almacenadas en iCloud no se subirán a Immich",
   "image_saved_successfully": "Imágenes guardas",
   "image_viewer_page_state_provider_download_error": "Error de descarga",
   "image_viewer_page_state_provider_download_started": "Descarga Iniciada",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "Error al compartir",
   "invalid_date": "Fecha incorrecta",
   "invalid_date_format": "Formato de fecha incorrecto",
+  "library": "Biblioteca",
   "library_page_albums": "Álbumes",
   "library_page_archive": "Archivo",
   "library_page_device_albums": "Álbumes en el dispositivo",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Foto más antigua",
   "library_page_sort_most_recent_photo": "Foto más reciente",
   "library_page_sort_title": "Título del álbum",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Elegir en el mapa",
   "location_picker_latitude": "Latitud",
   "location_picker_latitude_error": "Introduce una latitud válida",
@@ -312,10 +357,10 @@
   "map_location_dialog_cancel": "Cancelar",
   "map_location_dialog_yes": "Sí",
   "map_location_picker_page_use_location": "Usar esta ubicación",
-  "map_location_service_disabled_content": "Los servicios de ubicación deben estar activados para mostrar elementos de tu ubicación actual. Deseas activarlos ahora?",
+  "map_location_service_disabled_content": "Los servicios de ubicación deben estar activados para mostrar elementos de tu ubicación actual. ¿Deseas activarlos ahora?",
   "map_location_service_disabled_title": "Servicios de ubicación desactivados",
   "map_no_assets_in_bounds": "No hay fotos en esta zona",
-  "map_no_location_permission_content": "Se necesitan permisos de ubicación para mostrar elementos de tu ubicación actual. Deseas activarlos ahora?",
+  "map_no_location_permission_content": "Se necesitan permisos de ubicación para mostrar elementos de tu ubicación actual. ¿Deseas activarlos ahora?",
   "map_no_location_permission_title": "Permisos de ubicación denegados",
   "map_settings_dark_mode": "Modo oscuro",
   "map_settings_date_range_option_all": "Todo",
@@ -342,14 +387,18 @@
   "motion_photos_page_title": "Foto en Movimiento",
   "multiselect_grid_edit_date_time_err_read_only": "No se puede cambiar la fecha del archivo(s) de solo lectura, omitiendo",
   "multiselect_grid_edit_gps_err_read_only": "No se puede cambiar la localización de archivos de solo lectura. Saltando.",
+  "my_albums": "Mis álbumes",
+  "networking_settings": "Red",
+  "networking_subtitle": "Configuraciones de acceso por URL al servidor",
   "no_assets_to_show": "No hay elementos a mostrar",
-  "no_name": "No name",
+  "no_name": "Sin nombre",
   "notification_permission_dialog_cancel": "Cancelar",
   "notification_permission_dialog_content": "Para activar las notificaciones, ve a Configuración y selecciona permitir.",
   "notification_permission_dialog_settings": "Ajustes",
   "notification_permission_list_tile_content": "Concede permiso para habilitar las notificaciones.",
   "notification_permission_list_tile_enable_button": "Permitir notificaciones",
   "notification_permission_list_tile_title": "Permisos de Notificacion",
+  "on_this_device": "En este dispositivo",
   "partner_list_user_photos": "Fotos de {user}",
   "partner_list_view_all": "Ver todas",
   "partner_page_add_partner": "Agregar compañero",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{}  ya no podrá acceder a tus fotos",
   "partner_page_stop_sharing_title": "¿Dejar de compartir tus fotos?",
   "partner_page_title": "Compañero",
+  "partners": "Colaboradores",
+  "people": "Personas",
   "permission_onboarding_back": "Volver",
   "permission_onboarding_continue_anyway": "Continuar de todos modos",
   "permission_onboarding_get_started": "Empezar",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "¡Permiso concedido! Todo listo.",
   "permission_onboarding_permission_limited": "Permiso limitado. Para permitir que Immich haga copia de seguridad y gestione toda tu colección de galería, concede permisos de fotos y videos en Configuración.",
   "permission_onboarding_request": "Immich requiere permiso para ver tus fotos y videos.",
+  "places": "Lugares",
+  "preferences_settings_subtitle": "Configuraciones de la aplicación",
   "preferences_settings_title": "Preferencias",
   "profile_drawer_app_logs": "Registros",
   "profile_drawer_client_out_of_date_major": "La app está desactualizada. Por favor actualiza a la última versión principal.",
@@ -383,15 +436,18 @@
   "profile_drawer_settings": "Configuración",
   "profile_drawer_sign_out": "Cerrar Sesión",
   "profile_drawer_trash": "Papelera",
+  "recently_added": "Añadidos recientemente",
   "recently_added_page_title": "Recién Agregadas",
+  "save": "Save",
   "save_to_gallery": "Guardado en la galería",
   "scaffold_body_error_occurred": "Ha ocurrido un error",
+  "search_albums": "Buscar álbum",
   "search_bar_hint": "Busca tus fotos",
   "search_filter_apply": "Aplicar filtros",
   "search_filter_camera": "Cámara",
   "search_filter_camera_make": "Marca",
   "search_filter_camera_model": "Modelo",
-  "search_filter_camera_title": "Select camera type",
+  "search_filter_camera_title": "Elige tipo de cámara",
   "search_filter_date": "Fecha",
   "search_filter_date_interval": "{start} al {end}",
   "search_filter_date_title": "Selecciona un intervalo de fechas",
@@ -405,13 +461,13 @@
   "search_filter_location_country": "País",
   "search_filter_location_state": "Estado",
   "search_filter_location_title": "Seleccionar una ubicación",
-  "search_filter_media_type": "Media Type",
+  "search_filter_media_type": "Tipo de archivo",
   "search_filter_media_type_all": "Todos",
   "search_filter_media_type_image": "Imagen",
-  "search_filter_media_type_title": "Select media type",
+  "search_filter_media_type_title": "Selecciona el tipo de archivo",
   "search_filter_media_type_video": "Vídeo",
-  "search_filter_people": "People",
-  "search_filter_people_title": "Select people",
+  "search_filter_people": "Personas",
+  "search_filter_people_title": "Seleccionar personas",
   "search_page_categories": "Categorías",
   "search_page_favorites": "Favoritos",
   "search_page_motion_photos": "Foto en Movimiento",
@@ -428,6 +484,7 @@
   "search_page_places": "Lugares",
   "search_page_recently_added": "Recién agregadas",
   "search_page_screenshots": "Capturas de pantalla",
+  "search_page_search_photos_videos": "Busca tus fotos y videos",
   "search_page_selfies": "Selfies",
   "search_page_things": "Cosas",
   "search_page_videos": "Videos",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Sugerencias",
   "select_user_for_sharing_page_err_album": "Fallo al crear el álbum",
   "select_user_for_sharing_page_share_suggestions": "Sugerencias",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "Versión de la Aplicación",
   "server_info_box_latest_release": "Ultima versión",
   "server_info_box_server_url": "URL del servidor",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Cargar imagen de previsualización",
   "setting_image_viewer_title": "Imágenes",
   "setting_languages_apply": "Aplicar",
+  "setting_languages_subtitle": "Cambia el idioma de la aplicación",
   "setting_languages_title": "Idiomas",
   "setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {}",
   "setting_notifications_notify_hours": "{} horas",
@@ -466,7 +525,7 @@
   "setting_notifications_total_progress_title": "Mostrar progreso total de copia de seguridad en segundo plano",
   "setting_pages_app_bar_settings": "Ajustes",
   "settings_require_restart": "Por favor, reinicia Immich para aplicar este ajuste",
-  "setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
+  "setting_video_viewer_looping_subtitle": "Habilitar reproducción en bucle del video en la vista detallada",
   "setting_video_viewer_looping_title": "Bucle",
   "setting_video_viewer_title": "Vídeos",
   "share_add": "Agregar",
@@ -476,13 +535,13 @@
   "share_create_album": "Crear álbum",
   "shared_album_activities_input_disable": "Los comentarios están deshabilitados",
   "shared_album_activities_input_hint": "Comenta algo",
-  "shared_album_activity_remove_content": "Deseas eliminar esta actividad?",
+  "shared_album_activity_remove_content": "¿Deseas eliminar esta actividad?",
   "shared_album_activity_remove_title": "Eliminar Actividad",
   "shared_album_activity_setting_subtitle": "Permitir que otros respondan",
   "shared_album_activity_setting_title": "Comentarios y me gusta",
   "shared_album_section_people_action_error": "Error retirando/eliminando del album",
-  "shared_album_section_people_action_leave": "Eliminar usuario del album",
-  "shared_album_section_people_action_remove_user": "Eliminar usuario del album",
+  "shared_album_section_people_action_leave": "Eliminar usuario del álbum",
+  "shared_album_section_people_action_remove_user": "Eliminar usuario del álbum",
   "shared_album_section_people_owner_label": "Propietario",
   "shared_album_section_people_title": "PERSONAS",
   "share_dialog_preparing": "Preparando...",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Subir",
   "shared_link_manage_links": "Administrar enlaces compartidos",
   "shared_link_public_album": "Álbum público ",
+  "shared_links": "Enlaces",
   "share_done": "Hecho",
+  "shared_with_me": "Compartidos conmigo",
   "share_invite": "Invitar al álbum",
   "sharing_page_album": "Álbumes compartidos",
   "sharing_page_description": "Crea álbumes compartidos para compartir fotos y vídeos con las personas de tu red.",
@@ -540,8 +601,8 @@
   "sharing_silver_appbar_shared_links": "Enlaces compartidos",
   "sharing_silver_appbar_share_partner": "Compartir con el compañero",
   "sync": "Sincronizar",
-  "sync_albums": "Sync albums",
-  "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
+  "sync_albums": "Sincronizar álbumes",
+  "sync_albums_manual_subtitle": "Sincroniza todos los videos y fotos subidos con los álbumes seleccionados a respaldar",
   "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
   "tab_controller_nav_library": "Biblioteca",
   "tab_controller_nav_photos": "Fotos",
@@ -556,14 +617,15 @@
   "theme_setting_image_viewer_quality_title": "Calidad del visor de imágenes",
   "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.",
   "theme_setting_primary_color_title": "Primary color",
-  "theme_setting_system_primary_color_title": "Use system color",
+  "theme_setting_system_primary_color_title": "Usar color del sistema",
   "theme_setting_system_theme_switch": "Automático (seguir ajuste del sistema)",
   "theme_setting_theme_subtitle": "Elige la configuración del tema de la aplicación",
   "theme_setting_theme_title": "Tema",
   "theme_setting_three_stage_loading_subtitle": "La carga en tres etapas puede aumentar el rendimiento de carga pero provoca un consumo de red significativamente mayor",
   "theme_setting_three_stage_loading_title": "Activar carga en tres etapas",
   "translated_text_options": "Opciones",
-  "trash_emptied": "Emptied trash",
+  "trash": "Papelera",
+  "trash_emptied": "Papelera vaciada",
   "trash_page_delete": "Eliminar",
   "trash_page_delete_all": "Eliminar todos",
   "trash_page_empty_trash_btn": "Vaciar papelera",
@@ -577,16 +639,21 @@
   "trash_page_select_btn": "Seleccionar",
   "trash_page_title": "Papelera ({})",
   "upload_dialog_cancel": "Cancelar",
-  "upload_dialog_info": "Quieres hacer una copia de seguridad al servidor de los elementos seleccionados?",
+  "upload_dialog_info": "¿Quieres hacer una copia de seguridad al servidor de los elementos seleccionados?",
   "upload_dialog_ok": "Subir",
   "upload_dialog_title": "Subir elementos",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Aceptar",
   "version_announcement_overlay_release_notes": "notas de versión",
   "version_announcement_overlay_text_1": "Hola amigo, hay una nueva versión de",
   "version_announcement_overlay_text_2": "por favor, tómate tu tiempo para visitar las ",
   "version_announcement_overlay_text_3": " y asegúrate de que la configuración de docker-compose y .env estén actualizadas para evitar cualquier error de configuración, especialmente si utilizas WatchTower o cualquier mecanismo que actualice automáticamente la aplicación del servidor.",
   "version_announcement_overlay_title": "Nueva versión del servidor disponible \uD83C\uDF89",
+  "videos": "Videos",
   "viewer_remove_from_stack": "Quitar de la pila",
   "viewer_stack_use_as_main_asset": "Usar como elemento principal",
-  "viewer_unstack": "Desapilar"
+  "viewer_unstack": "Desapilar",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/es-MX.json b/mobile/assets/i18n/es-MX.json
index 8361e9a285..bcd25a556a 100644
--- a/mobile/assets/i18n/es-MX.json
+++ b/mobile/assets/i18n/es-MX.json
@@ -1,17 +1,19 @@
 {
   "action_common_back": "Back",
   "action_common_cancel": "Cancel",
-  "action_common_clear": "Clear",
+  "action_common_clear": "Limpiar",
   "action_common_confirm": "Confirm",
   "action_common_save": "Save",
   "action_common_select": "Select",
   "action_common_update": "Update",
+  "add_a_name": "Add a name",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Agregado a {album}",
   "add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}",
-  "advanced_settings_log_level_title": "Log level: {}",
+  "advanced_settings_log_level_title": "Nivel de registro: {}",
   "advanced_settings_prefer_remote_subtitle": "Algunos dispositivos tardan mucho en cargar las miniaturas de recursos encontrados el dispositivo. Activa esta opción para cargar imágenes remotas en su lugar.",
   "advanced_settings_prefer_remote_title": "Preferir imágenes remotas",
-  "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
+  "advanced_settings_proxy_headers_subtitle": "Configura headers HTTP que Immich incluirá en cada petición de red",
   "advanced_settings_proxy_headers_title": "Proxy Headers",
   "advanced_settings_self_signed_ssl_subtitle": "Omitir verificación del certificado SSL del servidor. Requerido para certificados autofirmados",
   "advanced_settings_self_signed_ssl_title": "Permitir certificados autofirmados",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Solución de problemas",
   "album_info_card_backup_album_excluded": "EXCLUIDOS",
   "album_info_card_backup_album_included": "INCLUIDOS",
+  "albums": "Álbumes",
   "album_thumbnail_card_item": "1 elemento",
   "album_thumbnail_card_items": "{} elementos",
   "album_thumbnail_card_shared": " · Compartido",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Eliminar del álbum",
   "album_viewer_appbar_share_to": "Share To",
   "album_viewer_page_share_add_users": "Agregar usuarios",
+  "all": "Todos",
   "all_people_page_title": "Personas",
   "all_videos_page_title": "Videos",
   "app_bar_signout_dialog_content": "¿Estás seguro que quieres cerrar sesión?",
   "app_bar_signout_dialog_ok": "Sí",
   "app_bar_signout_dialog_title": "Cerrar sesión",
+  "archived": "Archived",
   "archive_page_no_archived_assets": "No se encontraron recursos archivados",
   "archive_page_title": "Archivo ({})",
   "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
@@ -57,11 +62,16 @@
   "asset_restored_successfully": "Asset restored successfully",
   "assets_deleted_permanently": "{} asset(s) deleted permanently",
   "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server",
-  "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device",
+  "assets_removed_permanently_from_device": "{} elemento(s) eliminado(s) permanentemente de su dispositivo",
   "assets_restored_successfully": "{} asset(s) restored successfully",
   "assets_trashed": "{} asset(s) trashed",
   "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
-  "asset_viewer_settings_title": "Asset Viewer",
+  "asset_viewer_settings_subtitle": "Administra las configuracioens de tu visor de fotos",
+  "asset_viewer_settings_title": "Visor de fotos",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})",
   "backup_album_selection_page_albums_tap": "Pulsar para incluir, pulsar dos veces para excluir",
   "backup_album_selection_page_assets_scatter": "Los archivos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.",
@@ -127,12 +137,13 @@
   "backup_manual_success": "Éxito",
   "backup_manual_title": "Estado de la subida",
   "backup_options_page_title": "Backup options",
+  "backup_setting_subtitle": "Administra las configuraciones de respaldo en segundo y primer plano",
   "cache_settings_album_thumbnails": "Miniaturas de la página de la biblioteca ({} archivos)",
   "cache_settings_clear_cache_button": "Borrar caché",
   "cache_settings_clear_cache_button_title": "Borra la caché de la aplicación. Esto afectará significativamente el rendimiento de la aplicación hasta que se reconstruya la caché.",
-  "cache_settings_duplicated_assets_clear_button": "CLEAR",
+  "cache_settings_duplicated_assets_clear_button": "LIMPIAR",
   "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
-  "cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
+  "cache_settings_duplicated_assets_title": "Elementos duplicados ({})",
   "cache_settings_image_cache_size": "Tamaño de la caché de imágenes ({} archivos)",
   "cache_settings_statistics_album": "Miniaturas de la biblioteca",
   "cache_settings_statistics_assets": "{} archivos ({})",
@@ -145,17 +156,22 @@
   "cache_settings_tile_subtitle": "Controla el comportamiento del almacenamiento local",
   "cache_settings_tile_title": "Almacenamiento local",
   "cache_settings_title": "Configuración de la caché",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Confirmar Contraseña",
   "change_password_form_description": "Hola {name},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.",
   "change_password_form_new_password": "Nueva Contraseña",
   "change_password_form_password_mismatch": "Las contraseñas no coinciden",
   "change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseña",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Enter Password",
-  "client_cert_import": "Import",
+  "client_cert_import": "Importar",
   "client_cert_import_success_msg": "Client certificate is imported",
   "client_cert_invalid_msg": "Invalid certificate file or wrong password",
-  "client_cert_remove": "Remove",
+  "client_cert_remove": "Eliminar",
   "client_cert_remove_msg": "Client certificate is removed",
   "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login",
   "client_cert_title": "SSL Client Certificate",
@@ -164,7 +180,7 @@
   "common_create_new_album": "Crear nuevo álbum",
   "common_server_error": "Por favor, verifica tu conexión de red, asegúrate de que el servidor esté accesible y las versiones de la aplicación y del servidor sean compatibles.",
   "common_shared": "Compartido",
-  "contextual_search": "Sunrise on the beach",
+  "contextual_search": "Amanecer en la playa",
   "control_bottom_app_bar_add_to_album": "Agregar al álbum",
   "control_bottom_app_bar_album_info": "{} elementos",
   "control_bottom_app_bar_album_info_shared": "{} elementos · Compartidos",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Desarchivar",
   "control_bottom_app_bar_unfavorite": "Unfavorite",
   "control_bottom_app_bar_upload": "Subir",
+  "create_album": "Create album",
   "create_album_page_untitled": "Sin título",
+  "create_new": "CREATE NEW",
   "create_shared_album_page_create": "Crear",
   "create_shared_album_page_share": "Compartir",
   "create_shared_album_page_share_add_assets": "AGREGAR ARCHIVOS",
@@ -193,12 +211,13 @@
   "crop": "Crop",
   "curated_location_page_title": "Lugares",
   "curated_object_page_title": "Objetos",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, dd MMM",
   "daily_title_text_date_year": "E, dd de MMM de yyyy",
   "date_format": "E d, LLL y • h:mm a",
   "delete_dialog_alert": "Estos elementos se eliminarán permanentemente de Immich y de tu dispositivo",
   "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
-  "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
+  "delete_dialog_alert_local_non_backed_up": "Algunas de las imágenes no tienen copia de seguridad y serán borradas de forma permanente de tu dispositivo",
   "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
   "delete_dialog_cancel": "Cancelar",
   "delete_dialog_ok": "Eliminar",
@@ -206,32 +225,51 @@
   "delete_dialog_title": "Eliminar permanentemente",
   "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
   "delete_local_dialog_ok_force": "Delete Anyway",
-  "delete_shared_link_dialog_content": "Estás seguro que quieres eliminar este enlace compartido",
+  "delete_shared_link_dialog_content": "¿Estás seguro que quieres eliminar este enlace compartido?",
   "delete_shared_link_dialog_title": "Eliminar enlace compartido",
   "description_input_hint_text": "Agregar descripción...",
   "description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles",
+  "download_canceled": "Download canceled",
+  "download_complete": "Download complete",
+  "download_enqueue": "Download enqueued",
   "download_error": "Download Error",
+  "download_failed": "Download failed",
+  "download_filename": "file: {}",
+  "download_finished": "Download finished",
+  "downloading": "Downloading...",
+  "downloading_media": "Downloading media",
+  "download_notfound": "Download not found",
+  "download_paused": "Download paused",
   "download_started": "Download started",
   "download_sucess": "Download success",
   "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_waiting_to_retry": "Waiting to retry",
   "edit_date_time_dialog_date_time": "Date and Time",
   "edit_date_time_dialog_timezone": "Timezone",
   "edit_image_title": "Edit",
   "edit_location_dialog_title": "Location",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Error: {}",
   "exif_bottom_sheet_description": "Agregar Descripción...",
   "exif_bottom_sheet_details": "DETALLES",
   "exif_bottom_sheet_location": "UBICACIÓN",
   "exif_bottom_sheet_location_add": "Add a location",
-  "exif_bottom_sheet_people": "PEOPLE",
+  "exif_bottom_sheet_people": "PERSONAS",
   "exif_bottom_sheet_person_add_person": "Add name",
   "experimental_settings_new_asset_list_subtitle": "Trabajo en progreso",
   "experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental",
   "experimental_settings_subtitle": "Úsalo bajo tu responsabilidad",
   "experimental_settings_title": "Experimental",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favoritos",
   "favorites_page_no_favorites": "No se encontraron recursos marcados como favoritos",
   "favorites_page_title": "Favoritos",
   "filename_search": "File name or extension",
+  "filter": "Filter",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Enable haptic feedback",
   "haptic_feedback_title": "Haptic Feedback",
   "header_settings_add_header_tip": "Add Header",
@@ -239,8 +277,8 @@
   "header_settings_header_name_input": "Header name",
   "header_settings_header_value_input": "Header value",
   "header_settings_page_title": "Proxy Headers",
-  "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
-  "headers_settings_tile_title": "Custom proxy headers",
+  "headers_settings_tile_subtitle": "Configura headers HTTP que la aplicación incluirá en cada petición de red",
+  "headers_settings_tile_title": "Cabeceras de proxy personalizadas",
   "home_page_add_to_album_conflicts": "{added} elementos agregados al álbum {album}.\n{failed} elementos ya existen en el álbum.",
   "home_page_add_to_album_err_local": "Aún no se pueden agregar recursos locales a álbumes, omitiendo",
   "home_page_add_to_album_success": "{added} elementos agregados al álbum {album}. ",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "Si esta es la primera vez que usas la app, por favor, asegúrate de elegir un álbum de respaldo para que la línea de tiempo pueda cargar fotos y videos en los álbumes.",
   "home_page_share_err_local": "No se pueden compartir activos locales a través de un enlace, omitiendo",
   "home_page_upload_err_limit": "Solo se pueden subir 30 elementos simultáneamente, omitiendo",
+  "ignore_icloud_photos": "Ignore iCloud photos",
+  "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
   "image_saved_successfully": "Image saved",
   "image_viewer_page_state_provider_download_error": "Error de descarga",
   "image_viewer_page_state_provider_download_started": "Download Started",
@@ -262,18 +302,23 @@
   "image_viewer_page_state_provider_share_error": "Error al compartir",
   "invalid_date": "Invalid date",
   "invalid_date_format": "Invalid date format",
+  "library": "Biblioteca",
   "library_page_albums": "Álbumes",
   "library_page_archive": "Archivo",
   "library_page_device_albums": "Álbumes en el dispositivo",
   "library_page_favorites": "Favoritos",
   "library_page_new_album": "Nuevo álbum",
   "library_page_sharing": "Compartiendo",
-  "library_page_sort_asset_count": "Number of assets",
+  "library_page_sort_asset_count": "Número de elementos",
   "library_page_sort_created": "Creado más recientemente",
   "library_page_sort_last_modified": "Última modificación",
-  "library_page_sort_most_oldest_photo": "Oldest photo",
+  "library_page_sort_most_oldest_photo": "Foto más antigua",
   "library_page_sort_most_recent_photo": "Foto más reciente",
   "library_page_sort_title": "Título del álbum",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Choose on map",
   "location_picker_latitude": "Latitude",
   "location_picker_latitude_error": "Enter a valid latitude",
@@ -312,10 +357,10 @@
   "map_location_dialog_cancel": "Cancelar",
   "map_location_dialog_yes": "Sí",
   "map_location_picker_page_use_location": "Use this location",
-  "map_location_service_disabled_content": "Los servicios de localización deben estar activados para mostrar elementos de tu ubicación actual. Deseas activarlos ahora?",
+  "map_location_service_disabled_content": "Los servicios de localización deben estar activados para mostrar elementos de tu ubicación actual. ¿Deseas activarlos ahora?",
   "map_location_service_disabled_title": "Servicios de localización desactivados",
   "map_no_assets_in_bounds": "No hay fotos en esta zona",
-  "map_no_location_permission_content": "Se necesitan permisos de ubicación para mostrar elementos de tu ubicación actual. Deseas activarlos ahora?",
+  "map_no_location_permission_content": "Se necesitan permisos de ubicación para mostrar elementos de tu ubicación actual. ¿Deseas activarlos ahora?",
   "map_no_location_permission_title": "Permisos de ubicación denegados",
   "map_settings_dark_mode": "Modo oscuro",
   "map_settings_date_range_option_all": "All",
@@ -336,12 +381,15 @@
   "memories_check_back_tomorrow": "Check back tomorrow for more memories",
   "memories_start_over": "Start Over",
   "memories_swipe_to_close": "Swipe up to close",
-  "memories_year_ago": "A year ago",
-  "memories_years_ago": "{} years ago",
+  "memories_year_ago": "Hace un año",
+  "memories_years_ago": "Hace {} años",
   "monthly_title_text_date_format": "MMMM y",
   "motion_photos_page_title": "Foto en Movimiento",
   "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
   "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
+  "my_albums": "Mis álbumes",
+  "networking_settings": "Red",
+  "networking_subtitle": "Configuraciones de acceso por URL al servidor",
   "no_assets_to_show": "No assets to show",
   "no_name": "No name",
   "notification_permission_dialog_cancel": "Cancelar",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Concede permiso para habilitar las notificaciones.",
   "notification_permission_list_tile_enable_button": "Permitir notificaciones",
   "notification_permission_list_tile_title": "Permisos de Notificacion",
+  "on_this_device": "On this device",
   "partner_list_user_photos": "{user}'s photos",
   "partner_list_view_all": "View all",
   "partner_page_add_partner": "Agregar compañero",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{}  ya no podrá acceder a tus fotos",
   "partner_page_stop_sharing_title": "¿Dejar de compartir tus fotos?",
   "partner_page_title": "Compañero",
+  "partners": "Partners",
+  "people": "Personas",
   "permission_onboarding_back": "Volver",
   "permission_onboarding_continue_anyway": "Continuar de todos modos",
   "permission_onboarding_get_started": "Empezar",
@@ -371,7 +422,9 @@
   "permission_onboarding_permission_granted": "¡Permiso concedido! Todo listo.",
   "permission_onboarding_permission_limited": "Permiso limitado. Para permitir que Immich haga copia de seguridad y gestione toda tu colección de galería, concede permisos de fotos y videos en Configuración.",
   "permission_onboarding_request": "Immich requiere permiso para ver tus fotos y videos.",
-  "preferences_settings_title": "Preferences",
+  "places": "Places",
+  "preferences_settings_subtitle": "Configuraciones de la aplicación",
+  "preferences_settings_title": "Preferencias",
   "profile_drawer_app_logs": "Registros",
   "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
   "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
@@ -383,16 +436,19 @@
   "profile_drawer_settings": "Configuración",
   "profile_drawer_sign_out": "Cerrar sesión",
   "profile_drawer_trash": "Papelera",
+  "recently_added": "Añadidos recientemente",
   "recently_added_page_title": "Recién Agregadas",
+  "save": "Save",
   "save_to_gallery": "Save to gallery",
   "scaffold_body_error_occurred": "Error occurred",
+  "search_albums": "Buscar álbum",
   "search_bar_hint": "Busca tus fotos",
   "search_filter_apply": "Apply filter",
-  "search_filter_camera": "Camera",
-  "search_filter_camera_make": "Make",
-  "search_filter_camera_model": "Model",
-  "search_filter_camera_title": "Select camera type",
-  "search_filter_date": "Date",
+  "search_filter_camera": "Cámara",
+  "search_filter_camera_make": "Marca",
+  "search_filter_camera_model": "Modelo",
+  "search_filter_camera_title": "Elige tipo de cámara",
+  "search_filter_date": "Fecha",
   "search_filter_date_interval": "{start} to {end}",
   "search_filter_date_title": "Select a date range",
   "search_filter_display_option_archive": "Archive",
@@ -400,18 +456,18 @@
   "search_filter_display_option_not_in_album": "Not in album",
   "search_filter_display_options": "Display Options",
   "search_filter_display_options_title": "Display options",
-  "search_filter_location": "Location",
+  "search_filter_location": "Ubicación",
   "search_filter_location_city": "City",
   "search_filter_location_country": "Country",
   "search_filter_location_state": "State",
   "search_filter_location_title": "Select location",
-  "search_filter_media_type": "Media Type",
+  "search_filter_media_type": "Tipo de archivo",
   "search_filter_media_type_all": "All",
   "search_filter_media_type_image": "Image",
-  "search_filter_media_type_title": "Select media type",
+  "search_filter_media_type_title": "Selecciona el tipo de archivo",
   "search_filter_media_type_video": "Video",
-  "search_filter_people": "People",
-  "search_filter_people_title": "Select people",
+  "search_filter_people": "Personas",
+  "search_filter_people_title": "Seleccionar personas",
   "search_page_categories": "Categorías",
   "search_page_favorites": "Favoritos",
   "search_page_motion_photos": "Foto en Movimiento",
@@ -428,6 +484,7 @@
   "search_page_places": "Lugares",
   "search_page_recently_added": "Recién agregadas",
   "search_page_screenshots": "Capturas de pantalla",
+  "search_page_search_photos_videos": "Busca tus fotos y videos",
   "search_page_selfies": "Selfies",
   "search_page_things": "Cosas",
   "search_page_videos": "Videos",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Sugerencias",
   "select_user_for_sharing_page_err_album": "Error al crear álbum",
   "select_user_for_sharing_page_share_suggestions": "Sugerencias",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "Versión de la Aplicación",
   "server_info_box_latest_release": "Ultima versión",
   "server_info_box_server_url": "URL del servidor",
@@ -449,9 +507,10 @@
   "setting_image_viewer_original_title": "Cargar imagen original",
   "setting_image_viewer_preview_subtitle": "Activar para cargar una imagen de resolución media. Deshabilitar para cargar directamente la imagen original o usar una miniatura.",
   "setting_image_viewer_preview_title": "Cargar imagen de previsualización",
-  "setting_image_viewer_title": "Images",
-  "setting_languages_apply": "Apply",
-  "setting_languages_title": "Languages",
+  "setting_image_viewer_title": "Imágenes",
+  "setting_languages_apply": "Aplicar",
+  "setting_languages_subtitle": "Cambia el idioma de la aplicación",
+  "setting_languages_title": "Idiomas",
   "setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {}",
   "setting_notifications_notify_hours": "{} horas",
   "setting_notifications_notify_immediately": "inmediatamente",
@@ -466,8 +525,8 @@
   "setting_notifications_total_progress_title": "Mostrar progreso total de copia de seguridad en segundo plano",
   "setting_pages_app_bar_settings": "Ajustes",
   "settings_require_restart": "Por favor, reinicia Immich para aplicar este ajuste",
-  "setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
-  "setting_video_viewer_looping_title": "Looping",
+  "setting_video_viewer_looping_subtitle": "Habilitar reproducción en bucle del video en la vista detallada",
+  "setting_video_viewer_looping_title": "Bucle",
   "setting_video_viewer_title": "Videos",
   "share_add": "Agregar",
   "share_add_photos": "Agregar fotos",
@@ -476,15 +535,15 @@
   "share_create_album": "Crear álbum",
   "shared_album_activities_input_disable": "Los comentarios están deshabilitados",
   "shared_album_activities_input_hint": "Say something",
-  "shared_album_activity_remove_content": "Do you want to delete this activity?",
+  "shared_album_activity_remove_content": "¿Quieres eliminar esta actividad?",
   "shared_album_activity_remove_title": "Delete Activity",
   "shared_album_activity_setting_subtitle": "Let others respond",
   "shared_album_activity_setting_title": "Comments & likes",
-  "shared_album_section_people_action_error": "Error leaving/removing from album",
-  "shared_album_section_people_action_leave": "Remove user from album",
-  "shared_album_section_people_action_remove_user": "Remove user from album",
-  "shared_album_section_people_owner_label": "Owner",
-  "shared_album_section_people_title": "PEOPLE",
+  "shared_album_section_people_action_error": "Error retirando/eliminando del album",
+  "shared_album_section_people_action_leave": "Eliminar usuario del álbum",
+  "shared_album_section_people_action_remove_user": "Eliminar usuario del álbum",
+  "shared_album_section_people_owner_label": "Propietario",
+  "shared_album_section_people_title": "PERSONAS",
   "share_dialog_preparing": "Preparando...",
   "shared_link_app_bar_title": "Enlaces compartidos",
   "shared_link_clipboard_copied_massage": "Copied to clipboard",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Upload",
   "shared_link_manage_links": "Administrar enlaces compartidos",
   "shared_link_public_album": "Public album",
+  "shared_links": "Shared links",
   "share_done": "Hecho",
+  "shared_with_me": "Compartidos conmigo",
   "share_invite": "Invitar al álbum",
   "sharing_page_album": "Álbumes compartidos",
   "sharing_page_description": "Crea álbumes compartidos para compartir fotos y videos con personas de tu red.",
@@ -540,8 +601,8 @@
   "sharing_silver_appbar_shared_links": "Enlaces compartidos",
   "sharing_silver_appbar_share_partner": "Compartir con compañero",
   "sync": "Sync",
-  "sync_albums": "Sync albums",
-  "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
+  "sync_albums": "Sincronizar álbumes",
+  "sync_albums_manual_subtitle": "Sincroniza todos los videos y fotos subidos con los álbumes seleccionados a respaldar",
   "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
   "tab_controller_nav_library": "Biblioteca",
   "tab_controller_nav_photos": "Fotos",
@@ -563,11 +624,12 @@
   "theme_setting_three_stage_loading_subtitle": "La carga en tres etapas puede aumentar el rendimiento de carga pero provoca un consumo de red significativamente mayor",
   "theme_setting_three_stage_loading_title": "Activar carga en tres etapas",
   "translated_text_options": "Opciones",
+  "trash": "Trash",
   "trash_emptied": "Emptied trash",
   "trash_page_delete": "Eliminar",
   "trash_page_delete_all": "Eliminar todos",
   "trash_page_empty_trash_btn": "Vaciar papelera",
-  "trash_page_empty_trash_dialog_content": "Estás seguro que quieres eliminar los elementos? Estos elementos serán eliminados de Immich permanentemente",
+  "trash_page_empty_trash_dialog_content": "¿Estás seguro que quieres eliminar los elementos? Estos elementos serán eliminados de Immich permanentemente",
   "trash_page_empty_trash_dialog_ok": "Sí",
   "trash_page_info": "Los archivos en la papelera serán eliminados automáticamente después de {} días",
   "trash_page_no_assets": "No hay elementos en la papelera",
@@ -577,16 +639,21 @@
   "trash_page_select_btn": "Seleccionar",
   "trash_page_title": "Papelera ({})",
   "upload_dialog_cancel": "Cancelar",
-  "upload_dialog_info": "Quieres hacer una copia de seguridad al servidor de los elementos seleccionados?",
+  "upload_dialog_info": "¿Quieres hacer una copia de seguridad al servidor de los elementos seleccionados?",
   "upload_dialog_ok": "Subir",
   "upload_dialog_title": "Subir elementos",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Aceptar",
   "version_announcement_overlay_release_notes": "notas de la versión",
   "version_announcement_overlay_text_1": "Hola, amigo, hay una nueva versión de",
   "version_announcement_overlay_text_2": "por favor, tómate tu tiempo para visitar las ",
   "version_announcement_overlay_text_3": " y asegúrate de que la configuración de docker-compose y .env estén actualizadas para evitar cualquier error de configuración, especialmente si utilizas WatchTower o cualquier mecanismo que actualice automáticamente la aplicación del servidor.",
   "version_announcement_overlay_title": "Nueva versión del servidor disponible \uD83C\uDF89",
+  "videos": "Videos",
   "viewer_remove_from_stack": "Quitar de la pila",
   "viewer_stack_use_as_main_asset": "Usar como elemento principal",
-  "viewer_unstack": "Desapilar"
+  "viewer_unstack": "Desapilar",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/es-PE.json b/mobile/assets/i18n/es-PE.json
index cee06c9512..3e62730ef2 100644
--- a/mobile/assets/i18n/es-PE.json
+++ b/mobile/assets/i18n/es-PE.json
@@ -1,11 +1,13 @@
 {
   "action_common_back": "Back",
   "action_common_cancel": "Cancel",
-  "action_common_clear": "Clear",
+  "action_common_clear": "Limpiar",
   "action_common_confirm": "Confirm",
   "action_common_save": "Save",
   "action_common_select": "Select",
   "action_common_update": "Update",
+  "add_a_name": "Add a name",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Agregado a {album}",
   "add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}",
   "advanced_settings_log_level_title": "Log level: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Solución de problemas",
   "album_info_card_backup_album_excluded": "EXCLUIDOS",
   "album_info_card_backup_album_included": "INCLUIDOS",
+  "albums": "Albums",
   "album_thumbnail_card_item": "1 elemento",
   "album_thumbnail_card_items": "{} elementos",
   "album_thumbnail_card_shared": " · Compartido",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Eliminar del álbum",
   "album_viewer_appbar_share_to": "Compartir A",
   "album_viewer_page_share_add_users": "Agregar usuarios",
+  "all": "All",
   "all_people_page_title": "Personas",
   "all_videos_page_title": "Videos",
   "app_bar_signout_dialog_content": "¿Estás seguro que quieres cerrar sesión?",
   "app_bar_signout_dialog_ok": "Sí",
   "app_bar_signout_dialog_title": "Cerrar sesión",
+  "archived": "Archived",
   "archive_page_no_archived_assets": "No se encontraron recursos archivados",
   "archive_page_title": "Archivo ({})",
   "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} asset(s) restored successfully",
   "assets_trashed": "{} asset(s) trashed",
   "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "Asset Viewer",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})",
   "backup_album_selection_page_albums_tap": "Pulsar para incluir, pulsar dos veces para excluir",
   "backup_album_selection_page_assets_scatter": "Los archivos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.",
@@ -127,10 +137,11 @@
   "backup_manual_success": "Éxito",
   "backup_manual_title": "Estado de la subida",
   "backup_options_page_title": "Backup options",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Miniaturas de la página de la biblioteca ({} archivos)",
   "cache_settings_clear_cache_button": "Borrar caché",
   "cache_settings_clear_cache_button_title": "Borra la caché de la aplicación. Esto afectará significativamente el rendimiento de la aplicación hasta que se reconstruya la caché.",
-  "cache_settings_duplicated_assets_clear_button": "CLEAR",
+  "cache_settings_duplicated_assets_clear_button": "LIMPIAR",
   "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
   "cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
   "cache_settings_image_cache_size": "Tamaño de la caché de imágenes ({} archivos)",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Controla el comportamiento del almacenamiento local",
   "cache_settings_tile_title": "Almacenamiento local",
   "cache_settings_title": "Configuración de la caché",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Confirmar Contraseña",
   "change_password_form_description": "Hola {name},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.",
   "change_password_form_new_password": "Nueva Contraseña",
   "change_password_form_password_mismatch": "Las contraseñas no coinciden",
   "change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseña",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Enter Password",
   "client_cert_import": "Import",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Desarchivar",
   "control_bottom_app_bar_unfavorite": "Unfavorite",
   "control_bottom_app_bar_upload": "Subir",
+  "create_album": "Create album",
   "create_album_page_untitled": "Sin título",
+  "create_new": "CREATE NEW",
   "create_shared_album_page_create": "Crear",
   "create_shared_album_page_share": "Compartir",
   "create_shared_album_page_share_add_assets": "AGREGAR ARCHIVOS",
@@ -193,6 +211,7 @@
   "crop": "Crop",
   "curated_location_page_title": "Lugares",
   "curated_object_page_title": "Objetos",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, dd MMM",
   "daily_title_text_date_year": "E, dd de MMM de yyyy",
   "date_format": "E d, LLL y • h:mm a",
@@ -206,18 +225,31 @@
   "delete_dialog_title": "Eliminar permanentemente",
   "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
   "delete_local_dialog_ok_force": "Delete Anyway",
-  "delete_shared_link_dialog_content": "Estás seguro que quieres eliminar este enlace compartido",
+  "delete_shared_link_dialog_content": "¿Estás seguro que quieres eliminar este enlace compartido?",
   "delete_shared_link_dialog_title": "Eliminar enlace compartido",
   "description_input_hint_text": "Agregar descripción...",
   "description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles",
+  "download_canceled": "Download canceled",
+  "download_complete": "Download complete",
+  "download_enqueue": "Download enqueued",
   "download_error": "Download Error",
+  "download_failed": "Download failed",
+  "download_filename": "file: {}",
+  "download_finished": "Download finished",
+  "downloading": "Downloading...",
+  "downloading_media": "Downloading media",
+  "download_notfound": "Download not found",
+  "download_paused": "Download paused",
   "download_started": "Download started",
   "download_sucess": "Download success",
   "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_waiting_to_retry": "Waiting to retry",
   "edit_date_time_dialog_date_time": "Date and Time",
   "edit_date_time_dialog_timezone": "Timezone",
   "edit_image_title": "Edit",
   "edit_location_dialog_title": "Location",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Error: {}",
   "exif_bottom_sheet_description": "Agregar Descripción...",
   "exif_bottom_sheet_details": "DETALLES",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental",
   "experimental_settings_subtitle": "Úsalo bajo tu responsabilidad",
   "experimental_settings_title": "Experimental",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favorites",
   "favorites_page_no_favorites": "No se encontraron recursos marcados como favoritos",
   "favorites_page_title": "Favoritos",
   "filename_search": "File name or extension",
+  "filter": "Filter",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Enable haptic feedback",
   "haptic_feedback_title": "Haptic Feedback",
   "header_settings_add_header_tip": "Add Header",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "Si esta es la primera vez que usas la app, por favor, asegúrate de elegir un álbum de respaldo para que la línea de tiempo pueda cargar fotos y videos en los álbumes.",
   "home_page_share_err_local": "No se pueden compartir activos locales a través de un enlace, omitiendo",
   "home_page_upload_err_limit": "Solo se pueden subir 30 elementos simultáneamente, omitiendo",
+  "ignore_icloud_photos": "Ignore iCloud photos",
+  "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
   "image_saved_successfully": "Image saved",
   "image_viewer_page_state_provider_download_error": "Error de descarga",
   "image_viewer_page_state_provider_download_started": "Download Started",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "Error al compartir",
   "invalid_date": "Invalid date",
   "invalid_date_format": "Invalid date format",
+  "library": "Library",
   "library_page_albums": "Álbumes",
   "library_page_archive": "Archivo",
   "library_page_device_albums": "Álbumes en el dispositivo",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Oldest photo",
   "library_page_sort_most_recent_photo": "Foto más reciente",
   "library_page_sort_title": "Título del álbum",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Choose on map",
   "location_picker_latitude": "Latitude",
   "location_picker_latitude_error": "Enter a valid latitude",
@@ -312,10 +357,10 @@
   "map_location_dialog_cancel": "Cancelar",
   "map_location_dialog_yes": "Sí",
   "map_location_picker_page_use_location": "Use this location",
-  "map_location_service_disabled_content": "Los servicios de localización deben estar activados para mostrar elementos de tu ubicación actual. Deseas activarlos ahora?",
+  "map_location_service_disabled_content": "Los servicios de localización deben estar activados para mostrar elementos de tu ubicación actual. ¿Deseas activarlos ahora?",
   "map_location_service_disabled_title": "Servicios de localización desactivados",
   "map_no_assets_in_bounds": "No hay fotos en esta zona",
-  "map_no_location_permission_content": "Se necesitan permisos de ubicación para mostrar elementos de tu ubicación actual. Deseas activarlos ahora?",
+  "map_no_location_permission_content": "Se necesitan permisos de ubicación para mostrar elementos de tu ubicación actual. ¿Deseas activarlos ahora?",
   "map_no_location_permission_title": "Permisos de ubicación denegados",
   "map_settings_dark_mode": "Modo oscuro",
   "map_settings_date_range_option_all": "All",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Foto en Movimiento",
   "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
   "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
+  "my_albums": "My albums",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "No assets to show",
   "no_name": "No name",
   "notification_permission_dialog_cancel": "Cancelar",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Concede permiso para habilitar las notificaciones.",
   "notification_permission_list_tile_enable_button": "Permitir notificaciones",
   "notification_permission_list_tile_title": "Permisos de Notificacion",
+  "on_this_device": "On this device",
   "partner_list_user_photos": "{user}'s photos",
   "partner_list_view_all": "View all",
   "partner_page_add_partner": "Agregar compañero",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{}  ya no podrá acceder a tus fotos",
   "partner_page_stop_sharing_title": "¿Dejar de compartir tus fotos?",
   "partner_page_title": "Compañero",
+  "partners": "Partners",
+  "people": "People",
   "permission_onboarding_back": "Volver",
   "permission_onboarding_continue_anyway": "Continuar de todos modos",
   "permission_onboarding_get_started": "Empezar",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "¡Permiso concedido! Todo listo.",
   "permission_onboarding_permission_limited": "Permiso limitado. Para permitir que Immich haga copia de seguridad y gestione toda tu colección de galería, concede permisos de fotos y videos en Configuración.",
   "permission_onboarding_request": "Immich requiere permiso para ver tus fotos y videos.",
+  "places": "Places",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Preferences",
   "profile_drawer_app_logs": "Registros",
   "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Configuración",
   "profile_drawer_sign_out": "Cerrar sesión",
   "profile_drawer_trash": "Papelera",
+  "recently_added": "Recently added",
   "recently_added_page_title": "Recién Agregadas",
+  "save": "Save",
   "save_to_gallery": "Save to gallery",
   "scaffold_body_error_occurred": "Error occurred",
+  "search_albums": "Search albums",
   "search_bar_hint": "Busca tus fotos",
   "search_filter_apply": "Apply filter",
   "search_filter_camera": "Camera",
@@ -428,6 +484,7 @@
   "search_page_places": "Lugares",
   "search_page_recently_added": "Recién agregadas",
   "search_page_screenshots": "Capturas de pantalla",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Selfies",
   "search_page_things": "Cosas",
   "search_page_videos": "Videos",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Sugerencias",
   "select_user_for_sharing_page_err_album": "Error al crear álbum",
   "select_user_for_sharing_page_share_suggestions": "Sugerencias",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "Versión de la Aplicación",
   "server_info_box_latest_release": "Ultima versión",
   "server_info_box_server_url": "URL del servidor",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Cargar imagen de previsualización",
   "setting_image_viewer_title": "Images",
   "setting_languages_apply": "Apply",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Languages",
   "setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {}",
   "setting_notifications_notify_hours": "{} horas",
@@ -476,7 +535,7 @@
   "share_create_album": "Crear álbum",
   "shared_album_activities_input_disable": "Los comentarios están deshabilitados",
   "shared_album_activities_input_hint": "Comenta algo",
-  "shared_album_activity_remove_content": "Deseas eliminar esta actividad?",
+  "shared_album_activity_remove_content": "¿Deseas eliminar esta actividad?",
   "shared_album_activity_remove_title": "Eliminar Actividad",
   "shared_album_activity_setting_subtitle": "Permitir que otros respondan",
   "shared_album_activity_setting_title": "Comentarios y me gusta",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Upload",
   "shared_link_manage_links": "Administrar enlaces compartidos",
   "shared_link_public_album": "Public album",
+  "shared_links": "Shared links",
   "share_done": "Hecho",
+  "shared_with_me": "Shared with me",
   "share_invite": "Invitar al álbum",
   "sharing_page_album": "Álbumes compartidos",
   "sharing_page_description": "Crea álbumes compartidos para compartir fotos y videos con personas de tu red.",
@@ -563,11 +624,12 @@
   "theme_setting_three_stage_loading_subtitle": "La carga en tres etapas puede aumentar el rendimiento de carga pero provoca un consumo de red significativamente mayor",
   "theme_setting_three_stage_loading_title": "Activar carga en tres etapas",
   "translated_text_options": "Opciones",
+  "trash": "Trash",
   "trash_emptied": "Emptied trash",
   "trash_page_delete": "Eliminar",
   "trash_page_delete_all": "Eliminar todos",
   "trash_page_empty_trash_btn": "Vaciar papelera",
-  "trash_page_empty_trash_dialog_content": "Estás seguro que quieres eliminar los elementos? Estos elementos serán eliminados de Immich permanentemente",
+  "trash_page_empty_trash_dialog_content": "¿Estás seguro que quieres eliminar los elementos? Estos elementos serán eliminados de Immich permanentemente",
   "trash_page_empty_trash_dialog_ok": "Sí",
   "trash_page_info": "Los archivos en la papelera serán eliminados automáticamente después de {} días",
   "trash_page_no_assets": "No hay elementos en la papelera",
@@ -577,16 +639,21 @@
   "trash_page_select_btn": "Seleccionar",
   "trash_page_title": "Papelera ({})",
   "upload_dialog_cancel": "Cancelar",
-  "upload_dialog_info": "Quieres hacer una copia de seguridad al servidor de los elementos seleccionados?",
+  "upload_dialog_info": "¿Quieres hacer una copia de seguridad al servidor de los elementos seleccionados?",
   "upload_dialog_ok": "Subir",
   "upload_dialog_title": "Subir elementos",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Aceptar",
   "version_announcement_overlay_release_notes": "notas de la versión",
   "version_announcement_overlay_text_1": "Hola, amigo, hay una nueva versión de",
   "version_announcement_overlay_text_2": "por favor, tómate tu tiempo para visitar las ",
   "version_announcement_overlay_text_3": " y asegúrate de que la configuración de docker-compose y .env estén actualizadas para evitar cualquier error de configuración, especialmente si utilizas WatchTower o cualquier mecanismo que actualice automáticamente la aplicación del servidor.",
   "version_announcement_overlay_title": "Nueva versión del servidor disponible \uD83C\uDF89",
+  "videos": "Videos",
   "viewer_remove_from_stack": "Quitar de la pila",
   "viewer_stack_use_as_main_asset": "Usar como elemento principal",
-  "viewer_unstack": "Desapilar"
+  "viewer_unstack": "Desapilar",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/es-US.json b/mobile/assets/i18n/es-US.json
index 8cfae94c00..489232eb50 100644
--- a/mobile/assets/i18n/es-US.json
+++ b/mobile/assets/i18n/es-US.json
@@ -1,17 +1,19 @@
 {
   "action_common_back": "Back",
   "action_common_cancel": "Cancel",
-  "action_common_clear": "Clear",
+  "action_common_clear": "Limpiar",
   "action_common_confirm": "Confirm",
   "action_common_save": "Save",
   "action_common_select": "Select",
   "action_common_update": "Update",
+  "add_a_name": "Add a name",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Agregado a {album}",
   "add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}",
-  "advanced_settings_log_level_title": "Log level: {}",
+  "advanced_settings_log_level_title": "Nivel de registro: {}",
   "advanced_settings_prefer_remote_subtitle": "Algunos dispositivos tardan mucho en cargar las miniaturas de recursos encontrados en el dispositivo. Activa esta opción para cargar imágenes remotas en su lugar.",
   "advanced_settings_prefer_remote_title": "Preferir imágenes remotas",
-  "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
+  "advanced_settings_proxy_headers_subtitle": "Configura headers HTTP que Immich incluirá en cada petición de red",
   "advanced_settings_proxy_headers_title": "Proxy Headers",
   "advanced_settings_self_signed_ssl_subtitle": "Omite la verificación del certificado SSL para la URL del servidor. Requerido para certificados autofirmados.",
   "advanced_settings_self_signed_ssl_title": "Permitir certificados SSL autofirmados",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Solución de problemas",
   "album_info_card_backup_album_excluded": "EXCLUIDOS",
   "album_info_card_backup_album_included": "INCLUIDOS",
+  "albums": "Álbumes",
   "album_thumbnail_card_item": "1 elemento",
   "album_thumbnail_card_items": "{} elementos",
   "album_thumbnail_card_shared": " · Compartido",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Remover del álbum",
   "album_viewer_appbar_share_to": "Compartir con",
   "album_viewer_page_share_add_users": "Agregar usuarios",
+  "all": "Todos",
   "all_people_page_title": "Personas",
   "all_videos_page_title": "Videos",
   "app_bar_signout_dialog_content": "¿Estás seguro de que quieres cerrar sesión?",
   "app_bar_signout_dialog_ok": "Sí",
   "app_bar_signout_dialog_title": "Cerrar sesión",
+  "archived": "Archived",
   "archive_page_no_archived_assets": "No se encontraron recursos archivados",
   "archive_page_title": "Archivo ({})",
   "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
@@ -57,11 +62,16 @@
   "asset_restored_successfully": "Asset restored successfully",
   "assets_deleted_permanently": "{} asset(s) deleted permanently",
   "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server",
-  "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device",
+  "assets_removed_permanently_from_device": "{} elemento(s) eliminado(s) permanentemente de su dispositivo",
   "assets_restored_successfully": "{} asset(s) restored successfully",
   "assets_trashed": "{} asset(s) trashed",
   "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
-  "asset_viewer_settings_title": "Asset Viewer",
+  "asset_viewer_settings_subtitle": "Administra las configuracioens de tu visor de fotos",
+  "asset_viewer_settings_title": "Visor de fotos",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})",
   "backup_album_selection_page_albums_tap": "Pulsar para incluir, pulsar dos veces para excluir",
   "backup_album_selection_page_assets_scatter": "Los archivos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.",
@@ -127,12 +137,13 @@
   "backup_manual_success": "Exitoso",
   "backup_manual_title": "Estado de subida",
   "backup_options_page_title": "Backup options",
+  "backup_setting_subtitle": "Administra las configuraciones de respaldo en segundo y primer plano",
   "cache_settings_album_thumbnails": "Miniaturas de la página de la biblioteca ({} recursos)",
   "cache_settings_clear_cache_button": "Borrar caché",
   "cache_settings_clear_cache_button_title": "Borra la caché de la aplicación. Esto afectará significativamente el rendimiento de la aplicación hasta que se reconstruya la caché.",
-  "cache_settings_duplicated_assets_clear_button": "CLEAR",
+  "cache_settings_duplicated_assets_clear_button": "LIMPIAR",
   "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
-  "cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
+  "cache_settings_duplicated_assets_title": "Elementos duplicados ({})",
   "cache_settings_image_cache_size": "Tamaño de la caché de imágenes ({} recursos)",
   "cache_settings_statistics_album": "Miniaturas de la biblioteca",
   "cache_settings_statistics_assets": "{} recursos ({})",
@@ -145,17 +156,22 @@
   "cache_settings_tile_subtitle": "Controla el comportamiento del almacenamiento local",
   "cache_settings_tile_title": "Almacenamiento local",
   "cache_settings_title": "Configuración de la caché",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Confirmar Contraseña",
   "change_password_form_description": "Hola {name},\n\nÉsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.",
   "change_password_form_new_password": "Nueva Contraseña",
   "change_password_form_password_mismatch": "Las contraseñas no coinciden",
   "change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseña",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Enter Password",
-  "client_cert_import": "Import",
+  "client_cert_import": "Importar",
   "client_cert_import_success_msg": "Client certificate is imported",
   "client_cert_invalid_msg": "Invalid certificate file or wrong password",
-  "client_cert_remove": "Remove",
+  "client_cert_remove": "Eliminar",
   "client_cert_remove_msg": "Client certificate is removed",
   "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login",
   "client_cert_title": "SSL Client Certificate",
@@ -164,7 +180,7 @@
   "common_create_new_album": "Crear nuevo álbum",
   "common_server_error": "Por favor, verifica tu conexión de red, asegúrate de que el servidor esté accesible y las versiones de la aplicación y del servidor sean compatibles.",
   "common_shared": "Compartido",
-  "contextual_search": "Sunrise on the beach",
+  "contextual_search": "Amanecer en la playa",
   "control_bottom_app_bar_add_to_album": "Agregar al álbum",
   "control_bottom_app_bar_album_info": "{} elementos",
   "control_bottom_app_bar_album_info_shared": "{} elementos · Compartido",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Desarchivar",
   "control_bottom_app_bar_unfavorite": "Unfavorite",
   "control_bottom_app_bar_upload": "Subir",
+  "create_album": "Create album",
   "create_album_page_untitled": "Sin título",
+  "create_new": "CREATE NEW",
   "create_shared_album_page_create": "Crear",
   "create_shared_album_page_share": "Compartir",
   "create_shared_album_page_share_add_assets": "AGREGAR RECURSOS",
@@ -193,12 +211,13 @@
   "crop": "Crop",
   "curated_location_page_title": "Lugares",
   "curated_object_page_title": "Objetos",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, dd MMM",
   "daily_title_text_date_year": "E, dd de MMM, yyyy",
   "date_format": "E d, LLL y • h:mm a",
   "delete_dialog_alert": "Estos elementos se eliminarán permanentemente de Immich y de tu dispositivo",
   "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
-  "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
+  "delete_dialog_alert_local_non_backed_up": "Algunas de las imágenes no tienen copia de seguridad y serán borradas de forma permanente de tu dispositivo",
   "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
   "delete_dialog_cancel": "Cancelar",
   "delete_dialog_ok": "Eliminar",
@@ -210,28 +229,47 @@
   "delete_shared_link_dialog_title": "Eliminar enlace compartido",
   "description_input_hint_text": "Agregar descripción...",
   "description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles",
+  "download_canceled": "Download canceled",
+  "download_complete": "Download complete",
+  "download_enqueue": "Download enqueued",
   "download_error": "Download Error",
+  "download_failed": "Download failed",
+  "download_filename": "file: {}",
+  "download_finished": "Download finished",
+  "downloading": "Downloading...",
+  "downloading_media": "Downloading media",
+  "download_notfound": "Download not found",
+  "download_paused": "Download paused",
   "download_started": "Download started",
   "download_sucess": "Download success",
   "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_waiting_to_retry": "Waiting to retry",
   "edit_date_time_dialog_date_time": "Date and Time",
   "edit_date_time_dialog_timezone": "Timezone",
   "edit_image_title": "Edit",
   "edit_location_dialog_title": "Location",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Error: {}",
   "exif_bottom_sheet_description": "Agregar Descripción...",
   "exif_bottom_sheet_details": "DETALLES",
   "exif_bottom_sheet_location": "UBICACIÓN",
   "exif_bottom_sheet_location_add": "Add a location",
-  "exif_bottom_sheet_people": "PEOPLE",
+  "exif_bottom_sheet_people": "PERSONAS",
   "exif_bottom_sheet_person_add_person": "Add name",
   "experimental_settings_new_asset_list_subtitle": "Trabajo en progreso",
   "experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental",
   "experimental_settings_subtitle": "¡Úsalo bajo tu propio riesgo!",
   "experimental_settings_title": "Experimental",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favoritos",
   "favorites_page_no_favorites": "No se encontraron recursos marcados como favoritos",
   "favorites_page_title": "Favoritos",
   "filename_search": "File name or extension",
+  "filter": "Filter",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Enable haptic feedback",
   "haptic_feedback_title": "Haptic Feedback",
   "header_settings_add_header_tip": "Add Header",
@@ -239,8 +277,8 @@
   "header_settings_header_name_input": "Header name",
   "header_settings_header_value_input": "Header value",
   "header_settings_page_title": "Proxy Headers",
-  "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
-  "headers_settings_tile_title": "Custom proxy headers",
+  "headers_settings_tile_subtitle": "Configura headers HTTP que la aplicación incluirá en cada petición de red",
+  "headers_settings_tile_title": "Cabeceras de proxy personalizadas",
   "home_page_add_to_album_conflicts": "{added} recursos agregados al álbum {album}.\n{failed} recursos ya existen en el álbum.",
   "home_page_add_to_album_err_local": "Aún no se pueden agregar recursos locales a álbumes, omitiendo",
   "home_page_add_to_album_success": "{added} recursos agregados al álbum {album}.",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "Si ésta es la primera vez que usas la app, por favor, asegúrate de elegir un álbum de respaldo para que la línea de tiempo pueda cargar fotos y videos en los álbumes.",
   "home_page_share_err_local": "No se pueden compartir activos locales a través de un enlace, omitiendo",
   "home_page_upload_err_limit": "Sólo se pueden subir un máximo de 30 recursos a la vez, omitiendo",
+  "ignore_icloud_photos": "Ignore iCloud photos",
+  "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
   "image_saved_successfully": "Image saved",
   "image_viewer_page_state_provider_download_error": "Error de descarga",
   "image_viewer_page_state_provider_download_started": "Download Started",
@@ -262,18 +302,23 @@
   "image_viewer_page_state_provider_share_error": "Error al compartir",
   "invalid_date": "Invalid date",
   "invalid_date_format": "Invalid date format",
+  "library": "Biblioteca",
   "library_page_albums": "Álbumes",
   "library_page_archive": "Archivo",
   "library_page_device_albums": "Álbumes en el dispositivo",
   "library_page_favorites": "Favoritos",
   "library_page_new_album": "Nuevo álbum",
   "library_page_sharing": "Compartiendo",
-  "library_page_sort_asset_count": "Number of assets",
+  "library_page_sort_asset_count": "Número de recursos",
   "library_page_sort_created": "Creado más recientemente",
   "library_page_sort_last_modified": "Modificado más recientemente",
-  "library_page_sort_most_oldest_photo": "Oldest photo",
+  "library_page_sort_most_oldest_photo": "Foto más antigua",
   "library_page_sort_most_recent_photo": "Foto más reciente",
   "library_page_sort_title": "Título del álbum",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Choose on map",
   "location_picker_latitude": "Latitude",
   "location_picker_latitude_error": "Enter a valid latitude",
@@ -336,12 +381,15 @@
   "memories_check_back_tomorrow": "Check back tomorrow for more memories",
   "memories_start_over": "Start Over",
   "memories_swipe_to_close": "Swipe up to close",
-  "memories_year_ago": "A year ago",
-  "memories_years_ago": "{} years ago",
+  "memories_year_ago": "Hace un año",
+  "memories_years_ago": "Hace {} años",
   "monthly_title_text_date_format": "MMMM y",
   "motion_photos_page_title": "Fotos en movimiento",
   "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
   "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
+  "my_albums": "Mis álbumes",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Configuraciones de acceso por URL al servidor",
   "no_assets_to_show": "No assets to show",
   "no_name": "No name",
   "notification_permission_dialog_cancel": "Cancelar",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Concede permiso para activar las notificaciones.",
   "notification_permission_list_tile_enable_button": "Activar notificaciones",
   "notification_permission_list_tile_title": "Permisos de notificación",
+  "on_this_device": "On this device",
   "partner_list_user_photos": "{user}'s photos",
   "partner_list_view_all": "View all",
   "partner_page_add_partner": "Agregar compañero",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} ya no podrá acceder a tus fotos",
   "partner_page_stop_sharing_title": "¿Dejar de compartir tus fotos?",
   "partner_page_title": "Compañero",
+  "partners": "Partners",
+  "people": "Personas",
   "permission_onboarding_back": "Volver",
   "permission_onboarding_continue_anyway": "Continuar de todos modos",
   "permission_onboarding_get_started": "Empezar",
@@ -371,7 +422,9 @@
   "permission_onboarding_permission_granted": "¡Permiso concedido! Todo listo.",
   "permission_onboarding_permission_limited": "Permiso limitado. Para permitir que Immich haga copia de seguridad y gestione toda tu colección de galería, concede permisos de fotos y videos en Configuración.",
   "permission_onboarding_request": "Immich requiere permiso para ver tus fotos y videos.",
-  "preferences_settings_title": "Preferences",
+  "places": "Places",
+  "preferences_settings_subtitle": "Configuraciones de la aplicación",
+  "preferences_settings_title": "Preferencias",
   "profile_drawer_app_logs": "Registros",
   "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
   "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
@@ -383,16 +436,19 @@
   "profile_drawer_settings": "Configuración",
   "profile_drawer_sign_out": "Cerrar sesión",
   "profile_drawer_trash": "Papelera",
+  "recently_added": "Añadidos recientemente",
   "recently_added_page_title": "Recién Agregados",
+  "save": "Save",
   "save_to_gallery": "Save to gallery",
   "scaffold_body_error_occurred": "Error occurred",
+  "search_albums": "Buscar álbum",
   "search_bar_hint": "Busca tus fotos",
   "search_filter_apply": "Apply filter",
-  "search_filter_camera": "Camera",
-  "search_filter_camera_make": "Make",
-  "search_filter_camera_model": "Model",
-  "search_filter_camera_title": "Select camera type",
-  "search_filter_date": "Date",
+  "search_filter_camera": "Cámara",
+  "search_filter_camera_make": "Marca",
+  "search_filter_camera_model": "Modelo",
+  "search_filter_camera_title": "Elige tipo de cámara",
+  "search_filter_date": "Fecha",
   "search_filter_date_interval": "{start} to {end}",
   "search_filter_date_title": "Select a date range",
   "search_filter_display_option_archive": "Archive",
@@ -400,21 +456,21 @@
   "search_filter_display_option_not_in_album": "Not in album",
   "search_filter_display_options": "Display Options",
   "search_filter_display_options_title": "Display options",
-  "search_filter_location": "Location",
+  "search_filter_location": "Ubicación",
   "search_filter_location_city": "City",
   "search_filter_location_country": "Country",
   "search_filter_location_state": "State",
   "search_filter_location_title": "Select location",
-  "search_filter_media_type": "Media Type",
+  "search_filter_media_type": "Tipo de archivo",
   "search_filter_media_type_all": "All",
   "search_filter_media_type_image": "Image",
-  "search_filter_media_type_title": "Select media type",
+  "search_filter_media_type_title": "Selecciona el tipo de archivo",
   "search_filter_media_type_video": "Video",
-  "search_filter_people": "People",
-  "search_filter_people_title": "Select people",
+  "search_filter_people": "Personas",
+  "search_filter_people_title": "Seleccionar personas",
   "search_page_categories": "Categorías",
   "search_page_favorites": "Favoritos",
-  "search_page_motion_photos": "Fotos en movimiento",
+  "search_page_motion_photos": "Fotos en .ovimiento",
   "search_page_no_objects": "No hay información de objetos disponible",
   "search_page_no_places": "No hay información de lugares disponible",
   "search_page_people": "Personas",
@@ -428,6 +484,7 @@
   "search_page_places": "Lugares",
   "search_page_recently_added": "Recién agregados",
   "search_page_screenshots": "Capturas de pantalla",
+  "search_page_search_photos_videos": "Busca tus fotos y videos",
   "search_page_selfies": "Selfies",
   "search_page_things": "Cosas",
   "search_page_videos": "Videos",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Sugerencias",
   "select_user_for_sharing_page_err_album": "Error al crear álbum",
   "select_user_for_sharing_page_share_suggestions": "Sugerencias",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "Versión de la Aplicación",
   "server_info_box_latest_release": "Ultima versión",
   "server_info_box_server_url": "URL del Servidor",
@@ -449,9 +507,10 @@
   "setting_image_viewer_original_title": "Cargar imagen original",
   "setting_image_viewer_preview_subtitle": "Activar para cargar una imagen de resolución media. Deshabilitar para cargar directamente la imagen original o usar una miniatura.",
   "setting_image_viewer_preview_title": "Cargar imagen de previsualización",
-  "setting_image_viewer_title": "Images",
-  "setting_languages_apply": "Apply",
-  "setting_languages_title": "Languages",
+  "setting_image_viewer_title": "Imágenes",
+  "setting_languages_apply": "Aplicar",
+  "setting_languages_subtitle": "Cambia el idioma de la aplicación",
+  "setting_languages_title": "Idiomas",
   "setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {}",
   "setting_notifications_notify_hours": "{} horas",
   "setting_notifications_notify_immediately": "inmediatamente",
@@ -466,8 +525,8 @@
   "setting_notifications_total_progress_title": "Mostrar progreso total de copia de seguridad en segundo plano",
   "setting_pages_app_bar_settings": "Configuración",
   "settings_require_restart": "Por favor, reinicia Immich para aplicar este ajuste",
-  "setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
-  "setting_video_viewer_looping_title": "Looping",
+  "setting_video_viewer_looping_subtitle": "Habilitar reproducción en bucle del video en la vista detallada",
+  "setting_video_viewer_looping_title": "Bucle",
   "setting_video_viewer_title": "Videos",
   "share_add": "Agregar",
   "share_add_photos": "Agregar fotos",
@@ -480,11 +539,11 @@
   "shared_album_activity_remove_title": "Eliminar actividad",
   "shared_album_activity_setting_subtitle": "Permitir que otros respondan",
   "shared_album_activity_setting_title": "Comentarios y me gusta",
-  "shared_album_section_people_action_error": "Error leaving/removing from album",
-  "shared_album_section_people_action_leave": "Remove user from album",
-  "shared_album_section_people_action_remove_user": "Remove user from album",
-  "shared_album_section_people_owner_label": "Owner",
-  "shared_album_section_people_title": "PEOPLE",
+  "shared_album_section_people_action_error": "Error retirando/eliminando del album",
+  "shared_album_section_people_action_leave": "Eliminar usuario del álbum",
+  "shared_album_section_people_action_remove_user": "Eliminar usuario del álbum",
+  "shared_album_section_people_owner_label": "Propietario",
+  "shared_album_section_people_title": "PERSONAS",
   "share_dialog_preparing": "Preparando...",
   "shared_link_app_bar_title": "Enlaces compartidos",
   "shared_link_clipboard_copied_massage": "Copied to clipboard",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Upload",
   "shared_link_manage_links": "Administrar enlaces compartidos",
   "shared_link_public_album": "Public album",
+  "shared_links": "Shared links",
   "share_done": "Hecho",
+  "shared_with_me": "Compartidos conmigo",
   "share_invite": "Invitar al álbum",
   "sharing_page_album": "Álbumes compartidos",
   "sharing_page_description": "Crea álbumes compartidos para compartir fotos y videos con personas de tu red.",
@@ -540,8 +601,8 @@
   "sharing_silver_appbar_shared_links": "Enlaces compartidos",
   "sharing_silver_appbar_share_partner": "Compartir con compañero",
   "sync": "Sync",
-  "sync_albums": "Sync albums",
-  "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
+  "sync_albums": "Sincronizar álbumes",
+  "sync_albums_manual_subtitle": "Sincroniza todos los videos y fotos subidos con los álbumes seleccionados a respaldar",
   "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
   "tab_controller_nav_library": "Biblioteca",
   "tab_controller_nav_photos": "Fotos",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "La carga en tres etapas puede aumentar el rendimiento de carga pero provoca un consumo de red significativamente mayor",
   "theme_setting_three_stage_loading_title": "Activar carga en tres etapas",
   "translated_text_options": "Opciones",
+  "trash": "Trash",
   "trash_emptied": "Emptied trash",
   "trash_page_delete": "Eliminar",
   "trash_page_delete_all": "Eliminar todos",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "¿Quieres respaldar los recursos seleccionados en el servidor?",
   "upload_dialog_ok": "Subir",
   "upload_dialog_title": "Subir recurso",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Aceptar",
   "version_announcement_overlay_release_notes": "notas de la versión",
   "version_announcement_overlay_text_1": "Hola, amigo, hay una nueva versión de",
   "version_announcement_overlay_text_2": "por favor, tómate tu tiempo para visitar las ",
   "version_announcement_overlay_text_3": " y asegúrate de que la configuración de docker-compose y .env estén actualizadas para evitar cualquier error de configuración, especialmente si utilizas WatchTower o cualquier mecanismo que actualice automáticamente la aplicación del servidor.",
   "version_announcement_overlay_title": "Nueva versión del servidor disponible \uD83C\uDF89",
+  "videos": "Videos",
   "viewer_remove_from_stack": "Eliminar de la pila",
   "viewer_stack_use_as_main_asset": "Utilizar como recurso principal",
-  "viewer_unstack": "Desapilar"
-}
+  "viewer_unstack": "Desapilar",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
+}
\ No newline at end of file
diff --git a/mobile/assets/i18n/fi-FI.json b/mobile/assets/i18n/fi-FI.json
index cb687ecef5..8c5b1d74fe 100644
--- a/mobile/assets/i18n/fi-FI.json
+++ b/mobile/assets/i18n/fi-FI.json
@@ -6,6 +6,8 @@
   "action_common_save": "Save",
   "action_common_select": "Select",
   "action_common_update": "Päivitä",
+  "add_a_name": "Add a name",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Lisätty albumiin {album}",
   "add_to_album_bottom_sheet_already_exists": "Kohde on jo albumissa {album}",
   "advanced_settings_log_level_title": "Lokitaso: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Vianetsintä",
   "album_info_card_backup_album_excluded": "JÄTETTY POIS",
   "album_info_card_backup_album_included": "SISÄLLYTETTY",
+  "albums": "Albums",
   "album_thumbnail_card_item": "1 kohde",
   "album_thumbnail_card_items": "{} kohdetta",
   "album_thumbnail_card_shared": "Jaettu",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Poista albumista",
   "album_viewer_appbar_share_to": "Jaa",
   "album_viewer_page_share_add_users": "Lisää käyttäjiä",
+  "all": "All",
   "all_people_page_title": "Ihmiset",
   "all_videos_page_title": "Videot",
   "app_bar_signout_dialog_content": "Haluatko varmasti kirjautua ulos?",
   "app_bar_signout_dialog_ok": "Kyllä",
   "app_bar_signout_dialog_title": "Kirjaudu ulos",
+  "archived": "Archived",
   "archive_page_no_archived_assets": "Arkistoituja kohteita ei löytynyt",
   "archive_page_title": "Arkisto ({})",
   "asset_action_delete_err_read_only": "Vain luku-tilassa olevia kohteita ei voitu poistaa, ohitetaan",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} asset(s) restored successfully",
   "assets_trashed": "{} asset(s) trashed",
   "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "Katselin",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Laitteen albumit ({})",
   "backup_album_selection_page_albums_tap": "Napauta sisällyttääksesi, kaksoisnapauta jättääksesi pois",
   "backup_album_selection_page_assets_scatter": "Kohteet voivat olla hajaantuneina useisiin albumeihin. Albumeita voidaan sisällyttää varmuuskopiointiin tai jättää siitä pois.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Onnistui",
   "backup_manual_title": "Lähetyksen tila",
   "backup_options_page_title": "Varmuuskopioinnin asetukset",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Kirjastosivun esikatselukuvat ({} kohdetta)",
   "cache_settings_clear_cache_button": "Tyhjennä välimuisti",
   "cache_settings_clear_cache_button_title": "Tyhjennä sovelluksen välimuisti. Tämä vaikuttaa merkittävästi sovelluksen suorituskykyyn, kunnes välimuisti on rakennettu uudelleen.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Hallitse paikallista tallenustilaa",
   "cache_settings_tile_title": "Paikallinen tallennustila",
   "cache_settings_title": "Välimuistin asetukset",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Vahvista salasana",
   "change_password_form_description": "Hei {name},\n\nTämä on joko ensimmäinen kirjautumisesi järjestelmään tai salasanan vaihtaminen vaihtaminen on pakotettu. Ole hyvä ja syötä uusi salasana alle.",
   "change_password_form_new_password": "Uusi salasana",
   "change_password_form_password_mismatch": "Salasanat eivät täsmää",
   "change_password_form_reenter_new_password": "Uusi salasana uudelleen",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Enter Password",
   "client_cert_import": "Import",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Palauta arkistosta",
   "control_bottom_app_bar_unfavorite": "Poista suosikeista",
   "control_bottom_app_bar_upload": "Siirrä palvelimelle",
+  "create_album": "Create album",
   "create_album_page_untitled": "Nimetön",
+  "create_new": "CREATE NEW",
   "create_shared_album_page_create": "Luo",
   "create_shared_album_page_share": "Jaa",
   "create_shared_album_page_share_add_assets": "LISÄÄ KOHTEITA",
@@ -193,6 +211,7 @@
   "crop": "Crop",
   "curated_location_page_title": "Paikat",
   "curated_object_page_title": "Asiat",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "Poista jaettu linkki",
   "description_input_hint_text": "Lisää kuvaus...",
   "description_input_submit_error": "Virhe kuvauksen päivittämisessä, tarkista lisätiedot lokista",
+  "download_canceled": "Download canceled",
+  "download_complete": "Download complete",
+  "download_enqueue": "Download enqueued",
   "download_error": "Download Error",
+  "download_failed": "Download failed",
+  "download_filename": "file: {}",
+  "download_finished": "Download finished",
+  "downloading": "Downloading...",
+  "downloading_media": "Downloading media",
+  "download_notfound": "Download not found",
+  "download_paused": "Download paused",
   "download_started": "Download started",
   "download_sucess": "Download success",
   "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_waiting_to_retry": "Waiting to retry",
   "edit_date_time_dialog_date_time": "Päivämäärä ja aika",
   "edit_date_time_dialog_timezone": "Aikavyöhyke",
   "edit_image_title": "Edit",
   "edit_location_dialog_title": "Sijainti",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Error: {}",
   "exif_bottom_sheet_description": "Lisää kuvaus…",
   "exif_bottom_sheet_details": "TIEDOT",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Ota käyttöön kokeellinen kuvaruudukko",
   "experimental_settings_subtitle": "Käyttö omalla vastuulla!",
   "experimental_settings_title": "Kokeellinen",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favorites",
   "favorites_page_no_favorites": "Suosikkikohteita ei löytynyt",
   "favorites_page_title": "Suosikit",
   "filename_search": "File name or extension",
+  "filter": "Filter",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Ota haptinen palaute käyttöön",
   "haptic_feedback_title": "Haptinen palaute",
   "header_settings_add_header_tip": "Add Header",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "Jos käytät sovellusta ensimmäistä kertaa, muista valita varmuuskopioitavat albumi(t), jotta aikajanalla voi olla kuvia ja videoita.",
   "home_page_share_err_local": "Paikallisia kohteita ei voitu jakaa linkkien avulla. Hypätään yli",
   "home_page_upload_err_limit": "Voit lähettää palvelimelle enintään 30 kohdetta kerrallaan, ohitetaan",
+  "ignore_icloud_photos": "Ignore iCloud photos",
+  "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
   "image_saved_successfully": "Image saved",
   "image_viewer_page_state_provider_download_error": "Lataus epäonnistui",
   "image_viewer_page_state_provider_download_started": "Lataaminen aloitettu",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "Jakovirhe",
   "invalid_date": "Invalid date",
   "invalid_date_format": "Invalid date format",
+  "library": "Library",
   "library_page_albums": "Albumit",
   "library_page_archive": "Arkisto",
   "library_page_device_albums": "Laitteen albumit",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Vanhin kuva",
   "library_page_sort_most_recent_photo": "Viimeisin kuva",
   "library_page_sort_title": "Albumin otsikko",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Valitse kartalta",
   "location_picker_latitude": "Leveysaste",
   "location_picker_latitude_error": "Lisää kelvollinen leveysaste",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Liikekuvat",
   "multiselect_grid_edit_date_time_err_read_only": "Vain luku -tilassa olevien kohteiden päivämäärää ei voitu muokata, ohitetaan",
   "multiselect_grid_edit_gps_err_read_only": "Vain luku-tilassa olevien kohteiden sijantitietoja ei voitu muokata, ohitetaan",
+  "my_albums": "My albums",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "Ei näytettäviä kohteita",
   "no_name": "No name",
   "notification_permission_dialog_cancel": "Peruuta",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Myönnä käyttöoikeus ottaaksesi ilmoitukset käyttöön.",
   "notification_permission_list_tile_enable_button": "Ota ilmoitukset käyttöön",
   "notification_permission_list_tile_title": "Ilmoitusten käyttöoikeus",
+  "on_this_device": "On this device",
   "partner_list_user_photos": "Käyttäjän {user} kuvat",
   "partner_list_view_all": "Näytä kaikki",
   "partner_page_add_partner": "Lisää kumppani",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} ei voi enää käyttää kuviasi.",
   "partner_page_stop_sharing_title": "Lopetetaanko kuvien jakaminen?",
   "partner_page_title": "Kumppani",
+  "partners": "Partners",
+  "people": "People",
   "permission_onboarding_back": "Takaisin",
   "permission_onboarding_continue_anyway": "Jatka silti",
   "permission_onboarding_get_started": "Aloittaminen",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Käyttöoikeus myönnetty! Kaikki valmista.",
   "permission_onboarding_permission_limited": "Rajoitettu käyttöoikeus. Salliaksesi Immichin varmuuskopioida ja hallita koko kuvakirjastoasi, myönnä oikeus kuviin ja videoihin asetuksista.",
   "permission_onboarding_request": "Immich vaatii käyttöoikeuden kuvien ja videoiden käyttämiseen.",
+  "places": "Places",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Asetukset",
   "profile_drawer_app_logs": "Lokit",
   "profile_drawer_client_out_of_date_major": "Sovelluksen mobiiliversio on vanhentunut. Päivitä viimeisimpään merkittävään versioon.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Asetukset",
   "profile_drawer_sign_out": "Kirjaudu ulos",
   "profile_drawer_trash": "Roskakori",
+  "recently_added": "Recently added",
   "recently_added_page_title": "Viimeksi lisätyt",
+  "save": "Save",
   "save_to_gallery": "Save to gallery",
   "scaffold_body_error_occurred": "Tapahtui virhe",
+  "search_albums": "Search albums",
   "search_bar_hint": "Etsi kuvia",
   "search_filter_apply": "Käytä",
   "search_filter_camera": "Camera",
@@ -428,6 +484,7 @@
   "search_page_places": "Paikat",
   "search_page_recently_added": "Viimeksi lisätyt",
   "search_page_screenshots": "Näyttökuvat",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Selfiet",
   "search_page_things": "Asiat",
   "search_page_videos": "Videot",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Ehdotukset",
   "select_user_for_sharing_page_err_album": "Albumin luonti epäonnistui",
   "select_user_for_sharing_page_share_suggestions": "Ehdotukset",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "Sovelluksen versio",
   "server_info_box_latest_release": "Viimeisin versio",
   "server_info_box_server_url": "Palvelimen URL-osoite",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Lataa esikatselukuva",
   "setting_image_viewer_title": "Kuvat",
   "setting_languages_apply": "Käytä",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Kieli",
   "setting_notifications_notify_failures_grace_period": "Ilmoita taustavarmuuskopioinnin epäonnistumisista: {}",
   "setting_notifications_notify_hours": "{} tunnin välein",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Lähetä",
   "shared_link_manage_links": "Hallitse jaettuja linkkejä",
   "shared_link_public_album": "Julkinen albumi",
+  "shared_links": "Shared links",
   "share_done": "Valmis",
+  "shared_with_me": "Shared with me",
   "share_invite": "Kutsu albumiin",
   "sharing_page_album": "Jaetut albumit",
   "sharing_page_description": "Luo jaettuja albumeja jakaaksesi kuvia ja videoita läheisillesi.",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Kolmivaiheinen lataaminen saattaa parantaa latauksen suorituskykyä, mutta lisää kaistankäyttöä huomattavasti.",
   "theme_setting_three_stage_loading_title": "Ota kolmivaiheinen lataus käyttöön",
   "translated_text_options": "Vaihtoehdot",
+  "trash": "Trash",
   "trash_emptied": "Emptied trash",
   "trash_page_delete": "Poista",
   "trash_page_delete_all": "Poista kaikki",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Haluatko varmuuskopioida valitut kohteet palvelimelle?",
   "upload_dialog_ok": "Lähetä",
   "upload_dialog_title": "Lähetä kohde",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Tiedostan",
   "version_announcement_overlay_release_notes": "julkaisutiedoissa",
   "version_announcement_overlay_text_1": "Hei, kaveri! Uusi palvelinversio on saatavilla sovelluksesta",
   "version_announcement_overlay_text_2": "Ota hetki aikaa vieraillaksesi",
   "version_announcement_overlay_text_3": "ja varmista, että käyttämäsi docker-compose ja .env-asetukset ovat ajantasalla välttyäksesi asetusongelmilta. Varsinkin jos käytät WatchToweria tai jotain muuta mekanismia päivittääksesi palvelinsovellusta automaattisesti.",
   "version_announcement_overlay_title": "Uusi palvelinversio saatavilla \uD83C\uDF89",
+  "videos": "Videos",
   "viewer_remove_from_stack": "Poista pinosta",
   "viewer_stack_use_as_main_asset": "Käytä pääkohteena",
-  "viewer_unstack": "Pura pino"
+  "viewer_unstack": "Pura pino",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/fr-CA.json b/mobile/assets/i18n/fr-CA.json
index 8d742c3a59..056f3c7ae8 100644
--- a/mobile/assets/i18n/fr-CA.json
+++ b/mobile/assets/i18n/fr-CA.json
@@ -6,6 +6,8 @@
   "action_common_save": "Save",
   "action_common_select": "Select",
   "action_common_update": "Update",
+  "add_a_name": "Add a name",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Ajouté à {album}",
   "add_to_album_bottom_sheet_already_exists": "Déjà dans {album}",
   "advanced_settings_log_level_title": "Log level: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Dépannage",
   "album_info_card_backup_album_excluded": "EXCLUS",
   "album_info_card_backup_album_included": "INCLUS",
+  "albums": "Albums",
   "album_thumbnail_card_item": "1 élément",
   "album_thumbnail_card_items": "{} éléments",
   "album_thumbnail_card_shared": " · Partagé",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Retirer de l'album",
   "album_viewer_appbar_share_to": "Partager à",
   "album_viewer_page_share_add_users": "Ajouter des utilisateurs",
+  "all": "All",
   "all_people_page_title": "Personnes",
   "all_videos_page_title": "Vidéos",
   "app_bar_signout_dialog_content": "Êtes-vous sûr de vouloir vous déconnecter?",
   "app_bar_signout_dialog_ok": "Oui",
   "app_bar_signout_dialog_title": "Se déconnecter",
+  "archived": "Archived",
   "archive_page_no_archived_assets": "Aucun élément archivé n'a été trouvé",
   "archive_page_title": "Archive ({})",
   "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} asset(s) restored successfully",
   "assets_trashed": "{} asset(s) trashed",
   "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "Asset Viewer",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Albums sur l'appareil ({})",
   "backup_album_selection_page_albums_tap": "Tapez pour inclure, tapez deux fois pour exclure",
   "backup_album_selection_page_assets_scatter": "Les éléments peuvent être répartis sur plusieurs albums. De ce fait, les albums peuvent être inclus ou exclus pendant le processus de sauvegarde.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Succès ",
   "backup_manual_title": "Statut du téléchargement ",
   "backup_options_page_title": "Backup options",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "vignettes de la page bibliothèque ({} éléments)",
   "cache_settings_clear_cache_button": "Effacer le cache",
   "cache_settings_clear_cache_button_title": "Efface le cache de l'application. Cela aura un impact significatif sur les performances de l'application jusqu'à ce que le cache soit reconstruit.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Contrôler le comportement du stockage local",
   "cache_settings_tile_title": "Stockage local",
   "cache_settings_title": "Paramètres de mise en cache",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Confirmez le mot de passe",
   "change_password_form_description": "Bonjour {name},\n\nC'est la première fois que vous vous connectez au système ou vous avez demandé de changer votre mot de passe. Veuillez saisir le nouveau mot de passe ci-dessous.",
   "change_password_form_new_password": "Nouveau mot de passe",
   "change_password_form_password_mismatch": "Les mots de passe ne correspondent pas",
   "change_password_form_reenter_new_password": "Saisissez à nouveau le nouveau mot de passe",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Enter Password",
   "client_cert_import": "Import",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Désarchiver",
   "control_bottom_app_bar_unfavorite": "Unfavorite",
   "control_bottom_app_bar_upload": "Téléverser",
+  "create_album": "Create album",
   "create_album_page_untitled": "Sans titre",
+  "create_new": "CREATE NEW",
   "create_shared_album_page_create": "Créer",
   "create_shared_album_page_share": "Partager",
   "create_shared_album_page_share_add_assets": "AJOUTER DES ÉLÉMENTS",
@@ -193,6 +211,7 @@
   "crop": "Crop",
   "curated_location_page_title": "Places",
   "curated_object_page_title": "Objets",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, dd MMM",
   "daily_title_text_date_year": "E, dd MMM, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "Supprimer le lien partagé",
   "description_input_hint_text": "Ajouter une description...",
   "description_input_submit_error": "Erreur de mise à jour de la description, vérifier le journal pour plus de détails",
+  "download_canceled": "Download canceled",
+  "download_complete": "Download complete",
+  "download_enqueue": "Download enqueued",
   "download_error": "Download Error",
+  "download_failed": "Download failed",
+  "download_filename": "file: {}",
+  "download_finished": "Download finished",
+  "downloading": "Downloading...",
+  "downloading_media": "Downloading media",
+  "download_notfound": "Download not found",
+  "download_paused": "Download paused",
   "download_started": "Download started",
   "download_sucess": "Download success",
   "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_waiting_to_retry": "Waiting to retry",
   "edit_date_time_dialog_date_time": "Date and Time",
   "edit_date_time_dialog_timezone": "Timezone",
   "edit_image_title": "Edit",
   "edit_location_dialog_title": "Location",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Error: {}",
   "exif_bottom_sheet_description": "Ajouter une description...",
   "exif_bottom_sheet_details": "DÉTAILS",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Activer la grille de photos expérimentale",
   "experimental_settings_subtitle": "Utilisez à vos dépends!",
   "experimental_settings_title": "Expérimental",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favorites",
   "favorites_page_no_favorites": "Aucun élément favori n'a été trouvé",
   "favorites_page_title": "Favoris",
   "filename_search": "File name or extension",
+  "filter": "Filter",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Enable haptic feedback",
   "haptic_feedback_title": "Haptic Feedback",
   "header_settings_add_header_tip": "Add Header",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "Si c'est la première fois que vous utilisez l'application, veillez à choisir un ou plusieurs albums de sauvegarde afin que la chronologie puisse alimenter les photos et les vidéos de cet ou ces albums.",
   "home_page_share_err_local": "Can not share local assets via link, skipping",
   "home_page_upload_err_limit": "Limite de téléchargement de 30 éléments en même temps, demande ignorée",
+  "ignore_icloud_photos": "Ignore iCloud photos",
+  "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
   "image_saved_successfully": "Image saved",
   "image_viewer_page_state_provider_download_error": "Erreur de téléchargement",
   "image_viewer_page_state_provider_download_started": "Download Started",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "Erreur de partage",
   "invalid_date": "Invalid date",
   "invalid_date_format": "Invalid date format",
+  "library": "Library",
   "library_page_albums": "Albums",
   "library_page_archive": "Archive",
   "library_page_device_albums": "Albums sur l'appareil",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Oldest photo",
   "library_page_sort_most_recent_photo": "Photo la plus récente",
   "library_page_sort_title": "Titre de l'album",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Choose on map",
   "location_picker_latitude": "Latitude",
   "location_picker_latitude_error": "Enter a valid latitude",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Photos avec mouvement",
   "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
   "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
+  "my_albums": "My albums",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "No assets to show",
   "no_name": "No name",
   "notification_permission_dialog_cancel": "Annuler",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Accordez la permission d'activer les notifications.",
   "notification_permission_list_tile_enable_button": "Activer les notifications",
   "notification_permission_list_tile_title": "Permission de notification",
+  "on_this_device": "On this device",
   "partner_list_user_photos": "{user}'s photos",
   "partner_list_view_all": "View all",
   "partner_page_add_partner": "Ajouter un partenaire",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} ne pourra plus accéder à vos photos.",
   "partner_page_stop_sharing_title": "Arrêter de partager vos photos?",
   "partner_page_title": "Partenaire",
+  "partners": "Partners",
+  "people": "People",
   "permission_onboarding_back": "Retour",
   "permission_onboarding_continue_anyway": "Continuer quand même",
   "permission_onboarding_get_started": "Commencer",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Permission accordée! Vous êtes prêts.",
   "permission_onboarding_permission_limited": "Permission limitée. Pour permettre à Immich de sauvegarder et de gérer l'ensemble de votre bibliothèque, accordez l'autorisation pour les photos et vidéos dans les Paramètres.",
   "permission_onboarding_request": "Immich demande l'autorisation de visionner vos photos et vidéo",
+  "places": "Places",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Preferences",
   "profile_drawer_app_logs": "Journaux",
   "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Paramètres",
   "profile_drawer_sign_out": "Se déconnecter",
   "profile_drawer_trash": "Corbeille",
+  "recently_added": "Recently added",
   "recently_added_page_title": "Récemment ajouté",
+  "save": "Save",
   "save_to_gallery": "Save to gallery",
   "scaffold_body_error_occurred": "Error occurred",
+  "search_albums": "Search albums",
   "search_bar_hint": "Rechercher vos photos",
   "search_filter_apply": "Apply filter",
   "search_filter_camera": "Camera",
@@ -428,6 +484,7 @@
   "search_page_places": "Lieux",
   "search_page_recently_added": "Récemment ajouté",
   "search_page_screenshots": "Captures d'écran",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Selfies",
   "search_page_things": "Objets",
   "search_page_videos": "Vidéos",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Suggestions",
   "select_user_for_sharing_page_err_album": "Échec de la création de l'album",
   "select_user_for_sharing_page_share_suggestions": "Suggestions",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "Version de l'application",
   "server_info_box_latest_release": "Dernière version",
   "server_info_box_server_url": "URL du serveur",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Charger l'image d'aperçu",
   "setting_image_viewer_title": "Images",
   "setting_languages_apply": "Apply",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Languages",
   "setting_notifications_notify_failures_grace_period": "Notifier les échecs de la sauvegarde en arrière-plan: {}",
   "setting_notifications_notify_hours": "{} heures",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Upload",
   "shared_link_manage_links": "Gérer les liens partagés",
   "shared_link_public_album": "Public album",
+  "shared_links": "Shared links",
   "share_done": "Fait",
+  "shared_with_me": "Shared with me",
   "share_invite": "Inviter à l'album",
   "sharing_page_album": "Albums partagés",
   "sharing_page_description": "Créez des albums partagés pour partager des photos et des vidéos avec les personnes de votre réseau.",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Le chargement en trois étapes peut améliorer les performances de chargement, mais entraîne une augmentation significative de la charge du réseau.",
   "theme_setting_three_stage_loading_title": "Activer le chargement en trois étapes",
   "translated_text_options": "Options",
+  "trash": "Trash",
   "trash_emptied": "Emptied trash",
   "trash_page_delete": "Supprimer",
   "trash_page_delete_all": "Tout supprimer",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Voulez-vous sauvegarder la sélection vers le serveur?",
   "upload_dialog_ok": "Télécharger ",
   "upload_dialog_title": "Télécharger cet élément ",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Confirmer",
   "version_announcement_overlay_release_notes": "notes de mise à jour",
   "version_announcement_overlay_text_1": "Bonjour, une nouvelle version de",
   "version_announcement_overlay_text_2": "veuillez prendre le temps de visiter le ",
   "version_announcement_overlay_text_3": " et assurez-vous que votre configuration docker-compose et .env est à jour pour éviter toute erreur de configuration, en particulier si vous utilisez WatchTower ou tout autre mécanisme qui gère la mise à jour automatique de votre application serveur.",
   "version_announcement_overlay_title": "Nouvelle version serveur disponible \uD83C\uDF89",
+  "videos": "Videos",
   "viewer_remove_from_stack": "Retirer de la pile",
   "viewer_stack_use_as_main_asset": "Utiliser comme élément principal",
-  "viewer_unstack": "Désempiler"
+  "viewer_unstack": "Désempiler",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/fr-FR.json b/mobile/assets/i18n/fr-FR.json
index 9ff5c6f280..bc46840f5f 100644
--- a/mobile/assets/i18n/fr-FR.json
+++ b/mobile/assets/i18n/fr-FR.json
@@ -6,9 +6,11 @@
   "action_common_save": "Sauvegarder",
   "action_common_select": "Sélectionner",
   "action_common_update": "Mise à jour",
+  "add_a_name": "Ajouter un nom",
+  "add_endpoint": "Ajouter une adresse",
   "add_to_album_bottom_sheet_added": "Ajouté à {album}",
   "add_to_album_bottom_sheet_already_exists": "Déjà dans {album}",
-  "advanced_settings_log_level_title": "Log level: {}",
+  "advanced_settings_log_level_title": "Niveau de log : {}",
   "advanced_settings_prefer_remote_subtitle": "Certains appareils sont terriblement lents à charger des miniatures à partir de ressources présentes sur l'appareil. Activez ce paramètre pour charger des images distantes à la place.",
   "advanced_settings_prefer_remote_title": "Préférer les images distantes",
   "advanced_settings_proxy_headers_subtitle": "Ajoutez des en-têtes personnalisés à chaque requête réseau",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Dépannage",
   "album_info_card_backup_album_excluded": "EXCLU",
   "album_info_card_backup_album_included": "INCLUS",
+  "albums": "Albums",
   "album_thumbnail_card_item": "1 élément",
   "album_thumbnail_card_items": "{} éléments",
   "album_thumbnail_card_shared": " · Partagé",
@@ -36,13 +39,15 @@
   "album_viewer_appbar_share_remove": "Retirer de l'album",
   "album_viewer_appbar_share_to": "Partager à",
   "album_viewer_page_share_add_users": "Ajouter des utilisateurs",
+  "all": "Tous",
   "all_people_page_title": "Personnes",
   "all_videos_page_title": "Vidéos",
   "app_bar_signout_dialog_content": "Êtes-vous sûr de vouloir vous déconnecter ?",
   "app_bar_signout_dialog_ok": "Oui",
   "app_bar_signout_dialog_title": "Se déconnecter",
+  "archived": "Archives",
   "archive_page_no_archived_assets": "Aucun élément archivé n'a été trouvé",
-  "archive_page_title": "Archive ({})",
+  "archive_page_title": "Archives ({})",
   "asset_action_delete_err_read_only": "Impossible de supprimer le(s) élément(s) en lecture seule.",
   "asset_action_share_err_offline": "Impossible de récupérer le(s) élément(s) hors ligne.",
   "asset_list_group_by_sub_title": "Regrouper par",
@@ -59,9 +64,14 @@
   "assets_deleted_permanently_from_server": "{} élément(s) supprimé(s) définitivement du serveur Immich",
   "assets_removed_permanently_from_device": "\"{} élément(s) supprimé(s) définitivement de votre appareil",
   "assets_restored_successfully": "Élément restauré avec succès",
-  "assets_trashed": "{} élément(s) déplacé(s) vers la corbeill",
+  "assets_trashed": "{} élément(s) déplacé(s) vers la corbeille",
   "assets_trashed_from_server": "{} élément(s) déplacé(s) vers la corbeille du serveur Immich",
-  "asset_viewer_settings_title": "Visualisateur d'éléments",
+  "asset_viewer_settings_subtitle": "Modifier les paramètres du visualiseur photos",
+  "asset_viewer_settings_title": "Visualiseur d'éléments",
+  "automatic_endpoint_switching_subtitle": "Se connecter localement lorsque connecté au WI-FI spécifié mais utiliser une adresse alternative lorsque connecté à un autre réseau",
+  "automatic_endpoint_switching_title": "Changement automatique d'adresse",
+  "background_location_permission": "Permission de localisation en arrière plan",
+  "background_location_permission_content": "Afin de pouvoir changer d'adresse en arrière plan, Immich doit avoir *en permanence* accès à la localisation précise, afin d'accéder au le nom du réseau Wi-Fi utilisé",
   "backup_album_selection_page_albums_device": "Albums sur l'appareil ({})",
   "backup_album_selection_page_albums_tap": "Tapez pour inclure, tapez deux fois pour exclure",
   "backup_album_selection_page_assets_scatter": "Les éléments peuvent être répartis sur plusieurs albums. De ce fait, les albums peuvent être inclus ou exclus pendant le processus de sauvegarde.",
@@ -86,14 +96,14 @@
   "backup_controller_page_background_battery_info_title": "Optimisation de la batterie",
   "backup_controller_page_background_charging": "Seulement pendant la charge",
   "backup_controller_page_background_configure_error": "Échec de la configuration du service d'arrière-plan",
-  "backup_controller_page_background_delay": "Retarder la sauvegarde des nouveaux éléments d'actif : {}",
+  "backup_controller_page_background_delay": "Retarder la sauvegarde des nouveaux éléments : {}",
   "backup_controller_page_background_description": "Activez le service d'arrière-plan pour sauvegarder automatiquement tous les nouveaux éléments sans avoir à ouvrir l'application.",
   "backup_controller_page_background_is_off": "La sauvegarde automatique en arrière-plan est désactivée",
   "backup_controller_page_background_is_on": "La sauvegarde automatique en arrière-plan est activée",
   "backup_controller_page_background_turn_off": "Désactiver le service d'arrière-plan",
   "backup_controller_page_background_turn_on": "Activer le service d'arrière-plan",
   "backup_controller_page_background_wifi": "Uniquement en WiFi",
-  "backup_controller_page_backup": "Sauvegardé",
+  "backup_controller_page_backup": "Sauvegarde",
   "backup_controller_page_backup_selected": "Sélectionné : ",
   "backup_controller_page_backup_sub": "Photos et vidéos sauvegardées",
   "backup_controller_page_cancel": "Annuler",
@@ -118,7 +128,7 @@
   "backup_controller_page_total_sub": "Toutes les photos et vidéos uniques des albums sélectionnés",
   "backup_controller_page_turn_off": "Désactiver la sauvegarde",
   "backup_controller_page_turn_on": "Activer la sauvegarde",
-  "backup_controller_page_uploading_file_info": "Transfert des informations du fichier",
+  "backup_controller_page_uploading_file_info": "Transfert du fichier",
   "backup_err_only_album": "Impossible de retirer le seul album",
   "backup_info_card_assets": "éléments",
   "backup_manual_cancelled": "Annulé",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Succès ",
   "backup_manual_title": "Statut du téléchargement ",
   "backup_options_page_title": "Options de sauvegarde",
+  "backup_setting_subtitle": "Ajuster les paramètres de sauvegarde",
   "cache_settings_album_thumbnails": "Miniatures de la page bibliothèque ({} éléments)",
   "cache_settings_clear_cache_button": "Effacer le cache",
   "cache_settings_clear_cache_button_title": "Efface le cache de l'application. Cela aura un impact significatif sur les performances de l'application jusqu'à ce que le cache soit reconstruit.",
@@ -145,14 +156,19 @@
   "cache_settings_tile_subtitle": "Contrôler le comportement du stockage local",
   "cache_settings_tile_title": "Stockage local",
   "cache_settings_title": "Paramètres de mise en cache",
+  "cancel": "Annuler",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Confirmez le mot de passe",
   "change_password_form_description": "Bonjour {name},\n\nC'est la première fois que vous vous connectez au système ou vous avez demandé à changer votre mot de passe. Veuillez saisir le nouveau mot de passe ci-dessous.",
   "change_password_form_new_password": "Nouveau mot de passe",
   "change_password_form_password_mismatch": "Les mots de passe ne correspondent pas",
   "change_password_form_reenter_new_password": "Saisissez à nouveau le nouveau mot de passe",
+  "check_corrupt_asset_backup": "Vérifier la corruption des éléments enregistrés",
+  "check_corrupt_asset_backup_button": "Vérifier",
+  "check_corrupt_asset_backup_description": "Lancer cette vérification uniquement lorsque connecté à un réseau Wi-Fi et que tout le contenu a été enregistré. Cette procédure peut durer plusieurs minutes.",
   "client_cert_dialog_msg_confirm": "Ok",
   "client_cert_enter_password": "Entrer mot de passe",
-  "client_cert_import": "Imorted",
+  "client_cert_import": "Importer",
   "client_cert_import_success_msg": "Certificat importé",
   "client_cert_invalid_msg": "Fichier de certificat invalide ou mot de passe incorrect",
   "client_cert_remove": "Supprimer",
@@ -168,7 +184,7 @@
   "control_bottom_app_bar_add_to_album": "Ajouter à l'album",
   "control_bottom_app_bar_album_info": "{} éléments",
   "control_bottom_app_bar_album_info_shared": "{} éléments - Partagés",
-  "control_bottom_app_bar_archive": "Archive",
+  "control_bottom_app_bar_archive": "Archiver",
   "control_bottom_app_bar_create_new_album": "Créer un nouvel album",
   "control_bottom_app_bar_delete": "Supprimer",
   "control_bottom_app_bar_delete_from_immich": "Supprimer de Immich",
@@ -185,14 +201,17 @@
   "control_bottom_app_bar_unarchive": "Désarchiver",
   "control_bottom_app_bar_unfavorite": "Enlever des favoris",
   "control_bottom_app_bar_upload": "Téléverser",
+  "create_album": "Créer l'album",
   "create_album_page_untitled": "Sans titre",
+  "create_new": "NOUVEAU",
   "create_shared_album_page_create": "Créer",
   "create_shared_album_page_share": "Partager",
   "create_shared_album_page_share_add_assets": "AJOUTER DES ÉLÉMENTS",
   "create_shared_album_page_share_select_photos": "Sélectionner les photos",
-  "crop": "Crop",
+  "crop": "Recadrer",
   "curated_location_page_title": "Lieux",
   "curated_object_page_title": "Objets",
+  "current_server_address": "Adresse actuelle du serveur ",
   "daily_title_text_date": "E, dd MMM",
   "daily_title_text_date_year": "E, dd MMM, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,28 +229,47 @@
   "delete_shared_link_dialog_title": "Supprimer le lien partagé",
   "description_input_hint_text": "Ajouter une description…",
   "description_input_submit_error": "Erreur de mise à jour de la description, vérifier le journal pour plus de détails",
-  "download_error": "Download Error",
-  "download_started": "Download started",
-  "download_sucess": "Download success",
-  "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_canceled": "Téléchargement annulé",
+  "download_complete": "Téléchargement terminé",
+  "download_enqueue": "Téléchargement en attente",
+  "download_error": "Erreur de téléchargement",
+  "download_failed": "Téléchargement échoué",
+  "download_filename": "fichier : {}",
+  "download_finished": "Téléchargement terminé",
+  "downloading": "Téléchargement...",
+  "downloading_media": "Téléchargement du média",
+  "download_notfound": "Téléchargement non trouvé",
+  "download_paused": "Téléchargement en pause",
+  "download_started": "Téléchargement commencé",
+  "download_sucess": "Téléchargement réussi",
+  "download_sucess_android": "Le média a été téléchargé dans DCIM/Immich",
+  "download_waiting_to_retry": "Téléchargement en attente du prochain essai",
   "edit_date_time_dialog_date_time": "Date et heure",
   "edit_date_time_dialog_timezone": "Fuseau horaire",
-  "edit_image_title": "Edit",
+  "edit_image_title": "Modifier",
   "edit_location_dialog_title": "Localisation",
-  "error_saving_image": "Error: {}",
+  "enter_wifi_name": "Entrez le nom du réseau ",
+  "error_change_sort_album": "Failed to change album sort order",
+  "error_saving_image": "Erreur : {}",
   "exif_bottom_sheet_description": "Ajouter une description…",
   "exif_bottom_sheet_details": "DÉTAILS",
   "exif_bottom_sheet_location": "LOCALISATION",
-  "exif_bottom_sheet_location_add": "Add a location",
+  "exif_bottom_sheet_location_add": "Ajouter un lieu",
   "exif_bottom_sheet_people": "PERSONNES",
   "exif_bottom_sheet_person_add_person": "Ajouter un nom",
   "experimental_settings_new_asset_list_subtitle": "En cours de développement",
   "experimental_settings_new_asset_list_title": "Activer la grille de photos expérimentale",
   "experimental_settings_subtitle": "Utilisez à vos dépends !",
   "experimental_settings_title": "Expérimental",
+  "external_network": "Réseau externe",
+  "external_network_sheet_info": "Quand vous n'êtes pas connecté à votre réseau préféré, l'application va tenter de se connecter aux adresses ci-dessous, en commencant par le haut",
+  "favorites": "Favoris",
   "favorites_page_no_favorites": "Aucun élément favori n'a été trouvé",
   "favorites_page_title": "Favoris",
   "filename_search": "Nom de fichier ou extension",
+  "filter": "Filtres",
+  "get_wifiname_error": "Impossible d'obtenir le nom du réseau Wi-Fi. Assurez-vous d'avoir donné les permissions nécessaires à l'application et que vous êtes connecté à un réseau Wi-Fi.",
+  "grant_permission": "Accorder les permissions ",
   "haptic_feedback_switch": "Activer le retour haptique",
   "haptic_feedback_title": "Retour haptique",
   "header_settings_add_header_tip": "Ajouter un en-tête",
@@ -255,25 +293,32 @@
   "home_page_first_time_notice": "Si c'est la première fois que vous utilisez l'application, veillez à choisir un ou plusieurs albums de sauvegarde afin que la chronologie puisse alimenter les photos et les vidéos de cet ou ces albums.",
   "home_page_share_err_local": "Impossible de partager par lien les médias locaux, cette opération est donc ignorée.",
   "home_page_upload_err_limit": "Limite de téléchargement de 30 éléments en même temps, demande ignorée",
-  "image_saved_successfully": "Image saved",
+  "ignore_icloud_photos": "Ignorer les photos iCloud",
+  "ignore_icloud_photos_description": "Les photos stockées sur iCloud ne sont pas enregistrées sur Immich",
+  "image_saved_successfully": "Image enregistré",
   "image_viewer_page_state_provider_download_error": "Erreur de téléchargement",
   "image_viewer_page_state_provider_download_started": "Téléchargement démarré",
   "image_viewer_page_state_provider_download_success": "Téléchargement réussi",
   "image_viewer_page_state_provider_share_error": "Erreur de partage",
   "invalid_date": "Date invalide",
   "invalid_date_format": "Format de date invalide",
+  "library": "Bibliothèque",
   "library_page_albums": "Albums",
   "library_page_archive": "Archive",
   "library_page_device_albums": "Albums sur l'appareil",
   "library_page_favorites": "Favoris",
   "library_page_new_album": "Nouvel album",
   "library_page_sharing": "Partage",
-  "library_page_sort_asset_count": "Number of assets",
+  "library_page_sort_asset_count": "Nombre d'éléments",
   "library_page_sort_created": "Créations les plus récentes",
   "library_page_sort_last_modified": "Dernière modification",
   "library_page_sort_most_oldest_photo": "Photo la plus ancienne",
   "library_page_sort_most_recent_photo": "Photo la plus récente",
   "library_page_sort_title": "Titre de l'album",
+  "local_network": "Réseau local",
+  "local_network_sheet_info": "L'application va connecter au serveur via cette URL quand l'appareil est connecté au réseau Wi-Fi spécifié",
+  "location_permission": "Autorisation de localisation ",
+  "location_permission_content": "Afin de pouvoir changer d'adresse automatiquement, Immich doit avoir accès à la localisation précise, afin d'accéder au le nom du réseau Wi-Fi utilisé",
   "location_picker_choose_on_map": "Sélectionner sur la carte",
   "location_picker_latitude": "Latitude",
   "location_picker_latitude_error": "Saisir une latitude correcte",
@@ -342,7 +387,10 @@
   "motion_photos_page_title": "Photos avec mouvement",
   "multiselect_grid_edit_date_time_err_read_only": "Impossible de modifier la date d'un élément d'actif en lecture seule.",
   "multiselect_grid_edit_gps_err_read_only": "Impossible de modifier l'emplacement d'un élément en lecture seule.",
-  "no_assets_to_show": "Aucuns éléments à afficher",
+  "my_albums": "Mes albums",
+  "networking_settings": "Réseau ",
+  "networking_subtitle": "Gérer les adresses du serveur",
+  "no_assets_to_show": "Aucun élément à afficher",
   "no_name": "Sans nom",
   "notification_permission_dialog_cancel": "Annuler",
   "notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sélectionnez Autoriser.",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Accordez la permission d'activer les notifications.",
   "notification_permission_list_tile_enable_button": "Activer les notifications",
   "notification_permission_list_tile_title": "Permission de notification",
+  "on_this_device": "Sur cet appareil",
   "partner_list_user_photos": "Photos de {user}",
   "partner_list_view_all": "Voir tous",
   "partner_page_add_partner": "Ajouter un partenaire",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} ne pourra plus accéder à vos photos.",
   "partner_page_stop_sharing_title": "Arrêter de partager vos photos ?",
   "partner_page_title": "Partenaire",
+  "partners": "Partenaires",
+  "people": "Personnes",
   "permission_onboarding_back": "Retour",
   "permission_onboarding_continue_anyway": "Continuer quand même",
   "permission_onboarding_get_started": "Commencer",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Permission accordée ! Vous êtes prêts.",
   "permission_onboarding_permission_limited": "Permission limitée. Pour permettre à Immich de sauvegarder et de gérer l'ensemble de votre bibliothèque, accordez l'autorisation pour les photos et vidéos dans les Paramètres.",
   "permission_onboarding_request": "Immich demande l'autorisation de visionner vos photos et vidéo",
+  "places": "Lieux",
+  "preferences_settings_subtitle": "Gérer les préférences de l'application",
   "preferences_settings_title": "Préférences",
   "profile_drawer_app_logs": "Journaux",
   "profile_drawer_client_out_of_date_major": "L'application mobile est obsolète. Veuillez effectuer la mise à jour vers la dernière version majeure.",
@@ -383,19 +436,22 @@
   "profile_drawer_settings": "Paramètres",
   "profile_drawer_sign_out": "Se déconnecter",
   "profile_drawer_trash": "Corbeille",
+  "recently_added": "Récemment ajouté",
   "recently_added_page_title": "Récemment ajouté",
-  "save_to_gallery": "Save to gallery",
+  "save": "Enregistrer ",
+  "save_to_gallery": "Enregistrer",
   "scaffold_body_error_occurred": "Une erreur s'est produite",
+  "search_albums": "Rechercher des albums",
   "search_bar_hint": "Rechercher vos photos",
   "search_filter_apply": "Appliquer le filtre",
   "search_filter_camera": "Appareil",
   "search_filter_camera_make": "Fabricant",
-  "search_filter_camera_model": "Modéle",
+  "search_filter_camera_model": "Modèle",
   "search_filter_camera_title": "Sélectionner le type d'appareil",
   "search_filter_date": "Date",
   "search_filter_date_interval": "{start} à {end}",
   "search_filter_date_title": "Sélectionner une période",
-  "search_filter_display_option_archive": "Archive",
+  "search_filter_display_option_archive": "Archivé",
   "search_filter_display_option_favorite": "Favoris",
   "search_filter_display_option_not_in_album": "Pas dans un album",
   "search_filter_display_options": "Options d'affichage",
@@ -428,6 +484,7 @@
   "search_page_places": "Lieux",
   "search_page_recently_added": "Récemment ajouté",
   "search_page_screenshots": "Captures d'écran",
+  "search_page_search_photos_videos": "Rechercher dans vos photos et vidéos",
   "search_page_selfies": "Selfies",
   "search_page_things": "Objets",
   "search_page_videos": "Vidéos",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Suggestions",
   "select_user_for_sharing_page_err_album": "Échec de la création de l'album",
   "select_user_for_sharing_page_share_suggestions": "Suggestions",
+  "server_endpoint": "Adresse du serveur",
   "server_info_box_app_version": "Version de l'application",
   "server_info_box_latest_release": "Dernière version",
   "server_info_box_server_url": "URL du serveur",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Charger l'image d'aperçu",
   "setting_image_viewer_title": "Images",
   "setting_languages_apply": "Appliquer",
+  "setting_languages_subtitle": "Changer la langue de l'application",
   "setting_languages_title": "Langues",
   "setting_notifications_notify_failures_grace_period": "Notifier les échecs de la sauvegarde en arrière-plan : {}",
   "setting_notifications_notify_hours": "{} heures",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Chargement",
   "shared_link_manage_links": "Gérer les liens partagés",
   "shared_link_public_album": "Album public",
+  "shared_links": "Liens partagés",
   "share_done": "Fait",
+  "shared_with_me": "Partagé avec moi",
   "share_invite": "Inviter à l'album",
   "sharing_page_album": "Albums partagés",
   "sharing_page_description": "Créez des albums partagés pour partager des photos et des vidéos avec les personnes de votre réseau.",
@@ -539,10 +600,10 @@
   "sharing_silver_appbar_create_shared_album": "Créer un album partagé",
   "sharing_silver_appbar_shared_links": "Liens partagés",
   "sharing_silver_appbar_share_partner": "Partager avec un partenaire",
-  "sync": "Sync",
-  "sync_albums": "Sync albums",
-  "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
-  "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
+  "sync": "Synchroniser",
+  "sync_albums": "Synchroniser dans des albums",
+  "sync_albums_manual_subtitle": "Synchroniser toutes les vidéos et photos sauvegardées dans les albums sélectionnés",
+  "sync_upload_album_setting_subtitle": "Créer et sauvegarde vos photos et vidéos dans les albums sélectionnés sur Immich",
   "tab_controller_nav_library": "Bibliothèque",
   "tab_controller_nav_photos": "Photos",
   "tab_controller_nav_search": "Recherche",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Le chargement en trois étapes peut améliorer les performances de chargement, mais entraîne une augmentation significative de la charge du réseau.",
   "theme_setting_three_stage_loading_title": "Activer le chargement en trois étapes",
   "translated_text_options": "Options",
+  "trash": "Corbeille",
   "trash_emptied": "Corbeille vidée",
   "trash_page_delete": "Supprimer",
   "trash_page_delete_all": "Tout supprimer",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Voulez-vous sauvegarder la sélection vers le serveur ?",
   "upload_dialog_ok": "Télécharger ",
   "upload_dialog_title": "Télécharger cet élément ",
+  "use_current_connection": "Utiliser le réseau actuel ",
+  "validate_endpoint_error": "Merci d'entrer un lien valide",
   "version_announcement_overlay_ack": "Confirmer",
   "version_announcement_overlay_release_notes": "notes de mise à jour",
   "version_announcement_overlay_text_1": "Bonjour, une nouvelle version de",
   "version_announcement_overlay_text_2": "veuillez prendre le temps de visiter le ",
   "version_announcement_overlay_text_3": " et assurez-vous que votre configuration docker-compose et .env est à jour pour éviter toute erreur de configuration, en particulier si vous utilisez WatchTower ou tout autre mécanisme qui gère la mise à jour automatique de votre application serveur.",
   "version_announcement_overlay_title": "Nouvelle version serveur disponible \uD83C\uDF89",
+  "videos": "Vidéos",
   "viewer_remove_from_stack": "Retirer de la pile",
   "viewer_stack_use_as_main_asset": "Utiliser comme élément principal",
-  "viewer_unstack": "Désempiler"
+  "viewer_unstack": "Désempiler",
+  "wifi_name": "Nom du réseau ",
+  "your_wifi_name": "Nom du réseau Wi-Fi "
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/he-IL.json b/mobile/assets/i18n/he-IL.json
index 7ddbb392a0..a77e5eb796 100644
--- a/mobile/assets/i18n/he-IL.json
+++ b/mobile/assets/i18n/he-IL.json
@@ -6,6 +6,8 @@
   "action_common_save": "שמור",
   "action_common_select": "בחר",
   "action_common_update": "עדכון",
+  "add_a_name": "הוסף שם",
+  "add_endpoint": "הוסף נקודת קצה",
   "add_to_album_bottom_sheet_added": "נוסף ל {album}",
   "add_to_album_bottom_sheet_already_exists": "כבר ב {album}",
   "advanced_settings_log_level_title": "רמת תיעוד אירועים: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "פתרון בעיות",
   "album_info_card_backup_album_excluded": "הוחרגו",
   "album_info_card_backup_album_included": "נכללו",
+  "albums": "אלבומים",
   "album_thumbnail_card_item": "פריט 1",
   "album_thumbnail_card_items": "{} פריטים",
   "album_thumbnail_card_shared": " · משותף",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "הסרה מאלבום",
   "album_viewer_appbar_share_to": "שתף עם",
   "album_viewer_page_share_add_users": "הוסף משתמשים",
+  "all": "הכל",
   "all_people_page_title": "אנשים",
   "all_videos_page_title": "סרטונים",
   "app_bar_signout_dialog_content": "האם את/ה בטוח/ה שברצונך להתנתק?",
   "app_bar_signout_dialog_ok": "כן",
   "app_bar_signout_dialog_title": "התנתק",
+  "archived": "בארכיון",
   "archive_page_no_archived_assets": "לא נמצאו נכסים בארכיון",
   "archive_page_title": "ארכיון ({})",
   "asset_action_delete_err_read_only": "לא ניתן למחוק נכס(ים) לקריאה בלבד, מדלג",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} נכס(ים) שוחזרו בהצלחה",
   "assets_trashed": "{} נכס(ים) הועברו לאשפה",
   "assets_trashed_from_server": "{} נכס(ים) הועברו לאשפה משרת ה-Immich",
+  "asset_viewer_settings_subtitle": "ניהול הגדרות מציג הגלריה שלך",
   "asset_viewer_settings_title": "מציג הנכסים",
+  "automatic_endpoint_switching_subtitle": "התחבר מקומית דרך אינטרנט אלחוטי ייעודי כאשר זמין והשתמש בחיבורים חלופיים במקומות אחרים",
+  "automatic_endpoint_switching_title": "החלפת כתובת אוטומטית",
+  "background_location_permission": "הרשאת מיקום ברקע",
+  "background_location_permission_content": "כדי להחליף רשתות בעת ריצה ברקע, היישום צריך *תמיד* גישה למיקום מדויק על מנת לקרוא את השם של רשת האינטרנט האלחוטי",
   "backup_album_selection_page_albums_device": "אלבומים במכשיר ({})",
   "backup_album_selection_page_albums_tap": "הקש כדי לכלול, הקש פעמיים כדי להחריג",
   "backup_album_selection_page_assets_scatter": "נכסים יכולים להתפזר על פני אלבומים מרובים. לפיכך, ניתן לכלול או להחריג אלבומים במהלך תהליך הגיבוי",
@@ -127,6 +137,7 @@
   "backup_manual_success": "הצלחה",
   "backup_manual_title": "מצב העלאה",
   "backup_options_page_title": "אפשרויות גיבוי",
+  "backup_setting_subtitle": "ניהול הגדרות העלאת רקע וחזית",
   "cache_settings_album_thumbnails": "תמונות ממוזערות של דף ספרייה ({} נכסים)",
   "cache_settings_clear_cache_button": "ניקוי מטמון",
   "cache_settings_clear_cache_button_title": "מנקה את המטמון של היישום. זה ישפיע באופן משמעותי על הביצועים של היישום עד שהמטמון נבנה מחדש",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "שלוט בהתנהגות האחסון המקומי",
   "cache_settings_tile_title": "אחסון מקומי",
   "cache_settings_title": "הגדרות שמירת מטמון",
+  "cancel": "ביטול",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "אשר סיסמה",
   "change_password_form_description": "הי {name},\n\nזאת או הפעם הראשונה שאת/ה מתחבר/ת למערכת או שנעשתה בקשה לשינוי הסיסמה שלך. נא להזין את הסיסמה החדשה למטה.",
   "change_password_form_new_password": "סיסמה חדשה",
   "change_password_form_password_mismatch": "סיסמאות לא תואמות",
   "change_password_form_reenter_new_password": "הכנס שוב סיסמה חדשה",
+  "check_corrupt_asset_backup": "בדוק גיבויים פגומים של נכסים",
+  "check_corrupt_asset_backup_button": "בצע בדיקה",
+  "check_corrupt_asset_backup_description": "הרץ בדיקה זו רק על Wi-Fi ולאחר שכל הנכסים גובו. ההליך עשוי לקחת כמה דקות.",
   "client_cert_dialog_msg_confirm": "בסדר",
   "client_cert_enter_password": "הזן סיסמה",
   "client_cert_import": "ייבוא",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "הוצא מארכיון",
   "control_bottom_app_bar_unfavorite": "הסר ממועדפים",
   "control_bottom_app_bar_upload": "העלאה",
+  "create_album": "צור אלבום",
   "create_album_page_untitled": "ללא כותרת",
+  "create_new": "צור חדש",
   "create_shared_album_page_create": "יצירה",
   "create_shared_album_page_share": "שתף",
   "create_shared_album_page_share_add_assets": "הוסף נכסים",
@@ -193,6 +211,7 @@
   "crop": "חתוך",
   "curated_location_page_title": "מקומות",
   "curated_object_page_title": "דברים",
+  "current_server_address": "כתובת שרת נוכחית",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "מחק קישור משותף",
   "description_input_hint_text": "הוסף תיאור...",
   "description_input_submit_error": "שגיאה בעדכון תיאור, בדוק את היומן לפרטים נוספים",
-  "download_error": "Download Error",
-  "download_started": "Download started",
-  "download_sucess": "Download success",
-  "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_canceled": "הורדה בוטלה",
+  "download_complete": "הורדה הושלמה",
+  "download_enqueue": "הורדה נוספה לתור",
+  "download_error": "שגיאת הורדה",
+  "download_failed": "הורדה נכשלה",
+  "download_filename": "קובץ: {}",
+  "download_finished": "הורדה הסתיימה",
+  "downloading": "מוריד...",
+  "downloading_media": "מוריד מדיה",
+  "download_notfound": "הורדה לא נמצא",
+  "download_paused": "הורדה הופסקה",
+  "download_started": "הורדה החלה",
+  "download_sucess": "הצלחת הורדה",
+  "download_sucess_android": "המדיה הורדה אל DCIM/Immich",
+  "download_waiting_to_retry": "מחכה כדי לנסות שוב",
   "edit_date_time_dialog_date_time": "תאריך וזמן",
   "edit_date_time_dialog_timezone": "אזור זמן",
   "edit_image_title": "ערוך",
   "edit_location_dialog_title": "מיקום",
+  "enter_wifi_name": "הזן שם אינטרנט אלחוטי",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "שגיאה: {}",
   "exif_bottom_sheet_description": "הוסף תיאור...",
   "exif_bottom_sheet_details": "פרטים",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "אפשר רשת תמונות ניסיונית",
   "experimental_settings_subtitle": "השימוש הוא על אחריותך בלבד!",
   "experimental_settings_title": "נסיוני",
+  "external_network": "רשת חיצונית",
+  "external_network_sheet_info": "כאשר לא על רשת האינטרנט האלחוטי המועדפת, היישום יתחבר לשרת דרך הכתובת הראשונה שניתן להשיג מהכתובות שלהלן, החל מלמעלה למטה",
+  "favorites": "מועדפים",
   "favorites_page_no_favorites": "לא נמצאו נכסים מועדפים",
   "favorites_page_title": "מועדפים",
   "filename_search": "שם קובץ או סיומת",
+  "filter": "סנן",
+  "get_wifiname_error": "לא היה ניתן לקבל את שם האינטרנט האלחוטי שלך. יש לודא שהענקת את ההרשאות הדרושות ושאת/ה מחובר/ת לרשת אינטרנט אלחוטי",
+  "grant_permission": "להעניק הרשאה",
   "haptic_feedback_switch": "אפשר משוב ברטט",
   "haptic_feedback_title": "משוב ברטט",
   "header_settings_add_header_tip": "הוסף כותרת",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "אם זאת הפעם הראשונה שאת/ה משתמש/ת ביישום, נא להקפיד לבחור אלבומ(ים) לגיבוי כך שציר הזמן יוכל לאכלס תמונות וסרטונים באלבומ(ים)",
   "home_page_share_err_local": "לא ניתן לשתף נכסים מקומיים על ידי קישור, מדלג",
   "home_page_upload_err_limit": "ניתן להעלות רק מקסימום של 30 נכסים בכל פעם, מדלג",
+  "ignore_icloud_photos": "התעלם מתמונות iCloud",
+  "ignore_icloud_photos_description": "תמונות שמאוחסנות ב-iCloud לא יועלו לשרת ה-Immich",
   "image_saved_successfully": "תמונה נשמרה",
   "image_viewer_page_state_provider_download_error": "שגיאת הורדה",
   "image_viewer_page_state_provider_download_started": "ההורדה החלה",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "שיתוף שגיאה",
   "invalid_date": "תאריך לא תקין",
   "invalid_date_format": "פורמט תאריך לא תקין",
+  "library": "ספרייה",
   "library_page_albums": "אלבומים",
   "library_page_archive": "ארכיון",
   "library_page_device_albums": "אלבומים במכשיר",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "תמונה הכי ישנה",
   "library_page_sort_most_recent_photo": "תמונה אחרונה ביותר",
   "library_page_sort_title": "כותרת אלבום",
+  "local_network": "רשת מקומית",
+  "local_network_sheet_info": "היישום יתחבר לשרת דרך הכתובת הזאת כאשר משתמשים ברשת האינטרנט האלחוטי שמצוינת",
+  "location_permission": "הרשאת מיקום",
+  "location_permission_content": "כדי להשתמש בתכונת ההחלפה האוטומטית, היישום צריך הרשאה למיקום מדויק על מנת לקרוא את השם של רשת האינטרנט האלחוטי",
   "location_picker_choose_on_map": "בחר על מפה",
   "location_picker_latitude": "קו רוחב",
   "location_picker_latitude_error": "הזן קו רוחב חוקי",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "תמונות עם תנועה",
   "multiselect_grid_edit_date_time_err_read_only": "לא ניתן לערוך תאריך של נכס(ים) לקריאה בלבד, מדלג",
   "multiselect_grid_edit_gps_err_read_only": "לא ניתן לערוך מיקום של נכס(ים) לקריאה בלבד, מדלג",
+  "my_albums": "האלבומים שלי",
+  "networking_settings": "רשת",
+  "networking_subtitle": "ניהול הגדרות נקודת קצה שרת",
   "no_assets_to_show": "אין נכסים להציג",
   "no_name": "ללא שם",
   "notification_permission_dialog_cancel": "ביטול",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "הענק הרשאה כדי לאפשר התראות",
   "notification_permission_list_tile_enable_button": "אפשר התראות",
   "notification_permission_list_tile_title": "הרשאת התראה",
+  "on_this_device": "במכשיר הזה",
   "partner_list_user_photos": "תמונות של {user}",
   "partner_list_view_all": "הצג הכל",
   "partner_page_add_partner": "הוספת שותף",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} לא יוכל יותר לגשת לתמונות שלך",
   "partner_page_stop_sharing_title": "להפסיק לשתף את התמונות שלך?",
   "partner_page_title": "שותף",
+  "partners": "שותפים",
+  "people": "אנשים",
   "permission_onboarding_back": "חזרה",
   "permission_onboarding_continue_anyway": "המשך בכל זאת",
   "permission_onboarding_get_started": "להתחיל",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "ההרשאה ניתנה! את/ה מוכנ/ה",
   "permission_onboarding_permission_limited": "הרשאה מוגבלת. כדי לתת ליישום לגבות ולנהל את כל אוסף הגלריה שלך, הענק הרשאה לתמונות וסרטונים בהגדרות",
   "permission_onboarding_request": "היישום דורש הרשאה כדי לראות את התמונות והסרטונים שלך",
+  "places": "מקומות",
+  "preferences_settings_subtitle": "ניהול העדפות יישום",
   "preferences_settings_title": "העדפות",
   "profile_drawer_app_logs": "יומן",
   "profile_drawer_client_out_of_date_major": "האפליקציה לנייד היא מיושנת. נא לעדכן לגרסה הראשית האחרונה",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "הגדרות",
   "profile_drawer_sign_out": "יציאה",
   "profile_drawer_trash": "אשפה",
+  "recently_added": "נוסף לאחרונה",
   "recently_added_page_title": "נוסף לאחרונה",
+  "save": "שמירה",
   "save_to_gallery": "שמור לגלריה",
   "scaffold_body_error_occurred": "אירעה שגיאה",
+  "search_albums": "חפש/י אלבומים",
   "search_bar_hint": "חפש/י בתמונות שלך",
   "search_filter_apply": "החל סינון",
   "search_filter_camera": "מצלמה",
@@ -428,6 +484,7 @@
   "search_page_places": "מקומות",
   "search_page_recently_added": "נוסף לאחרונה",
   "search_page_screenshots": "צילומי מסך",
+  "search_page_search_photos_videos": "חפש את התמונות והסרטונים שלך",
   "search_page_selfies": "צילומי סלפי",
   "search_page_things": "דברים",
   "search_page_videos": "סרטונים",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "הצעות",
   "select_user_for_sharing_page_err_album": "יצירת אלבום נכשלה",
   "select_user_for_sharing_page_share_suggestions": "הצעות",
+  "server_endpoint": "נקודת קצה שרת",
   "server_info_box_app_version": "גרסת יישום",
   "server_info_box_latest_release": "גרסה עדכנית ביותר",
   "server_info_box_server_url": "כתובת שרת",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "טען תמונת תצוגה מקדימה",
   "setting_image_viewer_title": "תמונות",
   "setting_languages_apply": "החל",
+  "setting_languages_subtitle": "שינוי שפת היישום",
   "setting_languages_title": "שפות",
   "setting_notifications_notify_failures_grace_period": "הודע על כשלים בגיבוי ברקע: {}",
   "setting_notifications_notify_hours": "{} שעות",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "העלאה",
   "shared_link_manage_links": "ניהול קישורים משותפים",
   "shared_link_public_album": "אלבום ציבורי",
+  "shared_links": "קישורים משותפים",
   "share_done": "סיום",
+  "shared_with_me": "משותף איתי",
   "share_invite": "הזמן לאלבום",
   "sharing_page_album": "אלבומים משותפים",
   "sharing_page_description": "צור אלבומים משותפים כדי לשתף תמונות וסרטונים עם אנשים ברשת שלך",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "טעינה בשלושה שלבים עשויה לשפר את ביצועי הטעינה אבל גורמת באופן משמעותי לעומס רשת גבוה יותר",
   "theme_setting_three_stage_loading_title": "אפשר טעינה בשלושה שלבים",
   "translated_text_options": "אפשרויות",
+  "trash": "אשפה",
   "trash_emptied": "האשפה רוקנה",
   "trash_page_delete": "מחק",
   "trash_page_delete_all": "מחק הכל",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "האם ברצונך לגבות את הנכס(ים) שנבחרו לשרת?",
   "upload_dialog_ok": "העלאה",
   "upload_dialog_title": "העלאת נכס",
+  "use_current_connection": "השתמש בחיבור נוכחי",
+  "validate_endpoint_error": "נא להזין כתובת תקנית",
   "version_announcement_overlay_ack": "אשר",
   "version_announcement_overlay_release_notes": "הערות פרסום",
   "version_announcement_overlay_text_1": "הי חבר/ה, יש מהדורה חדשה של",
   "version_announcement_overlay_text_2": "אנא קח/י את הזמן שלך לבקר ב ",
   "version_announcement_overlay_text_3": " ולוודא שמבנה ה docker-compose וה env. שלך עדכני כדי למנוע תצורות שגויות, במיוחד אם את/ה משתמש/ת ב WatchTower או בכל מנגנון שמטפל בעדכון יישום השרת שלך באופן אוטומטי",
   "version_announcement_overlay_title": "גרסת שרת חדשה זמינה \uD83C\uDF89",
+  "videos": "סרטונים",
   "viewer_remove_from_stack": "הסר מערימה",
   "viewer_stack_use_as_main_asset": "השתמש כנכס ראשי",
-  "viewer_unstack": "ביטול ערימה"
+  "viewer_unstack": "ביטול ערימה",
+  "wifi_name": "שם אינטרנט אלחוטי",
+  "your_wifi_name": "שם אינטרנט אלחוטי שלך"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/hi-IN.json b/mobile/assets/i18n/hi-IN.json
index 534cae0622..af4b6b37d9 100644
--- a/mobile/assets/i18n/hi-IN.json
+++ b/mobile/assets/i18n/hi-IN.json
@@ -3,9 +3,11 @@
   "action_common_cancel": "Cancel",
   "action_common_clear": "Clear",
   "action_common_confirm": "Confirm",
-  "action_common_save": "Save",
-  "action_common_select": "Select",
+  "action_common_save": "सहेजें",
+  "action_common_select": "चुनें",
   "action_common_update": "Update",
+  "add_a_name": "नाम जोड़ें",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Added to {album}",
   "add_to_album_bottom_sheet_already_exists": "Already in {album}",
   "advanced_settings_log_level_title": "Log level: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Troubleshooting",
   "album_info_card_backup_album_excluded": "EXCLUDED",
   "album_info_card_backup_album_included": "INCLUDED",
+  "albums": "एल्बम",
   "album_thumbnail_card_item": "1 item",
   "album_thumbnail_card_items": "{} items",
   "album_thumbnail_card_shared": " · Shared",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Remove from album",
   "album_viewer_appbar_share_to": "साझा करें",
   "album_viewer_page_share_add_users": "Add users",
+  "all": "सभी",
   "all_people_page_title": "People",
   "all_videos_page_title": "Videos",
   "app_bar_signout_dialog_content": "क्या आप सुनिश्चित हैं कि आप लॉग आउट करना चाहते हैं?",
   "app_bar_signout_dialog_ok": "हाँ",
   "app_bar_signout_dialog_title": "लॉग आउट",
+  "archived": "संग्रहित",
   "archive_page_no_archived_assets": "No archived assets found",
   "archive_page_title": "Archive ({})",
   "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
@@ -54,14 +59,19 @@
   "asset_list_layout_sub_title": "Layout",
   "asset_list_settings_subtitle": "Photo grid layout settings",
   "asset_list_settings_title": "Photo Grid",
-  "asset_restored_successfully": "Asset restored successfully",
-  "assets_deleted_permanently": "{} asset(s) deleted permanently",
-  "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server",
-  "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device",
-  "assets_restored_successfully": "{} asset(s) restored successfully",
-  "assets_trashed": "{} asset(s) trashed",
-  "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_restored_successfully": "संपत्ति(याँ) सफलतापूर्वक पुनर्स्थापित की गईं",
+  "assets_deleted_permanently": "{} संपत्ति(याँ) स्थायी रूप से हटा दी गईं",
+  "assets_deleted_permanently_from_server": "{} संपत्ति(याँ) इमिच सर्वर से स्थायी रूप से हटा दी गईं",
+  "assets_removed_permanently_from_device": "{} संपत्ति(याँ) आपके डिवाइस से स्थायी रूप से हटा दी गईं",
+  "assets_restored_successfully": "{} संपत्ति(याँ) सफलतापूर्वक पुनर्स्थापित की गईं",
+  "assets_trashed": "{} संपत्ति(याँ) कचरे में डाली गईं",
+  "assets_trashed_from_server": "{} संपत्ति(याँ) इमिच सर्वर से कचरे में डाली गईं",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "Asset Viewer",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Albums on device ({})",
   "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
   "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Success",
   "backup_manual_title": "Upload status",
   "backup_options_page_title": "Backup options",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
   "cache_settings_clear_cache_button": "Clear cache",
   "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "स्थानीय संग्रहण के व्यवहार को नियंत्रित करें",
   "cache_settings_tile_title": "स्थानीय संग्रहण",
   "cache_settings_title": "Caching Settings",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Confirm Password",
   "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
   "change_password_form_new_password": "New Password",
   "change_password_form_password_mismatch": "Passwords do not match",
   "change_password_form_reenter_new_password": "Re-enter New Password",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Enter Password",
   "client_cert_import": "Import",
@@ -164,7 +180,7 @@
   "common_create_new_album": "Create new album",
   "common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.",
   "common_shared": "Shared",
-  "contextual_search": "Sunrise on the beach",
+  "contextual_search": "समुद्र तट पर सूर्योदय",
   "control_bottom_app_bar_add_to_album": "Add to album",
   "control_bottom_app_bar_album_info": "{} items",
   "control_bottom_app_bar_album_info_shared": "{} items · Shared",
@@ -173,8 +189,8 @@
   "control_bottom_app_bar_delete": "Delete",
   "control_bottom_app_bar_delete_from_immich": "Delete from Immich",
   "control_bottom_app_bar_delete_from_local": "Delete from device",
-  "control_bottom_app_bar_download": "Download",
-  "control_bottom_app_bar_edit": "Edit",
+  "control_bottom_app_bar_download": "डाउनलोड",
+  "control_bottom_app_bar_edit": "संपादित करें",
   "control_bottom_app_bar_edit_location": "Edit Location",
   "control_bottom_app_bar_edit_time": "Edit Date & Time",
   "control_bottom_app_bar_favorite": "Favorite",
@@ -185,14 +201,17 @@
   "control_bottom_app_bar_unarchive": "Unarchive",
   "control_bottom_app_bar_unfavorite": "Unfavorite",
   "control_bottom_app_bar_upload": "Upload",
+  "create_album": "एल्बम बनाएँ",
   "create_album_page_untitled": "Untitled",
+  "create_new": "नया बनाएं",
   "create_shared_album_page_create": "Create",
   "create_shared_album_page_share": "Share",
   "create_shared_album_page_share_add_assets": "ADD ASSETS",
   "create_shared_album_page_share_select_photos": "Select Photos",
-  "crop": "Crop",
+  "crop": "छाँटें",
   "curated_location_page_title": "Places",
   "curated_object_page_title": "Things",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,15 +229,28 @@
   "delete_shared_link_dialog_title": "साझा किए गए लिंक को हटाएं",
   "description_input_hint_text": "Add description...",
   "description_input_submit_error": "Error updating description, check the log for more details",
-  "download_error": "Download Error",
-  "download_started": "Download started",
-  "download_sucess": "Download success",
-  "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_canceled": "डाउनलोड रद्द कर दिया गया",
+  "download_complete": "डाउनलोड पूरा",
+  "download_enqueue": "डाउनलोड कतार में है",
+  "download_error": "डाउनलोड त्रुटि",
+  "download_failed": "डाउनलोड विफल",
+  "download_filename": "फ़ाइल: {}",
+  "download_finished": "डाउनलोड समाप्त",
+  "downloading": "डाउनलोड हो रहा है...",
+  "downloading_media": "मीडिया डाउनलोड हो रहा है",
+  "download_notfound": "डाउनलोड नहीं मिला",
+  "download_paused": "डाउनलोड स्थगित",
+  "download_started": "डाउनलोड प्रारंभ हुआ",
+  "download_sucess": "डाउनलोड सफल",
+  "download_sucess_android": "मीडिया DCIM/Immich में डाउनलोड हो गया है",
+  "download_waiting_to_retry": "पुनः प्रयास करने का इंतजार कर रहा है",
   "edit_date_time_dialog_date_time": "Date and Time",
   "edit_date_time_dialog_timezone": "Timezone",
-  "edit_image_title": "Edit",
+  "edit_image_title": "संपादित करें",
   "edit_location_dialog_title": "Location",
-  "error_saving_image": "Error: {}",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
+  "error_saving_image": "त्रुटि: {}",
   "exif_bottom_sheet_description": "Add Description...",
   "exif_bottom_sheet_details": "DETAILS",
   "exif_bottom_sheet_location": "LOCATION",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Enable experimental photo grid",
   "experimental_settings_subtitle": "Use at your own risk!",
   "experimental_settings_title": "Experimental",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "पसंदीदा",
   "favorites_page_no_favorites": "No favorite assets found",
   "favorites_page_title": "Favorites",
-  "filename_search": "File name or extension",
+  "filename_search": "फ़ाइल नाम या एक्सटेंशन",
+  "filter": "फ़िल्टर",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Enable haptic feedback",
   "haptic_feedback_title": "Haptic Feedback",
   "header_settings_add_header_tip": "Add Header",
@@ -255,13 +293,16 @@
   "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
   "home_page_share_err_local": "लोकल एसेट्स को लिंक के जरिए शेयर नहीं कर सकते, स्किप कर रहे हैं",
   "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
-  "image_saved_successfully": "Image saved",
+  "ignore_icloud_photos": "आइक्लाउड फ़ोटो को अनदेखा करें",
+  "ignore_icloud_photos_description": "आइक्लाउड पर स्टोर की गई फ़ोटोज़ इमिच सर्वर पर अपलोड नहीं की जाएंगी",
+  "image_saved_successfully": "इमेज सहेज दी गई",
   "image_viewer_page_state_provider_download_error": "Download Error",
   "image_viewer_page_state_provider_download_started": "Download Started",
   "image_viewer_page_state_provider_download_success": "Download Success",
   "image_viewer_page_state_provider_share_error": "Share Error",
-  "invalid_date": "Invalid date",
-  "invalid_date_format": "Invalid date format",
+  "invalid_date": "अमान्य तारीख़",
+  "invalid_date_format": "अमान्य तारीख़ प्रारूप",
+  "library": "गैलरी",
   "library_page_albums": "Albums",
   "library_page_archive": "Archive",
   "library_page_device_albums": "Albums on Device",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Oldest photo",
   "library_page_sort_most_recent_photo": "Most recent photo",
   "library_page_sort_title": "Album title",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Choose on map",
   "location_picker_latitude": "Latitude",
   "location_picker_latitude_error": "Enter a valid latitude",
@@ -342,14 +387,18 @@
   "motion_photos_page_title": "Motion Photos",
   "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
   "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
+  "my_albums": "मेरे एल्बम",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "No assets to show",
-  "no_name": "No name",
+  "no_name": "कोई नाम नहीं",
   "notification_permission_dialog_cancel": "Cancel",
   "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
   "notification_permission_dialog_settings": "Settings",
   "notification_permission_list_tile_content": "Grant permission to enable notifications.",
   "notification_permission_list_tile_enable_button": "Enable Notifications",
   "notification_permission_list_tile_title": "Notification Permission",
+  "on_this_device": "इस डिवाइस पर",
   "partner_list_user_photos": "{user}'s photos",
   "partner_list_view_all": "View all",
   "partner_page_add_partner": "Add partner",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
   "partner_page_stop_sharing_title": "Stop sharing your photos?",
   "partner_page_title": "Partner",
+  "partners": "साझेदार",
+  "people": "लोग",
   "permission_onboarding_back": "वापस",
   "permission_onboarding_continue_anyway": "Continue anyway",
   "permission_onboarding_get_started": "Get started",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Permission granted! You are all set.",
   "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
   "permission_onboarding_request": "Immich requires permission to view your photos and videos.",
+  "places": "स्थान",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Preferences",
   "profile_drawer_app_logs": "Logs",
   "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
@@ -383,35 +436,38 @@
   "profile_drawer_settings": "Settings",
   "profile_drawer_sign_out": "Sign Out",
   "profile_drawer_trash": "Trash",
+  "recently_added": "हाल ही में जोड़ा गया",
   "recently_added_page_title": "Recently Added",
-  "save_to_gallery": "Save to gallery",
+  "save": "Save",
+  "save_to_gallery": "गैलरी में सहेजें",
   "scaffold_body_error_occurred": "Error occurred",
+  "search_albums": "एल्बम खोजें",
   "search_bar_hint": "Search your photos",
   "search_filter_apply": "Apply filter",
-  "search_filter_camera": "Camera",
+  "search_filter_camera": "कैमरा",
   "search_filter_camera_make": "Make",
   "search_filter_camera_model": "Model",
-  "search_filter_camera_title": "Select camera type",
-  "search_filter_date": "Date",
-  "search_filter_date_interval": "{start} to {end}",
-  "search_filter_date_title": "Select a date range",
+  "search_filter_camera_title": "कैमरा प्रकार चुनें",
+  "search_filter_date": "तारीख़",
+  "search_filter_date_interval": "{start} से {end} तक",
+  "search_filter_date_title": "तारीख़ की सीमा चुनें",
   "search_filter_display_option_archive": "Archive",
   "search_filter_display_option_favorite": "Favorite",
   "search_filter_display_option_not_in_album": "Not in album",
-  "search_filter_display_options": "Display Options",
-  "search_filter_display_options_title": "Display options",
-  "search_filter_location": "Location",
+  "search_filter_display_options": "प्रदर्शन विकल्प",
+  "search_filter_display_options_title": "प्रदर्शन विकल्प",
+  "search_filter_location": "स्थान",
   "search_filter_location_city": "City",
   "search_filter_location_country": "Country",
   "search_filter_location_state": "State",
-  "search_filter_location_title": "Select location",
-  "search_filter_media_type": "Media Type",
+  "search_filter_location_title": "स्थान चुनें",
+  "search_filter_media_type": "मीडिया प्रकार",
   "search_filter_media_type_all": "All",
   "search_filter_media_type_image": "Image",
-  "search_filter_media_type_title": "Select media type",
+  "search_filter_media_type_title": "मीडिया प्रकार चुनें",
   "search_filter_media_type_video": "Video",
-  "search_filter_people": "People",
-  "search_filter_people_title": "Select people",
+  "search_filter_people": "लोग",
+  "search_filter_people_title": "लोगों का चयन करें",
   "search_page_categories": "Categories",
   "search_page_favorites": "Favorites",
   "search_page_motion_photos": "Motion Photos",
@@ -428,6 +484,7 @@
   "search_page_places": "Places",
   "search_page_recently_added": "Recently added",
   "search_page_screenshots": "Screenshots",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Selfies",
   "search_page_things": "Things",
   "search_page_videos": "Videos",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Suggestions",
   "select_user_for_sharing_page_err_album": "Failed to create album",
   "select_user_for_sharing_page_share_suggestions": "Suggestions",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "App Version",
   "server_info_box_latest_release": "लेटेस्ट वर्ज़न",
   "server_info_box_server_url": "सर्वर URL",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Load preview image",
   "setting_image_viewer_title": "Images",
   "setting_languages_apply": "Apply",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Languages",
   "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
   "setting_notifications_notify_hours": "{} hours",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Upload",
   "shared_link_manage_links": "साझा किए गए लिंक का प्रबंधन करें",
   "shared_link_public_album": "Public album",
+  "shared_links": "साझा किए गए लिंक",
   "share_done": "Done",
+  "shared_with_me": "मेरे साथ साझा किया गया",
   "share_invite": "Invite to album",
   "sharing_page_album": "Shared albums",
   "sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
@@ -539,31 +600,32 @@
   "sharing_silver_appbar_create_shared_album": "New shared album",
   "sharing_silver_appbar_shared_links": "Shared links",
   "sharing_silver_appbar_share_partner": "Share with partner",
-  "sync": "Sync",
-  "sync_albums": "Sync albums",
-  "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
-  "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
+  "sync": "सिंक करें",
+  "sync_albums": "एल्बम्स सिंक करें",
+  "sync_albums_manual_subtitle": "चुने हुए बैकअप एल्बम्स में सभी अपलोड की गई वीडियो और फ़ोटो सिंक करें",
+  "sync_upload_album_setting_subtitle": "अपनी फ़ोटो और वीडियो बनाएँ और उन्हें इमिच पर चुने हुए एल्बम्स में अपलोड करें",
   "tab_controller_nav_library": "Library",
   "tab_controller_nav_photos": "Photos",
   "tab_controller_nav_search": "Search",
   "tab_controller_nav_sharing": "Sharing",
   "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles",
   "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})",
-  "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.",
-  "theme_setting_colorful_interface_title": "Colorful interface",
+  "theme_setting_colorful_interface_subtitle": "प्राथमिक रंग को पृष्ठभूमि सतहों पर लागू करें",
+  "theme_setting_colorful_interface_title": "रंगीन इंटरफ़ेस",
   "theme_setting_dark_mode_switch": "Dark mode",
   "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer",
   "theme_setting_image_viewer_quality_title": "Image viewer quality",
-  "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.",
-  "theme_setting_primary_color_title": "Primary color",
-  "theme_setting_system_primary_color_title": "Use system color",
+  "theme_setting_primary_color_subtitle": "प्राथमिक क्रियाओं और उच्चारणों के लिए एक रंग चुनें",
+  "theme_setting_primary_color_title": "प्राथमिक रंग",
+  "theme_setting_system_primary_color_title": "सिस्टम रंग का उपयोग करें",
   "theme_setting_system_theme_switch": "Automatic (Follow system setting)",
   "theme_setting_theme_subtitle": "Choose the app's theme setting",
   "theme_setting_theme_title": "Theme",
   "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
   "theme_setting_three_stage_loading_title": "Enable three-stage loading",
   "translated_text_options": "Options",
-  "trash_emptied": "Emptied trash",
+  "trash": "कचरा",
+  "trash_emptied": "कचरा खाली कर दिया",
   "trash_page_delete": "Delete",
   "trash_page_delete_all": "Delete All",
   "trash_page_empty_trash_btn": "कूड़ेदान खाली करें",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
   "upload_dialog_ok": "Upload",
   "upload_dialog_title": "Upload Asset",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Acknowledge",
   "version_announcement_overlay_release_notes": "release notes",
   "version_announcement_overlay_text_1": "Hi friend, there is a new release of",
   "version_announcement_overlay_text_2": "please take your time to visit the ",
   "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
   "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
+  "videos": "वीडियो",
   "viewer_remove_from_stack": "स्टैक से हटाएं",
   "viewer_stack_use_as_main_asset": "मुख्य संपत्ति के रूप में उपयोग करें",
-  "viewer_unstack": "स्टैक रद्द करें"
+  "viewer_unstack": "स्टैक रद्द करें",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/hu-HU.json b/mobile/assets/i18n/hu-HU.json
index 8f14b9673a..ee17a2d174 100644
--- a/mobile/assets/i18n/hu-HU.json
+++ b/mobile/assets/i18n/hu-HU.json
@@ -1,18 +1,20 @@
 {
   "action_common_back": "Vissza",
   "action_common_cancel": "Mégsem",
-  "action_common_clear": "Kitöröl",
+  "action_common_clear": "Alaphelyzetbe állít",
   "action_common_confirm": "Jóváhagy",
-  "action_common_save": "Save",
-  "action_common_select": "Select",
+  "action_common_save": "Mentés",
+  "action_common_select": "Kiválaszt",
   "action_common_update": "Frissít",
+  "add_a_name": "Név hozzáadása",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Hozzáadva a(z) \"{album}\" albumhoz",
   "add_to_album_bottom_sheet_already_exists": "Már benne van a(z) \"{album}\" albumban",
   "advanced_settings_log_level_title": "Naplózás szintje: {}",
-  "advanced_settings_prefer_remote_subtitle": "Néhány eszköz fájdalmasan lassan tölti be az eszközön lévő bélyegképeket. Ezzel a beállítással inkább a távoli képeket töltjük be helyette.",
+  "advanced_settings_prefer_remote_subtitle": "Néhány eszköz fájdalmasan lassan tölti be az eszközön lévő bélyegképeket. Ez a beállítás inkább a távoli képeket tölti be helyettük.",
   "advanced_settings_prefer_remote_title": "Távoli képek előnyben részesítése",
-  "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
-  "advanced_settings_proxy_headers_title": "Proxy Headers",
+  "advanced_settings_proxy_headers_subtitle": "Add meg azokat a proxy fejléceket, amiket az app elküldjön minden hálózati kérésnél",
+  "advanced_settings_proxy_headers_title": "Proxy Fejlécek",
   "advanced_settings_self_signed_ssl_subtitle": "Nem ellenőrzi a szerver SSL tanúsítványát. Önaláírt tanúsítvány esetén szükséges beállítás.",
   "advanced_settings_self_signed_ssl_title": "Önaláírt SSL tanúsítványok engedélyezése",
   "advanced_settings_tile_subtitle": "Haladó felhasználói beállítások",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Hibaelhárítás",
   "album_info_card_backup_album_excluded": "KIHAGYVA",
   "album_info_card_backup_album_included": "BELEÉRTVE",
+  "albums": "Albumok",
   "album_thumbnail_card_item": "1 elem",
   "album_thumbnail_card_items": "{} elem",
   "album_thumbnail_card_shared": "· Megosztott",
@@ -28,54 +31,61 @@
   "album_thumbnail_shared_by": "Megosztotta: {}",
   "album_viewer_appbar_delete_confirm": "Biztos, hogy törölni szeretnéd ezt az albumot?",
   "album_viewer_appbar_share_delete": "Album törlése",
-  "album_viewer_appbar_share_err_delete": "Nem sikerült törölni az albumot",
+  "album_viewer_appbar_share_err_delete": "Az album törlése sikertelen",
   "album_viewer_appbar_share_err_leave": "Nem sikerült kilépni az albumból",
   "album_viewer_appbar_share_err_remove": "Néhány elemet nem sikerült törölni az albumból",
-  "album_viewer_appbar_share_err_title": "Nem sikerült átnevezni az albumot",
+  "album_viewer_appbar_share_err_title": "Az album átnevezése sikertelen",
   "album_viewer_appbar_share_leave": "Kilépés az albumból",
   "album_viewer_appbar_share_remove": "Eltávolítás az albumból",
   "album_viewer_appbar_share_to": "Megosztás Ide",
   "album_viewer_page_share_add_users": "Felhasználók hozzáadása",
+  "all": "Összes",
   "all_people_page_title": "Emberek",
   "all_videos_page_title": "Videók",
   "app_bar_signout_dialog_content": "Biztos, hogy ki szeretnél jelentkezni?",
   "app_bar_signout_dialog_ok": "Igen",
   "app_bar_signout_dialog_title": "Kijelentkezés",
+  "archived": "Archivált",
   "archive_page_no_archived_assets": "Nem található archivált elem",
   "archive_page_title": "Archívum ({})",
   "asset_action_delete_err_read_only": "Csak-olvasható elem(ek)et nem lehet törölni, így ezeket átugorjuk",
-  "asset_action_share_err_offline": "Nem sikerült betölteni a kapcsolat nélküli elem(ek)et, így ezeket kihagyjuk",
+  "asset_action_share_err_offline": "Nem lehet betölteni a kapcsolat nélküli elem(ek)et, így ezeket kihagyjuk",
   "asset_list_group_by_sub_title": "Csoportosítás",
   "asset_list_layout_settings_dynamic_layout_title": "Dinamikus elrendezés",
-  "asset_list_layout_settings_group_automatically": "Automatikus",
+  "asset_list_layout_settings_group_automatically": "Automatikusan",
   "asset_list_layout_settings_group_by": "Elemek csoportosítása",
-  "asset_list_layout_settings_group_by_month": "hónapok szerint",
-  "asset_list_layout_settings_group_by_month_day": "hónap és nap szerint",
+  "asset_list_layout_settings_group_by_month": "Hónapok szerint",
+  "asset_list_layout_settings_group_by_month_day": "Hónapok és napok szerint",
   "asset_list_layout_sub_title": "Elrendezés",
   "asset_list_settings_subtitle": "Fotórács elrendezése",
   "asset_list_settings_title": "Fotórács",
-  "asset_restored_successfully": "Asset restored successfully",
-  "assets_deleted_permanently": "{} asset(s) deleted permanently",
-  "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server",
-  "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device",
-  "assets_restored_successfully": "{} asset(s) restored successfully",
-  "assets_trashed": "{} asset(s) trashed",
-  "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_restored_successfully": "Elem sikeresen helyreállítva",
+  "assets_deleted_permanently": "{} elem véglegesen törölve",
+  "assets_deleted_permanently_from_server": "{} elem véglegesen törölve az Immich szerverről",
+  "assets_removed_permanently_from_device": "{} elem véglegesen törölve az eszközödről",
+  "assets_restored_successfully": "{} elem sikeresen helyreállítva",
+  "assets_trashed": "{} elem lomtárba helyezve",
+  "assets_trashed_from_server": "{} elem lomtárba helyezve az Immich szerveren",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "Elem Megjelenítő",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Ezen az eszközön lévő albumok ({})",
-  "backup_album_selection_page_albums_tap": "Koppincs a hozzáadáshoz, duplán koppincs az eltávolításhoz",
+  "backup_album_selection_page_albums_tap": "Koppints a hozzáadáshoz, duplán koppints az eltávolításhoz",
   "backup_album_selection_page_assets_scatter": "Egy elem több albumban is lehet. Ezért a mentéshez albumokat lehet hozzáadni vagy azokat a mentésből kihagyni.",
   "backup_album_selection_page_select_albums": "Válassz albumokat",
   "backup_album_selection_page_selection_info": "Összegzés",
   "backup_album_selection_page_total_assets": "Összes egyedi elem",
   "backup_all": "Összes",
-  "backup_background_service_backup_failed_message": "Hiba a mentés közben. Újrapróbálkozás...",
-  "backup_background_service_connection_failed_message": "Hiba a szerverhez való csatlakozás közben. Újrapróbálkozás...",
+  "backup_background_service_backup_failed_message": "Az elemek mentése sikertelen. Újrapróbálkozás...",
+  "backup_background_service_connection_failed_message": "A szerverhez csatlakozás sikertelen. Újrapróbálkozás...",
   "backup_background_service_current_upload_notification": "Feltöltés {}",
-  "backup_background_service_default_notification": "Új elemek keresése...",
-  "backup_background_service_error_title": "Hiba mentés közben",
+  "backup_background_service_default_notification": "Új elemek ellenőrzése...",
+  "backup_background_service_error_title": "Hiba a mentés közben",
   "backup_background_service_in_progress_notification": "Elemek mentése folyamatban…",
-  "backup_background_service_upload_failure_notification": "Hiba a feltöltés közben {}",
+  "backup_background_service_upload_failure_notification": "A feltöltés sikertelen {}",
   "backup_controller_page_albums": "Albumok Mentése",
   "backup_controller_page_background_app_refresh_disabled_content": "Engedélyezd a háttérben történő frissítést a Beállítások > Általános > Háttérben Frissítés menüpontban.",
   "backup_controller_page_background_app_refresh_disabled_title": "Háttérben frissítés kikapcsolva",
@@ -85,7 +95,7 @@
   "backup_controller_page_background_battery_info_ok": "OK",
   "backup_controller_page_background_battery_info_title": "Akkumulátor optimalizálás",
   "backup_controller_page_background_charging": "Csak töltés közben",
-  "backup_controller_page_background_configure_error": "Nem sikerült beállítani a háttér szolgáltatást",
+  "backup_controller_page_background_configure_error": "A háttérszolgáltatás beállítása sikertelen",
   "backup_controller_page_background_delay": "Új elemek mentésének késleltetése: {}",
   "backup_controller_page_background_description": "Kapcsold be a háttérfolyamatot, hogy automatikusan mentsen elemeket az applikáció megnyitása nélkül",
   "backup_controller_page_background_is_off": "Automatikus mentés a háttérben ki van kapcsolva",
@@ -98,7 +108,7 @@
   "backup_controller_page_backup_sub": "Mentett fotók és videók",
   "backup_controller_page_cancel": "Mégsem",
   "backup_controller_page_created": "Létrehozva: {}",
-  "backup_controller_page_desc_backup": "Ha engedélyezed az előtérben mentést, akkor az új elemek automatikusan feltöltődnek a szerverre, amikor megyitod az alkalmazást.",
+  "backup_controller_page_desc_backup": "Ha bekapcsolod az előtérben mentést, akkor az új elemek automatikusan feltöltődnek a szerverre, amikor megyitod az alkalmazást.",
   "backup_controller_page_excluded": "Kivéve:",
   "backup_controller_page_failed": "Sikertelen ({})",
   "backup_controller_page_filename": "Fájlnév: {}[{}]",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Sikeres",
   "backup_manual_title": "Feltöltés állapota",
   "backup_options_page_title": "Biztonági mentés beállításai",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Képtár oldalankénti bélyegképei ({} elem)",
   "cache_settings_clear_cache_button": "Gyorsítótár kiürítése",
   "cache_settings_clear_cache_button_title": "Kiüríti az alkalmazás gyorsítótárát. Ez jelentősen kihat az alkalmazás teljesítményére, amíg a gyorsítótár újra nem épül.",
@@ -145,36 +156,41 @@
   "cache_settings_tile_subtitle": "Helyi tárhely viselkedésének beállítása",
   "cache_settings_tile_title": "Helyi Tárhely",
   "cache_settings_title": "Gyorsítótár Beállítások",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Jelszó Megerősítése",
   "change_password_form_description": "Szia {name}!\n\nMost jelentkezel be először a rendszerbe vagy más okból szükséges a jelszavad meváltoztatása. Kérjük, add meg új jelszavad.",
   "change_password_form_new_password": "Új Jelszó",
   "change_password_form_password_mismatch": "A beírt jelszavak nem egyeznek",
-  "change_password_form_reenter_new_password": "Jelszó (még egyszer)",
+  "change_password_form_reenter_new_password": "Jelszó (Még Egyszer)",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
-  "client_cert_enter_password": "Enter Password",
-  "client_cert_import": "Import",
-  "client_cert_import_success_msg": "Client certificate is imported",
-  "client_cert_invalid_msg": "Invalid certificate file or wrong password",
-  "client_cert_remove": "Remove",
-  "client_cert_remove_msg": "Client certificate is removed",
-  "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login",
-  "client_cert_title": "SSL Client Certificate",
+  "client_cert_enter_password": "Jelszó Megadása",
+  "client_cert_import": "Importálás",
+  "client_cert_import_success_msg": "Kliens tanúsítvány importálva",
+  "client_cert_invalid_msg": "Érvénytelen tanúsítvány fájl vagy hibás jelszó",
+  "client_cert_remove": "Eltávolítás",
+  "client_cert_remove_msg": "Kliens tanúsítvány eltávolítva",
+  "client_cert_subtitle": "Csak a PKCS12 (.p12, .pfx) formátum támogatott. Tanúsítvány Importálása/Eltávolítása csak a bejelentkezés előtt lehetséges",
+  "client_cert_title": "SSL Kliens Tanúsítvány",
   "common_add_to_album": "Albumhoz ad",
   "common_change_password": "Jelszócsere",
   "common_create_new_album": "Új album létrehozása",
   "common_server_error": "Kérjük, ellenőrizd a hálózati kapcsolatot, gondoskodj róla, hogy a szerver elérhető legyen, valamint az alkalmazás és a szerver kompatibilis verziójú legyen.",
-  "common_shared": "Megosztva",
-  "contextual_search": "Sunrise on the beach",
+  "common_shared": "Megosztott",
+  "contextual_search": "Napfelkelte a tengerparton",
   "control_bottom_app_bar_add_to_album": "Albumhoz ad",
   "control_bottom_app_bar_album_info": "{} elem",
   "control_bottom_app_bar_album_info_shared": "{} elemek · Megosztva",
-  "control_bottom_app_bar_archive": "Archivál",
+  "control_bottom_app_bar_archive": "Archiválás",
   "control_bottom_app_bar_create_new_album": "Új album létrehozása",
   "control_bottom_app_bar_delete": "Törlés",
   "control_bottom_app_bar_delete_from_immich": "Törlés az Immich-ből",
   "control_bottom_app_bar_delete_from_local": "Törlés az eszközről",
-  "control_bottom_app_bar_download": "Download",
-  "control_bottom_app_bar_edit": "Edit",
+  "control_bottom_app_bar_download": "Letöltés",
+  "control_bottom_app_bar_edit": "Szerkesztés",
   "control_bottom_app_bar_edit_location": "Hely Módosítása",
   "control_bottom_app_bar_edit_time": "Dátum és Idő Módosítása",
   "control_bottom_app_bar_favorite": "Kedvenc",
@@ -185,14 +201,17 @@
   "control_bottom_app_bar_unarchive": "Nem Archivált",
   "control_bottom_app_bar_unfavorite": "Nem Kedvenc",
   "control_bottom_app_bar_upload": "Feltöltés",
+  "create_album": "Album létrehozása",
   "create_album_page_untitled": "Névtelen",
+  "create_new": "ÚJ LÉTREHOZÁSA",
   "create_shared_album_page_create": "Létrehoz",
   "create_shared_album_page_share": "Megosztás",
   "create_shared_album_page_share_add_assets": "ELEMEK HOZZÁADÁSA",
   "create_shared_album_page_share_select_photos": "Fotók választása",
-  "crop": "Crop",
+  "crop": "Kivágás",
   "curated_location_page_title": "Helyek",
   "curated_object_page_title": "Dolgok",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "MMM dd (E)",
   "daily_title_text_date_year": "yyyy MMM dd (E)",
   "date_format": "y LLL d (E) • HH:mm",
@@ -210,15 +229,28 @@
   "delete_shared_link_dialog_title": "Megosztott Link Törlése",
   "description_input_hint_text": "Leírás hozzáadása...",
   "description_input_submit_error": "Nem sikerült frissíteni a leírást. További információért kérjük, nézd meg az eseménynaplót",
-  "download_error": "Download Error",
-  "download_started": "Download started",
-  "download_sucess": "Download success",
-  "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_canceled": "Letöltés megszakítva",
+  "download_complete": "Letöltés kész",
+  "download_enqueue": "Letöltés sorba állítva",
+  "download_error": "Letöltési Hiba",
+  "download_failed": "Sikertelen letöltés",
+  "download_filename": "fájl: {}",
+  "download_finished": "Letöltés kész",
+  "downloading": "Letöltés...",
+  "downloading_media": "Média letöltése",
+  "download_notfound": "Letöltés nem található",
+  "download_paused": "Letöltés szüneteltetve",
+  "download_started": "Letöltés megkezdve",
+  "download_sucess": "Sikeres letöltés",
+  "download_sucess_android": "Média letöltve a DCIM/Immich mappába\n",
+  "download_waiting_to_retry": "Várakozás",
   "edit_date_time_dialog_date_time": "Dátum és Idő",
   "edit_date_time_dialog_timezone": "Időzóna",
-  "edit_image_title": "Edit",
+  "edit_image_title": "Szerkesztés",
   "edit_location_dialog_title": "Hely",
-  "error_saving_image": "Error: {}",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
+  "error_saving_image": "Hiba: {}",
   "exif_bottom_sheet_description": "Leírás Hozzáadása...",
   "exif_bottom_sheet_details": "RÉSZLETEK",
   "exif_bottom_sheet_location": "HELY",
@@ -229,18 +261,24 @@
   "experimental_settings_new_asset_list_title": "Kisérleti képrács engedélyezése",
   "experimental_settings_subtitle": "Csak saját felelősségre használd!",
   "experimental_settings_title": "Kísérleti",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Kedvencek",
   "favorites_page_no_favorites": "Nem található kedvencnek jelölt elem",
   "favorites_page_title": "Kedvencek",
-  "filename_search": "File name or extension",
+  "filename_search": "Fájlnév vagy kiterjesztés",
+  "filter": "Szűrő",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Rezgéses visszajelzés engedélyezése",
   "haptic_feedback_title": "Rezgéses Visszajelzés",
-  "header_settings_add_header_tip": "Add Header",
-  "header_settings_field_validator_msg": "Value cannot be empty",
-  "header_settings_header_name_input": "Header name",
-  "header_settings_header_value_input": "Header value",
-  "header_settings_page_title": "Proxy Headers",
-  "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
-  "headers_settings_tile_title": "Custom proxy headers",
+  "header_settings_add_header_tip": "Fejléc Hozzáadása",
+  "header_settings_field_validator_msg": "Az érték nem lehet üres",
+  "header_settings_header_name_input": "Fejléc neve",
+  "header_settings_header_value_input": "Fejléc értéke",
+  "header_settings_page_title": "Proxy Fejlécek",
+  "headers_settings_tile_subtitle": "Add meg azokat a proxy fejléceket, amiket az app elküldjön minden hálózati kérésnél",
+  "headers_settings_tile_title": "Egyéni proxy fejlécek",
   "home_page_add_to_album_conflicts": "{added} elem hozzáadva a(z) \"{album}\" albumhoz. {failed} elem már eleve az albumban volt.",
   "home_page_add_to_album_err_local": "Helyi elemeket még nem lehet albumba tenni. Kihagyjuk.",
   "home_page_add_to_album_success": "{added} elem hozzáadva a(z) \"{album}\" albumhoz.",
@@ -253,27 +291,34 @@
   "home_page_favorite_err_local": "Helyi elemeket még nem lehet a kedvencek közé tenni, úgyhogy ezeket kihagyjuk",
   "home_page_favorite_err_partner": "Partner elemeit még nem lehet a kedvencek közé tenni, úgyhogy ezeket kihagyjuk",
   "home_page_first_time_notice": "Ha most használod először az alkalmazást, akkor ahhoz, hogy megjelenjenek a fotók és a videók az idővonaladon, állítsd be, hogy melyik albumaidról készüljön biztonsági mentés.",
-  "home_page_share_err_local": "Helyi elemekről nem lehet megosztási linket készíteni, úgyhogy kihagyjuk",
+  "home_page_share_err_local": "Helyi elemekről nem lehet megosztott linket készíteni, úgyhogy kihagyjuk",
   "home_page_upload_err_limit": "Csak 30 elemet tudsz egyszerre feltölteni, úgyhogy kihagyjuk",
-  "image_saved_successfully": "Image saved",
+  "ignore_icloud_photos": "iCloud fotók figyelmen kívül hagyása",
+  "ignore_icloud_photos_description": "Az iCloud-ban tárolt fotók nem lesznek feltöltve az Immich szerverre",
+  "image_saved_successfully": "Kép elmentve",
   "image_viewer_page_state_provider_download_error": "Letöltési Hiba",
   "image_viewer_page_state_provider_download_started": "Letöltés Megkezdődött",
   "image_viewer_page_state_provider_download_success": "Letöltés Sikeres",
-  "image_viewer_page_state_provider_share_error": "Megosztási Hiba",
-  "invalid_date": "Invalid date",
-  "invalid_date_format": "Invalid date format",
+  "image_viewer_page_state_provider_share_error": "Megosztás Hiba",
+  "invalid_date": "Érvénytelen dátum",
+  "invalid_date_format": "Érvénytelen dátumformátum",
+  "library": "Képtár",
   "library_page_albums": "Albumok",
   "library_page_archive": "Archívum",
   "library_page_device_albums": "Albumok az Eszközön",
   "library_page_favorites": "Kedvencek",
   "library_page_new_album": "Új album",
   "library_page_sharing": "Megosztás",
-  "library_page_sort_asset_count": "Eszközök száma",
+  "library_page_sort_asset_count": "Elemek száma",
   "library_page_sort_created": "Létrehozás ideje",
   "library_page_sort_last_modified": "Utolsó módosítás ideje",
   "library_page_sort_most_oldest_photo": "Legrégebbi fotó",
   "library_page_sort_most_recent_photo": "Legújabb fotó",
   "library_page_sort_title": "Album címe",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Válassz a térképen",
   "location_picker_latitude": "Szélességi kör",
   "location_picker_latitude_error": "Érvényes szélességi kört írj be",
@@ -288,14 +333,14 @@
   "login_form_email_hint": "email@cimed.hu",
   "login_form_endpoint_hint": "http(s)://szerver-címe:port/api",
   "login_form_endpoint_url": "Szerver címe",
-  "login_form_err_http": "Kérjük, adj meg egy http:// vagy https:// címet",
+  "login_form_err_http": "Kérjük, hogy egy http:// vagy https:// címet adj meg",
   "login_form_err_invalid_email": "Érvénytelen email cím",
   "login_form_err_invalid_url": "Érvénytelen cím",
   "login_form_err_leading_whitespace": "Az első karakter szóköz",
   "login_form_err_trailing_whitespace": "Az utolsó karakter szóköz",
   "login_form_failed_get_oauth_server_config": "Nem sikerült az OAuth bejelentkezés. Ellenőrizd a szerver címét.",
   "login_form_failed_get_oauth_server_disable": "OAuth bejelentkezés nem elérhető ezen a szerveren",
-  "login_form_failed_login": "Hiba bejelentkezés közben, ellenőrizd a címet, email-t és a jelszót",
+  "login_form_failed_login": "Hiba a bejelentkezés közben, ellenőrizd a szerver címét, az emailt és a jelszót",
   "login_form_handshake_exception": "SSL Kézfogási Hiba törént. Engedélyezd az önaláírt tanúsítvényokat a beállításokban, hogy ha önaláírt tanúsítványt használsz.",
   "login_form_label_email": "Email",
   "login_form_label_password": "Jelszó",
@@ -317,7 +362,7 @@
   "map_no_assets_in_bounds": "Nincsenek fotók a környéken",
   "map_no_location_permission_content": "A helymeghatározást engedélyezni kell a jelenlegi helyednél lévő elemek megjelenítéséhez. Szeretnéd most engedélyezni?",
   "map_no_location_permission_title": "Helymeghatározás letiltva",
-  "map_settings_dark_mode": "Sötét mód",
+  "map_settings_dark_mode": "Sötét téma",
   "map_settings_date_range_option_all": "Összes",
   "map_settings_date_range_option_day": "Elmúlt 24 óra",
   "map_settings_date_range_option_days": "Elmúlt {} nap",
@@ -331,51 +376,59 @@
   "map_settings_only_relative_range": "Dátum intervallum",
   "map_settings_only_show_favorites": "Csak Kedvencek Mutatása",
   "map_settings_theme_settings": "Térkép Témája",
-  "map_zoom_to_see_photos": "Kicsinyíts, hogy láss fényképeket",
+  "map_zoom_to_see_photos": "Kicsinyítsd, hogy láss fényképeket",
   "memories_all_caught_up": "Naprakész vagy",
   "memories_check_back_tomorrow": "Nézz vissza holnap újabb emlékekért",
   "memories_start_over": "Újrakezdés",
   "memories_swipe_to_close": "Bezáráshoz söpörd ki felfelé",
-  "memories_year_ago": "A year ago",
-  "memories_years_ago": "{} years ago",
+  "memories_year_ago": "Egy éve",
+  "memories_years_ago": "{} éve",
   "monthly_title_text_date_format": "y MMMM",
-  "motion_photos_page_title": "Mozgó Fotók",
+  "motion_photos_page_title": "Mozgóképek",
   "multiselect_grid_edit_date_time_err_read_only": "Csak-olvasható elem(ek) dátuma nem módosítható, ezért kihagyjuk",
-  "multiselect_grid_edit_gps_err_read_only": "Csak-olvasható elem(ek) helyszíne nem módosítható, ezért kihagyjuk",
+  "multiselect_grid_edit_gps_err_read_only": "Csak-olvasható elem(ek) helye nem módosítható, ezért kihagyjuk",
+  "my_albums": "Saját albumaim",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "Nincs megjeleníthető elem",
-  "no_name": "No name",
+  "no_name": "Névtelen",
   "notification_permission_dialog_cancel": "Mégsem",
   "notification_permission_dialog_content": "Az értesítések bekapcsolásához a Beállítások menüben válaszd ki az Engedélyezés-t.",
   "notification_permission_dialog_settings": "Beállítások",
-  "notification_permission_list_tile_content": "Értesítések engedélyezése",
+  "notification_permission_list_tile_content": "Értesítések engedélyezése.",
   "notification_permission_list_tile_enable_button": "Értesítések Bekapcsolása",
   "notification_permission_list_tile_title": "Engedély az Értesítésekhez",
+  "on_this_device": "Ezen az eszközön",
   "partner_list_user_photos": "{user} fényképei",
   "partner_list_view_all": "Összes mutatása",
   "partner_page_add_partner": "Partner hozzáadása",
   "partner_page_empty_message": "Még senkivel nem osztottad meg a fényképeidet.",
-  "partner_page_no_more_users": "Nincs hozzáadható felhasználó",
-  "partner_page_partner_add_failed": "Nem sikerült hozzáadni a felhasználót",
+  "partner_page_no_more_users": "Nincs több hozzáadható felhasználó",
+  "partner_page_partner_add_failed": "Partner hozzáadása sikertelen",
   "partner_page_select_partner": "Partner kiválasztása",
   "partner_page_shared_to_title": "Megosztva: ",
   "partner_page_stop_sharing_content": "{} nem fog többé hozzáférni a fotóidhoz.",
   "partner_page_stop_sharing_title": "Fotók megosztásának megszűntetése?",
   "partner_page_title": "Partner",
+  "partners": "Partnerek",
+  "people": "Emberek",
   "permission_onboarding_back": "Vissza",
   "permission_onboarding_continue_anyway": "Folytatás mindenképp",
-  "permission_onboarding_get_started": "Kezdjük el",
+  "permission_onboarding_get_started": "Vágjunk bele",
   "permission_onboarding_go_to_settings": "Beállítások megnyitása",
-  "permission_onboarding_grant_permission": "Engedélyezés",
+  "permission_onboarding_grant_permission": "Engedély meadása",
   "permission_onboarding_log_out": "Kijelentkezés",
   "permission_onboarding_permission_denied": "Hozzáférés megtagadva. Az Immich használatához engedélyezni kell a fotó és videó hozzáférést a Beállításokban.",
   "permission_onboarding_permission_granted": "Hozzáférés engedélyezve! Minden készen áll.",
   "permission_onboarding_permission_limited": "Korlátozott hozzáférés. Ha szeretnéd, hogy az Immich a teljes galéria gyűjteményedet mentse és kezelje, akkor a Beállításokban engedélyezd a fotó és videó jogosultságokat.",
-  "permission_onboarding_request": "Engedélyezni kell, hogy az Immich hozzáférjen a képekhez és videókhoz",
+  "permission_onboarding_request": "Engedélyezni kell, hogy az Immich hozzáférjen a képeidhez és videóidhoz",
+  "places": "Helyek",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Beállítások",
   "profile_drawer_app_logs": "Naplók",
   "profile_drawer_client_out_of_date_major": "A mobilalkalmazás elavult. Kérjük, frissítsd a legfrisebb főverzióra.",
   "profile_drawer_client_out_of_date_minor": "A mobilalkalmazás elavult. Kérjük, frissítsd a legfrisebb alverzióra.",
-  "profile_drawer_client_server_up_to_date": "Kliens és a szerver is naprakész",
+  "profile_drawer_client_server_up_to_date": "A Kliens és a Szerver is naprakész",
   "profile_drawer_documentation": "Dokumentáció",
   "profile_drawer_github": "GitHub",
   "profile_drawer_server_out_of_date_major": "A szerver elavult. Kérjük, frissítsd a legfrisebb főverzióra.",
@@ -383,40 +436,43 @@
   "profile_drawer_settings": "Beállítások",
   "profile_drawer_sign_out": "Kijelentkezés",
   "profile_drawer_trash": "Lomtár",
+  "recently_added": "Nemrég hozzáadott",
   "recently_added_page_title": "Nemrég Hozzáadott",
-  "save_to_gallery": "Save to gallery",
+  "save": "Save",
+  "save_to_gallery": "Mentés a galériába",
   "scaffold_body_error_occurred": "Hiba történt",
+  "search_albums": "Albumok keresése",
   "search_bar_hint": "Fotók keresése",
   "search_filter_apply": "Szűrő alkalmazása",
-  "search_filter_camera": "Camera",
+  "search_filter_camera": "Kamera",
   "search_filter_camera_make": "Gyártó",
   "search_filter_camera_model": "Modell",
-  "search_filter_camera_title": "Select camera type",
-  "search_filter_date": "Date",
-  "search_filter_date_interval": "{start} to {end}",
-  "search_filter_date_title": "Select a date range",
+  "search_filter_camera_title": "Válaszd ki a kamera típusát",
+  "search_filter_date": "Dátum",
+  "search_filter_date_interval": "{start} - {end}",
+  "search_filter_date_title": "Válassz dátum intervallumot",
   "search_filter_display_option_archive": "Archivált",
   "search_filter_display_option_favorite": "Kedvenc",
   "search_filter_display_option_not_in_album": "Nincs albumban",
-  "search_filter_display_options": "Display Options",
-  "search_filter_display_options_title": "Display options",
-  "search_filter_location": "Location",
-  "search_filter_location_city": "Város",
+  "search_filter_display_options": "Megjelenítési Beállítások",
+  "search_filter_display_options_title": "Megjelenítési beállítások",
+  "search_filter_location": "Hely",
+  "search_filter_location_city": "Település",
   "search_filter_location_country": "Ország",
-  "search_filter_location_state": "Állam",
-  "search_filter_location_title": "Select location",
-  "search_filter_media_type": "Media Type",
+  "search_filter_location_state": "Megye/Állam",
+  "search_filter_location_title": "Válassz helyet",
+  "search_filter_media_type": "Média Típus",
   "search_filter_media_type_all": "Összes",
   "search_filter_media_type_image": "Kép",
-  "search_filter_media_type_title": "Select media type",
+  "search_filter_media_type_title": "Válassz média típust",
   "search_filter_media_type_video": "Videó",
-  "search_filter_people": "People",
-  "search_filter_people_title": "Select people",
+  "search_filter_people": "Emberek",
+  "search_filter_people_title": "Válassz embereket",
   "search_page_categories": "Kategóriák",
   "search_page_favorites": "Kedvencek",
-  "search_page_motion_photos": "Mozgó Fotók",
+  "search_page_motion_photos": "Mozgóképek",
   "search_page_no_objects": "Nincs Információ a Tárgyakról",
-  "search_page_no_places": "Nincs Információ a Helyszínekről",
+  "search_page_no_places": "Nincs Információ a Helyekről",
   "search_page_people": "Emberek",
   "search_page_person_add_name_dialog_cancel": "Mégsem",
   "search_page_person_add_name_dialog_hint": "Név",
@@ -428,18 +484,20 @@
   "search_page_places": "Helyek",
   "search_page_recently_added": "Nemrég hozzáadott",
   "search_page_screenshots": "Képernyőképek",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Szelfik",
   "search_page_things": "Dolgok",
   "search_page_videos": "Videók",
   "search_page_view_all_button": "Összes mutatása",
-  "search_page_your_activity": "Tevékenységek",
-  "search_page_your_map": "Térkép",
-  "search_result_page_new_search_hint": "Új keresés",
-  "search_suggestion_list_smart_search_hint_1": "Az intelligens keresés alapértelmezetten be van kapcsolva, metaadatokat így kereshetsz",
+  "search_page_your_activity": "Tevékenységeid",
+  "search_page_your_map": "Térképed",
+  "search_result_page_new_search_hint": "Új Keresés",
+  "search_suggestion_list_smart_search_hint_1": "Az intelligens keresés alapértelmezetten be van kapcsolva, metaadatokat így kereshetsz:",
   "search_suggestion_list_smart_search_hint_2": "m:keresési-kifejezés",
   "select_additional_user_for_sharing_page_suggestions": "Javaslatok",
-  "select_user_for_sharing_page_err_album": "Nem sikerült létrehozni az albumot",
+  "select_user_for_sharing_page_err_album": "Az album létrehozása sikertelen",
   "select_user_for_sharing_page_share_suggestions": "Javaslatok",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "Alkalmazás Verzió",
   "server_info_box_latest_release": "Legfrissebb Verzió",
   "server_info_box_server_url": "Szerver Címe",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Előnézet betöltése",
   "setting_image_viewer_title": "Képek",
   "setting_languages_apply": "Alkalmaz",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Nyelvek",
   "setting_notifications_notify_failures_grace_period": "Értesítés a háttérben történő mentés hibáiról: {}",
   "setting_notifications_notify_hours": "{} óra",
@@ -478,7 +537,7 @@
   "shared_album_activities_input_hint": "Szólj hozzá",
   "shared_album_activity_remove_content": "Törölni szeretnéd ezt a tevékenységet?",
   "shared_album_activity_remove_title": "Tevékenység Törlése",
-  "shared_album_activity_setting_subtitle": "Engedd, hogy mások reagáljanak",
+  "shared_album_activity_setting_subtitle": "Mások is reagálhatnak",
   "shared_album_activity_setting_title": "Hozzászólások és lájkok",
   "shared_album_section_people_action_error": "Hiba az albummal kapcsolatos kilépés/eltávolítás közben",
   "shared_album_section_people_action_leave": "Felhasználó eltávolítása az albumból",
@@ -489,8 +548,8 @@
   "shared_link_app_bar_title": "Megosztott Linkek",
   "shared_link_clipboard_copied_massage": "Vágólapra másolva",
   "shared_link_clipboard_text": "Link: {}\nJelszó: {}",
-  "shared_link_create_app_bar_title": "Megosztási link létrehozása",
-  "shared_link_create_error": "Hiba a megosztási link létrehozásakor",
+  "shared_link_create_app_bar_title": "Megosztott link létrehozása",
+  "shared_link_create_error": "Hiba a megosztott link létrehozásakor",
   "shared_link_create_info": "A linket használva bárki megnézheti a kiválasztott kép(ek)et",
   "shared_link_create_submit_button": "Link létrehozása",
   "shared_link_edit_allow_download": "Letöltés engedélyezése",
@@ -510,11 +569,11 @@
   "shared_link_edit_expire_after_option_never": "Soha",
   "shared_link_edit_expire_after_option_year": "{} év",
   "shared_link_edit_password": "Jelszó",
-  "shared_link_edit_password_hint": "Add meg a megosztási jelszót",
+  "shared_link_edit_password_hint": "Add meg a megosztáshoz tartozó jelszót",
   "shared_link_edit_show_meta": "Metaadatok mutatása",
   "shared_link_edit_submit_button": "Link frissítése",
-  "shared_link_empty": "Nincsenek megosztási linkek",
-  "shared_link_error_server_url_fetch": "A szerver címét nem sikerült betölteni",
+  "shared_link_empty": "Nincsenek megosztott linkek",
+  "shared_link_error_server_url_fetch": "A szerver címét nem lehet betölteni",
   "shared_link_expired": "Lejárt",
   "shared_link_expires_day": "{} nap múlva lejár",
   "shared_link_expires_days": "{} nap múlva lejár",
@@ -529,50 +588,53 @@
   "shared_link_info_chip_download": "Letöltés",
   "shared_link_info_chip_metadata": "EXIF",
   "shared_link_info_chip_upload": "Feltöltés",
-  "shared_link_manage_links": "Megosztási linkek kezelése",
+  "shared_link_manage_links": "Megosztott linkek kezelése",
   "shared_link_public_album": "Nyilvános album",
+  "shared_links": "Megosztott linkek",
   "share_done": "Kész",
+  "shared_with_me": "Velem megosztva",
   "share_invite": "Meghívás az albumba",
   "sharing_page_album": "Megosztott albumok",
-  "sharing_page_description": "Megosztott albumok létrehozásával fényképeket és videókat oszthatsz meg a hálózatodban lévő emberekkel.",
+  "sharing_page_description": "Megosztott albumok létrehozásával fényképeket és videókat oszthatsz meg az ismerőseiddel.",
   "sharing_page_empty_list": "ÜRES LISTA",
   "sharing_silver_appbar_create_shared_album": "Új megosztott album",
-  "sharing_silver_appbar_shared_links": "Megosztási linkek",
+  "sharing_silver_appbar_shared_links": "Megosztott linkek",
   "sharing_silver_appbar_share_partner": "Megosztás partnerrel",
-  "sync": "Sync",
-  "sync_albums": "Sync albums",
-  "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
-  "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
+  "sync": "Szinkronizálás",
+  "sync_albums": "Albumok szinkronizálása",
+  "sync_albums_manual_subtitle": "Összes fotó és videó létrehozása és szinkronizálása a kiválasztott Immich albumokba",
+  "sync_upload_album_setting_subtitle": "Fotók és videók létrehozása és szinkronizálása a kiválasztott Immich albumba",
   "tab_controller_nav_library": "Képtár",
   "tab_controller_nav_photos": "Képek",
   "tab_controller_nav_search": "Keresés",
   "tab_controller_nav_sharing": "Megosztás",
   "theme_setting_asset_list_storage_indicator_title": "Tárhely ikon mutatása az elemeken",
   "theme_setting_asset_list_tiles_per_row_title": "Elemek száma soronként ({})",
-  "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.",
-  "theme_setting_colorful_interface_title": "Colorful interface",
-  "theme_setting_dark_mode_switch": "Sötét mód",
+  "theme_setting_colorful_interface_subtitle": "Alapértelmezett szín használata a háttérben lévő felületekhez",
+  "theme_setting_colorful_interface_title": "Színes felhasználói felület",
+  "theme_setting_dark_mode_switch": "Sötét téma",
   "theme_setting_image_viewer_quality_subtitle": "Részletes képmegjelenítő minőségének beállítása",
   "theme_setting_image_viewer_quality_title": "Képmegjelenítő minősége",
-  "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.",
-  "theme_setting_primary_color_title": "Primary color",
-  "theme_setting_system_primary_color_title": "Use system color",
+  "theme_setting_primary_color_subtitle": "Válassz egy színt az alapértelmezett műveletekhez és kiemelésekhez",
+  "theme_setting_primary_color_title": "Alapértelmezett szín",
+  "theme_setting_system_primary_color_title": "Rendszerszínek használata",
   "theme_setting_system_theme_switch": "Automatikus (követi a rendszer témáját)",
   "theme_setting_theme_subtitle": "Alkalmazás témájának választása",
   "theme_setting_theme_title": "Téma",
   "theme_setting_three_stage_loading_subtitle": "A háromlépcsős betöltés javíthatja a betöltési teljesítményt, de jelentősen növeli a hálózati forgalmat",
   "theme_setting_three_stage_loading_title": "Háromlépcsős betöltés engedélyezése",
   "translated_text_options": "Beállítások",
-  "trash_emptied": "Emptied trash",
+  "trash": "Lomtár",
+  "trash_emptied": "Lomtár kiürítve",
   "trash_page_delete": "Töröl",
   "trash_page_delete_all": "Mindet Töröl",
-  "trash_page_empty_trash_btn": "Lomtár Ürítése",
+  "trash_page_empty_trash_btn": "Lomtár ürítése",
   "trash_page_empty_trash_dialog_content": "Ki szeretnéd üríteni a lomtárban lévő elemeket? Ezeket véglegesen eltávolítjuk az Immich-ből",
   "trash_page_empty_trash_dialog_ok": "Ok",
   "trash_page_info": "A Lomátrba helyezett elemek {} nap után véglegesen törlődnek",
-  "trash_page_no_assets": "Nincsen semmi a Lomtárban",
+  "trash_page_no_assets": "A Lomtár üres",
   "trash_page_restore": "Visszaállít",
-  "trash_page_restore_all": "Mindet Visszaállítja",
+  "trash_page_restore_all": "Mindet Visszaállít",
   "trash_page_select_assets_btn": "Elemek kiválasztása",
   "trash_page_select_btn": "Kiválaszt",
   "trash_page_title": "Lomtár ({})",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Szeretnél mentést készíteni a kiválasztott elem(ek)ről a szerverre?",
   "upload_dialog_ok": "Feltöltés",
   "upload_dialog_title": "Elem Feltöltése",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Megértettem",
-  "version_announcement_overlay_release_notes": "a változtatások listáját elolvasd",
-  "version_announcement_overlay_text_1": "Szia, egy új verzió érhető el",
-  "version_announcement_overlay_text_2": "kérlek szánj időt arra, hogy ",
-  "version_announcement_overlay_text_3": "és gyöződj meg róla, hogy a docker-compose és .env beállításai naprakészek és pontosak, különösen akkor,  ha watchtower-t vagy bármi olyan megoldást használsz, ami automatikusan frissíti a szervert.",
-  "version_announcement_overlay_title": "Új Szerververzió Érhető El \uD83C\uDF89",
+  "version_announcement_overlay_release_notes": "kiadási megjegyzések áttekintésére",
+  "version_announcement_overlay_text_1": "Szia barátom, ennek az alkalmazásnak van egy új verziója: ",
+  "version_announcement_overlay_text_2": "Kérjük, szánj időt a",
+  "version_announcement_overlay_text_3": ", és győződj meg róla, hogy a docker-compose.yml és az .env beállításaid naprakészek, hogy elkerüld a hibás konfigurációkat, különösen, ha a WatchTower-t vagy bármilyen automatikus frissítési megoldást használsz.",
+  "version_announcement_overlay_title": "Elérhető Új Szerververzió \uD83C\uDF89",
+  "videos": "Videók",
   "viewer_remove_from_stack": "Eltávolít a Csoportból",
   "viewer_stack_use_as_main_asset": "Fő Elemnek Beállít",
-  "viewer_unstack": "Csoport Megszűntetése"
+  "viewer_unstack": "Csoport Megszűntetése",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/it-IT.json b/mobile/assets/i18n/it-IT.json
index d7585c753c..4be66e3b03 100644
--- a/mobile/assets/i18n/it-IT.json
+++ b/mobile/assets/i18n/it-IT.json
@@ -6,6 +6,8 @@
   "action_common_save": "Save",
   "action_common_select": "Select",
   "action_common_update": "Aggiorna",
+  "add_a_name": "Add a name",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Aggiunto in {album}",
   "add_to_album_bottom_sheet_already_exists": "Già presente in {album}",
   "advanced_settings_log_level_title": "Livello log: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Risoluzione problemi",
   "album_info_card_backup_album_excluded": "ESCLUSI",
   "album_info_card_backup_album_included": "INCLUSI",
+  "albums": "Albums",
   "album_thumbnail_card_item": "1 elemento ",
   "album_thumbnail_card_items": "{} elementi",
   "album_thumbnail_card_shared": "Condiviso",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Rimuovere dall'album ",
   "album_viewer_appbar_share_to": "Condividi a",
   "album_viewer_page_share_add_users": "Aggiungi utenti",
+  "all": "All",
   "all_people_page_title": "Persone",
   "all_videos_page_title": "Video",
   "app_bar_signout_dialog_content": "Sei sicuro di volerti disconnettere?",
   "app_bar_signout_dialog_ok": "Si",
   "app_bar_signout_dialog_title": "Disconnetti",
+  "archived": "Archived",
   "archive_page_no_archived_assets": "Nessuna oggetto archiviato",
   "archive_page_title": "Archivia ({})",
   "asset_action_delete_err_read_only": "Non puoi eliminare risorse in sola lettura, azione ignorata",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} asset(s) restored successfully",
   "assets_trashed": "{} asset(s) trashed",
   "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "Visualizzazione risorse",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Album sul dispositivo ({})",
   "backup_album_selection_page_albums_tap": "Tap per includere, doppio tap per escludere.",
   "backup_album_selection_page_assets_scatter": "Visto che le risorse possono trovarsi in più album, questi possono essere inclusi o esclusi dal backup.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Successo",
   "backup_manual_title": "Stato del caricamento",
   "backup_options_page_title": "Opzioni di Backup",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Anteprime pagine librerie ({} risorse)",
   "cache_settings_clear_cache_button": "Pulisci cache",
   "cache_settings_clear_cache_button_title": "Pulisce la cache dell'app. Questo impatterà significativamente le prestazioni dell''app fino a quando la cache non sarà rigenerata.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Controlla il comportamento dello storage locale",
   "cache_settings_tile_title": "Archiviazione locale",
   "cache_settings_title": "Impostazioni della Cache",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Conferma Password",
   "change_password_form_description": "Ciao {name},\n\nQuesto è la prima volta che accedi al sistema oppure è stato fatto una richiesta di cambiare la password. Per favore inserisca la nuova password qui sotto",
   "change_password_form_new_password": "Nuova Password",
   "change_password_form_password_mismatch": "Le password non coincidono",
   "change_password_form_reenter_new_password": "Inserisci ancora la nuova password ",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Enter Password",
   "client_cert_import": "Import",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Rimuovi dagli archivi",
   "control_bottom_app_bar_unfavorite": "Rimuovi preferito",
   "control_bottom_app_bar_upload": "Carica",
+  "create_album": "Create album",
   "create_album_page_untitled": "Senza titolo",
+  "create_new": "CREATE NEW",
   "create_shared_album_page_create": "Crea",
   "create_shared_album_page_share": "Condividi",
   "create_shared_album_page_share_add_assets": "AGGIUNGI OGGETTI",
@@ -193,6 +211,7 @@
   "crop": "Crop",
   "curated_location_page_title": "Location",
   "curated_object_page_title": "Oggetti",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, dd MMM",
   "daily_title_text_date_year": "E, dd MMM, yyyy",
   "date_format": "E, d LLL, y • hh:mm",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "Elimina link condiviso",
   "description_input_hint_text": "Aggiungi descrizione...",
   "description_input_submit_error": "Errore modificare descrizione, controlli I log per maggiori dettagli",
+  "download_canceled": "Download canceled",
+  "download_complete": "Download complete",
+  "download_enqueue": "Download enqueued",
   "download_error": "Download Error",
+  "download_failed": "Download failed",
+  "download_filename": "file: {}",
+  "download_finished": "Download finished",
+  "downloading": "Downloading...",
+  "downloading_media": "Downloading media",
+  "download_notfound": "Download not found",
+  "download_paused": "Download paused",
   "download_started": "Download started",
   "download_sucess": "Download success",
   "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_waiting_to_retry": "Waiting to retry",
   "edit_date_time_dialog_date_time": "Data e ora",
   "edit_date_time_dialog_timezone": "Fuso orario",
   "edit_image_title": "Edit",
   "edit_location_dialog_title": "Posizione",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Error: {}",
   "exif_bottom_sheet_description": "Aggiungi una descrizione...",
   "exif_bottom_sheet_details": "DETTAGLI",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Attiva griglia foto sperimentale",
   "experimental_settings_subtitle": "Usalo a tuo rischio!",
   "experimental_settings_title": "Sperimentale",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favorites",
   "favorites_page_no_favorites": "Nessun preferito",
   "favorites_page_title": "Preferiti",
   "filename_search": "File name or extension",
+  "filter": "Filter",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Abilita feedback aptico",
   "haptic_feedback_title": "Feedback aptico",
   "header_settings_add_header_tip": "Add Header",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "Se è la prima volta che utilizzi l'app, assicurati di scegliere uno o più album di backup, in modo che la timeline possa popolare le foto e i video presenti negli album.",
   "home_page_share_err_local": "Non puoi condividere una risorsa locale tramite link, azione ignorata",
   "home_page_upload_err_limit": "Puoi caricare al massimo 30 file per volta, ignora quelli in eccesso",
+  "ignore_icloud_photos": "Ignore iCloud photos",
+  "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
   "image_saved_successfully": "Image saved",
   "image_viewer_page_state_provider_download_error": "Errore nel Download",
   "image_viewer_page_state_provider_download_started": "Download Started",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "Errore di condivisione",
   "invalid_date": "Invalid date",
   "invalid_date_format": "Invalid date format",
+  "library": "Library",
   "library_page_albums": "Album",
   "library_page_archive": "Archivia",
   "library_page_device_albums": "Album sul dispositivo",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Foto più vecchia",
   "library_page_sort_most_recent_photo": "Più recente",
   "library_page_sort_title": "Titolo album",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Scegli una mappa",
   "location_picker_latitude": "Latitudine",
   "location_picker_latitude_error": "Inserisci una latitudine valida",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Foto in movimento",
   "multiselect_grid_edit_date_time_err_read_only": "Non puoi modificare la data di risorse in sola lettura, azione ignorata",
   "multiselect_grid_edit_gps_err_read_only": "Non puoi modificare la posizione di risorse in sola lettura, azione ignorata",
+  "my_albums": "My albums",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "Nessuna risorsa da mostrare",
   "no_name": "No name",
   "notification_permission_dialog_cancel": "Annulla",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Concedi i permessi per attivare le notifiche",
   "notification_permission_list_tile_enable_button": "Attiva notifiche",
   "notification_permission_list_tile_title": "Permessi delle Notifiche",
+  "on_this_device": "On this device",
   "partner_list_user_photos": "Foto di {user}",
   "partner_list_view_all": "Mostra tutto",
   "partner_page_add_partner": "Aggiungi partner.",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} non sarà più in grado di accedere alle tue foto.",
   "partner_page_stop_sharing_title": "Stoppare la condivisione delle tue foto?",
   "partner_page_title": "Partner",
+  "partners": "Partners",
+  "people": "People",
   "permission_onboarding_back": "Indietro",
   "permission_onboarding_continue_anyway": "Continua lo stesso",
   "permission_onboarding_get_started": "Inizia",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Concessi i permessi! Ora sei tutto apposto",
   "permission_onboarding_permission_limited": "Permessi limitati. Per consentire a Immich di gestire e fare i backup di tutta la galleria, concedi i permessi Foto e Video dalle Impostazioni.",
   "permission_onboarding_request": "Immich richiede i permessi per vedere le tue foto e video",
+  "places": "Places",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Preferenze",
   "profile_drawer_app_logs": "Logs",
   "profile_drawer_client_out_of_date_major": "L'applicazione non è aggiornata. Per favore aggiorna all'ultima versione principale.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Impostazioni ",
   "profile_drawer_sign_out": "Esci",
   "profile_drawer_trash": "Cestino",
+  "recently_added": "Recently added",
   "recently_added_page_title": "Aggiunti di recente",
+  "save": "Save",
   "save_to_gallery": "Save to gallery",
   "scaffold_body_error_occurred": "Si è verificato un errore.",
+  "search_albums": "Search albums",
   "search_bar_hint": "Cerca le tue foto",
   "search_filter_apply": "Applica filtro",
   "search_filter_camera": "Camera",
@@ -428,6 +484,7 @@
   "search_page_places": "Luoghi",
   "search_page_recently_added": "Aggiunte di recente",
   "search_page_screenshots": "Screenshot",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Selfie",
   "search_page_things": "Oggetti",
   "search_page_videos": "Video",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Suggerimenti ",
   "select_user_for_sharing_page_err_album": "Impossibile nel creare l'album ",
   "select_user_for_sharing_page_share_suggestions": "Suggerimenti",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "Versione App",
   "server_info_box_latest_release": "Ultima Versione",
   "server_info_box_server_url": "Server URL",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Carica immagine di anteprima",
   "setting_image_viewer_title": "Images",
   "setting_languages_apply": "Applica",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Lingue",
   "setting_notifications_notify_failures_grace_period": "Notifica caricamenti falliti in background: {}",
   "setting_notifications_notify_hours": "{} ore",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Carica",
   "shared_link_manage_links": "Gestisci link condivisi",
   "shared_link_public_album": "Album Pubblico",
+  "shared_links": "Shared links",
   "share_done": "Fatto",
+  "shared_with_me": "Shared with me",
   "share_invite": "Invita nell'album ",
   "sharing_page_album": "Album condivisi",
   "sharing_page_description": "Crea un album condiviso per condividere foto e video con gli utenti della tua rete Immich.",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Il caricamento a tre stage aumenterà le performance di caricamento ma anche il consumo di banda",
   "theme_setting_three_stage_loading_title": "Abilita il caricamento a tre stage",
   "translated_text_options": "Opzioni",
+  "trash": "Trash",
   "trash_emptied": "Emptied trash",
   "trash_page_delete": "Elimina",
   "trash_page_delete_all": "Elimina tutti",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Vuoi fare il backup sul server delle risorse selezionate?",
   "upload_dialog_ok": "Carica",
   "upload_dialog_title": "Carica file",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Presa visione",
   "version_announcement_overlay_release_notes": "note di rilascio",
   "version_announcement_overlay_text_1": "Ciao, c'è una nuova versione di",
   "version_announcement_overlay_text_2": "per favore prenditi il tuo tempo per visitare le ",
   "version_announcement_overlay_text_3": " e verifica che il tuo docker-compose e il file .env siano aggiornati per impedire qualsiasi errore di configurazione, specialmente se utilizzate WatchTower o altri strumenti per l'aggiornamento automatico dell'applicativo",
   "version_announcement_overlay_title": "Nuova versione del server disponibile \uD83C\uDF89",
+  "videos": "Videos",
   "viewer_remove_from_stack": "Rimuovi dalla pila",
   "viewer_stack_use_as_main_asset": "Usa come risorsa principale",
-  "viewer_unstack": "Rimuovi dal gruppo"
+  "viewer_unstack": "Rimuovi dal gruppo",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/ja-JP.json b/mobile/assets/i18n/ja-JP.json
index 21b8bea9e3..aa034f0699 100644
--- a/mobile/assets/i18n/ja-JP.json
+++ b/mobile/assets/i18n/ja-JP.json
@@ -3,16 +3,18 @@
   "action_common_cancel": "キャンセル",
   "action_common_clear": "クリア",
   "action_common_confirm": "了解",
-  "action_common_save": "Save",
-  "action_common_select": "Select",
+  "action_common_save": "保存",
+  "action_common_select": "選択",
   "action_common_update": "更新",
+  "add_a_name": "名前を追加",
+  "add_endpoint": "エンドポイントを追加してください",
   "add_to_album_bottom_sheet_added": "{album}に追加",
   "add_to_album_bottom_sheet_already_exists": "{album}に追加済み",
   "advanced_settings_log_level_title": "ログレベル: {}",
   "advanced_settings_prefer_remote_subtitle": "デバイスによっては、デバイス上にあるサムネイルのロードに非常に時間がかかることがあります。このオプションをに有効にする事により、サーバーから直接画像をロードすることが可能です。",
   "advanced_settings_prefer_remote_title": "リモートを優先する",
-  "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
-  "advanced_settings_proxy_headers_title": "Proxy Headers",
+  "advanced_settings_proxy_headers_subtitle": "プロキシヘッダを設定する",
+  "advanced_settings_proxy_headers_title": "プロキシヘッダ",
   "advanced_settings_self_signed_ssl_subtitle": "SSLのチェックをスキップする。自己署名証明書が必要です。",
   "advanced_settings_self_signed_ssl_title": "自己署名証明書を許可する",
   "advanced_settings_tile_subtitle": "追加ユーザー設定",
@@ -21,12 +23,13 @@
   "advanced_settings_troubleshooting_title": "トラブルシューティング",
   "album_info_card_backup_album_excluded": "除外中",
   "album_info_card_backup_album_included": "選択中",
+  "albums": "アルバム",
   "album_thumbnail_card_item": "1枚",
   "album_thumbnail_card_items": "{}枚",
   "album_thumbnail_card_shared": "共有済み",
   "album_thumbnail_owned": "所有中",
   "album_thumbnail_shared_by": "{}が共有中",
-  "album_viewer_appbar_delete_confirm": "本当にこのアルバムをアカウントから削除しますか?",
+  "album_viewer_appbar_delete_confirm": "本当にこのアルバムを削除しますか?",
   "album_viewer_appbar_share_delete": "アルバムを削除",
   "album_viewer_appbar_share_err_delete": "削除失敗",
   "album_viewer_appbar_share_err_leave": "退出失敗",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "アルバムから削除",
   "album_viewer_appbar_share_to": "次の方々と共有します",
   "album_viewer_page_share_add_users": "ユーザーを追加",
-  "all_people_page_title": "ピープル",
+  "all": "全て",
+  "all_people_page_title": "人物",
   "all_videos_page_title": "ビデオ",
   "app_bar_signout_dialog_content": " サインアウトしますか?",
   "app_bar_signout_dialog_ok": "はい",
   "app_bar_signout_dialog_title": " サインアウト",
+  "archived": "アーカイブ済み",
   "archive_page_no_archived_assets": "アーカイブ済みの写真またはビデオがありません",
   "archive_page_title": "アーカイブ({})",
   "asset_action_delete_err_read_only": "読み取り専用の項目は削除できません。スキップします",
@@ -54,14 +59,19 @@
   "asset_list_layout_sub_title": "レイアウト",
   "asset_list_settings_subtitle": "グリッドに関する設定",
   "asset_list_settings_title": "グリッド",
-  "asset_restored_successfully": "Asset restored successfully",
-  "assets_deleted_permanently": "{} asset(s) deleted permanently",
-  "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server",
-  "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device",
-  "assets_restored_successfully": "{} asset(s) restored successfully",
-  "assets_trashed": "{} asset(s) trashed",
-  "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_restored_successfully": "{}項目を復元しました",
+  "assets_deleted_permanently": "{}項目を完全に削除しました",
+  "assets_deleted_permanently_from_server": "サーバー上の{}項目を完全に削除しました",
+  "assets_removed_permanently_from_device": "端末から{}項目を完全に削除しました",
+  "assets_restored_successfully": "{}項目を復元しました",
+  "assets_trashed": "{}項目をゴミ箱に移動しました",
+  "assets_trashed_from_server": "サーバー上の{}項目をゴミ箱に移動しました",
+  "asset_viewer_settings_subtitle": "ギャラリービューアーに関する設定",
   "asset_viewer_settings_title": "アセットビューアー",
+  "automatic_endpoint_switching_subtitle": "指定されたWi-Fiに接続時のみローカル接続を行い、他のネットワーク下では通常通りの接続を行います",
+  "automatic_endpoint_switching_title": "自動URL切り替え",
+  "background_location_permission": "バックグラウンド位置情報アクセス",
+  "background_location_permission_content": "正常にWi-Fiの名前(SSID)を獲得するにはアプリが常に詳細な位置情報にアクセスできる必要があります",
   "backup_album_selection_page_albums_device": "端末上のアルバム数: {} ",
   "backup_album_selection_page_albums_tap": "タップで選択、ダブルタップで除外",
   "backup_album_selection_page_assets_scatter": "アルバムを選択・除外してバックアップする写真を選ぶ (同じ写真が複数のアルバムに登録されていることがあるため)",
@@ -81,7 +91,7 @@
   "backup_controller_page_background_app_refresh_disabled_title": "バックグラウンド更新はオフになっています",
   "backup_controller_page_background_app_refresh_enable_button_text": "設定を開く",
   "backup_controller_page_background_battery_info_link": "詳細",
-  "backup_controller_page_background_battery_info_message": "バックグラウンド処理を正常に動作させるためには、Immichに適用されているバッテリーの最適化や自動調整をオフにしてください。\n\nデバイスによって変更方法が異なります。",
+  "backup_controller_page_background_battery_info_message": "バックグラウンド処理を正常に動作させるためには、Immichアプリに適用されているバッテリーの最適化をオフにしてください。\n\nデバイスによって設定方法が異なりますので各々調べてください",
   "backup_controller_page_background_battery_info_ok": "了解",
   "backup_controller_page_background_battery_info_title": "バッテリーの最適化",
   "backup_controller_page_background_charging": "充電中のみ",
@@ -112,7 +122,7 @@
   "backup_controller_page_start_backup": "バックアップ開始",
   "backup_controller_page_status_off": "バックアップがオフになっています",
   "backup_controller_page_status_on": "バックアップがオンになっています",
-  "backup_controller_page_storage_format": "使用済み: {}/{}",
+  "backup_controller_page_storage_format": "使用済み({}) - 全体({})",
   "backup_controller_page_to_backup": "バックアップされるアルバム",
   "backup_controller_page_total": "合計",
   "backup_controller_page_total_sub": "選択されたアルバムの写真と動画の数",
@@ -127,12 +137,13 @@
   "backup_manual_success": "成功",
   "backup_manual_title": "アップロード状況",
   "backup_options_page_title": "バックアップオプション",
+  "backup_setting_subtitle": "アップロードに関する設定",
   "cache_settings_album_thumbnails": "ライブラリのサムネイル ({}枚)",
   "cache_settings_clear_cache_button": "キャッシュをクリア",
   "cache_settings_clear_cache_button_title": "キャッシュを削除 (キャッシュが再生成されるまで、アプリのパフォーマンスが著しく低下します)",
   "cache_settings_duplicated_assets_clear_button": "クリア",
-  "cache_settings_duplicated_assets_subtitle": "アプリがブラックリストに追加している項目",
-  "cache_settings_duplicated_assets_title": "{}項目が重複",
+  "cache_settings_duplicated_assets_subtitle": "サーバーにアップロード済みと認識された写真や動画の数",
+  "cache_settings_duplicated_assets_title": "{}項目の重複",
   "cache_settings_image_cache_size": "キャッシュのサイズ ({}枚) ",
   "cache_settings_statistics_album": "ライブラリのサムネイル",
   "cache_settings_statistics_assets": "{}枚 ({}枚中)",
@@ -145,26 +156,31 @@
   "cache_settings_tile_subtitle": "ローカルストレージの挙動を確認する",
   "cache_settings_tile_title": "ローカルストレージ",
   "cache_settings_title": "キャッシュの設定",
+  "cancel": "キャンセル",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "確定",
   "change_password_form_description": "{name}さん こんにちは\n\nサーバーにアクセスするのが初めてか、パスワードリセットのリクエストがされました。新しいパスワードを入力してください",
   "change_password_form_new_password": "新しいパスワード",
   "change_password_form_password_mismatch": "パスワードが一致しません",
   "change_password_form_reenter_new_password": "再度パスワードを入力してください",
-  "client_cert_dialog_msg_confirm": "OK",
-  "client_cert_enter_password": "Enter Password",
-  "client_cert_import": "Import",
-  "client_cert_import_success_msg": "Client certificate is imported",
-  "client_cert_invalid_msg": "Invalid certificate file or wrong password",
-  "client_cert_remove": "Remove",
-  "client_cert_remove_msg": "Client certificate is removed",
-  "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login",
-  "client_cert_title": "SSL Client Certificate",
+  "check_corrupt_asset_backup": "破損されている項目を探す",
+  "check_corrupt_asset_backup_button": "チェックを行う",
+  "check_corrupt_asset_backup_description": "写真や動画などが全てアップロードし終えてからWi-Fiに接続時のみチェックを行なってください。作業が完了するには数分かかる場合があります",
+  "client_cert_dialog_msg_confirm": "了解",
+  "client_cert_enter_password": "パスワードを入力",
+  "client_cert_import": "インポート",
+  "client_cert_import_success_msg": "クライアント証明書が導入されました",
+  "client_cert_invalid_msg": "パスワードが間違っているか証明書が無効です",
+  "client_cert_remove": "削除",
+  "client_cert_remove_msg": "クライアント証明書が削除されました",
+  "client_cert_subtitle": "PKCS12 (.p12 .pfx) フォーマットのみ対応されてます。証明書の導入や削除はログイン前のみ行えます",
+  "client_cert_title": "SSLクライアント証明書",
   "common_add_to_album": "アルバムに追加",
   "common_change_password": "パスワードを変更",
   "common_create_new_album": "アルバムを作成",
   "common_server_error": "ネットワーク接続を確認し、サーバーが接続できる状態にあるか確認してください。アプリとサーバーのバージョンが一致しているかも確認してください。",
   "common_shared": "共有済み",
-  "contextual_search": "Sunrise on the beach",
+  "contextual_search": "ビーチと朝日",
   "control_bottom_app_bar_add_to_album": "アルバムに追加",
   "control_bottom_app_bar_album_info": "{}枚",
   "control_bottom_app_bar_album_info_shared": "{}枚 · 共有済",
@@ -172,9 +188,9 @@
   "control_bottom_app_bar_create_new_album": "アルバムを作成",
   "control_bottom_app_bar_delete": "削除",
   "control_bottom_app_bar_delete_from_immich": "Immichから削除",
-  "control_bottom_app_bar_delete_from_local": "デバイスから削除",
-  "control_bottom_app_bar_download": "Download",
-  "control_bottom_app_bar_edit": "Edit",
+  "control_bottom_app_bar_delete_from_local": "端末上から削除",
+  "control_bottom_app_bar_download": "ダウンロード",
+  "control_bottom_app_bar_edit": "編集",
   "control_bottom_app_bar_edit_location": "位置情報を編集",
   "control_bottom_app_bar_edit_time": "日時を変更",
   "control_bottom_app_bar_favorite": "お気に入り",
@@ -185,20 +201,23 @@
   "control_bottom_app_bar_unarchive": "アーカイブを解除",
   "control_bottom_app_bar_unfavorite": "お気に入りから外す",
   "control_bottom_app_bar_upload": "アップロード",
+  "create_album": "アルバム作成",
   "create_album_page_untitled": "タイトルなし",
+  "create_new": "新規作成",
   "create_shared_album_page_create": "作成",
   "create_shared_album_page_share": "共有",
   "create_shared_album_page_share_add_assets": "写真を追加",
   "create_shared_album_page_share_select_photos": "写真を選択",
-  "crop": "Crop",
+  "crop": "クロップ",
   "curated_location_page_title": "撮影場所",
   "curated_object_page_title": "被写体",
-  "daily_title_text_date": "MM月 DD日, EE",
-  "daily_title_text_date_year": "yyyy年 MM月 DD日, EE",
-  "date_format": "MM月 DD日, EE • hh時mm分",
+  "current_server_address": "現在のサーバーURL",
+  "daily_title_text_date": "MM DD, EE",
+  "daily_title_text_date_year": "yyyy MM DD, EE",
+  "date_format": "MM DD, EE • hh:mm",
   "delete_dialog_alert": "サーバーとデバイスの両方から永久的に削除されます!",
   "delete_dialog_alert_local": "選択された項目はデバイスから削除されますが、Immichには残ります",
-  "delete_dialog_alert_local_non_backed_up": "Immichにバックアップされていない項目があります。デバイスからも永久に削除されます",
+  "delete_dialog_alert_local_non_backed_up": "選択された項目のうち、Immichにバックアップされていない物が含まれています。デバイスからも完全に削除されます。",
   "delete_dialog_alert_remote": "選択された項目はImmichから永久に削除されます",
   "delete_dialog_cancel": "キャンセル",
   "delete_dialog_ok": "削除",
@@ -210,37 +229,56 @@
   "delete_shared_link_dialog_title": "共有リンクを消す",
   "description_input_hint_text": "説明を追加",
   "description_input_submit_error": "説明の編集に失敗しました。詳細はログを確認してください。",
-  "download_error": "Download Error",
-  "download_started": "Download started",
-  "download_sucess": "Download success",
-  "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_canceled": "ダウンロードがキャンセルされました",
+  "download_complete": "ダウンロード完了",
+  "download_enqueue": "ダウンロード待機中",
+  "download_error": "ダウンロードエラー",
+  "download_failed": "ダウンロード失敗",
+  "download_filename": "ファイル名: {}",
+  "download_finished": "ダウンロード終了",
+  "downloading": "ダウンロード中...",
+  "downloading_media": "ダウンロード中",
+  "download_notfound": "ダウンロードが見つかりません",
+  "download_paused": "ダウンロード停止",
+  "download_started": "ダウンロード開始",
+  "download_sucess": "ダウンロード成功",
+  "download_sucess_android": "DCIM/Immichに保存されました",
+  "download_waiting_to_retry": "リトライ中",
   "edit_date_time_dialog_date_time": "日付と時間",
   "edit_date_time_dialog_timezone": "タイムゾーン",
-  "edit_image_title": "Edit",
+  "edit_image_title": "編集",
   "edit_location_dialog_title": "位置情報",
-  "error_saving_image": "Error: {}",
+  "enter_wifi_name": "Wi-Fiの名前(SSID)を入力",
+  "error_change_sort_album": "Failed to change album sort order",
+  "error_saving_image": "エラー: {}",
   "exif_bottom_sheet_description": "説明を追加",
   "exif_bottom_sheet_details": "詳細",
   "exif_bottom_sheet_location": "撮影場所",
   "exif_bottom_sheet_location_add": "位置情報を追加",
-  "exif_bottom_sheet_people": "ピープル",
+  "exif_bottom_sheet_people": "人物",
   "exif_bottom_sheet_person_add_person": "名前を追加",
   "experimental_settings_new_asset_list_subtitle": "製作途中 (WIP)",
   "experimental_settings_new_asset_list_title": "試験的なグリッドを有効化",
   "experimental_settings_subtitle": "試験的機能につき自己責任で!",
   "experimental_settings_title": "試験的機能",
+  "external_network": "外部のネットワーク",
+  "external_network_sheet_info": "指定されたWi-Fiに繋がっていない時アプリはサーバーへの接続を指定されたURLで行います。優先順位は上から下です",
+  "favorites": "お気に入り",
   "favorites_page_no_favorites": "お気に入り登録された写真またはビデオがありません",
   "favorites_page_title": "お気に入り",
-  "filename_search": "File name or extension",
+  "filename_search": "ファイル名、又は拡張子",
+  "filter": "フィルター",
+  "get_wifiname_error": "Wi-Fiの名前(SSID)が入手できませんでした。Wi-Fiに繋がってるのと必要な権限を許可したか確認してください",
+  "grant_permission": "許可する",
   "haptic_feedback_switch": "ハプティックフィードバック",
   "haptic_feedback_title": "ハプティックフィードバックを有効にする",
-  "header_settings_add_header_tip": "Add Header",
-  "header_settings_field_validator_msg": "Value cannot be empty",
-  "header_settings_header_name_input": "Header name",
-  "header_settings_header_value_input": "Header value",
-  "header_settings_page_title": "Proxy Headers",
-  "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
-  "headers_settings_tile_title": "Custom proxy headers",
+  "header_settings_add_header_tip": "ヘッダを追加",
+  "header_settings_field_validator_msg": "ヘッダを空白にはできません",
+  "header_settings_header_name_input": "ヘッダの名前",
+  "header_settings_header_value_input": "ヘッダのバリュー",
+  "header_settings_page_title": "プロキシヘッダ",
+  "headers_settings_tile_subtitle": "プロキシヘッダを設定する",
+  "headers_settings_tile_title": "カスタムプロキシヘッダ",
   "home_page_add_to_album_conflicts": "{album}に{added}枚写真を追加しました。追加済みの{failed}枚はスキップしました。",
   "home_page_add_to_album_err_local": "まだアップロードされてない項目は、アルバムに登録できません",
   "home_page_add_to_album_success": "{album}に{added}枚写真を追加しました",
@@ -249,19 +287,22 @@
   "home_page_archive_err_partner": "パートナーの写真はアーカイブできません。スキップします",
   "home_page_building_timeline": "タイムライン構築中",
   "home_page_delete_err_partner": "パートナーの写真は削除できません。スキップします",
-  "home_page_delete_remote_err_local": "リモート削除の選択にローカルなアイテムが含まれています。スキップします",
+  "home_page_delete_remote_err_local": "サーバー上のアイテムの削除の選択に端末上のアイテムが含まれているのでスキップします",
   "home_page_favorite_err_local": "まだアップロードされてない項目はお気に入り登録できません",
   "home_page_favorite_err_partner": "まだパートナーの写真をお気に入り登録できません。スキップします (アップデートをお待ちください)",
   "home_page_first_time_notice": "はじめてアプリを使う場合、タイムラインに写真を表示するためにアルバムを選択してください",
   "home_page_share_err_local": "ローカルのみの項目をリンクで共有はできません。スキップします",
   "home_page_upload_err_limit": "1回でアップロードできる写真の数は30枚です。スキップします",
-  "image_saved_successfully": "Image saved",
+  "ignore_icloud_photos": "iCloud上の写真をスキップ",
+  "ignore_icloud_photos_description": "iCloudに保存済みの項目をImmichサーバー上にアップロードしません",
+  "image_saved_successfully": "画像が保存されました",
   "image_viewer_page_state_provider_download_error": "ダウンロード失敗",
   "image_viewer_page_state_provider_download_started": "ダウンロードが始まります",
   "image_viewer_page_state_provider_download_success": "ダウンロード成功",
   "image_viewer_page_state_provider_share_error": "共有エラー",
-  "invalid_date": "Invalid date",
-  "invalid_date_format": "Invalid date format",
+  "invalid_date": "日付が無効です",
+  "invalid_date_format": "日付のフォーマットが無効です",
+  "library": "ライブラリ",
   "library_page_albums": "アルバム",
   "library_page_archive": "アーカイブ",
   "library_page_device_albums": "デバイス上のアルバム",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "一番古い項目",
   "library_page_sort_most_recent_photo": "最近の項目",
   "library_page_sort_title": "アルバム名",
+  "local_network": "ローカルネットワーク",
+  "local_network_sheet_info": "アプリは指定されたWi-Fiに繋がっている時サーバーへの接続を下記のURLで行います",
+  "location_permission": "位置情報権限",
+  "location_permission_content": "自動URL切り替えを使用するにはWi-Fiの名前(SSID)を取得する必要があり、正常に機能するにはアプリが常に詳細な位置情報にアクセスできる必要があります",
   "location_picker_choose_on_map": "マップを選択",
   "location_picker_latitude": "緯度",
   "location_picker_latitude_error": "有効な緯度を入力してください",
@@ -330,26 +375,30 @@
   "map_settings_include_show_partners": "パートナーを含める",
   "map_settings_only_relative_range": "日付",
   "map_settings_only_show_favorites": "お気に入りのみを表示",
-  "map_settings_theme_settings": "マップの見た目",
+  "map_settings_theme_settings": "地図の見た目",
   "map_zoom_to_see_photos": "写真を見るにはズームアウト",
   "memories_all_caught_up": "すべて確認済み",
   "memories_check_back_tomorrow": "明日もう一度確認してください",
   "memories_start_over": "始める",
   "memories_swipe_to_close": "上にスワイプして閉じる",
-  "memories_year_ago": "A year ago",
-  "memories_years_ago": "{} years ago",
-  "monthly_title_text_date_format": "yyyy年 MM月",
+  "memories_year_ago": "一年前",
+  "memories_years_ago": "{}年前",
+  "monthly_title_text_date_format": "yyyy MM",
   "motion_photos_page_title": "モーションフォト",
   "multiselect_grid_edit_date_time_err_read_only": "読み取り専用の項目の日付を変更できません",
   "multiselect_grid_edit_gps_err_read_only": "読み取り専用の項目の位置情報を変更できません",
+  "my_albums": "自分のアルバム",
+  "networking_settings": "ネットワーク",
+  "networking_subtitle": "サーバーエンドポイントに関する設定",
   "no_assets_to_show": "表示する項目がありません",
-  "no_name": "No name",
+  "no_name": "名前がありません",
   "notification_permission_dialog_cancel": "キャンセル",
   "notification_permission_dialog_content": "通知を許可するには設定を開いてオンにしてください",
   "notification_permission_dialog_settings": "設定",
   "notification_permission_list_tile_content": "通知の許可 をオンにしてください",
   "notification_permission_list_tile_enable_button": "通知をオンにする",
   "notification_permission_list_tile_title": "通知の許可",
+  "on_this_device": "デバイス上の項目",
   "partner_list_user_photos": "{user}さんの写真",
   "partner_list_view_all": "すべて見る",
   "partner_page_add_partner": "パートナーを追加",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{}は写真へアクセスできなくなります",
   "partner_page_stop_sharing_title": "写真の共有を無効化しますか?",
   "partner_page_title": "パートナー",
+  "partners": "パートナー",
+  "people": "人物",
   "permission_onboarding_back": "戻る",
   "permission_onboarding_continue_anyway": "無視して続行",
   "permission_onboarding_get_started": "はじめる",
@@ -371,53 +422,58 @@
   "permission_onboarding_permission_granted": "写真へのアクセスが許可されました",
   "permission_onboarding_permission_limited": "写真へのアクセスが制限されています。Immichが写真のバックアップと管理を行うには、システム設定から写真と動画のアクセス権限を変更してください。",
   "permission_onboarding_request": "Immichは写真へのアクセス許可が必要です",
+  "places": "場所",
+  "preferences_settings_subtitle": "アプリに関する設定",
   "preferences_settings_title": "設定",
   "profile_drawer_app_logs": "ログ",
   "profile_drawer_client_out_of_date_major": "アプリが更新されてません。最新のバージョンに更新してください",
   "profile_drawer_client_out_of_date_minor": "アプリが更新されてません。最新のバージョンに更新してください",
-  "profile_drawer_client_server_up_to_date": "すべて最新です",
-  "profile_drawer_documentation": "Immichのドキュメント",
+  "profile_drawer_client_server_up_to_date": "すべて最新版です",
+  "profile_drawer_documentation": "Immich公式サイト(英語のみ)",
   "profile_drawer_github": "GitHub",
   "profile_drawer_server_out_of_date_major": "サーバーが更新されてません。最新のバージョンに更新してください",
   "profile_drawer_server_out_of_date_minor": "サーバーが更新されてません。最新のバージョンに更新してください",
   "profile_drawer_settings": "設定",
   "profile_drawer_sign_out": "サインアウト",
   "profile_drawer_trash": "ゴミ箱",
+  "recently_added": "最近追加された項目",
   "recently_added_page_title": "最近",
-  "save_to_gallery": "Save to gallery",
+  "save": "保存",
+  "save_to_gallery": "ギャラリーに保存",
   "scaffold_body_error_occurred": "エラーが発生しました",
+  "search_albums": "アルバムを探す",
   "search_bar_hint": "写真を検索",
   "search_filter_apply": "フィルターを適用する",
-  "search_filter_camera": "Camera",
+  "search_filter_camera": "カメラ",
   "search_filter_camera_make": "メーカー",
   "search_filter_camera_model": "モデル",
-  "search_filter_camera_title": "Select camera type",
-  "search_filter_date": "Date",
-  "search_filter_date_interval": "{start} to {end}",
-  "search_filter_date_title": "Select a date range",
+  "search_filter_camera_title": "カメラの種類を選択",
+  "search_filter_date": "撮影日",
+  "search_filter_date_interval": "{start}から{end}まで",
+  "search_filter_date_title": "撮影期間を選択",
   "search_filter_display_option_archive": "アーカイブ",
   "search_filter_display_option_favorite": "お気に入り",
   "search_filter_display_option_not_in_album": "アルバムにありません",
-  "search_filter_display_options": "Display Options",
-  "search_filter_display_options_title": "Display options",
-  "search_filter_location": "Location",
+  "search_filter_display_options": "表示オプション",
+  "search_filter_display_options_title": "表示オプション",
+  "search_filter_location": "場所",
   "search_filter_location_city": "市町村",
   "search_filter_location_country": "国",
   "search_filter_location_state": "都道府県",
-  "search_filter_location_title": "Select location",
-  "search_filter_media_type": "Media Type",
+  "search_filter_location_title": "場所を選択",
+  "search_filter_media_type": "メディアの種類",
   "search_filter_media_type_all": "すべて",
   "search_filter_media_type_image": "写真",
-  "search_filter_media_type_title": "Select media type",
+  "search_filter_media_type_title": "メディアの種類を選択",
   "search_filter_media_type_video": "動画",
-  "search_filter_people": "People",
-  "search_filter_people_title": "Select people",
+  "search_filter_people": "人物",
+  "search_filter_people_title": "人物を選択",
   "search_page_categories": "カテゴリ",
   "search_page_favorites": "お気に入り",
   "search_page_motion_photos": "モーションフォト",
   "search_page_no_objects": "被写体に関するデータがなし",
   "search_page_no_places": "場所に関するデータなし",
-  "search_page_people": "ピープル",
+  "search_page_people": "人物",
   "search_page_person_add_name_dialog_cancel": "キャンセル",
   "search_page_person_add_name_dialog_hint": "名前",
   "search_page_person_add_name_dialog_save": "保存",
@@ -428,6 +484,7 @@
   "search_page_places": "撮影地",
   "search_page_recently_added": "最近追加",
   "search_page_screenshots": "スクリーンショット",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "自撮り",
   "search_page_things": "被写体",
   "search_page_videos": "ビデオ",
@@ -440,17 +497,19 @@
   "select_additional_user_for_sharing_page_suggestions": "ユーザーリスト",
   "select_user_for_sharing_page_err_album": "アルバム作成に失敗",
   "select_user_for_sharing_page_share_suggestions": "ユーザ一覧",
+  "server_endpoint": "サーバーエンドポイント",
   "server_info_box_app_version": "アプリのバージョン",
   "server_info_box_latest_release": "最新バージョン",
   "server_info_box_server_url": " サーバーのURL",
   "server_info_box_server_version": "サーバーのバージョン",
-  "setting_image_viewer_help": "写真をタップするとサムネイル・中画質(要設定)・オリジナル(要設定)の順に読み込みます",
-  "setting_image_viewer_original_subtitle": "オリジナルの画像を表示したいときにオンにしてください。(最大画質で表示されるので、モバイルデータとストレージの消費量が増えます)",
-  "setting_image_viewer_original_title": "オリジナル画像を読み込む",
-  "setting_image_viewer_preview_subtitle": "中画質の写真をロードしたいときにオンにしてください。直接最大画質の写真を表示したい場合はオフにしてください。(ロード中はサムネイルが代わりに表示されます)",
-  "setting_image_viewer_preview_title": "プレビュー画像をロードする",
+  "setting_image_viewer_help": "写真をタップするとサムネイル・中画質・オリジナルの順に読み込みます",
+  "setting_image_viewer_original_subtitle": "オリジナルの画像を表示したいときにオンにしてください。(最大画質で表示されるので、データと端末のストレージの消費量が増えます)",
+  "setting_image_viewer_original_title": "オリジナルを読み込む",
+  "setting_image_viewer_preview_subtitle": "中画質の写真をロードしたいときにオンにしてください。このステップをスキップして直接最大画質の写真を表示したい場合はオフにしてください。(ロード中はサムネイルが代わりに表示されます)",
+  "setting_image_viewer_preview_title": "プレビューを読み込む",
   "setting_image_viewer_title": "画像",
   "setting_languages_apply": "適用する",
+  "setting_languages_subtitle": "アプリの言語を変更する",
   "setting_languages_title": "言語",
   "setting_notifications_notify_failures_grace_period": "バックアップ失敗の通知: {}",
   "setting_notifications_notify_hours": "{}時間後",
@@ -459,14 +518,14 @@
   "setting_notifications_notify_never": "行わない",
   "setting_notifications_notify_seconds": "{}秒後",
   "setting_notifications_single_progress_subtitle": "アップロード中の写真の詳細",
-  "setting_notifications_single_progress_title": "実行中のバックアップの詳細を表示",
+  "setting_notifications_single_progress_title": "バックアップの詳細な進行状況を表示",
   "setting_notifications_subtitle": "通知設定を変更する",
   "setting_notifications_title": "通知",
   "setting_notifications_total_progress_subtitle": "アップロードの進行状況 (完了済み/全体枚数)",
-  "setting_notifications_total_progress_title": "実行中のバックアップの進行状況を表示",
+  "setting_notifications_total_progress_title": "全体のバックアップの進行状況を表示",
   "setting_pages_app_bar_settings": "設定",
   "settings_require_restart": "Immichを再起動して設定を適用してください",
-  "setting_video_viewer_looping_subtitle": "有効にするとディテールビューで自動で動画がループします",
+  "setting_video_viewer_looping_subtitle": "有効にすると詳細表示で動画がループします",
   "setting_video_viewer_looping_title": "ループ中",
   "setting_video_viewer_title": "ビデオ",
   "share_add": "追加",
@@ -480,11 +539,11 @@
   "shared_album_activity_remove_title": "アクティビティを削除します",
   "shared_album_activity_setting_subtitle": "他のユーザーの返信を許可する",
   "shared_album_activity_setting_title": "お気に入りとコメント",
-  "shared_album_section_people_action_error": "アルバムからの退出に失敗",
+  "shared_album_section_people_action_error": "退出に失敗",
   "shared_album_section_people_action_leave": "ユーザーをアルバムから退出",
   "shared_album_section_people_action_remove_user": "ユーザーをアルバムから退出",
-  "shared_album_section_people_owner_label": "オーナー",
-  "shared_album_section_people_title": "ピープル",
+  "shared_album_section_people_owner_label": "アルバム作成者",
+  "shared_album_section_people_title": "人物",
   "share_dialog_preparing": "準備中",
   "shared_link_app_bar_title": "共有リンク",
   "shared_link_clipboard_copied_massage": "クリップボードにコピーしました",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "アップロード",
   "shared_link_manage_links": "共有済みのリンクを管理",
   "shared_link_public_album": "公開アルバム",
+  "shared_links": "共有済みリンク",
   "share_done": "完了",
+  "shared_with_me": "自分と共有中",
   "share_invite": "アルバムに招待",
   "sharing_page_album": "共有アルバム",
   "sharing_page_description": "共有アルバムを作成して同じネットワークにいる人たちに写真を共有",
@@ -539,31 +600,32 @@
   "sharing_silver_appbar_create_shared_album": "共有アルバムを作成",
   "sharing_silver_appbar_shared_links": "共有リンク",
   "sharing_silver_appbar_share_partner": "パートナーと共有",
-  "sync": "Sync",
-  "sync_albums": "Sync albums",
-  "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
-  "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
+  "sync": "同期",
+  "sync_albums": "アルバムを同期",
+  "sync_albums_manual_subtitle": "アップロード済みの全ての写真や動画を選択されたバックアップアルバムに同期する",
+  "sync_upload_album_setting_subtitle": "サーバー上のアルバムの内容を端末上のアルバムと同期します (サーバーにアルバムが無い場合自動で作成されます。また、アップロードされていない写真や動画は同期されません)",
   "tab_controller_nav_library": "ライブラリ",
   "tab_controller_nav_photos": "写真",
   "tab_controller_nav_search": "検索",
   "tab_controller_nav_sharing": "共有",
   "theme_setting_asset_list_storage_indicator_title": "ストレージに関する情報を表示",
-  "theme_setting_asset_list_tiles_per_row_title": "一列ごとの枚数: {}",
-  "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.",
-  "theme_setting_colorful_interface_title": "Colorful interface",
+  "theme_setting_asset_list_tiles_per_row_title": "一列ごとの表示枚数: {}",
+  "theme_setting_colorful_interface_subtitle": "アクセントカラーを背景にも使用する",
+  "theme_setting_colorful_interface_title": "カラフルなUI",
   "theme_setting_dark_mode_switch": "ダークモード",
   "theme_setting_image_viewer_quality_subtitle": "画像ビューの画質の設定",
   "theme_setting_image_viewer_quality_title": "画像ビュー",
-  "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.",
-  "theme_setting_primary_color_title": "Primary color",
-  "theme_setting_system_primary_color_title": "Use system color",
+  "theme_setting_primary_color_subtitle": "アクセント用の色を選択",
+  "theme_setting_primary_color_title": "アクセントカラー",
+  "theme_setting_system_primary_color_title": "端末で設定されている色を使う",
   "theme_setting_system_theme_switch": "自動 (デバイスの設定を反映)",
   "theme_setting_theme_subtitle": "テーマ設定",
   "theme_setting_theme_title": "テーマ",
   "theme_setting_three_stage_loading_subtitle": "三段階読み込みを有効にすると、パフォーマンスが改善する可能性がありますが、ネットワーク負荷が著しく増加します。",
   "theme_setting_three_stage_loading_title": "三段階読み込みをオンにする",
   "translated_text_options": "オプション",
-  "trash_emptied": "Emptied trash",
+  "trash": "ゴミ箱",
+  "trash_emptied": "ゴミ箱を空にしました",
   "trash_page_delete": "削除",
   "trash_page_delete_all": "すべて削除",
   "trash_page_empty_trash_btn": "コミ箱を空にする",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "選択した項目のバックアップをしますか?",
   "upload_dialog_ok": "アップロード",
   "upload_dialog_title": "アップロード",
+  "use_current_connection": "現在の接続情報を使用",
+  "validate_endpoint_error": "有効なURLを入力してください",
   "version_announcement_overlay_ack": "了解",
   "version_announcement_overlay_release_notes": "更新情報",
-  "version_announcement_overlay_text_1": "こんにちは!新しい",
+  "version_announcement_overlay_text_1": "新しい",
   "version_announcement_overlay_text_2": "のバージョンが公開中です。",
-  "version_announcement_overlay_text_3": "を確認してみてください。docker-composeや.envファイルが最新の状態に更新されているか、特にWatchTowerなどのツールを使ってDockerイメージを自動アップデートしてる人は確認してください。",
-  "version_announcement_overlay_title": "サーバーの新バージョンリリース\uD83C\uDF89",
+  "version_announcement_overlay_text_3": "を確認してください。docker-composeや.envファイルが最新の状態に更新済みか、特にWatchTowerなどのツールを使ってDockerイメージを自動アップデートされてる方は確認してください。",
+  "version_announcement_overlay_title": "サーバーの最新版が公開中\uD83C\uDF89",
+  "videos": "動画",
   "viewer_remove_from_stack": "スタックから外す",
   "viewer_stack_use_as_main_asset": "メインの画像として使用する",
-  "viewer_unstack": "スタックを解除"
+  "viewer_unstack": "スタックを解除",
+  "wifi_name": "Wi-Fiの名前(SSID)",
+  "your_wifi_name": "Wi-Fiの名前(SSID)"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/ko-KR.json b/mobile/assets/i18n/ko-KR.json
index e6da75c2f6..788368b131 100644
--- a/mobile/assets/i18n/ko-KR.json
+++ b/mobile/assets/i18n/ko-KR.json
@@ -6,6 +6,8 @@
   "action_common_save": "저장",
   "action_common_select": "선택",
   "action_common_update": "업데이트",
+  "add_a_name": "이름 추가",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "{album}에 추가되었습니다.",
   "add_to_album_bottom_sheet_already_exists": "{album}에 이미 존재하는 항목입니다.",
   "advanced_settings_log_level_title": "로그 레벨: {}",
@@ -21,26 +23,29 @@
   "advanced_settings_troubleshooting_title": "문제 해결",
   "album_info_card_backup_album_excluded": "제외됨",
   "album_info_card_backup_album_included": "선택됨",
-  "album_thumbnail_card_item": "1개 항목",
-  "album_thumbnail_card_items": "{}개 항목",
+  "albums": "앨범",
+  "album_thumbnail_card_item": "항목 1개",
+  "album_thumbnail_card_items": "항목 {}개",
   "album_thumbnail_card_shared": " · 공유됨",
   "album_thumbnail_owned": "소유함",
   "album_thumbnail_shared_by": "{}님이 공유함",
   "album_viewer_appbar_delete_confirm": "이 앨범을 삭제하시겠습니까?",
   "album_viewer_appbar_share_delete": "앨범 삭제",
-  "album_viewer_appbar_share_err_delete": "앨범을 삭제하지 못했습니다.",
-  "album_viewer_appbar_share_err_leave": "앨범에서 나가지 못했습니다.",
+  "album_viewer_appbar_share_err_delete": "앨범 삭제에 실패했습니다.",
+  "album_viewer_appbar_share_err_leave": "앨범 나가기에 실패했습니다.",
   "album_viewer_appbar_share_err_remove": "앨범에서 항목을 제거하지 못했습니다.",
-  "album_viewer_appbar_share_err_title": "앨범명을 변경하지 못했습니다.",
+  "album_viewer_appbar_share_err_title": "앨범명 변경에 실패했습니다.",
   "album_viewer_appbar_share_leave": "앨범 나가기",
   "album_viewer_appbar_share_remove": "앨범에서 제거",
   "album_viewer_appbar_share_to": "공유 대상",
   "album_viewer_page_share_add_users": "사용자 추가",
+  "all": "모두",
   "all_people_page_title": "인물",
   "all_videos_page_title": "동영상",
   "app_bar_signout_dialog_content": "정말 로그아웃하시겠습니까?",
   "app_bar_signout_dialog_ok": "네",
   "app_bar_signout_dialog_title": "로그아웃",
+  "archived": "보관함",
   "archive_page_no_archived_assets": "보관된 항목 없음",
   "archive_page_title": "보관함 ({})",
   "asset_action_delete_err_read_only": "읽기 전용 항목은 삭제할 수 없습니다. 건너뜁니다.",
@@ -56,12 +61,17 @@
   "asset_list_settings_title": "사진 배열",
   "asset_restored_successfully": "항목이 성공적으로 복원되었습니다.",
   "assets_deleted_permanently": "{}개 항목이 영구적으로 삭제됨",
-  "assets_deleted_permanently_from_server": "{}개 항목이 Immich 서버에서 영구적으로 삭제됨",
-  "assets_removed_permanently_from_device": "{}개 항목이 기기에서 영구적으로 삭제됨",
+  "assets_deleted_permanently_from_server": "Immich에서 항목 {}개가 영구적으로 삭제됨",
+  "assets_removed_permanently_from_device": "기기에서 항목 {}개가 영구적으로 삭제됨",
   "assets_restored_successfully": "항목 {}개를 복원했습니다.",
-  "assets_trashed": "휴지통으로 {}개 항목이 이동되었습니다.",
-  "assets_trashed_from_server": "휴지통으로 Immich 서버의 {}개 항목이 이동되었습니다.",
+  "assets_trashed": "휴지통으로 항목 {}개가 이동되었습니다.",
+  "assets_trashed_from_server": "휴지통으로 Immich 항목 {}개가 이동되었습니다.",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "보기 옵션",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "기기의 앨범 ({})",
   "backup_album_selection_page_albums_tap": "한 번 눌러 선택, 두 번 눌러 제외하세요.",
   "backup_album_selection_page_assets_scatter": "각 항목은 여러 앨범에 포함될 수 있으며, 백업 진행 중에도 대상 앨범을 포함하거나 제외할 수 있습니다.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "성공",
   "backup_manual_title": "업로드 상태",
   "backup_options_page_title": "백업 옵션",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "라이브러리 섬네일 ({})",
   "cache_settings_clear_cache_button": "캐시 지우기",
   "cache_settings_clear_cache_button_title": "앱 캐시를 지웁니다. 이 작업은 캐시가 다시 생성될 때까지 앱 성능에 상당한 영향을 미칠 수 있습니다.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "로컬 스토리지 동작 제어",
   "cache_settings_tile_title": "로컬 스토리지",
   "cache_settings_title": "캐시 설정",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "현재 비밀번호 입력",
   "change_password_form_description": "안녕하세요 {name}님,\n\n첫 로그인이거나, 비밀번호가 초기화되어 비밀번호를 설정해야 합니다. 아래에 새 비밀번호를 입력해주세요.",
   "change_password_form_new_password": "새 비밀번호 입력",
   "change_password_form_password_mismatch": "비밀번호가 일치하지 않습니다.",
   "change_password_form_reenter_new_password": "새 비밀번호 확인",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "확인",
   "client_cert_enter_password": "비밀번호 입력",
   "client_cert_import": "가져오기",
@@ -166,8 +182,8 @@
   "common_shared": "공유됨",
   "contextual_search": "동해안에서 맞이하는 새해 일출",
   "control_bottom_app_bar_add_to_album": "앨범에 추가",
-  "control_bottom_app_bar_album_info": "{}개 항목",
-  "control_bottom_app_bar_album_info_shared": "{}개 항목 · 공유됨",
+  "control_bottom_app_bar_album_info": "항목 {}개",
+  "control_bottom_app_bar_album_info_shared": "항목 {}개 · 공유됨",
   "control_bottom_app_bar_archive": "보관",
   "control_bottom_app_bar_create_new_album": "앨범 생성",
   "control_bottom_app_bar_delete": "삭제",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "보관 해제",
   "control_bottom_app_bar_unfavorite": "즐겨찾기 해제",
   "control_bottom_app_bar_upload": "업로드",
+  "create_album": "앨범 생성",
   "create_album_page_untitled": "제목 없음",
+  "create_new": "새로 만들기",
   "create_shared_album_page_create": "생성",
   "create_shared_album_page_share": "공유",
   "create_shared_album_page_share_add_assets": "항목 추가",
@@ -193,13 +211,14 @@
   "crop": "자르기",
   "curated_location_page_title": "장소",
   "curated_object_page_title": "사물",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "M월 d일 EEEE",
   "daily_title_text_date_year": "yyyy년 M월 d일 EEEE",
   "date_format": "yyyy년 M월 d일 EEEE • a h:mm",
-  "delete_dialog_alert": "선택한 항목이 Immich 및 기기에서 영구적으로 삭제됩니다.",
-  "delete_dialog_alert_local": "선택한 항목이 이 기기에서 영구적으로 삭제됩니다. Immich 서버에서는 계속 사용할 수 있습니다.",
-  "delete_dialog_alert_local_non_backed_up": "일부 항목은 Immich에 백업되지 않으며 기기에서 영구적으로 삭제됩니다.",
-  "delete_dialog_alert_remote": "선택한 항목이 Immich 서버에서 영구적으로 삭제됩니다.",
+  "delete_dialog_alert": "이 항목이 Immich 및 기기에서 영구적으로 삭제됩니다.",
+  "delete_dialog_alert_local": "이 항목이 기기에서 영구적으로 삭제됩니다. Immich에서는 삭제되지 않습니다.",
+  "delete_dialog_alert_local_non_backed_up": "일부 항목이 백업되지 않았습니다. 백업되지 않은 항목이 기기에서 영구적으로 삭제됩니다.",
+  "delete_dialog_alert_remote": "이 항목이 Immich에서 영구적으로 삭제됩니다.",
   "delete_dialog_cancel": "취소",
   "delete_dialog_ok": "삭제",
   "delete_dialog_ok_force": "무시하고 삭제",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "공유 링크 삭제",
   "description_input_hint_text": "설명 추가...",
   "description_input_submit_error": "설명을 변경하는 중 문제가 발생했습니다. 자세한 내용은 로그를 참조하세요.",
+  "download_canceled": "다운로드가 취소되었습니다.",
+  "download_complete": "다은로드가 완료되었습니다.",
+  "download_enqueue": "대기열에 다운로드",
   "download_error": "다운로드 중 문제가 발생했습니다.",
+  "download_failed": "다운로드에 실패하였습니다.",
+  "download_filename": "파일: {}",
+  "download_finished": "다운로드가 완료되었습니다.",
+  "downloading": "다운로드 중...",
+  "downloading_media": "미디어 다운로드 중",
+  "download_notfound": "다운로드할 수 없음",
+  "download_paused": "다운로드 일시 중지됨",
   "download_started": "다운로드가 시작되었습니다.",
   "download_sucess": "다운로드가 완료되었습니다.",
   "download_sucess_android": "미디어가 DCIM/Immich에 저장되었습니다.",
+  "download_waiting_to_retry": "재시도 대기 중",
   "edit_date_time_dialog_date_time": "날짜 및 시간",
   "edit_date_time_dialog_timezone": "시간대",
   "edit_image_title": "편집",
   "edit_location_dialog_title": "위치",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "오류: {}",
   "exif_bottom_sheet_description": "설명 추가...",
   "exif_bottom_sheet_details": "상세 정보",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "새 사진 배열 사용 (실험적)",
   "experimental_settings_subtitle": "본인 책임 하에 사용하세요!",
   "experimental_settings_title": "실험적",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "즐겨찾기",
   "favorites_page_no_favorites": "즐겨찾기된 항목 없음",
   "favorites_page_title": "즐겨찾기",
   "filename_search": "파일 이름 또는 확장자",
+  "filter": "필터",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "햅틱 피드백 활성화",
   "haptic_feedback_title": "햅틱 피드백",
   "header_settings_add_header_tip": "헤더 추가",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "앱을 처음 사용하는 경우 타임라인에 앨범의 사진과 동영상을 채울 수 있도록 백업할 앨범을 선택하세요.",
   "home_page_share_err_local": "기기의 항목은 링크로 공유할 수 없습니다. 건너뜁니다.",
   "home_page_upload_err_limit": "한 번에 최대 30개의 항목만 업로드할 수 있습니다.",
+  "ignore_icloud_photos": "iCloud 사진 제외",
+  "ignore_icloud_photos_description": "iCloud에 저장된 사진은 Immich 서버에 업로드되지 않습니다.",
   "image_saved_successfully": "이미지가 저장되었습니다.",
   "image_viewer_page_state_provider_download_error": "다운로드 오류",
   "image_viewer_page_state_provider_download_started": "다운로드가 시작되었습니다.",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "공유 오류",
   "invalid_date": "잘못된 날짜입니다.",
   "invalid_date_format": "잘못된 날짜 형식입니다.",
+  "library": "라이브러리",
   "library_page_albums": "앨범",
   "library_page_archive": "보관함",
   "library_page_device_albums": "기기의 앨범",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "오래된 순",
   "library_page_sort_most_recent_photo": "최신순",
   "library_page_sort_title": "앨범 제목",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "지도에서 선택",
   "location_picker_latitude": "위도",
   "location_picker_latitude_error": "유효한 위도를 입력하세요.",
@@ -291,8 +336,8 @@
   "login_form_err_http": "http:// 또는 https://로 시작해야 합니다.",
   "login_form_err_invalid_email": "유효하지 않은 이메일",
   "login_form_err_invalid_url": "잘못된 URL입니다.",
-  "login_form_err_leading_whitespace": "시작 부분에 공백이 있습니다.",
-  "login_form_err_trailing_whitespace": "끝 부분에 공백이 있습니다.",
+  "login_form_err_leading_whitespace": "문자 시작에 공백이 있습니다.",
+  "login_form_err_trailing_whitespace": "문자 끝에 공백이 있습니다.",
   "login_form_failed_get_oauth_server_config": "OAuth 로그인 중 문제 발생, 서버 URL을 확인하세요.",
   "login_form_failed_get_oauth_server_disable": "이 서버는 OAuth 기능을 지원하지 않습니다.",
   "login_form_failed_login": "로그인 오류. 서버 URL, 이메일 및 비밀번호를 확인하세요.",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "모션 포토",
   "multiselect_grid_edit_date_time_err_read_only": "읽기 전용 항목의 날짜는 변경할 수 없습니다. 건너뜁니다.",
   "multiselect_grid_edit_gps_err_read_only": "읽기 전용 항목의 위치는 변경할 수 없습니다. 건너뜁니다.",
+  "my_albums": "내 앨범",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "표시할 항목 없음",
   "no_name": "이름 없음",
   "notification_permission_dialog_cancel": "취소",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "알림을 활성화하려면 권한을 부여하세요.",
   "notification_permission_list_tile_enable_button": "알림 활성화",
   "notification_permission_list_tile_title": "알림 권한",
+  "on_this_device": "이 장치에서",
   "partner_list_user_photos": "{user}님의 사진",
   "partner_list_view_all": "모두 보기",
   "partner_page_add_partner": "파트너 추가",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "더 이상 {}님이 사진에 접근할 수 없습니다.",
   "partner_page_stop_sharing_title": "공유를 중단하시겠습니까?",
   "partner_page_title": "파트너",
+  "partners": "파트너",
+  "people": "인물",
   "permission_onboarding_back": "뒤로",
   "permission_onboarding_continue_anyway": "무시하고 진행",
   "permission_onboarding_get_started": "시작하기",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "권한이 부여되었습니다! 준비가 완료되었습니다.",
   "permission_onboarding_permission_limited": "권한이 없습니다. Immich가 전체 갤러리 컬렉션을 백업하고 관리할 수 있도록 하려면 설정에서 사진 및 동영상 권한을 부여하세요.",
   "permission_onboarding_request": "사진 및 동영상 권한이 필요합니다.",
+  "places": "장소",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "설정",
   "profile_drawer_app_logs": "로그",
   "profile_drawer_client_out_of_date_major": "모바일 앱이 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "설정",
   "profile_drawer_sign_out": "로그아웃",
   "profile_drawer_trash": "휴지통",
+  "recently_added": "최근 추가",
   "recently_added_page_title": "최근 추가",
+  "save": "Save",
   "save_to_gallery": "갤러리에 저장",
   "scaffold_body_error_occurred": "문제가 발생했습니다.",
+  "search_albums": "앨범 검색",
   "search_bar_hint": "사진 검색",
   "search_filter_apply": "필터 적용",
   "search_filter_camera": "카메라",
@@ -428,6 +484,7 @@
   "search_page_places": "장소",
   "search_page_recently_added": "최근 추가",
   "search_page_screenshots": "스크린샷",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "셀피",
   "search_page_things": "사물",
   "search_page_videos": "동영상",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "추천",
   "select_user_for_sharing_page_err_album": "앨범을 생성하지 못했습니다.",
   "select_user_for_sharing_page_share_suggestions": "제안",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "앱 버전",
   "server_info_box_latest_release": "최신 버전",
   "server_info_box_server_url": "서버 URL",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "미리 보기 이미지 불러오기",
   "setting_image_viewer_title": "이미지",
   "setting_languages_apply": "적용",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "언어",
   "setting_notifications_notify_failures_grace_period": "백그라운드 백업 실패 알림: {}",
   "setting_notifications_notify_hours": "{}시간 후",
@@ -471,7 +530,7 @@
   "setting_video_viewer_title": "동영상",
   "share_add": "추가",
   "share_add_photos": "사진 추가",
-  "share_add_title": "앨범명 추가",
+  "share_add_title": "제목 추가",
   "share_assets_selected": "{}개 항목 선택됨",
   "share_create_album": "앨범 생성",
   "shared_album_activities_input_disable": "댓글이 비활성화되었습니다",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "업로드",
   "shared_link_manage_links": "공유 링크 관리",
   "shared_link_public_album": "공개 앨범",
+  "shared_links": "공유 링크",
   "share_done": "완료",
+  "shared_with_me": "나와 공유됨",
   "share_invite": "앨범으로 초대",
   "sharing_page_album": "공유 앨범",
   "sharing_page_description": "공유 앨범을 만들어 주변 사람들과 사진 및 동영상을 공유하세요.",
@@ -542,7 +603,7 @@
   "sync": "동기화",
   "sync_albums": "앨범 동기화",
   "sync_albums_manual_subtitle": "업로드한 모든 동영상과 사진을 선택한 백업 앨범에 동기화",
-  "sync_upload_album_setting_subtitle": "선택한 앨범을 Immich에 생성하고 사진 및 동영상을 업로드하세요.",
+  "sync_upload_album_setting_subtitle": "선택한 앨범을 Immich에 생성하고 사진 및 동영상 업로드",
   "tab_controller_nav_library": "라이브러리",
   "tab_controller_nav_photos": "사진",
   "tab_controller_nav_search": "검색",
@@ -563,11 +624,12 @@
   "theme_setting_three_stage_loading_subtitle": "이 기능은 앱의 로드 성능을 향상시킬 수 있지만 더 많은 데이터를 사용합니다.",
   "theme_setting_three_stage_loading_title": "3단계 로드 활성화",
   "translated_text_options": "옵션",
-  "trash_emptied": "휴지통을 비움",
+  "trash": "휴지통",
+  "trash_emptied": "휴지통을 비웠습니다.",
   "trash_page_delete": "삭제",
   "trash_page_delete_all": "모두 삭제",
   "trash_page_empty_trash_btn": "휴지통 비우기",
-  "trash_page_empty_trash_dialog_content": "휴지통을 비우시겠습니까? 휴지통에 있는 항목이 Immich에서 영구적으로 제거됩니다.",
+  "trash_page_empty_trash_dialog_content": "휴지통을 비우시겠습니까? 휴지통에 있는 모든 항목이 Immich에서 영구적으로 제거됩니다.",
   "trash_page_empty_trash_dialog_ok": "확인",
   "trash_page_info": "휴지통으로 이동된 항목은 {}일 후 영구적으로 삭제됩니다.",
   "trash_page_no_assets": "휴지통이 비어 있음",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "선택한 항목을 서버에 백업하시겠습니까?",
   "upload_dialog_ok": "업로드",
   "upload_dialog_title": "항목 업로드",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "확인",
   "version_announcement_overlay_release_notes": "릴리스 노트",
   "version_announcement_overlay_text_1": "안녕하세요,",
   "version_announcement_overlay_text_2": "새 버전의 Immich를 사용할 수 있습니다.",
   "version_announcement_overlay_text_3": "WatchTower 등의 자동 업데이트 기능을 사용하는 경우 의도하지 않은 동작을 방지하기 위해 docker-compose.yml 및 .env 구성이 최신인지 확인하세요.",
   "version_announcement_overlay_title": "새 서버 버전 사용 가능 \uD83C\uDF89",
+  "videos": "동영상",
   "viewer_remove_from_stack": "스택에서 제거",
   "viewer_stack_use_as_main_asset": "대표 사진으로 설정",
-  "viewer_unstack": "스택 해제"
+  "viewer_unstack": "스택 해제",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/lt-LT.json b/mobile/assets/i18n/lt-LT.json
index 324c9069fd..a7f31f8440 100644
--- a/mobile/assets/i18n/lt-LT.json
+++ b/mobile/assets/i18n/lt-LT.json
@@ -6,6 +6,8 @@
   "action_common_save": "Save",
   "action_common_select": "Select",
   "action_common_update": "Update",
+  "add_a_name": "Add a name",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Added to {album}",
   "add_to_album_bottom_sheet_already_exists": "Already in {album}",
   "advanced_settings_log_level_title": "Log level: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Troubleshooting",
   "album_info_card_backup_album_excluded": "EXCLUDED",
   "album_info_card_backup_album_included": "INCLUDED",
+  "albums": "Albums",
   "album_thumbnail_card_item": "1 item",
   "album_thumbnail_card_items": "{} items",
   "album_thumbnail_card_shared": " · Shared",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Remove from album",
   "album_viewer_appbar_share_to": "Share To",
   "album_viewer_page_share_add_users": "Add users",
+  "all": "All",
   "all_people_page_title": "People",
   "all_videos_page_title": "Videos",
   "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
   "app_bar_signout_dialog_ok": "Yes",
   "app_bar_signout_dialog_title": "Sign out",
+  "archived": "Archived",
   "archive_page_no_archived_assets": "No archived assets found",
   "archive_page_title": "Archive ({})",
   "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} asset(s) restored successfully",
   "assets_trashed": "{} asset(s) trashed",
   "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "Asset Viewer",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Albums on device ({})",
   "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
   "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Success",
   "backup_manual_title": "Upload status",
   "backup_options_page_title": "Backup options",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
   "cache_settings_clear_cache_button": "Clear cache",
   "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Control the local storage behaviour",
   "cache_settings_tile_title": "Local Storage",
   "cache_settings_title": "Caching Settings",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Confirm Password",
   "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
   "change_password_form_new_password": "New Password",
   "change_password_form_password_mismatch": "Passwords do not match",
   "change_password_form_reenter_new_password": "Re-enter New Password",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Enter Password",
   "client_cert_import": "Import",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Unarchive",
   "control_bottom_app_bar_unfavorite": "Unfavorite",
   "control_bottom_app_bar_upload": "Upload",
+  "create_album": "Create album",
   "create_album_page_untitled": "Untitled",
+  "create_new": "CREATE NEW",
   "create_shared_album_page_create": "Create",
   "create_shared_album_page_share": "Share",
   "create_shared_album_page_share_add_assets": "ADD ASSETS",
@@ -193,6 +211,7 @@
   "crop": "Crop",
   "curated_location_page_title": "Places",
   "curated_object_page_title": "Things",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "Delete Shared Link",
   "description_input_hint_text": "Add description...",
   "description_input_submit_error": "Error updating description, check the log for more details",
+  "download_canceled": "Download canceled",
+  "download_complete": "Download complete",
+  "download_enqueue": "Download enqueued",
   "download_error": "Download Error",
+  "download_failed": "Download failed",
+  "download_filename": "file: {}",
+  "download_finished": "Download finished",
+  "downloading": "Downloading...",
+  "downloading_media": "Downloading media",
+  "download_notfound": "Download not found",
+  "download_paused": "Download paused",
   "download_started": "Download started",
   "download_sucess": "Download success",
   "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_waiting_to_retry": "Waiting to retry",
   "edit_date_time_dialog_date_time": "Date and Time",
   "edit_date_time_dialog_timezone": "Timezone",
   "edit_image_title": "Edit",
   "edit_location_dialog_title": "Location",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Error: {}",
   "exif_bottom_sheet_description": "Add Description...",
   "exif_bottom_sheet_details": "DETAILS",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Enable experimental photo grid",
   "experimental_settings_subtitle": "Use at your own risk!",
   "experimental_settings_title": "Experimental",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favorites",
   "favorites_page_no_favorites": "No favorite assets found",
   "favorites_page_title": "Favorites",
   "filename_search": "File name or extension",
+  "filter": "Filter",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Enable haptic feedback",
   "haptic_feedback_title": "Haptic Feedback",
   "header_settings_add_header_tip": "Add Header",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
   "home_page_share_err_local": "Can not share local assets via link, skipping",
   "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
+  "ignore_icloud_photos": "Ignore iCloud photos",
+  "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
   "image_saved_successfully": "Image saved",
   "image_viewer_page_state_provider_download_error": "Download Error",
   "image_viewer_page_state_provider_download_started": "Download Started",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "Share Error",
   "invalid_date": "Invalid date",
   "invalid_date_format": "Invalid date format",
+  "library": "Library",
   "library_page_albums": "Albums",
   "library_page_archive": "Archive",
   "library_page_device_albums": "Albums on Device",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Oldest photo",
   "library_page_sort_most_recent_photo": "Most recent photo",
   "library_page_sort_title": "Album title",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Choose on map",
   "location_picker_latitude": "Latitude",
   "location_picker_latitude_error": "Enter a valid latitude",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Motion Photos",
   "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
   "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
+  "my_albums": "My albums",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "No assets to show",
   "no_name": "No name",
   "notification_permission_dialog_cancel": "Cancel",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Grant permission to enable notifications.",
   "notification_permission_list_tile_enable_button": "Enable Notifications",
   "notification_permission_list_tile_title": "Notification Permission",
+  "on_this_device": "On this device",
   "partner_list_user_photos": "{user}'s photos",
   "partner_list_view_all": "View all",
   "partner_page_add_partner": "Add partner",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
   "partner_page_stop_sharing_title": "Stop sharing your photos?",
   "partner_page_title": "Partner",
+  "partners": "Partners",
+  "people": "People",
   "permission_onboarding_back": "Back",
   "permission_onboarding_continue_anyway": "Continue anyway",
   "permission_onboarding_get_started": "Get started",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Permission granted! You are all set.",
   "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
   "permission_onboarding_request": "Immich requires permission to view your photos and videos.",
+  "places": "Places",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Preferences",
   "profile_drawer_app_logs": "Logs",
   "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Settings",
   "profile_drawer_sign_out": "Sign Out",
   "profile_drawer_trash": "Trash",
+  "recently_added": "Recently added",
   "recently_added_page_title": "Recently Added",
+  "save": "Save",
   "save_to_gallery": "Save to gallery",
   "scaffold_body_error_occurred": "Error occurred",
+  "search_albums": "Search albums",
   "search_bar_hint": "Search your photos",
   "search_filter_apply": "Apply filter",
   "search_filter_camera": "Camera",
@@ -428,6 +484,7 @@
   "search_page_places": "Places",
   "search_page_recently_added": "Recently added",
   "search_page_screenshots": "Screenshots",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Selfies",
   "search_page_things": "Things",
   "search_page_videos": "Videos",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Suggestions",
   "select_user_for_sharing_page_err_album": "Failed to create album",
   "select_user_for_sharing_page_share_suggestions": "Suggestions",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "App Version",
   "server_info_box_latest_release": "Latest Version",
   "server_info_box_server_url": "Server URL",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Load preview image",
   "setting_image_viewer_title": "Images",
   "setting_languages_apply": "Apply",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Languages",
   "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
   "setting_notifications_notify_hours": "{} hours",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Upload",
   "shared_link_manage_links": "Manage Shared links",
   "shared_link_public_album": "Public album",
+  "shared_links": "Shared links",
   "share_done": "Done",
+  "shared_with_me": "Shared with me",
   "share_invite": "Invite to album",
   "sharing_page_album": "Shared albums",
   "sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
   "theme_setting_three_stage_loading_title": "Enable three-stage loading",
   "translated_text_options": "Options",
+  "trash": "Trash",
   "trash_emptied": "Emptied trash",
   "trash_page_delete": "Delete",
   "trash_page_delete_all": "Delete All",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
   "upload_dialog_ok": "Upload",
   "upload_dialog_title": "Upload Asset",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Acknowledge",
   "version_announcement_overlay_release_notes": "release notes",
   "version_announcement_overlay_text_1": "Hi friend, there is a new release of",
   "version_announcement_overlay_text_2": "please take your time to visit the ",
   "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
   "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
+  "videos": "Videos",
   "viewer_remove_from_stack": "Remove from Stack",
   "viewer_stack_use_as_main_asset": "Use as Main Asset",
-  "viewer_unstack": "Un-Stack"
+  "viewer_unstack": "Un-Stack",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/lv-LV.json b/mobile/assets/i18n/lv-LV.json
index c9f86535fc..fb9eaac8bf 100644
--- a/mobile/assets/i18n/lv-LV.json
+++ b/mobile/assets/i18n/lv-LV.json
@@ -6,6 +6,8 @@
   "action_common_save": "Save",
   "action_common_select": "Select",
   "action_common_update": "Atjaunināt",
+  "add_a_name": "Add a name",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Pievienots {album}",
   "add_to_album_bottom_sheet_already_exists": "Jau pievienots {album}",
   "advanced_settings_log_level_title": "Žurnalēšanas līmenis: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Problēmas novēršana",
   "album_info_card_backup_album_excluded": "NEIEKĻAUTS",
   "album_info_card_backup_album_included": "IEKĻAUTS",
+  "albums": "Albums",
   "album_thumbnail_card_item": "1 vienums",
   "album_thumbnail_card_items": "{} vienumi",
   "album_thumbnail_card_shared": "· Koplietots",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Noņemt no albuma",
   "album_viewer_appbar_share_to": "Kopīgot Uz",
   "album_viewer_page_share_add_users": "Pievienot lietotājus",
+  "all": "All",
   "all_people_page_title": "Cilvēki",
   "all_videos_page_title": "Videoklipi",
   "app_bar_signout_dialog_content": "Vai tiešām vēlaties izrakstīties?",
   "app_bar_signout_dialog_ok": "Jā",
   "app_bar_signout_dialog_title": "Izrakstīties",
+  "archived": "Archived",
   "archive_page_no_archived_assets": "Nav atrasts neviens arhivēts aktīvs",
   "archive_page_title": "Arhīvs ({})",
   "asset_action_delete_err_read_only": "Nevar dzēst read only aktīvu(-s), notiek izlaišana",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} asset(s) restored successfully",
   "assets_trashed": "{} asset(s) trashed",
   "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "Aktīvu Skatītājs",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Albumi ierīcē ({})",
   "backup_album_selection_page_albums_tap": "Pieskarieties, lai iekļautu, veiciet dubultskārienu, lai izslēgtu",
   "backup_album_selection_page_assets_scatter": "Aktīvi var būt izmētāti pa vairākiem albumiem. Tādējādi dublēšanas procesā albumus var iekļaut vai neiekļaut.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Veiksmīgi",
   "backup_manual_title": "Augšupielādes statuss",
   "backup_options_page_title": "Dublēšanas iestatījumi",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Bibliotēkas lapu sīktēli ({} aktīvi)",
   "cache_settings_clear_cache_button": "Iztīrīt kešatmiņu",
   "cache_settings_clear_cache_button_title": "Iztīra aplikācijas kešatmiņu. Tas būtiski ietekmēs lietotnes veiktspēju, līdz kešatmiņa būs pārbūvēta.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Kontrolēt lokālās krātuves uzvedību",
   "cache_settings_tile_title": "Lokālā Krātuve",
   "cache_settings_title": "Kešdarbes iestatījumi",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Apstiprināt Paroli",
   "change_password_form_description": "Sveiki {name},\n\nŠī ir pirmā reize, kad pierakstāties sistēmā, vai arī ir iesniegts pieprasījums mainīt paroli. Lūdzu, zemāk ievadiet jauno paroli.",
   "change_password_form_new_password": "Jauna Parole",
   "change_password_form_password_mismatch": "Paroles nesakrīt",
   "change_password_form_reenter_new_password": "Atkārtoti ievadīt jaunu paroli",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Enter Password",
   "client_cert_import": "Import",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Atarhivēt",
   "control_bottom_app_bar_unfavorite": "Noņemt no Izlases",
   "control_bottom_app_bar_upload": "Augšupielādēt",
+  "create_album": "Create album",
   "create_album_page_untitled": "Bez nosaukuma",
+  "create_new": "CREATE NEW",
   "create_shared_album_page_create": "Izveidot",
   "create_shared_album_page_share": "Kopīgot",
   "create_shared_album_page_share_add_assets": "PIEVIENOT AKTĪVUS",
@@ -193,6 +211,7 @@
   "crop": "Crop",
   "curated_location_page_title": "Vietas",
   "curated_object_page_title": "Lietas",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, gggg",
   "date_format": "E, LLL d, g • h:mm a",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "Dzēst Kopīgošanas saiti",
   "description_input_hint_text": "Pievienot aprakstu...",
   "description_input_submit_error": "Atjauninot aprakstu, radās kļūda; papildinformāciju skatiet žurnālā",
+  "download_canceled": "Download canceled",
+  "download_complete": "Download complete",
+  "download_enqueue": "Download enqueued",
   "download_error": "Download Error",
+  "download_failed": "Download failed",
+  "download_filename": "file: {}",
+  "download_finished": "Download finished",
+  "downloading": "Downloading...",
+  "downloading_media": "Downloading media",
+  "download_notfound": "Download not found",
+  "download_paused": "Download paused",
   "download_started": "Download started",
   "download_sucess": "Download success",
   "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_waiting_to_retry": "Waiting to retry",
   "edit_date_time_dialog_date_time": "Datums un Laiks",
   "edit_date_time_dialog_timezone": "Laika zona",
   "edit_image_title": "Edit",
   "edit_location_dialog_title": "Atrašanās vieta",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Error: {}",
   "exif_bottom_sheet_description": "Pievienot Aprakstu...",
   "exif_bottom_sheet_details": "INFORMĀCIJA",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Iespējot eksperimentālo fotorežģi",
   "experimental_settings_subtitle": "Izmanto uzņemoties risku!",
   "experimental_settings_title": "Eksperimentāls",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favorites",
   "favorites_page_no_favorites": "Nav atrasti iecienītākie aktīvi",
   "favorites_page_title": "Izlase",
   "filename_search": "File name or extension",
+  "filter": "Filter",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Iestatīt haptisku reakciju",
   "haptic_feedback_title": "Haptiska Reakcija",
   "header_settings_add_header_tip": "Add Header",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "Ja šī ir pirmā reize, kad izmantojat aplikāciju, lūdzu, izvēlieties dublējuma albumu(s), lai laika skala varētu aizpildīt fotoattēlus un videoklipus albumā(os).",
   "home_page_share_err_local": "Caur saiti nevarēja kopīgot lokālos aktīvus, notiek izlaišana",
   "home_page_upload_err_limit": "Vienlaikus var augšupielādēt ne vairāk kā 30 aktīvus, notiek izlaišana",
+  "ignore_icloud_photos": "Ignore iCloud photos",
+  "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
   "image_saved_successfully": "Image saved",
   "image_viewer_page_state_provider_download_error": "Lejupielādes Kļūda",
   "image_viewer_page_state_provider_download_started": "Lejupielāde Uzsākta",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "Kopīgošanas Kļūda",
   "invalid_date": "Invalid date",
   "invalid_date_format": "Invalid date format",
+  "library": "Library",
   "library_page_albums": "Albums",
   "library_page_archive": "Arhīvs",
   "library_page_device_albums": "Albumi ierīcē",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Vecākais fotoattēls",
   "library_page_sort_most_recent_photo": "Jaunākais fotoattēls",
   "library_page_sort_title": "Albuma virsraksts",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Izvēlēties uz kartes",
   "location_picker_latitude": "Ģeogrāfiskais platums",
   "location_picker_latitude_error": "Ievadiet korektu ģeogrāfisko platumu",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Kustību Fotoattēli",
   "multiselect_grid_edit_date_time_err_read_only": "Nevar rediģēt read only aktīva(-u) datumu, notiek izlaišana",
   "multiselect_grid_edit_gps_err_read_only": "Nevar rediģēt atrašanās vietu read only aktīva(-u) datumu, notiek izlaišana",
+  "my_albums": "My albums",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "Nav uzrādāmo aktīvu",
   "no_name": "No name",
   "notification_permission_dialog_cancel": "Atcelt",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Piešķirt atļauju, lai iespējotu paziņojumus.",
   "notification_permission_list_tile_enable_button": "Iespējot Paziņojumus",
   "notification_permission_list_tile_title": "Paziņojumu Atļaujas",
+  "on_this_device": "On this device",
   "partner_list_user_photos": "{user} fotoattēli",
   "partner_list_view_all": "Apskatīt visu",
   "partner_page_add_partner": "Pievienot partneri",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} vairs nevarēs piekļūt jūsu fotoattēliem.",
   "partner_page_stop_sharing_title": "Beigt kopīgot jūsu fotogrāfijas?",
   "partner_page_title": "Partneris",
+  "partners": "Partners",
+  "people": "People",
   "permission_onboarding_back": "Atpakaļ",
   "permission_onboarding_continue_anyway": "Tomēr turpināt",
   "permission_onboarding_get_started": "Darba sākšana",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Atļauja piešķirta! Jūs esat gatavi darbam.",
   "permission_onboarding_permission_limited": "Atļauja ierobežota. Lai atļautu Immich dublēšanu un varētu pārvaldīt visu galeriju kolekciju, sadaļā Iestatījumi piešķiriet fotoattēlu un video atļaujas.",
   "permission_onboarding_request": "Immich nepieciešama atļauja skatīt jūsu fotoattēlus un videoklipus.",
+  "places": "Places",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Iestatījumi",
   "profile_drawer_app_logs": "Žurnāli",
   "profile_drawer_client_out_of_date_major": "Mobilā Aplikācija ir novecojusi. Lūdzu atjaunojiet to uz jaunāko lielo versiju",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Iestatījumi",
   "profile_drawer_sign_out": "Izrakstīties",
   "profile_drawer_trash": "Atkritne",
+  "recently_added": "Recently added",
   "recently_added_page_title": "Nesen Pievienotais",
+  "save": "Save",
   "save_to_gallery": "Save to gallery",
   "scaffold_body_error_occurred": "Radās kļūda",
+  "search_albums": "Search albums",
   "search_bar_hint": "Meklēt Jūsu fotoattēlus",
   "search_filter_apply": "Lietot filtru",
   "search_filter_camera": "Camera",
@@ -428,6 +484,7 @@
   "search_page_places": "Vietas",
   "search_page_recently_added": "Nesen Pievienotais",
   "search_page_screenshots": "Ekrānuzņēmumi",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Selfiji",
   "search_page_things": "Lietas",
   "search_page_videos": "Videoklipi",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Ieteikumi",
   "select_user_for_sharing_page_err_album": "Neizdevās izveidot albumu",
   "select_user_for_sharing_page_share_suggestions": "Ieteikumi",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "Aplikācijas Versija",
   "server_info_box_latest_release": "Jaunākā Versija",
   "server_info_box_server_url": "Servera URL",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Ielādēt priekšskatījuma attēlu",
   "setting_image_viewer_title": "Attēli",
   "setting_languages_apply": "Lietot",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Valodas",
   "setting_notifications_notify_failures_grace_period": "Paziņot par fona dublēšanas kļūmēm: {}",
   "setting_notifications_notify_hours": "{} stundas",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Augšupielādēt",
   "shared_link_manage_links": "Pārvaldīt Kopīgotās saites",
   "shared_link_public_album": "Publisks albums",
+  "shared_links": "Shared links",
   "share_done": "Gatavs",
+  "shared_with_me": "Shared with me",
   "share_invite": "Uzaicināt albumā",
   "sharing_page_album": "Kopīgotie albumi",
   "sharing_page_description": "Izveidojiet koplietojamus albumus, lai kopīgotu fotoattēlus un videoklipus ar Jūsu tīkla lietotājiem.",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Trīspakāpju ielāde var palielināt ielādēšanas veiktspēju, bet izraisa ievērojami lielāku tīkla noslodzi",
   "theme_setting_three_stage_loading_title": "Iespējot trīspakāpju ielādi",
   "translated_text_options": "Iestatījumi",
+  "trash": "Trash",
   "trash_emptied": "Emptied trash",
   "trash_page_delete": "Dzēst",
   "trash_page_delete_all": "Dzēst Visu",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Vai vēlaties veikt izvēlētā(-o) aktīva(-u) dublējumu uz servera?",
   "upload_dialog_ok": "Augšupielādēt",
   "upload_dialog_title": "Augšupielādēt Aktīvu",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Atzīt",
   "version_announcement_overlay_release_notes": "informācija par laidienu",
   "version_announcement_overlay_text_1": "Sveiks draugs, ir jauns izlaidums no",
   "version_announcement_overlay_text_2": "lūdzu, veltiet laiku, lai apmeklētu",
   "version_announcement_overlay_text_3": " un pārliecinieties, vai docker-compose un .env iestatījumi ir atjaunināti, lai novērstu jebkādas nepareizas konfigurācijas, īpaši, ja izmantojat WatchTower vai mehānismu, kas automātiski veic servera lietojumprogrammas atjaunināšanu.",
   "version_announcement_overlay_title": "Pieejama jauna servera versija \uD83C\uDF89",
+  "videos": "Videos",
   "viewer_remove_from_stack": "Noņemt no Steka",
   "viewer_stack_use_as_main_asset": "Izmantot kā Galveno Aktīvu",
-  "viewer_unstack": "At-Stekot"
+  "viewer_unstack": "At-Stekot",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/mn-MN.json b/mobile/assets/i18n/mn-MN.json
index 54697af5da..a2128380e0 100644
--- a/mobile/assets/i18n/mn-MN.json
+++ b/mobile/assets/i18n/mn-MN.json
@@ -6,6 +6,8 @@
   "action_common_save": "Save",
   "action_common_select": "Select",
   "action_common_update": "Update",
+  "add_a_name": "Add a name",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Added to {album}",
   "add_to_album_bottom_sheet_already_exists": "Already in {album}",
   "advanced_settings_log_level_title": "Log level: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Troubleshooting",
   "album_info_card_backup_album_excluded": "EXCLUDED",
   "album_info_card_backup_album_included": "INCLUDED",
+  "albums": "Albums",
   "album_thumbnail_card_item": "1 item",
   "album_thumbnail_card_items": "{} items",
   "album_thumbnail_card_shared": " · Shared",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Remove from album",
   "album_viewer_appbar_share_to": "Share To",
   "album_viewer_page_share_add_users": "Add users",
+  "all": "All",
   "all_people_page_title": "People",
   "all_videos_page_title": "Videos",
   "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
   "app_bar_signout_dialog_ok": "Yes",
   "app_bar_signout_dialog_title": "Sign out",
+  "archived": "Archived",
   "archive_page_no_archived_assets": "No archived assets found",
   "archive_page_title": "Archive ({})",
   "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} asset(s) restored successfully",
   "assets_trashed": "{} asset(s) trashed",
   "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "Asset Viewer",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Albums on device ({})",
   "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
   "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Success",
   "backup_manual_title": "Upload status",
   "backup_options_page_title": "Backup options",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
   "cache_settings_clear_cache_button": "Clear cache",
   "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Control the local storage behaviour",
   "cache_settings_tile_title": "Local Storage",
   "cache_settings_title": "Caching Settings",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Confirm Password",
   "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
   "change_password_form_new_password": "New Password",
   "change_password_form_password_mismatch": "Passwords do not match",
   "change_password_form_reenter_new_password": "Re-enter New Password",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Enter Password",
   "client_cert_import": "Import",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Unarchive",
   "control_bottom_app_bar_unfavorite": "Unfavorite",
   "control_bottom_app_bar_upload": "Upload",
+  "create_album": "Create album",
   "create_album_page_untitled": "Untitled",
+  "create_new": "CREATE NEW",
   "create_shared_album_page_create": "Create",
   "create_shared_album_page_share": "Share",
   "create_shared_album_page_share_add_assets": "ADD ASSETS",
@@ -193,6 +211,7 @@
   "crop": "Crop",
   "curated_location_page_title": "Places",
   "curated_object_page_title": "Things",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "Delete Shared Link",
   "description_input_hint_text": "Add description...",
   "description_input_submit_error": "Error updating description, check the log for more details",
+  "download_canceled": "Download canceled",
+  "download_complete": "Download complete",
+  "download_enqueue": "Download enqueued",
   "download_error": "Download Error",
+  "download_failed": "Download failed",
+  "download_filename": "file: {}",
+  "download_finished": "Download finished",
+  "downloading": "Downloading...",
+  "downloading_media": "Downloading media",
+  "download_notfound": "Download not found",
+  "download_paused": "Download paused",
   "download_started": "Download started",
   "download_sucess": "Download success",
   "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_waiting_to_retry": "Waiting to retry",
   "edit_date_time_dialog_date_time": "Date and Time",
   "edit_date_time_dialog_timezone": "Timezone",
   "edit_image_title": "Edit",
   "edit_location_dialog_title": "Location",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Error: {}",
   "exif_bottom_sheet_description": "Add Description...",
   "exif_bottom_sheet_details": "DETAILS",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Enable experimental photo grid",
   "experimental_settings_subtitle": "Use at your own risk!",
   "experimental_settings_title": "Experimental",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favorites",
   "favorites_page_no_favorites": "No favorite assets found",
   "favorites_page_title": "Favorites",
   "filename_search": "File name or extension",
+  "filter": "Filter",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Enable haptic feedback",
   "haptic_feedback_title": "Haptic Feedback",
   "header_settings_add_header_tip": "Add Header",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
   "home_page_share_err_local": "Can not share local assets via link, skipping",
   "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
+  "ignore_icloud_photos": "Ignore iCloud photos",
+  "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
   "image_saved_successfully": "Image saved",
   "image_viewer_page_state_provider_download_error": "Download Error",
   "image_viewer_page_state_provider_download_started": "Download Started",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "Share Error",
   "invalid_date": "Invalid date",
   "invalid_date_format": "Invalid date format",
+  "library": "Library",
   "library_page_albums": "Albums",
   "library_page_archive": "Archive",
   "library_page_device_albums": "Albums on Device",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Oldest photo",
   "library_page_sort_most_recent_photo": "Most recent photo",
   "library_page_sort_title": "Album title",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Choose on map",
   "location_picker_latitude": "Latitude",
   "location_picker_latitude_error": "Enter a valid latitude",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Motion Photos",
   "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
   "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
+  "my_albums": "My albums",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "No assets to show",
   "no_name": "No name",
   "notification_permission_dialog_cancel": "Цуцлах",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Мэдэгдэл нээх эрх өгнө үү.\n",
   "notification_permission_list_tile_enable_button": "Мэдэгдэл нээх",
   "notification_permission_list_tile_title": "Мэдэгдлийн эрх",
+  "on_this_device": "On this device",
   "partner_list_user_photos": "{user}'s photos",
   "partner_list_view_all": "View all",
   "partner_page_add_partner": "Add partner",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
   "partner_page_stop_sharing_title": "Stop sharing your photos?",
   "partner_page_title": "Partner",
+  "partners": "Partners",
+  "people": "People",
   "permission_onboarding_back": "Back",
   "permission_onboarding_continue_anyway": "Continue anyway",
   "permission_onboarding_get_started": "Get started",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Permission granted! You are all set.",
   "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
   "permission_onboarding_request": "Immich requires permission to view your photos and videos.",
+  "places": "Places",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Preferences",
   "profile_drawer_app_logs": "Logs",
   "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Settings",
   "profile_drawer_sign_out": "Sign Out",
   "profile_drawer_trash": "Trash",
+  "recently_added": "Recently added",
   "recently_added_page_title": "Recently Added",
+  "save": "Save",
   "save_to_gallery": "Save to gallery",
   "scaffold_body_error_occurred": "Error occurred",
+  "search_albums": "Search albums",
   "search_bar_hint": "Search your photos",
   "search_filter_apply": "Apply filter",
   "search_filter_camera": "Camera",
@@ -428,6 +484,7 @@
   "search_page_places": "Places",
   "search_page_recently_added": "Recently added",
   "search_page_screenshots": "Screenshots",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Selfies",
   "search_page_things": "Things",
   "search_page_videos": "Videos",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Suggestions",
   "select_user_for_sharing_page_err_album": "Failed to create album",
   "select_user_for_sharing_page_share_suggestions": "Suggestions",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "App Version",
   "server_info_box_latest_release": "Latest Version",
   "server_info_box_server_url": "Server URL",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Load preview image",
   "setting_image_viewer_title": "Images",
   "setting_languages_apply": "Apply",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Languages",
   "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
   "setting_notifications_notify_hours": "{} hours",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Upload",
   "shared_link_manage_links": "Manage Shared links",
   "shared_link_public_album": "Public album",
+  "shared_links": "Shared links",
   "share_done": "Done",
+  "shared_with_me": "Shared with me",
   "share_invite": "Invite to album",
   "sharing_page_album": "Shared albums",
   "sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
   "theme_setting_three_stage_loading_title": "Enable three-stage loading",
   "translated_text_options": "Options",
+  "trash": "Trash",
   "trash_emptied": "Emptied trash",
   "trash_page_delete": "Delete",
   "trash_page_delete_all": "Delete All",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
   "upload_dialog_ok": "Upload",
   "upload_dialog_title": "Upload Asset",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Acknowledge",
   "version_announcement_overlay_release_notes": "release notes",
   "version_announcement_overlay_text_1": "Hi friend, there is a new release of",
   "version_announcement_overlay_text_2": "please take your time to visit the ",
   "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
   "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
+  "videos": "Videos",
   "viewer_remove_from_stack": "Remove from Stack",
   "viewer_stack_use_as_main_asset": "Use as Main Asset",
-  "viewer_unstack": "Un-Stack"
-}
+  "viewer_unstack": "Un-Stack",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
+}
\ No newline at end of file
diff --git a/mobile/assets/i18n/nb-NO.json b/mobile/assets/i18n/nb-NO.json
index 7141faef72..8dce0381e8 100644
--- a/mobile/assets/i18n/nb-NO.json
+++ b/mobile/assets/i18n/nb-NO.json
@@ -6,6 +6,8 @@
   "action_common_save": "Lagre",
   "action_common_select": "Velg",
   "action_common_update": "Oppdater",
+  "add_a_name": "Legg til navn",
+  "add_endpoint": "API endepunkt",
   "add_to_album_bottom_sheet_added": "Lagt til i {album}",
   "add_to_album_bottom_sheet_already_exists": "Allerede i {album}",
   "advanced_settings_log_level_title": "Loggnivå: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Feilsøking",
   "album_info_card_backup_album_excluded": "EKSKLUDERT",
   "album_info_card_backup_album_included": "INKLUDERT",
+  "albums": "Albumer",
   "album_thumbnail_card_item": "1 objekt",
   "album_thumbnail_card_items": "{} objekter",
   "album_thumbnail_card_shared": " · Delt",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Fjern fra album",
   "album_viewer_appbar_share_to": "Del til",
   "album_viewer_page_share_add_users": "Legg til brukere",
+  "all": "Alt",
   "all_people_page_title": "Folk",
   "all_videos_page_title": "Videoer",
   "app_bar_signout_dialog_content": "Er du sikker på at du vil logge ut?",
   "app_bar_signout_dialog_ok": "Ja",
   "app_bar_signout_dialog_title": "Logg ut",
+  "archived": "Arkivert",
   "archive_page_no_archived_assets": "Ingen arkiverte objekter funnet",
   "archive_page_title": "Arkiv ({})",
   "asset_action_delete_err_read_only": "Kan ikke slette objekt(er) med kun lese-rettighet, hopper over",
@@ -54,14 +59,19 @@
   "asset_list_layout_sub_title": "Fordeling",
   "asset_list_settings_subtitle": "Innstillinger for layout av fotorutenett",
   "asset_list_settings_title": "Fotorutenett",
-  "asset_restored_successfully": "Asset restored successfully",
-  "assets_deleted_permanently": "{} asset(s) deleted permanently",
-  "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server",
-  "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device",
-  "assets_restored_successfully": "{} asset(s) restored successfully",
-  "assets_trashed": "{} asset(s) trashed",
-  "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_restored_successfully": "{} objekt(er) Gjenopprettet",
+  "assets_deleted_permanently": "{} objekt(er) Slettet permanent",
+  "assets_deleted_permanently_from_server": "{} objekt(er) slettet permanent fra Immich serveren",
+  "assets_removed_permanently_from_device": "{} objekt(er) slettet permanent fra enheten din",
+  "assets_restored_successfully": "{} objekt(er) gjenopprettet",
+  "assets_trashed": "{} objekt(er) slettet",
+  "assets_trashed_from_server": "{} objekt(er) slettet fra Immich serveren",
+  "asset_viewer_settings_subtitle": "Endre dine visningsinnstillinger for galleriet",
   "asset_viewer_settings_title": "Objektviser",
+  "automatic_endpoint_switching_subtitle": "Koble til lokalt over angitt Wi-Fi når det er tilgjengelig, og bruk alternative tilkoblinger andre steder",
+  "automatic_endpoint_switching_title": "Automatisk URL bytte",
+  "background_location_permission": "Bakgrunnstillatelse for plassering",
+  "background_location_permission_content": "For å bytte nettverk når du kjører i bakgrunnen, må Immich *alltid* ha presis posisjonstilgang slik at appen kan lese Wi-Fi-nettverkets navn",
   "backup_album_selection_page_albums_device": "Album på enhet ({})",
   "backup_album_selection_page_albums_tap": "Trykk for å inkludere, dobbelttrykk for å ekskludere",
   "backup_album_selection_page_assets_scatter": "Objekter kan bli spredd over flere album. Album kan derfor bli inkludert eller ekskludert under sikkerhetskopieringen.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Vellykket",
   "backup_manual_title": "Opplastingsstatus",
   "backup_options_page_title": "Backupinnstillinger",
+  "backup_setting_subtitle": "Administrer opplastingsinnstillinger for bakgrunn og forgrunn",
   "cache_settings_album_thumbnails": "Bibliotekminiatyrbilder ({} objekter)",
   "cache_settings_clear_cache_button": "Tøm buffer",
   "cache_settings_clear_cache_button_title": "Tømmer app-ens buffer. Dette vil ha betydelig innvirkning på appens ytelse inntil bufferen er gjenoppbygd.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Kontroller lokal lagring",
   "cache_settings_tile_title": "Lokal lagring",
   "cache_settings_title": "Bufringsinnstillinger",
+  "cancel": "Avbryt",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Bekreft passord",
   "change_password_form_description": "Hei {name}!\n\nDette er enten første gang du logger på systemet, eller det er sendt en forespørsel om å endre passordet ditt. Vennligst skriv inn det nye passordet nedenfor.",
   "change_password_form_new_password": "Nytt passord",
   "change_password_form_password_mismatch": "Passordene stemmer ikke",
   "change_password_form_reenter_new_password": "Skriv nytt passord igjen",
+  "check_corrupt_asset_backup": "Sjekk etter korrupte backupobjekter",
+  "check_corrupt_asset_backup_button": "Utfør sjekk",
+  "check_corrupt_asset_backup_description": "Kjør denne sjekken kun over Wi-Fi og når alle objekter har blitt lastet opp.\nDenne sjekken kan ta noen minutter.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Skriv inn passord",
   "client_cert_import": "Importer",
@@ -173,7 +189,7 @@
   "control_bottom_app_bar_delete": "Slett",
   "control_bottom_app_bar_delete_from_immich": "Slett fra Immich",
   "control_bottom_app_bar_delete_from_local": "Slett fra enhet",
-  "control_bottom_app_bar_download": "Download",
+  "control_bottom_app_bar_download": "Last ned",
   "control_bottom_app_bar_edit": "Endre",
   "control_bottom_app_bar_edit_location": "Endre lokasjon",
   "control_bottom_app_bar_edit_time": "Endre Dato og tid",
@@ -185,14 +201,17 @@
   "control_bottom_app_bar_unarchive": "Fjern fra arkiv",
   "control_bottom_app_bar_unfavorite": "Fjern favoritt",
   "control_bottom_app_bar_upload": "Last opp",
+  "create_album": "Opprett album",
   "create_album_page_untitled": "Uten navn",
+  "create_new": "LAG NY",
   "create_shared_album_page_create": "Opprett",
   "create_shared_album_page_share": "Del",
   "create_shared_album_page_share_add_assets": "LEGG TIL OBJEKTER",
   "create_shared_album_page_share_select_photos": "Velg bilder",
-  "crop": "Crop",
+  "crop": "Beskjær",
   "curated_location_page_title": "Plasseringer",
   "curated_object_page_title": "Ting",
+  "current_server_address": "Nåværende serveradresse",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,15 +229,28 @@
   "delete_shared_link_dialog_title": "Slett delt link",
   "description_input_hint_text": "Legg til beskrivelse ...",
   "description_input_submit_error": "Feil ved oppdatering av beskrivelse, sjekk loggen for flere detaljer",
-  "download_error": "Download Error",
-  "download_started": "Download started",
-  "download_sucess": "Download success",
-  "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_canceled": "Nedlasting avbrutt",
+  "download_complete": "Nedlasting fullført",
+  "download_enqueue": "Nedlasting satt i kø",
+  "download_error": "Nedlasting feilet",
+  "download_failed": "Nedlasting feilet",
+  "download_filename": "fil: {}",
+  "download_finished": "Nedlasting fullført",
+  "downloading": "Laster ned...",
+  "downloading_media": "Laster ned media",
+  "download_notfound": "Nedlasting ikke funnet",
+  "download_paused": "Nedlasting pauset",
+  "download_started": "Nedlasting startet",
+  "download_sucess": "Nedlasting vellykket",
+  "download_sucess_android": "Objektet har blitt lastet ned til DCIM/Immich",
+  "download_waiting_to_retry": "Venter på nytt forsøk",
   "edit_date_time_dialog_date_time": "Dato og tid",
   "edit_date_time_dialog_timezone": "Tidssone",
-  "edit_image_title": "Edit",
+  "edit_image_title": "Endre",
   "edit_location_dialog_title": "Lokasjon",
-  "error_saving_image": "Error: {}",
+  "enter_wifi_name": "Skriv inn Wi-Fi navn",
+  "error_change_sort_album": "Failed to change album sort order",
+  "error_saving_image": "Feil: {}",
   "exif_bottom_sheet_description": "Legg til beskrivelse ...",
   "exif_bottom_sheet_details": "DETALJER",
   "exif_bottom_sheet_location": "PLASSERING",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Aktiver eksperimentell rutenettsvisning",
   "experimental_settings_subtitle": "Bruk på egen risiko!",
   "experimental_settings_title": "Eksperimentelt",
+  "external_network": "Eksternt nettverk",
+  "external_network_sheet_info": "Når du ikke er på det foretrukne Wi-Fi-nettverket, vil appen koble seg til serveren via den første av URL-ene nedenfor den kan nå, fra topp til bunn",
+  "favorites": "Favoritter",
   "favorites_page_no_favorites": "Ingen favorittobjekter funnet",
   "favorites_page_title": "Favoritter",
   "filename_search": "Filnavn eller filtype",
+  "filter": "Filter",
+  "get_wifiname_error": "Kunne ikke hente Wi-Fi-navnet. Sørg for at du har gitt de nødvendige tillatelsene og er koblet til et Wi-Fi-nettverk",
+  "grant_permission": "Gi tillatelse",
   "haptic_feedback_switch": "Aktivert haptisk tilbakemelding",
   "haptic_feedback_title": "Haptisk tilbakemelding",
   "header_settings_add_header_tip": "Legg til header",
@@ -255,13 +293,16 @@
   "home_page_first_time_notice": "Hvis dette er første gangen du benytter appen, velg et album (eller flere) for sikkerhetskopiering, slik at tidslinjen kan fylles med dine bilder og videoer.",
   "home_page_share_err_local": "Kan ikke dele lokale objekter via link, hopper over",
   "home_page_upload_err_limit": "Maksimalt 30 objekter kan lastes opp om gangen, hopper over",
-  "image_saved_successfully": "Image saved",
+  "ignore_icloud_photos": "Ignorer iCloud bilder",
+  "ignore_icloud_photos_description": "Bilder som er lagret på iCloud vil ikke lastes opp til Immich",
+  "image_saved_successfully": "Bilde lagret",
   "image_viewer_page_state_provider_download_error": "Nedlasting feilet",
   "image_viewer_page_state_provider_download_started": "Nedlasting startet",
   "image_viewer_page_state_provider_download_success": "Nedlasting vellykket",
   "image_viewer_page_state_provider_share_error": "Delingsfeil",
   "invalid_date": "Ugyldig dato",
   "invalid_date_format": "Ugyldig datoformat",
+  "library": "Bibliotek",
   "library_page_albums": "Albumer",
   "library_page_archive": "Arkiv",
   "library_page_device_albums": "Albumer på enheten",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Eldste bilde",
   "library_page_sort_most_recent_photo": "Siste bilde",
   "library_page_sort_title": "Albumtittel",
+  "local_network": "Lokalt nettverk",
+  "local_network_sheet_info": "Appen vil koble til serveren via denne URL-en når du bruker det angitte Wi-Fi-nettverket",
+  "location_permission": "Stedstillatelse",
+  "location_permission_content": "For å bruke funksjonen for automatisk veksling trenger Immich nøyaktig plasseringstillatelse slik at den kan lese navnet på det gjeldende Wi-Fi-nettverket",
   "location_picker_choose_on_map": "Velg på kart",
   "location_picker_latitude": "Breddegrad",
   "location_picker_latitude_error": "Skriv inn en gyldig bredddegrad",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Bevegelige bilder",
   "multiselect_grid_edit_date_time_err_read_only": "Kan ikke endre dato på objekt(er) med kun lese-rettigheter, hopper over",
   "multiselect_grid_edit_gps_err_read_only": "Kan ikke endre lokasjon på objekt(er) med kun lese-rettigheter, hopper over",
+  "my_albums": "Mine albumer",
+  "networking_settings": "Nettverk",
+  "networking_subtitle": "Administrer serverendepunktinnstillingene",
   "no_assets_to_show": "Ingen objekter å vise",
   "no_name": "Ingen navn",
   "notification_permission_dialog_cancel": "Avbryt",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Gi tilgang for å aktivere notifikasjoner",
   "notification_permission_list_tile_enable_button": "Aktiver notifikasjoner",
   "notification_permission_list_tile_title": "Notifikasjonstilgang",
+  "on_this_device": "På denne enheten",
   "partner_list_user_photos": "{user}'s bilder",
   "partner_list_view_all": "Vis alle",
   "partner_page_add_partner": "Legg til partner",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} vil ikke lenger ha tilgang til dine bilder.",
   "partner_page_stop_sharing_title": "Stopp deling av bildene dine?",
   "partner_page_title": "Partner",
+  "partners": "Partnere",
+  "people": "Mennesker",
   "permission_onboarding_back": "Tilbake",
   "permission_onboarding_continue_anyway": "Fortsett uansett",
   "permission_onboarding_get_started": "Kom i gang",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Tilgang gitt! Du er i gang.",
   "permission_onboarding_permission_limited": "Begrenset tilgang. For å la Immich sikkerhetskopiere og håndtere galleriet, tillatt bilde- og video-tilgang i Innstillinger.",
   "permission_onboarding_request": "Immich trenger tilgang til å se dine bilder og videoer",
+  "places": "Steder",
+  "preferences_settings_subtitle": "Administrer appens preferanser",
   "preferences_settings_title": "Innstillinger",
   "profile_drawer_app_logs": "Logg",
   "profile_drawer_client_out_of_date_major": "Mobilapp er utdatert. Vennligst oppdater til nyeste versjon.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Innstillinger",
   "profile_drawer_sign_out": "Logg ut",
   "profile_drawer_trash": "Søppelbøtte",
+  "recently_added": "Nylig lagt til",
   "recently_added_page_title": "Nylig lagt til",
-  "save_to_gallery": "Save to gallery",
+  "save": "Lagre",
+  "save_to_gallery": "Lagre til galleriet",
   "scaffold_body_error_occurred": "Feil oppstått",
+  "search_albums": "Søk i albumer",
   "search_bar_hint": "Søk i dine bilder",
   "search_filter_apply": "Aktiver filter",
   "search_filter_camera": "Kamera",
@@ -428,6 +484,7 @@
   "search_page_places": "Steder",
   "search_page_recently_added": "Nylig lagt til",
   "search_page_screenshots": "Skjermbilder",
+  "search_page_search_photos_videos": "Søk etter dine bilder og videoer",
   "search_page_selfies": "Selfier",
   "search_page_things": "Ting",
   "search_page_videos": "Videoer",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Forslag",
   "select_user_for_sharing_page_err_album": "Feilet ved oppretting av album",
   "select_user_for_sharing_page_share_suggestions": "Forslag",
+  "server_endpoint": "Server endepunkt",
   "server_info_box_app_version": "App-versjon",
   "server_info_box_latest_release": "Siste versjon",
   "server_info_box_server_url": "Server-adresse",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Last forhåndsvisningsbilde",
   "setting_image_viewer_title": "Bilder",
   "setting_languages_apply": "Bekreft",
+  "setting_languages_subtitle": "Endre app-språk",
   "setting_languages_title": "Språk",
   "setting_notifications_notify_failures_grace_period": "Varsle om sikkerhetskopieringsfeil i bakgrunnen: {}",
   "setting_notifications_notify_hours": "{} timer",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Last opp",
   "shared_link_manage_links": "Håndter delte linker",
   "shared_link_public_album": "Offentlig album",
+  "shared_links": "Delte linker",
   "share_done": "Ferdig",
+  "shared_with_me": "Delt med meg",
   "share_invite": "Inviter til album",
   "sharing_page_album": "Delte album",
   "sharing_page_description": "Lag delte albumer for å dele bilder og videoer med folk i nettverket ditt.",
@@ -539,10 +600,10 @@
   "sharing_silver_appbar_create_shared_album": "Lag delt album",
   "sharing_silver_appbar_shared_links": "Delte linker",
   "sharing_silver_appbar_share_partner": "Del med partner",
-  "sync": "Sync",
-  "sync_albums": "Sync albums",
-  "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
-  "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
+  "sync": "Synkroniser",
+  "sync_albums": "Synkroniser albumer",
+  "sync_albums_manual_subtitle": "Synkroniser alle opplastede videoer og bilder til det valgte backupalbumet",
+  "sync_upload_album_setting_subtitle": "Opprett og last opp dine bilder og videoer til det valgte albumet på Immich",
   "tab_controller_nav_library": "Bibliotek",
   "tab_controller_nav_photos": "Bilder",
   "tab_controller_nav_search": "Søk",
@@ -563,7 +624,8 @@
   "theme_setting_three_stage_loading_subtitle": "Tre-trinns innlasting kan øke lasteytelsen, men forårsaker betydelig høyere nettverksbelastning",
   "theme_setting_three_stage_loading_title": "Aktiver tre-trinns innlasting",
   "translated_text_options": "Valg",
-  "trash_emptied": "Emptied trash",
+  "trash": "Søppel",
+  "trash_emptied": "Søppelbøtte tømt",
   "trash_page_delete": "Slett",
   "trash_page_delete_all": "Slett alt",
   "trash_page_empty_trash_btn": "Tøm søppelbøtte",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Vil du utføre backup av valgte objekt(er) til serveren?",
   "upload_dialog_ok": "Last opp",
   "upload_dialog_title": "Last opp objekt",
+  "use_current_connection": "bruk nåværende tilkobling",
+  "validate_endpoint_error": "Skriv inn en gyldig URL",
   "version_announcement_overlay_ack": "Bekreft",
   "version_announcement_overlay_release_notes": "endringsloggen",
   "version_announcement_overlay_text_1": "Hei, det er en ny versjon av",
   "version_announcement_overlay_text_2": "vennligst ta deg tid til å besøke ",
   "version_announcement_overlay_text_3": " og verifiser at docker-compose og .env-oppsettet ditt er oppdatert for å forhindre en eventuell feilkonfigurasjon, spesielt hvis du benytter WatchTower eller en annen tjeneste som håndterer oppdatering av server-applikasjonen automatisk.",
   "version_announcement_overlay_title": "Ny serverversjon tilgjengelig",
+  "videos": "Videoer",
   "viewer_remove_from_stack": "Fjern fra stabling",
   "viewer_stack_use_as_main_asset": "Bruk som hovedobjekt",
-  "viewer_unstack": "avstable"
+  "viewer_unstack": "avstable",
+  "wifi_name": "Wi-Fi navn",
+  "your_wifi_name": "Ditt Wi-Fi navn"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/nl-NL.json b/mobile/assets/i18n/nl-NL.json
index a6a151d506..ae7afef89e 100644
--- a/mobile/assets/i18n/nl-NL.json
+++ b/mobile/assets/i18n/nl-NL.json
@@ -6,6 +6,8 @@
   "action_common_save": "Opslaan",
   "action_common_select": "Selecteren",
   "action_common_update": "Bijwerken",
+  "add_a_name": "Naam toevoegen",
+  "add_endpoint": "Server toevoegen",
   "add_to_album_bottom_sheet_added": "Toegevoegd aan {album}",
   "add_to_album_bottom_sheet_already_exists": "Staat al in {album}",
   "advanced_settings_log_level_title": "Log niveau: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Probleemoplossing",
   "album_info_card_backup_album_excluded": "UITGESLOTEN",
   "album_info_card_backup_album_included": "INBEGREPEN",
+  "albums": "Albums",
   "album_thumbnail_card_item": "1 item",
   "album_thumbnail_card_items": "{} items",
   "album_thumbnail_card_shared": " · Gedeeld",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Verwijder uit album",
   "album_viewer_appbar_share_to": "Delen via",
   "album_viewer_page_share_add_users": "Gebruikers toevoegen",
+  "all": "Alle",
   "all_people_page_title": "Mensen",
   "all_videos_page_title": "Video's",
   "app_bar_signout_dialog_content": "Weet je zeker dat je wilt uitloggen?",
   "app_bar_signout_dialog_ok": "Ja",
   "app_bar_signout_dialog_title": "Log uit",
+  "archived": "Gearchiveerd",
   "archive_page_no_archived_assets": "Geen gearchiveerde assets gevonden",
   "archive_page_title": "Archief ({})",
   "asset_action_delete_err_read_only": "Kan alleen-lezen asset(s) niet verwijderen, overslaan",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} asset(s) succesvol hersteld",
   "assets_trashed": "{} asset(s) naar de prullenbak verplaatst",
   "assets_trashed_from_server": "{} asset(s) naar de prullenbak verplaatst op de Immich server",
+  "asset_viewer_settings_subtitle": "Beheer je instellingen voor gallerijweergave",
   "asset_viewer_settings_title": "Foto weergave",
+  "automatic_endpoint_switching_subtitle": "Verbind lokaal bij het opgegeven wifi-netwerk en gebruik anders de externe url",
+  "automatic_endpoint_switching_title": "Automatische serverwissel",
+  "background_location_permission": "Achtergrond locatie toestemming",
+  "background_location_permission_content": "Om van netwerk te wisselen terwijl de app op de achtergrond draait, heeft Immich *altijd* toegang tot de exacte locatie nodig om de naam van het wifi-netwerk te kunnen lezen",
   "backup_album_selection_page_albums_device": "Albums op apparaat ({})",
   "backup_album_selection_page_albums_tap": "Tik om in te voegen, dubbel tik om uit te sluiten",
   "backup_album_selection_page_assets_scatter": "Assets kunnen over verschillende albums verdeeld zijn, dus albums kunnen inbegrepen of uitgesloten zijn van het backup proces.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Succes",
   "backup_manual_title": "Uploadstatus",
   "backup_options_page_title": "Back-up instellingen",
+  "backup_setting_subtitle": "Beheer achtergrond en voorgrond uploadinstellingen",
   "cache_settings_album_thumbnails": "Thumbnails bibliotheekpagina ({} assets)",
   "cache_settings_clear_cache_button": "Cache wissen",
   "cache_settings_clear_cache_button_title": "Wist de cache van de app. Dit zal de presentaties van de app aanzienlijk beïnvloeden totdat de cache opnieuw is opgebouwd.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Beheer het gedrag van lokale opslag",
   "cache_settings_tile_title": "Lokale opslag",
   "cache_settings_title": "Cache-instellingen",
+  "cancel": "Annuleren",
+  "change_display_order": "Weergavevolgorde wijzigen",
   "change_password_form_confirm_password": "Bevestig wachtwoord",
   "change_password_form_description": "Hallo {name},\n\nDit is ofwel de eerste keer dat je inlogt, of er is een verzoek gedaan om je wachtwoord te wijzigen. Vul hieronder een nieuw wachtwoord in.",
   "change_password_form_new_password": "Nieuw wachtwoord",
   "change_password_form_password_mismatch": "Wachtwoorden komen niet overeen",
   "change_password_form_reenter_new_password": "Vul het wachtwoord opnieuw in",
+  "check_corrupt_asset_backup": "Controleer op corrupte back-ups van assets",
+  "check_corrupt_asset_backup_button": "Controle uitvoeren",
+  "check_corrupt_asset_backup_description": "Voer deze controle alleen uit via wifi en nadat alle assets zijn geback-upt. De procedure kan een paar minuten duren.",
   "client_cert_dialog_msg_confirm": "Ok",
   "client_cert_enter_password": "Voer wachtwoord in",
   "client_cert_import": "Importeren",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Herstellen",
   "control_bottom_app_bar_unfavorite": "Onfavoriet",
   "control_bottom_app_bar_upload": "Uploaden",
+  "create_album": "Album aanmaken",
   "create_album_page_untitled": "Naamloos",
+  "create_new": "MAAK NIEUW",
   "create_shared_album_page_create": "Aanmaken",
   "create_shared_album_page_share": "Delen",
   "create_shared_album_page_share_add_assets": "ASSETS TOEVOEGEN",
@@ -193,6 +211,7 @@
   "crop": "Bijsnijden",
   "curated_location_page_title": "Plaatsen",
   "curated_object_page_title": "Dingen",
+  "current_server_address": "Huidige serveradres",
   "daily_title_text_date": "E dd MMM",
   "daily_title_text_date_year": "E dd MMM yyyy",
   "date_format": "E d LLL y • H:mm",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "Verwijder gedeelde link",
   "description_input_hint_text": "Beschrijving toevoegen...",
   "description_input_submit_error": "Beschrijving bijwerken mislukt, controleer het logboek voor meer details",
+  "download_canceled": "Download geannuleerd",
+  "download_complete": "Download voltooid",
+  "download_enqueue": "Download in wachtrij",
   "download_error": "Fout bij downloaden",
+  "download_failed": "Download mislukt",
+  "download_filename": "bestand: {}",
+  "download_finished": "Download voltooid",
+  "downloading": "Downloaden...",
+  "downloading_media": "Media aan het downloaden",
+  "download_notfound": "Download niet gevonden",
+  "download_paused": "Download gepauseerd",
   "download_started": "Download gestart",
   "download_sucess": "Succesvol gedownload",
   "download_sucess_android": "Het bestand is gedownload naar DCIM/Immich",
+  "download_waiting_to_retry": "Wachten om opnieuw te proberen",
   "edit_date_time_dialog_date_time": "Datum en tijd",
   "edit_date_time_dialog_timezone": "Tijdzone",
   "edit_image_title": "Bewerken",
   "edit_location_dialog_title": "Locatie",
+  "enter_wifi_name": "Voer de WiFi naam in",
+  "error_change_sort_album": "Sorteervolgorde van album wijzigen mislukt",
   "error_saving_image": "Fout: {}",
   "exif_bottom_sheet_description": "Beschrijving toevoegen...",
   "exif_bottom_sheet_details": "DETAILS",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Experimenteel fotoraster inschakelen",
   "experimental_settings_subtitle": "Gebruik op eigen risico!",
   "experimental_settings_title": "Experimenteel",
+  "external_network": "Extern netwerk",
+  "external_network_sheet_info": "Als je niet verbonden bent met het opgegeven wifi-netwerk, maakt de app verbinding met de server via de eerst bereikbare URL in de onderstaande lijst, van boven naar beneden",
+  "favorites": "Favorieten",
   "favorites_page_no_favorites": "Geen favoriete assets gevonden",
   "favorites_page_title": "Favorieten",
   "filename_search": "Bestandsnaam of extensie",
+  "filter": "Filter",
+  "get_wifiname_error": "Kon de Wi-Fi naam niet ophalen. Zorg ervoor dat je de benodigde machtigingen hebt verleend en verbonden bent met een Wi-Fi-netwerk",
+  "grant_permission": "Geef toestemming",
   "haptic_feedback_switch": "Aanraaktrillingen inschakelen",
   "haptic_feedback_title": "Aanraaktrillingen",
   "header_settings_add_header_tip": "Header toevoegen",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "Als dit de eerste keer is dat je de app gebruikt, zorg er dan voor dat je een back-up album kiest, zodat de tijdlijn gevuld kan worden met foto's en video's uit het album.",
   "home_page_share_err_local": "Lokale assets kunnen niet via een link gedeeld worden, overslaan",
   "home_page_upload_err_limit": "Kan maximaal 30 assets tegelijk uploaden, overslaan",
+  "ignore_icloud_photos": "Negeer iCloud foto's",
+  "ignore_icloud_photos_description": "Foto's die op iCloud zijn opgeslagen, worden niet geüpload naar de Immich server",
   "image_saved_successfully": "Afbeelding opgeslagen",
   "image_viewer_page_state_provider_download_error": "Download mislukt",
   "image_viewer_page_state_provider_download_started": "Download gestart",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "Deel Error",
   "invalid_date": "Ongeldige datum",
   "invalid_date_format": "Ongeldig datumformaat",
+  "library": "Bibliotheek",
   "library_page_albums": "Albums",
   "library_page_archive": "Archief",
   "library_page_device_albums": "Albums op apparaat",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Oudste foto",
   "library_page_sort_most_recent_photo": "Meest recente foto",
   "library_page_sort_title": "Albumtitel",
+  "local_network": "Lokaal netwerk",
+  "local_network_sheet_info": "De app maakt verbinding met de server via deze URL wanneer het opgegeven wifi-netwerk wordt gebruikt",
+  "location_permission": "Locatie toestemming",
+  "location_permission_content": "Om de functie voor automatische serverwissel te gebruiken, heeft Immich toegang tot de exacte locatie nodig om de naam van het huidige wifi-netwerk te kunnen bepalen.",
   "location_picker_choose_on_map": "Kies op kaart",
   "location_picker_latitude": "Breedtegraad",
   "location_picker_latitude_error": "Voer een geldige breedtegraad in",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Bewegende foto's",
   "multiselect_grid_edit_date_time_err_read_only": "Kan datum van alleen-lezen asset(s) niet wijzigen, overslaan",
   "multiselect_grid_edit_gps_err_read_only": "Kan locatie van alleen-lezen asset(s) niet wijzigen, overslaan",
+  "my_albums": "Mijn albums",
+  "networking_settings": "Netwerk",
+  "networking_subtitle": "Beheer de instellingen voor de server URL",
   "no_assets_to_show": "Geen foto's om te laten zien",
   "no_name": "Geen naam",
   "notification_permission_dialog_cancel": "Annuleren",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Geef toestemming om meldingen te versturen.",
   "notification_permission_list_tile_enable_button": "Meldingen inschakelen",
   "notification_permission_list_tile_title": "Meldingen toestaan",
+  "on_this_device": "Op dit apparaat",
   "partner_list_user_photos": "Foto's van {user}",
   "partner_list_view_all": "Bekijk alle",
   "partner_page_add_partner": "Partner toevoegen",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} zal geen toegang meer hebben tot je fotos's.",
   "partner_page_stop_sharing_title": "Stoppen met het delen van je foto's?",
   "partner_page_title": "Partner",
+  "partners": "Partners",
+  "people": "Mensen",
   "permission_onboarding_back": "Terug",
   "permission_onboarding_continue_anyway": "Toch doorgaan",
   "permission_onboarding_get_started": "Aan de slag",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Toestemming verleend. Je bent helemaal klaar.",
   "permission_onboarding_permission_limited": "Beperkte toestemming. Geef toestemming tot foto's en video's in Instellingen om Immich een back-up te laten maken van je galerij en deze te beheren.",
   "permission_onboarding_request": "Immich heeft toestemming nodig om je foto's en video's te bekijken.",
+  "places": "Plaatsen",
+  "preferences_settings_subtitle": "Beheer de voorkeuren van de app",
   "preferences_settings_title": "Voorkeuren",
   "profile_drawer_app_logs": "Logboek",
   "profile_drawer_client_out_of_date_major": "Mobiele app is verouderd. Werk bij naar de nieuwste hoofdversie.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Instellingen",
   "profile_drawer_sign_out": "Uitloggen",
   "profile_drawer_trash": "Prullenbak",
+  "recently_added": "Onlangs toegevoegd",
   "recently_added_page_title": "Recent toegevoegd",
+  "save": "Opslaan",
   "save_to_gallery": "Opslaan in galerij",
   "scaffold_body_error_occurred": "Fout opgetreden",
+  "search_albums": "Albums zoeken",
   "search_bar_hint": "Foto's doorzoeken",
   "search_filter_apply": "Filter toepassen",
   "search_filter_camera": "Camera",
@@ -428,6 +484,7 @@
   "search_page_places": "Plaatsen",
   "search_page_recently_added": "Recent toegevoegd",
   "search_page_screenshots": "Screenshots",
+  "search_page_search_photos_videos": "Zoek naar je foto's en video's",
   "search_page_selfies": "Selfies",
   "search_page_things": "Dingen",
   "search_page_videos": "Video's",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Suggesties",
   "select_user_for_sharing_page_err_album": "Album aanmaken mislukt",
   "select_user_for_sharing_page_share_suggestions": "Suggesties",
+  "server_endpoint": "Server url",
   "server_info_box_app_version": "Appversie",
   "server_info_box_latest_release": "Laatste Versie",
   "server_info_box_server_url": "Server URL",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Voorbeeldafbeelding laden",
   "setting_image_viewer_title": "Afbeeldingen",
   "setting_languages_apply": "Toepassen",
+  "setting_languages_subtitle": "Wijzig de taal van de app",
   "setting_languages_title": "Taal",
   "setting_notifications_notify_failures_grace_period": "Fouten van de achtergrond back-up melden: {}",
   "setting_notifications_notify_hours": "{} uur",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Uploaden",
   "shared_link_manage_links": "Beheer gedeelde links",
   "shared_link_public_album": "Publiek album",
+  "shared_links": "Gedeelde links",
   "share_done": "Klaar",
+  "shared_with_me": "Gedeeld met mij",
   "share_invite": "Uitnodigen voor album",
   "sharing_page_album": "Gedeelde albums",
   "sharing_page_description": "Maak gedeelde albums om foto's en video's te delen met mensen in je netwerk.",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Laden in drie fasen kan de laadprestaties verbeteren, maar veroorzaakt een aanzienlijk hogere netwerkbelasting",
   "theme_setting_three_stage_loading_title": "Laden in drie fasen inschakelen",
   "translated_text_options": "Opties",
+  "trash": "Prullenbak",
   "trash_emptied": "Prullenbak geleegd",
   "trash_page_delete": "Verwijderen",
   "trash_page_delete_all": "Verwijder alle",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Wil je een backup maken van de geselecteerde asset(s) op de server?",
   "upload_dialog_ok": "Uploaden",
   "upload_dialog_title": "Asset uploaden",
+  "use_current_connection": "gebruik huidige verbinding",
+  "validate_endpoint_error": "Vul een geldige URL in",
   "version_announcement_overlay_ack": "Bevestig",
   "version_announcement_overlay_release_notes": "releaseopmerkingen",
   "version_announcement_overlay_text_1": "Hoi, er is een nieuwe versie beschikbaar van",
   "version_announcement_overlay_text_2": "neem je tijd en bezoek de ",
   "version_announcement_overlay_text_3": " en controleer of je docker-compose en .env up-to-date zijn, om misconfiguraties te voorkomen, in het bijzonder als je gebruik maakt van WatchTower of een ander mechanisme dat je serverapplicatie automatisch bijwerkt.",
   "version_announcement_overlay_title": "Nieuwe serverversie beschikbaar \uD83C\uDF89",
+  "videos": "Video's",
   "viewer_remove_from_stack": "Verwijder van Stapel",
   "viewer_stack_use_as_main_asset": "Gebruik als Hoofd Asset",
-  "viewer_unstack": "Ontstapel"
+  "viewer_unstack": "Ontstapel",
+  "wifi_name": "WiFi naam",
+  "your_wifi_name": "Je WiFi naam"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/pl-PL.json b/mobile/assets/i18n/pl-PL.json
index ec9009e28f..d2a15c2c5a 100644
--- a/mobile/assets/i18n/pl-PL.json
+++ b/mobile/assets/i18n/pl-PL.json
@@ -6,12 +6,14 @@
   "action_common_save": "Zapisz",
   "action_common_select": "Wybierz",
   "action_common_update": "Aktualizuj",
+  "add_a_name": "Dodaj nazwę",
+  "add_endpoint": "Dodaj punkt końcowy",
   "add_to_album_bottom_sheet_added": "Dodano do {album}",
   "add_to_album_bottom_sheet_already_exists": "Już w {album}",
   "advanced_settings_log_level_title": "Poziom dziennika: {}",
   "advanced_settings_prefer_remote_subtitle": "Niektóre urządzenia bardzo wolno ładują miniatury z zasobów na urządzeniu. Aktywuj to ustawienie, aby ładować zdalne obrazy.",
   "advanced_settings_prefer_remote_title": "Preferuj obrazy zdalne",
-  "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
+  "advanced_settings_proxy_headers_subtitle": "Zdefiniuj nagłówki proxy, które Immich powinien wysyłać z każdym żądaniem sieciowym",
   "advanced_settings_proxy_headers_title": "Nagłówki proxy",
   "advanced_settings_self_signed_ssl_subtitle": "Pomija weryfikację certyfikatu SSL dla punktu końcowego serwera. Wymagane w przypadku certyfikatów z podpisem własnym.",
   "advanced_settings_self_signed_ssl_title": "Zezwalaj na certyfikaty SSL z podpisem własnym",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Rozwiązywanie problemów",
   "album_info_card_backup_album_excluded": "WYKLUCZONE",
   "album_info_card_backup_album_included": "WŁĄCZONE",
+  "albums": "Albumy",
   "album_thumbnail_card_item": "1 pozycja",
   "album_thumbnail_card_items": "{} pozycje",
   "album_thumbnail_card_shared": "Udostępniony",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Usuń z albumu",
   "album_viewer_appbar_share_to": "Udostępnij",
   "album_viewer_page_share_add_users": "Dodaj użytkowników",
+  "all": "Wszystko",
   "all_people_page_title": "Ludzie",
   "all_videos_page_title": "Filmy",
   "app_bar_signout_dialog_content": "Czy na pewno chcesz się wylogować?",
   "app_bar_signout_dialog_ok": "Tak",
   "app_bar_signout_dialog_title": "Wyloguj się",
+  "archived": "Zarchiwizowane",
   "archive_page_no_archived_assets": "Nie znaleziono zarchiwizowanych zasobów",
   "archive_page_title": "Archiwum ({})",
   "asset_action_delete_err_read_only": "Nie można usunąć zasobów tylko do odczytu, pomijam",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": " {} zasoby pomyślnie przywrócono",
   "assets_trashed": "{} zasoby zostały usunięte",
   "assets_trashed_from_server": "{} zasoby usunięte z serwera Immich",
+  "asset_viewer_settings_subtitle": "Zarządzaj ustawieniami przeglądarki galerii",
   "asset_viewer_settings_title": "Przeglądarka zasobów",
+  "automatic_endpoint_switching_subtitle": "Połącz się lokalnie przez wyznaczoną sieć Wi-Fi, jeśli jest dostępna, i korzystaj z alternatywnych połączeń gdzie indziej",
+  "automatic_endpoint_switching_title": "Automatyczne przełączanie adresów URL",
+  "background_location_permission": "Uprawnienia do lokalizacji w tle",
+  "background_location_permission_content": "Aby móc przełączać sieć podczas pracy w tle, Immich musi *zawsze* mieć dostęp do dokładnej lokalizacji, aby aplikacja mogła odczytać nazwę sieci Wi-Fi",
   "backup_album_selection_page_albums_device": "Albumy na urządzeniu ({})",
   "backup_album_selection_page_albums_tap": "Stuknij, aby włączyć, stuknij dwukrotnie, aby wykluczyć",
   "backup_album_selection_page_assets_scatter": "Pliki mogą być rozproszone w wielu albumach. Dzięki temu albumy mogą być włączane lub wyłączane podczas procesu tworzenia kopii zapasowej.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Sukces",
   "backup_manual_title": "Stan przesyłania",
   "backup_options_page_title": "Opcje kopi zapasowej",
+  "backup_setting_subtitle": "Zarządzaj ustawieniami przesyłania w tle i na pierwszym planie",
   "cache_settings_album_thumbnails": "Miniatury stron bibliotek ({} zasobów)",
   "cache_settings_clear_cache_button": "Wyczyść Cache",
   "cache_settings_clear_cache_button_title": "Czyści pamięć podręczną aplikacji. Wpłynie to znacząco na wydajność aplikacji, dopóki pamięć podręczna nie zostanie odbudowana.",
@@ -145,20 +156,25 @@
   "cache_settings_tile_subtitle": "Kontroluj zachowanie lokalnego magazynu",
   "cache_settings_tile_title": "Lokalny magazyn",
   "cache_settings_title": "Ustawienia Buforowania",
+  "cancel": "Anuluj",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Potwierdź Hasło",
   "change_password_form_description": "Cześć {name},\n\nPierwszy raz logujesz się do systemu, albo złożono prośbę o zmianę hasła. Wpisz poniżej nowe hasło.",
   "change_password_form_new_password": "Nowe Hasło",
   "change_password_form_password_mismatch": "Hasła nie są zgodne",
   "change_password_form_reenter_new_password": "Wprowadź ponownie Nowe Hasło",
+  "check_corrupt_asset_backup": "Sprawdź, czy kopie zapasowe zasobów nie są uszkodzone",
+  "check_corrupt_asset_backup_button": "Wykonaj sprawdzenie",
+  "check_corrupt_asset_backup_description": "Uruchom sprawdzenie tylko przez Wi-Fi i po utworzeniu kopii zapasowej wszystkich zasobów. Procedura może potrwać kilka minut.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Wprowadź hasło",
-  "client_cert_import": "Import",
-  "client_cert_import_success_msg": "Client certificate is imported",
-  "client_cert_invalid_msg": "Invalid certificate file or wrong password",
-  "client_cert_remove": "Remove",
-  "client_cert_remove_msg": "Client certificate is removed",
-  "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login",
-  "client_cert_title": "SSL Client Certificate",
+  "client_cert_import": "Importuj",
+  "client_cert_import_success_msg": "Certyfikat klienta został zaimportowany",
+  "client_cert_invalid_msg": "Nieprawidłowy plik certyfikatu lub nieprawidłowe hasło",
+  "client_cert_remove": "Usuń",
+  "client_cert_remove_msg": "Certyfikat klienta został usunięty",
+  "client_cert_subtitle": "Obsługuje tylko format PKCS12 (.p12, .pfx). Import/Usunięcie certyfikatu jest dostępne tylko przed zalogowaniem",
+  "client_cert_title": "Certyfikat klienta SSL",
   "common_add_to_album": "Dodaj do albumu",
   "common_change_password": "Zmień Hasło",
   "common_create_new_album": "Utwórz nowy album",
@@ -179,20 +195,23 @@
   "control_bottom_app_bar_edit_time": "Edytuj datę i godzinę",
   "control_bottom_app_bar_favorite": "Ulubione",
   "control_bottom_app_bar_share": "Udostępnij",
-  "control_bottom_app_bar_share_to": "Udostępnij",
+  "control_bottom_app_bar_share_to": "Wyślij",
   "control_bottom_app_bar_stack": "Stos",
   "control_bottom_app_bar_trash_from_immich": "Przenieść do kosza",
   "control_bottom_app_bar_unarchive": "Cofnij archiwizację",
   "control_bottom_app_bar_unfavorite": "Nieulubione",
   "control_bottom_app_bar_upload": "Prześlij",
+  "create_album": "Utwórz album",
   "create_album_page_untitled": "Bez tytułu",
+  "create_new": "UTWÓRZ NOWY",
   "create_shared_album_page_create": "Utwórz album",
   "create_shared_album_page_share": "Udostępnij",
   "create_shared_album_page_share_add_assets": "DODAJ ZASOBY",
   "create_shared_album_page_share_select_photos": "Zaznacz Zdjęcia",
-  "crop": "Crop",
+  "crop": "Przytnij",
   "curated_location_page_title": "Miejsca",
   "curated_object_page_title": "Rzeczy",
+  "current_server_address": "Aktualny adres serwera",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,15 +229,28 @@
   "delete_shared_link_dialog_title": "Usuń udostępniony link",
   "description_input_hint_text": "Dodaj opis...",
   "description_input_submit_error": "Błąd aktualizacji opisu, sprawdź dziennik, aby uzyskać więcej szczegółów",
-  "download_error": "Download Error",
-  "download_started": "Download started",
-  "download_sucess": "Download success",
-  "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_canceled": "Pobieranie anulowane",
+  "download_complete": "Pobieranie zakończone",
+  "download_enqueue": "Pobieranie w kolejce",
+  "download_error": "Błąd pobierania",
+  "download_failed": "Pobieranie nieudane",
+  "download_filename": "plik: {}",
+  "download_finished": "Pobieranie zakończone",
+  "downloading": "Pobieranie...",
+  "downloading_media": "Pobieranie multimediów",
+  "download_notfound": "Nie znaleziono pliku do pobrania",
+  "download_paused": "Pobieranie wstrzymane",
+  "download_started": "Pobieranie rozpoczęte",
+  "download_sucess": "Udane pobieranie",
+  "download_sucess_android": "Media zostały pobrane do DCIM/Immich",
+  "download_waiting_to_retry": "Oczekiwanie na ponowną próbę",
   "edit_date_time_dialog_date_time": "Data i godzina",
   "edit_date_time_dialog_timezone": "Strefa czasowa",
-  "edit_image_title": "Edit",
+  "edit_image_title": "Edytuj",
   "edit_location_dialog_title": "Lokalizacja",
-  "error_saving_image": "Error: {}",
+  "enter_wifi_name": "Wprowadź nazwę Wi-Fi",
+  "error_change_sort_album": "Failed to change album sort order",
+  "error_saving_image": "Błąd: {}",
   "exif_bottom_sheet_description": "Dodaj Opis...",
   "exif_bottom_sheet_details": "SZCZEGÓŁY",
   "exif_bottom_sheet_location": "LOKALIZACJA",
@@ -229,11 +261,17 @@
   "experimental_settings_new_asset_list_title": "Włącz eksperymentalną układ zdjęć",
   "experimental_settings_subtitle": "Używaj na własne ryzyko!",
   "experimental_settings_title": "Eksperymentalny",
+  "external_network": "Sieć zewnętrzna",
+  "external_network_sheet_info": "Jeśli nie korzystasz z preferowanej sieci Wi-Fi, aplikacja połączy się z serwerem za pośrednictwem pierwszego z poniższych adresów URL, do którego może dotrzeć, zaczynając od góry do dołu",
+  "favorites": "Ulubione",
   "favorites_page_no_favorites": "Nie znaleziono ulubionych zasobów",
   "favorites_page_title": "Ulubione",
   "filename_search": "Nazwa pliku lub rozszerzenie",
-  "haptic_feedback_switch": "Enable haptic feedback",
-  "haptic_feedback_title": "Haptic Feedback",
+  "filter": "Filtr",
+  "get_wifiname_error": "Nie można uzyskać nazwy Wi-Fi. Upewnij się, że udzieliłeś niezbędnych uprawnień i jesteś połączony z siecią Wi-Fi",
+  "grant_permission": "Udziel pozwolenia",
+  "haptic_feedback_switch": "Włącz technologię haptyczną",
+  "haptic_feedback_title": "Technologia haptyczna",
   "header_settings_add_header_tip": "Dodaj nagłówek",
   "header_settings_field_validator_msg": "Wartość nie może być pusta",
   "header_settings_header_name_input": "Nazwa nagłówka",
@@ -255,13 +293,16 @@
   "home_page_first_time_notice": "Jeśli korzystasz z aplikacji po raz pierwszy, pamiętaj o wybraniu albumów zapasowych, aby oś czasu mogła zapełnić zdjęcia i filmy w albumach.",
   "home_page_share_err_local": "Nie można udostępniać zasobów lokalnych za pośrednictwem linku, pomijajam",
   "home_page_upload_err_limit": "Można przesłać maksymalnie 30 zasobów jednocześnie, pomijanie",
-  "image_saved_successfully": "Image saved",
+  "ignore_icloud_photos": "Ignoruj ​​zdjęcia w iCloud",
+  "ignore_icloud_photos_description": "Zdjęcia przechowywane w usłudze iCloud nie zostaną przesłane na serwer Immich",
+  "image_saved_successfully": "Obraz zapisany",
   "image_viewer_page_state_provider_download_error": "Błąd pobierania",
   "image_viewer_page_state_provider_download_started": "Pobieranie rozpoczęte",
   "image_viewer_page_state_provider_download_success": "Pobieranie zakończone",
   "image_viewer_page_state_provider_share_error": "Udostępnij błąd",
   "invalid_date": "Nieprawidłowa data",
   "invalid_date_format": "Nieprawidłowy format daty",
+  "library": "Biblioteka",
   "library_page_albums": "Albumy",
   "library_page_archive": "Archiwum",
   "library_page_device_albums": "Albumy na Urządzeniu",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Najstarsze zdjęcie",
   "library_page_sort_most_recent_photo": "Najnowsze zdjęcie",
   "library_page_sort_title": "Tytuł albumu",
+  "local_network": "Sieć lokalna",
+  "local_network_sheet_info": "Aplikacja połączy się z serwerem za pośrednictwem tego adresu URL podczas korzystania z określonej sieci Wi-Fi",
+  "location_permission": "Zezwolenie na lokalizację",
+  "location_permission_content": "Aby móc korzystać z funkcji automatycznego przełączania, Immich potrzebuje precyzyjnego pozwolenia na lokalizację, aby móc odczytać nazwę bieżącej sieci WiFi",
   "location_picker_choose_on_map": "Wybierz na mapie",
   "location_picker_latitude": "Szerokość geograficzna",
   "location_picker_latitude_error": "Wprowadź prawidłową szerokość geograficzną",
@@ -342,14 +387,18 @@
   "motion_photos_page_title": "Zdjęcia ruchome",
   "multiselect_grid_edit_date_time_err_read_only": "Nie można edytować daty zasobów tylko do odczytu, pomijanie",
   "multiselect_grid_edit_gps_err_read_only": "Nie można edytować lokalizacji zasobów tylko do odczytu, pomijanie",
+  "my_albums": "Moje albumy",
+  "networking_settings": "Sieć",
+  "networking_subtitle": "Zarządzaj ustawieniami serwera końcowego ",
   "no_assets_to_show": "Brak zasobów do pokazania",
-  "no_name": "No name",
+  "no_name": "Bez nazwy",
   "notification_permission_dialog_cancel": "Anuluj",
   "notification_permission_dialog_content": "Aby włączyć powiadomienia, przejdź do Ustawień i wybierz opcję Zezwalaj.",
   "notification_permission_dialog_settings": "Ustawienia",
   "notification_permission_list_tile_content": "Przyznaj uprawnienia, aby włączyć powiadomienia.",
   "notification_permission_list_tile_enable_button": "Włącz Powiadomienia",
   "notification_permission_list_tile_title": "Pozwolenie na powiadomienia",
+  "on_this_device": "Na tym urządzeniu",
   "partner_list_user_photos": "{user} zdjęcia",
   "partner_list_view_all": "Pokaż wszystkie",
   "partner_page_add_partner": "Dodaj partnera",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} nie będziesz już mieć dostępu do swoich zdjęć.",
   "partner_page_stop_sharing_title": "Przestać udostępniać swoje zdjęcia?",
   "partner_page_title": "Partner",
+  "partners": "Partnerzy",
+  "people": "Ludzie",
   "permission_onboarding_back": "Cofnij",
   "permission_onboarding_continue_anyway": "Kontynuuj mimo to",
   "permission_onboarding_get_started": "Rozpocznij",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Pozwolenie udzielone! Wszystko gotowe.",
   "permission_onboarding_permission_limited": "Pozwolenie ograniczone. Aby umożliwić Immichowi tworzenie kopii zapasowych całej kolekcji galerii i zarządzanie nią, przyznaj uprawnienia do zdjęć i filmów w Ustawieniach.",
   "permission_onboarding_request": "Immich potrzebuje pozwolenia na przeglądanie Twoich zdjęć i filmów.",
+  "places": "Miejsca",
+  "preferences_settings_subtitle": "Zarządzaj preferencjami aplikacji",
   "preferences_settings_title": "Ustawienia",
   "profile_drawer_app_logs": "Logi",
   "profile_drawer_client_out_of_date_major": "Aplikacja mobilna jest nieaktualna. Zaktualizuj do najnowszej wersji głównej.",
@@ -383,24 +436,27 @@
   "profile_drawer_settings": "Ustawienia",
   "profile_drawer_sign_out": "Wyloguj się",
   "profile_drawer_trash": "Kosz",
+  "recently_added": "Ostatnio dodane",
   "recently_added_page_title": "Ostatnio Dodane",
-  "save_to_gallery": "Save to gallery",
+  "save": "Zapisz",
+  "save_to_gallery": "Zapisz w galerii",
   "scaffold_body_error_occurred": "Wystąpił błąd",
+  "search_albums": "Przeszukaj albumy",
   "search_bar_hint": "Szukaj swoich zdjęć",
   "search_filter_apply": "Zastosuj filtr",
-  "search_filter_camera": "Camera",
+  "search_filter_camera": "Kamera",
   "search_filter_camera_make": "Make",
   "search_filter_camera_model": "Model",
-  "search_filter_camera_title": "Select camera type",
-  "search_filter_date": "Date",
-  "search_filter_date_interval": "{start} to {end}",
-  "search_filter_date_title": "Select a date range",
+  "search_filter_camera_title": "Wybierz typ kamery",
+  "search_filter_date": "Data",
+  "search_filter_date_interval": "{start} do {end}",
+  "search_filter_date_title": "Wybierz zakres dat",
   "search_filter_display_option_archive": "Archiwum",
   "search_filter_display_option_favorite": "Ulubiony",
   "search_filter_display_option_not_in_album": "Nie w albumie",
-  "search_filter_display_options": "Display Options",
-  "search_filter_display_options_title": "Display options",
-  "search_filter_location": "Location",
+  "search_filter_display_options": "Opcje wyświetlania",
+  "search_filter_display_options_title": "Opcje wyświetlania",
+  "search_filter_location": "Lokalizacja",
   "search_filter_location_city": "Miasto",
   "search_filter_location_country": "Kraj",
   "search_filter_location_state": "State",
@@ -428,6 +484,7 @@
   "search_page_places": "Miejsca",
   "search_page_recently_added": "Ostatnio dodane",
   "search_page_screenshots": "Zrzuty ekranu",
+  "search_page_search_photos_videos": "Wyszukaj swoje zdjęcia i filmy",
   "search_page_selfies": "Selfi",
   "search_page_things": "Rzeczy",
   "search_page_videos": "Filmy",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Propozycje",
   "select_user_for_sharing_page_err_album": "Nie udało się utworzyć albumu",
   "select_user_for_sharing_page_share_suggestions": "Propozycje",
+  "server_endpoint": "Serwer końcowy",
   "server_info_box_app_version": "Wersja Aplikacji",
   "server_info_box_latest_release": "Ostatnia wersja",
   "server_info_box_server_url": "Adres URL",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Załaduj obraz podglądu",
   "setting_image_viewer_title": "Zdjęcia",
   "setting_languages_apply": "Zastosuj",
+  "setting_languages_subtitle": "Zmień język aplikacji",
   "setting_languages_title": "Języki",
   "setting_notifications_notify_failures_grace_period": "Powiadomienie o awariach kopii zapasowych w tle: {}",
   "setting_notifications_notify_hours": "{} godzin",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Wgraj",
   "shared_link_manage_links": "Zarządzaj udostępnionymi linkami",
   "shared_link_public_album": "Album publiczny",
+  "shared_links": "Udostępnione linki",
   "share_done": "Zrobione",
+  "shared_with_me": "Udostępniono mi",
   "share_invite": "Zaproś do albumu",
   "sharing_page_album": "Udostępnione albumy",
   "sharing_page_description": "Twórz wspóldzielone albumy, aby udostępniać zdjęcia i filmy osobom w sieci.",
@@ -539,10 +600,10 @@
   "sharing_silver_appbar_create_shared_album": "Utwórz współdzielony album",
   "sharing_silver_appbar_shared_links": "Udostępnione linki",
   "sharing_silver_appbar_share_partner": "Udostępnij partnerce/partnerowi",
-  "sync": "Sync",
-  "sync_albums": "Sync albums",
-  "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
-  "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
+  "sync": "Synchronizuj",
+  "sync_albums": "Synchronizuj albumy",
+  "sync_albums_manual_subtitle": "Zsynchronizuj wszystkie przesłane filmy i zdjęcia z wybranymi albumami kopii zapasowych",
+  "sync_upload_album_setting_subtitle": "Twórz i przesyłaj swoje zdjęcia i filmy do wybranych albumów w Immich",
   "tab_controller_nav_library": "Biblioteka",
   "tab_controller_nav_photos": "Zdjęcia",
   "tab_controller_nav_search": "Szukaj",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Trójstopniowe ładowanie może zwiększyć wydajność ładowania, ale powoduje znacznie większe obciążenie sieci",
   "theme_setting_three_stage_loading_title": "Włączenie trójstopniowego ładowania",
   "translated_text_options": "Opcje",
+  "trash": "Kosz",
   "trash_emptied": "Opróżnione śmieci",
   "trash_page_delete": "Usuń",
   "trash_page_delete_all": "Usuń wszystko",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Czy chcesz wykonać kopię zapasową wybranych zasobów na serwerze?",
   "upload_dialog_ok": "Prześlij",
   "upload_dialog_title": "Prześlij Zasób",
+  "use_current_connection": "użyj bieżącego połączenia",
+  "validate_endpoint_error": "Proszę wprowadzić prawidłowy adres URL",
   "version_announcement_overlay_ack": "Potwierdzam",
   "version_announcement_overlay_release_notes": "informacje o wydaniu",
   "version_announcement_overlay_text_1": "Cześć przyjacielu, jest nowe wydanie",
   "version_announcement_overlay_text_2": "prosimy o poświęcenie czasu na odwiedzenie ",
   "version_announcement_overlay_text_3": " i upewnij się, że twoja konfiguracja docker-compose i .env jest aktualna, aby zapobiec błędnym konfiguracjom, zwłaszcza jeśli używasz WatchTower lub dowolnego mechanizmu, który obsługuje automatyczną aktualizację aplikacji serwera.",
   "version_announcement_overlay_title": "Nowa wersja serwera dostępna \uD83C\uDF89",
+  "videos": "Filmy",
   "viewer_remove_from_stack": "Usuń ze stosu",
   "viewer_stack_use_as_main_asset": "Użyj jako głównego zasobu",
-  "viewer_unstack": "Usuń stos"
+  "viewer_unstack": "Usuń stos",
+  "wifi_name": "Nazwa Wi-Fi",
+  "your_wifi_name": "Twoja nazwa Wi-Fi"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/pt-PT.json b/mobile/assets/i18n/pt-PT.json
index 991fdfaf36..2411edb2f5 100644
--- a/mobile/assets/i18n/pt-PT.json
+++ b/mobile/assets/i18n/pt-PT.json
@@ -6,6 +6,8 @@
   "action_common_save": "Salvar",
   "action_common_select": "Selecionar",
   "action_common_update": "Atualizar",
+  "add_a_name": "Adicionar nome",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Adicionado a {album}",
   "add_to_album_bottom_sheet_already_exists": "Já existe em {album}",
   "advanced_settings_log_level_title": "Nível de log: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Resolução de problemas",
   "album_info_card_backup_album_excluded": "EXCLUÍDO",
   "album_info_card_backup_album_included": "INCLUÍDO",
+  "albums": "Álbuns",
   "album_thumbnail_card_item": "1 arquivo",
   "album_thumbnail_card_items": "{} arquivos",
   "album_thumbnail_card_shared": " · Compartilhado",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Remover do álbum",
   "album_viewer_appbar_share_to": "Compartilhar com",
   "album_viewer_page_share_add_users": "Adicionar usuários",
+  "all": "Todos",
   "all_people_page_title": "Pessoas",
   "all_videos_page_title": "Vídeos",
   "app_bar_signout_dialog_content": "Tem certeza que deseja sair?",
   "app_bar_signout_dialog_ok": "Sim",
   "app_bar_signout_dialog_title": "Sair",
+  "archived": "Arquivado",
   "archive_page_no_archived_assets": "Nenhum arquivo encontrado",
   "archive_page_title": "Arquivado ({})",
   "asset_action_delete_err_read_only": "Não é possível excluir arquivo só leitura, ignorando",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} arquivo(s) restaurados com sucesso",
   "assets_trashed": "{} arquivo(s) enviados para a lixeira",
   "assets_trashed_from_server": "{} arquivo(s) do servidor foram enviados para a lixeira",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "Visualizador",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Álbuns no dispositivo ({})",
   "backup_album_selection_page_albums_tap": "Toque para incluir, duplo toque para excluir",
   "backup_album_selection_page_assets_scatter": "Os arquivos podem estar espalhados em vários álbuns. Assim, os álbuns podem ser incluídos ou excluídos durante o processo de backup.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Sucesso",
   "backup_manual_title": "Estado do envio",
   "backup_options_page_title": "Opções de backup",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Miniaturas da página da biblioteca ({} arquivos)",
   "cache_settings_clear_cache_button": "Limpar cache",
   "cache_settings_clear_cache_button_title": "Limpa o cache do aplicativo. Isso afetará significativamente o desempenho do aplicativo até que o cache seja reconstruído.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Controlar o comportamento do armazenamento local",
   "cache_settings_tile_title": "Armazenamento local",
   "cache_settings_title": "Configurações de cache",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Confirme a senha",
   "change_password_form_description": "Esta é a primeira vez que você está acessando o sistema ou foi feita uma solicitação para alterar sua senha. Por favor, insira a nova senha abaixo.",
   "change_password_form_new_password": "Nova senha",
   "change_password_form_password_mismatch": "As senhas não estão iguais",
   "change_password_form_reenter_new_password": "Confirme a nova senha",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Digite a senha",
   "client_cert_import": "Importar",
@@ -185,17 +201,20 @@
   "control_bottom_app_bar_unarchive": "Desarquivar",
   "control_bottom_app_bar_unfavorite": "Remover favorito",
   "control_bottom_app_bar_upload": "Enviar",
+  "create_album": "Criar Álbum",
   "create_album_page_untitled": "Sem título",
+  "create_new": "CRIAR NOVO",
   "create_shared_album_page_create": "Criar",
   "create_shared_album_page_share": "Compartilhar",
   "create_shared_album_page_share_add_assets": "ADICIONAR ARQUIVOS",
   "create_shared_album_page_share_select_photos": "Selecionar Fotos",
-  "crop": "Crop",
+  "crop": "Cortar",
   "curated_location_page_title": "Locais",
   "curated_object_page_title": "Objetos",
-  "daily_title_text_date": "E, MMM dd",
-  "daily_title_text_date_year": "E, MMM dd, yyyy",
-  "date_format": "E, LLL d, y • h:mm a",
+  "current_server_address": "Current server address",
+  "daily_title_text_date": "E, dd MMM",
+  "daily_title_text_date_year": "E, dd MMM, yyyy",
+  "date_format": "E, d LLL, y • h:mm a",
   "delete_dialog_alert": "Esses arquivos serão permanentemente apagados do Immich e de seu dispositivo",
   "delete_dialog_alert_local": "Estes arquivos serão permanentemente excluídos do seu dispositivo, mas continuarão disponíveis no servidor Immich",
   "delete_dialog_alert_local_non_backed_up": "Não há backup de alguns dos arquivos no servidor e eles serão excluídos permanentemente do seu dispositivo",
@@ -210,15 +229,28 @@
   "delete_shared_link_dialog_title": "Excluir link compartilhado",
   "description_input_hint_text": "Adicionar descrição...",
   "description_input_submit_error": "Erro ao atualizar a descrição, verifique o registo para obter mais detalhes",
-  "download_error": "Download Error",
-  "download_started": "Download started",
-  "download_sucess": "Download success",
-  "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_canceled": "Cancelado",
+  "download_complete": "Sucesso",
+  "download_enqueue": "Na fila",
+  "download_error": "Erro ao baixar",
+  "download_failed": "Falha",
+  "download_filename": "arquivo: {}",
+  "download_finished": "Concluído",
+  "downloading": "Baixando...",
+  "downloading_media": "Baixando mídia",
+  "download_notfound": "Não encontrado",
+  "download_paused": "Pausado",
+  "download_started": "Iniciando",
+  "download_sucess": "Baixado com sucesso",
+  "download_sucess_android": "O arquivo foi baixado na pasta DCIM/Immich",
+  "download_waiting_to_retry": "Tentando novamente",
   "edit_date_time_dialog_date_time": "Data e Hora",
   "edit_date_time_dialog_timezone": "Fuso horário",
-  "edit_image_title": "Edit",
+  "edit_image_title": "Editar",
   "edit_location_dialog_title": "Localização",
-  "error_saving_image": "Error: {}",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
+  "error_saving_image": "Erro: {}",
   "exif_bottom_sheet_description": "Adicionar Descrição...",
   "exif_bottom_sheet_details": "DETALHES",
   "exif_bottom_sheet_location": "LOCALIZAÇÃO",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Ativar visualização de grade experimental",
   "experimental_settings_subtitle": "Use por sua conta e risco!",
   "experimental_settings_title": "Experimental",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favoritos",
   "favorites_page_no_favorites": "Nenhum favorito encontrado",
   "favorites_page_title": "Favoritos",
   "filename_search": "Nome do arquivo ou extensão",
+  "filter": "Filtro",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Habilitar vibração",
   "haptic_feedback_title": "Vibração",
   "header_settings_add_header_tip": "Adicionar cabeçalho",
@@ -255,13 +293,16 @@
   "home_page_first_time_notice": "Se é a primeira vez que utiliza o aplicativo, certifique-se de marcar um ou mais álbuns do dispositivo para backup, assim a linha do tempo será preenchida com as fotos e vídeos.",
   "home_page_share_err_local": "Não é possível compartilhar arquivos locais com um link, ignorando",
   "home_page_upload_err_limit": "Só é possível enviar 30 arquivos por vez, ignorando",
-  "image_saved_successfully": "Image saved",
+  "ignore_icloud_photos": "ignorar fotos no iCloud",
+  "ignore_icloud_photos_description": "Fotos que estão armazenadas no iCloud não serão carregadas para o servidor do Immich",
+  "image_saved_successfully": "Imagem salva",
   "image_viewer_page_state_provider_download_error": "Erro ao baixar",
   "image_viewer_page_state_provider_download_started": "Baixando arquivo",
   "image_viewer_page_state_provider_download_success": "Baixado com sucesso",
   "image_viewer_page_state_provider_share_error": "Erro ao compartilhar",
   "invalid_date": "Data inválida",
   "invalid_date_format": "Formato de data inválido",
+  "library": "Biblioteca",
   "library_page_albums": "Álbuns",
   "library_page_archive": "Arquivado",
   "library_page_device_albums": "Álbuns no dispositivo",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Foto mais antiga",
   "library_page_sort_most_recent_photo": "Foto mais recente",
   "library_page_sort_title": "Título do álbum",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Escolha no mapa",
   "location_picker_latitude": "Latitude",
   "location_picker_latitude_error": "Digite uma latitude válida",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Fotos com movimento",
   "multiselect_grid_edit_date_time_err_read_only": "Não é possível editar a data de arquivo só leitura, ignorando",
   "multiselect_grid_edit_gps_err_read_only": "Não é possível editar a localização de arquivo só leitura, ignorando",
+  "my_albums": "Meus álbuns",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "Não há arquivos para exibir",
   "no_name": "Sem nome",
   "notification_permission_dialog_cancel": "Cancelar",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Dar permissões para ativar notificações",
   "notification_permission_list_tile_enable_button": "Ativar notificações",
   "notification_permission_list_tile_title": "Permissão de notificações",
+  "on_this_device": "Neste dispositivo",
   "partner_list_user_photos": "Fotos de {user}",
   "partner_list_view_all": "Ver tudo",
   "partner_page_add_partner": "Adicionar parceiro",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} não poderá mais acessar as suas fotos.",
   "partner_page_stop_sharing_title": "Parar de compartilhar as suas fotos?",
   "partner_page_title": "Parceiro",
+  "partners": "Parceiros",
+  "people": "Pessoas",
   "permission_onboarding_back": "Voltar",
   "permission_onboarding_continue_anyway": "Continuar mesmo assim",
   "permission_onboarding_get_started": "Começar",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Permissão concedida! Está tudo pronto.",
   "permission_onboarding_permission_limited": "Permissão limitada. Para permitir que o Immich faça backups e gerencie sua galeria, conceda permissões para fotos e vídeos nas configurações.",
   "permission_onboarding_request": "O Immich requer autorização para ver as suas fotos e vídeos.",
+  "places": "Lugares",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Preferências",
   "profile_drawer_app_logs": "Logs",
   "profile_drawer_client_out_of_date_major": "O aplicativo está desatualizado. Por favor, atualize para a versão mais recente.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Configurações",
   "profile_drawer_sign_out": "Sair",
   "profile_drawer_trash": "Lixeira",
+  "recently_added": "Adicionados Recentemente",
   "recently_added_page_title": "Adicionado recentemente",
-  "save_to_gallery": "Save to gallery",
+  "save": "Save",
+  "save_to_gallery": "Salvar na galeria",
   "scaffold_body_error_occurred": "Ocorreu um erro",
+  "search_albums": "Pesquisar Álbuns",
   "search_bar_hint": "Pesquisar em suas fotos",
   "search_filter_apply": "Aplicar filtro",
   "search_filter_camera": "Câmera",
@@ -428,6 +484,7 @@
   "search_page_places": "Locais",
   "search_page_recently_added": "Adicionado recentemente",
   "search_page_screenshots": "Capturas de tela",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Selfies",
   "search_page_things": "Objetos",
   "search_page_videos": "Vídeos",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Sugestões",
   "select_user_for_sharing_page_err_album": "Falha ao criar o álbum",
   "select_user_for_sharing_page_share_suggestions": "Sugestões",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "Versão do app",
   "server_info_box_latest_release": "Versão mais recente",
   "server_info_box_server_url": "URL do servidor",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Carregar imagem de pré-visualização",
   "setting_image_viewer_title": "Imagens",
   "setting_languages_apply": "Aplicar",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Idioma",
   "setting_notifications_notify_failures_grace_period": "Notifique falhas de backup em segundo plano: {}",
   "setting_notifications_notify_hours": "{} horas",
@@ -531,18 +590,20 @@
   "shared_link_info_chip_upload": "Enviar",
   "shared_link_manage_links": "Gerenciar links compartilhados",
   "shared_link_public_album": "Álbum público",
+  "shared_links": "Links compartilhados",
   "share_done": "Feito",
+  "shared_with_me": "Compartilhado comigo",
   "share_invite": "Convidar para o álbum",
   "sharing_page_album": "Álbuns compartilhados",
   "sharing_page_description": "Crie álbuns compartilhados para compartilhar fotos e vídeos com pessoas da sua rede.",
   "sharing_page_empty_list": "LISTA VAZIA",
   "sharing_silver_appbar_create_shared_album": "Criar álbum partilhado",
   "sharing_silver_appbar_shared_links": "Links compartilhados",
-  "sharing_silver_appbar_share_partner": "Partilhar com parceiro",
-  "sync": "Sync",
-  "sync_albums": "Sync albums",
-  "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
-  "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
+  "sharing_silver_appbar_share_partner": "Compartilhar com parceiro",
+  "sync": "Sincronizar",
+  "sync_albums": "Sincronizar álbuns",
+  "sync_albums_manual_subtitle": "Sincronizar todas as fotos e vídeos enviados para o álbum de backup selecionado",
+  "sync_upload_album_setting_subtitle": "Crie e envie suas fotos e vídeos para o álbum selecionado no Immich",
   "tab_controller_nav_library": "Biblioteca",
   "tab_controller_nav_photos": "Fotos",
   "tab_controller_nav_search": "Pesquisar",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "O carregamento em três estágios pode aumentar o desempenho do carregamento, mas causa uma carga de rede significativamente maior",
   "theme_setting_three_stage_loading_title": "Habilitar carregamento em três estágios",
   "translated_text_options": "Opções",
+  "trash": "Lixo",
   "trash_emptied": "Lixeira esvaziada",
   "trash_page_delete": "Excluir",
   "trash_page_delete_all": "Excluir tudo",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Deseja fazer o backup dos arquivos selecionados no servidor?",
   "upload_dialog_ok": "Enviar",
   "upload_dialog_title": "Enviar arquivo",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Entendi",
   "version_announcement_overlay_release_notes": "notas da versão",
   "version_announcement_overlay_text_1": "Olá, há um novo lançamento de",
   "version_announcement_overlay_text_2": "por favor, Verifique com calma as ",
   "version_announcement_overlay_text_3": "e certifique-se de que a configuração do docker-compose e do arquivo .env estejam atualizadas para evitar configurações incorretas, especialmente se utiliza o WatchTower ou qualquer outro mecanismo que faça atualização automática do servidor.",
   "version_announcement_overlay_title": "Nova versão do servidor disponível \uD83C\uDF89",
+  "videos": "Vídeos",
   "viewer_remove_from_stack": "Remover da pilha",
   "viewer_stack_use_as_main_asset": "Usar como foto principal",
-  "viewer_unstack": "Desempilhar"
+  "viewer_unstack": "Desempilhar",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/ro-RO.json b/mobile/assets/i18n/ro-RO.json
index 4cb043d196..571956e4d8 100644
--- a/mobile/assets/i18n/ro-RO.json
+++ b/mobile/assets/i18n/ro-RO.json
@@ -6,6 +6,8 @@
   "action_common_save": "Save",
   "action_common_select": "Select",
   "action_common_update": "Actualizează",
+  "add_a_name": "Add a name",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Adăugat în {album}",
   "add_to_album_bottom_sheet_already_exists": "Deja în {album}",
   "advanced_settings_log_level_title": "Nivel log: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Depanare",
   "album_info_card_backup_album_excluded": "EXCLUSE",
   "album_info_card_backup_album_included": "INCLUSE",
+  "albums": "Albums",
   "album_thumbnail_card_item": "1 element",
   "album_thumbnail_card_items": "{} elemente",
   "album_thumbnail_card_shared": "Distribuit",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Șterge din album",
   "album_viewer_appbar_share_to": "Distribuire către",
   "album_viewer_page_share_add_users": "Adaugă utilizatori",
+  "all": "All",
   "all_people_page_title": "Persoane",
   "all_videos_page_title": "Videoclipuri",
   "app_bar_signout_dialog_content": "Ești sigur că vrei să te deconectezi?",
   "app_bar_signout_dialog_ok": "Da",
   "app_bar_signout_dialog_title": "Deconectare",
+  "archived": "Archived",
   "archive_page_no_archived_assets": "Nu au fost găsite resurse favorite",
   "archive_page_title": "Arhivă ({})",
   "asset_action_delete_err_read_only": "Fișierele cu permisiuni doar de citire nu au putut fi șterse, omitere",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} asset(s) restored successfully",
   "assets_trashed": "{} asset(s) trashed",
   "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "Asset Viewer",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Albume în dispozitiv ({})",
   "backup_album_selection_page_albums_tap": "Apasă odata pentru a include, de două ori pentru a exclude",
   "backup_album_selection_page_assets_scatter": "Resursele pot fi împrăștiate în mai multe albume. Prin urmare, albumele pot fi incluse sau excluse în timpul procesului de backup.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Succes",
   "backup_manual_title": "Status încărcare",
   "backup_options_page_title": "Backup options",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Miniaturi pagină galerie ({} resurse)",
   "cache_settings_clear_cache_button": "Șterge cache",
   "cache_settings_clear_cache_button_title": "Șterge memoria cache a aplicatiei. Performanța aplicației va fi semnificativ afectată până când va fi reconstruită.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Controlează modul stocării locale",
   "cache_settings_tile_title": "Stocare locală",
   "cache_settings_title": "Setări pentru memoria cache",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Confirmă parola",
   "change_password_form_description": "Salut {name},\n\nAceasta este fie prima dată când te conectazi la sistem, fie s-a făcut o cerere pentru schimbarea parolei. Te rugăm să introduci noua parolă mai jos.",
   "change_password_form_new_password": "Parolă nouă",
   "change_password_form_password_mismatch": "Parolele nu se potrivesc",
   "change_password_form_reenter_new_password": "Reintrodu noua parolă",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Enter Password",
   "client_cert_import": "Import",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Șterge din arhivă",
   "control_bottom_app_bar_unfavorite": "Șterge din favorite",
   "control_bottom_app_bar_upload": "Încarcă",
+  "create_album": "Create album",
   "create_album_page_untitled": "Fără nume",
+  "create_new": "CREATE NEW",
   "create_shared_album_page_create": "Creează",
   "create_shared_album_page_share": "Distribuie",
   "create_shared_album_page_share_add_assets": "ADAUGĂ RESURSE",
@@ -193,6 +211,7 @@
   "crop": "Crop",
   "curated_location_page_title": "Locuri",
   "curated_object_page_title": "Obiecte",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "Șterge link distribuire",
   "description_input_hint_text": "Adaugă descriere...",
   "description_input_submit_error": "Eroare actualizare descriere, verifică log-urile pentru mai multe detalii",
+  "download_canceled": "Download canceled",
+  "download_complete": "Download complete",
+  "download_enqueue": "Download enqueued",
   "download_error": "Download Error",
+  "download_failed": "Download failed",
+  "download_filename": "file: {}",
+  "download_finished": "Download finished",
+  "downloading": "Downloading...",
+  "downloading_media": "Downloading media",
+  "download_notfound": "Download not found",
+  "download_paused": "Download paused",
   "download_started": "Download started",
   "download_sucess": "Download success",
   "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_waiting_to_retry": "Waiting to retry",
   "edit_date_time_dialog_date_time": "Dată și Oră",
   "edit_date_time_dialog_timezone": "Fus orar",
   "edit_image_title": "Edit",
   "edit_location_dialog_title": "Locație",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Error: {}",
   "exif_bottom_sheet_description": "Adaugă Descriere...",
   "exif_bottom_sheet_details": "DETALII",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Activează grila experimentală de fotografii.",
   "experimental_settings_subtitle": "Folosește pe propria răspundere!",
   "experimental_settings_title": "Experimental",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favorites",
   "favorites_page_no_favorites": "Nu au fost găsite resurse favorite",
   "favorites_page_title": "Favorite",
   "filename_search": "File name or extension",
+  "filter": "Filter",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Enable haptic feedback",
   "haptic_feedback_title": "Haptic Feedback",
   "header_settings_add_header_tip": "Add Header",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "Dacă este prima dată când utilizezi aplicația, te rugăm să te asiguri că alegi unul sau mai multe albume de backup, astfel încât cronologia să poată fi populată cu fotografiile și videoclipurile din aceste albume.",
   "home_page_share_err_local": "Nu se pot distribui fișiere locale prin link, omitere",
   "home_page_upload_err_limit": "Se pot încărca maxim 30 de resurse odată, omitere",
+  "ignore_icloud_photos": "Ignore iCloud photos",
+  "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
   "image_saved_successfully": "Image saved",
   "image_viewer_page_state_provider_download_error": "Eroare descărcare",
   "image_viewer_page_state_provider_download_started": "Download Started",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "Eroare distribuire",
   "invalid_date": "Invalid date",
   "invalid_date_format": "Invalid date format",
+  "library": "Library",
   "library_page_albums": "Albume",
   "library_page_archive": "Arhivă",
   "library_page_device_albums": "Albume în dispozitiv",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Cea mai veche fotografie",
   "library_page_sort_most_recent_photo": "Cea mai recentă fotografie",
   "library_page_sort_title": "Titlu album",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Alege pe hartă",
   "location_picker_latitude": "Latitudine",
   "location_picker_latitude_error": "Introdu o latitudine validă",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Fotografii în mișcare",
   "multiselect_grid_edit_date_time_err_read_only": "Nu se poate edita data fișierului(lor) cu permisiuni doar pentru citire, omitere",
   "multiselect_grid_edit_gps_err_read_only": "Nu se poate edita locația fișierului(lor) cu permisiuni doar pentru citire, omitere",
+  "my_albums": "My albums",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "No assets to show",
   "no_name": "No name",
   "notification_permission_dialog_cancel": "Anulează",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Acordă permisiunea pentru a activa notificările.",
   "notification_permission_list_tile_enable_button": "Activează notificările",
   "notification_permission_list_tile_title": "Permisiuni de notificare",
+  "on_this_device": "On this device",
   "partner_list_user_photos": "{user}'s photos",
   "partner_list_view_all": "View all",
   "partner_page_add_partner": "Adaugă partener",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} nu va mai putea accesa fotografiile tale.",
   "partner_page_stop_sharing_title": "Încetezi distribuirea fotografiilor?",
   "partner_page_title": "Partener",
+  "partners": "Partners",
+  "people": "People",
   "permission_onboarding_back": "Înapoi",
   "permission_onboarding_continue_anyway": "Continuă oricum",
   "permission_onboarding_get_started": "Începe",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Permisiune acordată!",
   "permission_onboarding_permission_limited": "Permisiune limitată. Pentru a permite Immich să facă copii de siguranță și să gestioneze întreaga colecție de galerii, acordă permisiuni pentru fotografii și videoclipuri în Setări.",
   "permission_onboarding_request": "Immich necesită permisiunea de a vizualiza fotografiile și videoclipurile tale.",
+  "places": "Places",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Preferences",
   "profile_drawer_app_logs": "Log-uri",
   "profile_drawer_client_out_of_date_major": "Aplicația nu folosește ultima versiune. Te rugăm să actulizezi la ultima versiune majoră.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Setări",
   "profile_drawer_sign_out": "Deconectare",
   "profile_drawer_trash": "Coș",
+  "recently_added": "Recently added",
   "recently_added_page_title": "Adăugate recent",
+  "save": "Save",
   "save_to_gallery": "Save to gallery",
   "scaffold_body_error_occurred": "A apărut o eroare",
+  "search_albums": "Search albums",
   "search_bar_hint": "Căutare fotografii",
   "search_filter_apply": "Apply filter",
   "search_filter_camera": "Camera",
@@ -428,6 +484,7 @@
   "search_page_places": "Locuri",
   "search_page_recently_added": "Adăugate recent",
   "search_page_screenshots": "Capturi de ecran",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Selfie-uri",
   "search_page_things": "Obiecte",
   "search_page_videos": "Videoclipuri",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Sugestii",
   "select_user_for_sharing_page_err_album": "Creare album eșuată",
   "select_user_for_sharing_page_share_suggestions": "Sugestii",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "Versiune Aplicatie",
   "server_info_box_latest_release": "Ultima versiune",
   "server_info_box_server_url": "URL-ul server-ului",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Încarcă imaginea de previzualizare",
   "setting_image_viewer_title": "Images",
   "setting_languages_apply": "Apply",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Languages",
   "setting_notifications_notify_failures_grace_period": "Notificare eșuări backup în fundal: {}",
   "setting_notifications_notify_hours": "{} ore",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Încarcă",
   "shared_link_manage_links": "Administrează link-urile distribuite",
   "shared_link_public_album": "Public album",
+  "shared_links": "Shared links",
   "share_done": "Gata",
+  "shared_with_me": "Shared with me",
   "share_invite": "Invită în album",
   "sharing_page_album": "Albume distribuite",
   "sharing_page_description": "Creeză albume de distribuire pentru a distribui fotografii și videoclipuri cu persoanele din rețeaua ta.",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Încărcarea în trei etape are putea crește performanța încărcării dar generează un volum semnificativ mai mare de trafic pe rețea",
   "theme_setting_three_stage_loading_title": "Pornește încărcarea în 3 etape",
   "translated_text_options": "Opțiuni",
+  "trash": "Trash",
   "trash_emptied": "Emptied trash",
   "trash_page_delete": "Șterge",
   "trash_page_delete_all": "Șterge tot",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Vrei să backup resursele selectate pe server?",
   "upload_dialog_ok": "Incarcă",
   "upload_dialog_title": "Încarcă resursă",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Confirm",
   "version_announcement_overlay_release_notes": "informații update",
   "version_announcement_overlay_text_1": "Salut, există un update nou pentru",
   "version_announcement_overlay_text_2": "te rugăm verifică",
   "version_announcement_overlay_text_3": "și asigură-te că fișierul .env și configurația ta docker-compose sunt actualizate pentru a preveni orice erori de configurație, în special dacă folosești WatchTower sau orice mecanism care gestionează actualizarea automată a aplicației server-ului tău.",
   "version_announcement_overlay_title": "O nouă versiune pentru server este disponibilă  \uD83C\uDF89",
+  "videos": "Videos",
   "viewer_remove_from_stack": "Șterge din grup",
   "viewer_stack_use_as_main_asset": "Folosește ca resursă principală",
-  "viewer_unstack": "Anulează grup"
+  "viewer_unstack": "Anulează grup",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/ru-RU.json b/mobile/assets/i18n/ru-RU.json
index 1c5741a963..92d24b6a41 100644
--- a/mobile/assets/i18n/ru-RU.json
+++ b/mobile/assets/i18n/ru-RU.json
@@ -6,41 +6,46 @@
   "action_common_save": "Сохранить",
   "action_common_select": "Выбрать",
   "action_common_update": "Обновить",
+  "add_a_name": "Добавить имя",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Добавлено в {album}",
   "add_to_album_bottom_sheet_already_exists": "Уже в {album}",
-  "advanced_settings_log_level_title": "Log level: {}",
-  "advanced_settings_prefer_remote_subtitle": "Некоторые устройства очень медленно загружают предпросмотр объектов, находящихся на устройстве. Активируйте эту настройку, чтобы вместо них загружались изображения с сервера.",
+  "advanced_settings_log_level_title": "Уровень логирования:",
+  "advanced_settings_prefer_remote_subtitle": "Некоторые устройства очень медленно загружают локальные изображения. Активируйте эту настройку, чтобы изображения всегда загружались с сервера.",
   "advanced_settings_prefer_remote_title": "Предпочитать фото на сервере",
-  "advanced_settings_proxy_headers_subtitle": "Визначте заголовки проксі-сервера, які Immich має надсилати з кожним мережевим запитом.",
-  "advanced_settings_proxy_headers_title": "Прокси-заголовки",
-  "advanced_settings_self_signed_ssl_subtitle": "Пропускает проверку SSL-сертификата сервера. Требуется для самоподписанных сертификатов.",
+  "advanced_settings_proxy_headers_subtitle": "Определите заголовки прокси-сервера, которые Immich должен отправлять с каждым сетевым запросом.",
+  "advanced_settings_proxy_headers_title": "Заголовки прокси",
+  "advanced_settings_self_signed_ssl_subtitle": "Пропускать проверку SSL-сертификата сервера. Требуется для самоподписанных сертификатов.",
   "advanced_settings_self_signed_ssl_title": "Разрешить самоподписанные SSL-сертификаты",
-  "advanced_settings_tile_subtitle": "Расширенные настройки пользователя",
+  "advanced_settings_tile_subtitle": "Расширенные настройки",
   "advanced_settings_tile_title": "Расширенные",
   "advanced_settings_troubleshooting_subtitle": "Включить расширенные возможности для решения проблем",
   "advanced_settings_troubleshooting_title": "Решение проблем",
   "album_info_card_backup_album_excluded": "ИСКЛЮЧЕН",
   "album_info_card_backup_album_included": "ВКЛЮЧЕН",
-  "album_thumbnail_card_item": "1 объект",
-  "album_thumbnail_card_items": "{} объектов",
+  "albums": "Альбомы",
+  "album_thumbnail_card_item": "1 элемент",
+  "album_thumbnail_card_items": "{} элементов",
   "album_thumbnail_card_shared": "· Общий",
   "album_thumbnail_owned": "Автор",
   "album_thumbnail_shared_by": "Поделился {}",
-  "album_viewer_appbar_delete_confirm": "Вы уверены, что хотите удалить этот альбом из своей учетной записи?",
+  "album_viewer_appbar_delete_confirm": "Вы уверены, что хотите удалить альбом из своей учетной записи?",
   "album_viewer_appbar_share_delete": "Удалить альбом",
-  "album_viewer_appbar_share_err_delete": "Невозможно удалить альбом",
-  "album_viewer_appbar_share_err_leave": "Невозможно покинуть альбом",
+  "album_viewer_appbar_share_err_delete": "Не удалось удалить альбом",
+  "album_viewer_appbar_share_err_leave": "Не удалось покинуть альбом",
   "album_viewer_appbar_share_err_remove": "Возникли проблемы с удалением объектов из альбома",
-  "album_viewer_appbar_share_err_title": "Ошибка переименования альбома",
+  "album_viewer_appbar_share_err_title": "Не удалось переименовать альбом",
   "album_viewer_appbar_share_leave": "Покинуть альбом",
   "album_viewer_appbar_share_remove": "Удалить из альбома",
   "album_viewer_appbar_share_to": "Поделиться",
   "album_viewer_page_share_add_users": "Добавить пользователей",
+  "all": "Все",
   "all_people_page_title": "Люди",
   "all_videos_page_title": "Видео",
-  "app_bar_signout_dialog_content": "Вы уверены, что хотите выйти из системы?",
+  "app_bar_signout_dialog_content": "Вы уверены, что хотите выйти?",
   "app_bar_signout_dialog_ok": "Да",
-  "app_bar_signout_dialog_title": "Выйти из системы",
+  "app_bar_signout_dialog_title": "Выйти",
+  "archived": "Архив",
   "archive_page_no_archived_assets": "В архиве сейчас пусто",
   "archive_page_title": "Архив ({})",
   "asset_action_delete_err_read_only": "Невозможно удалить объект(ы) только для чтения, пропуск...",
@@ -48,11 +53,11 @@
   "asset_list_group_by_sub_title": "Группировать по",
   "asset_list_layout_settings_dynamic_layout_title": "Динамическое расположение",
   "asset_list_layout_settings_group_automatically": "Автоматически",
-  "asset_list_layout_settings_group_by": "Группировать объекты по:",
+  "asset_list_layout_settings_group_by": "Группировать объекты по",
   "asset_list_layout_settings_group_by_month": "Месяцу",
   "asset_list_layout_settings_group_by_month_day": "Месяцу и дню",
   "asset_list_layout_sub_title": "Разметка",
-  "asset_list_settings_subtitle": "Настройка макета сетки фотографий",
+  "asset_list_settings_subtitle": "Настройка сетки фотографий",
   "asset_list_settings_title": "Сетка фотографий",
   "asset_restored_successfully": "Объект успешно восстановлен",
   "assets_deleted_permanently": "{}  объект(ы) удален(ы) навсегда",
@@ -61,11 +66,16 @@
   "assets_restored_successfully": "{}  объект(ы) успешно восстановлен(ы)",
   "assets_trashed": "{} объект(ы) помещен(ы) в корзину",
   "assets_trashed_from_server": "{} объект(ы) помещен(ы) в корзину на сервере Immich",
-  "asset_viewer_settings_title": "Просмотрщик изображений",
-  "backup_album_selection_page_albums_device": "Альбомов на устройстве ({})",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
+  "asset_viewer_settings_title": "Просмотр изображений",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
+  "backup_album_selection_page_albums_device": "Альбомы на устройстве ({})",
   "backup_album_selection_page_albums_tap": "Нажмите, чтобы включить,\nнажмите дважды, чтобы исключить",
-  "backup_album_selection_page_assets_scatter": "Объекты могут быть разбросаны по нескольким альбомам. Таким образом, альбомы могут быть включены или исключены из процесса резервного копирования.",
-  "backup_album_selection_page_select_albums": "Выбрать альбомы",
+  "backup_album_selection_page_assets_scatter": "Ваши изображения и видео могут находиться в разных альбомах. Вы можете выбрать, какие альбомы включить, а какие исключить из резервного копирования.",
+  "backup_album_selection_page_select_albums": "Выбор альбомов",
   "backup_album_selection_page_selection_info": "Информация о выборе",
   "backup_album_selection_page_total_assets": "Всего уникальных объектов",
   "backup_all": "Все",
@@ -77,11 +87,11 @@
   "backup_background_service_in_progress_notification": "Резервное копирование ваших объектов…",
   "backup_background_service_upload_failure_notification": "Ошибка загрузки {}",
   "backup_controller_page_albums": "Резервное копирование альбомов",
-  "backup_controller_page_background_app_refresh_disabled_content": "Включите фоновое обновление приложений в меню Настройки > Общие > Фоновое обновление приложений, чтобы использовать фоновое резервное копирование.",
+  "backup_controller_page_background_app_refresh_disabled_content": "Включите фоновое обновление приложения в Настройки > Общие > Фоновое обновление приложений, чтобы использовать фоновое резервное копирование.",
   "backup_controller_page_background_app_refresh_disabled_title": "Фоновое обновление отключено",
   "backup_controller_page_background_app_refresh_enable_button_text": "Перейти в настройки",
-  "backup_controller_page_background_battery_info_link": "Показать как",
-  "backup_controller_page_background_battery_info_message": "Для наилучшего фонового резервного копирования отключите любые настройки оптимизации батареи, ограничивающие фоновую активность для Immich.\n\nПоскольку это зависит от устройства, найдите необходимую информацию для производителя вашего устройства.",
+  "backup_controller_page_background_battery_info_link": "Подробнее",
+  "backup_controller_page_background_battery_info_message": "Для стабильного резервного копирования в фоновом режиме, отключите любые настройки оптимизации батареи, ограничивающие фоновую активность приложения.\n\nПоскольку настройки зависят от устройства, найдите необходимую информацию для производителя вашего устройства.",
   "backup_controller_page_background_battery_info_ok": "ОК",
   "backup_controller_page_background_battery_info_title": "Оптимизация батареи",
   "backup_controller_page_background_charging": "Только во время зарядки",
@@ -98,7 +108,7 @@
   "backup_controller_page_backup_sub": "Загруженные фото и видео",
   "backup_controller_page_cancel": "Отмена",
   "backup_controller_page_created": "Создано: {}",
-  "backup_controller_page_desc_backup": "Включите резервное копирование в активном режиме, чтобы автоматически загружать новые объекты на сервер при открытии приложения.",
+  "backup_controller_page_desc_backup": "Включите резервное копирование в активном режиме, чтобы автоматически загружать новые объекты при открытии приложения.",
   "backup_controller_page_excluded": "Исключены:",
   "backup_controller_page_failed": "Неудачных ({})",
   "backup_controller_page_filename": "Имя файла: {} [{}]",
@@ -106,7 +116,7 @@
   "backup_controller_page_info": "Информация о резервном копировании",
   "backup_controller_page_none_selected": "Ничего не выбрано",
   "backup_controller_page_remainder": "Осталось",
-  "backup_controller_page_remainder_sub": "Оставшиеся фото и видео для резервного копирования из выбранного",
+  "backup_controller_page_remainder_sub": "Фото и видео для загрузки",
   "backup_controller_page_select": "Выбор",
   "backup_controller_page_server_storage": "Хранилище на сервере",
   "backup_controller_page_start_backup": "Начать резервное копирование",
@@ -116,20 +126,21 @@
   "backup_controller_page_to_backup": "Альбомы для резервного копирования",
   "backup_controller_page_total": "Всего",
   "backup_controller_page_total_sub": "Все уникальные фото и видео из выбранных альбомов",
-  "backup_controller_page_turn_off": "Выключить резервное копирование в активном режиме",
-  "backup_controller_page_turn_on": "Включить резервное копирование в активном режиме",
+  "backup_controller_page_turn_off": "Выключить",
+  "backup_controller_page_turn_on": "Включить",
   "backup_controller_page_uploading_file_info": "Информация о загружаемом файле",
   "backup_err_only_album": "Невозможно удалить единственный альбом",
   "backup_info_card_assets": "объектов",
   "backup_manual_cancelled": "Отменено",
   "backup_manual_failed": "Неудачно",
-  "backup_manual_in_progress": "Загрузка уже в процессе, попробуйте позже",
+  "backup_manual_in_progress": "Загрузка в процессе. Попробуйте позже",
   "backup_manual_success": "Успешно",
   "backup_manual_title": "Статус загрузки",
   "backup_options_page_title": "Резервное копирование",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Миниатюры страниц библиотеки ({} объектов)",
   "cache_settings_clear_cache_button": "Очистить кэш",
-  "cache_settings_clear_cache_button_title": "Очищает кэш приложения. Это значительно повлияет на производительность приложения, до тех пор, пока кэш не будет перестроен заново.",
+  "cache_settings_clear_cache_button_title": "Очищает кэш приложения. Это негативно повлияет на производительность, пока кэш не будет создан заново.",
   "cache_settings_duplicated_assets_clear_button": "ОЧИСТИТЬ",
   "cache_settings_duplicated_assets_subtitle": "Фото и видео, занесенные приложением в черный список",
   "cache_settings_duplicated_assets_title": "Дублирующиеся объекты ({})",
@@ -140,16 +151,21 @@
   "cache_settings_statistics_shared": "Миниатюры общих альбомов",
   "cache_settings_statistics_thumbnail": "Миниатюры",
   "cache_settings_statistics_title": "Размер кэша",
-  "cache_settings_subtitle": "Управление кэшированием мобильного приложения Immich",
-  "cache_settings_thumbnail_size": "Размер кэша эскизов ({} объектов)",
-  "cache_settings_tile_subtitle": "Управление поведением локального хранилища",
+  "cache_settings_subtitle": "Управление кэшированием мобильного приложения",
+  "cache_settings_thumbnail_size": "Размер кэша миниатюр ({} объектов)",
+  "cache_settings_tile_subtitle": "Управление локальным хранилищем",
   "cache_settings_tile_title": "Локальное хранилище",
   "cache_settings_title": "Настройки кэширования",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Подтвердите пароль",
-  "change_password_form_description": "Привет {name},\n\nЭто либо ваш первый вход в систему, либо был сделан запрос на смену пароля. Пожалуйста, введите новый пароль ниже.",
+  "change_password_form_description": "Привет, {name}!\n\nЛибо ваш первый вход в систему, либо вы запросили смену пароля. Пожалуйста, введите новый пароль ниже.",
   "change_password_form_new_password": "Новый пароль",
   "change_password_form_password_mismatch": "Пароли не совпадают",
   "change_password_form_reenter_new_password": "Повторно введите новый пароль",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Введите пароль",
   "client_cert_import": "Импорт",
@@ -157,7 +173,7 @@
   "client_cert_invalid_msg": "Неверный файл сертификата или неверный пароль",
   "client_cert_remove": "Удалить",
   "client_cert_remove_msg": "Клиентский сертификат удален",
-  "client_cert_subtitle": "Поддерживается только формат PKCS12 (.p12, .pfx). Импорт/удаление сертификата доступно только перед входом в систему.",
+  "client_cert_subtitle": "Поддерживается только формат PKCS12 (.p12, .pfx). Импорт/удаление сертификата доступно только перед входом в систему",
   "client_cert_title": "Клиентский SSL-сертификат ",
   "common_add_to_album": "Добавить в альбом",
   "common_change_password": "Изменить пароль",
@@ -166,39 +182,42 @@
   "common_shared": "Общие",
   "contextual_search": "Восход солнца на пляже",
   "control_bottom_app_bar_add_to_album": "Добавить в альбом",
-  "control_bottom_app_bar_album_info": "{} файлов",
-  "control_bottom_app_bar_album_info_shared": "{} файлов · Общий",
+  "control_bottom_app_bar_album_info": "{} элементов",
+  "control_bottom_app_bar_album_info_shared": "{} элементов · Общий",
   "control_bottom_app_bar_archive": "Архив",
-  "control_bottom_app_bar_create_new_album": "Создать новый альбом",
+  "control_bottom_app_bar_create_new_album": "Создать альбом",
   "control_bottom_app_bar_delete": "Удалить",
   "control_bottom_app_bar_delete_from_immich": "Удалить из Immich\n",
   "control_bottom_app_bar_delete_from_local": "Удалить с устройства",
   "control_bottom_app_bar_download": "Скачать",
-  "control_bottom_app_bar_edit": "Редактировать",
-  "control_bottom_app_bar_edit_location": "Редактировать местоположение",
-  "control_bottom_app_bar_edit_time": "Редактировать дату и время",
+  "control_bottom_app_bar_edit": "Изменить",
+  "control_bottom_app_bar_edit_location": "Изменить место",
+  "control_bottom_app_bar_edit_time": "Изменить дату",
   "control_bottom_app_bar_favorite": "В избранное",
   "control_bottom_app_bar_share": "Поделиться",
   "control_bottom_app_bar_share_to": "Поделиться",
   "control_bottom_app_bar_stack": "Стек",
-  "control_bottom_app_bar_trash_from_immich": "Переместить в корзину",
+  "control_bottom_app_bar_trash_from_immich": "В корзину",
   "control_bottom_app_bar_unarchive": "Восстановить",
   "control_bottom_app_bar_unfavorite": "Удалить из избранного",
   "control_bottom_app_bar_upload": "Загрузить",
+  "create_album": "Создать альбом",
   "create_album_page_untitled": "Без названия",
+  "create_new": "СОЗДАТЬ НОВЫЙ",
   "create_shared_album_page_create": "Создать",
   "create_shared_album_page_share": "Поделиться",
   "create_shared_album_page_share_add_assets": "ДОБАВИТЬ ОБЪЕКТЫ",
-  "create_shared_album_page_share_select_photos": "Выберите фотографии",
-  "crop": "Crop",
+  "create_shared_album_page_share_select_photos": "Выбрать фотографии",
+  "crop": "Обрезать",
   "curated_location_page_title": "Места",
   "curated_object_page_title": "Предметы",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
-  "delete_dialog_alert": "Эти элементы будут безвозвратно удалены с сервера Immich, а также с вашего устройства",
-  "delete_dialog_alert_local": "Эти объекты будут безвозвратно удалены с Вашего устройства, но по-прежнему будут доступны на сервере Immich",
-  "delete_dialog_alert_local_non_backed_up": "Резервные копии некоторых объектов не были загружены в Immich и будут безвозвратно удалены с Вашего устройства",
+  "delete_dialog_alert": "Эти элементы будут безвозвратно удалены с сервера, а также с вашего устройства",
+  "delete_dialog_alert_local": "Эти объекты будут безвозвратно удалены с вашего устройства, но по-прежнему будут доступны на сервере Immich",
+  "delete_dialog_alert_local_non_backed_up": "Резервные копии некоторых объектов не были загружены в Immich и будут безвозвратно удалены с вашего устройства",
   "delete_dialog_alert_remote": "Эти объекты будут безвозвратно удалены с сервера Immich",
   "delete_dialog_cancel": "Отменить",
   "delete_dialog_ok": "Удалить",
@@ -206,32 +225,51 @@
   "delete_dialog_title": "Удалить навсегда",
   "delete_local_dialog_ok_backed_up_only": "Удалить только резервные копии",
   "delete_local_dialog_ok_force": "Все равно удалить",
-  "delete_shared_link_dialog_content": "Вы уверены, что хотите удалить эту общую ссылку?",
-  "delete_shared_link_dialog_title": "Удалить общую ссылку",
+  "delete_shared_link_dialog_content": "Вы уверены, что хотите удалить публичную ссылку?",
+  "delete_shared_link_dialog_title": "Удалить публичную ссылку",
   "description_input_hint_text": "Добавить описание...",
   "description_input_submit_error": "Не удалось обновить описание, проверьте логи, чтобы узнать причину",
-  "download_error": "Download Error",
-  "download_started": "Download started",
-  "download_sucess": "Download success",
-  "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_canceled": "Загрузка отменена",
+  "download_complete": "Загрузка окончена",
+  "download_enqueue": "Загрузка в очереди",
+  "download_error": "Ошибка загрузки",
+  "download_failed": "Загрузка не удалась",
+  "download_filename": "файл: {}",
+  "download_finished": "Загрузка окончена",
+  "downloading": "Загрузка...",
+  "downloading_media": "Загрузка медиа",
+  "download_notfound": "Загрузка не найдена",
+  "download_paused": "Загрузка приостановлена",
+  "download_started": "Загрузка началась",
+  "download_sucess": "Успешная загрузка",
+  "download_sucess_android": "Медиафайлы загружены в DCIM/Immich",
+  "download_waiting_to_retry": "Ожидание повторной попытки",
   "edit_date_time_dialog_date_time": "Дата и время",
   "edit_date_time_dialog_timezone": "Часовой пояс",
-  "edit_image_title": "Edit",
+  "edit_image_title": "Редактировать",
   "edit_location_dialog_title": "Местоположение",
-  "error_saving_image": "Error: {}",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
+  "error_saving_image": "Ошибка: {}",
   "exif_bottom_sheet_description": "Добавить описание...",
   "exif_bottom_sheet_details": "ПОДРОБНОСТИ",
-  "exif_bottom_sheet_location": "Местоположение",
-  "exif_bottom_sheet_location_add": "Добавить местоположение",
+  "exif_bottom_sheet_location": "МЕСТО",
+  "exif_bottom_sheet_location_add": "Добавить место",
   "exif_bottom_sheet_people": "ЛЮДИ",
   "exif_bottom_sheet_person_add_person": "Добавить имя",
-  "experimental_settings_new_asset_list_subtitle": "Ведутся работы",
+  "experimental_settings_new_asset_list_subtitle": "В разработке",
   "experimental_settings_new_asset_list_title": "Включить экспериментальную сетку фотографий",
   "experimental_settings_subtitle": "Используйте на свой страх и риск!",
   "experimental_settings_title": "Экспериментальные функции",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Избранное",
   "favorites_page_no_favorites": "В избранном сейчас пусто",
   "favorites_page_title": "Избранное",
   "filename_search": "Имя или расширение файла",
+  "filter": "Фильтр",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Включить тактильную отдачу",
   "haptic_feedback_title": "Тактильная отдача",
   "header_settings_add_header_tip": "Добавить заголовок",
@@ -239,29 +277,32 @@
   "header_settings_header_name_input": "Имя заголовка",
   "header_settings_header_value_input": "Значение заголовка",
   "header_settings_page_title": "Прокси-заголовки",
-  "headers_settings_tile_subtitle": "Определите заголовки прокси, которые приложение должно отправлять с каждым сетевым запросом.",
+  "headers_settings_tile_subtitle": "Определите заголовки прокси, которые приложение должно отправлять с каждым сетевым запросом",
   "headers_settings_tile_title": "Пользовательские заголовки прокси",
-  "home_page_add_to_album_conflicts": "Добавлено {added} объектов в альбом {album}. Объекты {failed} уже есть в альбоме.",
-  "home_page_add_to_album_err_local": "Пока нельзя добавлять локальные объекты в альбомы, пропускаем",
-  "home_page_add_to_album_success": "Добавлено {added} объектов в альбом {album}.",
-  "home_page_album_err_partner": "Пока не удается добавить объекты партнера в альбом, пропуск...",
-  "home_page_archive_err_local": "Пока невозможно добавить локальные объекты в архив, пропускаем",
-  "home_page_archive_err_partner": "Невозможно архивировать объекты партнера, пропуск...",
+  "home_page_add_to_album_conflicts": "Добавлено {added} медиа в альбом {album}. {failed} медиа уже в альбоме.",
+  "home_page_add_to_album_err_local": "Пока нельзя добавлять локальные объекты в альбомы, пропуск",
+  "home_page_add_to_album_success": "Добавлено {added} медиа в альбом {album}.",
+  "home_page_album_err_partner": "Пока нельзя добавить медиа партнера в альбом, пропуск",
+  "home_page_archive_err_local": "Пока нельзя добавить локальные файлы в архив, пропуск",
+  "home_page_archive_err_partner": "Невозможно архивировать медиа партнера, пропуск",
   "home_page_building_timeline": "Построение хронологии",
-  "home_page_delete_err_partner": "Невозможно удалить объекты партнера, пропуск...",
-  "home_page_delete_remote_err_local": "Локальные объект(ы) уже в процессе удаления с сервера, пропуск...",
-  "home_page_favorite_err_local": "Пока не удается добавить в избранное локальные объекты, пропуск...",
-  "home_page_favorite_err_partner": "Пока не удается добавить в избранное объекты партнера, пропуск...",
-  "home_page_first_time_notice": "Если вы используете приложение впервые, убедитесь, что вы выбрали резервный(е) альбом(ы), чтобы временная шкала могла заполнить фотографии и видео в альбоме(ах).",
-  "home_page_share_err_local": "Невозможно поделиться локальными данными по ссылке, пропуск...",
-  "home_page_upload_err_limit": "Вы можете выгрузить максимум 30 файлов за раз",
-  "image_saved_successfully": "Image saved",
+  "home_page_delete_err_partner": "Невозможно удалить медиа партнера, пропуск",
+  "home_page_delete_remote_err_local": "Невозможно удалить локальные файлы с сервера, пропуск",
+  "home_page_favorite_err_local": "Пока нельзя добавить в избранное локальные файлы, пропуск",
+  "home_page_favorite_err_partner": "Пока нельзя добавить в избранное медиа партнера, пропуск",
+  "home_page_first_time_notice": "Если вы используете приложение впервые, выберите альбомы для резервного копирования или загрузите их вручную, чтобы заполнить ими временную шкалу.",
+  "home_page_share_err_local": "Нельзя поделиться локальными файлами по ссылке, пропуск",
+  "home_page_upload_err_limit": "Вы можете загрузить максимум 30 файлов за раз, пропуск",
+  "ignore_icloud_photos": "Пропускать файлы из iCloud",
+  "ignore_icloud_photos_description": "Не загружать файлы в Immich, если они хранятся в iCloud",
+  "image_saved_successfully": "Изображение сохранено",
   "image_viewer_page_state_provider_download_error": "Ошибка загрузки",
   "image_viewer_page_state_provider_download_started": "Загрузка началась",
   "image_viewer_page_state_provider_download_success": "Успешно загружено",
   "image_viewer_page_state_provider_share_error": "Ошибка общего доступа",
   "invalid_date": "Неверная дата",
   "invalid_date_format": "Неверный формат даты",
+  "library": "Библиотека",
   "library_page_albums": "Альбомы",
   "library_page_archive": "Архив",
   "library_page_device_albums": "Альбомы на устройстве",
@@ -271,39 +312,43 @@
   "library_page_sort_asset_count": "Количество объектов",
   "library_page_sort_created": "Недавно созданные",
   "library_page_sort_last_modified": "Последнее изменение",
-  "library_page_sort_most_oldest_photo": "Самые старые фото",
-  "library_page_sort_most_recent_photo": "Самые последние фото",
+  "library_page_sort_most_oldest_photo": "Старые фото",
+  "library_page_sort_most_recent_photo": "Последние фото",
   "library_page_sort_title": "Название альбома",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Выбрать на карте",
   "location_picker_latitude": "Широта",
   "location_picker_latitude_error": "Укажите правильную широту",
-  "location_picker_latitude_hint": "Укажите широту",
+  "location_picker_latitude_hint": "Введите широту",
   "location_picker_longitude": "Долгота",
   "location_picker_longitude_error": "Укажите правильную долготу",
-  "location_picker_longitude_hint": "Укажите долготу",
+  "location_picker_longitude_hint": "Введите долготу",
   "login_disabled": "Вход отключен",
-  "login_form_api_exception": "Ошибка при попытке взаимодействия с сервером. Проверьте URL-адрес до него и попробуйте еще раз.",
+  "login_form_api_exception": "Ошибка подключения к серверу. Проверьте URL-адрес и попробуйте еще раз.",
   "login_form_back_button_text": "Назад",
   "login_form_button_text": "Войти",
   "login_form_email_hint": "youremail@email.com",
   "login_form_endpoint_hint": "http://your-server-ip:port/api",
   "login_form_endpoint_url": "URL-aдрес сервера",
-  "login_form_err_http": "Пожалуйста, укажите http:// или https://",
-  "login_form_err_invalid_email": "Неверный адрес Email",
-  "login_form_err_invalid_url": "Неверная ссылка",
+  "login_form_err_http": "Пожалуйста, укажите протокол http:// или https://",
+  "login_form_err_invalid_email": "Некорректный адрес электронной почты",
+  "login_form_err_invalid_url": "Некорректный URL",
   "login_form_err_leading_whitespace": "Пробел до",
   "login_form_err_trailing_whitespace": "Пробел после",
   "login_form_failed_get_oauth_server_config": "Ошибка авторизации с использованием OAuth, проверьте URL-адрес сервера",
-  "login_form_failed_get_oauth_server_disable": "Функция OAuth недоступна на этом сервере.",
-  "login_form_failed_login": "Ошибка при входе в систему, проверьте URL-адрес сервера, адрес электронной почты и пароль",
-  "login_form_handshake_exception": "Произошло нарушение рукопожатия с сервером. Включите в настройках поддержку самоподписанных сертификатов, если вы используете самоподписанный сертификат.",
+  "login_form_failed_get_oauth_server_disable": "Авторизация через OAuth недоступна на этом сервере",
+  "login_form_failed_login": "Ошибка при входе, проверьте URL-адрес сервера, адрес электронной почты и пароль",
+  "login_form_handshake_exception": "Ошибка проверки сертификата. Если вы используете самоподписанный сертификат, включите поддержку самоподписанных сертификатов  в настройках.",
   "login_form_label_email": "Email",
   "login_form_label_password": "Пароль",
   "login_form_next_button": "Далее",
   "login_form_password_hint": "пароль",
   "login_form_save_login": "Оставаться в системе",
-  "login_form_server_empty": "Введите URL-адрес вашего сервера.",
-  "login_form_server_error": "Нет соединения с сервером.",
+  "login_form_server_empty": "Введите URL-адрес сервера.",
+  "login_form_server_error": "Не удалось установить соединение с сервером.",
   "login_password_changed_error": "Произошла ошибка при обновлении пароля",
   "login_password_changed_success": "Пароль успешно обновлен",
   "map_assets_in_bound": "{} фото",
@@ -312,37 +357,40 @@
   "map_location_dialog_cancel": "Отмена",
   "map_location_dialog_yes": "Да",
   "map_location_picker_page_use_location": "Это местоположение",
-  "map_location_service_disabled_content": "Для отображения объектов в данном месте необходимо включить службу определения местоположения. Хотите включить ее сейчас?",
+  "map_location_service_disabled_content": "Для отображения объектов в текущем месте необходимо включить службу определения местоположения. Включить?",
   "map_location_service_disabled_title": "Служба определения местоположения отключена",
   "map_no_assets_in_bounds": "Нет фотографий в этой области",
-  "map_no_location_permission_content": "Для отображения объектов из текущего местоположения необходимо разрешение на определение местоположения. Хотите ли вы разрешить его сейчас?",
+  "map_no_location_permission_content": "Для отображения объектов в текущем месте необходимо разрешение на определение местоположения. Предоставить разрешение?",
   "map_no_location_permission_title": "Доступ к местоположению отклонен",
   "map_settings_dark_mode": "Темный режим",
   "map_settings_date_range_option_all": "Все",
-  "map_settings_date_range_option_day": "Прошлые 24 часа",
-  "map_settings_date_range_option_days": "Прошлые {} дней",
-  "map_settings_date_range_option_year": "Прошлый год",
-  "map_settings_date_range_option_years": "Прошлые {} года",
+  "map_settings_date_range_option_day": "24 часа",
+  "map_settings_date_range_option_days": "{} дней",
+  "map_settings_date_range_option_year": "Год",
+  "map_settings_date_range_option_years": "{} года",
   "map_settings_dialog_cancel": "Отмена",
   "map_settings_dialog_save": "Сохранить",
   "map_settings_dialog_title": "Настройки карты",
-  "map_settings_include_show_archived": "Отображать архив",
-  "map_settings_include_show_partners": "Отображать снимки партнера",
+  "map_settings_include_show_archived": "Отображать архивированное",
+  "map_settings_include_show_partners": "Отображать медиа партнера",
   "map_settings_only_relative_range": "Период времени",
   "map_settings_only_show_favorites": "Показать только избранное",
-  "map_settings_theme_settings": "Тема карты",
+  "map_settings_theme_settings": "Цвет карты",
   "map_zoom_to_see_photos": "Уменьшение масштаба для просмотра фотографий",
   "memories_all_caught_up": "Это всё на сегодня",
   "memories_check_back_tomorrow": "Загляните завтра, чтобы увидеть больше воспоминаний",
   "memories_start_over": "Начать заново",
   "memories_swipe_to_close": "Смахните вверх, чтобы закрыть",
   "memories_year_ago": "Год назад",
-  "memories_years_ago": "{} лет назад",
+  "memories_years_ago": "Лет назад: {}",
   "monthly_title_text_date_format": "MMMM y",
   "motion_photos_page_title": "Динамические фото",
-  "multiselect_grid_edit_date_time_err_read_only": "Невозможно редактировать дату объектов только для чтения, пропуск...",
-  "multiselect_grid_edit_gps_err_read_only": "Невозможно редактировать местоположение объектов только для чтения, пропуск...",
-  "no_assets_to_show": "Объекты отсутствуют",
+  "multiselect_grid_edit_date_time_err_read_only": "Невозможно изменить дату файлов только для чтения, пропуск",
+  "multiselect_grid_edit_gps_err_read_only": "Невозможно изменить местоположение файлов только для чтения, пропуск",
+  "my_albums": "Мои альбомы",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
+  "no_assets_to_show": "Медиа отсутствуют",
   "no_name": "Без имени",
   "notification_permission_dialog_cancel": "Отмена",
   "notification_permission_dialog_content": "Чтобы включить уведомления, перейдите в «Настройки» и выберите «Разрешить».",
@@ -350,42 +398,50 @@
   "notification_permission_list_tile_content": "Предоставьте разрешение на включение уведомлений",
   "notification_permission_list_tile_enable_button": "Включить уведомления",
   "notification_permission_list_tile_title": "Разрешение на уведомление",
+  "on_this_device": "На этом устройстве",
   "partner_list_user_photos": "Фотографии {user}",
   "partner_list_view_all": "Посмотреть все",
   "partner_page_add_partner": "Добавить партнёра",
-  "partner_page_empty_message": "У вашего партнёра еще пока нет доступа к вашим фото",
+  "partner_page_empty_message": "У вашего партнёра еще нет доступа к вашим фото",
   "partner_page_no_more_users": "Выбраны все доступные пользователи",
   "partner_page_partner_add_failed": "Не удалось добавить партнёра",
   "partner_page_select_partner": "Выбрать партнёра",
   "partner_page_shared_to_title": "Поделиться с...",
   "partner_page_stop_sharing_content": "{} больше не сможет получить доступ к вашим фотографиям",
-  "partner_page_stop_sharing_title": "Закрыть доступ партнёра к вашим фото?",
+  "partner_page_stop_sharing_title": "Закрыть доступ к вашим фото?",
   "partner_page_title": "Партнёр",
+  "partners": "Партнёры",
+  "people": "Люди",
   "permission_onboarding_back": "Назад",
   "permission_onboarding_continue_anyway": "Все равно продолжить",
   "permission_onboarding_get_started": "Давайте начнём",
   "permission_onboarding_go_to_settings": "Перейти в настройки",
   "permission_onboarding_grant_permission": "Предоставить разрешение",
   "permission_onboarding_log_out": "Выйти",
-  "permission_onboarding_permission_denied": "Не удалось получить доступ.",
+  "permission_onboarding_permission_denied": "Не удалось получить доступ. Чтобы использовать приложение, разрешите доступ к \"Фото и видео\" в настройках.",
   "permission_onboarding_permission_granted": "Доступ получен! Всё готово.",
-  "permission_onboarding_permission_limited": "Доступ к файлам ограничен. Чтобы Immich мог создавать резервные копии и управлять вашей галереей, пожалуйста, предоставьте приложению разрешение на доступ к \"Фото и видео\" в Настройках.",
-  "permission_onboarding_request": "Immich просит вас предоставить разрешение на доступ к вашим фото и видео",
+  "permission_onboarding_permission_limited": "Доступ к файлам ограничен. Чтобы Immich мог создавать резервные копии и управлять вашей галереей, пожалуйста, предоставьте приложению разрешение на доступ к \"Фото и видео\" в настройках.",
+  "permission_onboarding_request": "Приложению необходимо разрешение на доступ к вашим фото и видео",
+  "places": "Места",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Параметры",
   "profile_drawer_app_logs": "Журнал",
-  "profile_drawer_client_out_of_date_major": "Версия мобильного приложения устарела. Пожалуйста, обновитесь до последней основной версии.",
-  "profile_drawer_client_out_of_date_minor": "Версия мобильного приложения устарела. Пожалуйста, обновитесь до последней вспомогательной версии.",
+  "profile_drawer_client_out_of_date_major": "Версия мобильного приложения устарела. Пожалуйста, обновите его.",
+  "profile_drawer_client_out_of_date_minor": "Версия мобильного приложения устарела. Пожалуйста, обновите его.",
   "profile_drawer_client_server_up_to_date": "Клиент и сервер обновлены",
   "profile_drawer_documentation": "Документация",
   "profile_drawer_github": "GitHub",
-  "profile_drawer_server_out_of_date_major": "Серверная версия устарела. Пожалуйста, обновитесь до последней основной версии.",
-  "profile_drawer_server_out_of_date_minor": "Серверная версия устарела. Пожалуйста, обновитесь до последней вспомогательной версии.",
+  "profile_drawer_server_out_of_date_major": "Версия сервера устарела. Пожалуйста, обновите его.",
+  "profile_drawer_server_out_of_date_minor": "Версия сервера устарела. Пожалуйста, обновите его.",
   "profile_drawer_settings": "Настройки",
   "profile_drawer_sign_out": "Выйти",
   "profile_drawer_trash": "Корзина",
+  "recently_added": "Недавно добавленные",
   "recently_added_page_title": "Недавно добавленные",
-  "save_to_gallery": "Save to gallery",
+  "save": "Save",
+  "save_to_gallery": "Сохранить в галерею",
   "scaffold_body_error_occurred": "Возникла ошибка",
+  "search_albums": "Поиск альбома",
   "search_bar_hint": "Поиск фотографий",
   "search_filter_apply": "Применить фильтр",
   "search_filter_camera": "Камера",
@@ -393,22 +449,22 @@
   "search_filter_camera_model": "Модель",
   "search_filter_camera_title": "Выберите тип камеры",
   "search_filter_date": "Дата",
-  "search_filter_date_interval": "{start} до {end}",
-  "search_filter_date_title": "Выберите диапазон дат",
+  "search_filter_date_interval": "{start} — {end}",
+  "search_filter_date_title": "Выберите промежуток",
   "search_filter_display_option_archive": "Архив",
   "search_filter_display_option_favorite": "Избранное",
   "search_filter_display_option_not_in_album": "Не в альбоме",
-  "search_filter_display_options": "Параметри відображення",
-  "search_filter_display_options_title": "Параметри відображення",
-  "search_filter_location": "Местоположение",
+  "search_filter_display_options": "Настройки отображения",
+  "search_filter_display_options_title": "Настройки отображения",
+  "search_filter_location": "Место",
   "search_filter_location_city": "Город",
   "search_filter_location_country": "Страна",
   "search_filter_location_state": "Регион",
-  "search_filter_location_title": "Выберите местонахождение",
-  "search_filter_media_type": "Тип носителя",
+  "search_filter_location_title": "Выберите место",
+  "search_filter_media_type": "Тип файла",
   "search_filter_media_type_all": "Все",
   "search_filter_media_type_image": "Изображения",
-  "search_filter_media_type_title": "Выберите тип носителя",
+  "search_filter_media_type_title": "Выберите тип медиа",
   "search_filter_media_type_video": "Видео",
   "search_filter_people": "Люди",
   "search_filter_people_title": "Выберите людей",
@@ -422,12 +478,13 @@
   "search_page_person_add_name_dialog_hint": "Имя",
   "search_page_person_add_name_dialog_save": "Сохранить",
   "search_page_person_add_name_dialog_title": "Добавить имя",
-  "search_page_person_add_name_subtitle": "Быстро найдите их по имени с помощью поиска",
+  "search_page_person_add_name_subtitle": "Быстро находите их по имени с помощью поиска",
   "search_page_person_add_name_title": "Добавить имя",
   "search_page_person_edit_name": "Редактировать имя",
   "search_page_places": "Места",
   "search_page_recently_added": "Недавно добавленные",
   "search_page_screenshots": "Снимки экрана",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Селфи",
   "search_page_things": "Предметы",
   "search_page_videos": "Видео",
@@ -440,27 +497,29 @@
   "select_additional_user_for_sharing_page_suggestions": "Предложения",
   "select_user_for_sharing_page_err_album": "Не удалось создать альбом",
   "select_user_for_sharing_page_share_suggestions": "Предложения",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "Версия приложения",
   "server_info_box_latest_release": "Последняя версия",
   "server_info_box_server_url": "URL сервера",
   "server_info_box_server_version": "Версия сервера",
-  "setting_image_viewer_help": "Полноэкранный просмотрщик сначала загружает изображение для предпросмотра в низком разрешении, затем загружает изображение в уменьшенном разрешении относительно оригинала (если включено) и в конце концов загружает оригинал (если включено).",
+  "setting_image_viewer_help": "При просмотре изображения сперва загружается миниатюра, затем \nуменьшенное изображение среднего качества (если включено), а затем оригинал (если включено).",
   "setting_image_viewer_original_subtitle": "Включите для загрузки исходного изображения в полном разрешении (большое!).\nОтключите, чтобы уменьшить объем данных (как сети, так и кэша устройства).",
   "setting_image_viewer_original_title": "Загружать исходное изображение",
-  "setting_image_viewer_preview_subtitle": "Включите для загрузки изображения среднего разрешения.\nОтключите, чтобы загружать оригинал напрямую или использовать только миниатюру.",
-  "setting_image_viewer_preview_title": "Загружать изображение для предварительного просмотра",
+  "setting_image_viewer_preview_subtitle": "Включите для загрузки изображения среднего разрешения.\nОтключите, чтобы загружать только оригинал или миниатюру.",
+  "setting_image_viewer_preview_title": "Загружать уменьшенное изображение",
   "setting_image_viewer_title": "Изображения",
   "setting_languages_apply": "Применить",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Язык",
   "setting_notifications_notify_failures_grace_period": "Уведомлять об ошибках фонового резервного копирования: {}",
-  "setting_notifications_notify_hours": "{} часов",
+  "setting_notifications_notify_hours": "{} ч.",
   "setting_notifications_notify_immediately": "немедленно",
-  "setting_notifications_notify_minutes": "{} минут",
+  "setting_notifications_notify_minutes": "{} мин.",
   "setting_notifications_notify_never": "никогда",
-  "setting_notifications_notify_seconds": "{} секунд",
+  "setting_notifications_notify_seconds": "{} сек.",
   "setting_notifications_single_progress_subtitle": "Подробная информация о ходе загрузки для каждого объекта",
   "setting_notifications_single_progress_title": "Показать ход выполнения фонового резервного копирования",
-  "setting_notifications_subtitle": "Настройка параметров уведомлени",
+  "setting_notifications_subtitle": "Настройка параметров уведомлений",
   "setting_notifications_title": "Уведомления",
   "setting_notifications_total_progress_subtitle": "Общий прогресс загрузки (выполнено/всего объектов)",
   "setting_notifications_total_progress_title": "Показать общий прогресс фонового резервного копирования",
@@ -474,11 +533,11 @@
   "share_add_title": "Добавить название",
   "share_assets_selected": "{} выбрано",
   "share_create_album": "Создать альбом",
-  "shared_album_activities_input_disable": "Комментирование отключено",
+  "shared_album_activities_input_disable": "Комментарии отключены",
   "shared_album_activities_input_hint": "Скажите что-нибудь",
-  "shared_album_activity_remove_content": "Хотите ли Вы удалить это сообщение?",
-  "shared_album_activity_remove_title": "Удалить сообщение",
-  "shared_album_activity_setting_subtitle": "Разрешить другим отвечат",
+  "shared_album_activity_remove_content": "Удалить сообщение?",
+  "shared_album_activity_remove_title": "Удалить",
+  "shared_album_activity_setting_subtitle": "Разрешить другим отвечать",
   "shared_album_activity_setting_title": "Комментарии и лайки",
   "shared_album_section_people_action_error": "Ошибка при выходе/удалении из альбома",
   "shared_album_section_people_action_leave": "Удалить пользователя из альбома",
@@ -486,19 +545,19 @@
   "shared_album_section_people_owner_label": "Владелец",
   "shared_album_section_people_title": "ЛЮДИ",
   "share_dialog_preparing": "Подготовка...",
-  "shared_link_app_bar_title": "Общие ссылки",
+  "shared_link_app_bar_title": "Публичные ссылки",
   "shared_link_clipboard_copied_massage": "Скопировано в буфер обмена",
   "shared_link_clipboard_text": "Ссылка: {}\nПароль: {}",
-  "shared_link_create_app_bar_title": "Создать ссылку общего доступа",
-  "shared_link_create_error": "Ошибка при создании общей ссылки",
+  "shared_link_create_app_bar_title": "Создать ссылку для общего доступа",
+  "shared_link_create_error": "Ошибка при создании публичной ссылки",
   "shared_link_create_info": "Разрешить всем, у кого есть ссылка, просматривать выбранные фото",
   "shared_link_create_submit_button": "Создать ссылку",
-  "shared_link_edit_allow_download": "Разрешить публичному пользователю скачивать файлы",
-  "shared_link_edit_allow_upload": "Разрешить публичному пользователю загружать файлы",
+  "shared_link_edit_allow_download": "Разрешить всем скачивать файлы",
+  "shared_link_edit_allow_upload": "Разрешить всем загружать файлы",
   "shared_link_edit_app_bar_title": "Редактировать ссылку",
-  "shared_link_edit_change_expiry": "Изменить срок действия доступа",
+  "shared_link_edit_change_expiry": "Изменить срок действия",
   "shared_link_edit_description": "Описание",
-  "shared_link_edit_description_hint": "Введите описание для общего доступа",
+  "shared_link_edit_description_hint": "Введите описание публичного доступа",
   "shared_link_edit_expire_after": "Истекает через",
   "shared_link_edit_expire_after_option_day": "1 день",
   "shared_link_edit_expire_after_option_days": "{} дней",
@@ -510,10 +569,10 @@
   "shared_link_edit_expire_after_option_never": "Никогда",
   "shared_link_edit_expire_after_option_year": "{} лет",
   "shared_link_edit_password": "Пароль",
-  "shared_link_edit_password_hint": "Введите пароль для общего доступа",
+  "shared_link_edit_password_hint": "Введите пароль для публичного доступа",
   "shared_link_edit_show_meta": "Показывать метаданные",
   "shared_link_edit_submit_button": "Обновить ссылку",
-  "shared_link_empty": "У вас нет общих ссылок",
+  "shared_link_empty": "У вас нет публичных ссылок",
   "shared_link_error_server_url_fetch": "Невозможно запросить URL с сервера",
   "shared_link_expired": "Срок действия истек",
   "shared_link_expires_day": "Истекает через {} день",
@@ -522,22 +581,24 @@
   "shared_link_expires_hours": "Истекает через {} часов",
   "shared_link_expires_minute": "Истекает через {} минуту",
   "shared_link_expires_minutes": "Истекает через {} минут",
-  "shared_link_expires_never": "Истекает ∞",
+  "shared_link_expires_never": "Вечная ссылка",
   "shared_link_expires_second": "Истекает через {} секунду",
   "shared_link_expires_seconds": "Истекает через {} секунд",
   "shared_link_individual_shared": "Индивидуальный общий доступ",
   "shared_link_info_chip_download": "Скачать",
   "shared_link_info_chip_metadata": "EXIF",
   "shared_link_info_chip_upload": "Загрузить",
-  "shared_link_manage_links": "Управление общими ссылками",
+  "shared_link_manage_links": "Управление публичными ссылками",
   "shared_link_public_album": "Публичный альбом",
+  "shared_links": "Публичные ссылки",
   "share_done": "Готово",
+  "shared_with_me": "Доступные мне",
   "share_invite": "Пригласить в альбом",
   "sharing_page_album": "Общие альбомы",
   "sharing_page_description": "Создавайте общие альбомы, чтобы делиться фотографиями и видео с людьми в вашей сети.",
   "sharing_page_empty_list": "ПУСТОЙ СПИСОК",
   "sharing_silver_appbar_create_shared_album": "Создать общий альбом",
-  "sharing_silver_appbar_shared_links": "Общие ссылки",
+  "sharing_silver_appbar_shared_links": "Публичные ссылки",
   "sharing_silver_appbar_share_partner": "Поделиться с партнёром",
   "sync": "Синхронизировать",
   "sync_albums": "Синхронизировать альбомы",
@@ -549,28 +610,29 @@
   "tab_controller_nav_sharing": "Общие",
   "theme_setting_asset_list_storage_indicator_title": "Показать индикатор хранилища на плитках объектов",
   "theme_setting_asset_list_tiles_per_row_title": "Количество объектов в строке ({})",
-  "theme_setting_colorful_interface_subtitle": "Применить основной цвет на поверхность фона.",
-  "theme_setting_colorful_interface_title": "Colorful interface",
+  "theme_setting_colorful_interface_subtitle": "Добавить оттенок к фону",
+  "theme_setting_colorful_interface_title": "Цвет фона",
   "theme_setting_dark_mode_switch": "Тёмная тема",
-  "theme_setting_image_viewer_quality_subtitle": "Настройка качества просмотра полноэкранных изображения",
+  "theme_setting_image_viewer_quality_subtitle": "Настройка качества просмотра изображения",
   "theme_setting_image_viewer_quality_title": "Качество просмотра изображений",
-  "theme_setting_primary_color_subtitle": "Выберите цвет для основных действий и акцентов.",
+  "theme_setting_primary_color_subtitle": "Основной цвет приложения.",
   "theme_setting_primary_color_title": "Основной цвет",
   "theme_setting_system_primary_color_title": "Использовать системный цвет",
   "theme_setting_system_theme_switch": "Автоматически (как в системе)",
   "theme_setting_theme_subtitle": "Настройка темы приложения",
   "theme_setting_theme_title": "Тема",
-  "theme_setting_three_stage_loading_subtitle": "Трехэтапная загрузка может повысить производительность загрузки, но вызывает значительно более высокую нагрузку на сеть",
+  "theme_setting_three_stage_loading_subtitle": "Трехэтапная загрузка может повысить производительность, но значительно нагружает сеть",
   "theme_setting_three_stage_loading_title": "Включить трехэтапную загрузку",
   "translated_text_options": "Опции",
+  "trash": "Корзина",
   "trash_emptied": "Корзина очищена",
   "trash_page_delete": "Удалить",
   "trash_page_delete_all": "Удалить все",
   "trash_page_empty_trash_btn": "Очистить корзину",
-  "trash_page_empty_trash_dialog_content": "Вы хотите очистить свою корзину? Эти объекты будут навсегда удалены из Immich.",
+  "trash_page_empty_trash_dialog_content": "Очистить корзину? Эти файлы будут навсегда удалены из Immich.",
   "trash_page_empty_trash_dialog_ok": "ОК",
-  "trash_page_info": "Удаленные элементы будут окончательно удалены через {} дней",
-  "trash_page_no_assets": "Удаленные объекты отсутсвуют",
+  "trash_page_info": "Элементы в корзине будут окончательно удалены через {} дней",
+  "trash_page_no_assets": "Корзина пуста",
   "trash_page_restore": "Восстановить",
   "trash_page_restore_all": "Восстановить все",
   "trash_page_select_assets_btn": "Выбранные объекты",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Хотите создать резервную копию выбранных объектов на сервере?",
   "upload_dialog_ok": "Загрузить",
   "upload_dialog_title": "Загрузить объект",
-  "version_announcement_overlay_ack": "Подтверждение",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
+  "version_announcement_overlay_ack": "Понятно",
   "version_announcement_overlay_release_notes": "примечания к выпуску",
-  "version_announcement_overlay_text_1": "Привет друг, вышел новый релиз",
-  "version_announcement_overlay_text_2": "пожалуйста, найдите время, чтобы посетить",
-  "version_announcement_overlay_text_3": " и убедитесь, что ваши настройки docker-compose и .env обновлены, чтобы предотвратить любые неправильные настройки, особенно если вы используете WatchTower или любой другой механизм, который обрабатывает обновление вашего серверного приложения автоматически.",
+  "version_announcement_overlay_text_1": "Привет, друг! Вышла новая версия",
+  "version_announcement_overlay_text_2": "пожалуйста, посетите",
+  "version_announcement_overlay_text_3": " и убедитесь, что ваши настройки docker-compose и .env обновлены, особенно если вы используете WatchTower или любой другой механизм, который автоматически обновляет сервер.",
   "version_announcement_overlay_title": "Доступна новая версия сервера \uD83C\uDF89",
+  "videos": "Видео",
   "viewer_remove_from_stack": "Удалить из стека",
   "viewer_stack_use_as_main_asset": "Использовать в качестве основного объекта",
-  "viewer_unstack": "Разобрать стек"
+  "viewer_unstack": "Разобрать стек",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/sk-SK.json b/mobile/assets/i18n/sk-SK.json
index 200db9e320..14d0e04524 100644
--- a/mobile/assets/i18n/sk-SK.json
+++ b/mobile/assets/i18n/sk-SK.json
@@ -6,6 +6,8 @@
   "action_common_save": "Save",
   "action_common_select": "Select",
   "action_common_update": "Aktualizovať",
+  "add_a_name": "Add a name",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Pridané do {album}",
   "add_to_album_bottom_sheet_already_exists": "Už v {album}",
   "advanced_settings_log_level_title": "Úroveň logovania: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Oprava chýb",
   "album_info_card_backup_album_excluded": "VYLÚČENÉ",
   "album_info_card_backup_album_included": "ZAHRNUTÉ",
+  "albums": "Albums",
   "album_thumbnail_card_item": "1 položka",
   "album_thumbnail_card_items": "{} položiek",
   "album_thumbnail_card_shared": "Zdieľané",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Odstrániť z albumu",
   "album_viewer_appbar_share_to": "Zdieľať s",
   "album_viewer_page_share_add_users": "Pridať používateľov",
+  "all": "All",
   "all_people_page_title": "Ľudia",
   "all_videos_page_title": "Videá",
   "app_bar_signout_dialog_content": "Skutočne sa chcete odhlásiť?",
   "app_bar_signout_dialog_ok": "Áno",
   "app_bar_signout_dialog_title": "Odhlásiť sa",
+  "archived": "Archived",
   "archive_page_no_archived_assets": "Žiadne archivované médiá",
   "archive_page_title": "Archív ({})",
   "asset_action_delete_err_read_only": "Nemožno vymazať položku len na čítanie, preskakujem",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} asset(s) restored successfully",
   "assets_trashed": "{} asset(s) trashed",
   "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "Zobrazovač položiek",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Albumy v zariadení ({})",
   "backup_album_selection_page_albums_tap": "Ťuknutím na položku ju zahrniete, dvojitým ťuknutím ju vylúčite",
   "backup_album_selection_page_assets_scatter": "Súbory môžu byť roztrúsené vo viacerých albumoch. To umožňuje zahrnúť alebo vylúčiť albumy počas procesu zálohovania.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Úspech",
   "backup_manual_title": "Stav nahrávania",
   "backup_options_page_title": "Možnosti zálohovania",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Náhľady stránok knižnice (položiek {})",
   "cache_settings_clear_cache_button": "Vymazať vyrovnávaciu pamäť",
   "cache_settings_clear_cache_button_title": "Vymaže vyrovnávaciu pamäť aplikácie. To výrazne ovplyvní výkon aplikácie, kým sa vyrovnávacia pamäť neobnoví.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Ovládanie správania lokálneho úložiska",
   "cache_settings_tile_title": "Lokálne úložisko",
   "cache_settings_title": "Nastavenia vyrovnávacej pamäte",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Potvrďte heslo",
   "change_password_form_description": "Dobrý deň, {name},\n\nBuď sa do systému prihlasujete prvýkrát, alebo bola podaná žiadosť o zmenu hesla. Prosím, zadajte nové heslo nižšie.",
   "change_password_form_new_password": "Nové heslo",
   "change_password_form_password_mismatch": "Heslá sa nezhodujú",
   "change_password_form_reenter_new_password": "Znova zadajte nové heslo",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Enter Password",
   "client_cert_import": "Import",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Odarchivovať",
   "control_bottom_app_bar_unfavorite": "Odznačiť ako obľúbené",
   "control_bottom_app_bar_upload": "Nahrať",
+  "create_album": "Create album",
   "create_album_page_untitled": "Bez názvu",
+  "create_new": "CREATE NEW",
   "create_shared_album_page_create": "Vytvoriť",
   "create_shared_album_page_share": "Zdieľať",
   "create_shared_album_page_share_add_assets": "Pridať položky",
@@ -193,6 +211,7 @@
   "crop": "Crop",
   "curated_location_page_title": "Miesta",
   "curated_object_page_title": "Veci",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "EEEE, d. MMMM",
   "daily_title_text_date_year": "EEEE, d. MMMM y",
   "date_format": "EEEE, d. MMMM y • H:mm",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "Odstrániť zdieľaný odkaz",
   "description_input_hint_text": "Pridať popis...",
   "description_input_submit_error": "Chyba pri aktualizovaní popisu, zobrazte log pre viac detailov",
+  "download_canceled": "Download canceled",
+  "download_complete": "Download complete",
+  "download_enqueue": "Download enqueued",
   "download_error": "Download Error",
+  "download_failed": "Download failed",
+  "download_filename": "file: {}",
+  "download_finished": "Download finished",
+  "downloading": "Downloading...",
+  "downloading_media": "Downloading media",
+  "download_notfound": "Download not found",
+  "download_paused": "Download paused",
   "download_started": "Download started",
   "download_sucess": "Download success",
   "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_waiting_to_retry": "Waiting to retry",
   "edit_date_time_dialog_date_time": "Dátum a čas",
   "edit_date_time_dialog_timezone": "Časové pásmo",
   "edit_image_title": "Edit",
   "edit_location_dialog_title": "Poloha",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Error: {}",
   "exif_bottom_sheet_description": "Pridať popis...",
   "exif_bottom_sheet_details": "PODROBNOSTI",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Povolenie experimentálnej mriežky fotografií",
   "experimental_settings_subtitle": "Používajte na vlastné riziko!",
   "experimental_settings_title": "Experimentálne",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favorites",
   "favorites_page_no_favorites": "Žiadne obľúbené médiá",
   "favorites_page_title": "Obľúbené",
   "filename_search": "File name or extension",
+  "filter": "Filter",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Povoliť hmatovú odozvu",
   "haptic_feedback_title": "Hmatová odozva",
   "header_settings_add_header_tip": "Add Header",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "Ak aplikáciu používate prvý krát, nezabudnite si vybrať zálohované albumy, aby sa na časovej osi mohli nachádzať fotografie a videá z vybraných albumoch.",
   "home_page_share_err_local": "Nemožno zdieľať lokálne médiá pomocou odkazu",
   "home_page_upload_err_limit": "Naraz môžete nahrať len 30 médií, preskakujem...",
+  "ignore_icloud_photos": "Ignore iCloud photos",
+  "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
   "image_saved_successfully": "Image saved",
   "image_viewer_page_state_provider_download_error": "Chyba sťahovania",
   "image_viewer_page_state_provider_download_started": "Sťahovanie sa začalo",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "Chyba zdieľania",
   "invalid_date": "Invalid date",
   "invalid_date_format": "Invalid date format",
+  "library": "Library",
   "library_page_albums": "Albumy",
   "library_page_archive": "Archív",
   "library_page_device_albums": "Albumy v zariadení",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Najstaršia fotka",
   "library_page_sort_most_recent_photo": "Najnovšia fotka",
   "library_page_sort_title": "Podľa názvu albumu",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Zvoľte mapu",
   "location_picker_latitude": "Zemepisná dĺžka",
   "location_picker_latitude_error": "Zadajte platnú zemepisnú dĺžku",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Pohyblivé fotky",
   "multiselect_grid_edit_date_time_err_read_only": "Nemožno upraviť dátum položky len na čítanie, preskakujem",
   "multiselect_grid_edit_gps_err_read_only": "Nemožno upraviť polohu položky len na čítanie, preskakujem",
+  "my_albums": "My albums",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "Žiadne položky",
   "no_name": "No name",
   "notification_permission_dialog_cancel": "Zrušiť",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Udeľte oprávnenie k aktivácii oznámení.",
   "notification_permission_list_tile_enable_button": "Povoliť upozornenia",
   "notification_permission_list_tile_title": "Povolenie oznámení",
+  "on_this_device": "On this device",
   "partner_list_user_photos": "Fotky používateľa {user}",
   "partner_list_view_all": "Zobraziť všetky",
   "partner_page_add_partner": "Pridať partnera",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} už nebude mať prístup ku vašim fotkám.",
   "partner_page_stop_sharing_title": "Zastaviť zdieľanie vašich fotiek?",
   "partner_page_title": "Partner",
+  "partners": "Partners",
+  "people": "People",
   "permission_onboarding_back": "Späť",
   "permission_onboarding_continue_anyway": "Pokračovať aj tak",
   "permission_onboarding_get_started": "Začať",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Povolenie udelené! Všetko je nastavené.",
   "permission_onboarding_permission_limited": "Povolenie obmedzené. Ak chcete, aby Immich zálohoval a spravoval celú vašu zbierku galérie, udeľte v Nastaveniach povolenia na fotografie a videá.",
   "permission_onboarding_request": "Immich vyžaduje povolenie na prezeranie vašich fotografií a videí.",
+  "places": "Places",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Preferencie",
   "profile_drawer_app_logs": "Logy",
   "profile_drawer_client_out_of_date_major": "Mobilná aplikácia je zastaralá. Prosím aktualizujte na najnovšiu verziu.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Nastavenia",
   "profile_drawer_sign_out": "Odhlásiť sa",
   "profile_drawer_trash": "Kôš",
+  "recently_added": "Recently added",
   "recently_added_page_title": "Nedávno pridané",
+  "save": "Save",
   "save_to_gallery": "Save to gallery",
   "scaffold_body_error_occurred": "Vyskytla sa chyba",
+  "search_albums": "Search albums",
   "search_bar_hint": "Prehľadajte svoje obrázky",
   "search_filter_apply": "Použiť filter",
   "search_filter_camera": "Camera",
@@ -428,6 +484,7 @@
   "search_page_places": "Miesta",
   "search_page_recently_added": "Nedávno pridané",
   "search_page_screenshots": "Snímky obrazovky",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Selfies",
   "search_page_things": "Veci",
   "search_page_videos": "Videá",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Návrhy",
   "select_user_for_sharing_page_err_album": "Nepodarilo sa vytvoriť album",
   "select_user_for_sharing_page_share_suggestions": "Návrhy",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "Verzia aplikácie",
   "server_info_box_latest_release": "Najnovšia verzia",
   "server_info_box_server_url": "URL Serveru",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Načítať náhľad obrázka",
   "setting_image_viewer_title": "Obrázky",
   "setting_languages_apply": "Použiť",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Jazyky",
   "setting_notifications_notify_failures_grace_period": "Oznámenie o zlyhaní zálohovania na pozadí: {}",
   "setting_notifications_notify_hours": "{} hodín",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Nahrať",
   "shared_link_manage_links": "Spravovať zdieľané odkazy",
   "shared_link_public_album": "Verejný album",
+  "shared_links": "Shared links",
   "share_done": "Hotovo",
+  "shared_with_me": "Shared with me",
   "share_invite": "Pozvať do albumu",
   "sharing_page_album": "Zdieľané albumy",
   "sharing_page_description": "Vytvárajte zdieľané albumy a zdieľajte fotografie a videá s ľuďmi vo vašej sieti.",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Trojstupňové načítanie môže zvýšiť výkonnosť načítania, ale vedie k výrazne vyššiemu zaťaženiu siete.",
   "theme_setting_three_stage_loading_title": "Povolenie trojstupňového načítavania",
   "translated_text_options": "Nastavenia",
+  "trash": "Trash",
   "trash_emptied": "Emptied trash",
   "trash_page_delete": "Vymazať",
   "trash_page_delete_all": "Vymazať všetky",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Chcete zálohovať zvolené médiá na server?",
   "upload_dialog_ok": "Nahrať",
   "upload_dialog_title": "Nahrať médiá",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Potvrdiť",
   "version_announcement_overlay_release_notes": "poznámky k vydaniu",
   "version_announcement_overlay_text_1": "Ahoj, je tu nová verzia",
   "version_announcement_overlay_text_2": "nájdite si čas na návštevu ",
   "version_announcement_overlay_text_3": " a uistite sa, že vaša konfigurácia docker-compose a .env je aktuálna, aby ste predišli nesprávnej konfigurácii, najmä ak používate WatchTower alebo akýkoľvek mechanizmus, ktorý podporuje automatické aktualizácie serverových aplikácií.",
   "version_announcement_overlay_title": "K dispozícii je nová verzia servera \uD83C\uDF89",
+  "videos": "Videos",
   "viewer_remove_from_stack": "Odstrániť zo zoskupenia",
   "viewer_stack_use_as_main_asset": "Použiť ako hlavnú fotku",
-  "viewer_unstack": "Odskupiť"
+  "viewer_unstack": "Odskupiť",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/sl-SI.json b/mobile/assets/i18n/sl-SI.json
index 7871d65de9..54b4dd5f71 100644
--- a/mobile/assets/i18n/sl-SI.json
+++ b/mobile/assets/i18n/sl-SI.json
@@ -3,9 +3,11 @@
   "action_common_cancel": "Prekliči",
   "action_common_clear": "Počisti",
   "action_common_confirm": "Potrdi",
-  "action_common_save": "Save",
-  "action_common_select": "Select",
+  "action_common_save": "Shrani",
+  "action_common_select": "Izberi",
   "action_common_update": "Posodobi",
+  "add_a_name": "Dodaj ime",
+  "add_endpoint": "Dodaj končno točko",
   "add_to_album_bottom_sheet_added": "Dodano v {album}",
   "add_to_album_bottom_sheet_already_exists": "Že v {albumu}",
   "advanced_settings_log_level_title": "Nivo dnevnika: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Odpravljanje težav",
   "album_info_card_backup_album_excluded": "IZKLJUČENO",
   "album_info_card_backup_album_included": "VKLJUČENO",
+  "albums": "Albumi",
   "album_thumbnail_card_item": "1 element",
   "album_thumbnail_card_items": "{} elementov",
   "album_thumbnail_card_shared": "· V skupni rabi",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Odstrani iz albuma",
   "album_viewer_appbar_share_to": "Deli z",
   "album_viewer_page_share_add_users": "Dodaj uporabnike",
+  "all": "Vse",
   "all_people_page_title": "Ljudje",
   "all_videos_page_title": "Videoposnetki",
   "app_bar_signout_dialog_content": "Ste prepričani, da se želite odjaviti?",
   "app_bar_signout_dialog_ok": "Da",
   "app_bar_signout_dialog_title": "Odjava",
+  "archived": "Arhivirano",
   "archive_page_no_archived_assets": "Ni arhiviranih sredstev",
   "archive_page_title": "Arhiv ({})\n",
   "asset_action_delete_err_read_only": "Sredstev samo za branje ni mogoče izbrisati, preskočim\n",
@@ -54,14 +59,19 @@
   "asset_list_layout_sub_title": "Postavitev",
   "asset_list_settings_subtitle": "Nastavitve postavitve mreže fotografij",
   "asset_list_settings_title": "Mreža fotografij",
-  "asset_restored_successfully": "Asset restored successfully",
-  "assets_deleted_permanently": "{} asset(s) deleted permanently",
-  "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server",
-  "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device",
-  "assets_restored_successfully": "{} asset(s) restored successfully",
-  "assets_trashed": "{} asset(s) trashed",
-  "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_restored_successfully": "Sredstvo uspešno obnovljeno",
+  "assets_deleted_permanently": "Št. za vedno izbrisanih sredstev: {}",
+  "assets_deleted_permanently_from_server": "Št. za vedno izbrisanih sredstev iz srežnika Immich: {}",
+  "assets_removed_permanently_from_device": "Št. za vedno izbrisanih sredstev iz vaše naprave: {}",
+  "assets_restored_successfully": "Št. uspešno obnovljenih sredstev: {}",
+  "assets_trashed": "Št. sredstev premaknjenih v smetnjak: {}",
+  "assets_trashed_from_server": "Št sredstev izbrisanih iz strežnika Immich: {}",
+  "asset_viewer_settings_subtitle": "Upravljaj nastavitve pregledovalnika galerije",
   "asset_viewer_settings_title": "Pregledovalnik sredstev",
+  "automatic_endpoint_switching_subtitle": "Povežite se lokalno prek določenega omrežja Wi-Fi, ko je na voljo, in uporabite druge povezave drugje",
+  "automatic_endpoint_switching_title": "Samodejno preklapljanje URL-jev",
+  "background_location_permission": "Dovoljenje za iskanje lokacije v ozadju",
+  "background_location_permission_content": "Ko deluje v ozadju mora imeti Immich za zamenjavo omrežij, *vedno* dostop do natančne lokacije, da lahko aplikacija prebere ime omrežja Wi-Fi",
   "backup_album_selection_page_albums_device": "Albumi v napravi ({})",
   "backup_album_selection_page_albums_tap": "Tapnite za vključitev, dvakrat tapnite za izključitev",
   "backup_album_selection_page_assets_scatter": "Sredstva so lahko razpršena po več albumih. Tako je mogoče med postopkom varnostnega kopiranja albume vključiti ali izključiti.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Uspeh",
   "backup_manual_title": "Status nalaganja",
   "backup_options_page_title": "Možnosti varnostne kopije",
+  "backup_setting_subtitle": "Upravljaj nastavitve nalaganja v ozadju in ospredju",
   "cache_settings_album_thumbnails": "Sličice strani knjižnice ({} sredstev)",
   "cache_settings_clear_cache_button": "Počisti predpomnilnik",
   "cache_settings_clear_cache_button_title": "Počisti predpomnilnik aplikacije. To bo znatno vplivalo na delovanje aplikacije, dokler se predpomnilnik ne obnovi.",
@@ -145,26 +156,31 @@
   "cache_settings_tile_subtitle": "Nadzoruj vedenje lokalnega shranjevanja",
   "cache_settings_tile_title": "Lokalna shramba",
   "cache_settings_title": "Nastavitve predpomnjenja",
+  "cancel": "Prekliči",
+  "change_display_order": "Spremeni vrstni red prikaza",
   "change_password_form_confirm_password": "Potrdi geslo",
   "change_password_form_description": "Pozdravljeni {name},\n\nTo je bodisi prvič, da se vpisujete v sistem ali pa je bila podana zahteva za spremembo vašega gesla. Spodaj vnesite novo geslo.",
   "change_password_form_new_password": "Novo geslo",
   "change_password_form_password_mismatch": "Gesli se ne ujemata",
   "change_password_form_reenter_new_password": "Znova vnesi novo geslo",
-  "client_cert_dialog_msg_confirm": "OK",
-  "client_cert_enter_password": "Enter Password",
-  "client_cert_import": "Import",
-  "client_cert_import_success_msg": "Client certificate is imported",
-  "client_cert_invalid_msg": "Invalid certificate file or wrong password",
-  "client_cert_remove": "Remove",
-  "client_cert_remove_msg": "Client certificate is removed",
-  "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login",
-  "client_cert_title": "SSL Client Certificate",
+  "check_corrupt_asset_backup": "Preverite poškodovane varnostne kopije sredstev",
+  "check_corrupt_asset_backup_button": "Izvedi preverjanje",
+  "check_corrupt_asset_backup_description": "To preverjanje zaženite samo prek omrežja Wi-Fi in potem, ko so vsa sredstva varnostno kopirana. Postopek lahko traja nekaj minut.",
+  "client_cert_dialog_msg_confirm": "V redu",
+  "client_cert_enter_password": "Vnesi geslo",
+  "client_cert_import": "Uvozi",
+  "client_cert_import_success_msg": "Potrdilo odjemalca je uvoženo",
+  "client_cert_invalid_msg": "Neveljavna datoteka potrdila ali napačno geslo",
+  "client_cert_remove": "Odstrani",
+  "client_cert_remove_msg": "Potrdilo odjemalca je odstranjeno",
+  "client_cert_subtitle": "Podpira samo format PKCS12 (.p12, .pfx). Uvoz/odstranitev potrdila je na voljo samo pred prijavo",
+  "client_cert_title": "Potrdilo odjemalca SSL",
   "common_add_to_album": "Dodaj v album",
   "common_change_password": "Zamenjaj geslo",
   "common_create_new_album": "Ustvari nov album",
   "common_server_error": "Preverite omrežno povezavo, preverite, ali je strežnik dosegljiv in ali sta različici aplikacije/strežnika združljivi.",
   "common_shared": "V skupni rabi",
-  "contextual_search": "Sunrise on the beach",
+  "contextual_search": "Sončni vzhod na plaži",
   "control_bottom_app_bar_add_to_album": "Dodaj v album",
   "control_bottom_app_bar_album_info": "{} elementov",
   "control_bottom_app_bar_album_info_shared": "{} elementov · V skupni rabi",
@@ -173,8 +189,8 @@
   "control_bottom_app_bar_delete": "Izbriši",
   "control_bottom_app_bar_delete_from_immich": "Izbriši iz Immicha",
   "control_bottom_app_bar_delete_from_local": "Izbriši iz naprave",
-  "control_bottom_app_bar_download": "Download",
-  "control_bottom_app_bar_edit": "Edit",
+  "control_bottom_app_bar_download": "Prenos",
+  "control_bottom_app_bar_edit": "Uredi",
   "control_bottom_app_bar_edit_location": "Uredi lokacijo",
   "control_bottom_app_bar_edit_time": "Uredi datum in uro",
   "control_bottom_app_bar_favorite": "Priljubljen",
@@ -185,14 +201,17 @@
   "control_bottom_app_bar_unarchive": "Odstrani iz arhiva",
   "control_bottom_app_bar_unfavorite": "Odstrani iz priljubljeno",
   "control_bottom_app_bar_upload": "Naloži",
+  "create_album": "Ustvari album",
   "create_album_page_untitled": "Brez naslova",
+  "create_new": "USTVARI NOVEGA",
   "create_shared_album_page_create": "Ustvari",
   "create_shared_album_page_share": "Deli",
   "create_shared_album_page_share_add_assets": "DODAJ SREDSTVO",
   "create_shared_album_page_share_select_photos": "Izberi fotografije",
-  "crop": "Crop",
+  "crop": "Obrezovanje",
   "curated_location_page_title": "Lokacije",
   "curated_object_page_title": "Stvari",
+  "current_server_address": "Trenutni naslov strežnika",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,15 +229,28 @@
   "delete_shared_link_dialog_title": "Izbriši povezavo skupne rabe",
   "description_input_hint_text": "Dodaj opis ...",
   "description_input_submit_error": "Napaka pri posodabljanju opisa, preverite dnevnik za več podrobnosti",
-  "download_error": "Download Error",
-  "download_started": "Download started",
-  "download_sucess": "Download success",
-  "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_canceled": "Prenos preklican",
+  "download_complete": "Prenos končan",
+  "download_enqueue": "Prenos v čakalni vrsti",
+  "download_error": "Napaka pri prenosu",
+  "download_failed": "Prenos ni uspel",
+  "download_filename": "datoteka: {}",
+  "download_finished": "Prenos zaključen",
+  "downloading": "Prenašam...",
+  "downloading_media": "Prenašanje medijev",
+  "download_notfound": "Prenosa ni bilo mogoče najti",
+  "download_paused": "Prenos zaustavljen",
+  "download_started": "Prenos se je začel",
+  "download_sucess": "Prenos uspešen",
+  "download_sucess_android": "Medij je bil prenesen v DCIM/Immich",
+  "download_waiting_to_retry": "Čakam na ponovni poskus",
   "edit_date_time_dialog_date_time": "Datum in ura",
   "edit_date_time_dialog_timezone": "Časovni pas",
-  "edit_image_title": "Edit",
+  "edit_image_title": "Urejanje",
   "edit_location_dialog_title": "Lokacija",
-  "error_saving_image": "Error: {}",
+  "enter_wifi_name": "Vnesi WiFi ime",
+  "error_change_sort_album": "Vrstnega reda albuma ni bilo mogoče spremeniti",
+  "error_saving_image": "Napaka: {}",
   "exif_bottom_sheet_description": "Dodaj opis..",
   "exif_bottom_sheet_details": "PODROBNOSTI",
   "exif_bottom_sheet_location": "LOKACIJA",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Omogoči eksperimentalno mrežo fotografij",
   "experimental_settings_subtitle": "Uporabljajte na lastno odgovornost!",
   "experimental_settings_title": "Eksperimentalno",
+  "external_network": "Zunanje omrežje",
+  "external_network_sheet_info": "Ko aplikacija ni v želenem omrežju WiFi, se bo povezala s strežnikom prek prvega od spodnjih URL-jev, ki jih lahko doseže, začenši od zgoraj navzdol",
+  "favorites": "Priljubljene",
   "favorites_page_no_favorites": "Ni priljubljenih sredstev",
   "favorites_page_title": "Priljubljene",
-  "filename_search": "File name or extension",
+  "filename_search": "Ime ali končnica datoteke",
+  "filter": "Filter",
+  "get_wifiname_error": "Imena Wi-Fi ni bilo mogoče dobiti. Prepričajte se, da ste podelili potrebna dovoljenja in ste povezani v omrežje Wi-Fi",
+  "grant_permission": "Podeli dovoljenje",
   "haptic_feedback_switch": "Uporabi haptičen odziv",
   "haptic_feedback_title": "Haptičen odziv",
   "header_settings_add_header_tip": "Dodaj glavo",
@@ -255,13 +293,16 @@
   "home_page_first_time_notice": "Če aplikacijo uporabljate prvič, se prepričajte, da ste izbrali rezervne albume, tako da lahko časovna premica zapolni fotografije in videoposnetke v albumih.",
   "home_page_share_err_local": "Lokalnih sredstev ni mogoče deliti prek povezave, preskakujem",
   "home_page_upload_err_limit": "Hkrati lahko naložite največ 30 sredstev, preskakujem",
-  "image_saved_successfully": "Image saved",
+  "ignore_icloud_photos": "Ignoriraj fotografije iCloud",
+  "ignore_icloud_photos_description": "Fotografije, shranjene v iCloud, ne bodo naložene na strežnik Immich",
+  "image_saved_successfully": "Slika shranjena",
   "image_viewer_page_state_provider_download_error": "Napaka pri prenosu",
   "image_viewer_page_state_provider_download_started": "Prenos se je začel",
   "image_viewer_page_state_provider_download_success": "Prenos je uspel",
   "image_viewer_page_state_provider_share_error": "Napaka skupne rabe",
-  "invalid_date": "Invalid date",
-  "invalid_date_format": "Invalid date format",
+  "invalid_date": "Neveljaven datum",
+  "invalid_date_format": "Neveljavna oblika datuma",
+  "library": "Knjižnica",
   "library_page_albums": "Albumi",
   "library_page_archive": "Arhiv",
   "library_page_device_albums": "Albumi v napravi",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Najstarejša fotografija",
   "library_page_sort_most_recent_photo": "Najnovejša fotografija",
   "library_page_sort_title": "Naslov albuma",
+  "local_network": "Lokalno omrežje",
+  "local_network_sheet_info": "Aplikacija se bo povezala s strežnikom prek tega URL-ja, ko bo uporabljala navedeno omrežje Wi-Fi",
+  "location_permission": "Dovoljenje za lokacijo",
+  "location_permission_content": "Za uporabo funkcije samodejnega preklapljanja potrebuje Immich dovoljenje za natančno lokacijo, da lahko prebere ime trenutnega omrežja WiFi",
   "location_picker_choose_on_map": "Izberi na zemljevidu",
   "location_picker_latitude": "Zemljepisna širina",
   "location_picker_latitude_error": "Vnesi veljavno zemljepisno širino",
@@ -342,14 +387,18 @@
   "motion_photos_page_title": "Fotografije v gibanju",
   "multiselect_grid_edit_date_time_err_read_only": "Ni mogoče urediti datuma sredstev samo za branje, preskočim",
   "multiselect_grid_edit_gps_err_read_only": "Ni mogoče urediti lokacije sredstev samo za branje, preskočim",
+  "my_albums": "Moji albumi",
+  "networking_settings": "Omrežje",
+  "networking_subtitle": "Upravljaj nastavitve končne točke strežnika",
   "no_assets_to_show": "Ni sredstev za prikaz",
-  "no_name": "No name",
+  "no_name": "Brez imena",
   "notification_permission_dialog_cancel": "Prekliči",
   "notification_permission_dialog_content": "Če želite omogočiti obvestila, pojdite v Nastavitve in izberite Dovoli.",
   "notification_permission_dialog_settings": "Nastavitve",
   "notification_permission_list_tile_content": "Izdaj dovoljenje za omogočanje obvestil.",
   "notification_permission_list_tile_enable_button": "Omogoči obvestila",
   "notification_permission_list_tile_title": "Dovoljenje za obvestila",
+  "on_this_device": "Na tej napravi",
   "partner_list_user_photos": "{user}ovih fotografij",
   "partner_list_view_all": "Poglej vse",
   "partner_page_add_partner": "Dodaj partnerja",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} ne bo imel več dostopa do vaših fotografij.",
   "partner_page_stop_sharing_title": "Želite prenehati deliti svoje fotografije?",
   "partner_page_title": "Partner",
+  "partners": "Partnerji",
+  "people": "Ljudje",
   "permission_onboarding_back": "Sredstev partnerja ni mogoče izbrisati, preskakujem",
   "permission_onboarding_continue_anyway": "Vseeno nadaljuj",
   "permission_onboarding_get_started": "Začnimo",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Dovoljenje je izdano! Vse je pripravljeno.",
   "permission_onboarding_permission_limited": "Dovoljenje je omejeno. Če želite Immichu dovoliti varnostno kopiranje in upravljanje vaše celotne zbirke galerij, v nastavitvah podelite dovoljenja za fotografije in videoposnetke.",
   "permission_onboarding_request": "Immich potrebuje dovoljenje za ogled vaših fotografij in videoposnetkov.",
+  "places": "Kraji",
+  "preferences_settings_subtitle": "Upravljaj nastavitve aplikacije",
   "preferences_settings_title": "Nastavitve",
   "profile_drawer_app_logs": "Dnevniki",
   "profile_drawer_client_out_of_date_major": "Mobilna aplikacija je zastarela. Posodobite na najnovejšo glavno različico.",
@@ -383,35 +436,38 @@
   "profile_drawer_settings": "Nastavitve",
   "profile_drawer_sign_out": "Odjava",
   "profile_drawer_trash": "Smetnjak",
+  "recently_added": "Nedavno dodano",
   "recently_added_page_title": "Nedavno dodano",
-  "save_to_gallery": "Save to gallery",
+  "save": "Shrani",
+  "save_to_gallery": "Shrani v galerijo",
   "scaffold_body_error_occurred": "Prišlo je do napake",
+  "search_albums": "Iskanje albumov",
   "search_bar_hint": "Poišči svoje fotografije",
   "search_filter_apply": "Uporabi filter",
-  "search_filter_camera": "Camera",
+  "search_filter_camera": "Fotoaparat",
   "search_filter_camera_make": "Izdelava",
   "search_filter_camera_model": "Model",
-  "search_filter_camera_title": "Select camera type",
-  "search_filter_date": "Date",
-  "search_filter_date_interval": "{start} to {end}",
-  "search_filter_date_title": "Select a date range",
+  "search_filter_camera_title": "Izberi vrsto fotoaparata",
+  "search_filter_date": "Datum",
+  "search_filter_date_interval": "{start} do {end}",
+  "search_filter_date_title": "Izberi časovno obdobje",
   "search_filter_display_option_archive": "Arhiv",
   "search_filter_display_option_favorite": "Priljubljen",
   "search_filter_display_option_not_in_album": "Ni v albumu",
-  "search_filter_display_options": "Display Options",
-  "search_filter_display_options_title": "Display options",
-  "search_filter_location": "Location",
+  "search_filter_display_options": "Možnosti zaslona",
+  "search_filter_display_options_title": "Možnosti prikaza",
+  "search_filter_location": "Lokacija",
   "search_filter_location_city": "Mesto",
   "search_filter_location_country": "Država",
   "search_filter_location_state": "Dežela",
-  "search_filter_location_title": "Select location",
-  "search_filter_media_type": "Media Type",
+  "search_filter_location_title": "Izberi lokacijo",
+  "search_filter_media_type": "Vrsta medija",
   "search_filter_media_type_all": "Vse",
   "search_filter_media_type_image": "Slika",
-  "search_filter_media_type_title": "Select media type",
+  "search_filter_media_type_title": "Izberi vrsto medija",
   "search_filter_media_type_video": "Video",
-  "search_filter_people": "People",
-  "search_filter_people_title": "Select people",
+  "search_filter_people": "Ljudje",
+  "search_filter_people_title": "Izberi osebe",
   "search_page_categories": "Kategorije",
   "search_page_favorites": "Priljubljene",
   "search_page_motion_photos": "Fotografije v gibanju",
@@ -428,6 +484,7 @@
   "search_page_places": "Lokacije",
   "search_page_recently_added": "Nedavno dodano",
   "search_page_screenshots": "Posnetki zaslona",
+  "search_page_search_photos_videos": "Poišči svoje fotografije in videoposnetke",
   "search_page_selfies": "Selfiji",
   "search_page_things": "Stvari",
   "search_page_videos": "Videoposnetki",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Predlogi",
   "select_user_for_sharing_page_err_album": "Albuma ni bilo mogoče ustvariti",
   "select_user_for_sharing_page_share_suggestions": "Predlogi",
+  "server_endpoint": "Končna točka strežnika",
   "server_info_box_app_version": "Različica aplikacije",
   "server_info_box_latest_release": "Zadnja verzija",
   "server_info_box_server_url": "URL strežnika",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Naloži predogled slike",
   "setting_image_viewer_title": "Slike",
   "setting_languages_apply": "Uporabi",
+  "setting_languages_subtitle": "Spremeni jezik aplikacije",
   "setting_languages_title": "Jeziki",
   "setting_notifications_notify_failures_grace_period": "Obvesti o napakah varnostnega kopiranja v ozadju: {}",
   "setting_notifications_notify_hours": "{} ur",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Naloži",
   "shared_link_manage_links": "Upravljanje povezav v skupni rabi",
   "shared_link_public_album": "Javni album",
+  "shared_links": "Deljene povezave",
   "share_done": "Končano",
+  "shared_with_me": "Deljeno z mano",
   "share_invite": "Povabi v album",
   "sharing_page_album": "Albumi v skupni rabi",
   "sharing_page_description": "Ustvarite albume za skupno rabo fotografij in videoposnetkov z osebami v vašem omrežju.",
@@ -539,31 +600,32 @@
   "sharing_silver_appbar_create_shared_album": "Ustvari album v skupni rabi",
   "sharing_silver_appbar_shared_links": "Povezave skupne rabe",
   "sharing_silver_appbar_share_partner": "Deli z partnerjem",
-  "sync": "Sync",
-  "sync_albums": "Sync albums",
-  "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
-  "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
+  "sync": "Sinhronizacija",
+  "sync_albums": "Sinhronizacija albumov",
+  "sync_albums_manual_subtitle": "Sinhronizirajte vse naložene videoposnetke in fotografije v izbrane varnostne albume",
+  "sync_upload_album_setting_subtitle": "Ustvarite in naložite svoje fotografije in videoposnetke v izbrane albume na Immich",
   "tab_controller_nav_library": "Knjižnica",
   "tab_controller_nav_photos": "Slike",
   "tab_controller_nav_search": "Iskanje",
   "tab_controller_nav_sharing": "Deljeno",
   "theme_setting_asset_list_storage_indicator_title": "Pokaži indikator shrambe na ploščicah sredstev",
   "theme_setting_asset_list_tiles_per_row_title": "Število sredstev na vrstico ({})",
-  "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.",
-  "theme_setting_colorful_interface_title": "Colorful interface",
+  "theme_setting_colorful_interface_subtitle": "Nanesi primarno barvo na površine ozadja.",
+  "theme_setting_colorful_interface_title": "Barvit vmesnik",
   "theme_setting_dark_mode_switch": "Temni način",
   "theme_setting_image_viewer_quality_subtitle": "Prilagodite kakovost podrobnega pregledovalnika slik",
   "theme_setting_image_viewer_quality_title": "Kakovost pregledovalnika slik",
-  "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.",
-  "theme_setting_primary_color_title": "Primary color",
-  "theme_setting_system_primary_color_title": "Use system color",
+  "theme_setting_primary_color_subtitle": "Izberi barvo za primarna dejanja in poudarke.",
+  "theme_setting_primary_color_title": "Primarna barva",
+  "theme_setting_system_primary_color_title": "Uporabi sistemsko barvo",
   "theme_setting_system_theme_switch": "Samodejno (Sledi nastavitvi sistema)",
   "theme_setting_theme_subtitle": "Izberi nastavitev teme aplikacije",
   "theme_setting_theme_title": "Tema",
   "theme_setting_three_stage_loading_subtitle": "Tristopenjsko nalaganje lahko poveča zmogljivost nalaganja, vendar povzroči znatno večjo obremenitev omrežja",
   "theme_setting_three_stage_loading_title": "Omogoči tristopenjsko nalaganje",
   "translated_text_options": "Možnosti",
-  "trash_emptied": "Emptied trash",
+  "trash": "Smetnjak",
+  "trash_emptied": "Smetnjak je izpraznjen",
   "trash_page_delete": "Izbriši",
   "trash_page_delete_all": "Izbriši vse",
   "trash_page_empty_trash_btn": "Izprazni smeti",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Ali želite varnostno kopirati izbrana sredstva na strežnik?",
   "upload_dialog_ok": "Naloži",
   "upload_dialog_title": "Naloži sredstvo",
+  "use_current_connection": "uporabi trenutno povezavo",
+  "validate_endpoint_error": "Vnesite veljaven URL",
   "version_announcement_overlay_ack": "Preverite",
   "version_announcement_overlay_release_notes": "opombe ob izdaji",
   "version_announcement_overlay_text_1": "Živjo prijatelj, na voljo je nova izdaja",
   "version_announcement_overlay_text_2": "vzemi si čas in obišči",
   "version_announcement_overlay_text_3": "in zagotovite, da sta vaša nastavitev docker-compose in .env posodobljena, da preprečite morebitne napačne konfiguracije, zlasti če uporabljate WatchTower ali kateri koli mehanizem, ki samodejno posodablja vašo strežniško aplikacijo.",
   "version_announcement_overlay_title": "Na voljo je nova različica strežnika \uD83C\uDF89",
+  "videos": "Videoposnetki",
   "viewer_remove_from_stack": "Odstrani iz sklada",
   "viewer_stack_use_as_main_asset": "Uporabi kot glavno sredstvo",
-  "viewer_unstack": "Razkladi"
+  "viewer_unstack": "Razkladi",
+  "wifi_name": "WiFi ime",
+  "your_wifi_name": "Vaše ime WiFi"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/sr-Cyrl.json b/mobile/assets/i18n/sr-Cyrl.json
index 324c9069fd..a7f31f8440 100644
--- a/mobile/assets/i18n/sr-Cyrl.json
+++ b/mobile/assets/i18n/sr-Cyrl.json
@@ -6,6 +6,8 @@
   "action_common_save": "Save",
   "action_common_select": "Select",
   "action_common_update": "Update",
+  "add_a_name": "Add a name",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Added to {album}",
   "add_to_album_bottom_sheet_already_exists": "Already in {album}",
   "advanced_settings_log_level_title": "Log level: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Troubleshooting",
   "album_info_card_backup_album_excluded": "EXCLUDED",
   "album_info_card_backup_album_included": "INCLUDED",
+  "albums": "Albums",
   "album_thumbnail_card_item": "1 item",
   "album_thumbnail_card_items": "{} items",
   "album_thumbnail_card_shared": " · Shared",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Remove from album",
   "album_viewer_appbar_share_to": "Share To",
   "album_viewer_page_share_add_users": "Add users",
+  "all": "All",
   "all_people_page_title": "People",
   "all_videos_page_title": "Videos",
   "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
   "app_bar_signout_dialog_ok": "Yes",
   "app_bar_signout_dialog_title": "Sign out",
+  "archived": "Archived",
   "archive_page_no_archived_assets": "No archived assets found",
   "archive_page_title": "Archive ({})",
   "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} asset(s) restored successfully",
   "assets_trashed": "{} asset(s) trashed",
   "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "Asset Viewer",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Albums on device ({})",
   "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
   "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Success",
   "backup_manual_title": "Upload status",
   "backup_options_page_title": "Backup options",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
   "cache_settings_clear_cache_button": "Clear cache",
   "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Control the local storage behaviour",
   "cache_settings_tile_title": "Local Storage",
   "cache_settings_title": "Caching Settings",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Confirm Password",
   "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
   "change_password_form_new_password": "New Password",
   "change_password_form_password_mismatch": "Passwords do not match",
   "change_password_form_reenter_new_password": "Re-enter New Password",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Enter Password",
   "client_cert_import": "Import",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Unarchive",
   "control_bottom_app_bar_unfavorite": "Unfavorite",
   "control_bottom_app_bar_upload": "Upload",
+  "create_album": "Create album",
   "create_album_page_untitled": "Untitled",
+  "create_new": "CREATE NEW",
   "create_shared_album_page_create": "Create",
   "create_shared_album_page_share": "Share",
   "create_shared_album_page_share_add_assets": "ADD ASSETS",
@@ -193,6 +211,7 @@
   "crop": "Crop",
   "curated_location_page_title": "Places",
   "curated_object_page_title": "Things",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "Delete Shared Link",
   "description_input_hint_text": "Add description...",
   "description_input_submit_error": "Error updating description, check the log for more details",
+  "download_canceled": "Download canceled",
+  "download_complete": "Download complete",
+  "download_enqueue": "Download enqueued",
   "download_error": "Download Error",
+  "download_failed": "Download failed",
+  "download_filename": "file: {}",
+  "download_finished": "Download finished",
+  "downloading": "Downloading...",
+  "downloading_media": "Downloading media",
+  "download_notfound": "Download not found",
+  "download_paused": "Download paused",
   "download_started": "Download started",
   "download_sucess": "Download success",
   "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_waiting_to_retry": "Waiting to retry",
   "edit_date_time_dialog_date_time": "Date and Time",
   "edit_date_time_dialog_timezone": "Timezone",
   "edit_image_title": "Edit",
   "edit_location_dialog_title": "Location",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Error: {}",
   "exif_bottom_sheet_description": "Add Description...",
   "exif_bottom_sheet_details": "DETAILS",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Enable experimental photo grid",
   "experimental_settings_subtitle": "Use at your own risk!",
   "experimental_settings_title": "Experimental",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favorites",
   "favorites_page_no_favorites": "No favorite assets found",
   "favorites_page_title": "Favorites",
   "filename_search": "File name or extension",
+  "filter": "Filter",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Enable haptic feedback",
   "haptic_feedback_title": "Haptic Feedback",
   "header_settings_add_header_tip": "Add Header",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
   "home_page_share_err_local": "Can not share local assets via link, skipping",
   "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
+  "ignore_icloud_photos": "Ignore iCloud photos",
+  "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
   "image_saved_successfully": "Image saved",
   "image_viewer_page_state_provider_download_error": "Download Error",
   "image_viewer_page_state_provider_download_started": "Download Started",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "Share Error",
   "invalid_date": "Invalid date",
   "invalid_date_format": "Invalid date format",
+  "library": "Library",
   "library_page_albums": "Albums",
   "library_page_archive": "Archive",
   "library_page_device_albums": "Albums on Device",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Oldest photo",
   "library_page_sort_most_recent_photo": "Most recent photo",
   "library_page_sort_title": "Album title",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Choose on map",
   "location_picker_latitude": "Latitude",
   "location_picker_latitude_error": "Enter a valid latitude",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Motion Photos",
   "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
   "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
+  "my_albums": "My albums",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "No assets to show",
   "no_name": "No name",
   "notification_permission_dialog_cancel": "Cancel",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Grant permission to enable notifications.",
   "notification_permission_list_tile_enable_button": "Enable Notifications",
   "notification_permission_list_tile_title": "Notification Permission",
+  "on_this_device": "On this device",
   "partner_list_user_photos": "{user}'s photos",
   "partner_list_view_all": "View all",
   "partner_page_add_partner": "Add partner",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
   "partner_page_stop_sharing_title": "Stop sharing your photos?",
   "partner_page_title": "Partner",
+  "partners": "Partners",
+  "people": "People",
   "permission_onboarding_back": "Back",
   "permission_onboarding_continue_anyway": "Continue anyway",
   "permission_onboarding_get_started": "Get started",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Permission granted! You are all set.",
   "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
   "permission_onboarding_request": "Immich requires permission to view your photos and videos.",
+  "places": "Places",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Preferences",
   "profile_drawer_app_logs": "Logs",
   "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Settings",
   "profile_drawer_sign_out": "Sign Out",
   "profile_drawer_trash": "Trash",
+  "recently_added": "Recently added",
   "recently_added_page_title": "Recently Added",
+  "save": "Save",
   "save_to_gallery": "Save to gallery",
   "scaffold_body_error_occurred": "Error occurred",
+  "search_albums": "Search albums",
   "search_bar_hint": "Search your photos",
   "search_filter_apply": "Apply filter",
   "search_filter_camera": "Camera",
@@ -428,6 +484,7 @@
   "search_page_places": "Places",
   "search_page_recently_added": "Recently added",
   "search_page_screenshots": "Screenshots",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Selfies",
   "search_page_things": "Things",
   "search_page_videos": "Videos",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Suggestions",
   "select_user_for_sharing_page_err_album": "Failed to create album",
   "select_user_for_sharing_page_share_suggestions": "Suggestions",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "App Version",
   "server_info_box_latest_release": "Latest Version",
   "server_info_box_server_url": "Server URL",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Load preview image",
   "setting_image_viewer_title": "Images",
   "setting_languages_apply": "Apply",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Languages",
   "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
   "setting_notifications_notify_hours": "{} hours",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Upload",
   "shared_link_manage_links": "Manage Shared links",
   "shared_link_public_album": "Public album",
+  "shared_links": "Shared links",
   "share_done": "Done",
+  "shared_with_me": "Shared with me",
   "share_invite": "Invite to album",
   "sharing_page_album": "Shared albums",
   "sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
   "theme_setting_three_stage_loading_title": "Enable three-stage loading",
   "translated_text_options": "Options",
+  "trash": "Trash",
   "trash_emptied": "Emptied trash",
   "trash_page_delete": "Delete",
   "trash_page_delete_all": "Delete All",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
   "upload_dialog_ok": "Upload",
   "upload_dialog_title": "Upload Asset",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Acknowledge",
   "version_announcement_overlay_release_notes": "release notes",
   "version_announcement_overlay_text_1": "Hi friend, there is a new release of",
   "version_announcement_overlay_text_2": "please take your time to visit the ",
   "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
   "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
+  "videos": "Videos",
   "viewer_remove_from_stack": "Remove from Stack",
   "viewer_stack_use_as_main_asset": "Use as Main Asset",
-  "viewer_unstack": "Un-Stack"
+  "viewer_unstack": "Un-Stack",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/sr-Latn.json b/mobile/assets/i18n/sr-Latn.json
index 744ebe72ce..9042a27713 100644
--- a/mobile/assets/i18n/sr-Latn.json
+++ b/mobile/assets/i18n/sr-Latn.json
@@ -6,6 +6,8 @@
   "action_common_save": "Save",
   "action_common_select": "Select",
   "action_common_update": "Update",
+  "add_a_name": "Add a name",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Dodato u {album}",
   "add_to_album_bottom_sheet_already_exists": "Već u {album}",
   "advanced_settings_log_level_title": "Log level: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Troubleshooting",
   "album_info_card_backup_album_excluded": "ISKLJUČENO",
   "album_info_card_backup_album_included": "UKLJUČENO",
+  "albums": "Albums",
   "album_thumbnail_card_item": "1 stavka",
   "album_thumbnail_card_items": "{} stavki",
   "album_thumbnail_card_shared": "Deljeno",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Obriši iz albuma",
   "album_viewer_appbar_share_to": "Share To",
   "album_viewer_page_share_add_users": "Dodaj korisnike",
+  "all": "All",
   "all_people_page_title": "People",
   "all_videos_page_title": "Videos",
   "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
   "app_bar_signout_dialog_ok": "Yes",
   "app_bar_signout_dialog_title": "Sign out",
+  "archived": "Archived",
   "archive_page_no_archived_assets": "No archived assets found",
   "archive_page_title": "Archive ({})",
   "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} asset(s) restored successfully",
   "assets_trashed": "{} asset(s) trashed",
   "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "Asset Viewer",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Albuma na uređaju ({})",
   "backup_album_selection_page_albums_tap": "Dodirni da uključiš, dodirni dvaput da isključiš",
   "backup_album_selection_page_assets_scatter": "Zapisi se mogu naći u više različitih albuma. Odatle albumi se mogu uključiti ili isključiti tokom procesa pravljenja pozadinskih kopija.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Success",
   "backup_manual_title": "Upload status",
   "backup_options_page_title": "Backup options",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Sličice na stranici biblioteke",
   "cache_settings_clear_cache_button": "Obriši keš memoriju",
   "cache_settings_clear_cache_button_title": "Ova opcija briše keš memoriju aplikacije. Ovo će bitno uticati na performanse aplikacije dok se keš memorija ne učita ponovo.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Control the local storage behaviour",
   "cache_settings_tile_title": "Local Storage",
   "cache_settings_title": "Opcije za keširanje",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Ponovo unesite šifru",
   "change_password_form_description": "Ćao, {name}\n\nOvo je verovatno Vaše prvo pristupanje sistemu, ili je podnešen zahtev za promenu šifre. Molimo Vas, unesite novu šifru ispod",
   "change_password_form_new_password": "Nova šifra",
   "change_password_form_password_mismatch": "Šifre se ne podudaraju",
   "change_password_form_reenter_new_password": "Ponovo unesite novu šifru",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Enter Password",
   "client_cert_import": "Import",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Unarchive",
   "control_bottom_app_bar_unfavorite": "Unfavorite",
   "control_bottom_app_bar_upload": "Upload",
+  "create_album": "Create album",
   "create_album_page_untitled": "Bez naslova",
+  "create_new": "CREATE NEW",
   "create_shared_album_page_create": "Napravi",
   "create_shared_album_page_share": "Podeli",
   "create_shared_album_page_share_add_assets": "DODAJ ",
@@ -193,6 +211,7 @@
   "crop": "Crop",
   "curated_location_page_title": "Places",
   "curated_object_page_title": "Things",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "Delete Shared Link",
   "description_input_hint_text": "Add description...",
   "description_input_submit_error": "Error updating description, check the log for more details",
+  "download_canceled": "Download canceled",
+  "download_complete": "Download complete",
+  "download_enqueue": "Download enqueued",
   "download_error": "Download Error",
+  "download_failed": "Download failed",
+  "download_filename": "file: {}",
+  "download_finished": "Download finished",
+  "downloading": "Downloading...",
+  "downloading_media": "Downloading media",
+  "download_notfound": "Download not found",
+  "download_paused": "Download paused",
   "download_started": "Download started",
   "download_sucess": "Download success",
   "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_waiting_to_retry": "Waiting to retry",
   "edit_date_time_dialog_date_time": "Date and Time",
   "edit_date_time_dialog_timezone": "Timezone",
   "edit_image_title": "Edit",
   "edit_location_dialog_title": "Location",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Error: {}",
   "exif_bottom_sheet_description": "Dodaj opis...",
   "exif_bottom_sheet_details": "DETALJI",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Aktiviraj eksperimentalni mrežni prikaz fotografija",
   "experimental_settings_subtitle": "Koristiti na sopstvenu odgovornost!",
   "experimental_settings_title": "Eksperimentalno",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favorites",
   "favorites_page_no_favorites": "No favorite assets found",
   "favorites_page_title": "Omiljeno",
   "filename_search": "File name or extension",
+  "filter": "Filter",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Enable haptic feedback",
   "haptic_feedback_title": "Haptic Feedback",
   "header_settings_add_header_tip": "Add Header",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "Ako je ovo prvi put da koristite aplikaciju, molimo Vas da odaberete albume koje želite da sačuvate",
   "home_page_share_err_local": "Can not share local assets via link, skipping",
   "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
+  "ignore_icloud_photos": "Ignore iCloud photos",
+  "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
   "image_saved_successfully": "Image saved",
   "image_viewer_page_state_provider_download_error": "Preuzimanje Neuspešno",
   "image_viewer_page_state_provider_download_started": "Download Started",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "Share Error",
   "invalid_date": "Invalid date",
   "invalid_date_format": "Invalid date format",
+  "library": "Library",
   "library_page_albums": "Albumi",
   "library_page_archive": "Archive",
   "library_page_device_albums": "Albums on Device",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Oldest photo",
   "library_page_sort_most_recent_photo": "Most recent photo",
   "library_page_sort_title": "Naziv albuma",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Choose on map",
   "location_picker_latitude": "Latitude",
   "location_picker_latitude_error": "Enter a valid latitude",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Motion Photos",
   "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
   "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
+  "my_albums": "My albums",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "No assets to show",
   "no_name": "No name",
   "notification_permission_dialog_cancel": "Odustani",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Dozvoli Notifikacije\n",
   "notification_permission_list_tile_enable_button": "Uključi Notifikacije",
   "notification_permission_list_tile_title": "Dozvole za notifikacije",
+  "on_this_device": "On this device",
   "partner_list_user_photos": "{user}'s photos",
   "partner_list_view_all": "View all",
   "partner_page_add_partner": "Add partner",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
   "partner_page_stop_sharing_title": "Stop sharing your photos?",
   "partner_page_title": "Partner",
+  "partners": "Partners",
+  "people": "People",
   "permission_onboarding_back": "Back",
   "permission_onboarding_continue_anyway": "Continue anyway",
   "permission_onboarding_get_started": "Get started",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Permission granted! You are all set.",
   "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
   "permission_onboarding_request": "Immich requires permission to view your photos and videos.",
+  "places": "Places",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Preferences",
   "profile_drawer_app_logs": "Evidencija",
   "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Opcije",
   "profile_drawer_sign_out": "Odjavi se",
   "profile_drawer_trash": "Trash",
+  "recently_added": "Recently added",
   "recently_added_page_title": "Recently Added",
+  "save": "Save",
   "save_to_gallery": "Save to gallery",
   "scaffold_body_error_occurred": "Error occurred",
+  "search_albums": "Search albums",
   "search_bar_hint": "Pretražite Vaše fotografije",
   "search_filter_apply": "Apply filter",
   "search_filter_camera": "Camera",
@@ -428,6 +484,7 @@
   "search_page_places": "Mesta",
   "search_page_recently_added": "Recently added",
   "search_page_screenshots": "Screenshots",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Selfies",
   "search_page_things": "Stvari",
   "search_page_videos": "Videos",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Sugsetije",
   "select_user_for_sharing_page_err_album": "Neuspešno kreiranje albuma",
   "select_user_for_sharing_page_share_suggestions": "Sugestije",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "Verzija Aplikacije",
   "server_info_box_latest_release": "Latest Version",
   "server_info_box_server_url": "Server URL",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Pregledaj sliku",
   "setting_image_viewer_title": "Images",
   "setting_languages_apply": "Apply",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Languages",
   "setting_notifications_notify_failures_grace_period": "Neuspešne rezervne kopije: {}",
   "setting_notifications_notify_hours": "{} sati",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Upload",
   "shared_link_manage_links": "Manage Shared links",
   "shared_link_public_album": "Public album",
+  "shared_links": "Shared links",
   "share_done": "Done",
+  "shared_with_me": "Shared with me",
   "share_invite": "Pozivnica za album",
   "sharing_page_album": "Deljeni albumi",
   "sharing_page_description": "Napravi deljene albume da deliš fotografije i video zapise sa ljudima na tvojoj mreži",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Trostepeno učitavanje možda ubrza učitavanje, po cenu potrošnje podataka",
   "theme_setting_three_stage_loading_title": "Aktiviraj trostepeno učitavanje",
   "translated_text_options": "Options",
+  "trash": "Trash",
   "trash_emptied": "Emptied trash",
   "trash_page_delete": "Delete",
   "trash_page_delete_all": "Delete All",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
   "upload_dialog_ok": "Upload",
   "upload_dialog_title": "Upload Asset",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Priznati",
   "version_announcement_overlay_release_notes": "novine nove verzije",
   "version_announcement_overlay_text_1": "Ćao, nova verzija",
   "version_announcement_overlay_text_2": "molimo Vas izdvojite vremena da pogledate",
   "version_announcement_overlay_text_3": "i proverite da su Vaš docker-compose i .env najnovije verzije da bi izbegli greške u radu. Pogotovu ako koristite WatchTower ili bilo koji drugi mehanizam koji automatski instalira nove verzije vaše serverske aplikacije.",
   "version_announcement_overlay_title": "Nova verzija servera je dostupna \uD83C\uDF89",
+  "videos": "Videos",
   "viewer_remove_from_stack": "Remove from Stack",
   "viewer_stack_use_as_main_asset": "Use as Main Asset",
-  "viewer_unstack": "Un-Stack"
+  "viewer_unstack": "Un-Stack",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/sv-FI.json b/mobile/assets/i18n/sv-FI.json
index 324c9069fd..a7f31f8440 100644
--- a/mobile/assets/i18n/sv-FI.json
+++ b/mobile/assets/i18n/sv-FI.json
@@ -6,6 +6,8 @@
   "action_common_save": "Save",
   "action_common_select": "Select",
   "action_common_update": "Update",
+  "add_a_name": "Add a name",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Added to {album}",
   "add_to_album_bottom_sheet_already_exists": "Already in {album}",
   "advanced_settings_log_level_title": "Log level: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Troubleshooting",
   "album_info_card_backup_album_excluded": "EXCLUDED",
   "album_info_card_backup_album_included": "INCLUDED",
+  "albums": "Albums",
   "album_thumbnail_card_item": "1 item",
   "album_thumbnail_card_items": "{} items",
   "album_thumbnail_card_shared": " · Shared",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Remove from album",
   "album_viewer_appbar_share_to": "Share To",
   "album_viewer_page_share_add_users": "Add users",
+  "all": "All",
   "all_people_page_title": "People",
   "all_videos_page_title": "Videos",
   "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
   "app_bar_signout_dialog_ok": "Yes",
   "app_bar_signout_dialog_title": "Sign out",
+  "archived": "Archived",
   "archive_page_no_archived_assets": "No archived assets found",
   "archive_page_title": "Archive ({})",
   "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} asset(s) restored successfully",
   "assets_trashed": "{} asset(s) trashed",
   "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "Asset Viewer",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Albums on device ({})",
   "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
   "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Success",
   "backup_manual_title": "Upload status",
   "backup_options_page_title": "Backup options",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
   "cache_settings_clear_cache_button": "Clear cache",
   "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Control the local storage behaviour",
   "cache_settings_tile_title": "Local Storage",
   "cache_settings_title": "Caching Settings",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Confirm Password",
   "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
   "change_password_form_new_password": "New Password",
   "change_password_form_password_mismatch": "Passwords do not match",
   "change_password_form_reenter_new_password": "Re-enter New Password",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Enter Password",
   "client_cert_import": "Import",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Unarchive",
   "control_bottom_app_bar_unfavorite": "Unfavorite",
   "control_bottom_app_bar_upload": "Upload",
+  "create_album": "Create album",
   "create_album_page_untitled": "Untitled",
+  "create_new": "CREATE NEW",
   "create_shared_album_page_create": "Create",
   "create_shared_album_page_share": "Share",
   "create_shared_album_page_share_add_assets": "ADD ASSETS",
@@ -193,6 +211,7 @@
   "crop": "Crop",
   "curated_location_page_title": "Places",
   "curated_object_page_title": "Things",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "Delete Shared Link",
   "description_input_hint_text": "Add description...",
   "description_input_submit_error": "Error updating description, check the log for more details",
+  "download_canceled": "Download canceled",
+  "download_complete": "Download complete",
+  "download_enqueue": "Download enqueued",
   "download_error": "Download Error",
+  "download_failed": "Download failed",
+  "download_filename": "file: {}",
+  "download_finished": "Download finished",
+  "downloading": "Downloading...",
+  "downloading_media": "Downloading media",
+  "download_notfound": "Download not found",
+  "download_paused": "Download paused",
   "download_started": "Download started",
   "download_sucess": "Download success",
   "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_waiting_to_retry": "Waiting to retry",
   "edit_date_time_dialog_date_time": "Date and Time",
   "edit_date_time_dialog_timezone": "Timezone",
   "edit_image_title": "Edit",
   "edit_location_dialog_title": "Location",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Error: {}",
   "exif_bottom_sheet_description": "Add Description...",
   "exif_bottom_sheet_details": "DETAILS",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Enable experimental photo grid",
   "experimental_settings_subtitle": "Use at your own risk!",
   "experimental_settings_title": "Experimental",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favorites",
   "favorites_page_no_favorites": "No favorite assets found",
   "favorites_page_title": "Favorites",
   "filename_search": "File name or extension",
+  "filter": "Filter",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Enable haptic feedback",
   "haptic_feedback_title": "Haptic Feedback",
   "header_settings_add_header_tip": "Add Header",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
   "home_page_share_err_local": "Can not share local assets via link, skipping",
   "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
+  "ignore_icloud_photos": "Ignore iCloud photos",
+  "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
   "image_saved_successfully": "Image saved",
   "image_viewer_page_state_provider_download_error": "Download Error",
   "image_viewer_page_state_provider_download_started": "Download Started",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "Share Error",
   "invalid_date": "Invalid date",
   "invalid_date_format": "Invalid date format",
+  "library": "Library",
   "library_page_albums": "Albums",
   "library_page_archive": "Archive",
   "library_page_device_albums": "Albums on Device",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Oldest photo",
   "library_page_sort_most_recent_photo": "Most recent photo",
   "library_page_sort_title": "Album title",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Choose on map",
   "location_picker_latitude": "Latitude",
   "location_picker_latitude_error": "Enter a valid latitude",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Motion Photos",
   "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
   "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
+  "my_albums": "My albums",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "No assets to show",
   "no_name": "No name",
   "notification_permission_dialog_cancel": "Cancel",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Grant permission to enable notifications.",
   "notification_permission_list_tile_enable_button": "Enable Notifications",
   "notification_permission_list_tile_title": "Notification Permission",
+  "on_this_device": "On this device",
   "partner_list_user_photos": "{user}'s photos",
   "partner_list_view_all": "View all",
   "partner_page_add_partner": "Add partner",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
   "partner_page_stop_sharing_title": "Stop sharing your photos?",
   "partner_page_title": "Partner",
+  "partners": "Partners",
+  "people": "People",
   "permission_onboarding_back": "Back",
   "permission_onboarding_continue_anyway": "Continue anyway",
   "permission_onboarding_get_started": "Get started",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Permission granted! You are all set.",
   "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
   "permission_onboarding_request": "Immich requires permission to view your photos and videos.",
+  "places": "Places",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Preferences",
   "profile_drawer_app_logs": "Logs",
   "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Settings",
   "profile_drawer_sign_out": "Sign Out",
   "profile_drawer_trash": "Trash",
+  "recently_added": "Recently added",
   "recently_added_page_title": "Recently Added",
+  "save": "Save",
   "save_to_gallery": "Save to gallery",
   "scaffold_body_error_occurred": "Error occurred",
+  "search_albums": "Search albums",
   "search_bar_hint": "Search your photos",
   "search_filter_apply": "Apply filter",
   "search_filter_camera": "Camera",
@@ -428,6 +484,7 @@
   "search_page_places": "Places",
   "search_page_recently_added": "Recently added",
   "search_page_screenshots": "Screenshots",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Selfies",
   "search_page_things": "Things",
   "search_page_videos": "Videos",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Suggestions",
   "select_user_for_sharing_page_err_album": "Failed to create album",
   "select_user_for_sharing_page_share_suggestions": "Suggestions",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "App Version",
   "server_info_box_latest_release": "Latest Version",
   "server_info_box_server_url": "Server URL",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Load preview image",
   "setting_image_viewer_title": "Images",
   "setting_languages_apply": "Apply",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Languages",
   "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
   "setting_notifications_notify_hours": "{} hours",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Upload",
   "shared_link_manage_links": "Manage Shared links",
   "shared_link_public_album": "Public album",
+  "shared_links": "Shared links",
   "share_done": "Done",
+  "shared_with_me": "Shared with me",
   "share_invite": "Invite to album",
   "sharing_page_album": "Shared albums",
   "sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
   "theme_setting_three_stage_loading_title": "Enable three-stage loading",
   "translated_text_options": "Options",
+  "trash": "Trash",
   "trash_emptied": "Emptied trash",
   "trash_page_delete": "Delete",
   "trash_page_delete_all": "Delete All",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
   "upload_dialog_ok": "Upload",
   "upload_dialog_title": "Upload Asset",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Acknowledge",
   "version_announcement_overlay_release_notes": "release notes",
   "version_announcement_overlay_text_1": "Hi friend, there is a new release of",
   "version_announcement_overlay_text_2": "please take your time to visit the ",
   "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
   "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
+  "videos": "Videos",
   "viewer_remove_from_stack": "Remove from Stack",
   "viewer_stack_use_as_main_asset": "Use as Main Asset",
-  "viewer_unstack": "Un-Stack"
+  "viewer_unstack": "Un-Stack",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/sv-SE.json b/mobile/assets/i18n/sv-SE.json
index 0d6c7a3108..26ecd8cbe3 100644
--- a/mobile/assets/i18n/sv-SE.json
+++ b/mobile/assets/i18n/sv-SE.json
@@ -6,6 +6,8 @@
   "action_common_save": "Spara",
   "action_common_select": "Välj",
   "action_common_update": "Uppdatera",
+  "add_a_name": "Lägg till namn",
+  "add_endpoint": "Lägg till endpoint",
   "add_to_album_bottom_sheet_added": "Tillagd till {album}",
   "add_to_album_bottom_sheet_already_exists": "Redan i {album}",
   "advanced_settings_log_level_title": "Loggnivå: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Felsökning",
   "album_info_card_backup_album_excluded": "EXKLUDERAD",
   "album_info_card_backup_album_included": "INKLUDERAD",
+  "albums": "Album",
   "album_thumbnail_card_item": "1 objekt",
   "album_thumbnail_card_items": "{} objekt",
   "album_thumbnail_card_shared": " · Delad",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Ta bort från album",
   "album_viewer_appbar_share_to": "Dela Till",
   "album_viewer_page_share_add_users": "Lägg till användare",
+  "all": "Alla",
   "all_people_page_title": "Personer",
   "all_videos_page_title": "Videor",
   "app_bar_signout_dialog_content": "Är du säker på att du vill logga ut?",
   "app_bar_signout_dialog_ok": "Ja",
   "app_bar_signout_dialog_title": "Logga ut",
+  "archived": "Arkiverade",
   "archive_page_no_archived_assets": "Inga arkiverade objekt hittade",
   "archive_page_title": "Arkiv ({})",
   "asset_action_delete_err_read_only": "Kan inte ta bort skrivskyddade objekt, hoppar över",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} objekt har återställts",
   "assets_trashed": "{} objekt raderade",
   "assets_trashed_from_server": "{} objekt raderade från Immich-servern",
+  "asset_viewer_settings_subtitle": "Hantera inställningar för gallerivisare",
   "asset_viewer_settings_title": "Objektvisare",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatisk URL-växling",
+  "background_location_permission": "Tillåtelse för bakgrundsplats",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Album på enhet ({})",
   "backup_album_selection_page_albums_tap": "Tryck en gång för att inkludera, tryck två gånger för att exkludera",
   "backup_album_selection_page_assets_scatter": "Objekt kan vara utspridda över flera album. Därför kan album inkluderas eller exkluderas under säkerhetskopieringsprocessen",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Klart",
   "backup_manual_title": "Uppladdningsstatus",
   "backup_options_page_title": "Säkerhetskopieringsinställningar",
+  "backup_setting_subtitle": "Hantera inställningar för för- och bakgrundsuppladdning",
   "cache_settings_album_thumbnails": "Miniatyrbilder för bibliotek ({} bilder och videor)",
   "cache_settings_clear_cache_button": "Rensa cacheminnet",
   "cache_settings_clear_cache_button_title": "Rensar appens cacheminne. Detta kommer att avsevärt påverka appens prestanda tills cachen har byggts om.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Kontrollera beteende för lokal lagring",
   "cache_settings_tile_title": "Lokal Lagring",
   "cache_settings_title": "Cache Inställningar",
+  "cancel": "Avbryt",
+  "change_display_order": "Ändra visningsordning",
   "change_password_form_confirm_password": "Bekräfta lösenord",
   "change_password_form_description": "Hej {name},\n\nDet är antingen första gången du loggar in i systemet, eller så har det skett en förfrågan om återställning av ditt lösenord. Ange ditt nya lösenord nedan.",
   "change_password_form_new_password": "Nytt lösenord",
   "change_password_form_password_mismatch": "Lösenorden matchar inte",
   "change_password_form_reenter_new_password": "Ange Nytt Lösenord Igen",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Kontrollera",
+  "check_corrupt_asset_backup_description": "Kör kontrollen endast över Wi-Fi och när alla resurser har säkerhetskopierats. Det kan ta några minuter.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Ange Lösenord",
   "client_cert_import": "Importera",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Avarkivera",
   "control_bottom_app_bar_unfavorite": "Avfavorisera",
   "control_bottom_app_bar_upload": "Ladda Upp",
+  "create_album": "Skapa album",
   "create_album_page_untitled": "Namnlös",
+  "create_new": "SKAPA NY",
   "create_shared_album_page_create": "Skapa",
   "create_shared_album_page_share": "Dela",
   "create_shared_album_page_share_add_assets": "LÄGG TILL OBJEKT",
@@ -193,6 +211,7 @@
   "crop": "Beskär",
   "curated_location_page_title": "Platser",
   "curated_object_page_title": "Objekt",
+  "current_server_address": "Aktuell server-adress",
   "daily_title_text_date": "E, dd MMM",
   "daily_title_text_date_year": "E, dd MMM, yyyy",
   "date_format": "E d. LLL y • hh:mm",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "Ta Bort Delad Länk",
   "description_input_hint_text": "Lägg till beskrivning...",
   "description_input_submit_error": "Fel vid uppdatering av beskrivning, se loggen för fler detaljer",
+  "download_canceled": "Nedladdning avbruten",
+  "download_complete": "Nedladdning slutförd",
+  "download_enqueue": "Nedladdning köad",
   "download_error": "Fel vid nedladdning",
+  "download_failed": "Nedladdning misslyckades",
+  "download_filename": "fil: {}",
+  "download_finished": "Nedladdning klar",
+  "downloading": "Laddar ner...",
+  "downloading_media": "Laddar ner media",
+  "download_notfound": "Nedladdning kan inte hittas",
+  "download_paused": "Nedladdning pausad",
   "download_started": "Nedladdning påbörjad",
   "download_sucess": "Nedladdning lyckades",
   "download_sucess_android": "Media har laddats ner till DCIM/Immich",
+  "download_waiting_to_retry": "Väntar på omförsök",
   "edit_date_time_dialog_date_time": "Datum och Tid",
   "edit_date_time_dialog_timezone": "Tidszon",
   "edit_image_title": "Redigera",
   "edit_location_dialog_title": "Plats",
+  "enter_wifi_name": "Ange WiFi-namn",
+  "error_change_sort_album": "Kunde inte ändra sorteringsordning för album",
   "error_saving_image": "Fel: {}",
   "exif_bottom_sheet_description": "Lägg till beskrivning...",
   "exif_bottom_sheet_details": "DETALJER",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Aktivera experimentellt fotorutnät",
   "experimental_settings_subtitle": "Använd på egen risk!",
   "experimental_settings_title": "Experimentellt",
+  "external_network": "Externt nätverk",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favoriter",
   "favorites_page_no_favorites": "Inga favoritobjekt hittades",
   "favorites_page_title": "Favoriter",
   "filename_search": "Filnamn eller filändelse",
+  "filter": "Filter",
+  "get_wifiname_error": "Kunde inte hämta Wi-Fi-namn. Säkerställ att du tillåtit nödvändiga rättigheter och är ansluten till ett Wi-Fi-nätverk",
+  "grant_permission": "Ge tillåtelse",
   "haptic_feedback_switch": "Aktivera haptisk feedback",
   "haptic_feedback_title": "Haptisk Feedback",
   "header_settings_add_header_tip": "Lägg Till Header",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "Om det här är första gången du använder appen, välj ett eller flera backup-album så att tidslinjen kan fyllas med foton och videor från albumen.",
   "home_page_share_err_local": "Kan inte dela lokalt objekt via länk, hoppar över",
   "home_page_upload_err_limit": "Kan bara ladda upp max 30 objekt åt gången, hoppar över",
+  "ignore_icloud_photos": "Ignorera iCloud-foton",
+  "ignore_icloud_photos_description": "Foton lagrade i iCloud kommer inte laddas upp till Immich-servern",
   "image_saved_successfully": "Bild sparad",
   "image_viewer_page_state_provider_download_error": "Fel Vid Nedladdning",
   "image_viewer_page_state_provider_download_started": "Nedladdning Påbörjad",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "Delningsfel",
   "invalid_date": "Felaktigt datum",
   "invalid_date_format": "Felaktigt datumformat",
+  "library": "Bibliotek",
   "library_page_albums": "Album",
   "library_page_archive": "Arkiv",
   "library_page_device_albums": "Album på Enheten",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Äldsta foto",
   "library_page_sort_most_recent_photo": "Senaste foto",
   "library_page_sort_title": "Albumtitel",
+  "local_network": "Lokalt nätverk",
+  "local_network_sheet_info": "Appen kommer ansluta till servern via denna URL när det specificerade WiFi-nätverket används",
+  "location_permission": "Plats-rättighet",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Välj på karta",
   "location_picker_latitude": "Latitud",
   "location_picker_latitude_error": "Ange en giltig latitud",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Rörelsefoton",
   "multiselect_grid_edit_date_time_err_read_only": "Kan inte ändra datum på skrivskyddade objekt, hoppar över",
   "multiselect_grid_edit_gps_err_read_only": "Kan inte ändra plats på skrivskyddade objekt, hoppar över",
+  "my_albums": "Mina album",
+  "networking_settings": "Nätverk",
+  "networking_subtitle": "Hantera inställningar för server-endpointen",
   "no_assets_to_show": "Inga objekt att visa",
   "no_name": "Inget namn",
   "notification_permission_dialog_cancel": "Avbryt",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Tillåt rättighet för att slå på notiser.",
   "notification_permission_list_tile_enable_button": "Aktivera Notiser",
   "notification_permission_list_tile_title": "Notisrättighet",
+  "on_this_device": "På enheten",
   "partner_list_user_photos": "{user}s foton",
   "partner_list_view_all": "Visa alla",
   "partner_page_add_partner": "Lägg till partner",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} kommer inte längre att komma åt dina foton.",
   "partner_page_stop_sharing_title": "Sluta dela dina foton?",
   "partner_page_title": "Partner",
+  "partners": "Partner",
+  "people": "Människor",
   "permission_onboarding_back": "Bakåt",
   "permission_onboarding_continue_anyway": "Fortsätt ändå",
   "permission_onboarding_get_started": "Kom igång",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Rättigheten beviljad! Du är klar.",
   "permission_onboarding_permission_limited": "Rättighet begränsad. För att låta Immich säkerhetskopiera och hantera hela ditt galleri, tillåt foto- och video-rättigheter i Inställningar.",
   "permission_onboarding_request": "Immich kräver tillstånd för att se dina foton och videor.",
+  "places": "Platser",
+  "preferences_settings_subtitle": "Hantera appens inställningar",
   "preferences_settings_title": "Inställningar",
   "profile_drawer_app_logs": "Loggar",
   "profile_drawer_client_out_of_date_major": "Mobilappen är utdaterad. Uppdatera till senaste huvudversionen.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Inställningar",
   "profile_drawer_sign_out": "Logga ut",
   "profile_drawer_trash": "Papperskorg",
+  "recently_added": "Nyligen tillagda",
   "recently_added_page_title": "Nyligen tillagda",
+  "save": "Spara",
   "save_to_gallery": "Spara i galleri",
   "scaffold_body_error_occurred": "Fel uppstod",
+  "search_albums": "Sök i album",
   "search_bar_hint": "Sök bland dina foton",
   "search_filter_apply": "Aktivera filter",
   "search_filter_camera": "Kamera",
@@ -428,6 +484,7 @@
   "search_page_places": "Platser",
   "search_page_recently_added": "Nyligen tillagda",
   "search_page_screenshots": "Skärmdumpar",
+  "search_page_search_photos_videos": "Sök efter dina foton och videor",
   "search_page_selfies": "Selfies",
   "search_page_things": "Objekt",
   "search_page_videos": "Videor",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Förslag",
   "select_user_for_sharing_page_err_album": "Kunde inte skapa nytt album",
   "select_user_for_sharing_page_share_suggestions": "Förslag",
+  "server_endpoint": "Server-endpoint",
   "server_info_box_app_version": "App-version",
   "server_info_box_latest_release": "Senaste Version",
   "server_info_box_server_url": "Server-URL",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Ladda förhandsgranskning av bild",
   "setting_image_viewer_title": "Bilder",
   "setting_languages_apply": "Verkställ",
+  "setting_languages_subtitle": "Ändra appens språk",
   "setting_languages_title": "Språk",
   "setting_notifications_notify_failures_grace_period": "Rapportera säkerhetskopieringsfel i bakgrunden: {}",
   "setting_notifications_notify_hours": "{} timmar",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Ladda upp",
   "shared_link_manage_links": "Hantera Delade länkar",
   "shared_link_public_album": "Publikt album",
+  "shared_links": "Delade länkar",
   "share_done": "Klart",
+  "shared_with_me": "Delade med mig",
   "share_invite": "Bjuder in till album",
   "sharing_page_album": "Delade album",
   "sharing_page_description": "Skapa delade album för att dela foton och video med personer i ditt nätverk.",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Trestegsladdning kan öka prestandan, men kan också leda till signifikant högre nätverksbelastning",
   "theme_setting_three_stage_loading_title": "Aktivera trestegsladdning",
   "translated_text_options": "Val",
+  "trash": "Papperskorg",
   "trash_emptied": "Tömd papperskorg",
   "trash_page_delete": "Ta Bort",
   "trash_page_delete_all": "Ta Bort Alla",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Vill du säkerhetskopiera de valda objekten till servern?",
   "upload_dialog_ok": "Ladda Upp",
   "upload_dialog_title": "Ladda Upp Objekt",
+  "use_current_connection": "Använd aktuell anslutning",
+  "validate_endpoint_error": "Ange en giltig URL",
   "version_announcement_overlay_ack": "Bekräfta",
   "version_announcement_overlay_release_notes": "versionsinformation",
   "version_announcement_overlay_text_1": "Hej vännen, det finns en ny version av",
   "version_announcement_overlay_text_2": ". Ta gärna din tid att besöka ",
   "version_announcement_overlay_text_3": " för att se till att din docker-compose och .env-fil är uppdaterad för att undvika felkonfiguration, speciellt om du använder WatchTower eller liknande mekanism som automatiskt uppdaterar din container",
   "version_announcement_overlay_title": "Ny server-version finns tillgänglig \uD83C\uDF89",
+  "videos": "Videor",
   "viewer_remove_from_stack": "Ta bort från Stapeln",
   "viewer_stack_use_as_main_asset": "Använd som Huvudobjekt",
-  "viewer_unstack": "Stapla Av"
+  "viewer_unstack": "Stapla Av",
+  "wifi_name": "WiFi-namn",
+  "your_wifi_name": "Ditt WiFi-namn"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/th-TH.json b/mobile/assets/i18n/th-TH.json
index c93b0a37cf..d9ad20f50c 100644
--- a/mobile/assets/i18n/th-TH.json
+++ b/mobile/assets/i18n/th-TH.json
@@ -6,6 +6,8 @@
   "action_common_save": "Save",
   "action_common_select": "Select",
   "action_common_update": "อัปเดต",
+  "add_a_name": "Add a name",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "เพิ่มไปยัง {album}",
   "add_to_album_bottom_sheet_already_exists": "อยู่ใน {album} อยู่แล้ว",
   "advanced_settings_log_level_title": "ระดับการ Log: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "แก้ไขปัญหา",
   "album_info_card_backup_album_excluded": "ถูกยกเว้น",
   "album_info_card_backup_album_included": "รวม",
+  "albums": "Albums",
   "album_thumbnail_card_item": "1 รายการ",
   "album_thumbnail_card_items": "{} รายการ",
   "album_thumbnail_card_shared": " · ถูกแชร์",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "ลบออกจากอัลบั้ม",
   "album_viewer_appbar_share_to": "แชร์ให้",
   "album_viewer_page_share_add_users": "เพิ่มผู้ใช้งาน",
+  "all": "All",
   "all_people_page_title": "ผู้คน",
   "all_videos_page_title": "วิดีโอ",
   "app_bar_signout_dialog_content": "คุณแน่ใจว่าอยากออกจากระบบ",
   "app_bar_signout_dialog_ok": "ใช่",
   "app_bar_signout_dialog_title": "ออกจากระบบ",
+  "archived": "Archived",
   "archive_page_no_archived_assets": "ไม่พบทรัพยากรในที่เก็บถาวร",
   "archive_page_title": "เก็บถาวร ({})",
   "asset_action_delete_err_read_only": "ไม่สามารถลบทรัพยากรแบบอ่านอย่างเดียวได้ กำลังข้าม",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} asset(s) restored successfully",
   "assets_trashed": "{} asset(s) trashed",
   "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "ตัวดูทรัพยากร",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "อัลบั้มบนเครื่อง ({})",
   "backup_album_selection_page_albums_tap": "กดเพื่อรวม กดสองครั้งเพื่อยกเว้น",
   "backup_album_selection_page_assets_scatter": "ทรัพยาการสามารถกระจายไปในหลายอัลบั้ม ดังนั้นอัลบั้มสามารถถูกรวมหรือยกเว้นในกระบวนการสำรองข้อมูล",
@@ -127,6 +137,7 @@
   "backup_manual_success": "สำเร็จ",
   "backup_manual_title": "สถานะอัพโหลด",
   "backup_options_page_title": "ตัวเลือกการสำรองข้อมูล",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "รูปย่อคลังภาพ ({} ทรัพยากร)",
   "cache_settings_clear_cache_button": "ล้างแคช",
   "cache_settings_clear_cache_button_title": "ล้างแคชของแอพ จะส่งผลกระทบต่อประสิทธิภาพแอพจนกว่าแคชจะถูกสร้างใหม่",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "ควบคุมพฤติกรรมของที่จัดเก็บในตัวเครื่อง",
   "cache_settings_tile_title": "ที่จัดเก็บในตัวเครื่อง",
   "cache_settings_title": "ตั้งค่าแคช",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "ยืนยันรหัสผ่าน",
   "change_password_form_description": "สวัสดี {name},\n\nครั้งนี้อาจจะเป็นครั้งแรกที่คุณเข้าสู่ระบบ หรือมีคำขอเพื่อที่จะเปลี่ยนรหัสผ่านของคุI กรุณาเพิ่มรหัสผ่านใหม่ข้างล่าง",
   "change_password_form_new_password": "รหัสผ่านใหม่",
   "change_password_form_password_mismatch": "รหัสผ่านไม่ตรงกัน",
   "change_password_form_reenter_new_password": "กรอกรหัสผ่านใหม่",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Enter Password",
   "client_cert_import": "Import",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "นำออกจากที่เก็บถาวร",
   "control_bottom_app_bar_unfavorite": "นำออกจากรายการโปรด",
   "control_bottom_app_bar_upload": "อัพโหลด",
+  "create_album": "Create album",
   "create_album_page_untitled": "ไม่มีชื่อ",
+  "create_new": "CREATE NEW",
   "create_shared_album_page_create": "สร้าง",
   "create_shared_album_page_share": "แชร์",
   "create_shared_album_page_share_add_assets": "เพิ่มทรัพยากร",
@@ -193,6 +211,7 @@
   "crop": "Crop",
   "curated_location_page_title": "สถานที่",
   "curated_object_page_title": "สิ่งของ",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E dd MMM",
   "daily_title_text_date_year": "E dd MMM yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "ลบลิงก์ที่แชร์",
   "description_input_hint_text": "เพื่มรายละเอียด...",
   "description_input_submit_error": "อัพเดตรายละเอียดผิดพลาด ตรวจสอบ log เพื่อรายละเอียดเพิ่มเติม",
+  "download_canceled": "Download canceled",
+  "download_complete": "Download complete",
+  "download_enqueue": "Download enqueued",
   "download_error": "Download Error",
+  "download_failed": "Download failed",
+  "download_filename": "file: {}",
+  "download_finished": "Download finished",
+  "downloading": "Downloading...",
+  "downloading_media": "Downloading media",
+  "download_notfound": "Download not found",
+  "download_paused": "Download paused",
   "download_started": "Download started",
   "download_sucess": "Download success",
   "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_waiting_to_retry": "Waiting to retry",
   "edit_date_time_dialog_date_time": "วันและเวลา",
   "edit_date_time_dialog_timezone": "เขดเวลา",
   "edit_image_title": "Edit",
   "edit_location_dialog_title": "ตำแหน่ง",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Error: {}",
   "exif_bottom_sheet_description": "เพิ่มคำอธิบาย",
   "exif_bottom_sheet_details": "รายละเอียด",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "เปิดตารางรูปภาพที่กำลังทดลอง",
   "experimental_settings_subtitle": "ใช้ภายใต้ความเสี่ยงของคุณเอง!",
   "experimental_settings_title": "ทดลอง",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favorites",
   "favorites_page_no_favorites": "ไม่พบทรัพยากรในรายการโปรด",
   "favorites_page_title": "รายการโปรด",
   "filename_search": "File name or extension",
+  "filter": "Filter",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "เปิดการตอบสนองแบบสัมผัส",
   "haptic_feedback_title": "การตอบสนองแบบสัมผัส",
   "header_settings_add_header_tip": "Add Header",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "ถ้าครั้งนี้เป็นครั้งแรกที่ใช้แอปนี้ กรุณาเลือกอัลบั้มที่จะสำรองข้อมูล ไทม์ไลน์จะได้เพิ่มรูปภาพและวิดีโอที่อยู่ในอัลบั้ม",
   "home_page_share_err_local": "ไม่สามารถแชร์ผ่านลิงค์ได้ กำลังข้าม",
   "home_page_upload_err_limit": "สามารถอัพโหลดได้มากสุดครั้งละ 30 ทรัพยากร กำลังข้าม",
+  "ignore_icloud_photos": "Ignore iCloud photos",
+  "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
   "image_saved_successfully": "Image saved",
   "image_viewer_page_state_provider_download_error": "ดาวน์โหลดผิดพลาด",
   "image_viewer_page_state_provider_download_started": "ดาวน์โหลดเริ่มต้น",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "แชร์ผิดพลาด",
   "invalid_date": "Invalid date",
   "invalid_date_format": "Invalid date format",
+  "library": "Library",
   "library_page_albums": "อัลบั้ม",
   "library_page_archive": "เก็บถาวร",
   "library_page_device_albums": "อัลบั้มบนเครื่อง",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "รูปภาพที่เก่าที่สุด",
   "library_page_sort_most_recent_photo": "รูปล่าสุด",
   "library_page_sort_title": "ชื่ออัลบั้ม",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "เลือกบนแผนที่",
   "location_picker_latitude": "ละติจูต",
   "location_picker_latitude_error": "กรุณาเพิ่มละติจูตที่ถูกต้อง",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "ภาพเคลื่อนไหว",
   "multiselect_grid_edit_date_time_err_read_only": "ไม่สามารถแก้ไขวันที่ทรัพยากรแบบอ่านอย่างเดียว กำลังข้าม",
   "multiselect_grid_edit_gps_err_read_only": "ไม่สามารถแก้ตำแหน่งของทรัพยากรแบบอ่านอย่างเดียว กำลังข้าม",
+  "my_albums": "My albums",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "ไม่มีทรัพยากรให้แสดง",
   "no_name": "No name",
   "notification_permission_dialog_cancel": "ยกเลิก",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "อนุญาตการแจ้งเตือน",
   "notification_permission_list_tile_enable_button": "เปิดการแจ้งเดือน",
   "notification_permission_list_tile_title": "สิทธิ์การแจ้งเตือน",
+  "on_this_device": "On this device",
   "partner_list_user_photos": "รูปภาพของ {user}",
   "partner_list_view_all": "ดูทั้งหมด",
   "partner_page_add_partner": "เพิ่มพันธมิตร",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} จะไม่สามารถเข้าถึงรูปภาพของคุณ",
   "partner_page_stop_sharing_title": "หยุดแชร์รูปภาพ?",
   "partner_page_title": "พันธมิตร",
+  "partners": "Partners",
+  "people": "People",
   "permission_onboarding_back": "กลับ",
   "permission_onboarding_continue_anyway": "ดำเนินการต่อ",
   "permission_onboarding_get_started": "เริ่มต้น",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "ให้สิทธิ์สำเร็จ คุณพร้อมใช้งานแล้ว",
   "permission_onboarding_permission_limited": "สิทธ์จำกัด เพื่อให้ Immich สำรองข้อมูลและจัดการคลังภาพได้ ตั้งค่าสิทธิเข้าถึงรูปภาพและวิดีโอ",
   "permission_onboarding_request": "Immich จำเป็นจะต้องได้รับสิทธิ์ดูรูปภาพและวิดีโอ",
+  "places": "Places",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "การตั้งค่า",
   "profile_drawer_app_logs": "การบันทึก",
   "profile_drawer_client_out_of_date_major": "แอปพลิเคชันมีอัพเดต โปรดอัปเดตเป็นเวอร์ชันหลักล่าสุด",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "ตั้งค่า",
   "profile_drawer_sign_out": "ออกจากระบบ",
   "profile_drawer_trash": "ขยะ",
+  "recently_added": "Recently added",
   "recently_added_page_title": "เพิ่มล่าสุด",
+  "save": "Save",
   "save_to_gallery": "Save to gallery",
   "scaffold_body_error_occurred": "เกิดข้อผิดพลาด",
+  "search_albums": "Search albums",
   "search_bar_hint": "ค้นหารูปภาพของคุณ",
   "search_filter_apply": "บันทึกตัวกรอง",
   "search_filter_camera": "Camera",
@@ -428,6 +484,7 @@
   "search_page_places": "สถานที่",
   "search_page_recently_added": "เพิ่มล่าสุด",
   "search_page_screenshots": "แคปหน้าจอ",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "เซลฟี่",
   "search_page_things": "สิ่งของ",
   "search_page_videos": "วิดีโอ",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "ข้อเสนอแนะ",
   "select_user_for_sharing_page_err_album": "สร้างอัลบั้มล้มเหลว",
   "select_user_for_sharing_page_share_suggestions": "ข้อเสนอแนะ",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "เวอร์ชันแอพ",
   "server_info_box_latest_release": "เวอร์ชันล่าสุด",
   "server_info_box_server_url": "URL เซิร์ฟเวอร์",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "โหลดรูปภาพตัวอย่าง",
   "setting_image_viewer_title": "รูปภาพ",
   "setting_languages_apply": "บันทึก",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "ภาษา",
   "setting_notifications_notify_failures_grace_period": "แจ้งการสำรองข้อมูลในเบื้องหลังล้มเหลว: {}",
   "setting_notifications_notify_hours": "{} ชั่วโมง",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "อัพโหลด",
   "shared_link_manage_links": "บริหารลิงก์",
   "shared_link_public_album": "อัลบั้มสาธารณะ",
+  "shared_links": "Shared links",
   "share_done": "เสร็จ",
+  "shared_with_me": "Shared with me",
   "share_invite": "เชิญเข้าอัลบั้ม",
   "sharing_page_album": "อัลบั้มที่แชร์",
   "sharing_page_description": "สร้างอัลบั้มที่แชร์เพื่อแชร์รูปภาพและวิดีโอให้กับคนบนเครือข่ายคุณ",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "การโหลดแบบสามขั้นตอนอาจเพิ่มประสิทธิภาพในการโหลดแต่จะทำให้โหลดเครื่อข่ายเพิ่มขึ้นมาก",
   "theme_setting_three_stage_loading_title": "เปิดการโหลดสามขั้นตอน",
   "translated_text_options": "ตัวเลือก",
+  "trash": "Trash",
   "trash_emptied": "Emptied trash",
   "trash_page_delete": "ลบ",
   "trash_page_delete_all": "ลบทั้งหมด",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "คุณต้องการอัพโหลดทรัพยากรดังกล่าวบนเซิร์ฟเวอร์หรือไม่?",
   "upload_dialog_ok": "อัปโหลด",
   "upload_dialog_title": "อัปโหลดทรัพยากร",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "รับทราบ",
   "version_announcement_overlay_release_notes": "รายงานการอัพเดท",
   "version_announcement_overlay_text_1": "สวัสดีเพื่อน ขณะนี้มีเวอร์ชั้นใหม่ของ",
   "version_announcement_overlay_text_2": "กรุณาใช้เวลาดู",
   "version_announcement_overlay_text_3": "และรับรองว่าการติดตั้ง docker-compose และ .env เป็นปัจจุบันเพื่อไม่ให้เกิดการติดตั้งผิดพลาด โดยเฉพาะผู้ใช้ WatchTower หรือระบบอัพเดตแอปพลิเคชั่นเซิร์ฟเวอร์อัตโนมัติ",
   "version_announcement_overlay_title": "มีเวอร์ชันใหม่สำหรับเซิร์ฟเวอร์ \uD83C\uDF89",
+  "videos": "Videos",
   "viewer_remove_from_stack": "เอาออกจากที่ซ้อน",
   "viewer_stack_use_as_main_asset": "ใช้เป็นทรัพยากรหลัก",
-  "viewer_unstack": "หยุดซ้อน"
+  "viewer_unstack": "หยุดซ้อน",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/uk-UA.json b/mobile/assets/i18n/uk-UA.json
index f3b2b0ba5f..3c4303e6a7 100644
--- a/mobile/assets/i18n/uk-UA.json
+++ b/mobile/assets/i18n/uk-UA.json
@@ -6,12 +6,14 @@
   "action_common_save": "Зберегти",
   "action_common_select": "Вибрати",
   "action_common_update": "Оновити",
-  "add_to_album_bottom_sheet_added": "Додати до {album}",
+  "add_a_name": "Додати ім'я",
+  "add_endpoint": "Add endpoint",
+  "add_to_album_bottom_sheet_added": "Додано до {album}",
   "add_to_album_bottom_sheet_already_exists": "Вже є в {album}",
   "advanced_settings_log_level_title": "Log level: {}",
   "advanced_settings_prefer_remote_subtitle": "Деякі пристрої вельми повільно завантажують мініатюри із елементів на пристрої. Активуйте для завантаження віддалених мініатюр натомість.",
   "advanced_settings_prefer_remote_title": "Перевага віддаленим зображенням",
-  "advanced_settings_proxy_headers_subtitle": "Определите заголовки прокси-сервера, которые Immich должен отправлять с каждым сетевым запросом.",
+  "advanced_settings_proxy_headers_subtitle": "Визначте заголовки проксі-сервера, які Immich має надсилати з кожним мережевим запитом.",
   "advanced_settings_proxy_headers_title": "Проксі-заголовки",
   "advanced_settings_self_signed_ssl_subtitle": "Пропускає перевірку SSL-сертифіката сервера. Потрібне для самопідписаних сертифікатів.",
   "advanced_settings_self_signed_ssl_title": "Дозволити самопідписані SSL-сертифікати",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Усунення несправностей",
   "album_info_card_backup_album_excluded": "ВИЛУЧЕНИЙ",
   "album_info_card_backup_album_included": "ВКЛЮЧЕНИЙ",
+  "albums": "Альбоми",
   "album_thumbnail_card_item": "1 елемент",
   "album_thumbnail_card_items": "{} елементів",
   "album_thumbnail_card_shared": " · Спільний",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Видалити з альбому",
   "album_viewer_appbar_share_to": "Поділитися",
   "album_viewer_page_share_add_users": "Додати користувачів",
+  "all": "Усі",
   "all_people_page_title": "Люди",
   "all_videos_page_title": "Відео",
   "app_bar_signout_dialog_content": "Ви впевнені, що бажаєте вийти з аккаунта?",
   "app_bar_signout_dialog_ok": "Так",
   "app_bar_signout_dialog_title": "Вийти з аккаунта",
+  "archived": "Архів",
   "archive_page_no_archived_assets": "Немає архівних елементів",
   "archive_page_title": "Архів ({})",
   "asset_action_delete_err_read_only": "Неможливо видалити елемент(и) лише для читання, пропущено",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "{} елемент(и) успішно відновлено",
   "assets_trashed": "{} елемент(и) поміщено до кошика",
   "assets_trashed_from_server": "{} елемент(и) поміщено до кошика на сервері Immich",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "Переглядач зображень",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Альбоми на пристрої ({})",
   "backup_album_selection_page_albums_tap": "Торкніться, щоб включити,\nторкніться двічі, щоб виключити",
   "backup_album_selection_page_assets_scatter": "Елементи можуть належати до кількох альбомів водночас. Таким чином, альбоми можуть бути включені або вилучені під час резервного копіювання.",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Успіх",
   "backup_manual_title": "Стан завантаження",
   "backup_options_page_title": "Резервне копіювання",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Мініатюри сторінок бібліотеки ({} елементи)",
   "cache_settings_clear_cache_button": "Очистити кеш",
   "cache_settings_clear_cache_button_title": "Очищає кеш програми. Це суттєво знизить продуктивність програми, доки кеш не буде перебудовано.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Керування поведінкою локального сховища",
   "cache_settings_tile_title": "Локальне сховище",
   "cache_settings_title": "Налаштування кешування",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Підтвердити пароль",
   "change_password_form_description": "Привіт {name},\n\nВи або або вперше входите у систему, або було зроблено запит на зміну вашого пароля. \nВведіть ваш новий пароль.",
   "change_password_form_new_password": "Новий пароль",
   "change_password_form_password_mismatch": "Паролі не співпадають",
   "change_password_form_reenter_new_password": "Повторіть новий пароль",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "OK",
   "client_cert_enter_password": "Введіть пароль",
   "client_cert_import": "Імпорт",
@@ -185,14 +201,17 @@
   "control_bottom_app_bar_unarchive": "Розархівувати",
   "control_bottom_app_bar_unfavorite": "Видалити з улюблених",
   "control_bottom_app_bar_upload": "Завантажити",
+  "create_album": "Створити альбом",
   "create_album_page_untitled": "Без назви",
+  "create_new": "СТВОРИТИ НОВИЙ",
   "create_shared_album_page_create": "Створити",
   "create_shared_album_page_share": "Поділитися",
   "create_shared_album_page_share_add_assets": "ДОДАТИ ЕЛЕМЕНТИ",
   "create_shared_album_page_share_select_photos": "Вибрати Знімки",
-  "crop": "Crop",
+  "crop": "Кадрувати",
   "curated_location_page_title": "Місця",
   "curated_object_page_title": "Речі",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,15 +229,28 @@
   "delete_shared_link_dialog_title": "Видалити спільне посилання",
   "description_input_hint_text": "Додати опис...",
   "description_input_submit_error": "Помилка оновлення опису, перевірте логи для подробиць",
-  "download_error": "Download Error",
-  "download_started": "Download started",
-  "download_sucess": "Download success",
-  "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_canceled": "Завантаження скасовано",
+  "download_complete": "Завантаження закінчено",
+  "download_enqueue": "Завантаження поставлено в чергу",
+  "download_error": "Помилка завантаження",
+  "download_failed": "Завантаження не вдалося",
+  "download_filename": "файл: {}",
+  "download_finished": "Завантаження закінчено",
+  "downloading": "Завантаження...",
+  "downloading_media": "Завантаження медіа",
+  "download_notfound": "Завантаження не виявлено",
+  "download_paused": "Завантаження призупинено",
+  "download_started": "Завантаження розпочато",
+  "download_sucess": "Успішне завантаження",
+  "download_sucess_android": "Медіафайли завантажено в DCIM/Immich",
+  "download_waiting_to_retry": "Очікування повторної спроби",
   "edit_date_time_dialog_date_time": "Дата і час",
   "edit_date_time_dialog_timezone": "Часовий пояс",
-  "edit_image_title": "Edit",
+  "edit_image_title": "Редагувати",
   "edit_location_dialog_title": "Місцезнаходження",
-  "error_saving_image": "Error: {}",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
+  "error_saving_image": "Помилка: {}",
   "exif_bottom_sheet_description": "Додати опис...",
   "exif_bottom_sheet_details": "ПОДРОБИЦІ",
   "exif_bottom_sheet_location": "МІСЦЕ",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "Експериментальний макет знімків",
   "experimental_settings_subtitle": "На власний ризик!",
   "experimental_settings_title": "Експериментальні",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Вибране",
   "favorites_page_no_favorites": "Немає улюблених елементів",
   "favorites_page_title": "Улюблені",
   "filename_search": "Ім'я або розширення файлу",
+  "filter": "Фільтр",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
   "haptic_feedback_switch": "Увімкнути тактильну віддачу",
   "haptic_feedback_title": "Тактильна віддача",
   "header_settings_add_header_tip": "Додати заголовок",
@@ -255,13 +293,16 @@
   "home_page_first_time_notice": "Якщо ви вперше користуєтеся програмою, переконайтеся, що ви вибрали альбоми для резервування, щоб могти заповнювати хронологію знімків та відео в альбомах.",
   "home_page_share_err_local": "Неможливо поділитися локальними елементами через посилання, пропущено",
   "home_page_upload_err_limit": "Можна вантажити не більше 30 елементів водночас, пропущено",
-  "image_saved_successfully": "Image saved",
+  "ignore_icloud_photos": "Пропускати файли з iCloud",
+  "ignore_icloud_photos_description": "Не завантажувати файли в Immich, якщо вони зберігаються в iCloud",
+  "image_saved_successfully": "Зображення збережено",
   "image_viewer_page_state_provider_download_error": "Помилка завантаження",
   "image_viewer_page_state_provider_download_started": "Завантаження почалося",
   "image_viewer_page_state_provider_download_success": "Усіпшно завантажено",
   "image_viewer_page_state_provider_share_error": "Помилка спільного доступу",
   "invalid_date": "Недійсна дата",
   "invalid_date_format": "Недійсний формат дати",
+  "library": "Бібліотека",
   "library_page_albums": "Альбоми",
   "library_page_archive": "Архів",
   "library_page_device_albums": "Альбоми на пристрої",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Найдавніші фото",
   "library_page_sort_most_recent_photo": "Найновіші фото",
   "library_page_sort_title": "Назва альбому",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Обрати на мапі",
   "location_picker_latitude": "Широта",
   "location_picker_latitude_error": "Вкажіть дійсну широту",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Рухомі Знімки",
   "multiselect_grid_edit_date_time_err_read_only": "Неможливо редагувати дату елементів лише для читання, пропущено",
   "multiselect_grid_edit_gps_err_read_only": "Неможливо редагувати місцезнаходження елементів лише для читання, пропущено",
+  "my_albums": "Мої альбоми",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "Елементи відсутні",
   "no_name": "Без імені",
   "notification_permission_dialog_cancel": "Скасувати",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Надати дозвіл для сповіщень.",
   "notification_permission_list_tile_enable_button": "Увімкнути Сповіщення",
   "notification_permission_list_tile_title": "Дозвіл на Сповіщення",
+  "on_this_device": "На цьому пристрої",
   "partner_list_user_photos": "Фотографії {user}",
   "partner_list_view_all": "Переглянути усі",
   "partner_page_add_partner": "Додати партнера",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} втратить доступ до ваших знімків.",
   "partner_page_stop_sharing_title": "Припинити надання ваших знімків?",
   "partner_page_title": "Партнер",
+  "partners": "\nПартнери",
+  "people": "Люди",
   "permission_onboarding_back": "Назад",
   "permission_onboarding_continue_anyway": "Все одно продовжити",
   "permission_onboarding_get_started": "Розпочати",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Доступ надано! Все готово.",
   "permission_onboarding_permission_limited": "Обмежений доступ. Аби дозволити Immich резервне копіювання та керування вашою галереєю, надайте доступ до знімків та відео у Налаштуваннях",
   "permission_onboarding_request": "Immich потребує доступу до ваших знімків та відео.",
+  "places": "Місця",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Параметри",
   "profile_drawer_app_logs": "Журнал",
   "profile_drawer_client_out_of_date_major": "Мобільний додаток застарів. Будь ласка, оновіть до останньої мажорної версії.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Налаштування",
   "profile_drawer_sign_out": "Вийти",
   "profile_drawer_trash": "Кошик",
+  "recently_added": "Нещодавно додані",
   "recently_added_page_title": "Нещодавні",
-  "save_to_gallery": "Save to gallery",
+  "save": "Save",
+  "save_to_gallery": "Зберегти в галерею",
   "scaffold_body_error_occurred": "Виникла помилка",
+  "search_albums": "Пошук альбому",
   "search_bar_hint": "Шукати ваші знімки",
   "search_filter_apply": "Застосувати фільтр",
   "search_filter_camera": "Камера",
@@ -428,6 +484,7 @@
   "search_page_places": "Місця",
   "search_page_recently_added": "Нещодавно додані",
   "search_page_screenshots": "Знімки екрану",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Селфі",
   "search_page_things": "Речі",
   "search_page_videos": "Відео",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Пропозиції",
   "select_user_for_sharing_page_err_album": "Не вдалося створити альбом",
   "select_user_for_sharing_page_share_suggestions": "Пропозиції",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "Версія додатка",
   "server_info_box_latest_release": "Остання версія",
   "server_info_box_server_url": "URL сервера",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Завантажувати зображення попереднього перегляду",
   "setting_image_viewer_title": "Зображення",
   "setting_languages_apply": "Застосувати",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Мова",
   "setting_notifications_notify_failures_grace_period": "Повідомити про помилки фонового резервного копіювання: {}",
   "setting_notifications_notify_hours": "{} годин",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Завантажити",
   "shared_link_manage_links": "Керування спільними посиланнями",
   "shared_link_public_album": "Публічний альбом",
+  "shared_links": "Публічні посилання",
   "share_done": "Готово",
+  "shared_with_me": "Доступні мені",
   "share_invite": "Запросити в альбом",
   "sharing_page_album": "Спільні альбоми",
   "sharing_page_description": "Створюйте спільні альбоми, щоб ділитися знімками та відео з людьми у вашій мережі.",
@@ -550,7 +611,7 @@
   "theme_setting_asset_list_storage_indicator_title": "Показувати піктограму сховища на плитках елементів",
   "theme_setting_asset_list_tiles_per_row_title": "Кількість елементів у рядку ({})",
   "theme_setting_colorful_interface_subtitle": "Застосувати основний колір на поверхню фону.",
-  "theme_setting_colorful_interface_title": "Colorful interface",
+  "theme_setting_colorful_interface_title": "Барвистий інтерфейс",
   "theme_setting_dark_mode_switch": "Темна тема",
   "theme_setting_image_viewer_quality_subtitle": "Налаштування якості перегляду повноекранних зображень",
   "theme_setting_image_viewer_quality_title": "Якість перегляду зображень",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Триетапне завантаження може підвищити продуктивність завантаження, але спричинить значно більше навантаження на мережу",
   "theme_setting_three_stage_loading_title": "Увімкнути триетапне завантаження",
   "translated_text_options": "Налаштування",
+  "trash": "Кошик",
   "trash_emptied": "Кошик очищений",
   "trash_page_delete": "Видалити",
   "trash_page_delete_all": "Видалити усі",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Бажаєте створити резервну копію вибраних елементів на сервері?",
   "upload_dialog_ok": "Завантажити",
   "upload_dialog_title": "Завантажити Елементи",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Прийняти",
   "version_announcement_overlay_release_notes": "примітки до випуску",
   "version_announcement_overlay_text_1": "Вітаємо, є новий випуск ",
   "version_announcement_overlay_text_2": "знайдіть хвильку навідатися на ",
   "version_announcement_overlay_text_3": "і переконайтеся, що ваші налаштування docker-compose та .env оновлені, аби запобігти будь-якій неправильній конфігурації, особливо, якщо ви використовуєте WatchTower або інший механізм, для автоматичних оновлень вашої серверної частини.",
   "version_announcement_overlay_title": "Доступна нова версія сервера \uD83C\uDF89",
+  "videos": "Відео",
   "viewer_remove_from_stack": "Видалити зі стеку",
   "viewer_stack_use_as_main_asset": "Використовувати як основний елементи",
-  "viewer_unstack": "Розібрати стек"
+  "viewer_unstack": "Розібрати стек",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/vi-VN.json b/mobile/assets/i18n/vi-VN.json
index 6cd2a080e4..6990d1f266 100644
--- a/mobile/assets/i18n/vi-VN.json
+++ b/mobile/assets/i18n/vi-VN.json
@@ -6,6 +6,8 @@
   "action_common_save": "Lưu",
   "action_common_select": "Chọn",
   "action_common_update": "Cập nhật",
+  "add_a_name": "Add a name",
+  "add_endpoint": "Add endpoint",
   "add_to_album_bottom_sheet_added": "Thêm vào {album}",
   "add_to_album_bottom_sheet_already_exists": "Đã có sẵn trong {album}",
   "advanced_settings_log_level_title": "Phân loại nhật ký: {}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "Xử lý sự cố",
   "album_info_card_backup_album_excluded": "ĐÃ BỎ QUA",
   "album_info_card_backup_album_included": "ĐÃ THÊM",
+  "albums": "Albums",
   "album_thumbnail_card_item": "1 mục",
   "album_thumbnail_card_items": "{} mục",
   "album_thumbnail_card_shared": " · Chia sẻ",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "Xoá khỏi album",
   "album_viewer_appbar_share_to": "Chia sẻ với",
   "album_viewer_page_share_add_users": "Thêm người dùng",
+  "all": "Tất cả",
   "all_people_page_title": "Mọi người",
   "all_videos_page_title": "Video",
   "app_bar_signout_dialog_content": "Bạn có muốn đăng xuất?",
   "app_bar_signout_dialog_ok": "Có",
   "app_bar_signout_dialog_title": "Đăng xuất",
+  "archived": "Archived",
   "archive_page_no_archived_assets": "Không tìm thấy ảnh đã lưu trữ",
   "archive_page_title": "Kho lưu trữ ({})",
   "asset_action_delete_err_read_only": "Không thể xoá ảnh chỉ có quyền đọc, bỏ qua",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "Đã khôi phục {} mục thành công",
   "assets_trashed": "Đã chuyển {} mục vào thùng rác",
   "assets_trashed_from_server": "Đã chuyển {} mục từ máy chủ Immich vào thùng rác",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
   "asset_viewer_settings_title": "Trình xem ảnh",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
   "backup_album_selection_page_albums_device": "Album trên thiết bị ({})",
   "backup_album_selection_page_albums_tap": "Nhấn để chọn, nhấn đúp để bỏ qua",
   "backup_album_selection_page_assets_scatter": "Ảnh có thể có trong nhiều album khác nhau. Trong quá trình sao lưu, bạn có thể chọn để sao lưu tất cả các album hoặc chỉ một số album nhất định.",
@@ -117,7 +127,7 @@
   "backup_controller_page_total": "Tổng số",
   "backup_controller_page_total_sub": "Tất cả ảnh và video không trùng lập từ các album được chọn",
   "backup_controller_page_turn_off": "Tắt sao lưu khi ứng dụng hoạt động",
-  "backup_controller_page_turn_on": "Bật sao lưu khi ứng dụng hoạt động",
+  "backup_controller_page_turn_on": "Bật sao lưu khi mở ứng dụng",
   "backup_controller_page_uploading_file_info": "Thông tin tệp đang tải lên",
   "backup_err_only_album": "Không thể xóa album duy nhất",
   "backup_info_card_assets": "ảnh",
@@ -127,6 +137,7 @@
   "backup_manual_success": "Thành công",
   "backup_manual_title": "Trạng thái tải lên",
   "backup_options_page_title": "Tuỳ chỉnh sao lưu",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
   "cache_settings_album_thumbnails": "Trang thư viện hình thu nhỏ ({} ảnh)",
   "cache_settings_clear_cache_button": "Xoá bộ nhớ đệm",
   "cache_settings_clear_cache_button_title": "Xóa bộ nhớ đệm của ứng dụng. Điều này sẽ ảnh hưởng đến hiệu suất của ứng dụng đến khi bộ nhớ đệm được tạo lại.",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "Kiểm soát cách xử lý lưu trữ cục bộ",
   "cache_settings_tile_title": "Lưu trữ cục bộ",
   "cache_settings_title": "Cài đặt bộ nhớ đệm",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "Xác nhận mật khẩu",
   "change_password_form_description": "Xin chào {name},\n\nĐây là lần đầu tiên bạn đăng nhập vào hệ thống hoặc đã có yêu cầu thay đổi mật khẩu. Vui lòng nhập mật khẩu mới bên dưới.",
   "change_password_form_new_password": "Mật khẩu mới",
   "change_password_form_password_mismatch": "Mật khẩu không giống nhau",
   "change_password_form_reenter_new_password": "Nhập lại mật khẩu mới",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
   "client_cert_dialog_msg_confirm": "Đồng ý",
   "client_cert_enter_password": "Nhập mật khẩu",
   "client_cert_import": "Nhập",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "Huỷ lưu trữ",
   "control_bottom_app_bar_unfavorite": "Bỏ yêu thích",
   "control_bottom_app_bar_upload": "Tải lên",
+  "create_album": "Create album",
   "create_album_page_untitled": "Không tiêu đề",
+  "create_new": "CREATE NEW",
   "create_shared_album_page_create": "Tạo",
   "create_shared_album_page_share": "Chia sẻ",
   "create_shared_album_page_share_add_assets": "THÊM ẢNH",
@@ -193,6 +211,7 @@
   "crop": "Cắt",
   "curated_location_page_title": "Địa điểm",
   "curated_object_page_title": "Sự vật",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "Xoá liên kết đã chia sẻ",
   "description_input_hint_text": "Thêm mô tả...",
   "description_input_submit_error": "Cập nhật mô tả không thành công, vui lòng kiểm tra nhật ký để biết thêm chi tiết",
-  "download_error": "Download Error",
-  "download_started": "Download started",
-  "download_sucess": "Download success",
-  "download_sucess_android": "The media has been downloaded to DCIM/Immich",
+  "download_canceled": "Đã hủy tải xuống",
+  "download_complete": "Tải xuống hoàn tất",
+  "download_enqueue": "Đang chờ tải xuống",
+  "download_error": "Lỗi tải xuống",
+  "download_failed": "Tải xuống thất bại",
+  "download_filename": "tập tin: {}",
+  "download_finished": "Tải xuống hoàn tất",
+  "downloading": "Đang tải xuống...",
+  "downloading_media": "Đang tải xuống phương tiện",
+  "download_notfound": "Không tìm thấy tải xuống",
+  "download_paused": "Đã tạm dừng tải xuống",
+  "download_started": "Đã bắt đầu tải xuống",
+  "download_sucess": "Tải xuống thành công",
+  "download_sucess_android": "Phương tiện đã được tải vào DCIM/Immich",
+  "download_waiting_to_retry": "Đang chờ thử lại",
   "edit_date_time_dialog_date_time": "Ngày và Giờ",
   "edit_date_time_dialog_timezone": "Múi giờ",
   "edit_image_title": "Sửa",
   "edit_location_dialog_title": "Vị trí",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "Lỗi: {}",
   "exif_bottom_sheet_description": "Thêm mô tả...",
   "exif_bottom_sheet_details": "CHI TIẾT",
@@ -229,11 +261,17 @@
   "experimental_settings_new_asset_list_title": "Bật lưới ảnh thử nghiệm",
   "experimental_settings_subtitle": "Sử dụng có thể rủi ro!",
   "experimental_settings_title": "Chưa hoàn thiện",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "Favorites",
   "favorites_page_no_favorites": "Không tìm thấy ảnh yêu thích",
   "favorites_page_title": "Ảnh yêu thích",
   "filename_search": "Tên hoặc phần mở rộng tập tin",
-  "haptic_feedback_switch": "Bật phản hồi haptic\n",
-  "haptic_feedback_title": "Haptic Feedback\n",
+  "filter": "Bộ lọc",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
+  "haptic_feedback_switch": "Bật phản hồi haptic",
+  "haptic_feedback_title": "Phản hồi Hapic",
   "header_settings_add_header_tip": "Thêm Header",
   "header_settings_field_validator_msg": "Trường này không được để trống",
   "header_settings_header_name_input": "Tên Header",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "Nếu đây là lần đầu bạn sử dụng ứng dụng, đảm bảo chọn (các) album sao lưu để dòng thời gian có thể tự động thêm ảnh và video trong (các) album.\n",
   "home_page_share_err_local": "Không thể chia sẻ ảnh cục bộ qua liên kết, bỏ qua",
   "home_page_upload_err_limit": "Chỉ có thể tải lên tối đa 30 ảnh cùng một lúc, bỏ qua",
+  "ignore_icloud_photos": "Bỏ qua ảnh iCloud",
+  "ignore_icloud_photos_description": "Ảnh được lưu trữ trên iCloud sẽ không được tải lên máy chủ Immich",
   "image_saved_successfully": "Đã lưu ảnh",
   "image_viewer_page_state_provider_download_error": "Tải xuống không thành công",
   "image_viewer_page_state_provider_download_started": "Đã bắt đầu tải xuống",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "Chia sẻ không thành công",
   "invalid_date": "Ngày không hợp lệ",
   "invalid_date_format": "Định dạng ngày không hợp lệ",
+  "library": "Library",
   "library_page_albums": "Album",
   "library_page_archive": "Kho lưu trữ",
   "library_page_device_albums": "Album trên thiết bị",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "Ảnh cũ nhất",
   "library_page_sort_most_recent_photo": "Ảnh gần đây nhất",
   "library_page_sort_title": "Tiêu đề album",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
   "location_picker_choose_on_map": "Chọn trên bản đồ",
   "location_picker_latitude": "Vĩ độ",
   "location_picker_latitude_error": "Nhập vĩ độ hợp lệ",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "Ảnh động",
   "multiselect_grid_edit_date_time_err_read_only": "Không thể chỉnh sửa ngày của ảnh chỉ có quyền đọc, bỏ qua",
   "multiselect_grid_edit_gps_err_read_only": "Không thể chỉnh sửa vị trí của ảnh chỉ có quyền đọc, bỏ qua",
+  "my_albums": "My albums",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
   "no_assets_to_show": "Không có mục nào để hiển thị",
   "no_name": "Không có tên",
   "notification_permission_dialog_cancel": "Từ chối",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "Cấp quyền để bật thông báo",
   "notification_permission_list_tile_enable_button": "Bật thông báo",
   "notification_permission_list_tile_title": "Quyền thông báo",
+  "on_this_device": "On this device",
   "partner_list_user_photos": "Ảnh của {user}",
   "partner_list_view_all": "Xem tất cả",
   "partner_page_add_partner": "Thêm người thân",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} sẽ không thể truy cập ảnh của bạn.",
   "partner_page_stop_sharing_title": "Ngừng chia sẻ ảnh của bạn?",
   "partner_page_title": "Người thân",
+  "partners": "Partners",
+  "people": "People",
   "permission_onboarding_back": "Quay lại",
   "permission_onboarding_continue_anyway": "Vẫn tiếp tục",
   "permission_onboarding_get_started": "Bắt đầu",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "Cấp quyền hoàn tất!",
   "permission_onboarding_permission_limited": "Quyền truy cập vào ảnh của bạn bị hạn chế. Để Immich sao lưu và quản lý toàn bộ thư viện ảnh của bạn, hãy cấp quyền truy cập toàn bộ ảnh trong Cài đặt.",
   "permission_onboarding_request": "Immich cần quyền để xem ảnh và video của bạn",
+  "places": "Places",
+  "preferences_settings_subtitle": "Manage the app's preferences",
   "preferences_settings_title": "Tuỳ chỉnh",
   "profile_drawer_app_logs": "Nhật ký",
   "profile_drawer_client_out_of_date_major": "Ứng dụng đã lỗi thời. Vui lòng cập nhật lên phiên bản chính mới nhất.",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "Cài đặt",
   "profile_drawer_sign_out": "Đăng xuất",
   "profile_drawer_trash": "Thùng rác",
+  "recently_added": "Recently added",
   "recently_added_page_title": "Mới thêm gần đây",
+  "save": "Save",
   "save_to_gallery": "Lưu vào thư viện",
   "scaffold_body_error_occurred": "Xảy ra lỗi",
+  "search_albums": "Search albums",
   "search_bar_hint": "Tìm kiếm ảnh của bạn",
   "search_filter_apply": "Áp dụng bộ lọc",
   "search_filter_camera": "Máy ảnh",
@@ -428,6 +484,7 @@
   "search_page_places": "Địa điểm",
   "search_page_recently_added": "Mới thêm gần đây",
   "search_page_screenshots": "Ảnh màn hình",
+  "search_page_search_photos_videos": "Search for your photos and videos",
   "search_page_selfies": "Ảnh selfie",
   "search_page_things": "Sự vật",
   "search_page_videos": "Video",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "Gợi ý",
   "select_user_for_sharing_page_err_album": "Tạo album thất bại",
   "select_user_for_sharing_page_share_suggestions": "Gợi ý",
+  "server_endpoint": "Server Endpoint",
   "server_info_box_app_version": "Phiên bản ứng dụng",
   "server_info_box_latest_release": "Phiên bản mới nhất",
   "server_info_box_server_url": "Địa chỉ máy chủ",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "Tải ảnh xem trước",
   "setting_image_viewer_title": "Hình ảnh",
   "setting_languages_apply": "Áp dụng",
+  "setting_languages_subtitle": "Change the app's language",
   "setting_languages_title": "Ngôn ngữ",
   "setting_notifications_notify_failures_grace_period": "Thông báo sao lưu nền thất bại: {}",
   "setting_notifications_notify_hours": "{} giờ",
@@ -486,7 +545,7 @@
   "shared_album_section_people_owner_label": "Chủ sở hữu",
   "shared_album_section_people_title": "MỌI NGƯỜI",
   "share_dialog_preparing": "Đang xử lý...",
-  "shared_link_app_bar_title": "Đường liên kết chia sẻ",
+  "shared_link_app_bar_title": "Liên kết chia sẻ",
   "shared_link_clipboard_copied_massage": "Đã sao chép tới bản ghi tạm",
   "shared_link_clipboard_text": "Liên kết: {}\nMật khẩu: {}",
   "shared_link_create_app_bar_title": "Tạo liên kết để chia sẻ",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "Tải lên",
   "shared_link_manage_links": "Quản lý liên kết được chia sẻ",
   "shared_link_public_album": "Album công khai",
+  "shared_links": "Shared links",
   "share_done": "Hoàn tất",
+  "shared_with_me": "Shared with me",
   "share_invite": "Mời vào album",
   "sharing_page_album": "Album chia sẻ",
   "sharing_page_description": "Tạo album chia sẻ để chia sẻ ảnh và video với những người trong mạng của bạn.",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "Tải ba giai doạn có thể tăng hiệu năng tải ảnh nhưng sẽ tốn dữ liệu mạng đáng kể.",
   "theme_setting_three_stage_loading_title": "Bật tải ba giai đoạn",
   "translated_text_options": "Tuỳ chỉnh",
+  "trash": "Trash",
   "trash_emptied": "Đã dọn sạch thùng rác",
   "trash_page_delete": "Xoá",
   "trash_page_delete_all": "Xoá tất cả",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "Bạn có muốn sao lưu những mục đã chọn tới máy chủ không?",
   "upload_dialog_ok": "Tải lên",
   "upload_dialog_title": "Tải lên ảnh",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
   "version_announcement_overlay_ack": "Công nhận",
   "version_announcement_overlay_release_notes": "ghi chú phát hành",
   "version_announcement_overlay_text_1": "Chào bạn, có một bản phát hành mới của",
   "version_announcement_overlay_text_2": "vui lòng dành thời gian của bạn để đến thăm",
   "version_announcement_overlay_text_3": "và đảm bảo cài đặt docker-compose và tệp .env của bạn đã cập nhật để tránh bất kỳ cấu hình sai sót, đặc biệt nếu bạn dùng WatchTower hoặc bất kỳ cơ chế nào xử lý việc cập nhật ứng dụng máy chủ của bạn tự động.",
   "version_announcement_overlay_title": "Phiên bản máy chủ có bản cập nhật mới",
+  "videos": "Videos",
   "viewer_remove_from_stack": "Xoá khỏi nhóm",
   "viewer_stack_use_as_main_asset": "Đặt làm lựa chọn hàng đầu",
-  "viewer_unstack": "Huỷ xếp nhóm"
+  "viewer_unstack": "Huỷ xếp nhóm",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/zh-CN.json b/mobile/assets/i18n/zh-CN.json
index d4e7f0406e..36481f6bb6 100644
--- a/mobile/assets/i18n/zh-CN.json
+++ b/mobile/assets/i18n/zh-CN.json
@@ -6,6 +6,8 @@
   "action_common_save": "保存",
   "action_common_select": "选择",
   "action_common_update": "更新",
+  "add_a_name": "添加姓名",
+  "add_endpoint": "添加服务接口",
   "add_to_album_bottom_sheet_added": "添加到 {album}",
   "add_to_album_bottom_sheet_already_exists": "已在 {album} 中",
   "advanced_settings_log_level_title": "日志等级:{}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "故障排除",
   "album_info_card_backup_album_excluded": "已排除",
   "album_info_card_backup_album_included": "已选中",
+  "albums": "相册",
   "album_thumbnail_card_item": "1 项",
   "album_thumbnail_card_items": "{} 项",
   "album_thumbnail_card_shared": " · 已共享",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "从相册中移除",
   "album_viewer_appbar_share_to": "共享给",
   "album_viewer_page_share_add_users": "创建用户",
+  "all": "所有",
   "all_people_page_title": "人物",
   "all_videos_page_title": "视频",
   "app_bar_signout_dialog_content": "您确定要退出吗?",
   "app_bar_signout_dialog_ok": "是",
   "app_bar_signout_dialog_title": "退出登录",
+  "archived": "已存档",
   "archive_page_no_archived_assets": "未找到归档项目",
   "archive_page_title": "归档({})",
   "asset_action_delete_err_read_only": "无法删除只读项目,跳过",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "已成功恢复{}个项目",
   "assets_trashed": "{}个回收站项目",
   "assets_trashed_from_server": "{}个项目已放入回收站",
+  "asset_viewer_settings_subtitle": "管理图库浏览器设置",
   "asset_viewer_settings_title": "资源查看器",
+  "automatic_endpoint_switching_subtitle": "在可用的情况下,通过指定的 Wi-Fi 进行本地连接,并在其它地方使用替代连接",
+  "automatic_endpoint_switching_title": "自动切换URL",
+  "background_location_permission": "后台定位权限",
+  "background_location_permission_content": "为了在后台运行时切换网络,Immich 必须*始终*拥有精确的位置访问权限,这样应用程序才能读取 Wi-Fi 网络的名称",
   "backup_album_selection_page_albums_device": "设备上的相册({})",
   "backup_album_selection_page_albums_tap": "单击选中,双击取消",
   "backup_album_selection_page_assets_scatter": "项目会分散在多个相册中。因此,可以在备份过程中包含或排除相册。",
@@ -127,6 +137,7 @@
   "backup_manual_success": "成功",
   "backup_manual_title": "上传状态",
   "backup_options_page_title": "备份选项",
+  "backup_setting_subtitle": "管理后台和前台上传设置",
   "cache_settings_album_thumbnails": "图库缩略图({} 项)",
   "cache_settings_clear_cache_button": "清除缓存",
   "cache_settings_clear_cache_button_title": "清除应用缓存。在重新生成缓存之前,将显著影响应用的性能。",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "设置本地存储行为",
   "cache_settings_tile_title": "本地存储",
   "cache_settings_title": "缓存设置",
+  "cancel": "取消",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "确认密码",
   "change_password_form_description": "{name} 您好,\n\n这是您首次登录系统,或被管理员要求更改密码。\n请在下方输入新密码。",
   "change_password_form_new_password": "新密码",
   "change_password_form_password_mismatch": "密码不匹配",
   "change_password_form_reenter_new_password": "再次输入新密码",
+  "check_corrupt_asset_backup": "检查备份是否损坏",
+  "check_corrupt_asset_backup_button": "执行检查",
+  "check_corrupt_asset_backup_description": "仅在连接到Wi-Fi并完成所有项目备份后执行此检查。该过程可能需要几分钟。",
   "client_cert_dialog_msg_confirm": "确定",
   "client_cert_enter_password": "输入密码",
   "client_cert_import": "导入",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "取消归档",
   "control_bottom_app_bar_unfavorite": "取消收藏",
   "control_bottom_app_bar_upload": "上传",
+  "create_album": "创建相册",
   "create_album_page_untitled": "未命名",
+  "create_new": "新建",
   "create_shared_album_page_create": "创建",
   "create_shared_album_page_share": "共享",
   "create_shared_album_page_share_add_assets": "添加项目",
@@ -193,6 +211,7 @@
   "crop": "裁剪",
   "curated_location_page_title": "地点",
   "curated_object_page_title": "事物",
+  "current_server_address": "当前服务器地址",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "删除共享链接",
   "description_input_hint_text": "添加描述...",
   "description_input_submit_error": "更新描述时出错,请检查日志以获取更多详细信息",
+  "download_canceled": "下载已取消",
+  "download_complete": "下载完成",
+  "download_enqueue": "已加入下载队列",
   "download_error": "下载出错",
+  "download_failed": "下载失败",
+  "download_filename": "文件:{}",
+  "download_finished": "下载完成",
+  "downloading": "下载中...",
+  "downloading_media": "正在下载媒体",
+  "download_notfound": "无法找到下载",
+  "download_paused": "下载已暂停",
   "download_started": "开始下载",
   "download_sucess": "下载成功",
   "download_sucess_android": "媒体已下载至 DCIM/Immich",
+  "download_waiting_to_retry": "等待重试",
   "edit_date_time_dialog_date_time": "日期和时间",
   "edit_date_time_dialog_timezone": "时区",
   "edit_image_title": "编辑",
   "edit_location_dialog_title": "位置",
+  "enter_wifi_name": "输入 Wi-Fi 名称",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "错误:{}",
   "exif_bottom_sheet_description": "添加描述...",
   "exif_bottom_sheet_details": "详情",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "启用实验性照片网格",
   "experimental_settings_subtitle": "使用风险自负!",
   "experimental_settings_title": "实验性功能",
+  "external_network": "外部网络",
+  "external_network_sheet_info": "当不在首选的 Wi-Fi 网络上时,应用程序将通过下方第一个可连通的 URL 连接到服务器",
+  "favorites": "收藏",
   "favorites_page_no_favorites": "未找到收藏项目",
   "favorites_page_title": "收藏",
   "filename_search": "文件名或扩展名",
+  "filter": "筛选",
+  "get_wifiname_error": "无法获取 Wi-Fi 名称。确保已授予必要的权限,并已连接到 Wi-Fi 网络",
+  "grant_permission": "获取权限",
   "haptic_feedback_switch": "启用振动反馈",
   "haptic_feedback_title": "振动反馈",
   "header_settings_add_header_tip": "添加标头",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "如果这是您第一次使用该应用程序,请确保选择一个要备份的本地相册,以便可以在时间线中预览该相册中的照片和视频。",
   "home_page_share_err_local": "暂无法通过链接共享本地项目,跳过",
   "home_page_upload_err_limit": "一次最多只能上传 30 个项目,跳过",
+  "ignore_icloud_photos": "忽略iCloud照片",
+  "ignore_icloud_photos_description": "存储在iCloud中的照片不会上传至Immich服务器",
   "image_saved_successfully": "图片已保存",
   "image_viewer_page_state_provider_download_error": "下载出现错误",
   "image_viewer_page_state_provider_download_started": "下载启动",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "共享出错",
   "invalid_date": "无效的日期",
   "invalid_date_format": "无效的日期格式",
+  "library": "库",
   "library_page_albums": "相册",
   "library_page_archive": "归档",
   "library_page_device_albums": "设备上的相册",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "最早的照片",
   "library_page_sort_most_recent_photo": "最近的项目",
   "library_page_sort_title": "相册标题",
+  "local_network": "本地网络",
+  "local_network_sheet_info": "使用指定的 Wi-Fi 网络时,应用程序将通过此 URL 连接到服务器",
+  "location_permission": "定位权限",
+  "location_permission_content": "为了使用自动切换功能,Immich 需要精确的定位权限,这样才能读取当前 Wi-Fi 网络的名称",
   "location_picker_choose_on_map": "在地图上选择",
   "location_picker_latitude": "纬度",
   "location_picker_latitude_error": "输入有效的纬度值",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "动图",
   "multiselect_grid_edit_date_time_err_read_only": "无法编辑只读项目的日期,跳过",
   "multiselect_grid_edit_gps_err_read_only": "无法编辑只读项目的位置信息,跳过",
+  "my_albums": "我的相册",
+  "networking_settings": "网络",
+  "networking_subtitle": "管理服务接口设置",
   "no_assets_to_show": "无项目展示",
   "no_name": "无姓名",
   "notification_permission_dialog_cancel": "取消",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "授予通知权限。",
   "notification_permission_list_tile_enable_button": "启用通知",
   "notification_permission_list_tile_title": "通知权限",
+  "on_this_device": "在此设备",
   "partner_list_user_photos": "{user}的照片",
   "partner_list_view_all": "展示全部",
   "partner_page_add_partner": "添加同伴",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} 将无法再访问您的照片。",
   "partner_page_stop_sharing_title": "您确定要停止共享您的照片吗?",
   "partner_page_title": "同伴",
+  "partners": "伙伴",
+  "people": "人物",
   "permission_onboarding_back": "返回",
   "permission_onboarding_continue_anyway": "仍然继续",
   "permission_onboarding_get_started": "开始使用",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "已授权!一切就绪。",
   "permission_onboarding_permission_limited": "权限受限:要让 Immich 备份和管理您的整个图库收藏,请在“设置”中授予照片和视频权限。",
   "permission_onboarding_request": "Immich 需要权限才能查看您的照片和视频。",
+  "places": "地点",
+  "preferences_settings_subtitle": "管理应用的偏好设置",
   "preferences_settings_title": "偏好设置",
   "profile_drawer_app_logs": "日志",
   "profile_drawer_client_out_of_date_major": "客户端有大版本升级,请尽快升级至最新版。",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "设置",
   "profile_drawer_sign_out": "退出登录",
   "profile_drawer_trash": "回收站",
+  "recently_added": "近期添加",
   "recently_added_page_title": "最近添加",
+  "save": "保存",
   "save_to_gallery": "保存到图库",
   "scaffold_body_error_occurred": "发生错误",
+  "search_albums": "搜索相册",
   "search_bar_hint": "搜索照片",
   "search_filter_apply": "应用筛选",
   "search_filter_camera": "相机",
@@ -428,6 +484,7 @@
   "search_page_places": "地点",
   "search_page_recently_added": "最近添加",
   "search_page_screenshots": "屏幕截图",
+  "search_page_search_photos_videos": "搜索您的照片和视频",
   "search_page_selfies": "自拍",
   "search_page_things": "事物",
   "search_page_videos": "视频",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "建议",
   "select_user_for_sharing_page_err_album": "创建相册失败",
   "select_user_for_sharing_page_share_suggestions": "建议",
+  "server_endpoint": "服务接口",
   "server_info_box_app_version": "App 版本",
   "server_info_box_latest_release": "最新版本",
   "server_info_box_server_url": "服务器地址",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "加载预览图",
   "setting_image_viewer_title": "图片",
   "setting_languages_apply": "应用",
+  "setting_languages_subtitle": "更改应用语言",
   "setting_languages_title": "语言",
   "setting_notifications_notify_failures_grace_period": "后台备份失败通知:{}",
   "setting_notifications_notify_hours": "{} 小时",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "更新",
   "shared_link_manage_links": "管理共享链接",
   "shared_link_public_album": "公共相册",
+  "shared_links": "共享链接",
   "share_done": "完成",
+  "shared_with_me": "共享给我",
   "share_invite": "邀请到共享相册",
   "sharing_page_album": "共享相册",
   "sharing_page_description": "创建共享相册以与网络中的人共享照片和视频。",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "三段式加载可能会提升加载性能,但可能会导致更高的网络负载",
   "theme_setting_three_stage_loading_title": "启用三段式加载",
   "translated_text_options": "选项",
+  "trash": "回收站",
   "trash_emptied": "空回收站",
   "trash_page_delete": "删除",
   "trash_page_delete_all": "删除全部",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "是否要将所选项目备份到服务器?",
   "upload_dialog_ok": "上传",
   "upload_dialog_title": "上传项目",
+  "use_current_connection": "使用当前连接",
+  "validate_endpoint_error": "请输入有效的URL",
   "version_announcement_overlay_ack": "我知道了",
   "version_announcement_overlay_release_notes": "发行说明",
   "version_announcement_overlay_text_1": "号外号外,有新版本的",
   "version_announcement_overlay_text_2": "请花点时间访问",
   "version_announcement_overlay_text_3": "并检查您的 docker-compose 和 .env 是否为最新且正确的配置,特别是您在使用 WatchTower 或者其他自动更新的程序时,您需要更加细致的检查。",
   "version_announcement_overlay_title": "服务端有新版本啦 \uD83C\uDF89",
+  "videos": "视频",
   "viewer_remove_from_stack": "从堆叠中移除",
   "viewer_stack_use_as_main_asset": "作为主项目使用",
-  "viewer_unstack": "取消堆叠"
+  "viewer_unstack": "取消堆叠",
+  "wifi_name": "Wi-Fi 名称",
+  "your_wifi_name": "您的 Wi-Fi 名称"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/zh-Hans.json b/mobile/assets/i18n/zh-Hans.json
index f5ec6ab2a1..c6f361a756 100644
--- a/mobile/assets/i18n/zh-Hans.json
+++ b/mobile/assets/i18n/zh-Hans.json
@@ -6,6 +6,8 @@
   "action_common_save": "保存",
   "action_common_select": "选择",
   "action_common_update": "更新",
+  "add_a_name": "添加姓名",
+  "add_endpoint": "添加服务接口",
   "add_to_album_bottom_sheet_added": "添加到 {album}",
   "add_to_album_bottom_sheet_already_exists": "已在 {album} 中",
   "advanced_settings_log_level_title": "日志等级:{}",
@@ -21,6 +23,7 @@
   "advanced_settings_troubleshooting_title": "故障排除",
   "album_info_card_backup_album_excluded": "已排除",
   "album_info_card_backup_album_included": "已选中",
+  "albums": "相册",
   "album_thumbnail_card_item": "1 项",
   "album_thumbnail_card_items": "{} 项",
   "album_thumbnail_card_shared": " · 已共享",
@@ -36,11 +39,13 @@
   "album_viewer_appbar_share_remove": "从相册中移除",
   "album_viewer_appbar_share_to": "共享给",
   "album_viewer_page_share_add_users": "创建用户",
+  "all": "所有",
   "all_people_page_title": "人物",
   "all_videos_page_title": "视频",
   "app_bar_signout_dialog_content": "您确定要退出吗?",
   "app_bar_signout_dialog_ok": "是",
   "app_bar_signout_dialog_title": "退出登录",
+  "archived": "已存档",
   "archive_page_no_archived_assets": "未找到归档项目",
   "archive_page_title": "归档({})",
   "asset_action_delete_err_read_only": "无法删除只读项目,跳过",
@@ -61,7 +66,12 @@
   "assets_restored_successfully": "已成功恢复{}个项目",
   "assets_trashed": "{}个回收站项目",
   "assets_trashed_from_server": "{}个项目已放入回收站",
+  "asset_viewer_settings_subtitle": "管理图库浏览器设置",
   "asset_viewer_settings_title": "资源查看器",
+  "automatic_endpoint_switching_subtitle": "在可用的情况下,通过指定的 Wi-Fi 进行本地连接,并在其它地方使用替代连接",
+  "automatic_endpoint_switching_title": "自动切换URL",
+  "background_location_permission": "后台定位权限",
+  "background_location_permission_content": "为了在后台运行时切换网络,Immich 必须*始终*拥有精确的位置访问权限,这样应用程序才能读取 Wi-Fi 网络的名称",
   "backup_album_selection_page_albums_device": "设备上的相册({})",
   "backup_album_selection_page_albums_tap": "单击选中,双击取消",
   "backup_album_selection_page_assets_scatter": "项目会分散在多个相册中。因此,可以在备份过程中包含或排除相册。",
@@ -127,6 +137,7 @@
   "backup_manual_success": "成功",
   "backup_manual_title": "上传状态",
   "backup_options_page_title": "备份选项",
+  "backup_setting_subtitle": "管理后台和前台上传设置",
   "cache_settings_album_thumbnails": "图库缩略图({} 项)",
   "cache_settings_clear_cache_button": "清除缓存",
   "cache_settings_clear_cache_button_title": "清除应用缓存。在重新生成缓存之前,将显著影响应用的性能。",
@@ -145,11 +156,16 @@
   "cache_settings_tile_subtitle": "设置本地存储行为",
   "cache_settings_tile_title": "本地存储",
   "cache_settings_title": "缓存设置",
+  "cancel": "取消",
+  "change_display_order": "Change display order",
   "change_password_form_confirm_password": "确认密码",
   "change_password_form_description": "{name} 您好,\n\n这是您首次登录系统,或被管理员要求更改密码。\n请在下方输入新密码。",
   "change_password_form_new_password": "新密码",
   "change_password_form_password_mismatch": "密码不匹配",
   "change_password_form_reenter_new_password": "再次输入新密码",
+  "check_corrupt_asset_backup": "检查备份是否损坏",
+  "check_corrupt_asset_backup_button": "执行检查",
+  "check_corrupt_asset_backup_description": "仅在连接到Wi-Fi并完成所有项目备份后执行此检查。该过程可能需要几分钟。",
   "client_cert_dialog_msg_confirm": "确定",
   "client_cert_enter_password": "输入密码",
   "client_cert_import": "导入",
@@ -185,7 +201,9 @@
   "control_bottom_app_bar_unarchive": "取消归档",
   "control_bottom_app_bar_unfavorite": "取消收藏",
   "control_bottom_app_bar_upload": "上传",
+  "create_album": "创建相册",
   "create_album_page_untitled": "未命名",
+  "create_new": "新建",
   "create_shared_album_page_create": "创建",
   "create_shared_album_page_share": "共享",
   "create_shared_album_page_share_add_assets": "添加项目",
@@ -193,6 +211,7 @@
   "crop": "裁剪",
   "curated_location_page_title": "地点",
   "curated_object_page_title": "事物",
+  "current_server_address": "当前服务器地址",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
@@ -210,14 +229,27 @@
   "delete_shared_link_dialog_title": "删除共享链接",
   "description_input_hint_text": "添加描述...",
   "description_input_submit_error": "更新描述时出错,请检查日志以获取更多详细信息",
+  "download_canceled": "下载已取消",
+  "download_complete": "下载完成",
+  "download_enqueue": "已加入下载队列",
   "download_error": "下载出错",
+  "download_failed": "下载失败",
+  "download_filename": "文件:{}",
+  "download_finished": "下载完成",
+  "downloading": "下载中...",
+  "downloading_media": "正在下载媒体",
+  "download_notfound": "无法找到下载",
+  "download_paused": "下载已暂停",
   "download_started": "开始下载",
   "download_sucess": "下载成功",
   "download_sucess_android": "媒体已下载至 DCIM/Immich",
+  "download_waiting_to_retry": "等待重试",
   "edit_date_time_dialog_date_time": "日期和时间",
   "edit_date_time_dialog_timezone": "时区",
   "edit_image_title": "编辑",
   "edit_location_dialog_title": "位置",
+  "enter_wifi_name": "输入 Wi-Fi 名称",
+  "error_change_sort_album": "Failed to change album sort order",
   "error_saving_image": "错误:{}",
   "exif_bottom_sheet_description": "添加描述...",
   "exif_bottom_sheet_details": "详情",
@@ -229,9 +261,15 @@
   "experimental_settings_new_asset_list_title": "启用实验性照片网格",
   "experimental_settings_subtitle": "使用风险自负!",
   "experimental_settings_title": "实验性功能",
+  "external_network": "外部网络",
+  "external_network_sheet_info": "当不在首选的 Wi-Fi 网络上时,应用程序将通过下方第一个可连通的 URL 连接到服务器",
+  "favorites": "收藏",
   "favorites_page_no_favorites": "未找到收藏项目",
   "favorites_page_title": "收藏",
   "filename_search": "文件名或扩展名",
+  "filter": "筛选",
+  "get_wifiname_error": "无法获取 Wi-Fi 名称。确保已授予必要的权限,并已连接到 Wi-Fi 网络",
+  "grant_permission": "获取权限",
   "haptic_feedback_switch": "启用振动反馈",
   "haptic_feedback_title": "振动反馈",
   "header_settings_add_header_tip": "添加标头",
@@ -255,6 +293,8 @@
   "home_page_first_time_notice": "如果这是您第一次使用该应用程序,请确保选择一个要备份的本地相册,以便可以在时间线中预览该相册中的照片和视频。",
   "home_page_share_err_local": "暂无法通过链接共享本地项目,跳过",
   "home_page_upload_err_limit": "一次最多只能上传 30 个项目,跳过",
+  "ignore_icloud_photos": "忽略iCloud照片",
+  "ignore_icloud_photos_description": "存储在iCloud中的照片不会上传至Immich服务器",
   "image_saved_successfully": "图片已保存",
   "image_viewer_page_state_provider_download_error": "下载出现错误",
   "image_viewer_page_state_provider_download_started": "下载启动",
@@ -262,6 +302,7 @@
   "image_viewer_page_state_provider_share_error": "共享出错",
   "invalid_date": "无效的日期",
   "invalid_date_format": "无效的日期格式",
+  "library": "库",
   "library_page_albums": "相册",
   "library_page_archive": "归档",
   "library_page_device_albums": "设备上的相册",
@@ -274,6 +315,10 @@
   "library_page_sort_most_oldest_photo": "最早的照片",
   "library_page_sort_most_recent_photo": "最近的项目",
   "library_page_sort_title": "相册标题",
+  "local_network": "本地网络",
+  "local_network_sheet_info": "使用指定的 Wi-Fi 网络时,应用程序将通过此 URL 连接到服务器",
+  "location_permission": "定位权限",
+  "location_permission_content": "为了使用自动切换功能,Immich 需要精确的定位权限,这样才能读取当前 Wi-Fi 网络的名称",
   "location_picker_choose_on_map": "在地图上选择",
   "location_picker_latitude": "纬度",
   "location_picker_latitude_error": "输入有效的纬度值",
@@ -342,6 +387,9 @@
   "motion_photos_page_title": "动图",
   "multiselect_grid_edit_date_time_err_read_only": "无法编辑只读项目的日期,跳过",
   "multiselect_grid_edit_gps_err_read_only": "无法编辑只读项目的位置信息,跳过",
+  "my_albums": "我的相册",
+  "networking_settings": "网络",
+  "networking_subtitle": "管理服务接口设置",
   "no_assets_to_show": "无项目展示",
   "no_name": "无姓名",
   "notification_permission_dialog_cancel": "取消",
@@ -350,6 +398,7 @@
   "notification_permission_list_tile_content": "授予通知权限。",
   "notification_permission_list_tile_enable_button": "启用通知",
   "notification_permission_list_tile_title": "通知权限",
+  "on_this_device": "在此设备",
   "partner_list_user_photos": "{user}的照片",
   "partner_list_view_all": "展示全部",
   "partner_page_add_partner": "添加同伴失败",
@@ -361,6 +410,8 @@
   "partner_page_stop_sharing_content": "{} 将无法再访问您的照片。",
   "partner_page_stop_sharing_title": "您确定要停止共享您的照片吗?",
   "partner_page_title": "同伴",
+  "partners": "伙伴",
+  "people": "人物",
   "permission_onboarding_back": "返回",
   "permission_onboarding_continue_anyway": "仍然继续",
   "permission_onboarding_get_started": "开始使用",
@@ -371,6 +422,8 @@
   "permission_onboarding_permission_granted": "已授权!一切就绪。",
   "permission_onboarding_permission_limited": "权限受限:要让 Immich 备份和管理您的整个图库收藏,请在“设置”中授予照片和视频权限。",
   "permission_onboarding_request": "Immich 需要权限才能查看您的照片和视频。",
+  "places": "地点",
+  "preferences_settings_subtitle": "管理应用的偏好设置",
   "preferences_settings_title": "偏好设置",
   "profile_drawer_app_logs": "日志",
   "profile_drawer_client_out_of_date_major": "客户端有大版本升级,请尽快升级至最新版。",
@@ -383,9 +436,12 @@
   "profile_drawer_settings": "设置",
   "profile_drawer_sign_out": "退出登录",
   "profile_drawer_trash": "回收站",
+  "recently_added": "近期添加",
   "recently_added_page_title": "最近添加",
+  "save": "保存",
   "save_to_gallery": "保存到图库",
   "scaffold_body_error_occurred": "发生错误",
+  "search_albums": "搜索相册",
   "search_bar_hint": "搜索照片",
   "search_filter_apply": "应用筛选",
   "search_filter_camera": "相机",
@@ -428,6 +484,7 @@
   "search_page_places": "地点",
   "search_page_recently_added": "最近添加",
   "search_page_screenshots": "屏幕截图",
+  "search_page_search_photos_videos": "搜索您的照片和视频",
   "search_page_selfies": "自拍",
   "search_page_things": "事物",
   "search_page_videos": "视频",
@@ -440,6 +497,7 @@
   "select_additional_user_for_sharing_page_suggestions": "建议",
   "select_user_for_sharing_page_err_album": "创建相册失败",
   "select_user_for_sharing_page_share_suggestions": "建议",
+  "server_endpoint": "服务接口",
   "server_info_box_app_version": "App 版本",
   "server_info_box_latest_release": "最新版本",
   "server_info_box_server_url": "服务器地址",
@@ -451,6 +509,7 @@
   "setting_image_viewer_preview_title": "加载预览图",
   "setting_image_viewer_title": "图片",
   "setting_languages_apply": "应用",
+  "setting_languages_subtitle": "更改应用语言",
   "setting_languages_title": "语言",
   "setting_notifications_notify_failures_grace_period": "后台备份失败通知:{}",
   "setting_notifications_notify_hours": "{} 小时",
@@ -531,7 +590,9 @@
   "shared_link_info_chip_upload": "更新",
   "shared_link_manage_links": "管理共享链接",
   "shared_link_public_album": "公共相册",
+  "shared_links": "共享链接",
   "share_done": "完成",
+  "shared_with_me": "共享给我",
   "share_invite": "邀请到共享相册",
   "sharing_page_album": "共享相册",
   "sharing_page_description": "创建共享相册以与网络中的人共享照片和视频。",
@@ -563,6 +624,7 @@
   "theme_setting_three_stage_loading_subtitle": "三段式加载可能会提升加载性能,但可能会导致更高的网络负载",
   "theme_setting_three_stage_loading_title": "启用三段式加载",
   "translated_text_options": "选项",
+  "trash": "回收站",
   "trash_emptied": "空回收站",
   "trash_page_delete": "删除",
   "trash_page_delete_all": "删除全部",
@@ -580,13 +642,18 @@
   "upload_dialog_info": "是否要将所选项目备份到服务器?",
   "upload_dialog_ok": "上传",
   "upload_dialog_title": "上传项目",
+  "use_current_connection": "使用当前连接",
+  "validate_endpoint_error": "请输入有效的URL",
   "version_announcement_overlay_ack": "我知道了",
   "version_announcement_overlay_release_notes": "发行说明",
   "version_announcement_overlay_text_1": "号外号外,有新版本的",
   "version_announcement_overlay_text_2": "请花点时间访问",
   "version_announcement_overlay_text_3": "并检查您的 docker-compose 和 .env 是否为最新且正确的配置,特别是您在使用 WatchTower 或者其他自动更新的程序时,您需要更加细致的检查。",
   "version_announcement_overlay_title": "服务端有新版本啦 \uD83C\uDF89",
+  "videos": "视频",
   "viewer_remove_from_stack": "从堆叠中移除",
   "viewer_stack_use_as_main_asset": "作为主项目使用",
-  "viewer_unstack": "取消堆叠"
+  "viewer_unstack": "取消堆叠",
+  "wifi_name": "Wi-Fi 名称",
+  "your_wifi_name": "您的 Wi-Fi 名称"
 }
\ No newline at end of file
diff --git a/mobile/assets/i18n/zh-TW.json b/mobile/assets/i18n/zh-TW.json
index 324c9069fd..88d1d48aec 100644
--- a/mobile/assets/i18n/zh-TW.json
+++ b/mobile/assets/i18n/zh-TW.json
@@ -1,592 +1,659 @@
 {
-  "action_common_back": "Back",
-  "action_common_cancel": "Cancel",
-  "action_common_clear": "Clear",
-  "action_common_confirm": "Confirm",
-  "action_common_save": "Save",
-  "action_common_select": "Select",
-  "action_common_update": "Update",
-  "add_to_album_bottom_sheet_added": "Added to {album}",
-  "add_to_album_bottom_sheet_already_exists": "Already in {album}",
-  "advanced_settings_log_level_title": "Log level: {}",
-  "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
-  "advanced_settings_prefer_remote_title": "Prefer remote images",
-  "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
-  "advanced_settings_proxy_headers_title": "Proxy Headers",
-  "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
-  "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates",
-  "advanced_settings_tile_subtitle": "Advanced user's settings",
-  "advanced_settings_tile_title": "Advanced",
-  "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
-  "advanced_settings_troubleshooting_title": "Troubleshooting",
-  "album_info_card_backup_album_excluded": "EXCLUDED",
-  "album_info_card_backup_album_included": "INCLUDED",
-  "album_thumbnail_card_item": "1 item",
-  "album_thumbnail_card_items": "{} items",
-  "album_thumbnail_card_shared": " · Shared",
-  "album_thumbnail_owned": "Owned",
-  "album_thumbnail_shared_by": "Shared by {}",
-  "album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?",
-  "album_viewer_appbar_share_delete": "Delete album",
-  "album_viewer_appbar_share_err_delete": "Failed to delete album",
-  "album_viewer_appbar_share_err_leave": "Failed to leave album",
-  "album_viewer_appbar_share_err_remove": "There are problems in removing assets from album",
-  "album_viewer_appbar_share_err_title": "Failed to change album title",
-  "album_viewer_appbar_share_leave": "Leave album",
-  "album_viewer_appbar_share_remove": "Remove from album",
-  "album_viewer_appbar_share_to": "Share To",
-  "album_viewer_page_share_add_users": "Add users",
-  "all_people_page_title": "People",
-  "all_videos_page_title": "Videos",
-  "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
-  "app_bar_signout_dialog_ok": "Yes",
-  "app_bar_signout_dialog_title": "Sign out",
-  "archive_page_no_archived_assets": "No archived assets found",
-  "archive_page_title": "Archive ({})",
-  "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
-  "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
-  "asset_list_group_by_sub_title": "Group by",
-  "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
-  "asset_list_layout_settings_group_automatically": "Automatic",
-  "asset_list_layout_settings_group_by": "Group assets by",
-  "asset_list_layout_settings_group_by_month": "Month",
-  "asset_list_layout_settings_group_by_month_day": "Month + day",
-  "asset_list_layout_sub_title": "Layout",
-  "asset_list_settings_subtitle": "Photo grid layout settings",
-  "asset_list_settings_title": "Photo Grid",
-  "asset_restored_successfully": "Asset restored successfully",
-  "assets_deleted_permanently": "{} asset(s) deleted permanently",
-  "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server",
-  "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device",
-  "assets_restored_successfully": "{} asset(s) restored successfully",
-  "assets_trashed": "{} asset(s) trashed",
-  "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
-  "asset_viewer_settings_title": "Asset Viewer",
-  "backup_album_selection_page_albums_device": "Albums on device ({})",
-  "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
-  "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
-  "backup_album_selection_page_select_albums": "Select albums",
-  "backup_album_selection_page_selection_info": "Selection Info",
-  "backup_album_selection_page_total_assets": "Total unique assets",
-  "backup_all": "All",
-  "backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…",
-  "backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying…",
-  "backup_background_service_current_upload_notification": "Uploading {}",
-  "backup_background_service_default_notification": "Checking for new assets…",
-  "backup_background_service_error_title": "Backup error",
-  "backup_background_service_in_progress_notification": "Backing up your assets…",
-  "backup_background_service_upload_failure_notification": "Failed to upload {}",
-  "backup_controller_page_albums": "Backup Albums",
-  "backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.",
-  "backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled",
-  "backup_controller_page_background_app_refresh_enable_button_text": "Go to settings",
-  "backup_controller_page_background_battery_info_link": "Show me how",
-  "backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.",
-  "backup_controller_page_background_battery_info_ok": "OK",
-  "backup_controller_page_background_battery_info_title": "Battery optimizations",
-  "backup_controller_page_background_charging": "Only while charging",
-  "backup_controller_page_background_configure_error": "Failed to configure the background service",
-  "backup_controller_page_background_delay": "Delay new assets backup: {}",
-  "backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app",
-  "backup_controller_page_background_is_off": "Automatic background backup is off",
-  "backup_controller_page_background_is_on": "Automatic background backup is on",
-  "backup_controller_page_background_turn_off": "Turn off background service",
-  "backup_controller_page_background_turn_on": "Turn on background service",
-  "backup_controller_page_background_wifi": "Only on WiFi",
-  "backup_controller_page_backup": "Backup",
-  "backup_controller_page_backup_selected": "Selected: ",
-  "backup_controller_page_backup_sub": "Backed up photos and videos",
-  "backup_controller_page_cancel": "Cancel",
-  "backup_controller_page_created": "Created on: {}",
-  "backup_controller_page_desc_backup": "Turn on foreground backup to automatically upload new assets to the server when opening the app.",
-  "backup_controller_page_excluded": "Excluded: ",
-  "backup_controller_page_failed": "Failed ({})",
-  "backup_controller_page_filename": "File name: {} [{}]",
-  "backup_controller_page_id": "ID: {}",
-  "backup_controller_page_info": "Backup Information",
-  "backup_controller_page_none_selected": "None selected",
-  "backup_controller_page_remainder": "Remainder",
-  "backup_controller_page_remainder_sub": "Remaining photos and videos to back up from selection",
-  "backup_controller_page_select": "Select",
-  "backup_controller_page_server_storage": "Server Storage",
-  "backup_controller_page_start_backup": "Start Backup",
-  "backup_controller_page_status_off": "Automatic foreground backup is off",
-  "backup_controller_page_status_on": "Automatic foreground backup is on",
-  "backup_controller_page_storage_format": "{} of {} used",
-  "backup_controller_page_to_backup": "Albums to be backed up",
-  "backup_controller_page_total": "Total",
-  "backup_controller_page_total_sub": "All unique photos and videos from selected albums",
-  "backup_controller_page_turn_off": "Turn off foreground backup",
-  "backup_controller_page_turn_on": "Turn on foreground backup",
-  "backup_controller_page_uploading_file_info": "Uploading file info",
-  "backup_err_only_album": "Cannot remove the only album",
-  "backup_info_card_assets": "assets",
-  "backup_manual_cancelled": "Cancelled",
-  "backup_manual_failed": "Failed",
-  "backup_manual_in_progress": "Upload already in progress. Try after sometime",
-  "backup_manual_success": "Success",
-  "backup_manual_title": "Upload status",
-  "backup_options_page_title": "Backup options",
-  "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
-  "cache_settings_clear_cache_button": "Clear cache",
-  "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.",
-  "cache_settings_duplicated_assets_clear_button": "CLEAR",
-  "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
-  "cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
-  "cache_settings_image_cache_size": "Image cache size ({} assets)",
-  "cache_settings_statistics_album": "Library thumbnails",
-  "cache_settings_statistics_assets": "{} assets ({})",
-  "cache_settings_statistics_full": "Full images",
-  "cache_settings_statistics_shared": "Shared album thumbnails",
-  "cache_settings_statistics_thumbnail": "Thumbnails",
-  "cache_settings_statistics_title": "Cache usage",
-  "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application",
-  "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)",
-  "cache_settings_tile_subtitle": "Control the local storage behaviour",
-  "cache_settings_tile_title": "Local Storage",
-  "cache_settings_title": "Caching Settings",
-  "change_password_form_confirm_password": "Confirm Password",
-  "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
-  "change_password_form_new_password": "New Password",
-  "change_password_form_password_mismatch": "Passwords do not match",
-  "change_password_form_reenter_new_password": "Re-enter New Password",
-  "client_cert_dialog_msg_confirm": "OK",
-  "client_cert_enter_password": "Enter Password",
-  "client_cert_import": "Import",
-  "client_cert_import_success_msg": "Client certificate is imported",
-  "client_cert_invalid_msg": "Invalid certificate file or wrong password",
-  "client_cert_remove": "Remove",
-  "client_cert_remove_msg": "Client certificate is removed",
-  "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login",
-  "client_cert_title": "SSL Client Certificate",
-  "common_add_to_album": "Add to album",
-  "common_change_password": "Change Password",
-  "common_create_new_album": "Create new album",
-  "common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.",
-  "common_shared": "Shared",
-  "contextual_search": "Sunrise on the beach",
-  "control_bottom_app_bar_add_to_album": "Add to album",
-  "control_bottom_app_bar_album_info": "{} items",
-  "control_bottom_app_bar_album_info_shared": "{} items · Shared",
-  "control_bottom_app_bar_archive": "Archive",
-  "control_bottom_app_bar_create_new_album": "Create new album",
-  "control_bottom_app_bar_delete": "Delete",
-  "control_bottom_app_bar_delete_from_immich": "Delete from Immich",
-  "control_bottom_app_bar_delete_from_local": "Delete from device",
-  "control_bottom_app_bar_download": "Download",
-  "control_bottom_app_bar_edit": "Edit",
-  "control_bottom_app_bar_edit_location": "Edit Location",
-  "control_bottom_app_bar_edit_time": "Edit Date & Time",
-  "control_bottom_app_bar_favorite": "Favorite",
-  "control_bottom_app_bar_share": "Share",
-  "control_bottom_app_bar_share_to": "Share To",
-  "control_bottom_app_bar_stack": "Stack",
-  "control_bottom_app_bar_trash_from_immich": "Move to Trash",
-  "control_bottom_app_bar_unarchive": "Unarchive",
-  "control_bottom_app_bar_unfavorite": "Unfavorite",
-  "control_bottom_app_bar_upload": "Upload",
-  "create_album_page_untitled": "Untitled",
-  "create_shared_album_page_create": "Create",
-  "create_shared_album_page_share": "Share",
-  "create_shared_album_page_share_add_assets": "ADD ASSETS",
-  "create_shared_album_page_share_select_photos": "Select Photos",
-  "crop": "Crop",
-  "curated_location_page_title": "Places",
-  "curated_object_page_title": "Things",
+  "action_common_back": "後退",
+  "action_common_cancel": "取消",
+  "action_common_clear": "清空",
+  "action_common_confirm": "確定",
+  "action_common_save": "儲存",
+  "action_common_select": "選擇",
+  "action_common_update": "更新",
+  "add_a_name": "新增姓名",
+  "add_endpoint": "Add endpoint",
+  "add_to_album_bottom_sheet_added": "新增到 {album}",
+  "add_to_album_bottom_sheet_already_exists": "已在 {album} 中",
+  "advanced_settings_log_level_title": "日誌等級: {}",
+  "advanced_settings_prefer_remote_subtitle": "在某些裝置上,從本地的項目載入縮圖的速度非常慢。\n啓用此選項以載入遙距項目。",
+  "advanced_settings_prefer_remote_title": "優先遙距項目",
+  "advanced_settings_proxy_headers_subtitle": "定義代理標頭,套用於Immich的每次網絡請求",
+  "advanced_settings_proxy_headers_title": "代理標頭",
+  "advanced_settings_self_signed_ssl_subtitle": "略過伺服器端點的 SSL 證書驗證(該選項適用於使用自簽名證書的伺服器)。",
+  "advanced_settings_self_signed_ssl_title": "允許自簽名 SSL 證書",
+  "advanced_settings_tile_subtitle": "進階用戶設定",
+  "advanced_settings_tile_title": "進階",
+  "advanced_settings_troubleshooting_subtitle": "啓用用於故障排除的額外功能",
+  "advanced_settings_troubleshooting_title": "故障排除",
+  "album_info_card_backup_album_excluded": "已排除",
+  "album_info_card_backup_album_included": "已選中",
+  "albums": "相簿",
+  "album_thumbnail_card_item": "1 項",
+  "album_thumbnail_card_items": "{}  項",
+  "album_thumbnail_card_shared": " · 已共享",
+  "album_thumbnail_owned": "擁有",
+  "album_thumbnail_shared_by": "由  {}  共享",
+  "album_viewer_appbar_delete_confirm": "確定要從賬戶中刪除此相簿嗎?",
+  "album_viewer_appbar_share_delete": "刪除相簿",
+  "album_viewer_appbar_share_err_delete": "刪除相簿失敗",
+  "album_viewer_appbar_share_err_leave": "退出共享失敗",
+  "album_viewer_appbar_share_err_remove": "從相簿中移除時出現錯誤",
+  "album_viewer_appbar_share_err_title": "修改相簿標題失敗",
+  "album_viewer_appbar_share_leave": "退出共享",
+  "album_viewer_appbar_share_remove": "從相簿中移除",
+  "album_viewer_appbar_share_to": "共享給",
+  "album_viewer_page_share_add_users": "新增用戶",
+  "all": "所有",
+  "all_people_page_title": "人物",
+  "all_videos_page_title": "短片",
+  "app_bar_signout_dialog_content": "您確定要退出嗎?",
+  "app_bar_signout_dialog_ok": "是",
+  "app_bar_signout_dialog_title": "退出登入",
+  "archived": "已存檔",
+  "archive_page_no_archived_assets": "未找到歸檔項目",
+  "archive_page_title": "歸檔( {} )",
+  "asset_action_delete_err_read_only": "無法刪除唯讀項目,略過",
+  "asset_action_share_err_offline": "無法獲取離線項目,略過",
+  "asset_list_group_by_sub_title": "分組方式",
+  "asset_list_layout_settings_dynamic_layout_title": "動態佈局",
+  "asset_list_layout_settings_group_automatically": "自動",
+  "asset_list_layout_settings_group_by": "項目分組方式",
+  "asset_list_layout_settings_group_by_month": "月",
+  "asset_list_layout_settings_group_by_month_day": "月和日",
+  "asset_list_layout_sub_title": "佈局",
+  "asset_list_settings_subtitle": "照片網格佈局設定",
+  "asset_list_settings_title": "照片網格",
+  "asset_restored_successfully": "已成功恢復所有項目",
+  "assets_deleted_permanently": "{} 個項目已被永久刪除",
+  "assets_deleted_permanently_from_server": "已從伺服器中永久移除 {} 個項目",
+  "assets_removed_permanently_from_device": "已從裝置中永久移除 {} 個項目",
+  "assets_restored_successfully": "已成功恢復 {} 個項目",
+  "assets_trashed": "{} 個回收桶項目",
+  "assets_trashed_from_server": "{} 個項目已放入回收桶",
+  "asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
+  "asset_viewer_settings_title": "資源查看器",
+  "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
+  "automatic_endpoint_switching_title": "Automatic URL switching",
+  "background_location_permission": "Background location permission",
+  "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
+  "backup_album_selection_page_albums_device": "裝置上的相簿( {} )",
+  "backup_album_selection_page_albums_tap": "單擊選中,雙擊取消",
+  "backup_album_selection_page_assets_scatter": "項目會分散在多個相簿中。因此,可以在備份過程中包含或排除相簿。",
+  "backup_album_selection_page_select_albums": "選擇相簿",
+  "backup_album_selection_page_selection_info": "選擇資訊",
+  "backup_album_selection_page_total_assets": "總計",
+  "backup_all": "全部",
+  "backup_background_service_backup_failed_message": "備份失敗,正在重試…",
+  "backup_background_service_connection_failed_message": "連接伺服器失敗,正在重試…",
+  "backup_background_service_current_upload_notification": "正在上傳  {} ",
+  "backup_background_service_default_notification": "正在檢查新項目…",
+  "backup_background_service_error_title": "備份失敗",
+  "backup_background_service_in_progress_notification": "正在備份…",
+  "backup_background_service_upload_failure_notification": "上傳失敗  {} ",
+  "backup_controller_page_albums": "備份相簿",
+  "backup_controller_page_background_app_refresh_disabled_content": "要使用背景備份功能,請在「設定」>「備份」>「背景套用更新」中啓用背本程式更新。",
+  "backup_controller_page_background_app_refresh_disabled_title": "背景套用更新已禁用",
+  "backup_controller_page_background_app_refresh_enable_button_text": "前往設定",
+  "backup_controller_page_background_battery_info_link": "怎麼做",
+  "backup_controller_page_background_battery_info_message": "為了獲得最佳的背景備份體驗,請禁用會任何限制 Immich 背景活動的電池優化。\n\n由於這是裝置相關的,因此請查找裝置製造商提供的資訊進行操作。",
+  "backup_controller_page_background_battery_info_ok": "我知道了",
+  "backup_controller_page_background_battery_info_title": "電池最佳化",
+  "backup_controller_page_background_charging": "僅在充電時",
+  "backup_controller_page_background_configure_error": "設定背景服務失敗",
+  "backup_controller_page_background_delay": "延遲  {}  後備份",
+  "backup_controller_page_background_description": "打開背景服務以自動備份任何新項目,且無需打開套用",
+  "backup_controller_page_background_is_off": "背景自動備份已關閉",
+  "backup_controller_page_background_is_on": "背景自動備份已開啓",
+  "backup_controller_page_background_turn_off": "關閉背景服務",
+  "backup_controller_page_background_turn_on": "開啓背景服務",
+  "backup_controller_page_background_wifi": "僅使用 WiFi",
+  "backup_controller_page_backup": "備份",
+  "backup_controller_page_backup_selected": "已選中:",
+  "backup_controller_page_backup_sub": "已備份的照片和短片",
+  "backup_controller_page_cancel": "取消",
+  "backup_controller_page_created": "新增時間:  {} ",
+  "backup_controller_page_desc_backup": "打開前台備份,以本程式運行時自動備份新項目。",
+  "backup_controller_page_excluded": "已排除:",
+  "backup_controller_page_failed": "失敗( {} )",
+  "backup_controller_page_filename": "文件名稱:  {}  [ {} ]",
+  "backup_controller_page_id": "ID:  {} ",
+  "backup_controller_page_info": "備份資訊",
+  "backup_controller_page_none_selected": "未選擇",
+  "backup_controller_page_remainder": "剩餘",
+  "backup_controller_page_remainder_sub": "所選數據中尚未備份的數據",
+  "backup_controller_page_select": "選擇",
+  "backup_controller_page_server_storage": "伺服器存儲",
+  "backup_controller_page_start_backup": "開始備份",
+  "backup_controller_page_status_off": "前台自動備份已關閉",
+  "backup_controller_page_status_on": "前台自動備份已開啓",
+  "backup_controller_page_storage_format": " {} / {}  已使用",
+  "backup_controller_page_to_backup": "要備份的相簿",
+  "backup_controller_page_total": "總計",
+  "backup_controller_page_total_sub": "選中相簿中所有不重複的短片和圖片",
+  "backup_controller_page_turn_off": "關閉前台備份",
+  "backup_controller_page_turn_on": "開啓前台備份",
+  "backup_controller_page_uploading_file_info": "正在上傳中的文件資訊",
+  "backup_err_only_album": "不能移除唯一的相簿",
+  "backup_info_card_assets": "項",
+  "backup_manual_cancelled": "已取消",
+  "backup_manual_failed": "失敗",
+  "backup_manual_in_progress": "上傳正在進行中,請稍後再試",
+  "backup_manual_success": "成功",
+  "backup_manual_title": "上傳狀態",
+  "backup_options_page_title": "備份選項",
+  "backup_setting_subtitle": "Manage background and foreground upload settings",
+  "cache_settings_album_thumbnails": "圖庫縮圖( {}  項)",
+  "cache_settings_clear_cache_button": "清除緩存",
+  "cache_settings_clear_cache_button_title": "清除套用緩存。在重新生成緩存之前,將顯著影響套用的性能。",
+  "cache_settings_duplicated_assets_clear_button": "清除",
+  "cache_settings_duplicated_assets_subtitle": "已加入黑名單的照片和短片",
+  "cache_settings_duplicated_assets_title": "重複項目( {} )",
+  "cache_settings_image_cache_size": "圖片緩存大小( {}  項)",
+  "cache_settings_statistics_album": "圖庫縮圖",
+  "cache_settings_statistics_assets": " {}  項( {} )",
+  "cache_settings_statistics_full": "完整圖片",
+  "cache_settings_statistics_shared": "共享相簿縮圖",
+  "cache_settings_statistics_thumbnail": "縮圖",
+  "cache_settings_statistics_title": "緩存使用情況",
+  "cache_settings_subtitle": "控制 Immich app 的緩存行為",
+  "cache_settings_thumbnail_size": "縮圖緩存大小( {}  項)",
+  "cache_settings_tile_subtitle": "設定本地存儲行為",
+  "cache_settings_tile_title": "本地存儲",
+  "cache_settings_title": "緩存設定",
+  "cancel": "Cancel",
+  "change_display_order": "Change display order",
+  "change_password_form_confirm_password": "確認密碼",
+  "change_password_form_description": "您好 {name} :\n\n這是您首次登入系統,或被管理員要求更改密碼。\n請在下方輸入新密碼。",
+  "change_password_form_new_password": "新密碼",
+  "change_password_form_password_mismatch": "密碼不一致",
+  "change_password_form_reenter_new_password": "再次輸入新密碼",
+  "check_corrupt_asset_backup": "Check for corrupt asset backups",
+  "check_corrupt_asset_backup_button": "Perform check",
+  "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
+  "client_cert_dialog_msg_confirm": "確定",
+  "client_cert_enter_password": "輸入密碼",
+  "client_cert_import": "匯入",
+  "client_cert_import_success_msg": "已匯入客戶端證書",
+  "client_cert_invalid_msg": "無效的證書文件或密碼錯誤",
+  "client_cert_remove": "移除",
+  "client_cert_remove_msg": "客戶端證書已移除",
+  "client_cert_subtitle": "僅支持PKCS12 (.p12, .pfx)格式。僅可在登入前進行證書的匯入和移除",
+  "client_cert_title": "SSL客戶端證書",
+  "common_add_to_album": "新增到相簿",
+  "common_change_password": "更改密碼",
+  "common_create_new_album": "新增相簿",
+  "common_server_error": "請檢查您的網絡連接,確保伺服器可連接,且本程式與伺服器版本兼容。",
+  "common_shared": "共享",
+  "contextual_search": "海灘上的日出",
+  "control_bottom_app_bar_add_to_album": "新增到相簿",
+  "control_bottom_app_bar_album_info": " {}  項",
+  "control_bottom_app_bar_album_info_shared": " {}  項 · 已共享",
+  "control_bottom_app_bar_archive": "歸檔",
+  "control_bottom_app_bar_create_new_album": "新增相簿",
+  "control_bottom_app_bar_delete": "刪除",
+  "control_bottom_app_bar_delete_from_immich": "從Immich伺服器中刪除",
+  "control_bottom_app_bar_delete_from_local": "從移動裝置中刪除",
+  "control_bottom_app_bar_download": "下載",
+  "control_bottom_app_bar_edit": "編輯",
+  "control_bottom_app_bar_edit_location": "編輯位置資訊",
+  "control_bottom_app_bar_edit_time": "編輯日期和時間",
+  "control_bottom_app_bar_favorite": "收藏",
+  "control_bottom_app_bar_share": "共享",
+  "control_bottom_app_bar_share_to": "發送給",
+  "control_bottom_app_bar_stack": "堆疊",
+  "control_bottom_app_bar_trash_from_immich": "放入回收桶",
+  "control_bottom_app_bar_unarchive": "取消歸檔",
+  "control_bottom_app_bar_unfavorite": "取消收藏",
+  "control_bottom_app_bar_upload": "上傳",
+  "create_album": "新增相簿",
+  "create_album_page_untitled": "未命名",
+  "create_new": "新增",
+  "create_shared_album_page_create": "新增",
+  "create_shared_album_page_share": "共享",
+  "create_shared_album_page_share_add_assets": "新增項目",
+  "create_shared_album_page_share_select_photos": "選擇項目",
+  "crop": "裁剪",
+  "curated_location_page_title": "地點",
+  "curated_object_page_title": "事物",
+  "current_server_address": "Current server address",
   "daily_title_text_date": "E, MMM dd",
   "daily_title_text_date_year": "E, MMM dd, yyyy",
   "date_format": "E, LLL d, y • h:mm a",
-  "delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
-  "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
-  "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
-  "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
-  "delete_dialog_cancel": "Cancel",
-  "delete_dialog_ok": "Delete",
-  "delete_dialog_ok_force": "Delete Anyway",
-  "delete_dialog_title": "Delete Permanently",
-  "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
-  "delete_local_dialog_ok_force": "Delete Anyway",
-  "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?",
-  "delete_shared_link_dialog_title": "Delete Shared Link",
-  "description_input_hint_text": "Add description...",
-  "description_input_submit_error": "Error updating description, check the log for more details",
-  "download_error": "Download Error",
-  "download_started": "Download started",
-  "download_sucess": "Download success",
-  "download_sucess_android": "The media has been downloaded to DCIM/Immich",
-  "edit_date_time_dialog_date_time": "Date and Time",
-  "edit_date_time_dialog_timezone": "Timezone",
-  "edit_image_title": "Edit",
-  "edit_location_dialog_title": "Location",
-  "error_saving_image": "Error: {}",
-  "exif_bottom_sheet_description": "Add Description...",
-  "exif_bottom_sheet_details": "DETAILS",
-  "exif_bottom_sheet_location": "LOCATION",
-  "exif_bottom_sheet_location_add": "Add a location",
-  "exif_bottom_sheet_people": "PEOPLE",
-  "exif_bottom_sheet_person_add_person": "Add name",
-  "experimental_settings_new_asset_list_subtitle": "Work in progress",
-  "experimental_settings_new_asset_list_title": "Enable experimental photo grid",
-  "experimental_settings_subtitle": "Use at your own risk!",
-  "experimental_settings_title": "Experimental",
-  "favorites_page_no_favorites": "No favorite assets found",
-  "favorites_page_title": "Favorites",
-  "filename_search": "File name or extension",
-  "haptic_feedback_switch": "Enable haptic feedback",
-  "haptic_feedback_title": "Haptic Feedback",
-  "header_settings_add_header_tip": "Add Header",
-  "header_settings_field_validator_msg": "Value cannot be empty",
-  "header_settings_header_name_input": "Header name",
-  "header_settings_header_value_input": "Header value",
-  "header_settings_page_title": "Proxy Headers",
-  "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
-  "headers_settings_tile_title": "Custom proxy headers",
-  "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
-  "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
-  "home_page_add_to_album_success": "Added {added} assets to album {album}.",
-  "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping",
-  "home_page_archive_err_local": "Can not archive local assets yet, skipping",
-  "home_page_archive_err_partner": "Can not archive partner assets, skipping",
-  "home_page_building_timeline": "Building the timeline",
-  "home_page_delete_err_partner": "Can not delete partner assets, skipping",
-  "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
-  "home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
-  "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping",
-  "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
-  "home_page_share_err_local": "Can not share local assets via link, skipping",
-  "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
-  "image_saved_successfully": "Image saved",
-  "image_viewer_page_state_provider_download_error": "Download Error",
-  "image_viewer_page_state_provider_download_started": "Download Started",
-  "image_viewer_page_state_provider_download_success": "Download Success",
-  "image_viewer_page_state_provider_share_error": "Share Error",
-  "invalid_date": "Invalid date",
-  "invalid_date_format": "Invalid date format",
-  "library_page_albums": "Albums",
-  "library_page_archive": "Archive",
-  "library_page_device_albums": "Albums on Device",
-  "library_page_favorites": "Favorites",
-  "library_page_new_album": "New album",
-  "library_page_sharing": "Sharing",
-  "library_page_sort_asset_count": "Number of assets",
-  "library_page_sort_created": "Created date",
-  "library_page_sort_last_modified": "Last modified",
-  "library_page_sort_most_oldest_photo": "Oldest photo",
-  "library_page_sort_most_recent_photo": "Most recent photo",
-  "library_page_sort_title": "Album title",
-  "location_picker_choose_on_map": "Choose on map",
-  "location_picker_latitude": "Latitude",
-  "location_picker_latitude_error": "Enter a valid latitude",
-  "location_picker_latitude_hint": "Enter your latitude here",
-  "location_picker_longitude": "Longitude",
-  "location_picker_longitude_error": "Enter a valid longitude",
-  "location_picker_longitude_hint": "Enter your longitude here",
-  "login_disabled": "Login has been disabled",
-  "login_form_api_exception": "API exception. Please check the server URL and try again.",
-  "login_form_back_button_text": "Back",
-  "login_form_button_text": "Login",
+  "delete_dialog_alert": "這些項目將從 Immich 和您的裝置中永久刪除",
+  "delete_dialog_alert_local": "這些項目將從您的移動裝置中永久刪除,但仍然可以從Immich伺服器中再次獲取",
+  "delete_dialog_alert_local_non_backed_up": "部分項目還未備份至Immich伺服器,將從您的移動裝置中永久刪除",
+  "delete_dialog_alert_remote": "這些項目將從Immich伺服器中永久刪除",
+  "delete_dialog_cancel": "取消",
+  "delete_dialog_ok": "刪除",
+  "delete_dialog_ok_force": "確認刪除",
+  "delete_dialog_title": "永久刪除",
+  "delete_local_dialog_ok_backed_up_only": "僅刪除已備份項目",
+  "delete_local_dialog_ok_force": "確認刪除",
+  "delete_shared_link_dialog_content": "確定要刪除此共享鏈接?",
+  "delete_shared_link_dialog_title": "刪除共享鏈接",
+  "description_input_hint_text": "新增描述...",
+  "description_input_submit_error": "更新描述時出錯,請檢查日誌以獲取更多詳細資訊",
+  "download_canceled": "下載已取消",
+  "download_complete": "下載完成",
+  "download_enqueue": "已加入下載隊列",
+  "download_error": "下載出錯",
+  "download_failed": "下載失敗",
+  "download_filename": "文件: {} ",
+  "download_finished": "下載完成",
+  "downloading": "下載中...",
+  "downloading_media": "正在下載媒體",
+  "download_notfound": "無法找到下載",
+  "download_paused": "下載已暫停",
+  "download_started": "開始下載",
+  "download_sucess": "下載成功",
+  "download_sucess_android": "媒體已下載至 DCIM/Immich",
+  "download_waiting_to_retry": "等待重試",
+  "edit_date_time_dialog_date_time": "日期和時間",
+  "edit_date_time_dialog_timezone": "時區",
+  "edit_image_title": "編輯",
+  "edit_location_dialog_title": "位置",
+  "enter_wifi_name": "Enter WiFi name",
+  "error_change_sort_album": "Failed to change album sort order",
+  "error_saving_image": "錯誤: {} ",
+  "exif_bottom_sheet_description": "新增描述...",
+  "exif_bottom_sheet_details": "詳情",
+  "exif_bottom_sheet_location": "位置",
+  "exif_bottom_sheet_location_add": "新增位置資訊",
+  "exif_bottom_sheet_people": "人物",
+  "exif_bottom_sheet_person_add_person": "新增姓名",
+  "experimental_settings_new_asset_list_subtitle": "正在處理",
+  "experimental_settings_new_asset_list_title": "啓用實驗性照片網格",
+  "experimental_settings_subtitle": "使用風險自負!",
+  "experimental_settings_title": "實驗性功能",
+  "external_network": "External network",
+  "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
+  "favorites": "收藏",
+  "favorites_page_no_favorites": "未找到收藏項目",
+  "favorites_page_title": "收藏",
+  "filename_search": "文件名或副檔名",
+  "filter": "篩選",
+  "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
+  "grant_permission": "Grant permission",
+  "haptic_feedback_switch": "啓用振動反饋",
+  "haptic_feedback_title": "振動反饋",
+  "header_settings_add_header_tip": "新增標頭",
+  "header_settings_field_validator_msg": "設定不可為空",
+  "header_settings_header_name_input": "標頭名稱",
+  "header_settings_header_value_input": "標頭值",
+  "header_settings_page_title": "代理標頭",
+  "headers_settings_tile_subtitle": "定義代理標頭,套用於每次網絡請求",
+  "headers_settings_tile_title": "自定義代理標頭",
+  "home_page_add_to_album_conflicts": "已在相簿 {album} 中新增 {added} 項。\n其中 {failed} 項在相簿中已存在。",
+  "home_page_add_to_album_err_local": "暫不能將本地項目新增到相簿中,略過",
+  "home_page_add_to_album_success": "已在相簿 {album}  中新增 {added} 項。",
+  "home_page_album_err_partner": "暫無法將同伴的項目新增到相簿,略過",
+  "home_page_archive_err_local": "暫無法歸檔本地項目,略過",
+  "home_page_archive_err_partner": "無法存檔同伴的項目,略過",
+  "home_page_building_timeline": "正在生成時間線",
+  "home_page_delete_err_partner": "無法刪除同伴的項目,略過",
+  "home_page_delete_remote_err_local": "遙距項目刪除模式,略過本地項目",
+  "home_page_favorite_err_local": "暫不能收藏本地項目,略過",
+  "home_page_favorite_err_partner": "暫無法收藏同伴的項目,略過",
+  "home_page_first_time_notice": "如果這是您第一次使用本程式,請確保選擇一個要備份的本地相簿,以便可以在時間線中預覽該相簿中的照片和短片。",
+  "home_page_share_err_local": "暫無法通過鏈接共享本地項目,略過",
+  "home_page_upload_err_limit": "一次最多只能上傳 30 個項目,略過",
+  "ignore_icloud_photos": "忽略iCloud照片",
+  "ignore_icloud_photos_description": "存儲在iCloud中的照片不會上傳至Immich伺服器",
+  "image_saved_successfully": "圖片已儲存",
+  "image_viewer_page_state_provider_download_error": "下載出現錯誤",
+  "image_viewer_page_state_provider_download_started": "下載啓動",
+  "image_viewer_page_state_provider_download_success": "下載成功",
+  "image_viewer_page_state_provider_share_error": "共享出錯",
+  "invalid_date": "無效的日期",
+  "invalid_date_format": "無效的日期格式",
+  "library": "圖庫",
+  "library_page_albums": "相簿",
+  "library_page_archive": "歸檔",
+  "library_page_device_albums": "裝置上的相簿",
+  "library_page_favorites": "收藏",
+  "library_page_new_album": "新增相簿",
+  "library_page_sharing": "共享",
+  "library_page_sort_asset_count": "項目數量",
+  "library_page_sort_created": "新增日期",
+  "library_page_sort_last_modified": "上次修改",
+  "library_page_sort_most_oldest_photo": "最早的照片",
+  "library_page_sort_most_recent_photo": "最近的項目",
+  "library_page_sort_title": "相簿標題",
+  "local_network": "Local network",
+  "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
+  "location_permission": "Location permission",
+  "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
+  "location_picker_choose_on_map": "在地圖上選擇",
+  "location_picker_latitude": "緯度",
+  "location_picker_latitude_error": "輸入有效的緯度值",
+  "location_picker_latitude_hint": "請在此處輸入您的緯度值",
+  "location_picker_longitude": "經度",
+  "location_picker_longitude_error": "輸入有效的經度值",
+  "location_picker_longitude_hint": "請在此處輸入您的經度值",
+  "login_disabled": "已禁用登入",
+  "login_form_api_exception": "API 異常,請檢查伺服器地址並重試。",
+  "login_form_back_button_text": "後退",
+  "login_form_button_text": "登入",
   "login_form_email_hint": "youremail@email.com",
-  "login_form_endpoint_hint": "http://your-server-ip:port/api",
-  "login_form_endpoint_url": "Server Endpoint URL",
-  "login_form_err_http": "Please specify http:// or https://",
-  "login_form_err_invalid_email": "Invalid Email",
-  "login_form_err_invalid_url": "Invalid URL",
-  "login_form_err_leading_whitespace": "Leading whitespace",
-  "login_form_err_trailing_whitespace": "Trailing whitespace",
-  "login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL",
-  "login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server",
-  "login_form_failed_login": "Error logging you in, check server URL, email and password",
-  "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.",
-  "login_form_label_email": "Email",
-  "login_form_label_password": "Password",
-  "login_form_next_button": "Next",
-  "login_form_password_hint": "password",
-  "login_form_save_login": "Stay logged in",
-  "login_form_server_empty": "Enter a server URL.",
-  "login_form_server_error": "Could not connect to server.",
-  "login_password_changed_error": "There was an error updating your password",
-  "login_password_changed_success": "Password updated successfully",
-  "map_assets_in_bound": "{} photo",
-  "map_assets_in_bounds": "{} photos",
-  "map_cannot_get_user_location": "Cannot get user's location",
-  "map_location_dialog_cancel": "Cancel",
-  "map_location_dialog_yes": "Yes",
-  "map_location_picker_page_use_location": "Use this location",
-  "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?",
-  "map_location_service_disabled_title": "Location Service disabled",
-  "map_no_assets_in_bounds": "No photos in this area",
-  "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
-  "map_no_location_permission_title": "Location Permission denied",
-  "map_settings_dark_mode": "Dark mode",
-  "map_settings_date_range_option_all": "All",
-  "map_settings_date_range_option_day": "Past 24 hours",
-  "map_settings_date_range_option_days": "Past {} days",
-  "map_settings_date_range_option_year": "Past year",
-  "map_settings_date_range_option_years": "Past {} years",
-  "map_settings_dialog_cancel": "Cancel",
-  "map_settings_dialog_save": "Save",
-  "map_settings_dialog_title": "Map Settings",
-  "map_settings_include_show_archived": "Include Archived",
-  "map_settings_include_show_partners": "Include Partners",
-  "map_settings_only_relative_range": "Date range",
-  "map_settings_only_show_favorites": "Show Favorite Only",
-  "map_settings_theme_settings": "Map Theme",
-  "map_zoom_to_see_photos": "Zoom out to see photos",
-  "memories_all_caught_up": "All caught up",
-  "memories_check_back_tomorrow": "Check back tomorrow for more memories",
-  "memories_start_over": "Start Over",
-  "memories_swipe_to_close": "Swipe up to close",
-  "memories_year_ago": "A year ago",
-  "memories_years_ago": "{} years ago",
+  "login_form_endpoint_hint": "http(s)://您的伺服器地址:端口/api",
+  "login_form_endpoint_url": "伺服器鏈接地址",
+  "login_form_err_http": "請注明 http:// 或 https://",
+  "login_form_err_invalid_email": "電郵無效",
+  "login_form_err_invalid_url": "無效的地址",
+  "login_form_err_leading_whitespace": "帶有前導空格",
+  "login_form_err_trailing_whitespace": "帶有尾隨空格",
+  "login_form_failed_get_oauth_server_config": "使用 OAuth 登入時錯誤,請檢查伺服器地址",
+  "login_form_failed_get_oauth_server_disable": "OAuth 功能在此伺服器上不可用",
+  "login_form_failed_login": "登入失敗,請檢查伺服器地址、電郵和密碼",
+  "login_form_handshake_exception": "與伺服器通信時出現握手異常。如果您使用的是自簽名證書,請在設定中啓用自簽名證書支持。",
+  "login_form_label_email": "電郵",
+  "login_form_label_password": "密碼",
+  "login_form_next_button": "下一個",
+  "login_form_password_hint": "密碼",
+  "login_form_save_login": "保持登入",
+  "login_form_server_empty": "輸入伺服器地址",
+  "login_form_server_error": "無法連接到伺服器。",
+  "login_password_changed_error": "密碼更新失敗",
+  "login_password_changed_success": "密碼更新成功",
+  "map_assets_in_bound": " {} 張照片",
+  "map_assets_in_bounds": " {} 張照片",
+  "map_cannot_get_user_location": "無法獲取用戶位置",
+  "map_location_dialog_cancel": "取消",
+  "map_location_dialog_yes": "確定",
+  "map_location_picker_page_use_location": "使用此位置",
+  "map_location_service_disabled_content": "需要啓用定位服務才能顯示當前位置相關的項目。要現在啓用嗎?",
+  "map_location_service_disabled_title": "定位服務已禁用",
+  "map_no_assets_in_bounds": "此區域中沒有相關項目",
+  "map_no_location_permission_content": "需要位置權限才能顯示與當前位置相關的項目。要現在就授予位置權限嗎?",
+  "map_no_location_permission_title": "位置權限被拒絕",
+  "map_settings_dark_mode": "深色模式",
+  "map_settings_date_range_option_all": "所有",
+  "map_settings_date_range_option_day": "過去24小時",
+  "map_settings_date_range_option_days": " {} 天前",
+  "map_settings_date_range_option_year": "1年前",
+  "map_settings_date_range_option_years": " {} 年前",
+  "map_settings_dialog_cancel": "取消",
+  "map_settings_dialog_save": "儲存",
+  "map_settings_dialog_title": "地圖設定",
+  "map_settings_include_show_archived": "包括已歸檔項目",
+  "map_settings_include_show_partners": "包含夥伴",
+  "map_settings_only_relative_range": "日期範圍",
+  "map_settings_only_show_favorites": "僅顯示收藏的項目",
+  "map_settings_theme_settings": "地圖主題",
+  "map_zoom_to_see_photos": "縮小以查看項目",
+  "memories_all_caught_up": "已全部看完",
+  "memories_check_back_tomorrow": "明天再看",
+  "memories_start_over": "再看一次",
+  "memories_swipe_to_close": "上滑關閉",
+  "memories_year_ago": "1年前",
+  "memories_years_ago": " {} 年前",
   "monthly_title_text_date_format": "MMMM y",
-  "motion_photos_page_title": "Motion Photos",
-  "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
-  "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
-  "no_assets_to_show": "No assets to show",
-  "no_name": "No name",
-  "notification_permission_dialog_cancel": "Cancel",
-  "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
-  "notification_permission_dialog_settings": "Settings",
-  "notification_permission_list_tile_content": "Grant permission to enable notifications.",
-  "notification_permission_list_tile_enable_button": "Enable Notifications",
-  "notification_permission_list_tile_title": "Notification Permission",
-  "partner_list_user_photos": "{user}'s photos",
-  "partner_list_view_all": "View all",
-  "partner_page_add_partner": "Add partner",
-  "partner_page_empty_message": "Your photos are not yet shared with any partner.",
-  "partner_page_no_more_users": "No more users to add",
-  "partner_page_partner_add_failed": "Failed to add partner",
-  "partner_page_select_partner": "Select partner",
-  "partner_page_shared_to_title": "Shared to",
-  "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
-  "partner_page_stop_sharing_title": "Stop sharing your photos?",
-  "partner_page_title": "Partner",
-  "permission_onboarding_back": "Back",
-  "permission_onboarding_continue_anyway": "Continue anyway",
-  "permission_onboarding_get_started": "Get started",
-  "permission_onboarding_go_to_settings": "Go to settings",
-  "permission_onboarding_grant_permission": "Grant permission",
-  "permission_onboarding_log_out": "Log out",
-  "permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.",
-  "permission_onboarding_permission_granted": "Permission granted! You are all set.",
-  "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
-  "permission_onboarding_request": "Immich requires permission to view your photos and videos.",
-  "preferences_settings_title": "Preferences",
-  "profile_drawer_app_logs": "Logs",
-  "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
-  "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
-  "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
-  "profile_drawer_documentation": "Documentation",
+  "motion_photos_page_title": "動態照片",
+  "multiselect_grid_edit_date_time_err_read_only": "無法編輯唯讀項目的日期,略過",
+  "multiselect_grid_edit_gps_err_read_only": "無法編輯唯讀項目的位置資訊,略過",
+  "my_albums": "我的相簿",
+  "networking_settings": "Networking",
+  "networking_subtitle": "Manage the server endpoint settings",
+  "no_assets_to_show": "無項目展示",
+  "no_name": "無姓名",
+  "notification_permission_dialog_cancel": "取消",
+  "notification_permission_dialog_content": "要啓用通知,請前往「設定」,並選擇「允許」。",
+  "notification_permission_dialog_settings": "設定",
+  "notification_permission_list_tile_content": "授予通知權限。",
+  "notification_permission_list_tile_enable_button": "啓用通知",
+  "notification_permission_list_tile_title": "通知權限",
+  "on_this_device": "在此裝置",
+  "partner_list_user_photos": "{user} 的照片",
+  "partner_list_view_all": "展示全部",
+  "partner_page_add_partner": "新增同伴",
+  "partner_page_empty_message": "您的照片尚未與任何同伴共享。",
+  "partner_page_no_more_users": "無需新增更多用戶",
+  "partner_page_partner_add_failed": "新增同伴失敗",
+  "partner_page_select_partner": "選擇同伴",
+  "partner_page_shared_to_title": "共享給",
+  "partner_page_stop_sharing_content": " {}  將無法再存取您的照片。",
+  "partner_page_stop_sharing_title": "您確定要停止共享您的照片嗎?",
+  "partner_page_title": "同伴",
+  "partners": "同伴",
+  "people": "人物",
+  "permission_onboarding_back": "返回",
+  "permission_onboarding_continue_anyway": "仍然繼續",
+  "permission_onboarding_get_started": "開始使用",
+  "permission_onboarding_go_to_settings": "前往設定",
+  "permission_onboarding_grant_permission": "授予權限",
+  "permission_onboarding_log_out": "登出",
+  "permission_onboarding_permission_denied": "權限被拒:要使用 Immich,請在「設定」中授予照片和短片權限。",
+  "permission_onboarding_permission_granted": "已授權!一切就緒。",
+  "permission_onboarding_permission_limited": "權限受限:要讓 Immich 備份和管理您的整個圖庫收藏,請在「設定」中授予照片和短片權限。",
+  "permission_onboarding_request": "Immich 需要權限才能查看您的照片和短片。",
+  "places": "地點",
+  "preferences_settings_subtitle": "Manage the app's preferences",
+  "preferences_settings_title": "偏好設定",
+  "profile_drawer_app_logs": "日誌",
+  "profile_drawer_client_out_of_date_major": "客戶端有大版本升級,請盡快升級至最新版。",
+  "profile_drawer_client_out_of_date_minor": "客戶端有小版本升級,請盡快升級至最新版。",
+  "profile_drawer_client_server_up_to_date": "客戶端和服務端都是最新的",
+  "profile_drawer_documentation": "文檔",
   "profile_drawer_github": "GitHub",
-  "profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.",
-  "profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.",
-  "profile_drawer_settings": "Settings",
-  "profile_drawer_sign_out": "Sign Out",
-  "profile_drawer_trash": "Trash",
-  "recently_added_page_title": "Recently Added",
-  "save_to_gallery": "Save to gallery",
-  "scaffold_body_error_occurred": "Error occurred",
-  "search_bar_hint": "Search your photos",
-  "search_filter_apply": "Apply filter",
-  "search_filter_camera": "Camera",
-  "search_filter_camera_make": "Make",
-  "search_filter_camera_model": "Model",
-  "search_filter_camera_title": "Select camera type",
-  "search_filter_date": "Date",
-  "search_filter_date_interval": "{start} to {end}",
-  "search_filter_date_title": "Select a date range",
-  "search_filter_display_option_archive": "Archive",
-  "search_filter_display_option_favorite": "Favorite",
-  "search_filter_display_option_not_in_album": "Not in album",
-  "search_filter_display_options": "Display Options",
-  "search_filter_display_options_title": "Display options",
-  "search_filter_location": "Location",
-  "search_filter_location_city": "City",
-  "search_filter_location_country": "Country",
-  "search_filter_location_state": "State",
-  "search_filter_location_title": "Select location",
-  "search_filter_media_type": "Media Type",
-  "search_filter_media_type_all": "All",
-  "search_filter_media_type_image": "Image",
-  "search_filter_media_type_title": "Select media type",
-  "search_filter_media_type_video": "Video",
-  "search_filter_people": "People",
-  "search_filter_people_title": "Select people",
-  "search_page_categories": "Categories",
-  "search_page_favorites": "Favorites",
-  "search_page_motion_photos": "Motion Photos",
-  "search_page_no_objects": "No Objects Info Available",
-  "search_page_no_places": "No Places Info Available",
-  "search_page_people": "People",
-  "search_page_person_add_name_dialog_cancel": "Cancel",
-  "search_page_person_add_name_dialog_hint": "Name",
-  "search_page_person_add_name_dialog_save": "Save",
-  "search_page_person_add_name_dialog_title": "Add a name",
-  "search_page_person_add_name_subtitle": "Find them fast by name with search",
-  "search_page_person_add_name_title": "Add a name",
-  "search_page_person_edit_name": "Edit name",
-  "search_page_places": "Places",
-  "search_page_recently_added": "Recently added",
-  "search_page_screenshots": "Screenshots",
-  "search_page_selfies": "Selfies",
-  "search_page_things": "Things",
-  "search_page_videos": "Videos",
-  "search_page_view_all_button": "View all",
-  "search_page_your_activity": "Your activity",
-  "search_page_your_map": "Your Map",
-  "search_result_page_new_search_hint": "New Search",
-  "search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ",
-  "search_suggestion_list_smart_search_hint_2": "m:your-search-term",
-  "select_additional_user_for_sharing_page_suggestions": "Suggestions",
-  "select_user_for_sharing_page_err_album": "Failed to create album",
-  "select_user_for_sharing_page_share_suggestions": "Suggestions",
-  "server_info_box_app_version": "App Version",
-  "server_info_box_latest_release": "Latest Version",
-  "server_info_box_server_url": "Server URL",
-  "server_info_box_server_version": "Server Version",
-  "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
-  "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).",
-  "setting_image_viewer_original_title": "Load original image",
-  "setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.",
-  "setting_image_viewer_preview_title": "Load preview image",
-  "setting_image_viewer_title": "Images",
-  "setting_languages_apply": "Apply",
-  "setting_languages_title": "Languages",
-  "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
-  "setting_notifications_notify_hours": "{} hours",
-  "setting_notifications_notify_immediately": "immediately",
-  "setting_notifications_notify_minutes": "{} minutes",
-  "setting_notifications_notify_never": "never",
-  "setting_notifications_notify_seconds": "{} seconds",
-  "setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset",
-  "setting_notifications_single_progress_title": "Show background backup detail progress",
-  "setting_notifications_subtitle": "Adjust your notification preferences",
-  "setting_notifications_title": "Notifications",
-  "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
-  "setting_notifications_total_progress_title": "Show background backup total progress",
-  "setting_pages_app_bar_settings": "Settings",
-  "settings_require_restart": "Please restart Immich to apply this setting",
-  "setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
-  "setting_video_viewer_looping_title": "Looping",
-  "setting_video_viewer_title": "Videos",
-  "share_add": "Add",
-  "share_add_photos": "Add photos",
-  "share_add_title": "Add a title",
-  "share_assets_selected": "{} selected",
-  "share_create_album": "Create album",
-  "shared_album_activities_input_disable": "Comment is disabled",
-  "shared_album_activities_input_hint": "Say something",
-  "shared_album_activity_remove_content": "Do you want to delete this activity?",
-  "shared_album_activity_remove_title": "Delete Activity",
-  "shared_album_activity_setting_subtitle": "Let others respond",
-  "shared_album_activity_setting_title": "Comments & likes",
-  "shared_album_section_people_action_error": "Error leaving/removing from album",
-  "shared_album_section_people_action_leave": "Remove user from album",
-  "shared_album_section_people_action_remove_user": "Remove user from album",
-  "shared_album_section_people_owner_label": "Owner",
-  "shared_album_section_people_title": "PEOPLE",
-  "share_dialog_preparing": "Preparing...",
-  "shared_link_app_bar_title": "Shared Links",
-  "shared_link_clipboard_copied_massage": "Copied to clipboard",
-  "shared_link_clipboard_text": "Link: {}\nPassword: {}",
-  "shared_link_create_app_bar_title": "Create link to share",
-  "shared_link_create_error": "Error while creating shared link",
-  "shared_link_create_info": "Let anyone with the link see the selected photo(s)",
-  "shared_link_create_submit_button": "Create link",
-  "shared_link_edit_allow_download": "Allow public user to download",
-  "shared_link_edit_allow_upload": "Allow public user to upload",
-  "shared_link_edit_app_bar_title": "Edit link",
-  "shared_link_edit_change_expiry": "Change expiration time",
-  "shared_link_edit_description": "Description",
-  "shared_link_edit_description_hint": "Enter the share description",
-  "shared_link_edit_expire_after": "Expire after",
-  "shared_link_edit_expire_after_option_day": "1 day",
-  "shared_link_edit_expire_after_option_days": "{} days",
-  "shared_link_edit_expire_after_option_hour": "1 hour",
-  "shared_link_edit_expire_after_option_hours": "{} hours",
-  "shared_link_edit_expire_after_option_minute": "1 minute",
-  "shared_link_edit_expire_after_option_minutes": "{} minutes",
-  "shared_link_edit_expire_after_option_months": "{} months",
-  "shared_link_edit_expire_after_option_never": "Never",
-  "shared_link_edit_expire_after_option_year": "{} year",
-  "shared_link_edit_password": "Password",
-  "shared_link_edit_password_hint": "Enter the share password",
-  "shared_link_edit_show_meta": "Show metadata",
-  "shared_link_edit_submit_button": "Update link",
-  "shared_link_empty": "You don't have any shared links",
-  "shared_link_error_server_url_fetch": "Cannot fetch the server url",
-  "shared_link_expired": "Expired",
-  "shared_link_expires_day": "Expires in {} day",
-  "shared_link_expires_days": "Expires in {} days",
-  "shared_link_expires_hour": "Expires in {} hour",
-  "shared_link_expires_hours": "Expires in {} hours",
-  "shared_link_expires_minute": "Expires in {} minute",
-  "shared_link_expires_minutes": "Expires in {} minutes",
-  "shared_link_expires_never": "Expires ∞",
-  "shared_link_expires_second": "Expires in {} second",
-  "shared_link_expires_seconds": "Expires in {} seconds",
-  "shared_link_individual_shared": "Individual shared",
-  "shared_link_info_chip_download": "Download",
+  "profile_drawer_server_out_of_date_major": "服務端有大版本升級,請盡快升級至最新版。",
+  "profile_drawer_server_out_of_date_minor": "服務端有小版本升級,請盡快升級至最新版。",
+  "profile_drawer_settings": "設定",
+  "profile_drawer_sign_out": "登出",
+  "profile_drawer_trash": "回收桶",
+  "recently_added": "近期新增",
+  "recently_added_page_title": "最近新增",
+  "save": "Save",
+  "save_to_gallery": "儲存到圖庫",
+  "scaffold_body_error_occurred": "發生錯誤",
+  "search_albums": "搜尋相簿",
+  "search_bar_hint": "搜尋照片",
+  "search_filter_apply": "套用篩選",
+  "search_filter_camera": "相機",
+  "search_filter_camera_make": "製造商",
+  "search_filter_camera_model": "型號",
+  "search_filter_camera_title": "選擇相機類型",
+  "search_filter_date": "日期",
+  "search_filter_date_interval": "從 {start} 到 {end}",
+  "search_filter_date_title": "選擇日期範圍",
+  "search_filter_display_option_archive": "歸檔",
+  "search_filter_display_option_favorite": "收藏",
+  "search_filter_display_option_not_in_album": "不在相簿中",
+  "search_filter_display_options": "顯示選項",
+  "search_filter_display_options_title": "顯示選項",
+  "search_filter_location": "位置",
+  "search_filter_location_city": "城市",
+  "search_filter_location_country": "國家",
+  "search_filter_location_state": "省",
+  "search_filter_location_title": "選擇位置",
+  "search_filter_media_type": "媒體類型",
+  "search_filter_media_type_all": "所有",
+  "search_filter_media_type_image": "照片",
+  "search_filter_media_type_title": "選擇媒體類型",
+  "search_filter_media_type_video": "短片",
+  "search_filter_people": "人物",
+  "search_filter_people_title": "選擇人物",
+  "search_page_categories": "類別",
+  "search_page_favorites": "收藏",
+  "search_page_motion_photos": "動態照片\n",
+  "search_page_no_objects": "找不到物件資訊",
+  "search_page_no_places": "找不到地點資訊",
+  "search_page_people": "人物",
+  "search_page_person_add_name_dialog_cancel": "取消",
+  "search_page_person_add_name_dialog_hint": "姓名",
+  "search_page_person_add_name_dialog_save": "儲存",
+  "search_page_person_add_name_dialog_title": "新增姓名",
+  "search_page_person_add_name_subtitle": "通過姓名快速查找",
+  "search_page_person_add_name_title": "新增姓名",
+  "search_page_person_edit_name": "編輯姓名",
+  "search_page_places": "地點",
+  "search_page_recently_added": "最近新增",
+  "search_page_screenshots": "屏幕截圖",
+  "search_page_search_photos_videos": "Search for your photos and videos",
+  "search_page_selfies": "自拍",
+  "search_page_things": "事物",
+  "search_page_videos": "短片",
+  "search_page_view_all_button": "查看全部",
+  "search_page_your_activity": "您的活動",
+  "search_page_your_map": "你的足跡",
+  "search_result_page_new_search_hint": "搜尋新的",
+  "search_suggestion_list_smart_search_hint_1": "默認情況下啓用智能搜尋,要搜尋中繼數據,請使用相關語法",
+  "search_suggestion_list_smart_search_hint_2": "m:您的搜尋關鍵詞",
+  "select_additional_user_for_sharing_page_suggestions": "建議",
+  "select_user_for_sharing_page_err_album": "新增相簿失敗",
+  "select_user_for_sharing_page_share_suggestions": "建議",
+  "server_endpoint": "Server Endpoint",
+  "server_info_box_app_version": "App 版本",
+  "server_info_box_latest_release": "最新版本",
+  "server_info_box_server_url": "伺服器地址",
+  "server_info_box_server_version": "伺服器版本",
+  "setting_image_viewer_help": "詳細資訊查看器首先載入小縮圖,然後載入中等大小的預覽圖(若啓用),最後載入原始圖片。",
+  "setting_image_viewer_original_subtitle": "啓用以載入原圖,禁用以減少數據使用量(包括網絡和裝置緩存)。",
+  "setting_image_viewer_original_title": "載入原圖",
+  "setting_image_viewer_preview_subtitle": "啓用以載入中等質量的圖片,禁用以載入原圖或縮圖。",
+  "setting_image_viewer_preview_title": "載入預覽圖",
+  "setting_image_viewer_title": "圖片",
+  "setting_languages_apply": "套用",
+  "setting_languages_subtitle": "Change the app's language",
+  "setting_languages_title": "語言",
+  "setting_notifications_notify_failures_grace_period": "背景備份失敗通知: {} ",
+  "setting_notifications_notify_hours": " {}  小時",
+  "setting_notifications_notify_immediately": "立即",
+  "setting_notifications_notify_minutes": " {}  分鐘",
+  "setting_notifications_notify_never": "從不",
+  "setting_notifications_notify_seconds": " {}  秒",
+  "setting_notifications_single_progress_subtitle": "每項的詳細上傳進度資訊",
+  "setting_notifications_single_progress_title": "顯示背景備份詳細進度",
+  "setting_notifications_subtitle": "調整通知選項",
+  "setting_notifications_title": "通知",
+  "setting_notifications_total_progress_subtitle": "總體上傳進度(已完成/總計)",
+  "setting_notifications_total_progress_title": "顯示背景備份總進度",
+  "setting_pages_app_bar_settings": "設定",
+  "settings_require_restart": "請重啓 Immich 以使設定生效",
+  "setting_video_viewer_looping_subtitle": "對播放窗口中的短片開啓循環播放。",
+  "setting_video_viewer_looping_title": "循環播放",
+  "setting_video_viewer_title": "短片",
+  "share_add": "新增",
+  "share_add_photos": "新增項目",
+  "share_add_title": "新增標題",
+  "share_assets_selected": " {}  已選擇",
+  "share_create_album": "新增相簿",
+  "shared_album_activities_input_disable": "已禁用評論",
+  "shared_album_activities_input_hint": "評論一下",
+  "shared_album_activity_remove_content": "您確定要刪除此活動嗎?",
+  "shared_album_activity_remove_title": "刪除活動",
+  "shared_album_activity_setting_subtitle": "允許他人回覆",
+  "shared_album_activity_setting_title": "評論與讚好",
+  "shared_album_section_people_action_error": "退出/刪除相簿失敗",
+  "shared_album_section_people_action_leave": "從相簿中刪除用戶",
+  "shared_album_section_people_action_remove_user": "從相簿中刪除用戶",
+  "shared_album_section_people_owner_label": "所有者",
+  "shared_album_section_people_title": "人物",
+  "share_dialog_preparing": "正在準備...",
+  "shared_link_app_bar_title": "共享鏈接",
+  "shared_link_clipboard_copied_massage": "複製到剪貼板",
+  "shared_link_clipboard_text": "鏈接: {} \n密碼: {} ",
+  "shared_link_create_app_bar_title": "新增共享鏈接",
+  "shared_link_create_error": "新增共享鏈接出錯",
+  "shared_link_create_info": "任何獲得鏈接的人都可看到照片",
+  "shared_link_create_submit_button": "新增鏈接",
+  "shared_link_edit_allow_download": "允許遊客下載",
+  "shared_link_edit_allow_upload": "允許遊客上傳",
+  "shared_link_edit_app_bar_title": "編輯鏈接",
+  "shared_link_edit_change_expiry": "修改過期時間",
+  "shared_link_edit_description": "描述",
+  "shared_link_edit_description_hint": "編輯共享描述",
+  "shared_link_edit_expire_after": "有效期",
+  "shared_link_edit_expire_after_option_day": "1天",
+  "shared_link_edit_expire_after_option_days": " {} 天",
+  "shared_link_edit_expire_after_option_hour": "1小時",
+  "shared_link_edit_expire_after_option_hours": " {} 小時",
+  "shared_link_edit_expire_after_option_minute": "1分鐘",
+  "shared_link_edit_expire_after_option_minutes": " {} 分鐘",
+  "shared_link_edit_expire_after_option_months": " {}  個月",
+  "shared_link_edit_expire_after_option_never": "永久",
+  "shared_link_edit_expire_after_option_year": " {}  年",
+  "shared_link_edit_password": "密碼",
+  "shared_link_edit_password_hint": "輸入共享密碼",
+  "shared_link_edit_show_meta": "顯示中繼數據",
+  "shared_link_edit_submit_button": "更新鏈接",
+  "shared_link_empty": "您還沒有新增共享鏈接",
+  "shared_link_error_server_url_fetch": "無法獲取伺服器地址",
+  "shared_link_expired": "已過期",
+  "shared_link_expires_day": " {} 天後過期",
+  "shared_link_expires_days": " {} 天後過期",
+  "shared_link_expires_hour": " {} 小時後過期",
+  "shared_link_expires_hours": " {} 小時後過期",
+  "shared_link_expires_minute": " {} 分鐘後過期",
+  "shared_link_expires_minutes": "將在 {} 分鐘後過期",
+  "shared_link_expires_never": "永不過期",
+  "shared_link_expires_second": " {} 秒後過期",
+  "shared_link_expires_seconds": "將在 {} 秒後過期",
+  "shared_link_individual_shared": "個人共享",
+  "shared_link_info_chip_download": "下載",
   "shared_link_info_chip_metadata": "EXIF",
-  "shared_link_info_chip_upload": "Upload",
-  "shared_link_manage_links": "Manage Shared links",
-  "shared_link_public_album": "Public album",
-  "share_done": "Done",
-  "share_invite": "Invite to album",
-  "sharing_page_album": "Shared albums",
-  "sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
-  "sharing_page_empty_list": "EMPTY LIST",
-  "sharing_silver_appbar_create_shared_album": "New shared album",
-  "sharing_silver_appbar_shared_links": "Shared links",
-  "sharing_silver_appbar_share_partner": "Share with partner",
-  "sync": "Sync",
-  "sync_albums": "Sync albums",
-  "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
-  "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
-  "tab_controller_nav_library": "Library",
-  "tab_controller_nav_photos": "Photos",
-  "tab_controller_nav_search": "Search",
-  "tab_controller_nav_sharing": "Sharing",
-  "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles",
-  "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})",
-  "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.",
-  "theme_setting_colorful_interface_title": "Colorful interface",
-  "theme_setting_dark_mode_switch": "Dark mode",
-  "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer",
-  "theme_setting_image_viewer_quality_title": "Image viewer quality",
-  "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.",
-  "theme_setting_primary_color_title": "Primary color",
-  "theme_setting_system_primary_color_title": "Use system color",
-  "theme_setting_system_theme_switch": "Automatic (Follow system setting)",
-  "theme_setting_theme_subtitle": "Choose the app's theme setting",
-  "theme_setting_theme_title": "Theme",
-  "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
-  "theme_setting_three_stage_loading_title": "Enable three-stage loading",
-  "translated_text_options": "Options",
-  "trash_emptied": "Emptied trash",
-  "trash_page_delete": "Delete",
-  "trash_page_delete_all": "Delete All",
-  "trash_page_empty_trash_btn": "Empty trash",
-  "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich",
-  "trash_page_empty_trash_dialog_ok": "Ok",
-  "trash_page_info": "Trashed items will be permanently deleted after {} days",
-  "trash_page_no_assets": "No trashed assets",
-  "trash_page_restore": "Restore",
-  "trash_page_restore_all": "Restore All",
-  "trash_page_select_assets_btn": "Select assets",
-  "trash_page_select_btn": "Select",
-  "trash_page_title": "Trash ({})",
-  "upload_dialog_cancel": "Cancel",
-  "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
-  "upload_dialog_ok": "Upload",
-  "upload_dialog_title": "Upload Asset",
-  "version_announcement_overlay_ack": "Acknowledge",
-  "version_announcement_overlay_release_notes": "release notes",
-  "version_announcement_overlay_text_1": "Hi friend, there is a new release of",
-  "version_announcement_overlay_text_2": "please take your time to visit the ",
-  "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
-  "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
-  "viewer_remove_from_stack": "Remove from Stack",
-  "viewer_stack_use_as_main_asset": "Use as Main Asset",
-  "viewer_unstack": "Un-Stack"
+  "shared_link_info_chip_upload": "上載",
+  "shared_link_manage_links": "管理共享鏈接",
+  "shared_link_public_album": "公共相簿",
+  "shared_links": "共享鏈接",
+  "share_done": "完成",
+  "shared_with_me": "與我共享",
+  "share_invite": "邀請到共享相簿",
+  "sharing_page_album": "共享相簿",
+  "sharing_page_description": "新增共享相簿以與網絡中的人共享照片和短片。",
+  "sharing_page_empty_list": "空白清單",
+  "sharing_silver_appbar_create_shared_album": "新增共享相簿",
+  "sharing_silver_appbar_shared_links": "共享鏈接",
+  "sharing_silver_appbar_share_partner": "共享給同伴",
+  "sync": "同步",
+  "sync_albums": "同步相簿",
+  "sync_albums_manual_subtitle": "將所有上傳的短片和照片同步到選定的備份相簿",
+  "sync_upload_album_setting_subtitle": "新增照片和短片並上傳到 Immich 上的選定相簿中",
+  "tab_controller_nav_library": "圖庫",
+  "tab_controller_nav_photos": "照片",
+  "tab_controller_nav_search": "搜尋",
+  "tab_controller_nav_sharing": "共享",
+  "theme_setting_asset_list_storage_indicator_title": "在項目標題上顯示使用之儲存空間",
+  "theme_setting_asset_list_tiles_per_row_title": "每行展示  {}  項",
+  "theme_setting_colorful_interface_subtitle": "套用主色調到背景",
+  "theme_setting_colorful_interface_title": "彩色界面",
+  "theme_setting_dark_mode_switch": "深色模式",
+  "theme_setting_image_viewer_quality_subtitle": "調整查看大圖時的圖片質量",
+  "theme_setting_image_viewer_quality_title": "圖片質量",
+  "theme_setting_primary_color_subtitle": "選擇顏色作為主色調",
+  "theme_setting_primary_color_title": "主色調",
+  "theme_setting_system_primary_color_title": "使用系統顏色",
+  "theme_setting_system_theme_switch": "自動(跟隨系統設定)",
+  "theme_setting_theme_subtitle": "選擇套用主題",
+  "theme_setting_theme_title": "主題",
+  "theme_setting_three_stage_loading_subtitle": "三段式載入可能會提升載入性能,但可能會導致更高的網絡負載",
+  "theme_setting_three_stage_loading_title": "啓用三段式載入",
+  "translated_text_options": "選項",
+  "trash": "回收桶",
+  "trash_emptied": "已清空回收桶\n",
+  "trash_page_delete": "刪除",
+  "trash_page_delete_all": "刪除全部",
+  "trash_page_empty_trash_btn": "清空回收桶",
+  "trash_page_empty_trash_dialog_content": "是否清空回收桶?這些項目將被從Immich中永久刪除",
+  "trash_page_empty_trash_dialog_ok": "確定",
+  "trash_page_info": "回收桶中項目將在 {} 天後永久刪除",
+  "trash_page_no_assets": "暫無已刪除項目",
+  "trash_page_restore": "恢復",
+  "trash_page_restore_all": "恢復全部",
+  "trash_page_select_assets_btn": "選擇項目",
+  "trash_page_select_btn": "選擇",
+  "trash_page_title": "回收桶 ( {} )",
+  "upload_dialog_cancel": "取消",
+  "upload_dialog_info": "是否要將所選項目備份到伺服器?",
+  "upload_dialog_ok": "上傳",
+  "upload_dialog_title": "上傳項目",
+  "use_current_connection": "use current connection",
+  "validate_endpoint_error": "Please enter a valid URL",
+  "version_announcement_overlay_ack": "我知道了",
+  "version_announcement_overlay_release_notes": "發行說明",
+  "version_announcement_overlay_text_1": "好消息,有新版本的",
+  "version_announcement_overlay_text_2": "請花點時間訪問",
+  "version_announcement_overlay_text_3": "並檢查您的 docker-compose 和 .env 是否為最新且正確的設定,特別是您在使用 WatchTower 或者其他自動更新的程式時,您需要更加細緻的檢查。",
+  "version_announcement_overlay_title": "服務端有新版本啦 \uD83C\uDF89",
+  "videos": "短片",
+  "viewer_remove_from_stack": "從堆疊中移除",
+  "viewer_stack_use_as_main_asset": "作為主項目使用",
+  "viewer_unstack": "取消堆疊",
+  "wifi_name": "WiFi Name",
+  "your_wifi_name": "Your WiFi name"
 }
\ No newline at end of file
diff --git a/mobile/assets/polaroid-dark.png b/mobile/assets/polaroid-dark.png
new file mode 100644
index 0000000000..977897479b
Binary files /dev/null and b/mobile/assets/polaroid-dark.png differ
diff --git a/mobile/assets/polaroid-light.png b/mobile/assets/polaroid-light.png
new file mode 100644
index 0000000000..25cd7e5461
Binary files /dev/null and b/mobile/assets/polaroid-light.png differ
diff --git a/mobile/immich_lint/analysis_options.yaml b/mobile/immich_lint/analysis_options.yaml
new file mode 100644
index 0000000000..572dd239d0
--- /dev/null
+++ b/mobile/immich_lint/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:lints/recommended.yaml
diff --git a/mobile/immich_lint/lib/immich_mobile_immich_lint.dart b/mobile/immich_lint/lib/immich_mobile_immich_lint.dart
new file mode 100644
index 0000000000..65f3fc18f3
--- /dev/null
+++ b/mobile/immich_lint/lib/immich_mobile_immich_lint.dart
@@ -0,0 +1,86 @@
+import 'package:analyzer/error/listener.dart';
+import 'package:analyzer/error/error.dart' show ErrorSeverity;
+import 'package:custom_lint_builder/custom_lint_builder.dart';
+// ignore: depend_on_referenced_packages
+import 'package:glob/glob.dart';
+
+PluginBase createPlugin() => ImmichLinter();
+
+class ImmichLinter extends PluginBase {
+  @override
+  List<LintRule> getLintRules(CustomLintConfigs configs) {
+    final List<LintRule> rules = [];
+    for (final entry in configs.rules.entries) {
+      if (entry.value.enabled && entry.key.startsWith("import_rule_")) {
+        final code = makeCode(entry.key, entry.value);
+        final allowedPaths = getStrings(entry.value, "allowed");
+        final forbiddenPaths = getStrings(entry.value, "forbidden");
+        final restrict = getStrings(entry.value, "restrict");
+        rules.add(ImportRule(code, buildGlob(allowedPaths),
+            buildGlob(forbiddenPaths), restrict));
+      }
+    }
+    return rules;
+  }
+
+  static makeCode(String name, LintOptions options) => LintCode(
+        name: name,
+        problemMessage: options.json["message"] as String,
+        errorSeverity: ErrorSeverity.WARNING,
+      );
+
+  static List<String> getStrings(LintOptions options, String field) {
+    final List<String> result = [];
+    final excludeOption = options.json[field];
+    if (excludeOption is String) {
+      result.add(excludeOption);
+    } else if (excludeOption is List) {
+      result.addAll(excludeOption.map((option) => option));
+    }
+    return result;
+  }
+
+  Glob? buildGlob(List<String> globs) {
+    if (globs.isEmpty) return null;
+    if (globs.length == 1) return Glob(globs[0], caseSensitive: true);
+    return Glob("{${globs.join(",")}}", caseSensitive: true);
+  }
+}
+
+// ignore: must_be_immutable
+class ImportRule extends DartLintRule {
+  ImportRule(LintCode code, this._allowed, this._forbidden, this._restrict)
+      : super(code: code);
+
+  final Glob? _allowed;
+  final Glob? _forbidden;
+  final List<String> _restrict;
+  int _rootOffset = -1;
+
+  @override
+  void run(
+    CustomLintResolver resolver,
+    ErrorReporter reporter,
+    CustomLintContext context,
+  ) {
+    if (_rootOffset == -1) {
+      const project = "/immich/mobile/";
+      _rootOffset = resolver.path.indexOf(project) + project.length;
+    }
+    final path = resolver.path.substring(_rootOffset);
+
+    if ((_allowed != null && _allowed!.matches(path)) &&
+        (_forbidden == null || !_forbidden!.matches(path))) return;
+
+    context.registry.addImportDirective((node) {
+      final uri = node.uri.stringValue;
+      if (uri == null) return;
+      for (final restricted in _restrict) {
+        if (uri.startsWith(restricted) == true) {
+          reporter.atNode(node, code);
+          return;
+        }
+      }
+    });
+  }
+}
diff --git a/mobile/immich_lint/pubspec.lock b/mobile/immich_lint/pubspec.lock
new file mode 100644
index 0000000000..e81bad7da2
--- /dev/null
+++ b/mobile/immich_lint/pubspec.lock
@@ -0,0 +1,370 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  _fe_analyzer_shared:
+    dependency: transitive
+    description:
+      name: _fe_analyzer_shared
+      sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
+      url: "https://pub.dev"
+    source: hosted
+    version: "73.0.0"
+  _macros:
+    dependency: transitive
+    description: dart
+    source: sdk
+    version: "0.3.2"
+  analyzer:
+    dependency: "direct main"
+    description:
+      name: analyzer
+      sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.8.0"
+  analyzer_plugin:
+    dependency: "direct main"
+    description:
+      name: analyzer_plugin
+      sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.11.3"
+  args:
+    dependency: transitive
+    description:
+      name: args
+      sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.5.0"
+  async:
+    dependency: transitive
+    description:
+      name: async
+      sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.11.0"
+  boolean_selector:
+    dependency: transitive
+    description:
+      name: boolean_selector
+      sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.1"
+  checked_yaml:
+    dependency: transitive
+    description:
+      name: checked_yaml
+      sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.3"
+  ci:
+    dependency: transitive
+    description:
+      name: ci
+      sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.1.0"
+  cli_util:
+    dependency: transitive
+    description:
+      name: cli_util
+      sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.4.1"
+  collection:
+    dependency: transitive
+    description:
+      name: collection
+      sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.19.0"
+  convert:
+    dependency: transitive
+    description:
+      name: convert
+      sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.1"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.5"
+  custom_lint:
+    dependency: transitive
+    description:
+      name: custom_lint
+      sha256: "6e1ec47427ca968f22bce734d00028ae7084361999b41673291138945c5baca0"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.6.7"
+  custom_lint_builder:
+    dependency: "direct main"
+    description:
+      name: custom_lint_builder
+      sha256: ba2f90fff4eff71d202d097eb14b14f87087eaaef742e956208c0eb9d3a40a21
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.6.7"
+  custom_lint_core:
+    dependency: transitive
+    description:
+      name: custom_lint_core
+      sha256: "4ddbbdaa774265de44c97054dcec058a83d9081d071785ece601e348c18c267d"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.6.5"
+  dart_style:
+    dependency: transitive
+    description:
+      name: dart_style
+      sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.7"
+  file:
+    dependency: transitive
+    description:
+      name: file
+      sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.0"
+  fixnum:
+    dependency: transitive
+    description:
+      name: fixnum
+      sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.0"
+  freezed_annotation:
+    dependency: transitive
+    description:
+      name: freezed_annotation
+      sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.4"
+  glob:
+    dependency: "direct main"
+    description:
+      name: glob
+      sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
+  hotreloader:
+    dependency: transitive
+    description:
+      name: hotreloader
+      sha256: ed56fdc1f3a8ac924e717257621d09e9ec20e308ab6352a73a50a1d7a4d9158e
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.2.0"
+  json_annotation:
+    dependency: transitive
+    description:
+      name: json_annotation
+      sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.9.0"
+  lints:
+    dependency: "direct dev"
+    description:
+      name: lints
+      sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.0.0"
+  logging:
+    dependency: transitive
+    description:
+      name: logging
+      sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.0"
+  macros:
+    dependency: transitive
+    description:
+      name: macros
+      sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.1.2-main.4"
+  matcher:
+    dependency: transitive
+    description:
+      name: matcher
+      sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.12.16+1"
+  meta:
+    dependency: transitive
+    description:
+      name: meta
+      sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.15.0"
+  package_config:
+    dependency: transitive
+    description:
+      name: package_config
+      sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.0"
+  path:
+    dependency: transitive
+    description:
+      name: path
+      sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.9.0"
+  pub_semver:
+    dependency: transitive
+    description:
+      name: pub_semver
+      sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.4"
+  pubspec_parse:
+    dependency: transitive
+    description:
+      name: pubspec_parse
+      sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
+  rxdart:
+    dependency: transitive
+    description:
+      name: rxdart
+      sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.28.0"
+  source_span:
+    dependency: transitive
+    description:
+      name: source_span
+      sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.10.0"
+  sprintf:
+    dependency: transitive
+    description:
+      name: sprintf
+      sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.0"
+  stack_trace:
+    dependency: transitive
+    description:
+      name: stack_trace
+      sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.11.1"
+  stream_channel:
+    dependency: transitive
+    description:
+      name: stream_channel
+      sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
+  stream_transform:
+    dependency: transitive
+    description:
+      name: stream_transform
+      sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.0"
+  string_scanner:
+    dependency: transitive
+    description:
+      name: string_scanner
+      sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
+  term_glyph:
+    dependency: transitive
+    description:
+      name: term_glyph
+      sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.1"
+  test_api:
+    dependency: transitive
+    description:
+      name: test_api
+      sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.7.3"
+  typed_data:
+    dependency: transitive
+    description:
+      name: typed_data
+      sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.2"
+  uuid:
+    dependency: transitive
+    description:
+      name: uuid
+      sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.5.0"
+  vm_service:
+    dependency: transitive
+    description:
+      name: vm_service
+      sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
+      url: "https://pub.dev"
+    source: hosted
+    version: "14.2.5"
+  watcher:
+    dependency: transitive
+    description:
+      name: watcher
+      sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.0"
+  yaml:
+    dependency: transitive
+    description:
+      name: yaml
+      sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.2"
+sdks:
+  dart: ">=3.5.0 <4.0.0"
diff --git a/mobile/immich_lint/pubspec.yaml b/mobile/immich_lint/pubspec.yaml
new file mode 100644
index 0000000000..5d871b03e6
--- /dev/null
+++ b/mobile/immich_lint/pubspec.yaml
@@ -0,0 +1,14 @@
+name: immich_mobile_immich_lint
+publish_to: none
+
+environment:
+  sdk: '>=3.0.0 <4.0.0'
+
+dependencies:
+  analyzer: ^7.0.0
+  analyzer_plugin: ^0.11.3
+  custom_lint_builder: ^0.6.4
+  glob: ^2.1.2
+
+dev_dependencies:
+  lints: ^5.0.0
diff --git a/mobile/ios/Gemfile.lock b/mobile/ios/Gemfile.lock
index b41cba39e6..218b8c1355 100644
--- a/mobile/ios/Gemfile.lock
+++ b/mobile/ios/Gemfile.lock
@@ -169,8 +169,8 @@ GEM
       trailblazer-option (>= 0.1.1, < 0.2.0)
       uber (< 0.2.0)
     retriable (3.1.2)
-    rexml (3.2.8)
-      strscan (>= 3.0.9)
+    rexml (3.3.6)
+      strscan
     rouge (2.0.7)
     ruby2_keywords (0.0.5)
     rubyzip (2.3.2)
@@ -195,13 +195,13 @@ GEM
     uber (0.1.0)
     unicode-display_width (1.8.0)
     word_wrap (1.0.0)
-    xcodeproj (1.24.0)
+    xcodeproj (1.25.0)
       CFPropertyList (>= 2.3.3, < 4.0)
       atomos (~> 0.1.3)
       claide (>= 1.0.2, < 2.0)
       colored2 (~> 3.1)
       nanaimo (~> 0.3.0)
-      rexml (~> 3.2.4)
+      rexml (>= 3.3.2, < 4.0)
     xcpretty (0.3.0)
       rouge (~> 2.0.7)
     xcpretty-travis-formatter (1.0.1)
diff --git a/mobile/ios/Podfile b/mobile/ios/Podfile
index f38ac9619b..b048c0bb0c 100644
--- a/mobile/ios/Podfile
+++ b/mobile/ios/Podfile
@@ -102,6 +102,13 @@ post_install do |installer|
 
         ## dart: PermissionGroup.criticalAlerts
         # 'PERMISSION_CRITICAL_ALERTS=1'
+
+        ## The 'PERMISSION_LOCATION' macro enables the `locationWhenInUse` and `locationAlways` permission. If
+        ## the application only requires `locationWhenInUse`, only specify the `PERMISSION_LOCATION_WHENINUSE`
+        ## macro.
+        ##
+        ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
+        'PERMISSION_LOCATION=1',
       ]
 
     end
diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock
index 3b361c4e19..bc65bd4b7f 100644
--- a/mobile/ios/Podfile.lock
+++ b/mobile/ios/Podfile.lock
@@ -1,7 +1,9 @@
 PODS:
+  - background_downloader (0.0.1):
+    - Flutter
   - connectivity_plus (0.0.1):
     - Flutter
-    - ReachabilitySwift
+    - FlutterMacOS
   - device_info_plus (0.0.1):
     - Flutter
   - DKImagePickerController/Core (4.3.9):
@@ -46,7 +48,7 @@ PODS:
   - flutter_udid (0.0.1):
     - Flutter
     - SAMKeychain
-  - flutter_web_auth (0.5.0):
+  - flutter_web_auth (0.6.0):
     - Flutter
   - fluttertoast (0.0.2):
     - Flutter
@@ -63,6 +65,10 @@ PODS:
   - maplibre_gl (0.0.1):
     - Flutter
     - MapLibre (= 5.14.0-pre3)
+  - native_video_player (1.0.0):
+    - Flutter
+  - network_info_plus (0.0.1):
+    - Flutter
   - package_info_plus (0.4.5):
     - Flutter
   - path_provider_foundation (0.0.1):
@@ -75,7 +81,6 @@ PODS:
   - photo_manager (2.0.0):
     - Flutter
     - FlutterMacOS
-  - ReachabilitySwift (5.0.0)
   - SAMKeychain (1.5.3)
   - SDWebImage (5.19.4):
     - SDWebImage/Core (= 5.19.4)
@@ -92,14 +97,12 @@ PODS:
   - Toast (4.0.0)
   - url_launcher_ios (0.0.1):
     - Flutter
-  - video_player_avfoundation (0.0.1):
-    - Flutter
-    - FlutterMacOS
   - wakelock_plus (0.0.1):
     - Flutter
 
 DEPENDENCIES:
-  - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
+  - background_downloader (from `.symlinks/plugins/background_downloader/ios`)
+  - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
   - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
   - file_picker (from `.symlinks/plugins/file_picker/ios`)
   - Flutter (from `Flutter`)
@@ -113,6 +116,8 @@ DEPENDENCIES:
   - integration_test (from `.symlinks/plugins/integration_test/ios`)
   - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
   - maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`)
+  - native_video_player (from `.symlinks/plugins/native_video_player/ios`)
+  - network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
   - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
   - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
@@ -122,7 +127,6 @@ DEPENDENCIES:
   - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
   - sqflite (from `.symlinks/plugins/sqflite/darwin`)
   - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
-  - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
   - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
 
 SPEC REPOS:
@@ -130,15 +134,16 @@ SPEC REPOS:
     - DKImagePickerController
     - DKPhotoGallery
     - MapLibre
-    - ReachabilitySwift
     - SAMKeychain
     - SDWebImage
     - SwiftyGif
     - Toast
 
 EXTERNAL SOURCES:
+  background_downloader:
+    :path: ".symlinks/plugins/background_downloader/ios"
   connectivity_plus:
-    :path: ".symlinks/plugins/connectivity_plus/ios"
+    :path: ".symlinks/plugins/connectivity_plus/darwin"
   device_info_plus:
     :path: ".symlinks/plugins/device_info_plus/ios"
   file_picker:
@@ -165,6 +170,10 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/isar_flutter_libs/ios"
   maplibre_gl:
     :path: ".symlinks/plugins/maplibre_gl/ios"
+  native_video_player:
+    :path: ".symlinks/plugins/native_video_player/ios"
+  network_info_plus:
+    :path: ".symlinks/plugins/network_info_plus/ios"
   package_info_plus:
     :path: ".symlinks/plugins/package_info_plus/ios"
   path_provider_foundation:
@@ -183,14 +192,13 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/sqflite/darwin"
   url_launcher_ios:
     :path: ".symlinks/plugins/url_launcher_ios/ios"
-  video_player_avfoundation:
-    :path: ".symlinks/plugins/video_player_avfoundation/darwin"
   wakelock_plus:
     :path: ".symlinks/plugins/wakelock_plus/ios"
 
 SPEC CHECKSUMS:
-  connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
-  device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
+  background_downloader: 9f788ffc5de45acf87d6380e91ca0841066c18cf
+  connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
+  device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
   DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
   DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
   file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
@@ -198,31 +206,31 @@ SPEC CHECKSUMS:
   flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
   flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
   flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
-  flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d
+  flutter_web_auth: acc15a8fd7bba796a933c724a6dffc3d00f07c27
   fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
   geolocator_apple: 6cbaf322953988e009e5ecb481f07efece75c450
   image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
   integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
-  isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
+  isar_flutter_libs: fdf730ca925d05687f36d7f1d355e482529ed097
   MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef
   maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9
-  package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
+  native_video_player: d12af78a1a4a8cf09775a5177d5b392def6fd23c
+  network_info_plus: 6613d9d7cdeb0e6f366ed4dbe4b3c51c52d567a9
+  package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
   path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
   path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
   permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
   photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
-  ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
   SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
   SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d
-  share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
+  share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
   shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
   sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
   SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
   Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
   url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
-  video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
   wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
 
-PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
+PODFILE CHECKSUM: 2282844f7aed70427ae663932332dad1225156c8
 
 COCOAPODS: 1.15.2
diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj
index 2d7cdc153c..4f4297fe53 100644
--- a/mobile/ios/Runner.xcodeproj/project.pbxproj
+++ b/mobile/ios/Runner.xcodeproj/project.pbxproj
@@ -51,6 +51,7 @@
 		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
 		F7101BB0391A314774615E89 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
+		FA9973382CF6DF4B000EF859 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
 		FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
@@ -126,6 +127,7 @@
 		97C146F01CF9000F007C117D /* Runner */ = {
 			isa = PBXGroup;
 			children = (
+				FA9973382CF6DF4B000EF859 /* Runner.entitlements */,
 				65DD438629917FAD0047FFA8 /* BackgroundSync */,
 				FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */,
 				97C146FA1CF9000F007C117D /* Main.storyboard */,
@@ -401,7 +403,7 @@
 				CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 175;
+				CURRENT_PROJECT_VERSION = 186;
 				DEVELOPMENT_TEAM = 2F67MQ8R79;
 				ENABLE_BITCODE = NO;
 				INFOPLIST_FILE = Runner/Info.plist;
@@ -410,7 +412,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 1.107.0;
+				MARKETING_VERSION = 1.121.0;
 				PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile;
 				PRODUCT_NAME = "Immich-Profile";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -541,9 +543,10 @@
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CLANG_ENABLE_MODULES = YES;
+				CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 175;
+				CURRENT_PROJECT_VERSION = 186;
 				DEVELOPMENT_TEAM = 2F67MQ8R79;
 				ENABLE_BITCODE = NO;
 				INFOPLIST_FILE = Runner/Info.plist;
@@ -552,8 +555,8 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 1.107.0;
-				PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.debug;
+				MARKETING_VERSION = 1.121.0;
+				PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.vdebug;
 				PRODUCT_NAME = "Immich-Debug";
 				PROVISIONING_PROFILE_SPECIFIER = "";
 				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -569,9 +572,10 @@
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CLANG_ENABLE_MODULES = YES;
+				CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 175;
+				CURRENT_PROJECT_VERSION = 186;
 				DEVELOPMENT_TEAM = 2F67MQ8R79;
 				ENABLE_BITCODE = NO;
 				INFOPLIST_FILE = Runner/Info.plist;
@@ -580,7 +584,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 1.107.0;
+				MARKETING_VERSION = 1.121.0;
 				PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
 				PRODUCT_NAME = Immich;
 				PROVISIONING_PROFILE_SPECIFIER = "";
diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift
index 05cb061ca5..8f635bc61b 100644
--- a/mobile/ios/Runner/AppDelegate.swift
+++ b/mobile/ios/Runner/AppDelegate.swift
@@ -1,48 +1,57 @@
-import UIKit
-import shared_preferences_foundation
-import Flutter
 import BackgroundTasks
+import Flutter
+import network_info_plus
 import path_provider_ios
-import photo_manager
 import permission_handler_apple
+import photo_manager
+import shared_preferences_foundation
+import UIKit
 
 @main
 @objc class AppDelegate: FlutterAppDelegate {
-    
   override func application(
     _ application: UIApplication,
     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
   ) -> Bool {
+    // Required for flutter_local_notification
+    if #available(iOS 10.0, *) {
+      UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
+    }
 
-      // Required for flutter_local_notification
-      if #available(iOS 10.0, *) {
-        UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
+    do {
+      try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
+      try AVAudioSession.sharedInstance().setActive(true)
+    } catch {
+      print("Failed to set audio session category. Error: \(error)")
+    }
+
+    GeneratedPluginRegistrant.register(with: self)
+    BackgroundServicePlugin.registerBackgroundProcessing()
+
+    BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!)
+
+    BackgroundServicePlugin.setPluginRegistrantCallback { registry in
+      if !registry.hasPlugin("org.cocoapods.path-provider-ios") {
+        FLTPathProviderPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.path-provider-ios")!)
       }
 
-      GeneratedPluginRegistrant.register(with: self)
-      BackgroundServicePlugin.registerBackgroundProcessing()
-
-      BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!)
-            
-      BackgroundServicePlugin.setPluginRegistrantCallback { registry in
-          if !registry.hasPlugin("org.cocoapods.path-provider-ios") {
-              FLTPathProviderPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.path-provider-ios")!)
-          }
-          
-          if !registry.hasPlugin("org.cocoapods.photo-manager") {
-              PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!)
-          }
-          
-          if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") {
-              SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.shared-preferences-foundation")!)
-          }
-
-          if !registry.hasPlugin("org.cocoapods.permission-handler-apple") {
-              PermissionHandlerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.permission-handler-apple")!)
-          }
+      if !registry.hasPlugin("org.cocoapods.photo-manager") {
+        PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!)
       }
-      
-      return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+
+      if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") {
+        SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.shared-preferences-foundation")!)
+      }
+
+      if !registry.hasPlugin("org.cocoapods.permission-handler-apple") {
+        PermissionHandlerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.permission-handler-apple")!)
+      }
+
+      if !registry.hasPlugin("org.cocoapods.network-info-plus") {
+        FPPNetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.network-info-plus")!)
+      }
+    }
+
+    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
   }
-    
 }
diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift b/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift
index e391f5187e..88d9368308 100644
--- a/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift
+++ b/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift
@@ -86,7 +86,7 @@ class BackgroundSyncWorker {
             result(false)
             break
         default:
-            result(FlutterError())
+            result(FlutterError()) 
             self.complete(UIBackgroundFetchResult.failed)
         }
     }
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
index 1831798a42..ea7cbfc1a0 100644
--- a/mobile/ios/Runner/Info.plist
+++ b/mobile/ios/Runner/Info.plist
@@ -58,11 +58,11 @@
 	<key>CFBundlePackageType</key>
 	<string>APPL</string>
 	<key>CFBundleShortVersionString</key>
-	<string>1.115.0</string>
+	<string>1.123.0</string>
 	<key>CFBundleSignature</key>
 	<string>????</string>
 	<key>CFBundleVersion</key>
-	<string>175</string>
+	<string>186</string>
 	<key>FLTEnableImpeller</key>
 	<true/>
 	<key>ITSAppUsesNonExemptEncryption</key>
@@ -82,8 +82,12 @@
 	</dict>
 	<key>NSCameraUsageDescription</key>
 	<string>We need to access the camera to let you take beautiful video using this app</string>
+	<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
+	<string>We require this permission to access the local WiFi name for background upload mechanism</string>
+	<key>NSLocationUsageDescription</key>
+	<string>We require this permission to access the local WiFi name</string>
 	<key>NSLocationWhenInUseUsageDescription</key>
-	<string>Enable location setting to show position of assets on map</string>
+	<string>We require this permission to access the local WiFi name</string>
 	<key>NSMicrophoneUsageDescription</key>
 	<string>We need to access the microphone to let you take beautiful video using this app</string>
 	<key>NSPhotoLibraryAddUsageDescription</key>
diff --git a/mobile/ios/Runner/Runner.entitlements b/mobile/ios/Runner/Runner.entitlements
index 0c67376eba..ba21fbdaf2 100644
--- a/mobile/ios/Runner/Runner.entitlements
+++ b/mobile/ios/Runner/Runner.entitlements
@@ -1,5 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
-<dict/>
+<dict>
+	<key>com.apple.developer.networking.wifi-info</key>
+	<true/>
+</dict>
 </plist>
diff --git a/mobile/ios/Runner/RunnerProfile.entitlements b/mobile/ios/Runner/RunnerProfile.entitlements
index 903def2af5..75e36a143e 100644
--- a/mobile/ios/Runner/RunnerProfile.entitlements
+++ b/mobile/ios/Runner/RunnerProfile.entitlements
@@ -4,5 +4,7 @@
 <dict>
 	<key>aps-environment</key>
 	<string>development</string>
+	<key>com.apple.developer.networking.wifi-info</key>
+	<true/>
 </dict>
 </plist>
diff --git a/mobile/ios/build/XCBuildData/a34f3d77f077776687d3b444cba8f1c4.xcbuilddata/manifest.json b/mobile/ios/build/XCBuildData/a34f3d77f077776687d3b444cba8f1c4.xcbuilddata/manifest.json
new file mode 100644
index 0000000000..7391713b6f
--- /dev/null
+++ b/mobile/ios/build/XCBuildData/a34f3d77f077776687d3b444cba8f1c4.xcbuilddata/manifest.json
@@ -0,0 +1 @@
+{"client":{"name":"basic","version":0,"file-system":"device-agnostic","perform-ownership-analysis":"no"},"targets":{"":["<all>"]},"commands":{"<all>":{"tool":"phony","inputs":["<WorkspaceHeaderMapVFSFilesWritten>"],"outputs":["<all>"]},"P0:::Gate WorkspaceHeaderMapVFSFilesWritten":{"tool":"phony","inputs":[],"outputs":["<WorkspaceHeaderMapVFSFilesWritten>"]}}}
\ No newline at end of file
diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile
index 870c9b8e31..b22a983015 100644
--- a/mobile/ios/fastlane/Fastfile
+++ b/mobile/ios/fastlane/Fastfile
@@ -19,7 +19,7 @@ platform :ios do
   desc "iOS Release"
   lane :release do
     increment_version_number(
-      version_number: "1.115.0"
+      version_number: "1.123.0"
     )
     increment_build_number(
       build_number: latest_testflight_build_number + 1,
diff --git a/mobile/lib/constants/colors.dart b/mobile/lib/constants/colors.dart
new file mode 100644
index 0000000000..ade878d6f6
--- /dev/null
+++ b/mobile/lib/constants/colors.dart
@@ -0,0 +1,23 @@
+import 'package:flutter/material.dart';
+
+enum ImmichColorPreset {
+  indigo,
+  deepPurple,
+  pink,
+  red,
+  orange,
+  yellow,
+  lime,
+  green,
+  cyan,
+  slateGray
+}
+
+const ImmichColorPreset defaultColorPreset = ImmichColorPreset.indigo;
+const String defaultColorPresetName = "indigo";
+
+const Color immichBrandColorLight = Color(0xFF4150AF);
+const Color immichBrandColorDark = Color(0xFFACCBFA);
+const Color whiteOpacity75 = Color.fromARGB((0.75 * 255) ~/ 1, 255, 255, 255);
+const Color red400 = Color(0xFFEF5350);
+const Color grey200 = Color(0xFFEEEEEE);
diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart
new file mode 100644
index 0000000000..8b74b1a66f
--- /dev/null
+++ b/mobile/lib/constants/constants.dart
@@ -0,0 +1 @@
+const int noDbId = -9223372036854775808; // from Isar
diff --git a/mobile/lib/constants/enums.dart b/mobile/lib/constants/enums.dart
new file mode 100644
index 0000000000..a9b5107426
--- /dev/null
+++ b/mobile/lib/constants/enums.dart
@@ -0,0 +1,4 @@
+enum SortOrder {
+  asc,
+  desc,
+}
diff --git a/mobile/lib/constants/filters.dart b/mobile/lib/constants/filters.dart
new file mode 100644
index 0000000000..d9fa2920b7
--- /dev/null
+++ b/mobile/lib/constants/filters.dart
@@ -0,0 +1,799 @@
+import 'package:flutter/material.dart';
+
+List<ColorFilter> filters = [
+  //Original
+  const ColorFilter.matrix([
+    1,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Vintage
+  const ColorFilter.matrix([
+    0.8,
+    0.1,
+    0.1,
+    0,
+    20,
+    0.1,
+    0.8,
+    0.1,
+    0,
+    20,
+    0.1,
+    0.1,
+    0.8,
+    0,
+    20,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Mood
+  const ColorFilter.matrix([
+    1.2,
+    0.1,
+    0.1,
+    0,
+    10,
+    0.1,
+    1,
+    0.1,
+    0,
+    10,
+    0.1,
+    0.1,
+    1,
+    0,
+    10,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Crisp
+  const ColorFilter.matrix([
+    1.2,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1.2,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1.2,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Cool
+  const ColorFilter.matrix([
+    0.9,
+    0,
+    0.2,
+    0,
+    0,
+    0,
+    1,
+    0.1,
+    0,
+    0,
+    0.1,
+    0,
+    1.2,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Blush
+  const ColorFilter.matrix([
+    1.1,
+    0.1,
+    0.1,
+    0,
+    10,
+    0.1,
+    1,
+    0.1,
+    0,
+    10,
+    0.1,
+    0.1,
+    1,
+    0,
+    5,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Sunkissed
+  const ColorFilter.matrix([
+    1.3,
+    0,
+    0.1,
+    0,
+    15,
+    0,
+    1.1,
+    0.1,
+    0,
+    10,
+    0,
+    0,
+    0.9,
+    0,
+    5,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Fresh
+  const ColorFilter.matrix([
+    1.2,
+    0,
+    0,
+    0,
+    20,
+    0,
+    1.2,
+    0,
+    0,
+    20,
+    0,
+    0,
+    1.1,
+    0,
+    20,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Classic
+  const ColorFilter.matrix([
+    1.1,
+    0,
+    -0.1,
+    0,
+    10,
+    -0.1,
+    1.1,
+    0.1,
+    0,
+    5,
+    0,
+    -0.1,
+    1.1,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Lomo-ish
+  const ColorFilter.matrix([
+    1.5,
+    0,
+    0.1,
+    0,
+    0,
+    0,
+    1.45,
+    0,
+    0,
+    0,
+    0.1,
+    0,
+    1.3,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Nashville
+  const ColorFilter.matrix([
+    1.2,
+    0.15,
+    -0.15,
+    0,
+    15,
+    0.1,
+    1.1,
+    0.1,
+    0,
+    10,
+    -0.05,
+    0.2,
+    1.25,
+    0,
+    5,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Valencia
+  const ColorFilter.matrix([
+    1.15,
+    0.1,
+    0.1,
+    0,
+    20,
+    0.1,
+    1.1,
+    0,
+    0,
+    10,
+    0.1,
+    0.1,
+    1.2,
+    0,
+    5,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Clarendon
+  const ColorFilter.matrix([
+    1.2,
+    0,
+    0,
+    0,
+    10,
+    0,
+    1.25,
+    0,
+    0,
+    10,
+    0,
+    0,
+    1.3,
+    0,
+    10,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Moon
+  const ColorFilter.matrix([
+    0.33,
+    0.33,
+    0.33,
+    0,
+    0,
+    0.33,
+    0.33,
+    0.33,
+    0,
+    0,
+    0.33,
+    0.33,
+    0.33,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Willow
+  const ColorFilter.matrix([
+    0.5,
+    0.5,
+    0.5,
+    0,
+    20,
+    0.5,
+    0.5,
+    0.5,
+    0,
+    20,
+    0.5,
+    0.5,
+    0.5,
+    0,
+    20,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Kodak
+  const ColorFilter.matrix([
+    1.3,
+    0.1,
+    -0.1,
+    0,
+    10,
+    0,
+    1.25,
+    0.1,
+    0,
+    10,
+    0,
+    -0.1,
+    1.1,
+    0,
+    5,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Frost
+  const ColorFilter.matrix([
+    0.8,
+    0.2,
+    0.1,
+    0,
+    0,
+    0.2,
+    1.1,
+    0.1,
+    0,
+    0,
+    0.1,
+    0.1,
+    1.2,
+    0,
+    10,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Night Vision
+  const ColorFilter.matrix([
+    0.1,
+    0.95,
+    0.2,
+    0,
+    0,
+    0.1,
+    1.5,
+    0.1,
+    0,
+    0,
+    0.2,
+    0.7,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Sunset
+  const ColorFilter.matrix([
+    1.5,
+    0.2,
+    0,
+    0,
+    0,
+    0.1,
+    0.9,
+    0.1,
+    0,
+    0,
+    -0.1,
+    -0.2,
+    1.3,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Noir
+  const ColorFilter.matrix([
+    1.3,
+    -0.3,
+    0.1,
+    0,
+    0,
+    -0.1,
+    1.2,
+    -0.1,
+    0,
+    0,
+    0.1,
+    -0.2,
+    1.3,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Dreamy
+  const ColorFilter.matrix([
+    1.1,
+    0.1,
+    0.1,
+    0,
+    0,
+    0.1,
+    1.1,
+    0.1,
+    0,
+    0,
+    0.1,
+    0.1,
+    1.1,
+    0,
+    15,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Sepia
+  const ColorFilter.matrix([
+    0.393,
+    0.769,
+    0.189,
+    0,
+    0,
+    0.349,
+    0.686,
+    0.168,
+    0,
+    0,
+    0.272,
+    0.534,
+    0.131,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Radium
+  const ColorFilter.matrix([
+    1.438,
+    -0.062,
+    -0.062,
+    0,
+    0,
+    -0.122,
+    1.378,
+    -0.122,
+    0,
+    0,
+    -0.016,
+    -0.016,
+    1.483,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Aqua
+  const ColorFilter.matrix([
+    0.2126,
+    0.7152,
+    0.0722,
+    0,
+    0,
+    0.2126,
+    0.7152,
+    0.0722,
+    0,
+    0,
+    0.7873,
+    0.2848,
+    0.9278,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Purple Haze
+  const ColorFilter.matrix([
+    1.3,
+    0,
+    1.2,
+    0,
+    0,
+    0,
+    1.1,
+    0,
+    0,
+    0,
+    0.2,
+    0,
+    1.3,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Lemonade
+  const ColorFilter.matrix([
+    1.2,
+    0.1,
+    0,
+    0,
+    0,
+    0,
+    1.1,
+    0.2,
+    0,
+    0,
+    0.1,
+    0,
+    0.7,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Caramel
+  const ColorFilter.matrix([
+    1.6,
+    0.2,
+    0,
+    0,
+    0,
+    0.1,
+    1.3,
+    0.1,
+    0,
+    0,
+    0,
+    0.1,
+    0.9,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Peachy
+  const ColorFilter.matrix([
+    1.3,
+    0.5,
+    0,
+    0,
+    0,
+    0.2,
+    1.1,
+    0.3,
+    0,
+    0,
+    0.1,
+    0.1,
+    1.2,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Neon
+  const ColorFilter.matrix([
+    1,
+    0,
+    1,
+    0,
+    0,
+    0,
+    2,
+    0,
+    0,
+    0,
+    0,
+    0,
+    3,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Cold Morning
+  const ColorFilter.matrix([
+    0.9,
+    0.1,
+    0.2,
+    0,
+    0,
+    0,
+    1,
+    0.1,
+    0,
+    0,
+    0.1,
+    0,
+    1.2,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Lush
+  const ColorFilter.matrix([
+    0.9,
+    0.2,
+    0,
+    0,
+    0,
+    0,
+    1.2,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1.1,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Urban Neon
+  const ColorFilter.matrix([
+    1.1,
+    0,
+    0.3,
+    0,
+    0,
+    0,
+    0.9,
+    0.3,
+    0,
+    0,
+    0.3,
+    0.1,
+    1.2,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+  //Monochrome
+  const ColorFilter.matrix([
+    0.6,
+    0.2,
+    0.2,
+    0,
+    0,
+    0.2,
+    0.6,
+    0.2,
+    0,
+    0,
+    0.2,
+    0.2,
+    0.7,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    0,
+  ]),
+];
+
+const List<String> filterNames = [
+  'Original',
+  'Vintage',
+  'Mood',
+  'Crisp',
+  'Cool',
+  'Blush',
+  'Sunkissed',
+  'Fresh',
+  'Classic',
+  'Lomo-ish',
+  'Nashville',
+  'Valencia',
+  'Clarendon',
+  'Moon',
+  'Willow',
+  'Kodak',
+  'Frost',
+  'Night Vision',
+  'Sunset',
+  'Noir',
+  'Dreamy',
+  'Sepia',
+  'Radium',
+  'Aqua',
+  'Purple Haze',
+  'Lemonade',
+  'Caramel',
+  'Peachy',
+  'Neon',
+  'Cold Morning',
+  'Lush',
+  'Urban Neon',
+  'Monochrome',
+];
diff --git a/mobile/lib/constants/locales.dart b/mobile/lib/constants/locales.dart
index d92ae7660e..5c928c124e 100644
--- a/mobile/lib/constants/locales.dart
+++ b/mobile/lib/constants/locales.dart
@@ -48,3 +48,8 @@ const Map<String, Locale> locales = {
 };
 
 const String translationsPath = 'assets/i18n';
+
+const List<Locale> localesNotSupportedByOverpass = [
+  Locale('el', 'GR'),
+  Locale('sr', 'Cyrl'),
+];
diff --git a/mobile/lib/entities/album.entity.dart b/mobile/lib/entities/album.entity.dart
index c05b849dcd..8caff2255f 100644
--- a/mobile/lib/entities/album.entity.dart
+++ b/mobile/lib/entities/album.entity.dart
@@ -1,11 +1,12 @@
 import 'package:flutter/foundation.dart';
+import 'package:immich_mobile/constants/enums.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:immich_mobile/entities/store.entity.dart';
 import 'package:immich_mobile/entities/user.entity.dart';
 import 'package:immich_mobile/utils/datetime_comparison.dart';
 import 'package:isar/isar.dart';
+// ignore: implementation_imports
+import 'package:isar/src/common/isar_links_common.dart';
 import 'package:openapi/api.dart';
-import 'package:photo_manager/photo_manager.dart';
 
 part 'album.entity.g.dart';
 
@@ -23,8 +24,10 @@ class Album {
     this.lastModifiedAssetTimestamp,
     required this.shared,
     required this.activityEnabled,
+    this.sortOrder = SortOrder.desc,
   });
 
+  // fields stored in DB
   Id id = Isar.autoIncrement;
   @Index(unique: false, replace: false, type: IndexType.hash)
   String? remoteId;
@@ -38,11 +41,24 @@ class Album {
   DateTime? lastModifiedAssetTimestamp;
   bool shared;
   bool activityEnabled;
+  @enumerated
+  SortOrder sortOrder;
   final IsarLink<User> owner = IsarLink<User>();
   final IsarLink<Asset> thumbnail = IsarLink<Asset>();
   final IsarLinks<User> sharedUsers = IsarLinks<User>();
   final IsarLinks<Asset> assets = IsarLinks<Asset>();
 
+  // transient fields
+  @ignore
+  bool isAll = false;
+
+  @ignore
+  String? remoteThumbnailAssetId;
+
+  @ignore
+  int remoteAssetCount = 0;
+
+  // getters
   @ignore
   bool get isRemote => remoteId != null;
 
@@ -70,6 +86,21 @@ class Album {
     return name.join(' ');
   }
 
+  @ignore
+  String get eTagKeyAssetCount => "device-album-$localId-asset-count";
+
+  // the following getter are needed because Isar links do not make data
+  // accessible in an object freshly created (not loaded from DB)
+
+  @ignore
+  Iterable<User> get remoteUsers => sharedUsers.isEmpty
+      ? (sharedUsers as IsarLinksCommon<User>).addedObjects
+      : sharedUsers;
+
+  @ignore
+  Iterable<Asset> get remoteAssets =>
+      assets.isEmpty ? (assets as IsarLinksCommon<Asset>).addedObjects : assets;
+
   @override
   bool operator ==(other) {
     if (other is! Album) return false;
@@ -112,19 +143,6 @@ class Album {
       sharedUsers.length.hashCode ^
       assets.length.hashCode;
 
-  static Album local(AssetPathEntity ape) {
-    final Album a = Album(
-      name: ape.name,
-      createdAt: ape.lastModified?.toUtc() ?? DateTime.now().toUtc(),
-      modifiedAt: ape.lastModified?.toUtc() ?? DateTime.now().toUtc(),
-      shared: false,
-      activityEnabled: false,
-    );
-    a.owner.value = Store.get(StoreKey.currentUser);
-    a.localId = ape.id;
-    return a;
-  }
-
   static Future<Album> remote(AlbumResponseDto dto) async {
     final Isar db = Isar.getInstance()!;
     final Album a = Album(
@@ -138,7 +156,13 @@ class Album {
       endDate: dto.endDate,
       activityEnabled: dto.isActivityEnabled,
     );
+    a.remoteAssetCount = dto.assetCount;
     a.owner.value = await db.users.getById(dto.ownerId);
+    if (dto.order != null) {
+      a.sortOrder =
+          dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc;
+    }
+
     if (dto.albumThumbnailAssetId != null) {
       a.thumbnail.value = await db.assets
           .where()
@@ -164,19 +188,12 @@ class Album {
 }
 
 extension AssetsHelper on IsarCollection<Album> {
-  Future<void> store(Album a) async {
+  Future<Album> store(Album a) async {
     await put(a);
     await a.owner.save();
     await a.thumbnail.save();
     await a.sharedUsers.save();
     await a.assets.save();
+    return a;
   }
 }
-
-extension AlbumResponseDtoHelper on AlbumResponseDto {
-  List<Asset> getAssets() => assets.map(Asset.remote).toList();
-}
-
-extension AssetPathEntityHelper on AssetPathEntity {
-  String get eTagKeyAssetCount => "device-album-$id-asset-count";
-}
diff --git a/mobile/lib/entities/album.entity.g.dart b/mobile/lib/entities/album.entity.g.dart
index 11046ec1e0..327dc606ca 100644
Binary files a/mobile/lib/entities/album.entity.g.dart and b/mobile/lib/entities/album.entity.g.dart differ
diff --git a/mobile/lib/entities/android_device_asset.entity.g.dart b/mobile/lib/entities/android_device_asset.entity.g.dart
index 9b1eef0ae5..eaa7658565 100644
Binary files a/mobile/lib/entities/android_device_asset.entity.g.dart and b/mobile/lib/entities/android_device_asset.entity.g.dart differ
diff --git a/mobile/lib/entities/asset.entity.dart b/mobile/lib/entities/asset.entity.dart
index 97e10b3d20..4bec35970a 100644
--- a/mobile/lib/entities/asset.entity.dart
+++ b/mobile/lib/entities/asset.entity.dart
@@ -1,11 +1,11 @@
 import 'dart:convert';
+import 'dart:io';
 
 import 'package:immich_mobile/entities/exif_info.entity.dart';
-import 'package:immich_mobile/entities/store.entity.dart';
 import 'package:immich_mobile/utils/hash.dart';
 import 'package:isar/isar.dart';
 import 'package:openapi/api.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show AssetEntity;
 import 'package:immich_mobile/extensions/string_extensions.dart';
 import 'package:path/path.dart' as p;
 
@@ -42,33 +42,6 @@ class Asset {
         stackId = remote.stack?.id,
         thumbhash = remote.thumbhash;
 
-  Asset.local(AssetEntity local, List<int> hash)
-      : localId = local.id,
-        checksum = base64.encode(hash),
-        durationInSeconds = local.duration,
-        type = AssetType.values[local.typeInt],
-        height = local.height,
-        width = local.width,
-        fileName = local.title!,
-        ownerId = Store.get(StoreKey.currentUser).isarId,
-        fileModifiedAt = local.modifiedDateTime,
-        updatedAt = local.modifiedDateTime,
-        isFavorite = local.isFavorite,
-        isArchived = false,
-        isTrashed = false,
-        isOffline = false,
-        stackCount = 0,
-        fileCreatedAt = local.createDateTime {
-    if (fileCreatedAt.year == 1970) {
-      fileCreatedAt = fileModifiedAt;
-    }
-    if (local.latitude != null) {
-      exifInfo = ExifInfo(lat: local.latitude, long: local.longitude);
-    }
-    _local = local;
-    assert(hash.length == 20, "invalid SHA1 hash");
-  }
-
   Asset({
     this.id = Isar.autoIncrement,
     required this.checksum,
@@ -115,6 +88,29 @@ class Asset {
     return _local;
   }
 
+  set local(AssetEntity? assetEntity) => _local = assetEntity;
+
+  @ignore
+  bool _didUpdateLocal = false;
+
+  @ignore
+  Future<AssetEntity> get localAsync async {
+    final local = this.local;
+    if (local == null) {
+      throw Exception('Asset $fileName has no local data');
+    }
+
+    final updatedLocal =
+        _didUpdateLocal ? local : await local.obtainForNewProperties();
+    if (updatedLocal == null) {
+      throw Exception('Could not fetch local data for $fileName');
+    }
+
+    this.local = updatedLocal;
+    _didUpdateLocal = true;
+    return updatedLocal;
+  }
+
   Id id = Isar.autoIncrement;
 
   /// stores the raw SHA1 bytes as a base64 String
@@ -172,10 +168,21 @@ class Asset {
 
   int stackCount;
 
-  /// Aspect ratio of the asset
+  /// Returns null if the asset has no sync access to the exif info
   @ignore
-  double? get aspectRatio =>
-      width == null || height == null ? 0 : width! / height!;
+  double? get aspectRatio {
+    final orientatedWidth = this.orientatedWidth;
+    final orientatedHeight = this.orientatedHeight;
+
+    if (orientatedWidth != null &&
+        orientatedHeight != null &&
+        orientatedWidth > 0 &&
+        orientatedHeight > 0) {
+      return orientatedWidth.toDouble() / orientatedHeight.toDouble();
+    }
+
+    return null;
+  }
 
   /// `true` if this [Asset] is present on the device
   @ignore
@@ -194,6 +201,12 @@ class Asset {
   @ignore
   bool get isImage => type == AssetType.image;
 
+  @ignore
+  bool get isVideo => type == AssetType.video;
+
+  @ignore
+  bool get isMotionPhoto => livePhotoVideoId != null;
+
   @ignore
   AssetState get storage {
     if (isRemote && isLocal) {
@@ -210,6 +223,54 @@ class Asset {
   @ignore
   Duration get duration => Duration(seconds: durationInSeconds);
 
+  // ignore: invalid_annotation_target
+  @ignore
+  set byteHash(List<int> hash) => checksum = base64.encode(hash);
+
+  /// Returns null if the asset has no sync access to the exif info
+  @ignore
+  @pragma('vm:prefer-inline')
+  bool? get isFlipped {
+    final exifInfo = this.exifInfo;
+    if (exifInfo != null) {
+      return exifInfo.isFlipped;
+    }
+
+    if (_didUpdateLocal && Platform.isAndroid) {
+      final local = this.local;
+      if (local == null) {
+        throw Exception('Asset $fileName has no local data');
+      }
+      return local.orientation == 90 || local.orientation == 270;
+    }
+
+    return null;
+  }
+
+  /// Returns null if the asset has no sync access to the exif info
+  @ignore
+  @pragma('vm:prefer-inline')
+  int? get orientatedHeight {
+    final isFlipped = this.isFlipped;
+    if (isFlipped == null) {
+      return null;
+    }
+
+    return isFlipped ? width : height;
+  }
+
+  /// Returns null if the asset has no sync access to the exif info
+  @ignore
+  @pragma('vm:prefer-inline')
+  int? get orientatedWidth {
+    final isFlipped = this.isFlipped;
+    if (isFlipped == null) {
+      return null;
+    }
+
+    return isFlipped ? height : width;
+  }
+
   @override
   bool operator ==(other) {
     if (other is! Asset) return false;
diff --git a/mobile/lib/entities/asset.entity.g.dart b/mobile/lib/entities/asset.entity.g.dart
index 23bf236046..07eee4825e 100644
Binary files a/mobile/lib/entities/asset.entity.g.dart and b/mobile/lib/entities/asset.entity.g.dart differ
diff --git a/mobile/lib/entities/backup_album.entity.g.dart b/mobile/lib/entities/backup_album.entity.g.dart
index 7fb6c0e03b..23d00e43ca 100644
Binary files a/mobile/lib/entities/backup_album.entity.g.dart and b/mobile/lib/entities/backup_album.entity.g.dart differ
diff --git a/mobile/lib/entities/duplicated_asset.entity.g.dart b/mobile/lib/entities/duplicated_asset.entity.g.dart
index 28faa05b6d..8965d47c97 100644
Binary files a/mobile/lib/entities/duplicated_asset.entity.g.dart and b/mobile/lib/entities/duplicated_asset.entity.g.dart differ
diff --git a/mobile/lib/entities/etag.entity.g.dart b/mobile/lib/entities/etag.entity.g.dart
index 5327f6041a..afabca4aea 100644
Binary files a/mobile/lib/entities/etag.entity.g.dart and b/mobile/lib/entities/etag.entity.g.dart differ
diff --git a/mobile/lib/entities/exif_info.entity.dart b/mobile/lib/entities/exif_info.entity.dart
index 63d06f5d2c..c46f3dddc1 100644
--- a/mobile/lib/entities/exif_info.entity.dart
+++ b/mobile/lib/entities/exif_info.entity.dart
@@ -23,6 +23,7 @@ class ExifInfo {
   String? state;
   String? country;
   String? description;
+  String? orientation;
 
   @ignore
   bool get hasCoordinates =>
@@ -45,6 +46,13 @@ class ExifInfo {
   @ignore
   String get focalLength => mm != null ? mm!.toStringAsFixed(1) : "";
 
+  @ignore
+  bool? _isFlipped;
+
+  @ignore
+  @pragma('vm:prefer-inline')
+  bool get isFlipped => _isFlipped ??= _isOrientationFlipped(orientation);
+
   @ignore
   double? get latitude => lat;
 
@@ -67,7 +75,8 @@ class ExifInfo {
         city = dto.city,
         state = dto.state,
         country = dto.country,
-        description = dto.description;
+        description = dto.description,
+        orientation = dto.orientation;
 
   ExifInfo({
     this.id,
@@ -87,6 +96,7 @@ class ExifInfo {
     this.state,
     this.country,
     this.description,
+    this.orientation,
   });
 
   ExifInfo copyWith({
@@ -107,6 +117,7 @@ class ExifInfo {
     String? state,
     String? country,
     String? description,
+    String? orientation,
   }) =>
       ExifInfo(
         id: id ?? this.id,
@@ -126,6 +137,7 @@ class ExifInfo {
         state: state ?? this.state,
         country: country ?? this.country,
         description: description ?? this.description,
+        orientation: orientation ?? this.orientation,
       );
 
   @override
@@ -147,7 +159,8 @@ class ExifInfo {
         city == other.city &&
         state == other.state &&
         country == other.country &&
-        description == other.description;
+        description == other.description &&
+        orientation == other.orientation;
   }
 
   @override
@@ -169,7 +182,8 @@ class ExifInfo {
       city.hashCode ^
       state.hashCode ^
       country.hashCode ^
-      description.hashCode;
+      description.hashCode ^
+      orientation.hashCode;
 
   @override
   String toString() {
@@ -192,10 +206,21 @@ class ExifInfo {
   state: $state,
   country: $country,
   description: $description,
+  orientation: $orientation
 }""";
   }
 }
 
+bool _isOrientationFlipped(String? orientation) {
+  final value = orientation != null ? int.tryParse(orientation) : null;
+  if (value == null) {
+    return false;
+  }
+  final isRotated90CW = value == 5 || value == 6 || value == 90;
+  final isRotated270CW = value == 7 || value == 8 || value == -90;
+  return isRotated90CW || isRotated270CW;
+}
+
 double? _exposureTimeToSeconds(String? s) {
   if (s == null) {
     return null;
diff --git a/mobile/lib/entities/exif_info.entity.g.dart b/mobile/lib/entities/exif_info.entity.g.dart
index 016f6d7126..0b744e5f20 100644
Binary files a/mobile/lib/entities/exif_info.entity.g.dart and b/mobile/lib/entities/exif_info.entity.g.dart differ
diff --git a/mobile/lib/entities/ios_device_asset.entity.g.dart b/mobile/lib/entities/ios_device_asset.entity.g.dart
index 6ecf9f0b73..ffed338c91 100644
Binary files a/mobile/lib/entities/ios_device_asset.entity.g.dart and b/mobile/lib/entities/ios_device_asset.entity.g.dart differ
diff --git a/mobile/lib/entities/logger_message.entity.g.dart b/mobile/lib/entities/logger_message.entity.g.dart
index 50c7fcf8ed..e292e7173a 100644
Binary files a/mobile/lib/entities/logger_message.entity.g.dart and b/mobile/lib/entities/logger_message.entity.g.dart differ
diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart
index 1dda2b9a12..316859b064 100644
--- a/mobile/lib/entities/store.entity.dart
+++ b/mobile/lib/entities/store.entity.dart
@@ -236,6 +236,12 @@ enum StoreKey<T> {
   colorfulInterface<bool>(130, type: bool),
 
   syncAlbums<bool>(131, type: bool),
+
+  // Auto endpoint switching
+  autoEndpointSwitching<bool>(132, type: bool),
+  preferredWifiName<String>(133, type: String),
+  localEndpoint<String>(134, type: String),
+  externalEndpointList<String>(135, type: String),
   ;
 
   const StoreKey(
diff --git a/mobile/lib/entities/store.entity.g.dart b/mobile/lib/entities/store.entity.g.dart
index eb8fa62f40..7d3210ff85 100644
Binary files a/mobile/lib/entities/store.entity.g.dart and b/mobile/lib/entities/store.entity.g.dart differ
diff --git a/mobile/lib/entities/user.entity.g.dart b/mobile/lib/entities/user.entity.g.dart
index a0ecc4705c..a7aaee44bf 100644
Binary files a/mobile/lib/entities/user.entity.g.dart and b/mobile/lib/entities/user.entity.g.dart differ
diff --git a/mobile/lib/extensions/build_context_extensions.dart b/mobile/lib/extensions/build_context_extensions.dart
index 141a1ede15..69a9c3b347 100644
--- a/mobile/lib/extensions/build_context_extensions.dart
+++ b/mobile/lib/extensions/build_context_extensions.dart
@@ -4,6 +4,9 @@ extension ContextHelper on BuildContext {
   // Returns the current padding from MediaQuery
   EdgeInsets get padding => MediaQuery.paddingOf(this);
 
+  // Returns the current view insets from MediaQuery
+  EdgeInsets get viewInsets => MediaQuery.viewInsetsOf(this);
+
   // Returns the current width from MediaQuery
   double get width => MediaQuery.sizeOf(this).width;
 
@@ -13,6 +16,15 @@ extension ContextHelper on BuildContext {
   // Returns true if the app is running on a mobile device (!tablets)
   bool get isMobile => width < 550;
 
+  // Returns the current device pixel ratio from MediaQuery
+  double get devicePixelRatio => MediaQuery.devicePixelRatioOf(this);
+
+  // Returns the current orientation from MediaQuery
+  Orientation get orientation => MediaQuery.orientationOf(this);
+
+  // Returns the current platform brightness from MediaQuery
+  Brightness get platformBrightness => MediaQuery.platformBrightnessOf(this);
+
   // Returns the current ThemeData
   ThemeData get themeData => Theme.of(this);
 
@@ -31,6 +43,19 @@ extension ContextHelper on BuildContext {
   // Current ColorScheme used
   ColorScheme get colorScheme => themeData.colorScheme;
 
+  // Navigate by pushing or popping routes from the current context
+  NavigatorState get navigator => Navigator.of(this);
+
+  // Showing material banners from the current context
+  ScaffoldMessengerState get scaffoldMessenger => ScaffoldMessenger.of(this);
+
   // Pop-out from the current context with optional result
   void pop<T>([T? result]) => Navigator.of(this).pop(result);
+
+  // Managing focus within the widget tree from the current context
+  FocusScopeNode get focusScope => FocusScope.of(this);
+
+  // Show SnackBars from the current context
+  void showSnackBar(SnackBar snackBar) =>
+      ScaffoldMessenger.of(this).showSnackBar(snackBar);
 }
diff --git a/mobile/lib/extensions/collection_extensions.dart b/mobile/lib/extensions/collection_extensions.dart
index 769bec472b..d27c9e9500 100644
--- a/mobile/lib/extensions/collection_extensions.dart
+++ b/mobile/lib/extensions/collection_extensions.dart
@@ -70,18 +70,6 @@ extension AssetListExtension on Iterable<Asset> {
     }
     return this;
   }
-
-  /// Filters out offline assets and returns those that are still accessible by the Immich server
-  Iterable<Asset> nonOfflineOnly({
-    void Function()? errorCallback,
-  }) {
-    final bool onlyLive = every((e) => !e.isOffline);
-    if (!onlyLive) {
-      if (errorCallback != null) errorCallback();
-      return where((a) => !a.isOffline);
-    }
-    return this;
-  }
 }
 
 extension SortedByProperty<T> on Iterable<T> {
diff --git a/mobile/lib/extensions/scroll_extensions.dart b/mobile/lib/extensions/scroll_extensions.dart
new file mode 100644
index 0000000000..5bbd73163a
--- /dev/null
+++ b/mobile/lib/extensions/scroll_extensions.dart
@@ -0,0 +1,38 @@
+import 'package:flutter/cupertino.dart';
+
+// https://stackoverflow.com/a/74453792
+class FastScrollPhysics extends ScrollPhysics {
+  const FastScrollPhysics({super.parent});
+
+  @override
+  FastScrollPhysics applyTo(ScrollPhysics? ancestor) {
+    return FastScrollPhysics(parent: buildParent(ancestor));
+  }
+
+  @override
+  SpringDescription get spring => const SpringDescription(
+        mass: 40,
+        stiffness: 100,
+        damping: 1,
+      );
+}
+
+class FastClampingScrollPhysics extends ClampingScrollPhysics {
+  const FastClampingScrollPhysics({super.parent});
+
+  @override
+  FastClampingScrollPhysics applyTo(ScrollPhysics? ancestor) {
+    return FastClampingScrollPhysics(parent: buildParent(ancestor));
+  }
+
+  @override
+  SpringDescription get spring => const SpringDescription(
+        // When swiping between videos on Android, the placeholder of the first opened video
+        // can briefly be seen and cause a flicker effect if the video begins to initialize
+        // before the animation finishes - probably a bug in PhotoViewGallery's animation handling
+        // Making the animation faster is not just stylistic, but also helps to avoid this flicker
+        mass: 80,
+        stiffness: 100,
+        damping: 1,
+      );
+}
diff --git a/mobile/lib/interfaces/activity_api.interface.dart b/mobile/lib/interfaces/activity_api.interface.dart
new file mode 100644
index 0000000000..99aef6f4d4
--- /dev/null
+++ b/mobile/lib/interfaces/activity_api.interface.dart
@@ -0,0 +1,16 @@
+import 'package:immich_mobile/models/activities/activity.model.dart';
+
+abstract interface class IActivityApiRepository {
+  Future<List<Activity>> getAll(
+    String albumId, {
+    String? assetId,
+  });
+  Future<Activity> create(
+    String albumId,
+    ActivityType type, {
+    String? assetId,
+    String? comment,
+  });
+  Future<void> delete(String id);
+  Future<ActivityStats> getStats(String albumId, {String? assetId});
+}
diff --git a/mobile/lib/interfaces/album.interface.dart b/mobile/lib/interfaces/album.interface.dart
new file mode 100644
index 0000000000..bdf11f18de
--- /dev/null
+++ b/mobile/lib/interfaces/album.interface.dart
@@ -0,0 +1,46 @@
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/entities/user.entity.dart';
+import 'package:immich_mobile/interfaces/database.interface.dart';
+import 'package:immich_mobile/models/albums/album_search.model.dart';
+
+abstract interface class IAlbumRepository implements IDatabaseRepository {
+  Future<Album> create(Album album);
+
+  Future<Album?> get(int id);
+
+  Future<Album?> getByName(
+    String name, {
+    bool? shared,
+    bool? remote,
+  });
+
+  Future<List<Album>> getAll({
+    bool? shared,
+    bool? remote,
+    int? ownerId,
+    AlbumSort? sortBy,
+  });
+
+  Future<Album> update(Album album);
+
+  Future<void> delete(int albumId);
+
+  Future<void> deleteAllLocal();
+
+  Future<int> count({bool? local});
+
+  Future<void> addUsers(Album album, List<User> users);
+
+  Future<void> removeUsers(Album album, List<User> users);
+
+  Future<void> addAssets(Album album, List<Asset> assets);
+
+  Future<void> removeAssets(Album album, List<Asset> assets);
+
+  Future<Album> recalculateMetadata(Album album);
+
+  Future<List<Album>> search(String searchTerm, QuickFilterMode filterMode);
+}
+
+enum AlbumSort { remoteId, localId }
diff --git a/mobile/lib/interfaces/album_api.interface.dart b/mobile/lib/interfaces/album_api.interface.dart
new file mode 100644
index 0000000000..b751ccc170
--- /dev/null
+++ b/mobile/lib/interfaces/album_api.interface.dart
@@ -0,0 +1,42 @@
+import 'package:immich_mobile/constants/enums.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
+
+abstract interface class IAlbumApiRepository {
+  Future<Album> get(String id);
+
+  Future<List<Album>> getAll({bool? shared});
+
+  Future<Album> create(
+    String name, {
+    required Iterable<String> assetIds,
+    Iterable<String> sharedUserIds = const [],
+  });
+
+  Future<Album> update(
+    String albumId, {
+    String? name,
+    String? thumbnailAssetId,
+    String? description,
+    bool? activityEnabled,
+    SortOrder? sortOrder,
+  });
+
+  Future<void> delete(String albumId);
+
+  Future<({List<String> added, List<String> duplicates})> addAssets(
+    String albumId,
+    Iterable<String> assetIds,
+  );
+
+  Future<({List<String> removed, List<String> failed})> removeAssets(
+    String albumId,
+    Iterable<String> assetIds,
+  );
+
+  Future<Album> addUsers(
+    String albumId,
+    Iterable<String> userIds,
+  );
+
+  Future<void> removeUser(String albumId, {required String userId});
+}
diff --git a/mobile/lib/interfaces/album_media.interface.dart b/mobile/lib/interfaces/album_media.interface.dart
new file mode 100644
index 0000000000..fd5f3c8af1
--- /dev/null
+++ b/mobile/lib/interfaces/album_media.interface.dart
@@ -0,0 +1,21 @@
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+
+abstract interface class IAlbumMediaRepository {
+  Future<List<Album>> getAll();
+
+  Future<List<String>> getAssetIds(String albumId);
+
+  Future<int> getAssetCount(String albumId);
+
+  Future<List<Asset>> getAssets(
+    String albumId, {
+    int start = 0,
+    int end = 0x7fffffffffffffff,
+    DateTime? modifiedFrom,
+    DateTime? modifiedUntil,
+    bool orderByModificationDate = false,
+  });
+
+  Future<Album> get(String id);
+}
diff --git a/mobile/lib/interfaces/asset.interface.dart b/mobile/lib/interfaces/asset.interface.dart
new file mode 100644
index 0000000000..5aec594eb1
--- /dev/null
+++ b/mobile/lib/interfaces/asset.interface.dart
@@ -0,0 +1,62 @@
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/entities/device_asset.entity.dart';
+import 'package:immich_mobile/interfaces/database.interface.dart';
+
+abstract interface class IAssetRepository implements IDatabaseRepository {
+  Future<Asset?> getByRemoteId(String id);
+
+  Future<Asset?> getByOwnerIdChecksum(int ownerId, String checksum);
+
+  Future<List<Asset>> getAllByRemoteId(
+    Iterable<String> ids, {
+    AssetState? state,
+  });
+
+  Future<List<Asset?>> getAllByOwnerIdChecksum(
+    List<int> ids,
+    List<String> checksums,
+  );
+
+  Future<List<Asset>> getAll({
+    required int ownerId,
+    AssetState? state,
+    AssetSort? sortBy,
+    int? limit,
+  });
+
+  Future<List<Asset>> getAllLocal();
+
+  Future<List<Asset>> getByAlbum(
+    Album album, {
+    Iterable<int> notOwnedBy = const [],
+    int? ownerId,
+    AssetState? state,
+    AssetSort? sortBy,
+  });
+
+  Future<Asset> update(Asset asset);
+
+  Future<List<Asset>> updateAll(List<Asset> assets);
+
+  Future<void> deleteAllByRemoteId(List<String> ids, {AssetState? state});
+
+  Future<void> deleteById(List<int> ids);
+
+  Future<List<Asset>> getMatches({
+    required List<Asset> assets,
+    required int ownerId,
+    AssetState? state,
+    int limit = 100,
+  });
+
+  Future<List<DeviceAsset?>> getDeviceAssetsById(List<Object> ids);
+
+  Future<void> upsertDeviceAssets(List<DeviceAsset> deviceAssets);
+
+  Future<void> upsertDuplicatedAssets(Iterable<String> duplicatedAssets);
+
+  Future<List<String>> getAllDuplicatedAssetIds();
+}
+
+enum AssetSort { checksum, ownerIdChecksum }
diff --git a/mobile/lib/interfaces/asset_api.interface.dart b/mobile/lib/interfaces/asset_api.interface.dart
new file mode 100644
index 0000000000..fe3320c9bb
--- /dev/null
+++ b/mobile/lib/interfaces/asset_api.interface.dart
@@ -0,0 +1,18 @@
+import 'package:immich_mobile/entities/asset.entity.dart';
+
+abstract interface class IAssetApiRepository {
+  // Future<Asset> get(String id);
+
+  // Future<List<Asset>> getAll();
+
+  // Future<Asset> create(Asset asset);
+
+  Future<Asset> update(
+    String id, {
+    String? description,
+  });
+
+  // Future<void> delete(String id);
+
+  Future<List<Asset>> search({List<String> personIds = const []});
+}
diff --git a/mobile/lib/interfaces/asset_media.interface.dart b/mobile/lib/interfaces/asset_media.interface.dart
new file mode 100644
index 0000000000..2606d5c23c
--- /dev/null
+++ b/mobile/lib/interfaces/asset_media.interface.dart
@@ -0,0 +1,10 @@
+import 'package:immich_mobile/entities/asset.entity.dart';
+
+abstract interface class IAssetMediaRepository {
+  Future<List<String>> deleteAll(List<String> ids);
+
+  Future<Asset?> get(String id);
+
+  /// Obtaining the correct original filename of the asset
+  Future<String?> getOriginalFilename(String id);
+}
diff --git a/mobile/lib/interfaces/auth.interface.dart b/mobile/lib/interfaces/auth.interface.dart
new file mode 100644
index 0000000000..57088f4569
--- /dev/null
+++ b/mobile/lib/interfaces/auth.interface.dart
@@ -0,0 +1,11 @@
+import 'package:immich_mobile/interfaces/database.interface.dart';
+import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
+
+abstract interface class IAuthRepository implements IDatabaseRepository {
+  Future<void> clearLocalData();
+  String getAccessToken();
+  bool getEndpointSwitchingFeature();
+  String? getPreferredWifiName();
+  String? getLocalEndpoint();
+  List<AuxilaryEndpoint> getExternalEndpointList();
+}
diff --git a/mobile/lib/interfaces/auth_api.interface.dart b/mobile/lib/interfaces/auth_api.interface.dart
new file mode 100644
index 0000000000..0a4b235ff3
--- /dev/null
+++ b/mobile/lib/interfaces/auth_api.interface.dart
@@ -0,0 +1,9 @@
+import 'package:immich_mobile/models/auth/login_response.model.dart';
+
+abstract interface class IAuthApiRepository {
+  Future<LoginResponse> login(String email, String password);
+
+  Future<void> logout();
+
+  Future<void> changePassword(String newPassword);
+}
diff --git a/mobile/lib/interfaces/backup.interface.dart b/mobile/lib/interfaces/backup.interface.dart
new file mode 100644
index 0000000000..c32199a58f
--- /dev/null
+++ b/mobile/lib/interfaces/backup.interface.dart
@@ -0,0 +1,16 @@
+import 'package:immich_mobile/entities/backup_album.entity.dart';
+import 'package:immich_mobile/interfaces/database.interface.dart';
+
+abstract interface class IBackupRepository implements IDatabaseRepository {
+  Future<List<BackupAlbum>> getAll({BackupAlbumSort? sort});
+
+  Future<List<String>> getIdsBySelection(BackupSelection backup);
+
+  Future<List<BackupAlbum>> getAllBySelection(BackupSelection backup);
+
+  Future<void> updateAll(List<BackupAlbum> backupAlbums);
+
+  Future<void> deleteAll(List<int> ids);
+}
+
+enum BackupAlbumSort { id }
diff --git a/mobile/lib/interfaces/database.interface.dart b/mobile/lib/interfaces/database.interface.dart
new file mode 100644
index 0000000000..5645d15c47
--- /dev/null
+++ b/mobile/lib/interfaces/database.interface.dart
@@ -0,0 +1,3 @@
+abstract interface class IDatabaseRepository {
+  Future<T> transaction<T>(Future<T> Function() callback);
+}
diff --git a/mobile/lib/interfaces/download.interface.dart b/mobile/lib/interfaces/download.interface.dart
new file mode 100644
index 0000000000..dc4f0f57f8
--- /dev/null
+++ b/mobile/lib/interfaces/download.interface.dart
@@ -0,0 +1,14 @@
+import 'package:background_downloader/background_downloader.dart';
+
+abstract interface class IDownloadRepository {
+  void Function(TaskStatusUpdate)? onImageDownloadStatus;
+  void Function(TaskStatusUpdate)? onVideoDownloadStatus;
+  void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus;
+  void Function(TaskProgressUpdate)? onTaskProgress;
+
+  Future<List<TaskRecord>> getLiveVideoTasks();
+  Future<bool> download(DownloadTask task);
+  Future<bool> cancel(String id);
+  Future<void> deleteAllTrackingRecords();
+  Future<void> deleteRecordsWithIds(List<String> id);
+}
diff --git a/mobile/lib/interfaces/etag.interface.dart b/mobile/lib/interfaces/etag.interface.dart
new file mode 100644
index 0000000000..e567235d1b
--- /dev/null
+++ b/mobile/lib/interfaces/etag.interface.dart
@@ -0,0 +1,14 @@
+import 'package:immich_mobile/entities/etag.entity.dart';
+import 'package:immich_mobile/interfaces/database.interface.dart';
+
+abstract interface class IETagRepository implements IDatabaseRepository {
+  Future<ETag?> get(int id);
+
+  Future<ETag?> getById(String id);
+
+  Future<List<String>> getAllIds();
+
+  Future<void> upsertAll(List<ETag> etags);
+
+  Future<void> deleteByIds(List<String> ids);
+}
diff --git a/mobile/lib/interfaces/exif_info.interface.dart b/mobile/lib/interfaces/exif_info.interface.dart
new file mode 100644
index 0000000000..86608c26d0
--- /dev/null
+++ b/mobile/lib/interfaces/exif_info.interface.dart
@@ -0,0 +1,12 @@
+import 'package:immich_mobile/entities/exif_info.entity.dart';
+import 'package:immich_mobile/interfaces/database.interface.dart';
+
+abstract interface class IExifInfoRepository implements IDatabaseRepository {
+  Future<ExifInfo?> get(int id);
+
+  Future<ExifInfo> update(ExifInfo exifInfo);
+
+  Future<List<ExifInfo>> updateAll(List<ExifInfo> exifInfos);
+
+  Future<void> delete(int id);
+}
diff --git a/mobile/lib/interfaces/file_media.interface.dart b/mobile/lib/interfaces/file_media.interface.dart
new file mode 100644
index 0000000000..ea01819dc3
--- /dev/null
+++ b/mobile/lib/interfaces/file_media.interface.dart
@@ -0,0 +1,36 @@
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:immich_mobile/entities/asset.entity.dart';
+
+abstract interface class IFileMediaRepository {
+  Future<Asset?> saveImage(
+    Uint8List data, {
+    required String title,
+    String? relativePath,
+  });
+
+  Future<Asset?> saveImageWithFile(
+    String filePath, {
+    String? title,
+    String? relativePath,
+  });
+
+  Future<Asset?> saveVideo(
+    File file, {
+    required String title,
+    String? relativePath,
+  });
+
+  Future<Asset?> saveLivePhoto({
+    required File image,
+    required File video,
+    required String title,
+  });
+
+  Future<void> clearFileCache();
+
+  Future<void> enableBackgroundAccess();
+
+  Future<void> requestExtendedPermissions();
+}
diff --git a/mobile/lib/interfaces/network.interface.dart b/mobile/lib/interfaces/network.interface.dart
new file mode 100644
index 0000000000..098d67a27b
--- /dev/null
+++ b/mobile/lib/interfaces/network.interface.dart
@@ -0,0 +1,4 @@
+abstract interface class INetworkRepository {
+  Future<String?> getWifiName();
+  Future<String?> getWifiIp();
+}
diff --git a/mobile/lib/interfaces/partner_api.interface.dart b/mobile/lib/interfaces/partner_api.interface.dart
new file mode 100644
index 0000000000..bca1baf66d
--- /dev/null
+++ b/mobile/lib/interfaces/partner_api.interface.dart
@@ -0,0 +1,13 @@
+import 'package:immich_mobile/entities/user.entity.dart';
+
+abstract interface class IPartnerApiRepository {
+  Future<List<User>> getAll(Direction direction);
+  Future<User> create(String id);
+  Future<User> update(String id, {required bool inTimeline});
+  Future<void> delete(String id);
+}
+
+enum Direction {
+  sharedWithMe,
+  sharedByMe,
+}
diff --git a/mobile/lib/interfaces/person_api.interface.dart b/mobile/lib/interfaces/person_api.interface.dart
new file mode 100644
index 0000000000..b2fa28df8c
--- /dev/null
+++ b/mobile/lib/interfaces/person_api.interface.dart
@@ -0,0 +1,22 @@
+abstract interface class IPersonApiRepository {
+  Future<List<Person>> getAll();
+  Future<Person> update(String id, {String? name});
+}
+
+class Person {
+  Person({
+    required this.id,
+    required this.isHidden,
+    required this.name,
+    required this.thumbnailPath,
+    this.birthDate,
+    this.updatedAt,
+  });
+
+  final String id;
+  final DateTime? birthDate;
+  final bool isHidden;
+  final String name;
+  final String thumbnailPath;
+  final DateTime? updatedAt;
+}
diff --git a/mobile/lib/interfaces/user.interface.dart b/mobile/lib/interfaces/user.interface.dart
new file mode 100644
index 0000000000..e6175a7dc9
--- /dev/null
+++ b/mobile/lib/interfaces/user.interface.dart
@@ -0,0 +1,23 @@
+import 'package:immich_mobile/entities/user.entity.dart';
+import 'package:immich_mobile/interfaces/database.interface.dart';
+
+abstract interface class IUserRepository implements IDatabaseRepository {
+  Future<User?> get(String id);
+
+  Future<List<User>> getByIds(List<String> ids);
+
+  Future<List<User>> getAll({bool self = true, UserSort? sortBy});
+
+  /// Returns all users whose assets can be accessed (self+partners)
+  Future<List<User>> getAllAccessible();
+
+  Future<List<User>> upsertAll(List<User> users);
+
+  Future<User> update(User user);
+
+  Future<void> deleteById(List<int> ids);
+
+  Future<User> me();
+}
+
+enum UserSort { id }
diff --git a/mobile/lib/interfaces/user_api.interface.dart b/mobile/lib/interfaces/user_api.interface.dart
new file mode 100644
index 0000000000..67ac3c0883
--- /dev/null
+++ b/mobile/lib/interfaces/user_api.interface.dart
@@ -0,0 +1,11 @@
+import 'dart:typed_data';
+
+import 'package:immich_mobile/entities/user.entity.dart';
+
+abstract interface class IUserApiRepository {
+  Future<List<User>> getAll();
+  Future<({String profileImagePath})> createProfileImage({
+    required String name,
+    required Uint8List data,
+  });
+}
diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart
index dc1df746cb..807212fc65 100644
--- a/mobile/lib/main.dart
+++ b/mobile/lib/main.dart
@@ -1,22 +1,29 @@
 import 'dart:async';
 import 'dart:io';
 
+import 'package:background_downloader/background_downloader.dart';
 import 'package:device_info_plus/device_info_plus.dart';
 import 'package:easy_localization/easy_localization.dart';
+import 'package:intl/date_symbol_data_local.dart';
+import 'package:timezone/data/latest.dart';
+import 'package:isar/isar.dart';
+import 'package:logging/logging.dart';
+import 'package:path_provider/path_provider.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_displaymode/flutter_displaymode.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:timezone/data/latest.dart';
 import 'package:immich_mobile/constants/locales.dart';
-import 'package:immich_mobile/services/background.service.dart';
-import 'package:immich_mobile/entities/backup_album.entity.dart';
-import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
+import 'package:immich_mobile/providers/locale_provider.dart';
+import 'package:immich_mobile/providers/theme.provider.dart';
+import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
+import 'package:immich_mobile/providers/db.provider.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/routing/tab_navigation_observer.dart';
-import 'package:immich_mobile/utils/cache/widgets_binding.dart';
+import 'package:immich_mobile/entities/backup_album.entity.dart';
+import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
 import 'package:immich_mobile/entities/album.entity.dart';
 import 'package:immich_mobile/entities/android_device_asset.entity.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
@@ -26,16 +33,15 @@ import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
 import 'package:immich_mobile/entities/logger_message.entity.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
 import 'package:immich_mobile/entities/user.entity.dart';
-import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
-import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/services/background.service.dart';
 import 'package:immich_mobile/services/immich_logger.service.dart';
 import 'package:immich_mobile/services/local_notification.service.dart';
-import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
-import 'package:immich_mobile/utils/immich_app_theme.dart';
 import 'package:immich_mobile/utils/migration.dart';
-import 'package:isar/isar.dart';
-import 'package:logging/logging.dart';
-import 'package:path_provider/path_provider.dart';
+import 'package:immich_mobile/utils/download.dart';
+import 'package:immich_mobile/utils/cache/widgets_binding.dart';
+import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
+import 'package:immich_mobile/theme/theme_data.dart';
+import 'package:immich_mobile/theme/dynamic_theme.dart';
 
 void main() async {
   ImmichWidgetsBinding();
@@ -54,6 +60,7 @@ void main() async {
 
 Future<void> initApp() async {
   await EasyLocalization.ensureInitialized();
+  await initializeDateFormatting();
 
   if (kReleaseMode && Platform.isAndroid) {
     try {
@@ -64,15 +71,14 @@ Future<void> initApp() async {
     }
   }
 
-  await fetchSystemPalette();
+  await DynamicTheme.fetchSystemPalette();
 
   // Initialize Immich Logger Service
   ImmichLogger();
 
-  var log = Logger("ImmichErrorLogger");
+  final log = Logger("ImmichErrorLogger");
 
   FlutterError.onError = (details) {
-    debugPrint("FlutterError - Catch all: $details");
     FlutterError.presentError(details);
     log.severe(
       'FlutterError - Catch all',
@@ -82,11 +88,29 @@ Future<void> initApp() async {
   };
 
   PlatformDispatcher.instance.onError = (error, stack) {
+    debugPrint("FlutterError - Catch all: $error \n $stack");
     log.severe('PlatformDispatcher - Catch all', error, stack);
     return true;
   };
 
   initializeTimeZones();
+
+  FileDownloader().configureNotification(
+    running: TaskNotification(
+      'downloading_media'.tr(),
+      'file: {filename}',
+    ),
+    complete: TaskNotification(
+      'download_finished'.tr(),
+      'file: {filename}',
+    ),
+    progressBar: true,
+  );
+
+  FileDownloader().trackTasksInGroup(
+    downloadGroupLivePhoto,
+    markDownloadedComplete: false,
+  );
 }
 
 Future<Isar> loadDb() async {
@@ -170,6 +194,12 @@ class ImmichAppState extends ConsumerState<ImmichApp>
     await ref.read(localNotificationService).setup();
   }
 
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+    Intl.defaultLocale = context.locale.toLanguageTag();
+  }
+
   @override
   initState() {
     super.initState();
@@ -188,23 +218,34 @@ class ImmichAppState extends ConsumerState<ImmichApp>
 
   @override
   Widget build(BuildContext context) {
-    var router = ref.watch(appRouterProvider);
-    var immichTheme = ref.watch(immichThemeProvider);
+    final router = ref.watch(appRouterProvider);
+    final immichTheme = ref.watch(immichThemeProvider);
 
-    return MaterialApp(
-      localizationsDelegates: context.localizationDelegates,
-      supportedLocales: context.supportedLocales,
-      locale: context.locale,
-      debugShowCheckedModeBanner: true,
-      home: MaterialApp.router(
-        title: 'Immich',
-        debugShowCheckedModeBanner: false,
-        themeMode: ref.watch(immichThemeModeProvider),
-        darkTheme: getThemeData(colorScheme: immichTheme.dark),
-        theme: getThemeData(colorScheme: immichTheme.light),
-        routeInformationParser: router.defaultRouteParser(),
-        routerDelegate: router.delegate(
-          navigatorObservers: () => [TabNavigationObserver(ref: ref)],
+    return ProviderScope(
+      overrides: [
+        localeProvider.overrideWithValue(context.locale),
+      ],
+      child: MaterialApp(
+        localizationsDelegates: context.localizationDelegates,
+        supportedLocales: context.supportedLocales,
+        locale: context.locale,
+        debugShowCheckedModeBanner: true,
+        home: MaterialApp.router(
+          title: 'Immich',
+          debugShowCheckedModeBanner: false,
+          themeMode: ref.watch(immichThemeModeProvider),
+          darkTheme: getThemeData(
+            colorScheme: immichTheme.dark,
+            locale: context.locale,
+          ),
+          theme: getThemeData(
+            colorScheme: immichTheme.light,
+            locale: context.locale,
+          ),
+          routeInformationParser: router.defaultRouteParser(),
+          routerDelegate: router.delegate(
+            navigatorObservers: () => [TabNavigationObserver(ref: ref)],
+          ),
         ),
       ),
     );
diff --git a/mobile/lib/models/activities/activity.model.dart b/mobile/lib/models/activities/activity.model.dart
index 6adb80dca9..4702753f41 100644
--- a/mobile/lib/models/activities/activity.model.dart
+++ b/mobile/lib/models/activities/activity.model.dart
@@ -1,5 +1,4 @@
 import 'package:immich_mobile/entities/user.entity.dart';
-import 'package:openapi/api.dart';
 
 enum ActivityType { comment, like }
 
@@ -38,16 +37,6 @@ class Activity {
     );
   }
 
-  Activity.fromDto(ActivityResponseDto dto)
-      : id = dto.id,
-        assetId = dto.assetId,
-        comment = dto.comment,
-        createdAt = dto.createdAt,
-        type = dto.type == ReactionType.comment
-            ? ActivityType.comment
-            : ActivityType.like,
-        user = User.fromSimpleUserDto(dto.user);
-
   @override
   String toString() {
     return 'Activity(id: $id, assetId: $assetId, comment: $comment, createdAt: $createdAt, type: $type, user: $user)';
@@ -75,3 +64,9 @@ class Activity {
         user.hashCode;
   }
 }
+
+class ActivityStats {
+  final int comments;
+
+  const ActivityStats({required this.comments});
+}
diff --git a/mobile/lib/models/albums/album_search.model.dart b/mobile/lib/models/albums/album_search.model.dart
new file mode 100644
index 0000000000..ac4eedbff1
--- /dev/null
+++ b/mobile/lib/models/albums/album_search.model.dart
@@ -0,0 +1,5 @@
+enum QuickFilterMode {
+  all,
+  sharedWithMe,
+  myAlbums,
+}
diff --git a/mobile/lib/models/asset_viewer/asset_viewer_page_state.model.dart b/mobile/lib/models/asset_viewer/asset_viewer_page_state.model.dart
deleted file mode 100644
index 0a354781f8..0000000000
--- a/mobile/lib/models/asset_viewer/asset_viewer_page_state.model.dart
+++ /dev/null
@@ -1,55 +0,0 @@
-import 'dart:convert';
-
-enum DownloadAssetStatus { idle, loading, success, error }
-
-class AssetViewerPageState {
-  // enum
-  final DownloadAssetStatus downloadAssetStatus;
-
-  AssetViewerPageState({
-    required this.downloadAssetStatus,
-  });
-
-  AssetViewerPageState copyWith({
-    DownloadAssetStatus? downloadAssetStatus,
-  }) {
-    return AssetViewerPageState(
-      downloadAssetStatus: downloadAssetStatus ?? this.downloadAssetStatus,
-    );
-  }
-
-  Map<String, dynamic> toMap() {
-    final result = <String, dynamic>{};
-
-    result.addAll({'downloadAssetStatus': downloadAssetStatus.index});
-
-    return result;
-  }
-
-  factory AssetViewerPageState.fromMap(Map<String, dynamic> map) {
-    return AssetViewerPageState(
-      downloadAssetStatus:
-          DownloadAssetStatus.values[map['downloadAssetStatus'] ?? 0],
-    );
-  }
-
-  String toJson() => json.encode(toMap());
-
-  factory AssetViewerPageState.fromJson(String source) =>
-      AssetViewerPageState.fromMap(json.decode(source));
-
-  @override
-  String toString() =>
-      'ImageViewerPageState(downloadAssetStatus: $downloadAssetStatus)';
-
-  @override
-  bool operator ==(Object other) {
-    if (identical(this, other)) return true;
-
-    return other is AssetViewerPageState &&
-        other.downloadAssetStatus == downloadAssetStatus;
-  }
-
-  @override
-  int get hashCode => downloadAssetStatus.hashCode;
-}
diff --git a/mobile/lib/models/authentication/authentication_state.model.dart b/mobile/lib/models/auth/auth_state.model.dart
similarity index 74%
rename from mobile/lib/models/authentication/authentication_state.model.dart
rename to mobile/lib/models/auth/auth_state.model.dart
index 9dcd320c81..fb65850f1d 100644
--- a/mobile/lib/models/authentication/authentication_state.model.dart
+++ b/mobile/lib/models/auth/auth_state.model.dart
@@ -1,62 +1,58 @@
-class AuthenticationState {
+class AuthState {
   final String deviceId;
   final String userId;
   final String userEmail;
   final bool isAuthenticated;
   final String name;
   final bool isAdmin;
-  final bool shouldChangePassword;
   final String profileImagePath;
-  AuthenticationState({
+
+  AuthState({
     required this.deviceId,
     required this.userId,
     required this.userEmail,
     required this.isAuthenticated,
     required this.name,
     required this.isAdmin,
-    required this.shouldChangePassword,
     required this.profileImagePath,
   });
 
-  AuthenticationState copyWith({
+  AuthState copyWith({
     String? deviceId,
     String? userId,
     String? userEmail,
     bool? isAuthenticated,
     String? name,
     bool? isAdmin,
-    bool? shouldChangePassword,
     String? profileImagePath,
   }) {
-    return AuthenticationState(
+    return AuthState(
       deviceId: deviceId ?? this.deviceId,
       userId: userId ?? this.userId,
       userEmail: userEmail ?? this.userEmail,
       isAuthenticated: isAuthenticated ?? this.isAuthenticated,
       name: name ?? this.name,
       isAdmin: isAdmin ?? this.isAdmin,
-      shouldChangePassword: shouldChangePassword ?? this.shouldChangePassword,
       profileImagePath: profileImagePath ?? this.profileImagePath,
     );
   }
 
   @override
   String toString() {
-    return 'AuthenticationState(deviceId: $deviceId, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, name: $name, isAdmin: $isAdmin, shouldChangePassword: $shouldChangePassword, profileImagePath: $profileImagePath)';
+    return 'AuthenticationState(deviceId: $deviceId, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, name: $name, isAdmin: $isAdmin, profileImagePath: $profileImagePath)';
   }
 
   @override
   bool operator ==(Object other) {
     if (identical(this, other)) return true;
 
-    return other is AuthenticationState &&
+    return other is AuthState &&
         other.deviceId == deviceId &&
         other.userId == userId &&
         other.userEmail == userEmail &&
         other.isAuthenticated == isAuthenticated &&
         other.name == name &&
         other.isAdmin == isAdmin &&
-        other.shouldChangePassword == shouldChangePassword &&
         other.profileImagePath == profileImagePath;
   }
 
@@ -68,7 +64,6 @@ class AuthenticationState {
         isAuthenticated.hashCode ^
         name.hashCode ^
         isAdmin.hashCode ^
-        shouldChangePassword.hashCode ^
         profileImagePath.hashCode;
   }
 }
diff --git a/mobile/lib/models/auth/auxilary_endpoint.model.dart b/mobile/lib/models/auth/auxilary_endpoint.model.dart
new file mode 100644
index 0000000000..89aba60913
--- /dev/null
+++ b/mobile/lib/models/auth/auxilary_endpoint.model.dart
@@ -0,0 +1,105 @@
+// ignore_for_file: public_member_api_docs, sort_constructors_first
+import 'dart:convert';
+
+class AuxilaryEndpoint {
+  final String url;
+  final AuxCheckStatus status;
+
+  AuxilaryEndpoint({
+    required this.url,
+    required this.status,
+  });
+
+  AuxilaryEndpoint copyWith({
+    String? url,
+    AuxCheckStatus? status,
+  }) {
+    return AuxilaryEndpoint(
+      url: url ?? this.url,
+      status: status ?? this.status,
+    );
+  }
+
+  @override
+  String toString() => 'AuxilaryEndpoint(url: $url, status: $status)';
+
+  @override
+  bool operator ==(covariant AuxilaryEndpoint other) {
+    if (identical(this, other)) return true;
+
+    return other.url == url && other.status == status;
+  }
+
+  @override
+  int get hashCode => url.hashCode ^ status.hashCode;
+
+  Map<String, dynamic> toMap() {
+    return <String, dynamic>{
+      'url': url,
+      'status': status.toMap(),
+    };
+  }
+
+  factory AuxilaryEndpoint.fromMap(Map<String, dynamic> map) {
+    return AuxilaryEndpoint(
+      url: map['url'] as String,
+      status: AuxCheckStatus.fromMap(map['status'] as Map<String, dynamic>),
+    );
+  }
+
+  String toJson() => json.encode(toMap());
+
+  factory AuxilaryEndpoint.fromJson(String source) =>
+      AuxilaryEndpoint.fromMap(json.decode(source) as Map<String, dynamic>);
+}
+
+class AuxCheckStatus {
+  final String name;
+  AuxCheckStatus({
+    required this.name,
+  });
+  const AuxCheckStatus._(this.name);
+
+  static const loading = AuxCheckStatus._('loading');
+  static const valid = AuxCheckStatus._('valid');
+  static const error = AuxCheckStatus._('error');
+  static const unknown = AuxCheckStatus._('unknown');
+
+  @override
+  bool operator ==(covariant AuxCheckStatus other) {
+    if (identical(this, other)) return true;
+
+    return other.name == name;
+  }
+
+  @override
+  int get hashCode => name.hashCode;
+
+  AuxCheckStatus copyWith({
+    String? name,
+  }) {
+    return AuxCheckStatus(
+      name: name ?? this.name,
+    );
+  }
+
+  Map<String, dynamic> toMap() {
+    return <String, dynamic>{
+      'name': name,
+    };
+  }
+
+  factory AuxCheckStatus.fromMap(Map<String, dynamic> map) {
+    return AuxCheckStatus(
+      name: map['name'] as String,
+    );
+  }
+
+  String toJson() => json.encode(toMap());
+
+  factory AuxCheckStatus.fromJson(String source) =>
+      AuxCheckStatus.fromMap(json.decode(source) as Map<String, dynamic>);
+
+  @override
+  String toString() => 'AuxCheckStatus(name: $name)';
+}
diff --git a/mobile/lib/models/auth/login_response.model.dart b/mobile/lib/models/auth/login_response.model.dart
new file mode 100644
index 0000000000..f1398418ca
--- /dev/null
+++ b/mobile/lib/models/auth/login_response.model.dart
@@ -0,0 +1,30 @@
+class LoginResponse {
+  final String accessToken;
+
+  final bool isAdmin;
+
+  final String name;
+
+  final String profileImagePath;
+
+  final bool shouldChangePassword;
+
+  final String userEmail;
+
+  final String userId;
+
+  LoginResponse({
+    required this.accessToken,
+    required this.isAdmin,
+    required this.name,
+    required this.profileImagePath,
+    required this.shouldChangePassword,
+    required this.userEmail,
+    required this.userId,
+  });
+
+  @override
+  String toString() {
+    return 'LoginResponse[accessToken=$accessToken, isAdmin=$isAdmin, name=$name, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, userEmail=$userEmail, userId=$userId]';
+  }
+}
diff --git a/mobile/lib/models/backup/available_album.model.dart b/mobile/lib/models/backup/available_album.model.dart
index 0b428eea0f..59c57582ce 100644
--- a/mobile/lib/models/backup/available_album.model.dart
+++ b/mobile/lib/models/backup/available_album.model.dart
@@ -1,45 +1,47 @@
 import 'dart:typed_data';
 
-import 'package:photo_manager/photo_manager.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
 
 class AvailableAlbum {
-  final AssetPathEntity albumEntity;
+  final Album album;
+  final int assetCount;
   final DateTime? lastBackup;
   AvailableAlbum({
-    required this.albumEntity,
+    required this.album,
+    required this.assetCount,
     this.lastBackup,
   });
 
   AvailableAlbum copyWith({
-    AssetPathEntity? albumEntity,
+    Album? album,
+    int? assetCount,
     DateTime? lastBackup,
     Uint8List? thumbnailData,
   }) {
     return AvailableAlbum(
-      albumEntity: albumEntity ?? this.albumEntity,
+      album: album ?? this.album,
+      assetCount: assetCount ?? this.assetCount,
       lastBackup: lastBackup ?? this.lastBackup,
     );
   }
 
-  String get name => albumEntity.name;
+  String get name => album.name;
 
-  Future<int> get assetCount => albumEntity.assetCountAsync;
+  String get id => album.localId!;
 
-  String get id => albumEntity.id;
-
-  bool get isAll => albumEntity.isAll;
+  bool get isAll => album.isAll;
 
   @override
   String toString() =>
-      'AvailableAlbum(albumEntity: $albumEntity, lastBackup: $lastBackup)';
+      'AvailableAlbum(albumEntity: $album, lastBackup: $lastBackup)';
 
   @override
   bool operator ==(Object other) {
     if (identical(this, other)) return true;
 
-    return other is AvailableAlbum && other.albumEntity == albumEntity;
+    return other is AvailableAlbum && other.album == album;
   }
 
   @override
-  int get hashCode => albumEntity.hashCode;
+  int get hashCode => album.hashCode;
 }
diff --git a/mobile/lib/models/backup/backup_candidate.model.dart b/mobile/lib/models/backup/backup_candidate.model.dart
index 5ef1516745..01c257dc05 100644
--- a/mobile/lib/models/backup/backup_candidate.model.dart
+++ b/mobile/lib/models/backup/backup_candidate.model.dart
@@ -1,9 +1,9 @@
-import 'package:photo_manager/photo_manager.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
 
 class BackupCandidate {
   BackupCandidate({required this.asset, required this.albumNames});
 
-  AssetEntity asset;
+  Asset asset;
   List<String> albumNames;
 
   @override
diff --git a/mobile/lib/models/backup/current_upload_asset.model.dart b/mobile/lib/models/backup/current_upload_asset.model.dart
index 9a761c9e4a..787f117269 100644
--- a/mobile/lib/models/backup/current_upload_asset.model.dart
+++ b/mobile/lib/models/backup/current_upload_asset.model.dart
@@ -18,6 +18,9 @@ class CurrentUploadAsset {
     this.iCloudAsset,
   });
 
+  @pragma('vm:prefer-inline')
+  bool get isIcloudAsset => iCloudAsset != null && iCloudAsset!;
+
   CurrentUploadAsset copyWith({
     String? id,
     DateTime? fileCreatedAt,
diff --git a/mobile/lib/models/backup/error_upload_asset.model.dart b/mobile/lib/models/backup/error_upload_asset.model.dart
index b63592eda8..38f241e748 100644
--- a/mobile/lib/models/backup/error_upload_asset.model.dart
+++ b/mobile/lib/models/backup/error_upload_asset.model.dart
@@ -1,11 +1,11 @@
-import 'package:photo_manager/photo_manager.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
 
 class ErrorUploadAsset {
   final String id;
   final DateTime fileCreatedAt;
   final String fileName;
   final String fileType;
-  final AssetEntity asset;
+  final Asset asset;
   final String errorMessage;
 
   const ErrorUploadAsset({
@@ -22,7 +22,7 @@ class ErrorUploadAsset {
     DateTime? fileCreatedAt,
     String? fileName,
     String? fileType,
-    AssetEntity? asset,
+    Asset? asset,
     String? errorMessage,
   }) {
     return ErrorUploadAsset(
diff --git a/mobile/lib/models/download/download_state.model.dart b/mobile/lib/models/download/download_state.model.dart
new file mode 100644
index 0000000000..edd2fa183e
--- /dev/null
+++ b/mobile/lib/models/download/download_state.model.dart
@@ -0,0 +1,109 @@
+// ignore_for_file: public_member_api_docs, sort_constructors_first
+import 'dart:convert';
+
+import 'package:background_downloader/background_downloader.dart';
+import 'package:collection/collection.dart';
+
+class DownloadInfo {
+  final String fileName;
+  final double progress;
+  // enum
+  final TaskStatus status;
+
+  DownloadInfo({
+    required this.fileName,
+    required this.progress,
+    required this.status,
+  });
+
+  DownloadInfo copyWith({
+    String? fileName,
+    double? progress,
+    TaskStatus? status,
+  }) {
+    return DownloadInfo(
+      fileName: fileName ?? this.fileName,
+      progress: progress ?? this.progress,
+      status: status ?? this.status,
+    );
+  }
+
+  Map<String, dynamic> toMap() {
+    return <String, dynamic>{
+      'fileName': fileName,
+      'progress': progress,
+      'status': status.index,
+    };
+  }
+
+  factory DownloadInfo.fromMap(Map<String, dynamic> map) {
+    return DownloadInfo(
+      fileName: map['fileName'] as String,
+      progress: map['progress'] as double,
+      status: TaskStatus.values[map['status'] as int],
+    );
+  }
+
+  String toJson() => json.encode(toMap());
+
+  factory DownloadInfo.fromJson(String source) =>
+      DownloadInfo.fromMap(json.decode(source) as Map<String, dynamic>);
+
+  @override
+  String toString() =>
+      'DownloadInfo(fileName: $fileName, progress: $progress, status: $status)';
+
+  @override
+  bool operator ==(covariant DownloadInfo other) {
+    if (identical(this, other)) return true;
+
+    return other.fileName == fileName &&
+        other.progress == progress &&
+        other.status == status;
+  }
+
+  @override
+  int get hashCode => fileName.hashCode ^ progress.hashCode ^ status.hashCode;
+}
+
+class DownloadState {
+  // enum
+  final TaskStatus downloadStatus;
+  final Map<String, DownloadInfo> taskProgress;
+  final bool showProgress;
+  DownloadState({
+    required this.downloadStatus,
+    required this.taskProgress,
+    required this.showProgress,
+  });
+
+  DownloadState copyWith({
+    TaskStatus? downloadStatus,
+    Map<String, DownloadInfo>? taskProgress,
+    bool? showProgress,
+  }) {
+    return DownloadState(
+      downloadStatus: downloadStatus ?? this.downloadStatus,
+      taskProgress: taskProgress ?? this.taskProgress,
+      showProgress: showProgress ?? this.showProgress,
+    );
+  }
+
+  @override
+  String toString() =>
+      'DownloadState(downloadStatus: $downloadStatus, taskProgress: $taskProgress, showProgress: $showProgress)';
+
+  @override
+  bool operator ==(covariant DownloadState other) {
+    if (identical(this, other)) return true;
+    final mapEquals = const DeepCollectionEquality().equals;
+
+    return other.downloadStatus == downloadStatus &&
+        mapEquals(other.taskProgress, taskProgress) &&
+        other.showProgress == showProgress;
+  }
+
+  @override
+  int get hashCode =>
+      downloadStatus.hashCode ^ taskProgress.hashCode ^ showProgress.hashCode;
+}
diff --git a/mobile/lib/models/download/livephotos_medatada.model.dart b/mobile/lib/models/download/livephotos_medatada.model.dart
new file mode 100644
index 0000000000..9c0c7ae4e9
--- /dev/null
+++ b/mobile/lib/models/download/livephotos_medatada.model.dart
@@ -0,0 +1,60 @@
+// ignore_for_file: public_member_api_docs, sort_constructors_first
+import 'dart:convert';
+
+enum LivePhotosPart {
+  video,
+  image,
+}
+
+class LivePhotosMetadata {
+  // enum
+  LivePhotosPart part;
+
+  String id;
+  LivePhotosMetadata({
+    required this.part,
+    required this.id,
+  });
+
+  LivePhotosMetadata copyWith({
+    LivePhotosPart? part,
+    String? id,
+  }) {
+    return LivePhotosMetadata(
+      part: part ?? this.part,
+      id: id ?? this.id,
+    );
+  }
+
+  Map<String, dynamic> toMap() {
+    return <String, dynamic>{
+      'part': part.index,
+      'id': id,
+    };
+  }
+
+  factory LivePhotosMetadata.fromMap(Map<String, dynamic> map) {
+    return LivePhotosMetadata(
+      part: LivePhotosPart.values[map['part'] as int],
+      id: map['id'] as String,
+    );
+  }
+
+  String toJson() => json.encode(toMap());
+
+  factory LivePhotosMetadata.fromJson(String source) =>
+      LivePhotosMetadata.fromMap(json.decode(source) as Map<String, dynamic>);
+
+  @override
+  String toString() => 'LivePhotosMetadata(part: $part, id: $id)';
+
+  @override
+  bool operator ==(covariant LivePhotosMetadata other) {
+    if (identical(this, other)) return true;
+
+    return other.part == part && other.id == id;
+  }
+
+  @override
+  int get hashCode => part.hashCode ^ id.hashCode;
+}
diff --git a/mobile/lib/models/search/search_filter.model.dart b/mobile/lib/models/search/search_filter.model.dart
index 6a7c612b15..47baf356b7 100644
--- a/mobile/lib/models/search/search_filter.model.dart
+++ b/mobile/lib/models/search/search_filter.model.dart
@@ -2,7 +2,7 @@
 import 'dart:convert';
 
 import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:openapi/api.dart';
+import 'package:immich_mobile/interfaces/person_api.interface.dart';
 
 class SearchLocationFilter {
   String? country;
@@ -235,7 +235,7 @@ class SearchDisplayFilters {
 class SearchFilter {
   String? context;
   String? filename;
-  Set<PersonResponseDto> people;
+  Set<Person> people;
   SearchLocationFilter location;
   SearchCameraFilter camera;
   SearchDateFilter date;
@@ -258,7 +258,7 @@ class SearchFilter {
   SearchFilter copyWith({
     String? context,
     String? filename,
-    Set<PersonResponseDto>? people,
+    Set<Person>? people,
     SearchLocationFilter? location,
     SearchCameraFilter? camera,
     SearchDateFilter? date,
@@ -266,8 +266,8 @@ class SearchFilter {
     AssetType? mediaType,
   }) {
     return SearchFilter(
-      context: context ?? this.context,
-      filename: filename ?? this.filename,
+      context: context,
+      filename: filename,
       people: people ?? this.people,
       location: location ?? this.location,
       camera: camera ?? this.camera,
diff --git a/mobile/lib/models/search/search_result.model.dart b/mobile/lib/models/search/search_result.model.dart
new file mode 100644
index 0000000000..f51353ad61
--- /dev/null
+++ b/mobile/lib/models/search/search_result.model.dart
@@ -0,0 +1,37 @@
+import 'package:collection/collection.dart';
+
+import 'package:immich_mobile/entities/asset.entity.dart';
+
+class SearchResult {
+  final List<Asset> assets;
+  final int? nextPage;
+
+  SearchResult({
+    required this.assets,
+    this.nextPage,
+  });
+
+  SearchResult copyWith({
+    List<Asset>? assets,
+    int? nextPage,
+  }) {
+    return SearchResult(
+      assets: assets ?? this.assets,
+      nextPage: nextPage ?? this.nextPage,
+    );
+  }
+
+  @override
+  String toString() => 'SearchResult(assets: $assets, nextPage: $nextPage)';
+
+  @override
+  bool operator ==(covariant SearchResult other) {
+    if (identical(this, other)) return true;
+    final listEquals = const DeepCollectionEquality().equals;
+
+    return listEquals(other.assets, assets) && other.nextPage == nextPage;
+  }
+
+  @override
+  int get hashCode => assets.hashCode ^ nextPage.hashCode;
+}
diff --git a/mobile/lib/models/server_info/server_config.model.dart b/mobile/lib/models/server_info/server_config.model.dart
index 8936939135..f07ffde522 100644
--- a/mobile/lib/models/server_info/server_config.model.dart
+++ b/mobile/lib/models/server_info/server_config.model.dart
@@ -4,11 +4,15 @@ class ServerConfig {
   final int trashDays;
   final String oauthButtonText;
   final String externalDomain;
+  final String mapDarkStyleUrl;
+  final String mapLightStyleUrl;
 
   const ServerConfig({
     required this.trashDays,
     required this.oauthButtonText,
     required this.externalDomain,
+    required this.mapDarkStyleUrl,
+    required this.mapLightStyleUrl,
   });
 
   ServerConfig copyWith({
@@ -20,6 +24,8 @@ class ServerConfig {
       trashDays: trashDays ?? this.trashDays,
       oauthButtonText: oauthButtonText ?? this.oauthButtonText,
       externalDomain: externalDomain ?? this.externalDomain,
+      mapDarkStyleUrl: mapDarkStyleUrl,
+      mapLightStyleUrl: mapLightStyleUrl,
     );
   }
 
@@ -30,7 +36,9 @@ class ServerConfig {
   ServerConfig.fromDto(ServerConfigDto dto)
       : trashDays = dto.trashDays,
         oauthButtonText = dto.oauthButtonText,
-        externalDomain = dto.externalDomain;
+        externalDomain = dto.externalDomain,
+        mapDarkStyleUrl = dto.mapDarkStyleUrl,
+        mapLightStyleUrl = dto.mapLightStyleUrl;
 
   @override
   bool operator ==(covariant ServerConfig other) {
diff --git a/mobile/lib/pages/common/album_additional_shared_user_selection.page.dart b/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart
similarity index 100%
rename from mobile/lib/pages/common/album_additional_shared_user_selection.page.dart
rename to mobile/lib/pages/album/album_additional_shared_user_selection.page.dart
diff --git a/mobile/lib/pages/common/album_asset_selection.page.dart b/mobile/lib/pages/album/album_asset_selection.page.dart
similarity index 100%
rename from mobile/lib/pages/common/album_asset_selection.page.dart
rename to mobile/lib/pages/album/album_asset_selection.page.dart
diff --git a/mobile/lib/pages/album/album_control_button.dart b/mobile/lib/pages/album/album_control_button.dart
new file mode 100644
index 0000000000..b0ac3fbfa5
--- /dev/null
+++ b/mobile/lib/pages/album/album_control_button.dart
@@ -0,0 +1,53 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/providers/album/current_album.provider.dart';
+import 'package:immich_mobile/providers/auth.provider.dart';
+import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
+
+// ignore: must_be_immutable
+class AlbumControlButton extends ConsumerWidget {
+  void Function() onAddPhotosPressed;
+  void Function() onAddUsersPressed;
+
+  AlbumControlButton({
+    super.key,
+    required this.onAddPhotosPressed,
+    required this.onAddUsersPressed,
+  });
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final userId = ref.watch(authProvider).userId;
+    final isOwner = ref.watch(
+      currentAlbumProvider.select((album) {
+        return album?.ownerId == userId;
+      }),
+    );
+
+    return Padding(
+      padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 16),
+      child: SizedBox(
+        height: 40,
+        child: ListView(
+          scrollDirection: Axis.horizontal,
+          children: [
+            AlbumActionFilledButton(
+              key: const ValueKey('add_photos_button'),
+              iconData: Icons.add_photo_alternate_outlined,
+              onPressed: onAddPhotosPressed,
+              labelText: "share_add_photos".tr(),
+            ),
+            if (isOwner)
+              AlbumActionFilledButton(
+                key: const ValueKey('add_users_button'),
+                iconData: Icons.person_add_alt_rounded,
+                onPressed: onAddUsersPressed,
+                labelText: "album_viewer_page_share_add_users".tr(),
+              ),
+          ],
+        ),
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/pages/album/album_date_range.dart b/mobile/lib/pages/album/album_date_range.dart
new file mode 100644
index 0000000000..5f7ef40d4b
--- /dev/null
+++ b/mobile/lib/pages/album/album_date_range.dart
@@ -0,0 +1,61 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/providers/album/current_album.provider.dart';
+
+class AlbumDateRange extends ConsumerWidget {
+  const AlbumDateRange({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final data = ref.watch(
+      currentAlbumProvider.select((album) {
+        if (album == null || album.assets.isEmpty) {
+          return null;
+        }
+
+        final startDate = album.startDate;
+        final endDate = album.endDate;
+        if (startDate == null || endDate == null) {
+          return null;
+        }
+        return (startDate, endDate, album.shared);
+      }),
+    );
+
+    if (data == null) {
+      return const SizedBox();
+    }
+    final (startDate, endDate, shared) = data;
+
+    return Padding(
+      padding: shared
+          ? const EdgeInsets.only(
+              left: 16.0,
+              bottom: 0.0,
+            )
+          : const EdgeInsets.only(left: 16.0, bottom: 8.0),
+      child: Text(
+        _getDateRangeText(startDate, endDate),
+        style: context.textTheme.labelLarge,
+      ),
+    );
+  }
+
+  @pragma('vm:prefer-inline')
+  String _getDateRangeText(DateTime startDate, DateTime endDate) {
+    if (startDate.day == endDate.day &&
+        startDate.month == endDate.month &&
+        startDate.year == endDate.year) {
+      return DateFormat.yMMMd().format(startDate);
+    }
+
+    final String startDateText = (startDate.year == endDate.year
+            ? DateFormat.MMMd()
+            : DateFormat.yMMMd())
+        .format(startDate);
+    final String endDateText = DateFormat.yMMMd().format(endDate);
+    return "$startDateText - $endDateText";
+  }
+}
diff --git a/mobile/lib/pages/common/album_options.page.dart b/mobile/lib/pages/album/album_options.page.dart
similarity index 89%
rename from mobile/lib/pages/common/album_options.page.dart
rename to mobile/lib/pages/album/album_options.page.dart
index 3cc30af7a9..0e9bfeb2ce 100644
--- a/mobile/lib/pages/common/album_options.page.dart
+++ b/mobile/lib/pages/album/album_options.page.dart
@@ -6,26 +6,29 @@ import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/theme_extensions.dart';
-import 'package:immich_mobile/providers/album/shared_album.provider.dart';
-import 'package:immich_mobile/providers/authentication.provider.dart';
+import 'package:immich_mobile/providers/album/album.provider.dart';
+import 'package:immich_mobile/providers/album/current_album.provider.dart';
+import 'package:immich_mobile/providers/auth.provider.dart';
 import 'package:immich_mobile/utils/immich_loading_overlay.dart';
 import 'package:immich_mobile/routing/router.dart';
-import 'package:immich_mobile/entities/album.entity.dart';
 import 'package:immich_mobile/entities/user.entity.dart';
 import 'package:immich_mobile/widgets/common/immich_toast.dart';
 import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
 
 @RoutePage()
 class AlbumOptionsPage extends HookConsumerWidget {
-  final Album album;
-
-  const AlbumOptionsPage({super.key, required this.album});
+  const AlbumOptionsPage({super.key});
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
+    final album = ref.watch(currentAlbumProvider);
+    if (album == null) {
+      return const SizedBox();
+    }
+
     final sharedUsers = useState(album.sharedUsers.toList());
     final owner = album.owner.value;
-    final userId = ref.watch(authenticationProvider).userId;
+    final userId = ref.watch(authProvider).userId;
     final activityEnabled = useState(album.activityEnabled);
     final isProcessing = useProcessingOverlay();
     final isOwner = owner?.id == userId;
@@ -45,11 +48,11 @@ class AlbumOptionsPage extends HookConsumerWidget {
 
       try {
         final isSuccess =
-            await ref.read(sharedAlbumProvider.notifier).leaveAlbum(album);
+            await ref.read(albumProvider.notifier).leaveAlbum(album);
 
         if (isSuccess) {
           context.navigateTo(
-            const TabControllerRoute(children: [SharingRoute()]),
+            const TabControllerRoute(children: [AlbumsRoute()]),
           );
         } else {
           showErrorMessage();
@@ -65,9 +68,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
       isProcessing.value = true;
 
       try {
-        await ref
-            .read(sharedAlbumProvider.notifier)
-            .removeUserFromAlbum(album, user);
+        await ref.read(albumProvider.notifier).removeUser(album, user);
         album.sharedUsers.remove(user);
         sharedUsers.value = album.sharedUsers.toList();
       } catch (error) {
@@ -200,8 +201,8 @@ class AlbumOptionsPage extends HookConsumerWidget {
               onChanged: (bool value) async {
                 activityEnabled.value = value;
                 if (await ref
-                    .read(sharedAlbumProvider.notifier)
-                    .setActivityEnabled(album, value)) {
+                    .read(albumProvider.notifier)
+                    .setActivitystatus(album, value)) {
                   album.activityEnabled = value;
                 }
               },
diff --git a/mobile/lib/pages/album/album_shared_user_icons.dart b/mobile/lib/pages/album/album_shared_user_icons.dart
new file mode 100644
index 0000000000..4cb9804e25
--- /dev/null
+++ b/mobile/lib/pages/album/album_shared_user_icons.dart
@@ -0,0 +1,56 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/user.entity.dart';
+import 'package:immich_mobile/providers/album/current_album.provider.dart';
+import 'package:immich_mobile/routing/router.dart';
+import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
+
+class AlbumSharedUserIcons extends HookConsumerWidget {
+  const AlbumSharedUserIcons({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final sharedUsers = useRef<List<User>>(const []);
+    sharedUsers.value = ref.watch(
+      currentAlbumProvider.select((album) {
+        if (album == null) {
+          return const [];
+        }
+
+        if (album.sharedUsers.length == sharedUsers.value.length) {
+          return sharedUsers.value;
+        }
+
+        return album.sharedUsers.toList(growable: false);
+      }),
+    );
+
+    if (sharedUsers.value.isEmpty) {
+      return const SizedBox();
+    }
+
+    return GestureDetector(
+      onTap: () => context.pushRoute(AlbumOptionsRoute()),
+      child: SizedBox(
+        height: 50,
+        child: ListView.builder(
+          padding: const EdgeInsets.only(left: 16),
+          scrollDirection: Axis.horizontal,
+          itemBuilder: ((context, index) {
+            return Padding(
+              padding: const EdgeInsets.only(right: 8.0),
+              child: UserCircleAvatar(
+                user: sharedUsers.value[index],
+                radius: 18,
+                size: 36,
+              ),
+            );
+          }),
+          itemCount: sharedUsers.value.length,
+        ),
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/pages/common/album_shared_user_selection.page.dart b/mobile/lib/pages/album/album_shared_user_selection.page.dart
similarity index 90%
rename from mobile/lib/pages/common/album_shared_user_selection.page.dart
rename to mobile/lib/pages/album/album_shared_user_selection.page.dart
index aefa8e2736..ed8a45194d 100644
--- a/mobile/lib/pages/common/album_shared_user_selection.page.dart
+++ b/mobile/lib/pages/album/album_shared_user_selection.page.dart
@@ -5,8 +5,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/providers/album/album.provider.dart';
 import 'package:immich_mobile/providers/album/album_title.provider.dart';
-import 'package:immich_mobile/providers/album/shared_album.provider.dart';
 import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
@@ -25,20 +25,15 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget {
     final suggestedShareUsers = ref.watch(otherUsersProvider);
 
     createSharedAlbum() async {
-      var newAlbum =
-          await ref.watch(sharedAlbumProvider.notifier).createSharedAlbum(
-                ref.watch(albumTitleProvider),
-                assets,
-                sharedUsersList.value,
-              );
+      var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
+            ref.watch(albumTitleProvider),
+            assets,
+          );
 
       if (newAlbum != null) {
-        await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
-        // ref.watch(assetSelectionProvider.notifier).removeAll();
         ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
         context.maybePop(true);
-        context
-            .navigateTo(const TabControllerRoute(children: [SharingRoute()]));
+        context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]));
       }
 
       ScaffoldMessenger(
diff --git a/mobile/lib/pages/album/album_title.dart b/mobile/lib/pages/album/album_title.dart
new file mode 100644
index 0000000000..435e282523
--- /dev/null
+++ b/mobile/lib/pages/album/album_title.dart
@@ -0,0 +1,41 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/providers/album/current_album.provider.dart';
+import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart';
+import 'package:immich_mobile/providers/auth.provider.dart';
+
+class AlbumTitle extends ConsumerWidget {
+  const AlbumTitle({super.key, required this.titleFocusNode});
+
+  final FocusNode titleFocusNode;
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final userId = ref.watch(authProvider).userId;
+    final (isOwner, isRemote, albumName) = ref.watch(
+      currentAlbumProvider.select((album) {
+        if (album == null) {
+          return const (false, false, '');
+        }
+
+        return (album.ownerId == userId, album.isRemote, album.name);
+      }),
+    );
+
+    if (isOwner && isRemote) {
+      return Padding(
+        padding: const EdgeInsets.only(left: 8, right: 8),
+        child: AlbumViewerEditableTitle(
+          albumName: albumName,
+          titleFocusNode: titleFocusNode,
+        ),
+      );
+    }
+
+    return Padding(
+      padding: const EdgeInsets.only(left: 16, right: 8),
+      child: Text(albumName, style: context.textTheme.headlineMedium),
+    );
+  }
+}
diff --git a/mobile/lib/pages/album/album_viewer.dart b/mobile/lib/pages/album/album_viewer.dart
new file mode 100644
index 0000000000..19782c4e30
--- /dev/null
+++ b/mobile/lib/pages/album/album_viewer.dart
@@ -0,0 +1,147 @@
+import 'dart:async';
+
+import 'package:auto_route/auto_route.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:fluttertoast/fluttertoast.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
+import 'package:immich_mobile/pages/album/album_control_button.dart';
+import 'package:immich_mobile/pages/album/album_date_range.dart';
+import 'package:immich_mobile/pages/album/album_shared_user_icons.dart';
+import 'package:immich_mobile/pages/album/album_title.dart';
+import 'package:immich_mobile/providers/album/album.provider.dart';
+import 'package:immich_mobile/providers/album/current_album.provider.dart';
+import 'package:immich_mobile/utils/immich_loading_overlay.dart';
+import 'package:immich_mobile/providers/multiselect.provider.dart';
+import 'package:immich_mobile/providers/auth.provider.dart';
+import 'package:immich_mobile/widgets/album/album_viewer_appbar.dart';
+import 'package:immich_mobile/routing/router.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/providers/asset.provider.dart';
+import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
+import 'package:immich_mobile/widgets/common/immich_toast.dart';
+
+class AlbumViewer extends HookConsumerWidget {
+  const AlbumViewer({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final album = ref.watch(currentAlbumProvider);
+    if (album == null) {
+      return const SizedBox();
+    }
+
+    final titleFocusNode = useFocusNode();
+    final userId = ref.watch(authProvider).userId;
+    final isMultiselecting = ref.watch(multiselectProvider);
+    final isProcessing = useProcessingOverlay();
+
+    Future<bool> onRemoveFromAlbumPressed(Iterable<Asset> assets) async {
+      final bool isSuccess =
+          await ref.read(albumProvider.notifier).removeAsset(album, assets);
+
+      if (!isSuccess) {
+        ImmichToast.show(
+          context: context,
+          msg: "album_viewer_appbar_share_err_remove".tr(),
+          toastType: ToastType.error,
+          gravity: ToastGravity.BOTTOM,
+        );
+      }
+      return isSuccess;
+    }
+
+    /// Find out if the assets in album exist on the device
+    /// If they exist, add to selected asset state to show they are already selected.
+    void onAddPhotosPressed() async {
+      AssetSelectionPageResult? returnPayload =
+          await context.pushRoute<AssetSelectionPageResult?>(
+        AlbumAssetSelectionRoute(
+          existingAssets: album.assets,
+          canDeselect: false,
+          query: getRemoteAssetQuery(ref),
+        ),
+      );
+
+      if (returnPayload != null && returnPayload.selectedAssets.isNotEmpty) {
+        // Check if there is new assets add
+        isProcessing.value = true;
+
+        await ref
+            .watch(albumProvider.notifier)
+            .addAssets(album, returnPayload.selectedAssets);
+
+        isProcessing.value = false;
+      }
+    }
+
+    void onAddUsersPressed() async {
+      List<String>? sharedUserIds = await context.pushRoute<List<String>?>(
+        AlbumAdditionalSharedUserSelectionRoute(album: album),
+      );
+
+      if (sharedUserIds != null) {
+        isProcessing.value = true;
+
+        await ref.watch(albumProvider.notifier).addUsers(album, sharedUserIds);
+
+        isProcessing.value = false;
+      }
+    }
+
+    onActivitiesPressed() {
+      if (album.remoteId != null) {
+        context.pushRoute(
+          const ActivitiesRoute(),
+        );
+      }
+    }
+
+    return Stack(
+      children: [
+        MultiselectGrid(
+          key: const ValueKey("albumViewerMultiselectGrid"),
+          renderListProvider: albumRenderlistProvider(album.id),
+          topWidget: Column(
+            mainAxisAlignment: MainAxisAlignment.end,
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              AlbumTitle(
+                key: const ValueKey("albumTitle"),
+                titleFocusNode: titleFocusNode,
+              ),
+              const AlbumDateRange(),
+              const AlbumSharedUserIcons(),
+              if (album.isRemote)
+                AlbumControlButton(
+                  key: const ValueKey("albumControlButton"),
+                  onAddPhotosPressed: onAddPhotosPressed,
+                  onAddUsersPressed: onAddUsersPressed,
+                ),
+            ],
+          ),
+          onRemoveFromAlbum: onRemoveFromAlbumPressed,
+          editEnabled: album.ownerId == userId,
+        ),
+        AnimatedPositioned(
+          key: const ValueKey("albumViewerAppbarPositioned"),
+          duration: const Duration(milliseconds: 300),
+          top: isMultiselecting ? -(kToolbarHeight + context.padding.top) : 0,
+          left: 0,
+          right: 0,
+          child: AlbumViewerAppbar(
+            key: const ValueKey("albumViewerAppbar"),
+            titleFocusNode: titleFocusNode,
+            userId: userId,
+            onAddPhotos: onAddPhotosPressed,
+            onAddUsers: onAddUsersPressed,
+            onActivities: onActivitiesPressed,
+          ),
+        ),
+      ],
+    );
+  }
+}
diff --git a/mobile/lib/pages/album/album_viewer.page.dart b/mobile/lib/pages/album/album_viewer.page.dart
new file mode 100644
index 0000000000..491bd3bb8d
--- /dev/null
+++ b/mobile/lib/pages/album/album_viewer.page.dart
@@ -0,0 +1,27 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/pages/album/album_viewer.dart';
+import 'package:immich_mobile/providers/album/album.provider.dart';
+import 'package:immich_mobile/providers/album/current_album.provider.dart';
+
+@RoutePage()
+class AlbumViewerPage extends HookConsumerWidget {
+  final int albumId;
+
+  const AlbumViewerPage({super.key, required this.albumId});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    // Listen provider to prevent autoDispose when navigating to other routes from within the viewer page
+    ref.listen(currentAlbumProvider, (_, __) {});
+
+    ref.listen(albumWatcher(albumId), (_, albumFuture) {
+      albumFuture.whenData(
+        (value) => ref.read(currentAlbumProvider.notifier).set(value),
+      );
+    });
+
+    return const Scaffold(body: AlbumViewer());
+  }
+}
diff --git a/mobile/lib/pages/albums/albums.page.dart b/mobile/lib/pages/albums/albums.page.dart
new file mode 100644
index 0000000000..6f7d99b727
--- /dev/null
+++ b/mobile/lib/pages/albums/albums.page.dart
@@ -0,0 +1,469 @@
+import 'dart:async';
+import 'dart:math';
+
+import 'package:auto_route/auto_route.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/extensions/theme_extensions.dart';
+import 'package:immich_mobile/models/albums/album_search.model.dart';
+import 'package:immich_mobile/pages/common/large_leading_tile.dart';
+import 'package:immich_mobile/providers/album/album.provider.dart';
+import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
+import 'package:immich_mobile/providers/user.provider.dart';
+import 'package:immich_mobile/routing/router.dart';
+import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
+import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
+import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
+
+@RoutePage()
+class AlbumsPage extends HookConsumerWidget {
+  const AlbumsPage({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final albums =
+        ref.watch(albumProvider).where((album) => album.isRemote).toList();
+    final albumSortOption = ref.watch(albumSortByOptionsProvider);
+    final albumSortIsReverse = ref.watch(albumSortOrderProvider);
+    final sorted = albumSortOption.sortFn(albums, albumSortIsReverse);
+    final isGrid = useState(false);
+    final searchController = useTextEditingController();
+    final debounceTimer = useRef<Timer?>(null);
+    final filterMode = useState(QuickFilterMode.all);
+    final userId = ref.watch(currentUserProvider)?.id;
+    final searchFocusNode = useFocusNode();
+
+    toggleViewMode() {
+      isGrid.value = !isGrid.value;
+    }
+
+    onSearch(String searchTerm, QuickFilterMode mode) {
+      debounceTimer.value?.cancel();
+      debounceTimer.value = Timer(const Duration(milliseconds: 300), () {
+        ref.read(albumProvider.notifier).searchAlbums(searchTerm, mode);
+      });
+    }
+
+    changeFilter(QuickFilterMode mode) {
+      filterMode.value = mode;
+    }
+
+    useEffect(
+      () {
+        searchController.addListener(() {
+          onSearch(searchController.text, filterMode.value);
+        });
+
+        return () {
+          searchController.removeListener(() {
+            onSearch(searchController.text, filterMode.value);
+          });
+          debounceTimer.value?.cancel();
+        };
+      },
+      [],
+    );
+
+    clearSearch() {
+      filterMode.value = QuickFilterMode.all;
+      searchController.clear();
+      onSearch('', QuickFilterMode.all);
+    }
+
+    return Scaffold(
+      appBar: ImmichAppBar(
+        showUploadButton: false,
+        actions: [
+          IconButton(
+            icon: const Icon(
+              Icons.add_rounded,
+              size: 28,
+            ),
+            onPressed: () => context.pushRoute(
+              CreateAlbumRoute(),
+            ),
+          ),
+        ],
+      ),
+      body: RefreshIndicator(
+        displacement: 70,
+        onRefresh: () async {
+          await ref.read(albumProvider.notifier).refreshRemoteAlbums();
+        },
+        child: ListView(
+          shrinkWrap: true,
+          padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12),
+          children: [
+            Container(
+              decoration: BoxDecoration(
+                border: Border.all(
+                  color: context.colorScheme.onSurface.withAlpha(0),
+                  width: 0,
+                ),
+                borderRadius: BorderRadius.circular(24),
+                gradient: LinearGradient(
+                  colors: [
+                    context.colorScheme.primary.withOpacity(0.075),
+                    context.colorScheme.primary.withOpacity(0.09),
+                    context.colorScheme.primary.withOpacity(0.075),
+                  ],
+                  begin: Alignment.topLeft,
+                  end: Alignment.bottomRight,
+                  transform: const GradientRotation(0.5 * pi),
+                ),
+              ),
+              child: TextField(
+                autofocus: false,
+                decoration: InputDecoration(
+                  contentPadding: const EdgeInsets.all(16),
+                  border: OutlineInputBorder(
+                    borderRadius: BorderRadius.circular(25),
+                    borderSide: BorderSide(
+                      color: context.colorScheme.surfaceDim,
+                    ),
+                  ),
+                  enabledBorder: OutlineInputBorder(
+                    borderRadius: BorderRadius.circular(25),
+                    borderSide: BorderSide(
+                      color: context.colorScheme.surfaceContainer,
+                    ),
+                  ),
+                  disabledBorder: OutlineInputBorder(
+                    borderRadius: BorderRadius.circular(25),
+                    borderSide: BorderSide(
+                      color: context.colorScheme.surfaceDim,
+                    ),
+                  ),
+                  focusedBorder: OutlineInputBorder(
+                    borderRadius: BorderRadius.circular(25),
+                    borderSide: BorderSide(
+                      color: context.colorScheme.primary.withAlpha(100),
+                    ),
+                  ),
+                  hintText: 'search_albums'.tr(),
+                  hintStyle: context.textTheme.bodyLarge?.copyWith(
+                    color: context.colorScheme.onSurfaceSecondary,
+                  ),
+                  prefixIcon: const Icon(Icons.search_rounded),
+                  suffixIcon: searchController.text.isNotEmpty
+                      ? IconButton(
+                          icon: const Icon(Icons.clear_rounded),
+                          onPressed: clearSearch,
+                        )
+                      : const SizedBox.shrink(),
+                ),
+                controller: searchController,
+                onChanged: (_) =>
+                    onSearch(searchController.text, filterMode.value),
+                focusNode: searchFocusNode,
+                onTapOutside: (_) => searchFocusNode.unfocus(),
+              ),
+            ),
+            const SizedBox(height: 8),
+            Wrap(
+              spacing: 4,
+              runSpacing: 4,
+              children: [
+                QuickFilterButton(
+                  label: 'all'.tr(),
+                  isSelected: filterMode.value == QuickFilterMode.all,
+                  onTap: () {
+                    changeFilter(QuickFilterMode.all);
+                    onSearch(searchController.text, QuickFilterMode.all);
+                  },
+                ),
+                QuickFilterButton(
+                  label: 'shared_with_me'.tr(),
+                  isSelected: filterMode.value == QuickFilterMode.sharedWithMe,
+                  onTap: () {
+                    changeFilter(QuickFilterMode.sharedWithMe);
+                    onSearch(
+                      searchController.text,
+                      QuickFilterMode.sharedWithMe,
+                    );
+                  },
+                ),
+                QuickFilterButton(
+                  label: 'my_albums'.tr(),
+                  isSelected: filterMode.value == QuickFilterMode.myAlbums,
+                  onTap: () {
+                    changeFilter(QuickFilterMode.myAlbums);
+                    onSearch(
+                      searchController.text,
+                      QuickFilterMode.myAlbums,
+                    );
+                  },
+                ),
+              ],
+            ),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: [
+                const SortButton(),
+                IconButton(
+                  icon: Icon(
+                    isGrid.value
+                        ? Icons.view_list_outlined
+                        : Icons.grid_view_outlined,
+                    size: 24,
+                  ),
+                  onPressed: toggleViewMode,
+                ),
+              ],
+            ),
+            const SizedBox(height: 5),
+            AnimatedSwitcher(
+              duration: const Duration(milliseconds: 500),
+              child: isGrid.value
+                  ? GridView.builder(
+                      shrinkWrap: true,
+                      physics: const ClampingScrollPhysics(),
+                      gridDelegate:
+                          const SliverGridDelegateWithMaxCrossAxisExtent(
+                        maxCrossAxisExtent: 250,
+                        mainAxisSpacing: 12,
+                        crossAxisSpacing: 12,
+                        childAspectRatio: .7,
+                      ),
+                      itemBuilder: (context, index) {
+                        return AlbumThumbnailCard(
+                          album: sorted[index],
+                          onTap: () => context.pushRoute(
+                            AlbumViewerRoute(albumId: sorted[index].id),
+                          ),
+                          showOwner: true,
+                        );
+                      },
+                      itemCount: sorted.length,
+                    )
+                  : ListView.builder(
+                      shrinkWrap: true,
+                      physics: const NeverScrollableScrollPhysics(),
+                      itemCount: sorted.length,
+                      itemBuilder: (context, index) {
+                        return Padding(
+                          padding: const EdgeInsets.only(bottom: 8.0),
+                          child: LargeLeadingTile(
+                            title: Text(
+                              sorted[index].name,
+                              maxLines: 2,
+                              overflow: TextOverflow.ellipsis,
+                              style: context.textTheme.titleSmall?.copyWith(
+                                fontWeight: FontWeight.w600,
+                              ),
+                            ),
+                            subtitle: sorted[index].ownerId == userId
+                                ? Text(
+                                    '${sorted[index].assetCount} items',
+                                    overflow: TextOverflow.ellipsis,
+                                    style:
+                                        context.textTheme.bodyMedium?.copyWith(
+                                      color: context
+                                          .colorScheme.onSurfaceSecondary,
+                                    ),
+                                  )
+                                : sorted[index].ownerName != null
+                                    ? Text(
+                                        '${sorted[index].assetCount} items • ${'album_thumbnail_shared_by'.tr(
+                                          args: [
+                                            sorted[index].ownerName!,
+                                          ],
+                                        )}',
+                                        overflow: TextOverflow.ellipsis,
+                                        style: context.textTheme.bodyMedium
+                                            ?.copyWith(
+                                          color: context
+                                              .colorScheme.onSurfaceSecondary,
+                                        ),
+                                      )
+                                    : null,
+                            onTap: () => context.pushRoute(
+                              AlbumViewerRoute(albumId: sorted[index].id),
+                            ),
+                            leadingPadding: const EdgeInsets.only(
+                              right: 16,
+                            ),
+                            leading: ClipRRect(
+                              borderRadius: const BorderRadius.all(
+                                Radius.circular(15),
+                              ),
+                              child: ImmichThumbnail(
+                                asset: sorted[index].thumbnail.value,
+                                width: 80,
+                                height: 80,
+                              ),
+                            ),
+                            // minVerticalPadding: 1,
+                          ),
+                        );
+                      },
+                    ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+class QuickFilterButton extends StatelessWidget {
+  const QuickFilterButton({
+    super.key,
+    required this.isSelected,
+    required this.onTap,
+    required this.label,
+  });
+
+  final bool isSelected;
+  final VoidCallback onTap;
+  final String label;
+
+  @override
+  Widget build(BuildContext context) {
+    return TextButton(
+      onPressed: onTap,
+      style: ButtonStyle(
+        backgroundColor: WidgetStateProperty.all(
+          isSelected ? context.colorScheme.primary : Colors.transparent,
+        ),
+        shape: WidgetStateProperty.all(
+          RoundedRectangleBorder(
+            borderRadius: BorderRadius.circular(20),
+            side: BorderSide(
+              color: context.colorScheme.onSurface.withAlpha(25),
+              width: 1,
+            ),
+          ),
+        ),
+      ),
+      child: Text(
+        label,
+        style: TextStyle(
+          color: isSelected
+              ? context.colorScheme.onPrimary
+              : context.colorScheme.onSurface,
+          fontSize: 14,
+        ),
+      ),
+    );
+  }
+}
+
+class SortButton extends ConsumerWidget {
+  const SortButton({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final albumSortOption = ref.watch(albumSortByOptionsProvider);
+    final albumSortIsReverse = ref.watch(albumSortOrderProvider);
+
+    return MenuAnchor(
+      style: MenuStyle(
+        elevation: const WidgetStatePropertyAll(1),
+        shape: WidgetStateProperty.all(
+          RoundedRectangleBorder(
+            borderRadius: BorderRadius.circular(24),
+          ),
+        ),
+        padding: const WidgetStatePropertyAll(
+          EdgeInsets.all(4),
+        ),
+      ),
+      consumeOutsideTap: true,
+      menuChildren: AlbumSortMode.values
+          .map(
+            (mode) => MenuItemButton(
+              leadingIcon: albumSortOption == mode
+                  ? albumSortIsReverse
+                      ? Icon(
+                          Icons.keyboard_arrow_down,
+                          color: albumSortOption == mode
+                              ? context.colorScheme.onPrimary
+                              : context.colorScheme.onSurface,
+                        )
+                      : Icon(
+                          Icons.keyboard_arrow_up_rounded,
+                          color: albumSortOption == mode
+                              ? context.colorScheme.onPrimary
+                              : context.colorScheme.onSurface,
+                        )
+                  : const Icon(Icons.abc, color: Colors.transparent),
+              onPressed: () {
+                final selected = albumSortOption == mode;
+                // Switch direction
+                if (selected) {
+                  ref
+                      .read(albumSortOrderProvider.notifier)
+                      .changeSortDirection(!albumSortIsReverse);
+                } else {
+                  ref
+                      .read(albumSortByOptionsProvider.notifier)
+                      .changeSortMode(mode);
+                }
+              },
+              style: ButtonStyle(
+                padding: WidgetStateProperty.all(
+                  const EdgeInsets.fromLTRB(16, 16, 32, 16),
+                ),
+                backgroundColor: WidgetStateProperty.all(
+                  albumSortOption == mode
+                      ? context.colorScheme.primary
+                      : Colors.transparent,
+                ),
+                shape: WidgetStateProperty.all(
+                  RoundedRectangleBorder(
+                    borderRadius: BorderRadius.circular(24),
+                  ),
+                ),
+              ),
+              child: Text(
+                mode.label.tr(),
+                style: context.textTheme.titleSmall?.copyWith(
+                  fontWeight: FontWeight.w600,
+                  color: albumSortOption == mode
+                      ? context.colorScheme.onPrimary
+                      : context.colorScheme.onSurface.withAlpha(185),
+                ),
+              ),
+            ),
+          )
+          .toList(),
+      builder: (context, controller, child) {
+        return GestureDetector(
+          onTap: () {
+            if (controller.isOpen) {
+              controller.close();
+            } else {
+              controller.open();
+            }
+          },
+          child: Row(
+            children: [
+              Padding(
+                padding: const EdgeInsets.only(right: 5),
+                child: Transform.rotate(
+                  angle: 90 * pi / 180,
+                  child: Icon(
+                    Icons.compare_arrows_rounded,
+                    size: 18,
+                    color: context.colorScheme.onSurface.withAlpha(225),
+                  ),
+                ),
+              ),
+              Text(
+                albumSortOption.label.tr(),
+                style: context.textTheme.bodyLarge?.copyWith(
+                  fontWeight: FontWeight.w500,
+                  color: context.colorScheme.onSurface.withAlpha(225),
+                ),
+              ),
+            ],
+          ),
+        );
+      },
+    );
+  }
+}
diff --git a/mobile/lib/pages/backup/album_preview.page.dart b/mobile/lib/pages/backup/album_preview.page.dart
index 5cb5d418a0..b9fed41305 100644
--- a/mobile/lib/pages/backup/album_preview.page.dart
+++ b/mobile/lib/pages/backup/album_preview.page.dart
@@ -1,28 +1,27 @@
-import 'dart:typed_data';
-
 import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/theme_extensions.dart';
-import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:immich_mobile/repositories/album_media.repository.dart';
+import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
 
 @RoutePage()
 class AlbumPreviewPage extends HookConsumerWidget {
-  final AssetPathEntity album;
+  final Album album;
   const AlbumPreviewPage({super.key, required this.album});
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    final assets = useState<List<AssetEntity>>([]);
+    final assets = useState<List<Asset>>([]);
 
     getAssetsInAlbum() async {
-      assets.value = await album.getAssetListRange(
-        start: 0,
-        end: await album.assetCountAsync,
-      );
+      assets.value = await ref
+          .read(albumMediaRepositoryProvider)
+          .getAssets(album.localId!);
     }
 
     useEffect(
@@ -68,30 +67,10 @@ class AlbumPreviewPage extends HookConsumerWidget {
         ),
         itemCount: assets.value.length,
         itemBuilder: (context, index) {
-          Future<Uint8List?> thumbData =
-              assets.value[index].thumbnailDataWithSize(
-            const ThumbnailSize(200, 200),
-            quality: 50,
-          );
-
-          return FutureBuilder<Uint8List?>(
-            future: thumbData,
-            builder: ((context, snapshot) {
-              if (snapshot.hasData && snapshot.data != null) {
-                return Image.memory(
-                  snapshot.data!,
-                  width: 100,
-                  height: 100,
-                  fit: BoxFit.cover,
-                );
-              }
-
-              return const SizedBox(
-                width: 100,
-                height: 100,
-                child: ImmichLoadingIndicator(),
-              );
-            }),
+          return ImmichThumbnail(
+            asset: assets.value[index],
+            width: 100,
+            height: 100,
           );
         },
       ),
diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart
index 8dccece325..0869e75e9f 100644
--- a/mobile/lib/pages/backup/backup_album_selection.page.dart
+++ b/mobile/lib/pages/backup/backup_album_selection.page.dart
@@ -151,7 +151,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
 
     handleSyncAlbumToggle(bool isEnable) async {
       if (isEnable) {
-        await ref.read(albumProvider.notifier).getAllAlbums();
+        await ref.read(albumProvider.notifier).refreshRemoteAlbums();
         for (final album in selectedBackupAlbums) {
           await ref.read(albumProvider.notifier).createSyncAlbum(album.name);
         }
diff --git a/mobile/lib/pages/backup/backup_controller.page.dart b/mobile/lib/pages/backup/backup_controller.page.dart
index bb9d462e50..6783f7b54a 100644
--- a/mobile/lib/pages/backup/backup_controller.page.dart
+++ b/mobile/lib/pages/backup/backup_controller.page.dart
@@ -1,11 +1,9 @@
-import 'dart:async';
 import 'dart:io';
 import 'dart:math';
 
 import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
@@ -31,8 +29,6 @@ class BackupControllerPage extends HookConsumerWidget {
     BackUpState backupState = ref.watch(backupProvider);
     final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty;
     final didGetBackupInfo = useState(false);
-    final isScreenDarkened = useState(false);
-    final darkenScreenTimer = useRef<Timer?>(null);
 
     bool hasExclusiveAccess =
         backupState.backupProgress != BackUpProgressEnum.inBackground;
@@ -43,25 +39,6 @@ class BackupControllerPage extends HookConsumerWidget {
         ? false
         : true;
 
-    void startScreenDarkenTimer() {
-      darkenScreenTimer.value = Timer(const Duration(seconds: 30), () {
-        isScreenDarkened.value = true;
-        SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
-      });
-    }
-
-    void stopScreenDarkenTimer() {
-      darkenScreenTimer.value?.cancel();
-      isScreenDarkened.value = false;
-      SystemChrome.setEnabledSystemUIMode(
-        SystemUiMode.manual,
-        overlays: [
-          SystemUiOverlay.top,
-          SystemUiOverlay.bottom,
-        ],
-      );
-    }
-
     useEffect(
       () {
         // Update the background settings information just to make sure we
@@ -77,8 +54,6 @@ class BackupControllerPage extends HookConsumerWidget {
 
         return () {
           WakelockPlus.disable();
-          darkenScreenTimer.value?.cancel();
-          isScreenDarkened.value = false;
         };
       },
       [],
@@ -99,10 +74,8 @@ class BackupControllerPage extends HookConsumerWidget {
     useEffect(
       () {
         if (backupState.backupProgress == BackUpProgressEnum.inProgress) {
-          startScreenDarkenTimer();
           WakelockPlus.enable();
         } else {
-          stopScreenDarkenTimer();
           WakelockPlus.disable();
         }
 
@@ -212,7 +185,7 @@ class BackupControllerPage extends HookConsumerWidget {
                     .read(backupProvider.notifier)
                     .backupAlbumSelectionDone();
                 // waited until backup albums are stored in DB
-                ref.read(albumProvider.notifier).getDeviceAlbums();
+                ref.read(albumProvider.notifier).refreshDeviceAlbums();
               },
               child: const Text(
                 "backup_controller_page_select",
@@ -297,103 +270,77 @@ class BackupControllerPage extends HookConsumerWidget {
       );
     }
 
-    return GestureDetector(
-      onTap: () {
-        if (isScreenDarkened.value) {
-          stopScreenDarkenTimer();
-        }
-        if (backupState.backupProgress == BackUpProgressEnum.inProgress) {
-          startScreenDarkenTimer();
-        }
-      },
-      child: AnimatedOpacity(
-        opacity: isScreenDarkened.value ? 0.1 : 1.0,
-        duration: const Duration(seconds: 1),
-        child: Scaffold(
-          appBar: AppBar(
-            elevation: 0,
-            title: const Text(
-              "backup_controller_page_backup",
-            ).tr(),
-            leading: IconButton(
-              onPressed: () {
-                ref.watch(websocketProvider.notifier).listenUploadEvent();
-                context.maybePop(true);
-              },
-              splashRadius: 24,
-              icon: const Icon(
-                Icons.arrow_back_ios_rounded,
-              ),
-            ),
-            actions: [
-              Padding(
-                padding: const EdgeInsets.only(right: 8.0),
-                child: IconButton(
-                  onPressed: () =>
-                      context.pushRoute(const BackupOptionsRoute()),
-                  splashRadius: 24,
-                  icon: const Icon(
-                    Icons.settings_outlined,
-                  ),
-                ),
-              ),
-            ],
-          ),
-          body: Stack(
-            children: [
-              Padding(
-                padding:
-                    const EdgeInsets.only(left: 16.0, right: 16, bottom: 32),
-                child: ListView(
-                  // crossAxisAlignment: CrossAxisAlignment.start,
-                  children: hasAnyAlbum
-                      ? [
-                          buildFolderSelectionTile(),
-                          BackupInfoCard(
-                            title: "backup_controller_page_total".tr(),
-                            subtitle: "backup_controller_page_total_sub".tr(),
-                            info: ref
-                                    .watch(backupProvider)
-                                    .availableAlbums
-                                    .isEmpty
-                                ? "..."
-                                : "${backupState.allUniqueAssets.length}",
-                          ),
-                          BackupInfoCard(
-                            title: "backup_controller_page_backup".tr(),
-                            subtitle: "backup_controller_page_backup_sub".tr(),
-                            info: ref
-                                    .watch(backupProvider)
-                                    .availableAlbums
-                                    .isEmpty
-                                ? "..."
-                                : "${backupState.selectedAlbumsBackupAssetsIds.length}",
-                          ),
-                          BackupInfoCard(
-                            title: "backup_controller_page_remainder".tr(),
-                            subtitle:
-                                "backup_controller_page_remainder_sub".tr(),
-                            info: ref
-                                    .watch(backupProvider)
-                                    .availableAlbums
-                                    .isEmpty
-                                ? "..."
-                                : "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}",
-                          ),
-                          const Divider(),
-                          const CurrentUploadingAssetInfoBox(),
-                          if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
-                          buildBackupButton(),
-                        ]
-                      : [
-                          buildFolderSelectionTile(),
-                          if (!didGetBackupInfo.value) buildLoadingIndicator(),
-                        ],
-                ),
-              ),
-            ],
+    return Scaffold(
+      appBar: AppBar(
+        elevation: 0,
+        title: const Text(
+          "backup_controller_page_backup",
+        ).tr(),
+        leading: IconButton(
+          onPressed: () {
+            ref.watch(websocketProvider.notifier).listenUploadEvent();
+            context.maybePop(true);
+          },
+          splashRadius: 24,
+          icon: const Icon(
+            Icons.arrow_back_ios_rounded,
           ),
         ),
+        actions: [
+          Padding(
+            padding: const EdgeInsets.only(right: 8.0),
+            child: IconButton(
+              onPressed: () => context.pushRoute(const BackupOptionsRoute()),
+              splashRadius: 24,
+              icon: const Icon(
+                Icons.settings_outlined,
+              ),
+            ),
+          ),
+        ],
+      ),
+      body: Stack(
+        children: [
+          Padding(
+            padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32),
+            child: ListView(
+              // crossAxisAlignment: CrossAxisAlignment.start,
+              children: hasAnyAlbum
+                  ? [
+                      buildFolderSelectionTile(),
+                      BackupInfoCard(
+                        title: "backup_controller_page_total".tr(),
+                        subtitle: "backup_controller_page_total_sub".tr(),
+                        info: ref.watch(backupProvider).availableAlbums.isEmpty
+                            ? "..."
+                            : "${backupState.allUniqueAssets.length}",
+                      ),
+                      BackupInfoCard(
+                        title: "backup_controller_page_backup".tr(),
+                        subtitle: "backup_controller_page_backup_sub".tr(),
+                        info: ref.watch(backupProvider).availableAlbums.isEmpty
+                            ? "..."
+                            : "${backupState.selectedAlbumsBackupAssetsIds.length}",
+                      ),
+                      BackupInfoCard(
+                        title: "backup_controller_page_remainder".tr(),
+                        subtitle: "backup_controller_page_remainder_sub".tr(),
+                        info: ref.watch(backupProvider).availableAlbums.isEmpty
+                            ? "..."
+                            : "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}",
+                      ),
+                      const Divider(),
+                      const CurrentUploadingAssetInfoBox(),
+                      if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
+                      buildBackupButton(),
+                    ]
+                  : [
+                      buildFolderSelectionTile(),
+                      if (!didGetBackupInfo.value) buildLoadingIndicator(),
+                    ],
+            ),
+          ),
+        ],
       ),
     );
   }
diff --git a/mobile/lib/pages/backup/failed_backup_status.page.dart b/mobile/lib/pages/backup/failed_backup_status.page.dart
index 1c6d3a7aad..551555d75e 100644
--- a/mobile/lib/pages/backup/failed_backup_status.page.dart
+++ b/mobile/lib/pages/backup/failed_backup_status.page.dart
@@ -3,9 +3,8 @@ import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
+import 'package:immich_mobile/providers/image/immich_local_thumbnail_provider.dart';
 import 'package:intl/intl.dart';
-import 'package:photo_manager/photo_manager.dart';
-import 'package:photo_manager_image_provider/photo_manager_image_provider.dart';
 
 @RoutePage()
 class FailedBackupStatusPage extends HookConsumerWidget {
@@ -70,11 +69,10 @@ class FailedBackupStatusPage extends HookConsumerWidget {
                       clipBehavior: Clip.hardEdge,
                       child: Image(
                         fit: BoxFit.cover,
-                        image: AssetEntityImageProvider(
-                          errorAsset.asset,
-                          isOriginal: false,
-                          thumbnailSize: const ThumbnailSize.square(512),
-                          thumbnailFormat: ThumbnailFormat.jpeg,
+                        image: ImmichLocalThumbnailProvider(
+                          asset: errorAsset.asset,
+                          height: 512,
+                          width: 512,
                         ),
                       ),
                     ),
diff --git a/mobile/lib/pages/common/album_viewer.page.dart b/mobile/lib/pages/common/album_viewer.page.dart
deleted file mode 100644
index 33b314f3b1..0000000000
--- a/mobile/lib/pages/common/album_viewer.page.dart
+++ /dev/null
@@ -1,271 +0,0 @@
-import 'dart:async';
-
-import 'package:auto_route/auto_route.dart';
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:fluttertoast/fluttertoast.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
-import 'package:immich_mobile/providers/album/album.provider.dart';
-import 'package:immich_mobile/providers/album/current_album.provider.dart';
-import 'package:immich_mobile/providers/album/shared_album.provider.dart';
-import 'package:immich_mobile/utils/immich_loading_overlay.dart';
-import 'package:immich_mobile/services/album.service.dart';
-import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
-import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart';
-import 'package:immich_mobile/providers/multiselect.provider.dart';
-import 'package:immich_mobile/providers/authentication.provider.dart';
-import 'package:immich_mobile/widgets/album/album_viewer_appbar.dart';
-import 'package:immich_mobile/routing/router.dart';
-import 'package:immich_mobile/entities/album.entity.dart';
-import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:immich_mobile/providers/asset.provider.dart';
-import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
-import 'package:immich_mobile/widgets/common/immich_toast.dart';
-import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
-
-@RoutePage()
-class AlbumViewerPage extends HookConsumerWidget {
-  final int albumId;
-
-  const AlbumViewerPage({super.key, required this.albumId});
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    FocusNode titleFocusNode = useFocusNode();
-    final album = ref.watch(albumWatcher(albumId));
-    // Listen provider to prevent autoDispose when navigating to other routes from within the viewer page
-    ref.listen(currentAlbumProvider, (_, __) {});
-    album.whenData(
-      (value) => Future.microtask(
-        () => ref.read(currentAlbumProvider.notifier).set(value),
-      ),
-    );
-    final userId = ref.watch(authenticationProvider).userId;
-    final isProcessing = useProcessingOverlay();
-
-    Future<bool> onRemoveFromAlbumPressed(Iterable<Asset> assets) async {
-      final a = album.valueOrNull;
-      final bool isSuccess = a != null &&
-          await ref
-              .read(sharedAlbumProvider.notifier)
-              .removeAssetFromAlbum(a, assets);
-
-      if (!isSuccess) {
-        ImmichToast.show(
-          context: context,
-          msg: "album_viewer_appbar_share_err_remove".tr(),
-          toastType: ToastType.error,
-          gravity: ToastGravity.BOTTOM,
-        );
-      }
-      return isSuccess;
-    }
-
-    /// Find out if the assets in album exist on the device
-    /// If they exist, add to selected asset state to show they are already selected.
-    void onAddPhotosPressed(Album albumInfo) async {
-      AssetSelectionPageResult? returnPayload =
-          await context.pushRoute<AssetSelectionPageResult?>(
-        AlbumAssetSelectionRoute(
-          existingAssets: albumInfo.assets,
-          canDeselect: false,
-          query: getRemoteAssetQuery(ref),
-        ),
-      );
-
-      if (returnPayload != null && returnPayload.selectedAssets.isNotEmpty) {
-        // Check if there is new assets add
-        isProcessing.value = true;
-
-        await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
-              returnPayload.selectedAssets,
-              albumInfo,
-            );
-
-        isProcessing.value = false;
-      }
-    }
-
-    void onAddUsersPressed(Album album) async {
-      List<String>? sharedUserIds = await context.pushRoute<List<String>?>(
-        AlbumAdditionalSharedUserSelectionRoute(album: album),
-      );
-
-      if (sharedUserIds != null) {
-        isProcessing.value = true;
-
-        await ref
-            .watch(albumServiceProvider)
-            .addAdditionalUserToAlbum(sharedUserIds, album);
-
-        isProcessing.value = false;
-      }
-    }
-
-    Widget buildControlButton(Album album) {
-      return Padding(
-        padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 16),
-        child: SizedBox(
-          height: 40,
-          child: ListView(
-            scrollDirection: Axis.horizontal,
-            children: [
-              AlbumActionFilledButton(
-                iconData: Icons.add_photo_alternate_outlined,
-                onPressed: () => onAddPhotosPressed(album),
-                labelText: "share_add_photos".tr(),
-              ),
-              if (userId == album.ownerId)
-                AlbumActionFilledButton(
-                  iconData: Icons.person_add_alt_rounded,
-                  onPressed: () => onAddUsersPressed(album),
-                  labelText: "album_viewer_page_share_add_users".tr(),
-                ),
-            ],
-          ),
-        ),
-      );
-    }
-
-    Widget buildTitle(Album album) {
-      return Padding(
-        padding: const EdgeInsets.only(left: 8, right: 8),
-        child: userId == album.ownerId && album.isRemote
-            ? AlbumViewerEditableTitle(
-                album: album,
-                titleFocusNode: titleFocusNode,
-              )
-            : Padding(
-                padding: const EdgeInsets.only(left: 8.0),
-                child: Text(
-                  album.name,
-                  style: context.textTheme.headlineMedium,
-                ),
-              ),
-      );
-    }
-
-    Widget buildAlbumDateRange(Album album) {
-      final DateTime? startDate = album.startDate;
-      final DateTime? endDate = album.endDate;
-
-      if (startDate == null || endDate == null) {
-        return const SizedBox();
-      }
-
-      final String dateRangeText;
-      if (startDate.day == endDate.day &&
-          startDate.month == endDate.month &&
-          startDate.year == endDate.year) {
-        dateRangeText = DateFormat.yMMMd().format(startDate);
-      } else {
-        final String startDateText = (startDate.year == endDate.year
-                ? DateFormat.MMMd()
-                : DateFormat.yMMMd())
-            .format(startDate);
-        final String endDateText = DateFormat.yMMMd().format(endDate);
-        dateRangeText = "$startDateText - $endDateText";
-      }
-
-      return Padding(
-        padding: EdgeInsets.only(
-          left: 16.0,
-          bottom: album.shared ? 0.0 : 8.0,
-        ),
-        child: Text(
-          dateRangeText,
-          style: context.textTheme.labelLarge,
-        ),
-      );
-    }
-
-    Widget buildSharedUserIconsRow(Album album) {
-      return GestureDetector(
-        onTap: () => context.pushRoute(AlbumOptionsRoute(album: album)),
-        child: SizedBox(
-          height: 50,
-          child: ListView.builder(
-            padding: const EdgeInsets.only(left: 16),
-            scrollDirection: Axis.horizontal,
-            itemBuilder: ((context, index) {
-              return Padding(
-                padding: const EdgeInsets.only(right: 8.0),
-                child: UserCircleAvatar(
-                  user: album.sharedUsers.toList()[index],
-                  radius: 18,
-                  size: 36,
-                ),
-              );
-            }),
-            itemCount: album.sharedUsers.length,
-          ),
-        ),
-      );
-    }
-
-    Widget buildHeader(Album album) {
-      return Column(
-        mainAxisAlignment: MainAxisAlignment.end,
-        crossAxisAlignment: CrossAxisAlignment.start,
-        children: [
-          buildTitle(album),
-          if (album.assets.isNotEmpty == true) buildAlbumDateRange(album),
-          if (album.shared) buildSharedUserIconsRow(album),
-        ],
-      );
-    }
-
-    onActivitiesPressed(Album album) {
-      if (album.remoteId != null) {
-        context.pushRoute(
-          const ActivitiesRoute(),
-        );
-      }
-    }
-
-    return Scaffold(
-      body: Stack(
-        children: [
-          album.widgetWhen(
-            onData: (data) => MultiselectGrid(
-              renderListProvider: albumRenderlistProvider(albumId),
-              topWidget: Column(
-                crossAxisAlignment: CrossAxisAlignment.start,
-                children: [
-                  buildHeader(data),
-                  if (data.isRemote) buildControlButton(data),
-                ],
-              ),
-              onRemoveFromAlbum: onRemoveFromAlbumPressed,
-              editEnabled: data.ownerId == userId,
-            ),
-          ),
-          AnimatedPositioned(
-            duration: const Duration(milliseconds: 300),
-            top: ref.watch(multiselectProvider)
-                ? -(kToolbarHeight + MediaQuery.of(context).padding.top)
-                : 0,
-            left: 0,
-            right: 0,
-            child: album.when(
-              data: (data) => AlbumViewerAppbar(
-                titleFocusNode: titleFocusNode,
-                album: data,
-                userId: userId,
-                onAddPhotos: onAddPhotosPressed,
-                onAddUsers: onAddUsersPressed,
-                onActivities: onActivitiesPressed,
-              ),
-              error: (error, stackTrace) => AppBar(title: const Text("Error")),
-              loading: () => AppBar(),
-            ),
-          ),
-        ],
-      ),
-    );
-  }
-}
diff --git a/mobile/lib/pages/common/app_log_detail.page.dart b/mobile/lib/pages/common/app_log_detail.page.dart
index 1b9af6cfcf..dd6af81728 100644
--- a/mobile/lib/pages/common/app_log_detail.page.dart
+++ b/mobile/lib/pages/common/app_log_detail.page.dart
@@ -37,7 +37,7 @@ class AppLogDetailPage extends HookConsumerWidget {
                 IconButton(
                   onPressed: () {
                     Clipboard.setData(ClipboardData(text: text)).then((_) {
-                      ScaffoldMessenger.of(context).showSnackBar(
+                      context.scaffoldMessenger.showSnackBar(
                         SnackBar(
                           content: Text(
                             "Copied to clipboard",
diff --git a/mobile/lib/pages/common/create_album.page.dart b/mobile/lib/pages/common/create_album.page.dart
index 1fd860520d..55261f6d55 100644
--- a/mobile/lib/pages/common/create_album.page.dart
+++ b/mobile/lib/pages/common/create_album.page.dart
@@ -17,13 +17,11 @@ import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart';
 @RoutePage()
 // ignore: must_be_immutable
 class CreateAlbumPage extends HookConsumerWidget {
-  final bool isSharedAlbum;
-  final List<Asset>? initialAssets;
+  final List<Asset>? assets;
 
   const CreateAlbumPage({
     super.key,
-    required this.isSharedAlbum,
-    this.initialAssets,
+    this.assets,
   });
 
   @override
@@ -34,18 +32,9 @@ class CreateAlbumPage extends HookConsumerWidget {
     final isAlbumTitleTextFieldFocus = useState(false);
     final isAlbumTitleEmpty = useState(true);
     final selectedAssets = useState<Set<Asset>>(
-      initialAssets != null ? Set.from(initialAssets!) : const {},
+      assets != null ? Set.from(assets!) : const {},
     );
 
-    showSelectUserPage() async {
-      final bool? ok = await context.pushRoute<bool?>(
-        AlbumSharedUserSelectionRoute(assets: selectedAssets.value),
-      );
-      if (ok == true) {
-        selectedAssets.value = {};
-      }
-    }
-
     void onBackgroundTapped() {
       albumTitleTextFieldFocusNode.unfocus();
       isAlbumTitleTextFieldFocus.value = false;
@@ -199,7 +188,7 @@ class CreateAlbumPage extends HookConsumerWidget {
           );
 
       if (newAlbum != null) {
-        ref.watch(albumProvider.notifier).getAllAlbums();
+        ref.watch(albumProvider.notifier).refreshRemoteAlbums();
         selectedAssets.value = {};
         ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
 
@@ -223,36 +212,20 @@ class CreateAlbumPage extends HookConsumerWidget {
           'share_create_album',
         ).tr(),
         actions: [
-          if (isSharedAlbum)
-            TextButton(
-              onPressed: albumTitleController.text.isNotEmpty
-                  ? showSelectUserPage
-                  : null,
-              child: Text(
-                'create_shared_album_page_share'.tr(),
-                style: TextStyle(
-                  fontWeight: FontWeight.bold,
-                  color: albumTitleController.text.isEmpty
-                      ? context.themeData.disabledColor
-                      : context.primaryColor,
-                ),
-              ),
-            ),
-          if (!isSharedAlbum)
-            TextButton(
-              onPressed: albumTitleController.text.isNotEmpty
-                  ? createNonSharedAlbum
-                  : null,
-              child: Text(
-                'create_shared_album_page_create'.tr(),
-                style: TextStyle(
-                  fontWeight: FontWeight.bold,
-                  color: albumTitleController.text.isNotEmpty
-                      ? context.primaryColor
-                      : context.themeData.disabledColor,
-                ),
+          TextButton(
+            onPressed: albumTitleController.text.isNotEmpty
+                ? createNonSharedAlbum
+                : null,
+            child: Text(
+              'create_shared_album_page_create'.tr(),
+              style: TextStyle(
+                fontWeight: FontWeight.bold,
+                color: albumTitleController.text.isNotEmpty
+                    ? context.primaryColor
+                    : context.themeData.disabledColor,
               ),
             ),
+          ),
         ],
       ),
       body: GestureDetector(
diff --git a/mobile/lib/pages/common/download_panel.dart b/mobile/lib/pages/common/download_panel.dart
new file mode 100644
index 0000000000..4421e337e9
--- /dev/null
+++ b/mobile/lib/pages/common/download_panel.dart
@@ -0,0 +1,150 @@
+import 'package:background_downloader/background_downloader.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
+
+class DownloadPanel extends ConsumerWidget {
+  const DownloadPanel({
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final showProgress = ref.watch(
+      downloadStateProvider.select((state) => state.showProgress),
+    );
+
+    final tasks = ref
+        .watch(
+          downloadStateProvider.select((state) => state.taskProgress),
+        )
+        .entries
+        .toList();
+
+    onCancelDownload(String id) {
+      ref.watch(downloadStateProvider.notifier).cancelDownload(id);
+    }
+
+    return Positioned(
+      bottom: 140,
+      left: 16,
+      child: AnimatedSwitcher(
+        duration: const Duration(milliseconds: 300),
+        child: showProgress
+            ? ConstrainedBox(
+                constraints:
+                    BoxConstraints.loose(Size(context.width - 32, 300)),
+                child: ListView.builder(
+                  shrinkWrap: true,
+                  itemCount: tasks.length,
+                  itemBuilder: (context, index) {
+                    final task = tasks[index];
+                    return DownloadTaskTile(
+                      progress: task.value.progress,
+                      fileName: task.value.fileName,
+                      status: task.value.status,
+                      onCancelDownload: () => onCancelDownload(task.key),
+                    );
+                  },
+                ),
+              )
+            : const SizedBox.shrink(key: ValueKey('no_progress')),
+      ),
+    );
+  }
+}
+
+class DownloadTaskTile extends StatelessWidget {
+  final double progress;
+  final String fileName;
+  final TaskStatus status;
+  final VoidCallback onCancelDownload;
+
+  const DownloadTaskTile({
+    super.key,
+    required this.progress,
+    required this.fileName,
+    required this.status,
+    required this.onCancelDownload,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final progressPercent = (progress * 100).round();
+
+    getStatusText() {
+      switch (status) {
+        case TaskStatus.running:
+          return 'downloading'.tr();
+        case TaskStatus.complete:
+          return 'download_complete'.tr();
+        case TaskStatus.failed:
+          return 'download_failed'.tr();
+        case TaskStatus.canceled:
+          return 'download_canceled'.tr();
+        case TaskStatus.paused:
+          return 'download_paused'.tr();
+        case TaskStatus.enqueued:
+          return 'download_enqueue'.tr();
+        case TaskStatus.notFound:
+          return 'download_notfound'.tr();
+        case TaskStatus.waitingToRetry:
+          return 'download_waiting_to_retry'.tr();
+      }
+    }
+
+    return SizedBox(
+      key: const ValueKey('download_progress'),
+      width: context.width - 32,
+      child: Card(
+        clipBehavior: Clip.antiAlias,
+        shape: RoundedRectangleBorder(
+          borderRadius: BorderRadius.circular(16),
+        ),
+        child: ListTile(
+          minVerticalPadding: 18,
+          leading: const Icon(Icons.video_file_outlined),
+          title: Text(
+            getStatusText(),
+            style: context.textTheme.labelLarge,
+          ),
+          trailing: IconButton(
+            icon: Icon(Icons.close, color: context.colorScheme.onError),
+            onPressed: onCancelDownload,
+            style: ElevatedButton.styleFrom(
+              backgroundColor: context.colorScheme.error.withAlpha(200),
+            ),
+          ),
+          subtitle: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Text(
+                fileName,
+                style: context.textTheme.labelMedium,
+              ),
+              Row(
+                children: [
+                  Expanded(
+                    child: LinearProgressIndicator(
+                      minHeight: 8.0,
+                      value: progress,
+                      borderRadius:
+                          const BorderRadius.all(Radius.circular(10.0)),
+                    ),
+                  ),
+                  const SizedBox(width: 8),
+                  Text(
+                    '$progressPercent%',
+                    style: context.textTheme.labelSmall,
+                  ),
+                ],
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/pages/common/gallery_stacked_children.dart b/mobile/lib/pages/common/gallery_stacked_children.dart
new file mode 100644
index 0000000000..eafc325049
--- /dev/null
+++ b/mobile/lib/pages/common/gallery_stacked_children.dart
@@ -0,0 +1,91 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
+import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
+import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
+import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart';
+
+class GalleryStackedChildren extends HookConsumerWidget {
+  final ValueNotifier<int> stackIndex;
+
+  const GalleryStackedChildren(this.stackIndex, {super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final asset = ref.watch(currentAssetProvider);
+    if (asset == null) {
+      return const SizedBox();
+    }
+
+    final stackId = asset.stackId;
+    if (stackId == null) {
+      return const SizedBox();
+    }
+
+    final stackElements = ref.watch(assetStackStateProvider(stackId));
+    final showControls = ref.watch(showControlsProvider);
+
+    return IgnorePointer(
+      ignoring: !showControls,
+      child: AnimatedOpacity(
+        duration: const Duration(milliseconds: 100),
+        opacity: showControls ? 1.0 : 0.0,
+        child: SizedBox(
+          height: 80,
+          child: ListView.builder(
+            shrinkWrap: true,
+            scrollDirection: Axis.horizontal,
+            itemCount: stackElements.length,
+            padding: const EdgeInsets.only(
+              left: 5,
+              right: 5,
+              bottom: 30,
+            ),
+            itemBuilder: (context, index) {
+              final currentAsset = stackElements.elementAt(index);
+              final assetId = currentAsset.remoteId;
+              if (assetId == null) {
+                return const SizedBox();
+              }
+
+              return Padding(
+                key: ValueKey(currentAsset.id),
+                padding: const EdgeInsets.only(right: 5),
+                child: GestureDetector(
+                  onTap: () {
+                    stackIndex.value = index;
+                    ref.read(currentAssetProvider.notifier).set(currentAsset);
+                  },
+                  child: Container(
+                    width: 60,
+                    height: 60,
+                    decoration: index == stackIndex.value
+                        ? const BoxDecoration(
+                            color: Colors.white,
+                            borderRadius: BorderRadius.all(Radius.circular(6)),
+                            border: Border.fromBorderSide(
+                              BorderSide(color: Colors.white, width: 2),
+                            ),
+                          )
+                        : const BoxDecoration(
+                            color: Colors.white,
+                            borderRadius: BorderRadius.all(Radius.circular(6)),
+                            border: null,
+                          ),
+                    child: ClipRRect(
+                      borderRadius: const BorderRadius.all(Radius.circular(4)),
+                      child: Image(
+                        fit: BoxFit.cover,
+                        image: ImmichRemoteImageProvider(assetId: assetId),
+                      ),
+                    ),
+                  ),
+                ),
+              );
+            },
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart
index d8ea7cd89b..43ff43e573 100644
--- a/mobile/lib/pages/common/gallery_viewer.page.dart
+++ b/mobile/lib/pages/common/gallery_viewer.page.dart
@@ -10,14 +10,17 @@ import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:immich_mobile/pages/common/video_viewer.page.dart';
+import 'package:immich_mobile/extensions/scroll_extensions.dart';
+import 'package:immich_mobile/pages/common/download_panel.dart';
+import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
+import 'package:immich_mobile/pages/common/gallery_stacked_children.dart';
 import 'package:immich_mobile/providers/app_settings.provider.dart';
 import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
 import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
+import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
 import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
 import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
 import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
-import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart';
 import 'package:immich_mobile/services/app_settings.service.dart';
 import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
 import 'package:immich_mobile/widgets/asset_viewer/advanced_bottom_sheet.dart';
@@ -30,10 +33,10 @@ import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart';
 import 'package:immich_mobile/widgets/photo_view/src/photo_view_computed_scale.dart';
 import 'package:immich_mobile/widgets/photo_view/src/photo_view_scale_state.dart';
 import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_hero_attributes.dart';
-import 'package:isar/isar.dart';
 
 @RoutePage()
 // ignore: must_be_immutable
+/// Expects [currentAssetProvider] to be set before navigating to this page
 class GalleryViewerPage extends HookConsumerWidget {
   final int initialIndex;
   final int heroOffset;
@@ -52,79 +55,67 @@ class GalleryViewerPage extends HookConsumerWidget {
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    final settings = ref.watch(appSettingsServiceProvider);
-    final loadAsset = renderList.loadAsset;
     final totalAssets = useState(renderList.totalAssets);
-    final shouldLoopVideo = useState(AppSettingsEnum.loopVideo.defaultValue);
     final isZoomed = useState(false);
-    final isPlayingVideo = useState(false);
-    final localPosition = useState<Offset?>(null);
-    final currentIndex = useState(initialIndex);
-    final currentAsset = loadAsset(currentIndex.value);
-
-    // Update is playing motion video
-    ref.listen(videoPlaybackValueProvider.select((v) => v.state), (_, state) {
-      isPlayingVideo.value = state == VideoPlaybackState.playing;
-    });
-
-    final stackIndex = useState(-1);
-    final stack = showStack && currentAsset.stackCount > 0
-        ? ref.watch(assetStackStateProvider(currentAsset))
-        : <Asset>[];
-    final stackElements = showStack ? [currentAsset, ...stack] : <Asset>[];
-    // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
-    final isFromDto = currentAsset.id == Isar.autoIncrement;
-
-    Asset asset = stackIndex.value == -1
-        ? currentAsset
-        : stackElements.elementAt(stackIndex.value);
-
-    final isMotionPhoto = asset.livePhotoVideoId != null;
-    // Listen provider to prevent autoDispose when navigating to other routes from within the gallery page
-    ref.listen(currentAssetProvider, (_, __) {});
-    useEffect(
-      () {
-        // Delay state update to after the execution of build method
-        Future.microtask(
-          () => ref.read(currentAssetProvider.notifier).set(asset),
-        );
-        return null;
-      },
-      [asset],
-    );
-
-    useEffect(
-      () {
-        shouldLoopVideo.value =
-            settings.getSetting<bool>(AppSettingsEnum.loopVideo);
-        return null;
-      },
-      [],
-    );
+    final stackIndex = useState(0);
+    final localPosition = useRef<Offset?>(null);
+    final currentIndex = useValueNotifier(initialIndex);
+    final loadAsset = renderList.loadAsset;
+    final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider);
 
     Future<void> precacheNextImage(int index) async {
+      if (!context.mounted) {
+        return;
+      }
+
       void onError(Object exception, StackTrace? stackTrace) {
         // swallow error silently
-        debugPrint('Error precaching next image: $exception, $stackTrace');
+        log.severe('Error precaching next image: $exception, $stackTrace');
       }
 
       try {
         if (index < totalAssets.value && index >= 0) {
           final asset = loadAsset(index);
           await precacheImage(
-            ImmichImage.imageProvider(asset: asset),
+            ImmichImage.imageProvider(
+              asset: asset,
+              width: context.width,
+              height: context.height,
+            ),
             context,
             onError: onError,
           );
         }
       } catch (e) {
         // swallow error silently
-        debugPrint('Error precaching next image: $e');
+        log.severe('Error precaching next image: $e');
         context.maybePop();
       }
     }
 
+    useEffect(
+      () {
+        if (ref.read(showControlsProvider)) {
+          SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
+        } else {
+          SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
+        }
+
+        // Delay this a bit so we can finish loading the page
+        Timer(const Duration(milliseconds: 400), () {
+          precacheNextImage(currentIndex.value + 1);
+        });
+
+        return null;
+      },
+      const [],
+    );
+
     void showInfo() {
+      final asset = ref.read(currentAssetProvider);
+      if (asset == null) {
+        return;
+      }
       showModalBottomSheet(
         shape: const RoundedRectangleBorder(
           borderRadius: BorderRadius.all(Radius.circular(15.0)),
@@ -136,18 +127,29 @@ class GalleryViewerPage extends HookConsumerWidget {
         context: context,
         useSafeArea: true,
         builder: (context) {
-          return FractionallySizedBox(
-            heightFactor: 0.75,
-            child: Padding(
-              padding: EdgeInsets.only(
-                bottom: MediaQuery.viewInsetsOf(context).bottom,
-              ),
-              child: ref
-                      .watch(appSettingsServiceProvider)
-                      .getSetting<bool>(AppSettingsEnum.advancedTroubleshooting)
-                  ? AdvancedBottomSheet(assetDetail: asset)
-                  : DetailPanel(asset: asset),
-            ),
+          return DraggableScrollableSheet(
+            minChildSize: 0.5,
+            maxChildSize: 1,
+            initialChildSize: 0.75,
+            expand: false,
+            builder: (context, scrollController) {
+              return Padding(
+                padding: EdgeInsets.only(
+                  bottom: context.viewInsets.bottom,
+                ),
+                child: ref.watch(appSettingsServiceProvider).getSetting<bool>(
+                          AppSettingsEnum.advancedTroubleshooting,
+                        )
+                    ? AdvancedBottomSheet(
+                        assetDetail: asset,
+                        scrollController: scrollController,
+                      )
+                    : DetailPanel(
+                        asset: asset,
+                        scrollController: scrollController,
+                      ),
+              );
+            },
           );
         },
       );
@@ -182,86 +184,99 @@ class GalleryViewerPage extends HookConsumerWidget {
       }
     }
 
-    useEffect(
-      () {
-        if (ref.read(showControlsProvider)) {
-          SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
-        } else {
-          SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
-        }
-        isPlayingVideo.value = false;
-        return null;
-      },
-      [],
-    );
-
-    useEffect(
-      () {
-        // No need to await this
-        unawaited(
-          // Delay this a bit so we can finish loading the page
-          Future.delayed(const Duration(milliseconds: 400)).then(
-            // Precache the next image
-            (_) => precacheNextImage(currentIndex.value + 1),
-          ),
-        );
-        return null;
-      },
-      [],
-    );
-
     ref.listen(showControlsProvider, (_, show) {
-      if (show) {
+      if (show || Platform.isIOS) {
         SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
-      } else {
-        SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
+        return;
       }
+
+      // This prevents the bottom bar from "dropping" while the controls are being hidden
+      Timer(const Duration(milliseconds: 100), () {
+        SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
+      });
     });
 
-    Widget buildStackedChildren() {
-      return ListView.builder(
-        shrinkWrap: true,
-        scrollDirection: Axis.horizontal,
-        itemCount: stackElements.length,
-        padding: const EdgeInsets.only(
-          left: 5,
-          right: 5,
-          bottom: 30,
-        ),
-        itemBuilder: (context, index) {
-          final assetId = stackElements.elementAt(index).remoteId;
-          return Padding(
-            padding: const EdgeInsets.only(right: 5),
-            child: GestureDetector(
-              onTap: () => stackIndex.value = index,
-              child: Container(
-                width: 60,
-                height: 60,
-                decoration: BoxDecoration(
-                  color: Colors.white,
-                  borderRadius: BorderRadius.circular(6),
-                  border: (stackIndex.value == -1 && index == 0) ||
-                          index == stackIndex.value
-                      ? Border.all(
-                          color: Colors.white,
-                          width: 2,
-                        )
-                      : null,
-                ),
-                child: ClipRRect(
-                  borderRadius: BorderRadius.circular(4),
-                  child: Image(
-                    fit: BoxFit.cover,
-                    image: ImmichRemoteImageProvider(assetId: assetId!),
-                  ),
-                ),
-              ),
-            ),
-          );
+    PhotoViewGalleryPageOptions buildImage(BuildContext context, Asset asset) {
+      return PhotoViewGalleryPageOptions(
+        onDragStart: (_, details, __) {
+          localPosition.value = details.localPosition;
         },
+        onDragUpdate: (_, details, __) {
+          handleSwipeUpDown(details);
+        },
+        onTapDown: (_, __, ___) {
+          ref.read(showControlsProvider.notifier).toggle();
+        },
+        onLongPressStart: asset.isMotionPhoto
+            ? (_, __, ___) {
+                ref.read(isPlayingMotionVideoProvider.notifier).playing = true;
+              }
+            : null,
+        imageProvider: ImmichImage.imageProvider(asset: asset),
+        heroAttributes: _getHeroAttributes(asset),
+        filterQuality: FilterQuality.high,
+        tightMode: true,
+        minScale: PhotoViewComputedScale.contained,
+        errorBuilder: (context, error, stackTrace) => ImmichImage(
+          asset,
+          fit: BoxFit.contain,
+        ),
       );
     }
 
+    PhotoViewGalleryPageOptions buildVideo(BuildContext context, Asset asset) {
+      // This key is to prevent the video player from being re-initialized during the hero animation
+      final key = GlobalKey();
+      return PhotoViewGalleryPageOptions.customChild(
+        onDragStart: (_, details, __) =>
+            localPosition.value = details.localPosition,
+        onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
+        heroAttributes: _getHeroAttributes(asset),
+        filterQuality: FilterQuality.high,
+        initialScale: 1.0,
+        maxScale: 1.0,
+        minScale: 1.0,
+        basePosition: Alignment.center,
+        child: SizedBox(
+          width: context.width,
+          height: context.height,
+          child: NativeVideoViewerPage(
+            key: key,
+            asset: asset,
+            image: Image(
+              key: ValueKey(asset),
+              image: ImmichImage.imageProvider(
+                asset: asset,
+                width: context.width,
+                height: context.height,
+              ),
+              fit: BoxFit.contain,
+              height: context.height,
+              width: context.width,
+              alignment: Alignment.center,
+            ),
+          ),
+        ),
+      );
+    }
+
+    PhotoViewGalleryPageOptions buildAsset(BuildContext context, int index) {
+      var newAsset = loadAsset(index);
+      final stackId = newAsset.stackId;
+      if (stackId != null && currentIndex.value == index) {
+        final stackElements =
+            ref.read(assetStackStateProvider(newAsset.stackId!));
+        if (stackIndex.value < stackElements.length) {
+          newAsset = stackElements.elementAt(stackIndex.value);
+        }
+      }
+
+      if (newAsset.isImage && !isPlayingMotionVideo) {
+        return buildImage(context, newAsset);
+      }
+      return buildVideo(context, newAsset);
+    }
+
     return PopScope(
       // Change immersive mode back to normal "edgeToEdge" mode
       onPopInvokedWithResult: (didPop, _) =>
@@ -271,128 +286,79 @@ class GalleryViewerPage extends HookConsumerWidget {
         body: Stack(
           children: [
             PhotoViewGallery.builder(
+              key: ValueKey(isPlayingMotionVideo),
               scaleStateChangedCallback: (state) {
-                isZoomed.value = state != PhotoViewScaleState.initial;
-                ref.read(showControlsProvider.notifier).show = !isZoomed.value;
+                final asset = ref.read(currentAssetProvider);
+                if (asset == null) {
+                  return;
+                }
+
+                if (asset.isImage && !ref.read(isPlayingMotionVideoProvider)) {
+                  isZoomed.value = state != PhotoViewScaleState.initial;
+                  ref.read(showControlsProvider.notifier).show =
+                      !isZoomed.value;
+                }
               },
-              loadingBuilder: (context, event, index) => ClipRect(
-                child: Stack(
-                  fit: StackFit.expand,
-                  children: [
-                    BackdropFilter(
-                      filter: ui.ImageFilter.blur(
-                        sigmaX: 10,
-                        sigmaY: 10,
+              gaplessPlayback: true,
+              loadingBuilder: (context, event, index) {
+                final asset = loadAsset(index);
+                return ClipRect(
+                  child: Stack(
+                    fit: StackFit.expand,
+                    children: [
+                      BackdropFilter(
+                        filter: ui.ImageFilter.blur(
+                          sigmaX: 10,
+                          sigmaY: 10,
+                        ),
                       ),
-                    ),
-                    ImmichThumbnail(
-                      asset: asset,
-                      fit: BoxFit.contain,
-                    ),
-                  ],
-                ),
-              ),
+                      ImmichThumbnail(
+                        key: ValueKey(asset),
+                        asset: asset,
+                        fit: BoxFit.contain,
+                      ),
+                    ],
+                  ),
+                );
+              },
               pageController: controller,
               scrollPhysics: isZoomed.value
                   ? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in
                   : (Platform.isIOS
-                      ? const ScrollPhysics() // Use bouncing physics for iOS
-                      : const ClampingScrollPhysics() // Use heavy physics for Android
+                      ? const FastScrollPhysics() // Use bouncing physics for iOS
+                      : const FastClampingScrollPhysics() // Use heavy physics for Android
                   ),
               itemCount: totalAssets.value,
               scrollDirection: Axis.horizontal,
-              onPageChanged: (value) async {
+              onPageChanged: (value) {
                 final next = currentIndex.value < value ? value + 1 : value - 1;
 
                 ref.read(hapticFeedbackProvider.notifier).selectionClick();
 
+                final newAsset = loadAsset(value);
+
                 currentIndex.value = value;
-                stackIndex.value = -1;
-                isPlayingVideo.value = false;
+                stackIndex.value = 0;
 
-                // Wait for page change animation to finish
-                await Future.delayed(const Duration(milliseconds: 400));
-                // Then precache the next image
-                unawaited(precacheNextImage(next));
-              },
-              builder: (context, index) {
-                final a =
-                    index == currentIndex.value ? asset : loadAsset(index);
-
-                final ImageProvider provider =
-                    ImmichImage.imageProvider(asset: a);
-
-                if (a.isImage && !isPlayingVideo.value) {
-                  return PhotoViewGalleryPageOptions(
-                    onDragStart: (_, details, __) =>
-                        localPosition.value = details.localPosition,
-                    onDragUpdate: (_, details, __) =>
-                        handleSwipeUpDown(details),
-                    onTapDown: (_, __, ___) {
-                      ref.read(showControlsProvider.notifier).toggle();
-                    },
-                    onLongPressStart: (_, __, ___) {
-                      if (asset.livePhotoVideoId != null) {
-                        isPlayingVideo.value = true;
-                      }
-                    },
-                    imageProvider: provider,
-                    heroAttributes: PhotoViewHeroAttributes(
-                      tag: isFromDto
-                          ? '${currentAsset.remoteId}-$heroOffset'
-                          : currentAsset.id + heroOffset,
-                      transitionOnUserGestures: true,
-                    ),
-                    filterQuality: FilterQuality.high,
-                    tightMode: true,
-                    minScale: PhotoViewComputedScale.contained,
-                    errorBuilder: (context, error, stackTrace) => ImmichImage(
-                      a,
-                      fit: BoxFit.contain,
-                    ),
-                  );
-                } else {
-                  return PhotoViewGalleryPageOptions.customChild(
-                    onDragStart: (_, details, __) =>
-                        localPosition.value = details.localPosition,
-                    onDragUpdate: (_, details, __) =>
-                        handleSwipeUpDown(details),
-                    heroAttributes: PhotoViewHeroAttributes(
-                      tag: isFromDto
-                          ? '${currentAsset.remoteId}-$heroOffset'
-                          : currentAsset.id + heroOffset,
-                    ),
-                    filterQuality: FilterQuality.high,
-                    maxScale: 1.0,
-                    minScale: 1.0,
-                    basePosition: Alignment.center,
-                    child: VideoViewerPage(
-                      key: ValueKey(a),
-                      asset: a,
-                      isMotionVideo: a.livePhotoVideoId != null,
-                      loopVideo: shouldLoopVideo.value,
-                      placeholder: Image(
-                        image: provider,
-                        fit: BoxFit.contain,
-                        height: context.height,
-                        width: context.width,
-                        alignment: Alignment.center,
-                      ),
-                    ),
-                  );
+                ref.read(currentAssetProvider.notifier).set(newAsset);
+                if (newAsset.isVideo || newAsset.isMotionPhoto) {
+                  ref.read(videoPlaybackValueProvider.notifier).reset();
                 }
+
+                // Wait for page change animation to finish, then precache the next image
+                Timer(const Duration(milliseconds: 400), () {
+                  precacheNextImage(next);
+                });
               },
+              builder: buildAsset,
             ),
             Positioned(
               top: 0,
               left: 0,
               right: 0,
               child: GalleryAppBar(
-                asset: asset,
+                key: const ValueKey('app-bar'),
                 showInfo: showInfo,
-                isPlayingVideo: isPlayingVideo.value,
-                onToggleMotionVideo: () =>
-                    isPlayingVideo.value = !isPlayingVideo.value,
               ),
             ),
             Positioned(
@@ -401,29 +367,33 @@ class GalleryViewerPage extends HookConsumerWidget {
               right: 0,
               child: Column(
                 children: [
-                  Visibility(
-                    visible: stack.isNotEmpty,
-                    child: SizedBox(
-                      height: 80,
-                      child: buildStackedChildren(),
-                    ),
-                  ),
+                  GalleryStackedChildren(stackIndex),
                   BottomGalleryBar(
+                    key: const ValueKey('bottom-bar'),
                     renderList: renderList,
                     totalAssets: totalAssets,
                     controller: controller,
                     showStack: showStack,
-                    stackIndex: stackIndex.value,
-                    asset: asset,
+                    stackIndex: stackIndex,
                     assetIndex: currentIndex,
-                    showVideoPlayerControls: !asset.isImage && !isMotionPhoto,
                   ),
                 ],
               ),
             ),
+            const DownloadPanel(),
           ],
         ),
       ),
     );
   }
+
+  @pragma('vm:prefer-inline')
+  PhotoViewHeroAttributes _getHeroAttributes(Asset asset) {
+    return PhotoViewHeroAttributes(
+      tag: asset.isInDb
+          ? asset.id + heroOffset
+          : '${asset.remoteId}-$heroOffset',
+      transitionOnUserGestures: true,
+    );
+  }
 }
diff --git a/mobile/lib/pages/common/large_leading_tile.dart b/mobile/lib/pages/common/large_leading_tile.dart
new file mode 100644
index 0000000000..c6bbeb2e7d
--- /dev/null
+++ b/mobile/lib/pages/common/large_leading_tile.dart
@@ -0,0 +1,51 @@
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+
+class LargeLeadingTile extends StatelessWidget {
+  const LargeLeadingTile({
+    super.key,
+    required this.leading,
+    required this.onTap,
+    required this.title,
+    this.subtitle,
+    this.leadingPadding = const EdgeInsets.symmetric(
+      vertical: 8,
+      horizontal: 16.0,
+    ),
+    this.borderRadius = 20.0,
+  });
+
+  final Widget leading;
+  final VoidCallback onTap;
+  final Widget title;
+  final Widget? subtitle;
+  final EdgeInsetsGeometry leadingPadding;
+  final double borderRadius;
+
+  @override
+  Widget build(BuildContext context) {
+    return InkWell(
+      borderRadius: BorderRadius.circular(borderRadius),
+      onTap: onTap,
+      child: Row(
+        crossAxisAlignment: CrossAxisAlignment.center,
+        children: [
+          Padding(
+            padding: leadingPadding,
+            child: leading,
+          ),
+          Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              SizedBox(
+                width: context.width * 0.6,
+                child: title,
+              ),
+              subtitle ?? const SizedBox.shrink(),
+            ],
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/pages/common/native_video_viewer.page.dart b/mobile/lib/pages/common/native_video_viewer.page.dart
new file mode 100644
index 0000000000..9c50f49dbb
--- /dev/null
+++ b/mobile/lib/pages/common/native_video_viewer.page.dart
@@ -0,0 +1,386 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/providers/app_settings.provider.dart';
+import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
+import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
+import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
+import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
+import 'package:immich_mobile/services/api.service.dart';
+import 'package:immich_mobile/services/app_settings.service.dart';
+import 'package:immich_mobile/services/asset.service.dart';
+import 'package:immich_mobile/utils/debounce.dart';
+import 'package:immich_mobile/utils/hooks/interval_hook.dart';
+import 'package:immich_mobile/widgets/asset_viewer/custom_video_player_controls.dart';
+import 'package:logging/logging.dart';
+import 'package:native_video_player/native_video_player.dart';
+import 'package:wakelock_plus/wakelock_plus.dart';
+
+@RoutePage()
+class NativeVideoViewerPage extends HookConsumerWidget {
+  final Asset asset;
+  final bool showControls;
+  final Widget image;
+
+  const NativeVideoViewerPage({
+    super.key,
+    required this.asset,
+    required this.image,
+    this.showControls = true,
+  });
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final controller = useState<NativeVideoPlayerController?>(null);
+    final lastVideoPosition = useRef(-1);
+    final isBuffering = useRef(false);
+
+    // When a video is opened through the timeline, `isCurrent` will immediately be true.
+    // When swiping from video A to video B, `isCurrent` will initially be true for video A and false for video B.
+    // If the swipe is completed, `isCurrent` will be true for video B after a delay.
+    // If the swipe is canceled, `currentAsset` will not have changed and video A will continue to play.
+    final currentAsset = useState(ref.read(currentAssetProvider));
+    final isCurrent = currentAsset.value == asset;
+
+    // Used to show the placeholder during hero animations for remote videos to avoid a stutter
+    final isVisible = useState(Platform.isIOS && asset.isLocal);
+
+    final log = Logger('NativeVideoViewerPage');
+
+    Future<VideoSource?> createSource() async {
+      if (!context.mounted) {
+        return null;
+      }
+
+      try {
+        final local = asset.local;
+        if (local != null && asset.livePhotoVideoId == null) {
+          final file = await local.file;
+          if (file == null) {
+            throw Exception('No file found for the video');
+          }
+
+          final source = await VideoSource.init(
+            path: file.path,
+            type: VideoSourceType.file,
+          );
+          return source;
+        }
+
+        // Use a network URL for the video player controller
+        final serverEndpoint = Store.get(StoreKey.serverEndpoint);
+        final String videoUrl = asset.livePhotoVideoId != null
+            ? '$serverEndpoint/assets/${asset.livePhotoVideoId}/video/playback'
+            : '$serverEndpoint/assets/${asset.remoteId}/video/playback';
+
+        final source = await VideoSource.init(
+          path: videoUrl,
+          type: VideoSourceType.network,
+          headers: ApiService.getRequestHeaders(),
+        );
+        return source;
+      } catch (error) {
+        log.severe(
+          'Error creating video source for asset ${asset.fileName}: $error',
+        );
+        return null;
+      }
+    }
+
+    final videoSource = useMemoized<Future<VideoSource?>>(() => createSource());
+    final aspectRatio = useState<double?>(asset.aspectRatio);
+    useMemoized(
+      () async {
+        if (!context.mounted || aspectRatio.value != null) {
+          return null;
+        }
+
+        try {
+          aspectRatio.value =
+              await ref.read(assetServiceProvider).getAspectRatio(asset);
+        } catch (error) {
+          log.severe(
+            'Error getting aspect ratio for asset ${asset.fileName}: $error',
+          );
+        }
+      },
+    );
+
+    void checkIfBuffering() {
+      if (!context.mounted) {
+        return;
+      }
+
+      final videoPlayback = ref.read(videoPlaybackValueProvider);
+      if ((isBuffering.value ||
+              videoPlayback.state == VideoPlaybackState.initializing) &&
+          videoPlayback.state != VideoPlaybackState.buffering) {
+        ref.read(videoPlaybackValueProvider.notifier).value =
+            videoPlayback.copyWith(state: VideoPlaybackState.buffering);
+      }
+    }
+
+    // Timer to mark videos as buffering if the position does not change
+    useInterval(const Duration(seconds: 5), checkIfBuffering);
+
+    // When the position changes, seek to the position
+    // Debounce the seek to avoid seeking too often
+    // But also don't delay the seek too much to maintain visual feedback
+    final seekDebouncer = useDebouncer(
+      interval: const Duration(milliseconds: 100),
+      maxWaitTime: const Duration(milliseconds: 200),
+    );
+    ref.listen(videoPlayerControlsProvider, (oldControls, newControls) async {
+      final playerController = controller.value;
+      if (playerController == null) {
+        return;
+      }
+
+      final playbackInfo = playerController.playbackInfo;
+      if (playbackInfo == null) {
+        return;
+      }
+
+      final oldSeek = (oldControls?.position ?? 0) ~/ 1;
+      final newSeek = newControls.position ~/ 1;
+      if (oldSeek != newSeek || newControls.restarted) {
+        seekDebouncer.run(() => playerController.seekTo(newSeek));
+      }
+
+      if (oldControls?.pause != newControls.pause || newControls.restarted) {
+        // Make sure the last seek is complete before pausing or playing
+        // Otherwise, `onPlaybackPositionChanged` can receive outdated events
+        if (seekDebouncer.isActive) {
+          await seekDebouncer.drain();
+        }
+
+        try {
+          if (newControls.pause) {
+            await playerController.pause();
+          } else {
+            await playerController.play();
+          }
+        } catch (error) {
+          log.severe('Error pausing or playing video: $error');
+        }
+      }
+    });
+
+    void onPlaybackReady() async {
+      final videoController = controller.value;
+      if (videoController == null || !isCurrent || !context.mounted) {
+        return;
+      }
+
+      final videoPlayback =
+          VideoPlaybackValue.fromNativeController(videoController);
+      ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback;
+
+      try {
+        await videoController.play();
+        await videoController.setVolume(0.9);
+      } catch (error) {
+        log.severe('Error playing video: $error');
+      }
+    }
+
+    void onPlaybackStatusChanged() {
+      final videoController = controller.value;
+      if (videoController == null || !context.mounted) {
+        return;
+      }
+
+      final videoPlayback =
+          VideoPlaybackValue.fromNativeController(videoController);
+      if (videoPlayback.state == VideoPlaybackState.playing) {
+        // Sync with the controls playing
+        WakelockPlus.enable();
+      } else {
+        // Sync with the controls pause
+        WakelockPlus.disable();
+      }
+
+      ref.read(videoPlaybackValueProvider.notifier).status =
+          videoPlayback.state;
+    }
+
+    void onPlaybackPositionChanged() {
+      // When seeking, these events sometimes move the slider to an older position
+      if (seekDebouncer.isActive) {
+        return;
+      }
+
+      final videoController = controller.value;
+      if (videoController == null || !context.mounted) {
+        return;
+      }
+
+      final playbackInfo = videoController.playbackInfo;
+      if (playbackInfo == null) {
+        return;
+      }
+
+      ref.read(videoPlaybackValueProvider.notifier).position =
+          Duration(seconds: playbackInfo.position);
+
+      // Check if the video is buffering
+      if (playbackInfo.status == PlaybackStatus.playing) {
+        isBuffering.value = lastVideoPosition.value == playbackInfo.position;
+        lastVideoPosition.value = playbackInfo.position;
+      } else {
+        isBuffering.value = false;
+        lastVideoPosition.value = -1;
+      }
+    }
+
+    void onPlaybackEnded() {
+      final videoController = controller.value;
+      if (videoController == null || !context.mounted) {
+        return;
+      }
+
+      if (videoController.playbackInfo?.status == PlaybackStatus.stopped &&
+          !ref
+              .read(appSettingsServiceProvider)
+              .getSetting<bool>(AppSettingsEnum.loopVideo)) {
+        ref.read(isPlayingMotionVideoProvider.notifier).playing = false;
+      }
+    }
+
+    void removeListeners(NativeVideoPlayerController controller) {
+      controller.onPlaybackPositionChanged
+          .removeListener(onPlaybackPositionChanged);
+      controller.onPlaybackStatusChanged
+          .removeListener(onPlaybackStatusChanged);
+      controller.onPlaybackReady.removeListener(onPlaybackReady);
+      controller.onPlaybackEnded.removeListener(onPlaybackEnded);
+    }
+
+    void initController(NativeVideoPlayerController nc) async {
+      if (controller.value != null || !context.mounted) {
+        return;
+      }
+      ref.read(videoPlayerControlsProvider.notifier).reset();
+      ref.read(videoPlaybackValueProvider.notifier).reset();
+
+      final source = await videoSource;
+      if (source == null) {
+        return;
+      }
+
+      nc.onPlaybackPositionChanged.addListener(onPlaybackPositionChanged);
+      nc.onPlaybackStatusChanged.addListener(onPlaybackStatusChanged);
+      nc.onPlaybackReady.addListener(onPlaybackReady);
+      nc.onPlaybackEnded.addListener(onPlaybackEnded);
+
+      nc.loadVideoSource(source).catchError((error) {
+        log.severe('Error loading video source: $error');
+      });
+      final loopVideo = ref
+          .read(appSettingsServiceProvider)
+          .getSetting<bool>(AppSettingsEnum.loopVideo);
+      nc.setLoop(loopVideo);
+
+      controller.value = nc;
+      Timer(const Duration(milliseconds: 200), checkIfBuffering);
+    }
+
+    ref.listen(currentAssetProvider, (_, value) {
+      final playerController = controller.value;
+      if (playerController != null && value != asset) {
+        removeListeners(playerController);
+      }
+
+      final curAsset = currentAsset.value;
+      if (curAsset == asset) {
+        return;
+      }
+
+      final imageToVideo = curAsset != null && !curAsset.isVideo;
+
+      // No need to delay video playback when swiping from an image to a video
+      if (imageToVideo && Platform.isIOS) {
+        currentAsset.value = value;
+        onPlaybackReady();
+        return;
+      }
+
+      // Delay the video playback to avoid a stutter in the swipe animation
+      Timer(
+          Platform.isIOS
+              ? const Duration(milliseconds: 300)
+              : imageToVideo
+                  ? const Duration(milliseconds: 200)
+                  : const Duration(milliseconds: 400), () {
+        if (!context.mounted) {
+          return;
+        }
+
+        currentAsset.value = value;
+        if (currentAsset.value == asset) {
+          onPlaybackReady();
+        }
+      });
+    });
+
+    useEffect(
+      () {
+        // If opening a remote video from a hero animation, delay visibility to avoid a stutter
+        final timer = isVisible.value
+            ? null
+            : Timer(
+                const Duration(milliseconds: 300),
+                () => isVisible.value = true,
+              );
+
+        return () {
+          timer?.cancel();
+          final playerController = controller.value;
+          if (playerController == null) {
+            return;
+          }
+          removeListeners(playerController);
+          playerController.stop().catchError((error) {
+            log.fine('Error stopping video: $error');
+          });
+
+          WakelockPlus.disable();
+        };
+      },
+      const [],
+    );
+
+    return Stack(
+      children: [
+        // This remains under the video to avoid flickering
+        // For motion videos, this is the image portion of the asset
+        Center(key: ValueKey(asset.id), child: image),
+        if (aspectRatio.value != null)
+          Visibility.maintain(
+            key: ValueKey(asset),
+            visible: isVisible.value,
+            child: Center(
+              key: ValueKey(asset),
+              child: AspectRatio(
+                key: ValueKey(asset),
+                aspectRatio: aspectRatio.value!,
+                child: isCurrent
+                    ? NativeVideoPlayerView(
+                        key: ValueKey(asset),
+                        onViewReady: initController,
+                      )
+                    : null,
+              ),
+            ),
+          ),
+        if (showControls) const Center(child: CustomVideoPlayerControls()),
+      ],
+    );
+  }
+}
diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart
index 117b0aedc0..3cbded1787 100644
--- a/mobile/lib/pages/common/settings.page.dart
+++ b/mobile/lib/pages/common/settings.page.dart
@@ -8,36 +8,69 @@ import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_se
 import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart';
 import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart';
 import 'package:immich_mobile/widgets/settings/language_settings.dart';
+import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart';
 import 'package:immich_mobile/widgets/settings/notification_setting.dart';
 import 'package:immich_mobile/widgets/settings/preference_settings/preference_setting.dart';
 import 'package:immich_mobile/routing/router.dart';
 
 enum SettingSection {
+  advanced(
+    'advanced_settings_tile_title',
+    Icons.build_outlined,
+    "advanced_settings_tile_subtitle",
+  ),
+  assetViewer(
+    'asset_viewer_settings_title',
+    Icons.image_outlined,
+    "asset_viewer_settings_subtitle",
+  ),
+  backup(
+    'backup_controller_page_backup',
+    Icons.cloud_upload_outlined,
+    "backup_setting_subtitle",
+  ),
+  languages(
+    'setting_languages_title',
+    Icons.language,
+    "setting_languages_subtitle",
+  ),
+  networking(
+    'networking_settings',
+    Icons.wifi,
+    "networking_subtitle",
+  ),
   notifications(
     'setting_notifications_title',
     Icons.notifications_none_rounded,
+    "setting_notifications_subtitle",
   ),
-  languages('setting_languages_title', Icons.language),
-  preferences('preferences_settings_title', Icons.interests_outlined),
-  backup('backup_controller_page_backup', Icons.cloud_upload_outlined),
-  timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined),
-  viewer('asset_viewer_settings_title', Icons.image_outlined),
-  advanced('advanced_settings_tile_title', Icons.build_outlined);
+  preferences(
+    'preferences_settings_title',
+    Icons.interests_outlined,
+    "preferences_settings_subtitle",
+  ),
+  timeline(
+    'asset_list_settings_title',
+    Icons.auto_awesome_mosaic_outlined,
+    "asset_list_settings_subtitle",
+  );
 
   final String title;
+  final String subtitle;
   final IconData icon;
 
   Widget get widget => switch (this) {
-        SettingSection.notifications => const NotificationSetting(),
-        SettingSection.languages => const LanguageSettings(),
-        SettingSection.preferences => const PreferenceSetting(),
-        SettingSection.backup => const BackupSettings(),
-        SettingSection.timeline => const AssetListSettings(),
-        SettingSection.viewer => const AssetViewerSettings(),
         SettingSection.advanced => const AdvancedSettings(),
+        SettingSection.assetViewer => const AssetViewerSettings(),
+        SettingSection.backup => const BackupSettings(),
+        SettingSection.languages => const LanguageSettings(),
+        SettingSection.networking => const NetworkingSettings(),
+        SettingSection.notifications => const NotificationSetting(),
+        SettingSection.preferences => const PreferenceSetting(),
+        SettingSection.timeline => const AssetListSettings(),
       };
 
-  const SettingSection(this.title, this.icon);
+  const SettingSection(this.title, this.icon, this.subtitle);
 }
 
 @RoutePage()
@@ -46,6 +79,7 @@ class SettingsPage extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    context.locale;
     return Scaffold(
       appBar: AppBar(
         centerTitle: false,
@@ -60,22 +94,51 @@ class _MobileLayout extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return ListView(
+      physics: const ClampingScrollPhysics(),
+      padding: const EdgeInsets.symmetric(vertical: 10.0),
       children: SettingSection.values
           .map(
-            (s) => ListTile(
-              contentPadding:
-                  const EdgeInsets.symmetric(vertical: 2.0, horizontal: 16.0),
-              leading: Icon(s.icon),
-              title: Padding(
-                padding: const EdgeInsets.only(left: 8.0),
-                child: Text(
-                  s.title,
-                  style: const TextStyle(
-                    fontWeight: FontWeight.bold,
-                  ),
-                ).tr(),
+            (setting) => Padding(
+              padding: const EdgeInsets.symmetric(
+                horizontal: 16.0,
+              ),
+              child: Card(
+                elevation: 0,
+                clipBehavior: Clip.antiAlias,
+                color: context.colorScheme.surfaceContainer,
+                shape: const RoundedRectangleBorder(
+                  borderRadius: BorderRadius.all(Radius.circular(16)),
+                ),
+                margin: const EdgeInsets.symmetric(vertical: 4.0),
+                child: ListTile(
+                  contentPadding: const EdgeInsets.symmetric(
+                    horizontal: 16.0,
+                  ),
+                  leading: Container(
+                    decoration: BoxDecoration(
+                      borderRadius: const BorderRadius.all(Radius.circular(16)),
+                      color: context.isDarkTheme
+                          ? Colors.black26
+                          : Colors.white.withAlpha(100),
+                    ),
+                    padding: const EdgeInsets.all(16.0),
+                    child: Icon(setting.icon, color: context.primaryColor),
+                  ),
+                  title: Text(
+                    setting.title,
+                    style: context.textTheme.titleMedium!.copyWith(
+                      fontWeight: FontWeight.w600,
+                      color: context.primaryColor,
+                    ),
+                  ).tr(),
+                  subtitle: Text(
+                    setting.subtitle,
+                    style: context.textTheme.labelLarge,
+                  ).tr(),
+                  onTap: () =>
+                      context.pushRoute(SettingsSubRoute(section: setting)),
+                ),
               ),
-              onTap: () => context.pushRoute(SettingsSubRoute(section: s)),
             ),
           )
           .toList(),
@@ -129,6 +192,7 @@ class SettingsSubPage extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    context.locale;
     return Scaffold(
       appBar: AppBar(
         centerTitle: false,
diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart
index d23e25372c..6a060e19f0 100644
--- a/mobile/lib/pages/common/splash_screen.page.dart
+++ b/mobile/lib/pages/common/splash_screen.page.dart
@@ -1,81 +1,88 @@
 import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/providers/backup/backup.provider.dart';
-import 'package:immich_mobile/providers/authentication.provider.dart';
+import 'package:immich_mobile/providers/auth.provider.dart';
 import 'package:immich_mobile/providers/gallery_permission.provider.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
-import 'package:immich_mobile/providers/api.provider.dart';
 import 'package:logging/logging.dart';
 
 @RoutePage()
-class SplashScreenPage extends HookConsumerWidget {
+class SplashScreenPage extends StatefulHookConsumerWidget {
   const SplashScreenPage({super.key});
 
   @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    final apiService = ref.watch(apiServiceProvider);
+  SplashScreenPageState createState() => SplashScreenPageState();
+}
+
+class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
+  final log = Logger("SplashScreenPage");
+  @override
+  void initState() {
+    super.initState();
+    ref
+        .read(authProvider.notifier)
+        .setOpenApiServiceEndpoint()
+        .then(logConnectionInfo)
+        .whenComplete(() => resumeSession());
+  }
+
+  void logConnectionInfo(String? endpoint) {
+    if (endpoint == null) {
+      return;
+    }
+
+    log.info("Resuming session at $endpoint");
+  }
+
+  void resumeSession() async {
     final serverUrl = Store.tryGet(StoreKey.serverUrl);
     final endpoint = Store.tryGet(StoreKey.serverEndpoint);
     final accessToken = Store.tryGet(StoreKey.accessToken);
-    final log = Logger("SplashScreenPage");
 
-    void performLoggingIn() async {
-      bool isAuthSuccess = false;
+    bool isAuthSuccess = false;
 
-      if (accessToken != null && serverUrl != null && endpoint != null) {
-        apiService.setEndpoint(endpoint);
-
-        try {
-          isAuthSuccess = await ref
-              .read(authenticationProvider.notifier)
-              .setSuccessLoginInfo(
-                accessToken: accessToken,
-                serverUrl: serverUrl,
-              );
-        } catch (error, stackTrace) {
-          log.severe(
-            'Cannot set success login info',
-            error,
-            stackTrace,
-          );
-        }
-      } else {
-        isAuthSuccess = false;
+    if (accessToken != null && serverUrl != null && endpoint != null) {
+      try {
+        isAuthSuccess = await ref.read(authProvider.notifier).saveAuthInfo(
+              accessToken: accessToken,
+            );
+      } catch (error, stackTrace) {
         log.severe(
-          'Missing authentication, server, or endpoint info from the local store',
+          'Cannot set success login info',
+          error,
+          stackTrace,
         );
       }
-
-      if (!isAuthSuccess) {
-        log.severe(
-          'Unable to login using offline or online methods - Logging out completely',
-        );
-        ref.read(authenticationProvider.notifier).logout();
-        context.replaceRoute(const LoginRoute());
-        return;
-      }
-
-      context.replaceRoute(const TabControllerRoute());
-
-      final hasPermission =
-          await ref.read(galleryPermissionNotifier.notifier).hasPermission;
-      if (hasPermission) {
-        // Resume backup (if enable) then navigate
-        ref.watch(backupProvider.notifier).resumeBackup();
-      }
+    } else {
+      isAuthSuccess = false;
+      log.severe(
+        'Missing authentication, server, or endpoint info from the local store',
+      );
     }
 
-    useEffect(
-      () {
-        performLoggingIn();
-        return null;
-      },
-      [],
-    );
+    if (!isAuthSuccess) {
+      log.severe(
+        'Unable to login using offline or online methods - Logging out completely',
+      );
+      ref.read(authProvider.notifier).logout();
+      context.replaceRoute(const LoginRoute());
+      return;
+    }
 
+    context.replaceRoute(const TabControllerRoute());
+
+    final hasPermission =
+        await ref.read(galleryPermissionNotifier.notifier).hasPermission;
+    if (hasPermission) {
+      // Resume backup (if enable) then navigate
+      ref.watch(backupProvider.notifier).resumeBackup();
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
     return const Scaffold(
       body: Center(
         child: Image(
diff --git a/mobile/lib/pages/common/tab_controller.page.dart b/mobile/lib/pages/common/tab_controller.page.dart
index b619e003d2..1ba9650056 100644
--- a/mobile/lib/pages/common/tab_controller.page.dart
+++ b/mobile/lib/pages/common/tab_controller.page.dart
@@ -3,8 +3,10 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/providers/album/album.provider.dart';
 import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
 import 'package:immich_mobile/providers/multiselect.provider.dart';
+import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/providers/asset.provider.dart';
 import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
@@ -16,10 +18,11 @@ class TabControllerPage extends HookConsumerWidget {
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    final refreshing = ref.watch(assetProvider);
+    final isRefreshingAssets = ref.watch(assetProvider);
+    final isRefreshingRemoteAlbums = ref.watch(isRefreshingRemoteAlbumProvider);
 
-    Widget buildIcon(Widget icon) {
-      if (!refreshing) return icon;
+    Widget buildIcon({required Widget icon, required bool isProcessing}) {
+      if (!isProcessing) return icon;
       return Stack(
         alignment: Alignment.center,
         clipBehavior: Clip.none,
@@ -42,75 +45,27 @@ class TabControllerPage extends HookConsumerWidget {
       );
     }
 
-    navigationRail(TabsRouter tabsRouter) {
-      return NavigationRail(
-        labelType: NavigationRailLabelType.all,
-        selectedIndex: tabsRouter.activeIndex,
-        onDestinationSelected: (index) {
-          // Selected Photos while it is active
-          if (tabsRouter.activeIndex == 0 && index == 0) {
-            // Scroll to top
-            scrollToTopNotifierProvider.scrollToTop();
-          }
+    onNavigationSelected(TabsRouter router, int index) {
+      // On Photos page menu tapped
+      if (router.activeIndex == 0 && index == 0) {
+        scrollToTopNotifierProvider.scrollToTop();
+      }
 
-          ref.read(hapticFeedbackProvider.notifier).selectionClick();
-          tabsRouter.setActiveIndex(index);
-          ref.read(tabProvider.notifier).state = TabEnum.values[index];
-        },
-        selectedIconTheme: IconThemeData(
-          color: context.primaryColor,
-        ),
-        selectedLabelTextStyle: TextStyle(
-          color: context.primaryColor,
-        ),
-        useIndicator: false,
-        destinations: [
-          NavigationRailDestination(
-            padding: EdgeInsets.only(
-              top: MediaQuery.of(context).padding.top + 4,
-              left: 4,
-              right: 4,
-              bottom: 4,
-            ),
-            icon: const Icon(Icons.photo_library_outlined),
-            selectedIcon: const Icon(Icons.photo_library),
-            label: const Text('tab_controller_nav_photos').tr(),
-          ),
-          NavigationRailDestination(
-            padding: const EdgeInsets.all(4),
-            icon: const Icon(Icons.search_rounded),
-            selectedIcon: const Icon(Icons.search),
-            label: const Text('tab_controller_nav_search').tr(),
-          ),
-          NavigationRailDestination(
-            padding: const EdgeInsets.all(4),
-            icon: const Icon(Icons.share_rounded),
-            selectedIcon: const Icon(Icons.share),
-            label: const Text('tab_controller_nav_sharing').tr(),
-          ),
-          NavigationRailDestination(
-            padding: const EdgeInsets.all(4),
-            icon: const Icon(Icons.photo_album_outlined),
-            selectedIcon: const Icon(Icons.photo_album),
-            label: const Text('tab_controller_nav_library').tr(),
-          ),
-        ],
-      );
+      // On Search page tapped
+      if (router.activeIndex == 1 && index == 1) {
+        ref.read(searchInputFocusProvider).requestFocus();
+      }
+
+      ref.read(hapticFeedbackProvider.notifier).selectionClick();
+      router.setActiveIndex(index);
+      ref.read(tabProvider.notifier).state = TabEnum.values[index];
     }
 
     bottomNavigationBar(TabsRouter tabsRouter) {
       return NavigationBar(
         selectedIndex: tabsRouter.activeIndex,
-        onDestinationSelected: (index) {
-          if (tabsRouter.activeIndex == 0 && index == 0) {
-            // Scroll to top
-            scrollToTopNotifierProvider.scrollToTop();
-          }
-
-          ref.read(hapticFeedbackProvider.notifier).selectionClick();
-          tabsRouter.setActiveIndex(index);
-          ref.read(tabProvider.notifier).state = TabEnum.values[index];
-        },
+        onDestinationSelected: (index) =>
+            onNavigationSelected(tabsRouter, index),
         destinations: [
           NavigationDestination(
             label: 'tab_controller_nav_photos'.tr(),
@@ -118,7 +73,8 @@ class TabControllerPage extends HookConsumerWidget {
               Icons.photo_library_outlined,
             ),
             selectedIcon: buildIcon(
-              Icon(
+              isProcessing: isRefreshingAssets,
+              icon: Icon(
                 Icons.photo_library,
                 color: context.primaryColor,
               ),
@@ -135,38 +91,42 @@ class TabControllerPage extends HookConsumerWidget {
             ),
           ),
           NavigationDestination(
-            label: 'tab_controller_nav_sharing'.tr(),
-            icon: const Icon(
-              Icons.group_outlined,
-            ),
-            selectedIcon: Icon(
-              Icons.group,
-              color: context.primaryColor,
-            ),
-          ),
-          NavigationDestination(
-            label: 'tab_controller_nav_library'.tr(),
+            label: 'albums'.tr(),
             icon: const Icon(
               Icons.photo_album_outlined,
             ),
             selectedIcon: buildIcon(
-              Icon(
+              isProcessing: isRefreshingRemoteAlbums,
+              icon: Icon(
                 Icons.photo_album_rounded,
                 color: context.primaryColor,
               ),
             ),
           ),
+          NavigationDestination(
+            label: 'library'.tr(),
+            icon: const Icon(
+              Icons.space_dashboard_outlined,
+            ),
+            selectedIcon: buildIcon(
+              isProcessing: isRefreshingAssets,
+              icon: Icon(
+                Icons.space_dashboard_rounded,
+                color: context.primaryColor,
+              ),
+            ),
+          ),
         ],
       );
     }
 
     final multiselectEnabled = ref.watch(multiselectProvider);
     return AutoTabsRouter(
-      routes: const [
-        PhotosRoute(),
+      routes: [
+        const PhotosRoute(),
         SearchRoute(),
-        SharingRoute(),
-        LibraryRoute(),
+        const AlbumsRoute(),
+        const LibraryRoute(),
       ],
       duration: const Duration(milliseconds: 600),
       transitionBuilder: (context, child, animation) => FadeTransition(
@@ -179,33 +139,13 @@ class TabControllerPage extends HookConsumerWidget {
           canPop: tabsRouter.activeIndex == 0,
           onPopInvokedWithResult: (didPop, _) =>
               !didPop ? tabsRouter.setActiveIndex(0) : null,
-          child: LayoutBuilder(
-            builder: (context, constraints) {
-              const medium = 600;
-              final Widget? bottom;
-              final Widget body;
-              if (constraints.maxWidth < medium) {
-                // Normal phone width
-                bottom = bottomNavigationBar(tabsRouter);
-                body = child;
-              } else {
-                // Medium tablet width
-                bottom = null;
-                body = Row(
-                  children: [
-                    navigationRail(tabsRouter),
-                    Expanded(child: child),
-                  ],
-                );
-              }
-              return Scaffold(
-                body: HeroControllerScope(
-                  controller: HeroController(),
-                  child: body,
-                ),
-                bottomNavigationBar: multiselectEnabled ? null : bottom,
-              );
-            },
+          child: Scaffold(
+            body: HeroControllerScope(
+              controller: HeroController(),
+              child: child,
+            ),
+            bottomNavigationBar:
+                multiselectEnabled ? null : bottomNavigationBar(tabsRouter),
           ),
         );
       },
diff --git a/mobile/lib/pages/common/video_viewer.page.dart b/mobile/lib/pages/common/video_viewer.page.dart
deleted file mode 100644
index 573f7277f2..0000000000
--- a/mobile/lib/pages/common/video_viewer.page.dart
+++ /dev/null
@@ -1,168 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
-import 'package:immich_mobile/providers/asset_viewer/video_player_controller_provider.dart';
-import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
-import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
-import 'package:immich_mobile/widgets/asset_viewer/video_player.dart';
-import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart';
-import 'package:wakelock_plus/wakelock_plus.dart';
-
-class VideoViewerPage extends HookConsumerWidget {
-  final Asset asset;
-  final bool isMotionVideo;
-  final Widget? placeholder;
-  final Duration hideControlsTimer;
-  final bool showControls;
-  final bool showDownloadingIndicator;
-  final bool loopVideo;
-
-  const VideoViewerPage({
-    super.key,
-    required this.asset,
-    this.isMotionVideo = false,
-    this.placeholder,
-    this.showControls = true,
-    this.hideControlsTimer = const Duration(seconds: 5),
-    this.showDownloadingIndicator = true,
-    this.loopVideo = false,
-  });
-
-  @override
-  build(BuildContext context, WidgetRef ref) {
-    final controller =
-        ref.watch(videoPlayerControllerProvider(asset: asset)).value;
-    // The last volume of the video used when mute is toggled
-    final lastVolume = useState(0.5);
-
-    // When the volume changes, set the volume
-    ref.listen(videoPlayerControlsProvider.select((value) => value.mute),
-        (_, mute) {
-      if (mute) {
-        controller?.setVolume(0.0);
-      } else {
-        controller?.setVolume(lastVolume.value);
-      }
-    });
-
-    // When the position changes, seek to the position
-    ref.listen(videoPlayerControlsProvider.select((value) => value.position),
-        (_, position) {
-      if (controller == null) {
-        // No seeeking if there is no video
-        return;
-      }
-
-      // Find the position to seek to
-      final Duration seek = controller.value.duration * (position / 100.0);
-      controller.seekTo(seek);
-    });
-
-    // When the custom video controls paus or plays
-    ref.listen(videoPlayerControlsProvider.select((value) => value.pause),
-        (lastPause, pause) {
-      if (pause) {
-        controller?.pause();
-      } else {
-        controller?.play();
-      }
-    });
-
-    // Updates the [videoPlaybackValueProvider] with the current
-    // position and duration of the video from the Chewie [controller]
-    // Also sets the error if there is an error in the playback
-    void updateVideoPlayback() {
-      final videoPlayback = VideoPlaybackValue.fromController(controller);
-      ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback;
-      final state = videoPlayback.state;
-
-      // Enable the WakeLock while the video is playing
-      if (state == VideoPlaybackState.playing) {
-        // Sync with the controls playing
-        WakelockPlus.enable();
-      } else {
-        // Sync with the controls pause
-        WakelockPlus.disable();
-      }
-    }
-
-    // Adds and removes the listener to the video player
-    useEffect(
-      () {
-        Future.microtask(
-          () => ref.read(videoPlayerControlsProvider.notifier).reset(),
-        );
-        // Guard no controller
-        if (controller == null) {
-          return null;
-        }
-
-        // Hide the controls
-        // Done in a microtask to avoid setting the state while the is building
-        if (!isMotionVideo) {
-          Future.microtask(() {
-            ref.read(showControlsProvider.notifier).show = false;
-          });
-        }
-
-        // Subscribes to listener
-        Future.microtask(() {
-          controller.addListener(updateVideoPlayback);
-        });
-        return () {
-          // Removes listener when we dispose
-          controller.removeListener(updateVideoPlayback);
-          controller.pause();
-        };
-      },
-      [controller],
-    );
-
-    final size = MediaQuery.sizeOf(context);
-
-    return PopScope(
-      onPopInvokedWithResult: (didPop, _) {
-        ref.read(videoPlaybackValueProvider.notifier).value =
-            VideoPlaybackValue.uninitialized();
-      },
-      child: AnimatedSwitcher(
-        duration: const Duration(milliseconds: 400),
-        child: Stack(
-          children: [
-            Visibility(
-              visible: controller == null,
-              child: Stack(
-                children: [
-                  if (placeholder != null) placeholder!,
-                  const Positioned.fill(
-                    child: Center(
-                      child: DelayedLoadingIndicator(
-                        fadeInDuration: Duration(milliseconds: 500),
-                      ),
-                    ),
-                  ),
-                ],
-              ),
-            ),
-            if (controller != null)
-              SizedBox(
-                height: size.height,
-                width: size.width,
-                child: VideoPlayerViewer(
-                  controller: controller,
-                  isMotionVideo: isMotionVideo,
-                  placeholder: placeholder,
-                  hideControlsTimer: hideControlsTimer,
-                  showControls: showControls,
-                  showDownloadingIndicator: showDownloadingIndicator,
-                  loopVideo: loopVideo,
-                ),
-              ),
-          ],
-        ),
-      ),
-    );
-  }
-}
diff --git a/mobile/lib/pages/editing/crop.page.dart b/mobile/lib/pages/editing/crop.page.dart
index 729b59ded5..dc467f5740 100644
--- a/mobile/lib/pages/editing/crop.page.dart
+++ b/mobile/lib/pages/editing/crop.page.dart
@@ -51,107 +51,109 @@ class CropImagePage extends HookWidget {
         ],
       ),
       backgroundColor: context.scaffoldBackgroundColor,
-      body: LayoutBuilder(
-        builder: (BuildContext context, BoxConstraints constraints) {
-          return Column(
-            children: [
-              Container(
-                padding: const EdgeInsets.only(top: 20),
-                width: constraints.maxWidth * 0.9,
-                height: constraints.maxHeight * 0.6,
-                child: CropImage(
-                  controller: cropController,
-                  image: image,
-                  gridColor: Colors.white,
-                ),
-              ),
-              Expanded(
-                child: Container(
-                  width: double.infinity,
-                  decoration: BoxDecoration(
-                    color: context.scaffoldBackgroundColor,
-                    borderRadius: const BorderRadius.only(
-                      topLeft: Radius.circular(20),
-                      topRight: Radius.circular(20),
-                    ),
+      body: SafeArea(
+        child: LayoutBuilder(
+          builder: (BuildContext context, BoxConstraints constraints) {
+            return Column(
+              children: [
+                Container(
+                  padding: const EdgeInsets.only(top: 20),
+                  width: constraints.maxWidth * 0.9,
+                  height: constraints.maxHeight * 0.6,
+                  child: CropImage(
+                    controller: cropController,
+                    image: image,
+                    gridColor: Colors.white,
                   ),
-                  child: Center(
-                    child: Column(
-                      mainAxisAlignment: MainAxisAlignment.center,
-                      children: [
-                        Padding(
-                          padding: const EdgeInsets.only(
-                            left: 20,
-                            right: 20,
-                            bottom: 10,
+                ),
+                Expanded(
+                  child: Container(
+                    width: double.infinity,
+                    decoration: BoxDecoration(
+                      color: context.scaffoldBackgroundColor,
+                      borderRadius: const BorderRadius.only(
+                        topLeft: Radius.circular(20),
+                        topRight: Radius.circular(20),
+                      ),
+                    ),
+                    child: Center(
+                      child: Column(
+                        mainAxisAlignment: MainAxisAlignment.center,
+                        children: [
+                          Padding(
+                            padding: const EdgeInsets.only(
+                              left: 20,
+                              right: 20,
+                              bottom: 10,
+                            ),
+                            child: Row(
+                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                              children: [
+                                IconButton(
+                                  icon: Icon(
+                                    Icons.rotate_left,
+                                    color: context.themeData.iconTheme.color,
+                                  ),
+                                  onPressed: () {
+                                    cropController.rotateLeft();
+                                  },
+                                ),
+                                IconButton(
+                                  icon: Icon(
+                                    Icons.rotate_right,
+                                    color: context.themeData.iconTheme.color,
+                                  ),
+                                  onPressed: () {
+                                    cropController.rotateRight();
+                                  },
+                                ),
+                              ],
+                            ),
                           ),
-                          child: Row(
-                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                            children: [
-                              IconButton(
-                                icon: Icon(
-                                  Icons.rotate_left,
-                                  color: Theme.of(context).iconTheme.color,
-                                ),
-                                onPressed: () {
-                                  cropController.rotateLeft();
-                                },
+                          Row(
+                            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+                            children: <Widget>[
+                              _AspectRatioButton(
+                                cropController: cropController,
+                                aspectRatio: aspectRatio,
+                                ratio: null,
+                                label: 'Free',
                               ),
-                              IconButton(
-                                icon: Icon(
-                                  Icons.rotate_right,
-                                  color: Theme.of(context).iconTheme.color,
-                                ),
-                                onPressed: () {
-                                  cropController.rotateRight();
-                                },
+                              _AspectRatioButton(
+                                cropController: cropController,
+                                aspectRatio: aspectRatio,
+                                ratio: 1.0,
+                                label: '1:1',
+                              ),
+                              _AspectRatioButton(
+                                cropController: cropController,
+                                aspectRatio: aspectRatio,
+                                ratio: 16.0 / 9.0,
+                                label: '16:9',
+                              ),
+                              _AspectRatioButton(
+                                cropController: cropController,
+                                aspectRatio: aspectRatio,
+                                ratio: 3.0 / 2.0,
+                                label: '3:2',
+                              ),
+                              _AspectRatioButton(
+                                cropController: cropController,
+                                aspectRatio: aspectRatio,
+                                ratio: 7.0 / 5.0,
+                                label: '7:5',
                               ),
                             ],
                           ),
-                        ),
-                        Row(
-                          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
-                          children: <Widget>[
-                            _AspectRatioButton(
-                              cropController: cropController,
-                              aspectRatio: aspectRatio,
-                              ratio: null,
-                              label: 'Free',
-                            ),
-                            _AspectRatioButton(
-                              cropController: cropController,
-                              aspectRatio: aspectRatio,
-                              ratio: 1.0,
-                              label: '1:1',
-                            ),
-                            _AspectRatioButton(
-                              cropController: cropController,
-                              aspectRatio: aspectRatio,
-                              ratio: 16.0 / 9.0,
-                              label: '16:9',
-                            ),
-                            _AspectRatioButton(
-                              cropController: cropController,
-                              aspectRatio: aspectRatio,
-                              ratio: 3.0 / 2.0,
-                              label: '3:2',
-                            ),
-                            _AspectRatioButton(
-                              cropController: cropController,
-                              aspectRatio: aspectRatio,
-                              ratio: 7.0 / 5.0,
-                              label: '7:5',
-                            ),
-                          ],
-                        ),
-                      ],
+                        ],
+                      ),
                     ),
                   ),
                 ),
-              ),
-            ],
-          );
-        },
+              ],
+            );
+          },
+        ),
       ),
     );
   }
@@ -201,7 +203,7 @@ class _AspectRatioButton extends StatelessWidget {
             iconData,
             color: aspectRatio.value == ratio
                 ? context.primaryColor
-                : Theme.of(context).iconTheme.color,
+                : context.themeData.iconTheme.color,
           ),
           onPressed: () {
             cropController.crop = const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9);
diff --git a/mobile/lib/pages/editing/edit.page.dart b/mobile/lib/pages/editing/edit.page.dart
index c81e84877b..385140eb59 100644
--- a/mobile/lib/pages/editing/edit.page.dart
+++ b/mobile/lib/pages/editing/edit.page.dart
@@ -1,4 +1,3 @@
-import 'dart:io';
 import 'dart:typed_data';
 import 'dart:async';
 import 'dart:ui';
@@ -8,11 +7,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:immich_mobile/widgets/common/immich_image.dart';
+import 'package:immich_mobile/repositories/file_media.repository.dart';
 import 'package:immich_mobile/widgets/common/immich_toast.dart';
 import 'package:auto_route/auto_route.dart';
 import 'package:immich_mobile/routing/router.dart';
-import 'package:photo_manager/photo_manager.dart';
 import 'package:immich_mobile/providers/album/album.provider.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:path/path.dart' as p;
@@ -67,12 +65,12 @@ class EditImagePage extends ConsumerWidget {
   ) async {
     try {
       final Uint8List imageData = await _imageToUint8List(image);
-      await PhotoManager.editor.saveImage(
-        imageData,
-        title: "${p.withoutExtension(asset.fileName)}_edited.jpg",
-      );
-      await ref.read(albumProvider.notifier).getDeviceAlbums();
-      Navigator.of(context).popUntil((route) => route.isFirst);
+      await ref.read(fileMediaRepositoryProvider).saveImage(
+            imageData,
+            title: "${p.withoutExtension(asset.fileName)}_edited.jpg",
+          );
+      await ref.read(albumProvider.notifier).refreshDeviceAlbums();
+      context.navigator.popUntil((route) => route.isFirst);
       ImmichToast.show(
         durationInSecond: 3,
         context: context,
@@ -91,9 +89,6 @@ class EditImagePage extends ConsumerWidget {
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    final Image imageWidget =
-        Image(image: ImmichImage.imageProvider(asset: asset));
-
     return Scaffold(
       appBar: AppBar(
         title: Text("edit_image_title".tr()),
@@ -104,8 +99,7 @@ class EditImagePage extends ConsumerWidget {
             color: context.primaryColor,
             size: 24,
           ),
-          onPressed: () =>
-              Navigator.of(context).popUntil((route) => route.isFirst),
+          onPressed: () => context.navigator.popUntil((route) => route.isFirst),
         ),
         actions: <Widget>[
           TextButton(
@@ -125,8 +119,8 @@ class EditImagePage extends ConsumerWidget {
       body: Center(
         child: ConstrainedBox(
           constraints: BoxConstraints(
-            maxHeight: MediaQuery.of(context).size.height * 0.7,
-            maxWidth: MediaQuery.of(context).size.width * 0.9,
+            maxHeight: context.height * 0.7,
+            maxWidth: context.width * 0.9,
           ),
           child: Container(
             decoration: BoxDecoration(
@@ -157,24 +151,48 @@ class EditImagePage extends ConsumerWidget {
           color: context.scaffoldBackgroundColor,
           borderRadius: BorderRadius.circular(30),
         ),
-        child: Column(
-          mainAxisAlignment: MainAxisAlignment.center,
+        child: Row(
+          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
           children: <Widget>[
-            IconButton(
-              icon: Icon(
-                Platform.isAndroid
-                    ? Icons.crop_rotate_rounded
-                    : Icons.crop_rotate_rounded,
-                color: Theme.of(context).iconTheme.color,
-                size: 25,
-              ),
-              onPressed: () {
-                context.pushRoute(
-                  CropImageRoute(asset: asset, image: imageWidget),
-                );
-              },
+            Column(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: <Widget>[
+                IconButton(
+                  icon: Icon(
+                    Icons.crop_rotate_rounded,
+                    color: context.themeData.iconTheme.color,
+                    size: 25,
+                  ),
+                  onPressed: () {
+                    context.pushRoute(
+                      CropImageRoute(asset: asset, image: image),
+                    );
+                  },
+                ),
+                Text("crop".tr(), style: context.textTheme.displayMedium),
+              ],
+            ),
+            Column(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: <Widget>[
+                IconButton(
+                  icon: Icon(
+                    Icons.filter,
+                    color: context.themeData.iconTheme.color,
+                    size: 25,
+                  ),
+                  onPressed: () {
+                    context.pushRoute(
+                      FilterImageRoute(
+                        asset: asset,
+                        image: image,
+                      ),
+                    );
+                  },
+                ),
+                Text("filter".tr(), style: context.textTheme.displayMedium),
+              ],
             ),
-            Text("crop".tr(), style: context.textTheme.displayMedium),
           ],
         ),
       ),
diff --git a/mobile/lib/pages/editing/filter.page.dart b/mobile/lib/pages/editing/filter.page.dart
new file mode 100644
index 0000000000..f8b1f180df
--- /dev/null
+++ b/mobile/lib/pages/editing/filter.page.dart
@@ -0,0 +1,187 @@
+import 'dart:async';
+import 'dart:ui' as ui;
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/constants/filters.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:auto_route/auto_route.dart';
+import 'package:immich_mobile/routing/router.dart';
+
+/// A widget for filtering an image.
+/// This widget uses [HookWidget] to manage its lifecycle and state. It allows
+/// users to add filters to an image and then navigate to the [EditImagePage] with the
+/// final composition.'
+@RoutePage()
+class FilterImagePage extends HookWidget {
+  final Image image;
+  final Asset asset;
+
+  const FilterImagePage({
+    super.key,
+    required this.image,
+    required this.asset,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final colorFilter = useState<ColorFilter>(filters[0]);
+    final selectedFilterIndex = useState<int>(0);
+
+    Future<ui.Image> createFilteredImage(
+      ui.Image inputImage,
+      ColorFilter filter,
+    ) {
+      final completer = Completer<ui.Image>();
+      final size =
+          Size(inputImage.width.toDouble(), inputImage.height.toDouble());
+      final recorder = ui.PictureRecorder();
+      final canvas = Canvas(recorder);
+
+      final paint = Paint()..colorFilter = filter;
+      canvas.drawImage(inputImage, Offset.zero, paint);
+
+      recorder
+          .endRecording()
+          .toImage(size.width.round(), size.height.round())
+          .then((image) {
+        completer.complete(image);
+      });
+
+      return completer.future;
+    }
+
+    void applyFilter(ColorFilter filter, int index) {
+      colorFilter.value = filter;
+      selectedFilterIndex.value = index;
+    }
+
+    Future<Image> applyFilterAndConvert(ColorFilter filter) async {
+      final completer = Completer<ui.Image>();
+      image.image.resolve(ImageConfiguration.empty).addListener(
+        ImageStreamListener((ImageInfo info, bool _) {
+          completer.complete(info.image);
+        }),
+      );
+      final uiImage = await completer.future;
+
+      final filteredUiImage = await createFilteredImage(uiImage, filter);
+      final byteData =
+          await filteredUiImage.toByteData(format: ui.ImageByteFormat.png);
+      final pngBytes = byteData!.buffer.asUint8List();
+
+      return Image.memory(pngBytes, fit: BoxFit.contain);
+    }
+
+    return Scaffold(
+      appBar: AppBar(
+        backgroundColor: context.scaffoldBackgroundColor,
+        title: Text("filter".tr()),
+        leading: CloseButton(color: context.primaryColor),
+        actions: [
+          IconButton(
+            icon: Icon(
+              Icons.done_rounded,
+              color: context.primaryColor,
+              size: 24,
+            ),
+            onPressed: () async {
+              final filteredImage =
+                  await applyFilterAndConvert(colorFilter.value);
+              context.pushRoute(
+                EditImageRoute(
+                  asset: asset,
+                  image: filteredImage,
+                  isEdited: true,
+                ),
+              );
+            },
+          ),
+        ],
+      ),
+      backgroundColor: context.scaffoldBackgroundColor,
+      body: Column(
+        children: [
+          SizedBox(
+            height: context.height * 0.7,
+            child: Center(
+              child: ColorFiltered(
+                colorFilter: colorFilter.value,
+                child: image,
+              ),
+            ),
+          ),
+          SizedBox(
+            height: 120,
+            child: ListView.builder(
+              scrollDirection: Axis.horizontal,
+              itemCount: filters.length,
+              itemBuilder: (context, index) {
+                return Padding(
+                  padding: const EdgeInsets.symmetric(horizontal: 8.0),
+                  child: _FilterButton(
+                    image: image,
+                    label: filterNames[index],
+                    filter: filters[index],
+                    isSelected: selectedFilterIndex.value == index,
+                    onTap: () => applyFilter(filters[index], index),
+                  ),
+                );
+              },
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _FilterButton extends StatelessWidget {
+  final Image image;
+  final String label;
+  final ColorFilter filter;
+  final bool isSelected;
+  final VoidCallback onTap;
+
+  const _FilterButton({
+    required this.image,
+    required this.label,
+    required this.filter,
+    required this.isSelected,
+    required this.onTap,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: [
+        GestureDetector(
+          onTap: onTap,
+          child: Container(
+            width: 80,
+            height: 80,
+            decoration: BoxDecoration(
+              borderRadius: BorderRadius.circular(10),
+              border: isSelected
+                  ? Border.all(color: context.primaryColor, width: 3)
+                  : null,
+            ),
+            child: ClipRRect(
+              borderRadius: BorderRadius.circular(10),
+              child: ColorFiltered(
+                colorFilter: filter,
+                child: FittedBox(
+                  fit: BoxFit.cover,
+                  child: image,
+                ),
+              ),
+            ),
+          ),
+        ),
+        const SizedBox(height: 10),
+        Text(label, style: context.themeData.textTheme.bodyMedium),
+      ],
+    );
+  }
+}
diff --git a/mobile/lib/pages/library/favorite.page.dart b/mobile/lib/pages/library/favorite.page.dart
index 7462dc8f21..cc422f88c7 100644
--- a/mobile/lib/pages/library/favorite.page.dart
+++ b/mobile/lib/pages/library/favorite.page.dart
@@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/providers/favorite_provider.dart';
+import 'package:immich_mobile/providers/favorite.provider.dart';
 import 'package:immich_mobile/providers/multiselect.provider.dart';
 import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
 
diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart
index 5f03ed6871..92fe8cec17 100644
--- a/mobile/lib/pages/library/library.page.dart
+++ b/mobile/lib/pages/library/library.page.dart
@@ -1,329 +1,430 @@
 import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/user.entity.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/providers/album/album.provider.dart';
-import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
-import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
-import 'package:immich_mobile/routing/router.dart';
+import 'package:immich_mobile/providers/partner.provider.dart';
+import 'package:immich_mobile/providers/search/people.provider.dart';
 import 'package:immich_mobile/providers/server_info.provider.dart';
+import 'package:immich_mobile/routing/router.dart';
+import 'package:immich_mobile/services/api.service.dart';
+import 'package:immich_mobile/utils/image_url_builder.dart';
+import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
 import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
+import 'package:immich_mobile/widgets/common/user_avatar.dart';
+import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
 
 @RoutePage()
-class LibraryPage extends HookConsumerWidget {
+class LibraryPage extends ConsumerWidget {
   const LibraryPage({super.key});
-
   @override
   Widget build(BuildContext context, WidgetRef ref) {
+    context.locale;
     final trashEnabled =
         ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
-    final albums = ref.watch(albumProvider);
-    final albumSortOption = ref.watch(albumSortByOptionsProvider);
-    final albumSortIsReverse = ref.watch(albumSortOrderProvider);
 
-    useEffect(
-      () {
-        ref.read(albumProvider.notifier).getAllAlbums();
-        return null;
-      },
-      [],
-    );
-
-    Widget buildSortButton() {
-      return PopupMenuButton(
-        position: PopupMenuPosition.over,
-        itemBuilder: (BuildContext context) {
-          return AlbumSortMode.values
-              .map<PopupMenuEntry<AlbumSortMode>>((option) {
-            final selected = albumSortOption == option;
-            return PopupMenuItem(
-              value: option,
+    return Scaffold(
+      appBar: const ImmichAppBar(),
+      body: Padding(
+        padding: const EdgeInsets.symmetric(horizontal: 16),
+        child: ListView(
+          shrinkWrap: true,
+          children: [
+            Padding(
+              padding: const EdgeInsets.only(top: 16.0),
               child: Row(
                 children: [
-                  Padding(
-                    padding: const EdgeInsets.only(right: 12.0),
-                    child: Icon(
-                      Icons.check,
-                      color:
-                          selected ? context.primaryColor : Colors.transparent,
-                    ),
+                  ActionButton(
+                    onPressed: () => context.pushRoute(const FavoritesRoute()),
+                    icon: Icons.favorite_outline_rounded,
+                    label: 'favorites'.tr(),
                   ),
-                  Text(
-                    option.label.tr(),
-                    style: TextStyle(
-                      color: selected ? context.primaryColor : null,
-                      fontSize: 14.0,
-                    ),
+                  const SizedBox(width: 8),
+                  ActionButton(
+                    onPressed: () => context.pushRoute(const ArchiveRoute()),
+                    icon: Icons.archive_outlined,
+                    label: 'archived'.tr(),
                   ),
                 ],
               ),
-            );
-          }).toList();
-        },
-        onSelected: (AlbumSortMode value) {
-          final selected = albumSortOption == value;
-          // Switch direction
-          if (selected) {
-            ref
-                .read(albumSortOrderProvider.notifier)
-                .changeSortDirection(!albumSortIsReverse);
-          } else {
-            ref.read(albumSortByOptionsProvider.notifier).changeSortMode(value);
-          }
-        },
-        child: Row(
-          children: [
-            Padding(
-              padding: const EdgeInsets.only(right: 5),
-              child: Icon(
-                albumSortIsReverse
-                    ? Icons.arrow_downward_rounded
-                    : Icons.arrow_upward_rounded,
-                size: 14,
-                color: context.primaryColor,
-              ),
             ),
-            Text(
-              albumSortOption.label.tr(),
-              style: context.textTheme.labelLarge?.copyWith(
-                color: context.primaryColor,
-              ),
+            const SizedBox(height: 8),
+            Row(
+              children: [
+                ActionButton(
+                  onPressed: () => context.pushRoute(const SharedLinkRoute()),
+                  icon: Icons.link_outlined,
+                  label: 'shared_links'.tr(),
+                ),
+                SizedBox(width: trashEnabled ? 8 : 0),
+                trashEnabled
+                    ? ActionButton(
+                        onPressed: () => context.pushRoute(const TrashRoute()),
+                        icon: Icons.delete_outline_rounded,
+                        label: 'trash'.tr(),
+                      )
+                    : const SizedBox.shrink(),
+              ],
+            ),
+            const SizedBox(height: 12),
+            const Wrap(
+              spacing: 8,
+              runSpacing: 8,
+              children: [
+                PeopleCollectionCard(),
+                PlacesCollectionCard(),
+                LocalAlbumsCollectionCard(),
+              ],
+            ),
+            const SizedBox(height: 12),
+            const QuickAccessButtons(),
+            const SizedBox(
+              height: 32,
             ),
           ],
         ),
-      );
-    }
-
-    Widget buildCreateAlbumButton() {
-      return LayoutBuilder(
-        builder: (context, constraints) {
-          var cardSize = constraints.maxWidth;
-
-          return GestureDetector(
-            onTap: () =>
-                context.pushRoute(CreateAlbumRoute(isSharedAlbum: false)),
-            child: Padding(
-              padding:
-                  const EdgeInsets.only(bottom: 32), // Adjust padding to suit
-              child: Column(
-                mainAxisAlignment: MainAxisAlignment.start,
-                crossAxisAlignment: CrossAxisAlignment.start,
-                children: [
-                  Container(
-                    width: cardSize,
-                    height: cardSize,
-                    decoration: BoxDecoration(
-                      color: context.colorScheme.surfaceContainer,
-                      borderRadius: const BorderRadius.all(Radius.circular(20)),
-                    ),
-                    child: Center(
-                      child: Icon(
-                        Icons.add_rounded,
-                        size: 28,
-                        color: context.primaryColor,
-                      ),
-                    ),
-                  ),
-                  Padding(
-                    padding: const EdgeInsets.only(
-                      top: 8.0,
-                      bottom: 16,
-                    ),
-                    child: Text(
-                      'library_page_new_album',
-                      style: context.textTheme.labelLarge?.copyWith(
-                        color: context.colorScheme.onSurface,
-                      ),
-                    ).tr(),
-                  ),
-                ],
-              ),
-            ),
-          );
-        },
-      );
-    }
-
-    Widget buildLibraryNavButton(
-      String label,
-      IconData icon,
-      Function() onClick,
-    ) {
-      return Expanded(
-        child: FilledButton.icon(
-          onPressed: onClick,
-          label: Padding(
-            padding: const EdgeInsets.only(left: 8.0),
-            child: Text(
-              label,
-              style: TextStyle(
-                color: context.colorScheme.onSurface,
-              ),
-            ),
-          ),
-          style: FilledButton.styleFrom(
-            elevation: 0,
-            padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
-            backgroundColor: context.colorScheme.surfaceContainer,
-            alignment: Alignment.centerLeft,
-            shape: const RoundedRectangleBorder(
-              borderRadius: BorderRadius.all(Radius.circular(20)),
-            ),
-          ),
-          icon: Icon(
-            icon,
-            color: context.primaryColor,
-          ),
-        ),
-      );
-    }
-
-    final remote = albums.where((a) => a.isRemote).toList();
-    final sorted = albumSortOption.sortFn(remote, albumSortIsReverse);
-    final local = albums.where((a) => a.isLocal).toList();
-
-    Widget? shareTrashButton() {
-      return trashEnabled
-          ? InkWell(
-              onTap: () => context.pushRoute(const TrashRoute()),
-              borderRadius: const BorderRadius.all(Radius.circular(12)),
-              child: Icon(
-                Icons.delete_rounded,
-                size: 25,
-                semanticLabel: 'profile_drawer_trash'.tr(),
-              ),
-            )
-          : null;
-    }
-
-    return Scaffold(
-      appBar: ImmichAppBar(
-        action: shareTrashButton(),
       ),
-      body: CustomScrollView(
-        slivers: [
-          SliverToBoxAdapter(
-            child: Padding(
-              padding: const EdgeInsets.only(
-                left: 12.0,
-                right: 12.0,
-                top: 24.0,
-                bottom: 12.0,
-              ),
-              child: Row(
-                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
-                children: [
-                  buildLibraryNavButton(
-                      "library_page_favorites".tr(), Icons.favorite_border, () {
-                    context.navigateTo(const FavoritesRoute());
-                  }),
-                  const SizedBox(width: 12.0),
-                  buildLibraryNavButton(
-                      "library_page_archive".tr(), Icons.archive_outlined, () {
-                    context.navigateTo(const ArchiveRoute());
-                  }),
-                ],
-              ),
-            ),
-          ),
-          SliverToBoxAdapter(
-            child: Padding(
-              padding: const EdgeInsets.only(
-                top: 12.0,
-                left: 12.0,
-                right: 12.0,
-                bottom: 20.0,
-              ),
-              child: Row(
-                mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                children: [
-                  Text(
-                    'library_page_albums',
-                    style: context.textTheme.bodyLarge?.copyWith(
-                      color: context.colorScheme.onSurface,
-                      fontWeight: FontWeight.w500,
-                    ),
-                  ).tr(),
-                  buildSortButton(),
-                ],
-              ),
-            ),
-          ),
-          SliverPadding(
-            padding: const EdgeInsets.all(12.0),
-            sliver: SliverGrid(
-              gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
-                maxCrossAxisExtent: 250,
-                mainAxisSpacing: 12,
-                crossAxisSpacing: 12,
-                childAspectRatio: .7,
-              ),
-              delegate: SliverChildBuilderDelegate(
-                childCount: sorted.length + 1,
-                (context, index) {
-                  if (index == 0) {
-                    return buildCreateAlbumButton();
-                  }
+    );
+  }
+}
 
-                  return AlbumThumbnailCard(
-                    album: sorted[index - 1],
-                    onTap: () => context.pushRoute(
-                      AlbumViewerRoute(
-                        albumId: sorted[index - 1].id,
-                      ),
-                    ),
-                  );
-                },
+class QuickAccessButtons extends ConsumerWidget {
+  const QuickAccessButtons({super.key});
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final partners = ref.watch(partnerSharedWithProvider);
+
+    return Container(
+      decoration: BoxDecoration(
+        border: Border.all(
+          color: context.colorScheme.onSurface.withAlpha(10),
+          width: 1,
+        ),
+        borderRadius: BorderRadius.circular(20),
+        gradient: LinearGradient(
+          colors: [
+            context.colorScheme.primary.withAlpha(10),
+            context.colorScheme.primary.withAlpha(15),
+            context.colorScheme.primary.withAlpha(20),
+          ],
+          begin: Alignment.topCenter,
+          end: Alignment.bottomCenter,
+        ),
+      ),
+      child: ListView(
+        shrinkWrap: true,
+        physics: const NeverScrollableScrollPhysics(),
+        children: [
+          ListTile(
+            shape: RoundedRectangleBorder(
+              borderRadius: BorderRadius.only(
+                topLeft: const Radius.circular(20),
+                topRight: const Radius.circular(20),
+                bottomLeft: Radius.circular(partners.isEmpty ? 20 : 0),
+                bottomRight: Radius.circular(partners.isEmpty ? 20 : 0),
               ),
             ),
-          ),
-          SliverToBoxAdapter(
-            child: Padding(
-              padding: const EdgeInsets.only(
-                top: 12.0,
-                left: 12.0,
-                right: 12.0,
-                bottom: 20.0,
-              ),
-              child: Row(
-                mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                children: [
-                  Text(
-                    'library_page_device_albums',
-                    style: context.textTheme.bodyLarge?.copyWith(
-                      fontWeight: FontWeight.w500,
-                    ),
-                  ).tr(),
-                ],
-              ),
-            ),
-          ),
-          SliverPadding(
-            padding: const EdgeInsets.all(12.0),
-            sliver: SliverGrid(
-              gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
-                maxCrossAxisExtent: 250,
-                mainAxisSpacing: 12,
-                crossAxisSpacing: 12,
-                childAspectRatio: .7,
-              ),
-              delegate: SliverChildBuilderDelegate(
-                childCount: local.length,
-                (context, index) => AlbumThumbnailCard(
-                  album: local[index],
-                  onTap: () => context.pushRoute(
-                    AlbumViewerRoute(
-                      albumId: local[index].id,
-                    ),
-                  ),
-                ),
+            leading: const Icon(
+              Icons.group_outlined,
+              size: 26,
+            ),
+            title: Text(
+              'partners'.tr(),
+              style: context.textTheme.titleSmall?.copyWith(
+                fontWeight: FontWeight.w500,
               ),
             ),
+            onTap: () => context.pushRoute(const PartnerRoute()),
           ),
+          PartnerList(partners: partners),
         ],
       ),
     );
   }
 }
+
+class PartnerList extends ConsumerWidget {
+  const PartnerList({super.key, required this.partners});
+
+  final List<User> partners;
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    return ListView.builder(
+      physics: const NeverScrollableScrollPhysics(),
+      itemCount: partners.length,
+      shrinkWrap: true,
+      itemBuilder: (context, index) {
+        final partner = partners[index];
+        final isLastItem = index == partners.length - 1;
+        return ListTile(
+          shape: RoundedRectangleBorder(
+            borderRadius: BorderRadius.only(
+              bottomLeft: Radius.circular(isLastItem ? 20 : 0),
+              bottomRight: Radius.circular(isLastItem ? 20 : 0),
+            ),
+          ),
+          contentPadding: const EdgeInsets.only(
+            left: 12.0,
+            right: 18.0,
+          ),
+          leading: userAvatar(context, partner, radius: 16),
+          title: const Text(
+            "partner_list_user_photos",
+            style: TextStyle(
+              fontWeight: FontWeight.w500,
+            ),
+          ).tr(
+            namedArgs: {
+              'user': partner.name,
+            },
+          ),
+          onTap: () => context.pushRoute(
+            (PartnerDetailRoute(partner: partner)),
+          ),
+        );
+      },
+    );
+  }
+}
+
+class PeopleCollectionCard extends ConsumerWidget {
+  const PeopleCollectionCard({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final people = ref.watch(getAllPeopleProvider);
+    return LayoutBuilder(
+      builder: (context, constraints) {
+        final isTablet = constraints.maxWidth > 600;
+        final widthFactor = isTablet ? 0.25 : 0.5;
+        final size = context.width * widthFactor - 20.0;
+
+        return GestureDetector(
+          onTap: () => context.pushRoute(const PeopleCollectionRoute()),
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Container(
+                height: size,
+                width: size,
+                decoration: BoxDecoration(
+                  borderRadius: BorderRadius.circular(20),
+                  gradient: LinearGradient(
+                    colors: [
+                      context.colorScheme.primary.withAlpha(30),
+                      context.colorScheme.primary.withAlpha(25),
+                    ],
+                    begin: Alignment.topCenter,
+                    end: Alignment.bottomCenter,
+                  ),
+                ),
+                child: people.widgetWhen(
+                  onData: (people) {
+                    return GridView.count(
+                      crossAxisCount: 2,
+                      padding: const EdgeInsets.all(12),
+                      crossAxisSpacing: 8,
+                      mainAxisSpacing: 8,
+                      physics: const NeverScrollableScrollPhysics(),
+                      children: people.take(4).map((person) {
+                        return CircleAvatar(
+                          backgroundImage: NetworkImage(
+                            getFaceThumbnailUrl(person.id),
+                            headers: ApiService.getRequestHeaders(),
+                          ),
+                        );
+                      }).toList(),
+                    );
+                  },
+                ),
+              ),
+              Padding(
+                padding: const EdgeInsets.all(8.0),
+                child: Text(
+                  'people'.tr(),
+                  style: context.textTheme.titleSmall?.copyWith(
+                    color: context.colorScheme.onSurface,
+                    fontWeight: FontWeight.w500,
+                  ),
+                ),
+              ),
+            ],
+          ),
+        );
+      },
+    );
+  }
+}
+
+class LocalAlbumsCollectionCard extends HookConsumerWidget {
+  const LocalAlbumsCollectionCard({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final albums = ref.watch(localAlbumsProvider);
+
+    return LayoutBuilder(
+      builder: (context, constraints) {
+        final isTablet = constraints.maxWidth > 600;
+        final widthFactor = isTablet ? 0.25 : 0.5;
+        final size = context.width * widthFactor - 20.0;
+
+        return GestureDetector(
+          onTap: () => context.pushRoute(
+            const LocalAlbumsRoute(),
+          ),
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Container(
+                height: size,
+                width: size,
+                decoration: BoxDecoration(
+                  borderRadius: BorderRadius.circular(20),
+                  gradient: LinearGradient(
+                    colors: [
+                      context.colorScheme.primary.withAlpha(30),
+                      context.colorScheme.primary.withAlpha(25),
+                    ],
+                    begin: Alignment.topCenter,
+                    end: Alignment.bottomCenter,
+                  ),
+                ),
+                child: GridView.count(
+                  crossAxisCount: 2,
+                  padding: const EdgeInsets.all(12),
+                  crossAxisSpacing: 8,
+                  mainAxisSpacing: 8,
+                  physics: const NeverScrollableScrollPhysics(),
+                  children: albums.take(4).map((album) {
+                    return AlbumThumbnailCard(
+                      album: album,
+                      showTitle: false,
+                    );
+                  }).toList(),
+                ),
+              ),
+              Padding(
+                padding: const EdgeInsets.all(8.0),
+                child: Text(
+                  'on_this_device'.tr(),
+                  style: context.textTheme.titleSmall?.copyWith(
+                    color: context.colorScheme.onSurface,
+                    fontWeight: FontWeight.w500,
+                  ),
+                ),
+              ),
+            ],
+          ),
+        );
+      },
+    );
+  }
+}
+
+class PlacesCollectionCard extends StatelessWidget {
+  const PlacesCollectionCard({super.key});
+  @override
+  Widget build(BuildContext context) {
+    return LayoutBuilder(
+      builder: (context, constraints) {
+        final isTablet = constraints.maxWidth > 600;
+        final widthFactor = isTablet ? 0.25 : 0.5;
+        final size = context.width * widthFactor - 20.0;
+
+        return GestureDetector(
+          onTap: () => context.pushRoute(const PlacesCollectionRoute()),
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Container(
+                height: size,
+                width: size,
+                decoration: BoxDecoration(
+                  borderRadius: BorderRadius.circular(20),
+                  color: context.colorScheme.secondaryContainer.withAlpha(100),
+                ),
+                child: IgnorePointer(
+                  child: MapThumbnail(
+                    zoom: 8,
+                    centre: const LatLng(
+                      21.44950,
+                      -157.91959,
+                    ),
+                    showAttribution: false,
+                    themeMode:
+                        context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
+                  ),
+                ),
+              ),
+              Padding(
+                padding: const EdgeInsets.all(8.0),
+                child: Text(
+                  'places'.tr(),
+                  style: context.textTheme.titleSmall?.copyWith(
+                    color: context.colorScheme.onSurface,
+                    fontWeight: FontWeight.w500,
+                  ),
+                ),
+              ),
+            ],
+          ),
+        );
+      },
+    );
+  }
+}
+
+class ActionButton extends StatelessWidget {
+  final VoidCallback onPressed;
+  final IconData icon;
+  final String label;
+
+  const ActionButton({
+    super.key,
+    required this.onPressed,
+    required this.icon,
+    required this.label,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Expanded(
+      child: FilledButton.icon(
+        onPressed: onPressed,
+        label: Padding(
+          padding: const EdgeInsets.only(left: 4.0),
+          child: Text(
+            label,
+            style: TextStyle(
+              color: context.colorScheme.onSurface,
+              fontSize: 15,
+            ),
+          ),
+        ),
+        style: FilledButton.styleFrom(
+          elevation: 0,
+          padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
+          backgroundColor: context.colorScheme.surfaceContainerLow,
+          alignment: Alignment.centerLeft,
+          shape: RoundedRectangleBorder(
+            borderRadius: const BorderRadius.all(Radius.circular(25)),
+            side: BorderSide(
+              color: context.colorScheme.onSurface.withAlpha(10),
+              width: 1,
+            ),
+          ),
+        ),
+        icon: Icon(
+          icon,
+          color: context.primaryColor,
+        ),
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/pages/library/local_albums.page.dart b/mobile/lib/pages/library/local_albums.page.dart
new file mode 100644
index 0000000000..164ea3bad8
--- /dev/null
+++ b/mobile/lib/pages/library/local_albums.page.dart
@@ -0,0 +1,55 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/pages/common/large_leading_tile.dart';
+import 'package:immich_mobile/providers/album/album.provider.dart';
+import 'package:immich_mobile/routing/router.dart';
+import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
+
+@RoutePage()
+class LocalAlbumsPage extends HookConsumerWidget {
+  const LocalAlbumsPage({super.key});
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final albums = ref.watch(localAlbumsProvider);
+
+    return Scaffold(
+      appBar: AppBar(
+        title: Text('on_this_device'.tr()),
+      ),
+      body: ListView.builder(
+        padding: const EdgeInsets.all(18.0),
+        itemCount: albums.length,
+        itemBuilder: (context, index) {
+          return Padding(
+            padding: const EdgeInsets.only(bottom: 8.0),
+            child: LargeLeadingTile(
+              leadingPadding: const EdgeInsets.only(
+                right: 16,
+              ),
+              leading: ClipRRect(
+                borderRadius: const BorderRadius.all(Radius.circular(15)),
+                child: ImmichThumbnail(
+                  asset: albums[index].thumbnail.value,
+                  width: 80,
+                  height: 80,
+                ),
+              ),
+              title: Text(
+                albums[index].name,
+                style: context.textTheme.titleSmall?.copyWith(
+                  fontWeight: FontWeight.w600,
+                ),
+              ),
+              subtitle: Text('${albums[index].assetCount} items'),
+              onTap: () => context
+                  .pushRoute(AlbumViewerRoute(albumId: albums[index].id)),
+            ),
+          );
+        },
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/pages/sharing/partner/partner.page.dart b/mobile/lib/pages/library/partner/partner.page.dart
similarity index 93%
rename from mobile/lib/pages/sharing/partner/partner.page.dart
rename to mobile/lib/pages/library/partner/partner.page.dart
index 8dd31023c7..1e9e801210 100644
--- a/mobile/lib/pages/sharing/partner/partner.page.dart
+++ b/mobile/lib/pages/library/partner/partner.page.dart
@@ -86,12 +86,10 @@ class PartnerPage extends HookConsumerWidget {
         children: [
           Padding(
             padding: const EdgeInsets.only(left: 16.0, top: 16.0),
-            child: const Text(
+            child: Text(
               "partner_page_shared_to_title",
-              style: TextStyle(
-                fontSize: 14,
-                color: Colors.grey,
-                fontWeight: FontWeight.bold,
+              style: context.textTheme.titleSmall?.copyWith(
+                color: context.colorScheme.onSurface.withAlpha(200),
               ),
             ).tr(),
           ),
@@ -104,10 +102,7 @@ class PartnerPage extends HookConsumerWidget {
                   leading: userAvatar(context, users[index]),
                   title: Text(
                     users[index].email,
-                    style: const TextStyle(
-                      fontSize: 14,
-                      fontWeight: FontWeight.bold,
-                    ),
+                    style: context.textTheme.bodyLarge,
                   ),
                   trailing: IconButton(
                     icon: const Icon(Icons.person_remove),
@@ -148,7 +143,7 @@ class PartnerPage extends HookConsumerWidget {
 
     return Scaffold(
       appBar: AppBar(
-        title: const Text("partner_page_title").tr(),
+        title: const Text("partners").tr(),
         elevation: 0,
         centerTitle: false,
         actions: [
diff --git a/mobile/lib/pages/sharing/partner/partner_detail.page.dart b/mobile/lib/pages/library/partner/partner_detail.page.dart
similarity index 59%
rename from mobile/lib/pages/sharing/partner/partner_detail.page.dart
rename to mobile/lib/pages/library/partner/partner_detail.page.dart
index 8a2dd4b820..0874aacfa7 100644
--- a/mobile/lib/pages/sharing/partner/partner_detail.page.dart
+++ b/mobile/lib/pages/library/partner/partner_detail.page.dart
@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/providers/multiselect.provider.dart';
 import 'package:immich_mobile/providers/partner.provider.dart';
 import 'package:immich_mobile/entities/user.entity.dart';
@@ -22,7 +23,11 @@ class PartnerDetailPage extends HookConsumerWidget {
 
     useEffect(
       () {
-        ref.read(assetProvider.notifier).getAllAsset();
+        Future.microtask(
+          () async => {
+            await ref.read(assetProvider.notifier).getAllAsset(),
+          },
+        );
         return null;
       },
       [],
@@ -64,19 +69,47 @@ class PartnerDetailPage extends HookConsumerWidget {
               title: Text(partner.name),
               elevation: 0,
               centerTitle: false,
-              actions: [
-                IconButton(
-                  onPressed: toggleInTimeline,
-                  icon: Icon(
-                    inTimeline.value
-                        ? Icons.collections
-                        : Icons.collections_outlined,
-                  ),
-                  tooltip: "Show/hide photos on your main timeline",
-                ),
-              ],
             ),
       body: MultiselectGrid(
+        topWidget: Padding(
+          padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 16.0),
+          child: Container(
+            decoration: BoxDecoration(
+              border: Border.all(
+                color: context.colorScheme.onSurface.withAlpha(10),
+                width: 1,
+              ),
+              borderRadius: BorderRadius.circular(20),
+              gradient: LinearGradient(
+                colors: [
+                  context.colorScheme.primary.withAlpha(10),
+                  context.colorScheme.primary.withAlpha(15),
+                ],
+                begin: Alignment.topCenter,
+                end: Alignment.bottomCenter,
+              ),
+            ),
+            child: Padding(
+              padding: const EdgeInsets.all(8.0),
+              child: ListTile(
+                title: Text(
+                  "Show in timeline",
+                  style: context.textTheme.titleSmall?.copyWith(
+                    color: context.colorScheme.primary,
+                  ),
+                ),
+                subtitle: Text(
+                  "Show photos and videos from this user in your timeline",
+                  style: context.textTheme.bodyMedium,
+                ),
+                trailing: Switch(
+                  value: inTimeline.value,
+                  onChanged: (_) => toggleInTimeline(),
+                ),
+              ),
+            ),
+          ),
+        ),
         renderListProvider: assetsProvider(partner.isarId),
         onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(),
         deleteEnabled: false,
diff --git a/mobile/lib/pages/library/people/people_collection.page.dart b/mobile/lib/pages/library/people/people_collection.page.dart
new file mode 100644
index 0000000000..6c62d70058
--- /dev/null
+++ b/mobile/lib/pages/library/people/people_collection.page.dart
@@ -0,0 +1,113 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/providers/search/people.provider.dart';
+import 'package:immich_mobile/routing/router.dart';
+import 'package:immich_mobile/services/api.service.dart';
+import 'package:immich_mobile/utils/image_url_builder.dart';
+import 'package:immich_mobile/widgets/search/person_name_edit_form.dart';
+
+@RoutePage()
+class PeopleCollectionPage extends HookConsumerWidget {
+  const PeopleCollectionPage({super.key});
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final people = ref.watch(getAllPeopleProvider);
+    final headers = ApiService.getRequestHeaders();
+
+    showNameEditModel(
+      String personId,
+      String personName,
+    ) {
+      return showDialog(
+        context: context,
+        builder: (BuildContext context) {
+          return PersonNameEditForm(personId: personId, personName: personName);
+        },
+      );
+    }
+
+    return LayoutBuilder(
+      builder: (context, constraints) {
+        final isTablet = constraints.maxWidth > 600;
+        final isPortrait = context.orientation == Orientation.portrait;
+
+        return Scaffold(
+          appBar: AppBar(
+            title: Text('people'.tr()),
+          ),
+          body: people.when(
+            data: (people) {
+              return GridView.builder(
+                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+                  crossAxisCount: isTablet ? 6 : 3,
+                  childAspectRatio: 0.85,
+                  mainAxisSpacing: isPortrait && isTablet ? 36 : 0,
+                ),
+                padding: const EdgeInsets.symmetric(vertical: 32),
+                itemCount: people.length,
+                itemBuilder: (context, index) {
+                  final person = people[index];
+
+                  return Column(
+                    children: [
+                      GestureDetector(
+                        onTap: () {
+                          context.pushRoute(
+                            PersonResultRoute(
+                              personId: person.id,
+                              personName: person.name,
+                            ),
+                          );
+                        },
+                        child: Material(
+                          shape: const CircleBorder(side: BorderSide.none),
+                          elevation: 3,
+                          child: CircleAvatar(
+                            maxRadius: isTablet ? 120 / 2 : 96 / 2,
+                            backgroundImage: NetworkImage(
+                              getFaceThumbnailUrl(person.id),
+                              headers: headers,
+                            ),
+                          ),
+                        ),
+                      ),
+                      const SizedBox(height: 12),
+                      GestureDetector(
+                        onTap: () => showNameEditModel(person.id, person.name),
+                        child: person.name.isEmpty
+                            ? Text(
+                                'add_a_name'.tr(),
+                                style: context.textTheme.titleSmall?.copyWith(
+                                  fontWeight: FontWeight.w500,
+                                  color: context.colorScheme.primary,
+                                ),
+                              )
+                            : Padding(
+                                padding: const EdgeInsets.symmetric(
+                                  horizontal: 16.0,
+                                ),
+                                child: Text(
+                                  person.name,
+                                  overflow: TextOverflow.ellipsis,
+                                  style: context.textTheme.titleSmall?.copyWith(
+                                    fontWeight: FontWeight.w500,
+                                  ),
+                                ),
+                              ),
+                      ),
+                    ],
+                  );
+                },
+              );
+            },
+            error: (error, stack) => const Text("error"),
+            loading: () => const CircularProgressIndicator(),
+          ),
+        );
+      },
+    );
+  }
+}
diff --git a/mobile/lib/pages/library/places/places_collection.page.dart b/mobile/lib/pages/library/places/places_collection.page.dart
new file mode 100644
index 0000000000..f42febc373
--- /dev/null
+++ b/mobile/lib/pages/library/places/places_collection.page.dart
@@ -0,0 +1,125 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/models/search/search_filter.model.dart';
+import 'package:immich_mobile/pages/common/large_leading_tile.dart';
+import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
+import 'package:immich_mobile/routing/router.dart';
+import 'package:immich_mobile/services/api.service.dart';
+import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+
+@RoutePage()
+class PlacesCollectionPage extends HookConsumerWidget {
+  const PlacesCollectionPage({super.key});
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final places = ref.watch(getAllPlacesProvider);
+
+    return Scaffold(
+      appBar: AppBar(
+        title: Text('places'.tr()),
+      ),
+      body: ListView(
+        shrinkWrap: true,
+        children: [
+          Padding(
+            padding: const EdgeInsets.all(16.0),
+            child: SizedBox(
+              height: 200,
+              width: context.width,
+              child: MapThumbnail(
+                onTap: (_, __) => context.pushRoute(const MapRoute()),
+                zoom: 8,
+                centre: const LatLng(
+                  21.44950,
+                  -157.91959,
+                ),
+                showAttribution: false,
+                themeMode:
+                    context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
+              ),
+            ),
+          ),
+          places.when(
+            data: (places) {
+              return ListView.builder(
+                shrinkWrap: true,
+                physics: const NeverScrollableScrollPhysics(),
+                itemCount: places.length,
+                itemBuilder: (context, index) {
+                  final place = places[index];
+
+                  return PlaceTile(id: place.id, name: place.label);
+                },
+              );
+            },
+            error: (error, stask) => const Text('Error getting places'),
+            loading: () => const Center(child: CircularProgressIndicator()),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class PlaceTile extends StatelessWidget {
+  const PlaceTile({super.key, required this.id, required this.name});
+
+  final String id;
+  final String name;
+
+  @override
+  Widget build(BuildContext context) {
+    final thumbnailUrl =
+        '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail';
+
+    void navigateToPlace() {
+      context.pushRoute(
+        SearchRoute(
+          prefilter: SearchFilter(
+            people: {},
+            location: SearchLocationFilter(
+              city: name,
+            ),
+            camera: SearchCameraFilter(),
+            date: SearchDateFilter(),
+            display: SearchDisplayFilters(
+              isNotInAlbum: false,
+              isArchive: false,
+              isFavorite: false,
+            ),
+            mediaType: AssetType.other,
+          ),
+        ),
+      );
+    }
+
+    return LargeLeadingTile(
+      onTap: () => navigateToPlace(),
+      title: Text(
+        name,
+        style: context.textTheme.titleMedium?.copyWith(
+          fontWeight: FontWeight.w500,
+        ),
+      ),
+      leading: ClipRRect(
+        borderRadius: BorderRadius.circular(20),
+        child: CachedNetworkImage(
+          width: 80,
+          height: 80,
+          fit: BoxFit.cover,
+          imageUrl: thumbnailUrl,
+          httpHeaders: ApiService.getRequestHeaders(),
+          errorWidget: (context, url, error) =>
+              const Icon(Icons.image_not_supported_outlined),
+        ),
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/pages/sharing/shared_link/shared_link.page.dart b/mobile/lib/pages/library/shared_link/shared_link.page.dart
similarity index 100%
rename from mobile/lib/pages/sharing/shared_link/shared_link.page.dart
rename to mobile/lib/pages/library/shared_link/shared_link.page.dart
diff --git a/mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart b/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart
similarity index 99%
rename from mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart
rename to mobile/lib/pages/library/shared_link/shared_link_edit.page.dart
index 7f1008c655..82819c94bd 100644
--- a/mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart
+++ b/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart
@@ -279,7 +279,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
 
     void copyLinkToClipboard() {
       Clipboard.setData(ClipboardData(text: newShareLink.value)).then((_) {
-        ScaffoldMessenger.of(context).showSnackBar(
+        context.scaffoldMessenger.showSnackBar(
           SnackBar(
             content: Text(
               "shared_link_clipboard_copied_massage",
diff --git a/mobile/lib/pages/photos/memory.page.dart b/mobile/lib/pages/photos/memory.page.dart
index 3f86f5be08..74a94ed6ee 100644
--- a/mobile/lib/pages/photos/memory.page.dart
+++ b/mobile/lib/pages/photos/memory.page.dart
@@ -113,11 +113,15 @@ class MemoryPage extends HookConsumerWidget {
       }
 
       // Precache the asset
+      final size = MediaQuery.sizeOf(context);
       await precacheImage(
         ImmichImage.imageProvider(
           asset: asset,
+          width: size.width,
+          height: size.height,
         ),
         context,
+        size: size,
       );
     }
 
diff --git a/mobile/lib/pages/photos/photos.page.dart b/mobile/lib/pages/photos/photos.page.dart
index 3c5ff27296..30fe1ab3f2 100644
--- a/mobile/lib/pages/photos/photos.page.dart
+++ b/mobile/lib/pages/photos/photos.page.dart
@@ -7,7 +7,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/providers/album/album.provider.dart';
-import 'package:immich_mobile/providers/album/shared_album.provider.dart';
 import 'package:immich_mobile/providers/multiselect.provider.dart';
 import 'package:immich_mobile/widgets/memories/memory_lane.dart';
 import 'package:immich_mobile/providers/asset.provider.dart';
@@ -33,8 +32,7 @@ class PhotosPage extends HookConsumerWidget {
       () {
         ref.read(websocketProvider.notifier).connect();
         Future(() => ref.read(assetProvider.notifier).getAllAsset());
-        ref.read(albumProvider.notifier).getAllAlbums();
-        ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
+        Future(() => ref.read(albumProvider.notifier).refreshRemoteAlbums());
         ref.read(serverInfoProvider.notifier).getServerInfo();
         return;
       },
@@ -112,12 +110,12 @@ class PhotosPage extends HookConsumerWidget {
         AnimatedPositioned(
           duration: const Duration(milliseconds: 300),
           top: ref.watch(multiselectProvider)
-              ? -(kToolbarHeight + MediaQuery.of(context).padding.top)
+              ? -(kToolbarHeight + context.padding.top)
               : 0,
           left: 0,
           right: 0,
           child: Container(
-            height: kToolbarHeight + MediaQuery.of(context).padding.top,
+            height: kToolbarHeight + context.padding.top,
             color: context.themeData.appBarTheme.backgroundColor,
             child: const ImmichAppBar(),
           ),
diff --git a/mobile/lib/pages/search/map/map.page.dart b/mobile/lib/pages/search/map/map.page.dart
index d226ea55a3..52ce13f958 100644
--- a/mobile/lib/pages/search/map/map.page.dart
+++ b/mobile/lib/pages/search/map/map.page.dart
@@ -1,4 +1,5 @@
 import 'dart:math';
+
 import 'package:auto_route/auto_route.dart';
 import 'package:collection/collection.dart';
 import 'package:easy_localization/easy_localization.dart';
@@ -7,27 +8,29 @@ import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:geolocator/geolocator.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/latlngbounds_extension.dart';
 import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
 import 'package:immich_mobile/models/map/map_event.model.dart';
 import 'package:immich_mobile/models/map/map_marker.model.dart';
+import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
+import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
+import 'package:immich_mobile/providers/db.provider.dart';
 import 'package:immich_mobile/providers/map/map_marker.provider.dart';
 import 'package:immich_mobile/providers/map/map_state.provider.dart';
+import 'package:immich_mobile/routing/router.dart';
+import 'package:immich_mobile/utils/debounce.dart';
+import 'package:immich_mobile/utils/immich_loading_overlay.dart';
 import 'package:immich_mobile/utils/map_utils.dart';
 import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
+import 'package:immich_mobile/widgets/common/immich_toast.dart';
 import 'package:immich_mobile/widgets/map/map_app_bar.dart';
 import 'package:immich_mobile/widgets/map/map_asset_grid.dart';
 import 'package:immich_mobile/widgets/map/map_bottom_sheet.dart';
 import 'package:immich_mobile/widgets/map/map_theme_override.dart';
 import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart';
-import 'package:immich_mobile/routing/router.dart';
-import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:immich_mobile/providers/db.provider.dart';
-import 'package:immich_mobile/widgets/common/immich_toast.dart';
-import 'package:immich_mobile/utils/immich_loading_overlay.dart';
-import 'package:immich_mobile/utils/debounce.dart';
 import 'package:maplibre_gl/maplibre_gl.dart';
 
 @RoutePage()
@@ -98,8 +101,11 @@ class MapPage extends HookConsumerWidget {
 
     useEffect(
       () {
+        final currentAssetLink =
+            ref.read(currentAssetProvider.notifier).ref.keepAlive();
+
         loadMarkers();
-        return null;
+        return currentAssetLink.close;
       },
       [],
     );
@@ -185,6 +191,10 @@ class MapPage extends HookConsumerWidget {
         GroupAssetsBy.none,
       );
 
+      ref.read(currentAssetProvider.notifier).set(asset);
+      if (asset.isVideo) {
+        ref.read(showControlsProvider.notifier).show = false;
+      }
       context.pushRoute(
         GalleryViewerRoute(
           initialIndex: 0,
@@ -254,7 +264,7 @@ class MapPage extends HookConsumerWidget {
       selectedAssets.value = selected ? selection : {};
     }
 
-    return MapThemeOveride(
+    return MapThemeOverride(
       mapBuilder: (style) => context.isMobile
           // Single-column
           ? Scaffold(
@@ -304,7 +314,7 @@ class MapPage extends HookConsumerWidget {
                         ),
                         Positioned(
                           right: 0,
-                          bottom: MediaQuery.of(context).padding.bottom + 16,
+                          bottom: context.padding.bottom + 16,
                           child: ElevatedButton(
                             onPressed: onZoomToLocation,
                             style: ElevatedButton.styleFrom(
diff --git a/mobile/lib/pages/search/map/map_location_picker.page.dart b/mobile/lib/pages/search/map/map_location_picker.page.dart
index 2fd1e1ee9e..487de69a1e 100644
--- a/mobile/lib/pages/search/map/map_location_picker.page.dart
+++ b/mobile/lib/pages/search/map/map_location_picker.page.dart
@@ -58,7 +58,7 @@ class MapLocationPickerPage extends HookConsumerWidget {
       controller.value?.animateCamera(CameraUpdate.newLatLng(currentLatLng));
     }
 
-    return MapThemeOveride(
+    return MapThemeOverride(
       mapBuilder: (style) => Builder(
         builder: (ctx) => Scaffold(
           backgroundColor: ctx.themeData.cardColor,
diff --git a/mobile/lib/pages/search/person_result.page.dart b/mobile/lib/pages/search/person_result.page.dart
index 55824b8db9..8627c65bcc 100644
--- a/mobile/lib/pages/search/person_result.page.dart
+++ b/mobile/lib/pages/search/person_result.page.dart
@@ -92,6 +92,7 @@ class PersonResultPage extends HookConsumerWidget {
                   Text(
                     name.value,
                     style: context.textTheme.titleLarge,
+                    overflow: TextOverflow.ellipsis,
                   ),
                 ],
               ),
@@ -125,9 +126,11 @@ class PersonResultPage extends HookConsumerWidget {
                   headers: ApiService.getRequestHeaders(),
                 ),
               ),
-              Padding(
-                padding: const EdgeInsets.only(left: 16.0),
-                child: buildTitleBlock(),
+              Expanded(
+                child: Padding(
+                  padding: const EdgeInsets.only(left: 16.0, right: 16.0),
+                  child: buildTitleBlock(),
+                ),
               ),
             ],
           ),
diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart
index 173115185b..01119485cf 100644
--- a/mobile/lib/pages/search/search.page.dart
+++ b/mobile/lib/pages/search/search.page.dart
@@ -1,254 +1,786 @@
-import 'dart:math' as math;
-
-import 'package:auto_route/auto_route.dart';
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/material.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:immich_mobile/extensions/theme_extensions.dart';
-import 'package:immich_mobile/models/search/search_curated_content.model.dart';
-import 'package:immich_mobile/models/search/search_filter.model.dart';
-import 'package:immich_mobile/providers/search/people.provider.dart';
-import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
-import 'package:immich_mobile/widgets/search/curated_people_row.dart';
-import 'package:immich_mobile/widgets/search/curated_places_row.dart';
-import 'package:immich_mobile/widgets/search/person_name_edit_form.dart';
-import 'package:immich_mobile/widgets/search/search_row_section.dart';
-import 'package:immich_mobile/routing/router.dart';
-import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:immich_mobile/providers/server_info.provider.dart';
-import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
-import 'package:immich_mobile/widgets/common/scaffold_error_body.dart';
-
-@RoutePage()
-// ignore: must_be_immutable
-class SearchPage extends HookConsumerWidget {
-  const SearchPage({super.key});
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    final places = ref.watch(getPreviewPlacesProvider);
-    final curatedPeople = ref.watch(getAllPeopleProvider);
-    final isMapEnabled =
-        ref.watch(serverInfoProvider.select((v) => v.serverFeatures.map));
-    final double imageSize = math.min(context.width / 3, 150);
-
-    TextStyle categoryTitleStyle = const TextStyle(
-      fontWeight: FontWeight.w500,
-      fontSize: 15.0,
-    );
-
-    Color categoryIconColor = context.colorScheme.onSurface;
-
-    showNameEditModel(
-      String personId,
-      String personName,
-    ) {
-      return showDialog(
-        context: context,
-        builder: (BuildContext context) {
-          return PersonNameEditForm(personId: personId, personName: personName);
-        },
-      );
-    }
-
-    buildPeople() {
-      return curatedPeople.widgetWhen(
-        onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
-        onData: (people) {
-          return SearchRowSection(
-            onViewAllPressed: () => context.pushRoute(const AllPeopleRoute()),
-            title: "search_page_people".tr(),
-            isEmpty: people.isEmpty,
-            child: CuratedPeopleRow(
-              padding: const EdgeInsets.symmetric(horizontal: 16),
-              content: people
-                  .map((e) => SearchCuratedContent(label: e.name, id: e.id))
-                  .take(12)
-                  .toList(),
-              onTap: (content, index) {
-                context.pushRoute(
-                  PersonResultRoute(
-                    personId: content.id,
-                    personName: content.label,
-                  ),
-                );
-              },
-              onNameTap: (person, index) => {
-                showNameEditModel(person.id, person.label),
-              },
-            ),
-          );
-        },
-      );
-    }
-
-    buildPlaces() {
-      return places.widgetWhen(
-        onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
-        onData: (data) {
-          return SearchRowSection(
-            onViewAllPressed: () => context.pushRoute(const AllPlacesRoute()),
-            title: "search_page_places".tr(),
-            isEmpty: !isMapEnabled && data.isEmpty,
-            child: CuratedPlacesRow(
-              isMapEnabled: isMapEnabled,
-              content: data,
-              imageSize: imageSize,
-              onTap: (content, index) {
-                context.pushRoute(
-                  SearchInputRoute(
-                    prefilter: SearchFilter(
-                      people: {},
-                      location: SearchLocationFilter(
-                        city: content.label,
-                      ),
-                      camera: SearchCameraFilter(),
-                      date: SearchDateFilter(),
-                      display: SearchDisplayFilters(
-                        isNotInAlbum: false,
-                        isArchive: false,
-                        isFavorite: false,
-                      ),
-                      mediaType: AssetType.other,
-                    ),
-                  ),
-                );
-              },
-            ),
-          );
-        },
-      );
-    }
-
-    buildSearchButton() {
-      return GestureDetector(
-        onTap: () {
-          context.pushRoute(SearchInputRoute());
-        },
-        child: Card(
-          elevation: 0,
-          color: context.colorScheme.surfaceContainerHigh,
-          shape: RoundedRectangleBorder(
-            borderRadius: BorderRadius.circular(50),
-          ),
-          margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
-          child: Padding(
-            padding: const EdgeInsets.symmetric(
-              horizontal: 16.0,
-              vertical: 12.0,
-            ),
-            child: Row(
-              children: [
-                Icon(
-                  Icons.search,
-                  color: context.colorScheme.onSurfaceSecondary,
-                ),
-                const SizedBox(width: 16.0),
-                Text(
-                  "search_bar_hint",
-                  style: context.textTheme.bodyLarge?.copyWith(
-                    color: context.colorScheme.onSurfaceSecondary,
-                    fontWeight: FontWeight.w400,
-                  ),
-                ).tr(),
-              ],
-            ),
-          ),
-        ),
-      );
-    }
-
-    return Scaffold(
-      appBar: const ImmichAppBar(),
-      body: ListView(
-        children: [
-          buildSearchButton(),
-          const SizedBox(height: 8.0),
-          buildPeople(),
-          const SizedBox(height: 8.0),
-          buildPlaces(),
-          const SizedBox(height: 24.0),
-          Padding(
-            padding: const EdgeInsets.symmetric(horizontal: 16),
-            child: Text(
-              'search_page_your_activity',
-              style: context.textTheme.bodyLarge?.copyWith(
-                fontWeight: FontWeight.w500,
-              ),
-            ).tr(),
-          ),
-          ListTile(
-            leading: Icon(
-              Icons.favorite_border_rounded,
-              color: categoryIconColor,
-            ),
-            title:
-                Text('search_page_favorites', style: categoryTitleStyle).tr(),
-            onTap: () => context.pushRoute(const FavoritesRoute()),
-          ),
-          const CategoryDivider(),
-          ListTile(
-            leading: Icon(
-              Icons.schedule_outlined,
-              color: categoryIconColor,
-            ),
-            title: Text(
-              'search_page_recently_added',
-              style: categoryTitleStyle,
-            ).tr(),
-            onTap: () => context.pushRoute(const RecentlyAddedRoute()),
-          ),
-          const SizedBox(height: 24.0),
-          Padding(
-            padding: const EdgeInsets.symmetric(horizontal: 16.0),
-            child: Text(
-              'search_page_categories',
-              style: context.textTheme.bodyLarge?.copyWith(
-                fontWeight: FontWeight.w500,
-              ),
-            ).tr(),
-          ),
-          ListTile(
-            title: Text('search_page_videos', style: categoryTitleStyle).tr(),
-            leading: Icon(
-              Icons.play_circle_outline,
-              color: categoryIconColor,
-            ),
-            onTap: () => context.pushRoute(const AllVideosRoute()),
-          ),
-          const CategoryDivider(),
-          ListTile(
-            title: Text(
-              'search_page_motion_photos',
-              style: categoryTitleStyle,
-            ).tr(),
-            leading: Icon(
-              Icons.motion_photos_on_outlined,
-              color: categoryIconColor,
-            ),
-            onTap: () => context.pushRoute(const AllMotionPhotosRoute()),
-          ),
-        ],
-      ),
-    );
-  }
-}
-
-class CategoryDivider extends StatelessWidget {
-  const CategoryDivider({super.key});
-
-  @override
-  Widget build(BuildContext context) {
-    return const Padding(
-      padding: EdgeInsets.only(
-        left: 56,
-        right: 16,
-      ),
-      child: Divider(
-        height: 0,
-      ),
-    );
-  }
-}
+import 'dart:async';
+
+import 'package:auto_route/auto_route.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/extensions/theme_extensions.dart';
+import 'package:immich_mobile/interfaces/person_api.interface.dart';
+import 'package:immich_mobile/models/search/search_filter.model.dart';
+import 'package:immich_mobile/providers/search/paginated_search.provider.dart';
+import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
+import 'package:immich_mobile/routing/router.dart';
+import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
+import 'package:immich_mobile/widgets/search/search_filter/camera_picker.dart';
+import 'package:immich_mobile/widgets/search/search_filter/display_option_picker.dart';
+import 'package:immich_mobile/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart';
+import 'package:immich_mobile/widgets/search/search_filter/location_picker.dart';
+import 'package:immich_mobile/widgets/search/search_filter/media_type_picker.dart';
+import 'package:immich_mobile/widgets/search/search_filter/people_picker.dart';
+import 'package:immich_mobile/widgets/search/search_filter/search_filter_chip.dart';
+import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.dart';
+
+@RoutePage()
+class SearchPage extends HookConsumerWidget {
+  const SearchPage({super.key, this.prefilter});
+
+  final SearchFilter? prefilter;
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final isContextualSearch = useState(true);
+    final textSearchController = useTextEditingController();
+    final filter = useState<SearchFilter>(
+      SearchFilter(
+        people: prefilter?.people ?? {},
+        location: prefilter?.location ?? SearchLocationFilter(),
+        camera: prefilter?.camera ?? SearchCameraFilter(),
+        date: prefilter?.date ?? SearchDateFilter(),
+        display: prefilter?.display ??
+            SearchDisplayFilters(
+              isNotInAlbum: false,
+              isArchive: false,
+              isFavorite: false,
+            ),
+        mediaType: prefilter?.mediaType ?? AssetType.other,
+      ),
+    );
+
+    final previousFilter = useState(filter.value);
+
+    final peopleCurrentFilterWidget = useState<Widget?>(null);
+    final dateRangeCurrentFilterWidget = useState<Widget?>(null);
+    final cameraCurrentFilterWidget = useState<Widget?>(null);
+    final locationCurrentFilterWidget = useState<Widget?>(null);
+    final mediaTypeCurrentFilterWidget = useState<Widget?>(null);
+    final displayOptionCurrentFilterWidget = useState<Widget?>(null);
+
+    final isSearching = useState(false);
+
+    search() async {
+      if (prefilter == null && filter.value == previousFilter.value) return;
+
+      isSearching.value = true;
+      ref.watch(paginatedSearchProvider.notifier).clear();
+      await ref.watch(paginatedSearchProvider.notifier).search(filter.value);
+      previousFilter.value = filter.value;
+      isSearching.value = false;
+    }
+
+    loadMoreSearchResult() async {
+      isSearching.value = true;
+      await ref.watch(paginatedSearchProvider.notifier).search(filter.value);
+      isSearching.value = false;
+    }
+
+    searchPrefilter() {
+      if (prefilter != null) {
+        Future.delayed(
+          Duration.zero,
+          () {
+            search();
+
+            if (prefilter!.location.city != null) {
+              locationCurrentFilterWidget.value = Text(
+                prefilter!.location.city!,
+                style: context.textTheme.labelLarge,
+              );
+            }
+          },
+        );
+      }
+    }
+
+    useEffect(
+      () {
+        Future.microtask(
+          () => ref.invalidate(paginatedSearchProvider),
+        );
+        searchPrefilter();
+
+        return null;
+      },
+      [],
+    );
+
+    showPeoplePicker() {
+      handleOnSelect(Set<Person> value) {
+        filter.value = filter.value.copyWith(
+          people: value,
+        );
+
+        peopleCurrentFilterWidget.value = Text(
+          value.map((e) => e.name != '' ? e.name : 'no_name'.tr()).join(', '),
+          style: context.textTheme.labelLarge,
+        );
+      }
+
+      handleClear() {
+        filter.value = filter.value.copyWith(
+          people: {},
+        );
+
+        peopleCurrentFilterWidget.value = null;
+        search();
+      }
+
+      showFilterBottomSheet(
+        context: context,
+        isScrollControlled: true,
+        child: FractionallySizedBox(
+          heightFactor: 0.8,
+          child: FilterBottomSheetScaffold(
+            title: 'search_filter_people_title'.tr(),
+            expanded: true,
+            onSearch: search,
+            onClear: handleClear,
+            child: PeoplePicker(
+              onSelect: handleOnSelect,
+              filter: filter.value.people,
+            ),
+          ),
+        ),
+      );
+    }
+
+    showLocationPicker() {
+      handleOnSelect(Map<String, String?> value) {
+        filter.value = filter.value.copyWith(
+          location: SearchLocationFilter(
+            country: value['country'],
+            city: value['city'],
+            state: value['state'],
+          ),
+        );
+
+        final locationText = <String>[];
+        if (value['country'] != null) {
+          locationText.add(value['country']!);
+        }
+
+        if (value['state'] != null) {
+          locationText.add(value['state']!);
+        }
+
+        if (value['city'] != null) {
+          locationText.add(value['city']!);
+        }
+
+        locationCurrentFilterWidget.value = Text(
+          locationText.join(', '),
+          style: context.textTheme.labelLarge,
+        );
+      }
+
+      handleClear() {
+        filter.value = filter.value.copyWith(
+          location: SearchLocationFilter(),
+        );
+
+        locationCurrentFilterWidget.value = null;
+        search();
+      }
+
+      showFilterBottomSheet(
+        context: context,
+        isScrollControlled: true,
+        isDismissible: true,
+        child: FilterBottomSheetScaffold(
+          title: 'search_filter_location_title'.tr(),
+          onSearch: search,
+          onClear: handleClear,
+          child: Padding(
+            padding: const EdgeInsets.symmetric(vertical: 16.0),
+            child: Container(
+              padding: EdgeInsets.only(
+                bottom: context.viewInsets.bottom,
+              ),
+              child: Padding(
+                padding: const EdgeInsets.symmetric(horizontal: 16.0),
+                child: LocationPicker(
+                  onSelected: handleOnSelect,
+                  filter: filter.value.location,
+                ),
+              ),
+            ),
+          ),
+        ),
+      );
+    }
+
+    showCameraPicker() {
+      handleOnSelect(Map<String, String?> value) {
+        filter.value = filter.value.copyWith(
+          camera: SearchCameraFilter(
+            make: value['make'],
+            model: value['model'],
+          ),
+        );
+
+        cameraCurrentFilterWidget.value = Text(
+          '${value['make'] ?? ''} ${value['model'] ?? ''}',
+          style: context.textTheme.labelLarge,
+        );
+      }
+
+      handleClear() {
+        filter.value = filter.value.copyWith(
+          camera: SearchCameraFilter(),
+        );
+
+        cameraCurrentFilterWidget.value = null;
+        search();
+      }
+
+      showFilterBottomSheet(
+        context: context,
+        isScrollControlled: true,
+        isDismissible: true,
+        child: FilterBottomSheetScaffold(
+          title: 'search_filter_camera_title'.tr(),
+          onSearch: search,
+          onClear: handleClear,
+          child: Padding(
+            padding: const EdgeInsets.all(16.0),
+            child: CameraPicker(
+              onSelect: handleOnSelect,
+              filter: filter.value.camera,
+            ),
+          ),
+        ),
+      );
+    }
+
+    showDatePicker() async {
+      final firstDate = DateTime(1900);
+      final lastDate = DateTime.now();
+
+      final date = await showDateRangePicker(
+        context: context,
+        firstDate: firstDate,
+        lastDate: lastDate,
+        currentDate: DateTime.now(),
+        initialDateRange: DateTimeRange(
+          start: filter.value.date.takenAfter ?? lastDate,
+          end: filter.value.date.takenBefore ?? lastDate,
+        ),
+        helpText: 'search_filter_date_title'.tr(),
+        cancelText: 'action_common_cancel'.tr(),
+        confirmText: 'action_common_select'.tr(),
+        saveText: 'action_common_save'.tr(),
+        errorFormatText: 'invalid_date_format'.tr(),
+        errorInvalidText: 'invalid_date'.tr(),
+        fieldStartHintText: 'start_date'.tr(),
+        fieldEndHintText: 'end_date'.tr(),
+        initialEntryMode: DatePickerEntryMode.input,
+      );
+
+      if (date == null) {
+        filter.value = filter.value.copyWith(
+          date: SearchDateFilter(),
+        );
+
+        dateRangeCurrentFilterWidget.value = null;
+        search();
+        return;
+      }
+
+      filter.value = filter.value.copyWith(
+        date: SearchDateFilter(
+          takenAfter: date.start,
+          takenBefore: date.end.add(
+            const Duration(
+              hours: 23,
+              minutes: 59,
+              seconds: 59,
+            ),
+          ),
+        ),
+      );
+
+      // If date range is less than 24 hours, set the end date to the end of the day
+      if (date.end.difference(date.start).inHours < 24) {
+        dateRangeCurrentFilterWidget.value = Text(
+          DateFormat.yMMMd().format(date.start.toLocal()),
+          style: context.textTheme.labelLarge,
+        );
+      } else {
+        dateRangeCurrentFilterWidget.value = Text(
+          'search_filter_date_interval'.tr(
+            namedArgs: {
+              "start": DateFormat.yMMMd().format(date.start.toLocal()),
+              "end": DateFormat.yMMMd().format(date.end.toLocal()),
+            },
+          ),
+          style: context.textTheme.labelLarge,
+        );
+      }
+
+      search();
+    }
+
+    // MEDIA PICKER
+    showMediaTypePicker() {
+      handleOnSelected(AssetType assetType) {
+        filter.value = filter.value.copyWith(
+          mediaType: assetType,
+        );
+
+        mediaTypeCurrentFilterWidget.value = Text(
+          assetType == AssetType.image
+              ? 'search_filter_media_type_image'.tr()
+              : assetType == AssetType.video
+                  ? 'search_filter_media_type_video'.tr()
+                  : 'search_filter_media_type_all'.tr(),
+          style: context.textTheme.labelLarge,
+        );
+      }
+
+      handleClear() {
+        filter.value = filter.value.copyWith(
+          mediaType: AssetType.other,
+        );
+
+        mediaTypeCurrentFilterWidget.value = null;
+        search();
+      }
+
+      showFilterBottomSheet(
+        context: context,
+        child: FilterBottomSheetScaffold(
+          title: 'search_filter_media_type_title'.tr(),
+          onSearch: search,
+          onClear: handleClear,
+          child: MediaTypePicker(
+            onSelect: handleOnSelected,
+            filter: filter.value.mediaType,
+          ),
+        ),
+      );
+    }
+
+    // DISPLAY OPTION
+    showDisplayOptionPicker() {
+      handleOnSelect(Map<DisplayOption, bool> value) {
+        final filterText = <String>[];
+        value.forEach((key, value) {
+          switch (key) {
+            case DisplayOption.notInAlbum:
+              filter.value = filter.value.copyWith(
+                display: filter.value.display.copyWith(
+                  isNotInAlbum: value,
+                ),
+              );
+              if (value) {
+                filterText
+                    .add('search_filter_display_option_not_in_album'.tr());
+              }
+              break;
+            case DisplayOption.archive:
+              filter.value = filter.value.copyWith(
+                display: filter.value.display.copyWith(
+                  isArchive: value,
+                ),
+              );
+              if (value) {
+                filterText.add('search_filter_display_option_archive'.tr());
+              }
+              break;
+            case DisplayOption.favorite:
+              filter.value = filter.value.copyWith(
+                display: filter.value.display.copyWith(
+                  isFavorite: value,
+                ),
+              );
+              if (value) {
+                filterText.add('search_filter_display_option_favorite'.tr());
+              }
+              break;
+          }
+        });
+
+        if (filterText.isEmpty) {
+          displayOptionCurrentFilterWidget.value = null;
+          return;
+        }
+
+        displayOptionCurrentFilterWidget.value = Text(
+          filterText.join(', '),
+          style: context.textTheme.labelLarge,
+        );
+      }
+
+      handleClear() {
+        filter.value = filter.value.copyWith(
+          display: SearchDisplayFilters(
+            isNotInAlbum: false,
+            isArchive: false,
+            isFavorite: false,
+          ),
+        );
+
+        displayOptionCurrentFilterWidget.value = null;
+        search();
+      }
+
+      showFilterBottomSheet(
+        context: context,
+        child: FilterBottomSheetScaffold(
+          title: 'search_filter_display_options_title'.tr(),
+          onSearch: search,
+          onClear: handleClear,
+          child: DisplayOptionPicker(
+            onSelect: handleOnSelect,
+            filter: filter.value.display,
+          ),
+        ),
+      );
+    }
+
+    handleTextSubmitted(String value) {
+      if (value.isEmpty) {
+        return;
+      }
+
+      if (isContextualSearch.value) {
+        filter.value = filter.value.copyWith(
+          filename: null,
+          context: value,
+        );
+      } else {
+        filter.value = filter.value.copyWith(
+          filename: value,
+          context: null,
+        );
+      }
+
+      search();
+    }
+
+    return Scaffold(
+      resizeToAvoidBottomInset: true,
+      appBar: AppBar(
+        automaticallyImplyLeading: true,
+        actions: [
+          Padding(
+            padding: const EdgeInsets.only(right: 14.0),
+            child: IconButton(
+              icon: isContextualSearch.value
+                  ? const Icon(Icons.abc_rounded)
+                  : const Icon(Icons.image_search_rounded),
+              onPressed: () {
+                isContextualSearch.value = !isContextualSearch.value;
+                textSearchController.clear();
+              },
+            ),
+          ),
+        ],
+        title: Container(
+          decoration: BoxDecoration(
+            border: Border.all(
+              color: context.colorScheme.onSurface.withAlpha(0),
+              width: 0,
+            ),
+            borderRadius: BorderRadius.circular(24),
+            gradient: LinearGradient(
+              colors: [
+                context.colorScheme.primary.withOpacity(0.075),
+                context.colorScheme.primary.withOpacity(0.09),
+                context.colorScheme.primary.withOpacity(0.075),
+              ],
+              begin: Alignment.topLeft,
+              end: Alignment.bottomRight,
+            ),
+          ),
+          child: TextField(
+            controller: textSearchController,
+            decoration: InputDecoration(
+              contentPadding: prefilter != null
+                  ? const EdgeInsets.only(left: 24)
+                  : const EdgeInsets.all(8),
+              prefixIcon: prefilter != null
+                  ? null
+                  : Icon(
+                      Icons.search_rounded,
+                      color: context.colorScheme.primary,
+                    ),
+              hintText: isContextualSearch.value
+                  ? 'contextual_search'.tr()
+                  : 'filename_search'.tr(),
+              hintStyle: context.textTheme.bodyLarge?.copyWith(
+                color: context.themeData.colorScheme.onSurfaceSecondary,
+              ),
+              border: OutlineInputBorder(
+                borderRadius: BorderRadius.circular(25),
+                borderSide: BorderSide(
+                  color: context.colorScheme.surfaceDim,
+                ),
+              ),
+              enabledBorder: OutlineInputBorder(
+                borderRadius: BorderRadius.circular(25),
+                borderSide: BorderSide(
+                  color: context.colorScheme.surfaceContainer,
+                ),
+              ),
+              disabledBorder: OutlineInputBorder(
+                borderRadius: BorderRadius.circular(25),
+                borderSide: BorderSide(
+                  color: context.colorScheme.surfaceDim,
+                ),
+              ),
+              focusedBorder: OutlineInputBorder(
+                borderRadius: BorderRadius.circular(25),
+                borderSide: BorderSide(
+                  color: context.colorScheme.primary.withAlpha(100),
+                ),
+              ),
+            ),
+            onSubmitted: handleTextSubmitted,
+            focusNode: ref.watch(searchInputFocusProvider),
+            onTapOutside: (_) => ref.read(searchInputFocusProvider).unfocus(),
+          ),
+        ),
+      ),
+      body: Column(
+        children: [
+          Padding(
+            padding: const EdgeInsets.only(top: 12.0),
+            child: SizedBox(
+              height: 50,
+              child: ListView(
+                shrinkWrap: true,
+                scrollDirection: Axis.horizontal,
+                padding: const EdgeInsets.symmetric(horizontal: 16),
+                children: [
+                  SearchFilterChip(
+                    icon: Icons.people_alt_rounded,
+                    onTap: showPeoplePicker,
+                    label: 'search_filter_people'.tr(),
+                    currentFilter: peopleCurrentFilterWidget.value,
+                  ),
+                  SearchFilterChip(
+                    icon: Icons.location_pin,
+                    onTap: showLocationPicker,
+                    label: 'search_filter_location'.tr(),
+                    currentFilter: locationCurrentFilterWidget.value,
+                  ),
+                  SearchFilterChip(
+                    icon: Icons.camera_alt_rounded,
+                    onTap: showCameraPicker,
+                    label: 'search_filter_camera'.tr(),
+                    currentFilter: cameraCurrentFilterWidget.value,
+                  ),
+                  SearchFilterChip(
+                    icon: Icons.date_range_rounded,
+                    onTap: showDatePicker,
+                    label: 'search_filter_date'.tr(),
+                    currentFilter: dateRangeCurrentFilterWidget.value,
+                  ),
+                  SearchFilterChip(
+                    icon: Icons.video_collection_outlined,
+                    onTap: showMediaTypePicker,
+                    label: 'search_filter_media_type'.tr(),
+                    currentFilter: mediaTypeCurrentFilterWidget.value,
+                  ),
+                  SearchFilterChip(
+                    icon: Icons.display_settings_outlined,
+                    onTap: showDisplayOptionPicker,
+                    label: 'search_filter_display_options'.tr(),
+                    currentFilter: displayOptionCurrentFilterWidget.value,
+                  ),
+                ],
+              ),
+            ),
+          ),
+          SearchResultGrid(
+            onScrollEnd: loadMoreSearchResult,
+            isSearching: isSearching.value,
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class SearchResultGrid extends StatelessWidget {
+  final VoidCallback onScrollEnd;
+  final bool isSearching;
+
+  const SearchResultGrid({
+    super.key,
+    required this.onScrollEnd,
+    this.isSearching = false,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Expanded(
+      child: Padding(
+        padding: const EdgeInsets.only(top: 8.0),
+        child: NotificationListener<ScrollEndNotification>(
+          onNotification: (notification) {
+            final isBottomSheetNotification = notification.context
+                    ?.findAncestorWidgetOfExactType<
+                        DraggableScrollableSheet>() !=
+                null;
+
+            final metrics = notification.metrics;
+            final isVerticalScroll = metrics.axis == Axis.vertical;
+
+            if (metrics.pixels >= metrics.maxScrollExtent &&
+                isVerticalScroll &&
+                !isBottomSheetNotification) {
+              onScrollEnd();
+            }
+
+            return true;
+          },
+          child: MultiselectGrid(
+            renderListProvider: paginatedSearchRenderListProvider,
+            archiveEnabled: true,
+            deleteEnabled: true,
+            editEnabled: true,
+            favoriteEnabled: true,
+            stackEnabled: false,
+            emptyIndicator: Padding(
+              padding: const EdgeInsets.symmetric(horizontal: 16.0),
+              child: !isSearching
+                  ? const SearchEmptyContent()
+                  : const SizedBox.shrink(),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+class SearchEmptyContent extends StatelessWidget {
+  const SearchEmptyContent({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return NotificationListener<ScrollNotification>(
+      onNotification: (_) => true,
+      child: ListView(
+        shrinkWrap: false,
+        children: [
+          const SizedBox(height: 40),
+          Center(
+            child: Image.asset(
+              context.isDarkTheme
+                  ? 'assets/polaroid-dark.png'
+                  : 'assets/polaroid-light.png',
+              height: 125,
+            ),
+          ),
+          const SizedBox(height: 16),
+          Center(
+            child: Text(
+              'search_page_search_photos_videos'.tr(),
+              style: context.textTheme.labelLarge,
+            ),
+          ),
+          const SizedBox(height: 32),
+          const QuickLinkList(),
+        ],
+      ),
+    );
+  }
+}
+
+class QuickLinkList extends StatelessWidget {
+  const QuickLinkList({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      decoration: BoxDecoration(
+        borderRadius: BorderRadius.circular(20),
+        border: Border.all(
+          color: context.colorScheme.outline.withAlpha(10),
+          width: 1,
+        ),
+        gradient: LinearGradient(
+          colors: [
+            context.colorScheme.primary.withAlpha(10),
+            context.colorScheme.primary.withAlpha(15),
+            context.colorScheme.primary.withAlpha(20),
+          ],
+          begin: Alignment.topCenter,
+          end: Alignment.bottomCenter,
+        ),
+      ),
+      child: ListView(
+        shrinkWrap: true,
+        physics: const NeverScrollableScrollPhysics(),
+        children: [
+          QuickLink(
+            title: 'recently_added'.tr(),
+            icon: Icons.schedule_outlined,
+            isTop: true,
+            onTap: () => context.pushRoute(const RecentlyAddedRoute()),
+          ),
+          QuickLink(
+            title: 'videos'.tr(),
+            icon: Icons.play_circle_outline_rounded,
+            onTap: () => context.pushRoute(const AllVideosRoute()),
+          ),
+          QuickLink(
+            title: 'favorites'.tr(),
+            icon: Icons.favorite_border_rounded,
+            isBottom: true,
+            onTap: () => context.pushRoute(const FavoritesRoute()),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class QuickLink extends StatelessWidget {
+  final String title;
+  final IconData icon;
+  final VoidCallback onTap;
+  final bool isTop;
+  final bool isBottom;
+
+  const QuickLink({
+    super.key,
+    required this.title,
+    required this.icon,
+    required this.onTap,
+    this.isTop = false,
+    this.isBottom = false,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final borderRadius = BorderRadius.only(
+      topLeft: Radius.circular(isTop ? 20 : 0),
+      topRight: Radius.circular(isTop ? 20 : 0),
+      bottomLeft: Radius.circular(isBottom ? 20 : 0),
+      bottomRight: Radius.circular(isBottom ? 20 : 0),
+    );
+
+    return ListTile(
+      shape: RoundedRectangleBorder(
+        borderRadius: borderRadius,
+      ),
+      leading: Icon(
+        icon,
+        size: 26,
+      ),
+      title: Text(
+        title,
+        style: context.textTheme.titleSmall?.copyWith(
+          fontWeight: FontWeight.w500,
+        ),
+      ),
+      onTap: onTap,
+    );
+  }
+}
diff --git a/mobile/lib/pages/search/search_input.page.dart b/mobile/lib/pages/search/search_input.page.dart
deleted file mode 100644
index acabc75aa4..0000000000
--- a/mobile/lib/pages/search/search_input.page.dart
+++ /dev/null
@@ -1,582 +0,0 @@
-import 'dart:async';
-
-import 'package:auto_route/auto_route.dart';
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:immich_mobile/extensions/theme_extensions.dart';
-import 'package:immich_mobile/models/search/search_filter.model.dart';
-import 'package:immich_mobile/providers/search/paginated_search.provider.dart';
-import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
-import 'package:immich_mobile/widgets/search/search_filter/camera_picker.dart';
-import 'package:immich_mobile/widgets/search/search_filter/display_option_picker.dart';
-import 'package:immich_mobile/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart';
-import 'package:immich_mobile/widgets/search/search_filter/location_picker.dart';
-import 'package:immich_mobile/widgets/search/search_filter/media_type_picker.dart';
-import 'package:immich_mobile/widgets/search/search_filter/people_picker.dart';
-import 'package:immich_mobile/widgets/search/search_filter/search_filter_chip.dart';
-import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.dart';
-import 'package:openapi/api.dart';
-
-@RoutePage()
-class SearchInputPage extends HookConsumerWidget {
-  const SearchInputPage({super.key, this.prefilter});
-
-  final SearchFilter? prefilter;
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    final isContextualSearch = useState(true);
-    final textSearchController = useTextEditingController();
-    final filter = useState<SearchFilter>(
-      SearchFilter(
-        people: prefilter?.people ?? {},
-        location: prefilter?.location ?? SearchLocationFilter(),
-        camera: prefilter?.camera ?? SearchCameraFilter(),
-        date: prefilter?.date ?? SearchDateFilter(),
-        display: prefilter?.display ??
-            SearchDisplayFilters(
-              isNotInAlbum: false,
-              isArchive: false,
-              isFavorite: false,
-            ),
-        mediaType: prefilter?.mediaType ?? AssetType.other,
-      ),
-    );
-
-    final previousFilter = useState(filter.value);
-
-    final peopleCurrentFilterWidget = useState<Widget?>(null);
-    final dateRangeCurrentFilterWidget = useState<Widget?>(null);
-    final cameraCurrentFilterWidget = useState<Widget?>(null);
-    final locationCurrentFilterWidget = useState<Widget?>(null);
-    final mediaTypeCurrentFilterWidget = useState<Widget?>(null);
-    final displayOptionCurrentFilterWidget = useState<Widget?>(null);
-
-    final currentPage = useState(1);
-    final searchProvider = ref.watch(paginatedSearchProvider);
-    final searchResultCount = useState(0);
-
-    search() async {
-      if (prefilter == null && filter.value == previousFilter.value) return;
-
-      ref.watch(paginatedSearchProvider.notifier).clear();
-
-      currentPage.value = 1;
-
-      final searchResult = await ref
-          .watch(paginatedSearchProvider.notifier)
-          .getNextPage(filter.value, currentPage.value);
-      previousFilter.value = filter.value;
-
-      searchResultCount.value = searchResult.length;
-    }
-
-    searchPrefilter() {
-      if (prefilter != null) {
-        Future.delayed(
-          Duration.zero,
-          () {
-            search();
-
-            if (prefilter!.location.city != null) {
-              locationCurrentFilterWidget.value = Text(
-                prefilter!.location.city!,
-                style: context.textTheme.labelLarge,
-              );
-            }
-          },
-        );
-      }
-    }
-
-    useEffect(
-      () {
-        searchPrefilter();
-        return null;
-      },
-      [],
-    );
-
-    loadMoreSearchResult() async {
-      currentPage.value += 1;
-      final searchResult = await ref
-          .watch(paginatedSearchProvider.notifier)
-          .getNextPage(filter.value, currentPage.value);
-      searchResultCount.value = searchResult.length;
-    }
-
-    showPeoplePicker() {
-      handleOnSelect(Set<PersonResponseDto> value) {
-        filter.value = filter.value.copyWith(
-          people: value,
-        );
-
-        peopleCurrentFilterWidget.value = Text(
-          value.map((e) => e.name != '' ? e.name : 'no_name'.tr()).join(', '),
-          style: context.textTheme.labelLarge,
-        );
-      }
-
-      handleClear() {
-        filter.value = filter.value.copyWith(
-          people: {},
-        );
-
-        peopleCurrentFilterWidget.value = null;
-        search();
-      }
-
-      showFilterBottomSheet(
-        context: context,
-        isScrollControlled: true,
-        child: FractionallySizedBox(
-          heightFactor: 0.8,
-          child: FilterBottomSheetScaffold(
-            title: 'search_filter_people_title'.tr(),
-            expanded: true,
-            onSearch: search,
-            onClear: handleClear,
-            child: PeoplePicker(
-              onSelect: handleOnSelect,
-              filter: filter.value.people,
-            ),
-          ),
-        ),
-      );
-    }
-
-    showLocationPicker() {
-      handleOnSelect(Map<String, String?> value) {
-        filter.value = filter.value.copyWith(
-          location: SearchLocationFilter(
-            country: value['country'],
-            city: value['city'],
-            state: value['state'],
-          ),
-        );
-
-        final locationText = <String>[];
-        if (value['country'] != null) {
-          locationText.add(value['country']!);
-        }
-
-        if (value['state'] != null) {
-          locationText.add(value['state']!);
-        }
-
-        if (value['city'] != null) {
-          locationText.add(value['city']!);
-        }
-
-        locationCurrentFilterWidget.value = Text(
-          locationText.join(', '),
-          style: context.textTheme.labelLarge,
-        );
-      }
-
-      handleClear() {
-        filter.value = filter.value.copyWith(
-          location: SearchLocationFilter(),
-        );
-
-        locationCurrentFilterWidget.value = null;
-        search();
-      }
-
-      showFilterBottomSheet(
-        context: context,
-        isScrollControlled: true,
-        isDismissible: false,
-        child: FilterBottomSheetScaffold(
-          title: 'search_filter_location_title'.tr(),
-          onSearch: search,
-          onClear: handleClear,
-          child: Padding(
-            padding: const EdgeInsets.symmetric(vertical: 16.0),
-            child: Container(
-              padding: EdgeInsets.only(
-                bottom: MediaQuery.of(context).viewInsets.bottom,
-              ),
-              child: Padding(
-                padding: const EdgeInsets.symmetric(horizontal: 16.0),
-                child: LocationPicker(
-                  onSelected: handleOnSelect,
-                  filter: filter.value.location,
-                ),
-              ),
-            ),
-          ),
-        ),
-      );
-    }
-
-    showCameraPicker() {
-      handleOnSelect(Map<String, String?> value) {
-        filter.value = filter.value.copyWith(
-          camera: SearchCameraFilter(
-            make: value['make'],
-            model: value['model'],
-          ),
-        );
-
-        cameraCurrentFilterWidget.value = Text(
-          '${value['make'] ?? ''} ${value['model'] ?? ''}',
-          style: context.textTheme.labelLarge,
-        );
-      }
-
-      handleClear() {
-        filter.value = filter.value.copyWith(
-          camera: SearchCameraFilter(),
-        );
-
-        cameraCurrentFilterWidget.value = null;
-        search();
-      }
-
-      showFilterBottomSheet(
-        context: context,
-        isScrollControlled: true,
-        isDismissible: false,
-        child: FilterBottomSheetScaffold(
-          title: 'search_filter_camera_title'.tr(),
-          onSearch: search,
-          onClear: handleClear,
-          child: Padding(
-            padding: const EdgeInsets.all(16.0),
-            child: CameraPicker(
-              onSelect: handleOnSelect,
-              filter: filter.value.camera,
-            ),
-          ),
-        ),
-      );
-    }
-
-    showDatePicker() async {
-      final firstDate = DateTime(1900);
-      final lastDate = DateTime.now();
-
-      final date = await showDateRangePicker(
-        context: context,
-        firstDate: firstDate,
-        lastDate: lastDate,
-        currentDate: DateTime.now(),
-        initialDateRange: DateTimeRange(
-          start: filter.value.date.takenAfter ?? lastDate,
-          end: filter.value.date.takenBefore ?? lastDate,
-        ),
-        helpText: 'search_filter_date_title'.tr(),
-        cancelText: 'action_common_cancel'.tr(),
-        confirmText: 'action_common_select'.tr(),
-        saveText: 'action_common_save'.tr(),
-        errorFormatText: 'invalid_date_format'.tr(),
-        errorInvalidText: 'invalid_date'.tr(),
-        fieldStartHintText: 'start_date'.tr(),
-        fieldEndHintText: 'end_date'.tr(),
-        initialEntryMode: DatePickerEntryMode.input,
-      );
-
-      if (date == null) {
-        filter.value = filter.value.copyWith(
-          date: SearchDateFilter(),
-        );
-
-        dateRangeCurrentFilterWidget.value = null;
-        search();
-        return;
-      }
-
-      filter.value = filter.value.copyWith(
-        date: SearchDateFilter(
-          takenAfter: date.start,
-          takenBefore: date.end.add(
-            const Duration(
-              hours: 23,
-              minutes: 59,
-              seconds: 59,
-            ),
-          ),
-        ),
-      );
-
-      // If date range is less than 24 hours, set the end date to the end of the day
-      if (date.end.difference(date.start).inHours < 24) {
-        dateRangeCurrentFilterWidget.value = Text(
-          DateFormat.yMMMd().format(date.start.toLocal()),
-          style: context.textTheme.labelLarge,
-        );
-      } else {
-        dateRangeCurrentFilterWidget.value = Text(
-          'search_filter_date_interval'.tr(
-            namedArgs: {
-              "start": DateFormat.yMMMd().format(date.start.toLocal()),
-              "end": DateFormat.yMMMd().format(date.end.toLocal()),
-            },
-          ),
-          style: context.textTheme.labelLarge,
-        );
-      }
-
-      search();
-    }
-
-    // MEDIA PICKER
-    showMediaTypePicker() {
-      handleOnSelected(AssetType assetType) {
-        filter.value = filter.value.copyWith(
-          mediaType: assetType,
-        );
-
-        mediaTypeCurrentFilterWidget.value = Text(
-          assetType == AssetType.image
-              ? 'search_filter_media_type_image'.tr()
-              : assetType == AssetType.video
-                  ? 'search_filter_media_type_video'.tr()
-                  : 'search_filter_media_type_all'.tr(),
-          style: context.textTheme.labelLarge,
-        );
-      }
-
-      handleClear() {
-        filter.value = filter.value.copyWith(
-          mediaType: AssetType.other,
-        );
-
-        mediaTypeCurrentFilterWidget.value = null;
-        search();
-      }
-
-      showFilterBottomSheet(
-        context: context,
-        child: FilterBottomSheetScaffold(
-          title: 'search_filter_media_type_title'.tr(),
-          onSearch: search,
-          onClear: handleClear,
-          child: MediaTypePicker(
-            onSelect: handleOnSelected,
-            filter: filter.value.mediaType,
-          ),
-        ),
-      );
-    }
-
-    // DISPLAY OPTION
-    showDisplayOptionPicker() {
-      handleOnSelect(Map<DisplayOption, bool> value) {
-        final filterText = <String>[];
-
-        value.forEach((key, value) {
-          switch (key) {
-            case DisplayOption.notInAlbum:
-              filter.value = filter.value.copyWith(
-                display: filter.value.display.copyWith(
-                  isNotInAlbum: value,
-                ),
-              );
-              if (value) {
-                filterText
-                    .add('search_filter_display_option_not_in_album'.tr());
-              }
-              break;
-            case DisplayOption.archive:
-              filter.value = filter.value.copyWith(
-                display: filter.value.display.copyWith(
-                  isArchive: value,
-                ),
-              );
-              if (value) {
-                filterText.add('search_filter_display_option_archive'.tr());
-              }
-              break;
-            case DisplayOption.favorite:
-              filter.value = filter.value.copyWith(
-                display: filter.value.display.copyWith(
-                  isFavorite: value,
-                ),
-              );
-              if (value) {
-                filterText.add('search_filter_display_option_favorite'.tr());
-              }
-              break;
-          }
-        });
-
-        displayOptionCurrentFilterWidget.value = Text(
-          filterText.join(', '),
-          style: context.textTheme.labelLarge,
-        );
-      }
-
-      handleClear() {
-        filter.value = filter.value.copyWith(
-          display: SearchDisplayFilters(
-            isNotInAlbum: false,
-            isArchive: false,
-            isFavorite: false,
-          ),
-        );
-
-        displayOptionCurrentFilterWidget.value = null;
-        search();
-      }
-
-      showFilterBottomSheet(
-        context: context,
-        child: FilterBottomSheetScaffold(
-          title: 'search_filter_display_options_title'.tr(),
-          onSearch: search,
-          onClear: handleClear,
-          child: DisplayOptionPicker(
-            onSelect: handleOnSelect,
-            filter: filter.value.display,
-          ),
-        ),
-      );
-    }
-
-    handleTextSubmitted(String value) {
-      if (isContextualSearch.value) {
-        filter.value = filter.value.copyWith(
-          context: value,
-          filename: null,
-        );
-      } else {
-        filter.value = filter.value.copyWith(filename: value, context: null);
-      }
-
-      search();
-    }
-
-    buildSearchResult() {
-      return switch (searchProvider) {
-        AsyncData() => Expanded(
-            child: Padding(
-              padding: const EdgeInsets.only(top: 8.0),
-              child: NotificationListener<ScrollEndNotification>(
-                onNotification: (notification) {
-                  final metrics = notification.metrics;
-                  final shouldLoadMore = searchResultCount.value > 75;
-                  if (metrics.pixels >= metrics.maxScrollExtent &&
-                      shouldLoadMore) {
-                    loadMoreSearchResult();
-                  }
-                  return true;
-                },
-                child: MultiselectGrid(
-                  renderListProvider: paginatedSearchRenderListProvider,
-                  archiveEnabled: true,
-                  deleteEnabled: true,
-                  editEnabled: true,
-                  favoriteEnabled: true,
-                  stackEnabled: false,
-                  emptyIndicator: const SizedBox(),
-                ),
-              ),
-            ),
-          ),
-        AsyncError(:final error) => Text('Error: $error'),
-        _ => const Expanded(child: Center(child: CircularProgressIndicator())),
-      };
-    }
-
-    return Scaffold(
-      resizeToAvoidBottomInset: true,
-      appBar: AppBar(
-        automaticallyImplyLeading: true,
-        actions: [
-          IconButton(
-            icon: isContextualSearch.value
-                ? const Icon(Icons.abc_rounded)
-                : const Icon(Icons.image_search_rounded),
-            onPressed: () {
-              isContextualSearch.value = !isContextualSearch.value;
-              textSearchController.clear();
-            },
-          ),
-        ],
-        leading: IconButton(
-          icon: const Icon(Icons.arrow_back_ios_new_rounded),
-          onPressed: () => context.router.maybePop(),
-        ),
-        title: TextField(
-          controller: textSearchController,
-          decoration: InputDecoration(
-            hintText: isContextualSearch.value
-                ? 'contextual_search'.tr()
-                : 'filename_search'.tr(),
-            hintStyle: context.textTheme.bodyLarge?.copyWith(
-              color: context.themeData.colorScheme.onSurfaceSecondary,
-              fontWeight: FontWeight.w500,
-            ),
-            enabledBorder: const UnderlineInputBorder(
-              borderSide: BorderSide(color: Colors.transparent),
-            ),
-            focusedBorder: const UnderlineInputBorder(
-              borderSide: BorderSide(color: Colors.transparent),
-            ),
-          ),
-          onSubmitted: handleTextSubmitted,
-        ),
-      ),
-      body: Column(
-        children: [
-          Padding(
-            padding: const EdgeInsets.only(top: 12.0),
-            child: SizedBox(
-              height: 50,
-              child: ListView(
-                shrinkWrap: true,
-                scrollDirection: Axis.horizontal,
-                padding: const EdgeInsets.symmetric(horizontal: 16),
-                children: [
-                  SearchFilterChip(
-                    icon: Icons.people_alt_rounded,
-                    onTap: showPeoplePicker,
-                    label: 'search_filter_people'.tr(),
-                    currentFilter: peopleCurrentFilterWidget.value,
-                  ),
-                  SearchFilterChip(
-                    icon: Icons.location_pin,
-                    onTap: showLocationPicker,
-                    label: 'search_filter_location'.tr(),
-                    currentFilter: locationCurrentFilterWidget.value,
-                  ),
-                  SearchFilterChip(
-                    icon: Icons.camera_alt_rounded,
-                    onTap: showCameraPicker,
-                    label: 'search_filter_camera'.tr(),
-                    currentFilter: cameraCurrentFilterWidget.value,
-                  ),
-                  SearchFilterChip(
-                    icon: Icons.date_range_rounded,
-                    onTap: showDatePicker,
-                    label: 'search_filter_date'.tr(),
-                    currentFilter: dateRangeCurrentFilterWidget.value,
-                  ),
-                  SearchFilterChip(
-                    icon: Icons.video_collection_outlined,
-                    onTap: showMediaTypePicker,
-                    label: 'search_filter_media_type'.tr(),
-                    currentFilter: mediaTypeCurrentFilterWidget.value,
-                  ),
-                  SearchFilterChip(
-                    icon: Icons.display_settings_outlined,
-                    onTap: showDisplayOptionPicker,
-                    label: 'search_filter_display_options'.tr(),
-                    currentFilter: displayOptionCurrentFilterWidget.value,
-                  ),
-                ],
-              ),
-            ),
-          ),
-          buildSearchResult(),
-        ],
-      ),
-    );
-  }
-}
diff --git a/mobile/lib/pages/sharing/sharing.page.dart b/mobile/lib/pages/sharing/sharing.page.dart
deleted file mode 100644
index 98d4cfafe9..0000000000
--- a/mobile/lib/pages/sharing/sharing.page.dart
+++ /dev/null
@@ -1,283 +0,0 @@
-import 'package:auto_route/auto_route.dart';
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:immich_mobile/extensions/theme_extensions.dart';
-import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
-import 'package:immich_mobile/providers/album/shared_album.provider.dart';
-import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
-import 'package:immich_mobile/providers/partner.provider.dart';
-import 'package:immich_mobile/widgets/partner/partner_list.dart';
-import 'package:immich_mobile/routing/router.dart';
-import 'package:immich_mobile/providers/user.provider.dart';
-import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
-import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
-
-@RoutePage()
-class SharingPage extends HookConsumerWidget {
-  const SharingPage({super.key});
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    final albumSortOption = ref.watch(albumSortByOptionsProvider);
-    final albumSortIsReverse = ref.watch(albumSortOrderProvider);
-    final albums = ref.watch(sharedAlbumProvider);
-    final sharedAlbums = albumSortOption.sortFn(albums, albumSortIsReverse);
-    final userId = ref.watch(currentUserProvider)?.id;
-    final partner = ref.watch(partnerSharedWithProvider);
-
-    useEffect(
-      () {
-        ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
-        return null;
-      },
-      [],
-    );
-
-    buildAlbumGrid() {
-      return SliverPadding(
-        padding: const EdgeInsets.all(18.0),
-        sliver: SliverGrid(
-          gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
-            maxCrossAxisExtent: 250,
-            mainAxisSpacing: 12,
-            crossAxisSpacing: 12,
-            childAspectRatio: .7,
-          ),
-          delegate: SliverChildBuilderDelegate(
-            (context, index) {
-              return AlbumThumbnailCard(
-                album: sharedAlbums[index],
-                showOwner: true,
-                onTap: () => context.pushRoute(
-                  AlbumViewerRoute(albumId: sharedAlbums[index].id),
-                ),
-              );
-            },
-            childCount: sharedAlbums.length,
-          ),
-        ),
-      );
-    }
-
-    buildAlbumList() {
-      return SliverList(
-        delegate: SliverChildBuilderDelegate(
-          (BuildContext context, int index) {
-            final album = sharedAlbums[index];
-            final isOwner = album.ownerId == userId;
-
-            return ListTile(
-              contentPadding: const EdgeInsets.symmetric(horizontal: 12),
-              leading: ClipRRect(
-                borderRadius: const BorderRadius.all(Radius.circular(8)),
-                child: ImmichThumbnail(
-                  asset: album.thumbnail.value,
-                  width: 60,
-                  height: 60,
-                ),
-              ),
-              title: Text(
-                album.name,
-                maxLines: 1,
-                overflow: TextOverflow.ellipsis,
-                style: context.textTheme.bodyMedium?.copyWith(
-                  color: context.colorScheme.onSurface,
-                  fontWeight: FontWeight.w500,
-                ),
-              ),
-              subtitle: isOwner
-                  ? Text(
-                      'album_thumbnail_owned'.tr(),
-                      style: context.textTheme.bodyMedium?.copyWith(
-                        color: context.colorScheme.onSurfaceSecondary,
-                      ),
-                    )
-                  : album.ownerName != null
-                      ? Text(
-                          'album_thumbnail_shared_by'
-                              .tr(args: [album.ownerName!]),
-                          style: context.textTheme.bodyMedium?.copyWith(
-                            color: context.colorScheme.onSurfaceSecondary,
-                          ),
-                        )
-                      : null,
-              onTap: () => context
-                  .pushRoute(AlbumViewerRoute(albumId: sharedAlbums[index].id)),
-            );
-          },
-          childCount: sharedAlbums.length,
-        ),
-      );
-    }
-
-    buildTopBottons() {
-      return Padding(
-        padding: const EdgeInsets.only(
-          left: 12.0,
-          right: 12.0,
-          top: 24.0,
-          bottom: 12.0,
-        ),
-        child: Row(
-          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
-          children: [
-            Expanded(
-              child: ElevatedButton.icon(
-                onPressed: () =>
-                    context.pushRoute(CreateAlbumRoute(isSharedAlbum: true)),
-                icon: const Icon(
-                  Icons.photo_album_outlined,
-                  size: 20,
-                ),
-                label: const Text(
-                  "sharing_silver_appbar_create_shared_album",
-                  maxLines: 1,
-                  style: TextStyle(
-                    fontWeight: FontWeight.w500,
-                    fontSize: 12,
-                  ),
-                ).tr(),
-              ),
-            ),
-            const SizedBox(width: 12.0),
-            Expanded(
-              child: ElevatedButton.icon(
-                onPressed: () => context.pushRoute(const SharedLinkRoute()),
-                icon: const Icon(
-                  Icons.link,
-                  size: 20,
-                ),
-                label: const Text(
-                  "sharing_silver_appbar_shared_links",
-                  style: TextStyle(
-                    fontWeight: FontWeight.w500,
-                    fontSize: 12,
-                  ),
-                  maxLines: 1,
-                ).tr(),
-              ),
-            ),
-          ],
-        ),
-      );
-    }
-
-    buildEmptyListIndication() {
-      return SliverToBoxAdapter(
-        child: Padding(
-          padding: const EdgeInsets.all(8.0),
-          child: Card(
-            elevation: 0,
-            shape: RoundedRectangleBorder(
-              borderRadius: const BorderRadius.all(Radius.circular(20)),
-              side: BorderSide(
-                color: context.isDarkTheme
-                    ? const Color(0xFF383838)
-                    : Colors.black12,
-                width: 1,
-              ),
-            ),
-            child: Padding(
-              padding: const EdgeInsets.all(18.0),
-              child: Column(
-                crossAxisAlignment: CrossAxisAlignment.start,
-                children: [
-                  Padding(
-                    padding: const EdgeInsets.only(left: 5.0, bottom: 5),
-                    child: Icon(
-                      Icons.insert_photo_rounded,
-                      size: 50,
-                      color: context.primaryColor,
-                    ),
-                  ),
-                  Padding(
-                    padding: const EdgeInsets.all(8.0),
-                    child: Text(
-                      'sharing_page_empty_list',
-                      style: context.textTheme.displaySmall,
-                    ).tr(),
-                  ),
-                  Padding(
-                    padding: const EdgeInsets.all(8.0),
-                    child: Text(
-                      'sharing_page_description',
-                      style: context.textTheme.bodyMedium,
-                    ).tr(),
-                  ),
-                ],
-              ),
-            ),
-          ),
-        ),
-      );
-    }
-
-    Widget sharePartnerButton() {
-      return InkWell(
-        onTap: () => context.pushRoute(const PartnerRoute()),
-        borderRadius: const BorderRadius.all(Radius.circular(12)),
-        child: Icon(
-          Icons.swap_horizontal_circle_rounded,
-          size: 25,
-          semanticLabel: 'partner_page_title'.tr(),
-        ),
-      );
-    }
-
-    return RefreshIndicator(
-      onRefresh: () async {
-        ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
-      },
-      child: Scaffold(
-        appBar: ImmichAppBar(
-          action: sharePartnerButton(),
-        ),
-        body: CustomScrollView(
-          slivers: [
-            SliverToBoxAdapter(child: buildTopBottons()),
-            if (partner.isNotEmpty)
-              SliverPadding(
-                padding: const EdgeInsets.all(12),
-                sliver: SliverToBoxAdapter(
-                  child: Text(
-                    "partner_page_title",
-                    style: context.textTheme.bodyLarge?.copyWith(
-                      fontWeight: FontWeight.w500,
-                    ),
-                  ).tr(),
-                ),
-              ),
-            if (partner.isNotEmpty) PartnerList(partner: partner),
-            SliverPadding(
-              padding: const EdgeInsets.all(12),
-              sliver: SliverToBoxAdapter(
-                child: Text(
-                  "sharing_page_album",
-                  style: context.textTheme.bodyLarge?.copyWith(
-                    fontWeight: FontWeight.w500,
-                  ),
-                ).tr(),
-              ),
-            ),
-            SliverLayoutBuilder(
-              builder: (context, constraints) {
-                if (sharedAlbums.isEmpty) {
-                  return buildEmptyListIndication();
-                }
-
-                if (constraints.crossAxisExtent < 600) {
-                  return buildAlbumList();
-                } else {
-                  return buildAlbumGrid();
-                }
-              },
-            ),
-          ],
-        ),
-      ),
-    );
-  }
-}
diff --git a/mobile/lib/providers/activity_service.provider.dart b/mobile/lib/providers/activity_service.provider.dart
index dcfaac883f..6bd139c565 100644
--- a/mobile/lib/providers/activity_service.provider.dart
+++ b/mobile/lib/providers/activity_service.provider.dart
@@ -1,9 +1,9 @@
+import 'package:immich_mobile/repositories/activity_api.repository.dart';
 import 'package:immich_mobile/services/activity.service.dart';
-import 'package:immich_mobile/providers/api.provider.dart';
 import 'package:riverpod_annotation/riverpod_annotation.dart';
 
 part 'activity_service.provider.g.dart';
 
 @riverpod
 ActivityService activityService(ActivityServiceRef ref) =>
-    ActivityService(ref.watch(apiServiceProvider));
+    ActivityService(ref.watch(activityApiRepositoryProvider));
diff --git a/mobile/lib/providers/activity_service.provider.g.dart b/mobile/lib/providers/activity_service.provider.g.dart
index 8e5ef43260..d42b2a39e4 100644
Binary files a/mobile/lib/providers/activity_service.provider.g.dart and b/mobile/lib/providers/activity_service.provider.g.dart differ
diff --git a/mobile/lib/providers/activity_statistics.provider.dart b/mobile/lib/providers/activity_statistics.provider.dart
index afb43e8cba..b1d2b4b987 100644
--- a/mobile/lib/providers/activity_statistics.provider.dart
+++ b/mobile/lib/providers/activity_statistics.provider.dart
@@ -11,7 +11,7 @@ class ActivityStatistics extends _$ActivityStatistics {
     ref
         .watch(activityServiceProvider)
         .getStatistics(albumId, assetId: assetId)
-        .then((comments) => state = comments);
+        .then((stats) => state = stats.comments);
     return 0;
   }
 
diff --git a/mobile/lib/providers/activity_statistics.provider.g.dart b/mobile/lib/providers/activity_statistics.provider.g.dart
index 79856c525b..16a3c0e81b 100644
Binary files a/mobile/lib/providers/activity_statistics.provider.g.dart and b/mobile/lib/providers/activity_statistics.provider.g.dart differ
diff --git a/mobile/lib/providers/album/album.provider.dart b/mobile/lib/providers/album/album.provider.dart
index ed9dc07f5e..b3d619a815 100644
--- a/mobile/lib/providers/album/album.provider.dart
+++ b/mobile/lib/providers/album/album.provider.dart
@@ -1,21 +1,22 @@
 import 'dart:async';
 
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/constants/enums.dart';
+import 'package:immich_mobile/entities/user.entity.dart';
+import 'package:immich_mobile/models/albums/album_search.model.dart';
 import 'package:immich_mobile/services/album.service.dart';
 import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/entities/album.entity.dart';
-import 'package:immich_mobile/entities/store.entity.dart';
-import 'package:immich_mobile/entities/user.entity.dart';
 import 'package:immich_mobile/providers/db.provider.dart';
 import 'package:immich_mobile/utils/renderlist_generator.dart';
 import 'package:isar/isar.dart';
 
+final isRefreshingRemoteAlbumProvider = StateProvider<bool>((ref) => false);
+
 class AlbumNotifier extends StateNotifier<List<Album>> {
-  AlbumNotifier(this._albumService, Isar db) : super([]) {
-    final query = db.albums
-        .filter()
-        .owner((q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId));
+  AlbumNotifier(this._albumService, this.db, this.ref) : super([]) {
+    final query = db.albums.filter().remoteIdIsNotNull();
     query.findAll().then((value) {
       if (mounted) {
         state = value;
@@ -25,14 +26,17 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
   }
 
   final AlbumService _albumService;
+  final Isar db;
+  final Ref ref;
   late final StreamSubscription<List<Album>> _streamSub;
 
-  Future<void> getAllAlbums() => Future.wait([
-        _albumService.refreshDeviceAlbums(),
-        _albumService.refreshRemoteAlbums(isShared: false),
-      ]);
+  Future<void> refreshRemoteAlbums() async {
+    ref.read(isRefreshingRemoteAlbumProvider.notifier).state = true;
+    await _albumService.refreshRemoteAlbums();
+    ref.read(isRefreshingRemoteAlbumProvider.notifier).state = false;
+  }
 
-  Future<void> getDeviceAlbums() => _albumService.refreshDeviceAlbums();
+  Future<void> refreshDeviceAlbums() => _albumService.refreshDeviceAlbums();
 
   Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
 
@@ -59,6 +63,57 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
     await createAlbum(albumName, {});
   }
 
+  Future<bool> leaveAlbum(Album album) async {
+    var res = await _albumService.leaveAlbum(album);
+
+    if (res) {
+      await deleteAlbum(album);
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  void searchAlbums(String searchTerm, QuickFilterMode filterMode) async {
+    state = await _albumService.search(searchTerm, filterMode);
+  }
+
+  Future<void> addUsers(Album album, List<String> userIds) async {
+    await _albumService.addUsers(album, userIds);
+  }
+
+  Future<bool> removeUser(Album album, User user) async {
+    final isRemoved = await _albumService.removeUser(album, user);
+
+    if (isRemoved && album.sharedUsers.isEmpty) {
+      state = state.where((element) => element.id != album.id).toList();
+    }
+
+    return isRemoved;
+  }
+
+  Future<void> addAssets(Album album, Iterable<Asset> assets) async {
+    await _albumService.addAssets(album, assets);
+  }
+
+  Future<bool> removeAsset(Album album, Iterable<Asset> assets) async {
+    return await _albumService.removeAsset(album, assets);
+  }
+
+  Future<bool> setActivitystatus(
+    Album album,
+    bool enabled,
+  ) {
+    return _albumService.setActivityStatus(album, enabled);
+  }
+
+  Future<Album?> toggleSortOrder(Album album) {
+    final order =
+        album.sortOrder == SortOrder.asc ? SortOrder.desc : SortOrder.asc;
+
+    return _albumService.updateSortOrder(album, order);
+  }
+
   @override
   void dispose() {
     _streamSub.cancel();
@@ -71,6 +126,7 @@ final albumProvider =
   return AlbumNotifier(
     ref.watch(albumServiceProvider),
     ref.watch(dbProvider),
+    ref,
   );
 });
 
@@ -87,10 +143,49 @@ final albumWatcher =
 final albumRenderlistProvider =
     StreamProvider.autoDispose.family<RenderList, int>((ref, albumId) {
   final album = ref.watch(albumWatcher(albumId)).value;
+
   if (album != null) {
-    final query =
-        album.assets.filter().isTrashedEqualTo(false).sortByFileCreatedAtDesc();
-    return renderListGeneratorWithGroupBy(query, GroupAssetsBy.none);
+    final query = album.assets.filter().isTrashedEqualTo(false);
+    if (album.sortOrder == SortOrder.asc) {
+      return renderListGeneratorWithGroupBy(
+        query.sortByFileCreatedAt(),
+        GroupAssetsBy.none,
+      );
+    } else if (album.sortOrder == SortOrder.desc) {
+      return renderListGeneratorWithGroupBy(
+        query.sortByFileCreatedAtDesc(),
+        GroupAssetsBy.none,
+      );
+    }
   }
+
   return const Stream.empty();
 });
+
+class LocalAlbumsNotifier extends StateNotifier<List<Album>> {
+  LocalAlbumsNotifier(this.db) : super([]) {
+    final query = db.albums.where().remoteIdIsNull();
+
+    query.findAll().then((value) {
+      if (mounted) {
+        state = value;
+      }
+    });
+
+    _streamSub = query.watch().listen((data) => state = data);
+  }
+
+  final Isar db;
+  late final StreamSubscription<List<Album>> _streamSub;
+
+  @override
+  void dispose() {
+    _streamSub.cancel();
+    super.dispose();
+  }
+}
+
+final localAlbumsProvider =
+    StateNotifierProvider.autoDispose<LocalAlbumsNotifier, List<Album>>((ref) {
+  return LocalAlbumsNotifier(ref.watch(dbProvider));
+});
diff --git a/mobile/lib/providers/album/album_sort_by_options.provider.dart b/mobile/lib/providers/album/album_sort_by_options.provider.dart
index 216688ee15..cafde37253 100644
--- a/mobile/lib/providers/album/album_sort_by_options.provider.dart
+++ b/mobile/lib/providers/album/album_sort_by_options.provider.dart
@@ -39,12 +39,21 @@ class _AlbumSortHandlers {
   static const AlbumSortFn mostRecent = _sortByMostRecent;
   static List<Album> _sortByMostRecent(List<Album> albums, bool isReverse) {
     final sorted = albums.sorted((a, b) {
-      if (a.endDate != null && b.endDate != null) {
-        return a.endDate!.compareTo(b.endDate!);
+      if (a.endDate == null && b.endDate == null) {
+        return 0;
       }
-      if (a.endDate == null) return 1;
-      if (b.endDate == null) return -1;
-      return 0;
+
+      if (a.endDate == null) {
+        // Put nulls at the end for recent sorting
+        return 1;
+      }
+
+      if (b.endDate == null) {
+        return -1;
+      }
+
+      // Sort by descending recent date
+      return b.endDate!.compareTo(a.endDate!);
     });
     return (isReverse ? sorted.reversed : sorted).toList();
   }
diff --git a/mobile/lib/providers/album/album_viewer.provider.dart b/mobile/lib/providers/album/album_viewer.provider.dart
index f34ff4ef22..e418657782 100644
--- a/mobile/lib/providers/album/album_viewer.provider.dart
+++ b/mobile/lib/providers/album/album_viewer.provider.dart
@@ -1,6 +1,5 @@
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/models/albums/album_viewer_page_state.model.dart';
-import 'package:immich_mobile/providers/album/shared_album.provider.dart';
 import 'package:immich_mobile/services/album.service.dart';
 import 'package:immich_mobile/entities/album.entity.dart';
 
@@ -40,7 +39,6 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
 
     if (isSuccess) {
       state = state.copyWith(editTitleText: "", isEditAlbum: false);
-      ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
 
       return true;
     }
diff --git a/mobile/lib/providers/album/shared_album.provider.dart b/mobile/lib/providers/album/shared_album.provider.dart
deleted file mode 100644
index 0d58135375..0000000000
--- a/mobile/lib/providers/album/shared_album.provider.dart
+++ /dev/null
@@ -1,90 +0,0 @@
-import 'dart:async';
-
-import 'package:flutter/material.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/services/album.service.dart';
-import 'package:immich_mobile/entities/album.entity.dart';
-import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:immich_mobile/entities/user.entity.dart';
-import 'package:immich_mobile/providers/db.provider.dart';
-import 'package:isar/isar.dart';
-
-class SharedAlbumNotifier extends StateNotifier<List<Album>> {
-  SharedAlbumNotifier(this._albumService, Isar db) : super([]) {
-    final query = db.albums.filter().sharedEqualTo(true).sortByCreatedAtDesc();
-    query.findAll().then((value) {
-      if (mounted) {
-        state = value;
-      }
-    });
-    _streamSub = query.watch().listen((data) => state = data);
-  }
-
-  final AlbumService _albumService;
-  late final StreamSubscription<List<Album>> _streamSub;
-
-  Future<Album?> createSharedAlbum(
-    String albumName,
-    Iterable<Asset> assets,
-    Iterable<User> sharedUsers,
-  ) async {
-    try {
-      return await _albumService.createAlbum(
-        albumName,
-        assets,
-        sharedUsers,
-      );
-    } catch (e) {
-      debugPrint("Error createSharedAlbum  ${e.toString()}");
-    }
-    return null;
-  }
-
-  Future<void> getAllSharedAlbums() =>
-      _albumService.refreshRemoteAlbums(isShared: true);
-
-  Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
-
-  Future<bool> leaveAlbum(Album album) async {
-    var res = await _albumService.leaveAlbum(album);
-
-    if (res) {
-      await deleteAlbum(album);
-      return true;
-    } else {
-      return false;
-    }
-  }
-
-  Future<bool> removeAssetFromAlbum(Album album, Iterable<Asset> assets) {
-    return _albumService.removeAssetFromAlbum(album, assets);
-  }
-
-  Future<bool> removeUserFromAlbum(Album album, User user) async {
-    final result = await _albumService.removeUserFromAlbum(album, user);
-
-    if (result && album.sharedUsers.isEmpty) {
-      state = state.where((element) => element.id != album.id).toList();
-    }
-
-    return result;
-  }
-
-  Future<bool> setActivityEnabled(Album album, bool activityEnabled) {
-    return _albumService.setActivityEnabled(album, activityEnabled);
-  }
-
-  @override
-  void dispose() {
-    _streamSub.cancel();
-    super.dispose();
-  }
-}
-
-final sharedAlbumProvider =
-    StateNotifierProvider.autoDispose<SharedAlbumNotifier, List<Album>>((ref) {
-  return SharedAlbumNotifier(
-    ref.watch(albumServiceProvider),
-    ref.watch(dbProvider),
-  );
-});
diff --git a/mobile/lib/providers/album/suggested_shared_users.provider.dart b/mobile/lib/providers/album/suggested_shared_users.provider.dart
index 77518f47d0..fe8a1fccce 100644
--- a/mobile/lib/providers/album/suggested_shared_users.provider.dart
+++ b/mobile/lib/providers/album/suggested_shared_users.provider.dart
@@ -5,5 +5,5 @@ import 'package:immich_mobile/services/user.service.dart';
 final otherUsersProvider = FutureProvider.autoDispose<List<User>>((ref) {
   UserService userService = ref.watch(userServiceProvider);
 
-  return userService.getUsersInDb();
+  return userService.getUsers();
 });
diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart
index 938961efb6..780e22b818 100644
--- a/mobile/lib/providers/app_life_cycle.provider.dart
+++ b/mobile/lib/providers/app_life_cycle.provider.dart
@@ -1,12 +1,12 @@
+import 'package:flutter/foundation.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/providers/album/album.provider.dart';
-import 'package:immich_mobile/providers/album/shared_album.provider.dart';
 import 'package:immich_mobile/services/background.service.dart';
 import 'package:immich_mobile/models/backup/backup_state.model.dart';
 import 'package:immich_mobile/providers/backup/backup.provider.dart';
 import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart';
 import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
-import 'package:immich_mobile/providers/authentication.provider.dart';
+import 'package:immich_mobile/providers/auth.provider.dart';
 import 'package:immich_mobile/providers/memory.provider.dart';
 import 'package:immich_mobile/providers/gallery_permission.provider.dart';
 import 'package:immich_mobile/providers/notification_permission.provider.dart';
@@ -36,44 +36,60 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
     return state;
   }
 
-  void handleAppResume() {
+  void handleAppResume() async {
     state = AppLifeCycleEnum.resumed;
 
     // no need to resume because app was never really paused
     if (!_wasPaused) return;
     _wasPaused = false;
 
-    final isAuthenticated = _ref.read(authenticationProvider).isAuthenticated;
+    final isAuthenticated = _ref.read(authProvider).isAuthenticated;
 
     // Needs to be logged in
     if (isAuthenticated) {
+      // switch endpoint if needed
+      final endpoint =
+          await _ref.read(authProvider.notifier).setOpenApiServiceEndpoint();
+      if (kDebugMode) {
+        debugPrint("Using server URL: $endpoint");
+      }
+
       final permission = _ref.watch(galleryPermissionNotifier);
       if (permission.isGranted || permission.isLimited) {
-        _ref.read(backupProvider.notifier).resumeBackup();
-        _ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
+        await _ref.read(backupProvider.notifier).resumeBackup();
+        await _ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
       }
-      _ref.read(serverInfoProvider.notifier).getServerVersion();
+
+      await _ref.read(serverInfoProvider.notifier).getServerVersion();
+
       switch (_ref.read(tabProvider)) {
         case TabEnum.home:
-          _ref.read(assetProvider.notifier).getAllAsset();
+          await _ref.read(assetProvider.notifier).getAllAsset();
+          break;
         case TabEnum.search:
-        // nothing to do
-        case TabEnum.sharing:
-          _ref.read(assetProvider.notifier).getAllAsset();
-          _ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
+          // nothing to do
+          break;
+
+        case TabEnum.albums:
+          await _ref.read(albumProvider.notifier).refreshRemoteAlbums();
+          break;
         case TabEnum.library:
-          _ref.read(albumProvider.notifier).getAllAlbums();
+          // nothing to do
+          break;
       }
     }
 
     _ref.read(websocketProvider.notifier).connect();
 
-    _ref
+    await _ref
         .read(notificationPermissionProvider.notifier)
         .getNotificationPermission();
-    _ref.read(galleryPermissionNotifier.notifier).getGalleryPermissionStatus();
 
-    _ref.read(iOSBackgroundSettingsProvider.notifier).refresh();
+    await _ref
+        .read(galleryPermissionNotifier.notifier)
+        .getGalleryPermissionStatus();
+
+    await _ref.read(iOSBackgroundSettingsProvider.notifier).refresh();
 
     _ref.invalidate(memoryFutureProvider);
   }
@@ -86,12 +102,16 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
   void handleAppPause() {
     state = AppLifeCycleEnum.paused;
     _wasPaused = true;
-    // Do not cancel backup if manual upload is in progress
-    if (_ref.read(backupProvider.notifier).backupProgress !=
-        BackUpProgressEnum.manualInProgress) {
-      _ref.read(backupProvider.notifier).cancelBackup();
+
+    if (_ref.read(authProvider).isAuthenticated) {
+      // Do not cancel backup if manual upload is in progress
+      if (_ref.read(backupProvider.notifier).backupProgress !=
+          BackUpProgressEnum.manualInProgress) {
+        _ref.read(backupProvider.notifier).cancelBackup();
+      }
+      _ref.read(websocketProvider.notifier).disconnect();
     }
-    _ref.read(websocketProvider.notifier).disconnect();
+
     ImmichLogger().flush();
   }
 
diff --git a/mobile/lib/providers/asset.provider.dart b/mobile/lib/providers/asset.provider.dart
index 3c1a5ecc01..9252de01bf 100644
--- a/mobile/lib/providers/asset.provider.dart
+++ b/mobile/lib/providers/asset.provider.dart
@@ -1,6 +1,8 @@
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/providers/locale_provider.dart';
 import 'package:immich_mobile/providers/memory.provider.dart';
+import 'package:immich_mobile/repositories/asset_media.repository.dart';
 import 'package:immich_mobile/services/album.service.dart';
 import 'package:immich_mobile/entities/exif_info.entity.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
@@ -15,7 +17,6 @@ import 'package:immich_mobile/utils/db.dart';
 import 'package:immich_mobile/utils/renderlist_generator.dart';
 import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
-import 'package:photo_manager/photo_manager.dart';
 
 class AssetNotifier extends StateNotifier<bool> {
   final AssetService _assetService;
@@ -84,34 +85,48 @@ class AssetNotifier extends StateNotifier<bool> {
     _deleteInProgress = true;
     state = true;
     try {
+      // Filter the assets based on the backed-up status
       final assets = onlyBackedUp
           ? deleteAssets.where((e) => e.storage == AssetState.merged)
           : deleteAssets;
+
+      if (assets.isEmpty) {
+        return false; // No assets to delete
+      }
+
+      // Proceed with local deletion of the filtered assets
       final localDeleted = await _deleteLocalAssets(assets);
+
       if (localDeleted.isNotEmpty) {
-        final localOnlyIds = deleteAssets
+        final localOnlyIds = assets
             .where((e) => e.storage == AssetState.local)
             .map((e) => e.id)
             .toList();
-        // Update merged assets to remote only
+
+        // Update merged assets to remote-only
         final mergedAssets =
-            deleteAssets.where((e) => e.storage == AssetState.merged).map((e) {
+            assets.where((e) => e.storage == AssetState.merged).map((e) {
           e.localId = null;
           return e;
         }).toList();
+
+        // Update the local database
         await _db.writeTxn(() async {
           if (mergedAssets.isNotEmpty) {
-            await _db.assets.putAll(mergedAssets);
+            await _db.assets
+                .putAll(mergedAssets); // Use the filtered merged assets
           }
           await _db.exifInfos.deleteAll(localOnlyIds);
           await _db.assets.deleteAll(localOnlyIds);
         });
+
         return true;
       }
     } finally {
       _deleteInProgress = false;
       state = false;
     }
+
     return false;
   }
 
@@ -257,7 +272,7 @@ class AssetNotifier extends StateNotifier<bool> {
     // Delete asset from device
     if (local.isNotEmpty) {
       try {
-        return await PhotoManager.editor.deleteWithIds(local);
+        return await _ref.read(assetMediaRepositoryProvider).deleteAll(local);
       } catch (e, stack) {
         log.severe("Failed to delete asset from device", e, stack);
       }
@@ -275,28 +290,14 @@ class AssetNotifier extends StateNotifier<bool> {
     return isSuccess ? remote.toList() : [];
   }
 
-  Future<void> toggleFavorite(List<Asset> assets, [bool? status]) async {
+  Future<void> toggleFavorite(List<Asset> assets, [bool? status]) {
     status ??= !assets.every((a) => a.isFavorite);
-    final newAssets = await _assetService.changeFavoriteStatus(assets, status);
-    for (Asset? newAsset in newAssets) {
-      if (newAsset == null) {
-        log.severe("Change favorite status failed for asset");
-        continue;
-      }
-    }
+    return _assetService.changeFavoriteStatus(assets, status);
   }
 
-  Future<void> toggleArchive(List<Asset> assets, [bool? status]) async {
+  Future<void> toggleArchive(List<Asset> assets, [bool? status]) {
     status ??= !assets.every((a) => a.isArchived);
-    final newAssets = await _assetService.changeArchiveStatus(assets, status);
-    int i = 0;
-    for (Asset oldAsset in assets) {
-      final newAsset = newAssets[i++];
-      if (newAsset == null) {
-        log.severe("Change archive status failed for asset ${oldAsset.id}");
-        continue;
-      }
-    }
+    return _assetService.changeArchiveStatus(assets, status);
   }
 }
 
@@ -328,24 +329,31 @@ final assetWatcher =
   return db.assets.watchObject(asset.id, fireImmediately: true);
 });
 
-final assetsProvider = StreamProvider.family<RenderList, int?>((ref, userId) {
-  if (userId == null) return const Stream.empty();
-  final query = _commonFilterAndSort(
-    _assets(ref).where().ownerIdEqualToAnyChecksum(userId),
-  );
-  return renderListGenerator(query, ref);
-});
+final assetsProvider = StreamProvider.family<RenderList, int?>(
+  (ref, userId) {
+    if (userId == null) return const Stream.empty();
+    ref.watch(localeProvider);
+    final query = _commonFilterAndSort(
+      _assets(ref).where().ownerIdEqualToAnyChecksum(userId),
+    );
+    return renderListGenerator(query, ref);
+  },
+  dependencies: [localeProvider],
+);
 
-final multiUserAssetsProvider =
-    StreamProvider.family<RenderList, List<int>>((ref, userIds) {
-  if (userIds.isEmpty) return const Stream.empty();
-  final query = _commonFilterAndSort(
-    _assets(ref)
-        .where()
-        .anyOf(userIds, (q, u) => q.ownerIdEqualToAnyChecksum(u)),
-  );
-  return renderListGenerator(query, ref);
-});
+final multiUserAssetsProvider = StreamProvider.family<RenderList, List<int>>(
+  (ref, userIds) {
+    if (userIds.isEmpty) return const Stream.empty();
+    ref.watch(localeProvider);
+    final query = _commonFilterAndSort(
+      _assets(ref)
+          .where()
+          .anyOf(userIds, (q, u) => q.ownerIdEqualToAnyChecksum(u)),
+    );
+    return renderListGenerator(query, ref);
+  },
+  dependencies: [localeProvider],
+);
 
 QueryBuilder<Asset, Asset, QAfterSortBy>? getRemoteAssetQuery(WidgetRef ref) {
   final userId = ref.watch(currentUserProvider)?.isarId;
diff --git a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart
index c3e4414b39..407aef1610 100644
--- a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart
+++ b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart
@@ -7,49 +7,49 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
 part 'asset_stack.provider.g.dart';
 
 class AssetStackNotifier extends StateNotifier<List<Asset>> {
-  final Asset _asset;
+  final String _stackId;
   final Ref _ref;
 
-  AssetStackNotifier(
-    this._asset,
-    this._ref,
-  ) : super([]) {
-    fetchStackChildren();
+  AssetStackNotifier(this._stackId, this._ref) : super([]) {
+    _fetchStack(_stackId);
   }
 
-  void fetchStackChildren() async {
-    if (mounted) {
-      state = await _ref.read(assetStackProvider(_asset).future);
+  void _fetchStack(String stackId) async {
+    if (!mounted) {
+      return;
+    }
+
+    final stack = await _ref.read(assetStackProvider(stackId).future);
+    if (stack.isNotEmpty) {
+      state = stack;
     }
   }
 
   void removeChild(int index) {
     if (index < state.length) {
       state.removeAt(index);
+      state = List<Asset>.from(state);
     }
   }
 }
 
 final assetStackStateProvider = StateNotifierProvider.autoDispose
-    .family<AssetStackNotifier, List<Asset>, Asset>(
-  (ref, asset) => AssetStackNotifier(asset, ref),
+    .family<AssetStackNotifier, List<Asset>, String>(
+  (ref, stackId) => AssetStackNotifier(stackId, ref),
 );
 
 final assetStackProvider =
-    FutureProvider.autoDispose.family<List<Asset>, Asset>((ref, asset) async {
-  // Guard [local asset]
-  if (asset.remoteId == null) {
-    return [];
-  }
-
-  return await ref
+    FutureProvider.autoDispose.family<List<Asset>, String>((ref, stackId) {
+  return ref
       .watch(dbProvider)
       .assets
       .filter()
       .isArchivedEqualTo(false)
       .isTrashedEqualTo(false)
-      .stackPrimaryAssetIdEqualTo(asset.remoteId)
-      .sortByFileCreatedAtDesc()
+      .stackIdEqualTo(stackId)
+      // orders primary asset first as its ID is null
+      .sortByStackPrimaryAssetId()
+      .thenByFileCreatedAtDesc()
       .findAll();
 });
 
diff --git a/mobile/lib/providers/asset_viewer/download.provider.dart b/mobile/lib/providers/asset_viewer/download.provider.dart
new file mode 100644
index 0000000000..68b120c38a
--- /dev/null
+++ b/mobile/lib/providers/asset_viewer/download.provider.dart
@@ -0,0 +1,196 @@
+import 'package:background_downloader/background_downloader.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:fluttertoast/fluttertoast.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/models/download/download_state.model.dart';
+import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
+import 'package:immich_mobile/services/album.service.dart';
+import 'package:immich_mobile/services/download.service.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/services/share.service.dart';
+import 'package:immich_mobile/widgets/common/immich_toast.dart';
+import 'package:immich_mobile/widgets/common/share_dialog.dart';
+
+class DownloadStateNotifier extends StateNotifier<DownloadState> {
+  final DownloadService _downloadService;
+  final ShareService _shareService;
+  final AlbumService _albumService;
+
+  DownloadStateNotifier(
+    this._downloadService,
+    this._shareService,
+    this._albumService,
+  ) : super(
+          DownloadState(
+            downloadStatus: TaskStatus.complete,
+            showProgress: false,
+            taskProgress: <String, DownloadInfo>{},
+          ),
+        ) {
+    _downloadService.onImageDownloadStatus = _downloadImageCallback;
+    _downloadService.onVideoDownloadStatus = _downloadVideoCallback;
+    _downloadService.onLivePhotoDownloadStatus = _downloadLivePhotoCallback;
+    _downloadService.onTaskProgress = _taskProgressCallback;
+  }
+
+  void _updateDownloadStatus(String taskId, TaskStatus status) {
+    if (status == TaskStatus.canceled) {
+      return;
+    }
+
+    state = state.copyWith(
+      taskProgress: <String, DownloadInfo>{}
+        ..addAll(state.taskProgress)
+        ..addAll({
+          taskId: DownloadInfo(
+            progress: state.taskProgress[taskId]?.progress ?? 0,
+            fileName: state.taskProgress[taskId]?.fileName ?? '',
+            status: status,
+          ),
+        }),
+    );
+  }
+
+  // Download live photo callback
+  void _downloadLivePhotoCallback(TaskStatusUpdate update) {
+    _updateDownloadStatus(update.task.taskId, update.status);
+
+    switch (update.status) {
+      case TaskStatus.complete:
+        if (update.task.metaData.isEmpty) {
+          return;
+        }
+        final livePhotosId =
+            LivePhotosMetadata.fromJson(update.task.metaData).id;
+        _downloadService.saveLivePhotos(update.task, livePhotosId);
+        _onDownloadComplete(update.task.taskId);
+        break;
+
+      default:
+        break;
+    }
+  }
+
+  // Download image callback
+  void _downloadImageCallback(TaskStatusUpdate update) {
+    _updateDownloadStatus(update.task.taskId, update.status);
+
+    switch (update.status) {
+      case TaskStatus.complete:
+        _downloadService.saveImageWithPath(update.task);
+        _onDownloadComplete(update.task.taskId);
+        break;
+
+      default:
+        break;
+    }
+  }
+
+  // Download video callback
+  void _downloadVideoCallback(TaskStatusUpdate update) {
+    _updateDownloadStatus(update.task.taskId, update.status);
+
+    switch (update.status) {
+      case TaskStatus.complete:
+        _downloadService.saveVideo(update.task);
+        _onDownloadComplete(update.task.taskId);
+        break;
+
+      default:
+        break;
+    }
+  }
+
+  void _taskProgressCallback(TaskProgressUpdate update) {
+    // Ignore if the task is cancled or completed
+    if (update.progress == -2 || update.progress == -1) {
+      return;
+    }
+
+    state = state.copyWith(
+      showProgress: true,
+      taskProgress: <String, DownloadInfo>{}
+        ..addAll(state.taskProgress)
+        ..addAll({
+          update.task.taskId: DownloadInfo(
+            progress: update.progress,
+            fileName: update.task.filename,
+            status: TaskStatus.running,
+          ),
+        }),
+    );
+  }
+
+  void _onDownloadComplete(String id) {
+    Future.delayed(const Duration(seconds: 2), () {
+      state = state.copyWith(
+        taskProgress: <String, DownloadInfo>{}
+          ..addAll(state.taskProgress)
+          ..remove(id),
+      );
+
+      if (state.taskProgress.isEmpty) {
+        state = state.copyWith(
+          showProgress: false,
+        );
+      }
+      _albumService.refreshDeviceAlbums();
+    });
+  }
+
+  void downloadAsset(Asset asset, BuildContext context) async {
+    await _downloadService.download(asset);
+  }
+
+  void cancelDownload(String id) async {
+    final isCanceled = await _downloadService.cancelDownload(id);
+
+    if (isCanceled) {
+      state = state.copyWith(
+        taskProgress: <String, DownloadInfo>{}
+          ..addAll(state.taskProgress)
+          ..remove(id),
+      );
+    }
+
+    if (state.taskProgress.isEmpty) {
+      state = state.copyWith(
+        showProgress: false,
+      );
+    }
+  }
+
+  void shareAsset(Asset asset, BuildContext context) async {
+    showDialog(
+      context: context,
+      builder: (BuildContext buildContext) {
+        _shareService.shareAsset(asset, context).then(
+          (bool status) {
+            if (!status) {
+              ImmichToast.show(
+                context: context,
+                msg: 'image_viewer_page_state_provider_share_error'.tr(),
+                toastType: ToastType.error,
+                gravity: ToastGravity.BOTTOM,
+              );
+            }
+            buildContext.pop();
+          },
+        );
+        return const ShareDialog();
+      },
+      barrierDismissible: false,
+    );
+  }
+}
+
+final downloadStateProvider =
+    StateNotifierProvider<DownloadStateNotifier, DownloadState>(
+  ((ref) => DownloadStateNotifier(
+        ref.watch(downloadServiceProvider),
+        ref.watch(shareServiceProvider),
+        ref.watch(albumServiceProvider),
+      )),
+);
diff --git a/mobile/lib/providers/asset_viewer/image_viewer_page_state.provider.dart b/mobile/lib/providers/asset_viewer/image_viewer_page_state.provider.dart
deleted file mode 100644
index 631011f200..0000000000
--- a/mobile/lib/providers/asset_viewer/image_viewer_page_state.provider.dart
+++ /dev/null
@@ -1,99 +0,0 @@
-import 'dart:io';
-
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/material.dart';
-import 'package:fluttertoast/fluttertoast.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:immich_mobile/services/album.service.dart';
-import 'package:immich_mobile/models/asset_viewer/asset_viewer_page_state.model.dart';
-import 'package:immich_mobile/services/image_viewer.service.dart';
-import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:immich_mobile/services/share.service.dart';
-import 'package:immich_mobile/widgets/common/immich_toast.dart';
-import 'package:immich_mobile/widgets/common/share_dialog.dart';
-
-class ImageViewerStateNotifier extends StateNotifier<AssetViewerPageState> {
-  final ImageViewerService _imageViewerService;
-  final ShareService _shareService;
-  final AlbumService _albumService;
-
-  ImageViewerStateNotifier(
-    this._imageViewerService,
-    this._shareService,
-    this._albumService,
-  ) : super(
-          AssetViewerPageState(
-            downloadAssetStatus: DownloadAssetStatus.idle,
-          ),
-        );
-
-  void downloadAsset(Asset asset, BuildContext context) async {
-    state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading);
-
-    ImmichToast.show(
-      context: context,
-      msg: 'download_started'.tr(),
-      toastType: ToastType.info,
-      gravity: ToastGravity.BOTTOM,
-    );
-
-    bool isSuccess = await _imageViewerService.downloadAsset(asset);
-
-    if (isSuccess) {
-      state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.success);
-
-      ImmichToast.show(
-        context: context,
-        msg: Platform.isAndroid
-            ? 'download_sucess_android'.tr()
-            : 'download_sucess'.tr(),
-        toastType: ToastType.success,
-        gravity: ToastGravity.BOTTOM,
-      );
-      _albumService.refreshDeviceAlbums();
-    } else {
-      state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.error);
-      ImmichToast.show(
-        context: context,
-        msg: 'download_error'.tr(),
-        toastType: ToastType.error,
-        gravity: ToastGravity.BOTTOM,
-      );
-    }
-
-    state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle);
-  }
-
-  void shareAsset(Asset asset, BuildContext context) async {
-    showDialog(
-      context: context,
-      builder: (BuildContext buildContext) {
-        _shareService.shareAsset(asset, context).then(
-          (bool status) {
-            if (!status) {
-              ImmichToast.show(
-                context: context,
-                msg: 'image_viewer_page_state_provider_share_error'.tr(),
-                toastType: ToastType.error,
-                gravity: ToastGravity.BOTTOM,
-              );
-            }
-            buildContext.pop();
-          },
-        );
-        return const ShareDialog();
-      },
-      barrierDismissible: false,
-    );
-  }
-}
-
-final imageViewerStateProvider =
-    StateNotifierProvider<ImageViewerStateNotifier, AssetViewerPageState>(
-  ((ref) => ImageViewerStateNotifier(
-        ref.watch(imageViewerServiceProvider),
-        ref.watch(shareServiceProvider),
-        ref.watch(albumServiceProvider),
-      )),
-);
diff --git a/mobile/lib/providers/asset_viewer/is_motion_video_playing.provider.dart b/mobile/lib/providers/asset_viewer/is_motion_video_playing.provider.dart
new file mode 100644
index 0000000000..4af061f954
--- /dev/null
+++ b/mobile/lib/providers/asset_viewer/is_motion_video_playing.provider.dart
@@ -0,0 +1,23 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+
+/// Whether to display the video part of a motion photo
+final isPlayingMotionVideoProvider =
+    StateNotifierProvider<IsPlayingMotionVideo, bool>((ref) {
+  return IsPlayingMotionVideo(ref);
+});
+
+class IsPlayingMotionVideo extends StateNotifier<bool> {
+  IsPlayingMotionVideo(this.ref) : super(false);
+
+  final Ref ref;
+
+  bool get playing => state;
+
+  set playing(bool value) {
+    state = value;
+  }
+
+  void toggle() {
+    state = !state;
+  }
+}
diff --git a/mobile/lib/providers/asset_viewer/render_list_status_provider.dart b/mobile/lib/providers/asset_viewer/render_list_status_provider.dart
new file mode 100644
index 0000000000..903007031e
--- /dev/null
+++ b/mobile/lib/providers/asset_viewer/render_list_status_provider.dart
@@ -0,0 +1,20 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+
+enum RenderListStatusEnum { complete, empty, error, loading }
+
+final renderListStatusProvider =
+    StateNotifierProvider<RenderListStatus, RenderListStatusEnum>((ref) {
+  return RenderListStatus(ref);
+});
+
+class RenderListStatus extends StateNotifier<RenderListStatusEnum> {
+  RenderListStatus(this.ref) : super(RenderListStatusEnum.complete);
+
+  final Ref ref;
+
+  RenderListStatusEnum get status => state;
+
+  set status(RenderListStatusEnum value) {
+    state = value;
+  }
+}
diff --git a/mobile/lib/providers/asset_viewer/video_player_controller_provider.dart b/mobile/lib/providers/asset_viewer/video_player_controller_provider.dart
deleted file mode 100644
index 969e181cbb..0000000000
--- a/mobile/lib/providers/asset_viewer/video_player_controller_provider.dart
+++ /dev/null
@@ -1,46 +0,0 @@
-import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:immich_mobile/entities/store.entity.dart';
-import 'package:immich_mobile/services/api.service.dart';
-import 'package:riverpod_annotation/riverpod_annotation.dart';
-import 'package:video_player/video_player.dart';
-
-part 'video_player_controller_provider.g.dart';
-
-@riverpod
-Future<VideoPlayerController> videoPlayerController(
-  VideoPlayerControllerRef ref, {
-  required Asset asset,
-}) async {
-  late VideoPlayerController controller;
-  if (asset.isLocal && asset.livePhotoVideoId == null) {
-    // Use a local file for the video player controller
-    final file = await asset.local!.file;
-    if (file == null) {
-      throw Exception('No file found for the video');
-    }
-    controller = VideoPlayerController.file(file);
-  } else {
-    // Use a network URL for the video player controller
-    final serverEndpoint = Store.get(StoreKey.serverEndpoint);
-    final String videoUrl = asset.livePhotoVideoId != null
-        ? '$serverEndpoint/assets/${asset.livePhotoVideoId}/video/playback'
-        : '$serverEndpoint/assets/${asset.remoteId}/video/playback';
-
-    final url = Uri.parse(videoUrl);
-    controller = VideoPlayerController.networkUrl(
-      url,
-      httpHeaders: ApiService.getRequestHeaders(),
-      videoPlayerOptions: asset.livePhotoVideoId != null
-          ? VideoPlayerOptions(mixWithOthers: true)
-          : VideoPlayerOptions(mixWithOthers: false),
-    );
-  }
-
-  await controller.initialize();
-
-  ref.onDispose(() {
-    controller.dispose();
-  });
-
-  return controller;
-}
diff --git a/mobile/lib/providers/asset_viewer/video_player_controller_provider.g.dart b/mobile/lib/providers/asset_viewer/video_player_controller_provider.g.dart
deleted file mode 100644
index 00ad37648a..0000000000
Binary files a/mobile/lib/providers/asset_viewer/video_player_controller_provider.g.dart and /dev/null differ
diff --git a/mobile/lib/providers/asset_viewer/video_player_controls_provider.dart b/mobile/lib/providers/asset_viewer/video_player_controls_provider.dart
index d15b26ea20..69be91480f 100644
--- a/mobile/lib/providers/asset_viewer/video_player_controls_provider.dart
+++ b/mobile/lib/providers/asset_viewer/video_player_controls_provider.dart
@@ -1,15 +1,16 @@
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
 
 class VideoPlaybackControls {
-  VideoPlaybackControls({
+  const VideoPlaybackControls({
     required this.position,
-    required this.mute,
     required this.pause,
+    this.restarted = false,
   });
 
   final double position;
-  final bool mute;
   final bool pause;
+  final bool restarted;
 }
 
 final videoPlayerControlsProvider =
@@ -17,15 +18,11 @@ final videoPlayerControlsProvider =
   return VideoPlayerControls(ref);
 });
 
+const videoPlayerControlsDefault =
+    VideoPlaybackControls(position: 0, pause: false);
+
 class VideoPlayerControls extends StateNotifier<VideoPlaybackControls> {
-  VideoPlayerControls(this.ref)
-      : super(
-          VideoPlaybackControls(
-            position: 0,
-            pause: false,
-            mute: false,
-          ),
-        );
+  VideoPlayerControls(this.ref) : super(videoPlayerControlsDefault);
 
   final Ref ref;
 
@@ -36,75 +33,48 @@ class VideoPlayerControls extends StateNotifier<VideoPlaybackControls> {
   }
 
   void reset() {
-    state = VideoPlaybackControls(
-      position: 0,
-      pause: false,
-      mute: false,
-    );
+    state = videoPlayerControlsDefault;
   }
 
   double get position => state.position;
-  bool get mute => state.mute;
+  bool get paused => state.pause;
 
   set position(double value) {
-    state = VideoPlaybackControls(
-      position: value,
-      mute: state.mute,
-      pause: state.pause,
-    );
-  }
+    if (state.position == value) {
+      return;
+    }
 
-  set mute(bool value) {
-    state = VideoPlaybackControls(
-      position: state.position,
-      mute: value,
-      pause: state.pause,
-    );
-  }
-
-  void toggleMute() {
-    state = VideoPlaybackControls(
-      position: state.position,
-      mute: !state.mute,
-      pause: state.pause,
-    );
+    state = VideoPlaybackControls(position: value, pause: state.pause);
   }
 
   void pause() {
-    state = VideoPlaybackControls(
-      position: state.position,
-      mute: state.mute,
-      pause: true,
-    );
+    if (state.pause) {
+      return;
+    }
+
+    state = VideoPlaybackControls(position: state.position, pause: true);
   }
 
   void play() {
-    state = VideoPlaybackControls(
-      position: state.position,
-      mute: state.mute,
-      pause: false,
-    );
+    if (!state.pause) {
+      return;
+    }
+
+    state = VideoPlaybackControls(position: state.position, pause: false);
   }
 
   void togglePlay() {
-    state = VideoPlaybackControls(
-      position: state.position,
-      mute: state.mute,
-      pause: !state.pause,
-    );
+    state =
+        VideoPlaybackControls(position: state.position, pause: !state.pause);
   }
 
   void restart() {
-    state = VideoPlaybackControls(
-      position: 0,
-      mute: state.mute,
-      pause: true,
-    );
-
-    state = VideoPlaybackControls(
-      position: 0,
-      mute: state.mute,
-      pause: false,
-    );
+    state =
+        const VideoPlaybackControls(position: 0, pause: false, restarted: true);
+    ref.read(videoPlaybackValueProvider.notifier).value =
+        ref.read(videoPlaybackValueProvider.notifier).value.copyWith(
+              state: VideoPlaybackState.playing,
+              position: Duration.zero,
+            );
   }
 }
diff --git a/mobile/lib/providers/asset_viewer/video_player_value_provider.dart b/mobile/lib/providers/asset_viewer/video_player_value_provider.dart
index ebdf739ef0..1a3c54e9e9 100644
--- a/mobile/lib/providers/asset_viewer/video_player_value_provider.dart
+++ b/mobile/lib/providers/asset_viewer/video_player_value_provider.dart
@@ -1,5 +1,5 @@
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:video_player/video_player.dart';
+import 'package:native_video_player/native_video_player.dart';
 
 enum VideoPlaybackState {
   initializing,
@@ -22,56 +22,66 @@ class VideoPlaybackValue {
   /// The volume of the video
   final double volume;
 
-  VideoPlaybackValue({
+  const VideoPlaybackValue({
     required this.position,
     required this.duration,
     required this.state,
     required this.volume,
   });
 
-  factory VideoPlaybackValue.fromController(VideoPlayerController? controller) {
-    final video = controller?.value;
-    late VideoPlaybackState s;
-    if (video == null) {
-      s = VideoPlaybackState.initializing;
-    } else if (video.isCompleted) {
-      s = VideoPlaybackState.completed;
-    } else if (video.isPlaying) {
-      s = VideoPlaybackState.playing;
-    } else if (video.isBuffering) {
-      s = VideoPlaybackState.buffering;
-    } else {
-      s = VideoPlaybackState.paused;
+  factory VideoPlaybackValue.fromNativeController(
+    NativeVideoPlayerController controller,
+  ) {
+    final playbackInfo = controller.playbackInfo;
+    final videoInfo = controller.videoInfo;
+
+    if (playbackInfo == null || videoInfo == null) {
+      return videoPlaybackValueDefault;
     }
 
+    final VideoPlaybackState status = switch (playbackInfo.status) {
+      PlaybackStatus.playing => VideoPlaybackState.playing,
+      PlaybackStatus.paused => VideoPlaybackState.paused,
+      PlaybackStatus.stopped => VideoPlaybackState.completed,
+    };
+
     return VideoPlaybackValue(
-      position: video?.position ?? Duration.zero,
-      duration: video?.duration ?? Duration.zero,
-      state: s,
-      volume: video?.volume ?? 0.0,
+      position: Duration(seconds: playbackInfo.position),
+      duration: Duration(seconds: videoInfo.duration),
+      state: status,
+      volume: playbackInfo.volume,
     );
   }
 
-  factory VideoPlaybackValue.uninitialized() {
+  VideoPlaybackValue copyWith({
+    Duration? position,
+    Duration? duration,
+    VideoPlaybackState? state,
+    double? volume,
+  }) {
     return VideoPlaybackValue(
-      position: Duration.zero,
-      duration: Duration.zero,
-      state: VideoPlaybackState.initializing,
-      volume: 0.0,
+      position: position ?? this.position,
+      duration: duration ?? this.duration,
+      state: state ?? this.state,
+      volume: volume ?? this.volume,
     );
   }
 }
 
+const VideoPlaybackValue videoPlaybackValueDefault = VideoPlaybackValue(
+  position: Duration.zero,
+  duration: Duration.zero,
+  state: VideoPlaybackState.initializing,
+  volume: 0.0,
+);
+
 final videoPlaybackValueProvider =
     StateNotifierProvider<VideoPlaybackValueState, VideoPlaybackValue>((ref) {
   return VideoPlaybackValueState(ref);
 });
 
 class VideoPlaybackValueState extends StateNotifier<VideoPlaybackValue> {
-  VideoPlaybackValueState(this.ref)
-      : super(
-          VideoPlaybackValue.uninitialized(),
-        );
+  VideoPlaybackValueState(this.ref) : super(videoPlaybackValueDefault);
 
   final Ref ref;
 
@@ -82,6 +92,7 @@ class VideoPlaybackValueState extends StateNotifier<VideoPlaybackValue> {
   }
 
   set position(Duration value) {
+    if (state.position == value) return;
     state = VideoPlaybackValue(
       position: value,
       duration: state.duration,
@@ -89,4 +100,18 @@ class VideoPlaybackValueState extends StateNotifier<VideoPlaybackValue> {
       volume: state.volume,
     );
   }
+
+  set status(VideoPlaybackState value) {
+    if (state.state == value) return;
+    state = VideoPlaybackValue(
+      position: state.position,
+      duration: state.duration,
+      state: value,
+      volume: state.volume,
+    );
+  }
+
+  void reset() {
+    state = videoPlaybackValueDefault;
+  }
 }
diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart
new file mode 100644
index 0000000000..a23ffd3d68
--- /dev/null
+++ b/mobile/lib/providers/auth.provider.dart
@@ -0,0 +1,205 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter_udid/flutter_udid.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/models/auth/login_response.model.dart';
+import 'package:immich_mobile/models/auth/auth_state.model.dart';
+import 'package:immich_mobile/entities/user.entity.dart';
+import 'package:immich_mobile/providers/api.provider.dart';
+import 'package:immich_mobile/services/api.service.dart';
+import 'package:immich_mobile/services/auth.service.dart';
+import 'package:immich_mobile/utils/hash.dart';
+import 'package:logging/logging.dart';
+import 'package:openapi/api.dart';
+
+final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
+  return AuthNotifier(
+    ref.watch(authServiceProvider),
+    ref.watch(apiServiceProvider),
+  );
+});
+
+class AuthNotifier extends StateNotifier<AuthState> {
+  final AuthService _authService;
+  final ApiService _apiService;
+  final _log = Logger("AuthenticationNotifier");
+
+  static const Duration _timeoutDuration = Duration(seconds: 7);
+
+  AuthNotifier(
+    this._authService,
+    this._apiService,
+  ) : super(
+          AuthState(
+            deviceId: "",
+            userId: "",
+            userEmail: "",
+            name: '',
+            profileImagePath: '',
+            isAdmin: false,
+            isAuthenticated: false,
+          ),
+        );
+
+  Future<String> validateServerUrl(String url) {
+    return _authService.validateServerUrl(url);
+  }
+
+  /// Validating the url is the alternative connecting server url without
+  /// saving the infomation to the local database
+  Future<bool> validateAuxilaryServerUrl(String url) async {
+    try {
+      final validEndpoint = await _apiService.resolveEndpoint(url);
+      return await _authService.validateAuxilaryServerUrl(validEndpoint);
+    } catch (_) {
+      return false;
+    }
+  }
+
+  Future<LoginResponse> login(String email, String password) async {
+    final response = await _authService.login(email, password);
+    await saveAuthInfo(accessToken: response.accessToken);
+    return response;
+  }
+
+  Future<void> logout() async {
+    try {
+      await _authService.logout();
+    } finally {
+      await _cleanUp();
+    }
+  }
+
+  Future<void> _cleanUp() async {
+    state = AuthState(
+      deviceId: "",
+      userId: "",
+      userEmail: "",
+      name: '',
+      profileImagePath: '',
+      isAdmin: false,
+      isAuthenticated: false,
+    );
+  }
+
+  void updateUserProfileImagePath(String path) {
+    state = state.copyWith(profileImagePath: path);
+  }
+
+  Future<bool> changePassword(String newPassword) async {
+    try {
+      await _authService.changePassword(newPassword);
+      return true;
+    } catch (_) {
+      return false;
+    }
+  }
+
+  Future<bool> saveAuthInfo({
+    required String accessToken,
+  }) async {
+    _apiService.setAccessToken(accessToken);
+
+    // Get the deviceid from the store if it exists, otherwise generate a new one
+    String deviceId =
+        Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid;
+
+    User? user = Store.tryGet(StoreKey.currentUser);
+
+    UserAdminResponseDto? userResponse;
+    UserPreferencesResponseDto? userPreferences;
+    try {
+      final responses = await Future.wait([
+        _apiService.usersApi.getMyUser().timeout(_timeoutDuration),
+        _apiService.usersApi.getMyPreferences().timeout(_timeoutDuration),
+      ]);
+      userResponse = responses[0] as UserAdminResponseDto;
+      userPreferences = responses[1] as UserPreferencesResponseDto;
+    } on ApiException catch (error, stackTrace) {
+      if (error.code == 401) {
+        _log.severe("Unauthorized access, token likely expired. Logging out.");
+        return false;
+      }
+      _log.severe(
+        "Error getting user information from the server [API EXCEPTION]",
+        stackTrace,
+      );
+    } catch (error, stackTrace) {
+      _log.severe(
+        "Error getting user information from the server [CATCH ALL]",
+        error,
+        stackTrace,
+      );
+
+      if (kDebugMode) {
+        debugPrint(
+          "Error getting user information from the server [CATCH ALL] $error $stackTrace",
+        );
+      }
+    }
+
+    // If the user information is successfully retrieved, update the store
+    // Due to the flow of the code, this will always happen on first login
+    if (userResponse != null) {
+      Store.put(StoreKey.deviceId, deviceId);
+      Store.put(StoreKey.deviceIdHash, fastHash(deviceId));
+      Store.put(
+        StoreKey.currentUser,
+        User.fromUserDto(userResponse, userPreferences),
+      );
+      Store.put(StoreKey.accessToken, accessToken);
+
+      user = User.fromUserDto(userResponse, userPreferences);
+    } else {
+      _log.severe("Unable to get user information from the server.");
+    }
+
+    // If the user is null, the login was not successful
+    // and we don't have a local copy of the user from a prior successful login
+    if (user == null) {
+      return false;
+    }
+
+    state = state.copyWith(
+      isAuthenticated: true,
+      userId: user.id,
+      userEmail: user.email,
+      name: user.name,
+      profileImagePath: user.profileImagePath,
+      isAdmin: user.isAdmin,
+      deviceId: deviceId,
+    );
+
+    return true;
+  }
+
+  Future<void> saveWifiName(String wifiName) {
+    return Store.put(StoreKey.preferredWifiName, wifiName);
+  }
+
+  Future<void> saveLocalEndpoint(String url) {
+    return Store.put(StoreKey.localEndpoint, url);
+  }
+
+  String? getSavedWifiName() {
+    return Store.tryGet(StoreKey.preferredWifiName);
+  }
+
+  String? getSavedLocalEndpoint() {
+    return Store.tryGet(StoreKey.localEndpoint);
+  }
+
+  /// Returns the current server endpoint (with /api) URL from the store
+  String? getServerEndpoint() {
+    return Store.tryGet(StoreKey.serverEndpoint);
+  }
+
+  /// Returns the current server URL (input by the user) from the store
+  String? getServerUrl() {
+    return Store.tryGet(StoreKey.serverUrl);
+  }
+
+  Future<String?> setOpenApiServiceEndpoint() {
+    return _authService.setOpenApiServiceEndpoint();
+  }
+}
diff --git a/mobile/lib/providers/authentication.provider.dart b/mobile/lib/providers/authentication.provider.dart
deleted file mode 100644
index b56e71b11b..0000000000
--- a/mobile/lib/providers/authentication.provider.dart
+++ /dev/null
@@ -1,246 +0,0 @@
-import 'dart:io';
-
-import 'package:device_info_plus/device_info_plus.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_udid/flutter_udid.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/providers/album/album.provider.dart';
-import 'package:immich_mobile/providers/album/shared_album.provider.dart';
-import 'package:immich_mobile/entities/store.entity.dart';
-import 'package:immich_mobile/models/authentication/authentication_state.model.dart';
-import 'package:immich_mobile/entities/user.entity.dart';
-import 'package:immich_mobile/providers/api.provider.dart';
-import 'package:immich_mobile/providers/db.provider.dart';
-import 'package:immich_mobile/services/api.service.dart';
-import 'package:immich_mobile/utils/db.dart';
-import 'package:immich_mobile/utils/hash.dart';
-import 'package:isar/isar.dart';
-import 'package:logging/logging.dart';
-import 'package:openapi/api.dart';
-
-class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
-  AuthenticationNotifier(
-    this._apiService,
-    this._db,
-    this._ref,
-  ) : super(
-          AuthenticationState(
-            deviceId: "",
-            userId: "",
-            userEmail: "",
-            name: '',
-            profileImagePath: '',
-            isAdmin: false,
-            shouldChangePassword: false,
-            isAuthenticated: false,
-          ),
-        );
-
-  final ApiService _apiService;
-  final Isar _db;
-  final StateNotifierProviderRef<AuthenticationNotifier, AuthenticationState>
-      _ref;
-  final _log = Logger("AuthenticationNotifier");
-
-  Future<bool> login(
-    String email,
-    String password,
-    String serverUrl,
-  ) async {
-    try {
-      // Resolve API server endpoint from user provided serverUrl
-      await _apiService.resolveAndSetEndpoint(serverUrl);
-      await _apiService.serverInfoApi.pingServer();
-    } catch (e) {
-      debugPrint('Invalid Server Endpoint Url $e');
-      return false;
-    }
-
-    // Make sign-in request
-    DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
-
-    if (Platform.isIOS) {
-      var iosInfo = await deviceInfoPlugin.iosInfo;
-      _apiService.authenticationApi.apiClient
-          .addDefaultHeader('deviceModel', iosInfo.utsname.machine);
-      _apiService.authenticationApi.apiClient
-          .addDefaultHeader('deviceType', 'iOS');
-    } else {
-      var androidInfo = await deviceInfoPlugin.androidInfo;
-      _apiService.authenticationApi.apiClient
-          .addDefaultHeader('deviceModel', androidInfo.model);
-      _apiService.authenticationApi.apiClient
-          .addDefaultHeader('deviceType', 'Android');
-    }
-
-    try {
-      var loginResponse = await _apiService.authenticationApi.login(
-        LoginCredentialDto(
-          email: email,
-          password: password,
-        ),
-      );
-
-      if (loginResponse == null) {
-        debugPrint('Login Response is null');
-        return false;
-      }
-
-      return setSuccessLoginInfo(
-        accessToken: loginResponse.accessToken,
-        serverUrl: serverUrl,
-      );
-    } catch (e) {
-      debugPrint("Error logging in $e");
-      return false;
-    }
-  }
-
-  Future<void> logout() async {
-    var log = Logger('AuthenticationNotifier');
-    try {
-      String? userEmail = Store.tryGet(StoreKey.currentUser)?.email;
-
-      await _apiService.authenticationApi
-          .logout()
-          .then((_) => log.info("Logout was successful for $userEmail"))
-          .onError(
-            (error, stackTrace) =>
-                log.severe("Logout failed for $userEmail", error, stackTrace),
-          );
-
-      await Future.wait([
-        clearAssetsAndAlbums(_db),
-        Store.delete(StoreKey.currentUser),
-        Store.delete(StoreKey.accessToken),
-      ]);
-      _ref.invalidate(albumProvider);
-      _ref.invalidate(sharedAlbumProvider);
-
-      state = state.copyWith(
-        deviceId: "",
-        userId: "",
-        userEmail: "",
-        name: '',
-        profileImagePath: '',
-        isAdmin: false,
-        shouldChangePassword: false,
-        isAuthenticated: false,
-      );
-    } catch (e, stack) {
-      log.severe('Logout failed', e, stack);
-    }
-  }
-
-  updateUserProfileImagePath(String path) {
-    state = state.copyWith(profileImagePath: path);
-  }
-
-  Future<bool> changePassword(String newPassword) async {
-    try {
-      await _apiService.usersApi.updateMyUser(
-        UserUpdateMeDto(
-          password: newPassword,
-        ),
-      );
-
-      state = state.copyWith(shouldChangePassword: false);
-
-      return true;
-    } catch (e) {
-      debugPrint("Error changing password $e");
-      return false;
-    }
-  }
-
-  Future<bool> setSuccessLoginInfo({
-    required String accessToken,
-    required String serverUrl,
-  }) async {
-    _apiService.setAccessToken(accessToken);
-
-    // Get the deviceid from the store if it exists, otherwise generate a new one
-    String deviceId =
-        Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid;
-
-    bool shouldChangePassword = false;
-    User? user = Store.tryGet(StoreKey.currentUser);
-
-    UserAdminResponseDto? userResponse;
-    UserPreferencesResponseDto? userPreferences;
-    try {
-      final responses = await Future.wait([
-        _apiService.usersApi.getMyUser().timeout(const Duration(seconds: 7)),
-        _apiService.usersApi
-            .getMyPreferences()
-            .timeout(const Duration(seconds: 7)),
-      ]);
-      userResponse = responses[0] as UserAdminResponseDto;
-      userPreferences = responses[1] as UserPreferencesResponseDto;
-    } on ApiException catch (error, stackTrace) {
-      if (error.code == 401) {
-        _log.severe("Unauthorized access, token likely expired. Logging out.");
-        return false;
-      }
-      _log.severe(
-        "Error getting user information from the server [API EXCEPTION]",
-        stackTrace,
-      );
-    } catch (error, stackTrace) {
-      _log.severe(
-        "Error getting user information from the server [CATCH ALL]",
-        error,
-        stackTrace,
-      );
-      debugPrint(
-        "Error getting user information from the server [CATCH ALL] $error $stackTrace",
-      );
-    }
-
-    // If the user information is successfully retrieved, update the store
-    // Due to the flow of the code, this will always happen on first login
-    if (userResponse != null) {
-      Store.put(StoreKey.deviceId, deviceId);
-      Store.put(StoreKey.deviceIdHash, fastHash(deviceId));
-      Store.put(
-        StoreKey.currentUser,
-        User.fromUserDto(userResponse, userPreferences),
-      );
-      Store.put(StoreKey.serverUrl, serverUrl);
-      Store.put(StoreKey.accessToken, accessToken);
-
-      shouldChangePassword = userResponse.shouldChangePassword;
-      user = User.fromUserDto(userResponse, userPreferences);
-    } else {
-      _log.severe("Unable to get user information from the server.");
-    }
-
-    // If the user is null, the login was not successful
-    // and we don't have a local copy of the user from a prior successful login
-    if (user == null) {
-      return false;
-    }
-
-    state = state.copyWith(
-      isAuthenticated: true,
-      userId: user.id,
-      userEmail: user.email,
-      name: user.name,
-      profileImagePath: user.profileImagePath,
-      isAdmin: user.isAdmin,
-      shouldChangePassword: shouldChangePassword,
-      deviceId: deviceId,
-    );
-
-    return true;
-  }
-}
-
-final authenticationProvider =
-    StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
-  return AuthenticationNotifier(
-    ref.watch(apiServiceProvider),
-    ref.watch(dbProvider),
-    ref,
-  );
-});
diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart
index 02f1f07904..aab367485c 100644
--- a/mobile/lib/providers/backup/backup.provider.dart
+++ b/mobile/lib/providers/backup/backup.provider.dart
@@ -5,6 +5,10 @@ import 'package:collection/collection.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/widgets.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/interfaces/album_media.interface.dart';
+import 'package:immich_mobile/interfaces/backup.interface.dart';
+import 'package:immich_mobile/interfaces/file_media.interface.dart';
 import 'package:immich_mobile/models/backup/available_album.model.dart';
 import 'package:immich_mobile/entities/backup_album.entity.dart';
 import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
@@ -13,10 +17,13 @@ import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
 import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
 import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
 import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
+import 'package:immich_mobile/repositories/album_media.repository.dart';
+import 'package:immich_mobile/repositories/backup.repository.dart';
+import 'package:immich_mobile/repositories/file_media.repository.dart';
 import 'package:immich_mobile/services/background.service.dart';
 import 'package:immich_mobile/services/backup.service.dart';
-import 'package:immich_mobile/models/authentication/authentication_state.model.dart';
-import 'package:immich_mobile/providers/authentication.provider.dart';
+import 'package:immich_mobile/models/auth/auth_state.model.dart';
+import 'package:immich_mobile/providers/auth.provider.dart';
 import 'package:immich_mobile/providers/gallery_permission.provider.dart';
 import 'package:immich_mobile/models/server_info/server_disk_info.model.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
@@ -28,7 +35,7 @@ import 'package:immich_mobile/utils/diff.dart';
 import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
 import 'package:permission_handler/permission_handler.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
 
 class BackupNotifier extends StateNotifier<BackUpState> {
   BackupNotifier(
@@ -38,6 +45,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
     this._backgroundService,
     this._galleryPermissionNotifier,
     this._db,
+    this._albumMediaRepository,
+    this._fileMediaRepository,
+    this._backupRepository,
     this.ref,
   ) : super(
           BackUpState(
@@ -82,10 +92,13 @@ class BackupNotifier extends StateNotifier<BackUpState> {
   final log = Logger('BackupNotifier');
   final BackupService _backupService;
   final ServerInfoService _serverInfoService;
-  final AuthenticationState _authState;
+  final AuthState _authState;
   final BackgroundService _backgroundService;
   final GalleryPermissionNotifier _galleryPermissionNotifier;
   final Isar _db;
+  final IAlbumMediaRepository _albumMediaRepository;
+  final IFileMediaRepository _fileMediaRepository;
+  final IBackupRepository _backupRepository;
   final Ref ref;
 
   ///
@@ -224,38 +237,44 @@ class BackupNotifier extends StateNotifier<BackUpState> {
     Stopwatch stopwatch = Stopwatch()..start();
     // Get all albums on the device
     List<AvailableAlbum> availableAlbums = [];
-    List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
-      hasAll: true,
-      type: RequestType.common,
-    );
+    List<Album> albums = await _albumMediaRepository.getAll();
 
     // Map of id -> album for quick album lookup later on.
-    Map<String, AssetPathEntity> albumMap = {};
+    Map<String, Album> albumMap = {};
 
     log.info('Found ${albums.length} local albums');
 
-    for (AssetPathEntity album in albums) {
-      AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
+    for (Album album in albums) {
+      AvailableAlbum availableAlbum = AvailableAlbum(
+        album: album,
+        assetCount: await ref
+            .read(albumMediaRepositoryProvider)
+            .getAssetCount(album.localId!),
+      );
 
       availableAlbums.add(availableAlbum);
 
-      albumMap[album.id] = album;
+      albumMap[album.localId!] = album;
     }
     state = state.copyWith(availableAlbums: availableAlbums);
 
     final List<BackupAlbum> excludedBackupAlbums =
-        await _backupService.excludedAlbumsQuery().findAll();
+        await _backupRepository.getAllBySelection(BackupSelection.exclude);
     final List<BackupAlbum> selectedBackupAlbums =
-        await _backupService.selectedAlbumsQuery().findAll();
+        await _backupRepository.getAllBySelection(BackupSelection.select);
 
-    // Generate AssetPathEntity from id to add to local state
     final Set<AvailableAlbum> selectedAlbums = {};
     for (final BackupAlbum ba in selectedBackupAlbums) {
       final albumAsset = albumMap[ba.id];
 
       if (albumAsset != null) {
         selectedAlbums.add(
-          AvailableAlbum(albumEntity: albumAsset, lastBackup: ba.lastBackup),
+          AvailableAlbum(
+            album: albumAsset,
+            assetCount:
+                await _albumMediaRepository.getAssetCount(albumAsset.localId!),
+            lastBackup: ba.lastBackup,
+          ),
         );
       } else {
         log.severe('Selected album not found');
@@ -268,7 +287,13 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 
       if (albumAsset != null) {
         excludedAlbums.add(
-          AvailableAlbum(albumEntity: albumAsset, lastBackup: ba.lastBackup),
+          AvailableAlbum(
+            album: albumAsset,
+            assetCount: await ref
+                .read(albumMediaRepositoryProvider)
+                .getAssetCount(albumAsset.localId!),
+            lastBackup: ba.lastBackup,
+          ),
         );
       } else {
         log.severe('Excluded album not found');
@@ -292,28 +317,32 @@ class BackupNotifier extends StateNotifier<BackUpState> {
   /// Those assets are unique and are used as the total assets
   ///
   Future<void> _updateBackupAssetCount() async {
+    // Save to persistent storage
+    await _updatePersistentAlbumsSelection();
+
     final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds();
     final Set<BackupCandidate> assetsFromSelectedAlbums = {};
     final Set<BackupCandidate> assetsFromExcludedAlbums = {};
 
     for (final album in state.selectedBackupAlbums) {
-      final assetCount = await album.albumEntity.assetCountAsync;
+      final assetCount = await ref
+          .read(albumMediaRepositoryProvider)
+          .getAssetCount(album.album.localId!);
 
       if (assetCount == 0) {
         continue;
       }
 
-      final assets = await album.albumEntity.getAssetListRange(
-        start: 0,
-        end: assetCount,
-      );
+      final assets = await ref
+          .read(albumMediaRepositoryProvider)
+          .getAssets(album.album.localId!);
 
       // Add album's name to the asset info
       for (final asset in assets) {
         List<String> albumNames = [album.name];
 
         final existingAsset = assetsFromSelectedAlbums.firstWhereOrNull(
-          (a) => a.asset.id == asset.id,
+          (a) => a.asset.localId == asset.localId,
         );
 
         if (existingAsset != null) {
@@ -331,16 +360,17 @@ class BackupNotifier extends StateNotifier<BackUpState> {
     }
 
     for (final album in state.excludedBackupAlbums) {
-      final assetCount = await album.albumEntity.assetCountAsync;
+      final assetCount = await ref
+          .read(albumMediaRepositoryProvider)
+          .getAssetCount(album.album.localId!);
 
       if (assetCount == 0) {
         continue;
       }
 
-      final assets = await album.albumEntity.getAssetListRange(
-        start: 0,
-        end: assetCount,
-      );
+      final assets = await ref
+          .read(albumMediaRepositoryProvider)
+          .getAssets(album.album.localId!);
 
       for (final asset in assets) {
         assetsFromExcludedAlbums.add(
@@ -360,14 +390,14 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 
     // Find asset that were backup from selected albums
     final Set<String> selectedAlbumsBackupAssets =
-        Set.from(allUniqueAssets.map((e) => e.asset.id));
+        Set.from(allUniqueAssets.map((e) => e.asset.localId));
 
     selectedAlbumsBackupAssets
         .removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
 
     // Remove duplicated asset from all unique assets
     allUniqueAssets.removeWhere(
-      (candidate) => duplicatedAssetIds.contains(candidate.asset.id),
+      (candidate) => duplicatedAssetIds.contains(candidate.asset.localId),
     );
 
     if (allUniqueAssets.isEmpty) {
@@ -385,9 +415,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
         selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
       );
     }
-
-    // Save to persistent storage
-    await _updatePersistentAlbumsSelection();
   }
 
   /// Get all necessary information for calculating the available albums,
@@ -454,7 +481,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 
     final hasPermission = _galleryPermissionNotifier.hasPermission;
     if (hasPermission) {
-      await PhotoManager.clearFileCache();
+      await _fileMediaRepository.clearFileCache();
 
       if (state.allUniqueAssets.isEmpty) {
         log.info("No Asset On Device - Abort Backup Process");
@@ -465,7 +492,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
       Set<BackupCandidate> assetsWillBeBackup = Set.from(state.allUniqueAssets);
       // Remove item that has already been backed up
       for (final assetId in state.allAssetsInDatabase) {
-        assetsWillBeBackup.removeWhere((e) => e.asset.id == assetId);
+        assetsWillBeBackup.removeWhere((e) => e.asset.localId == assetId);
       }
 
       if (assetsWillBeBackup.isEmpty) {
@@ -531,7 +558,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
       state = state.copyWith(
         allUniqueAssets: state.allUniqueAssets
             .where(
-              (candidate) => candidate.asset.id != result.candidate.asset.id,
+              (candidate) =>
+                  candidate.asset.localId != result.candidate.asset.localId,
             )
             .toSet(),
       );
@@ -539,11 +567,11 @@ class BackupNotifier extends StateNotifier<BackUpState> {
       state = state.copyWith(
         selectedAlbumsBackupAssetsIds: {
           ...state.selectedAlbumsBackupAssetsIds,
-          result.candidate.asset.id,
+          result.candidate.asset.localId!,
         },
         allAssetsInDatabase: [
           ...state.allAssetsInDatabase,
-          result.candidate.asset.id,
+          result.candidate.asset.localId!,
         ],
       );
     }
@@ -552,7 +580,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
             state.selectedAlbumsBackupAssetsIds.length ==
         0) {
       final latestAssetBackup = state.allUniqueAssets
-          .map((candidate) => candidate.asset.modifiedDateTime)
+          .map((candidate) => candidate.asset.fileModifiedAt)
           .reduce(
             (v, e) => e.isAfter(v) ? e : v,
           );
@@ -737,10 +765,13 @@ final backupProvider =
   return BackupNotifier(
     ref.watch(backupServiceProvider),
     ref.watch(serverInfoServiceProvider),
-    ref.watch(authenticationProvider),
+    ref.watch(authProvider),
     ref.watch(backgroundServiceProvider),
     ref.watch(galleryPermissionNotifier.notifier),
     ref.watch(dbProvider),
+    ref.watch(albumMediaRepositoryProvider),
+    ref.watch(fileMediaRepositoryProvider),
+    ref.watch(backupRepositoryProvider),
     ref,
   );
 });
diff --git a/mobile/lib/providers/backup/backup_verification.provider.dart b/mobile/lib/providers/backup/backup_verification.provider.dart
index 894b807ec8..5881814320 100644
--- a/mobile/lib/providers/backup/backup_verification.provider.dart
+++ b/mobile/lib/providers/backup/backup_verification.provider.dart
@@ -35,7 +35,7 @@ class BackupVerification extends _$BackupVerification {
         return;
       }
       final connection = await Connectivity().checkConnectivity();
-      if (connection != ConnectivityResult.wifi) {
+      if (!connection.contains(ConnectivityResult.wifi)) {
         if (context.mounted) {
           ImmichToast.show(
             context: context,
diff --git a/mobile/lib/providers/backup/backup_verification.provider.g.dart b/mobile/lib/providers/backup/backup_verification.provider.g.dart
index f222c9bd83..9b52698847 100644
Binary files a/mobile/lib/providers/backup/backup_verification.provider.g.dart and b/mobile/lib/providers/backup/backup_verification.provider.g.dart differ
diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart
index a76b56fea7..192126f085 100644
--- a/mobile/lib/providers/backup/manual_upload.provider.dart
+++ b/mobile/lib/providers/backup/manual_upload.provider.dart
@@ -6,8 +6,11 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/widgets.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/backup_album.entity.dart';
 import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
 import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
+import 'package:immich_mobile/repositories/backup.repository.dart';
+import 'package:immich_mobile/repositories/file_media.repository.dart';
 import 'package:immich_mobile/services/background.service.dart';
 import 'package:immich_mobile/models/backup/backup_state.model.dart';
 import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
@@ -24,10 +27,9 @@ import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
 import 'package:immich_mobile/services/local_notification.service.dart';
 import 'package:immich_mobile/widgets/common/immich_toast.dart';
 import 'package:immich_mobile/utils/backup_progress.dart';
-import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
 import 'package:permission_handler/permission_handler.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
 
 final manualUploadProvider =
     StateNotifierProvider<ManualUploadNotifier, ManualUploadState>((ref) {
@@ -35,6 +37,7 @@ final manualUploadProvider =
     ref.watch(localNotificationService),
     ref.watch(backupProvider.notifier),
     ref.watch(backupServiceProvider),
+    ref.watch(backupRepositoryProvider),
     ref,
   );
 });
@@ -44,12 +47,14 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
   final LocalNotificationService _localNotificationService;
   final BackupNotifier _backupProvider;
   final BackupService _backupService;
+  final BackupRepository _backupRepository;
   final Ref ref;
 
   ManualUploadNotifier(
     this._localNotificationService,
     this._backupProvider,
     this._backupService,
+    this._backupRepository,
     this.ref,
   ) : super(
           ManualUploadState(
@@ -193,17 +198,10 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
       _backupProvider.updateBackupProgress(BackUpProgressEnum.manualInProgress);
 
       if (ref.read(galleryPermissionNotifier.notifier).hasPermission) {
-        await PhotoManager.clearFileCache();
+        await ref.read(fileMediaRepositoryProvider).clearFileCache();
 
-        // We do not have 1:1 mapping of all AssetEntity fields to Asset. This results in cases
-        // where platform specific fields such as `subtype` used to detect platform specific assets such as
-        // LivePhoto in iOS is lost when we directly fetch the local asset from Asset using Asset.local
-        List<AssetEntity?> allAssetsFromDevice = await Future.wait(
-          allManualUploads
-              // Filter local only assets
-              .where((e) => e.isLocal && !e.isRemote)
-              .map((e) => e.local!.obtainForNewProperties()),
-        );
+        final allAssetsFromDevice =
+            allManualUploads.where((e) => e.isLocal && !e.isRemote).toList();
 
         if (allAssetsFromDevice.length != allManualUploads.length) {
           _log.warning(
@@ -212,20 +210,26 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
         }
 
         final selectedBackupAlbums =
-            _backupService.selectedAlbumsQuery().findAllSync();
+            await _backupRepository.getAllBySelection(BackupSelection.select);
         final excludedBackupAlbums =
-            _backupService.excludedAlbumsQuery().findAllSync();
+            await _backupRepository.getAllBySelection(BackupSelection.exclude);
 
         // Get candidates from selected albums and excluded albums
         Set<BackupCandidate> candidates =
             await _backupService.buildUploadCandidates(
           selectedBackupAlbums,
           excludedBackupAlbums,
+          useTimeFilter: false,
         );
 
-        // Extrack candidate from allAssetsFromDevice.nonNulls
-        final uploadAssets = candidates
-            .where((e) => allAssetsFromDevice.nonNulls.contains(e.asset));
+        // Extrack candidate from allAssetsFromDevice
+        final uploadAssets = candidates.where(
+          (candidate) =>
+              allAssetsFromDevice.firstWhereOrNull(
+                (asset) => asset.localId == candidate.asset.localId,
+              ) !=
+              null,
+        );
 
         if (uploadAssets.isEmpty) {
           debugPrint("[_startUpload] No Assets to upload - Abort Process");
diff --git a/mobile/lib/providers/favorite_provider.dart b/mobile/lib/providers/favorite.provider.dart
similarity index 100%
rename from mobile/lib/providers/favorite_provider.dart
rename to mobile/lib/providers/favorite.provider.dart
diff --git a/mobile/lib/providers/image/immich_local_image_provider.dart b/mobile/lib/providers/image/immich_local_image_provider.dart
index dc1b8a9845..36fd3334b9 100644
--- a/mobile/lib/providers/image/immich_local_image_provider.dart
+++ b/mobile/lib/providers/image/immich_local_image_provider.dart
@@ -7,24 +7,23 @@ import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/painting.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:immich_mobile/entities/store.entity.dart';
-import 'package:immich_mobile/services/app_settings.service.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:logging/logging.dart';
+import 'package:photo_manager/photo_manager.dart' show ThumbnailSize;
 
 /// The local image provider for an asset
 class ImmichLocalImageProvider extends ImageProvider<ImmichLocalImageProvider> {
   final Asset asset;
+  // only used for videos
+  final double width;
+  final double height;
+  final Logger log = Logger('ImmichLocalImageProvider');
 
   ImmichLocalImageProvider({
     required this.asset,
+    required this.width,
+    required this.height,
   }) : assert(asset.local != null, 'Only usable when asset.local is set');
 
-  /// Whether to show the original file or load a compressed version
-  bool get _useOriginal => Store.get(
-        AppSettingsEnum.loadOriginal.storeKey,
-        AppSettingsEnum.loadOriginal.defaultValue,
-      );
-
   /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
   /// that describes the precise image to load.
   @override
@@ -50,56 +49,57 @@ class ImmichLocalImageProvider extends ImageProvider<ImmichLocalImageProvider> {
 
   // Streams in each stage of the image as we ask for it
   Stream<ui.Codec> _codec(
-    Asset key,
+    Asset asset,
     ImageDecoderCallback decode,
     StreamController<ImageChunkEvent> chunkEvents,
   ) async* {
-    // Load a small thumbnail
-    final thumbBytes = await asset.local?.thumbnailDataWithSize(
-      const ThumbnailSize.square(256),
-      quality: 80,
-    );
-    if (thumbBytes != null) {
-      final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
-      final codec = await decode(buffer);
-      yield codec;
-    } else {
-      debugPrint("Loading thumb for ${asset.fileName} failed");
-    }
-
-    if (asset.isImage) {
-      /// Using 2K thumbnail for local iOS image to avoid double swiping issue
-      if (Platform.isIOS) {
-        final largeImageBytes = _useOriginal
-            ? await asset.local?.originBytes
-            : await asset.local
-                ?.thumbnailDataWithSize(const ThumbnailSize(3840, 2160));
-
-        if (largeImageBytes == null) {
-          throw StateError(
-            "Loading thumb for local photo ${asset.fileName} failed",
-          );
-        }
-        final buffer = await ui.ImmutableBuffer.fromUint8List(largeImageBytes);
-        final codec = await decode(buffer);
-        yield codec;
-      } else {
-        // Use the original file for Android
-        final File? file = await asset.local?.originFile;
-        if (file == null) {
-          throw StateError("Opening file for asset ${asset.fileName} failed");
-        }
-        try {
-          final buffer = await ui.ImmutableBuffer.fromFilePath(file.path);
-          final codec = await decode(buffer);
-          yield codec;
-        } catch (error) {
-          throw StateError("Loading asset ${asset.fileName} failed");
-        }
+    ui.ImmutableBuffer? buffer;
+    try {
+      final local = asset.local;
+      if (local == null) {
+        throw StateError('Asset ${asset.fileName} has no local data');
       }
-    }
 
-    chunkEvents.close();
+      var thumbBytes = await local
+          .thumbnailDataWithSize(const ThumbnailSize.square(256), quality: 80);
+      if (thumbBytes == null) {
+        throw StateError("Loading thumbnail for ${asset.fileName} failed");
+      }
+      buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
+      thumbBytes = null;
+      yield await decode(buffer);
+      buffer = null;
+
+      switch (asset.type) {
+        case AssetType.image:
+          final File? file = await local.originFile;
+          if (file == null) {
+            throw StateError("Opening file for asset ${asset.fileName} failed");
+          }
+          buffer = await ui.ImmutableBuffer.fromFilePath(file.path);
+          yield await decode(buffer);
+          buffer = null;
+          break;
+        case AssetType.video:
+          final size = ThumbnailSize(width.ceil(), height.ceil());
+          thumbBytes = await local.thumbnailDataWithSize(size);
+          if (thumbBytes == null) {
+            throw StateError("Failed to load preview for ${asset.fileName}");
+          }
+          buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
+          thumbBytes = null;
+          yield await decode(buffer);
+          buffer = null;
+          break;
+        default:
+          throw StateError('Unsupported asset type ${asset.type}');
+      }
+    } catch (error, stack) {
+      log.severe('Error loading local image ${asset.fileName}', error, stack);
+      buffer?.dispose();
+    } finally {
+      chunkEvents.close();
+    }
   }
 
   @override
diff --git a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart
index 28e78ae762..69cdb105c0 100644
--- a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart
+++ b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart
@@ -6,7 +6,7 @@ import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/painting.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show ThumbnailSize;
 
 /// The local image provider for an asset
 /// Only viable
diff --git a/mobile/lib/providers/locale_provider.dart b/mobile/lib/providers/locale_provider.dart
new file mode 100644
index 0000000000..5de3fa009a
--- /dev/null
+++ b/mobile/lib/providers/locale_provider.dart
@@ -0,0 +1,4 @@
+import 'package:flutter/widgets.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+
+final localeProvider = Provider<Locale>((_) => throw UnimplementedError());
diff --git a/mobile/lib/providers/map/map_state.provider.dart b/mobile/lib/providers/map/map_state.provider.dart
index 6d1630bba2..189a23cd0a 100644
--- a/mobile/lib/providers/map/map_state.provider.dart
+++ b/mobile/lib/providers/map/map_state.provider.dart
@@ -1,28 +1,23 @@
-import 'dart:io';
-
 import 'package:flutter/material.dart';
-import 'package:immich_mobile/extensions/response_extensions.dart';
 import 'package:immich_mobile/models/map/map_state.model.dart';
 import 'package:immich_mobile/providers/app_settings.provider.dart';
+import 'package:immich_mobile/providers/server_info.provider.dart';
 import 'package:immich_mobile/services/app_settings.service.dart';
-import 'package:immich_mobile/providers/api.provider.dart';
-import 'package:logging/logging.dart';
-import 'package:openapi/api.dart';
-import 'package:path_provider/path_provider.dart';
 import 'package:riverpod_annotation/riverpod_annotation.dart';
 
 part 'map_state.provider.g.dart';
 
 @Riverpod(keepAlive: true)
 class MapStateNotifier extends _$MapStateNotifier {
-  final _log = Logger("MapStateNotifier");
-
   @override
   MapState build() {
     final appSettingsProvider = ref.read(appSettingsServiceProvider);
 
-    // Fetch and save the Style JSONs
-    loadStyles();
+    final lightStyleUrl =
+        ref.read(serverInfoProvider).serverConfig.mapLightStyleUrl;
+    final darkStyleUrl =
+        ref.read(serverInfoProvider).serverConfig.mapDarkStyleUrl;
+
     return MapState(
       themeMode: ThemeMode.values[
           appSettingsProvider.getSetting<int>(AppSettingsEnum.mapThemeMode)],
@@ -34,65 +29,11 @@ class MapStateNotifier extends _$MapStateNotifier {
           appSettingsProvider.getSetting<bool>(AppSettingsEnum.mapwithPartners),
       relativeTime:
           appSettingsProvider.getSetting<int>(AppSettingsEnum.mapRelativeDate),
+      lightStyleFetched: AsyncData(lightStyleUrl),
+      darkStyleFetched: AsyncData(darkStyleUrl),
     );
   }
 
-  void loadStyles() async {
-    final documents = (await getApplicationDocumentsDirectory()).path;
-
-    // Set to loading
-    state = state.copyWith(lightStyleFetched: const AsyncLoading());
-
-    // Fetch and save light theme
-    final lightResponse = await ref
-        .read(apiServiceProvider)
-        .mapApi
-        .getMapStyleWithHttpInfo(MapTheme.light);
-
-    if (lightResponse.statusCode >= HttpStatus.badRequest) {
-      state = state.copyWith(
-        lightStyleFetched: AsyncError(lightResponse.body, StackTrace.current),
-      );
-      _log.severe(
-        "Cannot fetch map light style",
-        lightResponse.toLoggerString(),
-      );
-      return;
-    }
-
-    final lightJSON = lightResponse.body;
-    final lightFile = await File("$documents/map-style-light.json")
-        .writeAsString(lightJSON, flush: true);
-
-    // Update state with path
-    state =
-        state.copyWith(lightStyleFetched: AsyncData(lightFile.absolute.path));
-
-    // Set to loading
-    state = state.copyWith(darkStyleFetched: const AsyncLoading());
-
-    // Fetch and save dark theme
-    final darkResponse = await ref
-        .read(apiServiceProvider)
-        .mapApi
-        .getMapStyleWithHttpInfo(MapTheme.dark);
-
-    if (darkResponse.statusCode >= HttpStatus.badRequest) {
-      state = state.copyWith(
-        darkStyleFetched: AsyncError(darkResponse.body, StackTrace.current),
-      );
-      _log.severe("Cannot fetch map dark style", darkResponse.toLoggerString());
-      return;
-    }
-
-    final darkJSON = darkResponse.body;
-    final darkFile = await File("$documents/map-style-dark.json")
-        .writeAsString(darkJSON, flush: true);
-
-    // Update state with path
-    state = state.copyWith(darkStyleFetched: AsyncData(darkFile.absolute.path));
-  }
-
   void switchTheme(ThemeMode mode) {
     ref.read(appSettingsServiceProvider).setSetting(
           AppSettingsEnum.mapThemeMode,
diff --git a/mobile/lib/providers/map/map_state.provider.g.dart b/mobile/lib/providers/map/map_state.provider.g.dart
index eff7b4b68e..23a570d1c8 100644
Binary files a/mobile/lib/providers/map/map_state.provider.g.dart and b/mobile/lib/providers/map/map_state.provider.g.dart differ
diff --git a/mobile/lib/providers/network.provider.dart b/mobile/lib/providers/network.provider.dart
new file mode 100644
index 0000000000..5cb2fae4b1
--- /dev/null
+++ b/mobile/lib/providers/network.provider.dart
@@ -0,0 +1,38 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/services/network.service.dart';
+
+final networkProvider = StateNotifierProvider<NetworkNotifier, String>((ref) {
+  return NetworkNotifier(
+    ref.watch(networkServiceProvider),
+  );
+});
+
+class NetworkNotifier extends StateNotifier<String> {
+  final NetworkService _networkService;
+
+  NetworkNotifier(this._networkService) : super('');
+
+  Future<String?> getWifiName() {
+    return _networkService.getWifiName();
+  }
+
+  Future<bool> getWifiReadPermission() {
+    return _networkService.getLocationWhenInUserPermission();
+  }
+
+  Future<bool> getWifiReadBackgroundPermission() {
+    return _networkService.getLocationAlwaysPermission();
+  }
+
+  Future<bool> requestWifiReadPermission() {
+    return _networkService.requestLocationWhenInUsePermission();
+  }
+
+  Future<bool> requestWifiReadBackgroundPermission() {
+    return _networkService.requestLocationAlwaysPermission();
+  }
+
+  Future<bool> openSettings() {
+    return _networkService.openSettings();
+  }
+}
diff --git a/mobile/lib/providers/partner.provider.dart b/mobile/lib/providers/partner.provider.dart
index 6ea335979f..bf638ae355 100644
--- a/mobile/lib/providers/partner.provider.dart
+++ b/mobile/lib/providers/partner.provider.dart
@@ -1,5 +1,6 @@
 import 'dart:async';
 
+import 'package:collection/collection.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
 import 'package:immich_mobile/services/partner.service.dart';
@@ -9,9 +10,19 @@ import 'package:isar/isar.dart';
 
 class PartnerSharedWithNotifier extends StateNotifier<List<User>> {
   PartnerSharedWithNotifier(Isar db, this._ps) : super([]) {
-    final query = db.users.filter().isPartnerSharedWithEqualTo(true);
-    query.findAll().then((partners) => state = partners);
-    query.watch().listen((partners) => state = partners);
+    Function eq = const ListEquality<User>().equals;
+    final query = db.users.filter().isPartnerSharedWithEqualTo(true).sortById();
+    query.findAll().then((partners) {
+      if (!eq(state, partners)) {
+        state = partners;
+      }
+    }).then((_) {
+      query.watch().listen((partners) {
+        if (!eq(state, partners)) {
+          state = partners;
+        }
+      });
+    });
   }
 
   Future<bool> updatePartner(User partner, {required bool inTimeline}) {
@@ -31,9 +42,19 @@ final partnerSharedWithProvider =
 
 class PartnerSharedByNotifier extends StateNotifier<List<User>> {
   PartnerSharedByNotifier(Isar db) : super([]) {
-    final query = db.users.filter().isPartnerSharedByEqualTo(true);
-    query.findAll().then((partners) => state = partners);
-    streamSub = query.watch().listen((partners) => state = partners);
+    Function eq = const ListEquality<User>().equals;
+    final query = db.users.filter().isPartnerSharedByEqualTo(true).sortById();
+    query.findAll().then((partners) {
+      if (!eq(state, partners)) {
+        state = partners;
+      }
+    }).then((_) {
+      streamSub = query.watch().listen((partners) {
+        if (!eq(state, partners)) {
+          state = partners;
+        }
+      });
+    });
   }
 
   late final StreamSubscription<List<User>> streamSub;
diff --git a/mobile/lib/providers/search/paginated_search.provider.dart b/mobile/lib/providers/search/paginated_search.provider.dart
index abf711f0ad..270f1148e8 100644
--- a/mobile/lib/providers/search/paginated_search.provider.dart
+++ b/mobile/lib/providers/search/paginated_search.provider.dart
@@ -1,46 +1,39 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/models/search/search_result.model.dart';
 import 'package:immich_mobile/providers/asset_viewer/render_list.provider.dart';
 import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
 import 'package:immich_mobile/models/search/search_filter.model.dart';
 import 'package:immich_mobile/services/search.service.dart';
-import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:riverpod_annotation/riverpod_annotation.dart';
 
 part 'paginated_search.provider.g.dart';
 
-@riverpod
-class PaginatedSearch extends _$PaginatedSearch {
-  Future<List<Asset>?> _search(SearchFilter filter, int page) async {
-    final service = ref.read(searchServiceProvider);
-    final result = await service.search(filter, page);
+final paginatedSearchProvider =
+    StateNotifierProvider<PaginatedSearchNotifier, SearchResult>(
+  (ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider)),
+);
 
-    return result;
-  }
+class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
+  final SearchService _searchService;
 
-  @override
-  Future<List<Asset>> build() async {
-    return [];
-  }
+  PaginatedSearchNotifier(this._searchService)
+      : super(SearchResult(assets: [], nextPage: 1));
 
-  Future<List<Asset>> getNextPage(SearchFilter filter, int nextPage) async {
-    state = const AsyncValue.loading();
+  search(SearchFilter filter) async {
+    if (state.nextPage == null) return;
 
-    final newState = await AsyncValue.guard(() async {
-      final assets = await _search(filter, nextPage);
+    final result = await _searchService.search(filter, state.nextPage!);
 
-      if (assets != null) {
-        return [...?state.value, ...assets];
-      }
-    });
+    if (result == null) return;
 
-    state = newState.valueOrNull == null
-        ? const AsyncValue.data([])
-        : AsyncValue.data(newState.value!);
-
-    return newState.valueOrNull ?? [];
+    state = SearchResult(
+      assets: [...state.assets, ...result.assets],
+      nextPage: result.nextPage,
+    );
   }
 
   clear() {
-    state = const AsyncValue.data([]);
+    state = SearchResult(assets: [], nextPage: 1);
   }
 }
 
@@ -48,15 +41,11 @@ class PaginatedSearch extends _$PaginatedSearch {
 AsyncValue<RenderList> paginatedSearchRenderList(
   PaginatedSearchRenderListRef ref,
 ) {
-  final assets = ref.watch(paginatedSearchProvider).value;
+  final result = ref.watch(paginatedSearchProvider);
 
-  if (assets != null) {
-    return ref.watch(
-      renderListProviderWithGrouping(
-        (assets, GroupAssetsBy.none),
-      ),
-    );
-  } else {
-    return const AsyncValue.loading();
-  }
+  return ref.watch(
+    renderListProviderWithGrouping(
+      (result.assets, GroupAssetsBy.none),
+    ),
+  );
 }
diff --git a/mobile/lib/providers/search/paginated_search.provider.g.dart b/mobile/lib/providers/search/paginated_search.provider.g.dart
index 3357be7776..cdf8cdd741 100644
Binary files a/mobile/lib/providers/search/paginated_search.provider.g.dart and b/mobile/lib/providers/search/paginated_search.provider.g.dart differ
diff --git a/mobile/lib/providers/search/people.provider.dart b/mobile/lib/providers/search/people.provider.dart
index e2c243354b..7c956f0a37 100644
--- a/mobile/lib/providers/search/people.provider.dart
+++ b/mobile/lib/providers/search/people.provider.dart
@@ -1,14 +1,14 @@
+import 'package:immich_mobile/interfaces/person_api.interface.dart';
 import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
 import 'package:immich_mobile/services/person.service.dart';
 import 'package:immich_mobile/providers/app_settings.provider.dart';
 import 'package:immich_mobile/services/app_settings.service.dart';
-import 'package:openapi/api.dart';
 import 'package:riverpod_annotation/riverpod_annotation.dart';
 
 part 'people.provider.g.dart';
 
 @riverpod
-Future<List<PersonResponseDto>> getAllPeople(
+Future<List<Person>> getAllPeople(
   GetAllPeopleRef ref,
 ) async {
   final PersonService personService = ref.read(personServiceProvider);
diff --git a/mobile/lib/providers/search/people.provider.g.dart b/mobile/lib/providers/search/people.provider.g.dart
index db2edfb956..c5ff6287cd 100644
Binary files a/mobile/lib/providers/search/people.provider.g.dart and b/mobile/lib/providers/search/people.provider.g.dart differ
diff --git a/mobile/lib/providers/search/search_input_focus.provider.dart b/mobile/lib/providers/search/search_input_focus.provider.dart
new file mode 100644
index 0000000000..4f6ed41ee0
--- /dev/null
+++ b/mobile/lib/providers/search/search_input_focus.provider.dart
@@ -0,0 +1,6 @@
+import 'package:flutter/widgets.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+
+final searchInputFocusProvider = Provider((ref) {
+  return FocusNode();
+});
diff --git a/mobile/lib/providers/server_info.provider.dart b/mobile/lib/providers/server_info.provider.dart
index 6327f992f5..a793acb3f6 100644
--- a/mobile/lib/providers/server_info.provider.dart
+++ b/mobile/lib/providers/server_info.provider.dart
@@ -34,6 +34,9 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
               trashDays: 30,
               oauthButtonText: '',
               externalDomain: '',
+              mapLightStyleUrl:
+                  'https://tiles.immich.cloud/v1/style/light.json',
+              mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
             ),
             serverDiskInfo: const ServerDiskInfo(
               diskAvailable: "0",
@@ -56,7 +59,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
     await getServerConfig();
   }
 
-  getServerVersion() async {
+  Future<void> getServerVersion() async {
     try {
       final serverVersion = await _serverInfoService.getServerVersion();
 
diff --git a/mobile/lib/providers/tab.provider.dart b/mobile/lib/providers/tab.provider.dart
index 2abed7c395..a4875115ce 100644
--- a/mobile/lib/providers/tab.provider.dart
+++ b/mobile/lib/providers/tab.provider.dart
@@ -1,11 +1,6 @@
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 
-enum TabEnum {
-  home,
-  search,
-  sharing,
-  library,
-}
+enum TabEnum { home, search, albums, library }
 
 /// Provides the currently active tab
 final tabProvider = StateProvider<TabEnum>(
diff --git a/mobile/lib/providers/theme.provider.dart b/mobile/lib/providers/theme.provider.dart
new file mode 100644
index 0000000000..73623bd026
--- /dev/null
+++ b/mobile/lib/providers/theme.provider.dart
@@ -0,0 +1,74 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+
+import 'package:immich_mobile/constants/colors.dart';
+import 'package:immich_mobile/theme/color_scheme.dart';
+import 'package:immich_mobile/theme/theme_data.dart';
+import 'package:immich_mobile/theme/dynamic_theme.dart';
+import 'package:immich_mobile/providers/app_settings.provider.dart';
+import 'package:immich_mobile/services/app_settings.service.dart';
+
+final immichThemeModeProvider = StateProvider<ThemeMode>((ref) {
+  final themeMode = ref
+      .watch(appSettingsServiceProvider)
+      .getSetting(AppSettingsEnum.themeMode);
+
+  debugPrint("Current themeMode $themeMode");
+
+  if (themeMode == ThemeMode.light.name) {
+    return ThemeMode.light;
+  } else if (themeMode == ThemeMode.dark.name) {
+    return ThemeMode.dark;
+  } else {
+    return ThemeMode.system;
+  }
+});
+
+final immichThemePresetProvider = StateProvider<ImmichColorPreset>((ref) {
+  final appSettingsProvider = ref.watch(appSettingsServiceProvider);
+  final primaryColorPreset =
+      appSettingsProvider.getSetting(AppSettingsEnum.primaryColor);
+
+  debugPrint("Current theme preset $primaryColorPreset");
+
+  try {
+    return ImmichColorPreset.values
+        .firstWhere((e) => e.name == primaryColorPreset);
+  } catch (e) {
+    debugPrint(
+      "Theme preset $primaryColorPreset not found. Applying default preset.",
+    );
+    appSettingsProvider.setSetting(
+      AppSettingsEnum.primaryColor,
+      defaultColorPresetName,
+    );
+    return defaultColorPreset;
+  }
+});
+
+final dynamicThemeSettingProvider = StateProvider<bool>((ref) {
+  return ref
+      .watch(appSettingsServiceProvider)
+      .getSetting(AppSettingsEnum.dynamicTheme);
+});
+
+final colorfulInterfaceSettingProvider = StateProvider<bool>((ref) {
+  return ref
+      .watch(appSettingsServiceProvider)
+      .getSetting(AppSettingsEnum.colorfulInterface);
+});
+
+// Provider for current selected theme
+final immichThemeProvider = StateProvider<ImmichTheme>((ref) {
+  final primaryColorPreset = ref.read(immichThemePresetProvider);
+  final useSystemColor = ref.watch(dynamicThemeSettingProvider);
+  final useColorfulInterface = ref.watch(colorfulInterfaceSettingProvider);
+  final ImmichTheme? dynamicTheme = DynamicTheme.theme;
+  final currentTheme = (useSystemColor && dynamicTheme != null)
+      ? dynamicTheme
+      : primaryColorPreset.themeOfPreset;
+
+  return useColorfulInterface
+      ? currentTheme
+      : decolorizeSurfaces(theme: currentTheme);
+});
diff --git a/mobile/lib/providers/trash.provider.dart b/mobile/lib/providers/trash.provider.dart
index 45ab1a5185..8bbac853c7 100644
--- a/mobile/lib/providers/trash.provider.dart
+++ b/mobile/lib/providers/trash.provider.dart
@@ -167,6 +167,6 @@ final trashedAssetsProvider = StreamProvider<RenderList>((ref) {
       .filter()
       .ownerIdEqualTo(user.isarId)
       .isTrashedEqualTo(true)
-      .sortByFileCreatedAt();
+      .sortByFileCreatedAtDesc();
   return renderListGeneratorWithGroupBy(query, GroupAssetsBy.none);
 });
diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart
index 6216a5de64..6889db7b7f 100644
--- a/mobile/lib/providers/websocket.provider.dart
+++ b/mobile/lib/providers/websocket.provider.dart
@@ -4,7 +4,7 @@ import 'package:collection/collection.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/widgets.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/providers/authentication.provider.dart';
+import 'package:immich_mobile/providers/auth.provider.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/models/server_info/server_version.model.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
@@ -103,7 +103,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
   /// Connects websocket to server unless already connected
   void connect() {
     if (state.isConnected) return;
-    final authenticationState = _ref.read(authenticationProvider);
+    final authenticationState = _ref.read(authProvider);
 
     if (authenticationState.isAuthenticated) {
       try {
diff --git a/mobile/lib/repositories/activity_api.repository.dart b/mobile/lib/repositories/activity_api.repository.dart
new file mode 100644
index 0000000000..8da3759709
--- /dev/null
+++ b/mobile/lib/repositories/activity_api.repository.dart
@@ -0,0 +1,67 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/user.entity.dart';
+import 'package:immich_mobile/interfaces/activity_api.interface.dart';
+import 'package:immich_mobile/models/activities/activity.model.dart';
+import 'package:immich_mobile/providers/api.provider.dart';
+import 'package:immich_mobile/repositories/api.repository.dart';
+import 'package:openapi/api.dart';
+
+final activityApiRepositoryProvider = Provider(
+  (ref) => ActivityApiRepository(ref.watch(apiServiceProvider).activitiesApi),
+);
+
+class ActivityApiRepository extends ApiRepository
+    implements IActivityApiRepository {
+  final ActivitiesApi _api;
+
+  ActivityApiRepository(this._api);
+
+  @override
+  Future<List<Activity>> getAll(String albumId, {String? assetId}) async {
+    final response =
+        await checkNull(_api.getActivities(albumId, assetId: assetId));
+    return response.map(_toActivity).toList();
+  }
+
+  @override
+  Future<Activity> create(
+    String albumId,
+    ActivityType type, {
+    String? assetId,
+    String? comment,
+  }) async {
+    final dto = ActivityCreateDto(
+      albumId: albumId,
+      type: type == ActivityType.comment
+          ? ReactionType.comment
+          : ReactionType.like,
+      assetId: assetId,
+      comment: comment,
+    );
+    final response = await checkNull(_api.createActivity(dto));
+    return _toActivity(response);
+  }
+
+  @override
+  Future<void> delete(String id) {
+    return checkNull(_api.deleteActivity(id));
+  }
+
+  @override
+  Future<ActivityStats> getStats(String albumId, {String? assetId}) async {
+    final response =
+        await checkNull(_api.getActivityStatistics(albumId, assetId: assetId));
+    return ActivityStats(comments: response.comments);
+  }
+
+  static Activity _toActivity(ActivityResponseDto dto) => Activity(
+        id: dto.id,
+        createdAt: dto.createdAt,
+        type: dto.type == ReactionType.comment
+            ? ActivityType.comment
+            : ActivityType.like,
+        user: User.fromSimpleUserDto(dto.user),
+        assetId: dto.assetId,
+        comment: dto.comment,
+      );
+}
diff --git a/mobile/lib/repositories/album.repository.dart b/mobile/lib/repositories/album.repository.dart
new file mode 100644
index 0000000000..2c78e4c238
--- /dev/null
+++ b/mobile/lib/repositories/album.repository.dart
@@ -0,0 +1,152 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/entities/user.entity.dart';
+import 'package:immich_mobile/interfaces/album.interface.dart';
+import 'package:immich_mobile/models/albums/album_search.model.dart';
+import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/repositories/database.repository.dart';
+import 'package:isar/isar.dart';
+
+final albumRepositoryProvider =
+    Provider((ref) => AlbumRepository(ref.watch(dbProvider)));
+
+class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
+  AlbumRepository(super.db);
+
+  @override
+  Future<int> count({bool? local}) {
+    final baseQuery = db.albums.where();
+    final QueryBuilder<Album, Album, QAfterWhereClause> query;
+    switch (local) {
+      case null:
+        query = baseQuery.noOp();
+      case true:
+        query = baseQuery.localIdIsNotNull();
+      case false:
+        query = baseQuery.remoteIdIsNotNull();
+    }
+    return query.count();
+  }
+
+  @override
+  Future<Album> create(Album album) => txn(() => db.albums.store(album));
+
+  @override
+  Future<Album?> getByName(String name, {bool? shared, bool? remote}) {
+    var query = db.albums.filter().nameEqualTo(name);
+    if (shared != null) {
+      query = query.sharedEqualTo(shared);
+    }
+    if (remote == true) {
+      query = query.localIdIsNull();
+    } else if (remote == false) {
+      query = query.remoteIdIsNull();
+    }
+    return query.findFirst();
+  }
+
+  @override
+  Future<Album> update(Album album) => txn(() => db.albums.store(album));
+
+  @override
+  Future<void> delete(int albumId) => txn(() => db.albums.delete(albumId));
+
+  @override
+  Future<List<Album>> getAll({
+    bool? shared,
+    bool? remote,
+    int? ownerId,
+    AlbumSort? sortBy,
+  }) {
+    final baseQuery = db.albums.where();
+    final QueryBuilder<Album, Album, QAfterWhereClause> afterWhere;
+    if (remote == null) {
+      afterWhere = baseQuery.noOp();
+    } else if (remote) {
+      afterWhere = baseQuery.remoteIdIsNotNull();
+    } else {
+      afterWhere = baseQuery.localIdIsNotNull();
+    }
+    QueryBuilder<Album, Album, QAfterFilterCondition> filterQuery =
+        afterWhere.filter().noOp();
+    if (shared != null) {
+      filterQuery = filterQuery.sharedEqualTo(true);
+    }
+    if (ownerId != null) {
+      filterQuery = filterQuery.owner((q) => q.isarIdEqualTo(ownerId));
+    }
+    final QueryBuilder<Album, Album, QAfterSortBy> query;
+    switch (sortBy) {
+      case null:
+        query = filterQuery.noOp();
+      case AlbumSort.remoteId:
+        query = filterQuery.sortByRemoteId();
+      case AlbumSort.localId:
+        query = filterQuery.sortByLocalId();
+    }
+    return query.findAll();
+  }
+
+  @override
+  Future<Album?> get(int id) => db.albums.get(id);
+
+  @override
+  Future<void> removeUsers(Album album, List<User> users) =>
+      txn(() => album.sharedUsers.update(unlink: users));
+
+  @override
+  Future<void> addAssets(Album album, List<Asset> assets) =>
+      txn(() => album.assets.update(link: assets));
+
+  @override
+  Future<void> removeAssets(Album album, List<Asset> assets) =>
+      txn(() => album.assets.update(unlink: assets));
+
+  @override
+  Future<Album> recalculateMetadata(Album album) async {
+    album.startDate = await album.assets.filter().fileCreatedAtProperty().min();
+    album.endDate = await album.assets.filter().fileCreatedAtProperty().max();
+    album.lastModifiedAssetTimestamp =
+        await album.assets.filter().updatedAtProperty().max();
+    return album;
+  }
+
+  @override
+  Future<void> addUsers(Album album, List<User> users) =>
+      txn(() => album.sharedUsers.update(link: users));
+
+  @override
+  Future<void> deleteAllLocal() =>
+      txn(() => db.albums.where().localIdIsNotNull().deleteAll());
+
+  @override
+  Future<List<Album>> search(
+    String searchTerm,
+    QuickFilterMode filterMode,
+  ) async {
+    var query = db.albums
+        .filter()
+        .nameContains(searchTerm, caseSensitive: false)
+        .remoteIdIsNotNull();
+
+    switch (filterMode) {
+      case QuickFilterMode.sharedWithMe:
+        query = query.owner(
+          (q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).isarId),
+        );
+        break;
+      case QuickFilterMode.myAlbums:
+        query = query.owner(
+          (q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId),
+        );
+        break;
+      case QuickFilterMode.all:
+      default:
+        break;
+    }
+
+    return await query.findAll();
+  }
+}
diff --git a/mobile/lib/repositories/album_api.repository.dart b/mobile/lib/repositories/album_api.repository.dart
new file mode 100644
index 0000000000..2438304158
--- /dev/null
+++ b/mobile/lib/repositories/album_api.repository.dart
@@ -0,0 +1,176 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/constants/enums.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/entities/user.entity.dart';
+import 'package:immich_mobile/interfaces/album_api.interface.dart';
+import 'package:immich_mobile/providers/api.provider.dart';
+import 'package:immich_mobile/repositories/api.repository.dart';
+import 'package:openapi/api.dart';
+
+final albumApiRepositoryProvider = Provider(
+  (ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi),
+);
+
+class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
+  final AlbumsApi _api;
+
+  AlbumApiRepository(this._api);
+
+  @override
+  Future<Album> get(String id) async {
+    final dto = await checkNull(_api.getAlbumInfo(id));
+    return _toAlbum(dto);
+  }
+
+  @override
+  Future<List<Album>> getAll({bool? shared}) async {
+    final dtos = await checkNull(_api.getAllAlbums(shared: shared));
+    return dtos.map(_toAlbum).toList();
+  }
+
+  @override
+  Future<Album> create(
+    String name, {
+    required Iterable<String> assetIds,
+    Iterable<String> sharedUserIds = const [],
+  }) async {
+    final users = sharedUserIds.map(
+      (id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor),
+    );
+    final responseDto = await checkNull(
+      _api.createAlbum(
+        CreateAlbumDto(
+          albumName: name,
+          assetIds: assetIds.toList(),
+          albumUsers: users.toList(),
+        ),
+      ),
+    );
+    return _toAlbum(responseDto);
+  }
+
+  @override
+  Future<Album> update(
+    String albumId, {
+    String? name,
+    String? thumbnailAssetId,
+    String? description,
+    bool? activityEnabled,
+    SortOrder? sortOrder,
+  }) async {
+    AssetOrder? order;
+    if (sortOrder != null) {
+      order = sortOrder == SortOrder.asc ? AssetOrder.asc : AssetOrder.desc;
+    }
+
+    final response = await checkNull(
+      _api.updateAlbumInfo(
+        albumId,
+        UpdateAlbumDto(
+          albumName: name,
+          albumThumbnailAssetId: thumbnailAssetId,
+          description: description,
+          isActivityEnabled: activityEnabled,
+          order: order,
+        ),
+      ),
+    );
+
+    return _toAlbum(response);
+  }
+
+  @override
+  Future<void> delete(String albumId) {
+    return _api.deleteAlbum(albumId);
+  }
+
+  @override
+  Future<({List<String> added, List<String> duplicates})> addAssets(
+    String albumId,
+    Iterable<String> assetIds,
+  ) async {
+    final response = await checkNull(
+      _api.addAssetsToAlbum(
+        albumId,
+        BulkIdsDto(ids: assetIds.toList()),
+      ),
+    );
+
+    final List<String> added = [];
+    final List<String> duplicates = [];
+
+    for (final result in response) {
+      if (result.success) {
+        added.add(result.id);
+      } else if (result.error == BulkIdResponseDtoErrorEnum.duplicate) {
+        duplicates.add(result.id);
+      }
+    }
+    return (added: added, duplicates: duplicates);
+  }
+
+  @override
+  Future<({List<String> removed, List<String> failed})> removeAssets(
+    String albumId,
+    Iterable<String> assetIds,
+  ) async {
+    final response = await checkNull(
+      _api.removeAssetFromAlbum(
+        albumId,
+        BulkIdsDto(ids: assetIds.toList()),
+      ),
+    );
+    final List<String> removed = [], failed = [];
+    for (final dto in response) {
+      if (dto.success) {
+        removed.add(dto.id);
+      } else {
+        failed.add(dto.id);
+      }
+    }
+    return (removed: removed, failed: failed);
+  }
+
+  @override
+  Future<Album> addUsers(String albumId, Iterable<String> userIds) async {
+    final albumUsers =
+        userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList();
+    final response = await checkNull(
+      _api.addUsersToAlbum(
+        albumId,
+        AddUsersDto(albumUsers: albumUsers),
+      ),
+    );
+    return _toAlbum(response);
+  }
+
+  @override
+  Future<void> removeUser(String albumId, {required String userId}) {
+    return _api.removeUserFromAlbum(albumId, userId);
+  }
+
+  static Album _toAlbum(AlbumResponseDto dto) {
+    final Album album = Album(
+      remoteId: dto.id,
+      name: dto.albumName,
+      createdAt: dto.createdAt,
+      modifiedAt: dto.updatedAt,
+      lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
+      shared: dto.shared,
+      startDate: dto.startDate,
+      endDate: dto.endDate,
+      activityEnabled: dto.isActivityEnabled,
+      sortOrder: dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc,
+    );
+    album.remoteAssetCount = dto.assetCount;
+    album.owner.value = User.fromSimpleUserDto(dto.owner);
+    album.remoteThumbnailAssetId = dto.albumThumbnailAssetId;
+    final users = dto.albumUsers
+        .map((albumUser) => User.fromSimpleUserDto(albumUser.user));
+    album.sharedUsers.addAll(users);
+    final assets = dto.assets.map(Asset.remote).toList();
+    album.assets.addAll(assets);
+    return album;
+  }
+}
diff --git a/mobile/lib/repositories/album_media.repository.dart b/mobile/lib/repositories/album_media.repository.dart
new file mode 100644
index 0000000000..dac9ccd4da
--- /dev/null
+++ b/mobile/lib/repositories/album_media.repository.dart
@@ -0,0 +1,92 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/interfaces/album_media.interface.dart';
+import 'package:immich_mobile/repositories/asset_media.repository.dart';
+import 'package:photo_manager/photo_manager.dart' hide AssetType;
+
+final albumMediaRepositoryProvider = Provider((ref) => AlbumMediaRepository());
+
+class AlbumMediaRepository implements IAlbumMediaRepository {
+  @override
+  Future<List<Album>> getAll() async {
+    final List<AssetPathEntity> assetPathEntities =
+        await PhotoManager.getAssetPathList(
+      hasAll: true,
+    );
+    return assetPathEntities.map(_toAlbum).toList();
+  }
+
+  @override
+  Future<List<String>> getAssetIds(String albumId) async {
+    final album = await AssetPathEntity.fromId(albumId);
+    final List<AssetEntity> assets =
+        await album.getAssetListRange(start: 0, end: 0x7fffffffffffffff);
+    return assets.map((e) => e.id).toList();
+  }
+
+  @override
+  Future<int> getAssetCount(String albumId) async {
+    final album = await AssetPathEntity.fromId(albumId);
+    return album.assetCountAsync;
+  }
+
+  @override
+  Future<List<Asset>> getAssets(
+    String albumId, {
+    int start = 0,
+    int end = 0x7fffffffffffffff,
+    DateTime? modifiedFrom,
+    DateTime? modifiedUntil,
+    bool orderByModificationDate = false,
+  }) async {
+    final onDevice = await AssetPathEntity.fromId(
+      albumId,
+      filterOption: FilterOptionGroup(
+        containsPathModified: true,
+        orders: orderByModificationDate
+            ? [const OrderOption(type: OrderOptionType.updateDate)]
+            : [],
+        imageOption: const FilterOption(needTitle: true),
+        videoOption: const FilterOption(needTitle: true),
+        updateTimeCond: modifiedFrom == null && modifiedUntil == null
+            ? null
+            : DateTimeCond(
+                min: modifiedFrom ?? DateTime.utc(-271820),
+                max: modifiedUntil ?? DateTime.utc(275760),
+              ),
+      ),
+    );
+
+    final List<AssetEntity> assets =
+        await onDevice.getAssetListRange(start: start, end: end);
+    return assets.map(AssetMediaRepository.toAsset).toList().cast();
+  }
+
+  @override
+  Future<Album> get(
+    String id, {
+    DateTime? modifiedFrom,
+    DateTime? modifiedUntil,
+  }) async {
+    final assetPathEntity = await AssetPathEntity.fromId(id);
+    return _toAlbum(assetPathEntity);
+  }
+
+  static Album _toAlbum(AssetPathEntity assetPathEntity) {
+    final Album album = Album(
+      name: assetPathEntity.name,
+      createdAt:
+          assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(),
+      modifiedAt:
+          assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(),
+      shared: false,
+      activityEnabled: false,
+    );
+    album.owner.value = Store.get(StoreKey.currentUser);
+    album.localId = assetPathEntity.id;
+    album.isAll = assetPathEntity.isAll;
+    return album;
+  }
+}
diff --git a/mobile/lib/repositories/api.repository.dart b/mobile/lib/repositories/api.repository.dart
new file mode 100644
index 0000000000..b454c77f9b
--- /dev/null
+++ b/mobile/lib/repositories/api.repository.dart
@@ -0,0 +1,9 @@
+import 'package:immich_mobile/constants/errors.dart';
+
+abstract class ApiRepository {
+  Future<T> checkNull<T>(Future<T?> future) async {
+    final response = await future;
+    if (response == null) throw NoResponseDtoError();
+    return response;
+  }
+}
diff --git a/mobile/lib/repositories/asset.repository.dart b/mobile/lib/repositories/asset.repository.dart
new file mode 100644
index 0000000000..eaaafd3045
--- /dev/null
+++ b/mobile/lib/repositories/asset.repository.dart
@@ -0,0 +1,248 @@
+import 'dart:io';
+
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/entities/android_device_asset.entity.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/entities/device_asset.entity.dart';
+import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
+import 'package:immich_mobile/entities/exif_info.entity.dart';
+import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
+import 'package:immich_mobile/interfaces/asset.interface.dart';
+import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/repositories/database.repository.dart';
+import 'package:isar/isar.dart';
+
+final assetRepositoryProvider =
+    Provider((ref) => AssetRepository(ref.watch(dbProvider)));
+
+class AssetRepository extends DatabaseRepository implements IAssetRepository {
+  AssetRepository(super.db);
+
+  @override
+  Future<List<Asset>> getByAlbum(
+    Album album, {
+    Iterable<int> notOwnedBy = const [],
+    int? ownerId,
+    AssetState? state,
+    AssetSort? sortBy,
+  }) {
+    var query = album.assets.filter();
+    if (notOwnedBy.length == 1) {
+      query = query.not().ownerIdEqualTo(notOwnedBy.first);
+    } else if (notOwnedBy.isNotEmpty) {
+      query =
+          query.not().anyOf(notOwnedBy, (q, int id) => q.ownerIdEqualTo(id));
+    }
+    if (ownerId != null) {
+      query = query.ownerIdEqualTo(ownerId);
+    }
+
+    switch (state) {
+      case null:
+        break;
+      case AssetState.local:
+        query = query.remoteIdIsNull();
+      case AssetState.remote:
+        query = query.localIdIsNull();
+      case AssetState.merged:
+        query = query.localIdIsNotNull().remoteIdIsNotNull();
+    }
+
+    final QueryBuilder<Asset, Asset, QAfterSortBy> sortedQuery;
+
+    switch (sortBy) {
+      case null:
+        sortedQuery = query.noOp();
+      case AssetSort.checksum:
+        sortedQuery = query.sortByChecksum();
+      case AssetSort.ownerIdChecksum:
+        sortedQuery = query.sortByOwnerId().thenByChecksum();
+    }
+
+    return sortedQuery.findAll();
+  }
+
+  @override
+  Future<void> deleteById(List<int> ids) => txn(() async {
+        await db.assets.deleteAll(ids);
+        await db.exifInfos.deleteAll(ids);
+      });
+
+  @override
+  Future<Asset?> getByRemoteId(String id) => db.assets.getByRemoteId(id);
+
+  @override
+  Future<List<Asset>> getAllByRemoteId(
+    Iterable<String> ids, {
+    AssetState? state,
+  }) =>
+      _getAllByRemoteIdImpl(ids, state).findAll();
+
+  QueryBuilder<Asset, Asset, QAfterFilterCondition> _getAllByRemoteIdImpl(
+    Iterable<String> ids,
+    AssetState? state,
+  ) {
+    final query = db.assets.remote(ids).filter();
+    switch (state) {
+      case null:
+        return query.noOp();
+      case AssetState.local:
+        return query.remoteIdIsNull();
+      case AssetState.remote:
+        return query.localIdIsNull();
+      case AssetState.merged:
+        return query.localIdIsNotEmpty().remoteIdIsNotNull();
+    }
+  }
+
+  @override
+  Future<List<Asset>> getAll({
+    required int ownerId,
+    AssetState? state,
+    AssetSort? sortBy,
+    int? limit,
+  }) {
+    final baseQuery = db.assets.where();
+    final QueryBuilder<Asset, Asset, QAfterFilterCondition> filteredQuery;
+    switch (state) {
+      case null:
+        filteredQuery = baseQuery.ownerIdEqualToAnyChecksum(ownerId).noOp();
+      case AssetState.local:
+        filteredQuery = baseQuery
+            .remoteIdIsNull()
+            .filter()
+            .localIdIsNotNull()
+            .ownerIdEqualTo(ownerId);
+      case AssetState.remote:
+        filteredQuery = baseQuery
+            .localIdIsNull()
+            .filter()
+            .remoteIdIsNotNull()
+            .ownerIdEqualTo(ownerId);
+      case AssetState.merged:
+        filteredQuery = baseQuery
+            .ownerIdEqualToAnyChecksum(ownerId)
+            .filter()
+            .remoteIdIsNotNull()
+            .localIdIsNotNull();
+    }
+
+    final QueryBuilder<Asset, Asset, QAfterSortBy> query;
+    switch (sortBy) {
+      case null:
+        query = filteredQuery.noOp();
+      case AssetSort.checksum:
+        query = filteredQuery.sortByChecksum();
+      case AssetSort.ownerIdChecksum:
+        query = filteredQuery.sortByOwnerId().thenByChecksum();
+    }
+
+    return limit == null ? query.findAll() : query.limit(limit).findAll();
+  }
+
+  @override
+  Future<List<Asset>> updateAll(List<Asset> assets) async {
+    await txn(() => db.assets.putAll(assets));
+    return assets;
+  }
+
+  @override
+  Future<List<Asset>> getMatches({
+    required List<Asset> assets,
+    required int ownerId,
+    AssetState? state,
+    int limit = 100,
+  }) {
+    final baseQuery = db.assets.where();
+    final QueryBuilder<Asset, Asset, QAfterFilterCondition> query;
+    switch (state) {
+      case null:
+        query = baseQuery.noOp();
+      case AssetState.local:
+        query = baseQuery.remoteIdIsNull().filter().localIdIsNotNull();
+      case AssetState.remote:
+        query = baseQuery.localIdIsNull().filter().remoteIdIsNotNull();
+      case AssetState.merged:
+        query = baseQuery.localIdIsNotNull().filter().remoteIdIsNotNull();
+    }
+    return _getMatchesImpl(query, ownerId, assets, limit);
+  }
+
+  @override
+  Future<List<DeviceAsset?>> getDeviceAssetsById(List<Object> ids) =>
+      Platform.isAndroid
+          ? db.androidDeviceAssets.getAll(ids.cast())
+          : db.iOSDeviceAssets.getAllById(ids.cast());
+
+  @override
+  Future<void> upsertDeviceAssets(List<DeviceAsset> deviceAssets) => txn(
+        () => Platform.isAndroid
+            ? db.androidDeviceAssets.putAll(deviceAssets.cast())
+            : db.iOSDeviceAssets.putAll(deviceAssets.cast()),
+      );
+
+  @override
+  Future<Asset> update(Asset asset) async {
+    await txn(() => asset.put(db));
+    return asset;
+  }
+
+  @override
+  Future<void> upsertDuplicatedAssets(Iterable<String> duplicatedAssets) => txn(
+        () => db.duplicatedAssets
+            .putAll(duplicatedAssets.map(DuplicatedAsset.new).toList()),
+      );
+
+  @override
+  Future<List<String>> getAllDuplicatedAssetIds() =>
+      db.duplicatedAssets.where().idProperty().findAll();
+
+  @override
+  Future<Asset?> getByOwnerIdChecksum(int ownerId, String checksum) =>
+      db.assets.getByOwnerIdChecksum(ownerId, checksum);
+
+  @override
+  Future<List<Asset?>> getAllByOwnerIdChecksum(
+    List<int> ids,
+    List<String> checksums,
+  ) =>
+      db.assets.getAllByOwnerIdChecksum(ids, checksums);
+
+  @override
+  Future<List<Asset>> getAllLocal() =>
+      db.assets.where().localIdIsNotNull().findAll();
+
+  @override
+  Future<void> deleteAllByRemoteId(List<String> ids, {AssetState? state}) =>
+      txn(() => _getAllByRemoteIdImpl(ids, state).deleteAll());
+}
+
+Future<List<Asset>> _getMatchesImpl(
+  QueryBuilder<Asset, Asset, QAfterFilterCondition> query,
+  int ownerId,
+  List<Asset> assets,
+  int limit,
+) =>
+    query
+        .ownerIdEqualTo(ownerId)
+        .anyOf(
+          assets,
+          (q, Asset a) => q
+              .fileNameEqualTo(a.fileName)
+              .and()
+              .durationInSecondsEqualTo(a.durationInSeconds)
+              .and()
+              .fileCreatedAtBetween(
+                a.fileCreatedAt.subtract(const Duration(hours: 12)),
+                a.fileCreatedAt.add(const Duration(hours: 12)),
+              )
+              .and()
+              .not()
+              .checksumEqualTo(a.checksum),
+        )
+        .sortByFileName()
+        .thenByFileCreatedAt()
+        .thenByFileModifiedAt()
+        .limit(limit)
+        .findAll();
diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart
new file mode 100644
index 0000000000..f4fcd8a6dd
--- /dev/null
+++ b/mobile/lib/repositories/asset_api.repository.dart
@@ -0,0 +1,51 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/interfaces/asset_api.interface.dart';
+import 'package:immich_mobile/providers/api.provider.dart';
+import 'package:immich_mobile/repositories/api.repository.dart';
+import 'package:openapi/api.dart';
+
+final assetApiRepositoryProvider = Provider(
+  (ref) => AssetApiRepository(
+    ref.watch(apiServiceProvider).assetsApi,
+    ref.watch(apiServiceProvider).searchApi,
+  ),
+);
+
+class AssetApiRepository extends ApiRepository implements IAssetApiRepository {
+  final AssetsApi _api;
+  final SearchApi _searchApi;
+
+  AssetApiRepository(this._api, this._searchApi);
+
+  @override
+  Future<Asset> update(String id, {String? description}) async {
+    final response = await checkNull(
+      _api.updateAsset(id, UpdateAssetDto(description: description)),
+    );
+    return Asset.remote(response);
+  }
+
+  @override
+  Future<List<Asset>> search({List<String> personIds = const []}) async {
+    // TODO this always fetches all assets, change API and usage to actually do pagination
+    final List<Asset> result = [];
+    bool hasNext = true;
+    int currentPage = 1;
+    while (hasNext) {
+      final response = await checkNull(
+        _searchApi.searchAssets(
+          MetadataSearchDto(
+            personIds: personIds,
+            page: currentPage,
+            size: 1000,
+          ),
+        ),
+      );
+      result.addAll(response.assets.items.map(Asset.remote));
+      hasNext = response.assets.nextPage != null;
+      currentPage++;
+    }
+    return result;
+  }
+}
diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart
new file mode 100644
index 0000000000..68fffa08a6
--- /dev/null
+++ b/mobile/lib/repositories/asset_media.repository.dart
@@ -0,0 +1,59 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/entities/exif_info.entity.dart';
+import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/interfaces/asset_media.interface.dart';
+import 'package:photo_manager/photo_manager.dart' hide AssetType;
+
+final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository());
+
+class AssetMediaRepository implements IAssetMediaRepository {
+  @override
+  Future<List<String>> deleteAll(List<String> ids) =>
+      PhotoManager.editor.deleteWithIds(ids);
+
+  @override
+  Future<Asset?> get(String id) async {
+    final entity = await AssetEntity.fromId(id);
+    return toAsset(entity);
+  }
+
+  static Asset? toAsset(AssetEntity? local) {
+    if (local == null) return null;
+    final Asset asset = Asset(
+      checksum: "",
+      localId: local.id,
+      ownerId: Store.get(StoreKey.currentUser).isarId,
+      fileCreatedAt: local.createDateTime,
+      fileModifiedAt: local.modifiedDateTime,
+      updatedAt: local.modifiedDateTime,
+      durationInSeconds: local.duration,
+      type: AssetType.values[local.typeInt],
+      fileName: local.title!,
+      width: local.width,
+      height: local.height,
+      isFavorite: local.isFavorite,
+    );
+    if (asset.fileCreatedAt.year == 1970) {
+      asset.fileCreatedAt = asset.fileModifiedAt;
+    }
+    if (local.latitude != null) {
+      asset.exifInfo = ExifInfo(lat: local.latitude, long: local.longitude);
+    }
+    asset.local = local;
+    return asset;
+  }
+
+  @override
+  Future<String?> getOriginalFilename(String id) async {
+    final entity = await AssetEntity.fromId(id);
+
+    if (entity == null) {
+      return null;
+    }
+
+    // titleAsync gets the correct original filename for some assets on iOS
+    // otherwise using the `entity.title` would return a random GUID
+    return await entity.titleAsync;
+  }
+}
diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart
new file mode 100644
index 0000000000..fa504e6ac3
--- /dev/null
+++ b/mobile/lib/repositories/auth.repository.dart
@@ -0,0 +1,69 @@
+import 'dart:convert';
+
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/entities/etag.entity.dart';
+import 'package:immich_mobile/entities/exif_info.entity.dart';
+import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/entities/user.entity.dart';
+import 'package:immich_mobile/interfaces/auth.interface.dart';
+import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
+import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/repositories/database.repository.dart';
+
+final authRepositoryProvider = Provider<IAuthRepository>(
+  (ref) => AuthRepository(ref.watch(dbProvider)),
+);
+
+class AuthRepository extends DatabaseRepository implements IAuthRepository {
+  AuthRepository(super.db);
+
+  @override
+  Future<void> clearLocalData() {
+    return db.writeTxn(() {
+      return Future.wait([
+        db.assets.clear(),
+        db.exifInfos.clear(),
+        db.albums.clear(),
+        db.eTags.clear(),
+        db.users.clear(),
+      ]);
+    });
+  }
+
+  @override
+  String getAccessToken() {
+    return Store.get(StoreKey.accessToken);
+  }
+
+  @override
+  bool getEndpointSwitchingFeature() {
+    return Store.tryGet(StoreKey.autoEndpointSwitching) ?? false;
+  }
+
+  @override
+  String? getPreferredWifiName() {
+    return Store.tryGet(StoreKey.preferredWifiName);
+  }
+
+  @override
+  String? getLocalEndpoint() {
+    return Store.tryGet(StoreKey.localEndpoint);
+  }
+
+  @override
+  List<AuxilaryEndpoint> getExternalEndpointList() {
+    final jsonString = Store.tryGet(StoreKey.externalEndpointList);
+
+    if (jsonString == null) {
+      return [];
+    }
+
+    final List<dynamic> jsonList = jsonDecode(jsonString);
+    final endpointList =
+        jsonList.map((e) => AuxilaryEndpoint.fromJson(e)).toList();
+
+    return endpointList;
+  }
+}
diff --git a/mobile/lib/repositories/auth_api.repository.dart b/mobile/lib/repositories/auth_api.repository.dart
new file mode 100644
index 0000000000..f3a1d52de3
--- /dev/null
+++ b/mobile/lib/repositories/auth_api.repository.dart
@@ -0,0 +1,58 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/interfaces/auth_api.interface.dart';
+import 'package:immich_mobile/models/auth/login_response.model.dart';
+import 'package:immich_mobile/providers/api.provider.dart';
+import 'package:immich_mobile/repositories/api.repository.dart';
+import 'package:immich_mobile/services/api.service.dart';
+import 'package:openapi/api.dart';
+
+final authApiRepositoryProvider =
+    Provider((ref) => AuthApiRepository(ref.watch(apiServiceProvider)));
+
+class AuthApiRepository extends ApiRepository implements IAuthApiRepository {
+  final ApiService _apiService;
+
+  AuthApiRepository(this._apiService);
+
+  @override
+  Future<void> changePassword(String newPassword) async {
+    await _apiService.usersApi.updateMyUser(
+      UserUpdateMeDto(
+        password: newPassword,
+      ),
+    );
+  }
+
+  @override
+  Future<LoginResponse> login(String email, String password) async {
+    final loginResponseDto = await checkNull(
+      _apiService.authenticationApi.login(
+        LoginCredentialDto(
+          email: email,
+          password: password,
+        ),
+      ),
+    );
+
+    return _mapLoginReponse(loginResponseDto);
+  }
+
+  @override
+  Future<void> logout() async {
+    await _apiService.authenticationApi
+        .logout()
+        .timeout(const Duration(seconds: 7));
+  }
+
+  _mapLoginReponse(LoginResponseDto dto) {
+    return LoginResponse(
+      accessToken: dto.accessToken,
+      isAdmin: dto.isAdmin,
+      name: dto.name,
+      profileImagePath: dto.profileImagePath,
+      shouldChangePassword: dto.shouldChangePassword,
+      userEmail: dto.userEmail,
+      userId: dto.userId,
+    );
+  }
+}
diff --git a/mobile/lib/repositories/backup.repository.dart b/mobile/lib/repositories/backup.repository.dart
new file mode 100644
index 0000000000..61997ff23a
--- /dev/null
+++ b/mobile/lib/repositories/backup.repository.dart
@@ -0,0 +1,42 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/backup_album.entity.dart';
+import 'package:immich_mobile/interfaces/backup.interface.dart';
+import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/repositories/database.repository.dart';
+import 'package:isar/isar.dart';
+
+final backupRepositoryProvider =
+    Provider((ref) => BackupRepository(ref.watch(dbProvider)));
+
+class BackupRepository extends DatabaseRepository implements IBackupRepository {
+  BackupRepository(super.db);
+
+  @override
+  Future<List<BackupAlbum>> getAll({BackupAlbumSort? sort}) {
+    final baseQuery = db.backupAlbums.where();
+    final QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> query;
+    switch (sort) {
+      case null:
+        query = baseQuery.noOp();
+      case BackupAlbumSort.id:
+        query = baseQuery.sortById();
+    }
+    return query.findAll();
+  }
+
+  @override
+  Future<List<String>> getIdsBySelection(BackupSelection backup) =>
+      db.backupAlbums.filter().selectionEqualTo(backup).idProperty().findAll();
+
+  @override
+  Future<List<BackupAlbum>> getAllBySelection(BackupSelection backup) =>
+      db.backupAlbums.filter().selectionEqualTo(backup).findAll();
+
+  @override
+  Future<void> deleteAll(List<int> ids) =>
+      txn(() => db.backupAlbums.deleteAll(ids));
+
+  @override
+  Future<void> updateAll(List<BackupAlbum> backupAlbums) =>
+      txn(() => db.backupAlbums.putAll(backupAlbums));
+}
diff --git a/mobile/lib/repositories/database.repository.dart b/mobile/lib/repositories/database.repository.dart
new file mode 100644
index 0000000000..3eb74621fa
--- /dev/null
+++ b/mobile/lib/repositories/database.repository.dart
@@ -0,0 +1,27 @@
+import 'dart:async';
+import 'package:immich_mobile/interfaces/database.interface.dart';
+import 'package:isar/isar.dart';
+
+/// copied from Isar; needed to check if an async transaction is already active
+const Symbol _zoneTxn = #zoneTxn;
+
+abstract class DatabaseRepository implements IDatabaseRepository {
+  final Isar db;
+  DatabaseRepository(this.db);
+
+  bool get inTxn => Zone.current[_zoneTxn] != null;
+
+  Future<T> txn<T>(Future<T> Function() callback) =>
+      inTxn ? callback() : transaction(callback);
+
+  @override
+  Future<T> transaction<T>(Future<T> Function() callback) =>
+      db.writeTxn(callback);
+}
+
+extension Asd<T> on QueryBuilder<T, dynamic, dynamic> {
+  QueryBuilder<T, T, O> noOp<O>() {
+    // ignore: invalid_use_of_protected_member
+    return QueryBuilder.apply(this, (query) => query);
+  }
+}
diff --git a/mobile/lib/repositories/download.repository.dart b/mobile/lib/repositories/download.repository.dart
new file mode 100644
index 0000000000..5b42f66b02
--- /dev/null
+++ b/mobile/lib/repositories/download.repository.dart
@@ -0,0 +1,68 @@
+import 'package:background_downloader/background_downloader.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/interfaces/download.interface.dart';
+import 'package:immich_mobile/utils/download.dart';
+
+final downloadRepositoryProvider = Provider((ref) => DownloadRepository());
+
+class DownloadRepository implements IDownloadRepository {
+  @override
+  void Function(TaskStatusUpdate)? onImageDownloadStatus;
+
+  @override
+  void Function(TaskStatusUpdate)? onVideoDownloadStatus;
+
+  @override
+  void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus;
+
+  @override
+  void Function(TaskProgressUpdate)? onTaskProgress;
+
+  DownloadRepository() {
+    FileDownloader().registerCallbacks(
+      group: downloadGroupImage,
+      taskStatusCallback: (update) => onImageDownloadStatus?.call(update),
+      taskProgressCallback: (update) => onTaskProgress?.call(update),
+    );
+
+    FileDownloader().registerCallbacks(
+      group: downloadGroupVideo,
+      taskStatusCallback: (update) => onVideoDownloadStatus?.call(update),
+      taskProgressCallback: (update) => onTaskProgress?.call(update),
+    );
+
+    FileDownloader().registerCallbacks(
+      group: downloadGroupLivePhoto,
+      taskStatusCallback: (update) => onLivePhotoDownloadStatus?.call(update),
+      taskProgressCallback: (update) => onTaskProgress?.call(update),
+    );
+  }
+
+  @override
+  Future<bool> download(DownloadTask task) {
+    return FileDownloader().enqueue(task);
+  }
+
+  @override
+  Future<void> deleteAllTrackingRecords() {
+    return FileDownloader().database.deleteAllRecords();
+  }
+
+  @override
+  Future<bool> cancel(String id) {
+    return FileDownloader().cancelTaskWithId(id);
+  }
+
+  @override
+  Future<List<TaskRecord>> getLiveVideoTasks() {
+    return FileDownloader().database.allRecordsWithStatus(
+          TaskStatus.complete,
+          group: downloadGroupLivePhoto,
+        );
+  }
+
+  @override
+  Future<void> deleteRecordsWithIds(List<String> ids) {
+    return FileDownloader().database.deleteRecordsWithIds(ids);
+  }
+}
diff --git a/mobile/lib/repositories/etag.repository.dart b/mobile/lib/repositories/etag.repository.dart
new file mode 100644
index 0000000000..9921b69f5e
--- /dev/null
+++ b/mobile/lib/repositories/etag.repository.dart
@@ -0,0 +1,29 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/etag.entity.dart';
+import 'package:immich_mobile/interfaces/etag.interface.dart';
+import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/repositories/database.repository.dart';
+import 'package:isar/isar.dart';
+
+final etagRepositoryProvider =
+    Provider((ref) => ETagRepository(ref.watch(dbProvider)));
+
+class ETagRepository extends DatabaseRepository implements IETagRepository {
+  ETagRepository(super.db);
+
+  @override
+  Future<List<String>> getAllIds() => db.eTags.where().idProperty().findAll();
+
+  @override
+  Future<ETag?> get(int id) => db.eTags.get(id);
+
+  @override
+  Future<void> upsertAll(List<ETag> etags) => txn(() => db.eTags.putAll(etags));
+
+  @override
+  Future<void> deleteByIds(List<String> ids) =>
+      txn(() => db.eTags.deleteAllById(ids));
+
+  @override
+  Future<ETag?> getById(String id) => db.eTags.getById(id);
+}
diff --git a/mobile/lib/repositories/exif_info.repository.dart b/mobile/lib/repositories/exif_info.repository.dart
new file mode 100644
index 0000000000..3ddb50104b
--- /dev/null
+++ b/mobile/lib/repositories/exif_info.repository.dart
@@ -0,0 +1,31 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/exif_info.entity.dart';
+import 'package:immich_mobile/interfaces/exif_info.interface.dart';
+import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/repositories/database.repository.dart';
+
+final exifInfoRepositoryProvider =
+    Provider((ref) => ExifInfoRepository(ref.watch(dbProvider)));
+
+class ExifInfoRepository extends DatabaseRepository
+    implements IExifInfoRepository {
+  ExifInfoRepository(super.db);
+
+  @override
+  Future<void> delete(int id) => txn(() => db.exifInfos.delete(id));
+
+  @override
+  Future<ExifInfo?> get(int id) => db.exifInfos.get(id);
+
+  @override
+  Future<ExifInfo> update(ExifInfo exifInfo) async {
+    await txn(() => db.exifInfos.put(exifInfo));
+    return exifInfo;
+  }
+
+  @override
+  Future<List<ExifInfo>> updateAll(List<ExifInfo> exifInfos) async {
+    await txn(() => db.exifInfos.putAll(exifInfos));
+    return exifInfos;
+  }
+}
diff --git a/mobile/lib/repositories/file_media.repository.dart b/mobile/lib/repositories/file_media.repository.dart
new file mode 100644
index 0000000000..15f7a51e15
--- /dev/null
+++ b/mobile/lib/repositories/file_media.repository.dart
@@ -0,0 +1,80 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/interfaces/file_media.interface.dart';
+import 'package:immich_mobile/repositories/asset_media.repository.dart';
+import 'package:photo_manager/photo_manager.dart' hide AssetType;
+
+final fileMediaRepositoryProvider = Provider((ref) => FileMediaRepository());
+
+class FileMediaRepository implements IFileMediaRepository {
+  @override
+  Future<Asset?> saveImage(
+    Uint8List data, {
+    required String title,
+    String? relativePath,
+  }) async {
+    final entity = await PhotoManager.editor.saveImage(
+      data,
+      filename: title,
+      title: title,
+      relativePath: relativePath,
+    );
+    return AssetMediaRepository.toAsset(entity);
+  }
+
+  @override
+  Future<Asset?> saveImageWithFile(
+    String filePath, {
+    String? title,
+    String? relativePath,
+  }) async {
+    final entity = await PhotoManager.editor.saveImageWithPath(
+      filePath,
+      title: title,
+      relativePath: relativePath,
+    );
+    return AssetMediaRepository.toAsset(entity);
+  }
+
+  @override
+  Future<Asset?> saveLivePhoto({
+    required File image,
+    required File video,
+    required String title,
+  }) async {
+    final entity = await PhotoManager.editor.darwin.saveLivePhoto(
+      imageFile: image,
+      videoFile: video,
+      title: title,
+    );
+    return AssetMediaRepository.toAsset(entity);
+  }
+
+  @override
+  Future<Asset?> saveVideo(
+    File file, {
+    required String title,
+    String? relativePath,
+  }) async {
+    final entity = await PhotoManager.editor.saveVideo(
+      file,
+      title: title,
+      relativePath: relativePath,
+    );
+    return AssetMediaRepository.toAsset(entity);
+  }
+
+  @override
+  Future<void> clearFileCache() => PhotoManager.clearFileCache();
+
+  @override
+  Future<void> enableBackgroundAccess() =>
+      PhotoManager.setIgnorePermissionCheck(true);
+
+  @override
+  Future<void> requestExtendedPermissions() =>
+      PhotoManager.requestPermissionExtend();
+}
diff --git a/mobile/lib/repositories/network.repository.dart b/mobile/lib/repositories/network.repository.dart
new file mode 100644
index 0000000000..54f527afb1
--- /dev/null
+++ b/mobile/lib/repositories/network.repository.dart
@@ -0,0 +1,37 @@
+import 'dart:io';
+
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/interfaces/network.interface.dart';
+import 'package:network_info_plus/network_info_plus.dart';
+
+final networkRepositoryProvider = Provider((_) {
+  final networkInfo = NetworkInfo();
+
+  return NetworkRepository(networkInfo);
+});
+
+class NetworkRepository implements INetworkRepository {
+  final NetworkInfo _networkInfo;
+
+  NetworkRepository(this._networkInfo);
+
+  @override
+  Future<String?> getWifiName() {
+    if (Platform.isAndroid) {
+      // remove quote around the return value on Android
+      // https://github.com/fluttercommunity/plus_plugins/tree/main/packages/network_info_plus/network_info_plus#android
+      return _networkInfo.getWifiName().then((value) {
+        if (value != null) {
+          return value.replaceAll(RegExp(r'"'), '');
+        }
+        return value;
+      });
+    }
+    return _networkInfo.getWifiName();
+  }
+
+  @override
+  Future<String?> getWifiIp() {
+    return _networkInfo.getWifiIP();
+  }
+}
diff --git a/mobile/lib/repositories/partner_api.repository.dart b/mobile/lib/repositories/partner_api.repository.dart
new file mode 100644
index 0000000000..1ae16d9d52
--- /dev/null
+++ b/mobile/lib/repositories/partner_api.repository.dart
@@ -0,0 +1,51 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/user.entity.dart';
+import 'package:immich_mobile/interfaces/partner_api.interface.dart';
+import 'package:immich_mobile/providers/api.provider.dart';
+import 'package:immich_mobile/repositories/api.repository.dart';
+import 'package:openapi/api.dart';
+
+final partnerApiRepositoryProvider = Provider(
+  (ref) => PartnerApiRepository(
+    ref.watch(apiServiceProvider).partnersApi,
+  ),
+);
+
+class PartnerApiRepository extends ApiRepository
+    implements IPartnerApiRepository {
+  final PartnersApi _api;
+
+  PartnerApiRepository(this._api);
+
+  @override
+  Future<List<User>> getAll(Direction direction) async {
+    final response = await checkNull(
+      _api.getPartners(
+        direction == Direction.sharedByMe
+            ? PartnerDirection.by
+            : PartnerDirection.with_,
+      ),
+    );
+    return response.map(User.fromPartnerDto).toList();
+  }
+
+  @override
+  Future<User> create(String id) async {
+    final dto = await checkNull(_api.createPartner(id));
+    return User.fromPartnerDto(dto);
+  }
+
+  @override
+  Future<void> delete(String id) => _api.removePartner(id);
+
+  @override
+  Future<User> update(String id, {required bool inTimeline}) async {
+    final dto = await checkNull(
+      _api.updatePartner(
+        id,
+        UpdatePartnerDto(inTimeline: inTimeline),
+      ),
+    );
+    return User.fromPartnerDto(dto);
+  }
+}
diff --git a/mobile/lib/repositories/permission.repository.dart b/mobile/lib/repositories/permission.repository.dart
new file mode 100644
index 0000000000..f825c36075
--- /dev/null
+++ b/mobile/lib/repositories/permission.repository.dart
@@ -0,0 +1,45 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:permission_handler/permission_handler.dart';
+
+final permissionRepositoryProvider = Provider((_) {
+  return PermissionRepository();
+});
+
+class PermissionRepository implements IPermissionRepository {
+  PermissionRepository();
+
+  @override
+  Future<bool> hasLocationWhenInUsePermission() {
+    return Permission.locationWhenInUse.isGranted;
+  }
+
+  @override
+  Future<bool> requestLocationWhenInUsePermission() async {
+    final result = await Permission.locationWhenInUse.request();
+    return result.isGranted;
+  }
+
+  @override
+  Future<bool> hasLocationAlwaysPermission() {
+    return Permission.locationAlways.isGranted;
+  }
+
+  @override
+  Future<bool> requestLocationAlwaysPermission() async {
+    final result = await Permission.locationAlways.request();
+    return result.isGranted;
+  }
+
+  @override
+  Future<bool> openSettings() {
+    return openAppSettings();
+  }
+}
+
+abstract interface class IPermissionRepository {
+  Future<bool> hasLocationWhenInUsePermission();
+  Future<bool> requestLocationWhenInUsePermission();
+  Future<bool> hasLocationAlwaysPermission();
+  Future<bool> requestLocationAlwaysPermission();
+  Future<bool> openSettings();
+}
diff --git a/mobile/lib/repositories/person_api.repository.dart b/mobile/lib/repositories/person_api.repository.dart
new file mode 100644
index 0000000000..d324a03edb
--- /dev/null
+++ b/mobile/lib/repositories/person_api.repository.dart
@@ -0,0 +1,38 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/interfaces/person_api.interface.dart';
+import 'package:immich_mobile/providers/api.provider.dart';
+import 'package:immich_mobile/repositories/api.repository.dart';
+import 'package:openapi/api.dart';
+
+final personApiRepositoryProvider = Provider(
+  (ref) => PersonApiRepository(ref.watch(apiServiceProvider).peopleApi),
+);
+
+class PersonApiRepository extends ApiRepository
+    implements IPersonApiRepository {
+  final PeopleApi _api;
+
+  PersonApiRepository(this._api);
+
+  @override
+  Future<List<Person>> getAll() async {
+    final dto = await checkNull(_api.getAllPeople());
+    return dto.people.map(_toPerson).toList();
+  }
+
+  @override
+  Future<Person> update(String id, {String? name}) async {
+    final dto = await checkNull(
+      _api.updatePerson(id, PersonUpdateDto(name: name)),
+    );
+    return _toPerson(dto);
+  }
+
+  static Person _toPerson(PersonResponseDto dto) => Person(
+        birthDate: dto.birthDate,
+        id: dto.id,
+        isHidden: dto.isHidden,
+        name: dto.name,
+        thumbnailPath: dto.thumbnailPath,
+      );
+}
diff --git a/mobile/lib/repositories/user.repository.dart b/mobile/lib/repositories/user.repository.dart
new file mode 100644
index 0000000000..fb4df84fe7
--- /dev/null
+++ b/mobile/lib/repositories/user.repository.dart
@@ -0,0 +1,63 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/entities/user.entity.dart';
+import 'package:immich_mobile/interfaces/user.interface.dart';
+import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/repositories/database.repository.dart';
+import 'package:isar/isar.dart';
+
+final userRepositoryProvider =
+    Provider((ref) => UserRepository(ref.watch(dbProvider)));
+
+class UserRepository extends DatabaseRepository implements IUserRepository {
+  UserRepository(super.db);
+
+  @override
+  Future<List<User>> getByIds(List<String> ids) async =>
+      (await db.users.getAllById(ids)).nonNulls.toList();
+
+  @override
+  Future<User?> get(String id) => db.users.getById(id);
+
+  @override
+  Future<List<User>> getAll({bool self = true, UserSort? sortBy}) {
+    final baseQuery = db.users.where();
+    final int userId = Store.get(StoreKey.currentUser).isarId;
+    final QueryBuilder<User, User, QAfterWhereClause> afterWhere =
+        self ? baseQuery.noOp() : baseQuery.isarIdNotEqualTo(userId);
+    final QueryBuilder<User, User, QAfterSortBy> query;
+    switch (sortBy) {
+      case null:
+        query = afterWhere.noOp();
+      case UserSort.id:
+        query = afterWhere.sortById();
+    }
+    return query.findAll();
+  }
+
+  @override
+  Future<User> update(User user) async {
+    await txn(() => db.users.put(user));
+    return user;
+  }
+
+  @override
+  Future<User> me() => Future.value(Store.get(StoreKey.currentUser));
+
+  @override
+  Future<void> deleteById(List<int> ids) => txn(() => db.users.deleteAll(ids));
+
+  @override
+  Future<List<User>> upsertAll(List<User> users) async {
+    await txn(() => db.users.putAll(users));
+    return users;
+  }
+
+  @override
+  Future<List<User>> getAllAccessible() => db.users
+      .filter()
+      .isPartnerSharedWithEqualTo(true)
+      .or()
+      .isarIdEqualTo(Store.get(StoreKey.currentUser).isarId)
+      .findAll();
+}
diff --git a/mobile/lib/repositories/user_api.repository.dart b/mobile/lib/repositories/user_api.repository.dart
new file mode 100644
index 0000000000..9641c4e0e6
--- /dev/null
+++ b/mobile/lib/repositories/user_api.repository.dart
@@ -0,0 +1,40 @@
+import 'dart:typed_data';
+
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:http/http.dart';
+import 'package:immich_mobile/entities/user.entity.dart';
+import 'package:immich_mobile/interfaces/user_api.interface.dart';
+import 'package:immich_mobile/providers/api.provider.dart';
+import 'package:immich_mobile/repositories/api.repository.dart';
+import 'package:openapi/api.dart';
+
+final userApiRepositoryProvider = Provider(
+  (ref) => UserApiRepository(
+    ref.watch(apiServiceProvider).usersApi,
+  ),
+);
+
+class UserApiRepository extends ApiRepository implements IUserApiRepository {
+  final UsersApi _api;
+
+  UserApiRepository(this._api);
+
+  @override
+  Future<List<User>> getAll() async {
+    final dto = await checkNull(_api.searchUsers());
+    return dto.map(User.fromSimpleUserDto).toList();
+  }
+
+  @override
+  Future<({String profileImagePath})> createProfileImage({
+    required String name,
+    required Uint8List data,
+  }) async {
+    final response = await checkNull(
+      _api.createProfileImage(
+        MultipartFile.fromBytes('file', data, filename: name),
+      ),
+    );
+    return (profileImagePath: response.profileImagePath);
+  }
+}
diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart
index 211c847726..5adfeb4061 100644
--- a/mobile/lib/routing/router.dart
+++ b/mobile/lib/routing/router.dart
@@ -13,12 +13,18 @@ import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
 import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
 import 'package:immich_mobile/pages/backup/backup_options.page.dart';
 import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
+import 'package:immich_mobile/pages/albums/albums.page.dart';
+import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
+import 'package:immich_mobile/pages/library/local_albums.page.dart';
+import 'package:immich_mobile/pages/library/people/people_collection.page.dart';
+import 'package:immich_mobile/pages/library/places/places_collection.page.dart';
+import 'package:immich_mobile/pages/library/library.page.dart';
 import 'package:immich_mobile/pages/common/activities.page.dart';
-import 'package:immich_mobile/pages/common/album_additional_shared_user_selection.page.dart';
-import 'package:immich_mobile/pages/common/album_asset_selection.page.dart';
-import 'package:immich_mobile/pages/common/album_options.page.dart';
-import 'package:immich_mobile/pages/common/album_shared_user_selection.page.dart';
-import 'package:immich_mobile/pages/common/album_viewer.page.dart';
+import 'package:immich_mobile/pages/album/album_additional_shared_user_selection.page.dart';
+import 'package:immich_mobile/pages/album/album_asset_selection.page.dart';
+import 'package:immich_mobile/pages/album/album_options.page.dart';
+import 'package:immich_mobile/pages/album/album_shared_user_selection.page.dart';
+import 'package:immich_mobile/pages/album/album_viewer.page.dart';
 import 'package:immich_mobile/pages/common/app_log.page.dart';
 import 'package:immich_mobile/pages/common/app_log_detail.page.dart';
 import 'package:immich_mobile/pages/common/create_album.page.dart';
@@ -29,9 +35,9 @@ import 'package:immich_mobile/pages/common/splash_screen.page.dart';
 import 'package:immich_mobile/pages/common/tab_controller.page.dart';
 import 'package:immich_mobile/pages/editing/edit.page.dart';
 import 'package:immich_mobile/pages/editing/crop.page.dart';
+import 'package:immich_mobile/pages/editing/filter.page.dart';
 import 'package:immich_mobile/pages/library/archive.page.dart';
 import 'package:immich_mobile/pages/library/favorite.page.dart';
-import 'package:immich_mobile/pages/library/library.page.dart';
 import 'package:immich_mobile/pages/library/trash.page.dart';
 import 'package:immich_mobile/pages/login/change_password.page.dart';
 import 'package:immich_mobile/pages/login/login.page.dart';
@@ -47,12 +53,10 @@ import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart';
 import 'package:immich_mobile/pages/search/person_result.page.dart';
 import 'package:immich_mobile/pages/search/recently_added.page.dart';
 import 'package:immich_mobile/pages/search/search.page.dart';
-import 'package:immich_mobile/pages/search/search_input.page.dart';
-import 'package:immich_mobile/pages/sharing/partner/partner.page.dart';
-import 'package:immich_mobile/pages/sharing/partner/partner_detail.page.dart';
-import 'package:immich_mobile/pages/sharing/shared_link/shared_link.page.dart';
-import 'package:immich_mobile/pages/sharing/shared_link/shared_link_edit.page.dart';
-import 'package:immich_mobile/pages/sharing/sharing.page.dart';
+import 'package:immich_mobile/pages/library/partner/partner.page.dart';
+import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart';
+import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart';
+import 'package:immich_mobile/pages/library/shared_link/shared_link_edit.page.dart';
 import 'package:immich_mobile/providers/api.provider.dart';
 import 'package:immich_mobile/providers/gallery_permission.provider.dart';
 import 'package:immich_mobile/routing/auth_guard.dart';
@@ -63,7 +67,6 @@ import 'package:immich_mobile/services/api.service.dart';
 import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
 import 'package:isar/isar.dart';
 import 'package:maplibre_gl/maplibre_gl.dart';
-import 'package:photo_manager/photo_manager.dart' hide LatLng;
 
 part 'router.gr.dart';
 
@@ -94,6 +97,11 @@ class AppRouter extends RootStackRouter {
     ),
     AutoRoute(page: LoginRoute.page, guards: [_duplicateGuard]),
     AutoRoute(page: ChangePasswordRoute.page),
+    AutoRoute(
+      page: SearchRoute.page,
+      guards: [_authGuard, _duplicateGuard],
+      maintainState: false,
+    ),
     CustomRoute(
       page: TabControllerRoute.page,
       guards: [_authGuard, _duplicateGuard],
@@ -105,15 +113,16 @@ class AppRouter extends RootStackRouter {
         AutoRoute(
           page: SearchRoute.page,
           guards: [_authGuard, _duplicateGuard],
-        ),
-        AutoRoute(
-          page: SharingRoute.page,
-          guards: [_authGuard, _duplicateGuard],
+          maintainState: false,
         ),
         AutoRoute(
           page: LibraryRoute.page,
           guards: [_authGuard, _duplicateGuard],
         ),
+        AutoRoute(
+          page: AlbumsRoute.page,
+          guards: [_authGuard, _duplicateGuard],
+        ),
       ],
       transitionsBuilder: TransitionsBuilders.fadeIn,
     ),
@@ -136,7 +145,12 @@ class AppRouter extends RootStackRouter {
     ),
     AutoRoute(page: EditImageRoute.page),
     AutoRoute(page: CropImageRoute.page),
-    AutoRoute(page: FavoritesRoute.page, guards: [_authGuard, _duplicateGuard]),
+    AutoRoute(page: FilterImageRoute.page),
+    CustomRoute(
+      page: FavoritesRoute.page,
+      guards: [_authGuard, _duplicateGuard],
+      transitionsBuilder: TransitionsBuilders.slideLeft,
+    ),
     AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]),
     AutoRoute(
       page: AllMotionPhotosRoute.page,
@@ -182,8 +196,16 @@ class AppRouter extends RootStackRouter {
     AutoRoute(page: SettingsSubRoute.page, guards: [_duplicateGuard]),
     AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]),
     AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]),
-    AutoRoute(page: ArchiveRoute.page, guards: [_authGuard, _duplicateGuard]),
-    AutoRoute(page: PartnerRoute.page, guards: [_authGuard, _duplicateGuard]),
+    CustomRoute(
+      page: ArchiveRoute.page,
+      guards: [_authGuard, _duplicateGuard],
+      transitionsBuilder: TransitionsBuilders.slideLeft,
+    ),
+    CustomRoute(
+      page: PartnerRoute.page,
+      guards: [_authGuard, _duplicateGuard],
+      transitionsBuilder: TransitionsBuilders.slideLeft,
+    ),
     AutoRoute(
       page: PartnerDetailRoute.page,
       guards: [_authGuard, _duplicateGuard],
@@ -199,10 +221,15 @@ class AppRouter extends RootStackRouter {
       page: AlbumOptionsRoute.page,
       guards: [_authGuard, _duplicateGuard],
     ),
-    AutoRoute(page: TrashRoute.page, guards: [_authGuard, _duplicateGuard]),
-    AutoRoute(
+    CustomRoute(
+      page: TrashRoute.page,
+      guards: [_authGuard, _duplicateGuard],
+      transitionsBuilder: TransitionsBuilders.slideLeft,
+    ),
+    CustomRoute(
       page: SharedLinkRoute.page,
       guards: [_authGuard, _duplicateGuard],
+      transitionsBuilder: TransitionsBuilders.slideLeft,
     ),
     AutoRoute(
       page: SharedLinkEditRoute.page,
@@ -222,15 +249,34 @@ class AppRouter extends RootStackRouter {
       page: BackupOptionsRoute.page,
       guards: [_authGuard, _duplicateGuard],
     ),
-    CustomRoute(
-      page: SearchInputRoute.page,
-      guards: [_authGuard, _duplicateGuard],
-      transitionsBuilder: TransitionsBuilders.noTransition,
-    ),
     AutoRoute(
       page: HeaderSettingsRoute.page,
       guards: [_duplicateGuard],
     ),
+    CustomRoute(
+      page: PeopleCollectionRoute.page,
+      guards: [_authGuard, _duplicateGuard],
+      transitionsBuilder: TransitionsBuilders.slideLeft,
+    ),
+    CustomRoute(
+      page: AlbumsRoute.page,
+      guards: [_authGuard, _duplicateGuard],
+      transitionsBuilder: TransitionsBuilders.slideLeft,
+    ),
+    CustomRoute(
+      page: LocalAlbumsRoute.page,
+      guards: [_authGuard, _duplicateGuard],
+      transitionsBuilder: TransitionsBuilders.slideLeft,
+    ),
+    CustomRoute(
+      page: PlacesCollectionRoute.page,
+      guards: [_authGuard, _duplicateGuard],
+      transitionsBuilder: TransitionsBuilders.slideLeft,
+    ),
+    AutoRoute(
+      page: NativeVideoViewerRoute.page,
+      guards: [_authGuard, _duplicateGuard],
+    ),
   ];
 }
 
diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart
index 90fc4cb0fe..3bd8966175 100644
--- a/mobile/lib/routing/router.gr.dart
+++ b/mobile/lib/routing/router.gr.dart
@@ -139,13 +139,11 @@ class AlbumAssetSelectionRouteArgs {
 class AlbumOptionsRoute extends PageRouteInfo<AlbumOptionsRouteArgs> {
   AlbumOptionsRoute({
     Key? key,
-    required Album album,
     List<PageRouteInfo>? children,
   }) : super(
           AlbumOptionsRoute.name,
           args: AlbumOptionsRouteArgs(
             key: key,
-            album: album,
           ),
           initialChildren: children,
         );
@@ -158,25 +156,19 @@ class AlbumOptionsRoute extends PageRouteInfo<AlbumOptionsRouteArgs> {
       final args = data.argsAs<AlbumOptionsRouteArgs>();
       return AlbumOptionsPage(
         key: args.key,
-        album: args.album,
       );
     },
   );
 }
 
 class AlbumOptionsRouteArgs {
-  const AlbumOptionsRouteArgs({
-    this.key,
-    required this.album,
-  });
+  const AlbumOptionsRouteArgs({this.key});
 
   final Key? key;
 
-  final Album album;
-
   @override
   String toString() {
-    return 'AlbumOptionsRouteArgs{key: $key, album: $album}';
+    return 'AlbumOptionsRouteArgs{key: $key}';
   }
 }
 
@@ -185,7 +177,7 @@ class AlbumOptionsRouteArgs {
 class AlbumPreviewRoute extends PageRouteInfo<AlbumPreviewRouteArgs> {
   AlbumPreviewRoute({
     Key? key,
-    required AssetPathEntity album,
+    required Album album,
     List<PageRouteInfo>? children,
   }) : super(
           AlbumPreviewRoute.name,
@@ -218,7 +210,7 @@ class AlbumPreviewRouteArgs {
 
   final Key? key;
 
-  final AssetPathEntity album;
+  final Album album;
 
   @override
   String toString() {
@@ -319,6 +311,25 @@ class AlbumViewerRouteArgs {
   }
 }
 
+/// generated route for
+/// [AlbumsPage]
+class AlbumsRoute extends PageRouteInfo<void> {
+  const AlbumsRoute({List<PageRouteInfo>? children})
+      : super(
+          AlbumsRoute.name,
+          initialChildren: children,
+        );
+
+  static const String name = 'AlbumsRoute';
+
+  static PageInfo page = PageInfo(
+    name,
+    builder: (data) {
+      return const AlbumsPage();
+    },
+  );
+}
+
 /// generated route for
 /// [AllMotionPhotosPage]
 class AllMotionPhotosRoute extends PageRouteInfo<void> {
@@ -560,15 +571,13 @@ class ChangePasswordRoute extends PageRouteInfo<void> {
 class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
   CreateAlbumRoute({
     Key? key,
-    required bool isSharedAlbum,
-    List<Asset>? initialAssets,
+    List<Asset>? assets,
     List<PageRouteInfo>? children,
   }) : super(
           CreateAlbumRoute.name,
           args: CreateAlbumRouteArgs(
             key: key,
-            isSharedAlbum: isSharedAlbum,
-            initialAssets: initialAssets,
+            assets: assets,
           ),
           initialChildren: children,
         );
@@ -578,11 +587,11 @@ class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
   static PageInfo page = PageInfo(
     name,
     builder: (data) {
-      final args = data.argsAs<CreateAlbumRouteArgs>();
+      final args = data.argsAs<CreateAlbumRouteArgs>(
+          orElse: () => const CreateAlbumRouteArgs());
       return CreateAlbumPage(
         key: args.key,
-        isSharedAlbum: args.isSharedAlbum,
-        initialAssets: args.initialAssets,
+        assets: args.assets,
       );
     },
   );
@@ -591,19 +600,16 @@ class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
 class CreateAlbumRouteArgs {
   const CreateAlbumRouteArgs({
     this.key,
-    required this.isSharedAlbum,
-    this.initialAssets,
+    this.assets,
   });
 
   final Key? key;
 
-  final bool isSharedAlbum;
-
-  final List<Asset>? initialAssets;
+  final List<Asset>? assets;
 
   @override
   String toString() {
-    return 'CreateAlbumRouteArgs{key: $key, isSharedAlbum: $isSharedAlbum, initialAssets: $initialAssets}';
+    return 'CreateAlbumRouteArgs{key: $key, assets: $assets}';
   }
 }
 
@@ -755,6 +761,58 @@ class FavoritesRoute extends PageRouteInfo<void> {
   );
 }
 
+/// generated route for
+/// [FilterImagePage]
+class FilterImageRoute extends PageRouteInfo<FilterImageRouteArgs> {
+  FilterImageRoute({
+    Key? key,
+    required Image image,
+    required Asset asset,
+    List<PageRouteInfo>? children,
+  }) : super(
+          FilterImageRoute.name,
+          args: FilterImageRouteArgs(
+            key: key,
+            image: image,
+            asset: asset,
+          ),
+          initialChildren: children,
+        );
+
+  static const String name = 'FilterImageRoute';
+
+  static PageInfo page = PageInfo(
+    name,
+    builder: (data) {
+      final args = data.argsAs<FilterImageRouteArgs>();
+      return FilterImagePage(
+        key: args.key,
+        image: args.image,
+        asset: args.asset,
+      );
+    },
+  );
+}
+
+class FilterImageRouteArgs {
+  const FilterImageRouteArgs({
+    this.key,
+    required this.image,
+    required this.asset,
+  });
+
+  final Key? key;
+
+  final Image image;
+
+  final Asset asset;
+
+  @override
+  String toString() {
+    return 'FilterImageRouteArgs{key: $key, image: $image, asset: $asset}';
+  }
+}
+
 /// generated route for
 /// [GalleryViewerPage]
 class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
@@ -857,6 +915,25 @@ class LibraryRoute extends PageRouteInfo<void> {
   );
 }
 
+/// generated route for
+/// [LocalAlbumsPage]
+class LocalAlbumsRoute extends PageRouteInfo<void> {
+  const LocalAlbumsRoute({List<PageRouteInfo>? children})
+      : super(
+          LocalAlbumsRoute.name,
+          initialChildren: children,
+        );
+
+  static const String name = 'LocalAlbumsRoute';
+
+  static PageInfo page = PageInfo(
+    name,
+    builder: (data) {
+      return const LocalAlbumsPage();
+    },
+  );
+}
+
 /// generated route for
 /// [LoginPage]
 class LoginRoute extends PageRouteInfo<void> {
@@ -994,6 +1071,64 @@ class MemoryRouteArgs {
   }
 }
 
+/// generated route for
+/// [NativeVideoViewerPage]
+class NativeVideoViewerRoute extends PageRouteInfo<NativeVideoViewerRouteArgs> {
+  NativeVideoViewerRoute({
+    Key? key,
+    required Asset asset,
+    required Widget image,
+    bool showControls = true,
+    List<PageRouteInfo>? children,
+  }) : super(
+          NativeVideoViewerRoute.name,
+          args: NativeVideoViewerRouteArgs(
+            key: key,
+            asset: asset,
+            image: image,
+            showControls: showControls,
+          ),
+          initialChildren: children,
+        );
+
+  static const String name = 'NativeVideoViewerRoute';
+
+  static PageInfo page = PageInfo(
+    name,
+    builder: (data) {
+      final args = data.argsAs<NativeVideoViewerRouteArgs>();
+      return NativeVideoViewerPage(
+        key: args.key,
+        asset: args.asset,
+        image: args.image,
+        showControls: args.showControls,
+      );
+    },
+  );
+}
+
+class NativeVideoViewerRouteArgs {
+  const NativeVideoViewerRouteArgs({
+    this.key,
+    required this.asset,
+    required this.image,
+    this.showControls = true,
+  });
+
+  final Key? key;
+
+  final Asset asset;
+
+  final Widget image;
+
+  final bool showControls;
+
+  @override
+  String toString() {
+    return 'NativeVideoViewerRouteArgs{key: $key, asset: $asset, image: $image, showControls: $showControls}';
+  }
+}
+
 /// generated route for
 /// [PartnerDetailPage]
 class PartnerDetailRoute extends PageRouteInfo<PartnerDetailRouteArgs> {
@@ -1059,6 +1194,25 @@ class PartnerRoute extends PageRouteInfo<void> {
   );
 }
 
+/// generated route for
+/// [PeopleCollectionPage]
+class PeopleCollectionRoute extends PageRouteInfo<void> {
+  const PeopleCollectionRoute({List<PageRouteInfo>? children})
+      : super(
+          PeopleCollectionRoute.name,
+          initialChildren: children,
+        );
+
+  static const String name = 'PeopleCollectionRoute';
+
+  static PageInfo page = PageInfo(
+    name,
+    builder: (data) {
+      return const PeopleCollectionPage();
+    },
+  );
+}
+
 /// generated route for
 /// [PermissionOnboardingPage]
 class PermissionOnboardingRoute extends PageRouteInfo<void> {
@@ -1149,6 +1303,25 @@ class PhotosRoute extends PageRouteInfo<void> {
   );
 }
 
+/// generated route for
+/// [PlacesCollectionPage]
+class PlacesCollectionRoute extends PageRouteInfo<void> {
+  const PlacesCollectionRoute({List<PageRouteInfo>? children})
+      : super(
+          PlacesCollectionRoute.name,
+          initialChildren: children,
+        );
+
+  static const String name = 'PlacesCollectionRoute';
+
+  static PageInfo page = PageInfo(
+    name,
+    builder: (data) {
+      return const PlacesCollectionPage();
+    },
+  );
+}
+
 /// generated route for
 /// [RecentlyAddedPage]
 class RecentlyAddedRoute extends PageRouteInfo<void> {
@@ -1169,29 +1342,29 @@ class RecentlyAddedRoute extends PageRouteInfo<void> {
 }
 
 /// generated route for
-/// [SearchInputPage]
-class SearchInputRoute extends PageRouteInfo<SearchInputRouteArgs> {
-  SearchInputRoute({
+/// [SearchPage]
+class SearchRoute extends PageRouteInfo<SearchRouteArgs> {
+  SearchRoute({
     Key? key,
     SearchFilter? prefilter,
     List<PageRouteInfo>? children,
   }) : super(
-          SearchInputRoute.name,
-          args: SearchInputRouteArgs(
+          SearchRoute.name,
+          args: SearchRouteArgs(
             key: key,
             prefilter: prefilter,
           ),
           initialChildren: children,
         );
 
-  static const String name = 'SearchInputRoute';
+  static const String name = 'SearchRoute';
 
   static PageInfo page = PageInfo(
     name,
     builder: (data) {
-      final args = data.argsAs<SearchInputRouteArgs>(
-          orElse: () => const SearchInputRouteArgs());
-      return SearchInputPage(
+      final args =
+          data.argsAs<SearchRouteArgs>(orElse: () => const SearchRouteArgs());
+      return SearchPage(
         key: args.key,
         prefilter: args.prefilter,
       );
@@ -1199,8 +1372,8 @@ class SearchInputRoute extends PageRouteInfo<SearchInputRouteArgs> {
   );
 }
 
-class SearchInputRouteArgs {
-  const SearchInputRouteArgs({
+class SearchRouteArgs {
+  const SearchRouteArgs({
     this.key,
     this.prefilter,
   });
@@ -1211,29 +1384,10 @@ class SearchInputRouteArgs {
 
   @override
   String toString() {
-    return 'SearchInputRouteArgs{key: $key, prefilter: $prefilter}';
+    return 'SearchRouteArgs{key: $key, prefilter: $prefilter}';
   }
 }
 
-/// generated route for
-/// [SearchPage]
-class SearchRoute extends PageRouteInfo<void> {
-  const SearchRoute({List<PageRouteInfo>? children})
-      : super(
-          SearchRoute.name,
-          initialChildren: children,
-        );
-
-  static const String name = 'SearchRoute';
-
-  static PageInfo page = PageInfo(
-    name,
-    builder: (data) {
-      return const SearchPage();
-    },
-  );
-}
-
 /// generated route for
 /// [SettingsPage]
 class SettingsRoute extends PageRouteInfo<void> {
@@ -1377,25 +1531,6 @@ class SharedLinkRoute extends PageRouteInfo<void> {
   );
 }
 
-/// generated route for
-/// [SharingPage]
-class SharingRoute extends PageRouteInfo<void> {
-  const SharingRoute({List<PageRouteInfo>? children})
-      : super(
-          SharingRoute.name,
-          initialChildren: children,
-        );
-
-  static const String name = 'SharingRoute';
-
-  static PageInfo page = PageInfo(
-    name,
-    builder: (data) {
-      return const SharingPage();
-    },
-  );
-}
-
 /// generated route for
 /// [SplashScreenPage]
 class SplashScreenRoute extends PageRouteInfo<void> {
diff --git a/mobile/lib/routing/tab_navigation_observer.dart b/mobile/lib/routing/tab_navigation_observer.dart
index e16fecb323..7d96b83d02 100644
--- a/mobile/lib/routing/tab_navigation_observer.dart
+++ b/mobile/lib/routing/tab_navigation_observer.dart
@@ -1,12 +1,8 @@
 import 'package:auto_route/auto_route.dart';
 import 'package:flutter/foundation.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/providers/album/album.provider.dart';
 import 'package:immich_mobile/providers/memory.provider.dart';
-import 'package:immich_mobile/providers/search/people.provider.dart';
 
-import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
-import 'package:immich_mobile/providers/album/shared_album.provider.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
 import 'package:immich_mobile/entities/user.entity.dart';
 import 'package:immich_mobile/providers/api.provider.dart';
@@ -21,35 +17,11 @@ class TabNavigationObserver extends AutoRouterObserver {
     required this.ref,
   });
 
-  @override
-  void didInitTabRoute(TabPageRoute route, TabPageRoute? previousRoute) {
-    // Perform tasks on first navigation to SearchRoute
-    if (route.name == 'SearchRoute') {
-      // ref.refresh(getCuratedLocationProvider);
-    }
-  }
-
   @override
   Future<void> didChangeTabRoute(
     TabPageRoute route,
     TabPageRoute previousRoute,
   ) async {
-    // Perform tasks on re-visit to SearchRoute
-    if (route.name == 'SearchRoute') {
-      // Refresh Location State
-      ref.invalidate(getPreviewPlacesProvider);
-      ref.invalidate(getAllPeopleProvider);
-    }
-
-    if (route.name == 'SharingRoute') {
-      ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
-      Future(() => ref.read(assetProvider.notifier).getAllAsset());
-    }
-
-    if (route.name == 'LibraryRoute') {
-      ref.read(albumProvider.notifier).getAllAlbums();
-    }
-
     if (route.name == 'HomeRoute') {
       ref.invalidate(memoryFutureProvider);
       Future(() => ref.read(assetProvider.notifier).getAllAsset());
diff --git a/mobile/lib/services/activity.service.dart b/mobile/lib/services/activity.service.dart
index 58af26e204..5496041416 100644
--- a/mobile/lib/services/activity.service.dart
+++ b/mobile/lib/services/activity.service.dart
@@ -1,41 +1,31 @@
-import 'package:immich_mobile/constants/errors.dart';
+import 'package:immich_mobile/interfaces/activity_api.interface.dart';
 import 'package:immich_mobile/mixins/error_logger.mixin.dart';
 import 'package:immich_mobile/models/activities/activity.model.dart';
-import 'package:immich_mobile/services/api.service.dart';
 import 'package:logging/logging.dart';
-import 'package:openapi/api.dart';
 
 class ActivityService with ErrorLoggerMixin {
-  final ApiService _apiService;
+  final IActivityApiRepository _activityApiRepository;
 
   @override
   final Logger logger = Logger("ActivityService");
 
-  ActivityService(this._apiService);
+  ActivityService(this._activityApiRepository);
 
   Future<List<Activity>> getAllActivities(
     String albumId, {
     String? assetId,
   }) async {
     return logError(
-      () async {
-        final list = await _apiService.activitiesApi
-            .getActivities(albumId, assetId: assetId);
-        return list != null ? list.map(Activity.fromDto).toList() : [];
-      },
+      () => _activityApiRepository.getAll(albumId, assetId: assetId),
       defaultValue: [],
       errorMessage: "Failed to get all activities for album $albumId",
     );
   }
 
-  Future<int> getStatistics(String albumId, {String? assetId}) async {
+  Future<ActivityStats> getStatistics(String albumId, {String? assetId}) async {
     return logError(
-      () async {
-        final dto = await _apiService.activitiesApi
-            .getActivityStatistics(albumId, assetId: assetId);
-        return dto?.comments ?? 0;
-      },
-      defaultValue: 0,
+      () => _activityApiRepository.getStats(albumId, assetId: assetId),
+      defaultValue: const ActivityStats(comments: 0),
       errorMessage: "Failed to statistics for album $albumId",
     );
   }
@@ -43,7 +33,7 @@ class ActivityService with ErrorLoggerMixin {
   Future<bool> removeActivity(String id) async {
     return logError(
       () async {
-        await _apiService.activitiesApi.deleteActivity(id);
+        await _activityApiRepository.delete(id);
         return true;
       },
       defaultValue: false,
@@ -58,22 +48,12 @@ class ActivityService with ErrorLoggerMixin {
     String? comment,
   }) async {
     return guardError(
-      () async {
-        final dto = await _apiService.activitiesApi.createActivity(
-          ActivityCreateDto(
-            albumId: albumId,
-            type: type == ActivityType.comment
-                ? ReactionType.comment
-                : ReactionType.like,
-            assetId: assetId,
-            comment: comment,
-          ),
-        );
-        if (dto != null) {
-          return Activity.fromDto(dto);
-        }
-        throw NoResponseDtoError();
-      },
+      () => _activityApiRepository.create(
+        albumId,
+        type,
+        assetId: assetId,
+        comment: comment,
+      ),
       errorMessage: "Failed to create $type for album $albumId",
     );
   }
diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart
index ef56f9bf6c..5f013c0e53 100644
--- a/mobile/lib/services/album.service.dart
+++ b/mobile/lib/services/album.service.dart
@@ -5,54 +5,66 @@ import 'dart:io';
 import 'package:collection/collection.dart';
 import 'package:flutter/foundation.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/constants/enums.dart';
+import 'package:immich_mobile/interfaces/album.interface.dart';
+import 'package:immich_mobile/interfaces/album_api.interface.dart';
+import 'package:immich_mobile/interfaces/album_media.interface.dart';
+import 'package:immich_mobile/interfaces/asset.interface.dart';
+import 'package:immich_mobile/interfaces/backup.interface.dart';
 import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart';
 import 'package:immich_mobile/entities/backup_album.entity.dart';
 import 'package:immich_mobile/entities/album.entity.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
 import 'package:immich_mobile/entities/user.entity.dart';
-import 'package:immich_mobile/providers/api.provider.dart';
-import 'package:immich_mobile/providers/db.provider.dart';
-import 'package:immich_mobile/services/api.service.dart';
+import 'package:immich_mobile/models/albums/album_search.model.dart';
+import 'package:immich_mobile/repositories/album.repository.dart';
+import 'package:immich_mobile/repositories/album_api.repository.dart';
+import 'package:immich_mobile/repositories/asset.repository.dart';
+import 'package:immich_mobile/repositories/backup.repository.dart';
+import 'package:immich_mobile/repositories/album_media.repository.dart';
+import 'package:immich_mobile/services/entity.service.dart';
 import 'package:immich_mobile/services/sync.service.dart';
 import 'package:immich_mobile/services/user.service.dart';
-import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
-import 'package:openapi/api.dart';
-import 'package:photo_manager/photo_manager.dart';
 
 final albumServiceProvider = Provider(
   (ref) => AlbumService(
-    ref.watch(apiServiceProvider),
     ref.watch(userServiceProvider),
     ref.watch(syncServiceProvider),
-    ref.watch(dbProvider),
+    ref.watch(entityServiceProvider),
+    ref.watch(albumRepositoryProvider),
+    ref.watch(assetRepositoryProvider),
+    ref.watch(backupRepositoryProvider),
+    ref.watch(albumMediaRepositoryProvider),
+    ref.watch(albumApiRepositoryProvider),
   ),
 );
 
 class AlbumService {
-  final ApiService _apiService;
   final UserService _userService;
   final SyncService _syncService;
-  final Isar _db;
+  final EntityService _entityService;
+  final IAlbumRepository _albumRepository;
+  final IAssetRepository _assetRepository;
+  final IBackupRepository _backupAlbumRepository;
+  final IAlbumMediaRepository _albumMediaRepository;
+  final IAlbumApiRepository _albumApiRepository;
   final Logger _log = Logger('AlbumService');
   Completer<bool> _localCompleter = Completer()..complete(false);
   Completer<bool> _remoteCompleter = Completer()..complete(false);
 
   AlbumService(
-    this._apiService,
     this._userService,
     this._syncService,
-    this._db,
+    this._entityService,
+    this._albumRepository,
+    this._assetRepository,
+    this._backupAlbumRepository,
+    this._albumMediaRepository,
+    this._albumApiRepository,
   );
 
-  QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
-      selectedAlbumsQuery() =>
-          _db.backupAlbums.filter().selectionEqualTo(BackupSelection.select);
-  QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
-      excludedAlbumsQuery() =>
-          _db.backupAlbums.filter().selectionEqualTo(BackupSelection.exclude);
-
   /// Checks all selected device albums for changes of albums and their assets
   /// Updates the local database and returns `true` if there were any changes
   Future<bool> refreshDeviceAlbums() async {
@@ -65,23 +77,23 @@ class AlbumService {
     final Stopwatch sw = Stopwatch()..start();
     bool changes = false;
     try {
-      final List<String> excludedIds =
-          await excludedAlbumsQuery().idProperty().findAll();
-      final List<String> selectedIds =
-          await selectedAlbumsQuery().idProperty().findAll();
+      final (selectedIds, excludedIds, onDevice) = await (
+        _backupAlbumRepository
+            .getIdsBySelection(BackupSelection.select)
+            .then((value) => value.toSet()),
+        _backupAlbumRepository
+            .getIdsBySelection(BackupSelection.exclude)
+            .then((value) => value.toSet()),
+        _albumMediaRepository.getAll()
+      ).wait;
+      _log.info("Found ${onDevice.length} device albums");
       if (selectedIds.isEmpty) {
-        final numLocal = await _db.albums.where().localIdIsNotNull().count();
+        final numLocal = await _albumRepository.count(local: true);
         if (numLocal > 0) {
           _syncService.removeAllLocalAlbumsAndAssets();
         }
         return false;
       }
-      final List<AssetPathEntity> onDevice =
-          await PhotoManager.getAssetPathList(
-        hasAll: true,
-        filterOption: FilterOptionGroup(containsPathModified: true),
-      );
-      _log.info("Found ${onDevice.length} device albums");
       Set<String>? excludedAssets;
       if (excludedIds.isNotEmpty) {
         if (Platform.isIOS) {
@@ -96,25 +108,24 @@ class AlbumService {
           _log.info("Found ${excludedAssets.length} assets to exclude");
         }
         // remove all excluded albums
-        onDevice.removeWhere((e) => excludedIds.contains(e.id));
+        onDevice.removeWhere((e) => excludedIds.contains(e.localId));
         _log.info(
           "Ignoring ${excludedIds.length} excluded albums resulting in ${onDevice.length} device albums",
         );
       }
-      final hasAll = selectedIds
-          .map((id) => onDevice.firstWhereOrNull((a) => a.id == id))
-          .whereNotNull()
-          .any((a) => a.isAll);
+
+      final allAlbum = onDevice.firstWhereOrNull((album) => album.isAll);
+      final hasAll = allAlbum != null && selectedIds.contains(allAlbum.localId);
       if (hasAll) {
         if (Platform.isAndroid) {
           // remove the virtual "Recent" album and keep and individual albums
           // on Android, the virtual "Recent" `lastModified` value is always null
-          onDevice.removeWhere((e) => e.isAll);
+          onDevice.removeWhere((album) => album.isAll);
           _log.info("'Recents' is selected, keeping all individual albums");
         }
       } else {
         // keep only the explicitly selected albums
-        onDevice.removeWhere((e) => !selectedIds.contains(e.id));
+        onDevice.removeWhere((album) => !selectedIds.contains(album.localId));
         _log.info("'Recents' is not selected, keeping only selected albums");
       }
       changes =
@@ -128,23 +139,27 @@ class AlbumService {
   }
 
   Future<Set<String>> _loadExcludedAssetIds(
-    List<AssetPathEntity> albums,
-    List<String> excludedAlbumIds,
+    List<Album> albums,
+    Set<String> excludedAlbumIds,
   ) async {
     final Set<String> result = HashSet<String>();
-    for (AssetPathEntity a in albums) {
-      if (excludedAlbumIds.contains(a.id)) {
-        final List<AssetEntity> assets =
-            await a.getAssetListRange(start: 0, end: 0x7fffffffffffffff);
-        result.addAll(assets.map((e) => e.id));
-      }
+    for (final batchAlbums in albums
+        .where((album) => excludedAlbumIds.contains(album.localId))
+        .slices(5)) {
+      await batchAlbums
+          .map(
+            (album) => _albumMediaRepository
+                .getAssetIds(album.localId!)
+                .then((assetIds) => result.addAll(assetIds)),
+          )
+          .wait;
     }
     return result;
   }
 
   /// Checks remote albums (owned if `isShared` is false) for changes,
   /// updates the local database and returns `true` if there were any changes
-  Future<bool> refreshRemoteAlbums({required bool isShared}) async {
+  Future<bool> refreshRemoteAlbums() async {
     if (!_remoteCompleter.isCompleted) {
       // guard against concurrent calls
       return _remoteCompleter.future;
@@ -154,18 +169,20 @@ class AlbumService {
     bool changes = false;
     try {
       await _userService.refreshUsers();
-      final List<AlbumResponseDto>? serverAlbums = await _apiService.albumsApi
-          .getAllAlbums(shared: isShared ? true : null);
-      if (serverAlbums == null) {
-        return false;
-      }
-      changes = await _syncService.syncRemoteAlbumsToDb(
-        serverAlbums,
-        isShared: isShared,
-        loadDetails: (dto) async => dto.assetCount == dto.assets.length
-            ? dto
-            : (await _apiService.albumsApi.getAlbumInfo(dto.id)) ?? dto,
+      final (sharedAlbum, ownedAlbum) = await (
+        _albumApiRepository.getAll(shared: true),
+        _albumApiRepository.getAll(shared: null)
+      ).wait;
+
+      final albums = HashSet<Album>(
+        equals: (a, b) => a.remoteId == b.remoteId,
+        hashCode: (a) => a.remoteId.hashCode,
       );
+
+      albums.addAll(sharedAlbum);
+      albums.addAll(ownedAlbum);
+
+      changes = await _syncService.syncRemoteAlbumsToDb(albums.toList());
     } finally {
       _remoteCompleter.complete(changes);
     }
@@ -178,30 +195,13 @@ class AlbumService {
     Iterable<Asset> assets, [
     Iterable<User> sharedUsers = const [],
   ]) async {
-    try {
-      AlbumResponseDto? remote = await _apiService.albumsApi.createAlbum(
-        CreateAlbumDto(
-          albumName: albumName,
-          assetIds: assets.map((asset) => asset.remoteId!).toList(),
-          albumUsers: sharedUsers
-              .map(
-                (e) => AlbumUserCreateDto(
-                  userId: e.id,
-                  role: AlbumUserRole.editor,
-                ),
-              )
-              .toList(),
-        ),
-      );
-      if (remote != null) {
-        Album album = await Album.remote(remote);
-        await _db.writeTxn(() => _db.albums.store(album));
-        return album;
-      }
-    } catch (e) {
-      debugPrint("Error createSharedAlbum  ${e.toString()}");
-    }
-    return null;
+    final Album album = await _albumApiRepository.create(
+      albumName,
+      assetIds: assets.map((asset) => asset.remoteId!),
+      sharedUserIds: sharedUsers.map((user) => user.id),
+    );
+    await _entityService.fillAlbumWithDatabaseEntities(album);
+    return _albumRepository.create(album);
   }
 
   /*
@@ -212,8 +212,7 @@ class AlbumService {
     for (int round = 0;; round++) {
       final proposedName = "$baseName${round == 0 ? "" : " ($round)"}";
 
-      if (null ==
-          await _db.albums.filter().nameEqualTo(proposedName).findFirst()) {
+      if (null == await _albumRepository.getByName(proposedName)) {
         return proposedName;
       }
     }
@@ -229,101 +228,55 @@ class AlbumService {
     );
   }
 
-  Future<AlbumAddAssetsResponse?> addAdditionalAssetToAlbum(
-    Iterable<Asset> assets,
+  Future<AlbumAddAssetsResponse?> addAssets(
     Album album,
+    Iterable<Asset> assets,
   ) async {
     try {
-      var response = await _apiService.albumsApi.addAssetsToAlbum(
+      final result = await _albumApiRepository.addAssets(
         album.remoteId!,
-        BulkIdsDto(ids: assets.map((asset) => asset.remoteId!).toList()),
+        assets.map((asset) => asset.remoteId!),
       );
 
-      if (response != null) {
-        List<Asset> successAssets = [];
-        List<String> duplicatedAssets = [];
+      final List<Asset> addedAssets = result.added
+          .map((id) => assets.firstWhere((asset) => asset.remoteId == id))
+          .toList();
 
-        for (final result in response) {
-          if (result.success) {
-            successAssets
-                .add(assets.firstWhere((asset) => asset.remoteId == result.id));
-          } else if (!result.success &&
-              result.error == BulkIdResponseDtoErrorEnum.duplicate) {
-            duplicatedAssets.add(result.id);
-          }
-        }
+      await _updateAssets(album.id, add: addedAssets);
 
-        await _updateAssets(album.id, add: successAssets);
-
-        return AlbumAddAssetsResponse(
-          alreadyInAlbum: duplicatedAssets,
-          successfullyAdded: successAssets.length,
-        );
-      }
+      return AlbumAddAssetsResponse(
+        alreadyInAlbum: result.duplicates,
+        successfullyAdded: addedAssets.length,
+      );
     } catch (e) {
-      debugPrint("Error addAdditionalAssetToAlbum  ${e.toString()}");
+      debugPrint("Error addAssets  ${e.toString()}");
     }
     return null;
   }
 
   Future<void> _updateAssets(
     int albumId, {
-    Iterable<Asset> add = const [],
-    Iterable<Asset> remove = const [],
-  }) {
-    return _db.writeTxn(() async {
-      final album = await _db.albums.get(albumId);
-      if (album == null) return;
-      await album.assets.update(link: add, unlink: remove);
-      album.startDate =
-          await album.assets.filter().fileCreatedAtProperty().min();
-      album.endDate = await album.assets.filter().fileCreatedAtProperty().max();
-      album.lastModifiedAssetTimestamp =
-          await album.assets.filter().updatedAtProperty().max();
-      await _db.albums.put(album);
-    });
-  }
+    List<Asset> add = const [],
+    List<Asset> remove = const [],
+  }) =>
+      _albumRepository.transaction(() async {
+        final album = await _albumRepository.get(albumId);
+        if (album == null) return;
+        await _albumRepository.addAssets(album, add);
+        await _albumRepository.removeAssets(album, remove);
+        await _albumRepository.recalculateMetadata(album);
+        await _albumRepository.update(album);
+      });
 
-  Future<bool> addAdditionalUserToAlbum(
-    List<String> sharedUserIds,
-    Album album,
-  ) async {
+  Future<bool> setActivityStatus(Album album, bool enabled) async {
     try {
-      final List<AlbumUserAddDto> albumUsers = sharedUserIds
-          .map((userId) => AlbumUserAddDto(userId: userId))
-          .toList();
-
-      final result = await _apiService.albumsApi.addUsersToAlbum(
+      final updatedAlbum = await _albumApiRepository.update(
         album.remoteId!,
-        AddUsersDto(albumUsers: albumUsers),
+        activityEnabled: enabled,
       );
-      if (result != null) {
-        album.sharedUsers
-            .addAll((await _db.users.getAllById(sharedUserIds)).cast());
-        album.shared = result.shared;
-        await _db.writeTxn(() async {
-          await _db.albums.put(album);
-          await album.sharedUsers.save();
-        });
-        return true;
-      }
-    } catch (e) {
-      debugPrint("Error addAdditionalUserToAlbum  ${e.toString()}");
-    }
-    return false;
-  }
-
-  Future<bool> setActivityEnabled(Album album, bool enabled) async {
-    try {
-      final result = await _apiService.albumsApi.updateAlbumInfo(
-        album.remoteId!,
-        UpdateAlbumDto(isActivityEnabled: enabled),
-      );
-      if (result != null) {
-        album.activityEnabled = enabled;
-        await _db.writeTxn(() => _db.albums.put(album));
-        return true;
-      }
+      album.activityEnabled = updatedAlbum.activityEnabled;
+      await _albumRepository.update(album);
+      return true;
     } catch (e) {
       debugPrint("Error setActivityEnabled  ${e.toString()}");
     }
@@ -334,27 +287,27 @@ class AlbumService {
     try {
       final userId = Store.get(StoreKey.currentUser).isarId;
       if (album.owner.value?.isarId == userId) {
-        await _apiService.albumsApi.deleteAlbum(album.remoteId!);
+        await _albumApiRepository.delete(album.remoteId!);
       }
       if (album.shared) {
         final foreignAssets =
-            await album.assets.filter().not().ownerIdEqualTo(userId).findAll();
-        await _db.writeTxn(() => _db.albums.delete(album.id));
-        final List<Album> albums =
-            await _db.albums.filter().sharedEqualTo(true).findAll();
+            await _assetRepository.getByAlbum(album, notOwnedBy: [userId]);
+        await _albumRepository.delete(album.id);
+
+        final List<Album> albums = await _albumRepository.getAll(shared: true);
         final List<Asset> existing = [];
-        for (Album a in albums) {
+        for (Album album in albums) {
           existing.addAll(
-            await a.assets.filter().not().ownerIdEqualTo(userId).findAll(),
+            await _assetRepository.getByAlbum(album, notOwnedBy: [userId]),
           );
         }
         final List<int> idsToRemove =
             _syncService.sharedAssetsToRemove(foreignAssets, existing);
         if (idsToRemove.isNotEmpty) {
-          await _db.writeTxn(() => _db.assets.deleteAll(idsToRemove));
+          await _assetRepository.deleteById(idsToRemove);
         }
       } else {
-        await _db.writeTxn(() => _db.albums.delete(album.id));
+        await _albumRepository.delete(album.id);
       }
       return true;
     } catch (e) {
@@ -365,7 +318,7 @@ class AlbumService {
 
   Future<bool> leaveAlbum(Album album) async {
     try {
-      await _apiService.albumsApi.removeUserFromAlbum(album.remoteId!, "me");
+      await _albumApiRepository.removeUser(album.remoteId!, userId: "me");
       return true;
     } catch (e) {
       debugPrint("Error leaveAlbum ${e.toString()}");
@@ -373,71 +326,81 @@ class AlbumService {
     }
   }
 
-  Future<bool> removeAssetFromAlbum(
+  Future<bool> removeAsset(
     Album album,
     Iterable<Asset> assets,
   ) async {
     try {
-      final response = await _apiService.albumsApi.removeAssetFromAlbum(
+      final result = await _albumApiRepository.removeAssets(
         album.remoteId!,
-        BulkIdsDto(
-          ids: assets.map((asset) => asset.remoteId!).toList(),
-        ),
+        assets.map((asset) => asset.remoteId!),
       );
-      if (response != null) {
-        final toRemove = response.every((e) => e.success)
-            ? assets
-            : response
-                .where((e) => e.success)
-                .map((e) => assets.firstWhere((a) => a.remoteId == e.id));
-        await _updateAssets(album.id, remove: toRemove);
-        return true;
-      }
+      final toRemove = result.removed
+          .map((id) => assets.firstWhere((asset) => asset.remoteId == id));
+      await _updateAssets(album.id, remove: toRemove.toList());
+      return true;
     } catch (e) {
       debugPrint("Error removeAssetFromAlbum ${e.toString()}");
     }
     return false;
   }
 
-  Future<bool> removeUserFromAlbum(
+  Future<bool> removeUser(
     Album album,
     User user,
   ) async {
     try {
-      await _apiService.albumsApi.removeUserFromAlbum(
+      await _albumApiRepository.removeUser(
         album.remoteId!,
-        user.id,
+        userId: user.id,
       );
 
       album.sharedUsers.remove(user);
-      await _db.writeTxn(() async {
-        await album.sharedUsers.update(unlink: [user]);
-        final a = await _db.albums.get(album.id);
-        // trigger watcher
-        await _db.albums.put(a!);
-      });
+      await _albumRepository.removeUsers(album, [user]);
+      final a = await _albumRepository.get(album.id);
+      // trigger watcher
+      await _albumRepository.update(a!);
 
       return true;
-    } catch (e) {
-      debugPrint("Error removeUserFromAlbum  ${e.toString()}");
+    } catch (error) {
+      debugPrint("Error removeUser  ${error.toString()}");
       return false;
     }
   }
 
+  Future<bool> addUsers(
+    Album album,
+    List<String> userIds,
+  ) async {
+    try {
+      final updatedAlbum =
+          await _albumApiRepository.addUsers(album.remoteId!, userIds);
+
+      album.sharedUsers.addAll(updatedAlbum.remoteUsers);
+      album.shared = true;
+
+      await _albumRepository.addUsers(album, album.sharedUsers.toList());
+      await _albumRepository.update(album);
+
+      return true;
+    } catch (error) {
+      debugPrint("Error addUsers ${error.toString()}");
+    }
+    return false;
+  }
+
   Future<bool> changeTitleAlbum(
     Album album,
     String newAlbumTitle,
   ) async {
     try {
-      await _apiService.albumsApi.updateAlbumInfo(
+      final updatedAlbum = await _albumApiRepository.update(
         album.remoteId!,
-        UpdateAlbumDto(
-          albumName: newAlbumTitle,
-        ),
+        name: newAlbumTitle,
       );
-      album.name = newAlbumTitle;
-      await _db.writeTxn(() => _db.albums.put(album));
 
+      album.name = updatedAlbum.name;
+      await _albumRepository.update(album);
       return true;
     } catch (e) {
       debugPrint("Error changeTitleAlbum  ${e.toString()}");
@@ -445,14 +408,8 @@ class AlbumService {
     }
   }
 
-  Future<Album?> getAlbumByName(String name, bool remoteOnly) async {
-    return _db.albums
-        .filter()
-        .optional(remoteOnly, (q) => q.localIdIsNull())
-        .nameEqualTo(name)
-        .sharedEqualTo(false)
-        .findFirst();
-  }
+  Future<Album?> getAlbumByName(String name, bool remoteOnly) =>
+      _albumRepository.getByName(name, remote: remoteOnly ? true : null);
 
   ///
   /// Add the uploaded asset to the selected albums
@@ -464,13 +421,33 @@ class AlbumService {
     for (final albumName in albumNames) {
       Album? album = await getAlbumByName(albumName, true);
       album ??= await createAlbum(albumName, []);
-
       if (album != null && album.remoteId != null) {
-        await _apiService.albumsApi.addAssetsToAlbum(
-          album.remoteId!,
-          BulkIdsDto(ids: assetIds),
-        );
+        await _albumApiRepository.addAssets(album.remoteId!, assetIds);
       }
     }
   }
+
+  Future<List<Album>> getAll() async {
+    return _albumRepository.getAll(remote: true);
+  }
+
+  Future<List<Album>> search(
+    String searchTerm,
+    QuickFilterMode filterMode,
+  ) async {
+    return _albumRepository.search(searchTerm, filterMode);
+  }
+
+  Future<Album?> updateSortOrder(Album album, SortOrder order) async {
+    try {
+      final updateAlbum =
+          await _albumApiRepository.update(album.remoteId!, sortOrder: order);
+      album.sortOrder = updateAlbum.sortOrder;
+
+      return _albumRepository.update(album);
+    } catch (error, stackTrace) {
+      _log.severe("Error updating album sort order", error, stackTrace);
+    }
+    return null;
+  }
 }
diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart
index 4a3cfb19a2..0f6fe8a100 100644
--- a/mobile/lib/services/api.service.dart
+++ b/mobile/lib/services/api.service.dart
@@ -2,6 +2,7 @@ import 'dart:async';
 import 'dart:convert';
 import 'dart:io';
 
+import 'package:device_info_plus/device_info_plus.dart';
 import 'package:flutter/material.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
 import 'package:immich_mobile/utils/url_helper.dart';
@@ -66,10 +67,10 @@ class ApiService implements Authentication {
   }
 
   Future<String> resolveAndSetEndpoint(String serverUrl) async {
-    final endpoint = await _resolveEndpoint(serverUrl);
+    final endpoint = await resolveEndpoint(serverUrl);
     setEndpoint(endpoint);
 
-    // Save in hivebox for next startup
+    // Save in local database for next startup
     Store.put(StoreKey.serverEndpoint, endpoint);
     return endpoint;
   }
@@ -81,7 +82,7 @@ class ApiService implements Authentication {
   ///  host   - required
   ///  port   - optional (default: based on schema)
   ///  path   - optional
-  Future<String> _resolveEndpoint(String serverUrl) async {
+  Future<String> resolveEndpoint(String serverUrl) async {
     final url = sanitizeUrl(serverUrl);
 
     if (!await _isEndpointAvailable(serverUrl)) {
@@ -97,27 +98,13 @@ class ApiService implements Authentication {
   }
 
   Future<bool> _isEndpointAvailable(String serverUrl) async {
-    final Client client = Client();
-
     if (!serverUrl.endsWith('/api')) {
       serverUrl += '/api';
     }
 
     try {
-      final response = await client
-          .get(
-            Uri.parse("$serverUrl/server-info/ping"),
-            headers: getRequestHeaders(),
-          )
-          .timeout(const Duration(seconds: 5));
-
-      _log.info("Pinging server with response code ${response.statusCode}");
-      if (response.statusCode != 200) {
-        _log.severe(
-          "Server Gateway Error: ${response.body} - Cannot communicate to the server",
-        );
-        return false;
-      }
+      await setEndpoint(serverUrl);
+      await serverInfoApi.pingServer().timeout(const Duration(seconds: 5));
     } on TimeoutException catch (_) {
       return false;
     } on SocketException catch (_) {
@@ -162,11 +149,27 @@ class ApiService implements Authentication {
     return "";
   }
 
-  setAccessToken(String accessToken) {
+  void setAccessToken(String accessToken) {
     _accessToken = accessToken;
     Store.put(StoreKey.accessToken, accessToken);
   }
 
+  Future<void> setDeviceInfoHeader() async {
+    DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
+
+    if (Platform.isIOS) {
+      final iosInfo = await deviceInfoPlugin.iosInfo;
+      authenticationApi.apiClient
+          .addDefaultHeader('deviceModel', iosInfo.utsname.machine);
+      authenticationApi.apiClient.addDefaultHeader('deviceType', 'iOS');
+    } else {
+      final androidInfo = await deviceInfoPlugin.androidInfo;
+      authenticationApi.apiClient
+          .addDefaultHeader('deviceModel', androidInfo.model);
+      authenticationApi.apiClient.addDefaultHeader('deviceType', 'Android');
+    }
+  }
+
   static Map<String, String> getRequestHeaders() {
     var accessToken = Store.get(StoreKey.accessToken, "");
     var customHeadersStr = Store.get(StoreKey.customHeaders, "");
diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart
index 8f773e1bb3..c3fde894d5 100644
--- a/mobile/lib/services/app_settings.service.dart
+++ b/mobile/lib/services/app_settings.service.dart
@@ -1,4 +1,4 @@
-import 'package:immich_mobile/constants/immich_colors.dart';
+import 'package:immich_mobile/constants/colors.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
 
 enum AppSettingsEnum<T> {
@@ -77,6 +77,7 @@ enum AppSettingsEnum<T> {
   ),
   enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true),
   syncAlbums<bool>(StoreKey.syncAlbums, null, false),
+  autoEndpointSwitching<bool>(StoreKey.autoEndpointSwitching, null, false),
   ;
 
   const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart
index c4f258e259..7d27d1b27b 100644
--- a/mobile/lib/services/asset.service.dart
+++ b/mobile/lib/services/asset.service.dart
@@ -1,66 +1,86 @@
-// ignore_for_file: null_argument_to_non_null_type
-
 import 'dart:async';
+import 'dart:io';
 
 import 'package:collection/collection.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:immich_mobile/entities/etag.entity.dart';
-import 'package:immich_mobile/entities/exif_info.entity.dart';
+import 'package:immich_mobile/entities/backup_album.entity.dart';
 import 'package:immich_mobile/entities/user.entity.dart';
+import 'package:immich_mobile/interfaces/asset.interface.dart';
+import 'package:immich_mobile/interfaces/asset_api.interface.dart';
+import 'package:immich_mobile/interfaces/backup.interface.dart';
+import 'package:immich_mobile/interfaces/etag.interface.dart';
+import 'package:immich_mobile/interfaces/exif_info.interface.dart';
+import 'package:immich_mobile/interfaces/user.interface.dart';
 import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
 import 'package:immich_mobile/providers/api.provider.dart';
-import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/repositories/asset.repository.dart';
+import 'package:immich_mobile/repositories/asset_api.repository.dart';
+import 'package:immich_mobile/repositories/backup.repository.dart';
+import 'package:immich_mobile/repositories/etag.repository.dart';
+import 'package:immich_mobile/repositories/exif_info.repository.dart';
+import 'package:immich_mobile/repositories/user.repository.dart';
 import 'package:immich_mobile/services/album.service.dart';
 import 'package:immich_mobile/services/api.service.dart';
 import 'package:immich_mobile/services/backup.service.dart';
 import 'package:immich_mobile/services/sync.service.dart';
 import 'package:immich_mobile/services/user.service.dart';
-import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
 import 'package:maplibre_gl/maplibre_gl.dart';
 import 'package:openapi/api.dart';
 
 final assetServiceProvider = Provider(
   (ref) => AssetService(
+    ref.watch(assetApiRepositoryProvider),
+    ref.watch(assetRepositoryProvider),
+    ref.watch(exifInfoRepositoryProvider),
+    ref.watch(userRepositoryProvider),
+    ref.watch(etagRepositoryProvider),
+    ref.watch(backupRepositoryProvider),
     ref.watch(apiServiceProvider),
     ref.watch(syncServiceProvider),
     ref.watch(userServiceProvider),
     ref.watch(backupServiceProvider),
     ref.watch(albumServiceProvider),
-    ref.watch(dbProvider),
   ),
 );
 
 class AssetService {
+  final IAssetApiRepository _assetApiRepository;
+  final IAssetRepository _assetRepository;
+  final IExifInfoRepository _exifInfoRepository;
+  final IUserRepository _userRepository;
+  final IETagRepository _etagRepository;
+  final IBackupRepository _backupRepository;
   final ApiService _apiService;
   final SyncService _syncService;
   final UserService _userService;
   final BackupService _backupService;
   final AlbumService _albumService;
   final log = Logger('AssetService');
-  final Isar _db;
 
   AssetService(
+    this._assetApiRepository,
+    this._assetRepository,
+    this._exifInfoRepository,
+    this._userRepository,
+    this._etagRepository,
+    this._backupRepository,
     this._apiService,
     this._syncService,
     this._userService,
     this._backupService,
     this._albumService,
-    this._db,
   );
 
   /// Checks the server for updated assets and updates the local database if
   /// required. Returns `true` if there were any changes.
   Future<bool> refreshRemoteAssets() async {
-    final syncedUserIds = await _db.eTags.where().idProperty().findAll();
+    final syncedUserIds = await _etagRepository.getAllIds();
     final List<User> syncedUsers = syncedUserIds.isEmpty
         ? []
-        : await _db.users
-            .where()
-            .anyOf(syncedUserIds, (q, id) => q.idEqualTo(id))
-            .findAll();
+        : await _userRepository.getByIds(syncedUserIds);
     final Stopwatch sw = Stopwatch()..start();
     final bool changes = await _syncService.syncRemoteAssetsToDb(
       users: syncedUsers,
@@ -165,7 +185,7 @@ class AssetService {
   /// Loads the exif information from the database. If there is none, loads
   /// the exif info from the server (remote assets only)
   Future<Asset> loadExif(Asset a) async {
-    a.exifInfo ??= await _db.exifInfos.get(a.id);
+    a.exifInfo ??= await _exifInfoRepository.get(a.id);
     // fileSize is always filled on the server but not set on client
     if (a.exifInfo?.fileSize == null) {
       if (a.isRemote) {
@@ -175,7 +195,7 @@ class AssetService {
           a.exifInfo = newExif;
           if (newExif != a.exifInfo) {
             if (a.isInDb) {
-              _db.writeTxn(() => a.put(_db));
+              _assetRepository.transaction(() => _assetRepository.update(a));
             } else {
               debugPrint("[loadExif] parameter Asset is not from DB!");
             }
@@ -204,7 +224,7 @@ class AssetService {
     );
   }
 
-  Future<List<Asset?>> changeFavoriteStatus(
+  Future<List<Asset>> changeFavoriteStatus(
     List<Asset> assets,
     bool isFavorite,
   ) async {
@@ -220,11 +240,11 @@ class AssetService {
       return assets;
     } catch (error, stack) {
       log.severe("Error while changing favorite status", error, stack);
-      return Future.value(null);
+      return [];
     }
   }
 
-  Future<List<Asset?>> changeArchiveStatus(
+  Future<List<Asset>> changeArchiveStatus(
     List<Asset> assets,
     bool isArchived,
   ) async {
@@ -240,11 +260,11 @@ class AssetService {
       return assets;
     } catch (error, stack) {
       log.severe("Error while changing archive status", error, stack);
-      return Future.value(null);
+      return [];
     }
   }
 
-  Future<List<Asset?>> changeDateTime(
+  Future<List<Asset>?> changeDateTime(
     List<Asset> assets,
     String updatedDt,
   ) async {
@@ -268,7 +288,7 @@ class AssetService {
     }
   }
 
-  Future<List<Asset?>> changeLocation(
+  Future<List<Asset>?> changeLocation(
     List<Asset> assets,
     LatLng location,
   ) async {
@@ -297,10 +317,10 @@ class AssetService {
 
   Future<void> syncUploadedAssetToAlbums() async {
     try {
-      final [selectedAlbums, excludedAlbums] = await Future.wait([
-        _backupService.selectedAlbumsQuery().findAll(),
-        _backupService.excludedAlbumsQuery().findAll(),
-      ]);
+      final selectedAlbums =
+          await _backupRepository.getAllBySelection(BackupSelection.select);
+      final excludedAlbums =
+          await _backupRepository.getAllBySelection(BackupSelection.exclude);
 
       final candidates = await _backupService.buildUploadCandidates(
         selectedAlbums,
@@ -309,19 +329,18 @@ class AssetService {
       );
 
       await refreshRemoteAssets();
-      final remoteAssets = await _db.assets
-          .where()
-          .localIdIsNotNull()
-          .filter()
-          .remoteIdIsNotNull()
-          .findAll();
+      final owner = await _userRepository.me();
+      final remoteAssets = await _assetRepository.getAll(
+        ownerId: owner.isarId,
+        state: AssetState.merged,
+      );
 
       /// Map<AlbumName, [AssetId]>
       Map<String, List<String>> assetToAlbums = {};
 
       for (BackupCandidate candidate in candidates) {
         final asset = remoteAssets.firstWhereOrNull(
-          (a) => a.localId == candidate.asset.id,
+          (a) => a.localId == candidate.asset.localId,
         );
 
         if (asset != null) {
@@ -342,4 +361,71 @@ class AssetService {
       log.severe("Error while syncing uploaded asset to albums", error, stack);
     }
   }
+
+  Future<void> setDescription(
+    Asset asset,
+    String newDescription,
+  ) async {
+    final remoteAssetId = asset.remoteId;
+    final localExifId = asset.exifInfo?.id;
+
+    // Guard [remoteAssetId] and [localExifId] null
+    if (remoteAssetId == null || localExifId == null) {
+      return;
+    }
+
+    final result = await _assetApiRepository.update(
+      remoteAssetId,
+      description: newDescription,
+    );
+
+    final description = result.exifInfo?.description;
+
+    if (description != null) {
+      var exifInfo = await _exifInfoRepository.get(localExifId);
+
+      if (exifInfo != null) {
+        exifInfo.description = description;
+        await _exifInfoRepository.update(exifInfo);
+      }
+    }
+  }
+
+  Future<String> getDescription(Asset asset) async {
+    final localExifId = asset.exifInfo?.id;
+
+    // Guard [remoteAssetId] and [localExifId] null
+    if (localExifId == null) {
+      return "";
+    }
+
+    final exifInfo = await _exifInfoRepository.get(localExifId);
+
+    return exifInfo?.description ?? "";
+  }
+
+  Future<double> getAspectRatio(Asset asset) async {
+    // platform_manager always returns 0 for orientation on iOS, so only prefer it on Android
+    if (asset.isLocal && Platform.isAndroid) {
+      await asset.localAsync;
+    } else if (asset.isRemote) {
+      asset = await loadExif(asset);
+    } else if (asset.isLocal) {
+      await asset.localAsync;
+    }
+
+    final aspectRatio = asset.aspectRatio;
+    if (aspectRatio != null) {
+      return aspectRatio;
+    }
+
+    final width = asset.width;
+    final height = asset.height;
+    if (width != null && height != null) {
+      // we don't know the orientation, so assume it's normal
+      return width / height;
+    }
+
+    return 1.0;
+  }
 }
diff --git a/mobile/lib/services/asset_description.service.dart b/mobile/lib/services/asset_description.service.dart
deleted file mode 100644
index 196e29dc6a..0000000000
--- a/mobile/lib/services/asset_description.service.dart
+++ /dev/null
@@ -1,66 +0,0 @@
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:immich_mobile/entities/exif_info.entity.dart';
-import 'package:immich_mobile/providers/api.provider.dart';
-import 'package:immich_mobile/providers/db.provider.dart';
-import 'package:immich_mobile/services/api.service.dart';
-import 'package:isar/isar.dart';
-import 'package:openapi/api.dart';
-
-class AssetDescriptionService {
-  AssetDescriptionService(this._db, this._api);
-
-  final Isar _db;
-  final ApiService _api;
-
-  Future<void> setDescription(
-    Asset asset,
-    String newDescription,
-  ) async {
-    final remoteAssetId = asset.remoteId;
-    final localExifId = asset.exifInfo?.id;
-
-    // Guard [remoteAssetId] and [localExifId] null
-    if (remoteAssetId == null || localExifId == null) {
-      return;
-    }
-
-    final result = await _api.assetsApi.updateAsset(
-      remoteAssetId,
-      UpdateAssetDto(description: newDescription),
-    );
-
-    final description = result?.exifInfo?.description;
-
-    if (description != null) {
-      var exifInfo = await _db.exifInfos.get(localExifId);
-
-      if (exifInfo != null) {
-        exifInfo.description = description;
-        await _db.writeTxn(
-          () => _db.exifInfos.put(exifInfo),
-        );
-      }
-    }
-  }
-
-  String getAssetDescription(Asset asset) {
-    final localExifId = asset.exifInfo?.id;
-
-    // Guard [remoteAssetId] and [localExifId] null
-    if (localExifId == null) {
-      return "";
-    }
-
-    final exifInfo = _db.exifInfos.getSync(localExifId);
-
-    return exifInfo?.description ?? "";
-  }
-}
-
-final assetDescriptionServiceProvider = Provider(
-  (ref) => AssetDescriptionService(
-    ref.watch(dbProvider),
-    ref.watch(apiServiceProvider),
-  ),
-);
diff --git a/mobile/lib/services/auth.service.dart b/mobile/lib/services/auth.service.dart
new file mode 100644
index 0000000000..08741a15db
--- /dev/null
+++ b/mobile/lib/services/auth.service.dart
@@ -0,0 +1,196 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/interfaces/auth.interface.dart';
+import 'package:immich_mobile/interfaces/auth_api.interface.dart';
+import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
+import 'package:immich_mobile/models/auth/login_response.model.dart';
+import 'package:immich_mobile/providers/api.provider.dart';
+import 'package:immich_mobile/repositories/auth.repository.dart';
+import 'package:immich_mobile/repositories/auth_api.repository.dart';
+import 'package:immich_mobile/services/api.service.dart';
+import 'package:immich_mobile/services/network.service.dart';
+import 'package:logging/logging.dart';
+import 'package:openapi/api.dart';
+
+final authServiceProvider = Provider(
+  (ref) => AuthService(
+    ref.watch(authApiRepositoryProvider),
+    ref.watch(authRepositoryProvider),
+    ref.watch(apiServiceProvider),
+    ref.watch(networkServiceProvider),
+  ),
+);
+
+class AuthService {
+  final IAuthApiRepository _authApiRepository;
+  final IAuthRepository _authRepository;
+  final ApiService _apiService;
+  final NetworkService _networkService;
+
+  final _log = Logger("AuthService");
+
+  AuthService(
+    this._authApiRepository,
+    this._authRepository,
+    this._apiService,
+    this._networkService,
+  );
+
+  /// Validates the provided server URL by resolving and setting the endpoint.
+  /// Also sets the device info header and stores the valid URL.
+  ///
+  /// [url] - The server URL to be validated.
+  ///
+  /// Returns the validated and resolved server URL as a [String].
+  ///
+  /// Throws an exception if the URL cannot be resolved or set.
+  Future<String> validateServerUrl(String url) async {
+    final validUrl = await _apiService.resolveAndSetEndpoint(url);
+    await _apiService.setDeviceInfoHeader();
+    Store.put(StoreKey.serverUrl, validUrl);
+
+    return validUrl;
+  }
+
+  Future<bool> validateAuxilaryServerUrl(String url) async {
+    final httpclient = HttpClient();
+    bool isValid = false;
+
+    try {
+      final uri = Uri.parse('$url/users/me');
+      final request = await httpclient.getUrl(uri);
+
+      // add auth token + any configured custom headers
+      final customHeaders = ApiService.getRequestHeaders();
+      customHeaders.forEach((key, value) {
+        request.headers.add(key, value);
+      });
+
+      final response = await request.close();
+      if (response.statusCode == 200) {
+        isValid = true;
+      }
+    } catch (error) {
+      _log.severe("Error validating auxilary endpoint", error);
+    } finally {
+      httpclient.close();
+    }
+
+    return isValid;
+  }
+
+  Future<LoginResponse> login(String email, String password) {
+    return _authApiRepository.login(email, password);
+  }
+
+  /// Performs user logout operation by making a server request and clearing local data.
+  ///
+  /// This method attempts to log out the user through the authentication API repository.
+  /// If the server request fails, the error is logged but local data is still cleared.
+  /// The local data cleanup is guaranteed to execute regardless of the server request outcome.
+  ///
+  /// Throws any unhandled exceptions from the API request or local data clearing operations.
+  Future<void> logout() async {
+    try {
+      await _authApiRepository.logout();
+    } catch (error, stackTrace) {
+      _log.severe("Error logging out", error, stackTrace);
+    } finally {
+      await clearLocalData().catchError((error, stackTrace) {
+        _log.severe("Error clearing local data", error, stackTrace);
+      });
+    }
+  }
+
+  /// Clears all local authentication-related data.
+  ///
+  /// This method performs a concurrent deletion of:
+  /// - Authentication repository data
+  /// - Current user information
+  /// - Access token
+  /// - Asset ETag
+  ///
+  /// All deletions are executed in parallel using [Future.wait].
+  Future<void> clearLocalData() {
+    return Future.wait([
+      _authRepository.clearLocalData(),
+      Store.delete(StoreKey.currentUser),
+      Store.delete(StoreKey.accessToken),
+      Store.delete(StoreKey.assetETag),
+      Store.delete(StoreKey.autoEndpointSwitching),
+      Store.delete(StoreKey.preferredWifiName),
+      Store.delete(StoreKey.localEndpoint),
+      Store.delete(StoreKey.externalEndpointList),
+    ]);
+  }
+
+  Future<void> changePassword(String newPassword) {
+    try {
+      return _authApiRepository.changePassword(newPassword);
+    } catch (error, stackTrace) {
+      _log.severe("Error changing password", error, stackTrace);
+      rethrow;
+    }
+  }
+
+  Future<String?> setOpenApiServiceEndpoint() async {
+    final enable = _authRepository.getEndpointSwitchingFeature();
+    if (!enable) {
+      return null;
+    }
+
+    final wifiName = await _networkService.getWifiName();
+    final savedWifiName = _authRepository.getPreferredWifiName();
+    String? endpoint;
+
+    if (wifiName == savedWifiName) {
+      endpoint = await _setLocalConnection();
+    }
+
+    endpoint ??= await _setRemoteConnection();
+
+    return endpoint;
+  }
+
+  Future<String?> _setLocalConnection() async {
+    try {
+      final localEndpoint = _authRepository.getLocalEndpoint();
+      if (localEndpoint != null) {
+        await _apiService.resolveAndSetEndpoint(localEndpoint);
+        return localEndpoint;
+      }
+    } catch (error, stackTrace) {
+      _log.severe("Cannot set local endpoint", error, stackTrace);
+    }
+
+    return null;
+  }
+
+  Future<String?> _setRemoteConnection() async {
+    List<AuxilaryEndpoint> endpointList;
+
+    try {
+      endpointList = _authRepository.getExternalEndpointList();
+    } catch (error, stackTrace) {
+      _log.severe("Cannot get external endpoint", error, stackTrace);
+      return null;
+    }
+
+    for (final endpoint in endpointList) {
+      try {
+        return await _apiService.resolveAndSetEndpoint(endpoint.url);
+      } on ApiException catch (error) {
+        _log.severe("Cannot resolve endpoint", error);
+        continue;
+      } catch (_) {
+        _log.severe("Auxilary server is not valid");
+        continue;
+      }
+    }
+
+    return null;
+  }
+}
diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart
index fc3feb174d..c059f48f0e 100644
--- a/mobile/lib/services/background.service.dart
+++ b/mobile/lib/services/background.service.dart
@@ -6,13 +6,33 @@ import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities;
 import 'package:cancellation_token_http/http.dart';
 import 'package:collection/collection.dart';
 import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/foundation.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/widgets.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/interfaces/backup.interface.dart';
 import 'package:immich_mobile/main.dart';
 import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
 import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
+import 'package:immich_mobile/repositories/album.repository.dart';
+import 'package:immich_mobile/repositories/album_api.repository.dart';
+import 'package:immich_mobile/repositories/asset.repository.dart';
+import 'package:immich_mobile/repositories/asset_media.repository.dart';
+import 'package:immich_mobile/repositories/auth.repository.dart';
+import 'package:immich_mobile/repositories/auth_api.repository.dart';
+import 'package:immich_mobile/repositories/backup.repository.dart';
+import 'package:immich_mobile/repositories/album_media.repository.dart';
+import 'package:immich_mobile/repositories/etag.repository.dart';
+import 'package:immich_mobile/repositories/exif_info.repository.dart';
+import 'package:immich_mobile/repositories/file_media.repository.dart';
+import 'package:immich_mobile/repositories/network.repository.dart';
+import 'package:immich_mobile/repositories/partner_api.repository.dart';
+import 'package:immich_mobile/repositories/permission.repository.dart';
+import 'package:immich_mobile/repositories/user.repository.dart';
+import 'package:immich_mobile/repositories/user_api.repository.dart';
 import 'package:immich_mobile/services/album.service.dart';
+import 'package:immich_mobile/services/auth.service.dart';
+import 'package:immich_mobile/services/entity.service.dart';
 import 'package:immich_mobile/services/hash.service.dart';
 import 'package:immich_mobile/services/localization.service.dart';
 import 'package:immich_mobile/entities/backup_album.entity.dart';
@@ -22,15 +42,15 @@ import 'package:immich_mobile/services/backup.service.dart';
 import 'package:immich_mobile/services/app_settings.service.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
 import 'package:immich_mobile/services/api.service.dart';
-import 'package:immich_mobile/services/partner.service.dart';
+import 'package:immich_mobile/services/network.service.dart';
 import 'package:immich_mobile/services/sync.service.dart';
 import 'package:immich_mobile/services/user.service.dart';
 import 'package:immich_mobile/utils/backup_progress.dart';
 import 'package:immich_mobile/utils/diff.dart';
 import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
-import 'package:isar/isar.dart';
+import 'package:network_info_plus/network_info_plus.dart';
 import 'package:path_provider_ios/path_provider_ios.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
 
 final backgroundServiceProvider = Provider(
   (ref) => BackgroundService(),
@@ -347,30 +367,95 @@ class BackgroundService {
   }
 
   Future<bool> _onAssetsChanged() async {
-    final Isar db = await loadDb();
+    final db = await loadDb();
 
     HttpOverrides.global = HttpSSLCertOverride();
     ApiService apiService = ApiService();
     apiService.setAccessToken(Store.get(StoreKey.accessToken));
-    AppSettingsService settingService = AppSettingsService();
     AppSettingsService settingsService = AppSettingsService();
-    PartnerService partnerService = PartnerService(apiService, db);
-    HashService hashService = HashService(db, this);
-    SyncService syncSerive = SyncService(db, hashService);
-    UserService userService =
-        UserService(apiService, db, syncSerive, partnerService);
-    AlbumService albumService =
-        AlbumService(apiService, userService, syncSerive, db);
-    BackupService backupService =
-        BackupService(apiService, db, settingService, albumService);
+    AlbumRepository albumRepository = AlbumRepository(db);
+    AssetRepository assetRepository = AssetRepository(db);
+    BackupRepository backupRepository = BackupRepository(db);
+    ExifInfoRepository exifInfoRepository = ExifInfoRepository(db);
+    ETagRepository eTagRepository = ETagRepository(db);
+    AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
+    FileMediaRepository fileMediaRepository = FileMediaRepository();
+    AssetMediaRepository assetMediaRepository = AssetMediaRepository();
+    UserRepository userRepository = UserRepository(db);
+    UserApiRepository userApiRepository =
+        UserApiRepository(apiService.usersApi);
+    AlbumApiRepository albumApiRepository =
+        AlbumApiRepository(apiService.albumsApi);
+    PartnerApiRepository partnerApiRepository =
+        PartnerApiRepository(apiService.partnersApi);
+    HashService hashService =
+        HashService(assetRepository, this, albumMediaRepository);
+    EntityService entityService =
+        EntityService(assetRepository, userRepository);
+    SyncService syncSerive = SyncService(
+      hashService,
+      entityService,
+      albumMediaRepository,
+      albumApiRepository,
+      albumRepository,
+      assetRepository,
+      exifInfoRepository,
+      userRepository,
+      eTagRepository,
+    );
+    UserService userService = UserService(
+      partnerApiRepository,
+      userApiRepository,
+      userRepository,
+      syncSerive,
+    );
+    AlbumService albumService = AlbumService(
+      userService,
+      syncSerive,
+      entityService,
+      albumRepository,
+      assetRepository,
+      backupRepository,
+      albumMediaRepository,
+      albumApiRepository,
+    );
+    BackupService backupService = BackupService(
+      apiService,
+      settingsService,
+      albumService,
+      albumMediaRepository,
+      fileMediaRepository,
+      assetRepository,
+      assetMediaRepository,
+    );
 
-    final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
-    final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync();
+    AuthApiRepository authApiRepository = AuthApiRepository(apiService);
+    AuthRepository authRepository = AuthRepository(db);
+    NetworkRepository networkRepository = NetworkRepository(NetworkInfo());
+    PermissionRepository permissionRepository = PermissionRepository();
+    NetworkService networkService =
+        NetworkService(networkRepository, permissionRepository);
+    AuthService authService = AuthService(
+      authApiRepository,
+      authRepository,
+      apiService,
+      networkService,
+    );
+
+    final endpoint = await authService.setOpenApiServiceEndpoint();
+    if (kDebugMode) {
+      debugPrint("[BG UPLOAD] Using endpoint: $endpoint");
+    }
+
+    final selectedAlbums =
+        await backupRepository.getAllBySelection(BackupSelection.select);
+    final excludedAlbums =
+        await backupRepository.getAllBySelection(BackupSelection.exclude);
     if (selectedAlbums.isEmpty) {
       return true;
     }
 
-    await PhotoManager.setIgnorePermissionCheck(true);
+    await fileMediaRepository.enableBackgroundAccess();
 
     do {
       final bool backupOk = await _runBackup(
@@ -383,28 +468,28 @@ class BackgroundService {
         await Store.delete(StoreKey.backupFailedSince);
         final backupAlbums = [...selectedAlbums, ...excludedAlbums];
         backupAlbums.sortBy((e) => e.id);
-        db.writeTxnSync(() {
-          final dbAlbums = db.backupAlbums.where().sortById().findAllSync();
-          final List<int> toDelete = [];
-          final List<BackupAlbum> toUpsert = [];
-          // stores the most recent `lastBackup` per album but always keeps the `selection` from the most recent DB state
-          diffSortedListsSync(
-            dbAlbums,
-            backupAlbums,
-            compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id),
-            both: (BackupAlbum a, BackupAlbum b) {
-              a.lastBackup = a.lastBackup.isAfter(b.lastBackup)
-                  ? a.lastBackup
-                  : b.lastBackup;
-              toUpsert.add(a);
-              return true;
-            },
-            onlyFirst: (BackupAlbum a) => toUpsert.add(a),
-            onlySecond: (BackupAlbum b) => toDelete.add(b.isarId),
-          );
-          db.backupAlbums.deleteAllSync(toDelete);
-          db.backupAlbums.putAllSync(toUpsert);
-        });
+
+        final dbAlbums =
+            await backupRepository.getAll(sort: BackupAlbumSort.id);
+        final List<int> toDelete = [];
+        final List<BackupAlbum> toUpsert = [];
+        // stores the most recent `lastBackup` per album but always keeps the `selection` from the most recent DB state
+        diffSortedListsSync(
+          dbAlbums,
+          backupAlbums,
+          compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id),
+          both: (BackupAlbum a, BackupAlbum b) {
+            a.lastBackup = a.lastBackup.isAfter(b.lastBackup)
+                ? a.lastBackup
+                : b.lastBackup;
+            toUpsert.add(a);
+            return true;
+          },
+          onlyFirst: (BackupAlbum a) => toUpsert.add(a),
+          onlySecond: (BackupAlbum b) => toDelete.add(b.isarId),
+        );
+        await backupRepository.deleteAll(toDelete);
+        await backupRepository.updateAll(toUpsert);
       } else if (Store.tryGet(StoreKey.backupFailedSince) == null) {
         Store.put(StoreKey.backupFailedSince, DateTime.now());
         return false;
diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart
index 858499443e..7bce1047e2 100644
--- a/mobile/lib/services/backup.service.dart
+++ b/mobile/lib/services/backup.service.dart
@@ -6,48 +6,64 @@ import 'package:cancellation_token_http/http.dart' as http;
 import 'package:collection/collection.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/entities/backup_album.entity.dart';
-import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/interfaces/album_media.interface.dart';
+import 'package:immich_mobile/interfaces/asset.interface.dart';
+import 'package:immich_mobile/interfaces/asset_media.interface.dart';
+import 'package:immich_mobile/interfaces/file_media.interface.dart';
 import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
 import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
 import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
 import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
 import 'package:immich_mobile/providers/api.provider.dart';
 import 'package:immich_mobile/providers/app_settings.provider.dart';
-import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/repositories/album_media.repository.dart';
+import 'package:immich_mobile/repositories/asset.repository.dart';
+import 'package:immich_mobile/repositories/asset_media.repository.dart';
+import 'package:immich_mobile/repositories/file_media.repository.dart';
 import 'package:immich_mobile/services/album.service.dart';
 import 'package:immich_mobile/services/api.service.dart';
 import 'package:immich_mobile/services/app_settings.service.dart';
-import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
 import 'package:openapi/api.dart';
 import 'package:path/path.dart' as p;
 import 'package:permission_handler/permission_handler.dart' as pm;
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
 
 final backupServiceProvider = Provider(
   (ref) => BackupService(
     ref.watch(apiServiceProvider),
-    ref.watch(dbProvider),
     ref.watch(appSettingsServiceProvider),
     ref.watch(albumServiceProvider),
+    ref.watch(albumMediaRepositoryProvider),
+    ref.watch(fileMediaRepositoryProvider),
+    ref.watch(assetRepositoryProvider),
+    ref.watch(assetMediaRepositoryProvider),
   ),
 );
 
 class BackupService {
   final httpClient = http.Client();
   final ApiService _apiService;
-  final Isar _db;
   final Logger _log = Logger("BackupService");
   final AppSettingsService _appSetting;
   final AlbumService _albumService;
+  final IAlbumMediaRepository _albumMediaRepository;
+  final IFileMediaRepository _fileMediaRepository;
+  final IAssetRepository _assetRepository;
+  final IAssetMediaRepository _assetMediaRepository;
 
   BackupService(
     this._apiService,
-    this._db,
     this._appSetting,
     this._albumService,
+    this._albumMediaRepository,
+    this._fileMediaRepository,
+    this._assetRepository,
+    this._assetMediaRepository,
   );
 
   Future<List<String>?> getDeviceBackupAsset() async {
@@ -61,24 +77,17 @@ class BackupService {
     }
   }
 
-  Future<void> _saveDuplicatedAssetIds(List<String> deviceAssetIds) {
-    final duplicates = deviceAssetIds.map((id) => DuplicatedAsset(id)).toList();
-    return _db.writeTxn(() => _db.duplicatedAssets.putAll(duplicates));
-  }
+  Future<void> _saveDuplicatedAssetIds(List<String> deviceAssetIds) =>
+      _assetRepository.transaction(
+        () => _assetRepository.upsertDuplicatedAssets(deviceAssetIds),
+      );
 
   /// Get duplicated asset id from database
   Future<Set<String>> getDuplicatedAssetIds() async {
-    final duplicates = await _db.duplicatedAssets.where().findAll();
-    return duplicates.map((e) => e.id).toSet();
+    final duplicates = await _assetRepository.getAllDuplicatedAssetIds();
+    return duplicates.toSet();
   }
 
-  QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
-      selectedAlbumsQuery() =>
-          _db.backupAlbums.filter().selectionEqualTo(BackupSelection.select);
-  QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
-      excludedAlbumsQuery() =>
-          _db.backupAlbums.filter().selectionEqualTo(BackupSelection.exclude);
-
   /// Returns all assets newer than the last successful backup per album
   /// if `useTimeFilter` is set to true, all assets will be returned
   Future<Set<BackupCandidate>> buildUploadCandidates(
@@ -86,44 +95,17 @@ class BackupService {
     List<BackupAlbum> excludedBackupAlbums, {
     bool useTimeFilter = true,
   }) async {
-    final filter = FilterOptionGroup(
-      containsPathModified: true,
-      orders: [const OrderOption(type: OrderOptionType.updateDate)],
-      // title is needed to create Assets
-      imageOption: const FilterOption(needTitle: true),
-      videoOption: const FilterOption(needTitle: true),
-    );
     final now = DateTime.now();
 
-    final List<AssetPathEntity?> selectedAlbums =
-        await _loadAlbumsWithTimeFilter(
-      selectedBackupAlbums,
-      filter,
-      now,
-      useTimeFilter: useTimeFilter,
-    );
-
-    if (selectedAlbums.every((e) => e == null)) {
-      return {};
-    }
-
-    final List<AssetPathEntity?> excludedAlbums =
-        await _loadAlbumsWithTimeFilter(
-      excludedBackupAlbums,
-      filter,
-      now,
-      useTimeFilter: useTimeFilter,
-    );
-
     final Set<BackupCandidate> toAdd = await _fetchAssetsAndUpdateLastBackup(
-      selectedAlbums,
       selectedBackupAlbums,
       now,
       useTimeFilter: useTimeFilter,
     );
 
+    if (toAdd.isEmpty) return {};
+
     final Set<BackupCandidate> toRemove = await _fetchAssetsAndUpdateLastBackup(
-      excludedAlbums,
       excludedBackupAlbums,
       now,
       useTimeFilter: useTimeFilter,
@@ -132,92 +114,62 @@ class BackupService {
     return toAdd.difference(toRemove);
   }
 
-  Future<List<AssetPathEntity?>> _loadAlbumsWithTimeFilter(
-    List<BackupAlbum> albums,
-    FilterOptionGroup filter,
-    DateTime now, {
-    bool useTimeFilter = true,
-  }) async {
-    List<AssetPathEntity?> result = [];
-    for (BackupAlbum backupAlbum in albums) {
-      try {
-        final optionGroup = useTimeFilter
-            ? filter.copyWith(
-                updateTimeCond: DateTimeCond(
-                  // subtract 2 seconds to prevent missing assets due to rounding issues
-                  min: backupAlbum.lastBackup
-                      .subtract(const Duration(seconds: 2)),
-                  max: now,
-                ),
-              )
-            : filter;
-
-        final AssetPathEntity album =
-            await AssetPathEntity.obtainPathFromProperties(
-          id: backupAlbum.id,
-          optionGroup: optionGroup,
-          maxDateTimeToNow: false,
-        );
-
-        result.add(album);
-      } on StateError {
-        // either there are no assets matching the filter criteria OR the album no longer exists
-      }
-    }
-
-    return result;
-  }
-
   Future<Set<BackupCandidate>> _fetchAssetsAndUpdateLastBackup(
-    List<AssetPathEntity?> localAlbums,
     List<BackupAlbum> backupAlbums,
     DateTime now, {
     bool useTimeFilter = true,
   }) async {
-    Set<BackupCandidate> candidate = {};
+    Set<BackupCandidate> candidates = {};
 
-    for (int i = 0; i < localAlbums.length; i++) {
-      final localAlbum = localAlbums[i];
-      if (localAlbum == null) {
+    for (final BackupAlbum backupAlbum in backupAlbums) {
+      final Album localAlbum;
+      try {
+        localAlbum = await _albumMediaRepository.get(backupAlbum.id);
+      } on StateError {
+        // the album no longer exists
         continue;
       }
 
       if (useTimeFilter &&
-          localAlbum.lastModified?.isBefore(backupAlbums[i].lastBackup) ==
-              true) {
+          localAlbum.modifiedAt.isBefore(backupAlbum.lastBackup)) {
+        continue;
+      }
+      final List<Asset> assets;
+      try {
+        assets = await _albumMediaRepository.getAssets(
+          backupAlbum.id,
+          modifiedFrom: useTimeFilter
+              ?
+              // subtract 2 seconds to prevent missing assets due to rounding issues
+              backupAlbum.lastBackup.subtract(const Duration(seconds: 2))
+              : null,
+          modifiedUntil: useTimeFilter ? now : null,
+        );
+      } on StateError {
+        // either there are no assets matching the filter criteria OR the album no longer exists
         continue;
       }
-
-      final assets = await localAlbum.getAssetListRange(
-        start: 0,
-        end: await localAlbum.assetCountAsync,
-      );
 
       // Add album's name to the asset info
       for (final asset in assets) {
         List<String> albumNames = [localAlbum.name];
 
-        final existingAsset = candidate.firstWhereOrNull(
-          (a) => a.asset.id == asset.id,
+        final existingAsset = candidates.firstWhereOrNull(
+          (candidate) => candidate.asset.localId == asset.localId,
         );
 
         if (existingAsset != null) {
           albumNames.addAll(existingAsset.albumNames);
-          candidate.remove(existingAsset);
+          candidates.remove(existingAsset);
         }
 
-        candidate.add(
-          BackupCandidate(
-            asset: asset,
-            albumNames: albumNames,
-          ),
-        );
+        candidates.add(BackupCandidate(asset: asset, albumNames: albumNames));
       }
 
-      backupAlbums[i].lastBackup = now;
+      backupAlbum.lastBackup = now;
     }
 
-    return candidate;
+    return candidates;
   }
 
   /// Returns a new list of assets not yet uploaded
@@ -230,7 +182,7 @@ class BackupService {
 
     final Set<String> duplicatedAssetIds = await getDuplicatedAssetIds();
     candidates.removeWhere(
-      (candidate) => duplicatedAssetIds.contains(candidate.asset.id),
+      (candidate) => duplicatedAssetIds.contains(candidate.asset.localId),
     );
 
     if (candidates.isEmpty) {
@@ -243,7 +195,7 @@ class BackupService {
       final CheckExistingAssetsResponseDto? duplicates =
           await _apiService.assetsApi.checkExistingAssets(
         CheckExistingAssetsDto(
-          deviceAssetIds: candidates.map((c) => c.asset.id).toList(),
+          deviceAssetIds: candidates.map((c) => c.asset.localId!).toList(),
           deviceId: deviceId,
         ),
       );
@@ -259,7 +211,7 @@ class BackupService {
     }
 
     if (existing.isNotEmpty) {
-      candidates.removeWhere((c) => existing.contains(c.asset.id));
+      candidates.removeWhere((c) => existing.contains(c.asset.localId));
     }
 
     return candidates;
@@ -278,7 +230,7 @@ class BackupService {
 
     // DON'T KNOW WHY BUT THIS HELPS BACKGROUND BACKUP TO WORK ON IOS
     if (Platform.isIOS) {
-      await PhotoManager.requestPermissionExtend();
+      await _fileMediaRepository.requestExtendedPermissions();
     }
 
     return true;
@@ -289,9 +241,9 @@ class BackupService {
   List<BackupCandidate> _sortPhotosFirst(List<BackupCandidate> candidates) {
     return candidates.sorted(
       (a, b) {
-        final cmp = a.asset.typeInt - b.asset.typeInt;
+        final cmp = a.asset.type.index - b.asset.type.index;
         if (cmp != 0) return cmp;
-        return a.asset.createDateTime.compareTo(b.asset.createDateTime);
+        return a.asset.fileCreatedAt.compareTo(b.asset.fileCreatedAt);
       },
     );
   }
@@ -325,13 +277,13 @@ class BackupService {
     }
 
     for (final candidate in candidates) {
-      final AssetEntity entity = candidate.asset;
+      final Asset asset = candidate.asset;
       File? file;
       File? livePhotoFile;
 
       try {
         final isAvailableLocally =
-            await entity.isLocallyAvailable(isOrigin: true);
+            await asset.local!.isLocallyAvailable(isOrigin: true);
 
         // Handle getting files from iCloud
         if (!isAvailableLocally && Platform.isIOS) {
@@ -342,39 +294,40 @@ class BackupService {
 
           onCurrentAsset(
             CurrentUploadAsset(
-              id: entity.id,
-              fileCreatedAt: entity.createDateTime.year == 1970
-                  ? entity.modifiedDateTime
-                  : entity.createDateTime,
-              fileName: await entity.titleAsync,
-              fileType: _getAssetType(entity.type),
+              id: asset.localId!,
+              fileCreatedAt: asset.fileCreatedAt.year == 1970
+                  ? asset.fileModifiedAt
+                  : asset.fileCreatedAt,
+              fileName: asset.fileName,
+              fileType: _getAssetType(asset.type),
               iCloudAsset: true,
             ),
           );
 
-          file = await entity.loadFile(progressHandler: pmProgressHandler);
-          if (entity.isLivePhoto) {
-            livePhotoFile = await entity.loadFile(
+          file =
+              await asset.local!.loadFile(progressHandler: pmProgressHandler);
+          if (asset.local!.isLivePhoto) {
+            livePhotoFile = await asset.local!.loadFile(
               withSubtype: true,
               progressHandler: pmProgressHandler,
             );
           }
         } else {
-          if (entity.type == AssetType.video) {
-            file = await entity.originFile;
-          } else {
-            file = await entity.originFile.timeout(const Duration(seconds: 5));
-            if (entity.isLivePhoto) {
-              livePhotoFile = await entity.originFileWithSubtype
-                  .timeout(const Duration(seconds: 5));
-            }
+          file =
+              await asset.local!.originFile.timeout(const Duration(seconds: 5));
+
+          if (asset.local!.isLivePhoto) {
+            livePhotoFile = await asset.local!.originFileWithSubtype
+                .timeout(const Duration(seconds: 5));
           }
         }
 
         if (file != null) {
-          String originalFileName = await entity.titleAsync;
+          String? originalFileName =
+              await _assetMediaRepository.getOriginalFilename(asset.localId!);
+          originalFileName ??= asset.fileName;
 
-          if (entity.isLivePhoto) {
+          if (asset.local!.isLivePhoto) {
             if (livePhotoFile == null) {
               _log.warning(
                 "Failed to obtain motion part of the livePhoto - $originalFileName",
@@ -398,31 +351,31 @@ class BackupService {
 
           baseRequest.headers.addAll(ApiService.getRequestHeaders());
           baseRequest.headers["Transfer-Encoding"] = "chunked";
-          baseRequest.fields['deviceAssetId'] = entity.id;
+          baseRequest.fields['deviceAssetId'] = asset.localId!;
           baseRequest.fields['deviceId'] = deviceId;
           baseRequest.fields['fileCreatedAt'] =
-              entity.createDateTime.toUtc().toIso8601String();
+              asset.fileCreatedAt.toUtc().toIso8601String();
           baseRequest.fields['fileModifiedAt'] =
-              entity.modifiedDateTime.toUtc().toIso8601String();
-          baseRequest.fields['isFavorite'] = entity.isFavorite.toString();
-          baseRequest.fields['duration'] = entity.videoDuration.toString();
+              asset.fileModifiedAt.toUtc().toIso8601String();
+          baseRequest.fields['isFavorite'] = asset.isFavorite.toString();
+          baseRequest.fields['duration'] = asset.duration.toString();
           baseRequest.files.add(assetRawUploadData);
 
           onCurrentAsset(
             CurrentUploadAsset(
-              id: entity.id,
-              fileCreatedAt: entity.createDateTime.year == 1970
-                  ? entity.modifiedDateTime
-                  : entity.createDateTime,
+              id: asset.localId!,
+              fileCreatedAt: asset.fileCreatedAt.year == 1970
+                  ? asset.fileModifiedAt
+                  : asset.fileCreatedAt,
               fileName: originalFileName,
-              fileType: _getAssetType(entity.type),
+              fileType: _getAssetType(asset.type),
               fileSize: file.lengthSync(),
               iCloudAsset: false,
             ),
           );
 
           String? livePhotoVideoId;
-          if (entity.isLivePhoto && livePhotoFile != null) {
+          if (asset.local!.isLivePhoto && livePhotoFile != null) {
             livePhotoVideoId = await uploadLivePhotoVideo(
               originalFileName,
               livePhotoFile,
@@ -448,16 +401,16 @@ class BackupService {
             final errorMessage = error['message'] ?? error['error'];
 
             debugPrint(
-              "Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}",
+              "Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}",
             );
 
             onError(
               ErrorUploadAsset(
-                asset: entity,
-                id: entity.id,
-                fileCreatedAt: entity.createDateTime,
+                asset: asset,
+                id: asset.localId!,
+                fileCreatedAt: asset.fileCreatedAt,
                 fileName: originalFileName,
-                fileType: _getAssetType(entity.type),
+                fileType: _getAssetType(candidate.asset.type),
                 errorMessage: errorMessage,
               ),
             );
@@ -473,7 +426,7 @@ class BackupService {
           bool isDuplicate = false;
           if (response.statusCode == 200) {
             isDuplicate = true;
-            duplicatedAssetIds.add(entity.id);
+            duplicatedAssetIds.add(asset.localId!);
           }
 
           onSuccess(
diff --git a/mobile/lib/services/backup_verification.service.dart b/mobile/lib/services/backup_verification.service.dart
index c7cd134cb1..82cfb8347a 100644
--- a/mobile/lib/services/backup_verification.service.dart
+++ b/mobile/lib/services/backup_verification.service.dart
@@ -8,39 +8,46 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/entities/exif_info.entity.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
-import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/interfaces/asset.interface.dart';
+import 'package:immich_mobile/interfaces/exif_info.interface.dart';
+import 'package:immich_mobile/interfaces/file_media.interface.dart';
+import 'package:immich_mobile/repositories/asset.repository.dart';
+import 'package:immich_mobile/repositories/exif_info.repository.dart';
+import 'package:immich_mobile/repositories/file_media.repository.dart';
 import 'package:immich_mobile/services/api.service.dart';
 import 'package:immich_mobile/utils/diff.dart';
-import 'package:isar/isar.dart';
-import 'package:photo_manager/photo_manager.dart' show PhotoManager;
 
 /// Finds duplicates originating from missing EXIF information
 class BackupVerificationService {
-  final Isar _db;
+  final IFileMediaRepository _fileMediaRepository;
+  final IAssetRepository _assetRepository;
+  final IExifInfoRepository _exifInfoRepository;
 
-  BackupVerificationService(this._db);
+  BackupVerificationService(
+    this._fileMediaRepository,
+    this._assetRepository,
+    this._exifInfoRepository,
+  );
 
   /// Returns at most [limit] assets that were backed up without exif
   Future<List<Asset>> findWronglyBackedUpAssets({int limit = 100}) async {
     final owner = Store.get(StoreKey.currentUser).isarId;
-    final List<Asset> onlyLocal = await _db.assets
-        .where()
-        .remoteIdIsNull()
-        .filter()
-        .ownerIdEqualTo(owner)
-        .localIdIsNotNull()
-        .findAll();
-    final List<Asset> remoteMatches = await _getMatches(
-      _db.assets.where().localIdIsNull().filter().remoteIdIsNotNull(),
-      owner,
-      onlyLocal,
-      limit,
+    final List<Asset> onlyLocal = await _assetRepository.getAll(
+      ownerId: owner,
+      state: AssetState.local,
+      limit: limit,
     );
-    final List<Asset> localMatches = await _getMatches(
-      _db.assets.where().remoteIdIsNull().filter().localIdIsNotNull(),
-      owner,
-      remoteMatches,
-      limit,
+    final List<Asset> remoteMatches = await _assetRepository.getMatches(
+      assets: onlyLocal,
+      ownerId: owner,
+      state: AssetState.remote,
+      limit: limit,
+    );
+    final List<Asset> localMatches = await _assetRepository.getMatches(
+      assets: remoteMatches,
+      ownerId: owner,
+      state: AssetState.local,
+      limit: limit,
     );
 
     final List<Asset> deleteCandidates = [], originals = [];
@@ -50,7 +57,7 @@ class BackupVerificationService {
       localMatches,
       compare: (a, b) => a.fileName.compareTo(b.fileName),
       both: (a, b) async {
-        a.exifInfo = await _db.exifInfos.get(a.id);
+        a.exifInfo = await _exifInfoRepository.get(a.id);
         deleteCandidates.add(a);
         originals.add(b);
         return false;
@@ -71,6 +78,7 @@ class BackupVerificationService {
           auth: Store.get(StoreKey.accessToken),
           endpoint: Store.get(StoreKey.serverEndpoint),
           rootIsolateToken: isolateToken,
+          fileMediaRepository: _fileMediaRepository,
         ),
       );
       final upper = compute(
@@ -81,6 +89,7 @@ class BackupVerificationService {
           auth: Store.get(StoreKey.accessToken),
           endpoint: Store.get(StoreKey.serverEndpoint),
           rootIsolateToken: isolateToken,
+          fileMediaRepository: _fileMediaRepository,
         ),
       );
       toDelete = await lower + await upper;
@@ -93,6 +102,7 @@ class BackupVerificationService {
           auth: Store.get(StoreKey.accessToken),
           endpoint: Store.get(StoreKey.serverEndpoint),
           rootIsolateToken: isolateToken,
+          fileMediaRepository: _fileMediaRepository,
         ),
       );
     }
@@ -106,12 +116,13 @@ class BackupVerificationService {
       String auth,
       String endpoint,
       RootIsolateToken rootIsolateToken,
+      IFileMediaRepository fileMediaRepository,
     }) tuple,
   ) async {
     assert(tuple.deleteCandidates.length == tuple.originals.length);
     final List<Asset> result = [];
     BackgroundIsolateBinaryMessenger.ensureInitialized(tuple.rootIsolateToken);
-    await PhotoManager.setIgnorePermissionCheck(true);
+    await tuple.fileMediaRepository.enableBackgroundAccess();
     final ApiService apiService = ApiService();
     apiService.setEndpoint(tuple.endpoint);
     apiService.setAccessToken(tuple.auth);
@@ -186,35 +197,6 @@ class BackupVerificationService {
     return bytes.buffer.asUint64List(start);
   }
 
-  static Future<List<Asset>> _getMatches(
-    QueryBuilder<Asset, Asset, QAfterFilterCondition> query,
-    int ownerId,
-    List<Asset> assets,
-    int limit,
-  ) =>
-      query
-          .ownerIdEqualTo(ownerId)
-          .anyOf(
-            assets,
-            (q, Asset a) => q
-                .fileNameEqualTo(a.fileName)
-                .and()
-                .durationInSecondsEqualTo(a.durationInSeconds)
-                .and()
-                .fileCreatedAtBetween(
-                  a.fileCreatedAt.subtract(const Duration(hours: 12)),
-                  a.fileCreatedAt.add(const Duration(hours: 12)),
-                )
-                .and()
-                .not()
-                .checksumEqualTo(a.checksum),
-          )
-          .sortByFileName()
-          .thenByFileCreatedAt()
-          .thenByFileModifiedAt()
-          .limit(limit)
-          .findAll();
-
   static bool _sameExceptTimeZone(DateTime a, DateTime b) {
     final ms = a.isAfter(b)
         ? a.millisecondsSinceEpoch - b.millisecondsSinceEpoch
@@ -227,6 +209,8 @@ class BackupVerificationService {
 
 final backupVerificationServiceProvider = Provider(
   (ref) => BackupVerificationService(
-    ref.watch(dbProvider),
+    ref.watch(fileMediaRepositoryProvider),
+    ref.watch(assetRepositoryProvider),
+    ref.watch(exifInfoRepositoryProvider),
   ),
 );
diff --git a/mobile/lib/services/device.service.dart b/mobile/lib/services/device.service.dart
new file mode 100644
index 0000000000..e1676d5683
--- /dev/null
+++ b/mobile/lib/services/device.service.dart
@@ -0,0 +1,24 @@
+import 'package:flutter_udid/flutter_udid.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/store.entity.dart';
+
+final deviceServiceProvider = Provider((ref) => DeviceService());
+
+class DeviceService {
+  DeviceService();
+
+  createDeviceId() {
+    return FlutterUdid.consistentUdid;
+  }
+
+  /// Returns the device ID from local storage or creates a new one if not found.
+  ///
+  /// This method first attempts to retrieve the device ID from the local store using
+  /// [StoreKey.deviceId]. If no device ID is found (returns null), it generates a
+  /// new device ID by calling [createDeviceId].
+  ///
+  /// Returns a [String] representing the device's unique identifier.
+  String getDeviceId() {
+    return Store.tryGet(StoreKey.deviceId) ?? createDeviceId();
+  }
+}
diff --git a/mobile/lib/services/download.service.dart b/mobile/lib/services/download.service.dart
new file mode 100644
index 0000000000..7cf6f309e9
--- /dev/null
+++ b/mobile/lib/services/download.service.dart
@@ -0,0 +1,230 @@
+import 'dart:io';
+
+import 'package:background_downloader/background_downloader.dart';
+import 'package:flutter/services.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/interfaces/download.interface.dart';
+import 'package:immich_mobile/interfaces/file_media.interface.dart';
+import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
+import 'package:immich_mobile/repositories/download.repository.dart';
+import 'package:immich_mobile/repositories/file_media.repository.dart';
+import 'package:immich_mobile/services/api.service.dart';
+import 'package:immich_mobile/utils/download.dart';
+import 'package:logging/logging.dart';
+
+final downloadServiceProvider = Provider(
+  (ref) => DownloadService(
+    ref.watch(fileMediaRepositoryProvider),
+    ref.watch(downloadRepositoryProvider),
+  ),
+);
+
+class DownloadService {
+  final IDownloadRepository _downloadRepository;
+  final IFileMediaRepository _fileMediaRepository;
+  final Logger _log = Logger("DownloadService");
+  void Function(TaskStatusUpdate)? onImageDownloadStatus;
+  void Function(TaskStatusUpdate)? onVideoDownloadStatus;
+  void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus;
+  void Function(TaskProgressUpdate)? onTaskProgress;
+
+  DownloadService(
+    this._fileMediaRepository,
+    this._downloadRepository,
+  ) {
+    _downloadRepository.onImageDownloadStatus = _onImageDownloadCallback;
+    _downloadRepository.onVideoDownloadStatus = _onVideoDownloadCallback;
+    _downloadRepository.onLivePhotoDownloadStatus =
+        _onLivePhotoDownloadCallback;
+    _downloadRepository.onTaskProgress = _onTaskProgressCallback;
+  }
+
+  void _onTaskProgressCallback(TaskProgressUpdate update) {
+    onTaskProgress?.call(update);
+  }
+
+  void _onImageDownloadCallback(TaskStatusUpdate update) {
+    onImageDownloadStatus?.call(update);
+  }
+
+  void _onVideoDownloadCallback(TaskStatusUpdate update) {
+    onVideoDownloadStatus?.call(update);
+  }
+
+  void _onLivePhotoDownloadCallback(TaskStatusUpdate update) {
+    onLivePhotoDownloadStatus?.call(update);
+  }
+
+  Future<bool> saveImageWithPath(Task task) async {
+    final filePath = await task.filePath();
+    final title = task.filename;
+    final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
+    try {
+      final Asset? resultAsset = await _fileMediaRepository.saveImageWithFile(
+        filePath,
+        title: title,
+        relativePath: relativePath,
+      );
+      return resultAsset != null;
+    } catch (error, stack) {
+      _log.severe("Error saving image", error, stack);
+      return false;
+    } finally {
+      if (await File(filePath).exists()) {
+        await File(filePath).delete();
+      }
+    }
+  }
+
+  Future<bool> saveVideo(Task task) async {
+    final filePath = await task.filePath();
+    final title = task.filename;
+    final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
+    final file = File(filePath);
+    try {
+      final Asset? resultAsset = await _fileMediaRepository.saveVideo(
+        file,
+        title: title,
+        relativePath: relativePath,
+      );
+      return resultAsset != null;
+    } catch (error, stack) {
+      _log.severe("Error saving video", error, stack);
+      return false;
+    } finally {
+      if (await file.exists()) {
+        await file.delete();
+      }
+    }
+  }
+
+  Future<bool> saveLivePhotos(
+    Task task,
+    String livePhotosId,
+  ) async {
+    final records = await _downloadRepository.getLiveVideoTasks();
+    if (records.length < 2) {
+      return false;
+    }
+
+    final imageRecord =
+        _findTaskRecord(records, livePhotosId, LivePhotosPart.image);
+    final videoRecord =
+        _findTaskRecord(records, livePhotosId, LivePhotosPart.video);
+    final imageFilePath = await imageRecord.task.filePath();
+    final videoFilePath = await videoRecord.task.filePath();
+
+    try {
+      final result = await _fileMediaRepository.saveLivePhoto(
+        image: File(imageFilePath),
+        video: File(videoFilePath),
+        title: task.filename,
+      );
+
+      return result != null;
+    } on PlatformException catch (error, stack) {
+      // Handle saving MotionPhotos on iOS
+      if (error.code == 'PHPhotosErrorDomain (-1)') {
+        final result = await _fileMediaRepository
+            .saveImageWithFile(imageFilePath, title: task.filename);
+        return result != null;
+      }
+      _log.severe("Error saving live photo", error, stack);
+      return false;
+    } catch (error, stack) {
+      _log.severe("Error saving live photo", error, stack);
+      return false;
+    } finally {
+      final imageFile = File(imageFilePath);
+      if (await imageFile.exists()) {
+        await imageFile.delete();
+      }
+
+      final videoFile = File(videoFilePath);
+      if (await videoFile.exists()) {
+        await videoFile.delete();
+      }
+
+      await _downloadRepository.deleteRecordsWithIds([
+        imageRecord.task.taskId,
+        videoRecord.task.taskId,
+      ]);
+    }
+  }
+
+  Future<bool> cancelDownload(String id) async {
+    return await FileDownloader().cancelTaskWithId(id);
+  }
+
+  Future<void> download(Asset asset) async {
+    if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) {
+      await _downloadRepository.download(
+        _buildDownloadTask(
+          asset.remoteId!,
+          asset.fileName,
+          group: downloadGroupLivePhoto,
+          metadata: LivePhotosMetadata(
+            part: LivePhotosPart.image,
+            id: asset.remoteId!,
+          ).toJson(),
+        ),
+      );
+
+      await _downloadRepository.download(
+        _buildDownloadTask(
+          asset.livePhotoVideoId!,
+          asset.fileName
+              .toUpperCase()
+              .replaceAll(RegExp(r"\.(JPG|HEIC)$"), '.MOV'),
+          group: downloadGroupLivePhoto,
+          metadata: LivePhotosMetadata(
+            part: LivePhotosPart.video,
+            id: asset.remoteId!,
+          ).toJson(),
+        ),
+      );
+    } else {
+      await _downloadRepository.download(
+        _buildDownloadTask(
+          asset.remoteId!,
+          asset.fileName,
+          group: asset.isImage ? downloadGroupImage : downloadGroupVideo,
+        ),
+      );
+    }
+  }
+
+  DownloadTask _buildDownloadTask(
+    String id,
+    String filename, {
+    String? group,
+    String? metadata,
+  }) {
+    final path = r'/assets/{id}/original'.replaceAll('{id}', id);
+    final serverEndpoint = Store.get(StoreKey.serverEndpoint);
+    final headers = ApiService.getRequestHeaders();
+
+    return DownloadTask(
+      taskId: id,
+      url: serverEndpoint + path,
+      headers: headers,
+      filename: filename,
+      updates: Updates.statusAndProgress,
+      group: group ?? '',
+      metaData: metadata ?? '',
+    );
+  }
+}
+
+TaskRecord _findTaskRecord(
+  List<TaskRecord> records,
+  String livePhotosId,
+  LivePhotosPart part,
+) {
+  return records.firstWhere((record) {
+    final metadata = LivePhotosMetadata.fromJson(record.task.metaData);
+    return metadata.id == livePhotosId && metadata.part == part;
+  });
+}
diff --git a/mobile/lib/services/entity.service.dart b/mobile/lib/services/entity.service.dart
new file mode 100644
index 0000000000..ddbe77f8c9
--- /dev/null
+++ b/mobile/lib/services/entity.service.dart
@@ -0,0 +1,53 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/interfaces/asset.interface.dart';
+import 'package:immich_mobile/interfaces/user.interface.dart';
+import 'package:immich_mobile/repositories/asset.repository.dart';
+import 'package:immich_mobile/repositories/user.repository.dart';
+
+class EntityService {
+  final IAssetRepository _assetRepository;
+  final IUserRepository _userRepository;
+  EntityService(
+    this._assetRepository,
+    this._userRepository,
+  );
+
+  Future<Album> fillAlbumWithDatabaseEntities(Album album) async {
+    final ownerId = album.ownerId;
+    if (ownerId != null) {
+      // replace owner with user from database
+      album.owner.value = await _userRepository.get(ownerId);
+    }
+    final thumbnailAssetId =
+        album.remoteThumbnailAssetId ?? album.thumbnail.value?.remoteId;
+    if (thumbnailAssetId != null) {
+      // set thumbnail with asset from database
+      album.thumbnail.value =
+          await _assetRepository.getByRemoteId(thumbnailAssetId);
+    }
+    if (album.remoteUsers.isNotEmpty) {
+      // replace all users with users from database
+      final users = await _userRepository
+          .getByIds(album.remoteUsers.map((user) => user.id).toList());
+      album.sharedUsers.clear();
+      album.sharedUsers.addAll(users);
+      album.shared = true;
+    }
+    if (album.remoteAssets.isNotEmpty) {
+      // replace all assets with assets from database
+      final assets = await _assetRepository
+          .getAllByRemoteId(album.remoteAssets.map((asset) => asset.remoteId!));
+      album.assets.clear();
+      album.assets.addAll(assets);
+    }
+    return album;
+  }
+}
+
+final entityServiceProvider = Provider(
+  (ref) => EntityService(
+    ref.watch(assetRepositoryProvider),
+    ref.watch(userRepositoryProvider),
+  ),
+);
diff --git a/mobile/lib/services/hash.service.dart b/mobile/lib/services/hash.service.dart
index ffc81a3445..bb19340d2f 100644
--- a/mobile/lib/services/hash.service.dart
+++ b/mobile/lib/services/hash.service.dart
@@ -2,70 +2,92 @@ import 'dart:io';
 
 import 'package:flutter/foundation.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/interfaces/album_media.interface.dart';
+import 'package:immich_mobile/interfaces/asset.interface.dart';
+import 'package:immich_mobile/repositories/album_media.repository.dart';
+import 'package:immich_mobile/repositories/asset.repository.dart';
 import 'package:immich_mobile/services/background.service.dart';
 import 'package:immich_mobile/entities/android_device_asset.entity.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/entities/device_asset.entity.dart';
 import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
-import 'package:immich_mobile/providers/db.provider.dart';
 import 'package:immich_mobile/extensions/string_extensions.dart';
-import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
-import 'package:photo_manager/photo_manager.dart';
 
 class HashService {
-  HashService(this._db, this._backgroundService);
-  final Isar _db;
+  HashService(
+    this._assetRepository,
+    this._backgroundService,
+    this._albumMediaRepository,
+  );
+  final IAssetRepository _assetRepository;
   final BackgroundService _backgroundService;
+  final IAlbumMediaRepository _albumMediaRepository;
   final _log = Logger('HashService');
 
   /// Returns all assets that were successfully hashed
   Future<List<Asset>> getHashedAssets(
-    AssetPathEntity album, {
+    Album album, {
     int start = 0,
     int end = 0x7fffffffffffffff,
+    DateTime? modifiedFrom,
+    DateTime? modifiedUntil,
     Set<String>? excludedAssets,
   }) async {
-    final entities = await album.getAssetListRange(start: start, end: end);
+    final entities = await _albumMediaRepository.getAssets(
+      album.localId!,
+      start: start,
+      end: end,
+      modifiedFrom: modifiedFrom,
+      modifiedUntil: modifiedUntil,
+    );
     final filtered = excludedAssets == null
         ? entities
-        : entities.where((e) => !excludedAssets.contains(e.id)).toList();
+        : entities.where((e) => !excludedAssets.contains(e.localId!)).toList();
     return _hashAssets(filtered);
   }
 
-  /// Converts a list of [AssetEntity]s to [Asset]s including only those
+  /// Processes a list of local [Asset]s, storing their hash and returning only those
   /// that were successfully hashed. Hashes are looked up in a DB table
   /// [AndroidDeviceAsset] / [IOSDeviceAsset] by local id. Only missing
   /// entries are newly hashed and added to the DB table.
-  Future<List<Asset>> _hashAssets(List<AssetEntity> assetEntities) async {
+  Future<List<Asset>> _hashAssets(List<Asset> assets) async {
     const int batchFileCount = 128;
     const int batchDataSize = 1024 * 1024 * 1024; // 1GB
 
-    final ids = assetEntities
-        .map(Platform.isAndroid ? (a) => a.id.toInt() : (a) => a.id)
+    final ids = assets
+        .map(Platform.isAndroid ? (a) => a.localId!.toInt() : (a) => a.localId!)
         .toList();
-    final List<DeviceAsset?> hashes = await _lookupHashes(ids);
+    final List<DeviceAsset?> hashes =
+        await _assetRepository.getDeviceAssetsById(ids);
     final List<DeviceAsset> toAdd = [];
     final List<String> toHash = [];
 
     int bytes = 0;
 
-    for (int i = 0; i < assetEntities.length; i++) {
+    for (int i = 0; i < assets.length; i++) {
       if (hashes[i] != null) {
         continue;
       }
-      final file = await assetEntities[i].originFile;
-      if (file == null) {
-        final fileName = await assetEntities[i].titleAsync.catchError((error) {
-          _log.warning(
-            "Failed to get title for asset ${assetEntities[i].id}",
-          );
 
-          return "";
-        });
+      File? file;
+
+      try {
+        file = await assets[i].local!.originFile;
+      } catch (error, stackTrace) {
+        _log.warning(
+          "Error getting file to hash for asset ${assets[i].localId}, name: ${assets[i].fileName}, created on: ${assets[i].fileCreatedAt}, skipping",
+          error,
+          stackTrace,
+        );
+      }
+
+      if (file == null) {
+        final fileName = assets[i].fileName;
 
         _log.warning(
-          "Failed to get file for asset ${assetEntities[i].id}, name: $fileName, created on: ${assetEntities[i].createDateTime}, skipping",
+          "Failed to get file for asset ${assets[i].localId}, name: $fileName, created on: ${assets[i].fileCreatedAt}, skipping",
         );
         continue;
       }
@@ -86,15 +108,9 @@ class HashService {
     if (toHash.isNotEmpty) {
       await _processBatch(toHash, toAdd);
     }
-    return _mapAllHashedAssets(assetEntities, hashes);
+    return _getHashedAssets(assets, hashes);
   }
 
-  /// Lookup hashes of assets by their local ID
-  Future<List<DeviceAsset?>> _lookupHashes(List<Object> ids) =>
-      Platform.isAndroid
-          ? _db.androidDeviceAssets.getAll(ids.cast())
-          : _db.iOSDeviceAssets.getAllById(ids.cast());
-
   /// Processes a batch of files and saves any successfully hashed
   /// values to the DB table.
   Future<void> _processBatch(
@@ -114,11 +130,9 @@ class HashService {
     final validHashes = anyNull
         ? toAdd.where((e) => e.hash.length == 20).toList(growable: false)
         : toAdd;
-    await _db.writeTxn(
-      () => Platform.isAndroid
-          ? _db.androidDeviceAssets.putAll(validHashes.cast())
-          : _db.iOSDeviceAssets.putAll(validHashes.cast()),
-    );
+
+    await _assetRepository
+        .transaction(() => _assetRepository.upsertDeviceAssets(validHashes));
     _log.fine("Hashed ${validHashes.length}/${toHash.length} assets");
   }
 
@@ -133,15 +147,16 @@ class HashService {
     return hashes;
   }
 
-  /// Converts [AssetEntity]s that were successfully hashed to [Asset]s
-  List<Asset> _mapAllHashedAssets(
-    List<AssetEntity> assets,
+  /// Returns all successfully hashed [Asset]s with their hash value set
+  List<Asset> _getHashedAssets(
+    List<Asset> assets,
     List<DeviceAsset?> hashes,
   ) {
     final List<Asset> result = [];
     for (int i = 0; i < assets.length; i++) {
       if (hashes[i] != null && hashes[i]!.hash.isNotEmpty) {
-        result.add(Asset.local(assets[i], hashes[i]!.hash));
+        assets[i].byteHash = hashes[i]!.hash;
+        result.add(assets[i]);
       }
     }
     return result;
@@ -150,7 +165,8 @@ class HashService {
 
 final hashServiceProvider = Provider(
   (ref) => HashService(
-    ref.watch(dbProvider),
+    ref.watch(assetRepositoryProvider),
     ref.watch(backgroundServiceProvider),
+    ref.watch(albumMediaRepositoryProvider),
   ),
 );
diff --git a/mobile/lib/services/image_viewer.service.dart b/mobile/lib/services/image_viewer.service.dart
deleted file mode 100644
index 9bcaba1d26..0000000000
--- a/mobile/lib/services/image_viewer.service.dart
+++ /dev/null
@@ -1,114 +0,0 @@
-import 'dart:io';
-
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/extensions/response_extensions.dart';
-import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:immich_mobile/providers/api.provider.dart';
-import 'package:immich_mobile/services/api.service.dart';
-import 'package:logging/logging.dart';
-
-import 'package:photo_manager/photo_manager.dart';
-import 'package:path_provider/path_provider.dart';
-
-final imageViewerServiceProvider =
-    Provider((ref) => ImageViewerService(ref.watch(apiServiceProvider)));
-
-class ImageViewerService {
-  final ApiService _apiService;
-  final Logger _log = Logger("ImageViewerService");
-
-  ImageViewerService(this._apiService);
-
-  Future<bool> downloadAsset(Asset asset) async {
-    File? imageFile;
-    File? videoFile;
-    try {
-      // Download LivePhotos image and motion part
-      if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) {
-        var imageResponse =
-            await _apiService.assetsApi.downloadAssetWithHttpInfo(
-          asset.remoteId!,
-        );
-
-        var motionResponse =
-            await _apiService.assetsApi.downloadAssetWithHttpInfo(
-          asset.livePhotoVideoId!,
-        );
-
-        if (imageResponse.statusCode != 200 ||
-            motionResponse.statusCode != 200) {
-          final failedResponse =
-              imageResponse.statusCode != 200 ? imageResponse : motionResponse;
-          _log.severe(
-            "Motion asset download failed",
-            failedResponse.toLoggerString(),
-          );
-          return false;
-        }
-
-        AssetEntity? entity;
-
-        final tempDir = await getTemporaryDirectory();
-        videoFile = await File('${tempDir.path}/livephoto.mov').create();
-        imageFile = await File('${tempDir.path}/livephoto.heic').create();
-        videoFile.writeAsBytesSync(motionResponse.bodyBytes);
-        imageFile.writeAsBytesSync(imageResponse.bodyBytes);
-
-        entity = await PhotoManager.editor.darwin.saveLivePhoto(
-          imageFile: imageFile,
-          videoFile: videoFile,
-          title: asset.fileName,
-        );
-
-        if (entity == null) {
-          _log.warning(
-            "Asset cannot be saved as a live photo. This is most likely a motion photo. Saving only the image file",
-          );
-
-          entity = await PhotoManager.editor.saveImage(
-            imageResponse.bodyBytes,
-            title: asset.fileName,
-          );
-        }
-
-        return entity != null;
-      } else {
-        var res = await _apiService.assetsApi
-            .downloadAssetWithHttpInfo(asset.remoteId!);
-
-        if (res.statusCode != 200) {
-          _log.severe("Asset download failed", res.toLoggerString());
-          return false;
-        }
-
-        final AssetEntity? entity;
-        final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
-
-        if (asset.isImage) {
-          entity = await PhotoManager.editor.saveImage(
-            res.bodyBytes,
-            title: asset.fileName,
-            relativePath: relativePath,
-          );
-        } else {
-          final tempDir = await getTemporaryDirectory();
-          videoFile = await File('${tempDir.path}/${asset.fileName}').create();
-          videoFile.writeAsBytesSync(res.bodyBytes);
-          entity = await PhotoManager.editor.saveVideo(
-            videoFile,
-            title: asset.fileName,
-            relativePath: relativePath,
-          );
-        }
-        return entity != null;
-      }
-    } catch (error, stack) {
-      _log.severe("Error saving downloaded asset", error, stack);
-      return false;
-    } finally {
-      // Clear temp files
-      imageFile?.delete();
-      videoFile?.delete();
-    }
-  }
-}
diff --git a/mobile/lib/services/memory.service.dart b/mobile/lib/services/memory.service.dart
index ea07f7c019..b95899df67 100644
--- a/mobile/lib/services/memory.service.dart
+++ b/mobile/lib/services/memory.service.dart
@@ -1,18 +1,17 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/interfaces/asset.interface.dart';
 import 'package:immich_mobile/models/memories/memory.model.dart';
 import 'package:immich_mobile/providers/api.provider.dart';
-import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/repositories/asset.repository.dart';
 import 'package:immich_mobile/services/api.service.dart';
-import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
 import 'package:openapi/api.dart';
 
 final memoryServiceProvider = StateProvider<MemoryService>((ref) {
   return MemoryService(
     ref.watch(apiServiceProvider),
-    ref.watch(dbProvider),
+    ref.watch(assetRepositoryProvider),
   );
 });
 
@@ -20,9 +19,9 @@ class MemoryService {
   final log = Logger("MemoryService");
 
   final ApiService _apiService;
-  final Isar _db;
+  final IAssetRepository _assetRepository;
 
-  MemoryService(this._apiService, this._db);
+  MemoryService(this._apiService, this._assetRepository);
 
   Future<List<Memory>?> getMemoryLane() async {
     try {
@@ -39,7 +38,7 @@ class MemoryService {
       List<Memory> memories = [];
       for (final MemoryLaneResponseDto(:yearsAgo, :assets) in data) {
         final dbAssets =
-            await _db.assets.getAllByRemoteId(assets.map((e) => e.id));
+            await _assetRepository.getAllByRemoteId(assets.map((e) => e.id));
         if (dbAssets.isNotEmpty) {
           final String title = yearsAgo <= 1
               ? 'memories_year_ago'.tr()
diff --git a/mobile/lib/services/network.service.dart b/mobile/lib/services/network.service.dart
new file mode 100644
index 0000000000..f2d2de325d
--- /dev/null
+++ b/mobile/lib/services/network.service.dart
@@ -0,0 +1,47 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/interfaces/network.interface.dart';
+import 'package:immich_mobile/repositories/network.repository.dart';
+import 'package:immich_mobile/repositories/permission.repository.dart';
+
+final networkServiceProvider = Provider((ref) {
+  return NetworkService(
+    ref.watch(networkRepositoryProvider),
+    ref.watch(permissionRepositoryProvider),
+  );
+});
+
+class NetworkService {
+  final INetworkRepository _repository;
+  final IPermissionRepository _permissionRepository;
+
+  NetworkService(this._repository, this._permissionRepository);
+
+  Future<bool> getLocationWhenInUserPermission() {
+    return _permissionRepository.hasLocationWhenInUsePermission();
+  }
+
+  Future<bool> requestLocationWhenInUsePermission() {
+    return _permissionRepository.requestLocationWhenInUsePermission();
+  }
+
+  Future<bool> getLocationAlwaysPermission() {
+    return _permissionRepository.hasLocationAlwaysPermission();
+  }
+
+  Future<bool> requestLocationAlwaysPermission() {
+    return _permissionRepository.requestLocationAlwaysPermission();
+  }
+
+  Future<String?> getWifiName() async {
+    final canRead = await getLocationWhenInUserPermission();
+    if (!canRead) {
+      return null;
+    }
+
+    return await _repository.getWifiName();
+  }
+
+  Future<bool> openSettings() {
+    return _permissionRepository.openSettings();
+  }
+}
diff --git a/mobile/lib/services/partner.service.dart b/mobile/lib/services/partner.service.dart
index 8cd2fe424f..67d7f4e1d1 100644
--- a/mobile/lib/services/partner.service.dart
+++ b/mobile/lib/services/partner.service.dart
@@ -1,43 +1,33 @@
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/entities/user.entity.dart';
-import 'package:immich_mobile/providers/api.provider.dart';
-import 'package:immich_mobile/providers/db.provider.dart';
-import 'package:immich_mobile/services/api.service.dart';
-import 'package:isar/isar.dart';
+import 'package:immich_mobile/interfaces/partner_api.interface.dart';
+import 'package:immich_mobile/interfaces/user.interface.dart';
+import 'package:immich_mobile/repositories/partner_api.repository.dart';
+import 'package:immich_mobile/repositories/user.repository.dart';
 import 'package:logging/logging.dart';
-import 'package:openapi/api.dart';
 
 final partnerServiceProvider = Provider(
   (ref) => PartnerService(
-    ref.watch(apiServiceProvider),
-    ref.watch(dbProvider),
+    ref.watch(partnerApiRepositoryProvider),
+    ref.watch(userRepositoryProvider),
   ),
 );
 
 class PartnerService {
-  final ApiService _apiService;
-  final Isar _db;
+  final IPartnerApiRepository _partnerApiRepository;
+  final IUserRepository _userRepository;
   final Logger _log = Logger("PartnerService");
 
-  PartnerService(this._apiService, this._db);
-
-  Future<List<User>?> getPartners(PartnerDirection direction) async {
-    try {
-      final userDtos = await _apiService.partnersApi.getPartners(direction);
-      if (userDtos != null) {
-        return userDtos.map((u) => User.fromPartnerDto(u)).toList();
-      }
-    } catch (e) {
-      _log.warning("Failed to get partners for direction $direction", e);
-    }
-    return null;
-  }
+  PartnerService(
+    this._partnerApiRepository,
+    this._userRepository,
+  );
 
   Future<bool> removePartner(User partner) async {
     try {
-      await _apiService.partnersApi.removePartner(partner.id);
+      await _partnerApiRepository.delete(partner.id);
       partner.isPartnerSharedBy = false;
-      await _db.writeTxn(() => _db.users.put(partner));
+      await _userRepository.update(partner);
     } catch (e) {
       _log.warning("Failed to remove partner ${partner.id}", e);
       return false;
@@ -47,12 +37,10 @@ class PartnerService {
 
   Future<bool> addPartner(User partner) async {
     try {
-      final dto = await _apiService.partnersApi.createPartner(partner.id);
-      if (dto != null) {
-        partner.isPartnerSharedBy = true;
-        await _db.writeTxn(() => _db.users.put(partner));
-        return true;
-      }
+      await _partnerApiRepository.create(partner.id);
+      partner.isPartnerSharedBy = true;
+      await _userRepository.update(partner);
+      return true;
     } catch (e) {
       _log.warning("Failed to add partner ${partner.id}", e);
     }
@@ -61,13 +49,13 @@ class PartnerService {
 
   Future<bool> updatePartner(User partner, {required bool inTimeline}) async {
     try {
-      final dto = await _apiService.partnersApi
-          .updatePartner(partner.id, UpdatePartnerDto(inTimeline: inTimeline));
-      if (dto != null) {
-        partner.inTimeline = dto.inTimeline ?? partner.inTimeline;
-        await _db.writeTxn(() => _db.users.put(partner));
-        return true;
-      }
+      final dto = await _partnerApiRepository.update(
+        partner.id,
+        inTimeline: inTimeline,
+      );
+      partner.inTimeline = dto.inTimeline;
+      await _userRepository.update(partner);
+      return true;
     } catch (e) {
       _log.warning("Failed to update partner ${partner.id}", e);
     }
diff --git a/mobile/lib/services/person.service.dart b/mobile/lib/services/person.service.dart
index ddb61f5e48..5b325acdc5 100644
--- a/mobile/lib/services/person.service.dart
+++ b/mobile/lib/services/person.service.dart
@@ -1,29 +1,37 @@
 import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:immich_mobile/providers/api.provider.dart';
-import 'package:immich_mobile/providers/db.provider.dart';
-import 'package:immich_mobile/services/api.service.dart';
-import 'package:isar/isar.dart';
+import 'package:immich_mobile/interfaces/asset.interface.dart';
+import 'package:immich_mobile/interfaces/asset_api.interface.dart';
+import 'package:immich_mobile/interfaces/person_api.interface.dart';
+import 'package:immich_mobile/repositories/asset.repository.dart';
+import 'package:immich_mobile/repositories/asset_api.repository.dart';
+import 'package:immich_mobile/repositories/person_api.repository.dart';
 import 'package:logging/logging.dart';
-import 'package:openapi/api.dart';
 import 'package:riverpod_annotation/riverpod_annotation.dart';
 
 part 'person.service.g.dart';
 
 @riverpod
-PersonService personService(PersonServiceRef ref) =>
-    PersonService(ref.read(apiServiceProvider), ref.read(dbProvider));
+PersonService personService(PersonServiceRef ref) => PersonService(
+      ref.watch(personApiRepositoryProvider),
+      ref.watch(assetApiRepositoryProvider),
+      ref.read(assetRepositoryProvider),
+    );
 
 class PersonService {
   final Logger _log = Logger("PersonService");
-  final ApiService _apiService;
-  final Isar _db;
+  final IPersonApiRepository _personApiRepository;
+  final IAssetApiRepository _assetApiRepository;
+  final IAssetRepository _assetRepository;
 
-  PersonService(this._apiService, this._db);
+  PersonService(
+    this._personApiRepository,
+    this._assetApiRepository,
+    this._assetRepository,
+  );
 
-  Future<List<PersonResponseDto>> getAllPeople() async {
+  Future<List<Person>> getAllPeople() async {
     try {
-      final peopleResponseDto = await _apiService.peopleApi.getAllPeople();
-      return peopleResponseDto?.people ?? [];
+      return await _personApiRepository.getAll();
     } catch (error, stack) {
       _log.severe("Error while fetching curated people", error, stack);
       return [];
@@ -31,50 +39,19 @@ class PersonService {
   }
 
   Future<List<Asset>> getPersonAssets(String id) async {
-    List<Asset> result = [];
-    var hasNext = true;
-    var currentPage = 1;
-
     try {
-      while (hasNext) {
-        final response = await _apiService.searchApi.searchMetadata(
-          MetadataSearchDto(
-            personIds: [id],
-            page: currentPage,
-            size: 1000,
-          ),
-        );
-
-        if (response == null) {
-          break;
-        }
-
-        if (response.assets.nextPage == null) {
-          hasNext = false;
-        }
-
-        final assets = response.assets.items;
-        final mapAssets =
-            await _db.assets.getAllByRemoteId(assets.map((e) => e.id));
-        result.addAll(mapAssets);
-
-        currentPage++;
-      }
+      final assets = await _assetApiRepository.search(personIds: [id]);
+      return await _assetRepository
+          .getAllByRemoteId(assets.map((a) => a.remoteId!));
     } catch (error, stack) {
       _log.severe("Error while fetching person assets", error, stack);
     }
-
-    return result;
+    return [];
   }
 
-  Future<PersonResponseDto?> updateName(String id, String name) async {
+  Future<Person?> updateName(String id, String name) async {
     try {
-      return await _apiService.peopleApi.updatePerson(
-        id,
-        PersonUpdateDto(
-          name: name,
-        ),
-      );
+      return await _personApiRepository.update(id, name: name);
     } catch (error, stack) {
       _log.severe("Error while updating person name", error, stack);
     }
diff --git a/mobile/lib/services/person.service.g.dart b/mobile/lib/services/person.service.g.dart
index 01a5ed8f30..9a24069fbf 100644
Binary files a/mobile/lib/services/person.service.g.dart and b/mobile/lib/services/person.service.g.dart differ
diff --git a/mobile/lib/services/search.service.dart b/mobile/lib/services/search.service.dart
index cf3905e5ca..14e53c3ce4 100644
--- a/mobile/lib/services/search.service.dart
+++ b/mobile/lib/services/search.service.dart
@@ -1,27 +1,29 @@
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/string_extensions.dart';
+import 'package:immich_mobile/interfaces/asset.interface.dart';
 import 'package:immich_mobile/models/search/search_filter.model.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/models/search/search_result.model.dart';
 import 'package:immich_mobile/providers/api.provider.dart';
-import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/repositories/asset.repository.dart';
 import 'package:immich_mobile/services/api.service.dart';
-import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
 import 'package:openapi/api.dart';
 
 final searchServiceProvider = Provider(
   (ref) => SearchService(
     ref.watch(apiServiceProvider),
-    ref.watch(dbProvider),
+    ref.watch(assetRepositoryProvider),
   ),
 );
 
 class SearchService {
   final ApiService _apiService;
-  final Isar _db;
+  final IAssetRepository _assetRepository;
 
   final _log = Logger("SearchService");
-  SearchService(this._apiService, this._db);
+  SearchService(this._apiService, this._assetRepository);
 
   Future<List<String>?> getSearchSuggestions(
     SearchSuggestionType type, {
@@ -44,7 +46,7 @@ class SearchService {
     }
   }
 
-  Future<List<Asset>?> search(SearchFilter filter, int page) async {
+  Future<SearchResult?> search(SearchFilter filter, int page) async {
     try {
       SearchResponseDto? response;
       AssetTypeEnum? type;
@@ -75,7 +77,7 @@ class SearchService {
           ),
         );
       } else {
-        response = await _apiService.searchApi.searchMetadata(
+        response = await _apiService.searchApi.searchAssets(
           MetadataSearchDto(
             originalFileName:
                 filter.filename != null && filter.filename!.isNotEmpty
@@ -103,8 +105,12 @@ class SearchService {
         return null;
       }
 
-      return _db.assets
-          .getAllByRemoteId(response.assets.items.map((e) => e.id));
+      return SearchResult(
+        assets: await _assetRepository.getAllByRemoteId(
+          response.assets.items.map((e) => e.id),
+        ),
+        nextPage: response.assets.nextPage?.toInt(),
+      );
     } catch (error, stackTrace) {
       _log.severe("Failed to search for assets", error, stackTrace);
     }
diff --git a/mobile/lib/services/share.service.dart b/mobile/lib/services/share.service.dart
index 40a2e0402b..d44d75931d 100644
--- a/mobile/lib/services/share.service.dart
+++ b/mobile/lib/services/share.service.dart
@@ -64,10 +64,13 @@ class ShareService {
         );
       }
 
-      final box = context.findRenderObject() as RenderBox?;
+      final size = MediaQuery.of(context).size;
       Share.shareXFiles(
         downloadedXFiles,
-        sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
+        sharePositionOrigin: Rect.fromPoints(
+          Offset.zero,
+          Offset(size.width / 3, size.height),
+        ),
       );
       return true;
     } catch (error) {
diff --git a/mobile/lib/services/stack.service.dart b/mobile/lib/services/stack.service.dart
index 75074101c2..1ca56ff279 100644
--- a/mobile/lib/services/stack.service.dart
+++ b/mobile/lib/services/stack.service.dart
@@ -1,17 +1,17 @@
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/interfaces/asset.interface.dart';
 import 'package:immich_mobile/providers/api.provider.dart';
-import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/repositories/asset.repository.dart';
 import 'package:immich_mobile/services/api.service.dart';
-import 'package:isar/isar.dart';
 import 'package:openapi/api.dart';
 
 class StackService {
-  StackService(this._api, this._db);
+  StackService(this._api, this._assetRepository);
 
   final ApiService _api;
-  final Isar _db;
+  final IAssetRepository _assetRepository;
 
   Future<StackResponseDto?> getStack(String stackId) async {
     try {
@@ -61,10 +61,8 @@ class StackService {
 
         removeAssets.add(asset);
       }
-
-      _db.writeTxn(() async {
-        await _db.assets.putAll(removeAssets);
-      });
+      await _assetRepository
+          .transaction(() => _assetRepository.updateAll(removeAssets));
     } catch (error) {
       debugPrint("Error while deleting stack: $error");
     }
@@ -74,6 +72,6 @@ class StackService {
 final stackServiceProvider = Provider(
   (ref) => StackService(
     ref.watch(apiServiceProvider),
-    ref.watch(dbProvider),
+    ref.watch(assetRepositoryProvider),
   ),
 );
diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart
index 8ec56e925f..086ec097d1 100644
--- a/mobile/lib/services/sync.service.dart
+++ b/mobile/lib/services/sync.service.dart
@@ -5,31 +5,67 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/entities/album.entity.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/entities/etag.entity.dart';
-import 'package:immich_mobile/entities/exif_info.entity.dart';
-import 'package:immich_mobile/entities/store.entity.dart';
 import 'package:immich_mobile/entities/user.entity.dart';
-import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/interfaces/album.interface.dart';
+import 'package:immich_mobile/interfaces/album_api.interface.dart';
+import 'package:immich_mobile/interfaces/album_media.interface.dart';
+import 'package:immich_mobile/interfaces/asset.interface.dart';
+import 'package:immich_mobile/interfaces/etag.interface.dart';
+import 'package:immich_mobile/interfaces/exif_info.interface.dart';
+import 'package:immich_mobile/interfaces/user.interface.dart';
+import 'package:immich_mobile/repositories/album.repository.dart';
+import 'package:immich_mobile/repositories/album_api.repository.dart';
+import 'package:immich_mobile/repositories/album_media.repository.dart';
+import 'package:immich_mobile/repositories/asset.repository.dart';
+import 'package:immich_mobile/repositories/etag.repository.dart';
+import 'package:immich_mobile/repositories/exif_info.repository.dart';
+import 'package:immich_mobile/repositories/user.repository.dart';
+import 'package:immich_mobile/services/entity.service.dart';
 import 'package:immich_mobile/services/hash.service.dart';
 import 'package:immich_mobile/utils/async_mutex.dart';
 import 'package:immich_mobile/extensions/collection_extensions.dart';
 import 'package:immich_mobile/utils/datetime_comparison.dart';
 import 'package:immich_mobile/utils/diff.dart';
-import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
-import 'package:openapi/api.dart';
-import 'package:photo_manager/photo_manager.dart';
 
 final syncServiceProvider = Provider(
-  (ref) => SyncService(ref.watch(dbProvider), ref.watch(hashServiceProvider)),
+  (ref) => SyncService(
+    ref.watch(hashServiceProvider),
+    ref.watch(entityServiceProvider),
+    ref.watch(albumMediaRepositoryProvider),
+    ref.watch(albumApiRepositoryProvider),
+    ref.watch(albumRepositoryProvider),
+    ref.watch(assetRepositoryProvider),
+    ref.watch(exifInfoRepositoryProvider),
+    ref.watch(userRepositoryProvider),
+    ref.watch(etagRepositoryProvider),
+  ),
 );
 
 class SyncService {
-  final Isar _db;
   final HashService _hashService;
+  final EntityService _entityService;
+  final IAlbumMediaRepository _albumMediaRepository;
+  final IAlbumApiRepository _albumApiRepository;
+  final IAlbumRepository _albumRepository;
+  final IAssetRepository _assetRepository;
+  final IExifInfoRepository _exifInfoRepository;
+  final IUserRepository _userRepository;
+  final IETagRepository _eTagRepository;
   final AsyncMutex _lock = AsyncMutex();
   final Logger _log = Logger('SyncService');
 
-  SyncService(this._db, this._hashService);
+  SyncService(
+    this._hashService,
+    this._entityService,
+    this._albumMediaRepository,
+    this._albumApiRepository,
+    this._albumRepository,
+    this._assetRepository,
+    this._exifInfoRepository,
+    this._userRepository,
+    this._eTagRepository,
+  );
 
   // public methods:
 
@@ -59,16 +95,14 @@ class SyncService {
   /// Syncs remote albums to the database
   /// returns `true` if there were any changes
   Future<bool> syncRemoteAlbumsToDb(
-    List<AlbumResponseDto> remote, {
-    required bool isShared,
-    required FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
-  }) =>
-      _lock.run(() => _syncRemoteAlbumsToDb(remote, isShared, loadDetails));
+    List<Album> remote,
+  ) =>
+      _lock.run(() => _syncRemoteAlbumsToDb(remote));
 
   /// Syncs all device albums and their assets to the database
   /// Returns `true` if there were any changes
   Future<bool> syncLocalAlbumAssetsToDb(
-    List<AssetPathEntity> onDevice, [
+    List<Album> onDevice, [
     Set<String>? excludedAssets,
   ]) =>
       _lock.run(() => _syncLocalAlbumAssetsToDb(onDevice, excludedAssets));
@@ -102,8 +136,7 @@ class SyncService {
   /// Returns `true`if there were any changes
   Future<bool> _syncUsersFromServer(List<User> users) async {
     users.sortBy((u) => u.id);
-    final dbUsers = await _db.users.where().sortById().findAll();
-    assert(dbUsers.isSortedBy((u) => u.id), "dbUsers not sorted!");
+    final dbUsers = await _userRepository.getAll(sortBy: UserSort.id);
     final List<int> toDelete = [];
     final List<User> toUpsert = [];
     final changes = diffSortedListsSync(
@@ -124,9 +157,9 @@ class SyncService {
       onlySecond: (User b) => toDelete.add(b.isarId),
     );
     if (changes) {
-      await _db.writeTxn(() async {
-        await _db.users.deleteAll(toDelete);
-        await _db.users.putAll(toUpsert);
+      await _userRepository.transaction(() async {
+        await _userRepository.deleteById(toDelete);
+        await _userRepository.upsertAll(toUpsert);
       });
     }
     return changes;
@@ -135,15 +168,15 @@ class SyncService {
   /// Syncs a new asset to the db. Returns `true` if successful
   Future<bool> _syncNewAssetToDb(Asset a) async {
     final Asset? inDb =
-        await _db.assets.getByOwnerIdChecksum(a.ownerId, a.checksum);
+        await _assetRepository.getByOwnerIdChecksum(a.ownerId, a.checksum);
     if (inDb != null) {
       // unify local/remote assets by replacing the
       // local-only asset in the DB with a local&remote asset
       a = inDb.updatedCopy(a);
     }
     try {
-      await _db.writeTxn(() => a.put(_db));
-    } on IsarError catch (e) {
+      await _assetRepository.update(a);
+    } catch (e) {
       _log.severe("Failed to put new asset into db", e);
       return false;
     }
@@ -158,9 +191,9 @@ class SyncService {
       DateTime since,
     ) getChangedAssets,
   ) async {
-    final currentUser = Store.get(StoreKey.currentUser);
+    final currentUser = await _userRepository.me();
     final DateTime? since =
-        _db.eTags.getSync(currentUser.isarId)?.time?.toUtc();
+        (await _eTagRepository.get(currentUser.isarId))?.time?.toUtc();
     if (since == null) return null;
     final DateTime now = DateTime.now();
     final (toUpsert, toDelete) = await getChangedAssets(users, since);
@@ -181,7 +214,7 @@ class SyncService {
         return true;
       }
       return false;
-    } on IsarError catch (e) {
+    } catch (e) {
       _log.severe("Failed to sync remote assets to db", e);
     }
     return null;
@@ -189,23 +222,21 @@ class SyncService {
 
   /// Deletes remote-only assets, updates merged assets to be local-only
   Future<void> handleRemoteAssetRemoval(List<String> idsToDelete) {
-    return _db.writeTxn(() async {
-      final idsToRemove = await _db.assets
-          .remote(idsToDelete)
-          .filter()
-          .localIdIsNull()
-          .idProperty()
-          .findAll();
-      await _db.assets.deleteAll(idsToRemove);
-      await _db.exifInfos.deleteAll(idsToRemove);
-      final onlyLocal = await _db.assets.remote(idsToDelete).findAll();
-      if (onlyLocal.isNotEmpty) {
-        for (final Asset a in onlyLocal) {
-          a.remoteId = null;
-          a.isTrashed = false;
-        }
-        await _db.assets.putAll(onlyLocal);
+    return _assetRepository.transaction(() async {
+      await _assetRepository.deleteAllByRemoteId(
+        idsToDelete,
+        state: AssetState.remote,
+      );
+      final merged = await _assetRepository.getAllByRemoteId(
+        idsToDelete,
+        state: AssetState.merged,
+      );
+      if (merged.isEmpty) return;
+      for (final Asset asset in merged) {
+        asset.remoteId = null;
+        asset.isTrashed = false;
       }
+      await _assetRepository.updateAll(merged);
     });
   }
 
@@ -220,12 +251,7 @@ class SyncService {
       return false;
     }
     await _syncUsersFromServer(serverUsers);
-    final List<User> users = await _db.users
-        .filter()
-        .isPartnerSharedWithEqualTo(true)
-        .or()
-        .isarIdEqualTo(Store.get(StoreKey.currentUser).isarId)
-        .findAll();
+    final List<User> users = await _userRepository.getAllAccessible();
     bool changes = false;
     for (User u in users) {
       changes |= await _syncRemoteAssetsForUser(u, loadAssets);
@@ -242,11 +268,10 @@ class SyncService {
     if (remote == null) {
       return false;
     }
-    final List<Asset> inDb = await _db.assets
-        .where()
-        .ownerIdEqualToAnyChecksum(user.isarId)
-        .sortByChecksum()
-        .findAll();
+    final List<Asset> inDb = await _assetRepository.getAll(
+      ownerId: user.isarId,
+      sortBy: AssetSort.checksum,
+    );
     assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
 
     remote.sort(Asset.compareByChecksum);
@@ -261,9 +286,9 @@ class SyncService {
     }
     final idsToDelete = toRemove.map((e) => e.id).toList();
     try {
-      await _db.writeTxn(() => _db.assets.deleteAll(idsToDelete));
+      await _assetRepository.deleteById(idsToDelete);
       await upsertAssetsWithExif(toAdd + toUpdate);
-    } on IsarError catch (e) {
+    } catch (e) {
       _log.severe("Failed to sync remote assets to db", e);
     }
     await _updateUserAssetsETag([user], now);
@@ -272,55 +297,44 @@ class SyncService {
 
   Future<void> _updateUserAssetsETag(List<User> users, DateTime time) {
     final etags = users.map((u) => ETag(id: u.id, time: time)).toList();
-    return _db.writeTxn(() => _db.eTags.putAll(etags));
+    return _eTagRepository.upsertAll(etags);
   }
 
   Future<void> _clearUserAssetsETag(List<User> users) {
     final ids = users.map((u) => u.id).toList();
-    return _db.writeTxn(() => _db.eTags.deleteAllById(ids));
+    return _eTagRepository.deleteByIds(ids);
   }
 
   /// Syncs remote albums to the database
   /// returns `true` if there were any changes
   Future<bool> _syncRemoteAlbumsToDb(
-    List<AlbumResponseDto> remote,
-    bool isShared,
-    FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
+    List<Album> remoteAlbums,
   ) async {
-    remote.sortBy((e) => e.id);
+    remoteAlbums.sortBy((e) => e.remoteId!);
 
-    final baseQuery = _db.albums.where().remoteIdIsNotNull().filter();
-    final QueryBuilder<Album, Album, QAfterFilterCondition> query;
-    if (isShared) {
-      query = baseQuery.sharedEqualTo(true);
-    } else {
-      final User me = Store.get(StoreKey.currentUser);
-      query = baseQuery.owner((q) => q.isarIdEqualTo(me.isarId));
-    }
-    final List<Album> dbAlbums = await query.sortByRemoteId().findAll();
-    assert(dbAlbums.isSortedBy((e) => e.remoteId!), "dbAlbums not sorted!");
+    final List<Album> dbAlbums = await _albumRepository.getAll(
+      remote: true,
+      sortBy: AlbumSort.remoteId,
+    );
 
     final List<Asset> toDelete = [];
     final List<Asset> existing = [];
 
     final bool changes = await diffSortedLists(
-      remote,
+      remoteAlbums,
       dbAlbums,
-      compare: (AlbumResponseDto a, Album b) => a.id.compareTo(b.remoteId!),
-      both: (AlbumResponseDto a, Album b) =>
-          _syncRemoteAlbum(a, b, toDelete, existing, loadDetails),
-      onlyFirst: (AlbumResponseDto a) =>
-          _addAlbumFromServer(a, existing, loadDetails),
-      onlySecond: (Album a) => _removeAlbumFromDb(a, toDelete),
+      compare: (remoteAlbum, dbAlbum) =>
+          remoteAlbum.remoteId!.compareTo(dbAlbum.remoteId!),
+      both: (remoteAlbum, dbAlbum) =>
+          _syncRemoteAlbum(remoteAlbum, dbAlbum, toDelete, existing),
+      onlyFirst: (remoteAlbum) => _addAlbumFromServer(remoteAlbum, existing),
+      onlySecond: (dbAlbum) => _removeAlbumFromDb(dbAlbum, toDelete),
     );
 
-    if (isShared && toDelete.isNotEmpty) {
+    if (toDelete.isNotEmpty) {
       final List<int> idsToRemove = sharedAssetsToRemove(toDelete, existing);
       if (idsToRemove.isNotEmpty) {
-        await _db.writeTxn(() async {
-          await _db.assets.deleteAll(idsToRemove);
-          await _db.exifInfos.deleteAll(idsToRemove);
-        });
+        await _assetRepository.deleteById(idsToRemove);
       }
     } else {
       assert(toDelete.isEmpty);
@@ -332,26 +346,25 @@ class SyncService {
   /// syncing changes from local back to server)
   /// accumulates
   Future<bool> _syncRemoteAlbum(
-    AlbumResponseDto dto,
+    Album dto,
     Album album,
     List<Asset> deleteCandidates,
     List<Asset> existing,
-    FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
   ) async {
-    if (!_hasAlbumResponseDtoChanged(dto, album)) {
+    if (!_hasRemoteAlbumChanged(dto, album)) {
       return false;
     }
     // loadDetails (/api/album/:id) will not include lastModifiedAssetTimestamp,
     // i.e. it will always be null. Save it here.
     final originalDto = dto;
-    dto = await loadDetails(dto);
-    if (dto.assetCount != dto.assets.length) {
-      return false;
-    }
-    final assetsInDb =
-        await album.assets.filter().sortByOwnerId().thenByChecksum().findAll();
+    dto = await _albumApiRepository.get(dto.remoteId!);
+
+    final assetsInDb = await _assetRepository.getByAlbum(
+      album,
+      sortBy: AssetSort.ownerIdChecksum,
+    );
     assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!");
-    final List<Asset> assetsOnRemote = dto.getAssets();
+    final List<Asset> assetsOnRemote = dto.remoteAssets.toList();
     assetsOnRemote.sort(Asset.compareByOwnerChecksum);
     final (toAdd, toUpdate, toUnlink) = _diffAssets(
       assetsOnRemote,
@@ -362,15 +375,16 @@ class SyncService {
     // update shared users
     final List<User> sharedUsers = album.sharedUsers.toList(growable: false);
     sharedUsers.sort((a, b) => a.id.compareTo(b.id));
-    dto.albumUsers.sort((a, b) => a.user.id.compareTo(b.user.id));
+    final List<User> users = dto.remoteUsers.toList()
+      ..sort((a, b) => a.id.compareTo(b.id));
     final List<String> userIdsToAdd = [];
     final List<User> usersToUnlink = [];
     diffSortedListsSync(
-      dto.albumUsers,
+      users,
       sharedUsers,
-      compare: (AlbumUserResponseDto a, User b) => a.user.id.compareTo(b.id),
+      compare: (User a, User b) => a.id.compareTo(b.id),
       both: (a, b) => false,
-      onlyFirst: (AlbumUserResponseDto a) => userIdsToAdd.add(a.user.id),
+      onlyFirst: (User a) => userIdsToAdd.add(a.id),
       onlySecond: (User a) => usersToUnlink.add(a),
     );
 
@@ -378,43 +392,46 @@ class SyncService {
     final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd);
     await upsertAssetsWithExif(updated);
     final assetsToLink = existingInDb + updated;
-    final usersToLink = (await _db.users.getAllById(userIdsToAdd)).cast<User>();
+    final usersToLink = await _userRepository.getByIds(userIdsToAdd);
 
-    album.name = dto.albumName;
+    album.name = dto.name;
     album.shared = dto.shared;
     album.createdAt = dto.createdAt;
-    album.modifiedAt = dto.updatedAt;
+    album.modifiedAt = dto.modifiedAt;
     album.startDate = dto.startDate;
     album.endDate = dto.endDate;
     album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp;
     album.shared = dto.shared;
-    album.activityEnabled = dto.isActivityEnabled;
-    if (album.thumbnail.value?.remoteId != dto.albumThumbnailAssetId) {
-      album.thumbnail.value = await _db.assets
-          .where()
-          .remoteIdEqualTo(dto.albumThumbnailAssetId)
-          .findFirst();
+    album.activityEnabled = dto.activityEnabled;
+    album.sortOrder = dto.sortOrder;
+
+    final remoteThumbnailAssetId = dto.remoteThumbnailAssetId;
+    if (remoteThumbnailAssetId != null &&
+        album.thumbnail.value?.remoteId != remoteThumbnailAssetId) {
+      album.thumbnail.value =
+          await _assetRepository.getByRemoteId(remoteThumbnailAssetId);
     }
 
     // write & commit all changes to DB
     try {
-      await _db.writeTxn(() async {
-        await _db.assets.putAll(toUpdate);
-        await album.thumbnail.save();
-        await album.sharedUsers
-            .update(link: usersToLink, unlink: usersToUnlink);
-        await album.assets.update(link: assetsToLink, unlink: toUnlink.cast());
-        await _db.albums.put(album);
+      await _assetRepository.transaction(() async {
+        await _assetRepository.updateAll(toUpdate);
+        await _albumRepository.addUsers(album, usersToLink);
+        await _albumRepository.removeUsers(album, usersToUnlink);
+        await _albumRepository.addAssets(album, assetsToLink);
+        await _albumRepository.removeAssets(album, toUnlink);
+        await _albumRepository.recalculateMetadata(album);
+        await _albumRepository.update(album);
       });
       _log.info("Synced changes of remote album ${album.name} to DB");
-    } on IsarError catch (e) {
+    } catch (e) {
       _log.severe("Failed to sync remote album to database", e);
     }
 
     if (album.shared || dto.shared) {
-      final userId = Store.get(StoreKey.currentUser).isarId;
+      final userId = (await _userRepository.me()).isarId;
       final foreign =
-          await album.assets.filter().not().ownerIdEqualTo(userId).findAll();
+          await _assetRepository.getByAlbum(album, notOwnedBy: [userId]);
       existing.addAll(foreign);
 
       // delete assets in DB unless they belong to this user or part of some other shared album
@@ -428,27 +445,26 @@ class SyncService {
   /// (shared) assets to the database beforehand
   /// accumulates assets already existing in the database
   Future<void> _addAlbumFromServer(
-    AlbumResponseDto dto,
+    Album album,
     List<Asset> existing,
-    FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
   ) async {
-    if (dto.assetCount != dto.assets.length) {
-      dto = await loadDetails(dto);
+    if (album.remoteAssetCount != album.remoteAssets.length) {
+      album = await _albumApiRepository.get(album.remoteId!);
     }
-    if (dto.assetCount == dto.assets.length) {
+    if (album.remoteAssetCount == album.remoteAssets.length) {
       // in case an album contains assets not yet present in local DB:
       // put missing album assets into local DB
       final (existingInDb, updated) =
-          await _linkWithExistingFromDb(dto.getAssets());
+          await _linkWithExistingFromDb(album.remoteAssets.toList());
       existing.addAll(existingInDb);
       await upsertAssetsWithExif(updated);
 
-      final Album a = await Album.remote(dto);
-      await _db.writeTxn(() => _db.albums.store(a));
+      await _entityService.fillAlbumWithDatabaseEntities(album);
+      await _albumRepository.create(album);
     } else {
       _log.warning(
-          "Failed to add album from server: assetCount ${dto.assetCount} != "
-          "asset array length ${dto.assets.length} for album ${dto.albumName}");
+          "Failed to add album from server: assetCount ${album.remoteAssetCount} != "
+          "asset array length ${album.remoteAssets.length} for album ${album.name}");
     }
   }
 
@@ -462,27 +478,18 @@ class SyncService {
       _log.info("Removing local album $album from DB");
       // delete assets in DB unless they are remote or part of some other album
       deleteCandidates.addAll(
-        await album.assets.filter().remoteIdIsNull().findAll(),
+        await _assetRepository.getByAlbum(album, state: AssetState.local),
       );
     } else if (album.shared) {
-      final User user = Store.get(StoreKey.currentUser);
       // delete assets in DB unless they belong to this user or are part of some other shared album or belong to a partner
-      final userIds = await _db.users
-          .filter()
-          .isPartnerSharedWithEqualTo(true)
-          .isarIdProperty()
-          .findAll();
-      userIds.add(user.isarId);
-      final orphanedAssets = await album.assets
-          .filter()
-          .not()
-          .anyOf(userIds, (q, int id) => q.ownerIdEqualTo(id))
-          .findAll();
+      final userIds =
+          (await _userRepository.getAllAccessible()).map((user) => user.isarId);
+      final orphanedAssets =
+          await _assetRepository.getByAlbum(album, notOwnedBy: userIds);
       deleteCandidates.addAll(orphanedAssets);
     }
     try {
-      final bool ok = await _db.writeTxn(() => _db.albums.delete(album.id));
-      assert(ok);
+      await _albumRepository.delete(album.id);
       _log.info("Removed local album $album from DB");
     } catch (e) {
       _log.severe("Failed to remove local album $album from DB", e);
@@ -492,28 +499,26 @@ class SyncService {
   /// Syncs all device albums and their assets to the database
   /// Returns `true` if there were any changes
   Future<bool> _syncLocalAlbumAssetsToDb(
-    List<AssetPathEntity> onDevice, [
+    List<Album> onDevice, [
     Set<String>? excludedAssets,
   ]) async {
-    onDevice.sort((a, b) => a.id.compareTo(b.id));
+    onDevice.sort((a, b) => a.localId!.compareTo(b.localId!));
     final inDb =
-        await _db.albums.where().localIdIsNotNull().sortByLocalId().findAll();
+        await _albumRepository.getAll(remote: false, sortBy: AlbumSort.localId);
     final List<Asset> deleteCandidates = [];
     final List<Asset> existing = [];
-    assert(inDb.isSorted((a, b) => a.localId!.compareTo(b.localId!)), "sort!");
     final bool anyChanges = await diffSortedLists(
       onDevice,
       inDb,
-      compare: (AssetPathEntity a, Album b) => a.id.compareTo(b.localId!),
-      both: (AssetPathEntity ape, Album album) => _syncAlbumInDbAndOnDevice(
-        ape,
-        album,
+      compare: (Album a, Album b) => a.localId!.compareTo(b.localId!),
+      both: (Album a, Album b) => _syncAlbumInDbAndOnDevice(
+        a,
+        b,
         deleteCandidates,
         existing,
         excludedAssets,
       ),
-      onlyFirst: (AssetPathEntity ape) =>
-          _addAlbumFromDevice(ape, existing, excludedAssets),
+      onlyFirst: (Album a) => _addAlbumFromDevice(a, existing, excludedAssets),
       onlySecond: (Album a) => _removeAlbumFromDb(a, deleteCandidates),
     );
     _log.fine(
@@ -525,10 +530,9 @@ class SyncService {
       "${toDelete.length} assets to delete, ${toUpdate.length} to update",
     );
     if (toDelete.isNotEmpty || toUpdate.isNotEmpty) {
-      await _db.writeTxn(() async {
-        await _db.assets.deleteAll(toDelete);
-        await _db.exifInfos.deleteAll(toDelete);
-        await _db.assets.putAll(toUpdate);
+      await _assetRepository.transaction(() async {
+        await _assetRepository.deleteById(toDelete);
+        await _assetRepository.updateAll(toUpdate);
       });
       _log.info(
         "Removed ${toDelete.length} and updated ${toUpdate.length} local assets from DB",
@@ -541,58 +545,64 @@ class SyncService {
   /// returns `true` if there were any changes
   /// Accumulates asset candidates to delete and those already existing in DB
   Future<bool> _syncAlbumInDbAndOnDevice(
-    AssetPathEntity ape,
-    Album album,
+    Album deviceAlbum,
+    Album dbAlbum,
     List<Asset> deleteCandidates,
     List<Asset> existing, [
     Set<String>? excludedAssets,
     bool forceRefresh = false,
   ]) async {
-    if (!forceRefresh && !await _hasAssetPathEntityChanged(ape, album)) {
-      _log.fine("Local album ${ape.name} has not changed. Skipping sync.");
+    if (!forceRefresh && !await _hasAlbumChangeOnDevice(deviceAlbum, dbAlbum)) {
+      _log.fine(
+        "Local album ${deviceAlbum.name} has not changed. Skipping sync.",
+      );
       return false;
     }
     if (!forceRefresh &&
         excludedAssets == null &&
-        await _syncDeviceAlbumFast(ape, album)) {
+        await _syncDeviceAlbumFast(deviceAlbum, dbAlbum)) {
       return true;
     }
-
     // general case, e.g. some assets have been deleted or there are excluded albums on iOS
-    final inDb = await album.assets
-        .filter()
-        .ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
-        .sortByChecksum()
-        .findAll();
+    final inDb = await _assetRepository.getByAlbum(
+      dbAlbum,
+      ownerId: (await _userRepository.me()).isarId,
+      sortBy: AssetSort.checksum,
+    );
+
     assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
-    final int assetCountOnDevice = await ape.assetCountAsync;
-    final List<Asset> onDevice =
-        await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets);
+    final int assetCountOnDevice =
+        await _albumMediaRepository.getAssetCount(deviceAlbum.localId!);
+    final List<Asset> onDevice = await _hashService.getHashedAssets(
+      deviceAlbum,
+      excludedAssets: excludedAssets,
+    );
     _removeDuplicates(onDevice);
     // _removeDuplicates sorts `onDevice` by checksum
     final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb);
     if (toAdd.isEmpty &&
         toUpdate.isEmpty &&
         toDelete.isEmpty &&
-        album.name == ape.name &&
-        ape.lastModified != null &&
-        album.modifiedAt.isAtSameMomentAs(ape.lastModified!)) {
+        dbAlbum.name == deviceAlbum.name &&
+        dbAlbum.modifiedAt.isAtSameMomentAs(deviceAlbum.modifiedAt)) {
       // changes only affeted excluded albums
       _log.fine(
-        "Only excluded assets in local album ${ape.name} changed. Stopping sync.",
+        "Only excluded assets in local album ${deviceAlbum.name} changed. Stopping sync.",
       );
       if (assetCountOnDevice !=
-          _db.eTags.getByIdSync(ape.eTagKeyAssetCount)?.assetCount) {
-        await _db.writeTxn(
-          () => _db.eTags.put(
-            ETag(id: ape.eTagKeyAssetCount, assetCount: assetCountOnDevice),
+          (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))
+              ?.assetCount) {
+        await _eTagRepository.upsertAll([
+          ETag(
+            id: deviceAlbum.eTagKeyAssetCount,
+            assetCount: assetCountOnDevice,
           ),
-        );
+        ]);
       }
       return false;
     }
     _log.fine(
-      "Syncing local album ${ape.name}. ${toAdd.length} assets to add, ${toUpdate.length} to update, ${toDelete.length} to delete",
+      "Syncing local album ${deviceAlbum.name}. ${toAdd.length} assets to add, ${toUpdate.length} to update, ${toDelete.length} to delete",
     );
     final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd);
     _log.fine(
@@ -600,28 +610,29 @@ class SyncService {
     );
     deleteCandidates.addAll(toDelete);
     existing.addAll(existingInDb);
-    album.name = ape.name;
-    album.modifiedAt = ape.lastModified ?? DateTime.now();
-    if (album.thumbnail.value != null &&
-        toDelete.contains(album.thumbnail.value)) {
-      album.thumbnail.value = null;
+    dbAlbum.name = deviceAlbum.name;
+    dbAlbum.modifiedAt = deviceAlbum.modifiedAt;
+    if (dbAlbum.thumbnail.value != null &&
+        toDelete.contains(dbAlbum.thumbnail.value)) {
+      dbAlbum.thumbnail.value = null;
     }
     try {
-      await _db.writeTxn(() async {
-        await _db.assets.putAll(updated);
-        await _db.assets.putAll(toUpdate);
-        await album.assets
-            .update(link: existingInDb + updated, unlink: toDelete);
-        await _db.albums.put(album);
-        album.thumbnail.value ??= await album.assets.filter().findFirst();
-        await album.thumbnail.save();
-        await _db.eTags.put(
-          ETag(id: ape.eTagKeyAssetCount, assetCount: assetCountOnDevice),
-        );
+      await _assetRepository.transaction(() async {
+        await _assetRepository.updateAll(updated + toUpdate);
+        await _albumRepository.addAssets(dbAlbum, existingInDb + updated);
+        await _albumRepository.removeAssets(dbAlbum, toDelete);
+        await _albumRepository.recalculateMetadata(dbAlbum);
+        await _albumRepository.update(dbAlbum);
+        await _eTagRepository.upsertAll([
+          ETag(
+            id: deviceAlbum.eTagKeyAssetCount,
+            assetCount: assetCountOnDevice,
+          ),
+        ]);
       });
-      _log.info("Synced changes of local album ${ape.name} to DB");
-    } on IsarError catch (e) {
-      _log.severe("Failed to update synced album ${ape.name} in DB", e);
+      _log.info("Synced changes of local album ${deviceAlbum.name} to DB");
+    } catch (e) {
+      _log.severe("Failed to update synced album ${deviceAlbum.name} in DB", e);
     }
 
     return true;
@@ -629,45 +640,47 @@ class SyncService {
 
   /// fast path for common case: only new assets were added to device album
   /// returns `true` if successfull, else `false`
-  Future<bool> _syncDeviceAlbumFast(AssetPathEntity ape, Album album) async {
-    if (!(ape.lastModified ?? DateTime.now()).isAfter(album.modifiedAt)) {
+  Future<bool> _syncDeviceAlbumFast(Album deviceAlbum, Album dbAlbum) async {
+    if (!deviceAlbum.modifiedAt.isAfter(dbAlbum.modifiedAt)) {
       return false;
     }
-    final int totalOnDevice = await ape.assetCountAsync;
+    final int totalOnDevice =
+        await _albumMediaRepository.getAssetCount(deviceAlbum.localId!);
     final int lastKnownTotal =
-        (await _db.eTags.getById(ape.eTagKeyAssetCount))?.assetCount ?? 0;
-    final AssetPathEntity? modified = totalOnDevice > lastKnownTotal
-        ? await ape.fetchPathProperties(
-            filterOptionGroup: FilterOptionGroup(
-              updateTimeCond: DateTimeCond(
-                min: album.modifiedAt.add(const Duration(seconds: 1)),
-                max: ape.lastModified ?? DateTime.now(),
-              ),
-            ),
-          )
-        : null;
-    if (modified == null) {
+        (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))
+                ?.assetCount ??
+            0;
+    if (totalOnDevice <= lastKnownTotal) {
       return false;
     }
-    final List<Asset> newAssets = await _hashService.getHashedAssets(modified);
+    final List<Asset> newAssets = await _hashService.getHashedAssets(
+      deviceAlbum,
+      modifiedFrom: dbAlbum.modifiedAt.add(const Duration(seconds: 1)),
+      modifiedUntil: deviceAlbum.modifiedAt,
+    );
 
     if (totalOnDevice != lastKnownTotal + newAssets.length) {
       return false;
     }
-    album.modifiedAt = ape.lastModified ?? DateTime.now();
+    dbAlbum.modifiedAt = deviceAlbum.modifiedAt;
     _removeDuplicates(newAssets);
     final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets);
     try {
-      await _db.writeTxn(() async {
-        await _db.assets.putAll(updated);
-        await album.assets.update(link: existingInDb + updated);
-        await _db.albums.put(album);
-        await _db.eTags
-            .put(ETag(id: ape.eTagKeyAssetCount, assetCount: totalOnDevice));
+      await _assetRepository.transaction(() async {
+        await _assetRepository.updateAll(updated);
+        await _albumRepository.addAssets(dbAlbum, existingInDb + updated);
+        await _albumRepository.recalculateMetadata(dbAlbum);
+        await _albumRepository.update(dbAlbum);
+        await _eTagRepository.upsertAll(
+          [ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: totalOnDevice)],
+        );
       });
-      _log.info("Fast synced local album ${ape.name} to DB");
-    } on IsarError catch (e) {
-      _log.severe("Failed to fast sync local album ${ape.name} to DB", e);
+      _log.info("Fast synced local album ${deviceAlbum.name} to DB");
+    } catch (e) {
+      _log.severe(
+        "Failed to fast sync local album ${deviceAlbum.name} to DB",
+        e,
+      );
       return false;
     }
 
@@ -677,14 +690,15 @@ class SyncService {
   /// Adds a new album from the device to the database and Accumulates all
   /// assets already existing in the database to the list of `existing` assets
   Future<void> _addAlbumFromDevice(
-    AssetPathEntity ape,
+    Album album,
     List<Asset> existing, [
     Set<String>? excludedAssets,
   ]) async {
-    _log.info("Syncing a new local album to DB: ${ape.name}");
-    final Album a = Album.local(ape);
-    final assets =
-        await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets);
+    _log.info("Syncing a new local album to DB: ${album.name}");
+    final assets = await _hashService.getHashedAssets(
+      album,
+      excludedAssets: excludedAssets,
+    );
     _removeDuplicates(assets);
     final (existingInDb, updated) = await _linkWithExistingFromDb(assets);
     _log.info(
@@ -692,15 +706,15 @@ class SyncService {
     );
     await upsertAssetsWithExif(updated);
     existing.addAll(existingInDb);
-    a.assets.addAll(existingInDb);
-    a.assets.addAll(updated);
+    album.assets.addAll(existingInDb);
+    album.assets.addAll(updated);
     final thumb = existingInDb.firstOrNull ?? updated.firstOrNull;
-    a.thumbnail.value = thumb;
+    album.thumbnail.value = thumb;
     try {
-      await _db.writeTxn(() => _db.albums.store(a));
-      _log.info("Added a new local album to DB: ${ape.name}");
-    } on IsarError catch (e) {
-      _log.severe("Failed to add new local album ${ape.name} to DB", e);
+      await _albumRepository.create(album);
+      _log.info("Added a new local album to DB: ${album.name}");
+    } catch (e) {
+      _log.severe("Failed to add new local album ${album.name} to DB", e);
     }
   }
 
@@ -710,7 +724,7 @@ class SyncService {
   ) async {
     if (assets.isEmpty) return ([].cast<Asset>(), [].cast<Asset>());
 
-    final List<Asset?> inDb = await _db.assets.getAllByOwnerIdChecksum(
+    final List<Asset?> inDb = await _assetRepository.getAllByOwnerIdChecksum(
       assets.map((a) => a.ownerId).toInt64List(),
       assets.map((a) => a.checksum).toList(growable: false),
     );
@@ -724,7 +738,7 @@ class SyncService {
       }
       if (b.canUpdate(assets[i])) {
         final updated = b.updatedCopy(assets[i]);
-        assert(updated.id != Isar.autoIncrement);
+        assert(updated.isInDb);
         toUpsert.add(updated);
       } else {
         existing.add(b);
@@ -736,24 +750,22 @@ class SyncService {
 
   /// Inserts or updates the assets in the database with their ExifInfo (if any)
   Future<void> upsertAssetsWithExif(List<Asset> assets) async {
-    if (assets.isEmpty) {
-      return;
-    }
-    final exifInfos = assets.map((e) => e.exifInfo).whereNotNull().toList();
+    if (assets.isEmpty) return;
+    final exifInfos = assets.map((e) => e.exifInfo).nonNulls.toList();
     try {
-      await _db.writeTxn(() async {
-        await _db.assets.putAll(assets);
+      await _assetRepository.transaction(() async {
+        await _assetRepository.updateAll(assets);
         for (final Asset added in assets) {
           added.exifInfo?.id = added.id;
         }
-        await _db.exifInfos.putAll(exifInfos);
+        await _exifInfoRepository.updateAll(exifInfos);
       });
       _log.info("Upserted ${assets.length} assets into the DB");
-    } on IsarError catch (e) {
+    } catch (e) {
       _log.severe("Failed to upsert ${assets.length} assets into the DB", e);
       // give details on the errors
       assets.sort(Asset.compareByOwnerChecksum);
-      final inDb = await _db.assets.getAllByOwnerIdChecksum(
+      final inDb = await _assetRepository.getAllByOwnerIdChecksum(
         assets.map((e) => e.ownerId).toInt64List(),
         assets.map((e) => e.checksum).toList(growable: false),
       );
@@ -761,7 +773,7 @@ class SyncService {
         final Asset a = assets[i];
         final Asset? b = inDb[i];
         if (b == null) {
-          if (a.id != Isar.autoIncrement) {
+          if (!a.isInDb) {
             _log.warning(
               "Trying to update an asset that does not exist in DB:\n$a",
             );
@@ -787,8 +799,7 @@ class SyncService {
     assets.sort(Asset.compareByOwnerChecksumCreatedModified);
     assets.uniqueConsecutive(
       compare: Asset.compareByOwnerChecksum,
-      onDuplicate: (a, b) =>
-          _log.info("Ignoring duplicate assets on device:\n$a\n$b"),
+      onDuplicate: (a, b) => {},
     );
     final int duplicates = before - assets.length;
     if (duplicates > 0) {
@@ -798,23 +809,26 @@ class SyncService {
   }
 
   /// returns `true` if the albums differ on the surface
-  Future<bool> _hasAssetPathEntityChanged(AssetPathEntity a, Album b) async {
-    return a.name != b.name ||
-        a.lastModified == null ||
-        !a.lastModified!.isAtSameMomentAs(b.modifiedAt) ||
-        await a.assetCountAsync !=
-            (await _db.eTags.getById(a.eTagKeyAssetCount))?.assetCount;
+  Future<bool> _hasAlbumChangeOnDevice(
+    Album deviceAlbum,
+    Album dbAlbum,
+  ) async {
+    return deviceAlbum.name != dbAlbum.name ||
+        !deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
+        await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) !=
+            (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))
+                ?.assetCount;
   }
 
   Future<bool> _removeAllLocalAlbumsAndAssets() async {
     try {
-      final assets = await _db.assets.where().localIdIsNotNull().findAll();
+      final assets = await _assetRepository.getAllLocal();
       final (toDelete, toUpdate) =
           _handleAssetRemoval(assets, [], remote: false);
-      await _db.writeTxn(() async {
-        await _db.assets.deleteAll(toDelete);
-        await _db.assets.putAll(toUpdate);
-        await _db.albums.where().localIdIsNotNull().deleteAll();
+      await _assetRepository.transaction(() async {
+        await _assetRepository.deleteById(toDelete);
+        await _assetRepository.updateAll(toUpdate);
+        await _albumRepository.deleteAllLocal();
       });
       return true;
     } catch (e) {
@@ -900,17 +914,17 @@ class SyncService {
 }
 
 /// returns `true` if the albums differ on the surface
-bool _hasAlbumResponseDtoChanged(AlbumResponseDto dto, Album a) {
-  return dto.assetCount != a.assetCount ||
-      dto.albumName != a.name ||
-      dto.albumThumbnailAssetId != a.thumbnail.value?.remoteId ||
-      dto.shared != a.shared ||
-      dto.albumUsers.length != a.sharedUsers.length ||
-      !dto.updatedAt.isAtSameMomentAs(a.modifiedAt) ||
-      !isAtSameMomentAs(dto.startDate, a.startDate) ||
-      !isAtSameMomentAs(dto.endDate, a.endDate) ||
+bool _hasRemoteAlbumChanged(Album remoteAlbum, Album dbAlbum) {
+  return remoteAlbum.remoteAssetCount != dbAlbum.assetCount ||
+      remoteAlbum.name != dbAlbum.name ||
+      remoteAlbum.remoteThumbnailAssetId != dbAlbum.thumbnail.value?.remoteId ||
+      remoteAlbum.shared != dbAlbum.shared ||
+      remoteAlbum.remoteUsers.length != dbAlbum.sharedUsers.length ||
+      !remoteAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
+      !isAtSameMomentAs(remoteAlbum.startDate, dbAlbum.startDate) ||
+      !isAtSameMomentAs(remoteAlbum.endDate, dbAlbum.endDate) ||
       !isAtSameMomentAs(
-        dto.lastModifiedAssetTimestamp,
-        a.lastModifiedAssetTimestamp,
+        remoteAlbum.lastModifiedAssetTimestamp,
+        dbAlbum.lastModifiedAssetTimestamp,
       );
 }
diff --git a/mobile/lib/services/user.service.dart b/mobile/lib/services/user.service.dart
index 9631141c41..13adcc4e7a 100644
--- a/mobile/lib/services/user.service.dart
+++ b/mobile/lib/services/user.service.dart
@@ -1,68 +1,49 @@
 import 'package:collection/collection.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:http/http.dart';
 import 'package:image_picker/image_picker.dart';
-import 'package:immich_mobile/services/partner.service.dart';
-import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/interfaces/partner_api.interface.dart';
+import 'package:immich_mobile/interfaces/user.interface.dart';
+import 'package:immich_mobile/interfaces/user_api.interface.dart';
+import 'package:immich_mobile/repositories/partner_api.repository.dart';
+import 'package:immich_mobile/repositories/user.repository.dart';
+import 'package:immich_mobile/repositories/user_api.repository.dart';
 import 'package:immich_mobile/entities/user.entity.dart';
-import 'package:immich_mobile/providers/api.provider.dart';
-import 'package:immich_mobile/providers/db.provider.dart';
-import 'package:immich_mobile/services/api.service.dart';
 import 'package:immich_mobile/services/sync.service.dart';
 import 'package:immich_mobile/utils/diff.dart';
-import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
-import 'package:openapi/api.dart';
 
 final userServiceProvider = Provider(
   (ref) => UserService(
-    ref.watch(apiServiceProvider),
-    ref.watch(dbProvider),
+    ref.watch(partnerApiRepositoryProvider),
+    ref.watch(userApiRepositoryProvider),
+    ref.watch(userRepositoryProvider),
     ref.watch(syncServiceProvider),
-    ref.watch(partnerServiceProvider),
   ),
 );
 
 class UserService {
-  final ApiService _apiService;
-  final Isar _db;
+  final IPartnerApiRepository _partnerApiRepository;
+  final IUserApiRepository _userApiRepository;
+  final IUserRepository _userRepository;
   final SyncService _syncService;
-  final PartnerService _partnerService;
   final Logger _log = Logger("UserService");
 
   UserService(
-    this._apiService,
-    this._db,
+    this._partnerApiRepository,
+    this._userApiRepository,
+    this._userRepository,
     this._syncService,
-    this._partnerService,
   );
 
-  Future<List<User>?> _getAllUsers() async {
-    try {
-      final dto = await _apiService.usersApi.searchUsers();
-      return dto?.map(User.fromSimpleUserDto).toList();
-    } catch (e) {
-      _log.warning("Failed get all users", e);
-      return null;
-    }
+  Future<List<User>> getUsers({bool self = false}) {
+    return _userRepository.getAll(self: self);
   }
 
-  Future<List<User>> getUsersInDb({bool self = false}) async {
-    if (self) {
-      return _db.users.where().findAll();
-    }
-    final int userId = Store.get(StoreKey.currentUser).isarId;
-    return _db.users.where().isarIdNotEqualTo(userId).findAll();
-  }
-
-  Future<CreateProfileImageResponseDto?> uploadProfileImage(XFile image) async {
+  Future<({String profileImagePath})?> uploadProfileImage(XFile image) async {
     try {
-      return await _apiService.usersApi.createProfileImage(
-        MultipartFile.fromBytes(
-          'file',
-          await image.readAsBytes(),
-          filename: image.name,
-        ),
+      return await _userApiRepository.createProfileImage(
+        name: image.name,
+        data: await image.readAsBytes(),
       );
     } catch (e) {
       _log.warning("Failed to upload profile image", e);
@@ -71,13 +52,19 @@ class UserService {
   }
 
   Future<List<User>?> getUsersFromServer() async {
-    final List<User>? users = await _getAllUsers();
-    final List<User>? sharedBy =
-        await _partnerService.getPartners(PartnerDirection.by);
-    final List<User>? sharedWith =
-        await _partnerService.getPartners(PartnerDirection.with_);
+    List<User>? users;
+    try {
+      users = await _userApiRepository.getAll();
+    } catch (e) {
+      _log.warning("Failed to fetch users", e);
+      users = null;
+    }
+    final List<User> sharedBy =
+        await _partnerApiRepository.getAll(Direction.sharedByMe);
+    final List<User> sharedWith =
+        await _partnerApiRepository.getAll(Direction.sharedWithMe);
 
-    if (users == null || sharedBy == null || sharedWith == null) {
+    if (users == null) {
       _log.warning("Failed to refresh users");
       return null;
     }
diff --git a/mobile/lib/constants/immich_colors.dart b/mobile/lib/theme/color_scheme.dart
similarity index 82%
rename from mobile/lib/constants/immich_colors.dart
rename to mobile/lib/theme/color_scheme.dart
index 38deac3f0e..c01b7cfa5a 100644
--- a/mobile/lib/constants/immich_colors.dart
+++ b/mobile/lib/theme/color_scheme.dart
@@ -1,30 +1,15 @@
 import 'package:flutter/material.dart';
-import 'package:immich_mobile/utils/immich_app_theme.dart';
+import 'package:immich_mobile/constants/colors.dart';
+import 'package:immich_mobile/theme/theme_data.dart';
 
-enum ImmichColorPreset {
-  indigo,
-  deepPurple,
-  pink,
-  red,
-  orange,
-  yellow,
-  lime,
-  green,
-  cyan,
-  slateGray
-}
-
-const ImmichColorPreset defaultColorPreset = ImmichColorPreset.indigo;
-const String defaultColorPresetName = "indigo";
-
-const Color immichBrandColorLight = Color(0xFF4150AF);
-const Color immichBrandColorDark = Color(0xFFACCBFA);
-
-final Map<ImmichColorPreset, ImmichTheme> _themePresetsMap = {
+final Map<ImmichColorPreset, ImmichTheme> _themePresets = {
   ImmichColorPreset.indigo: ImmichTheme(
     light: ColorScheme.fromSeed(
       seedColor: immichBrandColorLight,
-    ).copyWith(primary: immichBrandColorLight),
+    ).copyWith(
+      primary: immichBrandColorLight,
+      onSurface: const Color.fromARGB(255, 34, 31, 32),
+    ),
     dark: ColorScheme.fromSeed(
       seedColor: immichBrandColorDark,
       brightness: Brightness.dark,
@@ -104,5 +89,5 @@ final Map<ImmichColorPreset, ImmichTheme> _themePresetsMap = {
 };
 
 extension ImmichColorModeExtension on ImmichColorPreset {
-  ImmichTheme getTheme() => _themePresetsMap[this]!;
+  ImmichTheme get themeOfPreset => _themePresets[this]!;
 }
diff --git a/mobile/lib/theme/dynamic_theme.dart b/mobile/lib/theme/dynamic_theme.dart
new file mode 100644
index 0000000000..39d6b6ee45
--- /dev/null
+++ b/mobile/lib/theme/dynamic_theme.dart
@@ -0,0 +1,38 @@
+import 'package:flutter/material.dart';
+import 'package:dynamic_color/dynamic_color.dart';
+
+import 'package:immich_mobile/theme/theme_data.dart';
+
+abstract final class DynamicTheme {
+  DynamicTheme._();
+
+  static ImmichTheme? _theme;
+  // Method to fetch dynamic system colors
+  static Future<void> fetchSystemPalette() async {
+    try {
+      final corePalette = await DynamicColorPlugin.getCorePalette();
+      if (corePalette != null) {
+        final primaryColor = corePalette.toColorScheme().primary;
+        debugPrint('dynamic_color: Core palette detected.');
+
+        // Some palettes do not generate surface container colors accurately,
+        // so we regenerate all colors using the primary color
+        _theme = ImmichTheme(
+          light: ColorScheme.fromSeed(
+            seedColor: primaryColor,
+            brightness: Brightness.light,
+          ),
+          dark: ColorScheme.fromSeed(
+            seedColor: primaryColor,
+            brightness: Brightness.dark,
+          ),
+        );
+      }
+    } catch (error) {
+      debugPrint('dynamic_color: Failed to obtain core palette: $error');
+    }
+  }
+
+  static ImmichTheme? get theme => _theme;
+  static bool get isAvailable => _theme != null;
+}
diff --git a/mobile/lib/utils/immich_app_theme.dart b/mobile/lib/theme/theme_data.dart
similarity index 54%
rename from mobile/lib/utils/immich_app_theme.dart
rename to mobile/lib/theme/theme_data.dart
index 0aac5b476e..de96e12c5d 100644
--- a/mobile/lib/utils/immich_app_theme.dart
+++ b/mobile/lib/theme/theme_data.dart
@@ -1,116 +1,175 @@
-import 'package:dynamic_color/dynamic_color.dart';
 import 'package:flutter/material.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/constants/immich_colors.dart';
+
+import 'package:immich_mobile/constants/locales.dart';
 import 'package:immich_mobile/extensions/theme_extensions.dart';
-import 'package:immich_mobile/providers/app_settings.provider.dart';
-import 'package:immich_mobile/services/app_settings.service.dart';
 
 class ImmichTheme {
-  ColorScheme light;
-  ColorScheme dark;
+  final ColorScheme light;
+  final ColorScheme dark;
 
-  ImmichTheme({required this.light, required this.dark});
+  const ImmichTheme({required this.light, required this.dark});
 }
 
-ImmichTheme? _immichDynamicTheme;
-bool get isDynamicThemeAvailable => _immichDynamicTheme != null;
+ThemeData getThemeData({
+  required ColorScheme colorScheme,
+  required Locale locale,
+}) {
+  final isDark = colorScheme.brightness == Brightness.dark;
 
-final immichThemeModeProvider = StateProvider<ThemeMode>((ref) {
-  var themeMode = ref
-      .watch(appSettingsServiceProvider)
-      .getSetting(AppSettingsEnum.themeMode);
-
-  debugPrint("Current themeMode $themeMode");
-
-  if (themeMode == "light") {
-    return ThemeMode.light;
-  } else if (themeMode == "dark") {
-    return ThemeMode.dark;
-  } else {
-    return ThemeMode.system;
-  }
-});
-
-final immichThemePresetProvider = StateProvider<ImmichColorPreset>((ref) {
-  var appSettingsProvider = ref.watch(appSettingsServiceProvider);
-  var primaryColorName =
-      appSettingsProvider.getSetting(AppSettingsEnum.primaryColor);
-
-  debugPrint("Current theme preset $primaryColorName");
-
-  try {
-    return ImmichColorPreset.values
-        .firstWhere((e) => e.name == primaryColorName);
-  } catch (e) {
-    debugPrint(
-      "Theme preset $primaryColorName not found. Applying default preset.",
-    );
-    appSettingsProvider.setSetting(
-      AppSettingsEnum.primaryColor,
-      defaultColorPresetName,
-    );
-    return defaultColorPreset;
-  }
-});
-
-final dynamicThemeSettingProvider = StateProvider<bool>((ref) {
-  return ref
-      .watch(appSettingsServiceProvider)
-      .getSetting(AppSettingsEnum.dynamicTheme);
-});
-
-final colorfulInterfaceSettingProvider = StateProvider<bool>((ref) {
-  return ref
-      .watch(appSettingsServiceProvider)
-      .getSetting(AppSettingsEnum.colorfulInterface);
-});
-
-// Provider for current selected theme
-final immichThemeProvider = StateProvider<ImmichTheme>((ref) {
-  var primaryColor = ref.read(immichThemePresetProvider);
-  var useSystemColor = ref.watch(dynamicThemeSettingProvider);
-  var useColorfulInterface = ref.watch(colorfulInterfaceSettingProvider);
-
-  var currentTheme = (useSystemColor && _immichDynamicTheme != null)
-      ? _immichDynamicTheme!
-      : primaryColor.getTheme();
-
-  return useColorfulInterface
-      ? currentTheme
-      : _decolorizeSurfaces(theme: currentTheme);
-});
-
-// Method to fetch dynamic system colors
-Future<void> fetchSystemPalette() async {
-  try {
-    final corePalette = await DynamicColorPlugin.getCorePalette();
-    if (corePalette != null) {
-      final primaryColor = corePalette.toColorScheme().primary;
-      debugPrint('dynamic_color: Core palette detected.');
-
-      // Some palettes do not generate surface container colors accurately,
-      // so we regenerate all colors using the primary color
-      _immichDynamicTheme = ImmichTheme(
-        light: ColorScheme.fromSeed(
-          seedColor: primaryColor,
-          brightness: Brightness.light,
+  return ThemeData(
+    useMaterial3: true,
+    brightness: colorScheme.brightness,
+    colorScheme: colorScheme,
+    primaryColor: colorScheme.primary,
+    hintColor: colorScheme.onSurfaceSecondary,
+    focusColor: colorScheme.primary,
+    scaffoldBackgroundColor: colorScheme.surface,
+    splashColor: colorScheme.primary.withOpacity(0.1),
+    highlightColor: colorScheme.primary.withOpacity(0.1),
+    dialogBackgroundColor: colorScheme.surfaceContainer,
+    bottomSheetTheme: BottomSheetThemeData(
+      backgroundColor: colorScheme.surfaceContainer,
+    ),
+    fontFamily: _getFontFamilyFromLocale(locale),
+    snackBarTheme: SnackBarThemeData(
+      contentTextStyle: TextStyle(
+        fontFamily: _getFontFamilyFromLocale(locale),
+        color: colorScheme.primary,
+        fontWeight: FontWeight.bold,
+      ),
+      backgroundColor: colorScheme.surfaceContainerHighest,
+    ),
+    appBarTheme: AppBarTheme(
+      titleTextStyle: TextStyle(
+        color: colorScheme.primary,
+        fontFamily: _getFontFamilyFromLocale(locale),
+        fontWeight: FontWeight.bold,
+        fontSize: 18,
+      ),
+      backgroundColor:
+          isDark ? colorScheme.surfaceContainer : colorScheme.surface,
+      foregroundColor: colorScheme.primary,
+      elevation: 0,
+      scrolledUnderElevation: 0,
+      centerTitle: true,
+    ),
+    textTheme: const TextTheme(
+      displayLarge: TextStyle(
+        fontSize: 26,
+        fontWeight: FontWeight.bold,
+      ),
+      displayMedium: TextStyle(
+        fontSize: 14,
+        fontWeight: FontWeight.bold,
+      ),
+      displaySmall: TextStyle(
+        fontSize: 12,
+        fontWeight: FontWeight.bold,
+      ),
+      titleSmall: TextStyle(
+        fontSize: 16.0,
+        fontWeight: FontWeight.bold,
+      ),
+      titleMedium: TextStyle(
+        fontSize: 18.0,
+        fontWeight: FontWeight.bold,
+      ),
+      titleLarge: TextStyle(
+        fontSize: 26.0,
+        fontWeight: FontWeight.bold,
+      ),
+    ),
+    elevatedButtonTheme: ElevatedButtonThemeData(
+      style: ElevatedButton.styleFrom(
+        backgroundColor: colorScheme.primary,
+        foregroundColor: isDark ? Colors.black87 : Colors.white,
+      ),
+    ),
+    chipTheme: const ChipThemeData(
+      side: BorderSide.none,
+    ),
+    sliderTheme: const SliderThemeData(
+      thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7),
+      trackHeight: 2.0,
+    ),
+    bottomNavigationBarTheme: const BottomNavigationBarThemeData(
+      type: BottomNavigationBarType.fixed,
+    ),
+    popupMenuTheme: const PopupMenuThemeData(
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.all(Radius.circular(10)),
+      ),
+    ),
+    navigationBarTheme: NavigationBarThemeData(
+      backgroundColor:
+          isDark ? colorScheme.surfaceContainer : colorScheme.surface,
+      labelTextStyle: const WidgetStatePropertyAll(
+        TextStyle(
+          fontSize: 14,
+          fontWeight: FontWeight.w500,
         ),
-        dark: ColorScheme.fromSeed(
-          seedColor: primaryColor,
-          brightness: Brightness.dark,
+      ),
+    ),
+    inputDecorationTheme: InputDecorationTheme(
+      focusedBorder: OutlineInputBorder(
+        borderSide: BorderSide(
+          color: colorScheme.primary,
         ),
-      );
-    }
-  } catch (e) {
-    debugPrint('dynamic_color: Failed to obtain core palette.');
-  }
+        borderRadius: const BorderRadius.all(Radius.circular(15)),
+      ),
+      enabledBorder: OutlineInputBorder(
+        borderSide: BorderSide(
+          color: colorScheme.outlineVariant,
+        ),
+        borderRadius: const BorderRadius.all(Radius.circular(15)),
+      ),
+      labelStyle: TextStyle(
+        color: colorScheme.primary,
+      ),
+      hintStyle: const TextStyle(
+        fontSize: 14.0,
+        fontWeight: FontWeight.normal,
+      ),
+    ),
+    textSelectionTheme: TextSelectionThemeData(
+      cursorColor: colorScheme.primary,
+    ),
+    dropdownMenuTheme: DropdownMenuThemeData(
+      menuStyle: const MenuStyle(
+        shape: WidgetStatePropertyAll<OutlinedBorder>(
+          RoundedRectangleBorder(
+            borderRadius: BorderRadius.all(Radius.circular(15)),
+          ),
+        ),
+      ),
+      inputDecorationTheme: InputDecorationTheme(
+        focusedBorder: OutlineInputBorder(
+          borderSide: BorderSide(
+            color: colorScheme.primary,
+          ),
+        ),
+        enabledBorder: OutlineInputBorder(
+          borderSide: BorderSide(
+            color: colorScheme.outlineVariant,
+          ),
+          borderRadius: const BorderRadius.all(Radius.circular(15)),
+        ),
+        labelStyle: TextStyle(
+          color: colorScheme.primary,
+        ),
+        hintStyle: const TextStyle(
+          fontSize: 14.0,
+          fontWeight: FontWeight.normal,
+        ),
+      ),
+    ),
+  );
 }
 
 // This method replaces all surface shades in ImmichTheme to a static ones
 // as we are creating the colorscheme through seedColor the default surfaces are
 // tinted with primary color
-ImmichTheme _decolorizeSurfaces({
+ImmichTheme decolorizeSurfaces({
   required ImmichTheme theme,
 }) {
   return ImmichTheme(
@@ -145,159 +204,10 @@ ImmichTheme _decolorizeSurfaces({
   );
 }
 
-ThemeData getThemeData({required ColorScheme colorScheme}) {
-  var isDark = colorScheme.brightness == Brightness.dark;
-  var primaryColor = colorScheme.primary;
-
-  return ThemeData(
-    useMaterial3: true,
-    brightness: isDark ? Brightness.dark : Brightness.light,
-    colorScheme: colorScheme,
-    primaryColor: primaryColor,
-    hintColor: colorScheme.onSurfaceSecondary,
-    focusColor: primaryColor,
-    scaffoldBackgroundColor: colorScheme.surface,
-    splashColor: primaryColor.withOpacity(0.1),
-    highlightColor: primaryColor.withOpacity(0.1),
-    dialogBackgroundColor: colorScheme.surfaceContainer,
-    bottomSheetTheme: BottomSheetThemeData(
-      backgroundColor: colorScheme.surfaceContainer,
-    ),
-    fontFamily: 'Overpass',
-    snackBarTheme: SnackBarThemeData(
-      contentTextStyle: TextStyle(
-        fontFamily: 'Overpass',
-        color: primaryColor,
-        fontWeight: FontWeight.bold,
-      ),
-      backgroundColor: colorScheme.surfaceContainerHighest,
-    ),
-    appBarTheme: AppBarTheme(
-      titleTextStyle: TextStyle(
-        color: primaryColor,
-        fontFamily: 'Overpass',
-        fontWeight: FontWeight.bold,
-        fontSize: 18,
-      ),
-      backgroundColor:
-          isDark ? colorScheme.surfaceContainer : colorScheme.surface,
-      foregroundColor: primaryColor,
-      elevation: 0,
-      scrolledUnderElevation: 0,
-      centerTitle: true,
-    ),
-    textTheme: TextTheme(
-      displayLarge: TextStyle(
-        fontSize: 26,
-        fontWeight: FontWeight.bold,
-        color: isDark ? Colors.white : primaryColor,
-      ),
-      displayMedium: TextStyle(
-        fontSize: 14,
-        fontWeight: FontWeight.bold,
-        color: isDark ? Colors.white : Colors.black87,
-      ),
-      displaySmall: TextStyle(
-        fontSize: 12,
-        fontWeight: FontWeight.bold,
-        color: primaryColor,
-      ),
-      titleSmall: const TextStyle(
-        fontSize: 16.0,
-        fontWeight: FontWeight.bold,
-      ),
-      titleMedium: const TextStyle(
-        fontSize: 18.0,
-        fontWeight: FontWeight.bold,
-      ),
-      titleLarge: const TextStyle(
-        fontSize: 26.0,
-        fontWeight: FontWeight.bold,
-      ),
-    ),
-    elevatedButtonTheme: ElevatedButtonThemeData(
-      style: ElevatedButton.styleFrom(
-        backgroundColor: primaryColor,
-        foregroundColor: isDark ? Colors.black87 : Colors.white,
-      ),
-    ),
-    chipTheme: const ChipThemeData(
-      side: BorderSide.none,
-    ),
-    sliderTheme: const SliderThemeData(
-      thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7),
-      trackHeight: 2.0,
-    ),
-    bottomNavigationBarTheme: const BottomNavigationBarThemeData(
-      type: BottomNavigationBarType.fixed,
-    ),
-    popupMenuTheme: const PopupMenuThemeData(
-      shape: RoundedRectangleBorder(
-        borderRadius: BorderRadius.all(Radius.circular(10)),
-      ),
-    ),
-    navigationBarTheme: NavigationBarThemeData(
-      backgroundColor:
-          isDark ? colorScheme.surfaceContainer : colorScheme.surface,
-      labelTextStyle: const WidgetStatePropertyAll(
-        TextStyle(
-          fontSize: 13,
-          fontWeight: FontWeight.w500,
-        ),
-      ),
-    ),
-    inputDecorationTheme: InputDecorationTheme(
-      focusedBorder: OutlineInputBorder(
-        borderSide: BorderSide(
-          color: primaryColor,
-        ),
-        borderRadius: const BorderRadius.all(Radius.circular(15)),
-      ),
-      enabledBorder: OutlineInputBorder(
-        borderSide: BorderSide(
-          color: colorScheme.outlineVariant,
-        ),
-        borderRadius: const BorderRadius.all(Radius.circular(15)),
-      ),
-      labelStyle: TextStyle(
-        color: primaryColor,
-      ),
-      hintStyle: const TextStyle(
-        fontSize: 14.0,
-        fontWeight: FontWeight.normal,
-      ),
-    ),
-    textSelectionTheme: TextSelectionThemeData(
-      cursorColor: primaryColor,
-    ),
-    dropdownMenuTheme: DropdownMenuThemeData(
-      menuStyle: MenuStyle(
-        shape: WidgetStatePropertyAll<OutlinedBorder>(
-          RoundedRectangleBorder(
-            borderRadius: BorderRadius.circular(15),
-          ),
-        ),
-      ),
-      inputDecorationTheme: InputDecorationTheme(
-        focusedBorder: OutlineInputBorder(
-          borderSide: BorderSide(
-            color: primaryColor,
-          ),
-        ),
-        enabledBorder: OutlineInputBorder(
-          borderSide: BorderSide(
-            color: colorScheme.outlineVariant,
-          ),
-          borderRadius: const BorderRadius.all(Radius.circular(15)),
-        ),
-        labelStyle: TextStyle(
-          color: primaryColor,
-        ),
-        hintStyle: const TextStyle(
-          fontSize: 14.0,
-          fontWeight: FontWeight.normal,
-        ),
-      ),
-    ),
-  );
+String? _getFontFamilyFromLocale(Locale locale) {
+  if (localesNotSupportedByOverpass.contains(locale)) {
+    // Let Flutter use the default font
+    return null;
+  }
+  return 'Overpass';
 }
diff --git a/mobile/lib/utils/debounce.dart b/mobile/lib/utils/debounce.dart
index ca5f8fc2be..78870151a6 100644
--- a/mobile/lib/utils/debounce.dart
+++ b/mobile/lib/utils/debounce.dart
@@ -3,20 +3,52 @@ import 'dart:async';
 import 'package:flutter_hooks/flutter_hooks.dart';
 
 /// Used to debounce function calls with the [interval] provided.
+/// If [maxWaitTime] is provided, the first [run] call as well as the next call since [maxWaitTime] has passed will be immediately executed, even if [interval] is not satisfied.
 class Debouncer {
-  Debouncer({required this.interval});
+  Debouncer({required this.interval, this.maxWaitTime});
   final Duration interval;
+  final Duration? maxWaitTime;
   Timer? _timer;
   FutureOr<void> Function()? _lastAction;
+  DateTime? _lastActionTime;
+  Future<void>? _actionFuture;
 
   void run(FutureOr<void> Function() action) {
     _lastAction = action;
     _timer?.cancel();
+
+    if (maxWaitTime != null &&
+        // _actionFuture == null && // TODO: should this check be here?
+        (_lastActionTime == null ||
+            DateTime.now().difference(_lastActionTime!) > maxWaitTime!)) {
+      _callAndRest();
+      return;
+    }
     _timer = Timer(interval, _callAndRest);
   }
 
+  Future<void>? drain() {
+    if (_timer != null && _timer!.isActive) {
+      _timer!.cancel();
+      if (_lastAction != null) {
+        _callAndRest();
+      }
+    }
+    return _actionFuture;
+  }
+
+  @pragma('vm:prefer-inline')
   void _callAndRest() {
-    _lastAction?.call();
+    _lastActionTime = DateTime.now();
+    final action = _lastAction;
+    _lastAction = null;
+
+    final result = action!();
+    if (result is Future) {
+      _actionFuture = result.whenComplete(() {
+        _actionFuture = null;
+      });
+    }
     _timer = null;
   }
 
@@ -24,31 +56,48 @@ class Debouncer {
     _timer?.cancel();
     _timer = null;
     _lastAction = null;
+    _lastActionTime = null;
+    _actionFuture = null;
   }
+
+  bool get isActive =>
+      _actionFuture != null || (_timer != null && _timer!.isActive);
 }
 
 /// Creates a [Debouncer] that will be disposed automatically. If no [interval] is provided, a
 /// default interval of 300ms is used to debounce the function calls
 Debouncer useDebouncer({
   Duration interval = const Duration(milliseconds: 300),
+  Duration? maxWaitTime,
   List<Object?>? keys,
 }) =>
-    use(_DebouncerHook(interval: interval, keys: keys));
+    use(
+      _DebouncerHook(
+        interval: interval,
+        maxWaitTime: maxWaitTime,
+        keys: keys,
+      ),
+    );
 
 class _DebouncerHook extends Hook<Debouncer> {
   const _DebouncerHook({
     required this.interval,
+    this.maxWaitTime,
     super.keys,
   });
 
   final Duration interval;
+  final Duration? maxWaitTime;
 
   @override
   HookState<Debouncer, Hook<Debouncer>> createState() => _DebouncerHookState();
 }
 
 class _DebouncerHookState extends HookState<Debouncer, _DebouncerHook> {
-  late final debouncer = Debouncer(interval: hook.interval);
+  late final debouncer = Debouncer(
+    interval: hook.interval,
+    maxWaitTime: hook.maxWaitTime,
+  );
 
   @override
   Debouncer build(_) => debouncer;
diff --git a/mobile/lib/utils/diff.dart b/mobile/lib/utils/diff.dart
index 18e3843819..a36902d8c7 100644
--- a/mobile/lib/utils/diff.dart
+++ b/mobile/lib/utils/diff.dart
@@ -1,16 +1,20 @@
 import 'dart:async';
 
+import 'package:collection/collection.dart';
+
 /// Efficiently compares two sorted lists in O(n), calling the given callback
 /// for each item.
 /// Return `true` if there are any differences found, else `false`
-Future<bool> diffSortedLists<A, B>(
-  List<A> la,
-  List<B> lb, {
-  required int Function(A a, B b) compare,
-  required FutureOr<bool> Function(A a, B b) both,
-  required FutureOr<void> Function(A a) onlyFirst,
-  required FutureOr<void> Function(B b) onlySecond,
+Future<bool> diffSortedLists<T>(
+  List<T> la,
+  List<T> lb, {
+  required int Function(T a, T b) compare,
+  required FutureOr<bool> Function(T a, T b) both,
+  required FutureOr<void> Function(T a) onlyFirst,
+  required FutureOr<void> Function(T b) onlySecond,
 }) async {
+  assert(la.isSorted(compare), "first argument must be sorted");
+  assert(lb.isSorted(compare), "second argument must be sorted");
   bool diff = false;
   int i = 0, j = 0;
   for (; i < la.length && j < lb.length;) {
@@ -38,14 +42,16 @@ Future<bool> diffSortedLists<A, B>(
 /// Efficiently compares two sorted lists in O(n), calling the given callback
 /// for each item.
 /// Return `true` if there are any differences found, else `false`
-bool diffSortedListsSync<A, B>(
-  List<A> la,
-  List<B> lb, {
-  required int Function(A a, B b) compare,
-  required bool Function(A a, B b) both,
-  required void Function(A a) onlyFirst,
-  required void Function(B b) onlySecond,
+bool diffSortedListsSync<T>(
+  List<T> la,
+  List<T> lb, {
+  required int Function(T a, T b) compare,
+  required bool Function(T a, T b) both,
+  required void Function(T a) onlyFirst,
+  required void Function(T b) onlySecond,
 }) {
+  assert(la.isSorted(compare), "first argument must be sorted");
+  assert(lb.isSorted(compare), "second argument must be sorted");
   bool diff = false;
   int i = 0, j = 0;
   for (; i < la.length && j < lb.length;) {
diff --git a/mobile/lib/utils/download.dart b/mobile/lib/utils/download.dart
new file mode 100644
index 0000000000..c701f353a2
--- /dev/null
+++ b/mobile/lib/utils/download.dart
@@ -0,0 +1,3 @@
+const downloadGroupImage = 'group_image';
+const downloadGroupVideo = 'group_video';
+const downloadGroupLivePhoto = 'group_livephoto';
diff --git a/mobile/lib/utils/hooks/chewiew_controller_hook.dart b/mobile/lib/utils/hooks/chewiew_controller_hook.dart
deleted file mode 100644
index 2868e896cf..0000000000
--- a/mobile/lib/utils/hooks/chewiew_controller_hook.dart
+++ /dev/null
@@ -1,161 +0,0 @@
-import 'package:chewie/chewie.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:video_player/video_player.dart';
-
-/// Provides the initialized video player controller
-/// If the asset is local, use the local file
-/// Otherwise, use a video player with a URL
-ChewieController useChewieController({
-  required VideoPlayerController controller,
-  EdgeInsets controlsSafeAreaMinimum = const EdgeInsets.only(
-    bottom: 100,
-  ),
-  bool showOptions = true,
-  bool showControlsOnInitialize = false,
-  bool autoPlay = true,
-  bool allowFullScreen = false,
-  bool allowedScreenSleep = false,
-  bool showControls = true,
-  bool loopVideo = false,
-  Widget? customControls,
-  Widget? placeholder,
-  Duration hideControlsTimer = const Duration(seconds: 1),
-  VoidCallback? onPlaying,
-  VoidCallback? onPaused,
-  VoidCallback? onVideoEnded,
-}) {
-  return use(
-    _ChewieControllerHook(
-      controller: controller,
-      placeholder: placeholder,
-      showOptions: showOptions,
-      controlsSafeAreaMinimum: controlsSafeAreaMinimum,
-      autoPlay: autoPlay,
-      allowFullScreen: allowFullScreen,
-      customControls: customControls,
-      hideControlsTimer: hideControlsTimer,
-      showControlsOnInitialize: showControlsOnInitialize,
-      showControls: showControls,
-      loopVideo: loopVideo,
-      allowedScreenSleep: allowedScreenSleep,
-      onPlaying: onPlaying,
-      onPaused: onPaused,
-      onVideoEnded: onVideoEnded,
-    ),
-  );
-}
-
-class _ChewieControllerHook extends Hook<ChewieController> {
-  final VideoPlayerController controller;
-  final EdgeInsets controlsSafeAreaMinimum;
-  final bool showOptions;
-  final bool showControlsOnInitialize;
-  final bool autoPlay;
-  final bool allowFullScreen;
-  final bool allowedScreenSleep;
-  final bool showControls;
-  final bool loopVideo;
-  final Widget? customControls;
-  final Widget? placeholder;
-  final Duration hideControlsTimer;
-  final VoidCallback? onPlaying;
-  final VoidCallback? onPaused;
-  final VoidCallback? onVideoEnded;
-
-  const _ChewieControllerHook({
-    required this.controller,
-    this.controlsSafeAreaMinimum = const EdgeInsets.only(
-      bottom: 100,
-    ),
-    this.showOptions = true,
-    this.showControlsOnInitialize = false,
-    this.autoPlay = true,
-    this.allowFullScreen = false,
-    this.allowedScreenSleep = false,
-    this.showControls = true,
-    this.loopVideo = false,
-    this.customControls,
-    this.placeholder,
-    this.hideControlsTimer = const Duration(seconds: 3),
-    this.onPlaying,
-    this.onPaused,
-    this.onVideoEnded,
-  });
-
-  @override
-  createState() => _ChewieControllerHookState();
-}
-
-class _ChewieControllerHookState
-    extends HookState<ChewieController, _ChewieControllerHook> {
-  late ChewieController chewieController = ChewieController(
-    videoPlayerController: hook.controller,
-    controlsSafeAreaMinimum: hook.controlsSafeAreaMinimum,
-    showOptions: hook.showOptions,
-    showControlsOnInitialize: hook.showControlsOnInitialize,
-    autoPlay: hook.autoPlay,
-    allowFullScreen: hook.allowFullScreen,
-    allowedScreenSleep: hook.allowedScreenSleep,
-    showControls: hook.showControls,
-    looping: hook.loopVideo,
-    customControls: hook.customControls,
-    placeholder: hook.placeholder,
-    hideControlsTimer: hook.hideControlsTimer,
-  );
-
-  @override
-  void dispose() {
-    chewieController.dispose();
-    super.dispose();
-  }
-
-  @override
-  ChewieController build(BuildContext context) {
-    return chewieController;
-  }
-
-  /*
-  /// Initializes the chewie controller and video player controller
-  Future<void> _initialize() async {
-    if (hook.asset.isLocal && hook.asset.livePhotoVideoId == null) {
-      // Use a local file for the video player controller
-      final file = await hook.asset.local!.file;
-      if (file == null) {
-        throw Exception('No file found for the video');
-      }
-      videoPlayerController = VideoPlayerController.file(file);
-    } else {
-      // Use a network URL for the video player controller
-      final serverEndpoint = store.Store.get(store.StoreKey.serverEndpoint);
-      final String videoUrl = hook.asset.livePhotoVideoId != null
-          ? '$serverEndpoint/assets/${hook.asset.livePhotoVideoId}/video/playback'
-          : '$serverEndpoint/assets/${hook.asset.remoteId}/video/playback';
-
-      final url = Uri.parse(videoUrl);
-      final accessToken = store.Store.get(StoreKey.accessToken);
-
-      videoPlayerController = VideoPlayerController.networkUrl(
-        url,
-        httpHeaders: {"x-immich-user-token": accessToken},
-      );
-    }
-
-    await videoPlayerController!.initialize();
-
-    chewieController = ChewieController(
-      videoPlayerController: videoPlayerController!,
-      controlsSafeAreaMinimum: hook.controlsSafeAreaMinimum,
-      showOptions: hook.showOptions,
-      showControlsOnInitialize: hook.showControlsOnInitialize,
-      autoPlay: hook.autoPlay,
-      allowFullScreen: hook.allowFullScreen,
-      allowedScreenSleep: hook.allowedScreenSleep,
-      showControls: hook.showControls,
-      customControls: hook.customControls,
-      placeholder: hook.placeholder,
-      hideControlsTimer: hook.hideControlsTimer,
-    );
-  }
-  */
-}
diff --git a/mobile/lib/utils/hooks/interval_hook.dart b/mobile/lib/utils/hooks/interval_hook.dart
new file mode 100644
index 0000000000..0c346065f7
--- /dev/null
+++ b/mobile/lib/utils/hooks/interval_hook.dart
@@ -0,0 +1,18 @@
+import 'dart:async';
+import 'dart:ui';
+
+import 'package:flutter_hooks/flutter_hooks.dart';
+
+// https://github.com/rrousselGit/flutter_hooks/issues/233#issuecomment-840416638
+void useInterval(Duration delay, VoidCallback callback) {
+  final savedCallback = useRef(callback);
+  savedCallback.value = callback;
+
+  useEffect(
+    () {
+      final timer = Timer.periodic(delay, (_) => savedCallback.value());
+      return timer.cancel;
+    },
+    [delay],
+  );
+}
diff --git a/mobile/lib/utils/image_url_builder.dart b/mobile/lib/utils/image_url_builder.dart
index e7a1b9e39e..9fc7b13eed 100644
--- a/mobile/lib/utils/image_url_builder.dart
+++ b/mobile/lib/utils/image_url_builder.dart
@@ -1,7 +1,7 @@
+import 'package:immich_mobile/constants/constants.dart';
 import 'package:immich_mobile/entities/album.entity.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
-import 'package:isar/isar.dart';
 import 'package:openapi/api.dart';
 
 String getThumbnailUrl(
@@ -61,7 +61,7 @@ String getOriginalUrlForRemoteId(final String id) {
 
 String getImageCacheKey(final Asset asset) {
   // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
-  final isFromDto = asset.id == Isar.autoIncrement;
+  final isFromDto = asset.id == noDbId;
   return '${isFromDto ? asset.remoteId : asset.id}_fullStage';
 }
 
diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart
index 2b02a5ff8f..681f8a22ce 100644
--- a/mobile/lib/utils/migration.dart
+++ b/mobile/lib/utils/migration.dart
@@ -4,7 +4,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
 import 'package:immich_mobile/utils/db.dart';
 import 'package:isar/isar.dart';
 
-const int targetVersion = 6;
+const int targetVersion = 8;
 
 Future<void> migrateDatabaseIfNeeded(Isar db) async {
   final int version = Store.get(StoreKey.version, 1);
diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart
index 349b2322af..255ad01247 100644
--- a/mobile/lib/utils/openapi_patching.dart
+++ b/mobile/lib/utils/openapi_patching.dart
@@ -12,6 +12,29 @@ dynamic upgradeDto(dynamic value, String targetType) {
         addDefault(value, 'tags', TagsResponse().toJson());
       }
       break;
+    case 'ServerConfigDto':
+      if (value is Map) {
+        addDefault(
+          value,
+          'mapLightStyleUrl',
+          'https://tiles.immich.cloud/v1/style/light.json',
+        );
+        addDefault(
+          value,
+          'mapDarkStyleUrl',
+          'https://tiles.immich.cloud/v1/style/dark.json',
+        );
+      }
+    case 'UserResponseDto':
+      if (value is Map) {
+        addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
+      }
+      break;
+    case 'UserAdminResponseDto':
+      if (value is Map) {
+        addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
+      }
+      break;
   }
 }
 
diff --git a/mobile/lib/utils/provider_utils.dart b/mobile/lib/utils/provider_utils.dart
new file mode 100644
index 0000000000..3eac55089d
--- /dev/null
+++ b/mobile/lib/utils/provider_utils.dart
@@ -0,0 +1,16 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/repositories/activity_api.repository.dart';
+import 'package:immich_mobile/repositories/album_api.repository.dart';
+import 'package:immich_mobile/repositories/asset_api.repository.dart';
+import 'package:immich_mobile/repositories/partner_api.repository.dart';
+import 'package:immich_mobile/repositories/person_api.repository.dart';
+import 'package:immich_mobile/repositories/user_api.repository.dart';
+
+void invalidateAllApiRepositoryProviders(WidgetRef ref) {
+  ref.invalidate(userApiRepositoryProvider);
+  ref.invalidate(activityApiRepositoryProvider);
+  ref.invalidate(partnerApiRepositoryProvider);
+  ref.invalidate(albumApiRepositoryProvider);
+  ref.invalidate(personApiRepositoryProvider);
+  ref.invalidate(assetApiRepositoryProvider);
+}
diff --git a/mobile/lib/utils/throttle.dart b/mobile/lib/utils/throttle.dart
index 9a54e01fc1..bc0dcf9e2f 100644
--- a/mobile/lib/utils/throttle.dart
+++ b/mobile/lib/utils/throttle.dart
@@ -1,5 +1,3 @@
-import 'dart:async';
-
 import 'package:flutter_hooks/flutter_hooks.dart';
 
 /// Throttles function calls with the [interval] provided.
@@ -10,12 +8,15 @@ class Throttler {
 
   Throttler({required this.interval});
 
-  void run(FutureOr<void> Function() action) {
+  T? run<T>(T Function() action) {
     if (_lastActionTime == null ||
         (DateTime.now().difference(_lastActionTime!) > interval)) {
-      action();
+      final response = action();
       _lastActionTime = DateTime.now();
+      return response;
     }
+
+    return null;
   }
 
   void dispose() {
diff --git a/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart b/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart
index 46fa0b1fe8..6856ae184d 100644
--- a/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart
+++ b/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart
@@ -5,7 +5,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/providers/album/album.provider.dart';
-import 'package:immich_mobile/providers/album/shared_album.provider.dart';
 import 'package:immich_mobile/services/album.service.dart';
 import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart';
 import 'package:immich_mobile/routing/router.dart';
@@ -27,13 +26,11 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
   Widget build(BuildContext context, WidgetRef ref) {
     final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
     final albumService = ref.watch(albumServiceProvider);
-    final sharedAlbums = ref.watch(sharedAlbumProvider);
 
     useEffect(
       () {
         // Fetch album updates, e.g., cover image
-        ref.read(albumProvider.notifier).getAllAlbums();
-        ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
+        ref.read(albumProvider.notifier).refreshRemoteAlbums();
 
         return null;
       },
@@ -41,9 +38,9 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
     );
 
     void addToAlbum(Album album) async {
-      final result = await albumService.addAdditionalAssetToAlbum(
-        assets,
+      final result = await albumService.addAssets(
         album,
+        assets,
       );
 
       if (result != null) {
@@ -107,8 +104,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
                         onPressed: () {
                           context.pushRoute(
                             CreateAlbumRoute(
-                              isSharedAlbum: false,
-                              initialAssets: assets,
+                              assets: assets,
                             ),
                           );
                         },
@@ -123,7 +119,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
             padding: const EdgeInsets.symmetric(horizontal: 16),
             sliver: AddToAlbumSliverList(
               albums: albums,
-              sharedAlbums: sharedAlbums,
+              sharedAlbums: albums.where((a) => a.shared).toList(),
               onAddToAlbum: addToAlbum,
             ),
           ),
diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart
index 42fa55cdd4..b728f2b541 100644
--- a/mobile/lib/widgets/album/album_thumbnail_card.dart
+++ b/mobile/lib/widgets/album/album_thumbnail_card.dart
@@ -12,12 +12,14 @@ class AlbumThumbnailCard extends StatelessWidget {
   /// Whether or not to show the owner of the album (or "Owned")
   /// in the subtitle of the album
   final bool showOwner;
+  final bool showTitle;
 
   const AlbumThumbnailCard({
     super.key,
     required this.album,
     this.onTap,
     this.showOwner = false,
+    this.showTitle = true,
   });
 
   final Album album;
@@ -76,7 +78,7 @@ class AlbumThumbnailCard extends StatelessWidget {
                       : 'album_thumbnail_card_items'
                           .tr(args: ['${album.assetCount}']),
                 ),
-                if (owner != null) const TextSpan(text: ' · '),
+                if (owner != null) const TextSpan(text: ' • '),
                 if (owner != null) TextSpan(text: owner),
               ],
             ),
@@ -102,21 +104,23 @@ class AlbumThumbnailCard extends StatelessWidget {
                             : buildAlbumThumbnail(),
                       ),
                     ),
-                    Padding(
-                      padding: const EdgeInsets.only(top: 8.0),
-                      child: SizedBox(
-                        width: cardSize,
-                        child: Text(
-                          album.name,
-                          overflow: TextOverflow.ellipsis,
-                          style: context.textTheme.bodyMedium?.copyWith(
-                            color: context.colorScheme.onSurface,
-                            fontWeight: FontWeight.w500,
+                    if (showTitle) ...[
+                      Padding(
+                        padding: const EdgeInsets.only(top: 8.0),
+                        child: SizedBox(
+                          width: cardSize,
+                          child: Text(
+                            album.name,
+                            overflow: TextOverflow.ellipsis,
+                            style: context.textTheme.titleSmall?.copyWith(
+                              color: context.colorScheme.onSurface,
+                              fontWeight: FontWeight.w500,
+                            ),
                           ),
                         ),
                       ),
-                    ),
-                    buildAlbumTextRow(),
+                      buildAlbumTextRow(),
+                    ],
                   ],
                 ),
               ),
diff --git a/mobile/lib/widgets/album/album_viewer_appbar.dart b/mobile/lib/widgets/album/album_viewer_appbar.dart
index 1067d7241e..7c36ebc21d 100644
--- a/mobile/lib/widgets/album/album_viewer_appbar.dart
+++ b/mobile/lib/widgets/album/album_viewer_appbar.dart
@@ -1,23 +1,21 @@
 import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/providers/activity_statistics.provider.dart';
 import 'package:immich_mobile/providers/album/album.provider.dart';
 import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
-import 'package:immich_mobile/providers/album/shared_album.provider.dart';
-import 'package:immich_mobile/utils/immich_loading_overlay.dart';
+import 'package:immich_mobile/providers/album/current_album.provider.dart';
 import 'package:immich_mobile/routing/router.dart';
-import 'package:immich_mobile/entities/album.entity.dart';
 import 'package:immich_mobile/widgets/common/immich_toast.dart';
 
 class AlbumViewerAppbar extends HookConsumerWidget
     implements PreferredSizeWidget {
   const AlbumViewerAppbar({
     super.key,
-    required this.album,
     required this.userId,
     required this.titleFocusNode,
     this.onAddPhotos,
@@ -25,36 +23,48 @@ class AlbumViewerAppbar extends HookConsumerWidget
     required this.onActivities,
   });
 
-  final Album album;
   final String userId;
   final FocusNode titleFocusNode;
-  final Function(Album album)? onAddPhotos;
-  final Function(Album album)? onAddUsers;
-  final Function(Album album) onActivities;
+  final void Function()? onAddPhotos;
+  final void Function()? onAddUsers;
+  final void Function() onActivities;
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
-    final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
-    final isProcessing = useProcessingOverlay();
+    final albumState = useState(ref.read(currentAlbumProvider));
+    final album = albumState.value;
+    ref.listen(currentAlbumProvider, (_, newAlbum) {
+      final oldAlbum = albumState.value;
+      if (oldAlbum != null && newAlbum != null && oldAlbum.id == newAlbum.id) {
+        return;
+      }
+
+      albumState.value = newAlbum;
+    });
+
+    if (album == null) {
+      return const SizedBox();
+    }
+
+    final albumViewer = ref.watch(albumViewerProvider);
+    final newAlbumTitle = albumViewer.editTitleText;
+    final isEditAlbum = albumViewer.isEditAlbum;
+
     final comments = album.shared
         ? ref.watch(activityStatisticsProvider(album.remoteId!))
         : 0;
 
     deleteAlbum() async {
-      isProcessing.value = true;
+      final bool success =
+          await ref.watch(albumProvider.notifier).deleteAlbum(album);
 
-      final bool success;
       if (album.shared) {
-        success =
-            await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(album);
-        context
-            .navigateTo(const TabControllerRoute(children: [SharingRoute()]));
+        context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]));
       } else {
-        success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
         context
             .navigateTo(const TabControllerRoute(children: [LibraryRoute()]));
       }
+
       if (!success) {
         ImmichToast.show(
           context: context,
@@ -63,11 +73,9 @@ class AlbumViewerAppbar extends HookConsumerWidget
           gravity: ToastGravity.BOTTOM,
         );
       }
-
-      isProcessing.value = false;
     }
 
-    Future<void> showConfirmationDialog() async {
+    Future<void> onDeleteAlbumPressed() {
       return showDialog<void>(
         context: context,
         barrierDismissible: false, // user must tap button!
@@ -105,19 +113,12 @@ class AlbumViewerAppbar extends HookConsumerWidget
       );
     }
 
-    void onDeleteAlbumPressed() async {
-      showConfirmationDialog();
-    }
-
     void onLeaveAlbumPressed() async {
-      isProcessing.value = true;
-
       bool isSuccess =
-          await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album);
+          await ref.watch(albumProvider.notifier).leaveAlbum(album);
 
       if (isSuccess) {
-        context
-            .navigateTo(const TabControllerRoute(children: [SharingRoute()]));
+        context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]));
       } else {
         context.pop();
         ImmichToast.show(
@@ -127,8 +128,6 @@ class AlbumViewerAppbar extends HookConsumerWidget
           gravity: ToastGravity.BOTTOM,
         );
       }
-
-      isProcessing.value = false;
     }
 
     buildBottomSheetActions() {
@@ -140,7 +139,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
                   'album_viewer_appbar_share_delete',
                   style: TextStyle(fontWeight: FontWeight.w500),
                 ).tr(),
-                onTap: () => onDeleteAlbumPressed(),
+                onTap: onDeleteAlbumPressed,
               )
             : ListTile(
                 leading: const Icon(Icons.person_remove_rounded),
@@ -148,25 +147,52 @@ class AlbumViewerAppbar extends HookConsumerWidget
                   'album_viewer_appbar_share_leave',
                   style: TextStyle(fontWeight: FontWeight.w500),
                 ).tr(),
-                onTap: () => onLeaveAlbumPressed(),
+                onTap: onLeaveAlbumPressed,
               ),
       ];
       // }
     }
 
+    void onSortOrderToggled() async {
+      final updatedAlbum =
+          await ref.read(albumProvider.notifier).toggleSortOrder(album);
+
+      if (updatedAlbum == null) {
+        ImmichToast.show(
+          context: context,
+          msg: "error_change_sort_album".tr(),
+          toastType: ToastType.error,
+          gravity: ToastGravity.BOTTOM,
+        );
+      }
+
+      context.pop();
+    }
+
     void buildBottomSheet() {
       final ownerActions = [
         ListTile(
           leading: const Icon(Icons.person_add_alt_rounded),
           onTap: () {
             context.pop();
-            onAddUsers!(album);
+            final onAddUsers = this.onAddUsers;
+            if (onAddUsers != null) {
+              onAddUsers();
+            }
           },
           title: const Text(
             "album_viewer_page_share_add_users",
             style: TextStyle(fontWeight: FontWeight.w500),
           ).tr(),
         ),
+        ListTile(
+          leading: const Icon(Icons.swap_vert_rounded),
+          onTap: onSortOrderToggled,
+          title: const Text(
+            "change_display_order",
+            style: TextStyle(fontWeight: FontWeight.w500),
+          ).tr(),
+        ),
         ListTile(
           leading: const Icon(Icons.share_rounded),
           onTap: () {
@@ -180,7 +206,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
         ),
         ListTile(
           leading: const Icon(Icons.settings_rounded),
-          onTap: () => context.navigateTo(AlbumOptionsRoute(album: album)),
+          onTap: () => context.navigateTo(AlbumOptionsRoute()),
           title: const Text(
             "translated_text_options",
             style: TextStyle(fontWeight: FontWeight.w500),
@@ -193,7 +219,10 @@ class AlbumViewerAppbar extends HookConsumerWidget
           leading: const Icon(Icons.add_photo_alternate_outlined),
           onTap: () {
             context.pop();
-            onAddPhotos!(album);
+            final onAddPhotos = this.onAddPhotos;
+            if (onAddPhotos != null) {
+              onAddPhotos();
+            }
           },
           title: const Text(
             "share_add_photos",
@@ -226,9 +255,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
 
     Widget buildActivitiesButton() {
       return IconButton(
-        onPressed: () {
-          onActivities(album);
-        },
+        onPressed: onActivities,
         icon: Row(
           crossAxisAlignment: CrossAxisAlignment.center,
           children: [
@@ -275,7 +302,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
         );
       } else {
         return IconButton(
-          onPressed: () async => await context.maybePop(),
+          onPressed: context.maybePop,
           icon: const Icon(Icons.arrow_back_ios_rounded),
           splashRadius: 25,
         );
@@ -289,12 +316,13 @@ class AlbumViewerAppbar extends HookConsumerWidget
       actions: [
         if (album.shared && (album.activityEnabled || comments != 0))
           buildActivitiesButton(),
-        if (album.isRemote)
+        if (album.isRemote) ...[
           IconButton(
             splashRadius: 25,
             onPressed: buildBottomSheet,
             icon: const Icon(Icons.more_horiz_rounded),
           ),
+        ],
       ],
     );
   }
diff --git a/mobile/lib/widgets/album/album_viewer_editable_title.dart b/mobile/lib/widgets/album/album_viewer_editable_title.dart
index 59e09aa050..7547dff932 100644
--- a/mobile/lib/widgets/album/album_viewer_editable_title.dart
+++ b/mobile/lib/widgets/album/album_viewer_editable_title.dart
@@ -4,20 +4,19 @@ import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
-import 'package:immich_mobile/entities/album.entity.dart';
 
 class AlbumViewerEditableTitle extends HookConsumerWidget {
-  final Album album;
+  final String albumName;
   final FocusNode titleFocusNode;
   const AlbumViewerEditableTitle({
     super.key,
-    required this.album,
+    required this.albumName,
     required this.titleFocusNode,
   });
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    final titleTextEditController = useTextEditingController(text: album.name);
+    final titleTextEditController = useTextEditingController(text: albumName);
 
     void onFocusModeChange() {
       if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
@@ -49,9 +48,9 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
         style: context.textTheme.headlineMedium,
         controller: titleTextEditController,
         onTap: () {
-          FocusScope.of(context).requestFocus(titleFocusNode);
+          context.focusScope.requestFocus(titleFocusNode);
 
-          ref.watch(albumViewerProvider.notifier).setEditTitleText(album.name);
+          ref.watch(albumViewerProvider.notifier).setEditTitleText(albumName);
           ref.watch(albumViewerProvider.notifier).enableEditAlbum();
 
           if (titleTextEditController.text == 'Untitled') {
diff --git a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart
index e6d769a3d7..ec054d08ee 100644
--- a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart
+++ b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart
@@ -4,7 +4,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/providers/album/album.provider.dart';
-import 'package:immich_mobile/providers/album/shared_album.provider.dart';
 import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart';
 import 'package:immich_mobile/models/asset_selection_state.dart';
 import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
@@ -72,7 +71,8 @@ class ControlBottomAppBar extends HookConsumerWidget {
     final trashEnabled =
         ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
     final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
-    final sharedAlbums = ref.watch(sharedAlbumProvider);
+    final sharedAlbums =
+        ref.watch(albumProvider).where((a) => a.shared).toList();
     const bottomPadding = 0.20;
     final scrollController = useDraggableScrollController();
 
diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart
index 38e499b5de..c38e61a473 100644
--- a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart
+++ b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart
@@ -12,7 +12,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/collection_extensions.dart';
 import 'package:immich_mobile/extensions/theme_extensions.dart';
+import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
+import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
 import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
+import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
 import 'package:immich_mobile/widgets/asset_grid/asset_drag_region.dart';
 import 'package:immich_mobile/widgets/asset_grid/thumbnail_image.dart';
 import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart';
@@ -89,6 +92,7 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
       ScrollOffsetController();
   final ItemPositionsListener _itemPositionsListener =
       ItemPositionsListener.create();
+  late final KeepAliveLink currentAssetLink;
 
   /// The timestamp when the haptic feedback was last invoked
   int _hapticFeedbackTS = 0;
@@ -201,6 +205,13 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
       allAssetsSelected: _allAssetsSelected,
       showStack: widget.showStack,
       heroOffset: widget.heroOffset,
+      onAssetTap: (asset) {
+        ref.read(currentAssetProvider.notifier).set(asset);
+        ref.read(isPlayingMotionVideoProvider.notifier).playing = false;
+        if (asset.isVideo) {
+          ref.read(showControlsProvider.notifier).show = false;
+        }
+      },
     );
   }
 
@@ -348,6 +359,7 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
   @override
   void initState() {
     super.initState();
+    currentAssetLink = ref.read(currentAssetProvider.notifier).ref.keepAlive();
     scrollToTopNotifierProvider.addListener(_scrollToTop);
     scrollToDateNotifierProvider.addListener(_scrollToDate);
 
@@ -369,6 +381,7 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
       _itemPositionsListener.itemPositions.removeListener(_positionListener);
     }
     _itemPositionsListener.itemPositions.removeListener(_hapticsListener);
+    currentAssetLink.close();
     super.dispose();
   }
 
@@ -595,12 +608,13 @@ class _Section extends StatelessWidget {
   final RenderList renderList;
   final bool selectionActive;
   final bool dynamicLayout;
-  final Function(List<Asset>) selectAssets;
-  final Function(List<Asset>) deselectAssets;
+  final void Function(List<Asset>) selectAssets;
+  final void Function(List<Asset>) deselectAssets;
   final bool Function(List<Asset>) allAssetsSelected;
   final bool showStack;
   final int heroOffset;
   final bool showStorageIndicator;
+  final void Function(Asset) onAssetTap;
 
   const _Section({
     required this.section,
@@ -618,6 +632,7 @@ class _Section extends StatelessWidget {
     required this.showStack,
     required this.heroOffset,
     required this.showStorageIndicator,
+    required this.onAssetTap,
   });
 
   @override
@@ -683,6 +698,7 @@ class _Section extends StatelessWidget {
                       selectionActive: selectionActive,
                       onSelect: (asset) => selectAssets([asset]),
                       onDeselect: (asset) => deselectAssets([asset]),
+                      onAssetTap: onAssetTap,
                     ),
           ],
         );
@@ -724,9 +740,9 @@ class _Title extends StatelessWidget {
   final String title;
   final List<Asset> assets;
   final bool selectionActive;
-  final Function(List<Asset>) selectAssets;
-  final Function(List<Asset>) deselectAssets;
-  final Function(List<Asset>) allAssetsSelected;
+  final void Function(List<Asset>) selectAssets;
+  final void Function(List<Asset>) deselectAssets;
+  final bool Function(List<Asset>) allAssetsSelected;
 
   const _Title({
     required this.title,
@@ -765,8 +781,9 @@ class _AssetRow extends StatelessWidget {
   final bool showStorageIndicator;
   final int heroOffset;
   final bool showStack;
-  final Function(Asset)? onSelect;
-  final Function(Asset)? onDeselect;
+  final void Function(Asset) onAssetTap;
+  final void Function(Asset)? onSelect;
+  final void Function(Asset)? onDeselect;
   final bool isSelectionActive;
 
   const _AssetRow({
@@ -786,6 +803,7 @@ class _AssetRow extends StatelessWidget {
     required this.showStack,
     required this.isSelectionActive,
     required this.selectedAssets,
+    required this.onAssetTap,
     this.onSelect,
     this.onDeselect,
   });
@@ -838,6 +856,8 @@ class _AssetRow extends StatelessWidget {
                   onSelect?.call(asset);
                 }
               } else {
+                final asset = renderList.loadAsset(absoluteOffset + index);
+                onAssetTap(asset);
                 context.pushRoute(
                   GalleryViewerRoute(
                     renderList: renderList,
diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid.dart b/mobile/lib/widgets/asset_grid/multiselect_grid.dart
index 3263373554..6bcd6e5784 100644
--- a/mobile/lib/widgets/asset_grid/multiselect_grid.dart
+++ b/mobile/lib/widgets/asset_grid/multiselect_grid.dart
@@ -9,7 +9,6 @@ import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/collection_extensions.dart';
 import 'package:immich_mobile/providers/album/album.provider.dart';
-import 'package:immich_mobile/providers/album/shared_album.provider.dart';
 import 'package:immich_mobile/services/album.service.dart';
 import 'package:immich_mobile/services/stack.service.dart';
 import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
@@ -131,11 +130,7 @@ class MultiselectGrid extends HookConsumerWidget {
       processing.value = true;
       if (shareLocal) {
         // Share = Download + Send to OS specific share sheet
-        // Filter offline assets since we cannot fetch their original file
-        final liveAssets = selection.value.nonOfflineOnly(
-          errorCallback: errorBuilder('asset_action_share_err_offline'.tr()),
-        );
-        handleShareAssets(ref, context, liveAssets);
+        handleShareAssets(ref, context, selection.value);
       } else {
         final ids =
             remoteSelection(errorMessage: "home_page_share_err_local".tr())
@@ -208,18 +203,30 @@ class MultiselectGrid extends HookConsumerWidget {
     void onDeleteLocal(bool onlyBackedUp) async {
       processing.value = true;
       try {
+        // Select only the local assets from the selection
         final localIds = selection.value.where((a) => a.isLocal).toList();
 
+        // Delete only the backed-up assets if 'onlyBackedUp' is true
         final isDeleted = await ref
             .read(assetProvider.notifier)
             .deleteLocalOnlyAssets(localIds, onlyBackedUp: onlyBackedUp);
+
         if (isDeleted) {
+          // Show a toast with the correct number of deleted assets
+          final deletedCount = localIds
+              .where(
+                (e) => !onlyBackedUp || e.isRemote,
+              ) // Only count backed-up assets
+              .length;
+
           ImmichToast.show(
             context: context,
             msg: 'assets_removed_permanently_from_device'
-                .tr(args: ["${localIds.length}"]),
+                .tr(args: ["$deletedCount"]),
             gravity: ToastGravity.BOTTOM,
           );
+
+          // Reset the selection
           selectionEnabledHook.value = false;
         }
       } finally {
@@ -276,11 +283,10 @@ class MultiselectGrid extends HookConsumerWidget {
         if (assets.isEmpty) {
           return;
         }
-        final result =
-            await ref.read(albumServiceProvider).addAdditionalAssetToAlbum(
-                  assets,
-                  album,
-                );
+        final result = await ref.read(albumServiceProvider).addAssets(
+              album,
+              assets,
+            );
 
         if (result != null) {
           if (result.alreadyInAlbum.isNotEmpty) {
@@ -327,8 +333,7 @@ class MultiselectGrid extends HookConsumerWidget {
             .createAlbumWithGeneratedName(assets);
 
         if (result != null) {
-          ref.watch(albumProvider.notifier).getAllAlbums();
-          ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
+          ref.watch(albumProvider.notifier).refreshRemoteAlbums();
           selectionEnabledHook.value = false;
 
           context.pushRoute(AlbumViewerRoute(albumId: result.id));
@@ -428,6 +433,7 @@ class MultiselectGrid extends HookConsumerWidget {
               ),
           if (selectionEnabledHook.value)
             ControlBottomAppBar(
+              key: const ValueKey("controlBottomAppBar"),
               onShare: onShareAssets,
               onFavorite: favoriteEnabled ? onFavoriteAssets : null,
               onArchive: archiveEnabled ? onArchiveAsset : null,
diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart b/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart
new file mode 100644
index 0000000000..b17029f2af
--- /dev/null
+++ b/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart
@@ -0,0 +1,35 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/providers/asset_viewer/render_list_status_provider.dart';
+import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart';
+
+class MultiselectGridStatusIndicator extends HookConsumerWidget {
+  const MultiselectGridStatusIndicator({
+    super.key,
+    this.buildLoadingIndicator,
+    this.emptyIndicator,
+  });
+
+  final Widget Function()? buildLoadingIndicator;
+  final Widget? emptyIndicator;
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final renderListStatus = ref.watch(renderListStatusProvider);
+    return switch (renderListStatus) {
+      RenderListStatusEnum.loading => buildLoadingIndicator == null
+          ? const Center(
+              child: DelayedLoadingIndicator(
+                delay: Duration(milliseconds: 500),
+              ),
+            )
+          : buildLoadingIndicator!(),
+      RenderListStatusEnum.empty =>
+        emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()),
+      RenderListStatusEnum.error =>
+        Center(child: const Text("error_loading_assets").tr()),
+      RenderListStatusEnum.complete => const SizedBox()
+    };
+  }
+}
diff --git a/mobile/lib/widgets/asset_grid/thumbnail_image.dart b/mobile/lib/widgets/asset_grid/thumbnail_image.dart
index 8e818f64fb..35013bb595 100644
--- a/mobile/lib/widgets/asset_grid/thumbnail_image.dart
+++ b/mobile/lib/widgets/asset_grid/thumbnail_image.dart
@@ -1,11 +1,11 @@
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/constants/constants.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/extensions/theme_extensions.dart';
 import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
 import 'package:immich_mobile/utils/storage_indicator.dart';
-import 'package:isar/isar.dart';
 
 class ThumbnailImage extends ConsumerWidget {
   /// The asset to show the thumbnail image for
@@ -46,7 +46,7 @@ class ThumbnailImage extends ConsumerWidget {
         ? context.primaryColor.darken(amount: 0.6)
         : context.primaryColor.lighten(amount: 0.8);
     // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
-    final isFromDto = asset.id == Isar.autoIncrement;
+    final isFromDto = asset.id == noDbId;
 
     Widget buildSelectionIcon(Asset asset) {
       if (isSelected) {
@@ -131,17 +131,36 @@ class ThumbnailImage extends ConsumerWidget {
     }
 
     Widget buildImage() {
-      final image = SizedBox(
-        width: 300,
-        height: 300,
+      final image = SizedBox.expand(
         child: Hero(
           tag: isFromDto
               ? '${asset.remoteId}-$heroOffset'
               : asset.id + heroOffset,
-          child: ImmichThumbnail(
-            asset: asset,
-            height: 250,
-            width: 250,
+          child: Stack(
+            children: [
+              SizedBox.expand(
+                child: ImmichThumbnail(
+                  asset: asset,
+                  height: 250,
+                  width: 250,
+                ),
+              ),
+              Container(
+                decoration: const BoxDecoration(
+                  gradient: LinearGradient(
+                    colors: [
+                      Color.fromRGBO(0, 0, 0, 0.1),
+                      Colors.transparent,
+                      Colors.transparent,
+                      Color.fromRGBO(0, 0, 0, 0.1),
+                    ],
+                    begin: Alignment.topCenter,
+                    end: Alignment.bottomCenter,
+                    stops: [0, 0.3, 0.6, 1],
+                  ),
+                ),
+              ),
+            ],
           ),
         ),
       );
@@ -153,11 +172,8 @@ class ThumbnailImage extends ConsumerWidget {
           color: canDeselect ? assetContainerColor : Colors.grey,
         ),
         child: ClipRRect(
-          borderRadius: const BorderRadius.only(
-            topRight: Radius.circular(15.0),
-            bottomRight: Radius.circular(15.0),
-            bottomLeft: Radius.circular(15.0),
-            topLeft: Radius.zero,
+          borderRadius: const BorderRadius.all(
+            Radius.circular(15.0),
           ),
           child: image,
         ),
@@ -177,7 +193,33 @@ class ThumbnailImage extends ConsumerWidget {
                   )
                 : const Border(),
           ),
-          child: buildImage(),
+          child: Stack(
+            children: [
+              buildImage(),
+              if (showStorageIndicator)
+                Positioned(
+                  right: 8,
+                  bottom: 5,
+                  child: Icon(
+                    storageIcon(asset),
+                    color: Colors.white.withOpacity(.8),
+                    size: 16,
+                  ),
+                ),
+              if (asset.isFavorite)
+                const Positioned(
+                  left: 8,
+                  bottom: 5,
+                  child: Icon(
+                    Icons.favorite,
+                    color: Colors.white,
+                    size: 16,
+                  ),
+                ),
+              if (!asset.isImage) buildVideoIcon(),
+              if (asset.stackCount > 0) buildStackIcon(),
+            ],
+          ),
         ),
         if (multiselectEnabled)
           Padding(
@@ -187,28 +229,6 @@ class ThumbnailImage extends ConsumerWidget {
               child: buildSelectionIcon(asset),
             ),
           ),
-        if (showStorageIndicator)
-          Positioned(
-            right: 8,
-            bottom: 5,
-            child: Icon(
-              storageIcon(asset),
-              color: Colors.white.withOpacity(.8),
-              size: 16,
-            ),
-          ),
-        if (asset.isFavorite)
-          const Positioned(
-            left: 8,
-            bottom: 5,
-            child: Icon(
-              Icons.favorite,
-              color: Colors.white,
-              size: 18,
-            ),
-          ),
-        if (!asset.isImage) buildVideoIcon(),
-        if (asset.stackCount > 0) buildStackIcon(),
       ],
     );
   }
diff --git a/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart b/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart
index c63d98fb59..1e6aba2bda 100644
--- a/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart
+++ b/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart
@@ -6,12 +6,18 @@ import 'package:immich_mobile/entities/asset.entity.dart';
 
 class AdvancedBottomSheet extends HookConsumerWidget {
   final Asset assetDetail;
+  final ScrollController? scrollController;
 
-  const AdvancedBottomSheet({super.key, required this.assetDetail});
+  const AdvancedBottomSheet({
+    super.key,
+    required this.assetDetail,
+    this.scrollController,
+  });
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
     return SingleChildScrollView(
+      controller: scrollController,
       child: Container(
         margin: const EdgeInsets.symmetric(horizontal: 8.0),
         child: LayoutBuilder(
@@ -54,7 +60,7 @@ class AdvancedBottomSheet extends HookConsumerWidget {
                                   text: assetDetail.toString(),
                                 ),
                               ).then((_) {
-                                ScaffoldMessenger.of(context).showSnackBar(
+                                context.scaffoldMessenger.showSnackBar(
                                   SnackBar(
                                     content: Text(
                                       "Copied to clipboard",
diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart
index 7e6136c256..256141dc7d 100644
--- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart
+++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart
@@ -6,10 +6,11 @@ import 'package:flutter/material.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/providers/album/album.provider.dart';
 import 'package:immich_mobile/providers/album/current_album.provider.dart';
-import 'package:immich_mobile/providers/album/shared_album.provider.dart';
 import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
-import 'package:immich_mobile/providers/asset_viewer/image_viewer_page_state.provider.dart';
+import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
+import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
 import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
 import 'package:immich_mobile/services/stack.service.dart';
 import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
@@ -25,12 +26,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
 import 'package:immich_mobile/pages/editing/edit.page.dart';
 
 class BottomGalleryBar extends ConsumerWidget {
-  final Asset asset;
   final ValueNotifier<int> assetIndex;
   final bool showStack;
-  final int stackIndex;
+  final ValueNotifier<int> stackIndex;
   final ValueNotifier<int> totalAssets;
-  final bool showVideoPlayerControls;
   final PageController controller;
   final RenderList renderList;
 
@@ -38,20 +37,24 @@ class BottomGalleryBar extends ConsumerWidget {
     super.key,
     required this.showStack,
     required this.stackIndex,
-    required this.asset,
     required this.assetIndex,
     required this.controller,
     required this.totalAssets,
-    required this.showVideoPlayerControls,
     required this.renderList,
   });
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
+    final asset = ref.watch(currentAssetProvider);
+    if (asset == null) {
+      return const SizedBox();
+    }
     final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId;
+    final showControls = ref.watch(showControlsProvider);
+    final stackId = asset.stackId;
 
-    final stackItems = showStack && asset.stackCount > 0
-        ? ref.watch(assetStackStateProvider(asset))
+    final stackItems = showStack && stackId != null
+        ? ref.watch(assetStackStateProvider(stackId))
         : <Asset>[];
     bool isStackPrimaryAsset = asset.stackPrimaryAssetId == null;
     final navStack = AutoRouter.of(context).stackData;
@@ -63,10 +66,10 @@ class BottomGalleryBar extends ConsumerWidget {
     final isInAlbum = ref.watch(currentAlbumProvider)?.isRemote ?? false;
 
     void removeAssetFromStack() {
-      if (stackIndex > 0 && showStack) {
+      if (stackIndex.value > 0 && showStack && stackId != null) {
         ref
-            .read(assetStackStateProvider(asset).notifier)
-            .removeChild(stackIndex - 1);
+            .read(assetStackStateProvider(stackId).notifier)
+            .removeChild(stackIndex.value - 1);
       }
     }
 
@@ -134,7 +137,7 @@ class BottomGalleryBar extends ConsumerWidget {
 
       await ref
           .read(stackServiceProvider)
-          .deleteStack(asset.stackId!, [asset, ...stackItems]);
+          .deleteStack(asset.stackId!, stackItems);
     }
 
     void showStackActionItems() {
@@ -181,21 +184,13 @@ class BottomGalleryBar extends ConsumerWidget {
         );
         return;
       }
-      ref.read(imageViewerStateProvider.notifier).shareAsset(asset, context);
+      ref.read(downloadStateProvider.notifier).shareAsset(asset, context);
     }
 
     void handleEdit() async {
       final image = Image(image: ImmichImage.imageProvider(asset: asset));
-      if (asset.isOffline) {
-        ImmichToast.show(
-          durationInSecond: 1,
-          context: context,
-          msg: 'asset_action_edit_err_offline'.tr(),
-          gravity: ToastGravity.BOTTOM,
-        );
-        return;
-      }
-      Navigator.of(context).push(
+
+      context.navigator.push(
         MaterialPageRoute(
           builder: (context) => EditImagePage(
             asset: asset,
@@ -229,7 +224,7 @@ class BottomGalleryBar extends ConsumerWidget {
         return;
       }
 
-      ref.read(imageViewerStateProvider.notifier).downloadAsset(
+      ref.read(downloadStateProvider.notifier).downloadAsset(
             asset,
             context,
           );
@@ -238,9 +233,7 @@ class BottomGalleryBar extends ConsumerWidget {
     handleRemoveFromAlbum() async {
       final album = ref.read(currentAlbumProvider);
       final bool isSuccess = album != null &&
-          await ref
-              .read(sharedAlbumProvider.notifier)
-              .removeAssetFromAlbum(album, [asset]);
+          await ref.read(albumProvider.notifier).removeAsset(album, [asset]);
 
       if (isSuccess) {
         // Workaround for asset remaining in the gallery
@@ -333,43 +326,55 @@ class BottomGalleryBar extends ConsumerWidget {
         },
     ];
     return IgnorePointer(
-      ignoring: !ref.watch(showControlsProvider),
+      ignoring: !showControls,
       child: AnimatedOpacity(
         duration: const Duration(milliseconds: 100),
-        opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
-        child: Column(
-          children: [
-            Visibility(
-              visible: showVideoPlayerControls,
-              child: const VideoControls(),
+        opacity: showControls ? 1.0 : 0.0,
+        child: DecoratedBox(
+          decoration: const BoxDecoration(
+            gradient: LinearGradient(
+              begin: Alignment.bottomCenter,
+              end: Alignment.topCenter,
+              colors: [Colors.black, Colors.transparent],
             ),
-            BottomNavigationBar(
-              backgroundColor: Colors.black.withOpacity(0.4),
-              unselectedIconTheme: const IconThemeData(color: Colors.white),
-              selectedIconTheme: const IconThemeData(color: Colors.white),
-              unselectedLabelStyle: const TextStyle(
-                color: Colors.white,
-                fontWeight: FontWeight.w500,
-                height: 2.3,
-              ),
-              selectedLabelStyle: const TextStyle(
-                color: Colors.white,
-                fontWeight: FontWeight.w500,
-                height: 2.3,
-              ),
-              unselectedFontSize: 14,
-              selectedFontSize: 14,
-              selectedItemColor: Colors.white,
-              unselectedItemColor: Colors.white,
-              showSelectedLabels: true,
-              showUnselectedLabels: true,
-              items:
-                  albumActions.map((e) => e.keys.first).toList(growable: false),
-              onTap: (index) {
-                albumActions[index].values.first.call(index);
-              },
+          ),
+          position: DecorationPosition.background,
+          child: Padding(
+            padding: const EdgeInsets.only(top: 40.0),
+            child: Column(
+              children: [
+                if (asset.isVideo) const VideoControls(),
+                BottomNavigationBar(
+                  elevation: 0.0,
+                  backgroundColor: Colors.transparent,
+                  unselectedIconTheme: const IconThemeData(color: Colors.white),
+                  selectedIconTheme: const IconThemeData(color: Colors.white),
+                  unselectedLabelStyle: const TextStyle(
+                    color: Colors.white,
+                    fontWeight: FontWeight.w500,
+                    height: 2.3,
+                  ),
+                  selectedLabelStyle: const TextStyle(
+                    color: Colors.white,
+                    fontWeight: FontWeight.w500,
+                    height: 2.3,
+                  ),
+                  unselectedFontSize: 14,
+                  selectedFontSize: 14,
+                  selectedItemColor: Colors.white,
+                  unselectedItemColor: Colors.white,
+                  showSelectedLabels: true,
+                  showUnselectedLabels: true,
+                  items: albumActions
+                      .map((e) => e.keys.first)
+                      .toList(growable: false),
+                  onTap: (index) {
+                    albumActions[index].values.first.call(index);
+                  },
+                ),
+              ],
             ),
-          ],
+          ),
         ),
       ),
     );
diff --git a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart b/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart
index a34fcb9baf..d759b0d80b 100644
--- a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart
+++ b/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart
@@ -1,38 +1,48 @@
 import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
 import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
 import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
 import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
+import 'package:immich_mobile/utils/hooks/timer_hook.dart';
 import 'package:immich_mobile/widgets/asset_viewer/center_play_button.dart';
 import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart';
-import 'package:immich_mobile/utils/hooks/timer_hook.dart';
 
 class CustomVideoPlayerControls extends HookConsumerWidget {
   final Duration hideTimerDuration;
 
   const CustomVideoPlayerControls({
     super.key,
-    this.hideTimerDuration = const Duration(seconds: 3),
+    this.hideTimerDuration = const Duration(seconds: 5),
   });
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
+    final assetIsVideo = ref.watch(
+      currentAssetProvider.select((asset) => asset != null && asset.isVideo),
+    );
+    final showControls = ref.watch(showControlsProvider);
+    final VideoPlaybackState state =
+        ref.watch(videoPlaybackValueProvider.select((value) => value.state));
+
     // A timer to hide the controls
     final hideTimer = useTimer(
       hideTimerDuration,
       () {
+        if (!context.mounted) {
+          return;
+        }
         final state = ref.read(videoPlaybackValueProvider).state;
+
         // Do not hide on paused
-        if (state != VideoPlaybackState.paused) {
+        if (state != VideoPlaybackState.paused &&
+            state != VideoPlaybackState.completed &&
+            assetIsVideo) {
           ref.read(showControlsProvider.notifier).show = false;
         }
       },
     );
-
-    final showBuffering = useState(false);
-    final VideoPlaybackState state =
-        ref.watch(videoPlaybackValueProvider).state;
+    final showBuffering = state == VideoPlaybackState.buffering;
 
     /// Shows the controls and starts the timer to hide them
     void showControlsAndStartHideTimer() {
@@ -40,28 +50,15 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
       ref.read(showControlsProvider.notifier).show = true;
     }
 
-    // When we mute, show the controls
-    ref.listen(videoPlayerControlsProvider.select((v) => v.mute),
-        (previous, next) {
-      showControlsAndStartHideTimer();
-    });
-
     // When we change position, show or hide timer
     ref.listen(videoPlayerControlsProvider.select((v) => v.position),
         (previous, next) {
       showControlsAndStartHideTimer();
     });
 
-    ref.listen(videoPlaybackValueProvider.select((value) => value.state),
-        (_, state) {
-      // Show buffering
-      showBuffering.value = state == VideoPlaybackState.buffering;
-    });
-
     /// Toggles between playing and pausing depending on the state of the video
     void togglePlay() {
       showControlsAndStartHideTimer();
-      final state = ref.read(videoPlaybackValueProvider).state;
       if (state == VideoPlaybackState.playing) {
         ref.read(videoPlayerControlsProvider.notifier).pause();
       } else if (state == VideoPlaybackState.completed) {
@@ -75,10 +72,10 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
       behavior: HitTestBehavior.opaque,
       onTap: showControlsAndStartHideTimer,
       child: AbsorbPointer(
-        absorbing: !ref.watch(showControlsProvider),
+        absorbing: !showControls,
         child: Stack(
           children: [
-            if (showBuffering.value)
+            if (showBuffering)
               const Center(
                 child: DelayedLoadingIndicator(
                   fadeInDuration: Duration(milliseconds: 400),
@@ -86,18 +83,14 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
               )
             else
               GestureDetector(
-                onTap: () {
-                  if (state != VideoPlaybackState.playing) {
-                    togglePlay();
-                  }
-                  ref.read(showControlsProvider.notifier).show = false;
-                },
+                onTap: () =>
+                    ref.read(showControlsProvider.notifier).show = false,
                 child: CenterPlayButton(
                   backgroundColor: Colors.black54,
                   iconColor: Colors.white,
                   isFinished: state == VideoPlaybackState.completed,
                   isPlaying: state == VideoPlaybackState.playing,
-                  show: ref.watch(showControlsProvider),
+                  show: assetIsVideo && showControls,
                   onPressed: togglePlay,
                 ),
               ),
diff --git a/mobile/lib/widgets/asset_viewer/description_input.dart b/mobile/lib/widgets/asset_viewer/description_input.dart
index 18ef394e2d..3fdd40130a 100644
--- a/mobile/lib/widgets/asset_viewer/description_input.dart
+++ b/mobile/lib/widgets/asset_viewer/description_input.dart
@@ -8,7 +8,7 @@ import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/extensions/theme_extensions.dart';
 import 'package:immich_mobile/providers/asset.provider.dart';
 import 'package:immich_mobile/providers/user.provider.dart';
-import 'package:immich_mobile/services/asset_description.service.dart';
+import 'package:immich_mobile/services/asset.service.dart';
 import 'package:immich_mobile/widgets/common/immich_toast.dart';
 import 'package:logging/logging.dart';
 
@@ -29,14 +29,16 @@ class DescriptionInput extends HookConsumerWidget {
     final focusNode = useFocusNode();
     final isFocus = useState(false);
     final isTextEmpty = useState(controller.text.isEmpty);
-    final descriptionProvider = ref.watch(assetDescriptionServiceProvider);
+    final assetService = ref.watch(assetServiceProvider);
     final owner = ref.watch(currentUserProvider);
     final hasError = useState(false);
     final assetWithExif = ref.watch(assetDetailProvider(asset));
 
     useEffect(
       () {
-        controller.text = descriptionProvider.getAssetDescription(asset);
+        assetService
+            .getDescription(asset)
+            .then((value) => controller.text = value);
         return null;
       },
       [assetWithExif.value],
@@ -45,7 +47,7 @@ class DescriptionInput extends HookConsumerWidget {
     submitDescription(String description) async {
       hasError.value = false;
       try {
-        await descriptionProvider.setDescription(
+        await assetService.setDescription(
           asset,
           description,
         );
diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart b/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart
index db9dafebcb..8ad2cdc687 100644
--- a/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart
+++ b/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart
@@ -9,12 +9,14 @@ import 'package:immich_mobile/entities/asset.entity.dart';
 
 class DetailPanel extends HookConsumerWidget {
   final Asset asset;
+  final ScrollController? scrollController;
 
-  const DetailPanel({super.key, required this.asset});
+  const DetailPanel({super.key, required this.asset, this.scrollController});
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
     return ListView(
+      controller: scrollController,
       shrinkWrap: true,
       children: [
         Padding(
diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart
index 3c650bdc6a..4af9846cf6 100644
--- a/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart
+++ b/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart
@@ -15,9 +15,10 @@ class FileInfo extends StatelessWidget {
   Widget build(BuildContext context) {
     final textColor = context.isDarkTheme ? Colors.white : Colors.black;
 
-    String resolution = asset.width != null && asset.height != null
-        ? "${asset.height} x ${asset.width}  "
-        : "";
+    final height = asset.orientatedHeight ?? asset.height;
+    final width = asset.orientatedWidth ?? asset.width;
+    String resolution =
+        height != null && width != null ? "$width x $height  " : "";
     String fileSize = asset.exifInfo?.fileSize != null
         ? formatBytes(asset.exifInfo!.fileSize!)
         : "";
diff --git a/mobile/lib/widgets/asset_viewer/formatted_duration.dart b/mobile/lib/widgets/asset_viewer/formatted_duration.dart
new file mode 100644
index 0000000000..a34aab7d12
--- /dev/null
+++ b/mobile/lib/widgets/asset_viewer/formatted_duration.dart
@@ -0,0 +1,33 @@
+import 'package:flutter/material.dart';
+
+@pragma('vm:prefer-inline')
+String _formatDuration(Duration position) {
+  final seconds = position.inSeconds.remainder(60).toString().padLeft(2, "0");
+  final minutes = position.inMinutes.remainder(60).toString().padLeft(2, "0");
+  if (position.inHours == 0) {
+    return "$minutes:$seconds";
+  }
+  final hours = position.inHours.toString().padLeft(2, '0');
+  return "$hours:$minutes:$seconds";
+}
+
+class FormattedDuration extends StatelessWidget {
+  final Duration data;
+  const FormattedDuration(this.data, {super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      width: data.inHours > 0 ? 70 : 60, // use a fixed width to prevent jitter
+      child: Text(
+        _formatDuration(data),
+        style: const TextStyle(
+          fontSize: 14.0,
+          color: Colors.white,
+          fontWeight: FontWeight.w500,
+        ),
+        textAlign: TextAlign.center,
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart
index 6de8f5da33..f7e2158ea9 100644
--- a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart
+++ b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart
@@ -4,8 +4,9 @@ import 'package:flutter/material.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/providers/album/current_album.provider.dart';
+import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
 import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart';
-import 'package:immich_mobile/providers/asset_viewer/image_viewer_page_state.provider.dart';
+import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
 import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
 import 'package:immich_mobile/widgets/asset_viewer/top_control_app_bar.dart';
 import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
@@ -19,23 +20,19 @@ import 'package:immich_mobile/providers/user.provider.dart';
 import 'package:immich_mobile/widgets/common/immich_toast.dart';
 
 class GalleryAppBar extends ConsumerWidget {
-  final Asset asset;
   final void Function() showInfo;
-  final void Function() onToggleMotionVideo;
-  final bool isPlayingVideo;
 
-  const GalleryAppBar({
-    super.key,
-    required this.asset,
-    required this.showInfo,
-    required this.onToggleMotionVideo,
-    required this.isPlayingVideo,
-  });
+  const GalleryAppBar({super.key, required this.showInfo});
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
+    final asset = ref.watch(currentAssetProvider);
+    if (asset == null) {
+      return const SizedBox();
+    }
     final album = ref.watch(currentAlbumProvider);
     final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId;
+    final showControls = ref.watch(showControlsProvider);
 
     final isPartner = ref
         .watch(partnerSharedWithProvider)
@@ -94,27 +91,25 @@ class GalleryAppBar extends ConsumerWidget {
     }
 
     handleDownloadAsset() {
-      ref.read(imageViewerStateProvider.notifier).downloadAsset(asset, context);
+      ref.read(downloadStateProvider.notifier).downloadAsset(asset, context);
     }
 
     return IgnorePointer(
-      ignoring: !ref.watch(showControlsProvider),
+      ignoring: !showControls,
       child: AnimatedOpacity(
         duration: const Duration(milliseconds: 100),
-        opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
+        opacity: showControls ? 1.0 : 0.0,
         child: Container(
           color: Colors.black.withOpacity(0.4),
           child: TopControlAppBar(
             isOwner: isOwner,
             isPartner: isPartner,
-            isPlayingMotionVideo: isPlayingVideo,
             asset: asset,
             onMoreInfoPressed: showInfo,
             onFavorite: toggleFavorite,
             onRestorePressed: () => handleRestore(asset),
             onUploadPressed: asset.isLocal ? () => handleUpload(asset) : null,
             onDownloadPressed: asset.isLocal ? null : handleDownloadAsset,
-            onToggleMotionVideo: onToggleMotionVideo,
             onAddToAlbumPressed: () => addToAlbum(asset),
             onActivitiesPressed: handleActivities,
           ),
diff --git a/mobile/lib/widgets/asset_viewer/motion_photo_button.dart b/mobile/lib/widgets/asset_viewer/motion_photo_button.dart
new file mode 100644
index 0000000000..f5479ab86e
--- /dev/null
+++ b/mobile/lib/widgets/asset_viewer/motion_photo_button.dart
@@ -0,0 +1,22 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/constants/colors.dart';
+import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
+
+class MotionPhotoButton extends ConsumerWidget {
+  const MotionPhotoButton({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final isPlaying = ref.watch(isPlayingMotionVideoProvider);
+
+    return IconButton(
+      onPressed: () {
+        ref.read(isPlayingMotionVideoProvider.notifier).toggle();
+      },
+      icon: isPlaying
+          ? const Icon(Icons.motion_photos_pause_outlined, color: grey200)
+          : const Icon(Icons.play_circle_outline_rounded, color: grey200),
+    );
+  }
+}
diff --git a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart
index 2157a1aebb..2bdbb72ec0 100644
--- a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart
+++ b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart
@@ -5,6 +5,7 @@ import 'package:immich_mobile/providers/activity_statistics.provider.dart';
 import 'package:immich_mobile/providers/album/current_album.provider.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/providers/asset.provider.dart';
+import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart';
 
 class TopControlAppBar extends HookConsumerWidget {
   const TopControlAppBar({
@@ -14,8 +15,6 @@ class TopControlAppBar extends HookConsumerWidget {
     required this.onDownloadPressed,
     required this.onAddToAlbumPressed,
     required this.onRestorePressed,
-    required this.onToggleMotionVideo,
-    required this.isPlayingMotionVideo,
     required this.onFavorite,
     required this.onUploadPressed,
     required this.isOwner,
@@ -27,12 +26,10 @@ class TopControlAppBar extends HookConsumerWidget {
   final Function onMoreInfoPressed;
   final VoidCallback? onUploadPressed;
   final VoidCallback? onDownloadPressed;
-  final VoidCallback onToggleMotionVideo;
   final VoidCallback onAddToAlbumPressed;
   final VoidCallback onRestorePressed;
   final VoidCallback onActivitiesPressed;
   final Function(Asset) onFavorite;
-  final bool isPlayingMotionVideo;
   final bool isOwner;
   final bool isPartner;
 
@@ -57,23 +54,6 @@ class TopControlAppBar extends HookConsumerWidget {
       );
     }
 
-    Widget buildLivePhotoButton() {
-      return IconButton(
-        onPressed: () {
-          onToggleMotionVideo();
-        },
-        icon: isPlayingMotionVideo
-            ? Icon(
-                Icons.motion_photos_pause_outlined,
-                color: Colors.grey[200],
-              )
-            : Icon(
-                Icons.play_circle_outline_rounded,
-                color: Colors.grey[200],
-              ),
-      );
-    }
-
     Widget buildMoreInfoButton() {
       return IconButton(
         onPressed: () {
@@ -175,16 +155,13 @@ class TopControlAppBar extends HookConsumerWidget {
       foregroundColor: Colors.grey[100],
       backgroundColor: Colors.transparent,
       leading: buildBackButton(),
-      actionsIconTheme: const IconThemeData(
-        size: iconSize,
-      ),
+      actionsIconTheme: const IconThemeData(size: iconSize),
       shape: const Border(),
       actions: [
         if (asset.isRemote && isOwner) buildFavoriteButton(a),
-        if (asset.livePhotoVideoId != null) buildLivePhotoButton(),
+        if (asset.livePhotoVideoId != null) const MotionPhotoButton(),
         if (asset.isLocal && !asset.isRemote) buildUploadButton(),
-        if (asset.isRemote && !asset.isLocal && !asset.isOffline && isOwner)
-          buildDownloadButton(),
+        if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(),
         if (asset.isRemote && (isOwner || isPartner) && !asset.isTrashed)
           buildAddToAlbumButton(),
         if (asset.isTrashed) buildRestoreButton(),
diff --git a/mobile/lib/widgets/asset_viewer/video_controls.dart b/mobile/lib/widgets/asset_viewer/video_controls.dart
index a5f5f18ce8..22aa2b17d1 100644
--- a/mobile/lib/widgets/asset_viewer/video_controls.dart
+++ b/mobile/lib/widgets/asset_viewer/video_controls.dart
@@ -1,125 +1,20 @@
-import 'dart:math';
-
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
-import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
-import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/widgets/asset_viewer/video_position.dart';
 
-/// The video controls for the [videPlayerControlsProvider]
+/// The video controls for the [videoPlayerControlsProvider]
 class VideoControls extends ConsumerWidget {
   const VideoControls({super.key});
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    final duration =
-        ref.watch(videoPlaybackValueProvider.select((v) => v.duration));
-    final position =
-        ref.watch(videoPlaybackValueProvider.select((v) => v.position));
-
-    return AnimatedOpacity(
-      opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
-      duration: const Duration(milliseconds: 100),
-      child: OrientationBuilder(
-        builder: (context, orientation) => Container(
-          padding: EdgeInsets.symmetric(
-            horizontal: orientation == Orientation.portrait ? 12.0 : 64.0,
-          ),
-          color: Colors.black.withOpacity(0.4),
-          child: Padding(
-            padding: MediaQuery.of(context).orientation == Orientation.portrait
-                ? const EdgeInsets.symmetric(horizontal: 12.0)
-                : const EdgeInsets.symmetric(horizontal: 64.0),
-            child: Row(
-              children: [
-                Text(
-                  _formatDuration(position),
-                  style: TextStyle(
-                    fontSize: 14.0,
-                    color: Colors.white.withOpacity(.75),
-                    fontWeight: FontWeight.normal,
-                  ),
-                ),
-                Expanded(
-                  child: Slider(
-                    value: duration == Duration.zero
-                        ? 0.0
-                        : min(
-                            position.inMicroseconds /
-                                duration.inMicroseconds *
-                                100,
-                            100,
-                          ),
-                    min: 0,
-                    max: 100,
-                    thumbColor: Colors.white,
-                    activeColor: Colors.white,
-                    inactiveColor: Colors.white.withOpacity(0.75),
-                    onChanged: (position) {
-                      ref.read(videoPlayerControlsProvider.notifier).position =
-                          position;
-                    },
-                  ),
-                ),
-                Text(
-                  _formatDuration(duration),
-                  style: TextStyle(
-                    fontSize: 14.0,
-                    color: Colors.white.withOpacity(.75),
-                    fontWeight: FontWeight.normal,
-                  ),
-                ),
-                IconButton(
-                  icon: Icon(
-                    ref.watch(
-                      videoPlayerControlsProvider.select((value) => value.mute),
-                    )
-                        ? Icons.volume_off
-                        : Icons.volume_up,
-                  ),
-                  onPressed: () => ref
-                      .read(videoPlayerControlsProvider.notifier)
-                      .toggleMute(),
-                  color: Colors.white,
-                ),
-              ],
-            ),
-          ),
-        ),
-      ),
-    );
-  }
-
-  String _formatDuration(Duration position) {
-    final ms = position.inMilliseconds;
-
-    int seconds = ms ~/ 1000;
-    final int hours = seconds ~/ 3600;
-    seconds = seconds % 3600;
-    final minutes = seconds ~/ 60;
-    seconds = seconds % 60;
-
-    final hoursString = hours >= 10
-        ? '$hours'
-        : hours == 0
-            ? '00'
-            : '0$hours';
-
-    final minutesString = minutes >= 10
-        ? '$minutes'
-        : minutes == 0
-            ? '00'
-            : '0$minutes';
-
-    final secondsString = seconds >= 10
-        ? '$seconds'
-        : seconds == 0
-            ? '00'
-            : '0$seconds';
-
-    final formattedTime =
-        '${hoursString == '00' ? '' : '$hoursString:'}$minutesString:$secondsString';
-
-    return formattedTime;
+    final isPortrait = context.orientation == Orientation.portrait;
+    return isPortrait
+        ? const VideoPosition()
+        : const Padding(
+            padding: EdgeInsets.symmetric(horizontal: 60.0),
+            child: VideoPosition(),
+          );
   }
 }
diff --git a/mobile/lib/widgets/asset_viewer/video_player.dart b/mobile/lib/widgets/asset_viewer/video_player.dart
deleted file mode 100644
index ebf158b59a..0000000000
--- a/mobile/lib/widgets/asset_viewer/video_player.dart
+++ /dev/null
@@ -1,48 +0,0 @@
-import 'package:chewie/chewie.dart';
-import 'package:flutter/material.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/utils/hooks/chewiew_controller_hook.dart';
-import 'package:immich_mobile/widgets/asset_viewer/custom_video_player_controls.dart';
-import 'package:video_player/video_player.dart';
-
-class VideoPlayerViewer extends HookConsumerWidget {
-  final VideoPlayerController controller;
-  final bool isMotionVideo;
-  final Widget? placeholder;
-  final Duration hideControlsTimer;
-  final bool showControls;
-  final bool showDownloadingIndicator;
-  final bool loopVideo;
-
-  const VideoPlayerViewer({
-    super.key,
-    required this.controller,
-    required this.isMotionVideo,
-    this.placeholder,
-    required this.hideControlsTimer,
-    required this.showControls,
-    required this.showDownloadingIndicator,
-    required this.loopVideo,
-  });
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    final chewie = useChewieController(
-      controller: controller,
-      controlsSafeAreaMinimum: const EdgeInsets.only(
-        bottom: 100,
-      ),
-      placeholder: SizedBox.expand(child: placeholder),
-      customControls: CustomVideoPlayerControls(
-        hideTimerDuration: hideControlsTimer,
-      ),
-      showControls: showControls && !isMotionVideo,
-      hideControlsTimer: hideControlsTimer,
-      loopVideo: loopVideo,
-    );
-
-    return Chewie(
-      controller: chewie,
-    );
-  }
-}
diff --git a/mobile/lib/widgets/asset_viewer/video_position.dart b/mobile/lib/widgets/asset_viewer/video_position.dart
new file mode 100644
index 0000000000..4d0e7aa17f
--- /dev/null
+++ b/mobile/lib/widgets/asset_viewer/video_position.dart
@@ -0,0 +1,116 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/constants/colors.dart';
+import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
+import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
+import 'package:immich_mobile/widgets/asset_viewer/formatted_duration.dart';
+
+class VideoPosition extends HookConsumerWidget {
+  const VideoPosition({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final (position, duration) = ref.watch(
+      videoPlaybackValueProvider.select((v) => (v.position, v.duration)),
+    );
+    final wasPlaying = useRef<bool>(true);
+    return duration == Duration.zero
+        ? const _VideoPositionPlaceholder()
+        : Column(
+            children: [
+              Padding(
+                // align with slider's inherent padding
+                padding: const EdgeInsets.symmetric(horizontal: 12.0),
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                  children: [
+                    FormattedDuration(position),
+                    FormattedDuration(duration),
+                  ],
+                ),
+              ),
+              Row(
+                children: [
+                  Expanded(
+                    child: Slider(
+                      value: min(
+                        position.inMicroseconds / duration.inMicroseconds * 100,
+                        100,
+                      ),
+                      min: 0,
+                      max: 100,
+                      thumbColor: Colors.white,
+                      activeColor: Colors.white,
+                      inactiveColor: whiteOpacity75,
+                      onChangeStart: (value) {
+                        final state =
+                            ref.read(videoPlaybackValueProvider).state;
+                        wasPlaying.value = state != VideoPlaybackState.paused;
+                        ref.read(videoPlayerControlsProvider.notifier).pause();
+                      },
+                      onChangeEnd: (value) {
+                        if (wasPlaying.value) {
+                          ref.read(videoPlayerControlsProvider.notifier).play();
+                        }
+                      },
+                      onChanged: (value) {
+                        final inSeconds =
+                            (duration * (value / 100.0)).inSeconds;
+                        final position = inSeconds.toDouble();
+                        ref
+                            .read(videoPlayerControlsProvider.notifier)
+                            .position = position;
+                        // This immediately updates the slider position without waiting for the video to update
+                        ref.read(videoPlaybackValueProvider.notifier).position =
+                            Duration(seconds: inSeconds);
+                      },
+                    ),
+                  ),
+                ],
+              ),
+            ],
+          );
+  }
+}
+
+class _VideoPositionPlaceholder extends StatelessWidget {
+  const _VideoPositionPlaceholder();
+
+  static void _onChangedDummy(_) {}
+
+  @override
+  Widget build(BuildContext context) {
+    return const Column(
+      children: [
+        Padding(
+          padding: EdgeInsets.symmetric(horizontal: 12.0),
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: [
+              FormattedDuration(Duration.zero),
+              FormattedDuration(Duration.zero),
+            ],
+          ),
+        ),
+        Row(
+          children: [
+            Expanded(
+              child: Slider(
+                value: 0.0,
+                min: 0,
+                max: 100,
+                thumbColor: Colors.white,
+                activeColor: Colors.white,
+                inactiveColor: whiteOpacity75,
+                onChanged: _onChangedDummy,
+              ),
+            ),
+          ],
+        ),
+      ],
+    );
+  }
+}
diff --git a/mobile/lib/widgets/backup/album_info_card.dart b/mobile/lib/widgets/backup/album_info_card.dart
index 0c9cd2d89d..7b04855809 100644
--- a/mobile/lib/widgets/backup/album_info_card.dart
+++ b/mobile/lib/widgets/backup/album_info_card.dart
@@ -183,23 +183,13 @@ class AlbumInfoCard extends HookConsumerWidget {
                         ),
                         Padding(
                           padding: const EdgeInsets.only(top: 2.0),
-                          child: FutureBuilder(
-                            builder: ((context, snapshot) {
-                              if (snapshot.hasData) {
-                                return Text(
-                                  snapshot.data.toString() +
-                                      (album.isAll
-                                          ? " (${'backup_all'.tr()})"
-                                          : ""),
-                                  style: TextStyle(
-                                    fontSize: 12,
-                                    color: Colors.grey[600],
-                                  ),
-                                );
-                              }
-                              return const Text("0");
-                            }),
-                            future: album.assetCount,
+                          child: Text(
+                            album.assetCount.toString() +
+                                (album.isAll ? " (${'backup_all'.tr()})" : ""),
+                            style: TextStyle(
+                              fontSize: 12,
+                              color: Colors.grey[600],
+                            ),
                           ),
                         ),
                       ],
@@ -208,7 +198,7 @@ class AlbumInfoCard extends HookConsumerWidget {
                   IconButton(
                     onPressed: () {
                       context.pushRoute(
-                        AlbumPreviewRoute(album: album.albumEntity),
+                        AlbumPreviewRoute(album: album.album),
                       );
                     },
                     icon: Icon(
diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart
index d326bad3e0..a263c004bd 100644
--- a/mobile/lib/widgets/backup/album_info_list_tile.dart
+++ b/mobile/lib/widgets/backup/album_info_list_tile.dart
@@ -1,6 +1,5 @@
 import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
@@ -24,19 +23,10 @@ class AlbumInfoListTile extends HookConsumerWidget {
         ref.watch(backupProvider).selectedBackupAlbums.contains(album);
     final bool isExcluded =
         ref.watch(backupProvider).excludedBackupAlbums.contains(album);
-    final assetCount = useState(0);
     final syncAlbum = ref
         .watch(appSettingsServiceProvider)
         .getSetting(AppSettingsEnum.syncAlbums);
 
-    useEffect(
-      () {
-        album.assetCount.then((value) => assetCount.value = value);
-        return null;
-      },
-      [album],
-    );
-
     buildTileColor() {
       if (isSelected) {
         return context.isDarkTheme
@@ -117,11 +107,11 @@ class AlbumInfoListTile extends HookConsumerWidget {
             fontWeight: FontWeight.bold,
           ),
         ),
-        subtitle: Text(assetCount.value.toString()),
+        subtitle: Text(album.assetCount.toString()),
         trailing: IconButton(
           onPressed: () {
             context.pushRoute(
-              AlbumPreviewRoute(album: album.albumEntity),
+              AlbumPreviewRoute(album: album.album),
             );
           },
           icon: Icon(
diff --git a/mobile/lib/widgets/backup/asset_info_table.dart b/mobile/lib/widgets/backup/asset_info_table.dart
new file mode 100644
index 0000000000..bbcbbe375f
--- /dev/null
+++ b/mobile/lib/widgets/backup/asset_info_table.dart
@@ -0,0 +1,102 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/extensions/theme_extensions.dart';
+import 'package:immich_mobile/models/backup/backup_state.model.dart';
+import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
+import 'package:immich_mobile/providers/backup/backup.provider.dart';
+import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
+
+class BackupAssetInfoTable extends ConsumerWidget {
+  const BackupAssetInfoTable({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final isManualUpload = ref.watch(
+      backupProvider.select(
+        (value) => value.backupProgress == BackUpProgressEnum.manualInProgress,
+      ),
+    );
+
+    final asset = isManualUpload
+        ? ref.watch(
+            manualUploadProvider.select((value) => value.currentUploadAsset),
+          )
+        : ref.watch(backupProvider.select((value) => value.currentUploadAsset));
+
+    return Padding(
+      padding: const EdgeInsets.only(top: 8.0),
+      child: Table(
+        border: TableBorder.all(
+          color: context.colorScheme.outlineVariant,
+          width: 1,
+        ),
+        children: [
+          TableRow(
+            children: [
+              TableCell(
+                verticalAlignment: TableCellVerticalAlignment.middle,
+                child: Padding(
+                  padding: const EdgeInsets.all(6.0),
+                  child: Text(
+                    'backup_controller_page_filename',
+                    style: TextStyle(
+                      color: context.colorScheme.onSurfaceSecondary,
+                      fontWeight: FontWeight.bold,
+                      fontSize: 10.0,
+                    ),
+                  ).tr(
+                    args: [asset.fileName, asset.fileType.toLowerCase()],
+                  ),
+                ),
+              ),
+            ],
+          ),
+          TableRow(
+            children: [
+              TableCell(
+                verticalAlignment: TableCellVerticalAlignment.middle,
+                child: Padding(
+                  padding: const EdgeInsets.all(6.0),
+                  child: Text(
+                    "backup_controller_page_created",
+                    style: TextStyle(
+                      color: context.colorScheme.onSurfaceSecondary,
+                      fontWeight: FontWeight.bold,
+                      fontSize: 10.0,
+                    ),
+                  ).tr(
+                    args: [_getAssetCreationDate(asset)],
+                  ),
+                ),
+              ),
+            ],
+          ),
+          TableRow(
+            children: [
+              TableCell(
+                child: Padding(
+                  padding: const EdgeInsets.all(6.0),
+                  child: Text(
+                    "backup_controller_page_id",
+                    style: TextStyle(
+                      color: context.colorScheme.onSurfaceSecondary,
+                      fontWeight: FontWeight.bold,
+                      fontSize: 10.0,
+                    ),
+                  ).tr(args: [asset.id]),
+                ),
+              ),
+            ],
+          ),
+        ],
+      ),
+    );
+  }
+
+  @pragma('vm:prefer-inline')
+  String _getAssetCreationDate(CurrentUploadAsset asset) {
+    return DateFormat.yMMMMd().format(asset.fileCreatedAt.toLocal());
+  }
+}
diff --git a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart
index 8e58905aaa..b6d0edb200 100644
--- a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart
+++ b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart
@@ -1,307 +1,43 @@
 import 'dart:io';
 
-import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:immich_mobile/extensions/theme_extensions.dart';
-import 'package:immich_mobile/models/backup/backup_state.model.dart';
-import 'package:immich_mobile/providers/backup/backup.provider.dart';
-import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
-import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
-import 'package:immich_mobile/routing/router.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:immich_mobile/widgets/backup/asset_info_table.dart';
+import 'package:immich_mobile/widgets/backup/error_chip.dart';
+import 'package:immich_mobile/widgets/backup/icloud_download_progress_bar.dart';
+import 'package:immich_mobile/widgets/backup/upload_progress_bar.dart';
+import 'package:immich_mobile/widgets/backup/upload_stats.dart';
 
-class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
+class CurrentUploadingAssetInfoBox extends StatelessWidget {
   const CurrentUploadingAssetInfoBox({super.key});
+
   @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    var isManualUpload = ref.watch(backupProvider).backupProgress ==
-        BackUpProgressEnum.manualInProgress;
-    var asset = !isManualUpload
-        ? ref.watch(backupProvider).currentUploadAsset
-        : ref.watch(manualUploadProvider).currentUploadAsset;
-    var uploadProgress = !isManualUpload
-        ? ref.watch(backupProvider).progressInPercentage
-        : ref.watch(manualUploadProvider).progressInPercentage;
-    var uploadFileProgress = !isManualUpload
-        ? ref.watch(backupProvider).progressInFileSize
-        : ref.watch(manualUploadProvider).progressInFileSize;
-    var uploadFileSpeed = !isManualUpload
-        ? ref.watch(backupProvider).progressInFileSpeed
-        : ref.watch(manualUploadProvider).progressInFileSpeed;
-    var iCloudDownloadProgress =
-        ref.watch(backupProvider).iCloudDownloadProgress;
-    final isShowThumbnail = useState(false);
-
-    String formatUploadFileSpeed(double uploadFileSpeed) {
-      if (uploadFileSpeed < 1024) {
-        return '${uploadFileSpeed.toStringAsFixed(2)} B/s';
-      } else if (uploadFileSpeed < 1024 * 1024) {
-        return '${(uploadFileSpeed / 1024).toStringAsFixed(2)} KB/s';
-      } else if (uploadFileSpeed < 1024 * 1024 * 1024) {
-        return '${(uploadFileSpeed / (1024 * 1024)).toStringAsFixed(2)} MB/s';
-      } else {
-        return '${(uploadFileSpeed / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB/s';
-      }
-    }
-
-    String getAssetCreationDate() {
-      return DateFormat.yMMMMd().format(
-        DateTime.parse(
-          asset.fileCreatedAt.toString(),
-        ).toLocal(),
-      );
-    }
-
-    Widget buildErrorChip() {
-      return ActionChip(
-        avatar: Icon(
-          Icons.info,
-          color: Colors.red[400],
-        ),
-        elevation: 1,
-        visualDensity: VisualDensity.compact,
-        label: Text(
-          "backup_controller_page_failed",
-          style: TextStyle(
-            color: Colors.red[400],
-            fontWeight: FontWeight.bold,
-            fontSize: 11,
-          ),
-        ).tr(
-          args: [ref.watch(errorBackupListProvider).length.toString()],
-        ),
-        backgroundColor: Colors.white,
-        onPressed: () => context.pushRoute(const FailedBackupStatusRoute()),
-      );
-    }
-
-    Widget buildAssetInfoTable() {
-      return Table(
-        border: TableBorder.all(
-          color: context.colorScheme.outlineVariant,
-          width: 1,
-        ),
+  Widget build(BuildContext context) {
+    return ListTile(
+      isThreeLine: true,
+      leading: Icon(
+        Icons.image_outlined,
+        color: context.primaryColor,
+        size: 30,
+      ),
+      title: Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
         children: [
-          TableRow(
-            children: [
-              TableCell(
-                verticalAlignment: TableCellVerticalAlignment.middle,
-                child: Padding(
-                  padding: const EdgeInsets.all(6.0),
-                  child: Text(
-                    'backup_controller_page_filename',
-                    style: TextStyle(
-                      color: context.colorScheme.onSurfaceSecondary,
-                      fontWeight: FontWeight.bold,
-                      fontSize: 10.0,
-                    ),
-                  ).tr(
-                    args: [asset.fileName, asset.fileType.toLowerCase()],
-                  ),
-                ),
-              ),
-            ],
-          ),
-          TableRow(
-            children: [
-              TableCell(
-                verticalAlignment: TableCellVerticalAlignment.middle,
-                child: Padding(
-                  padding: const EdgeInsets.all(6.0),
-                  child: Text(
-                    "backup_controller_page_created",
-                    style: TextStyle(
-                      color: context.colorScheme.onSurfaceSecondary,
-                      fontWeight: FontWeight.bold,
-                      fontSize: 10.0,
-                    ),
-                  ).tr(
-                    args: [getAssetCreationDate()],
-                  ),
-                ),
-              ),
-            ],
-          ),
-          TableRow(
-            children: [
-              TableCell(
-                child: Padding(
-                  padding: const EdgeInsets.all(6.0),
-                  child: Text(
-                    "backup_controller_page_id",
-                    style: TextStyle(
-                      color: context.colorScheme.onSurfaceSecondary,
-                      fontWeight: FontWeight.bold,
-                      fontSize: 10.0,
-                    ),
-                  ).tr(args: [asset.id]),
-                ),
-              ),
-            ],
-          ),
+          Text(
+            "backup_controller_page_uploading_file_info",
+            style: context.textTheme.titleSmall,
+          ).tr(),
+          const BackupErrorChip(),
+        ],
+      ),
+      subtitle: Column(
+        children: [
+          if (Platform.isIOS) const IcloudDownloadProgressBar(),
+          const BackupUploadProgressBar(),
+          const BackupUploadStats(),
+          const BackupAssetInfoTable(),
         ],
-      );
-    }
-
-    buildAssetThumbnail() async {
-      var assetEntity = await AssetEntity.fromId(asset.id);
-
-      if (assetEntity != null) {
-        return assetEntity.thumbnailDataWithSize(
-          const ThumbnailSize(500, 500),
-          quality: 100,
-        );
-      }
-    }
-
-    buildiCloudDownloadProgerssBar() {
-      if (asset.iCloudAsset != null && asset.iCloudAsset!) {
-        return Padding(
-          padding: const EdgeInsets.only(top: 8.0),
-          child: Row(
-            children: [
-              SizedBox(
-                width: 110,
-                child: Text(
-                  "iCloud Download",
-                  style: context.textTheme.labelSmall,
-                ),
-              ),
-              Expanded(
-                child: LinearProgressIndicator(
-                  minHeight: 10.0,
-                  value: uploadProgress / 100.0,
-                  borderRadius: const BorderRadius.all(Radius.circular(10.0)),
-                ),
-              ),
-              Text(
-                " ${iCloudDownloadProgress.toStringAsFixed(0)}%",
-                style: const TextStyle(fontSize: 12),
-              ),
-            ],
-          ),
-        );
-      }
-
-      return const SizedBox();
-    }
-
-    buildUploadProgressBar() {
-      return Padding(
-        padding: const EdgeInsets.only(top: 8.0),
-        child: Row(
-          children: [
-            if (asset.iCloudAsset != null && asset.iCloudAsset!)
-              SizedBox(
-                width: 110,
-                child: Text(
-                  "Immich Upload",
-                  style: context.textTheme.labelSmall,
-                ),
-              ),
-            Expanded(
-              child: LinearProgressIndicator(
-                minHeight: 10.0,
-                value: uploadProgress / 100.0,
-                borderRadius: const BorderRadius.all(Radius.circular(10.0)),
-              ),
-            ),
-            Text(
-              " ${uploadProgress.toStringAsFixed(0)}%",
-              style: const TextStyle(fontSize: 12, fontFamily: "OverpassMono"),
-            ),
-          ],
-        ),
-      );
-    }
-
-    buildUploadStats() {
-      return Padding(
-        padding: const EdgeInsets.only(top: 2.0, bottom: 2.0),
-        child: Row(
-          mainAxisAlignment: MainAxisAlignment.spaceBetween,
-          children: [
-            Text(
-              uploadFileProgress,
-              style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"),
-            ),
-            Text(
-              formatUploadFileSpeed(uploadFileSpeed),
-              style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"),
-            ),
-          ],
-        ),
-      );
-    }
-
-    return FutureBuilder<Uint8List?>(
-      future: buildAssetThumbnail(),
-      builder: (context, thumbnail) => ListTile(
-        isThreeLine: true,
-        leading: AnimatedCrossFade(
-          alignment: Alignment.centerLeft,
-          firstChild: GestureDetector(
-            onTap: () => isShowThumbnail.value = false,
-            child: thumbnail.hasData
-                ? ClipRRect(
-                    borderRadius: BorderRadius.circular(5),
-                    child: Image.memory(
-                      thumbnail.data!,
-                      fit: BoxFit.cover,
-                      width: 50,
-                      height: 50,
-                    ),
-                  )
-                : const SizedBox(
-                    width: 50,
-                    height: 50,
-                    child: Padding(
-                      padding: EdgeInsets.all(8.0),
-                      child: CircularProgressIndicator.adaptive(
-                        strokeWidth: 1,
-                      ),
-                    ),
-                  ),
-          ),
-          secondChild: GestureDetector(
-            onTap: () => isShowThumbnail.value = true,
-            child: Icon(
-              Icons.image_outlined,
-              color: context.primaryColor,
-              size: 30,
-            ),
-          ),
-          crossFadeState: isShowThumbnail.value
-              ? CrossFadeState.showFirst
-              : CrossFadeState.showSecond,
-          duration: const Duration(milliseconds: 200),
-        ),
-        title: Row(
-          mainAxisAlignment: MainAxisAlignment.spaceBetween,
-          children: [
-            Text(
-              "backup_controller_page_uploading_file_info",
-              style: context.textTheme.titleSmall,
-            ).tr(),
-            if (ref.watch(errorBackupListProvider).isNotEmpty) buildErrorChip(),
-          ],
-        ),
-        subtitle: Column(
-          children: [
-            if (Platform.isIOS) buildiCloudDownloadProgerssBar(),
-            buildUploadProgressBar(),
-            buildUploadStats(),
-            Padding(
-              padding: const EdgeInsets.only(top: 8.0),
-              child: buildAssetInfoTable(),
-            ),
-          ],
-        ),
       ),
     );
   }
diff --git a/mobile/lib/widgets/backup/error_chip.dart b/mobile/lib/widgets/backup/error_chip.dart
new file mode 100644
index 0000000000..4df3e50f64
--- /dev/null
+++ b/mobile/lib/widgets/backup/error_chip.dart
@@ -0,0 +1,32 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/constants/colors.dart';
+import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
+import 'package:immich_mobile/routing/router.dart';
+import 'package:immich_mobile/widgets/backup/error_chip_text.dart';
+
+class BackupErrorChip extends ConsumerWidget {
+  const BackupErrorChip({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final hasErrors =
+        ref.watch(errorBackupListProvider.select((value) => value.isNotEmpty));
+    if (!hasErrors) {
+      return const SizedBox();
+    }
+
+    return ActionChip(
+      avatar: const Icon(
+        Icons.info,
+        color: red400,
+      ),
+      elevation: 1,
+      visualDensity: VisualDensity.compact,
+      label: const BackupErrorChipText(),
+      backgroundColor: Colors.white,
+      onPressed: () => context.pushRoute(const FailedBackupStatusRoute()),
+    );
+  }
+}
diff --git a/mobile/lib/widgets/backup/error_chip_text.dart b/mobile/lib/widgets/backup/error_chip_text.dart
new file mode 100644
index 0000000000..540e136722
--- /dev/null
+++ b/mobile/lib/widgets/backup/error_chip_text.dart
@@ -0,0 +1,28 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/constants/colors.dart';
+import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
+
+class BackupErrorChipText extends ConsumerWidget {
+  const BackupErrorChipText({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final count = ref.watch(errorBackupListProvider).length;
+    if (count == 0) {
+      return const SizedBox();
+    }
+
+    return const Text(
+      "backup_controller_page_failed",
+      style: TextStyle(
+        color: red400,
+        fontWeight: FontWeight.bold,
+        fontSize: 11,
+      ),
+    ).tr(
+      args: [count.toString()],
+    );
+  }
+}
diff --git a/mobile/lib/widgets/backup/icloud_download_progress_bar.dart b/mobile/lib/widgets/backup/icloud_download_progress_bar.dart
new file mode 100644
index 0000000000..c61fb1a0d1
--- /dev/null
+++ b/mobile/lib/widgets/backup/icloud_download_progress_bar.dart
@@ -0,0 +1,61 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/models/backup/backup_state.model.dart';
+import 'package:immich_mobile/providers/backup/backup.provider.dart';
+import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
+
+class IcloudDownloadProgressBar extends ConsumerWidget {
+  const IcloudDownloadProgressBar({super.key});
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final isManualUpload = ref.watch(
+      backupProvider.select(
+        (value) => value.backupProgress == BackUpProgressEnum.manualInProgress,
+      ),
+    );
+
+    final isIcloudAsset = isManualUpload
+        ? ref.watch(
+            manualUploadProvider
+                .select((value) => value.currentUploadAsset.isIcloudAsset),
+          )
+        : ref.watch(
+            backupProvider
+                .select((value) => value.currentUploadAsset.isIcloudAsset),
+          );
+
+    if (!isIcloudAsset) {
+      return const SizedBox();
+    }
+
+    final iCloudDownloadProgress = ref
+        .watch(backupProvider.select((value) => value.iCloudDownloadProgress));
+
+    return Padding(
+      padding: const EdgeInsets.only(top: 8.0),
+      child: Row(
+        children: [
+          SizedBox(
+            width: 110,
+            child: Text(
+              "iCloud Download",
+              style: context.textTheme.labelSmall,
+            ),
+          ),
+          Expanded(
+            child: LinearProgressIndicator(
+              minHeight: 10.0,
+              value: iCloudDownloadProgress / 100.0,
+              borderRadius: const BorderRadius.all(Radius.circular(10.0)),
+            ),
+          ),
+          Text(
+            " ${iCloudDownloadProgress ~/ 1}%",
+            style: const TextStyle(fontSize: 12),
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/widgets/backup/upload_progress_bar.dart b/mobile/lib/widgets/backup/upload_progress_bar.dart
new file mode 100644
index 0000000000..9281914d9c
--- /dev/null
+++ b/mobile/lib/widgets/backup/upload_progress_bar.dart
@@ -0,0 +1,64 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/models/backup/backup_state.model.dart';
+import 'package:immich_mobile/providers/backup/backup.provider.dart';
+import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
+
+class BackupUploadProgressBar extends ConsumerWidget {
+  const BackupUploadProgressBar({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final isManualUpload = ref.watch(
+      backupProvider.select(
+        (value) => value.backupProgress == BackUpProgressEnum.manualInProgress,
+      ),
+    );
+
+    final isIcloudAsset = isManualUpload
+        ? ref.watch(
+            manualUploadProvider
+                .select((value) => value.currentUploadAsset.isIcloudAsset),
+          )
+        : ref.watch(
+            backupProvider
+                .select((value) => value.currentUploadAsset.isIcloudAsset),
+          );
+
+    final uploadProgress = isManualUpload
+        ? ref.watch(
+            manualUploadProvider.select((value) => value.progressInPercentage),
+          )
+        : ref.watch(
+            backupProvider.select((value) => value.progressInPercentage),
+          );
+
+    return Padding(
+      padding: const EdgeInsets.only(top: 8.0),
+      child: Row(
+        children: [
+          if (isIcloudAsset)
+            SizedBox(
+              width: 110,
+              child: Text(
+                "Immich Upload",
+                style: context.textTheme.labelSmall,
+              ),
+            ),
+          Expanded(
+            child: LinearProgressIndicator(
+              minHeight: 10.0,
+              value: uploadProgress / 100.0,
+              borderRadius: const BorderRadius.all(Radius.circular(10.0)),
+            ),
+          ),
+          Text(
+            " ${uploadProgress.toStringAsFixed(0)}%",
+            style: const TextStyle(fontSize: 12, fontFamily: "OverpassMono"),
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/widgets/backup/upload_stats.dart b/mobile/lib/widgets/backup/upload_stats.dart
new file mode 100644
index 0000000000..965202ce33
--- /dev/null
+++ b/mobile/lib/widgets/backup/upload_stats.dart
@@ -0,0 +1,62 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/models/backup/backup_state.model.dart';
+import 'package:immich_mobile/providers/backup/backup.provider.dart';
+import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
+
+class BackupUploadStats extends ConsumerWidget {
+  const BackupUploadStats({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final isManualUpload = ref.watch(
+      backupProvider.select(
+        (value) => value.backupProgress == BackUpProgressEnum.manualInProgress,
+      ),
+    );
+
+    final uploadFileProgress = isManualUpload
+        ? ref.watch(
+            manualUploadProvider.select((value) => value.progressInFileSize),
+          )
+        : ref.watch(backupProvider.select((value) => value.progressInFileSize));
+
+    final uploadFileSpeed = isManualUpload
+        ? ref.watch(
+            manualUploadProvider.select((value) => value.progressInFileSpeed),
+          )
+        : ref.watch(
+            backupProvider.select((value) => value.progressInFileSpeed),
+          );
+
+    return Padding(
+      padding: const EdgeInsets.only(top: 2.0, bottom: 2.0),
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          Text(
+            uploadFileProgress,
+            style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"),
+          ),
+          Text(
+            _formatUploadFileSpeed(uploadFileSpeed),
+            style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"),
+          ),
+        ],
+      ),
+    );
+  }
+
+  @pragma('vm:prefer-inline')
+  String _formatUploadFileSpeed(double uploadFileSpeed) {
+    if (uploadFileSpeed < 1024) {
+      return '${uploadFileSpeed.toStringAsFixed(2)} B/s';
+    } else if (uploadFileSpeed < 1024 * 1024) {
+      return '${(uploadFileSpeed / 1024).toStringAsFixed(2)} KB/s';
+    } else if (uploadFileSpeed < 1024 * 1024 * 1024) {
+      return '${(uploadFileSpeed / (1024 * 1024)).toStringAsFixed(2)} MB/s';
+    } else {
+      return '${(uploadFileSpeed / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB/s';
+    }
+  }
+}
diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart
index cd694336bc..218e17cbe1 100644
--- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart
+++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart
@@ -7,7 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/models/backup/backup_state.model.dart';
 import 'package:immich_mobile/providers/backup/backup.provider.dart';
 import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
-import 'package:immich_mobile/providers/authentication.provider.dart';
+import 'package:immich_mobile/providers/auth.provider.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/providers/asset.provider.dart';
 import 'package:immich_mobile/providers/user.provider.dart';
@@ -28,6 +28,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
     bool isHorizontal = !context.isMobile;
     final horizontalPadding = isHorizontal ? 100.0 : 20.0;
     final user = ref.watch(currentUserProvider);
+    final isLoggingOut = useState(false);
 
     useEffect(
       () {
@@ -63,11 +64,16 @@ class ImmichAppBarDialog extends HookConsumerWidget {
       );
     }
 
-    buildActionButton(IconData icon, String text, Function() onTap) {
+    buildActionButton(
+      IconData icon,
+      String text,
+      Function() onTap, {
+      Widget? trailing,
+    }) {
       return ListTile(
         dense: true,
         visualDensity: VisualDensity.standard,
-        contentPadding: const EdgeInsets.only(left: 30),
+        contentPadding: const EdgeInsets.only(left: 30, right: 30),
         minLeadingWidth: 40,
         leading: SizedBox(
           child: Icon(
@@ -83,6 +89,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
           ),
         ).tr(),
         onTap: onTap,
+        trailing: trailing,
       );
     }
 
@@ -107,6 +114,10 @@ class ImmichAppBarDialog extends HookConsumerWidget {
         Icons.logout_rounded,
         "profile_drawer_sign_out",
         () async {
+          if (isLoggingOut.value) {
+            return;
+          }
+
           showDialog(
             context: context,
             builder: (BuildContext ctx) {
@@ -115,7 +126,11 @@ class ImmichAppBarDialog extends HookConsumerWidget {
                 content: "app_bar_signout_dialog_content",
                 ok: "app_bar_signout_dialog_ok",
                 onOk: () async {
-                  await ref.read(authenticationProvider.notifier).logout();
+                  isLoggingOut.value = true;
+                  await ref
+                      .read(authProvider.notifier)
+                      .logout()
+                      .whenComplete(() => isLoggingOut.value = false);
 
                   ref.read(manualUploadProvider.notifier).cancelBackup();
                   ref.read(backupProvider.notifier).cancelBackup();
@@ -127,6 +142,12 @@ class ImmichAppBarDialog extends HookConsumerWidget {
             },
           );
         },
+        trailing: isLoggingOut.value
+            ? const SizedBox.square(
+                dimension: 20,
+                child: CircularProgressIndicator(strokeWidth: 2),
+              )
+            : null,
       );
     }
 
@@ -238,8 +259,9 @@ class ImmichAppBarDialog extends HookConsumerWidget {
     }
 
     return Dismissible(
+      behavior: HitTestBehavior.translucent,
       direction: DismissDirection.down,
-      onDismissed: (_) => Navigator.of(context).pop(),
+      onDismissed: (_) => context.pop(),
       key: const Key('app_bar_dialog'),
       child: Dialog(
         clipBehavior: Clip.hardEdge,
diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart
index a40dcf914e..f0006d1ada 100644
--- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart
+++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart
@@ -7,8 +7,7 @@ import 'package:immich_mobile/providers/upload_profile_image.provider.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
 import 'package:immich_mobile/providers/user.provider.dart';
 import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
-import 'package:immich_mobile/models/authentication/authentication_state.model.dart';
-import 'package:immich_mobile/providers/authentication.provider.dart';
+import 'package:immich_mobile/providers/auth.provider.dart';
 import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
 
 class AppBarProfileInfoBox extends HookConsumerWidget {
@@ -18,7 +17,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    AuthenticationState authState = ref.watch(authenticationProvider);
+    final authState = ref.watch(authProvider);
     final uploadProfileImageStatus =
         ref.watch(uploadProfileImageProvider).status;
     final user = Store.tryGet(StoreKey.currentUser);
@@ -63,7 +62,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
         if (success) {
           final profileImagePath =
               ref.read(uploadProfileImageProvider).profileImagePath;
-          ref.watch(authenticationProvider.notifier).updateUserProfileImagePath(
+          ref.watch(authProvider.notifier).updateUserProfileImagePath(
                 profileImagePath,
               );
           if (user != null) {
diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart
index 8e2465fc9c..1831a2d168 100644
--- a/mobile/lib/widgets/common/immich_app_bar.dart
+++ b/mobile/lib/widgets/common/immich_app_bar.dart
@@ -18,9 +18,10 @@ import 'package:immich_mobile/providers/server_info.provider.dart';
 class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
   @override
   Size get preferredSize => const Size.fromHeight(kToolbarHeight);
-  final Widget? action;
+  final List<Widget>? actions;
+  final bool showUploadButton;
 
-  const ImmichAppBar({super.key, this.action});
+  const ImmichAppBar({super.key, this.actions, this.showUploadButton = true});
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
@@ -184,12 +185,18 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
         },
       ),
       actions: [
-        if (action != null)
-          Padding(padding: const EdgeInsets.only(right: 20), child: action!),
-        Padding(
-          padding: const EdgeInsets.only(right: 20),
-          child: buildBackupIndicator(),
-        ),
+        if (actions != null)
+          ...actions!.map(
+            (action) => Padding(
+              padding: const EdgeInsets.only(right: 16),
+              child: action,
+            ),
+          ),
+        if (showUploadButton)
+          Padding(
+            padding: const EdgeInsets.only(right: 20),
+            child: buildBackupIndicator(),
+          ),
         Padding(
           padding: const EdgeInsets.only(right: 20),
           child: buildProfileIndicator(),
diff --git a/mobile/lib/widgets/common/immich_image.dart b/mobile/lib/widgets/common/immich_image.dart
index 5946dee453..ab0f2584b5 100644
--- a/mobile/lib/widgets/common/immich_image.dart
+++ b/mobile/lib/widgets/common/immich_image.dart
@@ -28,12 +28,11 @@ class ImmichImage extends StatelessWidget {
   // either by using the asset ID or the asset itself
   /// [asset] is the Asset to request, or else use [assetId] to get a remote
   /// image provider
-  /// Use [isThumbnail] and [thumbnailSize] if you'd like to request a thumbnail
-  /// The size of the square thumbnail to request. Ignored if isThumbnail
-  /// is not true
   static ImageProvider imageProvider({
     Asset? asset,
     String? assetId,
+    double width = 1080,
+    double height = 1920,
   }) {
     if (asset == null && assetId == null) {
       throw Exception('Must supply either asset or assetId');
@@ -48,6 +47,8 @@ class ImmichImage extends StatelessWidget {
     if (useLocal(asset)) {
       return ImmichLocalImageProvider(
         asset: asset,
+        width: width,
+        height: height,
       );
     } else {
       return ImmichRemoteImageProvider(
@@ -87,6 +88,8 @@ class ImmichImage extends StatelessWidget {
       },
       image: ImmichImage.imageProvider(
         asset: asset,
+        width: context.width,
+        height: context.height,
       ),
       width: width,
       height: height,
diff --git a/mobile/lib/widgets/common/user_circle_avatar.dart b/mobile/lib/widgets/common/user_circle_avatar.dart
index bf3bd8a5a8..50da009676 100644
--- a/mobile/lib/widgets/common/user_circle_avatar.dart
+++ b/mobile/lib/widgets/common/user_circle_avatar.dart
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
 import 'package:immich_mobile/entities/user.entity.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/services/api.service.dart';
 import 'package:immich_mobile/widgets/common/transparent_image.dart';
 
@@ -23,7 +24,7 @@ class UserCircleAvatar extends ConsumerWidget {
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    bool isDarkTheme = Theme.of(context).brightness == Brightness.dark;
+    bool isDarkTheme = context.themeData.brightness == Brightness.dark;
     final profileImageUrl =
         '${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${Random().nextInt(1024)}';
 
diff --git a/mobile/lib/widgets/forms/change_password_form.dart b/mobile/lib/widgets/forms/change_password_form.dart
index 98ce66d2d1..fbb8fd927b 100644
--- a/mobile/lib/widgets/forms/change_password_form.dart
+++ b/mobile/lib/widgets/forms/change_password_form.dart
@@ -7,7 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/providers/backup/backup.provider.dart';
 import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
-import 'package:immich_mobile/providers/authentication.provider.dart';
+import 'package:immich_mobile/providers/auth.provider.dart';
 import 'package:immich_mobile/providers/asset.provider.dart';
 import 'package:immich_mobile/providers/websocket.provider.dart';
 import 'package:immich_mobile/widgets/common/immich_toast.dart';
@@ -21,7 +21,7 @@ class ChangePasswordForm extends HookConsumerWidget {
         useTextEditingController.fromValue(TextEditingValue.empty);
     final confirmPasswordController =
         useTextEditingController.fromValue(TextEditingValue.empty);
-    final authState = ref.watch(authenticationProvider);
+    final authState = ref.watch(authProvider);
     final formKey = GlobalKey<FormState>();
 
     return Center(
@@ -73,13 +73,11 @@ class ChangePasswordForm extends HookConsumerWidget {
                       onPressed: () async {
                         if (formKey.currentState!.validate()) {
                           var isSuccess = await ref
-                              .read(authenticationProvider.notifier)
+                              .read(authProvider.notifier)
                               .changePassword(passwordController.value.text);
 
                           if (isSuccess) {
-                            await ref
-                                .read(authenticationProvider.notifier)
-                                .logout();
+                            await ref.read(authProvider.notifier).logout();
 
                             ref
                                 .read(manualUploadProvider.notifier)
diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart
index 14a4e89dd6..30b6a74bb1 100644
--- a/mobile/lib/widgets/forms/login/login_form.dart
+++ b/mobile/lib/widgets/forms/login/login_form.dart
@@ -11,11 +11,10 @@ import 'package:immich_mobile/providers/oauth.provider.dart';
 import 'package:immich_mobile/providers/gallery_permission.provider.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
-import 'package:immich_mobile/providers/api.provider.dart';
-import 'package:immich_mobile/providers/asset.provider.dart';
-import 'package:immich_mobile/providers/authentication.provider.dart';
+import 'package:immich_mobile/providers/auth.provider.dart';
 import 'package:immich_mobile/providers/backup/backup.provider.dart';
 import 'package:immich_mobile/providers/server_info.provider.dart';
+import 'package:immich_mobile/utils/provider_utils.dart';
 import 'package:immich_mobile/utils/version_compatibility.dart';
 import 'package:immich_mobile/widgets/common/immich_logo.dart';
 import 'package:immich_mobile/widgets/common/immich_title_text.dart';
@@ -39,13 +38,12 @@ class LoginForm extends HookConsumerWidget {
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    final usernameController =
+    final emailController =
         useTextEditingController.fromValue(TextEditingValue.empty);
     final passwordController =
         useTextEditingController.fromValue(TextEditingValue.empty);
     final serverEndpointController =
         useTextEditingController.fromValue(TextEditingValue.empty);
-    final apiService = ref.watch(apiServiceProvider);
     final emailFocusNode = useFocusNode();
     final passwordFocusNode = useFocusNode();
     final serverEndpointFocusNode = useFocusNode();
@@ -84,7 +82,7 @@ class LoginForm extends HookConsumerWidget {
 
     /// Fetch the server login credential and enables oAuth login if necessary
     /// Returns true if successful, false otherwise
-    Future<bool> getServerLoginCredential() async {
+    Future<void> getServerAuthSettings() async {
       final serverUrl = sanitizeUrl(serverEndpointController.text);
 
       // Guard empty URL
@@ -94,13 +92,12 @@ class LoginForm extends HookConsumerWidget {
           msg: "login_form_server_empty".tr(),
           toastType: ToastType.error,
         );
-
-        return false;
       }
 
       try {
         isLoadingServer.value = true;
-        final endpoint = await apiService.resolveAndSetEndpoint(serverUrl);
+        final endpoint =
+            await ref.read(authProvider.notifier).validateServerUrl(serverUrl);
 
         // Fetch and load server config and features
         await ref.read(serverInfoProvider.notifier).getServerInfo();
@@ -126,7 +123,6 @@ class LoginForm extends HookConsumerWidget {
         isOauthEnable.value = false;
         isPasswordLoginEnable.value = true;
         isLoadingServer.value = false;
-        return false;
       } on HandshakeException {
         ImmichToast.show(
           context: context,
@@ -137,7 +133,6 @@ class LoginForm extends HookConsumerWidget {
         isOauthEnable.value = false;
         isPasswordLoginEnable.value = true;
         isLoadingServer.value = false;
-        return false;
       } catch (e) {
         ImmichToast.show(
           context: context,
@@ -148,11 +143,9 @@ class LoginForm extends HookConsumerWidget {
         isOauthEnable.value = false;
         isPasswordLoginEnable.value = true;
         isLoadingServer.value = false;
-        return false;
       }
 
       isLoadingServer.value = false;
-      return true;
     }
 
     useEffect(
@@ -167,64 +160,50 @@ class LoginForm extends HookConsumerWidget {
     );
 
     populateTestLoginInfo() {
-      usernameController.text = 'demo@immich.app';
+      emailController.text = 'demo@immich.app';
       passwordController.text = 'demo';
       serverEndpointController.text = 'https://demo.immich.app';
     }
 
     populateTestLoginInfo1() {
-      usernameController.text = 'testuser@email.com';
+      emailController.text = 'testuser@email.com';
       passwordController.text = 'password';
-      serverEndpointController.text = 'http://10.1.15.216:2283/api';
+      serverEndpointController.text = 'http://10.1.15.216:3000/api';
     }
 
     login() async {
       TextInput.finishAutofillContext();
-      // Start loading
+
       isLoading.value = true;
 
-      // This will remove current cache asset state of previous user login.
-      ref.read(assetProvider.notifier).clearAllAsset();
+      // Invalidate all api repository provider instance to take into account new access token
+      invalidateAllApiRepositoryProviders(ref);
 
       try {
-        final isAuthenticated =
-            await ref.read(authenticationProvider.notifier).login(
-                  usernameController.text,
-                  passwordController.text,
-                  sanitizeUrl(serverEndpointController.text),
-                );
-        if (isAuthenticated) {
-          // Resume backup (if enable) then navigate
-          if (ref.read(authenticationProvider).shouldChangePassword &&
-              !ref.read(authenticationProvider).isAdmin) {
-            context.pushRoute(const ChangePasswordRoute());
-          } else {
-            final hasPermission = await ref
-                .read(galleryPermissionNotifier.notifier)
-                .hasPermission;
-            if (hasPermission) {
-              // Don't resume the backup until we have gallery permission
-              ref.read(backupProvider.notifier).resumeBackup();
-            }
-            context.replaceRoute(const TabControllerRoute());
-          }
+        final result = await ref.read(authProvider.notifier).login(
+              emailController.text,
+              passwordController.text,
+            );
+
+        if (result.shouldChangePassword && !result.isAdmin) {
+          context.pushRoute(const ChangePasswordRoute());
         } else {
-          ImmichToast.show(
-            context: context,
-            msg: "login_form_failed_login".tr(),
-            toastType: ToastType.error,
-            gravity: ToastGravity.TOP,
-          );
+          context.replaceRoute(const TabControllerRoute());
         }
+      } catch (error) {
+        ImmichToast.show(
+          context: context,
+          msg: "login_form_failed_login".tr(),
+          toastType: ToastType.error,
+          gravity: ToastGravity.TOP,
+        );
       } finally {
-        // Make sure we stop loading
         isLoading.value = false;
       }
     }
 
     oAuthLogin() async {
       var oAuthService = ref.watch(oAuthServiceProvider);
-      ref.watch(assetProvider.notifier).clearAllAsset();
       String? oAuthServerUrl;
 
       try {
@@ -258,11 +237,8 @@ class LoginForm extends HookConsumerWidget {
             "Finished OAuth login with response: ${loginResponseDto.userEmail}",
           );
 
-          final isSuccess = await ref
-              .watch(authenticationProvider.notifier)
-              .setSuccessLoginInfo(
+          final isSuccess = await ref.watch(authProvider.notifier).saveAuthInfo(
                 accessToken: loginResponseDto.accessToken,
-                serverUrl: sanitizeUrl(serverEndpointController.text),
               );
 
           if (isSuccess) {
@@ -305,7 +281,7 @@ class LoginForm extends HookConsumerWidget {
           ServerEndpointInput(
             controller: serverEndpointController,
             focusNode: serverEndpointFocusNode,
-            onSubmit: getServerLoginCredential,
+            onSubmit: getServerAuthSettings,
           ),
           const SizedBox(height: 18),
           Row(
@@ -340,7 +316,7 @@ class LoginForm extends HookConsumerWidget {
                     ),
                   ),
                   onPressed:
-                      isLoadingServer.value ? null : getServerLoginCredential,
+                      isLoadingServer.value ? null : getServerAuthSettings,
                   icon: const Icon(Icons.arrow_forward_rounded),
                   label: const Text(
                     'login_form_next_button',
@@ -398,7 +374,7 @@ class LoginForm extends HookConsumerWidget {
             if (isPasswordLoginEnable.value) ...[
               const SizedBox(height: 18),
               EmailInput(
-                controller: usernameController,
+                controller: emailController,
                 focusNode: emailFocusNode,
                 onSubmit: passwordFocusNode.requestFocus,
               ),
diff --git a/mobile/lib/widgets/map/map_app_bar.dart b/mobile/lib/widgets/map/map_app_bar.dart
index 42bc598915..4de5721486 100644
--- a/mobile/lib/widgets/map/map_app_bar.dart
+++ b/mobile/lib/widgets/map/map_app_bar.dart
@@ -19,7 +19,7 @@ class MapAppBar extends HookWidget implements PreferredSizeWidget {
   @override
   Widget build(BuildContext context) {
     return Padding(
-      padding: EdgeInsets.only(top: MediaQuery.paddingOf(context).top + 25),
+      padding: EdgeInsets.only(top: context.padding.top + 25),
       child: ValueListenableBuilder(
         valueListenable: selectedAssets,
         builder: (ctx, value, child) => value.isNotEmpty
diff --git a/mobile/lib/widgets/map/map_theme_override.dart b/mobile/lib/widgets/map/map_theme_override.dart
index 3b66a1cc35..65425f9e78 100644
--- a/mobile/lib/widgets/map/map_theme_override.dart
+++ b/mobile/lib/widgets/map/map_theme_override.dart
@@ -1,22 +1,24 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/providers/locale_provider.dart';
 import 'package:immich_mobile/providers/map/map_state.provider.dart';
-import 'package:immich_mobile/utils/immich_app_theme.dart';
+import 'package:immich_mobile/providers/theme.provider.dart';
+import 'package:immich_mobile/theme/theme_data.dart';
 
 /// Overrides the theme below the widget tree to use the theme data based on the
 /// map settings instead of the one from the app settings
-class MapThemeOveride extends StatefulHookConsumerWidget {
+class MapThemeOverride extends StatefulHookConsumerWidget {
   final ThemeMode? themeMode;
   final Widget Function(AsyncValue<String> style) mapBuilder;
 
-  const MapThemeOveride({required this.mapBuilder, this.themeMode, super.key});
+  const MapThemeOverride({required this.mapBuilder, this.themeMode, super.key});
 
   @override
-  ConsumerState createState() => _MapThemeOverideState();
+  ConsumerState createState() => _MapThemeOverrideState();
 }
 
-class _MapThemeOverideState extends ConsumerState<MapThemeOveride>
+class _MapThemeOverrideState extends ConsumerState<MapThemeOverride>
     with WidgetsBindingObserver {
   late ThemeMode _theme;
   bool _isDarkTheme = false;
@@ -71,6 +73,7 @@ class _MapThemeOverideState extends ConsumerState<MapThemeOveride>
     _theme = widget.themeMode ??
         ref.watch(mapStateNotifierProvider.select((v) => v.themeMode));
     var appTheme = ref.watch(immichThemeProvider);
+    final locale = ref.watch(localeProvider);
 
     useValueChanged<ThemeMode, void>(_theme, (_, __) {
       if (_theme == ThemeMode.system) {
@@ -85,8 +88,8 @@ class _MapThemeOverideState extends ConsumerState<MapThemeOveride>
 
     return Theme(
       data: _isDarkTheme
-          ? getThemeData(colorScheme: appTheme.dark)
-          : getThemeData(colorScheme: appTheme.light),
+          ? getThemeData(colorScheme: appTheme.dark, locale: locale)
+          : getThemeData(colorScheme: appTheme.light, locale: locale),
       child: widget.mapBuilder.call(
         ref.watch(
           mapStateNotifierProvider.select(
diff --git a/mobile/lib/widgets/map/map_thumbnail.dart b/mobile/lib/widgets/map/map_thumbnail.dart
index d02c016791..b856f09787 100644
--- a/mobile/lib/widgets/map/map_thumbnail.dart
+++ b/mobile/lib/widgets/map/map_thumbnail.dart
@@ -62,7 +62,7 @@ class MapThumbnail extends HookConsumerWidget {
       }
     }
 
-    return MapThemeOveride(
+    return MapThemeOverride(
       themeMode: themeMode,
       mapBuilder: (style) => SizedBox(
         height: height,
diff --git a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart b/mobile/lib/widgets/map/positioned_asset_marker_icon.dart
index ac176b4701..2cf82517ae 100644
--- a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart
+++ b/mobile/lib/widgets/map/positioned_asset_marker_icon.dart
@@ -26,7 +26,7 @@ class PositionedAssetMarkerIcon extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    final ratio = Platform.isIOS ? 1.0 : MediaQuery.devicePixelRatioOf(context);
+    final ratio = Platform.isIOS ? 1.0 : context.devicePixelRatio;
     return AnimatedPositioned(
       left: point.x / ratio - size / 2,
       top: point.y / ratio - size,
diff --git a/mobile/lib/widgets/memories/memory_card.dart b/mobile/lib/widgets/memories/memory_card.dart
index fb7cc882a0..4954d0bfcc 100644
--- a/mobile/lib/widgets/memories/memory_card.dart
+++ b/mobile/lib/widgets/memories/memory_card.dart
@@ -2,9 +2,9 @@ import 'dart:ui';
 
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:immich_mobile/pages/common/video_viewer.page.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
 import 'package:immich_mobile/utils/hooks/blurhash_hook.dart';
 import 'package:immich_mobile/widgets/common/immich_image.dart';
 
@@ -68,18 +68,20 @@ class MemoryCard extends StatelessWidget {
               } else {
                 return Hero(
                   tag: 'memory-${asset.id}',
-                  child: VideoViewerPage(
-                    key: ValueKey(asset),
-                    asset: asset,
-                    showDownloadingIndicator: false,
-                    placeholder: SizedBox.expand(
-                      child: ImmichImage(
+                  child: SizedBox(
+                    width: context.width,
+                    height: context.height,
+                    child: NativeVideoViewerPage(
+                      key: ValueKey(asset.id),
+                      asset: asset,
+                      showControls: false,
+                      image: ImmichImage(
                         asset,
+                        width: context.width,
+                        height: context.height,
                         fit: fit,
                       ),
                     ),
-                    hideControlsTimer: const Duration(seconds: 2),
-                    showControls: false,
                   ),
                 );
               }
@@ -137,6 +139,8 @@ class _BlurredBackdrop extends HookWidget {
             image: DecorationImage(
               image: ImmichImage.imageProvider(
                 asset: asset,
+                height: context.height,
+                width: context.width,
               ),
               fit: BoxFit.cover,
             ),
diff --git a/mobile/lib/widgets/partner/partner_list.dart b/mobile/lib/widgets/partner/partner_list.dart
deleted file mode 100644
index 53a27c48ab..0000000000
--- a/mobile/lib/widgets/partner/partner_list.dart
+++ /dev/null
@@ -1,48 +0,0 @@
-import 'package:auto_route/auto_route.dart';
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/material.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:immich_mobile/routing/router.dart';
-import 'package:immich_mobile/entities/user.entity.dart';
-import 'package:immich_mobile/widgets/common/user_avatar.dart';
-
-class PartnerList extends HookConsumerWidget {
-  const PartnerList({super.key, required this.partner});
-
-  final List<User> partner;
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    return SliverList(
-      delegate:
-          SliverChildBuilderDelegate(listEntry, childCount: partner.length),
-    );
-  }
-
-  Widget listEntry(BuildContext context, int index) {
-    final User p = partner[index];
-    return ListTile(
-      contentPadding: const EdgeInsets.only(
-        left: 12.0,
-        right: 18.0,
-      ),
-      leading: userAvatar(context, p, radius: 24),
-      title: Text(
-        "partner_list_user_photos",
-        style: context.textTheme.labelLarge,
-      ).tr(
-        namedArgs: {
-          'user': p.name,
-        },
-      ),
-      trailing: Text(
-        "partner_list_view_all",
-        style: context.textTheme.labelLarge?.copyWith(
-          color: context.primaryColor,
-        ),
-      ).tr(),
-      onTap: () => context.pushRoute((PartnerDetailRoute(partner: p))),
-    );
-  }
-}
diff --git a/mobile/lib/widgets/photo_view/photo_view.dart b/mobile/lib/widgets/photo_view/photo_view.dart
index 55be81a5b3..7f72750afe 100644
--- a/mobile/lib/widgets/photo_view/photo_view.dart
+++ b/mobile/lib/widgets/photo_view/photo_view.dart
@@ -1,5 +1,3 @@
-library photo_view;
-
 import 'package:flutter/material.dart';
 
 import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_controller.dart';
diff --git a/mobile/lib/widgets/photo_view/photo_view_gallery.dart b/mobile/lib/widgets/photo_view/photo_view_gallery.dart
index 9594912078..b8918309bc 100644
--- a/mobile/lib/widgets/photo_view/photo_view_gallery.dart
+++ b/mobile/lib/widgets/photo_view/photo_view_gallery.dart
@@ -1,5 +1,3 @@
-library photo_view_gallery;
-
 import 'package:flutter/material.dart';
 import 'package:flutter/widgets.dart';
 import 'package:immich_mobile/widgets/photo_view/photo_view.dart'
diff --git a/mobile/lib/widgets/search/explore_grid.dart b/mobile/lib/widgets/search/explore_grid.dart
index 8e90cc8504..cd937a6a42 100644
--- a/mobile/lib/widgets/search/explore_grid.dart
+++ b/mobile/lib/widgets/search/explore_grid.dart
@@ -59,7 +59,7 @@ class ExploreGrid extends StatelessWidget {
                     ),
                   )
                 : context.pushRoute(
-                    SearchInputRoute(
+                    SearchRoute(
                       prefilter: SearchFilter(
                         people: {},
                         location: SearchLocationFilter(
diff --git a/mobile/lib/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart b/mobile/lib/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart
index d636c8c7ce..bda9335c77 100644
--- a/mobile/lib/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart
+++ b/mobile/lib/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart
@@ -47,7 +47,7 @@ class FilterBottomSheetScaffold extends StatelessWidget {
                 OutlinedButton(
                   onPressed: () {
                     onClear();
-                    Navigator.of(context).pop();
+                    context.pop();
                   },
                   child: const Text('action_common_clear').tr(),
                 ),
@@ -55,7 +55,7 @@ class FilterBottomSheetScaffold extends StatelessWidget {
                 ElevatedButton(
                   onPressed: () {
                     onSearch();
-                    Navigator.of(context).pop();
+                    context.pop();
                   },
                   child: const Text('search_filter_apply').tr(),
                 ),
diff --git a/mobile/lib/widgets/search/search_filter/people_picker.dart b/mobile/lib/widgets/search/search_filter/people_picker.dart
index d79ae5bd95..dfc435c807 100644
--- a/mobile/lib/widgets/search/search_filter/people_picker.dart
+++ b/mobile/lib/widgets/search/search_filter/people_picker.dart
@@ -3,23 +3,23 @@ import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/interfaces/person_api.interface.dart';
 import 'package:immich_mobile/providers/search/people.provider.dart';
 import 'package:immich_mobile/services/api.service.dart';
 import 'package:immich_mobile/utils/image_url_builder.dart';
-import 'package:openapi/api.dart';
 
 class PeoplePicker extends HookConsumerWidget {
   const PeoplePicker({super.key, required this.onSelect, this.filter});
 
-  final Function(Set<PersonResponseDto>) onSelect;
-  final Set<PersonResponseDto>? filter;
+  final Function(Set<Person>) onSelect;
+  final Set<Person>? filter;
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
     var imageSize = 45.0;
     final people = ref.watch(getAllPeopleProvider);
     final headers = ApiService.getRequestHeaders();
-    final selectedPeople = useState<Set<PersonResponseDto>>(filter ?? {});
+    final selectedPeople = useState<Set<Person>>(filter ?? {});
 
     return people.widgetWhen(
       onData: (people) {
diff --git a/mobile/lib/widgets/search/search_filter/search_filter_chip.dart b/mobile/lib/widgets/search/search_filter/search_filter_chip.dart
index 7db2eea70b..2a445c8ad7 100644
--- a/mobile/lib/widgets/search/search_filter/search_filter_chip.dart
+++ b/mobile/lib/widgets/search/search_filter/search_filter_chip.dart
@@ -48,7 +48,7 @@ class SearchFilterChip extends StatelessWidget {
       child: Card(
         elevation: 0,
         shape: StadiumBorder(
-          side: BorderSide(color: context.colorScheme.outline.withOpacity(.5)),
+          side: BorderSide(color: context.colorScheme.outline.withAlpha(15)),
         ),
         child: Padding(
           padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 14.0),
diff --git a/mobile/lib/widgets/search/search_map_thumbnail.dart b/mobile/lib/widgets/search/search_map_thumbnail.dart
index 20747913fb..b4a12ab826 100644
--- a/mobile/lib/widgets/search/search_map_thumbnail.dart
+++ b/mobile/lib/widgets/search/search_map_thumbnail.dart
@@ -13,6 +13,7 @@ class SearchMapThumbnail extends StatelessWidget {
   });
 
   final double size;
+  final bool showTitle = true;
 
   @override
   Widget build(BuildContext context) {
diff --git a/mobile/lib/widgets/settings/backup_settings/background_settings.dart b/mobile/lib/widgets/settings/backup_settings/background_settings.dart
index a772aaaf5d..4cdeb501c1 100644
--- a/mobile/lib/widgets/settings/backup_settings/background_settings.dart
+++ b/mobile/lib/widgets/settings/backup_settings/background_settings.dart
@@ -33,7 +33,7 @@ class BackgroundBackupSettings extends ConsumerWidget {
         ),
         backgroundColor: Colors.red,
       );
-      ScaffoldMessenger.of(context).showSnackBar(snackBar);
+      context.scaffoldMessenger.showSnackBar(snackBar);
     }
 
     void showBatteryOptimizationInfoToUser() {
diff --git a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart
index c093e8f1e3..6c681e01df 100644
--- a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart
+++ b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart
@@ -48,14 +48,13 @@ class BackupSettings extends HookConsumerWidget {
       if (Platform.isIOS)
         SettingsSwitchListTile(
           valueNotifier: ignoreIcloudAssets,
-          title: 'Ignore iCloud photos',
-          subtitle:
-              'Photos that are stored on iCloud will not be uploaded to the Immich server',
+          title: 'ignore_icloud_photos'.tr(),
+          subtitle: 'ignore_icloud_photos_description'.tr(),
         ),
       if (Platform.isAndroid && isAdvancedTroubleshooting.value)
         SettingsButtonListTile(
           icon: Icons.warning_rounded,
-          title: 'Check for corrupt asset backups',
+          title: 'check_corrupt_asset_backup'.tr(),
           subtitle: isCorruptCheckInProgress
               ? const Column(
                   children: [
@@ -66,9 +65,9 @@ class BackupSettings extends HookConsumerWidget {
                 )
               : null,
           subtileText: !isCorruptCheckInProgress
-              ? 'Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.'
+              ? 'check_corrupt_asset_backup_description'.tr()
               : null,
-          buttonText: 'Perform check',
+          buttonText: 'check_corrupt_asset_backup_button'.tr(),
           onButtonTap: !isCorruptCheckInProgress
               ? () => ref
                   .read(backupVerificationProvider.notifier)
diff --git a/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart
new file mode 100644
index 0000000000..6302f9422a
--- /dev/null
+++ b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart
@@ -0,0 +1,155 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
+import 'package:immich_mobile/providers/auth.provider.dart';
+import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart';
+
+class EndpointInput extends StatefulHookConsumerWidget {
+  const EndpointInput({
+    super.key,
+    required this.initialValue,
+    required this.index,
+    required this.onValidated,
+    required this.onDismissed,
+    this.enabled = true,
+  });
+
+  final AuxilaryEndpoint initialValue;
+  final int index;
+  final Function(String url, int index, AuxCheckStatus status) onValidated;
+  final Function(int index) onDismissed;
+  final bool enabled;
+
+  @override
+  EndpointInputState createState() => EndpointInputState();
+}
+
+class EndpointInputState extends ConsumerState<EndpointInput> {
+  late final TextEditingController controller;
+  late final FocusNode focusNode;
+  late AuxCheckStatus auxCheckStatus;
+  bool isInputValid = false;
+
+  @override
+  void initState() {
+    super.initState();
+    controller = TextEditingController(text: widget.initialValue.url);
+    focusNode = FocusNode()..addListener(_onOutFocus);
+
+    setState(() {
+      auxCheckStatus = widget.initialValue.status;
+    });
+  }
+
+  @override
+  void dispose() {
+    focusNode.removeListener(_onOutFocus);
+    focusNode.dispose();
+    controller.dispose();
+    super.dispose();
+  }
+
+  void _onOutFocus() {
+    if (!focusNode.hasFocus && isInputValid) {
+      validateAuxilaryServerUrl();
+    }
+  }
+
+  Future<void> validateAuxilaryServerUrl() async {
+    final url = controller.text;
+    setState(() => auxCheckStatus = AuxCheckStatus.loading);
+
+    final isValid =
+        await ref.read(authProvider.notifier).validateAuxilaryServerUrl(url);
+
+    setState(() {
+      if (mounted) {
+        auxCheckStatus = isValid ? AuxCheckStatus.valid : AuxCheckStatus.error;
+      }
+    });
+
+    widget.onValidated(url, widget.index, auxCheckStatus);
+  }
+
+  String? validateUrl(String? url) {
+    try {
+      if (url == null || url.isEmpty || !Uri.parse(url).isAbsolute) {
+        isInputValid = false;
+        return 'validate_endpoint_error'.tr();
+      }
+    } catch (_) {
+      isInputValid = false;
+      return 'validate_endpoint_error'.tr();
+    }
+
+    isInputValid = true;
+    return null;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Dismissible(
+      key: ValueKey(widget.index.toString()),
+      direction: DismissDirection.endToStart,
+      onDismissed: (_) => widget.onDismissed(widget.index),
+      background: Container(
+        color: Colors.red,
+        alignment: Alignment.centerRight,
+        padding: const EdgeInsets.only(right: 16),
+        child: const Icon(
+          Icons.delete,
+          color: Colors.white,
+        ),
+      ),
+      child: ListTile(
+        contentPadding: const EdgeInsets.symmetric(horizontal: 24),
+        trailing: ReorderableDragStartListener(
+          enabled: widget.enabled,
+          index: widget.index,
+          child: const Icon(Icons.drag_handle_rounded),
+        ),
+        leading: NetworkStatusIcon(
+          key: ValueKey('status_$auxCheckStatus'),
+          status: auxCheckStatus,
+          enabled: widget.enabled,
+        ),
+        subtitle: TextFormField(
+          enabled: widget.enabled,
+          onTapOutside: (_) => focusNode.unfocus(),
+          autovalidateMode: AutovalidateMode.onUserInteraction,
+          validator: validateUrl,
+          keyboardType: TextInputType.url,
+          style: const TextStyle(
+            fontFamily: 'Inconsolata',
+            fontWeight: FontWeight.w600,
+            fontSize: 14,
+          ),
+          decoration: InputDecoration(
+            hintText: 'http(s)://immich.domain.com',
+            contentPadding: const EdgeInsets.all(16),
+            filled: true,
+            fillColor: context.colorScheme.surfaceContainer,
+            border: const OutlineInputBorder(
+              borderRadius: BorderRadius.all(Radius.circular(16)),
+            ),
+            errorBorder: OutlineInputBorder(
+              borderSide: BorderSide(color: Colors.red[300]!),
+              borderRadius: const BorderRadius.all(Radius.circular(16)),
+            ),
+            disabledBorder: OutlineInputBorder(
+              borderSide: BorderSide(
+                color:
+                    context.isDarkTheme ? Colors.grey[900]! : Colors.grey[300]!,
+              ),
+              borderRadius: const BorderRadius.all(Radius.circular(16)),
+            ),
+          ),
+          controller: controller,
+          focusNode: focusNode,
+        ),
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart
new file mode 100644
index 0000000000..13c109fa0e
--- /dev/null
+++ b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart
@@ -0,0 +1,189 @@
+import 'dart:convert';
+
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
+import 'package:immich_mobile/entities/store.entity.dart' as db_store;
+import 'package:immich_mobile/widgets/settings/networking_settings/endpoint_input.dart';
+
+class ExternalNetworkPreference extends HookConsumerWidget {
+  const ExternalNetworkPreference({super.key, required this.enabled});
+
+  final bool enabled;
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final entries =
+        useState([AuxilaryEndpoint(url: '', status: AuxCheckStatus.unknown)]);
+    final canSave = useState(false);
+
+    saveEndpointList() {
+      canSave.value =
+          entries.value.every((e) => e.status == AuxCheckStatus.valid);
+
+      final endpointList = entries.value
+          .where((url) => url.status == AuxCheckStatus.valid)
+          .toList();
+
+      final jsonString = jsonEncode(endpointList);
+
+      db_store.Store.put(
+        db_store.StoreKey.externalEndpointList,
+        jsonString,
+      );
+    }
+
+    updateValidationStatus(String url, int index, AuxCheckStatus status) {
+      entries.value[index] =
+          entries.value[index].copyWith(url: url, status: status);
+
+      saveEndpointList();
+    }
+
+    handleReorder(int oldIndex, int newIndex) {
+      if (oldIndex < newIndex) {
+        newIndex -= 1;
+      }
+
+      final entry = entries.value.removeAt(oldIndex);
+      entries.value.insert(newIndex, entry);
+      entries.value = [...entries.value];
+
+      saveEndpointList();
+    }
+
+    handleDismiss(int index) {
+      entries.value = [...entries.value..removeAt(index)];
+
+      saveEndpointList();
+    }
+
+    Widget proxyDecorator(
+      Widget child,
+      int index,
+      Animation<double> animation,
+    ) {
+      return AnimatedBuilder(
+        animation: animation,
+        builder: (BuildContext context, Widget? child) {
+          return Material(
+            color: context.colorScheme.surfaceContainerHighest,
+            shadowColor: context.colorScheme.primary.withOpacity(0.2),
+            child: child,
+          );
+        },
+        child: child,
+      );
+    }
+
+    useEffect(
+      () {
+        final jsonString =
+            db_store.Store.tryGet(db_store.StoreKey.externalEndpointList);
+
+        if (jsonString == null) {
+          return null;
+        }
+
+        final List<dynamic> jsonList = jsonDecode(jsonString);
+        entries.value =
+            jsonList.map((e) => AuxilaryEndpoint.fromJson(e)).toList();
+        return null;
+      },
+      const [],
+    );
+
+    return Padding(
+      padding: const EdgeInsets.symmetric(horizontal: 16.0),
+      child: Container(
+        clipBehavior: Clip.antiAlias,
+        decoration: BoxDecoration(
+          borderRadius: const BorderRadius.all(Radius.circular(16)),
+          color: context.colorScheme.surfaceContainerLow,
+          border: Border.all(
+            color: context.colorScheme.surfaceContainerHighest,
+            width: 1,
+          ),
+        ),
+        child: Stack(
+          children: [
+            Positioned(
+              bottom: -36,
+              right: -36,
+              child: Icon(
+                Icons.dns_rounded,
+                size: 120,
+                color: context.primaryColor.withOpacity(0.05),
+              ),
+            ),
+            ListView(
+              padding: const EdgeInsets.symmetric(vertical: 16.0),
+              physics: const ClampingScrollPhysics(),
+              shrinkWrap: true,
+              children: [
+                Padding(
+                  padding: const EdgeInsets.symmetric(
+                    vertical: 4.0,
+                    horizontal: 24,
+                  ),
+                  child: Text(
+                    "external_network_sheet_info".tr(),
+                    style: context.textTheme.bodyMedium,
+                  ),
+                ),
+                const SizedBox(height: 4),
+                Divider(color: context.colorScheme.surfaceContainerHighest),
+                Form(
+                  key: GlobalKey<FormState>(),
+                  child: ReorderableListView.builder(
+                    buildDefaultDragHandles: false,
+                    proxyDecorator: proxyDecorator,
+                    shrinkWrap: true,
+                    physics: const NeverScrollableScrollPhysics(),
+                    itemCount: entries.value.length,
+                    onReorder: handleReorder,
+                    itemBuilder: (context, index) {
+                      return EndpointInput(
+                        key: Key(index.toString()),
+                        index: index,
+                        initialValue: entries.value[index],
+                        onValidated: updateValidationStatus,
+                        onDismissed: handleDismiss,
+                        enabled: enabled,
+                      );
+                    },
+                  ),
+                ),
+                const SizedBox(height: 24),
+                Padding(
+                  padding: const EdgeInsets.symmetric(horizontal: 24.0),
+                  child: SizedBox(
+                    height: 48,
+                    child: OutlinedButton.icon(
+                      icon: const Icon(Icons.add),
+                      label: Text('add_endpoint'.tr().toUpperCase()),
+                      onPressed: enabled
+                          ? () {
+                              entries.value = [
+                                ...entries.value,
+                                AuxilaryEndpoint(
+                                  url: '',
+                                  status: AuxCheckStatus.unknown,
+                                ),
+                              ];
+                            }
+                          : null,
+                    ),
+                  ),
+                ),
+              ],
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart
new file mode 100644
index 0000000000..0258cc3847
--- /dev/null
+++ b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart
@@ -0,0 +1,256 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/providers/auth.provider.dart';
+import 'package:immich_mobile/providers/network.provider.dart';
+
+class LocalNetworkPreference extends HookConsumerWidget {
+  const LocalNetworkPreference({
+    super.key,
+    required this.enabled,
+  });
+
+  final bool enabled;
+
+  Future<String?> _showEditDialog(
+    BuildContext context,
+    String title,
+    String hintText,
+    String initialValue,
+  ) {
+    final controller = TextEditingController(text: initialValue);
+
+    return showDialog<String>(
+      context: context,
+      builder: (context) => AlertDialog(
+        title: Text(title),
+        content: TextField(
+          controller: controller,
+          autofocus: true,
+          decoration: InputDecoration(
+            border: const OutlineInputBorder(),
+            hintText: hintText,
+          ),
+        ),
+        actions: [
+          TextButton(
+            onPressed: () => Navigator.pop(context),
+            child: Text(
+              'cancel'.tr().toUpperCase(),
+              style: const TextStyle(color: Colors.red),
+            ),
+          ),
+          TextButton(
+            onPressed: () => Navigator.pop(context, controller.text),
+            child: Text('save'.tr().toUpperCase()),
+          ),
+        ],
+      ),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final wifiNameText = useState("");
+    final localEndpointText = useState("");
+
+    useEffect(
+      () {
+        final wifiName = ref.read(authProvider.notifier).getSavedWifiName();
+        final localEndpoint =
+            ref.read(authProvider.notifier).getSavedLocalEndpoint();
+
+        if (wifiName != null) {
+          wifiNameText.value = wifiName;
+        }
+
+        if (localEndpoint != null) {
+          localEndpointText.value = localEndpoint;
+        }
+
+        return null;
+      },
+      [],
+    );
+
+    saveWifiName(String wifiName) {
+      wifiNameText.value = wifiName;
+      return ref.read(authProvider.notifier).saveWifiName(wifiName);
+    }
+
+    saveLocalEndpoint(String url) {
+      localEndpointText.value = url;
+      return ref.read(authProvider.notifier).saveLocalEndpoint(url);
+    }
+
+    handleEditWifiName() async {
+      final wifiName = await _showEditDialog(
+        context,
+        "wifi_name".tr(),
+        "your_wifi_name".tr(),
+        wifiNameText.value,
+      );
+
+      if (wifiName != null) {
+        await saveWifiName(wifiName);
+      }
+    }
+
+    handleEditServerEndpoint() async {
+      final localEndpoint = await _showEditDialog(
+        context,
+        "server_endpoint".tr(),
+        "http://local-ip:2283/api",
+        localEndpointText.value,
+      );
+
+      if (localEndpoint != null) {
+        await saveLocalEndpoint(localEndpoint);
+      }
+    }
+
+    autofillCurrentNetwork() async {
+      final wifiName = await ref.read(networkProvider.notifier).getWifiName();
+
+      if (wifiName == null) {
+        context.showSnackBar(
+          SnackBar(
+            content: Text(
+              "get_wifiname_error".tr(),
+              style: context.textTheme.bodyMedium?.copyWith(
+                fontWeight: FontWeight.w500,
+                color: context.colorScheme.onSecondary,
+              ),
+            ),
+            backgroundColor: context.colorScheme.secondary,
+          ),
+        );
+      } else {
+        saveWifiName(wifiName);
+      }
+
+      final serverEndpoint =
+          ref.read(authProvider.notifier).getServerEndpoint();
+
+      if (serverEndpoint != null) {
+        saveLocalEndpoint(serverEndpoint);
+      }
+    }
+
+    return Padding(
+      padding: const EdgeInsets.symmetric(horizontal: 16.0),
+      child: Stack(
+        children: [
+          Container(
+            clipBehavior: Clip.antiAlias,
+            decoration: BoxDecoration(
+              borderRadius: const BorderRadius.all(Radius.circular(16)),
+              color: context.colorScheme.surfaceContainerLow,
+              border: Border.all(
+                color: context.colorScheme.surfaceContainerHighest,
+                width: 1,
+              ),
+            ),
+            child: Stack(
+              children: [
+                Positioned(
+                  bottom: -36,
+                  right: -36,
+                  child: Icon(
+                    Icons.home_outlined,
+                    size: 120,
+                    color: context.primaryColor.withOpacity(0.05),
+                  ),
+                ),
+                ListView(
+                  padding: const EdgeInsets.symmetric(vertical: 16.0),
+                  physics: const ClampingScrollPhysics(),
+                  shrinkWrap: true,
+                  children: [
+                    Padding(
+                      padding: const EdgeInsets.symmetric(
+                        vertical: 4.0,
+                        horizontal: 24,
+                      ),
+                      child: Text(
+                        "local_network_sheet_info".tr(),
+                        style: context.textTheme.bodyMedium,
+                      ),
+                    ),
+                    const SizedBox(height: 4),
+                    Divider(
+                      color: context.colorScheme.surfaceContainerHighest,
+                    ),
+                    ListTile(
+                      enabled: enabled,
+                      contentPadding: const EdgeInsets.only(left: 24, right: 8),
+                      leading: const Icon(Icons.wifi_rounded),
+                      title: Text("wifi_name".tr()),
+                      subtitle: wifiNameText.value.isEmpty
+                          ? Text("enter_wifi_name".tr())
+                          : Text(
+                              wifiNameText.value,
+                              style: context.textTheme.labelLarge?.copyWith(
+                                fontWeight: FontWeight.bold,
+                                color: enabled
+                                    ? context.primaryColor
+                                    : context.colorScheme.onSurface
+                                        .withAlpha(100),
+                                fontFamily: 'Inconsolata',
+                              ),
+                            ),
+                      trailing: IconButton(
+                        onPressed: enabled ? handleEditWifiName : null,
+                        icon: const Icon(Icons.edit_rounded),
+                      ),
+                    ),
+                    ListTile(
+                      enabled: enabled,
+                      contentPadding: const EdgeInsets.only(left: 24, right: 8),
+                      leading: const Icon(Icons.lan_rounded),
+                      title: Text("server_endpoint".tr()),
+                      subtitle: localEndpointText.value.isEmpty
+                          ? const Text("http://local-ip:2283/api")
+                          : Text(
+                              localEndpointText.value,
+                              style: context.textTheme.labelLarge?.copyWith(
+                                fontWeight: FontWeight.bold,
+                                color: enabled
+                                    ? context.primaryColor
+                                    : context.colorScheme.onSurface
+                                        .withAlpha(100),
+                                fontFamily: 'Inconsolata',
+                              ),
+                            ),
+                      trailing: IconButton(
+                        onPressed: enabled ? handleEditServerEndpoint : null,
+                        icon: const Icon(Icons.edit_rounded),
+                      ),
+                    ),
+                    const SizedBox(height: 16),
+                    Padding(
+                      padding: const EdgeInsets.symmetric(
+                        horizontal: 24.0,
+                      ),
+                      child: SizedBox(
+                        height: 48,
+                        child: OutlinedButton.icon(
+                          icon: const Icon(Icons.wifi_find_rounded),
+                          label:
+                              Text('use_current_connection'.tr().toUpperCase()),
+                          onPressed: enabled ? autofillCurrentNetwork : null,
+                        ),
+                      ),
+                    ),
+                  ],
+                ),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart
new file mode 100644
index 0000000000..59d05fd4cf
--- /dev/null
+++ b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart
@@ -0,0 +1,266 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
+import 'package:immich_mobile/providers/network.provider.dart';
+import 'package:immich_mobile/services/app_settings.service.dart';
+import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
+import 'package:immich_mobile/widgets/settings/networking_settings/external_network_preference.dart';
+import 'package:immich_mobile/widgets/settings/networking_settings/local_network_preference.dart';
+import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
+
+import 'package:immich_mobile/entities/store.entity.dart' as db_store;
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+
+class NetworkingSettings extends HookConsumerWidget {
+  const NetworkingSettings({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final currentEndpoint =
+        db_store.Store.get(db_store.StoreKey.serverEndpoint);
+    final featureEnabled =
+        useAppSettingsState(AppSettingsEnum.autoEndpointSwitching);
+
+    Future<void> checkWifiReadPermission() async {
+      final [hasLocationInUse, hasLocationAlways] = await Future.wait([
+        ref.read(networkProvider.notifier).getWifiReadPermission(),
+        ref.read(networkProvider.notifier).getWifiReadBackgroundPermission(),
+      ]);
+
+      bool? isGrantLocationAlwaysPermission;
+
+      if (!hasLocationInUse) {
+        await showDialog(
+          context: context,
+          builder: (context) {
+            return AlertDialog(
+              title: Text("location_permission".tr()),
+              content: Text("location_permission_content".tr()),
+              actions: [
+                TextButton(
+                  onPressed: () async {
+                    final isGrant = await ref
+                        .read(networkProvider.notifier)
+                        .requestWifiReadPermission();
+
+                    Navigator.pop(context, isGrant);
+                  },
+                  child: Text("grant_permission".tr()),
+                ),
+              ],
+            );
+          },
+        );
+      }
+
+      if (!hasLocationAlways) {
+        isGrantLocationAlwaysPermission = await showDialog(
+          context: context,
+          builder: (context) {
+            return AlertDialog(
+              title: Text("background_location_permission".tr()),
+              content: Text("background_location_permission_content".tr()),
+              actions: [
+                TextButton(
+                  onPressed: () async {
+                    final isGrant = await ref
+                        .read(networkProvider.notifier)
+                        .requestWifiReadBackgroundPermission();
+
+                    Navigator.pop(context, isGrant);
+                  },
+                  child: Text("grant_permission".tr()),
+                ),
+              ],
+            );
+          },
+        );
+      }
+
+      if (isGrantLocationAlwaysPermission != null &&
+          !isGrantLocationAlwaysPermission) {
+        await ref.read(networkProvider.notifier).openSettings();
+      }
+    }
+
+    useEffect(
+      () {
+        if (featureEnabled.value == true) {
+          checkWifiReadPermission();
+        }
+        return null;
+      },
+      [featureEnabled.value],
+    );
+
+    return ListView(
+      padding: const EdgeInsets.only(bottom: 96),
+      physics: const ClampingScrollPhysics(),
+      children: <Widget>[
+        Padding(
+          padding: const EdgeInsets.only(top: 8, left: 16, bottom: 8),
+          child: NetworkPreferenceTitle(
+            title: "current_server_address".tr().toUpperCase(),
+            icon: currentEndpoint.startsWith('https')
+                ? Icons.https_outlined
+                : Icons.http_outlined,
+          ),
+        ),
+        Padding(
+          padding: const EdgeInsets.symmetric(horizontal: 8),
+          child: Card(
+            elevation: 0,
+            shape: RoundedRectangleBorder(
+              borderRadius: const BorderRadius.all(Radius.circular(16)),
+              side: BorderSide(
+                color: context.colorScheme.surfaceContainerHighest,
+                width: 1,
+              ),
+            ),
+            child: ListTile(
+              leading:
+                  const Icon(Icons.check_circle_rounded, color: Colors.green),
+              title: Text(
+                currentEndpoint,
+                style: TextStyle(
+                  fontSize: 16,
+                  fontFamily: 'Inconsolata',
+                  fontWeight: FontWeight.bold,
+                  color: context.primaryColor,
+                ),
+              ),
+            ),
+          ),
+        ),
+        Padding(
+          padding: const EdgeInsets.only(top: 10.0),
+          child: Divider(
+            color: context.colorScheme.surfaceContainerHighest,
+          ),
+        ),
+        SettingsSwitchListTile(
+          enabled: true,
+          valueNotifier: featureEnabled,
+          title: "automatic_endpoint_switching_title".tr(),
+          subtitle: "automatic_endpoint_switching_subtitle".tr(),
+        ),
+        Padding(
+          padding: const EdgeInsets.only(top: 8, left: 16, bottom: 16),
+          child: NetworkPreferenceTitle(
+            title: "local_network".tr().toUpperCase(),
+            icon: Icons.home_outlined,
+          ),
+        ),
+        LocalNetworkPreference(
+          enabled: featureEnabled.value,
+        ),
+        Padding(
+          padding: const EdgeInsets.only(top: 32, left: 16, bottom: 16),
+          child: NetworkPreferenceTitle(
+            title: "external_network".tr().toUpperCase(),
+            icon: Icons.dns_outlined,
+          ),
+        ),
+        ExternalNetworkPreference(
+          enabled: featureEnabled.value,
+        ),
+      ],
+    );
+  }
+}
+
+class NetworkPreferenceTitle extends StatelessWidget {
+  const NetworkPreferenceTitle({
+    super.key,
+    required this.icon,
+    required this.title,
+  });
+
+  final IconData icon;
+  final String title;
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [
+        Icon(
+          icon,
+          color: context.colorScheme.onSurface.withAlpha(150),
+        ),
+        const SizedBox(width: 8),
+        Text(
+          title,
+          style: context.textTheme.displaySmall?.copyWith(
+            color: context.colorScheme.onSurface.withAlpha(200),
+            fontWeight: FontWeight.w500,
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+class NetworkStatusIcon extends StatelessWidget {
+  const NetworkStatusIcon({
+    super.key,
+    required this.status,
+    this.enabled = true,
+  }) : super();
+
+  final AuxCheckStatus status;
+  final bool enabled;
+
+  @override
+  Widget build(BuildContext context) {
+    return AnimatedSwitcher(
+      duration: const Duration(milliseconds: 200),
+      child: _buildIcon(context),
+    );
+  }
+
+  Widget _buildIcon(BuildContext context) {
+    switch (status) {
+      case AuxCheckStatus.loading:
+        return Padding(
+          padding: const EdgeInsets.only(left: 4.0),
+          child: SizedBox(
+            width: 18,
+            height: 18,
+            child: CircularProgressIndicator(
+              color: context.primaryColor,
+              strokeWidth: 2,
+              key: const ValueKey('loading'),
+            ),
+          ),
+        );
+      case AuxCheckStatus.valid:
+        return enabled
+            ? const Icon(
+                Icons.check_circle_rounded,
+                color: Colors.green,
+                key: ValueKey('success'),
+              )
+            : Icon(
+                Icons.check_circle_rounded,
+                color: context.colorScheme.onSurface.withAlpha(100),
+                key: const ValueKey('success'),
+              );
+      case AuxCheckStatus.error:
+        return enabled
+            ? const Icon(
+                Icons.error_rounded,
+                color: Colors.red,
+                key: ValueKey('error'),
+              )
+            : const Icon(
+                Icons.error_rounded,
+                color: Colors.grey,
+                key: ValueKey('error'),
+              );
+      default:
+        return const Icon(Icons.circle_outlined, key: ValueKey('unknown'));
+    }
+  }
+}
diff --git a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart
index 1c7cd1f207..119407ccad 100644
--- a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart
+++ b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart
@@ -2,12 +2,14 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/constants/immich_colors.dart';
+import 'package:immich_mobile/constants/colors.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/theme_extensions.dart';
+import 'package:immich_mobile/providers/theme.provider.dart';
 import 'package:immich_mobile/services/app_settings.service.dart';
-import 'package:immich_mobile/utils/immich_app_theme.dart';
 import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
+import 'package:immich_mobile/theme/color_scheme.dart';
+import 'package:immich_mobile/theme/dynamic_theme.dart';
 
 class PrimaryColorSetting extends HookConsumerWidget {
   const PrimaryColorSetting({
@@ -124,7 +126,7 @@ class PrimaryColorSetting extends HookConsumerWidget {
               style: context.textTheme.titleLarge,
             ),
           ),
-          if (isDynamicThemeAvailable)
+          if (DynamicTheme.isAvailable)
             Container(
               padding: const EdgeInsets.symmetric(horizontal: 20),
               margin: const EdgeInsets.only(top: 10),
@@ -153,16 +155,16 @@ class PrimaryColorSetting extends HookConsumerWidget {
             padding: const EdgeInsets.symmetric(horizontal: 20),
             child: Wrap(
               crossAxisAlignment: WrapCrossAlignment.center,
-              children: ImmichColorPreset.values.map((themePreset) {
-                var theme = themePreset.getTheme();
+              children: ImmichColorPreset.values.map((preset) {
+                final theme = preset.themeOfPreset;
 
                 return GestureDetector(
-                  onTap: () => onPrimaryColorChange(themePreset),
+                  onTap: () => onPrimaryColorChange(preset),
                   child: buildPrimaryColorTile(
                     topColor: theme.light.primary,
                     bottomColor: theme.dark.primary,
                     tileSize: tileSize,
-                    showSelector: currentPreset.value == themePreset &&
+                    showSelector: currentPreset.value == preset &&
                         !systemPrimaryColorSetting.value,
                   ),
                 );
diff --git a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart
index 050593a229..b9ba7aa7b7 100644
--- a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart
+++ b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart
@@ -2,12 +2,13 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/providers/theme.provider.dart';
 import 'package:immich_mobile/services/app_settings.service.dart';
 import 'package:immich_mobile/widgets/settings/preference_settings/primary_color_setting.dart';
 import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
 import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
 import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
-import 'package:immich_mobile/utils/immich_app_theme.dart';
 
 class ThemeSetting extends HookConsumerWidget {
   const ThemeSetting({
@@ -58,8 +59,7 @@ class ThemeSetting extends HookConsumerWidget {
         isSystemTheme.value = true;
         ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.system;
       } else {
-        final currentSystemBrightness =
-            MediaQuery.platformBrightnessOf(context);
+        final currentSystemBrightness = context.platformBrightness;
         isSystemTheme.value = false;
         isDarkTheme.value = currentSystemBrightness == Brightness.dark;
         if (currentSystemBrightness == Brightness.light) {
diff --git a/mobile/lib/widgets/shared_link/shared_link_item.dart b/mobile/lib/widgets/shared_link/shared_link_item.dart
index 9e29f5f9a0..a9ed359280 100644
--- a/mobile/lib/widgets/shared_link/shared_link_item.dart
+++ b/mobile/lib/widgets/shared_link/shared_link_item.dart
@@ -94,7 +94,7 @@ class SharedLinkItem extends ConsumerWidget {
       Clipboard.setData(
         ClipboardData(text: "${serverUrl}share/${sharedLink.key}"),
       ).then((_) {
-        ScaffoldMessenger.of(context).showSnackBar(
+        context.scaffoldMessenger.showSnackBar(
           SnackBar(
             content: Text(
               "shared_link_clipboard_copied_massage",
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index 36b2c7bbf4..a28035c01a 100644
Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ
diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index 091e900145..73eb02d89e 100644
Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ
diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart
index ceba3574cd..fd89986980 100644
Binary files a/mobile/openapi/lib/api/assets_api.dart and b/mobile/openapi/lib/api/assets_api.dart differ
diff --git a/mobile/openapi/lib/api/deprecated_api.dart b/mobile/openapi/lib/api/deprecated_api.dart
index 96cb3c2ef0..30e35b451c 100644
Binary files a/mobile/openapi/lib/api/deprecated_api.dart and b/mobile/openapi/lib/api/deprecated_api.dart differ
diff --git a/mobile/openapi/lib/api/jobs_api.dart b/mobile/openapi/lib/api/jobs_api.dart
index 5f9501d126..78afc15c93 100644
Binary files a/mobile/openapi/lib/api/jobs_api.dart and b/mobile/openapi/lib/api/jobs_api.dart differ
diff --git a/mobile/openapi/lib/api/libraries_api.dart b/mobile/openapi/lib/api/libraries_api.dart
index 53ab0e19ce..36d98d9a88 100644
Binary files a/mobile/openapi/lib/api/libraries_api.dart and b/mobile/openapi/lib/api/libraries_api.dart differ
diff --git a/mobile/openapi/lib/api/map_api.dart b/mobile/openapi/lib/api/map_api.dart
index 2846dae6c3..9644fbfc5c 100644
Binary files a/mobile/openapi/lib/api/map_api.dart and b/mobile/openapi/lib/api/map_api.dart differ
diff --git a/mobile/openapi/lib/api/notifications_api.dart b/mobile/openapi/lib/api/notifications_api.dart
index a3506b9bc1..323fbcc3d6 100644
Binary files a/mobile/openapi/lib/api/notifications_api.dart and b/mobile/openapi/lib/api/notifications_api.dart differ
diff --git a/mobile/openapi/lib/api/people_api.dart b/mobile/openapi/lib/api/people_api.dart
index 95c4a2fd45..92bd0fdeea 100644
Binary files a/mobile/openapi/lib/api/people_api.dart and b/mobile/openapi/lib/api/people_api.dart differ
diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart
index 4b6cdfea78..70af3ab0a3 100644
Binary files a/mobile/openapi/lib/api/search_api.dart and b/mobile/openapi/lib/api/search_api.dart differ
diff --git a/mobile/openapi/lib/api/server_api.dart b/mobile/openapi/lib/api/server_api.dart
index bde8d595b6..7a832ad61a 100644
Binary files a/mobile/openapi/lib/api/server_api.dart and b/mobile/openapi/lib/api/server_api.dart differ
diff --git a/mobile/openapi/lib/api/trash_api.dart b/mobile/openapi/lib/api/trash_api.dart
index 9c346870ec..8f8c6ffb3a 100644
Binary files a/mobile/openapi/lib/api/trash_api.dart and b/mobile/openapi/lib/api/trash_api.dart differ
diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index 9ec00aecc8..a6f8d551da 100644
Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ
diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart
index 8dcef880f5..b7c6ad5e01 100644
Binary files a/mobile/openapi/lib/api_helper.dart and b/mobile/openapi/lib/api_helper.dart differ
diff --git a/mobile/openapi/lib/model/activity_create_dto.dart b/mobile/openapi/lib/model/activity_create_dto.dart
index b54fa2ca72..ce4b4a0176 100644
Binary files a/mobile/openapi/lib/model/activity_create_dto.dart and b/mobile/openapi/lib/model/activity_create_dto.dart differ
diff --git a/mobile/openapi/lib/model/activity_response_dto.dart b/mobile/openapi/lib/model/activity_response_dto.dart
index bfffd8485b..25fb0f53f8 100644
Binary files a/mobile/openapi/lib/model/activity_response_dto.dart and b/mobile/openapi/lib/model/activity_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/activity_statistics_response_dto.dart b/mobile/openapi/lib/model/activity_statistics_response_dto.dart
index 20d4696b1b..ad0b814a58 100644
Binary files a/mobile/openapi/lib/model/activity_statistics_response_dto.dart and b/mobile/openapi/lib/model/activity_statistics_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/add_users_dto.dart b/mobile/openapi/lib/model/add_users_dto.dart
index 2daa571265..531c1ec785 100644
Binary files a/mobile/openapi/lib/model/add_users_dto.dart and b/mobile/openapi/lib/model/add_users_dto.dart differ
diff --git a/mobile/openapi/lib/model/admin_onboarding_update_dto.dart b/mobile/openapi/lib/model/admin_onboarding_update_dto.dart
index 2277f0958c..298bf318a2 100644
Binary files a/mobile/openapi/lib/model/admin_onboarding_update_dto.dart and b/mobile/openapi/lib/model/admin_onboarding_update_dto.dart differ
diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart
index c98a95775d..547a6a70fd 100644
Binary files a/mobile/openapi/lib/model/album_response_dto.dart and b/mobile/openapi/lib/model/album_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/album_statistics_response_dto.dart b/mobile/openapi/lib/model/album_statistics_response_dto.dart
index 90dbe52016..9e19002cf1 100644
Binary files a/mobile/openapi/lib/model/album_statistics_response_dto.dart and b/mobile/openapi/lib/model/album_statistics_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/album_user_add_dto.dart b/mobile/openapi/lib/model/album_user_add_dto.dart
index e654a2ff5d..e1f24377d7 100644
Binary files a/mobile/openapi/lib/model/album_user_add_dto.dart and b/mobile/openapi/lib/model/album_user_add_dto.dart differ
diff --git a/mobile/openapi/lib/model/album_user_create_dto.dart b/mobile/openapi/lib/model/album_user_create_dto.dart
index 708acd472b..93a0661b30 100644
Binary files a/mobile/openapi/lib/model/album_user_create_dto.dart and b/mobile/openapi/lib/model/album_user_create_dto.dart differ
diff --git a/mobile/openapi/lib/model/album_user_response_dto.dart b/mobile/openapi/lib/model/album_user_response_dto.dart
index 8f86cf254e..bbae03fba7 100644
Binary files a/mobile/openapi/lib/model/album_user_response_dto.dart and b/mobile/openapi/lib/model/album_user_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/all_job_status_response_dto.dart b/mobile/openapi/lib/model/all_job_status_response_dto.dart
index 1ee5253c38..787d02dd0e 100644
Binary files a/mobile/openapi/lib/model/all_job_status_response_dto.dart and b/mobile/openapi/lib/model/all_job_status_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/api_key_create_dto.dart b/mobile/openapi/lib/model/api_key_create_dto.dart
index 433855c4cf..848774e9c9 100644
Binary files a/mobile/openapi/lib/model/api_key_create_dto.dart and b/mobile/openapi/lib/model/api_key_create_dto.dart differ
diff --git a/mobile/openapi/lib/model/api_key_create_response_dto.dart b/mobile/openapi/lib/model/api_key_create_response_dto.dart
index 93065654ac..cdaa70e37d 100644
Binary files a/mobile/openapi/lib/model/api_key_create_response_dto.dart and b/mobile/openapi/lib/model/api_key_create_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/api_key_response_dto.dart b/mobile/openapi/lib/model/api_key_response_dto.dart
index b6ca86c050..fd0d91f673 100644
Binary files a/mobile/openapi/lib/model/api_key_response_dto.dart and b/mobile/openapi/lib/model/api_key_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/api_key_update_dto.dart b/mobile/openapi/lib/model/api_key_update_dto.dart
index 318f4936e1..7295d1ea1f 100644
Binary files a/mobile/openapi/lib/model/api_key_update_dto.dart and b/mobile/openapi/lib/model/api_key_update_dto.dart differ
diff --git a/mobile/openapi/lib/model/asset_bulk_delete_dto.dart b/mobile/openapi/lib/model/asset_bulk_delete_dto.dart
index 0f6913a7f4..c4453054b1 100644
Binary files a/mobile/openapi/lib/model/asset_bulk_delete_dto.dart and b/mobile/openapi/lib/model/asset_bulk_delete_dto.dart differ
diff --git a/mobile/openapi/lib/model/asset_bulk_update_dto.dart b/mobile/openapi/lib/model/asset_bulk_update_dto.dart
index c9b21683fb..da23d2f09d 100644
Binary files a/mobile/openapi/lib/model/asset_bulk_update_dto.dart and b/mobile/openapi/lib/model/asset_bulk_update_dto.dart differ
diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart
index 55ea41b598..36c13bfdf6 100644
Binary files a/mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart and b/mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart differ
diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_item.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_item.dart
index 16294cdae6..13dfa340fa 100644
Binary files a/mobile/openapi/lib/model/asset_bulk_upload_check_item.dart and b/mobile/openapi/lib/model/asset_bulk_upload_check_item.dart differ
diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart
index 5bfacbff57..8c3651e9fa 100644
Binary files a/mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart and b/mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart
index a016b357e7..88e46dae7d 100644
Binary files a/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart and b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart differ
diff --git a/mobile/openapi/lib/model/asset_delta_sync_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_dto.dart
index a5ee10f33e..845aadcdcd 100644
Binary files a/mobile/openapi/lib/model/asset_delta_sync_dto.dart and b/mobile/openapi/lib/model/asset_delta_sync_dto.dart differ
diff --git a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart
index 3b14fa68cf..a64e1a2fbe 100644
Binary files a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart and b/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/asset_face_response_dto.dart b/mobile/openapi/lib/model/asset_face_response_dto.dart
index 7a8588ce5c..c05b511649 100644
Binary files a/mobile/openapi/lib/model/asset_face_response_dto.dart and b/mobile/openapi/lib/model/asset_face_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/asset_face_update_dto.dart b/mobile/openapi/lib/model/asset_face_update_dto.dart
index 58def49ae1..71bdde8e9a 100644
Binary files a/mobile/openapi/lib/model/asset_face_update_dto.dart and b/mobile/openapi/lib/model/asset_face_update_dto.dart differ
diff --git a/mobile/openapi/lib/model/asset_face_update_item.dart b/mobile/openapi/lib/model/asset_face_update_item.dart
index 5ea37ea4db..c2c4803259 100644
Binary files a/mobile/openapi/lib/model/asset_face_update_item.dart and b/mobile/openapi/lib/model/asset_face_update_item.dart differ
diff --git a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart
index ecfe06bd7d..8bf07e1534 100644
Binary files a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart and b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/asset_full_sync_dto.dart b/mobile/openapi/lib/model/asset_full_sync_dto.dart
index e80638f6b0..7151094b95 100644
Binary files a/mobile/openapi/lib/model/asset_full_sync_dto.dart and b/mobile/openapi/lib/model/asset_full_sync_dto.dart differ
diff --git a/mobile/openapi/lib/model/asset_ids_dto.dart b/mobile/openapi/lib/model/asset_ids_dto.dart
index c8c7a69b89..b44888f396 100644
Binary files a/mobile/openapi/lib/model/asset_ids_dto.dart and b/mobile/openapi/lib/model/asset_ids_dto.dart differ
diff --git a/mobile/openapi/lib/model/asset_ids_response_dto.dart b/mobile/openapi/lib/model/asset_ids_response_dto.dart
index a642c0924c..ff63091caa 100644
Binary files a/mobile/openapi/lib/model/asset_ids_response_dto.dart and b/mobile/openapi/lib/model/asset_ids_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/asset_job_name.dart b/mobile/openapi/lib/model/asset_job_name.dart
index a5b42f4ee5..11e0555b86 100644
Binary files a/mobile/openapi/lib/model/asset_job_name.dart and b/mobile/openapi/lib/model/asset_job_name.dart differ
diff --git a/mobile/openapi/lib/model/asset_jobs_dto.dart b/mobile/openapi/lib/model/asset_jobs_dto.dart
index 16ed2644fd..0f8bfab009 100644
Binary files a/mobile/openapi/lib/model/asset_jobs_dto.dart and b/mobile/openapi/lib/model/asset_jobs_dto.dart differ
diff --git a/mobile/openapi/lib/model/asset_media_response_dto.dart b/mobile/openapi/lib/model/asset_media_response_dto.dart
index c2801c93cc..75428ec5f6 100644
Binary files a/mobile/openapi/lib/model/asset_media_response_dto.dart and b/mobile/openapi/lib/model/asset_media_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart
index bfb461efdc..5f01f84419 100644
Binary files a/mobile/openapi/lib/model/asset_response_dto.dart and b/mobile/openapi/lib/model/asset_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/asset_stack_response_dto.dart b/mobile/openapi/lib/model/asset_stack_response_dto.dart
index 89d30f7810..bb4becb129 100644
Binary files a/mobile/openapi/lib/model/asset_stack_response_dto.dart and b/mobile/openapi/lib/model/asset_stack_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/asset_stats_response_dto.dart b/mobile/openapi/lib/model/asset_stats_response_dto.dart
index c21d7fdbff..d11ce55a5c 100644
Binary files a/mobile/openapi/lib/model/asset_stats_response_dto.dart and b/mobile/openapi/lib/model/asset_stats_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/audio_codec.dart b/mobile/openapi/lib/model/audio_codec.dart
index ca195f7d06..ea1e96f36e 100644
Binary files a/mobile/openapi/lib/model/audio_codec.dart and b/mobile/openapi/lib/model/audio_codec.dart differ
diff --git a/mobile/openapi/lib/model/audit_deletes_response_dto.dart b/mobile/openapi/lib/model/audit_deletes_response_dto.dart
index 690a52e811..6b1df74eb4 100644
Binary files a/mobile/openapi/lib/model/audit_deletes_response_dto.dart and b/mobile/openapi/lib/model/audit_deletes_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/avatar_response.dart b/mobile/openapi/lib/model/avatar_response.dart
index edd242df4e..8ce0287565 100644
Binary files a/mobile/openapi/lib/model/avatar_response.dart and b/mobile/openapi/lib/model/avatar_response.dart differ
diff --git a/mobile/openapi/lib/model/avatar_update.dart b/mobile/openapi/lib/model/avatar_update.dart
index b92eb8dcbd..875eb138a8 100644
Binary files a/mobile/openapi/lib/model/avatar_update.dart and b/mobile/openapi/lib/model/avatar_update.dart differ
diff --git a/mobile/openapi/lib/model/bulk_id_response_dto.dart b/mobile/openapi/lib/model/bulk_id_response_dto.dart
index ef3cf2e0db..67a587e8d0 100644
Binary files a/mobile/openapi/lib/model/bulk_id_response_dto.dart and b/mobile/openapi/lib/model/bulk_id_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/bulk_ids_dto.dart b/mobile/openapi/lib/model/bulk_ids_dto.dart
index 6942875f0a..6a7f8ceeec 100644
Binary files a/mobile/openapi/lib/model/bulk_ids_dto.dart and b/mobile/openapi/lib/model/bulk_ids_dto.dart differ
diff --git a/mobile/openapi/lib/model/change_password_dto.dart b/mobile/openapi/lib/model/change_password_dto.dart
index 1074aaf74d..33b7f4a607 100644
Binary files a/mobile/openapi/lib/model/change_password_dto.dart and b/mobile/openapi/lib/model/change_password_dto.dart differ
diff --git a/mobile/openapi/lib/model/check_existing_assets_dto.dart b/mobile/openapi/lib/model/check_existing_assets_dto.dart
index 49ef36cc09..42ce6d5c3e 100644
Binary files a/mobile/openapi/lib/model/check_existing_assets_dto.dart and b/mobile/openapi/lib/model/check_existing_assets_dto.dart differ
diff --git a/mobile/openapi/lib/model/check_existing_assets_response_dto.dart b/mobile/openapi/lib/model/check_existing_assets_response_dto.dart
index d8b0f43a6d..ad93578ebc 100644
Binary files a/mobile/openapi/lib/model/check_existing_assets_response_dto.dart and b/mobile/openapi/lib/model/check_existing_assets_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/clip_config.dart b/mobile/openapi/lib/model/clip_config.dart
index 6e95c15fbf..b500d20f2e 100644
Binary files a/mobile/openapi/lib/model/clip_config.dart and b/mobile/openapi/lib/model/clip_config.dart differ
diff --git a/mobile/openapi/lib/model/create_album_dto.dart b/mobile/openapi/lib/model/create_album_dto.dart
index fa28b782ac..ff8c1df647 100644
Binary files a/mobile/openapi/lib/model/create_album_dto.dart and b/mobile/openapi/lib/model/create_album_dto.dart differ
diff --git a/mobile/openapi/lib/model/create_library_dto.dart b/mobile/openapi/lib/model/create_library_dto.dart
index 65ceec8e8a..2b8085be6f 100644
Binary files a/mobile/openapi/lib/model/create_library_dto.dart and b/mobile/openapi/lib/model/create_library_dto.dart differ
diff --git a/mobile/openapi/lib/model/create_profile_image_response_dto.dart b/mobile/openapi/lib/model/create_profile_image_response_dto.dart
index c9ae3ea651..ee98142e86 100644
Binary files a/mobile/openapi/lib/model/create_profile_image_response_dto.dart and b/mobile/openapi/lib/model/create_profile_image_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/database_backup_config.dart b/mobile/openapi/lib/model/database_backup_config.dart
new file mode 100644
index 0000000000..d82128bd44
Binary files /dev/null and b/mobile/openapi/lib/model/database_backup_config.dart differ
diff --git a/mobile/openapi/lib/model/download_archive_info.dart b/mobile/openapi/lib/model/download_archive_info.dart
index e324850bdc..5f3fd1a8c1 100644
Binary files a/mobile/openapi/lib/model/download_archive_info.dart and b/mobile/openapi/lib/model/download_archive_info.dart differ
diff --git a/mobile/openapi/lib/model/download_info_dto.dart b/mobile/openapi/lib/model/download_info_dto.dart
index 4c38769010..6f4777975c 100644
Binary files a/mobile/openapi/lib/model/download_info_dto.dart and b/mobile/openapi/lib/model/download_info_dto.dart differ
diff --git a/mobile/openapi/lib/model/download_response.dart b/mobile/openapi/lib/model/download_response.dart
index 25c5159a8b..041da44b71 100644
Binary files a/mobile/openapi/lib/model/download_response.dart and b/mobile/openapi/lib/model/download_response.dart differ
diff --git a/mobile/openapi/lib/model/download_response_dto.dart b/mobile/openapi/lib/model/download_response_dto.dart
index f32cba9253..5c6bd11266 100644
Binary files a/mobile/openapi/lib/model/download_response_dto.dart and b/mobile/openapi/lib/model/download_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/download_update.dart b/mobile/openapi/lib/model/download_update.dart
index 2c3839a687..8df825a922 100644
Binary files a/mobile/openapi/lib/model/download_update.dart and b/mobile/openapi/lib/model/download_update.dart differ
diff --git a/mobile/openapi/lib/model/duplicate_detection_config.dart b/mobile/openapi/lib/model/duplicate_detection_config.dart
index 0bc6091784..e4fc352028 100644
Binary files a/mobile/openapi/lib/model/duplicate_detection_config.dart and b/mobile/openapi/lib/model/duplicate_detection_config.dart differ
diff --git a/mobile/openapi/lib/model/duplicate_response_dto.dart b/mobile/openapi/lib/model/duplicate_response_dto.dart
index b93ecfe5f5..6ac7c46871 100644
Binary files a/mobile/openapi/lib/model/duplicate_response_dto.dart and b/mobile/openapi/lib/model/duplicate_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/email_notifications_response.dart b/mobile/openapi/lib/model/email_notifications_response.dart
index cef92957c6..d6dcfb9273 100644
Binary files a/mobile/openapi/lib/model/email_notifications_response.dart and b/mobile/openapi/lib/model/email_notifications_response.dart differ
diff --git a/mobile/openapi/lib/model/email_notifications_update.dart b/mobile/openapi/lib/model/email_notifications_update.dart
index dcd1ec4322..dad0a52fde 100644
Binary files a/mobile/openapi/lib/model/email_notifications_update.dart and b/mobile/openapi/lib/model/email_notifications_update.dart differ
diff --git a/mobile/openapi/lib/model/exif_response_dto.dart b/mobile/openapi/lib/model/exif_response_dto.dart
index 0185f300fa..17397b2081 100644
Binary files a/mobile/openapi/lib/model/exif_response_dto.dart and b/mobile/openapi/lib/model/exif_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/face_dto.dart b/mobile/openapi/lib/model/face_dto.dart
index 4fcc86debf..c84a518b8c 100644
Binary files a/mobile/openapi/lib/model/face_dto.dart and b/mobile/openapi/lib/model/face_dto.dart differ
diff --git a/mobile/openapi/lib/model/facial_recognition_config.dart b/mobile/openapi/lib/model/facial_recognition_config.dart
index 52400fd7e1..439efbbfae 100644
Binary files a/mobile/openapi/lib/model/facial_recognition_config.dart and b/mobile/openapi/lib/model/facial_recognition_config.dart differ
diff --git a/mobile/openapi/lib/model/file_checksum_dto.dart b/mobile/openapi/lib/model/file_checksum_dto.dart
index c7e8aa1da6..7dc9ccdf2f 100644
Binary files a/mobile/openapi/lib/model/file_checksum_dto.dart and b/mobile/openapi/lib/model/file_checksum_dto.dart differ
diff --git a/mobile/openapi/lib/model/file_checksum_response_dto.dart b/mobile/openapi/lib/model/file_checksum_response_dto.dart
index d4bae3c273..7b963c8bd5 100644
Binary files a/mobile/openapi/lib/model/file_checksum_response_dto.dart and b/mobile/openapi/lib/model/file_checksum_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/file_report_dto.dart b/mobile/openapi/lib/model/file_report_dto.dart
index 422215ff6c..3dc892e5e7 100644
Binary files a/mobile/openapi/lib/model/file_report_dto.dart and b/mobile/openapi/lib/model/file_report_dto.dart differ
diff --git a/mobile/openapi/lib/model/file_report_fix_dto.dart b/mobile/openapi/lib/model/file_report_fix_dto.dart
index cf09242b0f..d46cdeb4b7 100644
Binary files a/mobile/openapi/lib/model/file_report_fix_dto.dart and b/mobile/openapi/lib/model/file_report_fix_dto.dart differ
diff --git a/mobile/openapi/lib/model/file_report_item_dto.dart b/mobile/openapi/lib/model/file_report_item_dto.dart
index 5255005daa..1ef08c2b48 100644
Binary files a/mobile/openapi/lib/model/file_report_item_dto.dart and b/mobile/openapi/lib/model/file_report_item_dto.dart differ
diff --git a/mobile/openapi/lib/model/folders_response.dart b/mobile/openapi/lib/model/folders_response.dart
index 5bfc4c793d..248b64b054 100644
Binary files a/mobile/openapi/lib/model/folders_response.dart and b/mobile/openapi/lib/model/folders_response.dart differ
diff --git a/mobile/openapi/lib/model/folders_update.dart b/mobile/openapi/lib/model/folders_update.dart
index 088c98a4d8..0234717754 100644
Binary files a/mobile/openapi/lib/model/folders_update.dart and b/mobile/openapi/lib/model/folders_update.dart differ
diff --git a/mobile/openapi/lib/model/job_command_dto.dart b/mobile/openapi/lib/model/job_command_dto.dart
index 5c56715644..32274037f6 100644
Binary files a/mobile/openapi/lib/model/job_command_dto.dart and b/mobile/openapi/lib/model/job_command_dto.dart differ
diff --git a/mobile/openapi/lib/model/job_counts_dto.dart b/mobile/openapi/lib/model/job_counts_dto.dart
index cf1d0b457d..afc90d1084 100644
Binary files a/mobile/openapi/lib/model/job_counts_dto.dart and b/mobile/openapi/lib/model/job_counts_dto.dart differ
diff --git a/mobile/openapi/lib/model/job_create_dto.dart b/mobile/openapi/lib/model/job_create_dto.dart
new file mode 100644
index 0000000000..fe6743cba0
Binary files /dev/null and b/mobile/openapi/lib/model/job_create_dto.dart differ
diff --git a/mobile/openapi/lib/model/job_name.dart b/mobile/openapi/lib/model/job_name.dart
index 072da76d4c..6b9a002cbe 100644
Binary files a/mobile/openapi/lib/model/job_name.dart and b/mobile/openapi/lib/model/job_name.dart differ
diff --git a/mobile/openapi/lib/model/job_settings_dto.dart b/mobile/openapi/lib/model/job_settings_dto.dart
index 9c59d503ca..af354bef9e 100644
Binary files a/mobile/openapi/lib/model/job_settings_dto.dart and b/mobile/openapi/lib/model/job_settings_dto.dart differ
diff --git a/mobile/openapi/lib/model/job_status_dto.dart b/mobile/openapi/lib/model/job_status_dto.dart
index fd925bd53a..18fab8dfb3 100644
Binary files a/mobile/openapi/lib/model/job_status_dto.dart and b/mobile/openapi/lib/model/job_status_dto.dart differ
diff --git a/mobile/openapi/lib/model/library_response_dto.dart b/mobile/openapi/lib/model/library_response_dto.dart
index e27b489104..3cf1248508 100644
Binary files a/mobile/openapi/lib/model/library_response_dto.dart and b/mobile/openapi/lib/model/library_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/library_stats_response_dto.dart b/mobile/openapi/lib/model/library_stats_response_dto.dart
index 8cfb292855..afe67da31a 100644
Binary files a/mobile/openapi/lib/model/library_stats_response_dto.dart and b/mobile/openapi/lib/model/library_stats_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/license_key_dto.dart b/mobile/openapi/lib/model/license_key_dto.dart
index aece85f81e..d27d579bb4 100644
Binary files a/mobile/openapi/lib/model/license_key_dto.dart and b/mobile/openapi/lib/model/license_key_dto.dart differ
diff --git a/mobile/openapi/lib/model/license_response_dto.dart b/mobile/openapi/lib/model/license_response_dto.dart
index f83668af57..6d3009433f 100644
Binary files a/mobile/openapi/lib/model/license_response_dto.dart and b/mobile/openapi/lib/model/license_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/login_credential_dto.dart b/mobile/openapi/lib/model/login_credential_dto.dart
index ac2f511691..7e892ab5fb 100644
Binary files a/mobile/openapi/lib/model/login_credential_dto.dart and b/mobile/openapi/lib/model/login_credential_dto.dart differ
diff --git a/mobile/openapi/lib/model/login_response_dto.dart b/mobile/openapi/lib/model/login_response_dto.dart
index 6a0eb2355c..dbc82d07ba 100644
Binary files a/mobile/openapi/lib/model/login_response_dto.dart and b/mobile/openapi/lib/model/login_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/logout_response_dto.dart b/mobile/openapi/lib/model/logout_response_dto.dart
index ca1e8d23bb..aa94904e2a 100644
Binary files a/mobile/openapi/lib/model/logout_response_dto.dart and b/mobile/openapi/lib/model/logout_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/manual_job_name.dart b/mobile/openapi/lib/model/manual_job_name.dart
new file mode 100644
index 0000000000..7e8d9d51b2
Binary files /dev/null and b/mobile/openapi/lib/model/manual_job_name.dart differ
diff --git a/mobile/openapi/lib/model/map_marker_response_dto.dart b/mobile/openapi/lib/model/map_marker_response_dto.dart
index ca1ec3c8a1..74ac51a271 100644
Binary files a/mobile/openapi/lib/model/map_marker_response_dto.dart and b/mobile/openapi/lib/model/map_marker_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart b/mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart
index ac99dd91a9..6d8757d39f 100644
Binary files a/mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart and b/mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/map_theme.dart b/mobile/openapi/lib/model/map_theme.dart
deleted file mode 100644
index e2553790c6..0000000000
Binary files a/mobile/openapi/lib/model/map_theme.dart and /dev/null differ
diff --git a/mobile/openapi/lib/model/memories_response.dart b/mobile/openapi/lib/model/memories_response.dart
index e215a66a03..b9f8b5d8b1 100644
Binary files a/mobile/openapi/lib/model/memories_response.dart and b/mobile/openapi/lib/model/memories_response.dart differ
diff --git a/mobile/openapi/lib/model/memories_update.dart b/mobile/openapi/lib/model/memories_update.dart
index d309491361..71efd71ae7 100644
Binary files a/mobile/openapi/lib/model/memories_update.dart and b/mobile/openapi/lib/model/memories_update.dart differ
diff --git a/mobile/openapi/lib/model/memory_create_dto.dart b/mobile/openapi/lib/model/memory_create_dto.dart
index 2efdf88936..15985f2f1c 100644
Binary files a/mobile/openapi/lib/model/memory_create_dto.dart and b/mobile/openapi/lib/model/memory_create_dto.dart differ
diff --git a/mobile/openapi/lib/model/memory_lane_response_dto.dart b/mobile/openapi/lib/model/memory_lane_response_dto.dart
index 4abe607381..27248d05c1 100644
Binary files a/mobile/openapi/lib/model/memory_lane_response_dto.dart and b/mobile/openapi/lib/model/memory_lane_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/memory_response_dto.dart b/mobile/openapi/lib/model/memory_response_dto.dart
index f794be53cd..652c993536 100644
Binary files a/mobile/openapi/lib/model/memory_response_dto.dart and b/mobile/openapi/lib/model/memory_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/memory_update_dto.dart b/mobile/openapi/lib/model/memory_update_dto.dart
index 318f4b42ad..e750f9faad 100644
Binary files a/mobile/openapi/lib/model/memory_update_dto.dart and b/mobile/openapi/lib/model/memory_update_dto.dart differ
diff --git a/mobile/openapi/lib/model/merge_person_dto.dart b/mobile/openapi/lib/model/merge_person_dto.dart
index ea23042e2c..fd225276b6 100644
Binary files a/mobile/openapi/lib/model/merge_person_dto.dart and b/mobile/openapi/lib/model/merge_person_dto.dart differ
diff --git a/mobile/openapi/lib/model/metadata_search_dto.dart b/mobile/openapi/lib/model/metadata_search_dto.dart
index fabf7a2610..0aef1f623e 100644
Binary files a/mobile/openapi/lib/model/metadata_search_dto.dart and b/mobile/openapi/lib/model/metadata_search_dto.dart differ
diff --git a/mobile/openapi/lib/model/o_auth_authorize_response_dto.dart b/mobile/openapi/lib/model/o_auth_authorize_response_dto.dart
index ffd017f816..869c3be753 100644
Binary files a/mobile/openapi/lib/model/o_auth_authorize_response_dto.dart and b/mobile/openapi/lib/model/o_auth_authorize_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/o_auth_callback_dto.dart b/mobile/openapi/lib/model/o_auth_callback_dto.dart
index 89ad0f60b0..d0b98d5c6f 100644
Binary files a/mobile/openapi/lib/model/o_auth_callback_dto.dart and b/mobile/openapi/lib/model/o_auth_callback_dto.dart differ
diff --git a/mobile/openapi/lib/model/o_auth_config_dto.dart b/mobile/openapi/lib/model/o_auth_config_dto.dart
index 7d76758864..86c79b4e04 100644
Binary files a/mobile/openapi/lib/model/o_auth_config_dto.dart and b/mobile/openapi/lib/model/o_auth_config_dto.dart differ
diff --git a/mobile/openapi/lib/model/on_this_day_dto.dart b/mobile/openapi/lib/model/on_this_day_dto.dart
index be170caf85..bfcc4fd630 100644
Binary files a/mobile/openapi/lib/model/on_this_day_dto.dart and b/mobile/openapi/lib/model/on_this_day_dto.dart differ
diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart
index 7c3cf03bd9..f61df86b42 100644
Binary files a/mobile/openapi/lib/model/partner_response_dto.dart and b/mobile/openapi/lib/model/partner_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/people_response.dart b/mobile/openapi/lib/model/people_response.dart
index e12f86eeab..1312c73874 100644
Binary files a/mobile/openapi/lib/model/people_response.dart and b/mobile/openapi/lib/model/people_response.dart differ
diff --git a/mobile/openapi/lib/model/people_response_dto.dart b/mobile/openapi/lib/model/people_response_dto.dart
index 87e8c34fb0..49f0e85aad 100644
Binary files a/mobile/openapi/lib/model/people_response_dto.dart and b/mobile/openapi/lib/model/people_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/people_update.dart b/mobile/openapi/lib/model/people_update.dart
index 7803e62970..fb4eeeb434 100644
Binary files a/mobile/openapi/lib/model/people_update.dart and b/mobile/openapi/lib/model/people_update.dart differ
diff --git a/mobile/openapi/lib/model/people_update_dto.dart b/mobile/openapi/lib/model/people_update_dto.dart
index 9fcfdc8761..f771084f75 100644
Binary files a/mobile/openapi/lib/model/people_update_dto.dart and b/mobile/openapi/lib/model/people_update_dto.dart differ
diff --git a/mobile/openapi/lib/model/people_update_item.dart b/mobile/openapi/lib/model/people_update_item.dart
index 8af0a8b11a..042e4fa36f 100644
Binary files a/mobile/openapi/lib/model/people_update_item.dart and b/mobile/openapi/lib/model/people_update_item.dart differ
diff --git a/mobile/openapi/lib/model/person_create_dto.dart b/mobile/openapi/lib/model/person_create_dto.dart
index 9889328dee..36bd6dfee9 100644
Binary files a/mobile/openapi/lib/model/person_create_dto.dart and b/mobile/openapi/lib/model/person_create_dto.dart differ
diff --git a/mobile/openapi/lib/model/person_response_dto.dart b/mobile/openapi/lib/model/person_response_dto.dart
index 50ee28f0af..0b36fcde3b 100644
Binary files a/mobile/openapi/lib/model/person_response_dto.dart and b/mobile/openapi/lib/model/person_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/person_statistics_response_dto.dart b/mobile/openapi/lib/model/person_statistics_response_dto.dart
index 929fbc29d2..d9f84e9f4c 100644
Binary files a/mobile/openapi/lib/model/person_statistics_response_dto.dart and b/mobile/openapi/lib/model/person_statistics_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/person_update_dto.dart b/mobile/openapi/lib/model/person_update_dto.dart
index 1af03890a2..51a7ea25d0 100644
Binary files a/mobile/openapi/lib/model/person_update_dto.dart and b/mobile/openapi/lib/model/person_update_dto.dart differ
diff --git a/mobile/openapi/lib/model/person_with_faces_response_dto.dart b/mobile/openapi/lib/model/person_with_faces_response_dto.dart
index af2e7101c3..b14bad7895 100644
Binary files a/mobile/openapi/lib/model/person_with_faces_response_dto.dart and b/mobile/openapi/lib/model/person_with_faces_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/places_response_dto.dart b/mobile/openapi/lib/model/places_response_dto.dart
index d3e1fc449b..4f77788263 100644
Binary files a/mobile/openapi/lib/model/places_response_dto.dart and b/mobile/openapi/lib/model/places_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/purchase_response.dart b/mobile/openapi/lib/model/purchase_response.dart
index 284d899528..a117206977 100644
Binary files a/mobile/openapi/lib/model/purchase_response.dart and b/mobile/openapi/lib/model/purchase_response.dart differ
diff --git a/mobile/openapi/lib/model/purchase_update.dart b/mobile/openapi/lib/model/purchase_update.dart
index ca0a27e3bc..69057e6c55 100644
Binary files a/mobile/openapi/lib/model/purchase_update.dart and b/mobile/openapi/lib/model/purchase_update.dart differ
diff --git a/mobile/openapi/lib/model/queue_status_dto.dart b/mobile/openapi/lib/model/queue_status_dto.dart
index 7f7d310f6f..77591affe2 100644
Binary files a/mobile/openapi/lib/model/queue_status_dto.dart and b/mobile/openapi/lib/model/queue_status_dto.dart differ
diff --git a/mobile/openapi/lib/model/random_search_dto.dart b/mobile/openapi/lib/model/random_search_dto.dart
new file mode 100644
index 0000000000..3fcab05bbb
Binary files /dev/null and b/mobile/openapi/lib/model/random_search_dto.dart differ
diff --git a/mobile/openapi/lib/model/ratings_response.dart b/mobile/openapi/lib/model/ratings_response.dart
index c8791aa91a..8e1951277a 100644
Binary files a/mobile/openapi/lib/model/ratings_response.dart and b/mobile/openapi/lib/model/ratings_response.dart differ
diff --git a/mobile/openapi/lib/model/ratings_update.dart b/mobile/openapi/lib/model/ratings_update.dart
index bde51bad1b..5d9f9a655f 100644
Binary files a/mobile/openapi/lib/model/ratings_update.dart and b/mobile/openapi/lib/model/ratings_update.dart differ
diff --git a/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart b/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart
index eb414be984..5b3648b46b 100644
Binary files a/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart and b/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/scan_library_dto.dart b/mobile/openapi/lib/model/scan_library_dto.dart
deleted file mode 100644
index 1b31aaaf01..0000000000
Binary files a/mobile/openapi/lib/model/scan_library_dto.dart and /dev/null differ
diff --git a/mobile/openapi/lib/model/search_album_response_dto.dart b/mobile/openapi/lib/model/search_album_response_dto.dart
index 46ce5273ac..e9b47e85ec 100644
Binary files a/mobile/openapi/lib/model/search_album_response_dto.dart and b/mobile/openapi/lib/model/search_album_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/search_asset_response_dto.dart b/mobile/openapi/lib/model/search_asset_response_dto.dart
index 21ddbbb213..3d214e61d9 100644
Binary files a/mobile/openapi/lib/model/search_asset_response_dto.dart and b/mobile/openapi/lib/model/search_asset_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/search_explore_item.dart b/mobile/openapi/lib/model/search_explore_item.dart
index 951fdd1bc8..d44b2cd704 100644
Binary files a/mobile/openapi/lib/model/search_explore_item.dart and b/mobile/openapi/lib/model/search_explore_item.dart differ
diff --git a/mobile/openapi/lib/model/search_explore_response_dto.dart b/mobile/openapi/lib/model/search_explore_response_dto.dart
index 5bc601de9e..3b5d4f9849 100644
Binary files a/mobile/openapi/lib/model/search_explore_response_dto.dart and b/mobile/openapi/lib/model/search_explore_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/search_facet_count_response_dto.dart b/mobile/openapi/lib/model/search_facet_count_response_dto.dart
index b40710e525..f8eee84485 100644
Binary files a/mobile/openapi/lib/model/search_facet_count_response_dto.dart and b/mobile/openapi/lib/model/search_facet_count_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/search_facet_response_dto.dart b/mobile/openapi/lib/model/search_facet_response_dto.dart
index 0784921c6b..aeec873c8d 100644
Binary files a/mobile/openapi/lib/model/search_facet_response_dto.dart and b/mobile/openapi/lib/model/search_facet_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/search_response_dto.dart b/mobile/openapi/lib/model/search_response_dto.dart
index 9b2b7fd3cf..ca742ae35c 100644
Binary files a/mobile/openapi/lib/model/search_response_dto.dart and b/mobile/openapi/lib/model/search_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/server_about_response_dto.dart b/mobile/openapi/lib/model/server_about_response_dto.dart
index 9c71d1fccd..5d53d5fdee 100644
Binary files a/mobile/openapi/lib/model/server_about_response_dto.dart and b/mobile/openapi/lib/model/server_about_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/server_config_dto.dart b/mobile/openapi/lib/model/server_config_dto.dart
index 47cc52fb2c..01c82af4d9 100644
Binary files a/mobile/openapi/lib/model/server_config_dto.dart and b/mobile/openapi/lib/model/server_config_dto.dart differ
diff --git a/mobile/openapi/lib/model/server_features_dto.dart b/mobile/openapi/lib/model/server_features_dto.dart
index 0a7d8a4b47..5149c3796a 100644
Binary files a/mobile/openapi/lib/model/server_features_dto.dart and b/mobile/openapi/lib/model/server_features_dto.dart differ
diff --git a/mobile/openapi/lib/model/server_media_types_response_dto.dart b/mobile/openapi/lib/model/server_media_types_response_dto.dart
index 35ddef1956..506cbb44b4 100644
Binary files a/mobile/openapi/lib/model/server_media_types_response_dto.dart and b/mobile/openapi/lib/model/server_media_types_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/server_ping_response.dart b/mobile/openapi/lib/model/server_ping_response.dart
index e23dc15c61..621ebfa294 100644
Binary files a/mobile/openapi/lib/model/server_ping_response.dart and b/mobile/openapi/lib/model/server_ping_response.dart differ
diff --git a/mobile/openapi/lib/model/server_stats_response_dto.dart b/mobile/openapi/lib/model/server_stats_response_dto.dart
index 6996e49aa5..531fa8f03e 100644
Binary files a/mobile/openapi/lib/model/server_stats_response_dto.dart and b/mobile/openapi/lib/model/server_stats_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/server_storage_response_dto.dart b/mobile/openapi/lib/model/server_storage_response_dto.dart
index 89d97d32ea..8d12e77834 100644
Binary files a/mobile/openapi/lib/model/server_storage_response_dto.dart and b/mobile/openapi/lib/model/server_storage_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/server_theme_dto.dart b/mobile/openapi/lib/model/server_theme_dto.dart
index 65b9b9163e..69e1b2d2c8 100644
Binary files a/mobile/openapi/lib/model/server_theme_dto.dart and b/mobile/openapi/lib/model/server_theme_dto.dart differ
diff --git a/mobile/openapi/lib/model/server_version_history_response_dto.dart b/mobile/openapi/lib/model/server_version_history_response_dto.dart
new file mode 100644
index 0000000000..c81cb0e8b9
Binary files /dev/null and b/mobile/openapi/lib/model/server_version_history_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/server_version_response_dto.dart b/mobile/openapi/lib/model/server_version_response_dto.dart
index e507f3372a..751347fabd 100644
Binary files a/mobile/openapi/lib/model/server_version_response_dto.dart and b/mobile/openapi/lib/model/server_version_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/session_response_dto.dart b/mobile/openapi/lib/model/session_response_dto.dart
index 82673b3874..92e2dc6067 100644
Binary files a/mobile/openapi/lib/model/session_response_dto.dart and b/mobile/openapi/lib/model/session_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/shared_link_create_dto.dart b/mobile/openapi/lib/model/shared_link_create_dto.dart
index 623bc3125f..bc96b31fd2 100644
Binary files a/mobile/openapi/lib/model/shared_link_create_dto.dart and b/mobile/openapi/lib/model/shared_link_create_dto.dart differ
diff --git a/mobile/openapi/lib/model/shared_link_edit_dto.dart b/mobile/openapi/lib/model/shared_link_edit_dto.dart
index 2369c85db1..a394ba9b3b 100644
Binary files a/mobile/openapi/lib/model/shared_link_edit_dto.dart and b/mobile/openapi/lib/model/shared_link_edit_dto.dart differ
diff --git a/mobile/openapi/lib/model/shared_link_response_dto.dart b/mobile/openapi/lib/model/shared_link_response_dto.dart
index 018a1a51de..9cc8b3ac80 100644
Binary files a/mobile/openapi/lib/model/shared_link_response_dto.dart and b/mobile/openapi/lib/model/shared_link_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/sign_up_dto.dart b/mobile/openapi/lib/model/sign_up_dto.dart
index 772749fdba..7e0ff4045c 100644
Binary files a/mobile/openapi/lib/model/sign_up_dto.dart and b/mobile/openapi/lib/model/sign_up_dto.dart differ
diff --git a/mobile/openapi/lib/model/smart_info_response_dto.dart b/mobile/openapi/lib/model/smart_info_response_dto.dart
deleted file mode 100644
index 52e7c108b8..0000000000
Binary files a/mobile/openapi/lib/model/smart_info_response_dto.dart and /dev/null differ
diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart
index 2a42b75768..4e1408cafa 100644
Binary files a/mobile/openapi/lib/model/smart_search_dto.dart and b/mobile/openapi/lib/model/smart_search_dto.dart differ
diff --git a/mobile/openapi/lib/model/stack_create_dto.dart b/mobile/openapi/lib/model/stack_create_dto.dart
index 9b37bc6e2e..cb51081eb1 100644
Binary files a/mobile/openapi/lib/model/stack_create_dto.dart and b/mobile/openapi/lib/model/stack_create_dto.dart differ
diff --git a/mobile/openapi/lib/model/stack_response_dto.dart b/mobile/openapi/lib/model/stack_response_dto.dart
index 3d0aaf91d1..b6cb747caf 100644
Binary files a/mobile/openapi/lib/model/stack_response_dto.dart and b/mobile/openapi/lib/model/stack_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/stack_update_dto.dart b/mobile/openapi/lib/model/stack_update_dto.dart
index 0e97127210..0101499edf 100644
Binary files a/mobile/openapi/lib/model/stack_update_dto.dart and b/mobile/openapi/lib/model/stack_update_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_backups_dto.dart b/mobile/openapi/lib/model/system_config_backups_dto.dart
new file mode 100644
index 0000000000..82cd6e59eb
Binary files /dev/null and b/mobile/openapi/lib/model/system_config_backups_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart
index aff8062c8a..59d5f09fc9 100644
Binary files a/mobile/openapi/lib/model/system_config_dto.dart and b/mobile/openapi/lib/model/system_config_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart
index a75a77c669..0acfc9e8fb 100644
Binary files a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart and b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_faces_dto.dart b/mobile/openapi/lib/model/system_config_faces_dto.dart
index 980e494fb7..4e18eb8de2 100644
Binary files a/mobile/openapi/lib/model/system_config_faces_dto.dart and b/mobile/openapi/lib/model/system_config_faces_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_generated_image_dto.dart b/mobile/openapi/lib/model/system_config_generated_image_dto.dart
new file mode 100644
index 0000000000..2192a7cb0c
Binary files /dev/null and b/mobile/openapi/lib/model/system_config_generated_image_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_image_dto.dart b/mobile/openapi/lib/model/system_config_image_dto.dart
index 388949c759..5309f7745c 100644
Binary files a/mobile/openapi/lib/model/system_config_image_dto.dart and b/mobile/openapi/lib/model/system_config_image_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_job_dto.dart b/mobile/openapi/lib/model/system_config_job_dto.dart
index 1bc0f6b29c..c0fed5cccc 100644
Binary files a/mobile/openapi/lib/model/system_config_job_dto.dart and b/mobile/openapi/lib/model/system_config_job_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_library_dto.dart b/mobile/openapi/lib/model/system_config_library_dto.dart
index 4f55e33e80..e728b0bf20 100644
Binary files a/mobile/openapi/lib/model/system_config_library_dto.dart and b/mobile/openapi/lib/model/system_config_library_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_library_scan_dto.dart b/mobile/openapi/lib/model/system_config_library_scan_dto.dart
index 31df272594..6a6558b4b3 100644
Binary files a/mobile/openapi/lib/model/system_config_library_scan_dto.dart and b/mobile/openapi/lib/model/system_config_library_scan_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_library_watch_dto.dart b/mobile/openapi/lib/model/system_config_library_watch_dto.dart
index 9d152f366a..1a1f5d7126 100644
Binary files a/mobile/openapi/lib/model/system_config_library_watch_dto.dart and b/mobile/openapi/lib/model/system_config_library_watch_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_logging_dto.dart b/mobile/openapi/lib/model/system_config_logging_dto.dart
index 60c0be3d2c..f025221eff 100644
Binary files a/mobile/openapi/lib/model/system_config_logging_dto.dart and b/mobile/openapi/lib/model/system_config_logging_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart
index 3923bacad4..a4a9ca7d82 100644
Binary files a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart and b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_map_dto.dart b/mobile/openapi/lib/model/system_config_map_dto.dart
index 6631885182..d53d5711db 100644
Binary files a/mobile/openapi/lib/model/system_config_map_dto.dart and b/mobile/openapi/lib/model/system_config_map_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_metadata_dto.dart b/mobile/openapi/lib/model/system_config_metadata_dto.dart
index 60ca35c835..3c32fc551d 100644
Binary files a/mobile/openapi/lib/model/system_config_metadata_dto.dart and b/mobile/openapi/lib/model/system_config_metadata_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_new_version_check_dto.dart b/mobile/openapi/lib/model/system_config_new_version_check_dto.dart
index c7b8c98695..c63d2abc1b 100644
Binary files a/mobile/openapi/lib/model/system_config_new_version_check_dto.dart and b/mobile/openapi/lib/model/system_config_new_version_check_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_notifications_dto.dart b/mobile/openapi/lib/model/system_config_notifications_dto.dart
index 22f08b3ab4..35d3d31833 100644
Binary files a/mobile/openapi/lib/model/system_config_notifications_dto.dart and b/mobile/openapi/lib/model/system_config_notifications_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_o_auth_dto.dart b/mobile/openapi/lib/model/system_config_o_auth_dto.dart
index 6ebbe8d25c..9125bb7bba 100644
Binary files a/mobile/openapi/lib/model/system_config_o_auth_dto.dart and b/mobile/openapi/lib/model/system_config_o_auth_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_password_login_dto.dart b/mobile/openapi/lib/model/system_config_password_login_dto.dart
index 61896a890c..69c8942bb6 100644
Binary files a/mobile/openapi/lib/model/system_config_password_login_dto.dart and b/mobile/openapi/lib/model/system_config_password_login_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart b/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart
index 2eb586cac6..6c1673d46c 100644
Binary files a/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart and b/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_server_dto.dart b/mobile/openapi/lib/model/system_config_server_dto.dart
index ccb48ee61d..8099292dd0 100644
Binary files a/mobile/openapi/lib/model/system_config_server_dto.dart and b/mobile/openapi/lib/model/system_config_server_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_smtp_dto.dart b/mobile/openapi/lib/model/system_config_smtp_dto.dart
index 6588d244ee..fcde49cf35 100644
Binary files a/mobile/openapi/lib/model/system_config_smtp_dto.dart and b/mobile/openapi/lib/model/system_config_smtp_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_smtp_transport_dto.dart b/mobile/openapi/lib/model/system_config_smtp_transport_dto.dart
index 63dfdca4cf..bdaaa426c5 100644
Binary files a/mobile/openapi/lib/model/system_config_smtp_transport_dto.dart and b/mobile/openapi/lib/model/system_config_smtp_transport_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_storage_template_dto.dart b/mobile/openapi/lib/model/system_config_storage_template_dto.dart
index 13323aebda..596aafc195 100644
Binary files a/mobile/openapi/lib/model/system_config_storage_template_dto.dart and b/mobile/openapi/lib/model/system_config_storage_template_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_template_emails_dto.dart b/mobile/openapi/lib/model/system_config_template_emails_dto.dart
new file mode 100644
index 0000000000..9db85509f5
Binary files /dev/null and b/mobile/openapi/lib/model/system_config_template_emails_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_template_storage_option_dto.dart b/mobile/openapi/lib/model/system_config_template_storage_option_dto.dart
index 82e0a6f747..f8586d344c 100644
Binary files a/mobile/openapi/lib/model/system_config_template_storage_option_dto.dart and b/mobile/openapi/lib/model/system_config_template_storage_option_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_templates_dto.dart b/mobile/openapi/lib/model/system_config_templates_dto.dart
new file mode 100644
index 0000000000..a5e8834978
Binary files /dev/null and b/mobile/openapi/lib/model/system_config_templates_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_theme_dto.dart b/mobile/openapi/lib/model/system_config_theme_dto.dart
index 2f7f4d2f3b..a97c2cf84c 100644
Binary files a/mobile/openapi/lib/model/system_config_theme_dto.dart and b/mobile/openapi/lib/model/system_config_theme_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_trash_dto.dart b/mobile/openapi/lib/model/system_config_trash_dto.dart
index 336019fde4..51b39e9a55 100644
Binary files a/mobile/openapi/lib/model/system_config_trash_dto.dart and b/mobile/openapi/lib/model/system_config_trash_dto.dart differ
diff --git a/mobile/openapi/lib/model/system_config_user_dto.dart b/mobile/openapi/lib/model/system_config_user_dto.dart
index c466374460..8e6bd3c9c3 100644
Binary files a/mobile/openapi/lib/model/system_config_user_dto.dart and b/mobile/openapi/lib/model/system_config_user_dto.dart differ
diff --git a/mobile/openapi/lib/model/tag_bulk_assets_dto.dart b/mobile/openapi/lib/model/tag_bulk_assets_dto.dart
index c11cb66ce0..26a575e193 100644
Binary files a/mobile/openapi/lib/model/tag_bulk_assets_dto.dart and b/mobile/openapi/lib/model/tag_bulk_assets_dto.dart differ
diff --git a/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart
index d4dcb91d8c..009f26bfe4 100644
Binary files a/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart and b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/tag_create_dto.dart b/mobile/openapi/lib/model/tag_create_dto.dart
index dd7e537a0a..9a5171074d 100644
Binary files a/mobile/openapi/lib/model/tag_create_dto.dart and b/mobile/openapi/lib/model/tag_create_dto.dart differ
diff --git a/mobile/openapi/lib/model/tag_response_dto.dart b/mobile/openapi/lib/model/tag_response_dto.dart
index 1d1a88c3cf..cd684b163a 100644
Binary files a/mobile/openapi/lib/model/tag_response_dto.dart and b/mobile/openapi/lib/model/tag_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/tag_update_dto.dart b/mobile/openapi/lib/model/tag_update_dto.dart
index 661f65896e..ab1adb127b 100644
Binary files a/mobile/openapi/lib/model/tag_update_dto.dart and b/mobile/openapi/lib/model/tag_update_dto.dart differ
diff --git a/mobile/openapi/lib/model/tag_upsert_dto.dart b/mobile/openapi/lib/model/tag_upsert_dto.dart
index 941d25b6ae..d60a00f466 100644
Binary files a/mobile/openapi/lib/model/tag_upsert_dto.dart and b/mobile/openapi/lib/model/tag_upsert_dto.dart differ
diff --git a/mobile/openapi/lib/model/tags_response.dart b/mobile/openapi/lib/model/tags_response.dart
index 3a5ea3b20b..2470edf979 100644
Binary files a/mobile/openapi/lib/model/tags_response.dart and b/mobile/openapi/lib/model/tags_response.dart differ
diff --git a/mobile/openapi/lib/model/tags_update.dart b/mobile/openapi/lib/model/tags_update.dart
index 8355b00a00..d992369140 100644
Binary files a/mobile/openapi/lib/model/tags_update.dart and b/mobile/openapi/lib/model/tags_update.dart differ
diff --git a/mobile/openapi/lib/model/template_dto.dart b/mobile/openapi/lib/model/template_dto.dart
new file mode 100644
index 0000000000..f818e0508a
Binary files /dev/null and b/mobile/openapi/lib/model/template_dto.dart differ
diff --git a/mobile/openapi/lib/model/template_response_dto.dart b/mobile/openapi/lib/model/template_response_dto.dart
new file mode 100644
index 0000000000..3c3224a54b
Binary files /dev/null and b/mobile/openapi/lib/model/template_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/test_email_response_dto.dart b/mobile/openapi/lib/model/test_email_response_dto.dart
new file mode 100644
index 0000000000..33e6c042d8
Binary files /dev/null and b/mobile/openapi/lib/model/test_email_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/time_bucket_response_dto.dart b/mobile/openapi/lib/model/time_bucket_response_dto.dart
index 2c86a56b3c..56044b27a8 100644
Binary files a/mobile/openapi/lib/model/time_bucket_response_dto.dart and b/mobile/openapi/lib/model/time_bucket_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/trash_response_dto.dart b/mobile/openapi/lib/model/trash_response_dto.dart
new file mode 100644
index 0000000000..2df154d06c
Binary files /dev/null and b/mobile/openapi/lib/model/trash_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/update_album_dto.dart b/mobile/openapi/lib/model/update_album_dto.dart
index f9c9762887..8353dba14e 100644
Binary files a/mobile/openapi/lib/model/update_album_dto.dart and b/mobile/openapi/lib/model/update_album_dto.dart differ
diff --git a/mobile/openapi/lib/model/update_album_user_dto.dart b/mobile/openapi/lib/model/update_album_user_dto.dart
index f77223acf5..43218cae6e 100644
Binary files a/mobile/openapi/lib/model/update_album_user_dto.dart and b/mobile/openapi/lib/model/update_album_user_dto.dart differ
diff --git a/mobile/openapi/lib/model/update_asset_dto.dart b/mobile/openapi/lib/model/update_asset_dto.dart
index 9aa413d242..9ebce5fd92 100644
Binary files a/mobile/openapi/lib/model/update_asset_dto.dart and b/mobile/openapi/lib/model/update_asset_dto.dart differ
diff --git a/mobile/openapi/lib/model/update_library_dto.dart b/mobile/openapi/lib/model/update_library_dto.dart
index 85847c0ddf..6a4f36906f 100644
Binary files a/mobile/openapi/lib/model/update_library_dto.dart and b/mobile/openapi/lib/model/update_library_dto.dart differ
diff --git a/mobile/openapi/lib/model/update_partner_dto.dart b/mobile/openapi/lib/model/update_partner_dto.dart
index f695f99535..3af3c83ad1 100644
Binary files a/mobile/openapi/lib/model/update_partner_dto.dart and b/mobile/openapi/lib/model/update_partner_dto.dart differ
diff --git a/mobile/openapi/lib/model/usage_by_user_dto.dart b/mobile/openapi/lib/model/usage_by_user_dto.dart
index 0bbbba00bb..80235915fe 100644
Binary files a/mobile/openapi/lib/model/usage_by_user_dto.dart and b/mobile/openapi/lib/model/usage_by_user_dto.dart differ
diff --git a/mobile/openapi/lib/model/user_admin_create_dto.dart b/mobile/openapi/lib/model/user_admin_create_dto.dart
index db514a1d57..f2709be57b 100644
Binary files a/mobile/openapi/lib/model/user_admin_create_dto.dart and b/mobile/openapi/lib/model/user_admin_create_dto.dart differ
diff --git a/mobile/openapi/lib/model/user_admin_delete_dto.dart b/mobile/openapi/lib/model/user_admin_delete_dto.dart
index 7778b15775..2cf68ad7b2 100644
Binary files a/mobile/openapi/lib/model/user_admin_delete_dto.dart and b/mobile/openapi/lib/model/user_admin_delete_dto.dart differ
diff --git a/mobile/openapi/lib/model/user_admin_response_dto.dart b/mobile/openapi/lib/model/user_admin_response_dto.dart
index af1ad3ad1c..e5ae8e1d4e 100644
Binary files a/mobile/openapi/lib/model/user_admin_response_dto.dart and b/mobile/openapi/lib/model/user_admin_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/user_admin_update_dto.dart b/mobile/openapi/lib/model/user_admin_update_dto.dart
index dd0db767fe..6c6f73ae8e 100644
Binary files a/mobile/openapi/lib/model/user_admin_update_dto.dart and b/mobile/openapi/lib/model/user_admin_update_dto.dart differ
diff --git a/mobile/openapi/lib/model/user_license.dart b/mobile/openapi/lib/model/user_license.dart
index c7abb085f2..9bed8d5c43 100644
Binary files a/mobile/openapi/lib/model/user_license.dart and b/mobile/openapi/lib/model/user_license.dart differ
diff --git a/mobile/openapi/lib/model/user_preferences_response_dto.dart b/mobile/openapi/lib/model/user_preferences_response_dto.dart
index d3927df8d7..23d9ea84ec 100644
Binary files a/mobile/openapi/lib/model/user_preferences_response_dto.dart and b/mobile/openapi/lib/model/user_preferences_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/user_preferences_update_dto.dart b/mobile/openapi/lib/model/user_preferences_update_dto.dart
index 2841c2f572..208dbf6860 100644
Binary files a/mobile/openapi/lib/model/user_preferences_update_dto.dart and b/mobile/openapi/lib/model/user_preferences_update_dto.dart differ
diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart
index 41c1899848..a02da29948 100644
Binary files a/mobile/openapi/lib/model/user_response_dto.dart and b/mobile/openapi/lib/model/user_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/user_update_me_dto.dart b/mobile/openapi/lib/model/user_update_me_dto.dart
index 2d665fc784..8f3f4df37a 100644
Binary files a/mobile/openapi/lib/model/user_update_me_dto.dart and b/mobile/openapi/lib/model/user_update_me_dto.dart differ
diff --git a/mobile/openapi/lib/model/validate_access_token_response_dto.dart b/mobile/openapi/lib/model/validate_access_token_response_dto.dart
index e970f7e840..5e36efcfed 100644
Binary files a/mobile/openapi/lib/model/validate_access_token_response_dto.dart and b/mobile/openapi/lib/model/validate_access_token_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/validate_library_dto.dart b/mobile/openapi/lib/model/validate_library_dto.dart
index 05e122b1a1..79ddb9a540 100644
Binary files a/mobile/openapi/lib/model/validate_library_dto.dart and b/mobile/openapi/lib/model/validate_library_dto.dart differ
diff --git a/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart b/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart
index 23aac0b742..11fbbd74c2 100644
Binary files a/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart and b/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/validate_library_response_dto.dart b/mobile/openapi/lib/model/validate_library_response_dto.dart
index b213f9ba98..e0dc2a2d14 100644
Binary files a/mobile/openapi/lib/model/validate_library_response_dto.dart and b/mobile/openapi/lib/model/validate_library_response_dto.dart differ
diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock
index c9493f6490..34eb217828 100644
--- a/mobile/pubspec.lock
+++ b/mobile/pubspec.lock
@@ -66,10 +66,10 @@ packages:
     dependency: "direct main"
     description:
       name: auto_route
-      sha256: bb673104dbdc22667d01ec668df3d2a358b6e3da481428eeb1151933cfc1a7d6
+      sha256: b83e8ce46da7228cdd019b5a11205454847f0a971bca59a7529b98df9876889b
       url: "https://pub.dev"
     source: hosted
-    version: "9.2.0"
+    version: "9.2.2"
   auto_route_generator:
     dependency: "direct dev"
     description:
@@ -78,6 +78,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "9.0.0"
+  background_downloader:
+    dependency: "direct main"
+    description:
+      name: background_downloader
+      sha256: "6a945db1a1c7727a4bc9c1d7c882cfb1a819f873b77e01d5e5dd6a3fb231cb28"
+      url: "https://pub.dev"
+    source: hosted
+    version: "8.5.5"
   boolean_selector:
     dependency: transitive
     description:
@@ -206,14 +214,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.0.3"
-  chewie:
-    dependency: "direct main"
-    description:
-      name: chewie
-      sha256: "2243e41e79e865d426d9dd9c1a9624aa33c4ad11de2d0cd680f826e2cd30e879"
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.8.3"
   ci:
     dependency: transitive
     description:
@@ -258,18 +258,18 @@ packages:
     dependency: "direct main"
     description:
       name: connectivity_plus
-      sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0"
+      sha256: "2056db5241f96cdc0126bd94459fc4cdc13876753768fc7a31c425e50a7177d0"
       url: "https://pub.dev"
     source: hosted
-    version: "5.0.2"
+    version: "6.0.5"
   connectivity_plus_platform_interface:
     dependency: transitive
     description:
       name: connectivity_plus_platform_interface
-      sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a
+      sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204"
       url: "https://pub.dev"
     source: hosted
-    version: "1.2.4"
+    version: "2.0.1"
   convert:
     dependency: transitive
     description:
@@ -310,14 +310,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.0.0"
-  cupertino_icons:
-    dependency: transitive
-    description:
-      name: cupertino_icons
-      sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.0.8"
   custom_lint:
     dependency: "direct dev"
     description:
@@ -370,10 +362,10 @@ packages:
     dependency: "direct main"
     description:
       name: device_info_plus
-      sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110"
+      sha256: f545ffbadee826f26f2e1a0f0cbd667ae9a6011cc0f77c0f8f00a969655e6e95
       url: "https://pub.dev"
     source: hosted
-    version: "9.1.2"
+    version: "11.1.1"
   device_info_plus_platform_interface:
     dependency: transitive
     description:
@@ -442,10 +434,10 @@ packages:
     dependency: "direct main"
     description:
       name: file_picker
-      sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3"
+      sha256: aac85f20436608e01a6ffd1fdd4e746a7f33c93a2c83752e626bdfaea139b877
       url: "https://pub.dev"
     source: hosted
-    version: "8.0.7"
+    version: "8.1.3"
   file_selector_linux:
     dependency: transitive
     description:
@@ -524,18 +516,18 @@ packages:
     dependency: "direct dev"
     description:
       name: flutter_launcher_icons
-      sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
+      sha256: "619817c4b65b322b5104b6bb6dfe6cda62d9729bd7ad4303ecc8b4e690a67a77"
       url: "https://pub.dev"
     source: hosted
-    version: "0.13.1"
+    version: "0.14.1"
   flutter_lints:
     dependency: "direct dev"
     description:
       name: flutter_lints
-      sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
+      sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
       url: "https://pub.dev"
     source: hosted
-    version: "4.0.0"
+    version: "5.0.0"
   flutter_local_notifications:
     dependency: "direct main"
     description:
@@ -614,10 +606,10 @@ packages:
     dependency: "direct main"
     description:
       name: flutter_web_auth
-      sha256: a69fa8f43b9e4d86ac72176bf747b735e7b977dd7cf215076d95b87cb05affdd
+      sha256: "95e4856e24fb6ac1678f5ff334743b63f782d839ab324543d29ccbd295176209"
       url: "https://pub.dev"
     source: hosted
-    version: "0.5.0"
+    version: "0.6.0"
   flutter_web_plugins:
     dependency: transitive
     description: flutter
@@ -627,10 +619,10 @@ packages:
     dependency: "direct main"
     description:
       name: fluttertoast
-      sha256: "7eae679e596a44fdf761853a706f74979f8dd3cd92cf4e23cae161fda091b847"
+      sha256: "95f349437aeebe524ef7d6c9bde3e6b4772717cf46a0eb6a3ceaddc740b297cc"
       url: "https://pub.dev"
     source: hosted
-    version: "8.2.6"
+    version: "8.2.8"
   freezed_annotation:
     dependency: transitive
     description:
@@ -744,10 +736,10 @@ packages:
     dependency: "direct main"
     description:
       name: http
-      sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
+      sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
       url: "https://pub.dev"
     source: hosted
-    version: "0.13.6"
+    version: "1.2.2"
   http_multi_server:
     dependency: transitive
     description:
@@ -836,6 +828,13 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.2.1+1"
+  immich_mobile_immich_lint:
+    dependency: "direct dev"
+    description:
+      path: immich_lint
+      relative: true
+    source: path
+    version: "0.0.0"
   integration_test:
     dependency: "direct dev"
     description: flutter
@@ -861,26 +860,26 @@ packages:
     dependency: "direct main"
     description:
       name: isar
-      sha256: "99165dadb2cf2329d3140198363a7e7bff9bbd441871898a87e26914d25cf1ea"
-      url: "https://pub.dev"
+      sha256: e17a9555bc7f22ff26568b8c64d019b4ffa2dc6bd4cb1c8d9b269aefd32e53ad
+      url: "https://pub.isar-community.dev"
     source: hosted
-    version: "3.1.0+1"
+    version: "3.1.8"
   isar_flutter_libs:
     dependency: "direct main"
     description:
       name: isar_flutter_libs
-      sha256: bc6768cc4b9c61aabff77152e7f33b4b17d2fc93134f7af1c3dd51500fe8d5e8
-      url: "https://pub.dev"
+      sha256: "78710781e658ce4bff59b3f38c5b2735e899e627f4e926e1221934e77b95231a"
+      url: "https://pub.isar-community.dev"
     source: hosted
-    version: "3.1.0+1"
+    version: "3.1.8"
   isar_generator:
     dependency: "direct dev"
     description:
       name: isar_generator
-      sha256: "76c121e1295a30423604f2f819bc255bc79f852f3bc8743a24017df6068ad133"
-      url: "https://pub.dev"
+      sha256: "484e73d3b7e81dbd816852fe0b9497333118a9aeb646fd2d349a62cc8980ffe1"
+      url: "https://pub.isar-community.dev"
     source: hosted
-    version: "3.1.0+1"
+    version: "3.1.8"
   js:
     dependency: transitive
     description:
@@ -925,10 +924,10 @@ packages:
     dependency: transitive
     description:
       name: lints
-      sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
+      sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
       url: "https://pub.dev"
     source: hosted
-    version: "4.0.0"
+    version: "5.0.0"
   logging:
     dependency: "direct main"
     description:
@@ -1009,14 +1008,31 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.0.4"
-  nested:
-    dependency: transitive
+  native_video_player:
+    dependency: "direct main"
     description:
-      name: nested
-      sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
+      path: "."
+      ref: ac78487
+      resolved-ref: ac78487b9a87c9e72cd15b428270a905ac551f29
+      url: "https://github.com/immich-app/native_video_player"
+    source: git
+    version: "1.3.1"
+  network_info_plus:
+    dependency: "direct main"
+    description:
+      name: network_info_plus
+      sha256: bf9e39e523e9951d741868dc33ac386b0bc24301e9b7c8a7d60dbc34879150a8
       url: "https://pub.dev"
     source: hosted
-    version: "1.0.0"
+    version: "6.1.1"
+  network_info_plus_platform_interface:
+    dependency: transitive
+    description:
+      name: network_info_plus_platform_interface
+      sha256: b7f35f4a7baef511159e524499f3c15464a49faa5ec10e92ee0bce265e664906
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.1"
   nm:
     dependency: transitive
     description:
@@ -1052,10 +1068,10 @@ packages:
     dependency: "direct main"
     description:
       name: package_info_plus
-      sha256: "4de6c36df77ffbcef0a5aefe04669d33f2d18397fea228277b852a2d4e58e860"
+      sha256: da8d9ac8c4b1df253d1a328b7bf01ae77ef132833479ab40763334db13b91cce
       url: "https://pub.dev"
     source: hosted
-    version: "8.0.1"
+    version: "8.1.1"
   package_info_plus_platform_interface:
     dependency: transitive
     description:
@@ -1164,10 +1180,10 @@ packages:
     dependency: transitive
     description:
       name: permission_handler_html
-      sha256: "6cac773d389e045a8d4f85418d07ad58ef9e42a56e063629ce14c4c26344de24"
+      sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851
       url: "https://pub.dev"
     source: hosted
-    version: "0.1.2"
+    version: "0.1.3+2"
   permission_handler_platform_interface:
     dependency: transitive
     description:
@@ -1196,18 +1212,18 @@ packages:
     dependency: "direct main"
     description:
       name: photo_manager
-      sha256: "1e8bbe46a6858870e34c976aafd85378bed221ce31c1201961eba9ad3d94df9f"
+      sha256: f5ef2618870e9a50d8bfeb81a02c242d580ae8614bd5ea9e1b80dbb7e49d4260
       url: "https://pub.dev"
     source: hosted
-    version: "3.2.3"
+    version: "3.6.1"
   photo_manager_image_provider:
     dependency: "direct main"
     description:
       name: photo_manager_image_provider
-      sha256: "38ef1023dc11de3a8669f16e7c981673b3c5cfee715d17120f4b87daa2cdd0af"
+      sha256: b6015b67b32f345f57cf32c126f871bced2501236c405aafaefa885f7c821e4f
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.1"
+    version: "2.2.0"
   platform:
     dependency: transitive
     description:
@@ -1240,14 +1256,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "5.0.2"
-  provider:
-    dependency: transitive
-    description:
-      name: provider
-      sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
-      url: "https://pub.dev"
-    source: hosted
-    version: "6.1.2"
   pub_semver:
     dependency: transitive
     description:
@@ -1324,18 +1332,18 @@ packages:
     dependency: "direct main"
     description:
       name: share_plus
-      sha256: "59dfd53f497340a0c3a81909b220cfdb9b8973a91055c4e5ab9b9b9ad7c513c0"
+      sha256: "9c9bafd4060728d7cdb2464c341743adbd79d327cb067ec7afb64583540b47c8"
       url: "https://pub.dev"
     source: hosted
-    version: "10.0.0"
+    version: "10.1.2"
   share_plus_platform_interface:
     dependency: transitive
     description:
       name: share_plus_platform_interface
-      sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5"
+      sha256: c57c0bbfec7142e3a0f55633be504b796af72e60e3c791b44d5a017b985f7a48
       url: "https://pub.dev"
     source: hosted
-    version: "5.0.0"
+    version: "5.0.1"
   shared_preferences:
     dependency: transitive
     description:
@@ -1693,46 +1701,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.4"
-  video_player:
-    dependency: "direct main"
-    description:
-      name: video_player
-      sha256: e30df0d226c4ef82e2c150ebf6834b3522cf3f654d8e2f9419d376cdc071425d
-      url: "https://pub.dev"
-    source: hosted
-    version: "2.9.1"
-  video_player_android:
-    dependency: transitive
-    description:
-      name: video_player_android
-      sha256: "4de50df9ee786f5891d3281e1e633d7b142ef1acf47392592eb91cba5d355849"
-      url: "https://pub.dev"
-    source: hosted
-    version: "2.6.0"
-  video_player_avfoundation:
-    dependency: transitive
-    description:
-      name: video_player_avfoundation
-      sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c
-      url: "https://pub.dev"
-    source: hosted
-    version: "2.6.1"
-  video_player_platform_interface:
-    dependency: transitive
-    description:
-      name: video_player_platform_interface
-      sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6"
-      url: "https://pub.dev"
-    source: hosted
-    version: "6.2.2"
-  video_player_web:
-    dependency: transitive
-    description:
-      name: video_player_web
-      sha256: "6dcdd298136523eaf7dfc31abaf0dfba9aa8a8dbc96670e87e9d42b6f2caf774"
-      url: "https://pub.dev"
-    source: hosted
-    version: "2.3.2"
   vm_service:
     dependency: transitive
     description:
@@ -1745,10 +1713,10 @@ packages:
     dependency: "direct main"
     description:
       name: wakelock_plus
-      sha256: "4fa83a128b4127619e385f686b4f080a5d2de46cff8e8c94eccac5fcf76550e5"
+      sha256: bf4ee6f17a2fa373ed3753ad0e602b7603f8c75af006d5b9bdade263928c0484
       url: "https://pub.dev"
     source: hosted
-    version: "1.2.7"
+    version: "1.2.8"
   wakelock_plus_platform_interface:
     dependency: transitive
     description:
@@ -1769,10 +1737,10 @@ packages:
     dependency: transitive
     description:
       name: web
-      sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
+      sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
       url: "https://pub.dev"
     source: hosted
-    version: "0.5.1"
+    version: "1.1.0"
   web_socket:
     dependency: transitive
     description:
@@ -1846,5 +1814,5 @@ packages:
     source: hosted
     version: "3.1.2"
 sdks:
-  dart: ">=3.4.0 <4.0.0"
-  flutter: ">=3.24.0"
+  dart: ">=3.5.3 <4.0.0"
+  flutter: ">=3.24.5"
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index 0061f563d2..beabbd89b6 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -2,19 +2,21 @@ name: immich_mobile
 description: Immich - selfhosted backup media file on mobile phone
 
 publish_to: 'none'
-version: 1.115.0+159
+version: 1.123.0+172
 
 environment:
   sdk: '>=3.3.0 <4.0.0'
-  flutter: 3.24.0
+  flutter: 3.24.5
+
+isar_version: &isar_version 3.1.8 # define the version to be used
 
 dependencies:
   flutter:
     sdk: flutter
 
   path_provider_ios:
-  photo_manager: ^3.2.3
-  photo_manager_image_provider: ^2.1.1
+  photo_manager: ^3.6.1
+  photo_manager_image_provider: ^2.2.0
   flutter_hooks: ^0.20.4
   hooks_riverpod: ^2.4.9
   riverpod_annotation: ^2.3.3
@@ -23,8 +25,6 @@ dependencies:
   intl: ^0.19.0
   auto_route: ^9.2.0
   fluttertoast: ^8.2.4
-  video_player: ^2.8.2
-  chewie: ^1.7.4
   socket_io_client: ^2.0.3+1
   maplibre_gl: 0.19.0+2
   geolocator: ^11.0.0 # used to move to current location in map view
@@ -32,7 +32,7 @@ dependencies:
   flutter_svg: ^2.0.9
   package_info_plus: ^8.0.1
   url_launcher: ^6.2.4
-  http: ^0.13.6
+  http: ^1.1.0
   cancellation_token_http: ^2.0.0
   easy_localization: ^3.0.3
   share_plus: ^10.0.0
@@ -42,13 +42,17 @@ dependencies:
   path_provider: ^2.1.2
   collection: ^1.18.0
   http_parser: ^4.0.2
-  flutter_web_auth: ^0.5.0
+  flutter_web_auth: 0.6.0
   easy_image_viewer: ^1.4.0
-  isar: ^3.1.0+1
-  isar_flutter_libs: ^3.1.0+1
+  isar:
+    version: *isar_version
+    hosted: https://pub.isar-community.dev/
+  isar_flutter_libs: # contains Isar Core
+    version: *isar_version
+    hosted: https://pub.isar-community.dev/
   permission_handler: ^11.2.0
-  device_info_plus: ^9.1.1
-  connectivity_plus: ^5.0.2
+  device_info_plus: ^11.0.0
+  connectivity_plus: ^6.0.0
   wakelock_plus: ^1.1.4
   flutter_local_notifications: ^17.2.1+2
   timezone: ^0.9.2
@@ -56,6 +60,12 @@ dependencies:
   thumbhash: 0.1.0+1
   async: ^2.11.0
   dynamic_color: ^1.7.0 #package to apply system theme
+  background_downloader: ^8.5.5
+  network_info_plus: ^6.1.1
+  native_video_player:
+    git:
+      url: https://github.com/immich-app/native_video_player
+      ref: ac78487
 
   #image editing packages
   crop_image: ^1.0.13
@@ -86,18 +96,22 @@ dependency_overrides:
 dev_dependencies:
   flutter_test:
     sdk: flutter
-  flutter_lints: ^4.0.0
+  flutter_lints: ^5.0.0
   build_runner: ^2.4.8
   auto_route_generator: ^9.0.0
-  flutter_launcher_icons: ^0.13.1
+  flutter_launcher_icons: ^0.14.0
   flutter_native_splash: ^2.3.9
-  isar_generator: ^3.1.0+1
+  isar_generator:
+    version: *isar_version
+    hosted: https://pub.isar-community.dev/
   integration_test:
     sdk: flutter
-  custom_lint: ^0.6.0
+  custom_lint: ^0.6.4
   riverpod_lint: ^2.3.7
   riverpod_generator: ^2.3.9
   mocktail: ^1.0.3
+  immich_mobile_immich_lint:
+    path: './immich_lint'
 
 flutter:
   uses-material-design: true
diff --git a/mobile/scripts/check_i18n_keys.py b/mobile/scripts/check_i18n_keys.py
index 8d748ceb06..c3b53dc5a6 100644
--- a/mobile/scripts/check_i18n_keys.py
+++ b/mobile/scripts/check_i18n_keys.py
@@ -1,18 +1,24 @@
 #!/usr/bin/env python3
 import json
 import subprocess
-
 def main():
-    with open('assets/i18n/en-US.json', 'r') as f:
+    with open('assets/i18n/en-US.json', 'r+') as f:
         data = json.load(f)
+        keys_to_delete = []
 
         for k in data.keys():
-            print(k)
-            sp = subprocess.run(['sh', '-c', f'grep -r --include="*.dart" "{k}"'])
+            sp = subprocess.run(['sh', '-c', f'grep -q -r --include="*.dart" "{k}"'])
 
             if sp.returncode != 0:
-                print("Not found in source code!")
-                return 1
+                print("Not found in source code, key:", k)
+                keys_to_delete.append(k)
+
+        for k in keys_to_delete:
+            del data[k]
+
+        f.seek(0)
+        f.truncate()
+        json.dump(data, f, indent=4)
 
 if __name__ == '__main__':
     main()
\ No newline at end of file
diff --git a/mobile/scripts/check_key_uniform.py b/mobile/scripts/check_key_uniform.py
deleted file mode 100644
index 970f491f36..0000000000
--- a/mobile/scripts/check_key_uniform.py
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/usr/bin/env python3
-import json
-import subprocess
-
-def main():
-    print("CHECK GERMAN TRANSLATIONS")
-    with open('assets/i18n/de-DE.json', 'r') as f:
-        data = json.load(f)
-
-        for k in data.keys():
-            print(k)
-            sp = subprocess.run(['sh', '-c', f'grep -r --include="./assets/i18n/en-US.json" "{k}"'])
-
-            if sp.returncode != 0:
-                print(f"Outdated Key! {k}")
-                return 1
-
-    print("CHECK FRENCH TRANSLATIONS")
-    with open('assets/i18n/fr-FR.json', 'r') as f:
-        data = json.load(f)
-
-        for k in data.keys():
-            print(k)
-            sp = subprocess.run(['sh', '-c', f'grep -r --include="./assets/i18n/en-US.json" "{k}"'])
-
-            if sp.returncode != 0:
-                print(f"Outdated Key! {k}")
-                return 1
-
-if __name__ == '__main__':
-    main()
\ No newline at end of file
diff --git a/mobile/scripts/fdroid_build_isar.sh b/mobile/scripts/fdroid_build_isar.sh
index 41517737c9..f42bc51d9a 100755
--- a/mobile/scripts/fdroid_build_isar.sh
+++ b/mobile/scripts/fdroid_build_isar.sh
@@ -8,11 +8,11 @@ bash tool/build_android.sh x64
 bash tool/build_android.sh armv7
 bash tool/build_android.sh arm64
 mv libisar_android_arm64.so libisar.so
-mv libisar.so ../.pub-cache/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/arm64-v8a/
+mv libisar.so ../.pub-cache/hosted/pub.isar-community.dev/isar_flutter_libs-*/android/src/main/jniLibs/arm64-v8a/
 mv libisar_android_armv7.so libisar.so
-mv libisar.so ../.pub-cache/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/armeabi-v7a/
+mv libisar.so ../.pub-cache/hosted/pub.isar-community.dev/isar_flutter_libs-*/android/src/main/jniLibs/armeabi-v7a/
 mv libisar_android_x64.so libisar.so
-mv libisar.so ../.pub-cache/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/x86_64/
+mv libisar.so ../.pub-cache/hosted/pub.isar-community.dev/isar_flutter_libs-*/android/src/main/jniLibs/x86_64/
 mv libisar_android_x86.so libisar.so
-mv libisar.so ../.pub-cache/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/x86/
+mv libisar.so ../.pub-cache/hosted/pub.isar-community.dev/isar_flutter_libs-*/android/src/main/jniLibs/x86/
 )
\ No newline at end of file
diff --git a/mobile/test/fixtures/album.stub.dart b/mobile/test/fixtures/album.stub.dart
index 4fa0dac1d2..e820f193d5 100644
--- a/mobile/test/fixtures/album.stub.dart
+++ b/mobile/test/fixtures/album.stub.dart
@@ -54,4 +54,49 @@ final class AlbumStub {
     ..assets.addAll([AssetStub.image1, AssetStub.image2])
     ..activityEnabled = true
     ..owner.value = UserStub.admin;
+
+  static final create2020end2020Album = Album(
+    name: "create2020update2020Album",
+    localId: "create2020update2020Album-local",
+    remoteId: "create2020update2020Album-remote",
+    createdAt: DateTime(2020),
+    modifiedAt: DateTime(2020),
+    shared: false,
+    activityEnabled: false,
+    startDate: DateTime(2020),
+    endDate: DateTime(2020),
+  );
+  static final create2020end2022Album = Album(
+    name: "create2020update2021Album",
+    localId: "create2020update2021Album-local",
+    remoteId: "create2020update2021Album-remote",
+    createdAt: DateTime(2020),
+    modifiedAt: DateTime(2022),
+    shared: false,
+    activityEnabled: false,
+    startDate: DateTime(2020),
+    endDate: DateTime(2022),
+  );
+  static final create2020end2024Album = Album(
+    name: "create2020update2022Album",
+    localId: "create2020update2022Album-local",
+    remoteId: "create2020update2022Album-remote",
+    createdAt: DateTime(2020),
+    modifiedAt: DateTime(2024),
+    shared: false,
+    activityEnabled: false,
+    startDate: DateTime(2020),
+    endDate: DateTime(2024),
+  );
+  static final create2020end2026Album = Album(
+    name: "create2020update2023Album",
+    localId: "create2020update2023Album-local",
+    remoteId: "create2020update2023Album-remote",
+    createdAt: DateTime(2020),
+    modifiedAt: DateTime(2026),
+    shared: false,
+    activityEnabled: false,
+    startDate: DateTime(2020),
+    endDate: DateTime(2026),
+  );
 }
diff --git a/mobile/test/modules/activity/activity_statistics_provider_test.dart b/mobile/test/modules/activity/activity_statistics_provider_test.dart
index 9edabcc0d0..0216528ddd 100644
--- a/mobile/test/modules/activity/activity_statistics_provider_test.dart
+++ b/mobile/test/modules/activity/activity_statistics_provider_test.dart
@@ -1,5 +1,6 @@
 import 'package:flutter_test/flutter_test.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/models/activities/activity.model.dart';
 import 'package:immich_mobile/providers/activity_service.provider.dart';
 import 'package:immich_mobile/providers/activity_statistics.provider.dart';
 import 'package:mocktail/mocktail.dart';
@@ -25,7 +26,7 @@ void main() {
   test('Returns the proper count family', () async {
     when(
       () => activityMock.getStatistics('test-album', assetId: 'test-asset'),
-    ).thenAnswer((_) async => 5);
+    ).thenAnswer((_) async => const ActivityStats(comments: 5));
 
     // Read here to make the getStatistics call
     container.read(activityStatisticsProvider('test-album', 'test-asset'));
@@ -50,7 +51,7 @@ void main() {
   test('Adds activity', () async {
     when(
       () => activityMock.getStatistics('test-album'),
-    ).thenAnswer((_) async => 10);
+    ).thenAnswer((_) async => const ActivityStats(comments: 10));
 
     final provider = activityStatisticsProvider('test-album');
     container.listen(
@@ -71,7 +72,7 @@ void main() {
   test('Removes activity', () async {
     when(
       () => activityMock.getStatistics('new-album', assetId: 'test-asset'),
-    ).thenAnswer((_) async => 10);
+    ).thenAnswer((_) async => const ActivityStats(comments: 10));
 
     final provider = activityStatisticsProvider('new-album', 'test-asset');
     container.listen(
diff --git a/mobile/test/modules/album/album_sort_by_options_provider_test.dart b/mobile/test/modules/album/album_sort_by_options_provider_test.dart
index 84a7e6e9b8..bfb61ef402 100644
--- a/mobile/test/modules/album/album_sort_by_options_provider_test.dart
+++ b/mobile/test/modules/album/album_sort_by_options_provider_test.dart
@@ -147,24 +147,40 @@ void main() {
     group("Album sort - Most Recent", () {
       const mostRecent = AlbumSortMode.mostRecent;
 
-      test("Most Recent - ASC", () {
-        final sorted = mostRecent.sortFn(albums, false);
+      test("Most Recent - DESC", () {
+        final sorted = mostRecent.sortFn(
+          [
+            AlbumStub.create2020end2020Album,
+            AlbumStub.create2020end2022Album,
+            AlbumStub.create2020end2024Album,
+            AlbumStub.create2020end2026Album,
+          ],
+          false,
+        );
         final sortedList = [
-          AlbumStub.sharedWithUser,
-          AlbumStub.twoAsset,
-          AlbumStub.oneAsset,
-          AlbumStub.emptyAlbum,
+          AlbumStub.create2020end2026Album,
+          AlbumStub.create2020end2024Album,
+          AlbumStub.create2020end2022Album,
+          AlbumStub.create2020end2020Album,
         ];
         expect(sorted, orderedEquals(sortedList));
       });
 
-      test("Most Recent - DESC", () {
-        final sorted = mostRecent.sortFn(albums, true);
+      test("Most Recent - ASC", () {
+        final sorted = mostRecent.sortFn(
+          [
+            AlbumStub.create2020end2020Album,
+            AlbumStub.create2020end2022Album,
+            AlbumStub.create2020end2024Album,
+            AlbumStub.create2020end2026Album,
+          ],
+          true,
+        );
         final sortedList = [
-          AlbumStub.emptyAlbum,
-          AlbumStub.oneAsset,
-          AlbumStub.twoAsset,
-          AlbumStub.sharedWithUser,
+          AlbumStub.create2020end2020Album,
+          AlbumStub.create2020end2022Album,
+          AlbumStub.create2020end2024Album,
+          AlbumStub.create2020end2026Album,
         ];
         expect(sorted, orderedEquals(sortedList));
       });
diff --git a/mobile/test/modules/map/map_theme_override_test.dart b/mobile/test/modules/map/map_theme_override_test.dart
index f399625ac2..bd000c8715 100644
--- a/mobile/test/modules/map/map_theme_override_test.dart
+++ b/mobile/test/modules/map/map_theme_override_test.dart
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/models/map/map_state.model.dart';
+import 'package:immich_mobile/providers/locale_provider.dart';
 import 'package:immich_mobile/providers/map/map_state.provider.dart';
 import 'package:immich_mobile/widgets/map/map_theme_override.dart';
 
@@ -24,14 +25,17 @@ void main() {
   setUp(() {
     mapState = MapState(themeMode: ThemeMode.dark);
     mapStateNotifier = MockMapStateNotifier(mapState);
-    overrides = [mapStateNotifierProvider.overrideWith(() => mapStateNotifier)];
+    overrides = [
+      mapStateNotifierProvider.overrideWith(() => mapStateNotifier),
+      localeProvider.overrideWithValue(const Locale("en")),
+    ];
   });
 
   testWidgets("Return dark theme style when theme mode is dark",
       (tester) async {
     AsyncValue<String>? mapStyle;
     await tester.pumpConsumerWidget(
-      MapThemeOveride(
+      MapThemeOverride(
         mapBuilder: (AsyncValue<String> style) {
           mapStyle = style;
           return const Text("Mock");
@@ -49,7 +53,7 @@ void main() {
   testWidgets("Return error when style is not fetched", (tester) async {
     AsyncValue<String>? mapStyle;
     await tester.pumpConsumerWidget(
-      MapThemeOveride(
+      MapThemeOverride(
         mapBuilder: (AsyncValue<String> style) {
           mapStyle = style;
           return const Text("Mock");
@@ -69,7 +73,7 @@ void main() {
       (tester) async {
     AsyncValue<String>? mapStyle;
     await tester.pumpConsumerWidget(
-      MapThemeOveride(
+      MapThemeOverride(
         mapBuilder: (AsyncValue<String> style) {
           mapStyle = style;
           return const Text("Mock");
@@ -90,7 +94,7 @@ void main() {
     testWidgets("Return dark theme style when system is dark", (tester) async {
       AsyncValue<String>? mapStyle;
       await tester.pumpConsumerWidget(
-        MapThemeOveride(
+        MapThemeOverride(
           mapBuilder: (AsyncValue<String> style) {
             mapStyle = style;
             return const Text("Mock");
@@ -114,7 +118,7 @@ void main() {
         (tester) async {
       AsyncValue<String>? mapStyle;
       await tester.pumpConsumerWidget(
-        MapThemeOveride(
+        MapThemeOverride(
           mapBuilder: (AsyncValue<String> style) {
             mapStyle = style;
             return const Text("Mock");
@@ -138,7 +142,7 @@ void main() {
         (tester) async {
       AsyncValue<String>? mapStyle;
       await tester.pumpConsumerWidget(
-        MapThemeOveride(
+        MapThemeOverride(
           mapBuilder: (AsyncValue<String> style) {
             mapStyle = style;
             return const Text("Mock");
diff --git a/mobile/test/modules/shared/shared_mocks.dart b/mobile/test/modules/shared/shared_mocks.dart
index a2aa7b2617..013232da3e 100644
--- a/mobile/test/modules/shared/shared_mocks.dart
+++ b/mobile/test/modules/shared/shared_mocks.dart
@@ -1,11 +1,8 @@
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/entities/user.entity.dart';
 import 'package:immich_mobile/providers/user.provider.dart';
-import 'package:immich_mobile/services/hash.service.dart';
 import 'package:mocktail/mocktail.dart';
 
-class MockHashService extends Mock implements HashService {}
-
 class MockCurrentUserProvider extends StateNotifier<User?>
     with Mock
     implements CurrentUserProvider {
diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart
index 07437289be..c85487c7d0 100644
--- a/mobile/test/modules/shared/sync_service_test.dart
+++ b/mobile/test/modules/shared/sync_service_test.dart
@@ -1,16 +1,21 @@
 import 'package:flutter/widgets.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/entities/etag.entity.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
 import 'package:immich_mobile/entities/user.entity.dart';
+import 'package:immich_mobile/interfaces/asset.interface.dart';
+import 'package:immich_mobile/interfaces/user.interface.dart';
 import 'package:immich_mobile/services/immich_logger.service.dart';
 import 'package:immich_mobile/services/sync.service.dart';
-import 'package:isar/isar.dart';
+import 'package:mocktail/mocktail.dart';
 
+import '../../repository.mocks.dart';
+import '../../service.mocks.dart';
 import '../../test_utils.dart';
-import 'shared_mocks.dart';
 
 void main() {
+  int assetIdCounter = 0;
   Asset makeAsset({
     required String checksum,
     String? localId,
@@ -19,6 +24,7 @@ void main() {
   }) {
     final DateTime date = DateTime(2000);
     return Asset(
+      id: assetIdCounter++,
       checksum: checksum,
       localId: localId,
       remoteId: remoteId,
@@ -36,8 +42,16 @@ void main() {
   }
 
   group('Test SyncService grouped', () {
-    late final Isar db;
     final MockHashService hs = MockHashService();
+    final MockEntityService entityService = MockEntityService();
+    final MockAlbumRepository albumRepository = MockAlbumRepository();
+    final MockAssetRepository assetRepository = MockAssetRepository();
+    final MockExifInfoRepository exifInfoRepository = MockExifInfoRepository();
+    final MockUserRepository userRepository = MockUserRepository();
+    final MockETagRepository eTagRepository = MockETagRepository();
+    final MockAlbumMediaRepository albumMediaRepository =
+        MockAlbumMediaRepository();
+    final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository();
     final owner = User(
       id: "1",
       updatedAt: DateTime.now(),
@@ -45,9 +59,10 @@ void main() {
       name: "first last",
       isAdmin: false,
     );
+    late SyncService s;
     setUpAll(() async {
       WidgetsFlutterBinding.ensureInitialized();
-      db = await TestUtils.initIsar();
+      final db = await TestUtils.initIsar();
       ImmichLogger();
       db.writeTxnSync(() => db.clearSync());
       Store.init(db);
@@ -61,19 +76,51 @@ void main() {
       makeAsset(checksum: "e", localId: "3"),
     ];
     setUp(() {
-      db.writeTxnSync(() {
-        db.assets.clearSync();
-        db.assets.putAllSync(initialAssets);
-      });
+      s = SyncService(
+        hs,
+        entityService,
+        albumMediaRepository,
+        albumApiRepository,
+        albumRepository,
+        assetRepository,
+        exifInfoRepository,
+        userRepository,
+        eTagRepository,
+      );
+      when(() => eTagRepository.get(owner.isarId))
+          .thenAnswer((_) async => ETag(id: owner.id, time: DateTime.now()));
+      when(() => eTagRepository.deleteByIds(["1"])).thenAnswer((_) async {});
+      when(() => eTagRepository.upsertAll(any())).thenAnswer((_) async {});
+      when(() => userRepository.me()).thenAnswer((_) async => owner);
+      when(() => userRepository.getAll(sortBy: UserSort.id))
+          .thenAnswer((_) async => [owner]);
+      when(() => userRepository.getAllAccessible())
+          .thenAnswer((_) async => [owner]);
+      when(
+        () => assetRepository.getAll(
+          ownerId: owner.isarId,
+          sortBy: AssetSort.checksum,
+        ),
+      ).thenAnswer((_) async => initialAssets);
+      when(() => assetRepository.getAllByOwnerIdChecksum(any(), any()))
+          .thenAnswer((_) async => [initialAssets[3], null, null]);
+      when(() => assetRepository.updateAll(any())).thenAnswer((_) async => []);
+      when(() => assetRepository.deleteById(any())).thenAnswer((_) async {});
+      when(() => exifInfoRepository.updateAll(any()))
+          .thenAnswer((_) async => []);
+      when(() => assetRepository.transaction<void>(any())).thenAnswer(
+        (call) => (call.positionalArguments.first as Function).call(),
+      );
+      when(() => assetRepository.transaction<Null>(any())).thenAnswer(
+        (call) => (call.positionalArguments.first as Function).call(),
+      );
     });
     test('test inserting existing assets', () async {
-      SyncService s = SyncService(db, hs);
       final List<Asset> remoteAssets = [
         makeAsset(checksum: "a", remoteId: "0-1"),
         makeAsset(checksum: "b", remoteId: "2-1"),
         makeAsset(checksum: "c", remoteId: "1-1"),
       ];
-      expect(db.assets.countSync(), 5);
       final bool c1 = await s.syncRemoteAssetsToDb(
         users: [owner],
         getChangedAssets: _failDiff,
@@ -81,11 +128,10 @@ void main() {
         refreshUsers: () => [owner],
       );
       expect(c1, isFalse);
-      expect(db.assets.countSync(), 5);
+      verifyNever(() => assetRepository.updateAll(any()));
     });
 
     test('test inserting new assets', () async {
-      SyncService s = SyncService(db, hs);
       final List<Asset> remoteAssets = [
         makeAsset(checksum: "a", remoteId: "0-1"),
         makeAsset(checksum: "b", remoteId: "2-1"),
@@ -94,7 +140,6 @@ void main() {
         makeAsset(checksum: "f", remoteId: "1-4"),
         makeAsset(checksum: "g", remoteId: "3-1"),
       ];
-      expect(db.assets.countSync(), 5);
       final bool c1 = await s.syncRemoteAssetsToDb(
         users: [owner],
         getChangedAssets: _failDiff,
@@ -102,11 +147,14 @@ void main() {
         refreshUsers: () => [owner],
       );
       expect(c1, isTrue);
-      expect(db.assets.countSync(), 7);
+      final updatedAsset = initialAssets[3].updatedCopy(remoteAssets[3]);
+      verify(
+        () => assetRepository
+            .updateAll([remoteAssets[4], remoteAssets[5], updatedAsset]),
+      );
     });
 
     test('test syncing duplicate assets', () async {
-      SyncService s = SyncService(db, hs);
       final List<Asset> remoteAssets = [
         makeAsset(checksum: "a", remoteId: "0-1"),
         makeAsset(checksum: "b", remoteId: "1-1"),
@@ -115,7 +163,6 @@ void main() {
         makeAsset(checksum: "i", remoteId: "2-1c"),
         makeAsset(checksum: "j", remoteId: "2-1d"),
       ];
-      expect(db.assets.countSync(), 5);
       final bool c1 = await s.syncRemoteAssetsToDb(
         users: [owner],
         getChangedAssets: _failDiff,
@@ -123,7 +170,12 @@ void main() {
         refreshUsers: () => [owner],
       );
       expect(c1, isTrue);
-      expect(db.assets.countSync(), 8);
+      when(
+        () => assetRepository.getAll(
+          ownerId: owner.isarId,
+          sortBy: AssetSort.checksum,
+        ),
+      ).thenAnswer((_) async => remoteAssets);
       final bool c2 = await s.syncRemoteAssetsToDb(
         users: [owner],
         getChangedAssets: _failDiff,
@@ -131,7 +183,13 @@ void main() {
         refreshUsers: () => [owner],
       );
       expect(c2, isFalse);
-      expect(db.assets.countSync(), 8);
+      final currentState = [...remoteAssets];
+      when(
+        () => assetRepository.getAll(
+          ownerId: owner.isarId,
+          sortBy: AssetSort.checksum,
+        ),
+      ).thenAnswer((_) async => currentState);
       remoteAssets.removeAt(4);
       final bool c3 = await s.syncRemoteAssetsToDb(
         users: [owner],
@@ -140,7 +198,6 @@ void main() {
         refreshUsers: () => [owner],
       );
       expect(c3, isTrue);
-      expect(db.assets.countSync(), 7);
       remoteAssets.add(makeAsset(checksum: "k", remoteId: "2-1e"));
       remoteAssets.add(makeAsset(checksum: "l", remoteId: "2-2"));
       final bool c4 = await s.syncRemoteAssetsToDb(
@@ -150,11 +207,21 @@ void main() {
         refreshUsers: () => [owner],
       );
       expect(c4, isTrue);
-      expect(db.assets.countSync(), 9);
     });
 
     test('test efficient sync', () async {
-      SyncService s = SyncService(db, hs);
+      when(
+        () => assetRepository.deleteAllByRemoteId(
+          [initialAssets[1].remoteId!, initialAssets[2].remoteId!],
+          state: AssetState.remote,
+        ),
+      ).thenAnswer((_) async {});
+      when(
+        () => assetRepository
+            .getAllByRemoteId(["2-1", "1-1"], state: AssetState.merged),
+      ).thenAnswer((_) async => [initialAssets[2]]);
+      when(() => assetRepository.getAllByOwnerIdChecksum(any(), any()))
+          .thenAnswer((_) async => [initialAssets[0], null, null]); //afg
       final List<Asset> toUpsert = [
         makeAsset(checksum: "a", remoteId: "0-1"), // changed
         makeAsset(checksum: "f", remoteId: "0-2"), // new
@@ -162,6 +229,8 @@ void main() {
       ];
       toUpsert[0].isFavorite = true;
       final List<String> toDelete = ["2-1", "1-1"];
+      final expected = [...toUpsert];
+      expected[0].id = initialAssets[0].id;
       final bool c = await s.syncRemoteAssetsToDb(
         users: [owner],
         getChangedAssets: (user, since) async => (toUpsert, toDelete),
@@ -169,7 +238,7 @@ void main() {
         refreshUsers: () => throw Exception(),
       );
       expect(c, isTrue);
-      expect(db.assets.countSync(), 6);
+      verify(() => assetRepository.updateAll(expected));
     });
   });
 }
diff --git a/mobile/test/repository.mocks.dart b/mobile/test/repository.mocks.dart
new file mode 100644
index 0000000000..3dda932cac
--- /dev/null
+++ b/mobile/test/repository.mocks.dart
@@ -0,0 +1,37 @@
+import 'package:immich_mobile/interfaces/album.interface.dart';
+import 'package:immich_mobile/interfaces/album_api.interface.dart';
+import 'package:immich_mobile/interfaces/album_media.interface.dart';
+import 'package:immich_mobile/interfaces/asset.interface.dart';
+import 'package:immich_mobile/interfaces/asset_media.interface.dart';
+import 'package:immich_mobile/interfaces/auth.interface.dart';
+import 'package:immich_mobile/interfaces/auth_api.interface.dart';
+import 'package:immich_mobile/interfaces/backup.interface.dart';
+import 'package:immich_mobile/interfaces/etag.interface.dart';
+import 'package:immich_mobile/interfaces/exif_info.interface.dart';
+import 'package:immich_mobile/interfaces/file_media.interface.dart';
+import 'package:immich_mobile/interfaces/user.interface.dart';
+import 'package:mocktail/mocktail.dart';
+
+class MockAlbumRepository extends Mock implements IAlbumRepository {}
+
+class MockAssetRepository extends Mock implements IAssetRepository {}
+
+class MockUserRepository extends Mock implements IUserRepository {}
+
+class MockBackupRepository extends Mock implements IBackupRepository {}
+
+class MockExifInfoRepository extends Mock implements IExifInfoRepository {}
+
+class MockETagRepository extends Mock implements IETagRepository {}
+
+class MockAlbumMediaRepository extends Mock implements IAlbumMediaRepository {}
+
+class MockAssetMediaRepository extends Mock implements IAssetMediaRepository {}
+
+class MockFileMediaRepository extends Mock implements IFileMediaRepository {}
+
+class MockAlbumApiRepository extends Mock implements IAlbumApiRepository {}
+
+class MockAuthApiRepository extends Mock implements IAuthApiRepository {}
+
+class MockAuthRepository extends Mock implements IAuthRepository {}
diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart
new file mode 100644
index 0000000000..507b4f281b
--- /dev/null
+++ b/mobile/test/service.mocks.dart
@@ -0,0 +1,19 @@
+import 'package:immich_mobile/services/api.service.dart';
+import 'package:immich_mobile/services/entity.service.dart';
+import 'package:immich_mobile/services/hash.service.dart';
+import 'package:immich_mobile/services/network.service.dart';
+import 'package:immich_mobile/services/sync.service.dart';
+import 'package:immich_mobile/services/user.service.dart';
+import 'package:mocktail/mocktail.dart';
+
+class MockApiService extends Mock implements ApiService {}
+
+class MockUserService extends Mock implements UserService {}
+
+class MockSyncService extends Mock implements SyncService {}
+
+class MockHashService extends Mock implements HashService {}
+
+class MockEntityService extends Mock implements EntityService {}
+
+class MockNetworkService extends Mock implements NetworkService {}
diff --git a/mobile/test/services/album.service_test.dart b/mobile/test/services/album.service_test.dart
new file mode 100644
index 0000000000..c0775a1c3e
--- /dev/null
+++ b/mobile/test/services/album.service_test.dart
@@ -0,0 +1,219 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:immich_mobile/entities/backup_album.entity.dart';
+import 'package:immich_mobile/services/album.service.dart';
+import 'package:mocktail/mocktail.dart';
+import '../fixtures/album.stub.dart';
+import '../fixtures/asset.stub.dart';
+import '../fixtures/user.stub.dart';
+import '../repository.mocks.dart';
+import '../service.mocks.dart';
+
+void main() {
+  late AlbumService sut;
+  late MockUserService userService;
+  late MockSyncService syncService;
+  late MockEntityService entityService;
+  late MockAlbumRepository albumRepository;
+  late MockAssetRepository assetRepository;
+  late MockBackupRepository backupRepository;
+  late MockAlbumMediaRepository albumMediaRepository;
+  late MockAlbumApiRepository albumApiRepository;
+
+  setUp(() {
+    userService = MockUserService();
+    syncService = MockSyncService();
+    entityService = MockEntityService();
+    albumRepository = MockAlbumRepository();
+    assetRepository = MockAssetRepository();
+    backupRepository = MockBackupRepository();
+    albumMediaRepository = MockAlbumMediaRepository();
+    albumApiRepository = MockAlbumApiRepository();
+
+    when(() => albumRepository.transaction<void>(any())).thenAnswer(
+      (call) => (call.positionalArguments.first as Function).call(),
+    );
+    when(() => assetRepository.transaction<Null>(any())).thenAnswer(
+      (call) => (call.positionalArguments.first as Function).call(),
+    );
+
+    sut = AlbumService(
+      userService,
+      syncService,
+      entityService,
+      albumRepository,
+      assetRepository,
+      backupRepository,
+      albumMediaRepository,
+      albumApiRepository,
+    );
+  });
+
+  group('refreshDeviceAlbums', () {
+    test('empty selection with one album in db', () async {
+      when(() => backupRepository.getIdsBySelection(BackupSelection.exclude))
+          .thenAnswer((_) async => []);
+      when(() => backupRepository.getIdsBySelection(BackupSelection.select))
+          .thenAnswer((_) async => []);
+      when(() => albumMediaRepository.getAll()).thenAnswer((_) async => []);
+      when(() => albumRepository.count(local: true)).thenAnswer((_) async => 1);
+      when(() => syncService.removeAllLocalAlbumsAndAssets())
+          .thenAnswer((_) async => true);
+      final result = await sut.refreshDeviceAlbums();
+      expect(result, false);
+      verify(() => syncService.removeAllLocalAlbumsAndAssets());
+    });
+
+    test('one selected albums, two on device', () async {
+      when(() => backupRepository.getIdsBySelection(BackupSelection.exclude))
+          .thenAnswer((_) async => []);
+      when(() => backupRepository.getIdsBySelection(BackupSelection.select))
+          .thenAnswer((_) async => [AlbumStub.oneAsset.localId!]);
+      when(() => albumMediaRepository.getAll())
+          .thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]);
+      when(() => syncService.syncLocalAlbumAssetsToDb(any(), any()))
+          .thenAnswer((_) async => true);
+      final result = await sut.refreshDeviceAlbums();
+      expect(result, true);
+      verify(
+        () => syncService.syncLocalAlbumAssetsToDb([AlbumStub.oneAsset], null),
+      ).called(1);
+      verifyNoMoreInteractions(syncService);
+    });
+  });
+
+  group('refreshRemoteAlbums', () {
+    test('is working', () async {
+      when(() => userService.refreshUsers()).thenAnswer((_) async => true);
+      when(() => albumApiRepository.getAll(shared: true))
+          .thenAnswer((_) async => [AlbumStub.sharedWithUser]);
+
+      when(() => albumApiRepository.getAll(shared: null))
+          .thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]);
+
+      when(
+        () => syncService.syncRemoteAlbumsToDb([
+          AlbumStub.twoAsset,
+          AlbumStub.oneAsset,
+          AlbumStub.sharedWithUser,
+        ]),
+      ).thenAnswer((_) async => true);
+      final result = await sut.refreshRemoteAlbums();
+      expect(result, true);
+      verify(() => userService.refreshUsers()).called(1);
+      verify(() => albumApiRepository.getAll(shared: true)).called(1);
+      verify(() => albumApiRepository.getAll(shared: null)).called(1);
+      verify(
+        () => syncService.syncRemoteAlbumsToDb(
+          [
+            AlbumStub.twoAsset,
+            AlbumStub.oneAsset,
+            AlbumStub.sharedWithUser,
+          ],
+        ),
+      ).called(1);
+      verifyNoMoreInteractions(userService);
+      verifyNoMoreInteractions(albumApiRepository);
+      verifyNoMoreInteractions(syncService);
+    });
+  });
+
+  group('createAlbum', () {
+    test('shared with assets', () async {
+      when(
+        () => albumApiRepository.create(
+          "name",
+          assetIds: any(named: "assetIds"),
+          sharedUserIds: any(named: "sharedUserIds"),
+        ),
+      ).thenAnswer((_) async => AlbumStub.oneAsset);
+
+      when(
+        () => entityService.fillAlbumWithDatabaseEntities(AlbumStub.oneAsset),
+      ).thenAnswer((_) async => AlbumStub.oneAsset);
+
+      when(
+        () => albumRepository.create(AlbumStub.oneAsset),
+      ).thenAnswer((_) async => AlbumStub.twoAsset);
+
+      final result =
+          await sut.createAlbum("name", [AssetStub.image1], [UserStub.user1]);
+      expect(result, AlbumStub.twoAsset);
+      verify(
+        () => albumApiRepository.create(
+          "name",
+          assetIds: [AssetStub.image1.remoteId!],
+          sharedUserIds: [UserStub.user1.id],
+        ),
+      ).called(1);
+      verify(
+        () => entityService.fillAlbumWithDatabaseEntities(AlbumStub.oneAsset),
+      ).called(1);
+    });
+  });
+
+  group('addAdditionalAssetToAlbum', () {
+    test('one added, one duplicate', () async {
+      when(
+        () => albumApiRepository.addAssets(AlbumStub.oneAsset.remoteId!, any()),
+      ).thenAnswer(
+        (_) async => (
+          added: [AssetStub.image2.remoteId!],
+          duplicates: [AssetStub.image1.remoteId!]
+        ),
+      );
+      when(
+        () => albumRepository.get(AlbumStub.oneAsset.id),
+      ).thenAnswer((_) async => AlbumStub.oneAsset);
+      when(
+        () => albumRepository.addAssets(AlbumStub.oneAsset, [AssetStub.image2]),
+      ).thenAnswer((_) async {});
+      when(
+        () => albumRepository.removeAssets(AlbumStub.oneAsset, []),
+      ).thenAnswer((_) async {});
+      when(
+        () => albumRepository.recalculateMetadata(AlbumStub.oneAsset),
+      ).thenAnswer((_) async => AlbumStub.oneAsset);
+      when(
+        () => albumRepository.update(AlbumStub.oneAsset),
+      ).thenAnswer((_) async => AlbumStub.oneAsset);
+
+      final result = await sut.addAssets(
+        AlbumStub.oneAsset,
+        [AssetStub.image1, AssetStub.image2],
+      );
+
+      expect(result != null, true);
+      expect(result!.alreadyInAlbum, [AssetStub.image1.remoteId!]);
+      expect(result.successfullyAdded, 1);
+    });
+  });
+
+  group('addAdditionalUserToAlbum', () {
+    test('one added', () async {
+      when(
+        () =>
+            albumApiRepository.addUsers(AlbumStub.emptyAlbum.remoteId!, any()),
+      ).thenAnswer(
+        (_) async => AlbumStub.sharedWithUser,
+      );
+
+      when(
+        () => albumRepository.addUsers(
+          AlbumStub.emptyAlbum,
+          AlbumStub.emptyAlbum.sharedUsers.toList(),
+        ),
+      ).thenAnswer((_) async => AlbumStub.emptyAlbum);
+
+      when(
+        () => albumRepository.update(AlbumStub.emptyAlbum),
+      ).thenAnswer((_) async => AlbumStub.emptyAlbum);
+
+      final result = await sut.addUsers(
+        AlbumStub.emptyAlbum,
+        [UserStub.user2.id],
+      );
+
+      expect(result, true);
+    });
+  });
+}
diff --git a/mobile/test/services/auth.service_test.dart b/mobile/test/services/auth.service_test.dart
new file mode 100644
index 0000000000..edbf6495e3
--- /dev/null
+++ b/mobile/test/services/auth.service_test.dart
@@ -0,0 +1,308 @@
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
+import 'package:immich_mobile/services/auth.service.dart';
+import 'package:mocktail/mocktail.dart';
+import 'package:openapi/api.dart';
+import '../repository.mocks.dart';
+import '../service.mocks.dart';
+import '../test_utils.dart';
+
+void main() {
+  late AuthService sut;
+  late MockAuthApiRepository authApiRepository;
+  late MockAuthRepository authRepository;
+  late MockApiService apiService;
+  late MockNetworkService networkService;
+
+  setUp(() async {
+    authApiRepository = MockAuthApiRepository();
+    authRepository = MockAuthRepository();
+    apiService = MockApiService();
+    networkService = MockNetworkService();
+
+    sut = AuthService(
+      authApiRepository,
+      authRepository,
+      apiService,
+      networkService,
+    );
+
+    registerFallbackValue(Uri());
+  });
+
+  group('validateServerUrl', () {
+    setUpAll(() async {
+      WidgetsFlutterBinding.ensureInitialized();
+      final db = await TestUtils.initIsar();
+      db.writeTxnSync(() => db.clearSync());
+      Store.init(db);
+    });
+
+    test('Should resolve HTTP endpoint', () async {
+      const testUrl = 'http://ip:2283';
+      const resolvedUrl = 'http://ip:2283/api';
+
+      when(() => apiService.resolveAndSetEndpoint(testUrl))
+          .thenAnswer((_) async => resolvedUrl);
+      when(() => apiService.setDeviceInfoHeader()).thenAnswer((_) async => {});
+
+      final result = await sut.validateServerUrl(testUrl);
+
+      expect(result, resolvedUrl);
+
+      verify(() => apiService.resolveAndSetEndpoint(testUrl)).called(1);
+      verify(() => apiService.setDeviceInfoHeader()).called(1);
+    });
+
+    test('Should resolve HTTPS endpoint', () async {
+      const testUrl = 'https://immich.domain.com';
+      const resolvedUrl = 'https://immich.domain.com/api';
+
+      when(() => apiService.resolveAndSetEndpoint(testUrl))
+          .thenAnswer((_) async => resolvedUrl);
+      when(() => apiService.setDeviceInfoHeader()).thenAnswer((_) async => {});
+
+      final result = await sut.validateServerUrl(testUrl);
+
+      expect(result, resolvedUrl);
+
+      verify(() => apiService.resolveAndSetEndpoint(testUrl)).called(1);
+      verify(() => apiService.setDeviceInfoHeader()).called(1);
+    });
+
+    test('Should throw error on invalid URL', () async {
+      const testUrl = 'invalid-url';
+
+      when(() => apiService.resolveAndSetEndpoint(testUrl))
+          .thenThrow(Exception('Invalid URL'));
+
+      expect(
+        () async => await sut.validateServerUrl(testUrl),
+        throwsA(isA<Exception>()),
+      );
+
+      verify(() => apiService.resolveAndSetEndpoint(testUrl)).called(1);
+      verifyNever(() => apiService.setDeviceInfoHeader());
+    });
+
+    test('Should throw error on unreachable server', () async {
+      const testUrl = 'https://unreachable.server';
+
+      when(() => apiService.resolveAndSetEndpoint(testUrl))
+          .thenThrow(Exception('Server is not reachable'));
+
+      expect(
+        () async => await sut.validateServerUrl(testUrl),
+        throwsA(isA<Exception>()),
+      );
+
+      verify(() => apiService.resolveAndSetEndpoint(testUrl)).called(1);
+      verifyNever(() => apiService.setDeviceInfoHeader());
+    });
+  });
+
+  group('logout', () {
+    test('Should logout user', () async {
+      when(() => authApiRepository.logout()).thenAnswer((_) async => {});
+      when(() => authRepository.clearLocalData())
+          .thenAnswer((_) => Future.value(null));
+
+      await sut.logout();
+
+      verify(() => authApiRepository.logout()).called(1);
+      verify(() => authRepository.clearLocalData()).called(1);
+    });
+
+    test('Should clear local data even on server error', () async {
+      when(() => authApiRepository.logout())
+          .thenThrow(Exception('Server error'));
+      when(() => authRepository.clearLocalData())
+          .thenAnswer((_) => Future.value(null));
+
+      await sut.logout();
+
+      verify(() => authApiRepository.logout()).called(1);
+      verify(() => authRepository.clearLocalData()).called(1);
+    });
+  });
+
+  group('setOpenApiServiceEndpoint', () {
+    setUp(() {
+      when(() => networkService.getWifiName())
+          .thenAnswer((_) async => 'TestWifi');
+    });
+
+    test('Should return null if auto endpoint switching is disabled', () async {
+      when(() => authRepository.getEndpointSwitchingFeature())
+          .thenReturn((false));
+
+      final result = await sut.setOpenApiServiceEndpoint();
+
+      expect(result, isNull);
+      verify(() => authRepository.getEndpointSwitchingFeature()).called(1);
+      verifyNever(() => networkService.getWifiName());
+    });
+
+    test('Should set local connection if wifi name matches', () async {
+      when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true);
+      when(() => authRepository.getPreferredWifiName()).thenReturn('TestWifi');
+      when(() => authRepository.getLocalEndpoint())
+          .thenReturn('http://local.endpoint');
+      when(() => apiService.resolveAndSetEndpoint('http://local.endpoint'))
+          .thenAnswer((_) async => 'http://local.endpoint');
+
+      final result = await sut.setOpenApiServiceEndpoint();
+
+      expect(result, 'http://local.endpoint');
+      verify(() => authRepository.getEndpointSwitchingFeature()).called(1);
+      verify(() => networkService.getWifiName()).called(1);
+      verify(() => authRepository.getPreferredWifiName()).called(1);
+      verify(() => authRepository.getLocalEndpoint()).called(1);
+      verify(() => apiService.resolveAndSetEndpoint('http://local.endpoint'))
+          .called(1);
+    });
+
+    test('Should set external endpoint if wifi name not matching', () async {
+      when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true);
+      when(() => authRepository.getPreferredWifiName())
+          .thenReturn('DifferentWifi');
+      when(() => authRepository.getExternalEndpointList()).thenReturn([
+        AuxilaryEndpoint(
+          url: 'https://external.endpoint',
+          status: AuxCheckStatus.valid,
+        ),
+      ]);
+      when(
+        () => apiService.resolveAndSetEndpoint('https://external.endpoint'),
+      ).thenAnswer((_) async => 'https://external.endpoint/api');
+
+      final result = await sut.setOpenApiServiceEndpoint();
+
+      expect(result, 'https://external.endpoint/api');
+      verify(() => authRepository.getEndpointSwitchingFeature()).called(1);
+      verify(() => networkService.getWifiName()).called(1);
+      verify(() => authRepository.getPreferredWifiName()).called(1);
+      verify(() => authRepository.getExternalEndpointList()).called(1);
+      verify(
+        () => apiService.resolveAndSetEndpoint('https://external.endpoint'),
+      ).called(1);
+    });
+
+    test('Should set second external endpoint if the first throw any error',
+        () async {
+      when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true);
+      when(() => authRepository.getPreferredWifiName())
+          .thenReturn('DifferentWifi');
+      when(() => authRepository.getExternalEndpointList()).thenReturn([
+        AuxilaryEndpoint(
+          url: 'https://external.endpoint',
+          status: AuxCheckStatus.valid,
+        ),
+        AuxilaryEndpoint(
+          url: 'https://external.endpoint2',
+          status: AuxCheckStatus.valid,
+        ),
+      ]);
+
+      when(
+        () => apiService.resolveAndSetEndpoint('https://external.endpoint'),
+      ).thenThrow(Exception('Invalid endpoint'));
+      when(
+        () => apiService.resolveAndSetEndpoint('https://external.endpoint2'),
+      ).thenAnswer((_) async => 'https://external.endpoint2/api');
+
+      final result = await sut.setOpenApiServiceEndpoint();
+
+      expect(result, 'https://external.endpoint2/api');
+      verify(() => authRepository.getEndpointSwitchingFeature()).called(1);
+      verify(() => networkService.getWifiName()).called(1);
+      verify(() => authRepository.getPreferredWifiName()).called(1);
+      verify(() => authRepository.getExternalEndpointList()).called(1);
+      verify(
+        () => apiService.resolveAndSetEndpoint('https://external.endpoint2'),
+      ).called(1);
+    });
+
+    test('Should set second external endpoint if the first throw ApiException',
+        () async {
+      when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true);
+      when(() => authRepository.getPreferredWifiName())
+          .thenReturn('DifferentWifi');
+      when(() => authRepository.getExternalEndpointList()).thenReturn([
+        AuxilaryEndpoint(
+          url: 'https://external.endpoint',
+          status: AuxCheckStatus.valid,
+        ),
+        AuxilaryEndpoint(
+          url: 'https://external.endpoint2',
+          status: AuxCheckStatus.valid,
+        ),
+      ]);
+
+      when(
+        () => apiService.resolveAndSetEndpoint('https://external.endpoint'),
+      ).thenThrow(ApiException(503, 'Invalid endpoint'));
+      when(
+        () => apiService.resolveAndSetEndpoint('https://external.endpoint2'),
+      ).thenAnswer((_) async => 'https://external.endpoint2/api');
+
+      final result = await sut.setOpenApiServiceEndpoint();
+
+      expect(result, 'https://external.endpoint2/api');
+      verify(() => authRepository.getEndpointSwitchingFeature()).called(1);
+      verify(() => networkService.getWifiName()).called(1);
+      verify(() => authRepository.getPreferredWifiName()).called(1);
+      verify(() => authRepository.getExternalEndpointList()).called(1);
+      verify(
+        () => apiService.resolveAndSetEndpoint('https://external.endpoint2'),
+      ).called(1);
+    });
+
+    test('Should handle error when setting local connection', () async {
+      when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true);
+      when(() => authRepository.getPreferredWifiName()).thenReturn('TestWifi');
+      when(() => authRepository.getLocalEndpoint())
+          .thenReturn('http://local.endpoint');
+      when(() => apiService.resolveAndSetEndpoint('http://local.endpoint'))
+          .thenThrow(Exception('Local endpoint error'));
+
+      final result = await sut.setOpenApiServiceEndpoint();
+
+      expect(result, isNull);
+      verify(() => authRepository.getEndpointSwitchingFeature()).called(1);
+      verify(() => networkService.getWifiName()).called(1);
+      verify(() => authRepository.getPreferredWifiName()).called(1);
+      verify(() => authRepository.getLocalEndpoint()).called(1);
+      verify(() => apiService.resolveAndSetEndpoint('http://local.endpoint'))
+          .called(1);
+    });
+
+    test('Should handle error when setting external connection', () async {
+      when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true);
+      when(() => authRepository.getPreferredWifiName())
+          .thenReturn('DifferentWifi');
+      when(() => authRepository.getExternalEndpointList()).thenReturn([
+        AuxilaryEndpoint(
+          url: 'https://external.endpoint',
+          status: AuxCheckStatus.valid,
+        ),
+      ]);
+      when(
+        () => apiService.resolveAndSetEndpoint('https://external.endpoint'),
+      ).thenThrow(Exception('External endpoint error'));
+
+      final result = await sut.setOpenApiServiceEndpoint();
+
+      expect(result, isNull);
+      verify(() => authRepository.getEndpointSwitchingFeature()).called(1);
+      verify(() => networkService.getWifiName()).called(1);
+      verify(() => authRepository.getPreferredWifiName()).called(1);
+      verify(() => authRepository.getExternalEndpointList()).called(1);
+      verify(
+        () => apiService.resolveAndSetEndpoint('https://external.endpoint'),
+      ).called(1);
+    });
+  });
+}
diff --git a/mobile/test/services/entity.service_test.dart b/mobile/test/services/entity.service_test.dart
new file mode 100644
index 0000000000..8c8b49a7e0
--- /dev/null
+++ b/mobile/test/services/entity.service_test.dart
@@ -0,0 +1,76 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/services/entity.service.dart';
+import 'package:mocktail/mocktail.dart';
+import '../fixtures/asset.stub.dart';
+import '../fixtures/user.stub.dart';
+import '../repository.mocks.dart';
+
+void main() {
+  late EntityService sut;
+  late MockAssetRepository assetRepository;
+  late MockUserRepository userRepository;
+
+  setUp(() {
+    assetRepository = MockAssetRepository();
+    userRepository = MockUserRepository();
+    sut = EntityService(assetRepository, userRepository);
+  });
+
+  group('fillAlbumWithDatabaseEntities', () {
+    test('remote album with owner, thumbnail, sharedUsers and assets',
+        () async {
+      final Album album = Album(
+        name: "album-with-two-assets-and-two-users",
+        localId: "album-with-two-assets-and-two-users-local",
+        remoteId: "album-with-two-assets-and-two-users-remote",
+        createdAt: DateTime(2001),
+        modifiedAt: DateTime(2010),
+        shared: true,
+        activityEnabled: true,
+        startDate: DateTime(2019),
+        endDate: DateTime(2020),
+      )
+        ..remoteThumbnailAssetId = AssetStub.image1.remoteId
+        ..assets.addAll([AssetStub.image1, AssetStub.image1])
+        ..owner.value = UserStub.user1
+        ..sharedUsers.addAll([UserStub.admin, UserStub.admin]);
+
+      when(() => userRepository.get(album.ownerId!))
+          .thenAnswer((_) async => UserStub.admin);
+
+      when(() => assetRepository.getByRemoteId(AssetStub.image1.remoteId!))
+          .thenAnswer((_) async => AssetStub.image1);
+
+      when(() => userRepository.getByIds(any()))
+          .thenAnswer((_) async => [UserStub.user1, UserStub.user2]);
+
+      when(() => assetRepository.getAllByRemoteId(any()))
+          .thenAnswer((_) async => [AssetStub.image1, AssetStub.image2]);
+
+      await sut.fillAlbumWithDatabaseEntities(album);
+      expect(album.owner.value, UserStub.admin);
+      expect(album.thumbnail.value, AssetStub.image1);
+      expect(album.remoteUsers.toSet(), {UserStub.user1, UserStub.user2});
+      expect(album.remoteAssets.toSet(), {AssetStub.image1, AssetStub.image2});
+    });
+
+    test('remote album without any info', () async {
+      makeEmptyAlbum() => Album(
+            name: "album-without-info",
+            localId: "album-without-info-local",
+            remoteId: "album-without-info-remote",
+            createdAt: DateTime(2001),
+            modifiedAt: DateTime(2010),
+            shared: false,
+            activityEnabled: false,
+          );
+
+      final album = makeEmptyAlbum();
+      await sut.fillAlbumWithDatabaseEntities(album);
+      verifyNoMoreInteractions(assetRepository);
+      verifyNoMoreInteractions(userRepository);
+      expect(album, makeEmptyAlbum());
+    });
+  });
+}
diff --git a/open-api/bin/generate-open-api.sh b/open-api/bin/generate-open-api.sh
index 2ca0463046..bf8b24b557 100755
--- a/open-api/bin/generate-open-api.sh
+++ b/open-api/bin/generate-open-api.sh
@@ -9,11 +9,7 @@ function dart {
   wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache
   patch --no-backup-if-mismatch -u native_class.mustache <native_class.mustache.patch
 
-  cd ../../
-  wget -O api_client.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/api_client.mustache
-  patch --no-backup-if-mismatch -u api_client.mustache <api_client.mustache.patch
-
-  cd ../../
+  cd ../../../../
   npx --yes @openapitools/openapi-generator-cli generate -g dart -i ./immich-openapi-specs.json -o ../mobile/openapi -t ./templates/mobile
 
   # Post generate patches
@@ -40,4 +36,4 @@ elif [[ $1 == 'typescript' ]]; then
 else
   dart
   typescript
-fi
\ No newline at end of file
+fi
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index b4ec4505b9..2686d4f96d 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -1424,7 +1424,6 @@
     },
     "/assets/bulk-upload-check": {
       "post": {
-        "description": "Checks if assets exist by checksums",
         "operationId": "checkBulkUpload",
         "parameters": [],
         "requestBody": {
@@ -1460,6 +1459,7 @@
             "api_key": []
           }
         ],
+        "summary": "Checks if assets exist by checksums",
         "tags": [
           "Assets"
         ]
@@ -1467,7 +1467,6 @@
     },
     "/assets/device/{deviceId}": {
       "get": {
-        "description": "Get all asset of a device that are in the database, ID only.",
         "operationId": "getAllUserAssetsByDeviceId",
         "parameters": [
           {
@@ -1505,6 +1504,7 @@
             "api_key": []
           }
         ],
+        "summary": "Get all asset of a device that are in the database, ID only.",
         "tags": [
           "Assets"
         ]
@@ -1512,7 +1512,6 @@
     },
     "/assets/exist": {
       "post": {
-        "description": "Checks if multiple assets exist on the server and returns all existing - used by background backup",
         "operationId": "checkExistingAssets",
         "parameters": [],
         "requestBody": {
@@ -1548,6 +1547,7 @@
             "api_key": []
           }
         ],
+        "summary": "Checks if multiple assets exist on the server and returns all existing - used by background backup",
         "tags": [
           "Assets"
         ]
@@ -1646,6 +1646,8 @@
     },
     "/assets/random": {
       "get": {
+        "deprecated": true,
+        "description": "This property was deprecated in v1.116.0",
         "operationId": "getRandom",
         "parameters": [
           {
@@ -1685,8 +1687,12 @@
           }
         ],
         "tags": [
-          "Assets"
-        ]
+          "Assets",
+          "Deprecated"
+        ],
+        "x-immich-lifecycle": {
+          "deprecatedAt": "v1.116.0"
+        }
       }
     },
     "/assets/statistics": {
@@ -1897,7 +1903,6 @@
         ]
       },
       "put": {
-        "description": "Replace the asset with new file, without changing its id",
         "operationId": "replaceAsset",
         "parameters": [
           {
@@ -1951,6 +1956,7 @@
             "api_key": []
           }
         ],
+        "summary": "Replace the asset with new file, without changing its id",
         "tags": [
           "Assets"
         ],
@@ -2561,6 +2567,39 @@
         "tags": [
           "Jobs"
         ]
+      },
+      "post": {
+        "operationId": "createJob",
+        "parameters": [],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/JobCreateDto"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "201": {
+            "description": ""
+          }
+        },
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "tags": [
+          "Jobs"
+        ]
       }
     },
     "/jobs/{id}": {
@@ -2814,41 +2853,6 @@
         ]
       }
     },
-    "/libraries/{id}/removeOffline": {
-      "post": {
-        "operationId": "removeOfflineFiles",
-        "parameters": [
-          {
-            "name": "id",
-            "required": true,
-            "in": "path",
-            "schema": {
-              "format": "uuid",
-              "type": "string"
-            }
-          }
-        ],
-        "responses": {
-          "204": {
-            "description": ""
-          }
-        },
-        "security": [
-          {
-            "bearer": []
-          },
-          {
-            "cookie": []
-          },
-          {
-            "api_key": []
-          }
-        ],
-        "tags": [
-          "Libraries"
-        ]
-      }
-    },
     "/libraries/{id}/scan": {
       "post": {
         "operationId": "scanLibrary",
@@ -2863,16 +2867,6 @@
             }
           }
         ],
-        "requestBody": {
-          "content": {
-            "application/json": {
-              "schema": {
-                "$ref": "#/components/schemas/ScanLibraryDto"
-              }
-            }
-          },
-          "required": true
-        },
         "responses": {
           "204": {
             "description": ""
@@ -3128,55 +3122,6 @@
         ]
       }
     },
-    "/map/style.json": {
-      "get": {
-        "operationId": "getMapStyle",
-        "parameters": [
-          {
-            "name": "key",
-            "required": false,
-            "in": "query",
-            "schema": {
-              "type": "string"
-            }
-          },
-          {
-            "name": "theme",
-            "required": true,
-            "in": "query",
-            "schema": {
-              "$ref": "#/components/schemas/MapTheme"
-            }
-          }
-        ],
-        "responses": {
-          "200": {
-            "content": {
-              "application/json": {
-                "schema": {
-                  "type": "object"
-                }
-              }
-            },
-            "description": ""
-          }
-        },
-        "security": [
-          {
-            "bearer": []
-          },
-          {
-            "cookie": []
-          },
-          {
-            "api_key": []
-          }
-        ],
-        "tags": [
-          "Map"
-        ]
-      }
-    },
     "/memories": {
       "get": {
         "operationId": "searchMemories",
@@ -3485,6 +3430,57 @@
         ]
       }
     },
+    "/notifications/templates/{name}": {
+      "post": {
+        "operationId": "getNotificationTemplate",
+        "parameters": [
+          {
+            "name": "name",
+            "required": true,
+            "in": "path",
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/TemplateDto"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/TemplateResponseDto"
+                }
+              }
+            },
+            "description": ""
+          }
+        },
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "tags": [
+          "Notifications"
+        ]
+      }
+    },
     "/notifications/test-email": {
       "post": {
         "operationId": "sendTestEmail",
@@ -3501,6 +3497,13 @@
         },
         "responses": {
           "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/TestEmailResponseDto"
+                }
+              }
+            },
             "description": ""
           }
         },
@@ -3843,6 +3846,24 @@
       "get": {
         "operationId": "getAllPeople",
         "parameters": [
+          {
+            "name": "closestAssetId",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "format": "uuid",
+              "type": "string"
+            }
+          },
+          {
+            "name": "closestPersonId",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "format": "uuid",
+              "type": "string"
+            }
+          },
           {
             "name": "page",
             "required": false,
@@ -4078,57 +4099,6 @@
         ]
       }
     },
-    "/people/{id}/assets": {
-      "get": {
-        "deprecated": true,
-        "description": "This property was deprecated in v1.113.0",
-        "operationId": "getPersonAssets",
-        "parameters": [
-          {
-            "name": "id",
-            "required": true,
-            "in": "path",
-            "schema": {
-              "format": "uuid",
-              "type": "string"
-            }
-          }
-        ],
-        "responses": {
-          "200": {
-            "content": {
-              "application/json": {
-                "schema": {
-                  "items": {
-                    "$ref": "#/components/schemas/AssetResponseDto"
-                  },
-                  "type": "array"
-                }
-              }
-            },
-            "description": ""
-          }
-        },
-        "security": [
-          {
-            "bearer": []
-          },
-          {
-            "cookie": []
-          },
-          {
-            "api_key": []
-          }
-        ],
-        "tags": [
-          "People",
-          "Deprecated"
-        ],
-        "x-immich-lifecycle": {
-          "deprecatedAt": "v1.113.0"
-        }
-      }
-    },
     "/people/{id}/merge": {
       "post": {
         "operationId": "mergePerson",
@@ -4508,7 +4478,7 @@
     },
     "/search/metadata": {
       "post": {
-        "operationId": "searchMetadata",
+        "operationId": "searchAssets",
         "parameters": [],
         "requestBody": {
           "content": {
@@ -4644,6 +4614,51 @@
         ]
       }
     },
+    "/search/random": {
+      "post": {
+        "operationId": "searchRandom",
+        "parameters": [],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/RandomSearchDto"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "items": {
+                    "$ref": "#/components/schemas/AssetResponseDto"
+                  },
+                  "type": "array"
+                }
+              }
+            },
+            "description": ""
+          }
+        },
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "tags": [
+          "Search"
+        ]
+      }
+    },
     "/search/smart": {
       "post": {
         "operationId": "searchSmart",
@@ -5091,6 +5106,30 @@
         ]
       }
     },
+    "/server/version-history": {
+      "get": {
+        "operationId": "getVersionHistory",
+        "parameters": [],
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "items": {
+                    "$ref": "#/components/schemas/ServerVersionHistoryResponseDto"
+                  },
+                  "type": "array"
+                }
+              }
+            },
+            "description": ""
+          }
+        },
+        "tags": [
+          "Server"
+        ]
+      }
+    },
     "/sessions": {
       "delete": {
         "operationId": "deleteAllSessions",
@@ -5275,8 +5314,8 @@
             "name": "password",
             "required": false,
             "in": "query",
-            "example": "password",
             "schema": {
+              "example": "password",
               "type": "string"
             }
           },
@@ -6806,7 +6845,14 @@
         "operationId": "emptyTrash",
         "parameters": [],
         "responses": {
-          "204": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/TrashResponseDto"
+                }
+              }
+            },
             "description": ""
           }
         },
@@ -6831,7 +6877,14 @@
         "operationId": "restoreTrash",
         "parameters": [],
         "responses": {
-          "204": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/TrashResponseDto"
+                }
+              }
+            },
             "description": ""
           }
         },
@@ -6866,7 +6919,14 @@
           "required": true
         },
         "responses": {
-          "204": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/TrashResponseDto"
+                }
+              }
+            },
             "description": ""
           }
         },
@@ -7394,7 +7454,7 @@
   "info": {
     "title": "Immich",
     "description": "Immich API",
-    "version": "1.115.0",
+    "version": "1.123.0",
     "contact": {}
   },
   "tags": [],
@@ -7432,6 +7492,7 @@
             "items": {
               "$ref": "#/components/schemas/Permission"
             },
+            "minItems": 1,
             "type": "array"
           }
         },
@@ -7512,7 +7573,11 @@
             "type": "string"
           },
           "type": {
-            "$ref": "#/components/schemas/ReactionType"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/ReactionType"
+              }
+            ]
           }
         },
         "required": [
@@ -7539,7 +7604,11 @@
             "type": "string"
           },
           "type": {
-            "$ref": "#/components/schemas/ReactionType"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/ReactionType"
+              }
+            ]
           },
           "user": {
             "$ref": "#/components/schemas/UserResponseDto"
@@ -7571,6 +7640,7 @@
             "items": {
               "$ref": "#/components/schemas/AlbumUserAddDto"
             },
+            "minItems": 1,
             "type": "array"
           }
         },
@@ -7639,7 +7709,11 @@
             "type": "string"
           },
           "order": {
-            "$ref": "#/components/schemas/AssetOrder"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AssetOrder"
+              }
+            ]
           },
           "owner": {
             "$ref": "#/components/schemas/UserResponseDto"
@@ -7699,7 +7773,12 @@
       "AlbumUserAddDto": {
         "properties": {
           "role": {
-            "$ref": "#/components/schemas/AlbumUserRole"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AlbumUserRole"
+              }
+            ],
+            "default": "editor"
           },
           "userId": {
             "format": "uuid",
@@ -7714,7 +7793,11 @@
       "AlbumUserCreateDto": {
         "properties": {
           "role": {
-            "$ref": "#/components/schemas/AlbumUserRole"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AlbumUserRole"
+              }
+            ]
           },
           "userId": {
             "format": "uuid",
@@ -7730,7 +7813,11 @@
       "AlbumUserResponseDto": {
         "properties": {
           "role": {
-            "$ref": "#/components/schemas/AlbumUserRole"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AlbumUserRole"
+              }
+            ]
           },
           "user": {
             "$ref": "#/components/schemas/UserResponseDto"
@@ -7754,6 +7841,9 @@
           "backgroundTask": {
             "$ref": "#/components/schemas/JobStatusDto"
           },
+          "backupDatabase": {
+            "$ref": "#/components/schemas/JobStatusDto"
+          },
           "duplicateDetection": {
             "$ref": "#/components/schemas/JobStatusDto"
           },
@@ -7796,6 +7886,7 @@
         },
         "required": [
           "backgroundTask",
+          "backupDatabase",
           "duplicateDetection",
           "faceDetection",
           "facialRecognition",
@@ -8023,7 +8114,11 @@
             "nullable": true
           },
           "sourceType": {
-            "$ref": "#/components/schemas/SourceType"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/SourceType"
+              }
+            ]
           }
         },
         "required": [
@@ -8094,7 +8189,11 @@
             "type": "integer"
           },
           "sourceType": {
-            "$ref": "#/components/schemas/SourceType"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/SourceType"
+              }
+            ]
           }
         },
         "required": [
@@ -8173,8 +8272,9 @@
       },
       "AssetJobName": {
         "enum": [
-          "regenerate-thumbnail",
+          "refresh-faces",
           "refresh-metadata",
+          "regenerate-thumbnail",
           "transcode-video"
         ],
         "type": "string"
@@ -8189,7 +8289,11 @@
             "type": "array"
           },
           "name": {
-            "$ref": "#/components/schemas/AssetJobName"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AssetJobName"
+              }
+            ]
           }
         },
         "required": [
@@ -8227,9 +8331,6 @@
           "isFavorite": {
             "type": "boolean"
           },
-          "isOffline": {
-            "type": "boolean"
-          },
           "isVisible": {
             "type": "boolean"
           },
@@ -8290,7 +8391,11 @@
             "type": "string"
           },
           "status": {
-            "$ref": "#/components/schemas/AssetMediaStatus"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AssetMediaStatus"
+              }
+            ]
           }
         },
         "required": [
@@ -8409,9 +8514,6 @@
             "description": "This property was deprecated in v1.113.0",
             "type": "boolean"
           },
-          "smartInfo": {
-            "$ref": "#/components/schemas/SmartInfoResponseDto"
-          },
           "stack": {
             "allOf": [
               {
@@ -8431,7 +8533,11 @@
             "type": "string"
           },
           "type": {
-            "$ref": "#/components/schemas/AssetTypeEnum"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AssetTypeEnum"
+              }
+            ]
           },
           "unassignedFaces": {
             "items": {
@@ -8518,7 +8624,8 @@
         "enum": [
           "mp3",
           "aac",
-          "libopus"
+          "libopus",
+          "pcm_s16le"
         ],
         "type": "string"
       },
@@ -8543,7 +8650,11 @@
       "AvatarResponse": {
         "properties": {
           "color": {
-            "$ref": "#/components/schemas/UserAvatarColor"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/UserAvatarColor"
+              }
+            ]
           }
         },
         "required": [
@@ -8554,7 +8665,11 @@
       "AvatarUpdate": {
         "properties": {
           "color": {
-            "$ref": "#/components/schemas/UserAvatarColor"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/UserAvatarColor"
+              }
+            ]
           }
         },
         "type": "object"
@@ -8645,6 +8760,7 @@
             "items": {
               "type": "string"
             },
+            "minItems": 1,
             "type": "array"
           },
           "deviceId": {
@@ -8711,13 +8827,17 @@
             "items": {
               "type": "string"
             },
-            "type": "array"
+            "maxItems": 128,
+            "type": "array",
+            "uniqueItems": true
           },
           "importPaths": {
             "items": {
               "type": "string"
             },
-            "type": "array"
+            "maxItems": 128,
+            "type": "array",
+            "uniqueItems": true
           },
           "name": {
             "type": "string"
@@ -8746,6 +8866,10 @@
       },
       "CreateProfileImageResponseDto": {
         "properties": {
+          "profileChangedAt": {
+            "format": "date-time",
+            "type": "string"
+          },
           "profileImagePath": {
             "type": "string"
           },
@@ -8754,11 +8878,32 @@
           }
         },
         "required": [
+          "profileChangedAt",
           "profileImagePath",
           "userId"
         ],
         "type": "object"
       },
+      "DatabaseBackupConfig": {
+        "properties": {
+          "cronExpression": {
+            "type": "string"
+          },
+          "enabled": {
+            "type": "boolean"
+          },
+          "keepLastAmount": {
+            "minimum": 1,
+            "type": "number"
+          }
+        },
+        "required": [
+          "cronExpression",
+          "enabled",
+          "keepLastAmount"
+        ],
+        "type": "object"
+      },
       "DownloadArchiveInfo": {
         "properties": {
           "assetIds": {
@@ -9061,7 +9206,7 @@
           "maxDistance": {
             "format": "double",
             "maximum": 2,
-            "minimum": 0,
+            "minimum": 0.1,
             "type": "number"
           },
           "minFaces": {
@@ -9071,7 +9216,7 @@
           "minScore": {
             "format": "double",
             "maximum": 1,
-            "minimum": 0,
+            "minimum": 0.1,
             "type": "number"
           },
           "modelName": {
@@ -9161,10 +9306,18 @@
             "type": "string"
           },
           "entityType": {
-            "$ref": "#/components/schemas/PathEntityType"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/PathEntityType"
+              }
+            ]
           },
           "pathType": {
-            "$ref": "#/components/schemas/PathType"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/PathType"
+              }
+            ]
           },
           "pathValue": {
             "type": "string"
@@ -9226,15 +9379,18 @@
       "JobCommandDto": {
         "properties": {
           "command": {
-            "$ref": "#/components/schemas/JobCommand"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/JobCommand"
+              }
+            ]
           },
           "force": {
             "type": "boolean"
           }
         },
         "required": [
-          "command",
-          "force"
+          "command"
         ],
         "type": "object"
       },
@@ -9269,6 +9425,21 @@
         ],
         "type": "object"
       },
+      "JobCreateDto": {
+        "properties": {
+          "name": {
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/ManualJobName"
+              }
+            ]
+          }
+        },
+        "required": [
+          "name"
+        ],
+        "type": "object"
+      },
       "JobName": {
         "enum": [
           "thumbnailGeneration",
@@ -9284,7 +9455,8 @@
           "search",
           "sidecar",
           "library",
-          "notifications"
+          "notifications",
+          "backupDatabase"
         ],
         "type": "string"
       },
@@ -9448,6 +9620,7 @@
         "properties": {
           "email": {
             "example": "testuser@email.com",
+            "format": "email",
             "type": "string"
           },
           "password": {
@@ -9511,6 +9684,14 @@
         ],
         "type": "object"
       },
+      "ManualJobName": {
+        "enum": [
+          "person-cleanup",
+          "tag-cleanup",
+          "user-cleanup"
+        ],
+        "type": "string"
+      },
       "MapMarkerResponseDto": {
         "properties": {
           "city": {
@@ -9569,13 +9750,6 @@
         ],
         "type": "object"
       },
-      "MapTheme": {
-        "enum": [
-          "light",
-          "dark"
-        ],
-        "type": "string"
-      },
       "MemoriesResponse": {
         "properties": {
           "enabled": {
@@ -9620,7 +9794,11 @@
             "type": "string"
           },
           "type": {
-            "$ref": "#/components/schemas/MemoryType"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/MemoryType"
+              }
+            ]
           }
         },
         "required": [
@@ -9685,7 +9863,11 @@
             "type": "string"
           },
           "type": {
-            "$ref": "#/components/schemas/MemoryType"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/MemoryType"
+              }
+            ]
           },
           "updatedAt": {
             "format": "date-time",
@@ -9814,7 +9996,11 @@
             "type": "string"
           },
           "order": {
-            "$ref": "#/components/schemas/AssetOrder"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AssetOrder"
+              }
+            ]
           },
           "originalFileName": {
             "type": "string"
@@ -9865,7 +10051,11 @@
             "type": "string"
           },
           "type": {
-            "$ref": "#/components/schemas/AssetTypeEnum"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AssetTypeEnum"
+              }
+            ]
           },
           "updatedAfter": {
             "format": "date-time",
@@ -9949,7 +10139,11 @@
       "PartnerResponseDto": {
         "properties": {
           "avatarColor": {
-            "$ref": "#/components/schemas/UserAvatarColor"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/UserAvatarColor"
+              }
+            ]
           },
           "email": {
             "type": "string"
@@ -9963,6 +10157,10 @@
           "name": {
             "type": "string"
           },
+          "profileChangedAt": {
+            "format": "date-time",
+            "type": "string"
+          },
           "profileImagePath": {
             "type": "string"
           }
@@ -9972,6 +10170,7 @@
           "email",
           "id",
           "name",
+          "profileChangedAt",
           "profileImagePath"
         ],
         "type": "object"
@@ -10371,6 +10570,130 @@
         ],
         "type": "object"
       },
+      "RandomSearchDto": {
+        "properties": {
+          "city": {
+            "nullable": true,
+            "type": "string"
+          },
+          "country": {
+            "nullable": true,
+            "type": "string"
+          },
+          "createdAfter": {
+            "format": "date-time",
+            "type": "string"
+          },
+          "createdBefore": {
+            "format": "date-time",
+            "type": "string"
+          },
+          "deviceId": {
+            "type": "string"
+          },
+          "isArchived": {
+            "type": "boolean"
+          },
+          "isEncoded": {
+            "type": "boolean"
+          },
+          "isFavorite": {
+            "type": "boolean"
+          },
+          "isMotion": {
+            "type": "boolean"
+          },
+          "isNotInAlbum": {
+            "type": "boolean"
+          },
+          "isOffline": {
+            "type": "boolean"
+          },
+          "isVisible": {
+            "type": "boolean"
+          },
+          "lensModel": {
+            "nullable": true,
+            "type": "string"
+          },
+          "libraryId": {
+            "format": "uuid",
+            "nullable": true,
+            "type": "string"
+          },
+          "make": {
+            "type": "string"
+          },
+          "model": {
+            "nullable": true,
+            "type": "string"
+          },
+          "personIds": {
+            "items": {
+              "format": "uuid",
+              "type": "string"
+            },
+            "type": "array"
+          },
+          "size": {
+            "maximum": 1000,
+            "minimum": 1,
+            "type": "number"
+          },
+          "state": {
+            "nullable": true,
+            "type": "string"
+          },
+          "takenAfter": {
+            "format": "date-time",
+            "type": "string"
+          },
+          "takenBefore": {
+            "format": "date-time",
+            "type": "string"
+          },
+          "trashedAfter": {
+            "format": "date-time",
+            "type": "string"
+          },
+          "trashedBefore": {
+            "format": "date-time",
+            "type": "string"
+          },
+          "type": {
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AssetTypeEnum"
+              }
+            ]
+          },
+          "updatedAfter": {
+            "format": "date-time",
+            "type": "string"
+          },
+          "updatedBefore": {
+            "format": "date-time",
+            "type": "string"
+          },
+          "withArchived": {
+            "default": false,
+            "type": "boolean"
+          },
+          "withDeleted": {
+            "type": "boolean"
+          },
+          "withExif": {
+            "type": "boolean"
+          },
+          "withPeople": {
+            "type": "boolean"
+          },
+          "withStacked": {
+            "type": "boolean"
+          }
+        },
+        "type": "object"
+      },
       "RatingsResponse": {
         "properties": {
           "enabled": {
@@ -10422,17 +10745,6 @@
         ],
         "type": "object"
       },
-      "ScanLibraryDto": {
-        "properties": {
-          "refreshAllFiles": {
-            "type": "boolean"
-          },
-          "refreshModifiedFiles": {
-            "type": "boolean"
-          }
-        },
-        "type": "object"
-      },
       "SearchAlbumResponseDto": {
         "properties": {
           "count": {
@@ -10634,6 +10946,18 @@
           "sourceUrl": {
             "type": "string"
           },
+          "thirdPartyBugFeatureUrl": {
+            "type": "string"
+          },
+          "thirdPartyDocumentationUrl": {
+            "type": "string"
+          },
+          "thirdPartySourceUrl": {
+            "type": "string"
+          },
+          "thirdPartySupportUrl": {
+            "type": "string"
+          },
           "version": {
             "type": "string"
           },
@@ -10662,9 +10986,18 @@
           "loginPageMessage": {
             "type": "string"
           },
+          "mapDarkStyleUrl": {
+            "type": "string"
+          },
+          "mapLightStyleUrl": {
+            "type": "string"
+          },
           "oauthButtonText": {
             "type": "string"
           },
+          "publicUsers": {
+            "type": "boolean"
+          },
           "trashDays": {
             "type": "integer"
           },
@@ -10677,7 +11010,10 @@
           "isInitialized",
           "isOnboarded",
           "loginPageMessage",
+          "mapDarkStyleUrl",
+          "mapLightStyleUrl",
           "oauthButtonText",
+          "publicUsers",
           "trashDays",
           "userDeleteDelay"
         ],
@@ -10804,7 +11140,9 @@
               {
                 "photos": 1,
                 "videos": 1,
-                "diskUsageRaw": 1
+                "diskUsageRaw": 2,
+                "usagePhotos": 1,
+                "usageVideos": 1
               }
             ],
             "items": {
@@ -10813,6 +11151,16 @@
             "title": "Array of usage for each user",
             "type": "array"
           },
+          "usagePhotos": {
+            "default": 0,
+            "format": "int64",
+            "type": "integer"
+          },
+          "usageVideos": {
+            "default": 0,
+            "format": "int64",
+            "type": "integer"
+          },
           "videos": {
             "default": 0,
             "type": "integer"
@@ -10822,6 +11170,8 @@
           "photos",
           "usage",
           "usageByUser",
+          "usagePhotos",
+          "usageVideos",
           "videos"
         ],
         "type": "object"
@@ -10876,6 +11226,26 @@
         ],
         "type": "object"
       },
+      "ServerVersionHistoryResponseDto": {
+        "properties": {
+          "createdAt": {
+            "format": "date-time",
+            "type": "string"
+          },
+          "id": {
+            "type": "string"
+          },
+          "version": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "createdAt",
+          "id",
+          "version"
+        ],
+        "type": "object"
+      },
       "ServerVersionResponseDto": {
         "properties": {
           "major": {
@@ -10963,7 +11333,11 @@
             "type": "boolean"
           },
           "type": {
-            "$ref": "#/components/schemas/SharedLinkType"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/SharedLinkType"
+              }
+            ]
           }
         },
         "required": [
@@ -11048,7 +11422,11 @@
             "type": "string"
           },
           "type": {
-            "$ref": "#/components/schemas/SharedLinkType"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/SharedLinkType"
+              }
+            ]
           },
           "userId": {
             "type": "string"
@@ -11081,6 +11459,7 @@
         "properties": {
           "email": {
             "example": "testuser@email.com",
+            "format": "email",
             "type": "string"
           },
           "name": {
@@ -11099,25 +11478,6 @@
         ],
         "type": "object"
       },
-      "SmartInfoResponseDto": {
-        "properties": {
-          "objects": {
-            "items": {
-              "type": "string"
-            },
-            "nullable": true,
-            "type": "array"
-          },
-          "tags": {
-            "items": {
-              "type": "string"
-            },
-            "nullable": true,
-            "type": "array"
-          }
-        },
-        "type": "object"
-      },
       "SmartSearchDto": {
         "properties": {
           "city": {
@@ -11216,7 +11576,11 @@
             "type": "string"
           },
           "type": {
-            "$ref": "#/components/schemas/AssetTypeEnum"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AssetTypeEnum"
+              }
+            ]
           },
           "updatedAfter": {
             "format": "date-time",
@@ -11257,6 +11621,7 @@
               "format": "uuid",
               "type": "string"
             },
+            "minItems": 2,
             "type": "array"
           }
         },
@@ -11296,8 +11661,22 @@
         },
         "type": "object"
       },
+      "SystemConfigBackupsDto": {
+        "properties": {
+          "database": {
+            "$ref": "#/components/schemas/DatabaseBackupConfig"
+          }
+        },
+        "required": [
+          "database"
+        ],
+        "type": "object"
+      },
       "SystemConfigDto": {
         "properties": {
+          "backup": {
+            "$ref": "#/components/schemas/SystemConfigBackupsDto"
+          },
           "ffmpeg": {
             "$ref": "#/components/schemas/SystemConfigFFmpegDto"
           },
@@ -11343,6 +11722,9 @@
           "storageTemplate": {
             "$ref": "#/components/schemas/SystemConfigStorageTemplateDto"
           },
+          "templates": {
+            "$ref": "#/components/schemas/SystemConfigTemplatesDto"
+          },
           "theme": {
             "$ref": "#/components/schemas/SystemConfigThemeDto"
           },
@@ -11354,6 +11736,7 @@
           }
         },
         "required": [
+          "backup",
           "ffmpeg",
           "image",
           "job",
@@ -11369,6 +11752,7 @@
           "reverseGeocoding",
           "server",
           "storageTemplate",
+          "templates",
           "theme",
           "trash",
           "user"
@@ -11378,7 +11762,11 @@
       "SystemConfigFFmpegDto": {
         "properties": {
           "accel": {
-            "$ref": "#/components/schemas/TranscodeHWAccel"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/TranscodeHWAccel"
+              }
+            ]
           },
           "accelDecode": {
             "type": "boolean"
@@ -11407,7 +11795,11 @@
             "type": "integer"
           },
           "cqMode": {
-            "$ref": "#/components/schemas/CQMode"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/CQMode"
+              }
+            ]
           },
           "crf": {
             "maximum": 51,
@@ -11421,10 +11813,6 @@
           "maxBitrate": {
             "type": "string"
           },
-          "npl": {
-            "minimum": 0,
-            "type": "integer"
-          },
           "preferredHwDevice": {
             "type": "string"
           },
@@ -11437,13 +11825,21 @@
             "type": "integer"
           },
           "targetAudioCodec": {
-            "$ref": "#/components/schemas/AudioCodec"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AudioCodec"
+              }
+            ]
           },
           "targetResolution": {
             "type": "string"
           },
           "targetVideoCodec": {
-            "$ref": "#/components/schemas/VideoCodec"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/VideoCodec"
+              }
+            ]
           },
           "temporalAQ": {
             "type": "boolean"
@@ -11453,10 +11849,18 @@
             "type": "integer"
           },
           "tonemap": {
-            "$ref": "#/components/schemas/ToneMapping"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/ToneMapping"
+              }
+            ]
           },
           "transcode": {
-            "$ref": "#/components/schemas/TranscodePolicy"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/TranscodePolicy"
+              }
+            ]
           },
           "twoPass": {
             "type": "boolean"
@@ -11473,7 +11877,6 @@
           "crf",
           "gopSize",
           "maxBitrate",
-          "npl",
           "preferredHwDevice",
           "preset",
           "refs",
@@ -11499,42 +11902,56 @@
         ],
         "type": "object"
       },
-      "SystemConfigImageDto": {
+      "SystemConfigGeneratedImageDto": {
         "properties": {
-          "colorspace": {
-            "$ref": "#/components/schemas/Colorspace"
-          },
-          "extractEmbedded": {
-            "type": "boolean"
-          },
-          "previewFormat": {
-            "$ref": "#/components/schemas/ImageFormat"
-          },
-          "previewSize": {
-            "minimum": 1,
-            "type": "integer"
+          "format": {
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/ImageFormat"
+              }
+            ]
           },
           "quality": {
             "maximum": 100,
             "minimum": 1,
             "type": "integer"
           },
-          "thumbnailFormat": {
-            "$ref": "#/components/schemas/ImageFormat"
-          },
-          "thumbnailSize": {
+          "size": {
             "minimum": 1,
             "type": "integer"
           }
         },
+        "required": [
+          "format",
+          "quality",
+          "size"
+        ],
+        "type": "object"
+      },
+      "SystemConfigImageDto": {
+        "properties": {
+          "colorspace": {
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/Colorspace"
+              }
+            ]
+          },
+          "extractEmbedded": {
+            "type": "boolean"
+          },
+          "preview": {
+            "$ref": "#/components/schemas/SystemConfigGeneratedImageDto"
+          },
+          "thumbnail": {
+            "$ref": "#/components/schemas/SystemConfigGeneratedImageDto"
+          }
+        },
         "required": [
           "colorspace",
           "extractEmbedded",
-          "previewFormat",
-          "previewSize",
-          "quality",
-          "thumbnailFormat",
-          "thumbnailSize"
+          "preview",
+          "thumbnail"
         ],
         "type": "object"
       },
@@ -11636,7 +12053,11 @@
             "type": "boolean"
           },
           "level": {
-            "$ref": "#/components/schemas/LogLevel"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/LogLevel"
+              }
+            ]
           }
         },
         "required": [
@@ -11660,7 +12081,18 @@
             "$ref": "#/components/schemas/FacialRecognitionConfig"
           },
           "url": {
+            "deprecated": true,
+            "description": "This property was deprecated in v1.122.0",
             "type": "string"
+          },
+          "urls": {
+            "format": "uri",
+            "items": {
+              "format": "uri",
+              "type": "string"
+            },
+            "minItems": 1,
+            "type": "array"
           }
         },
         "required": [
@@ -11668,19 +12100,21 @@
           "duplicateDetection",
           "enabled",
           "facialRecognition",
-          "url"
+          "urls"
         ],
         "type": "object"
       },
       "SystemConfigMapDto": {
         "properties": {
           "darkStyle": {
+            "format": "uri",
             "type": "string"
           },
           "enabled": {
             "type": "boolean"
           },
           "lightStyle": {
+            "format": "uri",
             "type": "string"
           }
         },
@@ -11755,6 +12189,7 @@
             "type": "boolean"
           },
           "mobileRedirectUri": {
+            "format": "uri",
             "type": "string"
           },
           "profileSigningAlgorithm": {
@@ -11817,15 +12252,20 @@
       "SystemConfigServerDto": {
         "properties": {
           "externalDomain": {
+            "format": "uri",
             "type": "string"
           },
           "loginPageMessage": {
             "type": "string"
+          },
+          "publicUsers": {
+            "type": "boolean"
           }
         },
         "required": [
           "externalDomain",
-          "loginPageMessage"
+          "loginPageMessage",
+          "publicUsers"
         ],
         "type": "object"
       },
@@ -11900,6 +12340,25 @@
         ],
         "type": "object"
       },
+      "SystemConfigTemplateEmailsDto": {
+        "properties": {
+          "albumInviteTemplate": {
+            "type": "string"
+          },
+          "albumUpdateTemplate": {
+            "type": "string"
+          },
+          "welcomeTemplate": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "albumInviteTemplate",
+          "albumUpdateTemplate",
+          "welcomeTemplate"
+        ],
+        "type": "object"
+      },
       "SystemConfigTemplateStorageOptionDto": {
         "properties": {
           "dayOptions": {
@@ -11963,6 +12422,17 @@
         ],
         "type": "object"
       },
+      "SystemConfigTemplatesDto": {
+        "properties": {
+          "email": {
+            "$ref": "#/components/schemas/SystemConfigTemplateEmailsDto"
+          }
+        },
+        "required": [
+          "email"
+        ],
+        "type": "object"
+      },
       "SystemConfigThemeDto": {
         "properties": {
           "customCss": {
@@ -12039,6 +12509,7 @@
       "TagCreateDto": {
         "properties": {
           "color": {
+            "pattern": "^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$",
             "type": "string"
           },
           "name": {
@@ -12094,6 +12565,7 @@
         "properties": {
           "color": {
             "nullable": true,
+            "pattern": "^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$",
             "type": "string"
           }
         },
@@ -12141,6 +12613,43 @@
         },
         "type": "object"
       },
+      "TemplateDto": {
+        "properties": {
+          "template": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "template"
+        ],
+        "type": "object"
+      },
+      "TemplateResponseDto": {
+        "properties": {
+          "html": {
+            "type": "string"
+          },
+          "name": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "html",
+          "name"
+        ],
+        "type": "object"
+      },
+      "TestEmailResponseDto": {
+        "properties": {
+          "messageId": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "messageId"
+        ],
+        "type": "object"
+      },
       "TimeBucketResponseDto": {
         "properties": {
           "count": {
@@ -12192,6 +12701,17 @@
         ],
         "type": "string"
       },
+      "TrashResponseDto": {
+        "properties": {
+          "count": {
+            "type": "integer"
+          }
+        },
+        "required": [
+          "count"
+        ],
+        "type": "object"
+      },
       "UpdateAlbumDto": {
         "properties": {
           "albumName": {
@@ -12208,7 +12728,11 @@
             "type": "boolean"
           },
           "order": {
-            "$ref": "#/components/schemas/AssetOrder"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AssetOrder"
+              }
+            ]
           }
         },
         "type": "object"
@@ -12216,7 +12740,11 @@
       "UpdateAlbumUserDto": {
         "properties": {
           "role": {
-            "$ref": "#/components/schemas/AlbumUserRole"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AlbumUserRole"
+              }
+            ]
           }
         },
         "required": [
@@ -12263,13 +12791,17 @@
             "items": {
               "type": "string"
             },
-            "type": "array"
+            "maxItems": 128,
+            "type": "array",
+            "uniqueItems": true
           },
           "importPaths": {
             "items": {
               "type": "string"
             },
-            "type": "array"
+            "maxItems": 128,
+            "type": "array",
+            "uniqueItems": true
           },
           "name": {
             "type": "string"
@@ -12302,6 +12834,14 @@
             "format": "int64",
             "type": "integer"
           },
+          "usagePhotos": {
+            "format": "int64",
+            "type": "integer"
+          },
+          "usageVideos": {
+            "format": "int64",
+            "type": "integer"
+          },
           "userId": {
             "type": "string"
           },
@@ -12316,6 +12856,8 @@
           "photos",
           "quotaSizeInBytes",
           "usage",
+          "usagePhotos",
+          "usageVideos",
           "userId",
           "userName",
           "videos"
@@ -12325,6 +12867,7 @@
       "UserAdminCreateDto": {
         "properties": {
           "email": {
+            "format": "email",
             "type": "string"
           },
           "name": {
@@ -12368,7 +12911,11 @@
       "UserAdminResponseDto": {
         "properties": {
           "avatarColor": {
-            "$ref": "#/components/schemas/UserAvatarColor"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/UserAvatarColor"
+              }
+            ]
           },
           "createdAt": {
             "format": "date-time",
@@ -12402,6 +12949,10 @@
           "oauthId": {
             "type": "string"
           },
+          "profileChangedAt": {
+            "format": "date-time",
+            "type": "string"
+          },
           "profileImagePath": {
             "type": "string"
           },
@@ -12419,7 +12970,11 @@
             "type": "boolean"
           },
           "status": {
-            "$ref": "#/components/schemas/UserStatus"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/UserStatus"
+              }
+            ]
           },
           "storageLabel": {
             "nullable": true,
@@ -12440,6 +12995,7 @@
           "license",
           "name",
           "oauthId",
+          "profileChangedAt",
           "profileImagePath",
           "quotaSizeInBytes",
           "quotaUsageInBytes",
@@ -12453,6 +13009,7 @@
       "UserAdminUpdateDto": {
         "properties": {
           "email": {
+            "format": "email",
             "type": "string"
           },
           "name": {
@@ -12590,7 +13147,11 @@
       "UserResponseDto": {
         "properties": {
           "avatarColor": {
-            "$ref": "#/components/schemas/UserAvatarColor"
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/UserAvatarColor"
+              }
+            ]
           },
           "email": {
             "type": "string"
@@ -12601,6 +13162,10 @@
           "name": {
             "type": "string"
           },
+          "profileChangedAt": {
+            "format": "date-time",
+            "type": "string"
+          },
           "profileImagePath": {
             "type": "string"
           }
@@ -12610,6 +13175,7 @@
           "email",
           "id",
           "name",
+          "profileChangedAt",
           "profileImagePath"
         ],
         "type": "object"
@@ -12625,6 +13191,7 @@
       "UserUpdateMeDto": {
         "properties": {
           "email": {
+            "format": "email",
             "type": "string"
           },
           "name": {
@@ -12653,13 +13220,17 @@
             "items": {
               "type": "string"
             },
-            "type": "array"
+            "maxItems": 128,
+            "type": "array",
+            "uniqueItems": true
           },
           "importPaths": {
             "items": {
               "type": "string"
             },
-            "type": "array"
+            "maxItems": 128,
+            "type": "array",
+            "uniqueItems": true
           }
         },
         "type": "object"
diff --git a/open-api/templates/mobile/api_client.mustache b/open-api/templates/mobile/api_client.mustache
deleted file mode 100644
index 7f464f026e..0000000000
--- a/open-api/templates/mobile/api_client.mustache
+++ /dev/null
@@ -1,264 +0,0 @@
-{{>header}}
-{{>part_of}}
-class ApiClient {
-  ApiClient({this.basePath = '{{{basePath}}}', this.authentication,});
-
-  final String basePath;
-  final Authentication? authentication;
-
-  var _client = Client();
-  final _defaultHeaderMap = <String, String>{};
-
-  /// Returns the current HTTP [Client] instance to use in this class.
-  ///
-  /// The return value is guaranteed to never be null.
-  Client get client => _client;
-
-  /// Requests to use a new HTTP [Client] in this class.
-  set client(Client newClient) {
-    _client = newClient;
-  }
-
-  Map<String, String> get defaultHeaderMap => _defaultHeaderMap;
-
-  void addDefaultHeader(String key, String value) {
-     _defaultHeaderMap[key] = value;
-  }
-
-  // We don't use a Map<String, String> for queryParams.
-  // If collectionFormat is 'multi', a key might appear multiple times.
-  Future<Response> invokeAPI(
-    String path,
-    String method,
-    List<QueryParam> queryParams,
-    Object? body,
-    Map<String, String> headerParams,
-    Map<String, String> formParams,
-    String? contentType,
-  ) async {
-    await authentication?.applyToParams(queryParams, headerParams);
-
-    headerParams.addAll(_defaultHeaderMap);
-    if (contentType != null) {
-      headerParams['Content-Type'] = contentType;
-    }
-
-    final urlEncodedQueryParams = queryParams.map((param) => '$param');
-    final queryString = urlEncodedQueryParams.isNotEmpty ? '?${urlEncodedQueryParams.join('&')}' : '';
-    final uri = Uri.parse('$basePath$path$queryString');
-
-    try {
-      // Special case for uploading a single file which isn't a 'multipart/form-data'.
-      if (
-        body is MultipartFile && (contentType == null ||
-        !contentType.toLowerCase().startsWith('multipart/form-data'))
-      ) {
-        final request = StreamedRequest(method, uri);
-        request.headers.addAll(headerParams);
-        request.contentLength = body.length;
-        body.finalize().listen(
-          request.sink.add,
-          onDone: request.sink.close,
-          // ignore: avoid_types_on_closure_parameters
-          onError: (Object error, StackTrace trace) => request.sink.close(),
-          cancelOnError: true,
-        );
-        final response = await _client.send(request);
-        return Response.fromStream(response);
-      }
-
-      if (body is MultipartRequest) {
-        final request = MultipartRequest(method, uri);
-        request.fields.addAll(body.fields);
-        request.files.addAll(body.files);
-        request.headers.addAll(body.headers);
-        request.headers.addAll(headerParams);
-        final response = await _client.send(request);
-        return Response.fromStream(response);
-      }
-
-      final msgBody = contentType == 'application/x-www-form-urlencoded'
-        ? formParams
-        : await serializeAsync(body);
-      final nullableHeaderParams = headerParams.isEmpty ? null : headerParams;
-
-      switch(method) {
-        case 'POST': return await _client.post(uri, headers: nullableHeaderParams, body: msgBody,);
-        case 'PUT': return await _client.put(uri, headers: nullableHeaderParams, body: msgBody,);
-        case 'DELETE': return await _client.delete(uri, headers: nullableHeaderParams, body: msgBody,);
-        case 'PATCH': return await _client.patch(uri, headers: nullableHeaderParams, body: msgBody,);
-        case 'HEAD': return await _client.head(uri, headers: nullableHeaderParams,);
-        case 'GET': return await _client.get(uri, headers: nullableHeaderParams,);
-      }
-    } on SocketException catch (error, trace) {
-      throw ApiException.withInner(
-        HttpStatus.badRequest,
-        'Socket operation failed: $method $path',
-        error,
-        trace,
-      );
-    } on TlsException catch (error, trace) {
-      throw ApiException.withInner(
-        HttpStatus.badRequest,
-        'TLS/SSL communication failed: $method $path',
-        error,
-        trace,
-      );
-    } on IOException catch (error, trace) {
-      throw ApiException.withInner(
-        HttpStatus.badRequest,
-        'I/O operation failed: $method $path',
-        error,
-        trace,
-      );
-    } on ClientException catch (error, trace) {
-      throw ApiException.withInner(
-        HttpStatus.badRequest,
-        'HTTP connection failed: $method $path',
-        error,
-        trace,
-      );
-    } on Exception catch (error, trace) {
-      throw ApiException.withInner(
-        HttpStatus.badRequest,
-        'Exception occurred: $method $path',
-        error,
-        trace,
-      );
-    }
-
-    throw ApiException(
-      HttpStatus.badRequest,
-      'Invalid HTTP operation: $method $path',
-    );
-  }
-{{#native_serialization}}
-
-  Future<dynamic> deserializeAsync(String value, String targetType, {bool growable = false,}) async =>
-    // ignore: deprecated_member_use_from_same_package
-    deserialize(value, targetType, growable: growable);
-
-  @Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use deserializeAsync() instead.')
-  dynamic deserialize(String value, String targetType, {bool growable = false,}) {
-    // Remove all spaces. Necessary for regular expressions as well.
-    targetType = targetType.replaceAll(' ', ''); // ignore: parameter_assignments
-
-    // If the expected target type is String, nothing to do...
-    return targetType == 'String'
-      ? value
-      : fromJson(json.decode(value), targetType, growable: growable);
-  }
-{{/native_serialization}}
-
-  // ignore: deprecated_member_use_from_same_package
-  Future<String> serializeAsync(Object? value) async => serialize(value);
-
-  @Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use serializeAsync() instead.')
-  String serialize(Object? value) => value == null ? '' : json.encode(value);
-
-{{#native_serialization}}
-  /// Returns a native instance of an OpenAPI class matching the [specified type][targetType].
-  static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) {
-    upgradeDto(value, targetType);
-    try {
-      switch (targetType) {
-        case 'String':
-          return value is String ? value : value.toString();
-        case 'int':
-          return value is int ? value : int.parse('$value');
-        case 'double':
-          return value is double ? value : double.parse('$value');
-        case 'bool':
-          if (value is bool) {
-            return value;
-          }
-          final valueString = '$value'.toLowerCase();
-          return valueString == 'true' || valueString == '1';
-        case 'DateTime':
-          return value is DateTime ? value : DateTime.tryParse(value);
-        {{#models}}
-          {{#model}}
-        case '{{{classname}}}':
-            {{#isEnum}}
-          {{#native_serialization}}return {{{classname}}}TypeTransformer().decode(value);{{/native_serialization}}
-            {{/isEnum}}
-            {{^isEnum}}
-          return {{{classname}}}.fromJson(value);
-            {{/isEnum}}
-          {{/model}}
-        {{/models}}
-        default:
-          dynamic match;
-          if (value is List && (match = _regList.firstMatch(targetType)?.group(1)) != null) {
-            return value
-              .map<dynamic>((dynamic v) => fromJson(v, match, growable: growable,))
-              .toList(growable: growable);
-          }
-          if (value is Set && (match = _regSet.firstMatch(targetType)?.group(1)) != null) {
-            return value
-              .map<dynamic>((dynamic v) => fromJson(v, match, growable: growable,))
-              .toSet();
-          }
-          if (value is Map && (match = _regMap.firstMatch(targetType)?.group(1)) != null) {
-            return Map<String, dynamic>.fromIterables(
-              value.keys.cast<String>(),
-              value.values.map<dynamic>((dynamic v) => fromJson(v, match, growable: growable,)),
-            );
-          }
-      }
-    } on Exception catch (error, trace) {
-      throw ApiException.withInner(HttpStatus.internalServerError, 'Exception during deserialization.', error, trace,);
-    }
-    throw ApiException(HttpStatus.internalServerError, 'Could not find a suitable class for deserialization',);
-  }
-{{/native_serialization}}
-}
-{{#native_serialization}}
-
-/// Primarily intended for use in an isolate.
-class DeserializationMessage {
-  const DeserializationMessage({
-    required this.json,
-    required this.targetType,
-    this.growable = false,
-  });
-
-  /// The JSON value to deserialize.
-  final String json;
-
-  /// Target type to deserialize to.
-  final String targetType;
-
-  /// Whether to make deserialized lists or maps growable.
-  final bool growable;
-}
-
-/// Primarily intended for use in an isolate.
-Future<dynamic> decodeAsync(DeserializationMessage message) async {
-  // Remove all spaces. Necessary for regular expressions as well.
-  final targetType = message.targetType.replaceAll(' ', '');
-
-  // If the expected target type is String, nothing to do...
-  return targetType == 'String'
-    ? message.json
-    : json.decode(message.json);
-}
-
-/// Primarily intended for use in an isolate.
-Future<dynamic> deserializeAsync(DeserializationMessage message) async {
-  // Remove all spaces. Necessary for regular expressions as well.
-  final targetType = message.targetType.replaceAll(' ', '');
-
-  // If the expected target type is String, nothing to do...
-  return targetType == 'String'
-    ? message.json
-    : ApiClient.fromJson(
-        json.decode(message.json),
-        targetType,
-        growable: message.growable,
-      );
-}
-{{/native_serialization}}
-
-/// Primarily intended for use in an isolate.
-Future<String> serializeAsync(Object? value) async => value == null ? '' : json.encode(value);
diff --git a/open-api/templates/mobile/api_client.mustache.patch b/open-api/templates/mobile/api_client.mustache.patch
deleted file mode 100644
index 3805cd8f79..0000000000
--- a/open-api/templates/mobile/api_client.mustache.patch
+++ /dev/null
@@ -1,10 +0,0 @@
---- api_client.mustache	2024-08-13 14:29:04.056364916 -0500
-+++ api_client_new.mustache	2024-08-13 14:29:36.224410735 -0500
-@@ -159,6 +159,7 @@
- {{#native_serialization}}
-   /// Returns a native instance of an OpenAPI class matching the [specified type][targetType].
-   static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) {
-+    upgradeDto(value, targetType);
-     try {
-       switch (targetType) {
-         case 'String':
diff --git a/open-api/templates/mobile/serialization/native/native_class.mustache b/open-api/templates/mobile/serialization/native/native_class.mustache
index 254843e00e..9a7b1439b1 100644
--- a/open-api/templates/mobile/serialization/native/native_class.mustache
+++ b/open-api/templates/mobile/serialization/native/native_class.mustache
@@ -111,6 +111,7 @@ class {{{classname}}} {
   /// [value] if it's a [Map], null otherwise.
   // ignore: prefer_constructors_over_static_methods
   static {{{classname}}}? fromJson(dynamic value) {
+    upgradeDto(value, "{{{classname}}}");
     if (value is Map) {
       final json = value.cast<String, dynamic>();
 
diff --git a/open-api/templates/mobile/serialization/native/native_class.mustache.patch b/open-api/templates/mobile/serialization/native/native_class.mustache.patch
index 02e07f933a..4ba6594966 100644
--- a/open-api/templates/mobile/serialization/native/native_class.mustache.patch
+++ b/open-api/templates/mobile/serialization/native/native_class.mustache.patch
@@ -1,5 +1,5 @@
---- native_class.mustache	2023-08-31 23:09:59.584269162 +0200
-+++ native_class1.mustache	2023-08-31 22:59:53.633083270 +0200
+--- native_class.mustache	2024-09-19 11:41:07.855683995 -0400
++++ native_class_temp.mustache	2024-09-19 11:41:57.113249395 -0400
 @@ -91,14 +91,14 @@
      {{/isDateTime}}
      {{#isNullable}}
@@ -17,10 +17,14 @@
      }
          {{/defaultValue}}
        {{/required}}
-@@ -114,17 +114,6 @@
+@@ -111,20 +111,10 @@
+   /// [value] if it's a [Map], null otherwise.
+   // ignore: prefer_constructors_over_static_methods
+   static {{{classname}}}? fromJson(dynamic value) {
++    upgradeDto(value, "{{{classname}}}");
      if (value is Map) {
        final json = value.cast<String, dynamic>();
- 
+
 -      // Ensure that the map contains the required keys.
 -      // Note 1: the values aren't checked for validity beyond being non-null.
 -      // Note 2: this code is stripped in release mode!
@@ -35,9 +39,9 @@
        return {{{classname}}}(
    {{#vars}}
      {{#isDateTime}}
-@@ -215,6 +204,10 @@
+@@ -215,6 +205,10 @@
              ? {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}}
-             : {{{datatypeWithEnum}}}.parse(json[r'{{{baseName}}}'].toString()),
+             : {{/isNullable}}{{{datatypeWithEnum}}}.parse('${json[r'{{{baseName}}}']}'),
                {{/isNumber}}
 +              {{#isDouble}}
 +        {{{name}}}: (mapValueOfType<num>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}).toDouble(),
@@ -46,7 +50,7 @@
                {{^isNumber}}
                  {{^isEnum}}
          {{{name}}}: mapValueOfType<{{{datatypeWithEnum}}}>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
-@@ -223,6 +216,7 @@
+@@ -223,6 +217,7 @@
          {{{name}}}: {{{enumName}}}.fromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
                  {{/isEnum}}
                {{/isNumber}}
diff --git a/open-api/typescript-sdk/.nvmrc b/open-api/typescript-sdk/.nvmrc
index 3516580bbb..1d9b7831ba 100644
--- a/open-api/typescript-sdk/.nvmrc
+++ b/open-api/typescript-sdk/.nvmrc
@@ -1 +1 @@
-20.17.0
+22.12.0
diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json
index 170ec83d7a..bef7d9d690 100644
--- a/open-api/typescript-sdk/package-lock.json
+++ b/open-api/typescript-sdk/package-lock.json
@@ -1,18 +1,18 @@
 {
   "name": "@immich/sdk",
-  "version": "1.115.0",
+  "version": "1.123.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@immich/sdk",
-      "version": "1.115.0",
+      "version": "1.123.0",
       "license": "GNU Affero General Public License version 3",
       "dependencies": {
         "@oazapfts/runtime": "^1.0.2"
       },
       "devDependencies": {
-        "@types/node": "^20.16.5",
+        "@types/node": "^22.10.2",
         "typescript": "^5.3.3"
       }
     },
@@ -22,19 +22,19 @@
       "integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g=="
     },
     "node_modules/@types/node": {
-      "version": "20.16.5",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz",
-      "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==",
+      "version": "22.10.2",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
+      "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "undici-types": "~6.19.2"
+        "undici-types": "~6.20.0"
       }
     },
     "node_modules/typescript": {
-      "version": "5.5.4",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
-      "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
+      "version": "5.6.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+      "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
       "dev": true,
       "license": "Apache-2.0",
       "bin": {
@@ -46,9 +46,9 @@
       }
     },
     "node_modules/undici-types": {
-      "version": "6.19.8",
-      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
-      "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
+      "version": "6.20.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
+      "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
       "dev": true,
       "license": "MIT"
     }
diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json
index 7773f3b71c..01a7543a69 100644
--- a/open-api/typescript-sdk/package.json
+++ b/open-api/typescript-sdk/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@immich/sdk",
-  "version": "1.115.0",
+  "version": "1.123.0",
   "description": "Auto-generated TypeScript SDK for the Immich API",
   "type": "module",
   "main": "./build/index.js",
@@ -19,7 +19,7 @@
     "@oazapfts/runtime": "^1.0.2"
   },
   "devDependencies": {
-    "@types/node": "^20.16.5",
+    "@types/node": "^22.10.2",
     "typescript": "^5.3.3"
   },
   "repository": {
@@ -28,6 +28,6 @@
     "directory": "open-api/typescript-sdk"
   },
   "volta": {
-    "node": "20.17.0"
+    "node": "22.12.0"
   }
 }
diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts
index 9350bd5604..c31e71d05e 100644
--- a/open-api/typescript-sdk/src/fetch-client.ts
+++ b/open-api/typescript-sdk/src/fetch-client.ts
@@ -1,6 +1,6 @@
 /**
  * Immich
- * 1.115.0
+ * 1.123.0
  * DO NOT MODIFY - This file has been generated using oazapfts.
  * See https://www.npmjs.com/package/oazapfts
  */
@@ -19,6 +19,7 @@ export type UserResponseDto = {
     email: string;
     id: string;
     name: string;
+    profileChangedAt: string;
     profileImagePath: string;
 };
 export type ActivityResponseDto = {
@@ -53,6 +54,7 @@ export type UserAdminResponseDto = {
     license: (UserLicense) | null;
     name: string;
     oauthId: string;
+    profileChangedAt: string;
     profileImagePath: string;
     quotaSizeInBytes: number | null;
     quotaUsageInBytes: number | null;
@@ -219,10 +221,6 @@ export type PersonWithFacesResponseDto = {
     /** This property was added in v1.107.0 */
     updatedAt?: string;
 };
-export type SmartInfoResponseDto = {
-    objects?: string[] | null;
-    tags?: string[] | null;
-};
 export type AssetStackResponseDto = {
     assetCount: number;
     id: string;
@@ -265,7 +263,6 @@ export type AssetResponseDto = {
     people?: PersonWithFacesResponseDto[];
     /** This property was deprecated in v1.113.0 */
     resized?: boolean;
-    smartInfo?: SmartInfoResponseDto;
     stack?: (AssetStackResponseDto) | null;
     tags?: TagResponseDto[];
     thumbhash: string | null;
@@ -364,7 +361,6 @@ export type AssetMediaCreateDto = {
     fileModifiedAt: string;
     isArchived?: boolean;
     isFavorite?: boolean;
-    isOffline?: boolean;
     isVisible?: boolean;
     livePhotoVideoId?: string;
     sidecarData?: Blob;
@@ -534,6 +530,7 @@ export type JobStatusDto = {
 };
 export type AllJobStatusResponseDto = {
     backgroundTask: JobStatusDto;
+    backupDatabase: JobStatusDto;
     duplicateDetection: JobStatusDto;
     faceDetection: JobStatusDto;
     facialRecognition: JobStatusDto;
@@ -548,9 +545,12 @@ export type AllJobStatusResponseDto = {
     thumbnailGeneration: JobStatusDto;
     videoConversion: JobStatusDto;
 };
+export type JobCreateDto = {
+    name: ManualJobName;
+};
 export type JobCommandDto = {
     command: JobCommand;
-    force: boolean;
+    force?: boolean;
 };
 export type LibraryResponseDto = {
     assetCount: number;
@@ -574,10 +574,6 @@ export type UpdateLibraryDto = {
     importPaths?: string[];
     name?: string;
 };
-export type ScanLibraryDto = {
-    refreshAllFiles?: boolean;
-    refreshModifiedFiles?: boolean;
-};
 export type LibraryStatsResponseDto = {
     photos: number;
     total: number;
@@ -638,6 +634,13 @@ export type MemoryUpdateDto = {
     memoryAt?: string;
     seenAt?: string;
 };
+export type TemplateDto = {
+    template: string;
+};
+export type TemplateResponseDto = {
+    html: string;
+    name: string;
+};
 export type SystemConfigSmtpTransportDto = {
     host: string;
     ignoreCert: boolean;
@@ -651,6 +654,9 @@ export type SystemConfigSmtpDto = {
     replyTo: string;
     transport: SystemConfigSmtpTransportDto;
 };
+export type TestEmailResponseDto = {
+    messageId: string;
+};
 export type OAuthConfigDto = {
     redirectUri: string;
 };
@@ -666,6 +672,7 @@ export type PartnerResponseDto = {
     id: string;
     inTimeline?: boolean;
     name: string;
+    profileChangedAt: string;
     profileImagePath: string;
 };
 export type UpdatePartnerDto = {
@@ -831,6 +838,39 @@ export type PlacesResponseDto = {
     longitude: number;
     name: string;
 };
+export type RandomSearchDto = {
+    city?: string | null;
+    country?: string | null;
+    createdAfter?: string;
+    createdBefore?: string;
+    deviceId?: string;
+    isArchived?: boolean;
+    isEncoded?: boolean;
+    isFavorite?: boolean;
+    isMotion?: boolean;
+    isNotInAlbum?: boolean;
+    isOffline?: boolean;
+    isVisible?: boolean;
+    lensModel?: string | null;
+    libraryId?: string | null;
+    make?: string;
+    model?: string | null;
+    personIds?: string[];
+    size?: number;
+    state?: string | null;
+    takenAfter?: string;
+    takenBefore?: string;
+    trashedAfter?: string;
+    trashedBefore?: string;
+    "type"?: AssetTypeEnum;
+    updatedAfter?: string;
+    updatedBefore?: string;
+    withArchived?: boolean;
+    withDeleted?: boolean;
+    withExif?: boolean;
+    withPeople?: boolean;
+    withStacked?: boolean;
+};
 export type SmartSearchDto = {
     city?: string | null;
     country?: string | null;
@@ -880,6 +920,10 @@ export type ServerAboutResponseDto = {
     sourceCommit?: string;
     sourceRef?: string;
     sourceUrl?: string;
+    thirdPartyBugFeatureUrl?: string;
+    thirdPartyDocumentationUrl?: string;
+    thirdPartySourceUrl?: string;
+    thirdPartySupportUrl?: string;
     version: string;
     versionUrl: string;
 };
@@ -888,7 +932,10 @@ export type ServerConfigDto = {
     isInitialized: boolean;
     isOnboarded: boolean;
     loginPageMessage: string;
+    mapDarkStyleUrl: string;
+    mapLightStyleUrl: string;
     oauthButtonText: string;
+    publicUsers: boolean;
     trashDays: number;
     userDeleteDelay: number;
 };
@@ -930,6 +977,8 @@ export type UsageByUserDto = {
     photos: number;
     quotaSizeInBytes: number | null;
     usage: number;
+    usagePhotos: number;
+    usageVideos: number;
     userId: string;
     userName: string;
     videos: number;
@@ -938,6 +987,8 @@ export type ServerStatsResponseDto = {
     photos: number;
     usage: number;
     usageByUser: UsageByUserDto[];
+    usagePhotos: number;
+    usageVideos: number;
     videos: number;
 };
 export type ServerStorageResponseDto = {
@@ -957,6 +1008,11 @@ export type ServerVersionResponseDto = {
     minor: number;
     patch: number;
 };
+export type ServerVersionHistoryResponseDto = {
+    createdAt: string;
+    id: string;
+    version: string;
+};
 export type SessionResponseDto = {
     createdAt: string;
     current: boolean;
@@ -1036,6 +1092,14 @@ export type AssetFullSyncDto = {
     updatedUntil: string;
     userId?: string;
 };
+export type DatabaseBackupConfig = {
+    cronExpression: string;
+    enabled: boolean;
+    keepLastAmount: number;
+};
+export type SystemConfigBackupsDto = {
+    database: DatabaseBackupConfig;
+};
 export type SystemConfigFFmpegDto = {
     accel: TranscodeHWAccel;
     accelDecode: boolean;
@@ -1047,7 +1111,6 @@ export type SystemConfigFFmpegDto = {
     crf: number;
     gopSize: number;
     maxBitrate: string;
-    npl: number;
     preferredHwDevice: string;
     preset: string;
     refs: number;
@@ -1060,14 +1123,16 @@ export type SystemConfigFFmpegDto = {
     transcode: TranscodePolicy;
     twoPass: boolean;
 };
+export type SystemConfigGeneratedImageDto = {
+    format: ImageFormat;
+    quality: number;
+    size: number;
+};
 export type SystemConfigImageDto = {
     colorspace: Colorspace;
     extractEmbedded: boolean;
-    previewFormat: ImageFormat;
-    previewSize: number;
-    quality: number;
-    thumbnailFormat: ImageFormat;
-    thumbnailSize: number;
+    preview: SystemConfigGeneratedImageDto;
+    thumbnail: SystemConfigGeneratedImageDto;
 };
 export type JobSettingsDto = {
     concurrency: number;
@@ -1120,7 +1185,9 @@ export type SystemConfigMachineLearningDto = {
     duplicateDetection: DuplicateDetectionConfig;
     enabled: boolean;
     facialRecognition: FacialRecognitionConfig;
-    url: string;
+    /** This property was deprecated in v1.122.0 */
+    url?: string;
+    urls: string[];
 };
 export type SystemConfigMapDto = {
     darkStyle: string;
@@ -1165,12 +1232,21 @@ export type SystemConfigReverseGeocodingDto = {
 export type SystemConfigServerDto = {
     externalDomain: string;
     loginPageMessage: string;
+    publicUsers: boolean;
 };
 export type SystemConfigStorageTemplateDto = {
     enabled: boolean;
     hashVerificationEnabled: boolean;
     template: string;
 };
+export type SystemConfigTemplateEmailsDto = {
+    albumInviteTemplate: string;
+    albumUpdateTemplate: string;
+    welcomeTemplate: string;
+};
+export type SystemConfigTemplatesDto = {
+    email: SystemConfigTemplateEmailsDto;
+};
 export type SystemConfigThemeDto = {
     customCss: string;
 };
@@ -1182,6 +1258,7 @@ export type SystemConfigUserDto = {
     deleteDelay: number;
 };
 export type SystemConfigDto = {
+    backup: SystemConfigBackupsDto;
     ffmpeg: SystemConfigFFmpegDto;
     image: SystemConfigImageDto;
     job: SystemConfigJobDto;
@@ -1197,6 +1274,7 @@ export type SystemConfigDto = {
     reverseGeocoding: SystemConfigReverseGeocodingDto;
     server: SystemConfigServerDto;
     storageTemplate: SystemConfigStorageTemplateDto;
+    templates: SystemConfigTemplatesDto;
     theme: SystemConfigThemeDto;
     trash: SystemConfigTrashDto;
     user: SystemConfigUserDto;
@@ -1240,6 +1318,9 @@ export type TimeBucketResponseDto = {
     count: number;
     timeBucket: string;
 };
+export type TrashResponseDto = {
+    count: number;
+};
 export type UserUpdateMeDto = {
     email?: string;
     name?: string;
@@ -1249,6 +1330,7 @@ export type CreateProfileImageDto = {
     file: Blob;
 };
 export type CreateProfileImageResponseDto = {
+    profileChangedAt: string;
     profileImagePath: string;
     userId: string;
 };
@@ -1686,6 +1768,9 @@ export function getMemoryLane({ day, month }: {
         ...opts
     }));
 }
+/**
+ * This property was deprecated in v1.116.0
+ */
 export function getRandom({ count }: {
     count?: number;
 }, opts?: Oazapfts.RequestOpts) {
@@ -1941,6 +2026,15 @@ export function getAllJobsStatus(opts?: Oazapfts.RequestOpts) {
         ...opts
     }));
 }
+export function createJob({ jobCreateDto }: {
+    jobCreateDto: JobCreateDto;
+}, opts?: Oazapfts.RequestOpts) {
+    return oazapfts.ok(oazapfts.fetchText("/jobs", oazapfts.json({
+        ...opts,
+        method: "POST",
+        body: jobCreateDto
+    })));
+}
 export function sendJobCommand({ id, jobCommandDto }: {
     id: JobName;
     jobCommandDto: JobCommandDto;
@@ -2005,24 +2099,14 @@ export function updateLibrary({ id, updateLibraryDto }: {
         body: updateLibraryDto
     })));
 }
-export function removeOfflineFiles({ id }: {
+export function scanLibrary({ id }: {
     id: string;
 }, opts?: Oazapfts.RequestOpts) {
-    return oazapfts.ok(oazapfts.fetchText(`/libraries/${encodeURIComponent(id)}/removeOffline`, {
+    return oazapfts.ok(oazapfts.fetchText(`/libraries/${encodeURIComponent(id)}/scan`, {
         ...opts,
         method: "POST"
     }));
 }
-export function scanLibrary({ id, scanLibraryDto }: {
-    id: string;
-    scanLibraryDto: ScanLibraryDto;
-}, opts?: Oazapfts.RequestOpts) {
-    return oazapfts.ok(oazapfts.fetchText(`/libraries/${encodeURIComponent(id)}/scan`, oazapfts.json({
-        ...opts,
-        method: "POST",
-        body: scanLibraryDto
-    })));
-}
 export function getLibraryStatistics({ id }: {
     id: string;
 }, opts?: Oazapfts.RequestOpts) {
@@ -2082,20 +2166,6 @@ export function reverseGeocode({ lat, lon }: {
         ...opts
     }));
 }
-export function getMapStyle({ key, theme }: {
-    key?: string;
-    theme: MapTheme;
-}, opts?: Oazapfts.RequestOpts) {
-    return oazapfts.ok(oazapfts.fetchJson<{
-        status: 200;
-        data: object;
-    }>(`/map/style.json${QS.query(QS.explode({
-        key,
-        theme
-    }))}`, {
-        ...opts
-    }));
-}
 export function searchMemories(opts?: Oazapfts.RequestOpts) {
     return oazapfts.ok(oazapfts.fetchJson<{
         status: 200;
@@ -2173,10 +2243,26 @@ export function addMemoryAssets({ id, bulkIdsDto }: {
         body: bulkIdsDto
     })));
 }
+export function getNotificationTemplate({ name, templateDto }: {
+    name: string;
+    templateDto: TemplateDto;
+}, opts?: Oazapfts.RequestOpts) {
+    return oazapfts.ok(oazapfts.fetchJson<{
+        status: 200;
+        data: TemplateResponseDto;
+    }>(`/notifications/templates/${encodeURIComponent(name)}`, oazapfts.json({
+        ...opts,
+        method: "POST",
+        body: templateDto
+    })));
+}
 export function sendTestEmail({ systemConfigSmtpDto }: {
     systemConfigSmtpDto: SystemConfigSmtpDto;
 }, opts?: Oazapfts.RequestOpts) {
-    return oazapfts.ok(oazapfts.fetchText("/notifications/test-email", oazapfts.json({
+    return oazapfts.ok(oazapfts.fetchJson<{
+        status: 200;
+        data: TestEmailResponseDto;
+    }>("/notifications/test-email", oazapfts.json({
         ...opts,
         method: "POST",
         body: systemConfigSmtpDto
@@ -2276,7 +2362,9 @@ export function updatePartner({ id, updatePartnerDto }: {
         body: updatePartnerDto
     })));
 }
-export function getAllPeople({ page, size, withHidden }: {
+export function getAllPeople({ closestAssetId, closestPersonId, page, size, withHidden }: {
+    closestAssetId?: string;
+    closestPersonId?: string;
     page?: number;
     size?: number;
     withHidden?: boolean;
@@ -2285,6 +2373,8 @@ export function getAllPeople({ page, size, withHidden }: {
         status: 200;
         data: PeopleResponseDto;
     }>(`/people${QS.query(QS.explode({
+        closestAssetId,
+        closestPersonId,
         page,
         size,
         withHidden
@@ -2339,19 +2429,6 @@ export function updatePerson({ id, personUpdateDto }: {
         body: personUpdateDto
     })));
 }
-/**
- * This property was deprecated in v1.113.0
- */
-export function getPersonAssets({ id }: {
-    id: string;
-}, opts?: Oazapfts.RequestOpts) {
-    return oazapfts.ok(oazapfts.fetchJson<{
-        status: 200;
-        data: AssetResponseDto[];
-    }>(`/people/${encodeURIComponent(id)}/assets`, {
-        ...opts
-    }));
-}
 export function mergePerson({ id, mergePersonDto }: {
     id: string;
     mergePersonDto: MergePersonDto;
@@ -2443,7 +2520,7 @@ export function getExploreData(opts?: Oazapfts.RequestOpts) {
         ...opts
     }));
 }
-export function searchMetadata({ metadataSearchDto }: {
+export function searchAssets({ metadataSearchDto }: {
     metadataSearchDto: MetadataSearchDto;
 }, opts?: Oazapfts.RequestOpts) {
     return oazapfts.ok(oazapfts.fetchJson<{
@@ -2481,6 +2558,18 @@ export function searchPlaces({ name }: {
         ...opts
     }));
 }
+export function searchRandom({ randomSearchDto }: {
+    randomSearchDto: RandomSearchDto;
+}, opts?: Oazapfts.RequestOpts) {
+    return oazapfts.ok(oazapfts.fetchJson<{
+        status: 200;
+        data: AssetResponseDto[];
+    }>("/search/random", oazapfts.json({
+        ...opts,
+        method: "POST",
+        body: randomSearchDto
+    })));
+}
 export function searchSmart({ smartSearchDto }: {
     smartSearchDto: SmartSearchDto;
 }, opts?: Oazapfts.RequestOpts) {
@@ -2615,6 +2704,14 @@ export function getServerVersion(opts?: Oazapfts.RequestOpts) {
         ...opts
     }));
 }
+export function getVersionHistory(opts?: Oazapfts.RequestOpts) {
+    return oazapfts.ok(oazapfts.fetchJson<{
+        status: 200;
+        data: ServerVersionHistoryResponseDto[];
+    }>("/server/version-history", {
+        ...opts
+    }));
+}
 export function deleteAllSessions(opts?: Oazapfts.RequestOpts) {
     return oazapfts.ok(oazapfts.fetchText("/sessions", {
         ...opts,
@@ -3057,13 +3154,19 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key
     }));
 }
 export function emptyTrash(opts?: Oazapfts.RequestOpts) {
-    return oazapfts.ok(oazapfts.fetchText("/trash/empty", {
+    return oazapfts.ok(oazapfts.fetchJson<{
+        status: 200;
+        data: TrashResponseDto;
+    }>("/trash/empty", {
         ...opts,
         method: "POST"
     }));
 }
 export function restoreTrash(opts?: Oazapfts.RequestOpts) {
-    return oazapfts.ok(oazapfts.fetchText("/trash/restore", {
+    return oazapfts.ok(oazapfts.fetchJson<{
+        status: 200;
+        data: TrashResponseDto;
+    }>("/trash/restore", {
         ...opts,
         method: "POST"
     }));
@@ -3071,7 +3174,10 @@ export function restoreTrash(opts?: Oazapfts.RequestOpts) {
 export function restoreAssets({ bulkIdsDto }: {
     bulkIdsDto: BulkIdsDto;
 }, opts?: Oazapfts.RequestOpts) {
-    return oazapfts.ok(oazapfts.fetchText("/trash/restore/assets", oazapfts.json({
+    return oazapfts.ok(oazapfts.fetchJson<{
+        status: 200;
+        data: TrashResponseDto;
+    }>("/trash/restore/assets", oazapfts.json({
         ...opts,
         method: "POST",
         body: bulkIdsDto
@@ -3352,8 +3458,9 @@ export enum Reason {
     UnsupportedFormat = "unsupported-format"
 }
 export enum AssetJobName {
-    RegenerateThumbnail = "regenerate-thumbnail",
+    RefreshFaces = "refresh-faces",
     RefreshMetadata = "refresh-metadata",
+    RegenerateThumbnail = "regenerate-thumbnail",
     TranscodeVideo = "transcode-video"
 }
 export enum AssetMediaSize {
@@ -3364,6 +3471,11 @@ export enum EntityType {
     Asset = "ASSET",
     Album = "ALBUM"
 }
+export enum ManualJobName {
+    PersonCleanup = "person-cleanup",
+    TagCleanup = "tag-cleanup",
+    UserCleanup = "user-cleanup"
+}
 export enum JobName {
     ThumbnailGeneration = "thumbnailGeneration",
     MetadataExtraction = "metadataExtraction",
@@ -3378,7 +3490,8 @@ export enum JobName {
     Search = "search",
     Sidecar = "sidecar",
     Library = "library",
-    Notifications = "notifications"
+    Notifications = "notifications",
+    BackupDatabase = "backupDatabase"
 }
 export enum JobCommand {
     Start = "start",
@@ -3387,10 +3500,6 @@ export enum JobCommand {
     Empty = "empty",
     ClearFailed = "clear-failed"
 }
-export enum MapTheme {
-    Light = "light",
-    Dark = "dark"
-}
 export enum MemoryType {
     OnThisDay = "on_this_day"
 }
@@ -3438,7 +3547,8 @@ export enum TranscodeHWAccel {
 export enum AudioCodec {
     Mp3 = "mp3",
     Aac = "aac",
-    Libopus = "libopus"
+    Libopus = "libopus",
+    PcmS16Le = "pcm_s16le"
 }
 export enum VideoContainer {
     Mov = "mov",
diff --git a/readme_i18n/README_ar_JO.md b/readme_i18n/README_ar_JO.md
index 7df39d226b..8fa4ac1195 100644
--- a/readme_i18n/README_ar_JO.md
+++ b/readme_i18n/README_ar_JO.md
@@ -32,6 +32,7 @@
   <a href="README_ru_RU.md">Русский</a>
   <a href="README_pt_BR.md">Português Brasileiro</a>
   <a href="README_sv_SE.md">Svenska</a>
+  <a href="README_th_TH.md">ภาษาไทย</a>
 </p>
 
 ## تنصل
diff --git a/readme_i18n/README_ca_ES.md b/readme_i18n/README_ca_ES.md
index ed14649e0a..66a8b584fd 100644
--- a/readme_i18n/README_ca_ES.md
+++ b/readme_i18n/README_ca_ES.md
@@ -32,6 +32,7 @@
   <a href="README_pt_BR.md">Português Brasileiro</a>
   <a href="README_sv_SE.md">Svenska</a>
   <a href="README_ar_JO.md">العربية</a>
+  <a href="README_th_TH.md">ภาษาไทย</a>
 </p>
 
 ## Avís legal
diff --git a/readme_i18n/README_de_DE.md b/readme_i18n/README_de_DE.md
index 7a59e3444e..70ad472aa1 100644
--- a/readme_i18n/README_de_DE.md
+++ b/readme_i18n/README_de_DE.md
@@ -32,6 +32,8 @@
   <a href="README_pt_BR.md">Português Brasileiro</a>
   <a href="README_sv_SE.md">Svenska</a>
   <a href="README_ar_JO.md">العربية</a>
+  <a href="README_vi_VN.md">Tiếng Việt</a>
+  <a href="README_th_TH.md">ภาษาไทย</a>
 </p>
 
 ## Warnung
@@ -101,6 +103,8 @@ Für die Handy-App kannst Du `https://demo.immich.app/api` als `Server Endpoint
 | Offline Unterstützung                                | Ja     | Nein  |
 | Schreibgeschützte Gallerie                           | Ja     | Ja    |
 | Gestapelte Bilder                                    | Ja     | Ja    |
+| Tags                                                 | Nein   | Ja    |
+| Ordner-Ansicht                                       | Nein   | Ja    |
 
 
 ## Übersetzungen
diff --git a/readme_i18n/README_es_ES.md b/readme_i18n/README_es_ES.md
index 726a504526..0b0dbf919d 100644
--- a/readme_i18n/README_es_ES.md
+++ b/readme_i18n/README_es_ES.md
@@ -32,6 +32,7 @@
   <a href="README_pt_BR.md">Português Brasileiro</a>
   <a href="README_sv_SE.md">Svenska</a>
   <a href="README_ar_JO.md">العربية</a>
+  <a href="README_th_TH.md">ภาษาไทย</a>
 </p>
 
 ## Advertencia
diff --git a/readme_i18n/README_fr_FR.md b/readme_i18n/README_fr_FR.md
index da52fe28a6..e2f979d254 100644
--- a/readme_i18n/README_fr_FR.md
+++ b/readme_i18n/README_fr_FR.md
@@ -32,6 +32,7 @@
   <a href="README_pt_BR.md">Português Brasileiro</a>
   <a href="README_sv_SE.md">Svenska</a>
   <a href="README_ar_JO.md">العربية</a>
+  <a href="README_th_TH.md">ภาษาไทย</a>
 </p>
 
 ## Clause de non-responsabilité
diff --git a/readme_i18n/README_it_IT.md b/readme_i18n/README_it_IT.md
index 1523143f06..7208df7e24 100644
--- a/readme_i18n/README_it_IT.md
+++ b/readme_i18n/README_it_IT.md
@@ -32,6 +32,7 @@
   <a href="README_pt_BR.md">Português Brasileiro</a>
   <a href="README_sv_SE.md">Svenska</a>
   <a href="README_ar_JO.md">العربية</a>
+  <a href="README_th_TH.md">ภาษาไทย</a>
 </p>
 
 ## Declino di responsabilità
diff --git a/readme_i18n/README_ja_JP.md b/readme_i18n/README_ja_JP.md
index 98ff8e68d9..828afa9812 100644
--- a/readme_i18n/README_ja_JP.md
+++ b/readme_i18n/README_ja_JP.md
@@ -32,6 +32,7 @@
   <a href="README_pt_BR.md">Português Brasileiro</a>
   <a href="README_sv_SE.md">Svenska</a>
   <a href="README_ar_JO.md">العربية</a>
+  <a href="README_th_TH.md">ภาษาไทย</a>
 </p>
 
 ## 免責事項
diff --git a/readme_i18n/README_ko_KR.md b/readme_i18n/README_ko_KR.md
index 66df040d75..8b280e0a9b 100644
--- a/readme_i18n/README_ko_KR.md
+++ b/readme_i18n/README_ko_KR.md
@@ -33,6 +33,7 @@
 <a href="README_pt_BR.md">Português Brasileiro</a>
 <a href="README_sv_SE.md">Svenska</a>
 <a href="README_ar_JO.md">العربية</a>
+<a href="README_th_TH.md">ภาษาไทย</a>
 
 </p>
 
diff --git a/readme_i18n/README_nl_NL.md b/readme_i18n/README_nl_NL.md
index 1c877d9d3e..e1cf6d66f5 100644
--- a/readme_i18n/README_nl_NL.md
+++ b/readme_i18n/README_nl_NL.md
@@ -32,6 +32,7 @@
   <a href="README_pt_BR.md">Português Brasileiro</a>
   <a href="README_sv_SE.md">Svenska</a>
   <a href="README_ar_JO.md">العربية</a>
+  <a href="README_th_TH.md">ภาษาไทย</a>
 </p>
 
 ## Disclaimer
diff --git a/readme_i18n/README_pt_BR.md b/readme_i18n/README_pt_BR.md
index d872b8435b..5468ebb4c4 100644
--- a/readme_i18n/README_pt_BR.md
+++ b/readme_i18n/README_pt_BR.md
@@ -1,84 +1,91 @@
-<p align="center"> 
-  <br/>  
+<p align="center">
+  <br/>
   <a href="https://opensource.org/license/agpl-v3"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="Licença: AGPLv3"></a>
   <a href="https://discord.immich.app">
     <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
   </a>
-  <br/>  
-  <br/>   
+  <br/>
+  <br/>
 </p>
 
 <p align="center">
-<img src="design/immich-logo.svg" width="150" title="Login com URL customizada">
+<img src="../design/immich-logo-stacked-light.svg" width="150" title="Immich Logo">
 </p>
-<h3 align="center">Immich - Solução self-hosted de alta performance para backup de fotos e vídeos</h3>
+<h3 align="center">Solução self-hosted de alta performance para backup de fotos e vídeos</h3>
 <br/>
 <a href="https://immich.app">
-<img src="design/immich-screenshots.png" title="Captura de tela princial">
+<img src="../design/immich-screenshots.png" title="Captura de tela princial">
 </a>
 <br/>
 <p align="center">
-  <a href="../README.md">English</a>
-  <a href="README_ca_ES.md">Català</a>
-  <a href="README_es_ES.md">Español</a>
-  <a href="README_fr_FR.md">Français</a>
-  <a href="README_it_IT.md">Italiano</a>
-  <a href="README_ja_JP.md">日本語</a>
-  <a href="README_ko_KR.md">한국어</a>
-  <a href="README_de_DE.md">Deutsch</a>
-  <a href="README_nl_NL.md">Nederlands</a>
-  <a href="README_tr_TR.md">Türkçe</a>
-  <a href="README_zh_CN.md">中文</a>
-  <a href="README_ru_RU.md">Русский</a>
-  <a href="README_sv_SE.md">Svenska</a>
-  <a href="README_ar_JO.md">العربية</a>
+
+<a href="../README.md">English</a>
+<a href="README_ca_ES.md">Català</a>
+<a href="README_es_ES.md">Español</a>
+<a href="README_fr_FR.md">Français</a>
+<a href="README_it_IT.md">Italiano</a>
+<a href="README_ja_JP.md">日本語</a>
+<a href="README_ko_KR.md">한국어</a>
+<a href="README_de_DE.md">Deutsch</a>
+<a href="README_nl_NL.md">Nederlands</a>
+<a href="README_tr_TR.md">Türkçe</a>
+<a href="README_zh_CN.md">中文</a>
+<a href="README_ru_RU.md">Русский</a>
+<a href="README_sv_SE.md">Svenska</a>
+<a href="README_ar_JO.md">العربية</a>
+<a href="README_vi_VN.md">Tiếng Việt</a>
+<a href="README_th_TH.md">ภาษาไทย</a>
+
 </p>
 
 ## Avisos
 
 - ⚠️ Este projeto está sob **desenvolvimento constante**.
-- ⚠️ Podem ocorrer bugs e _breaking changes_ (alterações que quebram a compatibilidade com versões anteriores).
-- ⚠️ **Não use esta solução como a única forma de fazer backup das suas fotos e vídeos.**
-- ⚠️ Sempre siga o plano [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) de backup para as suas mídias preciosas!
+- ⚠️ Podem ocorrer bugs e _breaking changes_ (alterações que quebram a
+  compatibilidade com versões anteriores).
+- ⚠️ **Não use esta solução como a única forma de fazer backup das suas fotos e
+  vídeos.**
+- ⚠️ Sempre siga o plano
+  [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) de backup
+  para as suas mídias preciosas!
 
-## Conteúdo
+> [!NOTE]
+> Você pode encontrar a documentação principal, incluindo guias de instalação, em https://immich.app/.
 
-- [Documentação Oficial](https://immich.app/docs)
-- [Roadmap](https://github.com/orgs/immich-app/projects/1)
-- [Demonstração](#demo)
-- [Recursos](#features)
-- [Introdução](https://immich.app/docs/overview/introduction)
+## Links
+
+- [Documentação](https://immich.app/docs)
+- [Sobre](https://immich.app/docs/overview/introduction)
 - [Instalação](https://immich.app/docs/install/requirements)
+- [Roadmap](https://github.com/orgs/immich-app/projects/1)
+- [Demonstração](#demonstração)
+- [Funcionalidades](#funcionalidades)
+- [Traduções](https://immich.app/docs/developer/translations)
 - [Diretrizes de Contribuição](https://immich.app/docs/overview/support-the-project)
 
-## Documentação
-
-Você pode encontrar a documentação principal, incluindo guias de instalação, em https://immich.app/.
-
 ## Demonstração
 
-Você pode acessar a demonstração web em https://demo.immich.app
+Acesse a demonstração [aqui](https://demo.immich.app). A demonstração está
+hospedada no Nível Gratuito da Oracle VM em Amsterdam com um processador 2.4Ghz
+quad-core ARM64 e 24GB de RAM.
 
-No aplicativo para dispositivos móveis, você pode usar `https://demo.immich.app/api` no campo `Server Endpoint URL`
+No aplicativo para dispositivos móveis, você pode usar
+`https://demo.immich.app/api` no campo `Server Endpoint URL`
 
-```bash title="Credenciais de Demonstração"
-Credenciais de Demonstração
-email: demo@immich.app
-senha: demo
-```
+### Credenciais de login
 
-```
-Especificações: Nível Gratuito da Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
-```
+| Email           | Senha |
+| --------------- | ----- |
+| demo@immich.app | demo  |
 
 ## Atividades
+
 ![Atividades](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Imagem de Analytics do Repobeats")
 
-## Recursos
+## Funcionalidades
 
-
-| Recursos                                            | Aplicativo Móvel | Web |
-|:----------------------------------------------------|------------------|-----|
+| Funcionalidades                                     | Aplicativo Móvel | Web |
+| :-------------------------------------------------- | ---------------- | --- |
 | Fazer upload e visualizar fotos e vídeos            | Sim              | Sim |
 | Backup automático ao abrir o aplicativo             | Sim              | N/A |
 | Prevenir a duplicação de arquivos                   | Sim              | Sim |
@@ -88,17 +95,17 @@ Especificações: Nível Gratuito da Oracle VM - Amsterdam - 2.4Ghz quad-core AR
 | Criação de álbuns e álbuns compartilhados           | Sim              | Sim |
 | Barra de rolagem arrastável                         | Sim              | Sim |
 | Suporta formatos RAW                                | Sim              | Sim |
-| Visualização de metadados (EXIF, map)               | Sim              | Sim |
-| Pesquisar por metadados, objetos, rostos, and CLIP  | Sim              | Sim |
+| Visualização de metadados (EXIF, mapa)              | Sim              | Sim |
+| Pesquisar por metadados, objetos, rostos, e CLIP    | Sim              | Sim |
 | Funções administrativas (gerenciamento de usuários) | Não              | Sim |
 | Backup em segundo plano                             | Sim              | N/A |
-| Virtual scroll                                      | Sim              | Sim |
+| Rolagem virtual                                     | Sim              | Sim |
 | Suporte OAuth                                       | Sim              | Sim |
 | Chaves de API                                       | N/A              | Sim |
-| Backup e visualização de LivePhoto/MotionPhoto      | Sim              | Sim |
+| Backup e reprodução de LivePhoto/MotionPhoto        | Sim              | Sim |
 | Visualização de imagens 360º                        | Não              | Sim |
 | Estrutura de armazenamento definida pelo usuário    | Sim              | Sim |
-| Compartilhar com o público                          | Não              | Sim |
+| Compartilhar com o público                          | Sim              | Sim |
 | Arquivo e Favoritos                                 | Sim              | Sim |
 | Mapa Global                                         | Sim              | Sim |
 | Compartilhamento com parceiro                       | Sim              | Sim |
@@ -108,6 +115,29 @@ Especificações: Nível Gratuito da Oracle VM - Amsterdam - 2.4Ghz quad-core AR
 | Galeria em modo apenas leitura                      | Sim              | Sim |
 | Empilhamento de fotos                               | Sim              | Sim |
 
+## Traduções
+
+Leia mais sobre as traduções
+[aqui](https://immich.app/docs/developer/translations).
+
+<a href="https://hosted.weblate.org/engage/immich/">
+<img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="Status da tradução" />
+</a>
+
+## Atividade do repositório
+
+![Activities](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Imagem de análise de atividade Repobeats")
+
+## Histórico de estrelas
+
+<a href="https://star-history.com/#immich-app/immich&Date">
+ <picture>
+   <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date&theme=dark" />
+   <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" />
+   <img alt="Gráfico de histórico de estrelas" src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" width="100%" />
+ </picture>
+</a>
+
 ## Contribuidores
 
 <a href="https://github.com/alextran1502/immich/graphs/contributors">
diff --git a/readme_i18n/README_ru_RU.md b/readme_i18n/README_ru_RU.md
index 11a2a34f33..0ff3e3f08f 100644
--- a/readme_i18n/README_ru_RU.md
+++ b/readme_i18n/README_ru_RU.md
@@ -32,6 +32,7 @@
   <a href="README_pt_BR.md">Português Brasileiro</a>
   <a href="README_sv_SE.md">Svenska</a>
   <a href="README_ar_JO.md">العربية</a>
+  <a href="README_th_TH.md">ภาษาไทย</a>
 </p>
 
 ## Предупреждение
diff --git a/readme_i18n/README_sv_SE.md b/readme_i18n/README_sv_SE.md
index 3673eab57c..29706acb55 100644
--- a/readme_i18n/README_sv_SE.md
+++ b/readme_i18n/README_sv_SE.md
@@ -33,6 +33,7 @@
   <a href="README_ru_RU.md">Русский</a>
   <a href="README_pt_BR.md">Português Brasileiro</a>
   <a href="README_ar_JO.md">العربية</a>
+  <a href="README_th_TH.md">ภาษาไทย</a>
 </p>
 
 ## Ansvarsfriskrivning
diff --git a/readme_i18n/README_th_TH.md b/readme_i18n/README_th_TH.md
new file mode 100644
index 0000000000..6a6b70d435
--- /dev/null
+++ b/readme_i18n/README_th_TH.md
@@ -0,0 +1,134 @@
+<p align="center"> 
+  <br/>  
+  <a href="https://opensource.org/license/agpl-v3"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: AGPLv3"></a>
+  <a href="https://discord.immich.app">
+    <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
+  </a>
+  <br/>  
+  <br/>   
+</p>
+
+<p align="center">
+  <img src="../design/immich-logo-stacked-light.svg" width="300" title="การเข้าสู่ระบบด้วย URL แบบกำหนดเอง">
+</p>
+
+<h3 align="center">โซลูชันการจัดการภาพถ่ายและวิดีโอแบบโฮสต์เองที่มีประสิทธิภาพสูง</h3>
+<br/>
+
+<a href="https://immich.app">
+  <img src="../design/immich-screenshots.png" title="ภาพหน้าจอหลัก">
+</a>
+<br/>
+
+<p align="center">
+  <a href="../README.md">English</a>
+  <a href="README_ca_ES.md">Català</a>
+  <a href="README_es_ES.md">Español</a>
+  <a href="README_fr_FR.md">Français</a>
+  <a href="README_it_IT.md">Italiano</a>
+  <a href="README_ja_JP.md">日本語</a>
+  <a href="README_ko_KR.md">한국어</a>
+  <a href="README_de_DE.md">Deutsch</a>
+  <a href="README_nl_NL.md">Nederlands</a>
+  <a href="README_tr_TR.md">Türkçe</a>
+  <a href="README_zh_CN.md">中文</a>
+  <a href="README_ru_RU.md">Русский</a>
+  <a href="README_pt_BR.md">Português Brasileiro</a>
+  <a href="README_sv_SE.md">Svenska</a>
+  <a href="README_ar_JO.md">العربية</a>
+  <a href="README_vi_VN.md">Tiếng Việt</a>
+</p>
+
+## ข้อจำกัดความรับผิดชอบ
+
+- ⚠️ โพรเจกต์นี้กำลังอยู่ระหว่างการพัฒนา**ที่มีการเปลี่ยนแปลงบ่อยมาก**
+- ⚠️ คาดว่าจะมีข้อผิดพลาดและการเปลี่ยนแปลงที่ส่งผลเสีย
+- ⚠️ **ห้ามใช้แอปนี้เป็นวิธีการเดียวในการจัดเก็บภาพถ่ายและวิดีโอของคุณ**
+- ⚠️ ปฏิบัติตามแผนการสำรองข้อมูลแบบ [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) สำหรับภาพถ่ายและวิดีโอที่สำคัญของคุณอยู่เสมอ!
+
+> [!NOTE]
+> คุณสามารถหาคู่มือหลัก รวมถึงคู่มือการติดตั้ง ได้ที่ https://immich.app/
+
+## ลิงก์
+
+- [คู่มือ](https://immich.app/docs)
+- [เกี่ยวกับ](https://immich.app/docs/overview/introduction)
+- [การติดตั้ง](https://immich.app/docs/install/requirements)
+- [โรดแมป](https://immich.app/roadmap)
+- [สาธิต](#สาธิต)
+- [คุณสมบัติ](#คุณสมบัติ)
+- [การแปลภาษา](https://immich.app/docs/developer/translations)
+- [สนับสนุนโพรเจกต์](https://immich.app/docs/overview/support-the-project)
+
+## สาธิต
+
+เข้าถึงการสาธิตได้ [ที่นี่](https://demo.immich.app) โดยการสาธิตนี้ทำงานบน Oracle VM Free-tier ตั้งอยู่ที่อัมสเตอร์ดัม ใช้ซีพียู ARM64 quad-core 2.4Ghz และแรม 24GB
+
+สำหรับแอปมือถือ คุณสามารถใช้ `https://demo.immich.app/api` เป็น `Server Endpoint URL`
+
+### ข้อมูลการเข้าสู่ระบบ
+
+| อีเมล           | รหัสผ่าน |
+| --------------- | -------- |
+| demo@immich.app | demo     |
+
+## คุณสมบัติ
+
+| คุณสมบัติ                                  | มือถือ | เว็บ   |
+| :----------------------------------------- | ------ | ------ |
+| อัปโหลดและดูวิดีโอและภาพถ่าย               | ใช่    | ใช่    |
+| การสำรองข้อมูลอัตโนมัติเมื่อเปิดแอป        | ใช่    | N/A    |
+| ป้องกันการซ้ำซ้อนของไฟล์                   | ใช่    | ใช่    |
+| เลือกอัลบั้มสำหรับสำรองข้อมูล              | ใช่    | N/A    |
+| ดาวน์โหลดภาพถ่ายและวิดีโอไปยังอุปกรณ์      | ใช่    | ใช่    |
+| รองรับผู้ใช้หลายคน                         | ใช่    | ใช่    |
+| อัลบั้มและอัลบั้มแชร์                      | ใช่    | ใช่    |
+| แถบเลื่อนแบบลากได้                         | ใช่    | ใช่    |
+| รองรับรูปแบบไฟล์ RAW                       | ใช่    | ใช่    |
+| ดูข้อมูลเมตา (EXIF, แผนที่)                | ใช่    | ใช่    |
+| ค้นหาจากข้อมูลเมตา วัตถุ ใบหน้า และ CLIP   | ใช่    | ใช่    |
+| ฟังก์ชันการจัดการผู้ดูแลระบบ               | ไม่ใช่ | ใช่    |
+| การสำรองข้อมูลพื้นหลัง                     | ใช่    | N/A    |
+| การเลื่อนแบบเสมือน                         | ใช่    | ใช่    |
+| รองรับ OAuth                               | ใช่    | ใช่    |
+| คีย์ API                                   | N/A    | ใช่    |
+| การสำรองและเล่น LivePhoto/MotionPhoto      | ใช่    | ใช่    |
+| รองรับการแสดงภาพ 360 องศา                  | ไม่ใช่ | ใช่    |
+| โครงสร้างการจัดเก็บข้อมูลที่ผู้ใช้กำหนดเอง | ใช่    | ใช่    |
+| การแชร์สาธารณะ                             | ใช่    | ใช่    |
+| การจัดเก็บและรายการโปรด                    | ใช่    | ใช่    |
+| แผนที่ทั่วโลก                              | ใช่    | ใช่    |
+| การแชร์กับคู่หู                            | ใช่    | ใช่    |
+| การจดจำใบหน้าและการจัดกลุ่ม                | ใช่    | ใช่    |
+| ความทรงจำ (x ปีที่แล้ว)                    | ใช่    | ใช่    |
+| รองรับแบบออฟไลน์                           | ใช่    | ไม่ใช่ |
+| แกลเลอรีแบบอ่านอย่างเดียว                  | ใช่    | ใช่    |
+| ภาพถ่ายซ้อนกัน                             | ใช่    | ใช่    |
+
+## การแปลภาษา
+
+อ่านเพิ่มเติมเกี่ยวกับการแปลภาษา [ที่นี่](https://immich.app/docs/developer/translations)
+
+<a href="https://hosted.weblate.org/engage/immich/">
+  <img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="สถานะการแปล" />
+</a>
+
+## กิจกรรมของคลังเก็บข้อมูล
+
+![กิจกรรม](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "ภาพการวิเคราะห์ของ Repobeats")
+
+## ประวัติการให้ดาว
+
+<a href="https://star-history.com/#immich-app/immich&Date">
+ <picture>
+   <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date&theme=dark" />
+   <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" />
+   <img alt="แผนภูมิประวัติการให้ดาว" src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" width="100%" />
+ </picture>
+</a>
+
+## ผู้ร่วมพัฒนา
+
+<a href="https://github.com/alextran1502/immich/graphs/contributors">
+  <img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
+</a>
diff --git a/readme_i18n/README_tr_TR.md b/readme_i18n/README_tr_TR.md
index f95d914880..6bf23be5f8 100644
--- a/readme_i18n/README_tr_TR.md
+++ b/readme_i18n/README_tr_TR.md
@@ -32,6 +32,7 @@
   <a href="README_pt_BR.md">Português Brasileiro</a>
   <a href="README_sv_SE.md">Svenska</a>
   <a href="README_ar_JO.md">العربية</a>
+  <a href="README_th_TH.md">ภาษาไทย</a>
 </p>
 
 ## Feragatname
diff --git a/readme_i18n/README_vi_VN.md b/readme_i18n/README_vi_VN.md
new file mode 100644
index 0000000000..69d7a151be
--- /dev/null
+++ b/readme_i18n/README_vi_VN.md
@@ -0,0 +1,134 @@
+<p align="center"> 
+  <br/>  
+  <a href="https://opensource.org/license/agpl-v3"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="Giấy phép: AGPLv3"></a>
+  <a href="https://discord.immich.app">
+    <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
+  </a>
+  <br/>  
+  <br/>   
+</p>
+
+<p align="center">
+<img src="../design/immich-logo-stacked-light.svg" width="300" title="Đăng nhập bằng URL Tuỳ chỉnh">
+</p>
+<h3 align="center">Giải pháp quản lý ảnh và video tự lưu trữ hiệu suất cao</h3>
+<br/>
+<a href="https://immich.app">
+<img src="../design/immich-screenshots.png" title="Ảnh chụp màn hình chính">
+</a>
+<br/>
+<p align="center">
+
+<a href="../README.md">English</a>
+<a href="README_ca_ES.md">Català</a>
+<a href="README_es_ES.md">Español</a>
+<a href="README_fr_FR.md">Français</a>
+<a href="README_it_IT.md">Italiano</a>
+<a href="README_ja_JP.md">日本語</a>
+<a href="README_ko_KR.md">한국어</a>
+<a href="README_de_DE.md">Deutsch</a>
+<a href="README_nl_NL.md">Nederlands</a>
+<a href="README_tr_TR.md">Türkçe</a>
+<a href="README_zh_CN.md">中文</a>
+<a href="README_ru_RU.md">Русский</a>
+<a href="README_pt_BR.md">Português Brasileiro</a>
+<a href="README_sv_SE.md">Svenska</a>
+<a href="README_ar_JO.md">العربية</a>
+<a href="README_vi_VN.md">Tiếng Việt</a>
+<a href="README_th_TH.md">ภาษาไทย</a>
+
+</p>
+
+## Tuyên bố miễn trừ trách nhiệm
+
+- ⚠️ Dự án đang được phát triển **rất tích cực**.
+- ⚠️ Dự kiến ​​sẽ có lỗi và thay đổi đột ngột.
+- ⚠️ **Không sử dụng ứng dụng như là cách duy nhất để lưu trữ ảnh và video của bạn.**
+- ⚠️ Luôn tuân thủ kế hoạch sao lưu [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) cho những bức ảnh và video quý giá của bạn!
+
+> [!NOTE]
+> Bạn có thể tìm thấy tài liệu chính, bao gồm hướng dẫn cài đặt, tại https://immich.app/.
+
+## Liên kết
+
+- [Tài liệu](https://immich.app/docs)
+- [Giới thiệu](https://immich.app/docs/overview/introduction)
+- [Cài đặt](https://immich.app/docs/install/requirements)
+- [Lộ trình](https://immich.app/roadmap)
+- [Demo](#demo)
+- [Tính năng](#Tính-năng)
+- [Dịch thuật](https://immich.app/docs/developer/translations)
+- [Đóng góp](https://immich.app/docs/overview/support-the-project)
+
+## Demo
+
+Truy cập bản demo [tại đây](https://demo.immich.app). Bản demo đang chạy trên máy ảo Oracle Free-tier ở Amsterdam với CPU ARM64 lõi tứ 2,4 GHz và RAM 24 GB.
+
+Đối với ứng dụng di động, bạn có thể sử dụng `https://demo.immich.app/api` cho `Server Endpoint URL`
+
+### Thông tin đăng nhập
+
+| Email           | Mật khẩu |
+| --------------- | -------- |
+| demo@immich.app | demo     |
+
+## Tính năng
+
+| Tính năng                                            | Mobile | Web   |
+| :--------------------------------------------------- | ------ | ----- |
+| Tải lên và xem video, ảnh                            | Có     | Có    |
+| Tự động sao lưu khi ứng dụng được mở                 | Có     | N/A   |
+| Ngăn chặn sự trùng lặp nội dung                      | Có     | Có    |
+| Album được chọn để sao lưu                           | Có     | N/A   |
+| Tải ảnh và video xuống thiết bị cục bộ               | Có     | Có    |
+| Hỗ trợ nhiều người dùng                              | Có     | Có    |
+| Album và Album được chia sẻ                          | Có     | Có    |
+| Thanh cuộn có thể chà / kéo                          | Có     | Có    |
+| Hỗ trợ định dạng raw                                 | Có     | Có    |
+| Xem metadata (EXIF, bản đồ)                          | Có     | Có    |
+| Tìm kiếm theo metadata, đối tượng, khuôn mặt và CLIP | Có     | Có    |
+| Chức năng quản trị (quản lý người dùng)              | Không  | Có    |
+| Sao lưu trong nền                                    | Có     | N/A   |
+| Cuộn ảo                                              | Có     | Có    |
+| Hỗ trợ OAuth                                         | Có     | Có    |
+| API Keys                                             | N/A    | Có    |
+| Sao lưu và phát lại Live Photo/Motion Photo          | Có     | Có    |
+| Hỗ trợ hiển thị hình ảnh 360 độ                      | Không  | Có    |
+| Cấu trúc lưu trữ do người dùng xác định              | Có     | Có    |
+| Chia sẻ công khai                                    | Có     | Có    |
+| Lưu trữ và Yêu thích                                 | Có     | Có    |
+| Bản đồ toàn cầu                                      | Có     | Có    |
+| Chia sẻ đối tác                                      | Có     | Có    |
+| Nhận dạng khuôn mặt và phân cụm                      | Có     | Có    |
+| Kỷ niệm (x năm trước)                                | Có     | Có    |
+| Hỗ trợ ngoại tuyến                                   | Có     | Không |
+| Thư viện chỉ đọc                                     | Có     | Có    |
+| Ảnh xếp chồng                                        | Có     | Có    |
+
+## Dịch thuật
+
+Đọc thêm về dịch thuật [tại đây](https://immich.app/docs/developer/translations).
+
+<a href="https://hosted.weblate.org/engage/immich/">
+<img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="Tình trạng dịch thuật" />
+</a>
+
+## Hoạt động của repository
+
+![Hoạt động](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Hình ảnh phân tích Repobeats")
+
+## Lịch sử Đánh dấu sao
+
+<a href="https://star-history.com/#immich-app/immich&Date">
+ <picture>
+   <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date&theme=dark" />
+   <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" />
+   <img alt="Biểu đồ Lịch sử Đánh dấu" src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" width="100%" />
+ </picture>
+</a>
+
+## Người đóng góp
+
+<a href="https://github.com/alextran1502/immich/graphs/contributors">
+  <img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
+</a>
diff --git a/readme_i18n/README_zh_CN.md b/readme_i18n/README_zh_CN.md
index 6355cd65ed..463e8aca9f 100644
--- a/readme_i18n/README_zh_CN.md
+++ b/readme_i18n/README_zh_CN.md
@@ -36,7 +36,9 @@
   <a href="README_pt_BR.md">Português Brasileiro</a>
   <a href="README_sv_SE.md">Svenska</a>
   <a href="README_ar_JO.md">العربية</a>
-  
+  <a href="README_vi_VN.md">Tiếng Việt</a>
+  <a href="README_th_TH.md">ภาษาไทย</a>
+
 </p>
 
 ## 免责声明
@@ -104,6 +106,8 @@
 | 离线支持                                    | 是     | 否     |
 | 只读相册                                    | 是     | 是     |
 | 照片堆叠                                    | 是     | 是     |
+| 标签                                        | 否     | 是     |
+| 文件夹浏览                                  | 否     | 是     |
 
 ## 多语言
 
diff --git a/renovate.json b/renovate.json
index ccfb75b19c..39e0e7f811 100644
--- a/renovate.json
+++ b/renovate.json
@@ -15,7 +15,7 @@
       "groupName": "typescript-projects",
       "matchUpdateTypes": ["minor", "patch"],
       "excludePackagePrefixes": ["exiftool", "reflect-metadata"],
-      "excludePackageNames": ["node", "@types/node"],
+      "excludePackageNames": ["node", "@types/node", "@mapbox/mapbox-gl-rtl-text"],
       "schedule": "on tuesday"
     },
     {
diff --git a/server/.nvmrc b/server/.nvmrc
index 3516580bbb..1d9b7831ba 100644
--- a/server/.nvmrc
+++ b/server/.nvmrc
@@ -1 +1 @@
-20.17.0
+22.12.0
diff --git a/server/Dockerfile b/server/Dockerfile
index f615f8712a..4c1aecb8fa 100644
--- a/server/Dockerfile
+++ b/server/Dockerfile
@@ -1,5 +1,5 @@
 # dev build
-FROM ghcr.io/immich-app/base-server-dev:20240910@sha256:3fd455fe051bef63b1440753596e2afa34ff0513fe30aa71a5b76ebb2d751e9f AS dev
+FROM ghcr.io/immich-app/base-server-dev:20241224@sha256:6832c632c2a8cba5e20053ab694c9a8080e621841c784ed5d4675ef9dd203588 AS dev
 
 RUN apt-get install --no-install-recommends -yqq tini
 WORKDIR /usr/src/app
@@ -25,7 +25,7 @@ COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img
 COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl
 
 # web build
-FROM node:20.17.0-alpine3.20@sha256:2d07db07a2df6830718ae2a47db6fedce6745f5bcd174c398f2acdda90a11c03 AS web
+FROM node:22.12.0-alpine3.20@sha256:96cc8323e25c8cc6ddcb8b965e135cfd57846e8003ec0d7bcec16c5fd5f6d39f AS web
 
 WORKDIR /usr/src/open-api/typescript-sdk
 COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
@@ -37,11 +37,12 @@ WORKDIR /usr/src/app
 COPY web/package*.json web/svelte.config.js ./
 RUN npm ci
 COPY web ./
+COPY i18n ../i18n
 RUN npm run build
 
 
 # prod build
-FROM ghcr.io/immich-app/base-server-prod:20240910@sha256:4e03fe801b74eede63e91d2d9bce3a7b05699f536c211391f2d82a83c1f63470
+FROM ghcr.io/immich-app/base-server-prod:20241224@sha256:69da007c241a961d6927d3d03f1c83ef0ec5c70bf656bff3ced32546a777e6f6
 
 WORKDIR /usr/src/app
 ENV NODE_ENV=production \
@@ -76,7 +77,7 @@ ENV IMMICH_SOURCE_COMMIT=${BUILD_SOURCE_COMMIT}
 ENV IMMICH_SOURCE_URL=https://github.com/immich-app/immich/commit/${BUILD_SOURCE_COMMIT}
 
 VOLUME /usr/src/app/upload
-EXPOSE 3001
+EXPOSE 2283
 ENTRYPOINT ["tini", "--", "/bin/bash"]
 CMD ["start.sh"]
 
diff --git a/server/bin/immich-healthcheck b/server/bin/immich-healthcheck
index 6043e526aa..81528157e4 100755
--- a/server/bin/immich-healthcheck
+++ b/server/bin/immich-healthcheck
@@ -1,3 +1,22 @@
 #!/usr/bin/env bash
 
-node /usr/src/app/dist/utils/healthcheck.js
+if [[ ( $IMMICH_WORKERS_INCLUDE != '' && $IMMICH_WORKERS_INCLUDE != *api* ) || $IMMICH_WORKERS_EXCLUDE == *api* ]]; then
+        echo "API worker excluded, skipping";
+        exit 0;
+fi
+
+IMMICH_HOST="${IMMICH_HOST:-localhost}"
+IMMICH_PORT="${IMMICH_PORT:-2283}"
+
+result=$(curl -fsS -m 2 http://"$IMMICH_HOST":"$IMMICH_PORT"/api/server/ping)
+result_exit=$?
+
+if [ $result_exit != 0 ]; then
+        echo "Fail: exit code is $result_exit";
+        exit 1;
+fi
+
+if [ "$result" != "{\"res\":\"pong\"}" ]; then
+        echo "Fail: didn't reply with pong";
+        exit 1;
+fi
diff --git a/server/package-lock.json b/server/package-lock.json
index 1f9514fdde..3bdc0dc3da 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -1,30 +1,29 @@
 {
   "name": "immich",
-  "version": "1.115.0",
-  "lockfileVersion": 2,
+  "version": "1.123.0",
+  "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "immich",
-      "version": "1.115.0",
+      "version": "1.123.0",
       "license": "GNU Affero General Public License version 3",
       "dependencies": {
         "@nestjs/bullmq": "^10.0.1",
         "@nestjs/common": "^10.2.2",
-        "@nestjs/config": "^3.0.0",
         "@nestjs/core": "^10.2.2",
         "@nestjs/event-emitter": "^2.0.4",
         "@nestjs/platform-express": "^10.2.2",
         "@nestjs/platform-socket.io": "^10.2.2",
         "@nestjs/schedule": "^4.0.0",
-        "@nestjs/swagger": "^7.1.8",
+        "@nestjs/swagger": "^8.0.0",
         "@nestjs/typeorm": "^10.0.0",
         "@nestjs/websockets": "^10.2.2",
-        "@opentelemetry/auto-instrumentations-node": "^0.49.0",
+        "@opentelemetry/auto-instrumentations-node": "^0.54.0",
         "@opentelemetry/context-async-hooks": "^1.24.0",
-        "@opentelemetry/exporter-prometheus": "^0.53.0",
-        "@opentelemetry/sdk-node": "^0.53.0",
-        "@react-email/components": "^0.0.24",
+        "@opentelemetry/exporter-prometheus": "^0.56.0",
+        "@opentelemetry/sdk-node": "^0.56.0",
+        "@react-email/components": "^0.0.31",
         "@socket.io/redis-adapter": "^8.3.0",
         "archiver": "^7.0.0",
         "async-lock": "^1.4.0",
@@ -34,7 +33,7 @@
         "class-transformer": "^0.5.1",
         "class-validator": "^0.14.0",
         "cookie-parser": "^1.4.6",
-        "exiftool-vendored": "^28.1.0",
+        "exiftool-vendored": "^28.3.1",
         "fast-glob": "^3.3.2",
         "fluent-ffmpeg": "^2.1.2",
         "geo-tz": "^8.0.0",
@@ -52,18 +51,19 @@
         "openid-client": "^5.4.3",
         "pg": "^8.11.3",
         "picomatch": "^4.0.2",
-        "react": "^18.3.1",
-        "react-email": "^3.0.0",
+        "react": "^19.0.0",
+        "react-email": "^3.0.4",
         "reflect-metadata": "^0.2.0",
         "rxjs": "^7.8.1",
         "sanitize-filename": "^1.6.3",
         "semver": "^7.6.2",
         "sharp": "^0.33.0",
-        "sirv": "^2.0.4",
+        "sirv": "^3.0.0",
         "tailwindcss-preset-email": "^1.3.2",
         "thumbhash": "^0.1.1",
         "typeorm": "^0.3.17",
-        "ua-parser-js": "^1.0.35"
+        "ua-parser-js": "^1.0.35",
+        "validator": "^13.12.0"
       },
       "devDependencies": {
         "@eslint/eslintrc": "^3.1.0",
@@ -83,22 +83,24 @@
         "@types/lodash": "^4.14.197",
         "@types/mock-fs": "^4.13.1",
         "@types/multer": "^1.4.7",
-        "@types/node": "^20.16.5",
+        "@types/node": "^22.10.2",
         "@types/nodemailer": "^6.4.14",
         "@types/picomatch": "^3.0.0",
-        "@types/react": "^18.3.4",
+        "@types/pngjs": "^6.0.5",
+        "@types/react": "^19.0.0",
         "@types/semver": "^7.5.8",
         "@types/supertest": "^6.0.0",
         "@types/ua-parser-js": "^0.7.36",
-        "@typescript-eslint/eslint-plugin": "^8.0.0",
-        "@typescript-eslint/parser": "^8.0.0",
+        "@typescript-eslint/eslint-plugin": "^8.15.0",
+        "@typescript-eslint/parser": "^8.15.0",
         "@vitest/coverage-v8": "^2.0.5",
-        "eslint": "^9.0.0",
+        "eslint": "^9.14.0",
         "eslint-config-prettier": "^9.1.0",
         "eslint-plugin-prettier": "^5.1.3",
-        "eslint-plugin-unicorn": "^55.0.0",
+        "eslint-plugin-unicorn": "^56.0.1",
         "globals": "^15.9.0",
         "mock-fs": "^5.2.0",
+        "pngjs": "^7.0.0",
         "prettier": "^3.0.2",
         "prettier-plugin-organize-imports": "^4.0.0",
         "rimraf": "^6.0.0",
@@ -112,19 +114,11 @@
         "vitest": "^2.0.5"
       }
     },
-    "node_modules/@aashutoshrathi/word-wrap": {
-      "version": "1.2.6",
-      "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
-      "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/@alloc/quick-lru": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
       "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+      "license": "MIT",
       "peer": true,
       "engines": {
         "node": ">=10"
@@ -137,6 +131,7 @@
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
       "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+      "license": "Apache-2.0",
       "dependencies": {
         "@jridgewell/gen-mapping": "^0.3.5",
         "@jridgewell/trace-mapping": "^0.3.24"
@@ -146,10 +141,11 @@
       }
     },
     "node_modules/@angular-devkit/core": {
-      "version": "17.3.8",
-      "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.8.tgz",
-      "integrity": "sha512-Q8q0voCGudbdCgJ7lXdnyaxKHbNQBARH68zPQV72WT8NWy+Gw/tys870i6L58NWbBaCJEUcIj/kb6KoakSRu+Q==",
+      "version": "17.3.11",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz",
+      "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "ajv": "8.12.0",
         "ajv-formats": "2.1.1",
@@ -172,11 +168,36 @@
         }
       }
     },
+    "node_modules/@angular-devkit/core/node_modules/ajv": {
+      "version": "8.12.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
+      "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/@angular-devkit/core/node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@angular-devkit/core/node_modules/picomatch": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz",
       "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=12"
       },
@@ -185,12 +206,13 @@
       }
     },
     "node_modules/@angular-devkit/schematics": {
-      "version": "17.3.8",
-      "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.8.tgz",
-      "integrity": "sha512-QRVEYpIfgkprNHc916JlPuNbLzOgrm9DZalHasnLUz4P6g7pR21olb8YCyM2OTJjombNhya9ZpckcADU5Qyvlg==",
+      "version": "17.3.11",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz",
+      "integrity": "sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@angular-devkit/core": "17.3.8",
+        "@angular-devkit/core": "17.3.11",
         "jsonc-parser": "3.2.1",
         "magic-string": "0.30.8",
         "ora": "5.4.1",
@@ -203,13 +225,14 @@
       }
     },
     "node_modules/@angular-devkit/schematics-cli": {
-      "version": "17.3.8",
-      "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.3.8.tgz",
-      "integrity": "sha512-TjmiwWJarX7oqvNiRAroQ5/LeKUatxBOCNEuKXO/PV8e7pn/Hr/BqfFm+UcYrQoFdZplmtNAfqmbqgVziKvCpA==",
+      "version": "17.3.11",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.3.11.tgz",
+      "integrity": "sha512-kcOMqp+PHAKkqRad7Zd7PbpqJ0LqLaNZdY1+k66lLWmkEBozgq8v4ASn/puPWf9Bo0HpCiK+EzLf0VHE8Z/y6Q==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@angular-devkit/core": "17.3.8",
-        "@angular-devkit/schematics": "17.3.8",
+        "@angular-devkit/core": "17.3.11",
+        "@angular-devkit/schematics": "17.3.11",
         "ansi-colors": "4.1.3",
         "inquirer": "9.2.15",
         "symbol-observable": "4.0.0",
@@ -229,6 +252,7 @@
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
       "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": "^12.17.0 || ^14.13 || >=16.0.0"
       },
@@ -241,6 +265,7 @@
       "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
       "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
       "dev": true,
+      "license": "ISC",
       "engines": {
         "node": ">= 12"
       }
@@ -250,6 +275,7 @@
       "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz",
       "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@ljharb/through": "^2.3.12",
         "ansi-escapes": "^4.3.2",
@@ -276,6 +302,7 @@
       "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
       "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==",
       "dev": true,
+      "license": "ISC",
       "engines": {
         "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
       }
@@ -285,16 +312,19 @@
       "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz",
       "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.12.0"
       }
     },
     "node_modules/@babel/code-frame": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz",
-      "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==",
+      "version": "7.26.2",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
+      "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
+      "license": "MIT",
       "dependencies": {
-        "@babel/highlight": "^7.24.6",
+        "@babel/helper-validator-identifier": "^7.25.9",
+        "js-tokens": "^4.0.0",
         "picocolors": "^1.0.0"
       },
       "engines": {
@@ -302,9 +332,10 @@
       }
     },
     "node_modules/@babel/compat-data": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.6.tgz",
-      "integrity": "sha512-aC2DGhBq5eEdyXWqrDInSqQjO0k8xtPRf5YylULqx8MCd6jBtzqfta/3ETMRpuKIc5hyswfO80ObyA1MvkCcUQ==",
+      "version": "7.26.3",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz",
+      "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==",
+      "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
       }
@@ -313,6 +344,7 @@
       "version": "7.24.5",
       "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz",
       "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==",
+      "license": "MIT",
       "dependencies": {
         "@ampproject/remapping": "^2.2.0",
         "@babel/code-frame": "^7.24.2",
@@ -342,43 +374,36 @@
       "version": "6.3.1",
       "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
       "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "license": "ISC",
       "bin": {
         "semver": "bin/semver.js"
       }
     },
     "node_modules/@babel/generator": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.6.tgz",
-      "integrity": "sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg==",
+      "version": "7.26.3",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz",
+      "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==",
+      "license": "MIT",
       "dependencies": {
-        "@babel/types": "^7.24.6",
+        "@babel/parser": "^7.26.3",
+        "@babel/types": "^7.26.3",
         "@jridgewell/gen-mapping": "^0.3.5",
         "@jridgewell/trace-mapping": "^0.3.25",
-        "jsesc": "^2.5.1"
+        "jsesc": "^3.0.2"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
-    "node_modules/@babel/generator/node_modules/jsesc": {
-      "version": "2.5.2",
-      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
-      "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
-      "bin": {
-        "jsesc": "bin/jsesc"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/@babel/helper-compilation-targets": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz",
-      "integrity": "sha512-VZQ57UsDGlX/5fFA7GkVPplZhHsVc+vuErWgdOiysI9Ksnw0Pbbd6pnPiR/mmJyKHgyIW0c7KT32gmhiF+cirg==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz",
+      "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==",
+      "license": "MIT",
       "dependencies": {
-        "@babel/compat-data": "^7.24.6",
-        "@babel/helper-validator-option": "^7.24.6",
-        "browserslist": "^4.22.2",
+        "@babel/compat-data": "^7.25.9",
+        "@babel/helper-validator-option": "^7.25.9",
+        "browserslist": "^4.24.0",
         "lru-cache": "^5.1.1",
         "semver": "^6.3.1"
       },
@@ -386,66 +411,52 @@
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
     "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
       "version": "6.3.1",
       "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
       "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "license": "ISC",
       "bin": {
         "semver": "bin/semver.js"
       }
     },
-    "node_modules/@babel/helper-environment-visitor": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz",
-      "integrity": "sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==",
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/helper-function-name": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz",
-      "integrity": "sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==",
-      "dependencies": {
-        "@babel/template": "^7.24.6",
-        "@babel/types": "^7.24.6"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/helper-hoist-variables": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.6.tgz",
-      "integrity": "sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==",
-      "dependencies": {
-        "@babel/types": "^7.24.6"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
+    "node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+      "license": "ISC"
     },
     "node_modules/@babel/helper-module-imports": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz",
-      "integrity": "sha512-a26dmxFJBF62rRO9mmpgrfTLsAuyHk4e1hKTUkD/fcMfynt8gvEKwQPQDVxWhca8dHoDck+55DFt42zV0QMw5g==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
+      "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
+      "license": "MIT",
       "dependencies": {
-        "@babel/types": "^7.24.6"
+        "@babel/traverse": "^7.25.9",
+        "@babel/types": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-module-transforms": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.6.tgz",
-      "integrity": "sha512-Y/YMPm83mV2HJTbX1Qh2sjgjqcacvOlhbzdCCsSlblOKjSYmQqEbO6rUniWQyRo9ncyfjT8hnUjlG06RXDEmcA==",
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
+      "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
+      "license": "MIT",
       "dependencies": {
-        "@babel/helper-environment-visitor": "^7.24.6",
-        "@babel/helper-module-imports": "^7.24.6",
-        "@babel/helper-simple-access": "^7.24.6",
-        "@babel/helper-split-export-declaration": "^7.24.6",
-        "@babel/helper-validator-identifier": "^7.24.6"
+        "@babel/helper-module-imports": "^7.25.9",
+        "@babel/helper-validator-identifier": "^7.25.9",
+        "@babel/traverse": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -454,146 +465,54 @@
         "@babel/core": "^7.0.0"
       }
     },
-    "node_modules/@babel/helper-simple-access": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.6.tgz",
-      "integrity": "sha512-nZzcMMD4ZhmB35MOOzQuiGO5RzL6tJbsT37Zx8M5L/i9KSrukGXWTjLe1knIbb/RmxoJE9GON9soq0c0VEMM5g==",
-      "dependencies": {
-        "@babel/types": "^7.24.6"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/helper-split-export-declaration": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz",
-      "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==",
-      "dependencies": {
-        "@babel/types": "^7.24.6"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
     "node_modules/@babel/helper-string-parser": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz",
-      "integrity": "sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
+      "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
+      "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-validator-identifier": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz",
-      "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+      "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+      "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-validator-option": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.6.tgz",
-      "integrity": "sha512-Jktc8KkF3zIkePb48QO+IapbXlSapOW9S+ogZZkcO6bABgYAxtZcjZ/O005111YLf+j4M84uEgwYoidDkXbCkQ==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
+      "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
+      "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helpers": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.6.tgz",
-      "integrity": "sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA==",
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
+      "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
+      "license": "MIT",
       "dependencies": {
-        "@babel/template": "^7.24.6",
-        "@babel/types": "^7.24.6"
+        "@babel/template": "^7.25.9",
+        "@babel/types": "^7.26.0"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
-    "node_modules/@babel/highlight": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz",
-      "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==",
-      "dependencies": {
-        "@babel/helper-validator-identifier": "^7.24.6",
-        "chalk": "^2.4.2",
-        "js-tokens": "^4.0.0",
-        "picocolors": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/highlight/node_modules/ansi-styles": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-      "dependencies": {
-        "color-convert": "^1.9.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/@babel/highlight/node_modules/chalk": {
-      "version": "2.4.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-      "dependencies": {
-        "ansi-styles": "^3.2.1",
-        "escape-string-regexp": "^1.0.5",
-        "supports-color": "^5.3.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/@babel/highlight/node_modules/color-convert": {
-      "version": "1.9.3",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "dependencies": {
-        "color-name": "1.1.3"
-      }
-    },
-    "node_modules/@babel/highlight/node_modules/color-name": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
-    },
-    "node_modules/@babel/highlight/node_modules/escape-string-regexp": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-      "engines": {
-        "node": ">=0.8.0"
-      }
-    },
-    "node_modules/@babel/highlight/node_modules/has-flag": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/@babel/highlight/node_modules/supports-color": {
-      "version": "5.5.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-      "dependencies": {
-        "has-flag": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/@babel/parser": {
-      "version": "7.24.5",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz",
-      "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==",
+      "version": "7.26.3",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
+      "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.26.3"
+      },
       "bin": {
         "parser": "bin/babel-parser.js"
       },
@@ -602,42 +521,30 @@
       }
     },
     "node_modules/@babel/template": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz",
-      "integrity": "sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
+      "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
+      "license": "MIT",
       "dependencies": {
-        "@babel/code-frame": "^7.24.6",
-        "@babel/parser": "^7.24.6",
-        "@babel/types": "^7.24.6"
+        "@babel/code-frame": "^7.25.9",
+        "@babel/parser": "^7.25.9",
+        "@babel/types": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
-    "node_modules/@babel/template/node_modules/@babel/parser": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz",
-      "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==",
-      "bin": {
-        "parser": "bin/babel-parser.js"
-      },
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
     "node_modules/@babel/traverse": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.6.tgz",
-      "integrity": "sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw==",
+      "version": "7.26.4",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz",
+      "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==",
+      "license": "MIT",
       "dependencies": {
-        "@babel/code-frame": "^7.24.6",
-        "@babel/generator": "^7.24.6",
-        "@babel/helper-environment-visitor": "^7.24.6",
-        "@babel/helper-function-name": "^7.24.6",
-        "@babel/helper-hoist-variables": "^7.24.6",
-        "@babel/helper-split-export-declaration": "^7.24.6",
-        "@babel/parser": "^7.24.6",
-        "@babel/types": "^7.24.6",
+        "@babel/code-frame": "^7.26.2",
+        "@babel/generator": "^7.26.3",
+        "@babel/parser": "^7.26.3",
+        "@babel/template": "^7.25.9",
+        "@babel/types": "^7.26.3",
         "debug": "^4.3.1",
         "globals": "^11.1.0"
       },
@@ -645,33 +552,23 @@
         "node": ">=6.9.0"
       }
     },
-    "node_modules/@babel/traverse/node_modules/@babel/parser": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz",
-      "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==",
-      "bin": {
-        "parser": "bin/babel-parser.js"
-      },
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
     "node_modules/@babel/traverse/node_modules/globals": {
       "version": "11.12.0",
       "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
       "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+      "license": "MIT",
       "engines": {
         "node": ">=4"
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz",
-      "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==",
+      "version": "7.26.3",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
+      "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
+      "license": "MIT",
       "dependencies": {
-        "@babel/helper-string-parser": "^7.24.6",
-        "@babel/helper-validator-identifier": "^7.24.6",
-        "to-fast-properties": "^2.0.0"
+        "@babel/helper-string-parser": "^7.25.9",
+        "@babel/helper-validator-identifier": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -681,65 +578,45 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz",
       "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==",
-      "dev": true
+      "dev": true,
+      "license": "Apache-2.0"
     },
     "node_modules/@bcoe/v8-coverage": {
       "version": "0.2.3",
       "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
       "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@colors/colors": {
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
       "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "engines": {
         "node": ">=0.1.90"
       }
     },
-    "node_modules/@cspotcode/source-map-support": {
-      "version": "0.8.1",
-      "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
-      "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
-      "optional": true,
-      "peer": true,
-      "dependencies": {
-        "@jridgewell/trace-mapping": "0.3.9"
-      },
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
-      "version": "0.3.9",
-      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
-      "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
-      "optional": true,
-      "peer": true,
-      "dependencies": {
-        "@jridgewell/resolve-uri": "^3.0.3",
-        "@jridgewell/sourcemap-codec": "^1.4.10"
-      }
-    },
     "node_modules/@emnapi/runtime": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz",
-      "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==",
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
+      "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==",
+      "license": "MIT",
       "optional": true,
       "dependencies": {
         "tslib": "^2.4.0"
       }
     },
     "node_modules/@esbuild/aix-ppc64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
-      "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz",
+      "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==",
       "cpu": [
         "ppc64"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "aix"
@@ -749,13 +626,13 @@
       }
     },
     "node_modules/@esbuild/android-arm": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
-      "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz",
+      "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==",
       "cpu": [
         "arm"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "android"
@@ -765,13 +642,13 @@
       }
     },
     "node_modules/@esbuild/android-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
-      "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz",
+      "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==",
       "cpu": [
         "arm64"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "android"
@@ -781,13 +658,13 @@
       }
     },
     "node_modules/@esbuild/android-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
-      "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz",
+      "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==",
       "cpu": [
         "x64"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "android"
@@ -797,13 +674,13 @@
       }
     },
     "node_modules/@esbuild/darwin-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
-      "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz",
+      "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==",
       "cpu": [
         "arm64"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
@@ -813,13 +690,13 @@
       }
     },
     "node_modules/@esbuild/darwin-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
-      "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz",
+      "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==",
       "cpu": [
         "x64"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
@@ -829,13 +706,13 @@
       }
     },
     "node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
-      "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz",
+      "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==",
       "cpu": [
         "arm64"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "freebsd"
@@ -845,13 +722,13 @@
       }
     },
     "node_modules/@esbuild/freebsd-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
-      "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz",
+      "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==",
       "cpu": [
         "x64"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "freebsd"
@@ -861,13 +738,13 @@
       }
     },
     "node_modules/@esbuild/linux-arm": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
-      "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz",
+      "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==",
       "cpu": [
         "arm"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -877,13 +754,13 @@
       }
     },
     "node_modules/@esbuild/linux-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
-      "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz",
+      "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==",
       "cpu": [
         "arm64"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -893,13 +770,13 @@
       }
     },
     "node_modules/@esbuild/linux-ia32": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
-      "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz",
+      "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==",
       "cpu": [
         "ia32"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -909,13 +786,13 @@
       }
     },
     "node_modules/@esbuild/linux-loong64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
-      "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz",
+      "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==",
       "cpu": [
         "loong64"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -925,13 +802,13 @@
       }
     },
     "node_modules/@esbuild/linux-mips64el": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
-      "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz",
+      "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==",
       "cpu": [
         "mips64el"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -941,13 +818,13 @@
       }
     },
     "node_modules/@esbuild/linux-ppc64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
-      "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz",
+      "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==",
       "cpu": [
         "ppc64"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -957,13 +834,13 @@
       }
     },
     "node_modules/@esbuild/linux-riscv64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
-      "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz",
+      "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==",
       "cpu": [
         "riscv64"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -973,13 +850,13 @@
       }
     },
     "node_modules/@esbuild/linux-s390x": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
-      "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz",
+      "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==",
       "cpu": [
         "s390x"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -989,13 +866,13 @@
       }
     },
     "node_modules/@esbuild/linux-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
-      "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz",
+      "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==",
       "cpu": [
         "x64"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -1005,13 +882,13 @@
       }
     },
     "node_modules/@esbuild/netbsd-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
-      "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz",
+      "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==",
       "cpu": [
         "x64"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "netbsd"
@@ -1021,13 +898,13 @@
       }
     },
     "node_modules/@esbuild/openbsd-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
-      "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz",
+      "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==",
       "cpu": [
         "x64"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "openbsd"
@@ -1037,13 +914,13 @@
       }
     },
     "node_modules/@esbuild/sunos-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
-      "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz",
+      "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==",
       "cpu": [
         "x64"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "sunos"
@@ -1053,13 +930,13 @@
       }
     },
     "node_modules/@esbuild/win32-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
-      "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz",
+      "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==",
       "cpu": [
         "arm64"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
@@ -1069,13 +946,13 @@
       }
     },
     "node_modules/@esbuild/win32-ia32": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
-      "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz",
+      "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==",
       "cpu": [
         "ia32"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
@@ -1085,13 +962,13 @@
       }
     },
     "node_modules/@esbuild/win32-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
-      "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz",
+      "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==",
       "cpu": [
         "x64"
       ],
-      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
@@ -1101,36 +978,42 @@
       }
     },
     "node_modules/@eslint-community/eslint-utils": {
-      "version": "4.4.0",
-      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
-      "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
+      "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "eslint-visitor-keys": "^3.3.0"
+        "eslint-visitor-keys": "^3.4.3"
       },
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      },
       "peerDependencies": {
         "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
       }
     },
     "node_modules/@eslint-community/regexpp": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz",
-      "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==",
+      "version": "4.12.1",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+      "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
       }
     },
     "node_modules/@eslint/config-array": {
-      "version": "0.18.0",
-      "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz",
-      "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==",
+      "version": "0.19.1",
+      "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz",
+      "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==",
       "dev": true,
+      "license": "Apache-2.0",
       "dependencies": {
-        "@eslint/object-schema": "^2.1.4",
+        "@eslint/object-schema": "^2.1.5",
         "debug": "^4.3.1",
         "minimatch": "^3.1.2"
       },
@@ -1138,10 +1021,23 @@
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       }
     },
+    "node_modules/@eslint/core": {
+      "version": "0.9.1",
+      "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz",
+      "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@types/json-schema": "^7.0.15"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
     "node_modules/@eslint/eslintrc": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
-      "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
+      "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -1162,22 +1058,6 @@
         "url": "https://opencollective.com/eslint"
       }
     },
-    "node_modules/@eslint/eslintrc/node_modules/ajv": {
-      "version": "6.12.6",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-      "dev": true,
-      "dependencies": {
-        "fast-deep-equal": "^3.1.1",
-        "fast-json-stable-stringify": "^2.0.0",
-        "json-schema-traverse": "^0.4.1",
-        "uri-js": "^4.2.2"
-      },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/epoberezkin"
-      }
-    },
     "node_modules/@eslint/eslintrc/node_modules/globals": {
       "version": "14.0.0",
       "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
@@ -1191,26 +1071,35 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-      "dev": true
-    },
     "node_modules/@eslint/js": {
-      "version": "9.9.1",
-      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz",
-      "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==",
+      "version": "9.17.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz",
+      "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       }
     },
     "node_modules/@eslint/object-schema": {
-      "version": "2.1.4",
-      "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz",
-      "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz",
+      "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==",
       "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/plugin-kit": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz",
+      "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "levn": "^0.4.1"
+      },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       }
@@ -1220,6 +1109,7 @@
       "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
       "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=14"
       }
@@ -1228,6 +1118,7 @@
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-4.0.1.tgz",
       "integrity": "sha512-HFXBJayEkYcU/bbxOztozONdWaZR34ZeJ2zRbZIWY8d5K26oPZQTvJ4L0STW3XVRGWtoE0WBpmx2YPNgYvcmJQ==",
+      "license": "MIT",
       "dependencies": {
         "lodash": "^4.17.21"
       },
@@ -1237,9 +1128,10 @@
       }
     },
     "node_modules/@grpc/grpc-js": {
-      "version": "1.10.10",
-      "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.10.tgz",
-      "integrity": "sha512-HPa/K5NX6ahMoeBv15njAc/sfF4/jmiXLar9UlC2UfHFKZzsCVLc3wbe7+7qua7w9VPh2/L6EBxyAV7/E8Wftg==",
+      "version": "1.12.4",
+      "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.4.tgz",
+      "integrity": "sha512-NBhrxEWnFh0FxeA0d//YP95lRFsSx2TNLEUQg4/W+5f/BMxcCjgOOIT24iD+ZB/tZw057j44DaIxja7w4XMrhg==",
+      "license": "Apache-2.0",
       "dependencies": {
         "@grpc/proto-loader": "^0.7.13",
         "@js-sdsl/ordered-map": "^4.4.2"
@@ -1252,6 +1144,7 @@
       "version": "0.7.13",
       "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz",
       "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==",
+      "license": "Apache-2.0",
       "dependencies": {
         "lodash.camelcase": "^4.3.0",
         "long": "^5.0.0",
@@ -1265,70 +1158,65 @@
         "node": ">=6"
       }
     },
-    "node_modules/@grpc/proto-loader/node_modules/cliui": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
-      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
-      "dependencies": {
-        "string-width": "^4.2.0",
-        "strip-ansi": "^6.0.1",
-        "wrap-ansi": "^7.0.0"
-      },
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/@grpc/proto-loader/node_modules/wrap-ansi": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
-      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-      "dependencies": {
-        "ansi-styles": "^4.0.0",
-        "string-width": "^4.1.0",
-        "strip-ansi": "^6.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
-      }
-    },
-    "node_modules/@grpc/proto-loader/node_modules/yargs": {
-      "version": "17.7.2",
-      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
-      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
-      "dependencies": {
-        "cliui": "^8.0.1",
-        "escalade": "^3.1.1",
-        "get-caller-file": "^2.0.5",
-        "require-directory": "^2.1.1",
-        "string-width": "^4.2.3",
-        "y18n": "^5.0.5",
-        "yargs-parser": "^21.1.1"
-      },
-      "engines": {
-        "node": ">=12"
-      }
-    },
     "node_modules/@hapi/hoek": {
       "version": "9.3.0",
       "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
-      "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="
+      "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@hapi/topo": {
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
       "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
+      "license": "BSD-3-Clause",
       "dependencies": {
         "@hapi/hoek": "^9.0.0"
       }
     },
+    "node_modules/@humanfs/core": {
+      "version": "0.19.1",
+      "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+      "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanfs/node": {
+      "version": "0.16.6",
+      "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
+      "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@humanfs/core": "^0.19.1",
+        "@humanwhocodes/retry": "^0.3.0"
+      },
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+      "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
     "node_modules/@humanwhocodes/module-importer": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
       "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
       "dev": true,
+      "license": "Apache-2.0",
       "engines": {
         "node": ">=12.22"
       },
@@ -1338,10 +1226,11 @@
       }
     },
     "node_modules/@humanwhocodes/retry": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz",
-      "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==",
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz",
+      "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==",
       "dev": true,
+      "license": "Apache-2.0",
       "engines": {
         "node": ">=18.18"
       },
@@ -1357,6 +1246,7 @@
       "cpu": [
         "arm64"
       ],
+      "license": "Apache-2.0",
       "optional": true,
       "os": [
         "darwin"
@@ -1378,6 +1268,7 @@
       "cpu": [
         "x64"
       ],
+      "license": "Apache-2.0",
       "optional": true,
       "os": [
         "darwin"
@@ -1399,6 +1290,7 @@
       "cpu": [
         "arm64"
       ],
+      "license": "LGPL-3.0-or-later",
       "optional": true,
       "os": [
         "darwin"
@@ -1414,6 +1306,7 @@
       "cpu": [
         "x64"
       ],
+      "license": "LGPL-3.0-or-later",
       "optional": true,
       "os": [
         "darwin"
@@ -1429,6 +1322,7 @@
       "cpu": [
         "arm"
       ],
+      "license": "LGPL-3.0-or-later",
       "optional": true,
       "os": [
         "linux"
@@ -1444,6 +1338,7 @@
       "cpu": [
         "arm64"
       ],
+      "license": "LGPL-3.0-or-later",
       "optional": true,
       "os": [
         "linux"
@@ -1459,6 +1354,7 @@
       "cpu": [
         "s390x"
       ],
+      "license": "LGPL-3.0-or-later",
       "optional": true,
       "os": [
         "linux"
@@ -1474,6 +1370,7 @@
       "cpu": [
         "x64"
       ],
+      "license": "LGPL-3.0-or-later",
       "optional": true,
       "os": [
         "linux"
@@ -1489,6 +1386,7 @@
       "cpu": [
         "arm64"
       ],
+      "license": "LGPL-3.0-or-later",
       "optional": true,
       "os": [
         "linux"
@@ -1504,6 +1402,7 @@
       "cpu": [
         "x64"
       ],
+      "license": "LGPL-3.0-or-later",
       "optional": true,
       "os": [
         "linux"
@@ -1519,6 +1418,7 @@
       "cpu": [
         "arm"
       ],
+      "license": "Apache-2.0",
       "optional": true,
       "os": [
         "linux"
@@ -1540,6 +1440,7 @@
       "cpu": [
         "arm64"
       ],
+      "license": "Apache-2.0",
       "optional": true,
       "os": [
         "linux"
@@ -1561,6 +1462,7 @@
       "cpu": [
         "s390x"
       ],
+      "license": "Apache-2.0",
       "optional": true,
       "os": [
         "linux"
@@ -1582,6 +1484,7 @@
       "cpu": [
         "x64"
       ],
+      "license": "Apache-2.0",
       "optional": true,
       "os": [
         "linux"
@@ -1603,6 +1506,7 @@
       "cpu": [
         "arm64"
       ],
+      "license": "Apache-2.0",
       "optional": true,
       "os": [
         "linux"
@@ -1624,6 +1528,7 @@
       "cpu": [
         "x64"
       ],
+      "license": "Apache-2.0",
       "optional": true,
       "os": [
         "linux"
@@ -1645,6 +1550,7 @@
       "cpu": [
         "wasm32"
       ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
       "optional": true,
       "dependencies": {
         "@emnapi/runtime": "^1.2.0"
@@ -1663,6 +1569,7 @@
       "cpu": [
         "ia32"
       ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
       "optional": true,
       "os": [
         "win32"
@@ -1681,6 +1588,7 @@
       "cpu": [
         "x64"
       ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
       "optional": true,
       "os": [
         "win32"
@@ -1695,12 +1603,14 @@
     "node_modules/@ioredis/commands": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
-      "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
+      "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==",
+      "license": "MIT"
     },
     "node_modules/@isaacs/cliui": {
       "version": "8.0.2",
       "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
       "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "license": "ISC",
       "dependencies": {
         "string-width": "^5.1.2",
         "string-width-cjs": "npm:string-width@^4.2.0",
@@ -1714,9 +1624,10 @@
       }
     },
     "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
-      "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+      "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+      "license": "MIT",
       "engines": {
         "node": ">=12"
       },
@@ -1728,6 +1639,7 @@
       "version": "6.2.1",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
       "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+      "license": "MIT",
       "engines": {
         "node": ">=12"
       },
@@ -1738,12 +1650,14 @@
     "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
       "version": "9.2.2",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
-      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+      "license": "MIT"
     },
     "node_modules/@isaacs/cliui/node_modules/string-width": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
       "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "license": "MIT",
       "dependencies": {
         "eastasianwidth": "^0.2.0",
         "emoji-regex": "^9.2.2",
@@ -1760,6 +1674,7 @@
       "version": "7.1.0",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
       "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+      "license": "MIT",
       "dependencies": {
         "ansi-regex": "^6.0.1"
       },
@@ -1774,6 +1689,7 @@
       "version": "8.1.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
       "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "license": "MIT",
       "dependencies": {
         "ansi-styles": "^6.1.0",
         "string-width": "^5.0.1",
@@ -1791,14 +1707,16 @@
       "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
       "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
     },
     "node_modules/@jridgewell/gen-mapping": {
-      "version": "0.3.5",
-      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
-      "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
+      "version": "0.3.8",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+      "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+      "license": "MIT",
       "dependencies": {
         "@jridgewell/set-array": "^1.2.1",
         "@jridgewell/sourcemap-codec": "^1.4.10",
@@ -1809,9 +1727,10 @@
       }
     },
     "node_modules/@jridgewell/resolve-uri": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
-      "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "license": "MIT",
       "engines": {
         "node": ">=6.0.0"
       }
@@ -1820,29 +1739,33 @@
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
       "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+      "license": "MIT",
       "engines": {
         "node": ">=6.0.0"
       }
     },
     "node_modules/@jridgewell/source-map": {
-      "version": "0.3.5",
-      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
-      "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
+      "version": "0.3.6",
+      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
+      "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@jridgewell/gen-mapping": "^0.3.0",
-        "@jridgewell/trace-mapping": "^0.3.9"
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.25"
       }
     },
     "node_modules/@jridgewell/sourcemap-codec": {
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
-      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
+      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+      "license": "MIT"
     },
     "node_modules/@jridgewell/trace-mapping": {
       "version": "0.3.25",
       "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
       "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+      "license": "MIT",
       "dependencies": {
         "@jridgewell/resolve-uri": "^3.1.0",
         "@jridgewell/sourcemap-codec": "^1.4.14"
@@ -1852,6 +1775,7 @@
       "version": "4.4.2",
       "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
       "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
+      "license": "MIT",
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/js-sdsl"
@@ -1862,6 +1786,7 @@
       "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz",
       "integrity": "sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "call-bind": "^1.0.7"
       },
@@ -1873,6 +1798,7 @@
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz",
       "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==",
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
@@ -1881,6 +1807,7 @@
       "version": "1.0.11",
       "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
       "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
+      "license": "BSD-3-Clause",
       "dependencies": {
         "detect-libc": "^2.0.0",
         "https-proxy-agent": "^5.0.0",
@@ -1900,6 +1827,8 @@
       "version": "7.2.3",
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
       "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
+      "license": "ISC",
       "dependencies": {
         "fs.realpath": "^1.0.0",
         "inflight": "^1.0.4",
@@ -1919,6 +1848,8 @@
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
       "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "deprecated": "Rimraf versions prior to v4 are no longer supported",
+      "license": "ISC",
       "dependencies": {
         "glob": "^7.1.3"
       },
@@ -1930,28 +1861,96 @@
       }
     },
     "node_modules/@microsoft/tsdoc": {
-      "version": "0.15.0",
-      "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz",
-      "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA=="
+      "version": "0.15.1",
+      "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz",
+      "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==",
+      "license": "MIT"
     },
     "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz",
-      "integrity": "sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==",
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
+      "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
       "cpu": [
         "arm64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
       ]
     },
+    "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
+      "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
+      "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
+      "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
+      "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
+      "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
     "node_modules/@nestjs/bull-shared": {
-      "version": "10.2.1",
-      "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.2.1.tgz",
-      "integrity": "sha512-zvnTvSq6OJ92omcsFUwaUmPbM3PRgWkIusHPB5TE3IFS7nNdM3OwF+kfe56sgKjMtQQMe/56lok0S04OtPMX5Q==",
+      "version": "10.2.3",
+      "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.2.3.tgz",
+      "integrity": "sha512-XcgAjNOgq6b5DVCytxhR5BKiwWo7hsusVeyE7sfFnlXRHeEtIuC2hYWBr/ZAtvL/RH0/O0tqtq0rVl972nbhJw==",
+      "license": "MIT",
       "dependencies": {
-        "tslib": "2.6.3"
+        "tslib": "2.8.1"
       },
       "peerDependencies": {
         "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
@@ -1959,12 +1958,13 @@
       }
     },
     "node_modules/@nestjs/bullmq": {
-      "version": "10.2.1",
-      "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.2.1.tgz",
-      "integrity": "sha512-nDR0hDabmtXt5gsb5R786BJsGIJoWh/79sVmRETXf4S45+fvdqG1XkCKAeHF9TO9USodw9m+XBNKysTnkY41gw==",
+      "version": "10.2.3",
+      "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.2.3.tgz",
+      "integrity": "sha512-Lo4W5kWD61/246Y6H70RNgV73ybfRbZyKKS4CBRDaMELpxgt89O+EgYZUB4pdoNrWH16rKcaT0AoVsB/iDztKg==",
+      "license": "MIT",
       "dependencies": {
-        "@nestjs/bull-shared": "^10.2.1",
-        "tslib": "2.6.3"
+        "@nestjs/bull-shared": "^10.2.3",
+        "tslib": "2.8.1"
       },
       "peerDependencies": {
         "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
@@ -1973,29 +1973,30 @@
       }
     },
     "node_modules/@nestjs/cli": {
-      "version": "10.4.4",
-      "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.4.tgz",
-      "integrity": "sha512-WKERbSZJGof0+9XeeMmWnb/9FpNxogcB5eTJTHjc9no0ymdTw3jTzT+KZL9iC/hGqBpuomDLaNFCYbAOt29nBw==",
+      "version": "10.4.9",
+      "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz",
+      "integrity": "sha512-s8qYd97bggqeK7Op3iD49X2MpFtW4LVNLAwXFkfbRxKME6IYT7X0muNTJ2+QfI8hpbNx9isWkrLWIp+g5FOhiA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@angular-devkit/core": "17.3.8",
-        "@angular-devkit/schematics": "17.3.8",
-        "@angular-devkit/schematics-cli": "17.3.8",
+        "@angular-devkit/core": "17.3.11",
+        "@angular-devkit/schematics": "17.3.11",
+        "@angular-devkit/schematics-cli": "17.3.11",
         "@nestjs/schematics": "^10.0.1",
         "chalk": "4.1.2",
         "chokidar": "3.6.0",
         "cli-table3": "0.6.5",
         "commander": "4.1.1",
         "fork-ts-checker-webpack-plugin": "9.0.2",
-        "glob": "10.4.2",
+        "glob": "10.4.5",
         "inquirer": "8.2.6",
         "node-emoji": "1.11.0",
         "ora": "5.4.1",
         "tree-kill": "1.2.2",
         "tsconfig-paths": "4.2.0",
-        "tsconfig-paths-webpack-plugin": "4.1.0",
-        "typescript": "5.3.3",
-        "webpack": "5.93.0",
+        "tsconfig-paths-webpack-plugin": "4.2.0",
+        "typescript": "5.7.2",
+        "webpack": "5.97.1",
         "webpack-node-externals": "3.0.0"
       },
       "bin": {
@@ -2005,7 +2006,7 @@
         "node": ">= 16.14"
       },
       "peerDependencies": {
-        "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0",
+        "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0",
         "@swc/core": "^1.3.62"
       },
       "peerDependenciesMeta": {
@@ -2017,26 +2018,14 @@
         }
       }
     },
-    "node_modules/@nestjs/cli/node_modules/typescript": {
-      "version": "5.3.3",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
-      "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
-      "dev": true,
-      "bin": {
-        "tsc": "bin/tsc",
-        "tsserver": "bin/tsserver"
-      },
-      "engines": {
-        "node": ">=14.17"
-      }
-    },
     "node_modules/@nestjs/common": {
-      "version": "10.4.1",
-      "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.1.tgz",
-      "integrity": "sha512-4CkrDx0s4XuWqFjX8WvOFV7Y6RGJd0P2OBblkhZS7nwoctoSuW5pyEa8SWak6YHNGrHRpFb6ymm5Ai4LncwRVA==",
+      "version": "10.4.15",
+      "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.15.tgz",
+      "integrity": "sha512-vaLg1ZgwhG29BuLDxPA9OAcIlgqzp9/N8iG0wGapyUNTf4IY4O6zAHgN6QalwLhFxq7nOI021vdRojR1oF3bqg==",
+      "license": "MIT",
       "dependencies": {
         "iterare": "1.2.1",
-        "tslib": "2.6.3",
+        "tslib": "2.8.1",
         "uid": "2.0.2"
       },
       "funding": {
@@ -2058,31 +2047,18 @@
         }
       }
     },
-    "node_modules/@nestjs/config": {
-      "version": "3.2.3",
-      "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.3.tgz",
-      "integrity": "sha512-p6yv/CvoBewJ72mBq4NXgOAi2rSQNWx3a+IMJLVKS2uiwFCOQQuiIatGwq6MRjXV3Jr+B41iUO8FIf4xBrZ4/w==",
-      "dependencies": {
-        "dotenv": "16.4.5",
-        "dotenv-expand": "10.0.0",
-        "lodash": "4.17.21"
-      },
-      "peerDependencies": {
-        "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
-        "rxjs": "^7.1.0"
-      }
-    },
     "node_modules/@nestjs/core": {
-      "version": "10.4.1",
-      "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.1.tgz",
-      "integrity": "sha512-9I1WdfOBCCHdUm+ClBJupOuZQS6UxzIWHIq6Vp1brAA5ZKl/Wq6BVwSsbnUJGBy3J3PM2XHmR0EQ4fwX3nR7lA==",
+      "version": "10.4.15",
+      "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.15.tgz",
+      "integrity": "sha512-UBejmdiYwaH6fTsz2QFBlC1cJHM+3UDeLZN+CiP9I1fRv2KlBZsmozGLbV5eS1JAVWJB4T5N5yQ0gjN8ZvcS2w==",
       "hasInstallScript": true,
+      "license": "MIT",
       "dependencies": {
         "@nuxtjs/opencollective": "0.3.2",
         "fast-safe-stringify": "2.1.1",
         "iterare": "1.2.1",
-        "path-to-regexp": "3.2.0",
-        "tslib": "2.6.3",
+        "path-to-regexp": "3.3.0",
+        "tslib": "2.8.1",
         "uid": "2.0.2"
       },
       "funding": {
@@ -2110,9 +2086,10 @@
       }
     },
     "node_modules/@nestjs/event-emitter": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-2.0.4.tgz",
-      "integrity": "sha512-quMiw8yOwoSul0pp3mOonGz8EyXWHSBTqBy8B0TbYYgpnG1Ix2wGUnuTksLWaaBiiOTDhciaZ41Y5fJZsSJE1Q==",
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-2.1.1.tgz",
+      "integrity": "sha512-6L6fBOZTyfFlL7Ih/JDdqlCzZeCW0RjCX28wnzGyg/ncv5F/EOeT1dfopQr1loBRQ3LTgu8OWM7n4zLN4xigsg==",
+      "license": "MIT",
       "dependencies": {
         "eventemitter2": "6.4.9"
       },
@@ -2122,9 +2099,10 @@
       }
     },
     "node_modules/@nestjs/mapped-types": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz",
-      "integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==",
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.6.tgz",
+      "integrity": "sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==",
+      "license": "MIT",
       "peerDependencies": {
         "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
         "class-transformer": "^0.4.0 || ^0.5.0",
@@ -2141,15 +2119,16 @@
       }
     },
     "node_modules/@nestjs/platform-express": {
-      "version": "10.4.1",
-      "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.1.tgz",
-      "integrity": "sha512-ccfqIDAq/bg1ShLI5KGtaLaYGykuAdvCi57ohewH7eKJSIpWY1DQjbgKlFfXokALYUq1YOMGqjeZ244OWHfDQg==",
+      "version": "10.4.15",
+      "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.15.tgz",
+      "integrity": "sha512-63ZZPkXHjoDyO7ahGOVcybZCRa7/Scp6mObQKjcX/fTEq1YJeU75ELvMsuQgc8U2opMGOBD7GVuc4DV0oeDHoA==",
+      "license": "MIT",
       "dependencies": {
-        "body-parser": "1.20.2",
+        "body-parser": "1.20.3",
         "cors": "2.8.5",
-        "express": "4.19.2",
+        "express": "4.21.2",
         "multer": "1.4.4-lts.1",
-        "tslib": "2.6.3"
+        "tslib": "2.8.1"
       },
       "funding": {
         "type": "opencollective",
@@ -2161,12 +2140,13 @@
       }
     },
     "node_modules/@nestjs/platform-socket.io": {
-      "version": "10.4.1",
-      "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.1.tgz",
-      "integrity": "sha512-cxn5vKBAbqtEVPl0qVcJpR4sC12+hzcY/mYXGW6ippOKQDBNc2OF8oZXu6V3O1MvAl+VM7eNNEsLmP9DRKQlnw==",
+      "version": "10.4.15",
+      "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.15.tgz",
+      "integrity": "sha512-KZAxNEADPwoORixh3NJgGYWMVGORVPKeTqjD7hbF8TPDLKWWxru9yasBQwEz2/wXH/WgpkQbbaYwx4nUjCIVpw==",
+      "license": "MIT",
       "dependencies": {
-        "socket.io": "4.7.5",
-        "tslib": "2.6.3"
+        "socket.io": "4.8.1",
+        "tslib": "2.8.1"
       },
       "funding": {
         "type": "opencollective",
@@ -2179,39 +2159,29 @@
       }
     },
     "node_modules/@nestjs/schedule": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.0.tgz",
-      "integrity": "sha512-WEc96WTXZW+VI/Ng+uBpiBUwm6TWtAbQ4RKWkfbmzKvmbRGzA/9k/UyAWDS9k0pp+ZcbC+MaZQtt7TjQHrwX6g==",
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.2.tgz",
+      "integrity": "sha512-hCTQ1lNjIA5EHxeu8VvQu2Ed2DBLS1GSC6uKPYlBiQe6LL9a7zfE9iVSK+zuK8E2odsApteEBmfAQchc8Hx0Gg==",
+      "license": "MIT",
       "dependencies": {
-        "cron": "3.1.7",
-        "uuid": "10.0.0"
+        "cron": "3.2.1",
+        "uuid": "11.0.3"
       },
       "peerDependencies": {
         "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
         "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0"
       }
     },
-    "node_modules/@nestjs/schedule/node_modules/uuid": {
-      "version": "10.0.0",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
-      "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
-      "funding": [
-        "https://github.com/sponsors/broofa",
-        "https://github.com/sponsors/ctavan"
-      ],
-      "bin": {
-        "uuid": "dist/bin/uuid"
-      }
-    },
     "node_modules/@nestjs/schematics": {
-      "version": "10.1.4",
-      "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.4.tgz",
-      "integrity": "sha512-QpY8ez9cTvXXPr3/KBrtSgXQHMSV6BkOUYy2c2TTe6cBqriEdGnCYqGl8cnfrQl3632q3lveQPaZ/c127dHsEw==",
+      "version": "10.2.3",
+      "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz",
+      "integrity": "sha512-4e8gxaCk7DhBxVUly2PjYL4xC2ifDFexCqq1/u4TtivLGXotVk0wHdYuPYe1tHTHuR1lsOkRbfOCpkdTnigLVg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@angular-devkit/core": "17.3.8",
-        "@angular-devkit/schematics": "17.3.8",
-        "comment-json": "4.2.3",
+        "@angular-devkit/core": "17.3.11",
+        "@angular-devkit/schematics": "17.3.11",
+        "comment-json": "4.2.5",
         "jsonc-parser": "3.3.1",
         "pluralize": "8.0.0"
       },
@@ -2223,19 +2193,21 @@
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
       "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@nestjs/swagger": {
-      "version": "7.4.0",
-      "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.0.tgz",
-      "integrity": "sha512-dCiwKkRxcR7dZs5jtrGspBAe/nqJd1AYzOBTzw9iCdbq3BGrLpwokelk6lFZPe4twpTsPQqzNKBwKzVbI6AR/g==",
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-8.1.0.tgz",
+      "integrity": "sha512-8hzH+r/31XshzXHC9vww4T0xjDAxMzvOaT1xAOvvY1LtXTWyNRCUP2iQsCYJOnnMrR+vydWjvRZiuB3hdvaHxA==",
+      "license": "MIT",
       "dependencies": {
         "@microsoft/tsdoc": "^0.15.0",
-        "@nestjs/mapped-types": "2.0.5",
+        "@nestjs/mapped-types": "2.0.6",
         "js-yaml": "4.1.0",
         "lodash": "4.17.21",
-        "path-to-regexp": "3.2.0",
-        "swagger-ui-dist": "5.17.14"
+        "path-to-regexp": "3.3.0",
+        "swagger-ui-dist": "5.18.2"
       },
       "peerDependencies": {
         "@fastify/static": "^6.0.0 || ^7.0.0",
@@ -2258,12 +2230,13 @@
       }
     },
     "node_modules/@nestjs/testing": {
-      "version": "10.4.1",
-      "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.1.tgz",
-      "integrity": "sha512-pR+su5+YGqCLH0RhhVkPowQK7FCORU0/PWAywPK7LScAOtD67ZoviZ7hAU4vnGdwkg4HCB0D7W8Bkg19CGU8Xw==",
+      "version": "10.4.15",
+      "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.15.tgz",
+      "integrity": "sha512-eGlWESkACMKti+iZk1hs6FUY/UqObmMaa8HAN9JLnaYkoLf1Jeh+EuHlGnfqo/Rq77oznNLIyaA3PFjrFDlNUg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "tslib": "2.6.3"
+        "tslib": "2.8.1"
       },
       "funding": {
         "type": "opencollective",
@@ -2288,6 +2261,7 @@
       "version": "10.0.2",
       "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.2.tgz",
       "integrity": "sha512-H738bJyydK4SQkRCTeh1aFBxoO1E9xdL/HaLGThwrqN95os5mEyAtK7BLADOS+vldP4jDZ2VQPLj4epWwRqCeQ==",
+      "license": "MIT",
       "dependencies": {
         "uuid": "9.0.1"
       },
@@ -2299,14 +2273,28 @@
         "typeorm": "^0.3.0"
       }
     },
+    "node_modules/@nestjs/typeorm/node_modules/uuid": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+      "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
     "node_modules/@nestjs/websockets": {
-      "version": "10.4.1",
-      "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.1.tgz",
-      "integrity": "sha512-p0Eq94WneczV2bnLEu9hl24iCIfH5eUCGgBuYOkVDySBwvya5L+gD4wUoqIqGoX1c6rkhQa+pMR7pi1EY4t93w==",
+      "version": "10.4.15",
+      "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.15.tgz",
+      "integrity": "sha512-OmCUJwvtagzXfMVko595O98UI3M9zg+URL+/HV7vd3QPMCZ3uGCKSq15YYJ99LHJn9NyK4e4Szm2KnHtUg2QzA==",
+      "license": "MIT",
       "dependencies": {
         "iterare": "1.2.1",
         "object-hash": "3.0.0",
-        "tslib": "2.6.3"
+        "tslib": "2.8.1"
       },
       "peerDependencies": {
         "@nestjs/common": "^10.0.0",
@@ -2322,17 +2310,19 @@
       }
     },
     "node_modules/@next/env": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz",
-      "integrity": "sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA=="
+      "version": "15.0.4",
+      "resolved": "https://registry.npmjs.org/@next/env/-/env-15.0.4.tgz",
+      "integrity": "sha512-WNRvtgnRVDD4oM8gbUcRc27IAhaL4eXQ/2ovGbgLnPGUvdyDr8UdXP4Q/IBDdAdojnD2eScryIDirv0YUCjUVw==",
+      "license": "MIT"
     },
     "node_modules/@next/swc-darwin-arm64": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.3.tgz",
-      "integrity": "sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==",
+      "version": "15.0.4",
+      "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.0.4.tgz",
+      "integrity": "sha512-QecQXPD0yRHxSXWL5Ff80nD+A56sUXZG9koUsjWJwA2Z0ZgVQfuy7gd0/otjxoOovPVHR2eVEvPMHbtZP+pf9w==",
       "cpu": [
         "arm64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
@@ -2342,12 +2332,13 @@
       }
     },
     "node_modules/@next/swc-darwin-x64": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz",
-      "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==",
+      "version": "15.0.4",
+      "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.4.tgz",
+      "integrity": "sha512-pb7Bye3y1Og3PlCtnz2oO4z+/b3pH2/HSYkLbL0hbVuTGil7fPen8/3pyyLjdiTLcFJ+ymeU3bck5hd4IPFFCA==",
       "cpu": [
         "x64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
@@ -2357,12 +2348,13 @@
       }
     },
     "node_modules/@next/swc-linux-arm64-gnu": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz",
-      "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==",
+      "version": "15.0.4",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.4.tgz",
+      "integrity": "sha512-12oSaBFjGpB227VHzoXF3gJoK2SlVGmFJMaBJSu5rbpaoT5OjP5OuCLuR9/jnyBF1BAWMs/boa6mLMoJPRriMA==",
       "cpu": [
         "arm64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -2372,12 +2364,13 @@
       }
     },
     "node_modules/@next/swc-linux-arm64-musl": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz",
-      "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==",
+      "version": "15.0.4",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.4.tgz",
+      "integrity": "sha512-QARO88fR/a+wg+OFC3dGytJVVviiYFEyjc/Zzkjn/HevUuJ7qGUUAUYy5PGVWY1YgTzeRYz78akQrVQ8r+sMjw==",
       "cpu": [
         "arm64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -2387,12 +2380,13 @@
       }
     },
     "node_modules/@next/swc-linux-x64-gnu": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz",
-      "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==",
+      "version": "15.0.4",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.4.tgz",
+      "integrity": "sha512-Z50b0gvYiUU1vLzfAMiChV8Y+6u/T2mdfpXPHraqpypP7yIT2UV9YBBhcwYkxujmCvGEcRTVWOj3EP7XW/wUnw==",
       "cpu": [
         "x64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -2402,12 +2396,13 @@
       }
     },
     "node_modules/@next/swc-linux-x64-musl": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz",
-      "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==",
+      "version": "15.0.4",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.4.tgz",
+      "integrity": "sha512-7H9C4FAsrTAbA/ENzvFWsVytqRYhaJYKa2B3fyQcv96TkOGVMcvyS6s+sj4jZlacxxTcn7ygaMXUPkEk7b78zw==",
       "cpu": [
         "x64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -2417,27 +2412,13 @@
       }
     },
     "node_modules/@next/swc-win32-arm64-msvc": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz",
-      "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==",
+      "version": "15.0.4",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.4.tgz",
+      "integrity": "sha512-Z/v3WV5xRaeWlgJzN9r4PydWD8sXV35ywc28W63i37G2jnUgScA4OOgS8hQdiXLxE3gqfSuHTicUhr7931OXPQ==",
       "cpu": [
         "arm64"
       ],
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">= 10"
-      }
-    },
-    "node_modules/@next/swc-win32-ia32-msvc": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz",
-      "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==",
-      "cpu": [
-        "ia32"
-      ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
@@ -2447,12 +2428,13 @@
       }
     },
     "node_modules/@next/swc-win32-x64-msvc": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz",
-      "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==",
+      "version": "15.0.4",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.4.tgz",
+      "integrity": "sha512-NGLchGruagh8lQpDr98bHLyWJXOBSmkEAfK980OiNBa7vNm6PsNoPvzTfstT78WyOeMRQphEQ455rggd7Eo+Dw==",
       "cpu": [
         "x64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
@@ -2465,6 +2447,7 @@
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
       "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "license": "MIT",
       "dependencies": {
         "@nodelib/fs.stat": "2.0.5",
         "run-parallel": "^1.1.9"
@@ -2477,6 +2460,7 @@
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
       "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "license": "MIT",
       "engines": {
         "node": ">= 8"
       }
@@ -2485,6 +2469,7 @@
       "version": "1.2.8",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
       "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "license": "MIT",
       "dependencies": {
         "@nodelib/fs.scandir": "2.1.5",
         "fastq": "^1.6.0"
@@ -2497,6 +2482,7 @@
       "version": "0.3.2",
       "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz",
       "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==",
+      "license": "MIT",
       "dependencies": {
         "chalk": "^4.1.0",
         "consola": "^2.15.0",
@@ -2510,82 +2496,80 @@
         "npm": ">=5.0.0"
       }
     },
-    "node_modules/@one-ini/wasm": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
-      "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="
-    },
     "node_modules/@opentelemetry/api": {
-      "version": "1.8.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz",
-      "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==",
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
+      "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
+      "license": "Apache-2.0",
       "engines": {
         "node": ">=8.0.0"
       }
     },
     "node_modules/@opentelemetry/api-logs": {
-      "version": "0.52.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.0.tgz",
-      "integrity": "sha512-HxjD7xH9iAE4OyhNaaSec65i1H6QZYBWSwWkowFfsc5YAcDvJG30/J1sRKXEQqdmUcKTXEAnA66UciqZha/4+Q==",
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.56.0.tgz",
+      "integrity": "sha512-Wr39+94UNNG3Ei9nv3pHd4AJ63gq5nSemMRpCd8fPwDL9rN3vK26lzxfH27mw16XzOSO+TpyQwBAMaLxaPWG0g==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/api": "^1.0.0"
+        "@opentelemetry/api": "^1.3.0"
       },
       "engines": {
         "node": ">=14"
       }
     },
     "node_modules/@opentelemetry/auto-instrumentations-node": {
-      "version": "0.49.2",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.49.2.tgz",
-      "integrity": "sha512-xtETEPmAby/3MMmedv8Z/873sdLTWg+Vq98rtm4wbwvAiXBB/ao8qRyzRlvR2MR6puEr+vIB/CXeyJnzNA3cyw==",
+      "version": "0.54.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.54.0.tgz",
+      "integrity": "sha512-MJYh3hUN7FupIXGy/cOiMoTIM3lTELXFiu9dFXD6YK9AE/Uez2YfgRnHyotD9h/qJeL7uDcI5DHAGkbb/2EdOQ==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/instrumentation-amqplib": "^0.41.0",
-        "@opentelemetry/instrumentation-aws-lambda": "^0.43.0",
-        "@opentelemetry/instrumentation-aws-sdk": "^0.43.1",
-        "@opentelemetry/instrumentation-bunyan": "^0.40.0",
-        "@opentelemetry/instrumentation-cassandra-driver": "^0.40.0",
-        "@opentelemetry/instrumentation-connect": "^0.38.0",
-        "@opentelemetry/instrumentation-cucumber": "^0.8.0",
-        "@opentelemetry/instrumentation-dataloader": "^0.11.0",
-        "@opentelemetry/instrumentation-dns": "^0.38.0",
-        "@opentelemetry/instrumentation-express": "^0.41.1",
-        "@opentelemetry/instrumentation-fastify": "^0.38.0",
-        "@opentelemetry/instrumentation-fs": "^0.14.0",
-        "@opentelemetry/instrumentation-generic-pool": "^0.38.1",
-        "@opentelemetry/instrumentation-graphql": "^0.42.0",
-        "@opentelemetry/instrumentation-grpc": "^0.52.0",
-        "@opentelemetry/instrumentation-hapi": "^0.40.0",
-        "@opentelemetry/instrumentation-http": "^0.52.0",
-        "@opentelemetry/instrumentation-ioredis": "^0.42.0",
-        "@opentelemetry/instrumentation-kafkajs": "^0.2.0",
-        "@opentelemetry/instrumentation-knex": "^0.39.0",
-        "@opentelemetry/instrumentation-koa": "^0.42.0",
-        "@opentelemetry/instrumentation-lru-memoizer": "^0.39.0",
-        "@opentelemetry/instrumentation-memcached": "^0.38.0",
-        "@opentelemetry/instrumentation-mongodb": "^0.46.0",
-        "@opentelemetry/instrumentation-mongoose": "^0.41.0",
-        "@opentelemetry/instrumentation-mysql": "^0.40.0",
-        "@opentelemetry/instrumentation-mysql2": "^0.40.0",
-        "@opentelemetry/instrumentation-nestjs-core": "^0.39.0",
-        "@opentelemetry/instrumentation-net": "^0.38.0",
-        "@opentelemetry/instrumentation-pg": "^0.43.0",
-        "@opentelemetry/instrumentation-pino": "^0.41.0",
-        "@opentelemetry/instrumentation-redis": "^0.41.0",
-        "@opentelemetry/instrumentation-redis-4": "^0.41.1",
-        "@opentelemetry/instrumentation-restify": "^0.40.0",
-        "@opentelemetry/instrumentation-router": "^0.39.0",
-        "@opentelemetry/instrumentation-socket.io": "^0.41.0",
-        "@opentelemetry/instrumentation-tedious": "^0.13.0",
-        "@opentelemetry/instrumentation-undici": "^0.5.0",
-        "@opentelemetry/instrumentation-winston": "^0.39.0",
-        "@opentelemetry/resource-detector-alibaba-cloud": "^0.29.0",
-        "@opentelemetry/resource-detector-aws": "^1.6.0",
-        "@opentelemetry/resource-detector-azure": "^0.2.10",
-        "@opentelemetry/resource-detector-container": "^0.4.0",
-        "@opentelemetry/resource-detector-gcp": "^0.29.10",
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/instrumentation-amqplib": "^0.45.0",
+        "@opentelemetry/instrumentation-aws-lambda": "^0.49.0",
+        "@opentelemetry/instrumentation-aws-sdk": "^0.48.0",
+        "@opentelemetry/instrumentation-bunyan": "^0.44.0",
+        "@opentelemetry/instrumentation-cassandra-driver": "^0.44.0",
+        "@opentelemetry/instrumentation-connect": "^0.42.0",
+        "@opentelemetry/instrumentation-cucumber": "^0.12.0",
+        "@opentelemetry/instrumentation-dataloader": "^0.15.0",
+        "@opentelemetry/instrumentation-dns": "^0.42.0",
+        "@opentelemetry/instrumentation-express": "^0.46.0",
+        "@opentelemetry/instrumentation-fastify": "^0.43.0",
+        "@opentelemetry/instrumentation-fs": "^0.18.0",
+        "@opentelemetry/instrumentation-generic-pool": "^0.42.0",
+        "@opentelemetry/instrumentation-graphql": "^0.46.0",
+        "@opentelemetry/instrumentation-grpc": "^0.56.0",
+        "@opentelemetry/instrumentation-hapi": "^0.44.0",
+        "@opentelemetry/instrumentation-http": "^0.56.0",
+        "@opentelemetry/instrumentation-ioredis": "^0.46.0",
+        "@opentelemetry/instrumentation-kafkajs": "^0.6.0",
+        "@opentelemetry/instrumentation-knex": "^0.43.0",
+        "@opentelemetry/instrumentation-koa": "^0.46.0",
+        "@opentelemetry/instrumentation-lru-memoizer": "^0.43.0",
+        "@opentelemetry/instrumentation-memcached": "^0.42.0",
+        "@opentelemetry/instrumentation-mongodb": "^0.50.0",
+        "@opentelemetry/instrumentation-mongoose": "^0.45.0",
+        "@opentelemetry/instrumentation-mysql": "^0.44.0",
+        "@opentelemetry/instrumentation-mysql2": "^0.44.0",
+        "@opentelemetry/instrumentation-nestjs-core": "^0.43.0",
+        "@opentelemetry/instrumentation-net": "^0.42.0",
+        "@opentelemetry/instrumentation-pg": "^0.49.0",
+        "@opentelemetry/instrumentation-pino": "^0.45.0",
+        "@opentelemetry/instrumentation-redis": "^0.45.0",
+        "@opentelemetry/instrumentation-redis-4": "^0.45.0",
+        "@opentelemetry/instrumentation-restify": "^0.44.0",
+        "@opentelemetry/instrumentation-router": "^0.43.0",
+        "@opentelemetry/instrumentation-socket.io": "^0.45.0",
+        "@opentelemetry/instrumentation-tedious": "^0.17.0",
+        "@opentelemetry/instrumentation-undici": "^0.9.0",
+        "@opentelemetry/instrumentation-winston": "^0.43.0",
+        "@opentelemetry/resource-detector-alibaba-cloud": "^0.29.6",
+        "@opentelemetry/resource-detector-aws": "^1.9.0",
+        "@opentelemetry/resource-detector-azure": "^0.4.0",
+        "@opentelemetry/resource-detector-container": "^0.5.2",
+        "@opentelemetry/resource-detector-gcp": "^0.31.0",
         "@opentelemetry/resources": "^1.24.0",
-        "@opentelemetry/sdk-node": "^0.52.0"
+        "@opentelemetry/sdk-node": "^0.56.0"
       },
       "engines": {
         "node": ">=14"
@@ -2594,99 +2578,11 @@
         "@opentelemetry/api": "^1.4.1"
       }
     },
-    "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/@opentelemetry/api-logs": {
-      "version": "0.52.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz",
-      "integrity": "sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==",
-      "dependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/@opentelemetry/core": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-      "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/@opentelemetry/instrumentation": {
-      "version": "0.52.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz",
-      "integrity": "sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==",
-      "dependencies": {
-        "@opentelemetry/api-logs": "0.52.1",
-        "@types/shimmer": "^1.0.2",
-        "import-in-the-middle": "^1.8.1",
-        "require-in-the-middle": "^7.1.1",
-        "semver": "^7.5.2",
-        "shimmer": "^1.2.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/@opentelemetry/sdk-node": {
-      "version": "0.52.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.52.1.tgz",
-      "integrity": "sha512-uEG+gtEr6eKd8CVWeKMhH2olcCHM9dEK68pe0qE0be32BcCRsvYURhHaD1Srngh1SQcnQzZ4TP324euxqtBOJA==",
-      "dependencies": {
-        "@opentelemetry/api-logs": "0.52.1",
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/exporter-trace-otlp-grpc": "0.52.1",
-        "@opentelemetry/exporter-trace-otlp-http": "0.52.1",
-        "@opentelemetry/exporter-trace-otlp-proto": "0.52.1",
-        "@opentelemetry/exporter-zipkin": "1.25.1",
-        "@opentelemetry/instrumentation": "0.52.1",
-        "@opentelemetry/resources": "1.25.1",
-        "@opentelemetry/sdk-logs": "0.52.1",
-        "@opentelemetry/sdk-metrics": "1.25.1",
-        "@opentelemetry/sdk-trace-base": "1.25.1",
-        "@opentelemetry/sdk-trace-node": "1.25.1",
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.3.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-      "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/import-in-the-middle": {
-      "version": "1.11.0",
-      "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz",
-      "integrity": "sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q==",
-      "dependencies": {
-        "acorn": "^8.8.2",
-        "acorn-import-attributes": "^1.9.5",
-        "cjs-module-lexer": "^1.2.2",
-        "module-details-from-path": "^1.0.3"
-      }
-    },
     "node_modules/@opentelemetry/context-async-hooks": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.26.0.tgz",
-      "integrity": "sha512-HedpXXYzzbaoutw6DFLWLDket2FwLkLpil4hGCZ1xYEIMTcivdfwEOISgdbLEWyG3HW52gTq2V9mOVJrONgiwg==",
+      "version": "1.29.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.29.0.tgz",
+      "integrity": "sha512-TKT91jcFXgHyIDF1lgJF3BHGIakn6x0Xp7Tq3zoS3TMPzT9IlP0xEavWP8C1zGjU9UmZP2VR1tJhW9Az1A3w8Q==",
+      "license": "Apache-2.0",
       "engines": {
         "node": ">=14"
       },
@@ -2695,11 +2591,12 @@
       }
     },
     "node_modules/@opentelemetry/core": {
-      "version": "1.25.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.0.tgz",
-      "integrity": "sha512-n0B3s8rrqGrasTgNkXLKXzN0fXo+6IYP7M5b7AMsrZM33f/y6DS6kJ0Btd7SespASWq8bgL3taLo0oe0vB52IQ==",
+      "version": "1.29.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.29.0.tgz",
+      "integrity": "sha512-gmT7vAreXl0DTHD2rVZcw3+l2g84+5XiHIqdBUxXbExymPCvSsGOpiwMmn8nkiJur28STV31wnhIDrzWDPzjfA==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.25.0"
+        "@opentelemetry/semantic-conventions": "1.28.0"
       },
       "engines": {
         "node": ">=14"
@@ -2709,92 +2606,16 @@
       }
     },
     "node_modules/@opentelemetry/exporter-logs-otlp-grpc": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.53.0.tgz",
-      "integrity": "sha512-x5ygAQgWAQOI+UOhyV3z9eW7QU2dCfnfOuIBiyYmC2AWr74f6x/3JBnP27IAcEx6aihpqBYWKnpoUTztkVPAZw==",
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.56.0.tgz",
+      "integrity": "sha512-/ef8wcphVKZ0uI7A1oqQI/gEMiBUlkeBkM9AGx6AviQFIbgPVSdNK3+bHBkyq5qMkyWgkeQCSJ0uhc5vJpf0dw==",
+      "license": "Apache-2.0",
       "dependencies": {
         "@grpc/grpc-js": "^1.7.1",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/otlp-grpc-exporter-base": "0.53.0",
-        "@opentelemetry/otlp-transformer": "0.53.0",
-        "@opentelemetry/sdk-logs": "0.53.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/api-logs": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz",
-      "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==",
-      "dependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/core": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz",
-      "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.27.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/otlp-exporter-base": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz",
-      "integrity": "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==",
-      "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/otlp-transformer": "0.53.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/otlp-grpc-exporter-base": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.53.0.tgz",
-      "integrity": "sha512-F7RCN8VN+lzSa4fGjewit8Z5fEUpY/lmMVy5EWn2ZpbAabg3EE3sCLuTNfOiooNGnmvzimUPruoeqeko/5/TzQ==",
-      "dependencies": {
-        "@grpc/grpc-js": "^1.7.1",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/otlp-exporter-base": "0.53.0",
-        "@opentelemetry/otlp-transformer": "0.53.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/otlp-transformer": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.53.0.tgz",
-      "integrity": "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==",
-      "dependencies": {
-        "@opentelemetry/api-logs": "0.53.0",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0",
-        "@opentelemetry/sdk-logs": "0.53.0",
-        "@opentelemetry/sdk-metrics": "1.26.0",
-        "@opentelemetry/sdk-trace-base": "1.26.0",
-        "protobufjs": "^7.3.0"
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/otlp-grpc-exporter-base": "0.56.0",
+        "@opentelemetry/otlp-transformer": "0.56.0",
+        "@opentelemetry/sdk-logs": "0.56.0"
       },
       "engines": {
         "node": ">=14"
@@ -2803,146 +2624,17 @@
         "@opentelemetry/api": "^1.3.0"
       }
     },
-    "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/resources": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz",
-      "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==",
-      "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/semantic-conventions": "1.27.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-logs": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.53.0.tgz",
-      "integrity": "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==",
-      "dependencies": {
-        "@opentelemetry/api-logs": "0.53.0",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.4.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz",
-      "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==",
-      "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.3.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-trace-base": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz",
-      "integrity": "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==",
-      "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0",
-        "@opentelemetry/semantic-conventions": "1.27.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.27.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
-      "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
     "node_modules/@opentelemetry/exporter-logs-otlp-http": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.53.0.tgz",
-      "integrity": "sha512-cSRKgD/n8rb+Yd+Cif6EnHEL/VZg1o8lEcEwFji1lwene6BdH51Zh3feAD9p2TyVoBKrl6Q9Zm2WltSp2k9gWQ==",
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.56.0.tgz",
+      "integrity": "sha512-gN/itg2B30pa+yAqiuIHBCf3E77sSBlyWVzb+U/MDLzEMOwfnexlMvOWULnIO1l2xR2MNLEuPCQAOrL92JHEJg==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/api-logs": "0.53.0",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/otlp-exporter-base": "0.53.0",
-        "@opentelemetry/otlp-transformer": "0.53.0",
-        "@opentelemetry/sdk-logs": "0.53.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/api-logs": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz",
-      "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==",
-      "dependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/core": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz",
-      "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.27.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/otlp-exporter-base": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz",
-      "integrity": "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==",
-      "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/otlp-transformer": "0.53.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/otlp-transformer": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.53.0.tgz",
-      "integrity": "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==",
-      "dependencies": {
-        "@opentelemetry/api-logs": "0.53.0",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0",
-        "@opentelemetry/sdk-logs": "0.53.0",
-        "@opentelemetry/sdk-metrics": "1.26.0",
-        "@opentelemetry/sdk-trace-base": "1.26.0",
-        "protobufjs": "^7.3.0"
+        "@opentelemetry/api-logs": "0.56.0",
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/otlp-exporter-base": "0.56.0",
+        "@opentelemetry/otlp-transformer": "0.56.0",
+        "@opentelemetry/sdk-logs": "0.56.0"
       },
       "engines": {
         "node": ">=14"
@@ -2951,148 +2643,19 @@
         "@opentelemetry/api": "^1.3.0"
       }
     },
-    "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/resources": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz",
-      "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==",
-      "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/semantic-conventions": "1.27.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/sdk-logs": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.53.0.tgz",
-      "integrity": "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==",
-      "dependencies": {
-        "@opentelemetry/api-logs": "0.53.0",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.4.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/sdk-metrics": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz",
-      "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==",
-      "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.3.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/sdk-trace-base": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz",
-      "integrity": "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==",
-      "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0",
-        "@opentelemetry/semantic-conventions": "1.27.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.27.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
-      "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
     "node_modules/@opentelemetry/exporter-logs-otlp-proto": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.53.0.tgz",
-      "integrity": "sha512-jhEcVL1deeWNmTUP05UZMriZPSWUBcfg94ng7JuBb1q2NExgnADQFl1VQQ+xo62/JepK+MxQe4xAwlsDQFbISA==",
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.56.0.tgz",
+      "integrity": "sha512-MaO+eGrdksd8MpEbDDLbWegHc3w6ualZV6CENxNOm3wqob0iOx78/YL2NVIKyP/0ktTUIs7xIppUYqfY3ogFLQ==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/api-logs": "0.53.0",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/otlp-exporter-base": "0.53.0",
-        "@opentelemetry/otlp-transformer": "0.53.0",
-        "@opentelemetry/resources": "1.26.0",
-        "@opentelemetry/sdk-logs": "0.53.0",
-        "@opentelemetry/sdk-trace-base": "1.26.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/api-logs": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz",
-      "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==",
-      "dependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/core": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz",
-      "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.27.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/otlp-exporter-base": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz",
-      "integrity": "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==",
-      "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/otlp-transformer": "0.53.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/otlp-transformer": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.53.0.tgz",
-      "integrity": "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==",
-      "dependencies": {
-        "@opentelemetry/api-logs": "0.53.0",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0",
-        "@opentelemetry/sdk-logs": "0.53.0",
-        "@opentelemetry/sdk-metrics": "1.26.0",
-        "@opentelemetry/sdk-trace-base": "1.26.0",
-        "protobufjs": "^7.3.0"
+        "@opentelemetry/api-logs": "0.56.0",
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/otlp-exporter-base": "0.56.0",
+        "@opentelemetry/otlp-transformer": "0.56.0",
+        "@opentelemetry/resources": "1.29.0",
+        "@opentelemetry/sdk-logs": "0.56.0",
+        "@opentelemetry/sdk-trace-base": "1.29.0"
       },
       "engines": {
         "node": ">=14"
@@ -3101,84 +2664,15 @@
         "@opentelemetry/api": "^1.3.0"
       }
     },
-    "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/resources": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz",
-      "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==",
-      "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/semantic-conventions": "1.27.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-logs": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.53.0.tgz",
-      "integrity": "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==",
-      "dependencies": {
-        "@opentelemetry/api-logs": "0.53.0",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.4.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-metrics": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz",
-      "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==",
-      "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.3.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz",
-      "integrity": "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==",
-      "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0",
-        "@opentelemetry/semantic-conventions": "1.27.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.27.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
-      "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
     "node_modules/@opentelemetry/exporter-prometheus": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.53.0.tgz",
-      "integrity": "sha512-STP2FZQOykUByPnibbouTirNxnG69Ph8TiMXDsaZuWxGDJ7wsYsRQydJkAVpvG+p0hTMP/hIfZp9zT/1iHpIkQ==",
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.56.0.tgz",
+      "integrity": "sha512-5kFcTumUveNREskg6n4aaXx2o3ADc9YxDkArGCIegzErlc3zfzreO4Y7HDc/fYBnV9aIhJUk5P8yotyVCuymkQ==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0",
-        "@opentelemetry/sdk-metrics": "1.26.0"
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/resources": "1.29.0",
+        "@opentelemetry/sdk-metrics": "1.29.0"
       },
       "engines": {
         "node": ">=14"
@@ -3187,188 +2681,74 @@
         "@opentelemetry/api": "^1.3.0"
       }
     },
-    "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/core": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz",
-      "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.27.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/resources": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz",
-      "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==",
-      "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/semantic-conventions": "1.27.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/sdk-metrics": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz",
-      "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==",
-      "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.3.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.27.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
-      "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
     "node_modules/@opentelemetry/exporter-trace-otlp-grpc": {
-      "version": "0.52.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.52.1.tgz",
-      "integrity": "sha512-pVkSH20crBwMTqB3nIN4jpQKUEoB0Z94drIHpYyEqs7UBr+I0cpYyOR3bqjA/UasQUMROb3GX8ZX4/9cVRqGBQ==",
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.56.0.tgz",
+      "integrity": "sha512-9hRHue78CV2XShAt30HadBK8XEtOBiQmnkYquR1RQyf2RYIdJvhiypEZ+Jh3NGW8Qi14icTII/1oPTQlhuyQdQ==",
+      "license": "Apache-2.0",
       "dependencies": {
         "@grpc/grpc-js": "^1.7.1",
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/otlp-grpc-exporter-base": "0.52.1",
-        "@opentelemetry/otlp-transformer": "0.52.1",
-        "@opentelemetry/resources": "1.25.1",
-        "@opentelemetry/sdk-trace-base": "1.25.1"
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/otlp-grpc-exporter-base": "0.56.0",
+        "@opentelemetry/otlp-transformer": "0.56.0",
+        "@opentelemetry/resources": "1.29.0",
+        "@opentelemetry/sdk-trace-base": "1.29.0"
       },
       "engines": {
         "node": ">=14"
       },
       "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/core": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-      "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-      "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==",
-      "engines": {
-        "node": ">=14"
+        "@opentelemetry/api": "^1.3.0"
       }
     },
     "node_modules/@opentelemetry/exporter-trace-otlp-http": {
-      "version": "0.52.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.52.1.tgz",
-      "integrity": "sha512-05HcNizx0BxcFKKnS5rwOV+2GevLTVIRA0tRgWYyw4yCgR53Ic/xk83toYKts7kbzcI+dswInUg/4s8oyA+tqg==",
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.56.0.tgz",
+      "integrity": "sha512-vqVuJvcwameA0r0cNrRzrZqPLB0otS+95g0XkZdiKOXUo81wYdY6r4kyrwz4nSChqTBEFm0lqi/H2OWGboOa6g==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/otlp-exporter-base": "0.52.1",
-        "@opentelemetry/otlp-transformer": "0.52.1",
-        "@opentelemetry/resources": "1.25.1",
-        "@opentelemetry/sdk-trace-base": "1.25.1"
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/otlp-exporter-base": "0.56.0",
+        "@opentelemetry/otlp-transformer": "0.56.0",
+        "@opentelemetry/resources": "1.29.0",
+        "@opentelemetry/sdk-trace-base": "1.29.0"
       },
       "engines": {
         "node": ">=14"
       },
       "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/core": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-      "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-      "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==",
-      "engines": {
-        "node": ">=14"
+        "@opentelemetry/api": "^1.3.0"
       }
     },
     "node_modules/@opentelemetry/exporter-trace-otlp-proto": {
-      "version": "0.52.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.52.1.tgz",
-      "integrity": "sha512-pt6uX0noTQReHXNeEslQv7x311/F1gJzMnp1HD2qgypLRPbXDeMzzeTngRTUaUbP6hqWNtPxuLr4DEoZG+TcEQ==",
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.56.0.tgz",
+      "integrity": "sha512-UYVtz8Kp1QZpZFg83ZrnwRIxF2wavNyi1XaIKuQNFjlYuGCh8JH4+GOuHUU4G8cIzOkWdjNR559vv0Q+MCz+1w==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/otlp-exporter-base": "0.52.1",
-        "@opentelemetry/otlp-transformer": "0.52.1",
-        "@opentelemetry/resources": "1.25.1",
-        "@opentelemetry/sdk-trace-base": "1.25.1"
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/otlp-exporter-base": "0.56.0",
+        "@opentelemetry/otlp-transformer": "0.56.0",
+        "@opentelemetry/resources": "1.29.0",
+        "@opentelemetry/sdk-trace-base": "1.29.0"
       },
       "engines": {
         "node": ">=14"
       },
       "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/core": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-      "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-      "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==",
-      "engines": {
-        "node": ">=14"
+        "@opentelemetry/api": "^1.3.0"
       }
     },
     "node_modules/@opentelemetry/exporter-zipkin": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.25.1.tgz",
-      "integrity": "sha512-RmOwSvkimg7ETwJbUOPTMhJm9A9bG1U8s7Zo3ajDh4zM7eYcycQ0dM7FbLD6NXWbI2yj7UY4q8BKinKYBQksyw==",
+      "version": "1.29.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.29.0.tgz",
+      "integrity": "sha512-9wNUxbl/sju2AvA3UhL2kLF1nfhJ4dVJgvktc3hx80Bg/fWHvF6ik4R3woZ/5gYFqZ97dcuik0dWPQEzLPNBtg==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/resources": "1.25.1",
-        "@opentelemetry/sdk-trace-base": "1.25.1",
-        "@opentelemetry/semantic-conventions": "1.25.1"
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/resources": "1.29.0",
+        "@opentelemetry/sdk-trace-base": "1.29.0",
+        "@opentelemetry/semantic-conventions": "1.28.0"
       },
       "engines": {
         "node": ">=14"
@@ -3377,35 +2757,14 @@
         "@opentelemetry/api": "^1.0.0"
       }
     },
-    "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/core": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-      "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-      "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
     "node_modules/@opentelemetry/host-metrics": {
-      "version": "0.35.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/host-metrics/-/host-metrics-0.35.1.tgz",
-      "integrity": "sha512-d49/Un/pzqUSsGLeO8PvrX2bLxVAORcaoL3nxjJCzGikXA6gjWXxGOfT8D4qePlgnocozppWszefMHoRFS2MsA==",
+      "version": "0.35.4",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/host-metrics/-/host-metrics-0.35.4.tgz",
+      "integrity": "sha512-3nPElbfYZ2oKNoMw2CkXkHxQryebqACcSgMbbKcn+GnGKp+h7MeOHyg21NmmTt9xgCvRHYiHNkWGkB4laP0oUw==",
+      "license": "Apache-2.0",
       "dependencies": {
         "@opentelemetry/sdk-metrics": "^1.8.0",
-        "systeminformation": "^5.21.20"
+        "systeminformation": "5.22.9"
       },
       "engines": {
         "node": ">=14"
@@ -3415,1251 +2774,12 @@
       }
     },
     "node_modules/@opentelemetry/instrumentation": {
-      "version": "0.52.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.52.0.tgz",
-      "integrity": "sha512-LPwSIrw+60cheWaXsfGL8stBap/AppKQJFE+qqRvzYrgttXFH2ofoIMxWadeqPTq4BYOXM/C7Bdh/T+B60xnlQ==",
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.56.0.tgz",
+      "integrity": "sha512-2KkGBKE+FPXU1F0zKww+stnlUxUTlBvLCiWdP63Z9sqXYeNI/ziNzsxAp4LAdUcTQmXjw1IWgvm5CAb/BHy99w==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/api-logs": "0.52.0",
-        "@types/shimmer": "^1.0.2",
-        "import-in-the-middle": "1.8.0",
-        "require-in-the-middle": "^7.1.1",
-        "semver": "^7.5.2",
-        "shimmer": "^1.2.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-amqplib": {
-      "version": "0.41.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.41.0.tgz",
-      "integrity": "sha512-00Oi6N20BxJVcqETjgNzCmVKN+I5bJH/61IlHiIWd00snj1FdgiIKlpE4hYVacTB2sjIBB3nTbHskttdZEE2eg==",
-      "dependencies": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-aws-lambda": {
-      "version": "0.43.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.43.0.tgz",
-      "integrity": "sha512-pSxcWlsE/pCWQRIw92sV2C+LmKXelYkjkA7C5s39iPUi4pZ2lA1nIiw+1R/y2pDEhUHcaKkNyljQr3cx9ZpVlQ==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/propagator-aws-xray": "^1.3.1",
-        "@opentelemetry/resources": "^1.8.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0",
-        "@types/aws-lambda": "8.10.122"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-aws-sdk": {
-      "version": "0.43.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.43.1.tgz",
-      "integrity": "sha512-qLT2cCniJ5W+6PFzKbksnoIQuq9pS83nmgaExfUwXVvlwi0ILc50dea0tWBHZMkdIDa/zZdcuFrJ7+fUcSnRow==",
-      "dependencies": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/propagation-utils": "^0.30.10",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-bunyan": {
-      "version": "0.40.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.40.0.tgz",
-      "integrity": "sha512-aZ4cXaGWwj79ZXSYrgFVsrDlE4mmf2wfvP9bViwRc0j75A6eN6GaHYHqufFGMTCqASQn5pIjjP+Bx+PWTGiofw==",
-      "dependencies": {
-        "@opentelemetry/api-logs": "^0.52.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@types/bunyan": "1.8.9"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-cassandra-driver": {
-      "version": "0.40.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.40.0.tgz",
-      "integrity": "sha512-JxbM39JU7HxE9MTKKwi6y5Z3mokjZB2BjwfqYi4B3Y29YO3I42Z7eopG6qq06yWZc+nQli386UDQe0d9xKmw0A==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-connect": {
-      "version": "0.38.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.38.0.tgz",
-      "integrity": "sha512-2/nRnx3pjYEmdPIaBwtgtSviTKHWnDZN3R+TkRUnhIVrvBKVcq+I5B2rtd6mr6Fe9cHlZ9Ojcuh7pkNh/xdWWg==",
-      "dependencies": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0",
-        "@types/connect": "3.4.36"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-cucumber": {
-      "version": "0.8.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cucumber/-/instrumentation-cucumber-0.8.0.tgz",
-      "integrity": "sha512-ieTm4RBIlZt2brPwtX5aEZYtYnkyqhAVXJI9RIohiBVMe5DxiwCwt+2Exep/nDVqGPX8zRBZUl4AEw423OxJig==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-dataloader": {
-      "version": "0.11.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.11.0.tgz",
-      "integrity": "sha512-27urJmwkH4KDaMJtEv1uy2S7Apk4XbN4AgWMdfMJbi7DnOduJmeuA+DpJCwXB72tEWXo89z5T3hUVJIDiSNmNw==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-dns": {
-      "version": "0.38.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dns/-/instrumentation-dns-0.38.0.tgz",
-      "integrity": "sha512-Um07I0TQXDWa+ZbEAKDFUxFH40dLtejtExDOMLNJ1CL8VmOmA71qx93Qi/QG4tGkiI1XWqr7gF/oiMCJ4m8buQ==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "semver": "^7.5.4"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-express": {
-      "version": "0.41.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.41.1.tgz",
-      "integrity": "sha512-uRx0V3LPGzjn2bxAnV8eUsDT82vT7NTwI0ezEuPMBOTOsnPpGhWdhcdNdhH80sM4TrWrOfXm9HGEdfWE3TRIww==",
-      "dependencies": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-fastify": {
-      "version": "0.38.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.38.0.tgz",
-      "integrity": "sha512-HBVLpTSYpkQZ87/Df3N0gAw7VzYZV3n28THIBrJWfuqw3Or7UqdhnjeuMIPQ04BKk3aZc0cWn2naSQObbh5vXw==",
-      "dependencies": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-fs": {
-      "version": "0.14.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.14.0.tgz",
-      "integrity": "sha512-pVc8P5AgliC1DphyyBUgsxXlm2XaPH4BpYvt7rAZDMIqUpRk8gs19SioABtKqqxvFzg5jPtgJfJsdxq0Y+maLw==",
-      "dependencies": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-generic-pool": {
-      "version": "0.38.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.38.1.tgz",
-      "integrity": "sha512-WvssuKCuavu/hlq661u82UWkc248cyI/sT+c2dEIj6yCk0BUkErY1D+9XOO+PmHdJNE+76i2NdcvQX5rJrOe/w==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-graphql": {
-      "version": "0.42.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.42.0.tgz",
-      "integrity": "sha512-N8SOwoKL9KQSX7z3gOaw5UaTeVQcfDO1c21csVHnmnmGUoqsXbArK2B8VuwPWcv6/BC/i3io+xTo7QGRZ/z28Q==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-grpc": {
-      "version": "0.52.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.52.0.tgz",
-      "integrity": "sha512-YYhA2pbhMWgF5Hp6eR7AHp1utzZQ3Y0VB8GIwd8zJoLtAuQRZa1N29DUtZ+t/pGRJF+xGPVI+vP+7ugHgeN0zQ==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "0.52.0",
-        "@opentelemetry/semantic-conventions": "1.25.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-hapi": {
-      "version": "0.40.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.40.0.tgz",
-      "integrity": "sha512-8U/w7Ifumtd2bSN1OLaSwAAFhb9FyqWUki3lMMB0ds+1+HdSxYBe9aspEJEgvxAqOkrQnVniAPTEGf1pGM7SOw==",
-      "dependencies": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-http": {
-      "version": "0.52.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.52.0.tgz",
-      "integrity": "sha512-E6ywZuxTa4LnVXZGwL1oj3e2Eog1yIaNqa8KjKXoGkDNKte9/SjQnePXOmhQYI0A9nf0UyFbP9aKd+yHrkJXUA==",
-      "dependencies": {
-        "@opentelemetry/core": "1.25.0",
-        "@opentelemetry/instrumentation": "0.52.0",
-        "@opentelemetry/semantic-conventions": "1.25.0",
-        "semver": "^7.5.2"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-ioredis": {
-      "version": "0.42.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.42.0.tgz",
-      "integrity": "sha512-P11H168EKvBB9TUSasNDOGJCSkpT44XgoM6d3gRIWAa9ghLpYhl0uRkS8//MqPzcJVHr3h3RmfXIpiYLjyIZTw==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/redis-common": "^0.36.2",
-        "@opentelemetry/semantic-conventions": "^1.23.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-kafkajs": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.2.0.tgz",
-      "integrity": "sha512-uKKmhEFd0zR280tJovuiBG7cfnNZT4kvVTvqtHPxQP7nOmRbJstCYHFH13YzjVcKjkmoArmxiSulmZmF7SLIlg==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.24.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-knex": {
-      "version": "0.39.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.39.0.tgz",
-      "integrity": "sha512-lRwTqIKQecPWDkH1KEcAUcFhCaNssbKSpxf4sxRTAROCwrCEnYkjOuqJHV+q1/CApjMTaKu0Er4LBv/6bDpoxA==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-koa": {
-      "version": "0.42.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.42.0.tgz",
-      "integrity": "sha512-H1BEmnMhho8o8HuNRq5zEI4+SIHDIglNB7BPKohZyWG4fWNuR7yM4GTlR01Syq21vODAS7z5omblScJD/eZdKw==",
-      "dependencies": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-lru-memoizer": {
-      "version": "0.39.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.39.0.tgz",
-      "integrity": "sha512-eU1Wx1RRTR/2wYXFzH9gcpB8EPmhYlNDIUHzUXjyUE0CAXEJhBLkYNlzdaVCoQDw2neDqS+Woshiia6+emWK9A==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-memcached": {
-      "version": "0.38.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-memcached/-/instrumentation-memcached-0.38.0.tgz",
-      "integrity": "sha512-tPmyqQEZNyrvg6G+iItdlguQEcGzfE+bJkpQifmBXmWBnoS5oU3UxqtyYuXGL2zI9qQM5yMBHH4nRXWALzy7WA==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.23.0",
-        "@types/memcached": "^2.2.6"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-mongodb": {
-      "version": "0.46.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.46.0.tgz",
-      "integrity": "sha512-VF/MicZ5UOBiXrqBslzwxhN7TVqzu1/LN/QDpkskqM0Zm0aZ4CVRbUygL8d7lrjLn15x5kGIe8VsSphMfPJzlA==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/sdk-metrics": "^1.9.1",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-mongoose": {
-      "version": "0.41.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.41.0.tgz",
-      "integrity": "sha512-ivJg4QnnabFxxoI7K8D+in7hfikjte38sYzJB9v1641xJk9Esa7jM3hmbPB7lxwcgWJLVEDvfPwobt1if0tXxA==",
-      "dependencies": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-mysql": {
-      "version": "0.40.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.40.0.tgz",
-      "integrity": "sha512-d7ja8yizsOCNMYIJt5PH/fKZXjb/mS48zLROO4BzZTtDfhNCl2UM/9VIomP2qkGIFVouSJrGr/T00EzY7bPtKA==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0",
-        "@types/mysql": "2.15.22"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-mysql2": {
-      "version": "0.40.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.40.0.tgz",
-      "integrity": "sha512-0xfS1xcqUmY7WE1uWjlmI67Xg3QsSUlNT+AcXHeA4BDUPwZtWqF4ezIwLgpVZfHOnkAEheqGfNSWd1PIu3Wnfg==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0",
-        "@opentelemetry/sql-common": "^0.40.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-nestjs-core": {
-      "version": "0.39.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.39.0.tgz",
-      "integrity": "sha512-mewVhEXdikyvIZoMIUry8eb8l3HUjuQjSjVbmLVTt4NQi35tkpnHQrG9bTRBrl3403LoWZ2njMPJyg4l6HfKvA==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.23.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-net": {
-      "version": "0.38.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-net/-/instrumentation-net-0.38.0.tgz",
-      "integrity": "sha512-stjow1PijcmUquSmRD/fSihm/H61DbjPlJuJhWUe7P22LFPjFhsrSeiB5vGj3vn+QGceNAs+kioUTzMGPbNxtg==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.23.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-pg": {
-      "version": "0.43.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.43.0.tgz",
-      "integrity": "sha512-og23KLyoxdnAeFs1UWqzSonuCkePUzCX30keSYigIzJe/6WSYA8rnEI5lobcxPEzg+GcU06J7jzokuEHbjVJNw==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0",
-        "@opentelemetry/sql-common": "^0.40.1",
-        "@types/pg": "8.6.1",
-        "@types/pg-pool": "2.0.4"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-pg/node_modules/@types/pg": {
-      "version": "8.6.1",
-      "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz",
-      "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==",
-      "dependencies": {
-        "@types/node": "*",
-        "pg-protocol": "*",
-        "pg-types": "^2.2.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-pino": {
-      "version": "0.41.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.41.0.tgz",
-      "integrity": "sha512-Kpv0fJRk/8iMzMk5Ue5BsUJfHkBJ2wQoIi/qduU1a1Wjx9GLj6J2G17PHjPK5mnZjPNzkFOXFADZMfgDioliQw==",
-      "dependencies": {
-        "@opentelemetry/api-logs": "^0.52.0",
-        "@opentelemetry/core": "^1.25.0",
-        "@opentelemetry/instrumentation": "^0.52.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-redis": {
-      "version": "0.41.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.41.0.tgz",
-      "integrity": "sha512-RJ1pwI3btykp67ts+5qZbaFSAAzacucwBet5/5EsKYtWBpHbWwV/qbGN/kIBzXg5WEZBhXLrR/RUq0EpEUpL3A==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/redis-common": "^0.36.2",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-redis-4": {
-      "version": "0.41.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.41.1.tgz",
-      "integrity": "sha512-UqJAbxraBk7s7pQTlFi5ND4sAUs4r/Ai7gsAVZTQDbHl2kSsOp7gpHcpIuN5dpcI2xnuhM2tkH4SmEhbrv2S6Q==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/redis-common": "^0.36.2",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-restify": {
-      "version": "0.40.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-restify/-/instrumentation-restify-0.40.0.tgz",
-      "integrity": "sha512-sm/rH/GysY/KOEvZqYBZSLYFeXlBkHCgqPDgWc07tz+bHCN6mPs9P3otGOSTe7o3KAIM8Nc6ncCO59vL+jb2cA==",
-      "dependencies": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-router": {
-      "version": "0.39.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-router/-/instrumentation-router-0.39.0.tgz",
-      "integrity": "sha512-LaXnVmD69WPC4hNeLzKexCCS19hRLrUw3xicneAMkzJSzNJvPyk7G6I7lz7VjQh1cooObPBt9gNyd3hhTCUrag==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-socket.io": {
-      "version": "0.41.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.41.0.tgz",
-      "integrity": "sha512-7fzDe9/FpO6NFizC/wnzXXX7bF9oRchsD//wFqy5g5hVEgXZCQ70IhxjrKdBvgjyIejR9T9zTvfQ6PfVKfkCAw==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-tedious": {
-      "version": "0.13.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.13.0.tgz",
-      "integrity": "sha512-Pob0+0R62AqXH50pjazTeGBy/1+SK4CYpFUBV5t7xpbpeuQezkkgVGvLca84QqjBqQizcXedjpUJLgHQDixPQg==",
-      "dependencies": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0",
-        "@types/tedious": "^4.0.14"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-undici": {
-      "version": "0.5.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.5.0.tgz",
-      "integrity": "sha512-aNTeSrFAVcM9qco5DfZ9DNXu6hpMRe8Kt8nCDHfMWDB3pwgGVUE76jTdohc+H/7eLRqh4L7jqs5NSQoHw7S6ww==",
-      "dependencies": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.7.0"
-      }
-    },
-    "node_modules/@opentelemetry/instrumentation-winston": {
-      "version": "0.39.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.39.0.tgz",
-      "integrity": "sha512-v/1xziLJ9CyB3YDjBSBzbB70Qd0JwWTo36EqWK5m3AR0CzsyMQQmf3ZIZM6sgx7hXMcRQ0pnEYhg6nhrUQPm9A==",
-      "dependencies": {
-        "@opentelemetry/api-logs": "^0.52.0",
-        "@opentelemetry/instrumentation": "^0.52.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.3.0"
-      }
-    },
-    "node_modules/@opentelemetry/otlp-exporter-base": {
-      "version": "0.52.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.52.1.tgz",
-      "integrity": "sha512-z175NXOtX5ihdlshtYBe5RpGeBoTXVCKPPLiQlD6FHvpM4Ch+p2B0yWKYSrBfLH24H9zjJiBdTrtD+hLlfnXEQ==",
-      "dependencies": {
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/otlp-transformer": "0.52.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-      "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-      "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/otlp-grpc-exporter-base": {
-      "version": "0.52.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.52.1.tgz",
-      "integrity": "sha512-zo/YrSDmKMjG+vPeA9aBBrsQM9Q/f2zo6N04WMB3yNldJRsgpRBeLLwvAt/Ba7dpehDLOEFBd1i2JCoaFtpCoQ==",
-      "dependencies": {
-        "@grpc/grpc-js": "^1.7.1",
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/otlp-exporter-base": "0.52.1",
-        "@opentelemetry/otlp-transformer": "0.52.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/core": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-      "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-      "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/otlp-transformer": {
-      "version": "0.52.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.52.1.tgz",
-      "integrity": "sha512-I88uCZSZZtVa0XniRqQWKbjAUm73I8tpEy/uJYPPYw5d7BRdVk0RfTBQw8kSUl01oVWEuqxLDa802222MYyWHg==",
-      "dependencies": {
-        "@opentelemetry/api-logs": "0.52.1",
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/resources": "1.25.1",
-        "@opentelemetry/sdk-logs": "0.52.1",
-        "@opentelemetry/sdk-metrics": "1.25.1",
-        "@opentelemetry/sdk-trace-base": "1.25.1",
-        "protobufjs": "^7.3.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.3.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/api-logs": {
-      "version": "0.52.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz",
-      "integrity": "sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==",
-      "dependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-      "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-      "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/propagation-utils": {
-      "version": "0.30.10",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/propagation-utils/-/propagation-utils-0.30.10.tgz",
-      "integrity": "sha512-hhTW8pFp9PSyosYzzuUL9rdm7HF97w3OCyElufFHyUnYnKkCBbu8ne2LyF/KSdI/xZ81ubxWZs78hX4S7pLq5g==",
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/propagator-aws-xray": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-aws-xray/-/propagator-aws-xray-1.3.1.tgz",
-      "integrity": "sha512-6fDMzFlt5r6VWv7MUd0eOpglXPFqykW8CnOuUxJ1VZyLy6mV1bzBlzpsqEmhx1bjvZYvH93vhGkQZqrm95mlrQ==",
-      "dependencies": {
-        "@opentelemetry/core": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/propagator-b3": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.25.1.tgz",
-      "integrity": "sha512-p6HFscpjrv7//kE+7L+3Vn00VEDUJB0n6ZrjkTYHrJ58QZ8B3ajSJhRbCcY6guQ3PDjTbxWklyvIN2ojVbIb1A==",
-      "dependencies": {
-        "@opentelemetry/core": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/propagator-b3/node_modules/@opentelemetry/core": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-      "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/propagator-b3/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-      "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/propagator-jaeger": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.25.1.tgz",
-      "integrity": "sha512-nBprRf0+jlgxks78G/xq72PipVK+4or9Ypntw0gVZYNTCSK8rg5SeaGV19tV920CMqBD/9UIOiFr23Li/Q8tiA==",
-      "dependencies": {
-        "@opentelemetry/core": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/propagator-jaeger/node_modules/@opentelemetry/core": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-      "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/propagator-jaeger/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-      "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/redis-common": {
-      "version": "0.36.2",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz",
-      "integrity": "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/resource-detector-alibaba-cloud": {
-      "version": "0.29.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.29.0.tgz",
-      "integrity": "sha512-cYL1DfBwszTQcpzjiezzFkZp1bzevXjaVJ+VClrufHzH17S0RADcaLRQcLq4GqbWCGfvkJKUqBNz6f1SgfePgw==",
-      "dependencies": {
-        "@opentelemetry/resources": "^1.0.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/resource-detector-aws": {
-      "version": "1.6.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-aws/-/resource-detector-aws-1.6.1.tgz",
-      "integrity": "sha512-A/3lqx9xoew7sFi+AVUUVr6VgB7UJ5qqddkKR3gQk9hWLm1R7HUXVJG09cLcZ7DMNpX13DohPRGmHE/vp1vafw==",
-      "dependencies": {
-        "@opentelemetry/core": "^1.0.0",
-        "@opentelemetry/resources": "^1.10.0",
-        "@opentelemetry/semantic-conventions": "^1.27.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/resource-detector-aws/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.27.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
-      "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/resource-detector-azure": {
-      "version": "0.2.11",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.2.11.tgz",
-      "integrity": "sha512-XepvQfTXWyHAoAziCfXGwYbSZL0LHtFk5iuKKN2VE2vzcoiw5Tepi0Qafuwb7CCtpQRReao4H7E29MFbCmh47g==",
-      "dependencies": {
-        "@opentelemetry/core": "^1.25.1",
-        "@opentelemetry/resources": "^1.10.1",
-        "@opentelemetry/semantic-conventions": "^1.27.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/resource-detector-azure/node_modules/@opentelemetry/core": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz",
-      "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.27.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/resource-detector-azure/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.27.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
-      "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/resource-detector-container": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.4.1.tgz",
-      "integrity": "sha512-v0bvO6RxYtbxvY/HwqrPQnZ4UtP4nBq4AOyS30iqV2vEtiLTY1gNTbNvTF1lwN/gg/g5CY1tRSrHcYODDOv0vw==",
-      "dependencies": {
-        "@opentelemetry/resources": "^1.10.0",
-        "@opentelemetry/semantic-conventions": "^1.27.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/resource-detector-container/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.27.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
-      "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/resource-detector-gcp": {
-      "version": "0.29.10",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.29.10.tgz",
-      "integrity": "sha512-rm2HKJ9lsdoVvrbmkr9dkOzg3Uk0FksXNxvNBgrCprM1XhMoJwThI5i0h/5sJypISUAJlEeJS6gn6nROj/NpkQ==",
-      "dependencies": {
-        "@opentelemetry/core": "^1.0.0",
-        "@opentelemetry/resources": "^1.0.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0",
-        "gcp-metadata": "^6.0.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/resources": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.25.1.tgz",
-      "integrity": "sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ==",
-      "dependencies": {
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-      "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-      "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-logs": {
-      "version": "0.52.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.52.1.tgz",
-      "integrity": "sha512-MBYh+WcPPsN8YpRHRmK1Hsca9pVlyyKd4BxOC4SsgHACnl/bPp4Cri9hWhVm5+2tiQ9Zf4qSc1Jshw9tOLGWQA==",
-      "dependencies": {
-        "@opentelemetry/api-logs": "0.52.1",
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/resources": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.4.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/api-logs": {
-      "version": "0.52.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz",
-      "integrity": "sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==",
-      "dependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-      "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-      "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-metrics": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.25.1.tgz",
-      "integrity": "sha512-9Mb7q5ioFL4E4dDrc4wC/A3NTHDat44v4I3p2pLPSxRvqUbDIQyMVr9uK+EU69+HWhlET1VaSrRzwdckWqY15Q==",
-      "dependencies": {
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/resources": "1.25.1",
-        "lodash.merge": "^4.6.2"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.3.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-      "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-      "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-node": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.53.0.tgz",
-      "integrity": "sha512-0hsxfq3BKy05xGktwG8YdGdxV978++x40EAKyKr1CaHZRh8uqVlXnclnl7OMi9xLMJEcXUw7lGhiRlArFcovyg==",
-      "dependencies": {
-        "@opentelemetry/api-logs": "0.53.0",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/exporter-logs-otlp-grpc": "0.53.0",
-        "@opentelemetry/exporter-logs-otlp-http": "0.53.0",
-        "@opentelemetry/exporter-logs-otlp-proto": "0.53.0",
-        "@opentelemetry/exporter-trace-otlp-grpc": "0.53.0",
-        "@opentelemetry/exporter-trace-otlp-http": "0.53.0",
-        "@opentelemetry/exporter-trace-otlp-proto": "0.53.0",
-        "@opentelemetry/exporter-zipkin": "1.26.0",
-        "@opentelemetry/instrumentation": "0.53.0",
-        "@opentelemetry/resources": "1.26.0",
-        "@opentelemetry/sdk-logs": "0.53.0",
-        "@opentelemetry/sdk-metrics": "1.26.0",
-        "@opentelemetry/sdk-trace-base": "1.26.0",
-        "@opentelemetry/sdk-trace-node": "1.26.0",
-        "@opentelemetry/semantic-conventions": "1.27.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.3.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/api-logs": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz",
-      "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==",
-      "dependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/core": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz",
-      "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.27.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-trace-otlp-grpc": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.53.0.tgz",
-      "integrity": "sha512-m6KSh6OBDwfDjpzPVbuJbMgMbkoZfpxYH2r262KckgX9cMYvooWXEKzlJYsNDC6ADr28A1rtRoUVRwNfIN4tUg==",
-      "dependencies": {
-        "@grpc/grpc-js": "^1.7.1",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/otlp-grpc-exporter-base": "0.53.0",
-        "@opentelemetry/otlp-transformer": "0.53.0",
-        "@opentelemetry/resources": "1.26.0",
-        "@opentelemetry/sdk-trace-base": "1.26.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-trace-otlp-http": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.53.0.tgz",
-      "integrity": "sha512-m7F5ZTq+V9mKGWYpX8EnZ7NjoqAU7VemQ1E2HAG+W/u0wpY1x0OmbxAXfGKFHCspdJk8UKlwPGrpcB8nay3P8A==",
-      "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/otlp-exporter-base": "0.53.0",
-        "@opentelemetry/otlp-transformer": "0.53.0",
-        "@opentelemetry/resources": "1.26.0",
-        "@opentelemetry/sdk-trace-base": "1.26.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-trace-otlp-proto": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.53.0.tgz",
-      "integrity": "sha512-T/bdXslwRKj23S96qbvGtaYOdfyew3TjPEKOk5mHjkCmkVl1O9C/YMdejwSsdLdOq2YW30KjR9kVi0YMxZushQ==",
-      "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/otlp-exporter-base": "0.53.0",
-        "@opentelemetry/otlp-transformer": "0.53.0",
-        "@opentelemetry/resources": "1.26.0",
-        "@opentelemetry/sdk-trace-base": "1.26.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-zipkin": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.26.0.tgz",
-      "integrity": "sha512-PW5R34n3SJHO4t0UetyHKiXL6LixIqWN6lWncg3eRXhKuT30x+b7m5sDJS0kEWRfHeS+kG7uCw2vBzmB2lk3Dw==",
-      "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0",
-        "@opentelemetry/sdk-trace-base": "1.26.0",
-        "@opentelemetry/semantic-conventions": "1.27.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/instrumentation": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz",
-      "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==",
-      "dependencies": {
-        "@opentelemetry/api-logs": "0.53.0",
+        "@opentelemetry/api-logs": "0.56.0",
         "@types/shimmer": "^1.2.0",
         "import-in-the-middle": "^1.8.1",
         "require-in-the-middle": "^7.1.1",
@@ -4673,13 +2793,117 @@
         "@opentelemetry/api": "^1.3.0"
       }
     },
-    "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/otlp-exporter-base": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz",
-      "integrity": "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==",
+    "node_modules/@opentelemetry/instrumentation-amqplib": {
+      "version": "0.45.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.45.0.tgz",
+      "integrity": "sha512-SlKLsOS65NGMIBG1Lh/hLrMDU9WzTUF25apnV6ZmWZB1bBmUwan7qrwwrTu1cL5LzJWCXOdZPuTaxP7pC9qxnQ==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/otlp-transformer": "0.53.0"
+        "@opentelemetry/core": "^1.8.0",
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-aws-lambda": {
+      "version": "0.49.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.49.0.tgz",
+      "integrity": "sha512-FIKQSzX/MSzfARqgm7lX9p/QUj7USyicioBYI5BFGuOOoLefxBlJINAcRs3EvCh1taEnJ7/LpbrhlcF7r4Yqvg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0",
+        "@types/aws-lambda": "8.10.143"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-aws-sdk": {
+      "version": "0.48.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.48.0.tgz",
+      "integrity": "sha512-Bl4geb9DS5Zxr5mOsDcDTLjwrfipQ4KDl1ZT5gmoOvVuZPp308reGdtnO1QmqbvMwcgMxD2aBdWUoYgtx1WgWw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.8.0",
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/propagation-utils": "^0.30.14",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-bunyan": {
+      "version": "0.44.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.44.0.tgz",
+      "integrity": "sha512-9JHcfUPejOx5ULuxrH5K5qOZ9GJSTisuMSZZFVkDigZJ42pMn26Zgmb1HhuiZXd/ZcFgOeLZcwQNpBmF1whftg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/api-logs": "^0.56.0",
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@types/bunyan": "1.8.9"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-cassandra-driver": {
+      "version": "0.44.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.44.0.tgz",
+      "integrity": "sha512-HbhNoqAelB1T4QtgKJbOy7wB26R15HToLyMmYwNFICyDtfY7nhRmGRSzPt6akpwXpyCq43/P+L6XYTmqSWTK/Q==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-connect": {
+      "version": "0.42.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.42.0.tgz",
+      "integrity": "sha512-bOoYHBmbnq/jFaLHmXJ55VQ6jrH5fHDMAPjFM0d3JvR0dvIqW7anEoNC33QqYGFYUfVJ50S0d/eoyF61ALqQuA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.8.0",
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0",
+        "@types/connect": "3.4.36"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-cucumber": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cucumber/-/instrumentation-cucumber-0.12.0.tgz",
+      "integrity": "sha512-0sAhKYaxi5/SM+z8nbwmezNVlnJGkcZgMA7ClenVMIoH5xjow/b2gzJOWr3Ch7FPEXBcyoY/sIqfYWRwmRXWiw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
       },
       "engines": {
         "node": ">=14"
@@ -4688,34 +2912,589 @@
         "@opentelemetry/api": "^1.0.0"
       }
     },
-    "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/otlp-grpc-exporter-base": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.53.0.tgz",
-      "integrity": "sha512-F7RCN8VN+lzSa4fGjewit8Z5fEUpY/lmMVy5EWn2ZpbAabg3EE3sCLuTNfOiooNGnmvzimUPruoeqeko/5/TzQ==",
+    "node_modules/@opentelemetry/instrumentation-dataloader": {
+      "version": "0.15.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.15.0.tgz",
+      "integrity": "sha512-5fP35A2jUPk4SerVcduEkpbRAIoqa2PaP5rWumn01T1uSbavXNccAr3Xvx1N6xFtZxXpLJq4FYqGFnMgDWgVng==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-dns": {
+      "version": "0.42.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dns/-/instrumentation-dns-0.42.0.tgz",
+      "integrity": "sha512-HsKYWwMADJAcdY4UkNNbvcg9cm5Xhz5wxBPyT15z7wigatiEoCXPrbbbRDmCe+eKTc2tRxUPmg49u6MsIGcUmg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-express": {
+      "version": "0.46.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.46.0.tgz",
+      "integrity": "sha512-BCEClDj/HPq/1xYRAlOr6z+OUnbp2eFp18DSrgyQz4IT9pkdYk8eWHnMi9oZSqlC6J5mQzkFmaW5RrKb1GLQhg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.8.0",
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-fastify": {
+      "version": "0.43.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.43.0.tgz",
+      "integrity": "sha512-Lmdsg7tYiV+K3/NKVAQfnnLNGmakUOFdB0PhoTh2aXuSyCmyNnnDvhn2MsArAPTZ68wnD5Llh5HtmiuTkf+DyQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.8.0",
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-fs": {
+      "version": "0.18.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.18.0.tgz",
+      "integrity": "sha512-kC40y6CEMONm8/MWwoF5GHWIC7gOdF+g3sgsjfwJaUkgD6bdWV+FgG0XApqSbTQndICKzw3RonVk8i7s6mHqhA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.8.0",
+        "@opentelemetry/instrumentation": "^0.56.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-generic-pool": {
+      "version": "0.42.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.42.0.tgz",
+      "integrity": "sha512-J4QxqiQ1imtB9ogzsOnHra0g3dmmLAx4JCeoK3o0rFes1OirljNHnO8Hsj4s1jAir8WmWvnEEQO1y8yk6j2tog==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-graphql": {
+      "version": "0.46.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.46.0.tgz",
+      "integrity": "sha512-tplk0YWINSECcK89PGM7IVtOYenXyoOuhOQlN0X0YrcDUfMS4tZMKkVc0vyhNWYYrexnUHwNry2YNBNugSpjlQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-grpc": {
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.56.0.tgz",
+      "integrity": "sha512-cmqCZqyKtyu4oLx3rQmPMeqAo69er7ULnbEBTFCW0++AAimIoAXJptrEvB5X9HYr0NP2TqF8As/vlV3IVmY5OQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "0.56.0",
+        "@opentelemetry/semantic-conventions": "1.28.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-hapi": {
+      "version": "0.44.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.44.0.tgz",
+      "integrity": "sha512-4HdNIMNXWK1O6nsaQOrACo83QWEVoyNODTdVDbUqtqXiv2peDfD0RAPhSQlSGWLPw3S4d9UoOmrV7s2HYj6T2A==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.8.0",
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-http": {
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.56.0.tgz",
+      "integrity": "sha512-/bWHBUAq8VoATnH9iLk5w8CE9+gj+RgYSUphe7hry472n6fYl7+4PvuScoQMdmSUTprKq/gyr2kOWL6zrC7FkQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/instrumentation": "0.56.0",
+        "@opentelemetry/semantic-conventions": "1.28.0",
+        "forwarded-parse": "2.1.2",
+        "semver": "^7.5.2"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-ioredis": {
+      "version": "0.46.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.46.0.tgz",
+      "integrity": "sha512-sOdsq8oGi29V58p1AkefHvuB3l2ymP1IbxRIX3y4lZesQWKL8fLhBmy8xYjINSQ5gHzWul2yoz7pe7boxhZcqQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/redis-common": "^0.36.2",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-kafkajs": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.6.0.tgz",
+      "integrity": "sha512-MGQrzqEUAl0tacKJUFpuNHJesyTi51oUzSVizn7FdvJplkRIdS11FukyZBZJEscofSEdk7Ycmg+kNMLi5QHUFg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-knex": {
+      "version": "0.43.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.43.0.tgz",
+      "integrity": "sha512-mOp0TRQNFFSBj5am0WF67fRO7UZMUmsF3/7HSDja9g3H4pnj+4YNvWWyZn4+q0rGrPtywminAXe0rxtgaGYIqg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-koa": {
+      "version": "0.46.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.46.0.tgz",
+      "integrity": "sha512-RcWXMQdJQANnPUaXbHY5G0Fg6gmleZ/ZtZeSsekWPaZmQq12FGk0L1UwodIgs31OlYfviAZ4yTeytoSUkgo5vQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.8.0",
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-lru-memoizer": {
+      "version": "0.43.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.43.0.tgz",
+      "integrity": "sha512-fZc+1eJUV+tFxaB3zkbupiA8SL3vhDUq89HbDNg1asweYrEb9OlHIB+Ot14ZiHUc1qCmmWmZHbPTwa56mVVwzg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-memcached": {
+      "version": "0.42.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-memcached/-/instrumentation-memcached-0.42.0.tgz",
+      "integrity": "sha512-6peg2nImB4JNpK+kW95b12B6tRSwRpc0KCm6Ol41uDYPli800J9vWi+DGoPsmTrgZpkEfCe9Z9Ob9Z6Fth2zwg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0",
+        "@types/memcached": "^2.2.6"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-mongodb": {
+      "version": "0.50.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.50.0.tgz",
+      "integrity": "sha512-DtwJMjYFXFT5auAvv8aGrBj1h3ciA/dXQom11rxL7B1+Oy3FopSpanvwYxJ+z0qmBrQ1/iMuWELitYqU4LnlkQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-mongoose": {
+      "version": "0.45.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.45.0.tgz",
+      "integrity": "sha512-zHgNh+A01C5baI2mb5dAGyMC7DWmUpOfwpV8axtC0Hd5Uzqv+oqKgKbVDIVhOaDkPxjgVJwYF9YQZl2pw2qxIA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.8.0",
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-mysql": {
+      "version": "0.44.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.44.0.tgz",
+      "integrity": "sha512-al7jbXvT/uT1KV8gdNDzaWd5/WXf+mrjrsF0/NtbnqLa0UUFGgQnoK3cyborgny7I+KxWhL8h7YPTf6Zq4nKsg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0",
+        "@types/mysql": "2.15.26"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-mysql2": {
+      "version": "0.44.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.44.0.tgz",
+      "integrity": "sha512-e9QY4AGsjGFwmfHd6kBa4yPaQZjAq2FuxMb0BbKlXCAjG+jwqw+sr9xWdJGR60jMsTq52hx3mAlE3dUJ9BipxQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0",
+        "@opentelemetry/sql-common": "^0.40.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-nestjs-core": {
+      "version": "0.43.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.43.0.tgz",
+      "integrity": "sha512-NEo4RU7HTjiaXk3curqXUvCb9alRiFWxQY//+hvDXwWLlADX2vB6QEmVCeEZrKO+6I/tBrI4vNdAnbCY9ldZVg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-net": {
+      "version": "0.42.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-net/-/instrumentation-net-0.42.0.tgz",
+      "integrity": "sha512-RCX1e4aHBxpTdm3xyQWDF6dbfclRY1xXAzZnEwuFj1IO+DAqnu8oO11NRBIfH6TNRBmeBKbpiaGbmzCV9ULwIA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-pg": {
+      "version": "0.49.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.49.0.tgz",
+      "integrity": "sha512-3alvNNjPXVdAPdY1G7nGRVINbDxRK02+KAugDiEpzw0jFQfU8IzFkSWA4jyU4/GbMxKvHD+XIOEfSjpieSodKw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.26.0",
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "1.27.0",
+        "@opentelemetry/sql-common": "^0.40.1",
+        "@types/pg": "8.6.1",
+        "@types/pg-pool": "2.0.6"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-pg/node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.27.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
+      "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-pino": {
+      "version": "0.45.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.45.0.tgz",
+      "integrity": "sha512-u7XwRdMDPzB6PHRo1EJNxTmjpHPnLpssYlr5t89aWFXP6fP3M2oRKjyX8EZHTSky/6GOMy860mzmded2VVFvfg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/api-logs": "^0.56.0",
+        "@opentelemetry/core": "^1.25.0",
+        "@opentelemetry/instrumentation": "^0.56.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-redis": {
+      "version": "0.45.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.45.0.tgz",
+      "integrity": "sha512-IKooJ9pUwPhL5nGEMi9QXvO6pMhwgJe6BzmZ0BMoZweKasC0Y0GekKjPw86Lhx+X1xoJCOFJhoWE9c5SnBJVcw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/redis-common": "^0.36.2",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-redis-4": {
+      "version": "0.45.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.45.0.tgz",
+      "integrity": "sha512-Sjgym1xn3mdxPRH5CNZtoz+bFd3E3NlGIu7FoYr4YrQouCc9PbnmoBcmSkEdDy5LYgzNildPgsjx9l0EKNjKTQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/redis-common": "^0.36.2",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-restify": {
+      "version": "0.44.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-restify/-/instrumentation-restify-0.44.0.tgz",
+      "integrity": "sha512-JUIs6NcSkH+AtUgaUknD+1M4GQA5vOPKqwJqdaJbaEQzHo+QTDn8GY1iiSKXktL68OwRddbyQv6tu2NyCGcKSw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.8.0",
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-router": {
+      "version": "0.43.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-router/-/instrumentation-router-0.43.0.tgz",
+      "integrity": "sha512-IkSBWfzlpwLZSJMj3rDG21bDYqbWvW3D/HEx5yCxjUUWVbcz9tRKXjxwG1LB6ZJfnXwwVIOgbz+7XW0HyAXr9Q==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-socket.io": {
+      "version": "0.45.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.45.0.tgz",
+      "integrity": "sha512-X/CUjHqX1mZHEqXjD4AgVA5VXW1JHIauj1LDEjUDky/3RCsUTysj031x0Sq+8yBwcPyHF6k9vZ8DNw+CfxscOQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-tedious": {
+      "version": "0.17.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.17.0.tgz",
+      "integrity": "sha512-yRBz2409an03uVd1Q2jWMt3SqwZqRFyKoWYYX3hBAtPDazJ4w5L+1VOij71TKwgZxZZNdDBXImTQjii+VeuzLg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.56.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0",
+        "@types/tedious": "^4.0.14"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-undici": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.9.0.tgz",
+      "integrity": "sha512-lxc3cpUZ28CqbrWcUHxGW/ObDpMOYbuxF/ZOzeFZq54P9uJ2Cpa8gcrC9F716mtuiMaekwk8D6n34vg/JtkkxQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.8.0",
+        "@opentelemetry/instrumentation": "^0.56.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.7.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-winston": {
+      "version": "0.43.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.43.0.tgz",
+      "integrity": "sha512-TVvRwqjmf4+CcjsdkXc+VHiIG0Qzzim5dx8cN5wXRt4+UYIjyZpnhi/WmSjC0fJdkKb6DNjTIw7ktmB/eRj/jQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/api-logs": "^0.56.0",
+        "@opentelemetry/instrumentation": "^0.56.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/otlp-exporter-base": {
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.56.0.tgz",
+      "integrity": "sha512-eURvv0fcmBE+KE1McUeRo+u0n18ZnUeSc7lDlW/dzlqFYasEbsztTK4v0Qf8C4vEY+aMTjPKUxBG0NX2Te3Pmw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/otlp-transformer": "0.56.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/otlp-grpc-exporter-base": {
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.56.0.tgz",
+      "integrity": "sha512-QqM4si8Ew8CW5xVk4mYbfusJzMXyk6tkYA5SI0w/5NBxmiZZaYPwQQ2cu58XUH2IMPAsi71yLJVJQaWBBCta0A==",
+      "license": "Apache-2.0",
       "dependencies": {
         "@grpc/grpc-js": "^1.7.1",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/otlp-exporter-base": "0.53.0",
-        "@opentelemetry/otlp-transformer": "0.53.0"
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/otlp-exporter-base": "0.56.0",
+        "@opentelemetry/otlp-transformer": "0.56.0"
       },
       "engines": {
         "node": ">=14"
       },
       "peerDependencies": {
-        "@opentelemetry/api": "^1.0.0"
+        "@opentelemetry/api": "^1.3.0"
       }
     },
-    "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/otlp-transformer": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.53.0.tgz",
-      "integrity": "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==",
+    "node_modules/@opentelemetry/otlp-transformer": {
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.56.0.tgz",
+      "integrity": "sha512-kVkH/W2W7EpgWWpyU5VnnjIdSD7Y7FljQYObAQSKdRcejiwMj2glypZtUdfq1LTJcv4ht0jyTrw1D3CCxssNtQ==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/api-logs": "0.53.0",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0",
-        "@opentelemetry/sdk-logs": "0.53.0",
-        "@opentelemetry/sdk-metrics": "1.26.0",
-        "@opentelemetry/sdk-trace-base": "1.26.0",
+        "@opentelemetry/api-logs": "0.56.0",
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/resources": "1.29.0",
+        "@opentelemetry/sdk-logs": "0.56.0",
+        "@opentelemetry/sdk-metrics": "1.29.0",
+        "@opentelemetry/sdk-trace-base": "1.29.0",
         "protobufjs": "^7.3.0"
       },
       "engines": {
@@ -4725,12 +3504,25 @@
         "@opentelemetry/api": "^1.3.0"
       }
     },
-    "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/propagator-b3": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.26.0.tgz",
-      "integrity": "sha512-vvVkQLQ/lGGyEy9GT8uFnI047pajSOVnZI2poJqVGD3nJ+B9sFGdlHNnQKophE3lHfnIH0pw2ubrCTjZCgIj+Q==",
+    "node_modules/@opentelemetry/propagation-utils": {
+      "version": "0.30.14",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/propagation-utils/-/propagation-utils-0.30.14.tgz",
+      "integrity": "sha512-RsdKGFd0PYG5Aop9aq8khYbR8Oq+lYTQBX/9/pk7b+8+0WwdFqrvGDmRxpBAH9hgIvtUgETeshlYctwjo2l9SQ==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.0.0"
+      }
+    },
+    "node_modules/@opentelemetry/propagator-b3": {
+      "version": "1.29.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.29.0.tgz",
+      "integrity": "sha512-ktsNDlqhu+/IPGEJRMj81upg2JupUp+SwW3n1ZVZTnrDiYUiMUW41vhaziA7Q6UDhbZvZ58skDpQhe2ZgNIPvg==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/core": "1.26.0"
+        "@opentelemetry/core": "1.29.0"
       },
       "engines": {
         "node": ">=14"
@@ -4739,12 +3531,13 @@
         "@opentelemetry/api": ">=1.0.0 <1.10.0"
       }
     },
-    "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/propagator-jaeger": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.26.0.tgz",
-      "integrity": "sha512-DelFGkCdaxA1C/QA0Xilszfr0t4YbGd3DjxiCDPh34lfnFr+VkkrjV9S8ZTJvAzfdKERXhfOxIKBoGPJwoSz7Q==",
+    "node_modules/@opentelemetry/propagator-jaeger": {
+      "version": "1.29.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.29.0.tgz",
+      "integrity": "sha512-EXIEYmFgybnFMijVgqx1mq/diWwSQcd0JWVksytAVQEnAiaDvP45WuncEVQkFIAC0gVxa2+Xr8wL5pF5jCVKbg==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/core": "1.26.0"
+        "@opentelemetry/core": "1.29.0"
       },
       "engines": {
         "node": ">=14"
@@ -4753,13 +3546,109 @@
         "@opentelemetry/api": ">=1.0.0 <1.10.0"
       }
     },
-    "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/resources": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz",
-      "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==",
+    "node_modules/@opentelemetry/redis-common": {
+      "version": "0.36.2",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz",
+      "integrity": "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/resource-detector-alibaba-cloud": {
+      "version": "0.29.6",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.29.6.tgz",
+      "integrity": "sha512-BrwutS9Koh08jFhwencsc1t60qEUueMxN+YcN78LE+3r6JMkYgrQzk7C8rJe0nww8KpjZ6A2n7PW+C0FAr8oxg==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/semantic-conventions": "1.27.0"
+        "@opentelemetry/core": "^1.26.0",
+        "@opentelemetry/resources": "^1.10.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.0.0"
+      }
+    },
+    "node_modules/@opentelemetry/resource-detector-aws": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-aws/-/resource-detector-aws-1.9.0.tgz",
+      "integrity": "sha512-oah9Gek5rrpohjMhQYESnXMDw79wrfhOp0NhjMSjKY9EvNJuDurk/HU3TJ8r2xd/xpGZlcHRZcsJ+qR+tLiQ4g==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.0.0",
+        "@opentelemetry/resources": "^1.10.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.0.0"
+      }
+    },
+    "node_modules/@opentelemetry/resource-detector-azure": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.4.0.tgz",
+      "integrity": "sha512-Ix3DwsbUWyLbBCZ1yqT3hJxc5wFPaJ6dvsIgJA/nmjScwscRCWQqTWXywY4+Q+tytLPnuAKZWbBhxcNvNlcn5Q==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.25.1",
+        "@opentelemetry/resources": "^1.10.1",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.0.0"
+      }
+    },
+    "node_modules/@opentelemetry/resource-detector-container": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.5.2.tgz",
+      "integrity": "sha512-P06PiIC3kDa/UTLupClJvhLeub84x3eNkDth2yXaMP3UZe/BRGv+R6eeUbMN/MvZhARkpSFnoWpXBHpnq/JiYQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.26.0",
+        "@opentelemetry/resources": "^1.10.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.0.0"
+      }
+    },
+    "node_modules/@opentelemetry/resource-detector-gcp": {
+      "version": "0.31.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.31.0.tgz",
+      "integrity": "sha512-KNd2Ab3hc0PsBVtWMie11AbQ7i1KXNPYlgTsyGPCHBed6KARVfPekfjWbPEbTXwart4la98abxL0sJLsfgyJSA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.0.0",
+        "@opentelemetry/resources": "^1.10.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0",
+        "gcp-metadata": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.0.0"
+      }
+    },
+    "node_modules/@opentelemetry/resources": {
+      "version": "1.29.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.29.0.tgz",
+      "integrity": "sha512-s7mLXuHZE7RQr1wwweGcaRp3Q4UJJ0wazeGlc/N5/XSe6UyXfsh1UQGMADYeg7YwD+cEdMtU1yJAUXdnFzYzyQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/semantic-conventions": "1.28.0"
       },
       "engines": {
         "node": ">=14"
@@ -4768,14 +3657,15 @@
         "@opentelemetry/api": ">=1.0.0 <1.10.0"
       }
     },
-    "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-logs": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.53.0.tgz",
-      "integrity": "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==",
+    "node_modules/@opentelemetry/sdk-logs": {
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.56.0.tgz",
+      "integrity": "sha512-OS0WPBJF++R/cSl+terUjQH5PebloidB1Jbbecgg2rnCmQbTST9xsRes23bLfDQVRvmegmHqDh884h0aRdJyLw==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/api-logs": "0.53.0",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0"
+        "@opentelemetry/api-logs": "0.56.0",
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/resources": "1.29.0"
       },
       "engines": {
         "node": ">=14"
@@ -4784,13 +3674,14 @@
         "@opentelemetry/api": ">=1.4.0 <1.10.0"
       }
     },
-    "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-metrics": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz",
-      "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==",
+    "node_modules/@opentelemetry/sdk-metrics": {
+      "version": "1.29.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.29.0.tgz",
+      "integrity": "sha512-MkVtuzDjXZaUJSuJlHn6BSXjcQlMvHcsDV7LjY4P6AJeffMa4+kIGDjzsCf6DkAh6Vqlwag5EWEam3KZOX5Drw==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0"
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/resources": "1.29.0"
       },
       "engines": {
         "node": ">=14"
@@ -4799,68 +3690,45 @@
         "@opentelemetry/api": ">=1.3.0 <1.10.0"
       }
     },
-    "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-trace-base": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz",
-      "integrity": "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==",
+    "node_modules/@opentelemetry/sdk-node": {
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.56.0.tgz",
+      "integrity": "sha512-FOY7tWboBBxqftLNHPJFmDXo9fRoPd2PlzfEvSd6058BJM9gY4pWCg8lbVlu03aBrQjcfCTAhXk/tz1Yqd/m6g==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0",
-        "@opentelemetry/semantic-conventions": "1.27.0"
+        "@opentelemetry/api-logs": "0.56.0",
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/exporter-logs-otlp-grpc": "0.56.0",
+        "@opentelemetry/exporter-logs-otlp-http": "0.56.0",
+        "@opentelemetry/exporter-logs-otlp-proto": "0.56.0",
+        "@opentelemetry/exporter-trace-otlp-grpc": "0.56.0",
+        "@opentelemetry/exporter-trace-otlp-http": "0.56.0",
+        "@opentelemetry/exporter-trace-otlp-proto": "0.56.0",
+        "@opentelemetry/exporter-zipkin": "1.29.0",
+        "@opentelemetry/instrumentation": "0.56.0",
+        "@opentelemetry/resources": "1.29.0",
+        "@opentelemetry/sdk-logs": "0.56.0",
+        "@opentelemetry/sdk-metrics": "1.29.0",
+        "@opentelemetry/sdk-trace-base": "1.29.0",
+        "@opentelemetry/sdk-trace-node": "1.29.0",
+        "@opentelemetry/semantic-conventions": "1.28.0"
       },
       "engines": {
         "node": ">=14"
       },
       "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-trace-node": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.26.0.tgz",
-      "integrity": "sha512-Fj5IVKrj0yeUwlewCRwzOVcr5avTuNnMHWf7GPc1t6WaT78J6CJyF3saZ/0RkZfdeNO8IcBl/bNcWMVZBMRW8Q==",
-      "dependencies": {
-        "@opentelemetry/context-async-hooks": "1.26.0",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/propagator-b3": "1.26.0",
-        "@opentelemetry/propagator-jaeger": "1.26.0",
-        "@opentelemetry/sdk-trace-base": "1.26.0",
-        "semver": "^7.5.2"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.27.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
-      "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-node/node_modules/import-in-the-middle": {
-      "version": "1.11.0",
-      "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz",
-      "integrity": "sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q==",
-      "dependencies": {
-        "acorn": "^8.8.2",
-        "acorn-import-attributes": "^1.9.5",
-        "cjs-module-lexer": "^1.2.2",
-        "module-details-from-path": "^1.0.3"
+        "@opentelemetry/api": ">=1.3.0 <1.10.0"
       }
     },
     "node_modules/@opentelemetry/sdk-trace-base": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz",
-      "integrity": "sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw==",
+      "version": "1.29.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.29.0.tgz",
+      "integrity": "sha512-hEOpAYLKXF3wGJpXOtWsxEtqBgde0SCv+w+jvr3/UusR4ll3QrENEGnSl1WDCyRrpqOQ5NCNOvZch9UFVa7MnQ==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/resources": "1.25.1",
-        "@opentelemetry/semantic-conventions": "1.25.1"
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/resources": "1.29.0",
+        "@opentelemetry/semantic-conventions": "1.28.0"
       },
       "engines": {
         "node": ">=14"
@@ -4869,38 +3737,17 @@
         "@opentelemetry/api": ">=1.0.0 <1.10.0"
       }
     },
-    "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/core": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-      "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-      "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
     "node_modules/@opentelemetry/sdk-trace-node": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.25.1.tgz",
-      "integrity": "sha512-nMcjFIKxnFqoez4gUmihdBrbpsEnAX/Xj16sGvZm+guceYE0NE00vLhpDVK6f3q8Q4VFI5xG8JjlXKMB/SkTTQ==",
+      "version": "1.29.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.29.0.tgz",
+      "integrity": "sha512-ZpGYt+VnMu6O0SRKzhuIivr7qJm3GpWnTCMuJspu4kt3QWIpIenwixo5Vvjuu3R4h2Onl/8dtqAiPIs92xd5ww==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@opentelemetry/context-async-hooks": "1.25.1",
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/propagator-b3": "1.25.1",
-        "@opentelemetry/propagator-jaeger": "1.25.1",
-        "@opentelemetry/sdk-trace-base": "1.25.1",
+        "@opentelemetry/context-async-hooks": "1.29.0",
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/propagator-b3": "1.29.0",
+        "@opentelemetry/propagator-jaeger": "1.29.0",
+        "@opentelemetry/sdk-trace-base": "1.29.0",
         "semver": "^7.5.2"
       },
       "engines": {
@@ -4910,43 +3757,11 @@
         "@opentelemetry/api": ">=1.0.0 <1.10.0"
       }
     },
-    "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/context-async-hooks": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.25.1.tgz",
-      "integrity": "sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ==",
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/core": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-      "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": ">=1.0.0 <1.10.0"
-      }
-    },
-    "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-      "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
     "node_modules/@opentelemetry/semantic-conventions": {
-      "version": "1.25.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.0.tgz",
-      "integrity": "sha512-M+kkXKRAIAiAP6qYyesfrC5TOmDpDVtsxuGfPcqd9B/iBrac+E14jYwrgm0yZBUIbIP2OnqC3j+UgkXLm1vxUQ==",
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+      "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+      "license": "Apache-2.0",
       "engines": {
         "node": ">=14"
       }
@@ -4955,6 +3770,7 @@
       "version": "0.40.1",
       "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz",
       "integrity": "sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==",
+      "license": "Apache-2.0",
       "dependencies": {
         "@opentelemetry/core": "^1.1.0"
       },
@@ -4966,24 +3782,27 @@
       }
     },
     "node_modules/@photostructure/tz-lookup": {
-      "version": "10.0.0",
-      "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-10.0.0.tgz",
-      "integrity": "sha512-8ZAjoj/irCuvUlyEinQ/HB6A8hP3bD1dgTOZvfl1b9nAwqniutFDHOQRcGM6Crea68bOwPj010f0Z4KkmuLHEA=="
+      "version": "11.0.0",
+      "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-11.0.0.tgz",
+      "integrity": "sha512-QMV5/dWtY/MdVPXZs/EApqzyhnqDq1keYEqpS+Xj2uidyaqw2Nk/fWcsszdruIXjdqp1VoWNzsgrO6bUHU1mFw==",
+      "license": "CC0-1.0"
     },
     "node_modules/@pkgjs/parseargs": {
       "version": "0.11.0",
       "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
       "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+      "license": "MIT",
       "optional": true,
       "engines": {
         "node": ">=14"
       }
     },
     "node_modules/@pkgr/core": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.0.tgz",
-      "integrity": "sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ==",
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
+      "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
       },
@@ -4992,34 +3811,40 @@
       }
     },
     "node_modules/@polka/url": {
-      "version": "1.0.0-next.25",
-      "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz",
-      "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ=="
+      "version": "1.0.0-next.28",
+      "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz",
+      "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==",
+      "license": "MIT"
     },
     "node_modules/@protobufjs/aspromise": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
-      "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
+      "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/base64": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
-      "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
+      "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/codegen": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
-      "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
+      "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/eventemitter": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
-      "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
+      "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/fetch": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
       "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+      "license": "BSD-3-Clause",
       "dependencies": {
         "@protobufjs/aspromise": "^1.1.1",
         "@protobufjs/inquire": "^1.1.0"
@@ -5028,40 +3853,47 @@
     "node_modules/@protobufjs/float": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
-      "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
+      "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/inquire": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
-      "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
+      "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/path": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
-      "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
+      "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/pool": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
-      "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
+      "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/utf8": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
-      "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
+      "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@react-email/body": {
-      "version": "0.0.10",
-      "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.10.tgz",
-      "integrity": "sha512-dMJyL9aU25ieatdPtVjCyQ/WHZYHwNc+Hy/XpF8Cc18gu21cUynVEeYQzFSeigDRMeBQ3PGAyjVDPIob7YlGwA==",
+      "version": "0.0.11",
+      "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.11.tgz",
+      "integrity": "sha512-ZSD2SxVSgUjHGrB0Wi+4tu3MEpB4fYSbezsFNEJk2xCWDBkFiOeEsjTmR5dvi+CxTK691hQTQlHv0XWuP7ENTg==",
+      "license": "MIT",
       "peerDependencies": {
         "react": "^18.0 || ^19.0 || ^19.0.0-rc"
       }
     },
     "node_modules/@react-email/button": {
-      "version": "0.0.17",
-      "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.17.tgz",
-      "integrity": "sha512-ioHdsk+BpGS/PqjU6JS7tUrVy9yvbUx92Z+Cem2+MbYp55oEwQ9VHf7u4f5NoM0gdhfKSehBwRdYlHt/frEMcg==",
+      "version": "0.0.19",
+      "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.19.tgz",
+      "integrity": "sha512-HYHrhyVGt7rdM/ls6FuuD6XE7fa7bjZTJqB2byn6/oGsfiEZaogY77OtoLL/mrQHjHjZiJadtAMSik9XLcm7+A==",
+      "license": "MIT",
       "engines": {
         "node": ">=18.0.0"
       },
@@ -5070,9 +3902,10 @@
       }
     },
     "node_modules/@react-email/code-block": {
-      "version": "0.0.8",
-      "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.8.tgz",
-      "integrity": "sha512-WbuAEpTnB262i9C3SGPmmErgZ4iU5KIpqLUjr7uBJijqldLqZc5x39e8wPWaRdF7NLcShmrc/+G7GJgI1bdC5w==",
+      "version": "0.0.11",
+      "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.11.tgz",
+      "integrity": "sha512-4D43p+LIMjDzm66gTDrZch0Flkip5je91mAT7iGs6+SbPyalHgIA+lFQoQwhz/VzHHLxuD0LV6gwmU/WUQ2WEg==",
+      "license": "MIT",
       "dependencies": {
         "prismjs": "1.29.0"
       },
@@ -5084,9 +3917,10 @@
       }
     },
     "node_modules/@react-email/code-inline": {
-      "version": "0.0.4",
-      "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.4.tgz",
-      "integrity": "sha512-zj3oMQiiUCZbddSNt3k0zNfIBFK0ZNDIzzDyBaJKy6ZASTtWfB+1WFX0cpTX8q0gUiYK+A94rk5Qp68L6YXjXQ==",
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.5.tgz",
+      "integrity": "sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==",
+      "license": "MIT",
       "engines": {
         "node": ">=18.0.0"
       },
@@ -5095,9 +3929,10 @@
       }
     },
     "node_modules/@react-email/column": {
-      "version": "0.0.12",
-      "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.12.tgz",
-      "integrity": "sha512-Rsl7iSdDaeHZO938xb+0wR5ud0Z3MVfdtPbNKJNojZi2hApwLAQXmDrnn/AcPDM5Lpl331ZljJS8vHTWxxkvKw==",
+      "version": "0.0.13",
+      "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.13.tgz",
+      "integrity": "sha512-Lqq17l7ShzJG/d3b1w/+lVO+gp2FM05ZUo/nW0rjxB8xBICXOVv6PqjDnn3FXKssvhO5qAV20lHM6S+spRhEwQ==",
+      "license": "MIT",
       "engines": {
         "node": ">=18.0.0"
       },
@@ -5106,30 +3941,31 @@
       }
     },
     "node_modules/@react-email/components": {
-      "version": "0.0.24",
-      "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.24.tgz",
-      "integrity": "sha512-/DNmfTREaT59UFdkHoIK3BewJ214LfRxmduiil3m7POj+gougkItANu1+BMmgbUATxjf7jH1WoBxo9x/rhFEFw==",
+      "version": "0.0.31",
+      "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.31.tgz",
+      "integrity": "sha512-rQsTY9ajobncix9raexhBjC7O6cXUMc87eNez2gnB1FwtkUO8DqWZcktbtwOJi7GKmuAPTx0o/IOFtiBNXziKA==",
+      "license": "MIT",
       "dependencies": {
-        "@react-email/body": "0.0.10",
-        "@react-email/button": "0.0.17",
-        "@react-email/code-block": "0.0.8",
-        "@react-email/code-inline": "0.0.4",
-        "@react-email/column": "0.0.12",
-        "@react-email/container": "0.0.14",
-        "@react-email/font": "0.0.8",
-        "@react-email/head": "0.0.11",
-        "@react-email/heading": "0.0.14",
-        "@react-email/hr": "0.0.10",
-        "@react-email/html": "0.0.10",
-        "@react-email/img": "0.0.10",
-        "@react-email/link": "0.0.10",
-        "@react-email/markdown": "0.0.12",
-        "@react-email/preview": "0.0.11",
-        "@react-email/render": "1.0.1",
-        "@react-email/row": "0.0.10",
-        "@react-email/section": "0.0.14",
-        "@react-email/tailwind": "0.1.0",
-        "@react-email/text": "0.0.10"
+        "@react-email/body": "0.0.11",
+        "@react-email/button": "0.0.19",
+        "@react-email/code-block": "0.0.11",
+        "@react-email/code-inline": "0.0.5",
+        "@react-email/column": "0.0.13",
+        "@react-email/container": "0.0.15",
+        "@react-email/font": "0.0.9",
+        "@react-email/head": "0.0.12",
+        "@react-email/heading": "0.0.15",
+        "@react-email/hr": "0.0.11",
+        "@react-email/html": "0.0.11",
+        "@react-email/img": "0.0.11",
+        "@react-email/link": "0.0.12",
+        "@react-email/markdown": "0.0.14",
+        "@react-email/preview": "0.0.12",
+        "@react-email/render": "1.0.3",
+        "@react-email/row": "0.0.12",
+        "@react-email/section": "0.0.16",
+        "@react-email/tailwind": "1.0.4",
+        "@react-email/text": "0.0.11"
       },
       "engines": {
         "node": ">=18.0.0"
@@ -5139,9 +3975,10 @@
       }
     },
     "node_modules/@react-email/container": {
-      "version": "0.0.14",
-      "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.14.tgz",
-      "integrity": "sha512-NgoaJJd9tTtsrveL86Ocr/AYLkGyN3prdXKd/zm5fQpfDhy/NXezyT3iF6VlwAOEUIu64ErHpAJd+P6ygR+vjg==",
+      "version": "0.0.15",
+      "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.15.tgz",
+      "integrity": "sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==",
+      "license": "MIT",
       "engines": {
         "node": ">=18.0.0"
       },
@@ -5150,17 +3987,19 @@
       }
     },
     "node_modules/@react-email/font": {
-      "version": "0.0.8",
-      "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.8.tgz",
-      "integrity": "sha512-fSBEqYyVPAyyACBBHcs3wEYzNknpHMuwcSAAKE8fOoDfGqURr/vSxKPdh4tOa9z7G4hlcEfgGrCYEa2iPT22cw==",
+      "version": "0.0.9",
+      "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.9.tgz",
+      "integrity": "sha512-4zjq23oT9APXkerqeslPH3OZWuh5X4crHK6nx82mVHV2SrLba8+8dPEnWbaACWTNjOCbcLIzaC9unk7Wq2MIXw==",
+      "license": "MIT",
       "peerDependencies": {
         "react": "^18.0 || ^19.0 || ^19.0.0-rc"
       }
     },
     "node_modules/@react-email/head": {
-      "version": "0.0.11",
-      "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.11.tgz",
-      "integrity": "sha512-skw5FUgyamIMK+LN+fZQ5WIKQYf0dPiRAvsUAUR2eYoZp9oRsfkIpFHr0GWPkKAYjFEj+uJjaxQ/0VzQH7svVg==",
+      "version": "0.0.12",
+      "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.12.tgz",
+      "integrity": "sha512-X2Ii6dDFMF+D4niNwMAHbTkeCjlYYnMsd7edXOsi0JByxt9wNyZ9EnhFiBoQdqkE+SMDcu8TlNNttMrf5sJeMA==",
+      "license": "MIT",
       "engines": {
         "node": ">=18.0.0"
       },
@@ -5169,9 +4008,10 @@
       }
     },
     "node_modules/@react-email/heading": {
-      "version": "0.0.14",
-      "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.14.tgz",
-      "integrity": "sha512-jZM7IVuZOXa0G110ES8OkxajPTypIKlzlO1K1RIe1auk76ukQRiCg1IRV4HZlWk1GGUbec5hNxsvZa2kU8cb9w==",
+      "version": "0.0.15",
+      "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.15.tgz",
+      "integrity": "sha512-xF2GqsvBrp/HbRHWEfOgSfRFX+Q8I5KBEIG5+Lv3Vb2R/NYr0s8A5JhHHGf2pWBMJdbP4B2WHgj/VUrhy8dkIg==",
+      "license": "MIT",
       "engines": {
         "node": ">=18.0.0"
       },
@@ -5180,9 +4020,10 @@
       }
     },
     "node_modules/@react-email/hr": {
-      "version": "0.0.10",
-      "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.10.tgz",
-      "integrity": "sha512-3AA4Yjgl3zEid/KVx6uf6TuLJHVZvUc2cG9Wm9ZpWeAX4ODA+8g9HyuC0tfnjbRsVMhMcCGiECuWWXINi+60vA==",
+      "version": "0.0.11",
+      "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.11.tgz",
+      "integrity": "sha512-S1gZHVhwOsd1Iad5IFhpfICwNPMGPJidG/Uysy1AwmspyoAP5a4Iw3OWEpINFdgh9MHladbxcLKO2AJO+cA9Lw==",
+      "license": "MIT",
       "engines": {
         "node": ">=18.0.0"
       },
@@ -5191,9 +4032,10 @@
       }
     },
     "node_modules/@react-email/html": {
-      "version": "0.0.10",
-      "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.10.tgz",
-      "integrity": "sha512-06uiuSKJBWQJfhCKv4MPupELei4Lepyz9Sth7Yq7Fq29CAeB1ejLgKkGqn1I+FZ72hQxPLdYF4iq4yloKv3JCg==",
+      "version": "0.0.11",
+      "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.11.tgz",
+      "integrity": "sha512-qJhbOQy5VW5qzU74AimjAR9FRFQfrMa7dn4gkEXKMB/S9xZN8e1yC1uA9C15jkXI/PzmJ0muDIWmFwatm5/+VA==",
+      "license": "MIT",
       "engines": {
         "node": ">=18.0.0"
       },
@@ -5202,9 +4044,10 @@
       }
     },
     "node_modules/@react-email/img": {
-      "version": "0.0.10",
-      "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.10.tgz",
-      "integrity": "sha512-pJ8glJjDNaJ53qoM95pvX9SK05yh0bNQY/oyBKmxlBDdUII6ixuMc3SCwYXPMl+tgkQUyDgwEBpSTrLAnjL3hA==",
+      "version": "0.0.11",
+      "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.11.tgz",
+      "integrity": "sha512-aGc8Y6U5C3igoMaqAJKsCpkbm1XjguQ09Acd+YcTKwjnC2+0w3yGUJkjWB2vTx4tN8dCqQCXO8FmdJpMfOA9EQ==",
+      "license": "MIT",
       "engines": {
         "node": ">=18.0.0"
       },
@@ -5213,9 +4056,10 @@
       }
     },
     "node_modules/@react-email/link": {
-      "version": "0.0.10",
-      "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.10.tgz",
-      "integrity": "sha512-tva3wvAWSR10lMJa9fVA09yRn7pbEki0ZZpHE6GD1jKbFhmzt38VgLO9B797/prqoDZdAr4rVK7LJFcdPx3GwA==",
+      "version": "0.0.12",
+      "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.12.tgz",
+      "integrity": "sha512-vF+xxQk2fGS1CN7UPQDbzvcBGfffr+GjTPNiWM38fhBfsLv6A/YUfaqxWlmL7zLzVmo0K2cvvV9wxlSyNba1aQ==",
+      "license": "MIT",
       "engines": {
         "node": ">=18.0.0"
       },
@@ -5224,11 +4068,12 @@
       }
     },
     "node_modules/@react-email/markdown": {
-      "version": "0.0.12",
-      "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.12.tgz",
-      "integrity": "sha512-wsuvj1XAb6O63aizCLNEeqVgKR3oFjAwt9vjfg2y2oh4G1dZeo8zonZM2x1fmkEkBZhzwSHraNi70jSXhA3A9w==",
+      "version": "0.0.14",
+      "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.14.tgz",
+      "integrity": "sha512-5IsobCyPkb4XwnQO8uFfGcNOxnsg3311GRXhJ3uKv51P7Jxme4ycC/MITnwIZ10w2zx7HIyTiqVzTj4XbuIHbg==",
+      "license": "MIT",
       "dependencies": {
-        "md-to-react-email": "5.0.2"
+        "md-to-react-email": "5.0.5"
       },
       "engines": {
         "node": ">=18.0.0"
@@ -5238,9 +4083,10 @@
       }
     },
     "node_modules/@react-email/preview": {
-      "version": "0.0.11",
-      "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.11.tgz",
-      "integrity": "sha512-7O/CT4b16YlSGrj18htTPx3Vbhu2suCGv/cSe5c+fuSrIM/nMiBSZ3Js16Vj0XJbAmmmlVmYFZw9L20wXJ+LjQ==",
+      "version": "0.0.12",
+      "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.12.tgz",
+      "integrity": "sha512-g/H5fa9PQPDK6WUEG7iTlC19sAktI23qyoiJtMLqQiXFCfWeQMhqjLGKeLSKkfzszqmfJCjZtpSiKtBoOdxp3Q==",
+      "license": "MIT",
       "engines": {
         "node": ">=18.0.0"
       },
@@ -5249,12 +4095,13 @@
       }
     },
     "node_modules/@react-email/render": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.1.tgz",
-      "integrity": "sha512-W3gTrcmLOVYnG80QuUp22ReIT/xfLsVJ+n7ghSlG2BITB8evNABn1AO2rGQoXuK84zKtDAlxCdm3hRyIpZdGSA==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.3.tgz",
+      "integrity": "sha512-VQ8g4SuIq/jWdfBTdTjb7B8Np0jj+OoD7VebfdHhLTZzVQKesR2aigpYqE/ZXmwj4juVxDm8T2b6WIIu48rPCg==",
+      "license": "MIT",
       "dependencies": {
         "html-to-text": "9.0.5",
-        "js-beautify": "^1.14.11",
+        "prettier": "3.3.3",
         "react-promise-suspense": "0.3.4"
       },
       "engines": {
@@ -5265,10 +4112,26 @@
         "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc"
       }
     },
+    "node_modules/@react-email/render/node_modules/prettier": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
+      "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
+      "license": "MIT",
+      "bin": {
+        "prettier": "bin/prettier.cjs"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
     "node_modules/@react-email/row": {
-      "version": "0.0.10",
-      "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.10.tgz",
-      "integrity": "sha512-jPyEhG3gsLX+Eb9U+A30fh0gK6hXJwF4ghJ+ZtFQtlKAKqHX+eCpWlqB3Xschd/ARJLod8WAswg0FB+JD9d0/A==",
+      "version": "0.0.12",
+      "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.12.tgz",
+      "integrity": "sha512-HkCdnEjvK3o+n0y0tZKXYhIXUNPDx+2vq1dJTmqappVHXS5tXS6W5JOPZr5j+eoZ8gY3PShI2LWj5rWF7ZEtIQ==",
+      "license": "MIT",
       "engines": {
         "node": ">=18.0.0"
       },
@@ -5277,9 +4140,10 @@
       }
     },
     "node_modules/@react-email/section": {
-      "version": "0.0.14",
-      "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.14.tgz",
-      "integrity": "sha512-+fYWLb4tPU1A/+GE5J1+SEMA7/wR3V30lQ+OR9t2kAJqNrARDbMx0bLnYnR1QL5TiFRz0pCF05SQUobk6gHEDQ==",
+      "version": "0.0.16",
+      "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.16.tgz",
+      "integrity": "sha512-FjqF9xQ8FoeUZYKSdt8sMIKvoT9XF8BrzhT3xiFKdEMwYNbsDflcjfErJe3jb7Wj/es/lKTbV5QR1dnLzGpL3w==",
+      "license": "MIT",
       "engines": {
         "node": ">=18.0.0"
       },
@@ -5288,9 +4152,10 @@
       }
     },
     "node_modules/@react-email/tailwind": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-0.1.0.tgz",
-      "integrity": "sha512-qysVUEY+M3SKUvu35XDpzn7yokhqFOT3tPU6Mj/pgc62TL5tQFj6msEbBtwoKs2qO3WZvai0DIHdLhaOxBQSow==",
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-1.0.4.tgz",
+      "integrity": "sha512-tJdcusncdqgvTUYZIuhNC6LYTfL9vNTSQpwWdTCQhQ1lsrNCEE4OKCSdzSV3S9F32pi0i0xQ+YPJHKIzGjdTSA==",
+      "license": "MIT",
       "engines": {
         "node": ">=18.0.0"
       },
@@ -5299,9 +4164,10 @@
       }
     },
     "node_modules/@react-email/text": {
-      "version": "0.0.10",
-      "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.10.tgz",
-      "integrity": "sha512-wNAnxeEAiFs6N+SxS0y6wTJWfewEzUETuyS2aZmT00xk50VijwyFRuhm4sYSjusMyshevomFwz5jNISCxRsGWw==",
+      "version": "0.0.11",
+      "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.11.tgz",
+      "integrity": "sha512-a7nl/2KLpRHOYx75YbYZpWspUbX1DFY7JIZbOv5x0QU8SvwDbJt+Hm01vG34PffFyYvHEXrc6Qnip2RTjljNjg==",
+      "license": "MIT",
       "engines": {
         "node": ">=18.0.0"
       },
@@ -5310,14 +4176,15 @@
       }
     },
     "node_modules/@rollup/pluginutils": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
-      "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==",
+      "version": "5.1.4",
+      "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
+      "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/estree": "^1.0.0",
         "estree-walker": "^2.0.2",
-        "picomatch": "^2.3.1"
+        "picomatch": "^4.0.2"
       },
       "engines": {
         "node": ">=14.0.0"
@@ -5331,236 +4198,284 @@
         }
       }
     },
-    "node_modules/@rollup/pluginutils/node_modules/estree-walker": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
-      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
-      "dev": true
-    },
-    "node_modules/@rollup/pluginutils/node_modules/picomatch": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
-      "dev": true,
-      "engines": {
-        "node": ">=8.6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/jonschlinkert"
-      }
-    },
     "node_modules/@rollup/rollup-android-arm-eabi": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.3.tgz",
-      "integrity": "sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz",
+      "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==",
       "cpu": [
         "arm"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "android"
       ]
     },
     "node_modules/@rollup/rollup-android-arm64": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.3.tgz",
-      "integrity": "sha512-eQK5JIi+POhFpzk+LnjKIy4Ks+pwJ+NXmPxOCSvOKSNRPONzKuUvWE+P9JxGZVxrtzm6BAYMaL50FFuPe0oWMQ==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz",
+      "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==",
       "cpu": [
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "android"
       ]
     },
     "node_modules/@rollup/rollup-darwin-arm64": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.3.tgz",
-      "integrity": "sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz",
+      "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==",
       "cpu": [
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
       ]
     },
     "node_modules/@rollup/rollup-darwin-x64": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.3.tgz",
-      "integrity": "sha512-0IMAO21axJeNIrvS9lSe/PGthc8ZUS+zC53O0VhF5gMxfmcKAP4ESkKOCwEi6u2asUrt4mQv2rjY8QseIEb1aw==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz",
+      "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==",
       "cpu": [
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
       ]
     },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz",
+      "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz",
+      "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
     "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.3.tgz",
-      "integrity": "sha512-ge2DC7tHRHa3caVEoSbPRJpq7azhG+xYsd6u2MEnJ6XzPSzQsTKyXvh6iWjXRf7Rt9ykIUWHtl0Uz3T6yXPpKw==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz",
+      "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==",
       "cpu": [
         "arm"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-musleabihf": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.14.3.tgz",
-      "integrity": "sha512-ljcuiDI4V3ySuc7eSk4lQ9wU8J8r8KrOUvB2U+TtK0TiW6OFDmJ+DdIjjwZHIw9CNxzbmXY39wwpzYuFDwNXuw==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz",
+      "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==",
       "cpu": [
         "arm"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-gnu": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.3.tgz",
-      "integrity": "sha512-Eci2us9VTHm1eSyn5/eEpaC7eP/mp5n46gTRB3Aar3BgSvDQGJZuicyq6TsH4HngNBgVqC5sDYxOzTExSU+NjA==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz",
+      "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==",
       "cpu": [
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-musl": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.3.tgz",
-      "integrity": "sha512-UrBoMLCq4E92/LCqlh+blpqMz5h1tJttPIniwUgOFJyjWI1qrtrDhhpHPuFxULlUmjFHfloWdixtDhSxJt5iKw==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz",
+      "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==",
       "cpu": [
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz",
+      "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.3.tgz",
-      "integrity": "sha512-5aRjvsS8q1nWN8AoRfrq5+9IflC3P1leMoy4r2WjXyFqf3qcqsxRCfxtZIV58tCxd+Yv7WELPcO9mY9aeQyAmw==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz",
+      "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==",
       "cpu": [
         "ppc64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@rollup/rollup-linux-riscv64-gnu": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.3.tgz",
-      "integrity": "sha512-sk/Qh1j2/RJSX7FhEpJn8n0ndxy/uf0kI/9Zc4b1ELhqULVdTfN6HL31CDaTChiBAOgLcsJ1sgVZjWv8XNEsAQ==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz",
+      "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==",
       "cpu": [
         "riscv64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@rollup/rollup-linux-s390x-gnu": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.3.tgz",
-      "integrity": "sha512-jOO/PEaDitOmY9TgkxF/TQIjXySQe5KVYB57H/8LRP/ux0ZoO8cSHCX17asMSv3ruwslXW/TLBcxyaUzGRHcqg==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz",
+      "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==",
       "cpu": [
         "s390x"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-gnu": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.3.tgz",
-      "integrity": "sha512-8ybV4Xjy59xLMyWo3GCfEGqtKV5M5gCSrZlxkPGvEPCGDLNla7v48S662HSGwRd6/2cSneMQWiv+QzcttLrrOA==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz",
+      "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==",
       "cpu": [
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-musl": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.3.tgz",
-      "integrity": "sha512-s+xf1I46trOY10OqAtZ5Rm6lzHre/UiLA1J2uOhCFXWkbZrJRkYBPO6FhvGfHmdtQ3Bx793MNa7LvoWFAm93bg==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz",
+      "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==",
       "cpu": [
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@rollup/rollup-win32-arm64-msvc": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.3.tgz",
-      "integrity": "sha512-+4h2WrGOYsOumDQ5S2sYNyhVfrue+9tc9XcLWLh+Kw3UOxAvrfOrSMFon60KspcDdytkNDh7K2Vs6eMaYImAZg==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz",
+      "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==",
       "cpu": [
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
       ]
     },
     "node_modules/@rollup/rollup-win32-ia32-msvc": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.3.tgz",
-      "integrity": "sha512-T1l7y/bCeL/kUwh9OD4PQT4aM7Bq43vX05htPJJ46RTI4r5KNt6qJRzAfNfM+OYMNEVBWQzR2Gyk+FXLZfogGw==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz",
+      "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==",
       "cpu": [
         "ia32"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
       ]
     },
     "node_modules/@rollup/rollup-win32-x64-msvc": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.3.tgz",
-      "integrity": "sha512-/BypzV0H1y1HzgYpxqRaXGBRqfodgoBBCcsrujT6QRcakDQdfU+Lq9PENPh5jB4I44YWq+0C2eHsHya+nZY1sA==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz",
+      "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==",
       "cpu": [
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
       ]
     },
+    "node_modules/@scarf/scarf": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
+      "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
+      "hasInstallScript": true,
+      "license": "Apache-2.0"
+    },
     "node_modules/@selderee/plugin-htmlparser2": {
       "version": "0.11.0",
       "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
       "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==",
+      "license": "MIT",
       "dependencies": {
         "domhandler": "^5.0.3",
         "selderee": "^0.11.0"
@@ -5573,6 +4488,7 @@
       "version": "4.1.5",
       "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
       "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
+      "license": "BSD-3-Clause",
       "dependencies": {
         "@hapi/hoek": "^9.0.0"
       }
@@ -5580,22 +4496,26 @@
     "node_modules/@sideway/formula": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
-      "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
+      "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@sideway/pinpoint": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
-      "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
+      "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@socket.io/component-emitter": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
-      "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+      "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+      "license": "MIT"
     },
     "node_modules/@socket.io/redis-adapter": {
       "version": "8.3.0",
       "resolved": "https://registry.npmjs.org/@socket.io/redis-adapter/-/redis-adapter-8.3.0.tgz",
       "integrity": "sha512-ly0cra+48hDmChxmIpnESKrc94LjRL80TEmZVscuQ/WWkRP81nNj8W8cCGMqbI4L6NCuAaPRSzZF1a9GlAxxnA==",
+      "license": "MIT",
       "dependencies": {
         "debug": "~4.3.1",
         "notepack.io": "~3.0.1",
@@ -5608,20 +4528,39 @@
         "socket.io-adapter": "^2.5.4"
       }
     },
+    "node_modules/@socket.io/redis-adapter/node_modules/debug": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@sqltools/formatter": {
       "version": "1.2.5",
       "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz",
-      "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw=="
+      "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==",
+      "license": "MIT"
     },
     "node_modules/@swc/core": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.21.tgz",
-      "integrity": "sha512-7/cN0SZ+y2V6e0hsDD8koGR0QVh7Jl3r756bwaHLLSN+kReoUb/yVcLsA8iTn90JLME3DkQK4CPjxDCQiyMXNg==",
-      "devOptional": true,
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.1.tgz",
+      "integrity": "sha512-rQ4dS6GAdmtzKiCRt3LFVxl37FaY1cgL9kSUTnhQ2xc3fmHOd7jdJK/V4pSZMG1ruGTd0bsi34O2R0Olg9Zo/w==",
+      "dev": true,
       "hasInstallScript": true,
+      "license": "Apache-2.0",
       "dependencies": {
         "@swc/counter": "^0.1.3",
-        "@swc/types": "^0.1.12"
+        "@swc/types": "^0.1.17"
       },
       "engines": {
         "node": ">=10"
@@ -5631,16 +4570,16 @@
         "url": "https://opencollective.com/swc"
       },
       "optionalDependencies": {
-        "@swc/core-darwin-arm64": "1.7.21",
-        "@swc/core-darwin-x64": "1.7.21",
-        "@swc/core-linux-arm-gnueabihf": "1.7.21",
-        "@swc/core-linux-arm64-gnu": "1.7.21",
-        "@swc/core-linux-arm64-musl": "1.7.21",
-        "@swc/core-linux-x64-gnu": "1.7.21",
-        "@swc/core-linux-x64-musl": "1.7.21",
-        "@swc/core-win32-arm64-msvc": "1.7.21",
-        "@swc/core-win32-ia32-msvc": "1.7.21",
-        "@swc/core-win32-x64-msvc": "1.7.21"
+        "@swc/core-darwin-arm64": "1.10.1",
+        "@swc/core-darwin-x64": "1.10.1",
+        "@swc/core-linux-arm-gnueabihf": "1.10.1",
+        "@swc/core-linux-arm64-gnu": "1.10.1",
+        "@swc/core-linux-arm64-musl": "1.10.1",
+        "@swc/core-linux-x64-gnu": "1.10.1",
+        "@swc/core-linux-x64-musl": "1.10.1",
+        "@swc/core-win32-arm64-msvc": "1.10.1",
+        "@swc/core-win32-ia32-msvc": "1.10.1",
+        "@swc/core-win32-x64-msvc": "1.10.1"
       },
       "peerDependencies": {
         "@swc/helpers": "*"
@@ -5652,13 +4591,14 @@
       }
     },
     "node_modules/@swc/core-darwin-arm64": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.21.tgz",
-      "integrity": "sha512-hh5uOZ7jWF66z2TRMhhXtWMQkssuPCSIZPy9VHf5KvZ46cX+5UeECDthchYklEVZQyy4Qr6oxfh4qff/5spoMA==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.1.tgz",
+      "integrity": "sha512-NyELPp8EsVZtxH/mEqvzSyWpfPJ1lugpTQcSlMduZLj1EASLO4sC8wt8hmL1aizRlsbjCX+r0PyL+l0xQ64/6Q==",
       "cpu": [
         "arm64"
       ],
       "dev": true,
+      "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
         "darwin"
@@ -5668,13 +4608,14 @@
       }
     },
     "node_modules/@swc/core-darwin-x64": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.21.tgz",
-      "integrity": "sha512-lTsPquqSierQ6jWiWM7NnYXXZGk9zx3NGkPLHjPbcH5BmyiauX0CC/YJYJx7YmS2InRLyALlGmidHkaF4JY28A==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.1.tgz",
+      "integrity": "sha512-L4BNt1fdQ5ZZhAk5qoDfUnXRabDOXKnXBxMDJ+PWLSxOGBbWE6aJTnu4zbGjJvtot0KM46m2LPAPY8ttknqaZA==",
       "cpu": [
         "x64"
       ],
       "dev": true,
+      "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
         "darwin"
@@ -5684,13 +4625,14 @@
       }
     },
     "node_modules/@swc/core-linux-arm-gnueabihf": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.21.tgz",
-      "integrity": "sha512-AgSd0fnSzAqCvWpzzZCq75z62JVGUkkXEOpfdi99jj/tryPy38KdXJtkVWJmufPXlRHokGTBitalk33WDJwsbA==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.1.tgz",
+      "integrity": "sha512-Y1u9OqCHgvVp2tYQAJ7hcU9qO5brDMIrA5R31rwWQIAKDkJKtv3IlTHF0hrbWk1wPR0ZdngkQSJZple7G+Grvw==",
       "cpu": [
         "arm"
       ],
       "dev": true,
+      "license": "Apache-2.0",
       "optional": true,
       "os": [
         "linux"
@@ -5700,13 +4642,14 @@
       }
     },
     "node_modules/@swc/core-linux-arm64-gnu": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.21.tgz",
-      "integrity": "sha512-l+jw6RQ4Y43/8dIst0c73uQE+W3kCWrCFqMqC/xIuE/iqHOnvYK6YbA1ffOct2dImkHzNiKuoehGqtQAc6cNaQ==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.1.tgz",
+      "integrity": "sha512-tNQHO/UKdtnqjc7o04iRXng1wTUXPgVd8Y6LI4qIbHVoVPwksZydISjMcilKNLKIwOoUQAkxyJ16SlOAeADzhQ==",
       "cpu": [
         "arm64"
       ],
       "dev": true,
+      "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
         "linux"
@@ -5716,13 +4659,14 @@
       }
     },
     "node_modules/@swc/core-linux-arm64-musl": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.21.tgz",
-      "integrity": "sha512-29KKZXrTo/c9F1JFL9WsNvCa6UCdIVhHP5EfuYhlKbn5/YmSsNFkuHdUtZFEd5U4+jiShXDmgGCtLW2d08LIwg==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.1.tgz",
+      "integrity": "sha512-x0L2Pd9weQ6n8dI1z1Isq00VHFvpBClwQJvrt3NHzmR+1wCT/gcYl1tp9P5xHh3ldM8Cn4UjWCw+7PaUgg8FcQ==",
       "cpu": [
         "arm64"
       ],
       "dev": true,
+      "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
         "linux"
@@ -5732,13 +4676,14 @@
       }
     },
     "node_modules/@swc/core-linux-x64-gnu": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.21.tgz",
-      "integrity": "sha512-HsP3JwddvQj5HvnjmOr+Bd5plEm6ccpfP5wUlm3hywzvdVkj+yR29bmD7UwpV/1zCQ60Ry35a7mXhKI6HQxFgw==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.1.tgz",
+      "integrity": "sha512-yyYEwQcObV3AUsC79rSzN9z6kiWxKAVJ6Ntwq2N9YoZqSPYph+4/Am5fM1xEQYf/kb99csj0FgOelomJSobxQA==",
       "cpu": [
         "x64"
       ],
       "dev": true,
+      "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
         "linux"
@@ -5748,13 +4693,14 @@
       }
     },
     "node_modules/@swc/core-linux-x64-musl": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.21.tgz",
-      "integrity": "sha512-hYKLVeUTHqvFK628DFJEwxoX6p42T3HaQ4QjNtf3oKhiJWFh9iTRUrN/oCB5YI3R9WMkFkKh+99gZ/Dd0T5lsg==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.1.tgz",
+      "integrity": "sha512-tcaS43Ydd7Fk7sW5ROpaf2Kq1zR+sI5K0RM+0qYLYYurvsJruj3GhBCaiN3gkzd8m/8wkqNqtVklWaQYSDsyqA==",
       "cpu": [
         "x64"
       ],
       "dev": true,
+      "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
         "linux"
@@ -5764,13 +4710,14 @@
       }
     },
     "node_modules/@swc/core-win32-arm64-msvc": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.21.tgz",
-      "integrity": "sha512-qyWAKW10aMBe6iUqeZ7NAJIswjfggVTUpDINpQGUJhz+pR71YZDidXgZXpaDB84YyDB2JAlRqd1YrLkl7CMiIw==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.1.tgz",
+      "integrity": "sha512-D3Qo1voA7AkbOzQ2UGuKNHfYGKL6eejN8VWOoQYtGHHQi1p5KK/Q7V1ku55oxXBsj79Ny5FRMqiRJpVGad7bjQ==",
       "cpu": [
         "arm64"
       ],
       "dev": true,
+      "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
         "win32"
@@ -5780,13 +4727,14 @@
       }
     },
     "node_modules/@swc/core-win32-ia32-msvc": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.21.tgz",
-      "integrity": "sha512-cy61wS3wgH5mEwBiQ5w6/FnQrchBDAdPsSh0dKSzNmI+4K8hDxS8uzdBycWqJXO0cc+mA77SIlwZC3hP3Kum2g==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.1.tgz",
+      "integrity": "sha512-WalYdFoU3454Og+sDKHM1MrjvxUGwA2oralknXkXL8S0I/8RkWZOB++p3pLaGbTvOO++T+6znFbQdR8KRaa7DA==",
       "cpu": [
         "ia32"
       ],
       "dev": true,
+      "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
         "win32"
@@ -5796,13 +4744,14 @@
       }
     },
     "node_modules/@swc/core-win32-x64-msvc": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.21.tgz",
-      "integrity": "sha512-/rexGItJURNJOdae+a48M+loT74nsEU+PyRRVAkZMKNRtLoYFAr0cpDlS5FodIgGunp/nqM0bst4H2w6Y05IKA==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.1.tgz",
+      "integrity": "sha512-JWobfQDbTnoqaIwPKQ3DVSywihVXlQMbDuwik/dDWlj33A8oEHcjPOGs4OqcA3RHv24i+lfCQpM3Mn4FAMfacA==",
       "cpu": [
         "x64"
       ],
       "dev": true,
+      "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
         "win32"
@@ -5814,99 +4763,87 @@
     "node_modules/@swc/counter": {
       "version": "0.1.3",
       "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
-      "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="
+      "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
+      "license": "Apache-2.0"
     },
     "node_modules/@swc/helpers": {
-      "version": "0.5.5",
-      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
-      "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
+      "version": "0.5.13",
+      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz",
+      "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "@swc/counter": "^0.1.3",
         "tslib": "^2.4.0"
       }
     },
     "node_modules/@swc/types": {
-      "version": "0.1.12",
-      "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz",
-      "integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==",
-      "devOptional": true,
+      "version": "0.1.17",
+      "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz",
+      "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==",
+      "dev": true,
+      "license": "Apache-2.0",
       "dependencies": {
         "@swc/counter": "^0.1.3"
       }
     },
     "node_modules/@testcontainers/postgresql": {
-      "version": "10.12.0",
-      "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.12.0.tgz",
-      "integrity": "sha512-n0Q0Btx0R923CDgm6KBXbesPH10CewpuuCPcnmEZzon3IneMzdk4UqVhhNNOeJFDGhtFrZBOoJ1o7CUI4J0vQw==",
+      "version": "10.16.0",
+      "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.16.0.tgz",
+      "integrity": "sha512-zWFQI+3QxlEELRvVv27i6zlVEPNUz9zKaSh7iWmFlCdfhcyr78daS0FG8FIfdQ79VK7YXA4jv+dTYXa2SwXu/w==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "testcontainers": "^10.12.0"
+        "testcontainers": "^10.16.0"
       }
     },
-    "node_modules/@tsconfig/node10": {
-      "version": "1.0.9",
-      "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
-      "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
-      "optional": true,
-      "peer": true
-    },
-    "node_modules/@tsconfig/node12": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
-      "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
-      "optional": true,
-      "peer": true
-    },
-    "node_modules/@tsconfig/node14": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
-      "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
-      "optional": true,
-      "peer": true
-    },
-    "node_modules/@tsconfig/node16": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
-      "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
-      "optional": true,
-      "peer": true
-    },
     "node_modules/@turf/boolean-point-in-polygon": {
-      "version": "6.5.0",
-      "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz",
-      "integrity": "sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A==",
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-7.1.0.tgz",
+      "integrity": "sha512-mprVsyIQ+ijWTZwbnO4Jhxu94ZW2M2CheqLiRTsGJy0Ooay9v6Av5/Nl3/Gst7ZVXxPqMeMaFYkSzcTc87AKew==",
+      "license": "MIT",
       "dependencies": {
-        "@turf/helpers": "^6.5.0",
-        "@turf/invariant": "^6.5.0"
+        "@turf/helpers": "^7.1.0",
+        "@turf/invariant": "^7.1.0",
+        "@types/geojson": "^7946.0.10",
+        "point-in-polygon-hao": "^1.1.0",
+        "tslib": "^2.6.2"
       },
       "funding": {
         "url": "https://opencollective.com/turf"
       }
     },
     "node_modules/@turf/helpers": {
-      "version": "6.5.0",
-      "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
-      "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==",
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.1.0.tgz",
+      "integrity": "sha512-dTeILEUVeNbaEeoZUOhxH5auv7WWlOShbx7QSd4s0T4Z0/iz90z9yaVCtZOLbU89umKotwKaJQltBNO9CzVgaQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/geojson": "^7946.0.10",
+        "tslib": "^2.6.2"
+      },
       "funding": {
         "url": "https://opencollective.com/turf"
       }
     },
     "node_modules/@turf/invariant": {
-      "version": "6.5.0",
-      "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz",
-      "integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==",
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-7.1.0.tgz",
+      "integrity": "sha512-OCLNqkItBYIP1nE9lJGuIUatWGtQ4rhBKAyTfFu0z8npVzGEYzvguEeof8/6LkKmTTEHW53tCjoEhSSzdRh08Q==",
+      "license": "MIT",
       "dependencies": {
-        "@turf/helpers": "^6.5.0"
+        "@turf/helpers": "^7.1.0",
+        "@types/geojson": "^7946.0.10",
+        "tslib": "^2.6.2"
       },
       "funding": {
         "url": "https://opencollective.com/turf"
       }
     },
     "node_modules/@types/archiver": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.2.tgz",
-      "integrity": "sha512-KmROQqbQzKGuaAbmK+ZcytkJ51+YqDa7NmbXjmtC5YBLSyQYo21YaUnQ3HbaPFKL1ooo6RQ6OPYPIDyxfpDDXw==",
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.3.tgz",
+      "integrity": "sha512-a6wUll6k3zX6qs5KlxIggs1P1JcYJaTCx2gnlr+f0S1yd2DoaEwoIK10HmBaLnZwWneBz+JBm0dwcZu0zECBcQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/readdir-glob": "*"
       }
@@ -5915,27 +4852,31 @@
       "version": "1.4.2",
       "resolved": "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.4.2.tgz",
       "integrity": "sha512-HlZ6Dcr205BmNhwkdXqrg2vkFMN2PluI7Lgr8In3B3wE5PiQHhjRqtW/lGdVU9gw+sM0JcIDx2AN+cW8oSWIcw==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@types/aws-lambda": {
-      "version": "8.10.122",
-      "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.122.tgz",
-      "integrity": "sha512-vBkIh9AY22kVOCEKo5CJlyCgmSWvasC+SWUxL/x/vOwRobMpI/HG1xp/Ae3AqmSiZeLUbOhW0FCD3ZjqqUxmXw=="
+      "version": "8.10.143",
+      "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.143.tgz",
+      "integrity": "sha512-u5vzlcR14ge/4pMTTMDQr3MF0wEe38B2F9o84uC4F43vN5DGTy63npRrB6jQhyt+C0lGv4ZfiRcRkqJoZuPnmg==",
+      "license": "MIT"
     },
     "node_modules/@types/bcrypt": {
       "version": "5.0.2",
       "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz",
       "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/node": "*"
       }
     },
     "node_modules/@types/body-parser": {
-      "version": "1.19.3",
-      "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz",
-      "integrity": "sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==",
+      "version": "1.19.5",
+      "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
+      "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/connect": "*",
         "@types/node": "*"
@@ -5945,6 +4886,7 @@
       "version": "1.8.9",
       "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.9.tgz",
       "integrity": "sha512-ZqS9JGpBxVOvsawzmVt30sP++gSQMTejCkIAQ3VdadOcRE8izTyW66hufvwLeH+YEGP6Js2AW7Gz+RMyvrEbmw==",
+      "license": "MIT",
       "dependencies": {
         "@types/node": "*"
       }
@@ -5953,6 +4895,7 @@
       "version": "3.4.36",
       "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz",
       "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==",
+      "license": "MIT",
       "dependencies": {
         "@types/node": "*"
       }
@@ -5960,14 +4903,16 @@
     "node_modules/@types/cookie": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
-      "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
+      "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==",
+      "license": "MIT"
     },
     "node_modules/@types/cookie-parser": {
-      "version": "1.4.7",
-      "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.7.tgz",
-      "integrity": "sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==",
+      "version": "1.4.8",
+      "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz",
+      "integrity": "sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==",
       "dev": true,
-      "dependencies": {
+      "license": "MIT",
+      "peerDependencies": {
         "@types/express": "*"
       }
     },
@@ -5975,12 +4920,14 @@
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
       "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@types/cors": {
-      "version": "2.8.14",
-      "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.14.tgz",
-      "integrity": "sha512-RXHUvNWYICtbP6s18PnOCaqToK8y14DnLd75c6HfyKf228dxy7pHNOQkxPtvXKp/hINFMDjbYzsj63nnpPMSRQ==",
+      "version": "2.8.17",
+      "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
+      "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
+      "license": "MIT",
       "dependencies": {
         "@types/node": "*"
       }
@@ -5990,16 +4937,18 @@
       "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz",
       "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/node": "*",
         "@types/ssh2": "*"
       }
     },
     "node_modules/@types/dockerode": {
-      "version": "3.3.29",
-      "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.29.tgz",
-      "integrity": "sha512-5PRRq/yt5OT/Jf77ltIdz4EiR9+VLnPF+HpU4xGFwUqmV24Co2HKBNW3w+slqZ1CYchbcDeqJASHDYWzZCcMiQ==",
+      "version": "3.3.32",
+      "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.32.tgz",
+      "integrity": "sha512-xxcG0g5AWKtNyh7I7wswLdFvym4Mlqks5ZlKzxEUrGHS0r0PUOfxm2T0mspwu10mHQqu3Ck3MI3V2HqvLWE1fg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/docker-modem": "*",
         "@types/node": "*",
@@ -6007,36 +4956,40 @@
       }
     },
     "node_modules/@types/eslint": {
-      "version": "8.44.3",
-      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz",
-      "integrity": "sha512-iM/WfkwAhwmPff3wZuPLYiHX18HI24jU8k1ZSH7P8FHwxTjZ2P6CoX2wnF43oprR+YXJM6UUxATkNvyv/JHd+g==",
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
+      "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/estree": "*",
         "@types/json-schema": "*"
       }
     },
     "node_modules/@types/eslint-scope": {
-      "version": "3.7.5",
-      "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.5.tgz",
-      "integrity": "sha512-JNvhIEyxVW6EoMIFIvj93ZOywYFatlpu9deeH6eSx6PE3WHYvHaQtmHmQeNw7aA81bYGBPPQqdtBm6b1SsQMmA==",
+      "version": "3.7.7",
+      "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
+      "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/eslint": "*",
         "@types/estree": "*"
       }
     },
     "node_modules/@types/estree": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
-      "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
-      "dev": true
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+      "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@types/express": {
       "version": "4.17.21",
       "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
       "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/body-parser": "*",
         "@types/express-serve-static-core": "^4.17.33",
@@ -6045,10 +4998,11 @@
       }
     },
     "node_modules/@types/express-serve-static-core": {
-      "version": "4.17.37",
-      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.37.tgz",
-      "integrity": "sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==",
+      "version": "4.19.6",
+      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
+      "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/node": "*",
         "@types/qs": "*",
@@ -6057,24 +5011,33 @@
       }
     },
     "node_modules/@types/fluent-ffmpeg": {
-      "version": "2.1.26",
-      "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.26.tgz",
-      "integrity": "sha512-0JVF3wdQG+pN0ImwWD0bNgJiKF2OHg/7CDBHw5UIbRTvlnkgGHK6V5doE54ltvhud4o31/dEiHm23CAlxFiUQg==",
+      "version": "2.1.27",
+      "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.27.tgz",
+      "integrity": "sha512-QiDWjihpUhriISNoBi2hJBRUUmoj/BMTYcfz+F+ZM9hHWBYABFAE6hjP/TbCZC0GWwlpa3FzvHH9RzFeRusZ7A==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/node": "*"
       }
     },
+    "node_modules/@types/geojson": {
+      "version": "7946.0.15",
+      "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.15.tgz",
+      "integrity": "sha512-9oSxFzDCT2Rj6DfcHF8G++jxBKS7mBqXl5xrRW+Kbvjry6Uduya2iiwqHPhVXpasAVMBYKkEPGgKhd3+/HZ6xA==",
+      "license": "MIT"
+    },
     "node_modules/@types/http-errors": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz",
-      "integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==",
-      "dev": true
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
+      "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@types/inquirer": {
-      "version": "8.2.6",
-      "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.6.tgz",
-      "integrity": "sha512-3uT88kxg8lNzY8ay2ZjP44DKcRaTGztqeIvN2zHvhzIBH/uAPaL75aBtdNRKbA7xXoMbBt5kX0M00VKAnfOYlA==",
+      "version": "8.2.10",
+      "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.10.tgz",
+      "integrity": "sha512-IdD5NmHyVjWM8SHWo/kPBgtzXatwPkfwzyP3fN1jF2g9BWt5WO+8hL2F4o2GKIYsU40PpqeevuUWvkS/roXJkA==",
+      "license": "MIT",
       "peer": true,
       "dependencies": {
         "@types/through": "*",
@@ -6085,29 +5048,34 @@
       "version": "4.0.9",
       "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
       "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@types/json-schema": {
       "version": "7.0.15",
       "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
       "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@types/lodash": {
-      "version": "4.17.7",
-      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz",
-      "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==",
-      "dev": true
+      "version": "4.17.13",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz",
+      "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@types/luxon": {
       "version": "3.4.2",
       "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
-      "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA=="
+      "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==",
+      "license": "MIT"
     },
     "node_modules/@types/memcached": {
       "version": "2.2.10",
       "resolved": "https://registry.npmjs.org/@types/memcached/-/memcached-2.2.10.tgz",
       "integrity": "sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==",
+      "license": "MIT",
       "dependencies": {
         "@types/node": "*"
       }
@@ -6116,19 +5084,22 @@
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
       "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@types/mime": {
-      "version": "1.3.3",
-      "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz",
-      "integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==",
-      "dev": true
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+      "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@types/mock-fs": {
       "version": "4.13.4",
       "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz",
       "integrity": "sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/node": "*"
       }
@@ -6138,31 +5109,35 @@
       "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz",
       "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/express": "*"
       }
     },
     "node_modules/@types/mysql": {
-      "version": "2.15.22",
-      "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.22.tgz",
-      "integrity": "sha512-wK1pzsJVVAjYCSZWQoWHziQZbNggXFDUEIGf54g4ZM/ERuP86uGdWeKZWMYlqTPMZfHJJvLPyogXGvCOg87yLQ==",
+      "version": "2.15.26",
+      "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz",
+      "integrity": "sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==",
+      "license": "MIT",
       "dependencies": {
         "@types/node": "*"
       }
     },
     "node_modules/@types/node": {
-      "version": "20.16.5",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz",
-      "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==",
+      "version": "22.10.2",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
+      "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
+      "license": "MIT",
       "dependencies": {
-        "undici-types": "~6.19.2"
+        "undici-types": "~6.20.0"
       }
     },
     "node_modules/@types/nodemailer": {
-      "version": "6.4.15",
-      "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.15.tgz",
-      "integrity": "sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==",
+      "version": "6.4.17",
+      "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz",
+      "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/node": "*"
       }
@@ -6171,117 +5146,76 @@
       "version": "2.4.4",
       "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
       "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@types/pg": {
-      "version": "8.10.9",
-      "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.9.tgz",
-      "integrity": "sha512-UksbANNE/f8w0wOMxVKKIrLCbEMV+oM1uKejmwXr39olg4xqcfBDbXxObJAt6XxHbDa4XTKOlUEcEltXDX+XLQ==",
+      "version": "8.6.1",
+      "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz",
+      "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==",
+      "license": "MIT",
       "dependencies": {
         "@types/node": "*",
         "pg-protocol": "*",
-        "pg-types": "^4.0.1"
+        "pg-types": "^2.2.0"
       }
     },
     "node_modules/@types/pg-pool": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.4.tgz",
-      "integrity": "sha512-qZAvkv1K3QbmHHFYSNRYPkRjOWRLBYrL4B9c+wG0GSVGBw0NtJwPcgx/DSddeDJvRGMHCEQ4VMEVfuJ/0gZ3XQ==",
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz",
+      "integrity": "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==",
+      "license": "MIT",
       "dependencies": {
         "@types/pg": "*"
       }
     },
-    "node_modules/@types/pg/node_modules/pg-types": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.1.tgz",
-      "integrity": "sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==",
-      "dependencies": {
-        "pg-int8": "1.0.1",
-        "pg-numeric": "1.0.2",
-        "postgres-array": "~3.0.1",
-        "postgres-bytea": "~3.0.0",
-        "postgres-date": "~2.0.1",
-        "postgres-interval": "^3.0.0",
-        "postgres-range": "^1.1.1"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/@types/pg/node_modules/postgres-array": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz",
-      "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==",
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/@types/pg/node_modules/postgres-bytea": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz",
-      "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==",
-      "dependencies": {
-        "obuf": "~1.1.2"
-      },
-      "engines": {
-        "node": ">= 6"
-      }
-    },
-    "node_modules/@types/pg/node_modules/postgres-date": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.0.1.tgz",
-      "integrity": "sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==",
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/@types/pg/node_modules/postgres-interval": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz",
-      "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==",
-      "engines": {
-        "node": ">=12"
-      }
-    },
     "node_modules/@types/picomatch": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-3.0.1.tgz",
       "integrity": "sha512-1MRgzpzY0hOp9pW/kLRxeQhUWwil6gnrUYd3oEpeYBqp/FexhaCPv3F8LsYr47gtUU45fO2cm1dbwkSrHEo8Uw==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
-    "node_modules/@types/prop-types": {
-      "version": "15.7.12",
-      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
-      "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
-      "dev": true
+    "node_modules/@types/pngjs": {
+      "version": "6.0.5",
+      "resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.5.tgz",
+      "integrity": "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
     },
     "node_modules/@types/qs": {
-      "version": "6.9.8",
-      "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz",
-      "integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==",
-      "dev": true
+      "version": "6.9.17",
+      "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz",
+      "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@types/range-parser": {
-      "version": "1.2.5",
-      "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz",
-      "integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==",
-      "dev": true
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+      "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@types/react": {
-      "version": "18.3.4",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz",
-      "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==",
+      "version": "19.0.1",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.1.tgz",
+      "integrity": "sha512-YW6614BDhqbpR5KtUYzTA+zlA7nayzJRA9ljz9CQoxthR0sDisYZLuvSMsil36t4EH/uAt8T52Xb4sVw17G+SQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@types/prop-types": "*",
         "csstype": "^3.0.2"
       }
     },
     "node_modules/@types/readdir-glob": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.2.tgz",
-      "integrity": "sha512-vwAYrNN/8yhp/FJRU6HUSD0yk6xfoOS8HrZa8ZL7j+X8hJpaC1hTcAiXX2IxaAkkvrz9mLyoEhYZTE3cEYvA9Q==",
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz",
+      "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/node": "*"
       }
@@ -6290,62 +5224,86 @@
       "version": "7.5.8",
       "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
       "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@types/send": {
-      "version": "0.17.2",
-      "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.2.tgz",
-      "integrity": "sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==",
+      "version": "0.17.4",
+      "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
+      "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/mime": "^1",
         "@types/node": "*"
       }
     },
     "node_modules/@types/serve-static": {
-      "version": "1.15.3",
-      "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.3.tgz",
-      "integrity": "sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==",
+      "version": "1.15.7",
+      "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
+      "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/http-errors": "*",
-        "@types/mime": "*",
-        "@types/node": "*"
+        "@types/node": "*",
+        "@types/send": "*"
       }
     },
     "node_modules/@types/shimmer": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
-      "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="
+      "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==",
+      "license": "MIT"
     },
     "node_modules/@types/ssh2": {
-      "version": "0.5.52",
-      "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz",
-      "integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==",
+      "version": "1.15.1",
+      "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz",
+      "integrity": "sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@types/node": "*",
-        "@types/ssh2-streams": "*"
+        "@types/node": "^18.11.18"
       }
     },
     "node_modules/@types/ssh2-streams": {
-      "version": "0.1.10",
-      "resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.10.tgz",
-      "integrity": "sha512-r3HYPL0kPxRwk7Nk1P4JxaWPyJ2Mfnfm5efuK0vYgYZu16RerZUTyun6Yqu5KEfc3AR7BvTL1x+nzf7+kbKftQ==",
+      "version": "0.1.12",
+      "resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.12.tgz",
+      "integrity": "sha512-Sy8tpEmCce4Tq0oSOYdfqaBpA3hDM8SoxoFh5vzFsu2oL+znzGz8oVWW7xb4K920yYMUY+PIG31qZnFMfPWNCg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/node": "*"
       }
     },
-    "node_modules/@types/superagent": {
-      "version": "8.1.6",
-      "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.6.tgz",
-      "integrity": "sha512-yzBOv+6meEHSzV2NThYYOA6RtqvPr3Hbob9ZLp3i07SH27CrYVfm8CrF7ydTmidtelsFiKx2I4gZAiAOamGgvQ==",
+    "node_modules/@types/ssh2/node_modules/@types/node": {
+      "version": "18.19.68",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.68.tgz",
+      "integrity": "sha512-QGtpFH1vB99ZmTa63K4/FU8twThj4fuVSBkGddTp7uIL/cuoLWIUSL2RcOaigBhfR+hg5pgGkBnkoOxrTVBMKw==",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~5.26.4"
+      }
+    },
+    "node_modules/@types/ssh2/node_modules/undici-types": {
+      "version": "5.26.5",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/superagent": {
+      "version": "8.1.9",
+      "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz",
+      "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==",
+      "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/cookiejar": "^2.1.5",
         "@types/methods": "^1.1.4",
-        "@types/node": "*"
+        "@types/node": "*",
+        "form-data": "^4.0.0"
       }
     },
     "node_modules/@types/supertest": {
@@ -6353,6 +5311,7 @@
       "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz",
       "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/methods": "^1.1.4",
         "@types/superagent": "^8.1.0"
@@ -6362,14 +5321,16 @@
       "version": "4.0.14",
       "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz",
       "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==",
+      "license": "MIT",
       "dependencies": {
         "@types/node": "*"
       }
     },
     "node_modules/@types/through": {
-      "version": "0.0.31",
-      "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.31.tgz",
-      "integrity": "sha512-LpKpmb7FGevYgXnBXYs6HWnmiFyVG07Pt1cnbgM1IhEacITTiUaBXXvOR3Y50ksaJWGSfhbEvQFivQEFGCC55w==",
+      "version": "0.0.33",
+      "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz",
+      "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==",
+      "license": "MIT",
       "peer": true,
       "dependencies": {
         "@types/node": "*"
@@ -6379,24 +5340,27 @@
       "version": "0.7.39",
       "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz",
       "integrity": "sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@types/validator": {
-      "version": "13.11.8",
-      "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.8.tgz",
-      "integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ=="
+      "version": "13.12.2",
+      "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz",
+      "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==",
+      "license": "MIT"
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz",
-      "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==",
+      "version": "8.18.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.1.tgz",
+      "integrity": "sha512-Ncvsq5CT3Gvh+uJG0Lwlho6suwDfUXH0HztslDf5I+F2wAFAZMRwYLEorumpKLzmO2suAXZ/td1tBg4NZIi9CQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@eslint-community/regexpp": "^4.10.0",
-        "@typescript-eslint/scope-manager": "8.3.0",
-        "@typescript-eslint/type-utils": "8.3.0",
-        "@typescript-eslint/utils": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0",
+        "@typescript-eslint/scope-manager": "8.18.1",
+        "@typescript-eslint/type-utils": "8.18.1",
+        "@typescript-eslint/utils": "8.18.1",
+        "@typescript-eslint/visitor-keys": "8.18.1",
         "graphemer": "^1.4.0",
         "ignore": "^5.3.1",
         "natural-compare": "^1.4.0",
@@ -6411,24 +5375,21 @@
       },
       "peerDependencies": {
         "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
-        "eslint": "^8.57.0 || ^9.0.0"
-      },
-      "peerDependenciesMeta": {
-        "typescript": {
-          "optional": true
-        }
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <5.8.0"
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz",
-      "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==",
+      "version": "8.18.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.1.tgz",
+      "integrity": "sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/scope-manager": "8.3.0",
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/typescript-estree": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0",
+        "@typescript-eslint/scope-manager": "8.18.1",
+        "@typescript-eslint/types": "8.18.1",
+        "@typescript-eslint/typescript-estree": "8.18.1",
+        "@typescript-eslint/visitor-keys": "8.18.1",
         "debug": "^4.3.4"
       },
       "engines": {
@@ -6439,22 +5400,19 @@
         "url": "https://opencollective.com/typescript-eslint"
       },
       "peerDependencies": {
-        "eslint": "^8.57.0 || ^9.0.0"
-      },
-      "peerDependenciesMeta": {
-        "typescript": {
-          "optional": true
-        }
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <5.8.0"
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz",
-      "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==",
+      "version": "8.18.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.1.tgz",
+      "integrity": "sha512-HxfHo2b090M5s2+/9Z3gkBhI6xBH8OJCFjH9MhQ+nnoZqxU3wNxkLT+VWXWSFWc3UF3Z+CfPAyqdCTdoXtDPCQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0"
+        "@typescript-eslint/types": "8.18.1",
+        "@typescript-eslint/visitor-keys": "8.18.1"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -6465,13 +5423,14 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz",
-      "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==",
+      "version": "8.18.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.1.tgz",
+      "integrity": "sha512-jAhTdK/Qx2NJPNOTxXpMwlOiSymtR2j283TtPqXkKBdH8OAMmhiUfP0kJjc/qSE51Xrq02Gj9NY7MwK+UxVwHQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "8.3.0",
-        "@typescript-eslint/utils": "8.3.0",
+        "@typescript-eslint/typescript-estree": "8.18.1",
+        "@typescript-eslint/utils": "8.18.1",
         "debug": "^4.3.4",
         "ts-api-utils": "^1.3.0"
       },
@@ -6482,17 +5441,17 @@
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
       },
-      "peerDependenciesMeta": {
-        "typescript": {
-          "optional": true
-        }
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <5.8.0"
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz",
-      "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==",
+      "version": "8.18.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.1.tgz",
+      "integrity": "sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       },
@@ -6502,13 +5461,14 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz",
-      "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==",
+      "version": "8.18.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.1.tgz",
+      "integrity": "sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0",
+        "@typescript-eslint/types": "8.18.1",
+        "@typescript-eslint/visitor-keys": "8.18.1",
         "debug": "^4.3.4",
         "fast-glob": "^3.3.2",
         "is-glob": "^4.0.3",
@@ -6523,10 +5483,8 @@
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
       },
-      "peerDependenciesMeta": {
-        "typescript": {
-          "optional": true
-        }
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <5.8.0"
       }
     },
     "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
@@ -6534,6 +5492,7 @@
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
       "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "balanced-match": "^1.0.0"
       }
@@ -6543,6 +5502,7 @@
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
       "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "brace-expansion": "^2.0.1"
       },
@@ -6554,15 +5514,16 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz",
-      "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==",
+      "version": "8.18.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.1.tgz",
+      "integrity": "sha512-8vikiIj2ebrC4WRdcAdDcmnu9Q/MXXwg+STf40BVfT8exDqBCUPdypvzcUPxEqRGKg9ALagZ0UWcYCtn+4W2iQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
-        "@typescript-eslint/scope-manager": "8.3.0",
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/typescript-estree": "8.3.0"
+        "@typescript-eslint/scope-manager": "8.18.1",
+        "@typescript-eslint/types": "8.18.1",
+        "@typescript-eslint/typescript-estree": "8.18.1"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -6572,17 +5533,19 @@
         "url": "https://opencollective.com/typescript-eslint"
       },
       "peerDependencies": {
-        "eslint": "^8.57.0 || ^9.0.0"
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <5.8.0"
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz",
-      "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==",
+      "version": "8.18.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.1.tgz",
+      "integrity": "sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.3.0",
-        "eslint-visitor-keys": "^3.4.3"
+        "@typescript-eslint/types": "8.18.1",
+        "eslint-visitor-keys": "^4.2.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -6592,22 +5555,36 @@
         "url": "https://opencollective.com/typescript-eslint"
       }
     },
-    "node_modules/@vitest/coverage-v8": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz",
-      "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==",
+    "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
       "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@vitest/coverage-v8": {
+      "version": "2.1.8",
+      "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.8.tgz",
+      "integrity": "sha512-2Y7BPlKH18mAZYAW1tYByudlCYrQyl5RGvnnDYJKW5tCiO5qg3KSAy3XAxcxKz900a0ZXxWtKrMuZLe3lKBpJw==",
+      "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@ampproject/remapping": "^2.3.0",
         "@bcoe/v8-coverage": "^0.2.3",
-        "debug": "^4.3.5",
+        "debug": "^4.3.7",
         "istanbul-lib-coverage": "^3.2.2",
         "istanbul-lib-report": "^3.0.1",
         "istanbul-lib-source-maps": "^5.0.6",
         "istanbul-reports": "^3.1.7",
-        "magic-string": "^0.30.10",
-        "magicast": "^0.3.4",
-        "std-env": "^3.7.0",
+        "magic-string": "^0.30.12",
+        "magicast": "^0.3.5",
+        "std-env": "^3.8.0",
         "test-exclude": "^7.0.1",
         "tinyrainbow": "^1.2.0"
       },
@@ -6615,38 +5592,94 @@
         "url": "https://opencollective.com/vitest"
       },
       "peerDependencies": {
-        "vitest": "2.0.5"
+        "@vitest/browser": "2.1.8",
+        "vitest": "2.1.8"
+      },
+      "peerDependenciesMeta": {
+        "@vitest/browser": {
+          "optional": true
+        }
       }
     },
     "node_modules/@vitest/coverage-v8/node_modules/magic-string": {
-      "version": "0.30.11",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
-      "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
+      "version": "0.30.17",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+      "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.5.0"
       }
     },
     "node_modules/@vitest/expect": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz",
-      "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==",
+      "version": "2.1.8",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz",
+      "integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@vitest/spy": "2.0.5",
-        "@vitest/utils": "2.0.5",
-        "chai": "^5.1.1",
+        "@vitest/spy": "2.1.8",
+        "@vitest/utils": "2.1.8",
+        "chai": "^5.1.2",
         "tinyrainbow": "^1.2.0"
       },
       "funding": {
         "url": "https://opencollective.com/vitest"
       }
     },
-    "node_modules/@vitest/pretty-format": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz",
-      "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==",
+    "node_modules/@vitest/mocker": {
+      "version": "2.1.8",
+      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz",
+      "integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/spy": "2.1.8",
+        "estree-walker": "^3.0.3",
+        "magic-string": "^0.30.12"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "msw": "^2.4.9",
+        "vite": "^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "msw": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vitest/mocker/node_modules/estree-walker": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+      "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "^1.0.0"
+      }
+    },
+    "node_modules/@vitest/mocker/node_modules/magic-string": {
+      "version": "0.30.17",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+      "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0"
+      }
+    },
+    "node_modules/@vitest/pretty-format": {
+      "version": "2.1.8",
+      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz",
+      "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==",
+      "dev": true,
+      "license": "MIT",
       "dependencies": {
         "tinyrainbow": "^1.2.0"
       },
@@ -6655,12 +5688,13 @@
       }
     },
     "node_modules/@vitest/runner": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz",
-      "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==",
+      "version": "2.1.8",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.8.tgz",
+      "integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@vitest/utils": "2.0.5",
+        "@vitest/utils": "2.1.8",
         "pathe": "^1.1.2"
       },
       "funding": {
@@ -6668,13 +5702,14 @@
       }
     },
     "node_modules/@vitest/snapshot": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz",
-      "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==",
+      "version": "2.1.8",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz",
+      "integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@vitest/pretty-format": "2.0.5",
-        "magic-string": "^0.30.10",
+        "@vitest/pretty-format": "2.1.8",
+        "magic-string": "^0.30.12",
         "pathe": "^1.1.2"
       },
       "funding": {
@@ -6682,35 +5717,37 @@
       }
     },
     "node_modules/@vitest/snapshot/node_modules/magic-string": {
-      "version": "0.30.11",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
-      "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
+      "version": "0.30.17",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+      "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.5.0"
       }
     },
     "node_modules/@vitest/spy": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz",
-      "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==",
+      "version": "2.1.8",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz",
+      "integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "tinyspy": "^3.0.0"
+        "tinyspy": "^3.0.2"
       },
       "funding": {
         "url": "https://opencollective.com/vitest"
       }
     },
     "node_modules/@vitest/utils": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz",
-      "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==",
+      "version": "2.1.8",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz",
+      "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@vitest/pretty-format": "2.0.5",
-        "estree-walker": "^3.0.3",
-        "loupe": "^3.1.1",
+        "@vitest/pretty-format": "2.1.8",
+        "loupe": "^3.1.2",
         "tinyrainbow": "^1.2.0"
       },
       "funding": {
@@ -6718,148 +5755,163 @@
       }
     },
     "node_modules/@webassemblyjs/ast": {
-      "version": "1.12.1",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
-      "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==",
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
+      "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@webassemblyjs/helper-numbers": "1.11.6",
-        "@webassemblyjs/helper-wasm-bytecode": "1.11.6"
+        "@webassemblyjs/helper-numbers": "1.13.2",
+        "@webassemblyjs/helper-wasm-bytecode": "1.13.2"
       }
     },
     "node_modules/@webassemblyjs/floating-point-hex-parser": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz",
-      "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==",
-      "dev": true
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
+      "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@webassemblyjs/helper-api-error": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz",
-      "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==",
-      "dev": true
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
+      "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@webassemblyjs/helper-buffer": {
-      "version": "1.12.1",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz",
-      "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==",
-      "dev": true
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
+      "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@webassemblyjs/helper-numbers": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz",
-      "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==",
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
+      "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@webassemblyjs/floating-point-hex-parser": "1.11.6",
-        "@webassemblyjs/helper-api-error": "1.11.6",
+        "@webassemblyjs/floating-point-hex-parser": "1.13.2",
+        "@webassemblyjs/helper-api-error": "1.13.2",
         "@xtuc/long": "4.2.2"
       }
     },
     "node_modules/@webassemblyjs/helper-wasm-bytecode": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz",
-      "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==",
-      "dev": true
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
+      "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@webassemblyjs/helper-wasm-section": {
-      "version": "1.12.1",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz",
-      "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==",
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
+      "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@webassemblyjs/ast": "1.12.1",
-        "@webassemblyjs/helper-buffer": "1.12.1",
-        "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
-        "@webassemblyjs/wasm-gen": "1.12.1"
+        "@webassemblyjs/ast": "1.14.1",
+        "@webassemblyjs/helper-buffer": "1.14.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+        "@webassemblyjs/wasm-gen": "1.14.1"
       }
     },
     "node_modules/@webassemblyjs/ieee754": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz",
-      "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==",
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
+      "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@xtuc/ieee754": "^1.2.0"
       }
     },
     "node_modules/@webassemblyjs/leb128": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz",
-      "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==",
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
+      "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
       "dev": true,
+      "license": "Apache-2.0",
       "dependencies": {
         "@xtuc/long": "4.2.2"
       }
     },
     "node_modules/@webassemblyjs/utf8": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz",
-      "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==",
-      "dev": true
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
+      "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@webassemblyjs/wasm-edit": {
-      "version": "1.12.1",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz",
-      "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==",
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
+      "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@webassemblyjs/ast": "1.12.1",
-        "@webassemblyjs/helper-buffer": "1.12.1",
-        "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
-        "@webassemblyjs/helper-wasm-section": "1.12.1",
-        "@webassemblyjs/wasm-gen": "1.12.1",
-        "@webassemblyjs/wasm-opt": "1.12.1",
-        "@webassemblyjs/wasm-parser": "1.12.1",
-        "@webassemblyjs/wast-printer": "1.12.1"
+        "@webassemblyjs/ast": "1.14.1",
+        "@webassemblyjs/helper-buffer": "1.14.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+        "@webassemblyjs/helper-wasm-section": "1.14.1",
+        "@webassemblyjs/wasm-gen": "1.14.1",
+        "@webassemblyjs/wasm-opt": "1.14.1",
+        "@webassemblyjs/wasm-parser": "1.14.1",
+        "@webassemblyjs/wast-printer": "1.14.1"
       }
     },
     "node_modules/@webassemblyjs/wasm-gen": {
-      "version": "1.12.1",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz",
-      "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==",
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
+      "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@webassemblyjs/ast": "1.12.1",
-        "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
-        "@webassemblyjs/ieee754": "1.11.6",
-        "@webassemblyjs/leb128": "1.11.6",
-        "@webassemblyjs/utf8": "1.11.6"
+        "@webassemblyjs/ast": "1.14.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+        "@webassemblyjs/ieee754": "1.13.2",
+        "@webassemblyjs/leb128": "1.13.2",
+        "@webassemblyjs/utf8": "1.13.2"
       }
     },
     "node_modules/@webassemblyjs/wasm-opt": {
-      "version": "1.12.1",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz",
-      "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==",
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
+      "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@webassemblyjs/ast": "1.12.1",
-        "@webassemblyjs/helper-buffer": "1.12.1",
-        "@webassemblyjs/wasm-gen": "1.12.1",
-        "@webassemblyjs/wasm-parser": "1.12.1"
+        "@webassemblyjs/ast": "1.14.1",
+        "@webassemblyjs/helper-buffer": "1.14.1",
+        "@webassemblyjs/wasm-gen": "1.14.1",
+        "@webassemblyjs/wasm-parser": "1.14.1"
       }
     },
     "node_modules/@webassemblyjs/wasm-parser": {
-      "version": "1.12.1",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz",
-      "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==",
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
+      "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@webassemblyjs/ast": "1.12.1",
-        "@webassemblyjs/helper-api-error": "1.11.6",
-        "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
-        "@webassemblyjs/ieee754": "1.11.6",
-        "@webassemblyjs/leb128": "1.11.6",
-        "@webassemblyjs/utf8": "1.11.6"
+        "@webassemblyjs/ast": "1.14.1",
+        "@webassemblyjs/helper-api-error": "1.13.2",
+        "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+        "@webassemblyjs/ieee754": "1.13.2",
+        "@webassemblyjs/leb128": "1.13.2",
+        "@webassemblyjs/utf8": "1.13.2"
       }
     },
     "node_modules/@webassemblyjs/wast-printer": {
-      "version": "1.12.1",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz",
-      "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==",
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
+      "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@webassemblyjs/ast": "1.12.1",
+        "@webassemblyjs/ast": "1.14.1",
         "@xtuc/long": "4.2.2"
       }
     },
@@ -6867,23 +5919,27 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
       "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
-      "dev": true
+      "dev": true,
+      "license": "BSD-3-Clause"
     },
     "node_modules/@xtuc/long": {
       "version": "4.2.2",
       "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
       "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
-      "dev": true
+      "dev": true,
+      "license": "Apache-2.0"
     },
     "node_modules/abbrev": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
-      "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
+      "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+      "license": "ISC"
     },
     "node_modules/abort-controller": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
       "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+      "license": "MIT",
       "dependencies": {
         "event-target-shim": "^5.0.0"
       },
@@ -6895,6 +5951,7 @@
       "version": "1.3.8",
       "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
       "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "license": "MIT",
       "dependencies": {
         "mime-types": "~2.1.34",
         "negotiator": "0.6.3"
@@ -6904,9 +5961,10 @@
       }
     },
     "node_modules/acorn": {
-      "version": "8.12.0",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz",
-      "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==",
+      "version": "8.14.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
+      "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
+      "license": "MIT",
       "bin": {
         "acorn": "bin/acorn"
       },
@@ -6918,6 +5976,7 @@
       "version": "1.9.5",
       "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
       "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
+      "license": "MIT",
       "peerDependencies": {
         "acorn": "^8"
       }
@@ -6927,24 +5986,16 @@
       "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
       "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
       "dev": true,
+      "license": "MIT",
       "peerDependencies": {
         "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
       }
     },
-    "node_modules/acorn-walk": {
-      "version": "8.3.2",
-      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
-      "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
-      "optional": true,
-      "peer": true,
-      "engines": {
-        "node": ">=0.4.0"
-      }
-    },
     "node_modules/agent-base": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
       "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "license": "MIT",
       "dependencies": {
         "debug": "4"
       },
@@ -6953,14 +6004,15 @@
       }
     },
     "node_modules/ajv": {
-      "version": "8.12.0",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
-      "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+      "version": "6.12.6",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "fast-deep-equal": "^3.1.1",
-        "json-schema-traverse": "^1.0.0",
-        "require-from-string": "^2.0.2",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
         "uri-js": "^4.2.2"
       },
       "funding": {
@@ -6973,6 +6025,7 @@
       "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
       "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "ajv": "^8.0.0"
       },
@@ -6985,11 +6038,46 @@
         }
       }
     },
+    "node_modules/ajv-formats/node_modules/ajv": {
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+      "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "fast-uri": "^3.0.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ajv-formats/node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/ajv-keywords": {
+      "version": "3.5.2",
+      "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+      "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "ajv": "^6.9.1"
+      }
+    },
     "node_modules/ansi-colors": {
       "version": "4.1.3",
       "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
       "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
@@ -6998,6 +6086,7 @@
       "version": "4.3.2",
       "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
       "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+      "license": "MIT",
       "dependencies": {
         "type-fest": "^0.21.3"
       },
@@ -7008,21 +6097,11 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/ansi-escapes/node_modules/type-fest": {
-      "version": "0.21.3",
-      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
-      "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/ansi-regex": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
       "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
@@ -7031,6 +6110,7 @@
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
       "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "license": "MIT",
       "dependencies": {
         "color-convert": "^2.0.1"
       },
@@ -7044,12 +6124,14 @@
     "node_modules/any-promise": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
-      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
+      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+      "license": "MIT"
     },
     "node_modules/anymatch": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
       "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "license": "ISC",
       "dependencies": {
         "normalize-path": "^3.0.0",
         "picomatch": "^2.0.4"
@@ -7062,6 +6144,7 @@
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
       "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "license": "MIT",
       "engines": {
         "node": ">=8.6"
       },
@@ -7073,6 +6156,7 @@
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz",
       "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==",
+      "license": "MIT",
       "engines": {
         "node": ">= 6.0.0"
       }
@@ -7080,17 +6164,20 @@
     "node_modules/append-field": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
-      "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="
+      "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
+      "license": "MIT"
     },
     "node_modules/aproba": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
-      "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
+      "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
+      "license": "ISC"
     },
     "node_modules/archiver": {
       "version": "7.0.1",
       "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz",
       "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==",
+      "license": "MIT",
       "dependencies": {
         "archiver-utils": "^5.0.2",
         "async": "^3.2.4",
@@ -7108,6 +6195,7 @@
       "version": "5.0.2",
       "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz",
       "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==",
+      "license": "MIT",
       "dependencies": {
         "glob": "^10.0.0",
         "graceful-fs": "^4.2.0",
@@ -7121,94 +6209,12 @@
         "node": ">= 14"
       }
     },
-    "node_modules/archiver-utils/node_modules/buffer": {
-      "version": "6.0.3",
-      "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
-      "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "dependencies": {
-        "base64-js": "^1.3.1",
-        "ieee754": "^1.2.1"
-      }
-    },
-    "node_modules/archiver-utils/node_modules/readable-stream": {
-      "version": "4.5.2",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
-      "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
-      "dependencies": {
-        "abort-controller": "^3.0.0",
-        "buffer": "^6.0.3",
-        "events": "^3.3.0",
-        "process": "^0.11.10",
-        "string_decoder": "^1.3.0"
-      },
-      "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      }
-    },
-    "node_modules/archiver/node_modules/buffer": {
-      "version": "6.0.3",
-      "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
-      "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "dependencies": {
-        "base64-js": "^1.3.1",
-        "ieee754": "^1.2.1"
-      }
-    },
-    "node_modules/archiver/node_modules/buffer-crc32": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
-      "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
-      "engines": {
-        "node": ">=8.0.0"
-      }
-    },
-    "node_modules/archiver/node_modules/readable-stream": {
-      "version": "4.5.2",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
-      "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
-      "dependencies": {
-        "abort-controller": "^3.0.0",
-        "buffer": "^6.0.3",
-        "events": "^3.3.0",
-        "process": "^0.11.10",
-        "string_decoder": "^1.3.0"
-      },
-      "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      }
-    },
     "node_modules/are-we-there-yet": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
       "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
+      "deprecated": "This package is no longer supported.",
+      "license": "ISC",
       "dependencies": {
         "delegates": "^1.0.0",
         "readable-stream": "^3.6.0"
@@ -7217,39 +6223,58 @@
         "node": ">=10"
       }
     },
+    "node_modules/are-we-there-yet/node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/arg": {
-      "version": "4.1.3",
-      "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
-      "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
-      "optional": true,
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+      "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+      "license": "MIT",
       "peer": true
     },
     "node_modules/argparse": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
-      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "license": "Python-2.0"
     },
     "node_modules/array-flatten": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
-      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
+      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+      "license": "MIT"
     },
     "node_modules/array-source": {
       "version": "0.0.4",
       "resolved": "https://registry.npmjs.org/array-source/-/array-source-0.0.4.tgz",
-      "integrity": "sha512-frNdc+zBn80vipY+GdcJkLEbMWj3xmzArYApmUGxoiV8uAu/ygcs9icPdsGdA26h0MkHUMW6EN2piIvVx+M5Mw=="
+      "integrity": "sha512-frNdc+zBn80vipY+GdcJkLEbMWj3xmzArYApmUGxoiV8uAu/ygcs9icPdsGdA26h0MkHUMW6EN2piIvVx+M5Mw==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/array-timsort": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz",
       "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/asn1": {
       "version": "0.2.6",
       "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
       "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "safer-buffer": "~2.1.0"
       }
@@ -7259,41 +6284,55 @@
       "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
       "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=12"
       }
     },
     "node_modules/async": {
-      "version": "3.2.4",
-      "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
-      "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
+      "version": "3.2.6",
+      "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+      "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+      "license": "MIT"
     },
     "node_modules/async-lock": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz",
-      "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="
+      "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==",
+      "license": "MIT"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/b4a": {
-      "version": "1.6.4",
-      "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
-      "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw=="
+      "version": "1.6.7",
+      "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
+      "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
+      "license": "Apache-2.0"
     },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
-      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "license": "MIT"
     },
     "node_modules/bare-events": {
-      "version": "2.4.2",
-      "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz",
-      "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==",
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz",
+      "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==",
+      "license": "Apache-2.0",
       "optional": true
     },
     "node_modules/bare-fs": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.1.tgz",
-      "integrity": "sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==",
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz",
+      "integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==",
       "dev": true,
+      "license": "Apache-2.0",
       "optional": true,
       "dependencies": {
         "bare-events": "^2.0.0",
@@ -7302,10 +6341,11 @@
       }
     },
     "node_modules/bare-os": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.0.tgz",
-      "integrity": "sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNg==",
+      "version": "2.4.4",
+      "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.4.tgz",
+      "integrity": "sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==",
       "dev": true,
+      "license": "Apache-2.0",
       "optional": true
     },
     "node_modules/bare-path": {
@@ -7313,19 +6353,21 @@
       "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz",
       "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==",
       "dev": true,
+      "license": "Apache-2.0",
       "optional": true,
       "dependencies": {
         "bare-os": "^2.1.0"
       }
     },
     "node_modules/bare-stream": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.1.3.tgz",
-      "integrity": "sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ==",
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.1.tgz",
+      "integrity": "sha512-eVZbtKM+4uehzrsj49KtCy3Pbg7kO1pJ3SKZ1SFrIH/0pnj9scuGGgUlNDf/7qS8WKtGdiJY5Kyhs/ivYPTB/g==",
       "dev": true,
+      "license": "Apache-2.0",
       "optional": true,
       "dependencies": {
-        "streamx": "^2.18.0"
+        "streamx": "^2.21.0"
       }
     },
     "node_modules/base64-js": {
@@ -7345,12 +6387,14 @@
           "type": "consulting",
           "url": "https://feross.org/support"
         }
-      ]
+      ],
+      "license": "MIT"
     },
     "node_modules/base64id": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
       "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+      "license": "MIT",
       "engines": {
         "node": "^4.5.0 || >= 5.9"
       }
@@ -7359,6 +6403,7 @@
       "version": "13.0.0",
       "resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-13.0.0.tgz",
       "integrity": "sha512-EreW0Vi8TwovhYUHBXXRA5tthuU2ynGsZFlboyMJHCCUXYa2AjgwnE3ubBOJs2xJLcuXFJbi6c/8pH5+FVj8Og==",
+      "license": "MIT",
       "engines": {
         "node": ">=14"
       }
@@ -7368,6 +6413,7 @@
       "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
       "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
       "hasInstallScript": true,
+      "license": "MIT",
       "dependencies": {
         "@mapbox/node-pre-gyp": "^1.0.11",
         "node-addon-api": "^5.0.0"
@@ -7381,45 +6427,62 @@
       "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
       "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "tweetnacl": "^0.14.3"
       }
     },
-    "node_modules/bcrypt/node_modules/node-addon-api": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
-      "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
-    },
     "node_modules/bignumber.js": {
       "version": "9.1.2",
       "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
       "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==",
+      "license": "MIT",
       "engines": {
         "node": "*"
       }
     },
     "node_modules/binary-extensions": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
-      "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+      "license": "MIT",
       "engines": {
         "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/bl": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
       "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+      "license": "MIT",
       "dependencies": {
         "buffer": "^5.5.0",
         "inherits": "^2.0.4",
         "readable-stream": "^3.4.0"
       }
     },
+    "node_modules/bl/node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/body-parser": {
-      "version": "1.20.2",
-      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
-      "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
+      "version": "1.20.3",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+      "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+      "license": "MIT",
       "dependencies": {
         "bytes": "3.1.2",
         "content-type": "~1.0.5",
@@ -7429,7 +6492,7 @@
         "http-errors": "2.0.0",
         "iconv-lite": "0.4.24",
         "on-finished": "2.4.1",
-        "qs": "6.11.0",
+        "qs": "6.13.0",
         "raw-body": "2.5.2",
         "type-is": "~1.6.18",
         "unpipe": "1.0.0"
@@ -7443,6 +6506,7 @@
       "version": "2.6.9",
       "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
       "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "license": "MIT",
       "dependencies": {
         "ms": "2.0.0"
       }
@@ -7450,12 +6514,14 @@
     "node_modules/body-parser/node_modules/ms": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "license": "MIT"
     },
     "node_modules/brace-expansion": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
       "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "license": "MIT",
       "dependencies": {
         "balanced-match": "^1.0.0",
         "concat-map": "0.0.1"
@@ -7465,6 +6531,7 @@
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
       "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "license": "MIT",
       "dependencies": {
         "fill-range": "^7.1.1"
       },
@@ -7473,9 +6540,9 @@
       }
     },
     "node_modules/browserslist": {
-      "version": "4.23.0",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
-      "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
+      "version": "4.24.3",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
+      "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
       "funding": [
         {
           "type": "opencollective",
@@ -7490,11 +6557,12 @@
           "url": "https://github.com/sponsors/ai"
         }
       ],
+      "license": "MIT",
       "dependencies": {
-        "caniuse-lite": "^1.0.30001587",
-        "electron-to-chromium": "^1.4.668",
-        "node-releases": "^2.0.14",
-        "update-browserslist-db": "^1.0.13"
+        "caniuse-lite": "^1.0.30001688",
+        "electron-to-chromium": "^1.5.73",
+        "node-releases": "^2.0.19",
+        "update-browserslist-db": "^1.1.1"
       },
       "bin": {
         "browserslist": "cli.js"
@@ -7521,15 +6589,26 @@
           "url": "https://feross.org/support"
         }
       ],
+      "license": "MIT",
       "dependencies": {
         "base64-js": "^1.3.1",
         "ieee754": "^1.1.13"
       }
     },
+    "node_modules/buffer-crc32": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
+      "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
     "node_modules/buffer-from": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
-      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
+      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+      "license": "MIT"
     },
     "node_modules/buildcheck": {
       "version": "0.0.6",
@@ -7546,6 +6625,7 @@
       "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
       "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       },
@@ -7554,15 +6634,16 @@
       }
     },
     "node_modules/bullmq": {
-      "version": "4.17.0",
-      "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.17.0.tgz",
-      "integrity": "sha512-URnHgB01rlCP8RTpmW3kFnvv3vdd2aI1OcBMYQwnqODxGiJUlz9MibDVXE83mq7ee1eS1IvD9lMQqGszX6E5Pw==",
+      "version": "4.18.2",
+      "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.18.2.tgz",
+      "integrity": "sha512-Cx0O98IlGiFw7UBa+zwGz+nH0Pcl1wfTvMVBlsMna3s0219hXroVovh1xPRgomyUcbyciHiugGCkW0RRNZDHYQ==",
+      "license": "MIT",
       "dependencies": {
         "cron-parser": "^4.6.0",
         "glob": "^8.0.3",
         "ioredis": "^5.3.2",
         "lodash": "^4.17.21",
-        "msgpackr": "^1.10.1",
+        "msgpackr": "^1.6.2",
         "node-abort-controller": "^3.1.1",
         "semver": "^7.5.4",
         "tslib": "^2.0.0",
@@ -7573,6 +6654,7 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
       "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "license": "MIT",
       "dependencies": {
         "balanced-match": "^1.0.0"
       }
@@ -7581,6 +6663,8 @@
       "version": "8.1.0",
       "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
       "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
+      "license": "ISC",
       "dependencies": {
         "fs.realpath": "^1.0.0",
         "inflight": "^1.0.4",
@@ -7599,6 +6683,7 @@
       "version": "5.1.6",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
       "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+      "license": "ISC",
       "dependencies": {
         "brace-expansion": "^2.0.1"
       },
@@ -7606,6 +6691,19 @@
         "node": ">=10"
       }
     },
+    "node_modules/bullmq/node_modules/uuid": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+      "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
     "node_modules/busboy": {
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -7622,6 +6720,7 @@
       "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz",
       "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -7630,6 +6729,7 @@
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
       "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "license": "MIT",
       "engines": {
         "node": ">= 0.8"
       }
@@ -7639,20 +6739,51 @@
       "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
       "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
     },
     "node_modules/call-bind": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
-      "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+      "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+      "dev": true,
+      "license": "MIT",
       "dependencies": {
+        "call-bind-apply-helpers": "^1.0.0",
         "es-define-property": "^1.0.0",
-        "es-errors": "^1.3.0",
-        "function-bind": "^1.1.2",
         "get-intrinsic": "^1.2.4",
-        "set-function-length": "^1.2.1"
+        "set-function-length": "^1.2.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
+      "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz",
+      "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "get-intrinsic": "^1.2.6"
       },
       "engines": {
         "node": ">= 0.4"
@@ -7665,6 +6796,7 @@
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
       "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
@@ -7673,15 +6805,16 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
       "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+      "license": "MIT",
       "peer": true,
       "engines": {
         "node": ">= 6"
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001618",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001618.tgz",
-      "integrity": "sha512-p407+D1tIkDvsEAPS22lJxLQQaG8OTBEqo0KhzfABGk0TU4juBNDSfH0hyAp/HRyx+M8L17z/ltyhxh27FTfQg==",
+      "version": "1.0.30001689",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz",
+      "integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==",
       "funding": [
         {
           "type": "opencollective",
@@ -7695,13 +6828,15 @@
           "type": "github",
           "url": "https://github.com/sponsors/ai"
         }
-      ]
+      ],
+      "license": "CC-BY-4.0"
     },
     "node_modules/chai": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
-      "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz",
+      "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "assertion-error": "^2.0.1",
         "check-error": "^2.1.1",
@@ -7717,6 +6852,7 @@
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
       "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "license": "MIT",
       "dependencies": {
         "ansi-styles": "^4.1.0",
         "supports-color": "^7.1.0"
@@ -7731,13 +6867,15 @@
     "node_modules/chardet": {
       "version": "0.7.0",
       "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
-      "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
+      "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+      "license": "MIT"
     },
     "node_modules/check-error": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
       "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">= 16"
       }
@@ -7746,6 +6884,7 @@
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
       "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+      "license": "MIT",
       "dependencies": {
         "anymatch": "~3.1.2",
         "braces": "~3.0.2",
@@ -7769,23 +6908,25 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
       "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+      "license": "ISC",
       "engines": {
         "node": ">=10"
       }
     },
     "node_modules/chrome-trace-event": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
-      "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==",
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
+      "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6.0"
       }
     },
     "node_modules/ci-info": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz",
-      "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz",
+      "integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==",
       "dev": true,
       "funding": [
         {
@@ -7793,24 +6934,28 @@
           "url": "https://github.com/sponsors/sibiraj-s"
         }
       ],
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
     },
     "node_modules/cjs-module-lexer": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz",
-      "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q=="
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz",
+      "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==",
+      "license": "MIT"
     },
     "node_modules/class-transformer": {
       "version": "0.5.1",
       "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
-      "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw=="
+      "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
+      "license": "MIT"
     },
     "node_modules/class-validator": {
       "version": "0.14.1",
       "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz",
       "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==",
+      "license": "MIT",
       "dependencies": {
         "@types/validator": "^13.11.8",
         "libphonenumber-js": "^1.10.53",
@@ -7822,6 +6967,7 @@
       "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz",
       "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "escape-string-regexp": "^1.0.5"
       },
@@ -7834,6 +6980,7 @@
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
       "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.8.0"
       }
@@ -7842,6 +6989,7 @@
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
       "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+      "license": "MIT",
       "dependencies": {
         "restore-cursor": "^3.1.0"
       },
@@ -7853,6 +7001,7 @@
       "version": "2.1.11",
       "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz",
       "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==",
+      "license": "ISC",
       "dependencies": {
         "chalk": "^4.0.0",
         "highlight.js": "^10.7.1",
@@ -7869,10 +7018,66 @@
         "npm": ">=5.0.0"
       }
     },
+    "node_modules/cli-highlight/node_modules/cliui": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+      "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.0",
+        "wrap-ansi": "^7.0.0"
+      }
+    },
+    "node_modules/cli-highlight/node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/cli-highlight/node_modules/yargs": {
+      "version": "16.2.0",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+      "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+      "license": "MIT",
+      "dependencies": {
+        "cliui": "^7.0.2",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.0",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^20.2.2"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/cli-highlight/node_modules/yargs-parser": {
+      "version": "20.2.9",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+      "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/cli-spinners": {
-      "version": "2.9.1",
-      "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.1.tgz",
-      "integrity": "sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==",
+      "version": "2.9.2",
+      "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+      "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       },
@@ -7885,6 +7090,7 @@
       "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz",
       "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "string-width": "^4.2.0"
       },
@@ -7899,6 +7105,7 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
       "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==",
+      "license": "ISC",
       "engines": {
         "node": ">= 10"
       }
@@ -7906,22 +7113,28 @@
     "node_modules/client-only": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
-      "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
+      "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
+      "license": "MIT"
     },
     "node_modules/cliui": {
-      "version": "7.0.4",
-      "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
-      "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+      "license": "ISC",
       "dependencies": {
         "string-width": "^4.2.0",
-        "strip-ansi": "^6.0.0",
+        "strip-ansi": "^6.0.1",
         "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
       }
     },
     "node_modules/cliui/node_modules/wrap-ansi": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
       "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "license": "MIT",
       "dependencies": {
         "ansi-styles": "^4.0.0",
         "string-width": "^4.1.0",
@@ -7938,6 +7151,7 @@
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
       "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
+      "license": "MIT",
       "engines": {
         "node": ">=0.8"
       }
@@ -7946,6 +7160,7 @@
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
       "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
+      "license": "Apache-2.0",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -7954,6 +7169,7 @@
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
       "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+      "license": "MIT",
       "dependencies": {
         "color-convert": "^2.0.1",
         "color-string": "^1.9.0"
@@ -7966,6 +7182,7 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
       "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "license": "MIT",
       "dependencies": {
         "color-name": "~1.1.4"
       },
@@ -7976,12 +7193,14 @@
     "node_modules/color-name": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "license": "MIT"
     },
     "node_modules/color-string": {
       "version": "1.9.1",
       "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
       "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+      "license": "MIT",
       "dependencies": {
         "color-name": "^1.0.0",
         "simple-swizzle": "^0.2.2"
@@ -7991,23 +7210,39 @@
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
       "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+      "license": "ISC",
       "bin": {
         "color-support": "bin.js"
       }
     },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/commander": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
       "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+      "license": "MIT",
       "engines": {
         "node": ">= 6"
       }
     },
     "node_modules/comment-json": {
-      "version": "4.2.3",
-      "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.3.tgz",
-      "integrity": "sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==",
+      "version": "4.2.5",
+      "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz",
+      "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "array-timsort": "^1.0.3",
         "core-util-is": "^1.0.3",
@@ -8023,6 +7258,7 @@
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
       "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==",
+      "license": "MIT",
       "dependencies": {
         "crc-32": "^1.2.0",
         "crc32-stream": "^6.0.0",
@@ -8034,48 +7270,11 @@
         "node": ">= 14"
       }
     },
-    "node_modules/compress-commons/node_modules/buffer": {
-      "version": "6.0.3",
-      "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
-      "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "dependencies": {
-        "base64-js": "^1.3.1",
-        "ieee754": "^1.2.1"
-      }
-    },
-    "node_modules/compress-commons/node_modules/readable-stream": {
-      "version": "4.5.2",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
-      "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
-      "dependencies": {
-        "abort-controller": "^3.0.0",
-        "buffer": "^6.0.3",
-        "events": "^3.3.0",
-        "process": "^0.11.10",
-        "string_decoder": "^1.3.0"
-      },
-      "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      }
-    },
     "node_modules/concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
-      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "license": "MIT"
     },
     "node_modules/concat-stream": {
       "version": "2.0.0",
@@ -8084,6 +7283,7 @@
       "engines": [
         "node >= 6.0"
       ],
+      "license": "MIT",
       "dependencies": {
         "buffer-from": "^1.0.0",
         "inherits": "^2.0.3",
@@ -8091,29 +7291,37 @@
         "typedarray": "^0.0.6"
       }
     },
-    "node_modules/config-chain": {
-      "version": "1.1.13",
-      "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
-      "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
+    "node_modules/concat-stream/node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "license": "MIT",
       "dependencies": {
-        "ini": "^1.3.4",
-        "proto-list": "~1.2.1"
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
       }
     },
     "node_modules/consola": {
       "version": "2.15.3",
       "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz",
-      "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw=="
+      "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==",
+      "license": "MIT"
     },
     "node_modules/console-control-strings": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
-      "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="
+      "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
+      "license": "ISC"
     },
     "node_modules/content-disposition": {
       "version": "0.5.4",
       "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
       "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "license": "MIT",
       "dependencies": {
         "safe-buffer": "5.2.1"
       },
@@ -8125,6 +7333,7 @@
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
       "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "license": "MIT",
       "engines": {
         "node": ">= 0.6"
       }
@@ -8132,22 +7341,25 @@
     "node_modules/convert-source-map": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
-      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "license": "MIT"
     },
     "node_modules/cookie": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
-      "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "license": "MIT",
       "engines": {
         "node": ">= 0.6"
       }
     },
     "node_modules/cookie-parser": {
-      "version": "1.4.6",
-      "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
-      "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
+      "version": "1.4.7",
+      "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
+      "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
+      "license": "MIT",
       "dependencies": {
-        "cookie": "0.4.1",
+        "cookie": "0.7.2",
         "cookie-signature": "1.0.6"
       },
       "engines": {
@@ -8157,15 +7369,17 @@
     "node_modules/cookie-signature": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
-      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
+      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+      "license": "MIT"
     },
     "node_modules/core-js-compat": {
-      "version": "3.37.1",
-      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
-      "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
+      "version": "3.39.0",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz",
+      "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "browserslist": "^4.23.0"
+        "browserslist": "^4.24.2"
       },
       "funding": {
         "type": "opencollective",
@@ -8175,12 +7389,14 @@
     "node_modules/core-util-is": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
-      "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
+      "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+      "license": "MIT"
     },
     "node_modules/cors": {
       "version": "2.8.5",
       "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
       "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+      "license": "MIT",
       "dependencies": {
         "object-assign": "^4",
         "vary": "^1"
@@ -8193,6 +7409,7 @@
       "version": "8.3.6",
       "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
       "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
+      "license": "MIT",
       "dependencies": {
         "import-fresh": "^3.3.0",
         "js-yaml": "^4.1.0",
@@ -8215,15 +7432,15 @@
       }
     },
     "node_modules/cpu-features": {
-      "version": "0.0.9",
-      "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.9.tgz",
-      "integrity": "sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ==",
+      "version": "0.0.10",
+      "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
+      "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
       "dev": true,
       "hasInstallScript": true,
       "optional": true,
       "dependencies": {
         "buildcheck": "~0.0.6",
-        "nan": "^2.17.0"
+        "nan": "^2.19.0"
       },
       "engines": {
         "node": ">=10.0.0"
@@ -8233,6 +7450,7 @@
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
       "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+      "license": "Apache-2.0",
       "bin": {
         "crc32": "bin/crc32.njs"
       },
@@ -8244,6 +7462,7 @@
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz",
       "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==",
+      "license": "MIT",
       "dependencies": {
         "crc-32": "^1.2.0",
         "readable-stream": "^4.0.0"
@@ -8252,64 +7471,21 @@
         "node": ">= 14"
       }
     },
-    "node_modules/crc32-stream/node_modules/buffer": {
-      "version": "6.0.3",
-      "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
-      "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "dependencies": {
-        "base64-js": "^1.3.1",
-        "ieee754": "^1.2.1"
-      }
-    },
-    "node_modules/crc32-stream/node_modules/readable-stream": {
-      "version": "4.5.2",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
-      "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
-      "dependencies": {
-        "abort-controller": "^3.0.0",
-        "buffer": "^6.0.3",
-        "events": "^3.3.0",
-        "process": "^0.11.10",
-        "string_decoder": "^1.3.0"
-      },
-      "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      }
-    },
-    "node_modules/create-require": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
-      "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
-      "optional": true,
-      "peer": true
-    },
     "node_modules/cron": {
-      "version": "3.1.7",
-      "resolved": "https://registry.npmjs.org/cron/-/cron-3.1.7.tgz",
-      "integrity": "sha512-tlBg7ARsAMQLzgwqVxy8AZl/qlTc5nibqYwtNGoCrd+cV+ugI+tvZC1oT/8dFH8W455YrywGykx/KMmAqOr7Jw==",
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/cron/-/cron-3.2.1.tgz",
+      "integrity": "sha512-w2n5l49GMmmkBFEsH9FIDhjZ1n1QgTMOCMGuQtOXs5veNiosZmso6bQGuqOJSYAXXrG84WQFVneNk+Yt0Ua9iw==",
+      "license": "MIT",
       "dependencies": {
         "@types/luxon": "~3.4.0",
-        "luxon": "~3.4.0"
+        "luxon": "~3.5.0"
       }
     },
     "node_modules/cron-parser": {
       "version": "4.9.0",
       "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
       "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==",
+      "license": "MIT",
       "dependencies": {
         "luxon": "^3.2.1"
       },
@@ -8317,18 +7493,11 @@
         "node": ">=12.0.0"
       }
     },
-    "node_modules/cron/node_modules/luxon": {
-      "version": "3.4.4",
-      "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
-      "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
-      "engines": {
-        "node": ">=12"
-      }
-    },
     "node_modules/cross-spawn": {
-      "version": "7.0.3",
-      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
-      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "license": "MIT",
       "dependencies": {
         "path-key": "^3.1.0",
         "shebang-command": "^2.0.0",
@@ -8342,6 +7511,7 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
       "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "license": "MIT",
       "peer": true,
       "bin": {
         "cssesc": "bin/cssesc"
@@ -8354,17 +7524,20 @@
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/dayjs": {
-      "version": "1.11.10",
-      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
-      "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
+      "version": "1.11.13",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
+      "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
+      "license": "MIT"
     },
     "node_modules/debounce": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.0.0.tgz",
       "integrity": "sha512-xRetU6gL1VJbs85Mc4FoEGSjQxzpdxRyFhe3lmWFyy2EzydIcD4xzUvRJMD+NPDfMwKNhxa3PvsIOU32luIWeA==",
+      "license": "MIT",
       "engines": {
         "node": ">=18"
       },
@@ -8373,11 +7546,12 @@
       }
     },
     "node_modules/debug": {
-      "version": "4.3.5",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
-      "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+      "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+      "license": "MIT",
       "dependencies": {
-        "ms": "2.1.2"
+        "ms": "^2.1.3"
       },
       "engines": {
         "node": ">=6.0"
@@ -8393,6 +7567,7 @@
       "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
       "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
@@ -8401,12 +7576,14 @@
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
       "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/deepmerge": {
       "version": "4.3.1",
       "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
       "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -8415,6 +7592,7 @@
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
       "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
+      "license": "MIT",
       "dependencies": {
         "clone": "^1.0.2"
       },
@@ -8423,14 +7601,15 @@
       }
     },
     "node_modules/define-data-property": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.3.tgz",
-      "integrity": "sha512-h3GBouC+RPtNX2N0hHVLo2ZwPYurq8mLmXpOLTsw71gr7lHt5VaI4vVkDUNOfiWmm48JEXe3VM7PmLX45AMmmg==",
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+      "dev": true,
+      "license": "MIT",
       "dependencies": {
+        "es-define-property": "^1.0.0",
         "es-errors": "^1.3.0",
-        "get-intrinsic": "^1.2.4",
-        "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.1"
+        "gopd": "^1.0.1"
       },
       "engines": {
         "node": ">= 0.4"
@@ -8439,15 +7618,27 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
     "node_modules/delegates": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
-      "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="
+      "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
+      "license": "MIT"
     },
     "node_modules/denque": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
       "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+      "license": "Apache-2.0",
       "engines": {
         "node": ">=0.10"
       }
@@ -8456,6 +7647,7 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
       "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "license": "MIT",
       "engines": {
         "node": ">= 0.8"
       }
@@ -8464,6 +7656,7 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
       "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+      "license": "MIT",
       "engines": {
         "node": ">= 0.8",
         "npm": "1.2.8000 || >= 1.4.16"
@@ -8473,6 +7666,7 @@
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
       "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
+      "license": "Apache-2.0",
       "engines": {
         "node": ">=8"
       }
@@ -8480,34 +7674,28 @@
     "node_modules/diacritics": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
-      "integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA=="
+      "integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==",
+      "license": "MIT"
     },
     "node_modules/didyoumean": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
       "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+      "license": "Apache-2.0",
       "peer": true
     },
-    "node_modules/diff": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
-      "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
-      "optional": true,
-      "peer": true,
-      "engines": {
-        "node": ">=0.3.1"
-      }
-    },
     "node_modules/discontinuous-range": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz",
       "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/dlv": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
       "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+      "license": "MIT",
       "peer": true
     },
     "node_modules/docker-compose": {
@@ -8515,6 +7703,7 @@
       "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.8.tgz",
       "integrity": "sha512-plizRs/Vf15H+GCVxq2EUvyPK7ei9b/cVesHvjnX4xaXjM9spHe2Ytq0BitndFgvTJ3E3NljPNUEl7BAN43iZw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "yaml": "^2.2.2"
       },
@@ -8527,6 +7716,7 @@
       "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.8.tgz",
       "integrity": "sha512-f0ReSURdM3pcKPNS30mxOHSbaFLcknGmQjwSfmbcdOw1XWKXVhukM3NJHhr7NpY9BIyyWQb0EBo3KQvvuU5egQ==",
       "dev": true,
+      "license": "Apache-2.0",
       "dependencies": {
         "debug": "^4.1.1",
         "readable-stream": "^3.5.0",
@@ -8537,11 +7727,27 @@
         "node": ">= 8.0"
       }
     },
+    "node_modules/docker-modem/node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/dockerode": {
       "version": "3.3.5",
       "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.3.5.tgz",
       "integrity": "sha512-/0YNa3ZDNeLr/tSckmD69+Gq+qVNhvKfAHNeZJBnp7EOP6RGKV8ORrJHkUn20So5wU+xxT7+1n5u8PjHbfjbSA==",
       "dev": true,
+      "license": "Apache-2.0",
       "dependencies": {
         "@balena/dockerignore": "^1.0.2",
         "docker-modem": "^3.0.0",
@@ -8555,13 +7761,30 @@
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
       "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
-      "dev": true
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/dockerode/node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
     },
     "node_modules/dockerode/node_modules/tar-fs": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz",
       "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "chownr": "^1.1.1",
         "mkdirp-classic": "^0.5.2",
@@ -8574,6 +7797,7 @@
       "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
       "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "bl": "^4.0.3",
         "end-of-stream": "^1.4.1",
@@ -8589,6 +7813,7 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
       "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+      "license": "MIT",
       "dependencies": {
         "domelementtype": "^2.3.0",
         "domhandler": "^5.0.2",
@@ -8607,12 +7832,14 @@
           "type": "github",
           "url": "https://github.com/sponsors/fb55"
         }
-      ]
+      ],
+      "license": "BSD-2-Clause"
     },
     "node_modules/domhandler": {
       "version": "5.0.3",
       "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
       "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+      "license": "BSD-2-Clause",
       "dependencies": {
         "domelementtype": "^2.3.0"
       },
@@ -8627,6 +7854,7 @@
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
       "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
+      "license": "BSD-2-Clause",
       "dependencies": {
         "dom-serializer": "^2.0.0",
         "domelementtype": "^2.3.0",
@@ -8637,9 +7865,10 @@
       }
     },
     "node_modules/dotenv": {
-      "version": "16.4.5",
-      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
-      "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
+      "version": "16.4.7",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
+      "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
+      "license": "BSD-2-Clause",
       "engines": {
         "node": ">=12"
       },
@@ -8647,85 +7876,49 @@
         "url": "https://dotenvx.com"
       }
     },
-    "node_modules/dotenv-expand": {
-      "version": "10.0.0",
-      "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz",
-      "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==",
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
       "engines": {
-        "node": ">=12"
+        "node": ">= 0.4"
       }
     },
     "node_modules/eastasianwidth": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
-      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
-    },
-    "node_modules/editorconfig": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
-      "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
-      "dependencies": {
-        "@one-ini/wasm": "0.1.1",
-        "commander": "^10.0.0",
-        "minimatch": "9.0.1",
-        "semver": "^7.5.3"
-      },
-      "bin": {
-        "editorconfig": "bin/editorconfig"
-      },
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/editorconfig/node_modules/brace-expansion": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
-      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-      "dependencies": {
-        "balanced-match": "^1.0.0"
-      }
-    },
-    "node_modules/editorconfig/node_modules/commander": {
-      "version": "10.0.1",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
-      "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/editorconfig/node_modules/minimatch": {
-      "version": "9.0.1",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
-      "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
-      "dependencies": {
-        "brace-expansion": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=16 || 14 >=14.17"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      }
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+      "license": "MIT"
     },
     "node_modules/ee-first": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
-      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "license": "MIT"
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.769",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.769.tgz",
-      "integrity": "sha512-bZu7p623NEA2rHTc9K1vykl57ektSPQYFFqQir8BOYf6EKOB+yIsbFB9Kpm7Cgt6tsLr9sRkqfqSZUw7LP1XxQ=="
+      "version": "1.5.74",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz",
+      "integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==",
+      "license": "ISC"
     },
     "node_modules/emoji-regex": {
       "version": "8.0.0",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
     },
     "node_modules/encodeurl": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
-      "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "license": "MIT",
       "engines": {
         "node": ">= 0.8"
       }
@@ -8735,43 +7928,64 @@
       "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
       "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "once": "^1.4.0"
       }
     },
     "node_modules/engine.io": {
-      "version": "6.5.2",
-      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.2.tgz",
-      "integrity": "sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA==",
+      "version": "6.6.2",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz",
+      "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==",
+      "license": "MIT",
       "dependencies": {
         "@types/cookie": "^0.4.1",
         "@types/cors": "^2.8.12",
         "@types/node": ">=10.0.0",
         "accepts": "~1.3.4",
         "base64id": "2.0.0",
-        "cookie": "~0.4.1",
+        "cookie": "~0.7.2",
         "cors": "~2.8.5",
         "debug": "~4.3.1",
         "engine.io-parser": "~5.2.1",
-        "ws": "~8.11.0"
+        "ws": "~8.17.1"
       },
       "engines": {
         "node": ">=10.2.0"
       }
     },
     "node_modules/engine.io-parser": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
-      "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==",
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+      "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+      "license": "MIT",
       "engines": {
         "node": ">=10.0.0"
       }
     },
+    "node_modules/engine.io/node_modules/debug": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/enhanced-resolve": {
-      "version": "5.17.0",
-      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz",
-      "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==",
+      "version": "5.17.1",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
+      "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "graceful-fs": "^4.2.4",
         "tapable": "^2.2.0"
@@ -8784,6 +7998,7 @@
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
       "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+      "license": "BSD-2-Clause",
       "engines": {
         "node": ">=0.12"
       },
@@ -8795,17 +8010,16 @@
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
       "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+      "license": "MIT",
       "dependencies": {
         "is-arrayish": "^0.2.1"
       }
     },
     "node_modules/es-define-property": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
-      "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
-      "dependencies": {
-        "get-intrinsic": "^1.2.4"
-      },
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
       "engines": {
         "node": ">= 0.4"
       }
@@ -8814,4335 +8028,36 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
       "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/es-module-lexer": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz",
-      "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==",
-      "dev": true
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
+      "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
+      "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
     },
     "node_modules/esbuild": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
-      "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
-      "dev": true,
-      "hasInstallScript": true,
-      "bin": {
-        "esbuild": "bin/esbuild"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "optionalDependencies": {
-        "@esbuild/aix-ppc64": "0.20.2",
-        "@esbuild/android-arm": "0.20.2",
-        "@esbuild/android-arm64": "0.20.2",
-        "@esbuild/android-x64": "0.20.2",
-        "@esbuild/darwin-arm64": "0.20.2",
-        "@esbuild/darwin-x64": "0.20.2",
-        "@esbuild/freebsd-arm64": "0.20.2",
-        "@esbuild/freebsd-x64": "0.20.2",
-        "@esbuild/linux-arm": "0.20.2",
-        "@esbuild/linux-arm64": "0.20.2",
-        "@esbuild/linux-ia32": "0.20.2",
-        "@esbuild/linux-loong64": "0.20.2",
-        "@esbuild/linux-mips64el": "0.20.2",
-        "@esbuild/linux-ppc64": "0.20.2",
-        "@esbuild/linux-riscv64": "0.20.2",
-        "@esbuild/linux-s390x": "0.20.2",
-        "@esbuild/linux-x64": "0.20.2",
-        "@esbuild/netbsd-x64": "0.20.2",
-        "@esbuild/openbsd-x64": "0.20.2",
-        "@esbuild/sunos-x64": "0.20.2",
-        "@esbuild/win32-arm64": "0.20.2",
-        "@esbuild/win32-ia32": "0.20.2",
-        "@esbuild/win32-x64": "0.20.2"
-      }
-    },
-    "node_modules/escalade": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
-      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/escape-html": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
-      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
-    },
-    "node_modules/escape-string-regexp": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
-      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
-      "dev": true,
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/eslint": {
-      "version": "9.9.1",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz",
-      "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==",
-      "dev": true,
-      "dependencies": {
-        "@eslint-community/eslint-utils": "^4.2.0",
-        "@eslint-community/regexpp": "^4.11.0",
-        "@eslint/config-array": "^0.18.0",
-        "@eslint/eslintrc": "^3.1.0",
-        "@eslint/js": "9.9.1",
-        "@humanwhocodes/module-importer": "^1.0.1",
-        "@humanwhocodes/retry": "^0.3.0",
-        "@nodelib/fs.walk": "^1.2.8",
-        "ajv": "^6.12.4",
-        "chalk": "^4.0.0",
-        "cross-spawn": "^7.0.2",
-        "debug": "^4.3.2",
-        "escape-string-regexp": "^4.0.0",
-        "eslint-scope": "^8.0.2",
-        "eslint-visitor-keys": "^4.0.0",
-        "espree": "^10.1.0",
-        "esquery": "^1.5.0",
-        "esutils": "^2.0.2",
-        "fast-deep-equal": "^3.1.3",
-        "file-entry-cache": "^8.0.0",
-        "find-up": "^5.0.0",
-        "glob-parent": "^6.0.2",
-        "ignore": "^5.2.0",
-        "imurmurhash": "^0.1.4",
-        "is-glob": "^4.0.0",
-        "is-path-inside": "^3.0.3",
-        "json-stable-stringify-without-jsonify": "^1.0.1",
-        "levn": "^0.4.1",
-        "lodash.merge": "^4.6.2",
-        "minimatch": "^3.1.2",
-        "natural-compare": "^1.4.0",
-        "optionator": "^0.9.3",
-        "strip-ansi": "^6.0.1",
-        "text-table": "^0.2.0"
-      },
-      "bin": {
-        "eslint": "bin/eslint.js"
-      },
-      "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
-      },
-      "funding": {
-        "url": "https://eslint.org/donate"
-      },
-      "peerDependencies": {
-        "jiti": "*"
-      },
-      "peerDependenciesMeta": {
-        "jiti": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/eslint-config-prettier": {
-      "version": "9.1.0",
-      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
-      "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
-      "dev": true,
-      "bin": {
-        "eslint-config-prettier": "bin/cli.js"
-      },
-      "peerDependencies": {
-        "eslint": ">=7.0.0"
-      }
-    },
-    "node_modules/eslint-plugin-prettier": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz",
-      "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==",
-      "dev": true,
-      "dependencies": {
-        "prettier-linter-helpers": "^1.0.0",
-        "synckit": "^0.9.1"
-      },
-      "engines": {
-        "node": "^14.18.0 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint-plugin-prettier"
-      },
-      "peerDependencies": {
-        "@types/eslint": ">=8.0.0",
-        "eslint": ">=8.0.0",
-        "eslint-config-prettier": "*",
-        "prettier": ">=3.0.0"
-      },
-      "peerDependenciesMeta": {
-        "@types/eslint": {
-          "optional": true
-        },
-        "eslint-config-prettier": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/eslint-plugin-unicorn": {
-      "version": "55.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz",
-      "integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==",
-      "dev": true,
-      "dependencies": {
-        "@babel/helper-validator-identifier": "^7.24.5",
-        "@eslint-community/eslint-utils": "^4.4.0",
-        "ci-info": "^4.0.0",
-        "clean-regexp": "^1.0.0",
-        "core-js-compat": "^3.37.0",
-        "esquery": "^1.5.0",
-        "globals": "^15.7.0",
-        "indent-string": "^4.0.0",
-        "is-builtin-module": "^3.2.1",
-        "jsesc": "^3.0.2",
-        "pluralize": "^8.0.0",
-        "read-pkg-up": "^7.0.1",
-        "regexp-tree": "^0.1.27",
-        "regjsparser": "^0.10.0",
-        "semver": "^7.6.1",
-        "strip-indent": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=18.18"
-      },
-      "funding": {
-        "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
-      },
-      "peerDependencies": {
-        "eslint": ">=8.56.0"
-      }
-    },
-    "node_modules/eslint-scope": {
-      "version": "8.0.2",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz",
-      "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==",
-      "dev": true,
-      "dependencies": {
-        "esrecurse": "^4.3.0",
-        "estraverse": "^5.2.0"
-      },
-      "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
-    "node_modules/eslint-visitor-keys": {
-      "version": "3.4.3",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
-      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
-      "dev": true,
-      "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
-    "node_modules/eslint/node_modules/ajv": {
-      "version": "6.12.6",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-      "dev": true,
-      "dependencies": {
-        "fast-deep-equal": "^3.1.1",
-        "fast-json-stable-stringify": "^2.0.0",
-        "json-schema-traverse": "^0.4.1",
-        "uri-js": "^4.2.2"
-      },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/epoberezkin"
-      }
-    },
-    "node_modules/eslint/node_modules/eslint-visitor-keys": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
-      "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
-      "dev": true,
-      "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
-    "node_modules/eslint/node_modules/glob-parent": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
-      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
-      "dev": true,
-      "dependencies": {
-        "is-glob": "^4.0.3"
-      },
-      "engines": {
-        "node": ">=10.13.0"
-      }
-    },
-    "node_modules/eslint/node_modules/json-schema-traverse": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-      "dev": true
-    },
-    "node_modules/espree": {
-      "version": "10.1.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz",
-      "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==",
-      "dev": true,
-      "dependencies": {
-        "acorn": "^8.12.0",
-        "acorn-jsx": "^5.3.2",
-        "eslint-visitor-keys": "^4.0.0"
-      },
-      "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
-    "node_modules/espree/node_modules/eslint-visitor-keys": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
-      "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
-      "dev": true,
-      "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
-    "node_modules/esprima": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
-      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
-      "dev": true,
-      "bin": {
-        "esparse": "bin/esparse.js",
-        "esvalidate": "bin/esvalidate.js"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/esquery": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
-      "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
-      "dev": true,
-      "dependencies": {
-        "estraverse": "^5.1.0"
-      },
-      "engines": {
-        "node": ">=0.10"
-      }
-    },
-    "node_modules/esrecurse": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
-      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
-      "dev": true,
-      "dependencies": {
-        "estraverse": "^5.2.0"
-      },
-      "engines": {
-        "node": ">=4.0"
-      }
-    },
-    "node_modules/estraverse": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
-      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
-      "dev": true,
-      "engines": {
-        "node": ">=4.0"
-      }
-    },
-    "node_modules/estree-walker": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
-      "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
-      "dev": true,
-      "dependencies": {
-        "@types/estree": "^1.0.0"
-      }
-    },
-    "node_modules/esutils": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
-      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/etag": {
-      "version": "1.8.1",
-      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
-      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/event-target-shim": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
-      "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/eventemitter2": {
-      "version": "6.4.9",
-      "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
-      "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg=="
-    },
-    "node_modules/events": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
-      "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
-      "engines": {
-        "node": ">=0.8.x"
-      }
-    },
-    "node_modules/execa": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
-      "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
-      "dev": true,
-      "dependencies": {
-        "cross-spawn": "^7.0.3",
-        "get-stream": "^8.0.1",
-        "human-signals": "^5.0.0",
-        "is-stream": "^3.0.0",
-        "merge-stream": "^2.0.0",
-        "npm-run-path": "^5.1.0",
-        "onetime": "^6.0.0",
-        "signal-exit": "^4.1.0",
-        "strip-final-newline": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=16.17"
-      },
-      "funding": {
-        "url": "https://github.com/sindresorhus/execa?sponsor=1"
-      }
-    },
-    "node_modules/execa/node_modules/is-stream": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
-      "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
-      "dev": true,
-      "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/execa/node_modules/mimic-fn": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
-      "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/execa/node_modules/onetime": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
-      "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
-      "dev": true,
-      "dependencies": {
-        "mimic-fn": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/exiftool-vendored": {
-      "version": "28.2.1",
-      "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.1.tgz",
-      "integrity": "sha512-D3YsKErr3BbjKeJzUVsv6CVZ+SQNgAJKPFWVLXu0CBtr24FNuE3CZBXWKWysGu0MjzeDCNwQrQI5+bXUFeiYVA==",
-      "dependencies": {
-        "@photostructure/tz-lookup": "^10.0.0",
-        "@types/luxon": "^3.4.2",
-        "batch-cluster": "^13.0.0",
-        "he": "^1.2.0",
-        "luxon": "^3.5.0"
-      },
-      "optionalDependencies": {
-        "exiftool-vendored.exe": "12.91.0",
-        "exiftool-vendored.pl": "12.91.0"
-      }
-    },
-    "node_modules/exiftool-vendored.exe": {
-      "version": "12.91.0",
-      "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.91.0.tgz",
-      "integrity": "sha512-nxcoGBaJL/D+Wb0jVe8qwyV8QZpRcCzU0aCKhG0S1XNGWGjJJJ4QV851aobcfDwI4NluFOdqkjTSf32pVijvHg==",
-      "optional": true,
-      "os": [
-        "win32"
-      ]
-    },
-    "node_modules/exiftool-vendored.pl": {
-      "version": "12.91.0",
-      "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.91.0.tgz",
-      "integrity": "sha512-GZMy9+Jiv8/C7R4uYe1kWtXsAaJdgVezTwYa+wDeoqvReHiX2t5uzkCrzWdjo4LGl5mPQkyKhN7/uPLYk5Ak6w==",
-      "optional": true,
-      "os": [
-        "!win32"
-      ]
-    },
-    "node_modules/express": {
-      "version": "4.19.2",
-      "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
-      "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
-      "dependencies": {
-        "accepts": "~1.3.8",
-        "array-flatten": "1.1.1",
-        "body-parser": "1.20.2",
-        "content-disposition": "0.5.4",
-        "content-type": "~1.0.4",
-        "cookie": "0.6.0",
-        "cookie-signature": "1.0.6",
-        "debug": "2.6.9",
-        "depd": "2.0.0",
-        "encodeurl": "~1.0.2",
-        "escape-html": "~1.0.3",
-        "etag": "~1.8.1",
-        "finalhandler": "1.2.0",
-        "fresh": "0.5.2",
-        "http-errors": "2.0.0",
-        "merge-descriptors": "1.0.1",
-        "methods": "~1.1.2",
-        "on-finished": "2.4.1",
-        "parseurl": "~1.3.3",
-        "path-to-regexp": "0.1.7",
-        "proxy-addr": "~2.0.7",
-        "qs": "6.11.0",
-        "range-parser": "~1.2.1",
-        "safe-buffer": "5.2.1",
-        "send": "0.18.0",
-        "serve-static": "1.15.0",
-        "setprototypeof": "1.2.0",
-        "statuses": "2.0.1",
-        "type-is": "~1.6.18",
-        "utils-merge": "1.0.1",
-        "vary": "~1.1.2"
-      },
-      "engines": {
-        "node": ">= 0.10.0"
-      }
-    },
-    "node_modules/express/node_modules/cookie": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
-      "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/express/node_modules/debug": {
-      "version": "2.6.9",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
-      "dependencies": {
-        "ms": "2.0.0"
-      }
-    },
-    "node_modules/express/node_modules/ms": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
-    },
-    "node_modules/express/node_modules/path-to-regexp": {
-      "version": "0.1.7",
-      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
-      "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
-    },
-    "node_modules/extend": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
-      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
-    },
-    "node_modules/external-editor": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
-      "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
-      "dependencies": {
-        "chardet": "^0.7.0",
-        "iconv-lite": "^0.4.24",
-        "tmp": "^0.0.33"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/fast-deep-equal": {
-      "version": "3.1.3",
-      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
-      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
-      "dev": true
-    },
-    "node_modules/fast-diff": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
-      "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
-      "dev": true
-    },
-    "node_modules/fast-fifo": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
-      "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="
-    },
-    "node_modules/fast-glob": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
-      "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
-      "dependencies": {
-        "@nodelib/fs.stat": "^2.0.2",
-        "@nodelib/fs.walk": "^1.2.3",
-        "glob-parent": "^5.1.2",
-        "merge2": "^1.3.0",
-        "micromatch": "^4.0.4"
-      },
-      "engines": {
-        "node": ">=8.6.0"
-      }
-    },
-    "node_modules/fast-json-stable-stringify": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
-      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
-      "dev": true
-    },
-    "node_modules/fast-levenshtein": {
-      "version": "2.0.6",
-      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
-      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
-      "dev": true
-    },
-    "node_modules/fast-safe-stringify": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
-      "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="
-    },
-    "node_modules/fastq": {
-      "version": "1.15.0",
-      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
-      "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
-      "dependencies": {
-        "reusify": "^1.0.4"
-      }
-    },
-    "node_modules/figures": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
-      "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
-      "dependencies": {
-        "escape-string-regexp": "^1.0.5"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/figures/node_modules/escape-string-regexp": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-      "engines": {
-        "node": ">=0.8.0"
-      }
-    },
-    "node_modules/file-entry-cache": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
-      "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
-      "dev": true,
-      "dependencies": {
-        "flat-cache": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=16.0.0"
-      }
-    },
-    "node_modules/file-source": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/file-source/-/file-source-0.6.1.tgz",
-      "integrity": "sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA==",
-      "dependencies": {
-        "stream-source": "0.3"
-      }
-    },
-    "node_modules/fill-range": {
-      "version": "7.1.1",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
-      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
-      "dependencies": {
-        "to-regex-range": "^5.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/finalhandler": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
-      "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
-      "dependencies": {
-        "debug": "2.6.9",
-        "encodeurl": "~1.0.2",
-        "escape-html": "~1.0.3",
-        "on-finished": "2.4.1",
-        "parseurl": "~1.3.3",
-        "statuses": "2.0.1",
-        "unpipe": "~1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/finalhandler/node_modules/debug": {
-      "version": "2.6.9",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
-      "dependencies": {
-        "ms": "2.0.0"
-      }
-    },
-    "node_modules/finalhandler/node_modules/ms": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
-    },
-    "node_modules/find-up": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
-      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
-      "dev": true,
-      "dependencies": {
-        "locate-path": "^6.0.0",
-        "path-exists": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/flat-cache": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
-      "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
-      "dev": true,
-      "dependencies": {
-        "flatted": "^3.2.9",
-        "keyv": "^4.5.4"
-      },
-      "engines": {
-        "node": ">=16"
-      }
-    },
-    "node_modules/flatted": {
-      "version": "3.3.1",
-      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
-      "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
-      "dev": true
-    },
-    "node_modules/fluent-ffmpeg": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz",
-      "integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==",
-      "dependencies": {
-        "async": "^0.2.9",
-        "which": "^1.1.1"
-      },
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/fluent-ffmpeg/node_modules/async": {
-      "version": "0.2.10",
-      "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
-      "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
-    },
-    "node_modules/fluent-ffmpeg/node_modules/which": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
-      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
-      "dependencies": {
-        "isexe": "^2.0.0"
-      },
-      "bin": {
-        "which": "bin/which"
-      }
-    },
-    "node_modules/foreground-child": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
-      "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
-      "dependencies": {
-        "cross-spawn": "^7.0.0",
-        "signal-exit": "^4.0.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      }
-    },
-    "node_modules/fork-ts-checker-webpack-plugin": {
-      "version": "9.0.2",
-      "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz",
-      "integrity": "sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==",
-      "dev": true,
-      "dependencies": {
-        "@babel/code-frame": "^7.16.7",
-        "chalk": "^4.1.2",
-        "chokidar": "^3.5.3",
-        "cosmiconfig": "^8.2.0",
-        "deepmerge": "^4.2.2",
-        "fs-extra": "^10.0.0",
-        "memfs": "^3.4.1",
-        "minimatch": "^3.0.4",
-        "node-abort-controller": "^3.0.1",
-        "schema-utils": "^3.1.1",
-        "semver": "^7.3.5",
-        "tapable": "^2.2.1"
-      },
-      "engines": {
-        "node": ">=12.13.0",
-        "yarn": ">=1.0.0"
-      },
-      "peerDependencies": {
-        "typescript": ">3.6.0",
-        "webpack": "^5.11.0"
-      }
-    },
-    "node_modules/forwarded": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
-      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/fresh": {
-      "version": "0.5.2",
-      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
-      "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/fs-constants": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
-      "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
-      "dev": true
-    },
-    "node_modules/fs-extra": {
-      "version": "10.1.0",
-      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
-      "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
-      "dev": true,
-      "dependencies": {
-        "graceful-fs": "^4.2.0",
-        "jsonfile": "^6.0.1",
-        "universalify": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/fs-minipass": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
-      "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
-      "dependencies": {
-        "minipass": "^3.0.0"
-      },
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/fs-minipass/node_modules/minipass": {
-      "version": "3.3.6",
-      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
-      "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/fs-minipass/node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
-    },
-    "node_modules/fs-monkey": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz",
-      "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==",
-      "dev": true
-    },
-    "node_modules/fs.realpath": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
-      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
-    },
-    "node_modules/fsevents": {
-      "version": "2.3.3",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
-      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
-      "hasInstallScript": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
-      }
-    },
-    "node_modules/function-bind": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
-      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/gauge": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
-      "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
-      "dependencies": {
-        "aproba": "^1.0.3 || ^2.0.0",
-        "color-support": "^1.1.2",
-        "console-control-strings": "^1.0.0",
-        "has-unicode": "^2.0.1",
-        "object-assign": "^4.1.1",
-        "signal-exit": "^3.0.0",
-        "string-width": "^4.2.3",
-        "strip-ansi": "^6.0.1",
-        "wide-align": "^1.1.2"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/gauge/node_modules/signal-exit": {
-      "version": "3.0.7",
-      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
-      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
-    },
-    "node_modules/gaxios": {
-      "version": "6.2.0",
-      "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.2.0.tgz",
-      "integrity": "sha512-H6+bHeoEAU5D6XNc6mPKeN5dLZqEDs9Gpk6I+SZBEzK5So58JVrHPmevNi35fRl1J9Y5TaeLW0kYx3pCJ1U2mQ==",
-      "dependencies": {
-        "extend": "^3.0.2",
-        "https-proxy-agent": "^7.0.1",
-        "is-stream": "^2.0.0",
-        "node-fetch": "^2.6.9"
-      },
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/gaxios/node_modules/agent-base": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
-      "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==",
-      "dependencies": {
-        "debug": "^4.3.4"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
-    },
-    "node_modules/gaxios/node_modules/https-proxy-agent": {
-      "version": "7.0.4",
-      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz",
-      "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==",
-      "dependencies": {
-        "agent-base": "^7.0.2",
-        "debug": "4"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
-    },
-    "node_modules/gcp-metadata": {
-      "version": "6.1.0",
-      "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz",
-      "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==",
-      "dependencies": {
-        "gaxios": "^6.0.0",
-        "json-bigint": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/gensync": {
-      "version": "1.0.0-beta.2",
-      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
-      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/geo-tz": {
-      "version": "8.0.2",
-      "resolved": "https://registry.npmjs.org/geo-tz/-/geo-tz-8.0.2.tgz",
-      "integrity": "sha512-NjEzJBzaMhO9C7lFZIsWDkVED7aLxcES3iEZOWJ97dhnDUGhEB8vhW7MaWR+2y4aWvtFV/VyuDi8Y0rUHvm4tw==",
-      "dependencies": {
-        "@turf/boolean-point-in-polygon": "^6.5.0",
-        "@turf/helpers": "^6.5.0",
-        "geobuf": "^3.0.2",
-        "pbf": "^3.2.1"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/evansiroky"
-      }
-    },
-    "node_modules/geobuf": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/geobuf/-/geobuf-3.0.2.tgz",
-      "integrity": "sha512-ASgKwEAQQRnyNFHNvpd5uAwstbVYmiTW0Caw3fBb509tNTqXyAAPMyFs5NNihsLZhLxU1j/kjFhkhLWA9djuVg==",
-      "dependencies": {
-        "concat-stream": "^2.0.0",
-        "pbf": "^3.2.1",
-        "shapefile": "~0.6.6"
-      },
-      "bin": {
-        "geobuf2json": "bin/geobuf2json",
-        "json2geobuf": "bin/json2geobuf",
-        "shp2geobuf": "bin/shp2geobuf"
-      }
-    },
-    "node_modules/get-caller-file": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
-      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
-      "engines": {
-        "node": "6.* || 8.* || >= 10.*"
-      }
-    },
-    "node_modules/get-func-name": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
-      "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
-      "dev": true,
-      "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/get-intrinsic": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
-      "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
-      "dependencies": {
-        "es-errors": "^1.3.0",
-        "function-bind": "^1.1.2",
-        "has-proto": "^1.0.1",
-        "has-symbols": "^1.0.3",
-        "hasown": "^2.0.0"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/get-port": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz",
-      "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/get-stdin": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
-      "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==",
-      "dev": true,
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/get-stream": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
-      "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
-      "dev": true,
-      "engines": {
-        "node": ">=16"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/glob": {
-      "version": "10.4.2",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz",
-      "integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==",
-      "dependencies": {
-        "foreground-child": "^3.1.0",
-        "jackspeak": "^3.1.2",
-        "minimatch": "^9.0.4",
-        "minipass": "^7.1.2",
-        "package-json-from-dist": "^1.0.0",
-        "path-scurry": "^1.11.1"
-      },
-      "bin": {
-        "glob": "dist/esm/bin.mjs"
-      },
-      "engines": {
-        "node": ">=16 || 14 >=14.18"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      }
-    },
-    "node_modules/glob-parent": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
-      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
-      "dependencies": {
-        "is-glob": "^4.0.1"
-      },
-      "engines": {
-        "node": ">= 6"
-      }
-    },
-    "node_modules/glob-to-regexp": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
-      "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
-      "dev": true
-    },
-    "node_modules/glob/node_modules/brace-expansion": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
-      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-      "dependencies": {
-        "balanced-match": "^1.0.0"
-      }
-    },
-    "node_modules/glob/node_modules/jackspeak": {
-      "version": "3.4.2",
-      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.2.tgz",
-      "integrity": "sha512-qH3nOSj8q/8+Eg8LUPOq3C+6HWkpUioIjDsq1+D4zY91oZvpPttw8GwtF1nReRYKXl+1AORyFqtm2f5Q1SB6/Q==",
-      "dependencies": {
-        "@isaacs/cliui": "^8.0.2"
-      },
-      "engines": {
-        "node": "14 >=14.21 || 16 >=16.20 || >=18"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      },
-      "optionalDependencies": {
-        "@pkgjs/parseargs": "^0.11.0"
-      }
-    },
-    "node_modules/glob/node_modules/minimatch": {
-      "version": "9.0.4",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
-      "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
-      "dependencies": {
-        "brace-expansion": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=16 || 14 >=14.17"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      }
-    },
-    "node_modules/globals": {
-      "version": "15.9.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz",
-      "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=18"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/globrex": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
-      "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
-      "dev": true
-    },
-    "node_modules/gopd": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
-      "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
-      "dependencies": {
-        "get-intrinsic": "^1.1.3"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/graceful-fs": {
-      "version": "4.2.11",
-      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
-      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
-    },
-    "node_modules/graphemer": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
-      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
-      "dev": true
-    },
-    "node_modules/handlebars": {
-      "version": "4.7.8",
-      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
-      "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
-      "dependencies": {
-        "minimist": "^1.2.5",
-        "neo-async": "^2.6.2",
-        "source-map": "^0.6.1",
-        "wordwrap": "^1.0.0"
-      },
-      "bin": {
-        "handlebars": "bin/handlebars"
-      },
-      "engines": {
-        "node": ">=0.4.7"
-      },
-      "optionalDependencies": {
-        "uglify-js": "^3.1.4"
-      }
-    },
-    "node_modules/handlebars/node_modules/source-map": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/has": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
-      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
-      "dependencies": {
-        "function-bind": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 0.4.0"
-      }
-    },
-    "node_modules/has-flag": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/has-own-prop": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz",
-      "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/has-property-descriptors": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
-      "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
-      "dependencies": {
-        "get-intrinsic": "^1.2.2"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/has-proto": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
-      "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/has-symbols": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
-      "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/has-unicode": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
-      "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
-    },
-    "node_modules/hasown": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
-      "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
-      "dependencies": {
-        "function-bind": "^1.1.2"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/he": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
-      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
-      "bin": {
-        "he": "bin/he"
-      }
-    },
-    "node_modules/highlight.js": {
-      "version": "10.7.3",
-      "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
-      "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
-      "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/hosted-git-info": {
-      "version": "2.8.9",
-      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
-      "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
-      "dev": true
-    },
-    "node_modules/html-escaper": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
-      "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
-      "dev": true
-    },
-    "node_modules/html-to-text": {
-      "version": "9.0.5",
-      "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
-      "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==",
-      "dependencies": {
-        "@selderee/plugin-htmlparser2": "^0.11.0",
-        "deepmerge": "^4.3.1",
-        "dom-serializer": "^2.0.0",
-        "htmlparser2": "^8.0.2",
-        "selderee": "^0.11.0"
-      },
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/htmlparser2": {
-      "version": "8.0.2",
-      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
-      "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
-      "funding": [
-        "https://github.com/fb55/htmlparser2?sponsor=1",
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/fb55"
-        }
-      ],
-      "dependencies": {
-        "domelementtype": "^2.3.0",
-        "domhandler": "^5.0.3",
-        "domutils": "^3.0.1",
-        "entities": "^4.4.0"
-      }
-    },
-    "node_modules/http-errors": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
-      "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
-      "dependencies": {
-        "depd": "2.0.0",
-        "inherits": "2.0.4",
-        "setprototypeof": "1.2.0",
-        "statuses": "2.0.1",
-        "toidentifier": "1.0.1"
-      },
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/https-proxy-agent": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
-      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
-      "dependencies": {
-        "agent-base": "6",
-        "debug": "4"
-      },
-      "engines": {
-        "node": ">= 6"
-      }
-    },
-    "node_modules/human-signals": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
-      "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=16.17.0"
-      }
-    },
-    "node_modules/i18n-iso-countries": {
-      "version": "7.11.3",
-      "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.11.3.tgz",
-      "integrity": "sha512-yxQVzNvxEaspSqNnCbqLvwTZNXXkGydWcSxytJYZYb0KH5pn13fdywuX0vFxmOg57Z8ff416AuKDx6Oqnx+j9w==",
-      "dependencies": {
-        "diacritics": "1.3.0"
-      },
-      "engines": {
-        "node": ">= 12"
-      }
-    },
-    "node_modules/iconv-lite": {
-      "version": "0.4.24",
-      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
-      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
-      "dependencies": {
-        "safer-buffer": ">= 2.1.2 < 3"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/ieee754": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
-      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ]
-    },
-    "node_modules/ignore": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
-      "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
-      "dev": true,
-      "engines": {
-        "node": ">= 4"
-      }
-    },
-    "node_modules/import-fresh": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
-      "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
-      "dependencies": {
-        "parent-module": "^1.0.0",
-        "resolve-from": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/import-in-the-middle": {
-      "version": "1.8.0",
-      "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.8.0.tgz",
-      "integrity": "sha512-/xQjze8szLNnJ5rvHSzn+dcVXqCAU6Plbk4P24U/jwPmg1wy7IIp9OjKIO5tYue8GSPhDpPDiApQjvBUmWwhsQ==",
-      "dependencies": {
-        "acorn": "^8.8.2",
-        "acorn-import-attributes": "^1.9.5",
-        "cjs-module-lexer": "^1.2.2",
-        "module-details-from-path": "^1.0.3"
-      }
-    },
-    "node_modules/imurmurhash": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
-      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.8.19"
-      }
-    },
-    "node_modules/indent-string": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
-      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/inflight": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
-      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
-      "dependencies": {
-        "once": "^1.3.0",
-        "wrappy": "1"
-      }
-    },
-    "node_modules/inherits": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
-      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
-    },
-    "node_modules/ini": {
-      "version": "1.3.8",
-      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
-      "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
-    },
-    "node_modules/inquirer": {
-      "version": "8.2.6",
-      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz",
-      "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==",
-      "dependencies": {
-        "ansi-escapes": "^4.2.1",
-        "chalk": "^4.1.1",
-        "cli-cursor": "^3.1.0",
-        "cli-width": "^3.0.0",
-        "external-editor": "^3.0.3",
-        "figures": "^3.0.0",
-        "lodash": "^4.17.21",
-        "mute-stream": "0.0.8",
-        "ora": "^5.4.1",
-        "run-async": "^2.4.0",
-        "rxjs": "^7.5.5",
-        "string-width": "^4.1.0",
-        "strip-ansi": "^6.0.0",
-        "through": "^2.3.6",
-        "wrap-ansi": "^6.0.1"
-      },
-      "engines": {
-        "node": ">=12.0.0"
-      }
-    },
-    "node_modules/ioredis": {
-      "version": "5.4.1",
-      "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz",
-      "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==",
-      "dependencies": {
-        "@ioredis/commands": "^1.1.1",
-        "cluster-key-slot": "^1.1.0",
-        "debug": "^4.3.4",
-        "denque": "^2.1.0",
-        "lodash.defaults": "^4.2.0",
-        "lodash.isarguments": "^3.1.0",
-        "redis-errors": "^1.2.0",
-        "redis-parser": "^3.0.0",
-        "standard-as-callback": "^2.1.0"
-      },
-      "engines": {
-        "node": ">=12.22.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/ioredis"
-      }
-    },
-    "node_modules/ipaddr.js": {
-      "version": "1.9.1",
-      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
-      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
-      "engines": {
-        "node": ">= 0.10"
-      }
-    },
-    "node_modules/is-arrayish": {
-      "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
-      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
-    },
-    "node_modules/is-binary-path": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
-      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
-      "dependencies": {
-        "binary-extensions": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/is-builtin-module": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
-      "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
-      "dev": true,
-      "dependencies": {
-        "builtin-modules": "^3.3.0"
-      },
-      "engines": {
-        "node": ">=6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/is-core-module": {
-      "version": "2.13.0",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
-      "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==",
-      "dependencies": {
-        "has": "^1.0.3"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/is-extglob": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/is-fullwidth-code-point": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/is-glob": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
-      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
-      "dependencies": {
-        "is-extglob": "^2.1.1"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/is-interactive": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
-      "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/is-number": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
-      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-      "engines": {
-        "node": ">=0.12.0"
-      }
-    },
-    "node_modules/is-path-inside": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
-      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/is-stream": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
-      "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/is-unicode-supported": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
-      "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/isarray": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
-      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
-    },
-    "node_modules/isexe": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
-    },
-    "node_modules/istanbul-lib-coverage": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
-      "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/istanbul-lib-report": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
-      "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
-      "dev": true,
-      "dependencies": {
-        "istanbul-lib-coverage": "^3.0.0",
-        "make-dir": "^4.0.0",
-        "supports-color": "^7.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/istanbul-lib-report/node_modules/make-dir": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
-      "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
-      "dev": true,
-      "dependencies": {
-        "semver": "^7.5.3"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/istanbul-lib-source-maps": {
-      "version": "5.0.6",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
-      "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
-      "dev": true,
-      "dependencies": {
-        "@jridgewell/trace-mapping": "^0.3.23",
-        "debug": "^4.1.1",
-        "istanbul-lib-coverage": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/istanbul-reports": {
-      "version": "3.1.7",
-      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
-      "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
-      "dev": true,
-      "dependencies": {
-        "html-escaper": "^2.0.0",
-        "istanbul-lib-report": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/iterare": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz",
-      "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==",
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/jackspeak": {
-      "version": "2.3.6",
-      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
-      "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
-      "dependencies": {
-        "@isaacs/cliui": "^8.0.2"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      },
-      "optionalDependencies": {
-        "@pkgjs/parseargs": "^0.11.0"
-      }
-    },
-    "node_modules/jiti": {
-      "version": "1.21.0",
-      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
-      "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
-      "peer": true,
-      "bin": {
-        "jiti": "bin/jiti.js"
-      }
-    },
-    "node_modules/joi": {
-      "version": "17.13.3",
-      "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
-      "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
-      "dependencies": {
-        "@hapi/hoek": "^9.3.0",
-        "@hapi/topo": "^5.1.0",
-        "@sideway/address": "^4.1.5",
-        "@sideway/formula": "^3.0.1",
-        "@sideway/pinpoint": "^2.0.0"
-      }
-    },
-    "node_modules/jose": {
-      "version": "4.15.5",
-      "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz",
-      "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==",
-      "funding": {
-        "url": "https://github.com/sponsors/panva"
-      }
-    },
-    "node_modules/js-beautify": {
-      "version": "1.15.1",
-      "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz",
-      "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==",
-      "dependencies": {
-        "config-chain": "^1.1.13",
-        "editorconfig": "^1.0.4",
-        "glob": "^10.3.3",
-        "js-cookie": "^3.0.5",
-        "nopt": "^7.2.0"
-      },
-      "bin": {
-        "css-beautify": "js/bin/css-beautify.js",
-        "html-beautify": "js/bin/html-beautify.js",
-        "js-beautify": "js/bin/js-beautify.js"
-      },
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/js-beautify/node_modules/abbrev": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
-      "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
-      "engines": {
-        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
-      }
-    },
-    "node_modules/js-beautify/node_modules/nopt": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz",
-      "integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==",
-      "dependencies": {
-        "abbrev": "^2.0.0"
-      },
-      "bin": {
-        "nopt": "bin/nopt.js"
-      },
-      "engines": {
-        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
-      }
-    },
-    "node_modules/js-cookie": {
-      "version": "3.0.5",
-      "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
-      "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/js-tokens": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
-      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
-    },
-    "node_modules/js-yaml": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
-      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
-      "dependencies": {
-        "argparse": "^2.0.1"
-      },
-      "bin": {
-        "js-yaml": "bin/js-yaml.js"
-      }
-    },
-    "node_modules/jsesc": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
-      "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
-      "dev": true,
-      "bin": {
-        "jsesc": "bin/jsesc"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/json-bigint": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
-      "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
-      "dependencies": {
-        "bignumber.js": "^9.0.0"
-      }
-    },
-    "node_modules/json-buffer": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
-      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
-      "dev": true
-    },
-    "node_modules/json-parse-even-better-errors": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
-      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
-    },
-    "node_modules/json-schema-traverse": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
-      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
-      "dev": true
-    },
-    "node_modules/json-stable-stringify-without-jsonify": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
-      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
-      "dev": true
-    },
-    "node_modules/json5": {
-      "version": "2.2.3",
-      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
-      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
-      "bin": {
-        "json5": "lib/cli.js"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/jsonc-parser": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz",
-      "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==",
-      "dev": true
-    },
-    "node_modules/jsonfile": {
-      "version": "6.1.0",
-      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
-      "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
-      "dev": true,
-      "dependencies": {
-        "universalify": "^2.0.0"
-      },
-      "optionalDependencies": {
-        "graceful-fs": "^4.1.6"
-      }
-    },
-    "node_modules/keyv": {
-      "version": "4.5.4",
-      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
-      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
-      "dev": true,
-      "dependencies": {
-        "json-buffer": "3.0.1"
-      }
-    },
-    "node_modules/lazystream": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
-      "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
-      "dependencies": {
-        "readable-stream": "^2.0.5"
-      },
-      "engines": {
-        "node": ">= 0.6.3"
-      }
-    },
-    "node_modules/lazystream/node_modules/readable-stream": {
-      "version": "2.3.8",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
-      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
-      "dependencies": {
-        "core-util-is": "~1.0.0",
-        "inherits": "~2.0.3",
-        "isarray": "~1.0.0",
-        "process-nextick-args": "~2.0.0",
-        "safe-buffer": "~5.1.1",
-        "string_decoder": "~1.1.1",
-        "util-deprecate": "~1.0.1"
-      }
-    },
-    "node_modules/lazystream/node_modules/safe-buffer": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
-      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
-    },
-    "node_modules/lazystream/node_modules/string_decoder": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
-      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
-      "dependencies": {
-        "safe-buffer": "~5.1.0"
-      }
-    },
-    "node_modules/leac": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
-      "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==",
-      "funding": {
-        "url": "https://ko-fi.com/killymxi"
-      }
-    },
-    "node_modules/levn": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
-      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
-      "dev": true,
-      "dependencies": {
-        "prelude-ls": "^1.2.1",
-        "type-check": "~0.4.0"
-      },
-      "engines": {
-        "node": ">= 0.8.0"
-      }
-    },
-    "node_modules/libphonenumber-js": {
-      "version": "1.10.53",
-      "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.53.tgz",
-      "integrity": "sha512-sDTnnqlWK4vH4AlDQuswz3n4Hx7bIQWTpIcScJX+Sp7St3LXHmfiax/ZFfyYxHmkdCvydOLSuvtAO/XpXiSySw=="
-    },
-    "node_modules/lilconfig": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
-      "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
-      "peer": true,
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/lines-and-columns": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
-      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
-    },
-    "node_modules/load-tsconfig": {
-      "version": "0.2.5",
-      "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz",
-      "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==",
-      "dev": true,
-      "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
-      }
-    },
-    "node_modules/loader-runner": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
-      "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
-      "dev": true,
-      "engines": {
-        "node": ">=6.11.5"
-      }
-    },
-    "node_modules/locate-path": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
-      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
-      "dev": true,
-      "dependencies": {
-        "p-locate": "^5.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/lodash": {
-      "version": "4.17.21",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
-      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
-    },
-    "node_modules/lodash.camelcase": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
-      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
-    },
-    "node_modules/lodash.defaults": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
-      "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
-    },
-    "node_modules/lodash.isarguments": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
-      "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="
-    },
-    "node_modules/lodash.merge": {
-      "version": "4.6.2",
-      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
-      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
-    },
-    "node_modules/log-symbols": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
-      "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
-      "dependencies": {
-        "chalk": "^4.1.0",
-        "is-unicode-supported": "^0.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/long": {
-      "version": "5.2.3",
-      "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
-      "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
-    },
-    "node_modules/loose-envify": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
-      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
-      "dependencies": {
-        "js-tokens": "^3.0.0 || ^4.0.0"
-      },
-      "bin": {
-        "loose-envify": "cli.js"
-      }
-    },
-    "node_modules/loupe": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz",
-      "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==",
-      "dev": true,
-      "dependencies": {
-        "get-func-name": "^2.0.1"
-      }
-    },
-    "node_modules/lru-cache": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
-      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
-      "dependencies": {
-        "yallist": "^3.0.2"
-      }
-    },
-    "node_modules/luxon": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
-      "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==",
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/magic-string": {
-      "version": "0.30.8",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
-      "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
-      "dev": true,
-      "dependencies": {
-        "@jridgewell/sourcemap-codec": "^1.4.15"
-      },
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/magicast": {
-      "version": "0.3.4",
-      "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz",
-      "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==",
-      "dev": true,
-      "dependencies": {
-        "@babel/parser": "^7.24.4",
-        "@babel/types": "^7.24.0",
-        "source-map-js": "^1.2.0"
-      }
-    },
-    "node_modules/make-dir": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
-      "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
-      "dependencies": {
-        "semver": "^6.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/make-dir/node_modules/semver": {
-      "version": "6.3.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
-      "bin": {
-        "semver": "bin/semver.js"
-      }
-    },
-    "node_modules/make-error": {
-      "version": "1.3.6",
-      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
-      "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
-      "optional": true,
-      "peer": true
-    },
-    "node_modules/marked": {
-      "version": "7.0.4",
-      "resolved": "https://registry.npmjs.org/marked/-/marked-7.0.4.tgz",
-      "integrity": "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==",
-      "bin": {
-        "marked": "bin/marked.js"
-      },
-      "engines": {
-        "node": ">= 16"
-      }
-    },
-    "node_modules/md-to-react-email": {
-      "version": "5.0.2",
-      "resolved": "https://registry.npmjs.org/md-to-react-email/-/md-to-react-email-5.0.2.tgz",
-      "integrity": "sha512-x6kkpdzIzUhecda/yahltfEl53mH26QdWu4abUF9+S0Jgam8P//Ciro8cdhyMHnT5MQUJYrIbO6ORM2UxPiNNA==",
-      "dependencies": {
-        "marked": "7.0.4"
-      },
-      "peerDependencies": {
-        "react": "18.x"
-      }
-    },
-    "node_modules/media-typer": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
-      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/memfs": {
-      "version": "3.5.3",
-      "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz",
-      "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==",
-      "dev": true,
-      "dependencies": {
-        "fs-monkey": "^1.0.4"
-      },
-      "engines": {
-        "node": ">= 4.0.0"
-      }
-    },
-    "node_modules/merge-descriptors": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
-      "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
-    },
-    "node_modules/merge-stream": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
-      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
-      "dev": true
-    },
-    "node_modules/merge2": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
-      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/methods": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
-      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/micromatch": {
-      "version": "4.0.5",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
-      "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
-      "dependencies": {
-        "braces": "^3.0.2",
-        "picomatch": "^2.3.1"
-      },
-      "engines": {
-        "node": ">=8.6"
-      }
-    },
-    "node_modules/micromatch/node_modules/picomatch": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
-      "engines": {
-        "node": ">=8.6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/jonschlinkert"
-      }
-    },
-    "node_modules/mime": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
-      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
-      "bin": {
-        "mime": "cli.js"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/mime-db": {
-      "version": "1.52.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
-      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/mime-types": {
-      "version": "2.1.35",
-      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
-      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
-      "dependencies": {
-        "mime-db": "1.52.0"
-      },
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/mimic-fn": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
-      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/min-indent": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
-      "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/minimatch": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-      "dependencies": {
-        "brace-expansion": "^1.1.7"
-      },
-      "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/minimist": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
-      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/minipass": {
-      "version": "7.1.2",
-      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
-      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
-      "engines": {
-        "node": ">=16 || 14 >=14.17"
-      }
-    },
-    "node_modules/minizlib": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
-      "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
-      "dependencies": {
-        "minipass": "^3.0.0",
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/minizlib/node_modules/minipass": {
-      "version": "3.3.6",
-      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
-      "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/minizlib/node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
-    },
-    "node_modules/mkdirp": {
-      "version": "0.5.6",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
-      "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
-      "dependencies": {
-        "minimist": "^1.2.6"
-      },
-      "bin": {
-        "mkdirp": "bin/cmd.js"
-      }
-    },
-    "node_modules/mkdirp-classic": {
-      "version": "0.5.3",
-      "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
-      "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
-      "dev": true
-    },
-    "node_modules/mock-fs": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.2.0.tgz",
-      "integrity": "sha512-2dF2R6YMSZbpip1V1WHKGLNjr/k48uQClqMVb5H3MOvwc9qhYis3/IWbj02qIg/Y8MDXKFF4c5v0rxx2o6xTZw==",
-      "dev": true,
-      "engines": {
-        "node": ">=12.0.0"
-      }
-    },
-    "node_modules/module-details-from-path": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz",
-      "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A=="
-    },
-    "node_modules/moo": {
-      "version": "0.5.2",
-      "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
-      "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==",
-      "dev": true
-    },
-    "node_modules/mrmime": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
-      "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==",
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/ms": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
-    },
-    "node_modules/msgpackr": {
-      "version": "1.10.1",
-      "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.10.1.tgz",
-      "integrity": "sha512-r5VRLv9qouXuLiIBrLpl2d5ZvPt8svdQTl5/vMvE4nzDMyEX4sgW5yWhuBBj5UmgwOTWj8CIdSXn5sAfsHAWIQ==",
-      "optionalDependencies": {
-        "msgpackr-extract": "^3.0.2"
-      }
-    },
-    "node_modules/msgpackr-extract": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz",
-      "integrity": "sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==",
-      "hasInstallScript": true,
-      "optional": true,
-      "dependencies": {
-        "node-gyp-build-optional-packages": "5.0.7"
-      },
-      "bin": {
-        "download-msgpackr-prebuilds": "bin/download-prebuilds.js"
-      },
-      "optionalDependencies": {
-        "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.2",
-        "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.2",
-        "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.2",
-        "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.2",
-        "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.2",
-        "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2"
-      }
-    },
-    "node_modules/multer": {
-      "version": "1.4.4-lts.1",
-      "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz",
-      "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==",
-      "dependencies": {
-        "append-field": "^1.0.0",
-        "busboy": "^1.0.0",
-        "concat-stream": "^1.5.2",
-        "mkdirp": "^0.5.4",
-        "object-assign": "^4.1.1",
-        "type-is": "^1.6.4",
-        "xtend": "^4.0.0"
-      },
-      "engines": {
-        "node": ">= 6.0.0"
-      }
-    },
-    "node_modules/multer/node_modules/concat-stream": {
-      "version": "1.6.2",
-      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
-      "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
-      "engines": [
-        "node >= 0.8"
-      ],
-      "dependencies": {
-        "buffer-from": "^1.0.0",
-        "inherits": "^2.0.3",
-        "readable-stream": "^2.2.2",
-        "typedarray": "^0.0.6"
-      }
-    },
-    "node_modules/multer/node_modules/readable-stream": {
-      "version": "2.3.8",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
-      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
-      "dependencies": {
-        "core-util-is": "~1.0.0",
-        "inherits": "~2.0.3",
-        "isarray": "~1.0.0",
-        "process-nextick-args": "~2.0.0",
-        "safe-buffer": "~5.1.1",
-        "string_decoder": "~1.1.1",
-        "util-deprecate": "~1.0.1"
-      }
-    },
-    "node_modules/multer/node_modules/safe-buffer": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
-      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
-    },
-    "node_modules/multer/node_modules/string_decoder": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
-      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
-      "dependencies": {
-        "safe-buffer": "~5.1.0"
-      }
-    },
-    "node_modules/mute-stream": {
-      "version": "0.0.8",
-      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
-      "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
-    },
-    "node_modules/mz": {
-      "version": "2.7.0",
-      "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
-      "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
-      "dependencies": {
-        "any-promise": "^1.0.0",
-        "object-assign": "^4.0.1",
-        "thenify-all": "^1.0.0"
-      }
-    },
-    "node_modules/nan": {
-      "version": "2.18.0",
-      "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
-      "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==",
-      "dev": true,
-      "optional": true
-    },
-    "node_modules/nanoid": {
-      "version": "3.3.7",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
-      "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/ai"
-        }
-      ],
-      "bin": {
-        "nanoid": "bin/nanoid.cjs"
-      },
-      "engines": {
-        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
-      }
-    },
-    "node_modules/natural-compare": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
-      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
-      "dev": true
-    },
-    "node_modules/nearley": {
-      "version": "2.20.1",
-      "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz",
-      "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==",
-      "dev": true,
-      "dependencies": {
-        "commander": "^2.19.0",
-        "moo": "^0.5.0",
-        "railroad-diagrams": "^1.0.0",
-        "randexp": "0.4.6"
-      },
-      "bin": {
-        "nearley-railroad": "bin/nearley-railroad.js",
-        "nearley-test": "bin/nearley-test.js",
-        "nearley-unparse": "bin/nearley-unparse.js",
-        "nearleyc": "bin/nearleyc.js"
-      },
-      "funding": {
-        "type": "individual",
-        "url": "https://nearley.js.org/#give-to-nearley"
-      }
-    },
-    "node_modules/nearley/node_modules/commander": {
-      "version": "2.20.3",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
-      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
-      "dev": true
-    },
-    "node_modules/negotiator": {
-      "version": "0.6.3",
-      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
-      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/neo-async": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
-      "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
-    },
-    "node_modules/nest-commander": {
-      "version": "3.15.0",
-      "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.15.0.tgz",
-      "integrity": "sha512-o9VEfFj/w2nm+hQi6fnkxL1GAFZW/KmuGcIE7/B/TX0gwm0QVy8svAF75EQm8wrDjcvWS7Cx/ArnkFn2C+iM2w==",
-      "dependencies": {
-        "@fig/complete-commander": "^3.0.0",
-        "@golevelup/nestjs-discovery": "4.0.1",
-        "commander": "11.1.0",
-        "cosmiconfig": "8.3.6",
-        "inquirer": "8.2.6"
-      },
-      "peerDependencies": {
-        "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
-        "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0",
-        "@types/inquirer": "^8.1.3"
-      }
-    },
-    "node_modules/nest-commander/node_modules/@fig/complete-commander": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/@fig/complete-commander/-/complete-commander-3.2.0.tgz",
-      "integrity": "sha512-1Holl3XtRiANVKURZwgpjCnPuV4RsHp+XC0MhgvyAX/avQwj7F2HUItYOvGi/bXjJCkEzgBZmVfCr0HBA+q+Bw==",
-      "dependencies": {
-        "prettier": "^3.2.5"
-      },
-      "peerDependencies": {
-        "commander": "^11.1.0"
-      }
-    },
-    "node_modules/nest-commander/node_modules/commander": {
-      "version": "11.1.0",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
-      "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
-      "engines": {
-        "node": ">=16"
-      }
-    },
-    "node_modules/nestjs-cls": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-4.4.1.tgz",
-      "integrity": "sha512-4yhldwm/cJ02lQ8ZAdM8KQ7gMfjAc1z3fo5QAQgXNyN4N6X5So9BCwv+BTLRugDCkELUo3qtzQHnKhGYL/ftPg==",
-      "engines": {
-        "node": ">=16"
-      },
-      "peerDependencies": {
-        "@nestjs/common": "> 7.0.0 < 11",
-        "@nestjs/core": "> 7.0.0 < 11",
-        "reflect-metadata": "*",
-        "rxjs": ">= 7"
-      }
-    },
-    "node_modules/nestjs-otel": {
-      "version": "6.1.1",
-      "resolved": "https://registry.npmjs.org/nestjs-otel/-/nestjs-otel-6.1.1.tgz",
-      "integrity": "sha512-hWuhDYkkaZrBXpHmi2v0jGqKa61uPqzu2YsVhww8/s+v9SaDILylR7ZdoOiygCQisgHG9rw5odP12GfsMS8cBA==",
-      "dependencies": {
-        "@opentelemetry/api": "^1.8.0",
-        "@opentelemetry/host-metrics": "^0.35.1",
-        "response-time": "^2.3.2"
-      },
-      "peerDependencies": {
-        "@nestjs/common": "^9.0.0 || ^10.0.0",
-        "@nestjs/core": "^9.0.0 || ^10.0.0"
-      }
-    },
-    "node_modules/next": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/next/-/next-14.2.3.tgz",
-      "integrity": "sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==",
-      "dependencies": {
-        "@next/env": "14.2.3",
-        "@swc/helpers": "0.5.5",
-        "busboy": "1.6.0",
-        "caniuse-lite": "^1.0.30001579",
-        "graceful-fs": "^4.2.11",
-        "postcss": "8.4.31",
-        "styled-jsx": "5.1.1"
-      },
-      "bin": {
-        "next": "dist/bin/next"
-      },
-      "engines": {
-        "node": ">=18.17.0"
-      },
-      "optionalDependencies": {
-        "@next/swc-darwin-arm64": "14.2.3",
-        "@next/swc-darwin-x64": "14.2.3",
-        "@next/swc-linux-arm64-gnu": "14.2.3",
-        "@next/swc-linux-arm64-musl": "14.2.3",
-        "@next/swc-linux-x64-gnu": "14.2.3",
-        "@next/swc-linux-x64-musl": "14.2.3",
-        "@next/swc-win32-arm64-msvc": "14.2.3",
-        "@next/swc-win32-ia32-msvc": "14.2.3",
-        "@next/swc-win32-x64-msvc": "14.2.3"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "^1.1.0",
-        "@playwright/test": "^1.41.2",
-        "react": "^18.2.0",
-        "react-dom": "^18.2.0",
-        "sass": "^1.3.0"
-      },
-      "peerDependenciesMeta": {
-        "@opentelemetry/api": {
-          "optional": true
-        },
-        "@playwright/test": {
-          "optional": true
-        },
-        "sass": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/next/node_modules/postcss": {
-      "version": "8.4.31",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
-      "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
-      "funding": [
-        {
-          "type": "opencollective",
-          "url": "https://opencollective.com/postcss/"
-        },
-        {
-          "type": "tidelift",
-          "url": "https://tidelift.com/funding/github/npm/postcss"
-        },
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/ai"
-        }
-      ],
-      "dependencies": {
-        "nanoid": "^3.3.6",
-        "picocolors": "^1.0.0",
-        "source-map-js": "^1.0.2"
-      },
-      "engines": {
-        "node": "^10 || ^12 || >=14"
-      }
-    },
-    "node_modules/node-abort-controller": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
-      "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="
-    },
-    "node_modules/node-emoji": {
-      "version": "1.11.0",
-      "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
-      "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==",
-      "dev": true,
-      "dependencies": {
-        "lodash": "^4.17.21"
-      }
-    },
-    "node_modules/node-fetch": {
-      "version": "2.7.0",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
-      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
-      "dependencies": {
-        "whatwg-url": "^5.0.0"
-      },
-      "engines": {
-        "node": "4.x || >=6.0.0"
-      },
-      "peerDependencies": {
-        "encoding": "^0.1.0"
-      },
-      "peerDependenciesMeta": {
-        "encoding": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/node-gyp-build-optional-packages": {
-      "version": "5.0.7",
-      "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz",
-      "integrity": "sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==",
-      "optional": true,
-      "bin": {
-        "node-gyp-build-optional-packages": "bin.js",
-        "node-gyp-build-optional-packages-optional": "optional.js",
-        "node-gyp-build-optional-packages-test": "build-test.js"
-      }
-    },
-    "node_modules/node-releases": {
-      "version": "2.0.14",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
-      "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
-    },
-    "node_modules/nodemailer": {
-      "version": "6.9.14",
-      "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz",
-      "integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==",
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
-    "node_modules/nopt": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
-      "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
-      "dependencies": {
-        "abbrev": "1"
-      },
-      "bin": {
-        "nopt": "bin/nopt.js"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/normalize-package-data": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
-      "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
-      "dev": true,
-      "dependencies": {
-        "hosted-git-info": "^2.1.4",
-        "resolve": "^1.10.0",
-        "semver": "2 || 3 || 4 || 5",
-        "validate-npm-package-license": "^3.0.1"
-      }
-    },
-    "node_modules/normalize-package-data/node_modules/semver": {
-      "version": "5.7.2",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
-      "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
-      "dev": true,
-      "bin": {
-        "semver": "bin/semver"
-      }
-    },
-    "node_modules/normalize-path": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
-      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/notepack.io": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-3.0.1.tgz",
-      "integrity": "sha512-TKC/8zH5pXIAMVQio2TvVDTtPRX+DJPHDqjRbxogtFiByHyzKmy96RA0JtCQJ+WouyyL4A10xomQzgbUT+1jCg=="
-    },
-    "node_modules/npm-run-path": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
-      "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
-      "dev": true,
-      "dependencies": {
-        "path-key": "^4.0.0"
-      },
-      "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/npm-run-path/node_modules/path-key": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
-      "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/npmlog": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
-      "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
-      "dependencies": {
-        "are-we-there-yet": "^2.0.0",
-        "console-control-strings": "^1.1.0",
-        "gauge": "^3.0.0",
-        "set-blocking": "^2.0.0"
-      }
-    },
-    "node_modules/object-assign": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
-      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/object-hash": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
-      "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
-      "engines": {
-        "node": ">= 6"
-      }
-    },
-    "node_modules/object-inspect": {
-      "version": "1.12.3",
-      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
-      "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/obuf": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
-      "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
-    },
-    "node_modules/oidc-token-hash": {
-      "version": "5.0.3",
-      "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",
-      "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==",
-      "engines": {
-        "node": "^10.13.0 || >=12.0.0"
-      }
-    },
-    "node_modules/on-finished": {
-      "version": "2.4.1",
-      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
-      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
-      "dependencies": {
-        "ee-first": "1.1.1"
-      },
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/on-headers": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
-      "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/once": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
-      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
-      "dependencies": {
-        "wrappy": "1"
-      }
-    },
-    "node_modules/onetime": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
-      "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
-      "dependencies": {
-        "mimic-fn": "^2.1.0"
-      },
-      "engines": {
-        "node": ">=6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/openid-client": {
-      "version": "5.6.5",
-      "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz",
-      "integrity": "sha512-5P4qO9nGJzB5PI0LFlhj4Dzg3m4odt0qsJTfyEtZyOlkgpILwEioOhVVJOrS1iVH494S4Ee5OCjjg6Bf5WOj3w==",
-      "dependencies": {
-        "jose": "^4.15.5",
-        "lru-cache": "^6.0.0",
-        "object-hash": "^2.2.0",
-        "oidc-token-hash": "^5.0.3"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/panva"
-      }
-    },
-    "node_modules/openid-client/node_modules/lru-cache": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/openid-client/node_modules/object-hash": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
-      "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
-      "engines": {
-        "node": ">= 6"
-      }
-    },
-    "node_modules/openid-client/node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
-    },
-    "node_modules/optionator": {
-      "version": "0.9.3",
-      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
-      "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
-      "dev": true,
-      "dependencies": {
-        "@aashutoshrathi/word-wrap": "^1.2.3",
-        "deep-is": "^0.1.3",
-        "fast-levenshtein": "^2.0.6",
-        "levn": "^0.4.1",
-        "prelude-ls": "^1.2.1",
-        "type-check": "^0.4.0"
-      },
-      "engines": {
-        "node": ">= 0.8.0"
-      }
-    },
-    "node_modules/ora": {
-      "version": "5.4.1",
-      "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
-      "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
-      "dependencies": {
-        "bl": "^4.1.0",
-        "chalk": "^4.1.0",
-        "cli-cursor": "^3.1.0",
-        "cli-spinners": "^2.5.0",
-        "is-interactive": "^1.0.0",
-        "is-unicode-supported": "^0.1.0",
-        "log-symbols": "^4.1.0",
-        "strip-ansi": "^6.0.0",
-        "wcwidth": "^1.0.1"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/os-tmpdir": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
-      "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/p-limit": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
-      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
-      "dev": true,
-      "dependencies": {
-        "yocto-queue": "^0.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/p-locate": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
-      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
-      "dev": true,
-      "dependencies": {
-        "p-limit": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/p-try": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
-      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/package-json-from-dist": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
-      "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw=="
-    },
-    "node_modules/parent-module": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
-      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
-      "dependencies": {
-        "callsites": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/parse-json": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
-      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
-      "dependencies": {
-        "@babel/code-frame": "^7.0.0",
-        "error-ex": "^1.3.1",
-        "json-parse-even-better-errors": "^2.3.0",
-        "lines-and-columns": "^1.1.6"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/parse5": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
-      "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="
-    },
-    "node_modules/parse5-htmlparser2-tree-adapter": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
-      "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==",
-      "dependencies": {
-        "parse5": "^6.0.1"
-      }
-    },
-    "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
-      "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
-    },
-    "node_modules/parseley": {
-      "version": "0.12.1",
-      "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
-      "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==",
-      "dependencies": {
-        "leac": "^0.6.0",
-        "peberminta": "^0.9.0"
-      },
-      "funding": {
-        "url": "https://ko-fi.com/killymxi"
-      }
-    },
-    "node_modules/parseurl": {
-      "version": "1.3.3",
-      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
-      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/path-exists": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
-      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/path-is-absolute": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
-      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/path-key": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
-      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/path-parse": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
-      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
-    },
-    "node_modules/path-scurry": {
-      "version": "1.11.1",
-      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
-      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
-      "dependencies": {
-        "lru-cache": "^10.2.0",
-        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
-      },
-      "engines": {
-        "node": ">=16 || 14 >=14.18"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      }
-    },
-    "node_modules/path-scurry/node_modules/lru-cache": {
-      "version": "10.2.0",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
-      "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==",
-      "engines": {
-        "node": "14 || >=16.14"
-      }
-    },
-    "node_modules/path-source": {
-      "version": "0.1.3",
-      "resolved": "https://registry.npmjs.org/path-source/-/path-source-0.1.3.tgz",
-      "integrity": "sha512-dWRHm5mIw5kw0cs3QZLNmpUWty48f5+5v9nWD2dw3Y0Hf+s01Ag8iJEWV0Sm0kocE8kK27DrIowha03e1YR+Qw==",
-      "dependencies": {
-        "array-source": "0.0",
-        "file-source": "0.6"
-      }
-    },
-    "node_modules/path-to-regexp": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz",
-      "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA=="
-    },
-    "node_modules/path-type": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
-      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/pathe": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
-      "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
-      "dev": true
-    },
-    "node_modules/pathval": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
-      "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
-      "dev": true,
-      "engines": {
-        "node": ">= 14.16"
-      }
-    },
-    "node_modules/pbf": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz",
-      "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==",
-      "dependencies": {
-        "ieee754": "^1.1.12",
-        "resolve-protobuf-schema": "^2.1.0"
-      },
-      "bin": {
-        "pbf": "bin/pbf"
-      }
-    },
-    "node_modules/peberminta": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
-      "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==",
-      "funding": {
-        "url": "https://ko-fi.com/killymxi"
-      }
-    },
-    "node_modules/pg": {
-      "version": "8.12.0",
-      "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz",
-      "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==",
-      "dependencies": {
-        "pg-connection-string": "^2.6.4",
-        "pg-pool": "^3.6.2",
-        "pg-protocol": "^1.6.1",
-        "pg-types": "^2.1.0",
-        "pgpass": "1.x"
-      },
-      "engines": {
-        "node": ">= 8.0.0"
-      },
-      "optionalDependencies": {
-        "pg-cloudflare": "^1.1.1"
-      },
-      "peerDependencies": {
-        "pg-native": ">=3.0.1"
-      },
-      "peerDependenciesMeta": {
-        "pg-native": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/pg-cloudflare": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
-      "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
-      "optional": true
-    },
-    "node_modules/pg-connection-string": {
-      "version": "2.6.4",
-      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz",
-      "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA=="
-    },
-    "node_modules/pg-int8": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
-      "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
-      "engines": {
-        "node": ">=4.0.0"
-      }
-    },
-    "node_modules/pg-numeric": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz",
-      "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==",
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/pg-pool": {
-      "version": "3.6.2",
-      "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz",
-      "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==",
-      "peerDependencies": {
-        "pg": ">=8.0"
-      }
-    },
-    "node_modules/pg-protocol": {
-      "version": "1.6.1",
-      "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz",
-      "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg=="
-    },
-    "node_modules/pg-types": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
-      "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
-      "dependencies": {
-        "pg-int8": "1.0.1",
-        "postgres-array": "~2.0.0",
-        "postgres-bytea": "~1.0.0",
-        "postgres-date": "~1.0.4",
-        "postgres-interval": "^1.1.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/pgpass": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
-      "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
-      "dependencies": {
-        "split2": "^4.1.0"
-      }
-    },
-    "node_modules/picocolors": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
-      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
-    },
-    "node_modules/picomatch": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
-      "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/jonschlinkert"
-      }
-    },
-    "node_modules/pify": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
-      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
-      "peer": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/pirates": {
-      "version": "4.0.6",
-      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
-      "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
-      "peer": true,
-      "engines": {
-        "node": ">= 6"
-      }
-    },
-    "node_modules/pluralize": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
-      "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/postcss": {
-      "version": "8.4.38",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
-      "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
-      "funding": [
-        {
-          "type": "opencollective",
-          "url": "https://opencollective.com/postcss/"
-        },
-        {
-          "type": "tidelift",
-          "url": "https://tidelift.com/funding/github/npm/postcss"
-        },
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/ai"
-        }
-      ],
-      "dependencies": {
-        "nanoid": "^3.3.7",
-        "picocolors": "^1.0.0",
-        "source-map-js": "^1.2.0"
-      },
-      "engines": {
-        "node": "^10 || ^12 || >=14"
-      }
-    },
-    "node_modules/postcss-import": {
-      "version": "15.1.0",
-      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
-      "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
-      "peer": true,
-      "dependencies": {
-        "postcss-value-parser": "^4.0.0",
-        "read-cache": "^1.0.0",
-        "resolve": "^1.1.7"
-      },
-      "engines": {
-        "node": ">=14.0.0"
-      },
-      "peerDependencies": {
-        "postcss": "^8.0.0"
-      }
-    },
-    "node_modules/postcss-js": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
-      "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
-      "peer": true,
-      "dependencies": {
-        "camelcase-css": "^2.0.1"
-      },
-      "engines": {
-        "node": "^12 || ^14 || >= 16"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/postcss/"
-      },
-      "peerDependencies": {
-        "postcss": "^8.4.21"
-      }
-    },
-    "node_modules/postcss-load-config": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
-      "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
-      "funding": [
-        {
-          "type": "opencollective",
-          "url": "https://opencollective.com/postcss/"
-        },
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/ai"
-        }
-      ],
-      "peer": true,
-      "dependencies": {
-        "lilconfig": "^3.0.0",
-        "yaml": "^2.3.4"
-      },
-      "engines": {
-        "node": ">= 14"
-      },
-      "peerDependencies": {
-        "postcss": ">=8.0.9",
-        "ts-node": ">=9.0.0"
-      },
-      "peerDependenciesMeta": {
-        "postcss": {
-          "optional": true
-        },
-        "ts-node": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/postcss-load-config/node_modules/lilconfig": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz",
-      "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==",
-      "peer": true,
-      "engines": {
-        "node": ">=14"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/antonk52"
-      }
-    },
-    "node_modules/postcss-nested": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
-      "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
-      "peer": true,
-      "dependencies": {
-        "postcss-selector-parser": "^6.0.11"
-      },
-      "engines": {
-        "node": ">=12.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/postcss/"
-      },
-      "peerDependencies": {
-        "postcss": "^8.2.14"
-      }
-    },
-    "node_modules/postcss-selector-parser": {
-      "version": "6.0.16",
-      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz",
-      "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==",
-      "peer": true,
-      "dependencies": {
-        "cssesc": "^3.0.0",
-        "util-deprecate": "^1.0.2"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/postcss-value-parser": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
-      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
-      "peer": true
-    },
-    "node_modules/postgres-array": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
-      "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/postgres-bytea": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
-      "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/postgres-date": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
-      "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/postgres-interval": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
-      "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
-      "dependencies": {
-        "xtend": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/postgres-range": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.3.tgz",
-      "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g=="
-    },
-    "node_modules/prelude-ls": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
-      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
-      "dev": true,
-      "engines": {
-        "node": ">= 0.8.0"
-      }
-    },
-    "node_modules/prettier": {
-      "version": "3.3.3",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
-      "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
-      "bin": {
-        "prettier": "bin/prettier.cjs"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "funding": {
-        "url": "https://github.com/prettier/prettier?sponsor=1"
-      }
-    },
-    "node_modules/prettier-linter-helpers": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
-      "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
-      "dev": true,
-      "dependencies": {
-        "fast-diff": "^1.1.2"
-      },
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
-    "node_modules/prettier-plugin-organize-imports": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
-      "integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
-      "dev": true,
-      "peerDependencies": {
-        "@vue/language-plugin-pug": "^2.0.24",
-        "prettier": ">=2.0",
-        "typescript": ">=2.9",
-        "vue-tsc": "^2.0.24"
-      },
-      "peerDependenciesMeta": {
-        "@vue/language-plugin-pug": {
-          "optional": true
-        },
-        "vue-tsc": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/prismjs": {
-      "version": "1.29.0",
-      "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
-      "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/process": {
-      "version": "0.11.10",
-      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
-      "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
-      "engines": {
-        "node": ">= 0.6.0"
-      }
-    },
-    "node_modules/process-nextick-args": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
-      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
-    },
-    "node_modules/proper-lockfile": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
-      "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
-      "dev": true,
-      "dependencies": {
-        "graceful-fs": "^4.2.4",
-        "retry": "^0.12.0",
-        "signal-exit": "^3.0.2"
-      }
-    },
-    "node_modules/proper-lockfile/node_modules/signal-exit": {
-      "version": "3.0.7",
-      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
-      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
-      "dev": true
-    },
-    "node_modules/properties-reader": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/properties-reader/-/properties-reader-2.3.0.tgz",
-      "integrity": "sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==",
-      "dev": true,
-      "dependencies": {
-        "mkdirp": "^1.0.4"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/steveukx/properties?sponsor=1"
-      }
-    },
-    "node_modules/properties-reader/node_modules/mkdirp": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
-      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
-      "dev": true,
-      "bin": {
-        "mkdirp": "bin/cmd.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/proto-list": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
-      "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="
-    },
-    "node_modules/protobufjs": {
-      "version": "7.3.2",
-      "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.2.tgz",
-      "integrity": "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==",
-      "hasInstallScript": true,
-      "dependencies": {
-        "@protobufjs/aspromise": "^1.1.2",
-        "@protobufjs/base64": "^1.1.2",
-        "@protobufjs/codegen": "^2.0.4",
-        "@protobufjs/eventemitter": "^1.1.0",
-        "@protobufjs/fetch": "^1.1.0",
-        "@protobufjs/float": "^1.0.2",
-        "@protobufjs/inquire": "^1.1.0",
-        "@protobufjs/path": "^1.1.2",
-        "@protobufjs/pool": "^1.1.0",
-        "@protobufjs/utf8": "^1.1.0",
-        "@types/node": ">=13.7.0",
-        "long": "^5.0.0"
-      },
-      "engines": {
-        "node": ">=12.0.0"
-      }
-    },
-    "node_modules/protocol-buffers-schema": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
-      "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="
-    },
-    "node_modules/proxy-addr": {
-      "version": "2.0.7",
-      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
-      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
-      "dependencies": {
-        "forwarded": "0.2.0",
-        "ipaddr.js": "1.9.1"
-      },
-      "engines": {
-        "node": ">= 0.10"
-      }
-    },
-    "node_modules/pump": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
-      "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
-      "dev": true,
-      "dependencies": {
-        "end-of-stream": "^1.1.0",
-        "once": "^1.3.1"
-      }
-    },
-    "node_modules/punycode": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
-      "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/qs": {
-      "version": "6.11.0",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
-      "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
-      "dependencies": {
-        "side-channel": "^1.0.4"
-      },
-      "engines": {
-        "node": ">=0.6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/queue-microtask": {
-      "version": "1.2.3",
-      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
-      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ]
-    },
-    "node_modules/queue-tick": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
-      "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="
-    },
-    "node_modules/railroad-diagrams": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
-      "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==",
-      "dev": true
-    },
-    "node_modules/randexp": {
-      "version": "0.4.6",
-      "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz",
-      "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==",
-      "dev": true,
-      "dependencies": {
-        "discontinuous-range": "1.0.0",
-        "ret": "~0.1.10"
-      },
-      "engines": {
-        "node": ">=0.12"
-      }
-    },
-    "node_modules/randombytes": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
-      "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
-      "dev": true,
-      "dependencies": {
-        "safe-buffer": "^5.1.0"
-      }
-    },
-    "node_modules/range-parser": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
-      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/raw-body": {
-      "version": "2.5.2",
-      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
-      "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
-      "dependencies": {
-        "bytes": "3.1.2",
-        "http-errors": "2.0.0",
-        "iconv-lite": "0.4.24",
-        "unpipe": "1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/react": {
-      "version": "18.3.1",
-      "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
-      "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
-      "dependencies": {
-        "loose-envify": "^1.1.0"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/react-dom": {
-      "version": "18.3.1",
-      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
-      "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
-      "peer": true,
-      "dependencies": {
-        "loose-envify": "^1.1.0",
-        "scheduler": "^0.23.2"
-      },
-      "peerDependencies": {
-        "react": "^18.3.1"
-      }
-    },
-    "node_modules/react-email": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/react-email/-/react-email-3.0.1.tgz",
-      "integrity": "sha512-G4Bkx2ULIScy/0Z8nnWywHt0W1iTkaYCdh9rWNuQ3eVZ6B3ttTUDE9uUy3VNQ8dtQbmG0cpt8+XmImw7mMBW6Q==",
-      "dependencies": {
-        "@babel/core": "7.24.5",
-        "@babel/parser": "7.24.5",
-        "chalk": "4.1.2",
-        "chokidar": "3.6.0",
-        "commander": "11.1.0",
-        "debounce": "2.0.0",
-        "esbuild": "0.19.11",
-        "glob": "10.3.4",
-        "log-symbols": "4.1.0",
-        "mime-types": "2.1.35",
-        "next": "14.2.3",
-        "normalize-path": "3.0.0",
-        "ora": "5.4.1",
-        "socket.io": "4.7.5"
-      },
-      "bin": {
-        "email": "dist/cli/index.js"
-      },
-      "engines": {
-        "node": ">=18.0.0"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/aix-ppc64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz",
-      "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==",
-      "cpu": [
-        "ppc64"
-      ],
-      "optional": true,
-      "os": [
-        "aix"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/android-arm": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz",
-      "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==",
-      "cpu": [
-        "arm"
-      ],
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/android-arm64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz",
-      "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==",
-      "cpu": [
-        "arm64"
-      ],
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/android-x64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz",
-      "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==",
-      "cpu": [
-        "x64"
-      ],
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/darwin-arm64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz",
-      "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==",
-      "cpu": [
-        "arm64"
-      ],
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/darwin-x64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz",
-      "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==",
-      "cpu": [
-        "x64"
-      ],
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz",
-      "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==",
-      "cpu": [
-        "arm64"
-      ],
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/freebsd-x64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz",
-      "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==",
-      "cpu": [
-        "x64"
-      ],
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/linux-arm": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz",
-      "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==",
-      "cpu": [
-        "arm"
-      ],
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/linux-arm64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz",
-      "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==",
-      "cpu": [
-        "arm64"
-      ],
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/linux-ia32": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz",
-      "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==",
-      "cpu": [
-        "ia32"
-      ],
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/linux-loong64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz",
-      "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==",
-      "cpu": [
-        "loong64"
-      ],
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/linux-mips64el": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz",
-      "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==",
-      "cpu": [
-        "mips64el"
-      ],
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/linux-ppc64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz",
-      "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==",
-      "cpu": [
-        "ppc64"
-      ],
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/linux-riscv64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz",
-      "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==",
-      "cpu": [
-        "riscv64"
-      ],
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/linux-s390x": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz",
-      "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==",
-      "cpu": [
-        "s390x"
-      ],
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/linux-x64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz",
-      "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==",
-      "cpu": [
-        "x64"
-      ],
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/netbsd-x64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz",
-      "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==",
-      "cpu": [
-        "x64"
-      ],
-      "optional": true,
-      "os": [
-        "netbsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/openbsd-x64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz",
-      "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==",
-      "cpu": [
-        "x64"
-      ],
-      "optional": true,
-      "os": [
-        "openbsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/sunos-x64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz",
-      "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==",
-      "cpu": [
-        "x64"
-      ],
-      "optional": true,
-      "os": [
-        "sunos"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/win32-arm64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz",
-      "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==",
-      "cpu": [
-        "arm64"
-      ],
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/win32-ia32": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz",
-      "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==",
-      "cpu": [
-        "ia32"
-      ],
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/@esbuild/win32-x64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz",
-      "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==",
-      "cpu": [
-        "x64"
-      ],
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/react-email/node_modules/brace-expansion": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
-      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-      "dependencies": {
-        "balanced-match": "^1.0.0"
-      }
-    },
-    "node_modules/react-email/node_modules/commander": {
-      "version": "11.1.0",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
-      "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
-      "engines": {
-        "node": ">=16"
-      }
-    },
-    "node_modules/react-email/node_modules/esbuild": {
       "version": "0.19.11",
       "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz",
       "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==",
       "hasInstallScript": true,
+      "license": "MIT",
       "bin": {
         "esbuild": "bin/esbuild"
       },
@@ -13175,10 +8090,4096 @@
         "@esbuild/win32-x64": "0.19.11"
       }
     },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/eslint": {
+      "version": "9.17.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz",
+      "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.2.0",
+        "@eslint-community/regexpp": "^4.12.1",
+        "@eslint/config-array": "^0.19.0",
+        "@eslint/core": "^0.9.0",
+        "@eslint/eslintrc": "^3.2.0",
+        "@eslint/js": "9.17.0",
+        "@eslint/plugin-kit": "^0.2.3",
+        "@humanfs/node": "^0.16.6",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@humanwhocodes/retry": "^0.4.1",
+        "@types/estree": "^1.0.6",
+        "@types/json-schema": "^7.0.15",
+        "ajv": "^6.12.4",
+        "chalk": "^4.0.0",
+        "cross-spawn": "^7.0.6",
+        "debug": "^4.3.2",
+        "escape-string-regexp": "^4.0.0",
+        "eslint-scope": "^8.2.0",
+        "eslint-visitor-keys": "^4.2.0",
+        "espree": "^10.3.0",
+        "esquery": "^1.5.0",
+        "esutils": "^2.0.2",
+        "fast-deep-equal": "^3.1.3",
+        "file-entry-cache": "^8.0.0",
+        "find-up": "^5.0.0",
+        "glob-parent": "^6.0.2",
+        "ignore": "^5.2.0",
+        "imurmurhash": "^0.1.4",
+        "is-glob": "^4.0.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "lodash.merge": "^4.6.2",
+        "minimatch": "^3.1.2",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.9.3"
+      },
+      "bin": {
+        "eslint": "bin/eslint.js"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://eslint.org/donate"
+      },
+      "peerDependencies": {
+        "jiti": "*"
+      },
+      "peerDependenciesMeta": {
+        "jiti": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/eslint-config-prettier": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
+      "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "eslint-config-prettier": "bin/cli.js"
+      },
+      "peerDependencies": {
+        "eslint": ">=7.0.0"
+      }
+    },
+    "node_modules/eslint-plugin-prettier": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz",
+      "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prettier-linter-helpers": "^1.0.0",
+        "synckit": "^0.9.1"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint-plugin-prettier"
+      },
+      "peerDependencies": {
+        "@types/eslint": ">=8.0.0",
+        "eslint": ">=8.0.0",
+        "eslint-config-prettier": "*",
+        "prettier": ">=3.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/eslint": {
+          "optional": true
+        },
+        "eslint-config-prettier": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/eslint-plugin-unicorn": {
+      "version": "56.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.1.tgz",
+      "integrity": "sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.24.7",
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "ci-info": "^4.0.0",
+        "clean-regexp": "^1.0.0",
+        "core-js-compat": "^3.38.1",
+        "esquery": "^1.6.0",
+        "globals": "^15.9.0",
+        "indent-string": "^4.0.0",
+        "is-builtin-module": "^3.2.1",
+        "jsesc": "^3.0.2",
+        "pluralize": "^8.0.0",
+        "read-pkg-up": "^7.0.1",
+        "regexp-tree": "^0.1.27",
+        "regjsparser": "^0.10.0",
+        "semver": "^7.6.3",
+        "strip-indent": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=18.18"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
+      },
+      "peerDependencies": {
+        "eslint": ">=8.56.0"
+      }
+    },
+    "node_modules/eslint-scope": {
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
+      "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint-visitor-keys": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint/node_modules/eslint-visitor-keys": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint/node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/espree": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
+      "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "acorn": "^8.14.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^4.2.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/espree/node_modules/eslint-visitor-keys": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/esprima": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "bin": {
+        "esparse": "bin/esparse.js",
+        "esvalidate": "bin/esvalidate.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/esquery": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+      "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "estraverse": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/event-target-shim": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+      "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/eventemitter2": {
+      "version": "6.4.9",
+      "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
+      "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==",
+      "license": "MIT"
+    },
+    "node_modules/events": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+      "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.x"
+      }
+    },
+    "node_modules/exiftool-vendored": {
+      "version": "28.8.0",
+      "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.8.0.tgz",
+      "integrity": "sha512-R7tirJLr9fWuH9JS/KFFLB+O7jNGKuPXGxREc6YybYangEudGb+X8ERsYXk9AifMiAWh/2agNfbgkbcQcF+MxA==",
+      "license": "MIT",
+      "dependencies": {
+        "@photostructure/tz-lookup": "^11.0.0",
+        "@types/luxon": "^3.4.2",
+        "batch-cluster": "^13.0.0",
+        "he": "^1.2.0",
+        "luxon": "^3.5.0"
+      },
+      "optionalDependencies": {
+        "exiftool-vendored.exe": "13.0.0",
+        "exiftool-vendored.pl": "13.0.1"
+      }
+    },
+    "node_modules/exiftool-vendored.exe": {
+      "version": "13.0.0",
+      "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-13.0.0.tgz",
+      "integrity": "sha512-4zAMuFGgxZkOoyQIzZMHv1HlvgyJK3AkNqjAgm8A8V0UmOZO7yv3pH49cDV1OduzFJqgs6yQ6eG4OGydhKtxlg==",
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/exiftool-vendored.pl": {
+      "version": "13.0.1",
+      "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-13.0.1.tgz",
+      "integrity": "sha512-+BRRzjselpWudKR0ltAW5SUt9T82D+gzQN8DdOQUgnSVWWp7oLCeTGBRptbQz+436Ihn/mPzmo/xnf0cv/Qw1A==",
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "!win32"
+      ]
+    },
+    "node_modules/expect-type": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz",
+      "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/express": {
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+      "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "~1.3.8",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.20.3",
+        "content-disposition": "0.5.4",
+        "content-type": "~1.0.4",
+        "cookie": "0.7.1",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "1.3.1",
+        "fresh": "0.5.2",
+        "http-errors": "2.0.0",
+        "merge-descriptors": "1.0.3",
+        "methods": "~1.1.2",
+        "on-finished": "2.4.1",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "0.1.12",
+        "proxy-addr": "~2.0.7",
+        "qs": "6.13.0",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.2.1",
+        "send": "0.19.0",
+        "serve-static": "1.16.2",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "type-is": "~1.6.18",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/express/node_modules/cookie": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+      "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/express/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "license": "MIT"
+    },
+    "node_modules/express/node_modules/path-to-regexp": {
+      "version": "0.1.12",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+      "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+      "license": "MIT"
+    },
+    "node_modules/extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+      "license": "MIT"
+    },
+    "node_modules/external-editor": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+      "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+      "license": "MIT",
+      "dependencies": {
+        "chardet": "^0.7.0",
+        "iconv-lite": "^0.4.24",
+        "tmp": "^0.0.33"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-diff": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
+      "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/fast-fifo": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+      "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
+      "license": "MIT"
+    },
+    "node_modules/fast-glob": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+      "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-safe-stringify": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
+      "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
+      "license": "MIT"
+    },
+    "node_modules/fast-uri": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz",
+      "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==",
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/fastq": {
+      "version": "1.17.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
+      "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
+      "license": "ISC",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/figures": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+      "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+      "license": "MIT",
+      "dependencies": {
+        "escape-string-regexp": "^1.0.5"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/figures/node_modules/escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/file-entry-cache": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+      "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flat-cache": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/file-source": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/file-source/-/file-source-0.6.1.tgz",
+      "integrity": "sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "stream-source": "0.3"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/finalhandler": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+      "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "on-finished": "2.4.1",
+        "parseurl": "~1.3.3",
+        "statuses": "2.0.1",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/finalhandler/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/finalhandler/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "license": "MIT"
+    },
+    "node_modules/find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/flat-cache": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+      "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flatted": "^3.2.9",
+        "keyv": "^4.5.4"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/flatted": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz",
+      "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/fluent-ffmpeg": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz",
+      "integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==",
+      "license": "MIT",
+      "dependencies": {
+        "async": "^0.2.9",
+        "which": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/fluent-ffmpeg/node_modules/async": {
+      "version": "0.2.10",
+      "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
+      "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
+    },
+    "node_modules/fluent-ffmpeg/node_modules/which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "which": "bin/which"
+      }
+    },
+    "node_modules/foreground-child": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
+      "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
+      "license": "ISC",
+      "dependencies": {
+        "cross-spawn": "^7.0.0",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin": {
+      "version": "9.0.2",
+      "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz",
+      "integrity": "sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.16.7",
+        "chalk": "^4.1.2",
+        "chokidar": "^3.5.3",
+        "cosmiconfig": "^8.2.0",
+        "deepmerge": "^4.2.2",
+        "fs-extra": "^10.0.0",
+        "memfs": "^3.4.1",
+        "minimatch": "^3.0.4",
+        "node-abort-controller": "^3.0.1",
+        "schema-utils": "^3.1.1",
+        "semver": "^7.3.5",
+        "tapable": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=12.13.0",
+        "yarn": ">=1.0.0"
+      },
+      "peerDependencies": {
+        "typescript": ">3.6.0",
+        "webpack": "^5.11.0"
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
+      "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/forwarded-parse": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz",
+      "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==",
+      "license": "MIT"
+    },
+    "node_modules/fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fs-constants": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+      "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fs-extra": {
+      "version": "10.1.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+      "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/fs-minipass": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+      "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/fs-minipass/node_modules/minipass": {
+      "version": "3.3.6",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+      "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/fs-monkey": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz",
+      "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==",
+      "dev": true,
+      "license": "Unlicense"
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "license": "ISC"
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gauge": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
+      "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
+      "deprecated": "This package is no longer supported.",
+      "license": "ISC",
+      "dependencies": {
+        "aproba": "^1.0.3 || ^2.0.0",
+        "color-support": "^1.1.2",
+        "console-control-strings": "^1.0.0",
+        "has-unicode": "^2.0.1",
+        "object-assign": "^4.1.1",
+        "signal-exit": "^3.0.0",
+        "string-width": "^4.2.3",
+        "strip-ansi": "^6.0.1",
+        "wide-align": "^1.1.2"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/gauge/node_modules/signal-exit": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+      "license": "ISC"
+    },
+    "node_modules/gaxios": {
+      "version": "6.7.1",
+      "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
+      "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "extend": "^3.0.2",
+        "https-proxy-agent": "^7.0.1",
+        "is-stream": "^2.0.0",
+        "node-fetch": "^2.6.9",
+        "uuid": "^9.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/gaxios/node_modules/agent-base": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
+      "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/gaxios/node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/gaxios/node_modules/uuid": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+      "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/gcp-metadata": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz",
+      "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "gaxios": "^6.0.0",
+        "json-bigint": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/geo-tz": {
+      "version": "8.1.2",
+      "resolved": "https://registry.npmjs.org/geo-tz/-/geo-tz-8.1.2.tgz",
+      "integrity": "sha512-S1udoP7MZ+CVu+7Iy/VayVNmEHTWgfJ52TjpfC2/4f+j0SB/ZXMjGrwZTqPMo6/O2m5lrGLCFCY0bkxUqiLN+g==",
+      "license": "MIT",
+      "dependencies": {
+        "@turf/boolean-point-in-polygon": "^7.1.0",
+        "@turf/helpers": "^7.1.0",
+        "geobuf": "^3.0.2",
+        "pbf": "^3.2.1"
+      },
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/evansiroky"
+      }
+    },
+    "node_modules/geobuf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/geobuf/-/geobuf-3.0.2.tgz",
+      "integrity": "sha512-ASgKwEAQQRnyNFHNvpd5uAwstbVYmiTW0Caw3fBb509tNTqXyAAPMyFs5NNihsLZhLxU1j/kjFhkhLWA9djuVg==",
+      "license": "ISC",
+      "dependencies": {
+        "concat-stream": "^2.0.0",
+        "pbf": "^3.2.1",
+        "shapefile": "~0.6.6"
+      },
+      "bin": {
+        "geobuf2json": "bin/geobuf2json",
+        "json2geobuf": "bin/json2geobuf",
+        "shp2geobuf": "bin/shp2geobuf"
+      }
+    },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "license": "ISC",
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz",
+      "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "dunder-proto": "^1.0.0",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0",
+        "function-bind": "^1.1.2",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-port": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz",
+      "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/get-stdin": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
+      "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/glob": {
+      "version": "10.4.5",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+      "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^1.11.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/glob-to-regexp": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+      "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+      "dev": true,
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/glob/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/glob/node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/globals": {
+      "version": "15.13.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-15.13.0.tgz",
+      "integrity": "sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/globrex": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
+      "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "license": "ISC"
+    },
+    "node_modules/graphemer": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/handlebars": {
+      "version": "4.7.8",
+      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
+      "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
+      "license": "MIT",
+      "dependencies": {
+        "minimist": "^1.2.5",
+        "neo-async": "^2.6.2",
+        "source-map": "^0.6.1",
+        "wordwrap": "^1.0.0"
+      },
+      "bin": {
+        "handlebars": "bin/handlebars"
+      },
+      "engines": {
+        "node": ">=0.4.7"
+      },
+      "optionalDependencies": {
+        "uglify-js": "^3.1.4"
+      }
+    },
+    "node_modules/handlebars/node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/has-own-prop": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz",
+      "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/has-property-descriptors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-define-property": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-unicode": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+      "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
+      "license": "ISC"
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "license": "MIT",
+      "bin": {
+        "he": "bin/he"
+      }
+    },
+    "node_modules/highlight.js": {
+      "version": "10.7.3",
+      "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
+      "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/hosted-git-info": {
+      "version": "2.8.9",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+      "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/html-escaper": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+      "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/html-to-text": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
+      "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==",
+      "license": "MIT",
+      "dependencies": {
+        "@selderee/plugin-htmlparser2": "^0.11.0",
+        "deepmerge": "^4.3.1",
+        "dom-serializer": "^2.0.0",
+        "htmlparser2": "^8.0.2",
+        "selderee": "^0.11.0"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/htmlparser2": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
+      "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
+      "funding": [
+        "https://github.com/fb55/htmlparser2?sponsor=1",
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fb55"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "domelementtype": "^2.3.0",
+        "domhandler": "^5.0.3",
+        "domutils": "^3.0.1",
+        "entities": "^4.4.0"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+      "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "2.0.0",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/i18n-iso-countries": {
+      "version": "7.13.0",
+      "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.13.0.tgz",
+      "integrity": "sha512-pVh4CjdgAHZswI98hzG+1BItQlsQfR+yGDsjDISoWIV/jHDAvCmSyZ5vj2YWwAjfVZ8/BhBDqWcFvuGOyHe4vg==",
+      "license": "MIT",
+      "dependencies": {
+        "diacritics": "1.3.0"
+      },
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/ieee754": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/ignore": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/import-fresh": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+      "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+      "license": "MIT",
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/import-in-the-middle": {
+      "version": "1.12.0",
+      "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.12.0.tgz",
+      "integrity": "sha512-yAgSE7GmtRcu4ZUSFX/4v69UGXwugFFSdIQJ14LHPOPPQrWv8Y7O9PHsw8Ovk7bKCLe4sjXMbZFqGFcLHpZ89w==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "acorn": "^8.8.2",
+        "acorn-import-attributes": "^1.9.5",
+        "cjs-module-lexer": "^1.2.2",
+        "module-details-from-path": "^1.0.3"
+      }
+    },
+    "node_modules/imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "node_modules/indent-string": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+      "license": "ISC",
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/inquirer": {
+      "version": "8.2.6",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz",
+      "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-escapes": "^4.2.1",
+        "chalk": "^4.1.1",
+        "cli-cursor": "^3.1.0",
+        "cli-width": "^3.0.0",
+        "external-editor": "^3.0.3",
+        "figures": "^3.0.0",
+        "lodash": "^4.17.21",
+        "mute-stream": "0.0.8",
+        "ora": "^5.4.1",
+        "run-async": "^2.4.0",
+        "rxjs": "^7.5.5",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0",
+        "through": "^2.3.6",
+        "wrap-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/ioredis": {
+      "version": "5.4.1",
+      "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz",
+      "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==",
+      "license": "MIT",
+      "dependencies": {
+        "@ioredis/commands": "^1.1.1",
+        "cluster-key-slot": "^1.1.0",
+        "debug": "^4.3.4",
+        "denque": "^2.1.0",
+        "lodash.defaults": "^4.2.0",
+        "lodash.isarguments": "^3.1.0",
+        "redis-errors": "^1.2.0",
+        "redis-parser": "^3.0.0",
+        "standard-as-callback": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=12.22.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/ioredis"
+      }
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+      "license": "MIT"
+    },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "license": "MIT",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-builtin-module": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
+      "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "builtin-modules": "^3.3.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-core-module": {
+      "version": "2.16.0",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.0.tgz",
+      "integrity": "sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==",
+      "license": "MIT",
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-interactive": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
+      "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-stream": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+      "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-unicode-supported": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+      "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+      "license": "MIT"
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "license": "ISC"
+    },
+    "node_modules/istanbul-lib-coverage": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+      "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/istanbul-lib-report": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+      "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "istanbul-lib-coverage": "^3.0.0",
+        "make-dir": "^4.0.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/istanbul-lib-report/node_modules/make-dir": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+      "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "semver": "^7.5.3"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/istanbul-lib-source-maps": {
+      "version": "5.0.6",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
+      "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@jridgewell/trace-mapping": "^0.3.23",
+        "debug": "^4.1.1",
+        "istanbul-lib-coverage": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/istanbul-reports": {
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
+      "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "html-escaper": "^2.0.0",
+        "istanbul-lib-report": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/iterare": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz",
+      "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/jackspeak": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+      "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
+    "node_modules/jest-worker": {
+      "version": "27.5.1",
+      "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
+      "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*",
+        "merge-stream": "^2.0.0",
+        "supports-color": "^8.0.0"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      }
+    },
+    "node_modules/jest-worker/node_modules/supports-color": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+      "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/supports-color?sponsor=1"
+      }
+    },
+    "node_modules/jiti": {
+      "version": "1.21.6",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
+      "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
+      "license": "MIT",
+      "peer": true,
+      "bin": {
+        "jiti": "bin/jiti.js"
+      }
+    },
+    "node_modules/joi": {
+      "version": "17.13.3",
+      "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
+      "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@hapi/hoek": "^9.3.0",
+        "@hapi/topo": "^5.1.0",
+        "@sideway/address": "^4.1.5",
+        "@sideway/formula": "^3.0.1",
+        "@sideway/pinpoint": "^2.0.0"
+      }
+    },
+    "node_modules/jose": {
+      "version": "4.15.9",
+      "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
+      "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/panva"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "license": "MIT"
+    },
+    "node_modules/js-yaml": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/json-bigint": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
+      "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "bignumber.js": "^9.0.0"
+      }
+    },
+    "node_modules/json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-parse-even-better-errors": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+      "license": "MIT"
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/jsonc-parser": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz",
+      "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/jsonfile": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+      "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "universalify": "^2.0.0"
+      },
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "node_modules/keyv": {
+      "version": "4.5.4",
+      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "json-buffer": "3.0.1"
+      }
+    },
+    "node_modules/lazystream": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
+      "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
+      "license": "MIT",
+      "dependencies": {
+        "readable-stream": "^2.0.5"
+      },
+      "engines": {
+        "node": ">= 0.6.3"
+      }
+    },
+    "node_modules/lazystream/node_modules/readable-stream": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+      "license": "MIT",
+      "dependencies": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "node_modules/lazystream/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "license": "MIT"
+    },
+    "node_modules/lazystream/node_modules/string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "node_modules/leac": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
+      "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://ko-fi.com/killymxi"
+      }
+    },
+    "node_modules/levn": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/libphonenumber-js": {
+      "version": "1.11.17",
+      "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.17.tgz",
+      "integrity": "sha512-Jr6v8thd5qRlOlc6CslSTzGzzQW03uiscab7KHQZX1Dfo4R6n6FDhZ0Hri6/X7edLIDv9gl4VMZXhxTjLnl0VQ==",
+      "license": "MIT"
+    },
+    "node_modules/lilconfig": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+      "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antonk52"
+      }
+    },
+    "node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+      "license": "MIT"
+    },
+    "node_modules/load-tsconfig": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz",
+      "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      }
+    },
+    "node_modules/loader-runner": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
+      "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.11.5"
+      }
+    },
+    "node_modules/locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.camelcase": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.defaults": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
+      "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isarguments": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
+      "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.merge": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/log-symbols": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+      "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+      "license": "MIT",
+      "dependencies": {
+        "chalk": "^4.1.0",
+        "is-unicode-supported": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/long": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
+      "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/loupe": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz",
+      "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/luxon": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
+      "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.8",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
+      "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.4.15"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/magicast": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
+      "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.25.4",
+        "@babel/types": "^7.25.4",
+        "source-map-js": "^1.2.0"
+      }
+    },
+    "node_modules/make-dir": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+      "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+      "license": "MIT",
+      "dependencies": {
+        "semver": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/make-dir/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/marked": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-7.0.4.tgz",
+      "integrity": "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==",
+      "license": "MIT",
+      "bin": {
+        "marked": "bin/marked.js"
+      },
+      "engines": {
+        "node": ">= 16"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz",
+      "integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/md-to-react-email": {
+      "version": "5.0.5",
+      "resolved": "https://registry.npmjs.org/md-to-react-email/-/md-to-react-email-5.0.5.tgz",
+      "integrity": "sha512-OvAXqwq57uOk+WZqFFNCMZz8yDp8BD3WazW1wAKHUrPbbdr89K9DWS6JXY09vd9xNdPNeurI8DU/X4flcfaD8A==",
+      "license": "MIT",
+      "dependencies": {
+        "marked": "7.0.4"
+      },
+      "peerDependencies": {
+        "react": "^18.0 || ^19.0"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/memfs": {
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz",
+      "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==",
+      "dev": true,
+      "license": "Unlicense",
+      "dependencies": {
+        "fs-monkey": "^1.0.4"
+      },
+      "engines": {
+        "node": ">= 4.0.0"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+      "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/merge-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/micromatch/node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "license": "MIT",
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mimic-fn": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/min-indent": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+      "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/minizlib": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+      "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+      "license": "MIT",
+      "dependencies": {
+        "minipass": "^3.0.0",
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/minizlib/node_modules/minipass": {
+      "version": "3.3.6",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+      "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/mkdirp": {
+      "version": "0.5.6",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+      "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+      "license": "MIT",
+      "dependencies": {
+        "minimist": "^1.2.6"
+      },
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      }
+    },
+    "node_modules/mkdirp-classic": {
+      "version": "0.5.3",
+      "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+      "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/mock-fs": {
+      "version": "5.4.1",
+      "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.4.1.tgz",
+      "integrity": "sha512-sz/Q8K1gXXXHR+qr0GZg2ysxCRr323kuN10O7CtQjraJsFDJ4SJ+0I5MzALz7aRp9lHk8Cc/YdsT95h9Ka1aFw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/module-details-from-path": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz",
+      "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==",
+      "license": "MIT"
+    },
+    "node_modules/moo": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
+      "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==",
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/mrmime": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
+      "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/msgpackr": {
+      "version": "1.11.2",
+      "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz",
+      "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==",
+      "license": "MIT",
+      "optionalDependencies": {
+        "msgpackr-extract": "^3.0.2"
+      }
+    },
+    "node_modules/msgpackr-extract": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
+      "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "node-gyp-build-optional-packages": "5.2.2"
+      },
+      "bin": {
+        "download-msgpackr-prebuilds": "bin/download-prebuilds.js"
+      },
+      "optionalDependencies": {
+        "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
+      }
+    },
+    "node_modules/multer": {
+      "version": "1.4.4-lts.1",
+      "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz",
+      "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==",
+      "license": "MIT",
+      "dependencies": {
+        "append-field": "^1.0.0",
+        "busboy": "^1.0.0",
+        "concat-stream": "^1.5.2",
+        "mkdirp": "^0.5.4",
+        "object-assign": "^4.1.1",
+        "type-is": "^1.6.4",
+        "xtend": "^4.0.0"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/multer/node_modules/concat-stream": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+      "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+      "engines": [
+        "node >= 0.8"
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "buffer-from": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^2.2.2",
+        "typedarray": "^0.0.6"
+      }
+    },
+    "node_modules/multer/node_modules/readable-stream": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+      "license": "MIT",
+      "dependencies": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "node_modules/multer/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "license": "MIT"
+    },
+    "node_modules/multer/node_modules/string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "node_modules/mute-stream": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+      "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
+      "license": "ISC"
+    },
+    "node_modules/mz": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+      "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+      "license": "MIT",
+      "dependencies": {
+        "any-promise": "^1.0.0",
+        "object-assign": "^4.0.1",
+        "thenify-all": "^1.0.0"
+      }
+    },
+    "node_modules/nan": {
+      "version": "2.22.0",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz",
+      "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.8",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+      "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nearley": {
+      "version": "2.20.1",
+      "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz",
+      "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "commander": "^2.19.0",
+        "moo": "^0.5.0",
+        "railroad-diagrams": "^1.0.0",
+        "randexp": "0.4.6"
+      },
+      "bin": {
+        "nearley-railroad": "bin/nearley-railroad.js",
+        "nearley-test": "bin/nearley-test.js",
+        "nearley-unparse": "bin/nearley-unparse.js",
+        "nearleyc": "bin/nearleyc.js"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://nearley.js.org/#give-to-nearley"
+      }
+    },
+    "node_modules/nearley/node_modules/commander": {
+      "version": "2.20.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/neo-async": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+      "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+      "license": "MIT"
+    },
+    "node_modules/nest-commander": {
+      "version": "3.15.0",
+      "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.15.0.tgz",
+      "integrity": "sha512-o9VEfFj/w2nm+hQi6fnkxL1GAFZW/KmuGcIE7/B/TX0gwm0QVy8svAF75EQm8wrDjcvWS7Cx/ArnkFn2C+iM2w==",
+      "license": "MIT",
+      "dependencies": {
+        "@fig/complete-commander": "^3.0.0",
+        "@golevelup/nestjs-discovery": "4.0.1",
+        "commander": "11.1.0",
+        "cosmiconfig": "8.3.6",
+        "inquirer": "8.2.6"
+      },
+      "peerDependencies": {
+        "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
+        "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0",
+        "@types/inquirer": "^8.1.3"
+      }
+    },
+    "node_modules/nest-commander/node_modules/@fig/complete-commander": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/@fig/complete-commander/-/complete-commander-3.2.0.tgz",
+      "integrity": "sha512-1Holl3XtRiANVKURZwgpjCnPuV4RsHp+XC0MhgvyAX/avQwj7F2HUItYOvGi/bXjJCkEzgBZmVfCr0HBA+q+Bw==",
+      "license": "MIT",
+      "dependencies": {
+        "prettier": "^3.2.5"
+      },
+      "peerDependencies": {
+        "commander": "^11.1.0"
+      }
+    },
+    "node_modules/nest-commander/node_modules/commander": {
+      "version": "11.1.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
+      "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/nestjs-cls": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-4.5.0.tgz",
+      "integrity": "sha512-oi3GNCc5pnsnVI5WJKMDwmg4NP+JyEw+edlwgepyUba5+RGGtJzpbVaaxXGW1iPbDuQde3/fA8Jdjq9j88BVcQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=16"
+      },
+      "peerDependencies": {
+        "@nestjs/common": "> 7.0.0 < 11",
+        "@nestjs/core": "> 7.0.0 < 11",
+        "reflect-metadata": "*",
+        "rxjs": ">= 7"
+      }
+    },
+    "node_modules/nestjs-otel": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/nestjs-otel/-/nestjs-otel-6.1.1.tgz",
+      "integrity": "sha512-hWuhDYkkaZrBXpHmi2v0jGqKa61uPqzu2YsVhww8/s+v9SaDILylR7ZdoOiygCQisgHG9rw5odP12GfsMS8cBA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/api": "^1.8.0",
+        "@opentelemetry/host-metrics": "^0.35.1",
+        "response-time": "^2.3.2"
+      },
+      "peerDependencies": {
+        "@nestjs/common": "^9.0.0 || ^10.0.0",
+        "@nestjs/core": "^9.0.0 || ^10.0.0"
+      }
+    },
+    "node_modules/next": {
+      "version": "15.0.4",
+      "resolved": "https://registry.npmjs.org/next/-/next-15.0.4.tgz",
+      "integrity": "sha512-nuy8FH6M1FG0lktGotamQDCXhh5hZ19Vo0ht1AOIQWrYJLP598TIUagKtvJrfJ5AGwB/WmDqkKaKhMpVifvGPA==",
+      "license": "MIT",
+      "dependencies": {
+        "@next/env": "15.0.4",
+        "@swc/counter": "0.1.3",
+        "@swc/helpers": "0.5.13",
+        "busboy": "1.6.0",
+        "caniuse-lite": "^1.0.30001579",
+        "postcss": "8.4.31",
+        "styled-jsx": "5.1.6"
+      },
+      "bin": {
+        "next": "dist/bin/next"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
+      },
+      "optionalDependencies": {
+        "@next/swc-darwin-arm64": "15.0.4",
+        "@next/swc-darwin-x64": "15.0.4",
+        "@next/swc-linux-arm64-gnu": "15.0.4",
+        "@next/swc-linux-arm64-musl": "15.0.4",
+        "@next/swc-linux-x64-gnu": "15.0.4",
+        "@next/swc-linux-x64-musl": "15.0.4",
+        "@next/swc-win32-arm64-msvc": "15.0.4",
+        "@next/swc-win32-x64-msvc": "15.0.4",
+        "sharp": "^0.33.5"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.1.0",
+        "@playwright/test": "^1.41.2",
+        "babel-plugin-react-compiler": "*",
+        "react": "^18.2.0 || 19.0.0-rc-66855b96-20241106 || ^19.0.0",
+        "react-dom": "^18.2.0 || 19.0.0-rc-66855b96-20241106 || ^19.0.0",
+        "sass": "^1.3.0"
+      },
+      "peerDependenciesMeta": {
+        "@opentelemetry/api": {
+          "optional": true
+        },
+        "@playwright/test": {
+          "optional": true
+        },
+        "babel-plugin-react-compiler": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/node-abort-controller": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
+      "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==",
+      "license": "MIT"
+    },
+    "node_modules/node-addon-api": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
+      "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
+      "license": "MIT"
+    },
+    "node_modules/node-emoji": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
+      "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "lodash": "^4.17.21"
+      }
+    },
+    "node_modules/node-fetch": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+      "license": "MIT",
+      "dependencies": {
+        "whatwg-url": "^5.0.0"
+      },
+      "engines": {
+        "node": "4.x || >=6.0.0"
+      },
+      "peerDependencies": {
+        "encoding": "^0.1.0"
+      },
+      "peerDependenciesMeta": {
+        "encoding": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/node-gyp-build-optional-packages": {
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
+      "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "detect-libc": "^2.0.1"
+      },
+      "bin": {
+        "node-gyp-build-optional-packages": "bin.js",
+        "node-gyp-build-optional-packages-optional": "optional.js",
+        "node-gyp-build-optional-packages-test": "build-test.js"
+      }
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.19",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+      "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+      "license": "MIT"
+    },
+    "node_modules/nodemailer": {
+      "version": "6.9.16",
+      "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz",
+      "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==",
+      "license": "MIT-0",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/nopt": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
+      "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
+      "license": "ISC",
+      "dependencies": {
+        "abbrev": "1"
+      },
+      "bin": {
+        "nopt": "bin/nopt.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/normalize-package-data": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+      "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "hosted-git-info": "^2.1.4",
+        "resolve": "^1.10.0",
+        "semver": "2 || 3 || 4 || 5",
+        "validate-npm-package-license": "^3.0.1"
+      }
+    },
+    "node_modules/normalize-package-data/node_modules/semver": {
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+      "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver"
+      }
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/notepack.io": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-3.0.1.tgz",
+      "integrity": "sha512-TKC/8zH5pXIAMVQio2TvVDTtPRX+DJPHDqjRbxogtFiByHyzKmy96RA0JtCQJ+WouyyL4A10xomQzgbUT+1jCg==",
+      "license": "MIT"
+    },
+    "node_modules/npmlog": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
+      "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
+      "deprecated": "This package is no longer supported.",
+      "license": "ISC",
+      "dependencies": {
+        "are-we-there-yet": "^2.0.0",
+        "console-control-strings": "^1.1.0",
+        "gauge": "^3.0.0",
+        "set-blocking": "^2.0.0"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-hash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+      "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.3",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
+      "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/oidc-token-hash": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",
+      "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==",
+      "license": "MIT",
+      "engines": {
+        "node": "^10.13.0 || >=12.0.0"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/on-headers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+      "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/onetime": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+      "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+      "license": "MIT",
+      "dependencies": {
+        "mimic-fn": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/openid-client": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz",
+      "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==",
+      "license": "MIT",
+      "dependencies": {
+        "jose": "^4.15.9",
+        "lru-cache": "^6.0.0",
+        "object-hash": "^2.2.0",
+        "oidc-token-hash": "^5.0.3"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/panva"
+      }
+    },
+    "node_modules/openid-client/node_modules/object-hash": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
+      "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/optionator": {
+      "version": "0.9.4",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+      "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0",
+        "word-wrap": "^1.2.5"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/ora": {
+      "version": "5.4.1",
+      "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
+      "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
+      "license": "MIT",
+      "dependencies": {
+        "bl": "^4.1.0",
+        "chalk": "^4.1.0",
+        "cli-cursor": "^3.1.0",
+        "cli-spinners": "^2.5.0",
+        "is-interactive": "^1.0.0",
+        "is-unicode-supported": "^0.1.0",
+        "log-symbols": "^4.1.0",
+        "strip-ansi": "^6.0.0",
+        "wcwidth": "^1.0.1"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/os-tmpdir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+      "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+      "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+      "license": "BlueOak-1.0.0"
+    },
+    "node_modules/parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "license": "MIT",
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/parse-json": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.0.0",
+        "error-ex": "^1.3.1",
+        "json-parse-even-better-errors": "^2.3.0",
+        "lines-and-columns": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parse5": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
+      "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
+      "license": "MIT"
+    },
+    "node_modules/parse5-htmlparser2-tree-adapter": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
+      "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==",
+      "license": "MIT",
+      "dependencies": {
+        "parse5": "^6.0.1"
+      }
+    },
+    "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
+      "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
+      "license": "MIT"
+    },
+    "node_modules/parseley": {
+      "version": "0.12.1",
+      "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
+      "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==",
+      "license": "MIT",
+      "dependencies": {
+        "leac": "^0.6.0",
+        "peberminta": "^0.9.0"
+      },
+      "funding": {
+        "url": "https://ko-fi.com/killymxi"
+      }
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "license": "MIT"
+    },
+    "node_modules/path-scurry": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^10.2.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/path-scurry/node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+      "license": "ISC"
+    },
+    "node_modules/path-source": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/path-source/-/path-source-0.1.3.tgz",
+      "integrity": "sha512-dWRHm5mIw5kw0cs3QZLNmpUWty48f5+5v9nWD2dw3Y0Hf+s01Ag8iJEWV0Sm0kocE8kK27DrIowha03e1YR+Qw==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "array-source": "0.0",
+        "file-source": "0.6"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz",
+      "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==",
+      "license": "MIT"
+    },
+    "node_modules/path-type": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/pathe": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+      "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/pathval": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
+      "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14.16"
+      }
+    },
+    "node_modules/pbf": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz",
+      "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "ieee754": "^1.1.12",
+        "resolve-protobuf-schema": "^2.1.0"
+      },
+      "bin": {
+        "pbf": "bin/pbf"
+      }
+    },
+    "node_modules/peberminta": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
+      "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://ko-fi.com/killymxi"
+      }
+    },
+    "node_modules/pg": {
+      "version": "8.13.1",
+      "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz",
+      "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==",
+      "license": "MIT",
+      "dependencies": {
+        "pg-connection-string": "^2.7.0",
+        "pg-pool": "^3.7.0",
+        "pg-protocol": "^1.7.0",
+        "pg-types": "^2.1.0",
+        "pgpass": "1.x"
+      },
+      "engines": {
+        "node": ">= 8.0.0"
+      },
+      "optionalDependencies": {
+        "pg-cloudflare": "^1.1.1"
+      },
+      "peerDependencies": {
+        "pg-native": ">=3.0.1"
+      },
+      "peerDependenciesMeta": {
+        "pg-native": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pg-cloudflare": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
+      "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/pg-connection-string": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz",
+      "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==",
+      "license": "MIT"
+    },
+    "node_modules/pg-int8": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+      "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/pg-pool": {
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz",
+      "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==",
+      "license": "MIT",
+      "peerDependencies": {
+        "pg": ">=8.0"
+      }
+    },
+    "node_modules/pg-protocol": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz",
+      "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==",
+      "license": "MIT"
+    },
+    "node_modules/pg-types": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+      "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+      "license": "MIT",
+      "dependencies": {
+        "pg-int8": "1.0.1",
+        "postgres-array": "~2.0.0",
+        "postgres-bytea": "~1.0.0",
+        "postgres-date": "~1.0.4",
+        "postgres-interval": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/pgpass": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+      "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+      "license": "MIT",
+      "dependencies": {
+        "split2": "^4.1.0"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+      "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/pirates": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
+      "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/pluralize": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
+      "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/pngjs": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
+      "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.19.0"
+      }
+    },
+    "node_modules/point-in-polygon-hao": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/point-in-polygon-hao/-/point-in-polygon-hao-1.2.3.tgz",
+      "integrity": "sha512-uZsWylGd8nthIYS8F7aSyM7Pot+4L/bgXheJcCNdRr4eLpsM/rMb3hIi5SqNxAVjUoDDao3QzCtdaVDzmeF9Cw==",
+      "license": "MIT",
+      "dependencies": {
+        "robust-predicates": "^3.0.2"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.4.31",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+      "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.6",
+        "picocolors": "^1.0.0",
+        "source-map-js": "^1.0.2"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postcss-import": {
+      "version": "15.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+      "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "postcss-value-parser": "^4.0.0",
+        "read-cache": "^1.0.0",
+        "resolve": "^1.1.7"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.0.0"
+      }
+    },
+    "node_modules/postcss-js": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
+      "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "camelcase-css": "^2.0.1"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >= 16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4.21"
+      }
+    },
+    "node_modules/postcss-load-config": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
+      "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "lilconfig": "^3.0.0",
+        "yaml": "^2.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      },
+      "peerDependencies": {
+        "postcss": ">=8.0.9",
+        "ts-node": ">=9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "postcss": {
+          "optional": true
+        },
+        "ts-node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/postcss-nested": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+      "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "postcss-selector-parser": "^6.1.1"
+      },
+      "engines": {
+        "node": ">=12.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2.14"
+      }
+    },
+    "node_modules/postcss-selector-parser": {
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+      "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postcss-value-parser": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/postgres-array": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+      "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postgres-bytea": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
+      "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/postgres-date": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+      "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/postgres-interval": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+      "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+      "license": "MIT",
+      "dependencies": {
+        "xtend": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/prelude-ls": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/prettier": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
+      "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
+      "license": "MIT",
+      "bin": {
+        "prettier": "bin/prettier.cjs"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
+    "node_modules/prettier-linter-helpers": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+      "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-diff": "^1.1.2"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/prettier-plugin-organize-imports": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz",
+      "integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "prettier": ">=2.0",
+        "typescript": ">=2.9",
+        "vue-tsc": "^2.1.0"
+      },
+      "peerDependenciesMeta": {
+        "vue-tsc": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/prismjs": {
+      "version": "1.29.0",
+      "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
+      "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+      "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
+    "node_modules/process-nextick-args": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+      "license": "MIT"
+    },
+    "node_modules/proper-lockfile": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
+      "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.4",
+        "retry": "^0.12.0",
+        "signal-exit": "^3.0.2"
+      }
+    },
+    "node_modules/proper-lockfile/node_modules/signal-exit": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/properties-reader": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/properties-reader/-/properties-reader-2.3.0.tgz",
+      "integrity": "sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mkdirp": "^1.0.4"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/steveukx/properties?sponsor=1"
+      }
+    },
+    "node_modules/properties-reader/node_modules/mkdirp": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/protobufjs": {
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
+      "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==",
+      "hasInstallScript": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@protobufjs/aspromise": "^1.1.2",
+        "@protobufjs/base64": "^1.1.2",
+        "@protobufjs/codegen": "^2.0.4",
+        "@protobufjs/eventemitter": "^1.1.0",
+        "@protobufjs/fetch": "^1.1.0",
+        "@protobufjs/float": "^1.0.2",
+        "@protobufjs/inquire": "^1.1.0",
+        "@protobufjs/path": "^1.1.2",
+        "@protobufjs/pool": "^1.1.0",
+        "@protobufjs/utf8": "^1.1.0",
+        "@types/node": ">=13.7.0",
+        "long": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/protocol-buffers-schema": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
+      "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==",
+      "license": "MIT"
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "license": "MIT",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/pump": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
+      "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.13.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+      "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.0.6"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/queue-tick": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
+      "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
+      "license": "MIT"
+    },
+    "node_modules/railroad-diagrams": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
+      "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==",
+      "dev": true,
+      "license": "CC0-1.0"
+    },
+    "node_modules/randexp": {
+      "version": "0.4.6",
+      "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz",
+      "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "discontinuous-range": "1.0.0",
+        "ret": "~0.1.10"
+      },
+      "engines": {
+        "node": ">=0.12"
+      }
+    },
+    "node_modules/randombytes": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+      "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+      "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "3.1.2",
+        "http-errors": "2.0.0",
+        "iconv-lite": "0.4.24",
+        "unpipe": "1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/react": {
+      "version": "19.0.0",
+      "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
+      "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-dom": {
+      "version": "19.0.0",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
+      "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "scheduler": "^0.25.0"
+      },
+      "peerDependencies": {
+        "react": "^19.0.0"
+      }
+    },
+    "node_modules/react-email": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/react-email/-/react-email-3.0.4.tgz",
+      "integrity": "sha512-nXdo9P3V+qYSW6m5yN3XpFGhHb/bflX86m0EDQEqDIgayprj6InmBJoBnMSIyC5EP4tPtoAljlclJns4lJG/MQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "7.24.5",
+        "@babel/parser": "7.24.5",
+        "chalk": "4.1.2",
+        "chokidar": "^4.0.1",
+        "commander": "11.1.0",
+        "debounce": "2.0.0",
+        "esbuild": "0.19.11",
+        "glob": "10.3.4",
+        "log-symbols": "4.1.0",
+        "mime-types": "2.1.35",
+        "next": "15.0.4",
+        "normalize-path": "3.0.0",
+        "ora": "5.4.1",
+        "socket.io": "4.8.0"
+      },
+      "bin": {
+        "email": "dist/cli/index.js"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/react-email/node_modules/@babel/parser": {
+      "version": "7.24.5",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz",
+      "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==",
+      "license": "MIT",
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/react-email/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/react-email/node_modules/chokidar": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.2.tgz",
+      "integrity": "sha512-/b57FK+bblSU+dfewfFe0rT1YjVDfOmeLQwCAuC+vwvgLkXboATqqmy+Ipux6JrF6L5joe5CBnFOw+gLWH6yKg==",
+      "license": "MIT",
+      "dependencies": {
+        "readdirp": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 14.16.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/react-email/node_modules/commander": {
+      "version": "11.1.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
+      "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/react-email/node_modules/debug": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/react-email/node_modules/glob": {
       "version": "10.3.4",
       "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz",
       "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==",
+      "license": "ISC",
       "dependencies": {
         "foreground-child": "^3.1.0",
         "jackspeak": "^2.0.3",
@@ -13196,10 +12197,29 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/react-email/node_modules/jackspeak": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
+      "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
     "node_modules/react-email/node_modules/minimatch": {
       "version": "9.0.5",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
       "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "license": "ISC",
       "dependencies": {
         "brace-expansion": "^2.0.1"
       },
@@ -13210,10 +12230,42 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/react-email/node_modules/readdirp": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
+      "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14.16.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/react-email/node_modules/socket.io": {
+      "version": "4.8.0",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz",
+      "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "~1.3.4",
+        "base64id": "~2.0.0",
+        "cors": "~2.8.5",
+        "debug": "~4.3.2",
+        "engine.io": "~6.6.0",
+        "socket.io-adapter": "~2.5.2",
+        "socket.io-parser": "~4.2.4"
+      },
+      "engines": {
+        "node": ">=10.2.0"
+      }
+    },
     "node_modules/react-promise-suspense": {
       "version": "0.3.4",
       "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz",
       "integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==",
+      "license": "MIT",
       "dependencies": {
         "fast-deep-equal": "^2.0.1"
       }
@@ -13221,12 +12273,14 @@
     "node_modules/react-promise-suspense/node_modules/fast-deep-equal": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
-      "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="
+      "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==",
+      "license": "MIT"
     },
     "node_modules/read-cache": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
       "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+      "license": "MIT",
       "peer": true,
       "dependencies": {
         "pify": "^2.3.0"
@@ -13237,6 +12291,7 @@
       "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
       "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/normalize-package-data": "^2.4.0",
         "normalize-package-data": "^2.5.0",
@@ -13252,6 +12307,7 @@
       "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz",
       "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "find-up": "^4.1.0",
         "read-pkg": "^5.2.0",
@@ -13269,6 +12325,7 @@
       "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
       "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "locate-path": "^5.0.0",
         "path-exists": "^4.0.0"
@@ -13282,6 +12339,7 @@
       "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
       "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "p-locate": "^4.1.0"
       },
@@ -13294,6 +12352,7 @@
       "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
       "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "p-try": "^2.0.0"
       },
@@ -13309,6 +12368,7 @@
       "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
       "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "p-limit": "^2.2.0"
       },
@@ -13321,6 +12381,7 @@
       "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
       "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
       "dev": true,
+      "license": "(MIT OR CC0-1.0)",
       "engines": {
         "node": ">=8"
       }
@@ -13330,27 +12391,56 @@
       "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
       "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
       "dev": true,
+      "license": "(MIT OR CC0-1.0)",
       "engines": {
         "node": ">=8"
       }
     },
     "node_modules/readable-stream": {
-      "version": "3.6.2",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
-      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
+      "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
+      "license": "MIT",
       "dependencies": {
-        "inherits": "^2.0.3",
-        "string_decoder": "^1.1.1",
-        "util-deprecate": "^1.0.1"
+        "abort-controller": "^3.0.0",
+        "buffer": "^6.0.3",
+        "events": "^3.3.0",
+        "process": "^0.11.10",
+        "string_decoder": "^1.3.0"
       },
       "engines": {
-        "node": ">= 6"
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      }
+    },
+    "node_modules/readable-stream/node_modules/buffer": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+      "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.2.1"
       }
     },
     "node_modules/readdir-glob": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
       "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
+      "license": "Apache-2.0",
       "dependencies": {
         "minimatch": "^5.1.0"
       }
@@ -13359,6 +12449,7 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
       "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "license": "MIT",
       "dependencies": {
         "balanced-match": "^1.0.0"
       }
@@ -13367,6 +12458,7 @@
       "version": "5.1.6",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
       "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+      "license": "ISC",
       "dependencies": {
         "brace-expansion": "^2.0.1"
       },
@@ -13378,6 +12470,7 @@
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
       "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "license": "MIT",
       "dependencies": {
         "picomatch": "^2.2.1"
       },
@@ -13389,6 +12482,7 @@
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
       "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "license": "MIT",
       "engines": {
         "node": ">=8.6"
       },
@@ -13400,6 +12494,7 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
       "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
+      "license": "MIT",
       "engines": {
         "node": ">=4"
       }
@@ -13408,6 +12503,7 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
       "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
+      "license": "MIT",
       "dependencies": {
         "redis-errors": "^1.0.0"
       },
@@ -13418,13 +12514,15 @@
     "node_modules/reflect-metadata": {
       "version": "0.2.2",
       "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
-      "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="
+      "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
+      "license": "Apache-2.0"
     },
     "node_modules/regexp-tree": {
       "version": "0.1.27",
       "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
       "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
       "dev": true,
+      "license": "MIT",
       "bin": {
         "regexp-tree": "bin/regexp-tree"
       }
@@ -13434,6 +12532,7 @@
       "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz",
       "integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==",
       "dev": true,
+      "license": "BSD-2-Clause",
       "dependencies": {
         "jsesc": "~0.5.0"
       },
@@ -13455,6 +12554,7 @@
       "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
       "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.10"
       }
@@ -13463,6 +12563,7 @@
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
       "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -13472,29 +12573,32 @@
       "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
       "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
     },
     "node_modules/require-in-the-middle": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz",
-      "integrity": "sha512-3TLx5TGyAY6AOqLBoXmHkNql0HIf2RGbuMgCDT2WO/uGVAPJs6h7Kl+bN6TIZGd9bWhWPwnDnTHGtW8Iu77sdw==",
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.4.0.tgz",
+      "integrity": "sha512-X34iHADNbNDfr6OTStIAHWSAvvKQRYgLO6duASaVf7J2VA3lvmNYboAHOuLC2huav1IwgZJtyEcJCKVzFxOSMQ==",
+      "license": "MIT",
       "dependencies": {
-        "debug": "^4.1.1",
+        "debug": "^4.3.5",
         "module-details-from-path": "^1.0.3",
-        "resolve": "^1.22.1"
+        "resolve": "^1.22.8"
       },
       "engines": {
         "node": ">=8.6.0"
       }
     },
     "node_modules/resolve": {
-      "version": "1.22.6",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz",
-      "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==",
+      "version": "1.22.9",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.9.tgz",
+      "integrity": "sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==",
+      "license": "MIT",
       "dependencies": {
-        "is-core-module": "^2.13.0",
+        "is-core-module": "^2.16.0",
         "path-parse": "^1.0.7",
         "supports-preserve-symlinks-flag": "^1.0.0"
       },
@@ -13509,6 +12613,7 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
       "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "license": "MIT",
       "engines": {
         "node": ">=4"
       }
@@ -13517,34 +12622,29 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
       "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
+      "license": "MIT",
       "dependencies": {
         "protocol-buffers-schema": "^3.3.1"
       }
     },
     "node_modules/response-time": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmjs.org/response-time/-/response-time-2.3.2.tgz",
-      "integrity": "sha512-MUIDaDQf+CVqflfTdQ5yam+aYCkXj1PY8fjlPDQ6ppxJlmgZb864pHtA750mayywNg8tx4rS7qH9JXd/OF+3gw==",
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/response-time/-/response-time-2.3.3.tgz",
+      "integrity": "sha512-SsjjOPHl/FfrTQNgmc5oen8Hr1Jxpn6LlHNXxCIFdYMHuK1kMeYMobb9XN3mvxaGQm3dbegqYFMX4+GDORfbWg==",
+      "license": "MIT",
       "dependencies": {
-        "depd": "~1.1.0",
+        "depd": "~2.0.0",
         "on-headers": "~1.0.1"
       },
       "engines": {
         "node": ">= 0.8.0"
       }
     },
-    "node_modules/response-time/node_modules/depd": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
-      "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
     "node_modules/restore-cursor": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
       "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+      "license": "MIT",
       "dependencies": {
         "onetime": "^5.1.0",
         "signal-exit": "^3.0.2"
@@ -13556,13 +12656,15 @@
     "node_modules/restore-cursor/node_modules/signal-exit": {
       "version": "3.0.7",
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
-      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
+      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+      "license": "ISC"
     },
     "node_modules/ret": {
       "version": "0.1.15",
       "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
       "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.12"
       }
@@ -13572,6 +12674,7 @@
       "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
       "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">= 4"
       }
@@ -13580,6 +12683,7 @@
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
       "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+      "license": "MIT",
       "engines": {
         "iojs": ">=1.0.0",
         "node": ">=0.10.0"
@@ -13590,6 +12694,7 @@
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz",
       "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "glob": "^11.0.0",
         "package-json-from-dist": "^1.0.0"
@@ -13609,6 +12714,7 @@
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
       "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "balanced-match": "^1.0.0"
       }
@@ -13618,6 +12724,7 @@
       "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz",
       "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "foreground-child": "^3.1.0",
         "jackspeak": "^4.0.1",
@@ -13637,10 +12744,11 @@
       }
     },
     "node_modules/rimraf/node_modules/jackspeak": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz",
-      "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==",
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz",
+      "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==",
       "dev": true,
+      "license": "BlueOak-1.0.0",
       "dependencies": {
         "@isaacs/cliui": "^8.0.2"
       },
@@ -13649,16 +12757,14 @@
       },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"
-      },
-      "optionalDependencies": {
-        "@pkgjs/parseargs": "^0.11.0"
       }
     },
     "node_modules/rimraf/node_modules/lru-cache": {
-      "version": "11.0.0",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz",
-      "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==",
+      "version": "11.0.2",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz",
+      "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==",
       "dev": true,
+      "license": "ISC",
       "engines": {
         "node": "20 || >=22"
       }
@@ -13668,6 +12774,7 @@
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
       "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "brace-expansion": "^2.0.1"
       },
@@ -13683,6 +12790,7 @@
       "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
       "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
       "dev": true,
+      "license": "BlueOak-1.0.0",
       "dependencies": {
         "lru-cache": "^11.0.0",
         "minipass": "^7.1.2"
@@ -13694,13 +12802,20 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/robust-predicates": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
+      "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==",
+      "license": "Unlicense"
+    },
     "node_modules/rollup": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.3.tgz",
-      "integrity": "sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz",
+      "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@types/estree": "1.0.5"
+        "@types/estree": "1.0.6"
       },
       "bin": {
         "rollup": "dist/bin/rollup"
@@ -13710,22 +12825,25 @@
         "npm": ">=8.0.0"
       },
       "optionalDependencies": {
-        "@rollup/rollup-android-arm-eabi": "4.14.3",
-        "@rollup/rollup-android-arm64": "4.14.3",
-        "@rollup/rollup-darwin-arm64": "4.14.3",
-        "@rollup/rollup-darwin-x64": "4.14.3",
-        "@rollup/rollup-linux-arm-gnueabihf": "4.14.3",
-        "@rollup/rollup-linux-arm-musleabihf": "4.14.3",
-        "@rollup/rollup-linux-arm64-gnu": "4.14.3",
-        "@rollup/rollup-linux-arm64-musl": "4.14.3",
-        "@rollup/rollup-linux-powerpc64le-gnu": "4.14.3",
-        "@rollup/rollup-linux-riscv64-gnu": "4.14.3",
-        "@rollup/rollup-linux-s390x-gnu": "4.14.3",
-        "@rollup/rollup-linux-x64-gnu": "4.14.3",
-        "@rollup/rollup-linux-x64-musl": "4.14.3",
-        "@rollup/rollup-win32-arm64-msvc": "4.14.3",
-        "@rollup/rollup-win32-ia32-msvc": "4.14.3",
-        "@rollup/rollup-win32-x64-msvc": "4.14.3",
+        "@rollup/rollup-android-arm-eabi": "4.28.1",
+        "@rollup/rollup-android-arm64": "4.28.1",
+        "@rollup/rollup-darwin-arm64": "4.28.1",
+        "@rollup/rollup-darwin-x64": "4.28.1",
+        "@rollup/rollup-freebsd-arm64": "4.28.1",
+        "@rollup/rollup-freebsd-x64": "4.28.1",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.28.1",
+        "@rollup/rollup-linux-arm-musleabihf": "4.28.1",
+        "@rollup/rollup-linux-arm64-gnu": "4.28.1",
+        "@rollup/rollup-linux-arm64-musl": "4.28.1",
+        "@rollup/rollup-linux-loongarch64-gnu": "4.28.1",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1",
+        "@rollup/rollup-linux-riscv64-gnu": "4.28.1",
+        "@rollup/rollup-linux-s390x-gnu": "4.28.1",
+        "@rollup/rollup-linux-x64-gnu": "4.28.1",
+        "@rollup/rollup-linux-x64-musl": "4.28.1",
+        "@rollup/rollup-win32-arm64-msvc": "4.28.1",
+        "@rollup/rollup-win32-ia32-msvc": "4.28.1",
+        "@rollup/rollup-win32-x64-msvc": "4.28.1",
         "fsevents": "~2.3.2"
       }
     },
@@ -13733,6 +12851,7 @@
       "version": "2.4.1",
       "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
       "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
+      "license": "MIT",
       "engines": {
         "node": ">=0.12.0"
       }
@@ -13755,6 +12874,7 @@
           "url": "https://feross.org/support"
         }
       ],
+      "license": "MIT",
       "dependencies": {
         "queue-microtask": "^1.2.2"
       }
@@ -13763,6 +12883,7 @@
       "version": "7.8.1",
       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
       "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+      "license": "Apache-2.0",
       "dependencies": {
         "tslib": "^2.1.0"
       }
@@ -13784,35 +12905,37 @@
           "type": "consulting",
           "url": "https://feross.org/support"
         }
-      ]
+      ],
+      "license": "MIT"
     },
     "node_modules/safer-buffer": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
-      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
     },
     "node_modules/sanitize-filename": {
       "version": "1.6.3",
       "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
       "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
+      "license": "WTFPL OR ISC",
       "dependencies": {
         "truncate-utf8-bytes": "^1.0.0"
       }
     },
     "node_modules/scheduler": {
-      "version": "0.23.2",
-      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
-      "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
-      "peer": true,
-      "dependencies": {
-        "loose-envify": "^1.1.0"
-      }
+      "version": "0.25.0",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
+      "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
+      "license": "MIT",
+      "peer": true
     },
     "node_modules/schema-utils": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
       "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/json-schema": "^7.0.8",
         "ajv": "^6.12.5",
@@ -13826,41 +12949,11 @@
         "url": "https://opencollective.com/webpack"
       }
     },
-    "node_modules/schema-utils/node_modules/ajv": {
-      "version": "6.12.6",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-      "dev": true,
-      "dependencies": {
-        "fast-deep-equal": "^3.1.1",
-        "fast-json-stable-stringify": "^2.0.0",
-        "json-schema-traverse": "^0.4.1",
-        "uri-js": "^4.2.2"
-      },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/epoberezkin"
-      }
-    },
-    "node_modules/schema-utils/node_modules/ajv-keywords": {
-      "version": "3.5.2",
-      "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
-      "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
-      "dev": true,
-      "peerDependencies": {
-        "ajv": "^6.9.1"
-      }
-    },
-    "node_modules/schema-utils/node_modules/json-schema-traverse": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-      "dev": true
-    },
     "node_modules/selderee": {
       "version": "0.11.0",
       "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
       "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==",
+      "license": "MIT",
       "dependencies": {
         "parseley": "^0.12.0"
       },
@@ -13872,6 +12965,7 @@
       "version": "7.6.3",
       "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
       "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+      "license": "ISC",
       "bin": {
         "semver": "bin/semver.js"
       },
@@ -13880,9 +12974,10 @@
       }
     },
     "node_modules/send": {
-      "version": "0.18.0",
-      "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
-      "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+      "version": "0.19.0",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+      "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+      "license": "MIT",
       "dependencies": {
         "debug": "2.6.9",
         "depd": "2.0.0",
@@ -13906,6 +13001,7 @@
       "version": "2.6.9",
       "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
       "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "license": "MIT",
       "dependencies": {
         "ms": "2.0.0"
       }
@@ -13913,31 +13009,38 @@
     "node_modules/send/node_modules/debug/node_modules/ms": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "license": "MIT"
     },
-    "node_modules/send/node_modules/ms": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
-      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+    "node_modules/send/node_modules/encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
     },
     "node_modules/serialize-javascript": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
       "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "randombytes": "^2.1.0"
       }
     },
     "node_modules/serve-static": {
-      "version": "1.15.0",
-      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
-      "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+      "version": "1.16.2",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+      "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+      "license": "MIT",
       "dependencies": {
-        "encodeurl": "~1.0.2",
+        "encodeurl": "~2.0.0",
         "escape-html": "~1.0.3",
         "parseurl": "~1.3.3",
-        "send": "0.18.0"
+        "send": "0.19.0"
       },
       "engines": {
         "node": ">= 0.8.0"
@@ -13946,19 +13049,22 @@
     "node_modules/set-blocking": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
-      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
+      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+      "license": "ISC"
     },
     "node_modules/set-function-length": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
-      "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==",
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+      "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+      "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "define-data-property": "^1.1.2",
+        "define-data-property": "^1.1.4",
         "es-errors": "^1.3.0",
         "function-bind": "^1.1.2",
-        "get-intrinsic": "^1.2.3",
+        "get-intrinsic": "^1.2.4",
         "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.1"
+        "has-property-descriptors": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -13967,12 +13073,14 @@
     "node_modules/setprototypeof": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
-      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "license": "ISC"
     },
     "node_modules/sha.js": {
       "version": "2.4.11",
       "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
       "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+      "license": "(MIT AND BSD-3-Clause)",
       "dependencies": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -13985,6 +13093,7 @@
       "version": "0.6.6",
       "resolved": "https://registry.npmjs.org/shapefile/-/shapefile-0.6.6.tgz",
       "integrity": "sha512-rLGSWeK2ufzCVx05wYd+xrWnOOdSV7xNUW5/XFgx3Bc02hBkpMlrd2F1dDII7/jhWzv0MSyBFh5uJIy9hLdfuw==",
+      "license": "BSD-3-Clause",
       "dependencies": {
         "array-source": "0.0",
         "commander": "2",
@@ -14001,13 +13110,15 @@
     "node_modules/shapefile/node_modules/commander": {
       "version": "2.20.3",
       "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
-      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
+      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+      "license": "MIT"
     },
     "node_modules/sharp": {
       "version": "0.33.5",
       "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
       "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
       "hasInstallScript": true,
+      "license": "Apache-2.0",
       "dependencies": {
         "color": "^4.2.3",
         "detect-libc": "^2.0.3",
@@ -14045,6 +13156,7 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
       "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "license": "MIT",
       "dependencies": {
         "shebang-regex": "^3.0.0"
       },
@@ -14056,6 +13168,7 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
       "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
@@ -14063,16 +13176,76 @@
     "node_modules/shimmer": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
-      "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="
+      "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==",
+      "license": "BSD-2-Clause"
     },
     "node_modules/side-channel": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
-      "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "license": "MIT",
       "dependencies": {
-        "call-bind": "^1.0.0",
-        "get-intrinsic": "^1.0.2",
-        "object-inspect": "^1.9.0"
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+      "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -14082,12 +13255,14 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
       "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
-      "dev": true
+      "dev": true,
+      "license": "ISC"
     },
     "node_modules/signal-exit": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
       "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "license": "ISC",
       "engines": {
         "node": ">=14"
       },
@@ -14099,6 +13274,7 @@
       "version": "0.2.2",
       "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
       "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
+      "license": "MIT",
       "dependencies": {
         "is-arrayish": "^0.3.1"
       }
@@ -14106,36 +13282,40 @@
     "node_modules/simple-swizzle/node_modules/is-arrayish": {
       "version": "0.3.2",
       "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
-      "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
+      "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
+      "license": "MIT"
     },
     "node_modules/sirv": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
-      "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz",
+      "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==",
+      "license": "MIT",
       "dependencies": {
         "@polka/url": "^1.0.0-next.24",
         "mrmime": "^2.0.0",
         "totalist": "^3.0.0"
       },
       "engines": {
-        "node": ">= 10"
+        "node": ">=18"
       }
     },
     "node_modules/slice-source": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/slice-source/-/slice-source-0.4.1.tgz",
-      "integrity": "sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg=="
+      "integrity": "sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/socket.io": {
-      "version": "4.7.5",
-      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz",
-      "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==",
+      "version": "4.8.1",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
+      "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
+      "license": "MIT",
       "dependencies": {
         "accepts": "~1.3.4",
         "base64id": "~2.0.0",
         "cors": "~2.8.5",
         "debug": "~4.3.2",
-        "engine.io": "~6.5.2",
+        "engine.io": "~6.6.0",
         "socket.io-adapter": "~2.5.2",
         "socket.io-parser": "~4.2.4"
       },
@@ -14147,27 +13327,25 @@
       "version": "2.5.5",
       "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
       "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
+      "license": "MIT",
       "dependencies": {
         "debug": "~4.3.4",
         "ws": "~8.17.1"
       }
     },
-    "node_modules/socket.io-adapter/node_modules/ws": {
-      "version": "8.17.1",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
-      "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
-      "engines": {
-        "node": ">=10.0.0"
+    "node_modules/socket.io-adapter/node_modules/debug": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
       },
-      "peerDependencies": {
-        "bufferutil": "^4.0.1",
-        "utf-8-validate": ">=5.0.2"
+      "engines": {
+        "node": ">=6.0"
       },
       "peerDependenciesMeta": {
-        "bufferutil": {
-          "optional": true
-        },
-        "utf-8-validate": {
+        "supports-color": {
           "optional": true
         }
       }
@@ -14176,6 +13354,7 @@
       "version": "4.2.4",
       "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
       "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+      "license": "MIT",
       "dependencies": {
         "@socket.io/component-emitter": "~3.1.0",
         "debug": "~4.3.1"
@@ -14184,19 +13363,55 @@
         "node": ">=10.0.0"
       }
     },
+    "node_modules/socket.io-parser/node_modules/debug": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/socket.io/node_modules/debug": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/source-map": {
       "version": "0.7.4",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
       "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "engines": {
         "node": ">= 8"
       }
     },
     "node_modules/source-map-js": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
-      "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -14206,6 +13421,7 @@
       "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
       "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "buffer-from": "^1.0.0",
         "source-map": "^0.6.0"
@@ -14216,6 +13432,7 @@
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
       "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -14225,52 +13442,59 @@
       "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
       "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
       "dev": true,
+      "license": "Apache-2.0",
       "dependencies": {
         "spdx-expression-parse": "^3.0.0",
         "spdx-license-ids": "^3.0.0"
       }
     },
     "node_modules/spdx-exceptions": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz",
-      "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==",
-      "dev": true
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+      "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
+      "dev": true,
+      "license": "CC-BY-3.0"
     },
     "node_modules/spdx-expression-parse": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
       "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "spdx-exceptions": "^2.1.0",
         "spdx-license-ids": "^3.0.0"
       }
     },
     "node_modules/spdx-license-ids": {
-      "version": "3.0.16",
-      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz",
-      "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==",
-      "dev": true
+      "version": "3.0.20",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz",
+      "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==",
+      "dev": true,
+      "license": "CC0-1.0"
     },
     "node_modules/split-ca": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
       "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==",
-      "dev": true
+      "dev": true,
+      "license": "ISC"
     },
     "node_modules/split2": {
       "version": "4.2.0",
       "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
       "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+      "license": "ISC",
       "engines": {
         "node": ">= 10.x"
       }
     },
     "node_modules/sql-formatter": {
-      "version": "15.4.1",
-      "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.4.1.tgz",
-      "integrity": "sha512-lw/G/emIJ+tVspOtOFzfD2YFFMN3MFPxGnbWl1DlJLB+fsX7X7zMqSRM1SLSn2YuaRJ0lTe7AMknHDqmIW1Y8w==",
+      "version": "15.4.6",
+      "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.4.6.tgz",
+      "integrity": "sha512-aH6kwvJpylljHqXe+zpie0Q5snL3uerDLLhjPEBjDCVK1NMRFq4nMJbuPJWYp08LaaaJJgBhShAdAfspcBYY0Q==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "argparse": "^2.0.1",
         "get-stdin": "=8.0.0",
@@ -14285,15 +13509,27 @@
       "resolved": "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz",
       "integrity": "sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/ssh2": "^0.5.48",
         "ssh2": "^1.4.0"
       }
     },
+    "node_modules/ssh-remote-port-forward/node_modules/@types/ssh2": {
+      "version": "0.5.52",
+      "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz",
+      "integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*",
+        "@types/ssh2-streams": "*"
+      }
+    },
     "node_modules/ssh2": {
-      "version": "1.14.0",
-      "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.14.0.tgz",
-      "integrity": "sha512-AqzD1UCqit8tbOKoj6ztDDi1ffJZ2rV2SwlgrVVrHPkV5vWqGJOVp5pmtj18PunkPJAuKQsnInyKV+/Nb2bUnA==",
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz",
+      "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==",
       "dev": true,
       "hasInstallScript": true,
       "dependencies": {
@@ -14304,39 +13540,44 @@
         "node": ">=10.16.0"
       },
       "optionalDependencies": {
-        "cpu-features": "~0.0.8",
-        "nan": "^2.17.0"
+        "cpu-features": "~0.0.10",
+        "nan": "^2.20.0"
       }
     },
     "node_modules/stackback": {
       "version": "0.0.2",
       "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
       "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/standard-as-callback": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
-      "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="
+      "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
+      "license": "MIT"
     },
     "node_modules/statuses": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
       "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+      "license": "MIT",
       "engines": {
         "node": ">= 0.8"
       }
     },
     "node_modules/std-env": {
-      "version": "3.7.0",
-      "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
-      "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==",
-      "dev": true
+      "version": "3.8.0",
+      "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz",
+      "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/stream-source": {
       "version": "0.3.5",
       "resolved": "https://registry.npmjs.org/stream-source/-/stream-source-0.3.5.tgz",
-      "integrity": "sha512-ZuEDP9sgjiAwUVoDModftG0JtYiLUV8K4ljYD1VyUMRWtbVf92474o4kuuul43iZ8t/hRuiDAx1dIJSvirrK/g=="
+      "integrity": "sha512-ZuEDP9sgjiAwUVoDModftG0JtYiLUV8K4ljYD1VyUMRWtbVf92474o4kuuul43iZ8t/hRuiDAx1dIJSvirrK/g==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/streamsearch": {
       "version": "1.1.0",
@@ -14347,9 +13588,10 @@
       }
     },
     "node_modules/streamx": {
-      "version": "2.18.0",
-      "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz",
-      "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==",
+      "version": "2.21.1",
+      "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.1.tgz",
+      "integrity": "sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw==",
+      "license": "MIT",
       "dependencies": {
         "fast-fifo": "^1.3.2",
         "queue-tick": "^1.0.1",
@@ -14363,6 +13605,7 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
       "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "license": "MIT",
       "dependencies": {
         "safe-buffer": "~5.2.0"
       }
@@ -14371,6 +13614,7 @@
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
       "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
       "dependencies": {
         "emoji-regex": "^8.0.0",
         "is-fullwidth-code-point": "^3.0.0",
@@ -14385,6 +13629,7 @@
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
       "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
       "dependencies": {
         "emoji-regex": "^8.0.0",
         "is-fullwidth-code-point": "^3.0.0",
@@ -14398,6 +13643,7 @@
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
       "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
       "dependencies": {
         "ansi-regex": "^5.0.1"
       },
@@ -14410,6 +13656,7 @@
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
       "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
       "dependencies": {
         "ansi-regex": "^5.0.1"
       },
@@ -14417,16 +13664,14 @@
         "node": ">=8"
       }
     },
-    "node_modules/strip-final-newline": {
+    "node_modules/strip-bom": {
       "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
-      "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+      "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": ">=4"
       }
     },
     "node_modules/strip-indent": {
@@ -14434,6 +13679,7 @@
       "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
       "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "min-indent": "^1.0.0"
       },
@@ -14446,6 +13692,7 @@
       "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
       "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       },
@@ -14454,9 +13701,10 @@
       }
     },
     "node_modules/styled-jsx": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
-      "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
+      "version": "5.1.6",
+      "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
+      "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
+      "license": "MIT",
       "dependencies": {
         "client-only": "0.0.1"
       },
@@ -14464,7 +13712,7 @@
         "node": ">= 12.0.0"
       },
       "peerDependencies": {
-        "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
+        "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
       },
       "peerDependenciesMeta": {
         "@babel/core": {
@@ -14479,6 +13727,7 @@
       "version": "3.35.0",
       "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
       "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
+      "license": "MIT",
       "peer": true,
       "dependencies": {
         "@jridgewell/gen-mapping": "^0.3.2",
@@ -14501,6 +13750,7 @@
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
       "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "license": "MIT",
       "dependencies": {
         "has-flag": "^4.0.0"
       },
@@ -14512,6 +13762,7 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
       "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "license": "MIT",
       "engines": {
         "node": ">= 0.4"
       },
@@ -14520,24 +13771,30 @@
       }
     },
     "node_modules/swagger-ui-dist": {
-      "version": "5.17.14",
-      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz",
-      "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw=="
+      "version": "5.18.2",
+      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz",
+      "integrity": "sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@scarf/scarf": "=1.4.0"
+      }
     },
     "node_modules/symbol-observable": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
       "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.10"
       }
     },
     "node_modules/synckit": {
-      "version": "0.9.1",
-      "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz",
-      "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==",
+      "version": "0.9.2",
+      "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz",
+      "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@pkgr/core": "^0.1.0",
         "tslib": "^2.6.2"
@@ -14550,9 +13807,10 @@
       }
     },
     "node_modules/systeminformation": {
-      "version": "5.22.0",
-      "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.22.0.tgz",
-      "integrity": "sha512-oAP80ymt8ssrAzjX8k3frbL7ys6AotqC35oikG6/SG15wBw+tG9nCk4oPaXIhEaAOAZ8XngxUv3ORq2IuR3r4Q==",
+      "version": "5.22.9",
+      "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.22.9.tgz",
+      "integrity": "sha512-qUWJhQ9JSBhdjzNUQywpvc0icxUAjMY3sZqUoS0GOtaJV9Ijq8s9zEP8Gaqmymn1dOefcICyPXK1L3kgKxlUpg==",
+      "license": "MIT",
       "os": [
         "darwin",
         "linux",
@@ -14575,33 +13833,34 @@
       }
     },
     "node_modules/tailwindcss": {
-      "version": "3.4.6",
-      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz",
-      "integrity": "sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==",
+      "version": "3.4.17",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
+      "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
+      "license": "MIT",
       "peer": true,
       "dependencies": {
         "@alloc/quick-lru": "^5.2.0",
         "arg": "^5.0.2",
-        "chokidar": "^3.5.3",
+        "chokidar": "^3.6.0",
         "didyoumean": "^1.2.2",
         "dlv": "^1.1.3",
-        "fast-glob": "^3.3.0",
+        "fast-glob": "^3.3.2",
         "glob-parent": "^6.0.2",
         "is-glob": "^4.0.3",
-        "jiti": "^1.21.0",
-        "lilconfig": "^2.1.0",
-        "micromatch": "^4.0.5",
+        "jiti": "^1.21.6",
+        "lilconfig": "^3.1.3",
+        "micromatch": "^4.0.8",
         "normalize-path": "^3.0.0",
         "object-hash": "^3.0.0",
-        "picocolors": "^1.0.0",
-        "postcss": "^8.4.23",
+        "picocolors": "^1.1.1",
+        "postcss": "^8.4.47",
         "postcss-import": "^15.1.0",
         "postcss-js": "^4.0.1",
-        "postcss-load-config": "^4.0.1",
-        "postcss-nested": "^6.0.1",
-        "postcss-selector-parser": "^6.0.11",
-        "resolve": "^1.22.2",
-        "sucrase": "^3.32.0"
+        "postcss-load-config": "^4.0.2",
+        "postcss-nested": "^6.2.0",
+        "postcss-selector-parser": "^6.1.2",
+        "resolve": "^1.22.8",
+        "sucrase": "^3.35.0"
       },
       "bin": {
         "tailwind": "lib/cli.js",
@@ -14612,9 +13871,10 @@
       }
     },
     "node_modules/tailwindcss-email-variants": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/tailwindcss-email-variants/-/tailwindcss-email-variants-3.0.1.tgz",
-      "integrity": "sha512-bRk4R2jnfaW7BBaL2kDgOdBl0SpVP/JPDE/yCkZb1n3YrPK9ZQyQGZoVX3OX06GxjMOrNO3wZACVdHJce7dm8w==",
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/tailwindcss-email-variants/-/tailwindcss-email-variants-3.0.3.tgz",
+      "integrity": "sha512-gchBYFNprLfRtmxnrglF4tayxFbv+hBV+3obXQycrBcluLj5CQF8uJsZH6ir0aIGQXfh5ukMdIkEgKOBzrBYxA==",
+      "license": "MIT",
       "engines": {
         "node": ">=18"
       },
@@ -14623,35 +13883,32 @@
       }
     },
     "node_modules/tailwindcss-mso": {
-      "version": "1.4.3",
-      "resolved": "https://registry.npmjs.org/tailwindcss-mso/-/tailwindcss-mso-1.4.3.tgz",
-      "integrity": "sha512-8YfZ4xnIComDrhoSr8FUwm7EGz1FkxsZy07Fs4Jm/JxHrFiubdiZjyxLuHMc3S8o02+U4fjRGHPOzoVXRus10A==",
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/tailwindcss-mso/-/tailwindcss-mso-1.4.4.tgz",
+      "integrity": "sha512-bSA7vLRhkaHjFhKkKNgr1RyOn2YhaEJ2hQQOCV7MgRtQOvYkqfAYMTSoZ2Z1YgCvOD02W4Tazsz+ym6FiPFIjQ==",
+      "license": "MIT",
       "peerDependencies": {
         "tailwindcss": ">=3.4.0"
       }
     },
     "node_modules/tailwindcss-preset-email": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/tailwindcss-preset-email/-/tailwindcss-preset-email-1.3.2.tgz",
-      "integrity": "sha512-kSPNZM5+tSi+uhCb4rk1XF9Q6zp8lhoNLCa3GQqe6gKmfI/nTqY8Y+5/DYNpwqhmUPCSHULlyI/LUCaF/q8sLg==",
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/tailwindcss-preset-email/-/tailwindcss-preset-email-1.3.3.tgz",
+      "integrity": "sha512-eB/qZrW9YPIDsGU2Spszbu+iBC3MdlZNqzkp85lBg1P/Dgxn7XWrfMt/46L6bTlmznr0zeMpgrMMOkuucJL7qQ==",
+      "license": "MIT",
       "dependencies": {
-        "tailwindcss-email-variants": "^3.0.0",
-        "tailwindcss-mso": "^1.4.3"
+        "tailwindcss-email-variants": "^3.0.2",
+        "tailwindcss-mso": "^1.4.4"
       },
       "peerDependencies": {
-        "tailwindcss": ">=3.4.6"
+        "tailwindcss": ">=3.4.15"
       }
     },
-    "node_modules/tailwindcss/node_modules/arg": {
-      "version": "5.0.2",
-      "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
-      "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
-      "peer": true
-    },
     "node_modules/tailwindcss/node_modules/glob-parent": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
       "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "license": "ISC",
       "peer": true,
       "dependencies": {
         "is-glob": "^4.0.3"
@@ -14660,19 +13917,50 @@
         "node": ">=10.13.0"
       }
     },
+    "node_modules/tailwindcss/node_modules/postcss": {
+      "version": "8.4.49",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+      "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "nanoid": "^3.3.7",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
     "node_modules/tapable": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
       "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
     },
     "node_modules/tar": {
-      "version": "6.2.0",
-      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
-      "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+      "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+      "license": "ISC",
       "dependencies": {
         "chownr": "^2.0.0",
         "fs-minipass": "^2.0.0",
@@ -14690,6 +13978,7 @@
       "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz",
       "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "pump": "^3.0.0",
         "tar-stream": "^3.1.5"
@@ -14700,9 +13989,10 @@
       }
     },
     "node_modules/tar-stream": {
-      "version": "3.1.6",
-      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz",
-      "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==",
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
+      "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
+      "license": "MIT",
       "dependencies": {
         "b4a": "^1.6.4",
         "fast-fifo": "^1.2.0",
@@ -14713,6 +14003,7 @@
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
       "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+      "license": "ISC",
       "engines": {
         "node": ">=8"
       }
@@ -14721,6 +14012,7 @@
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
       "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+      "license": "MIT",
       "bin": {
         "mkdirp": "bin/cmd.js"
       },
@@ -14728,16 +14020,12 @@
         "node": ">=10"
       }
     },
-    "node_modules/tar/node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
-    },
     "node_modules/terser": {
-      "version": "5.27.0",
-      "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz",
-      "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==",
+      "version": "5.37.0",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz",
+      "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==",
       "dev": true,
+      "license": "BSD-2-Clause",
       "dependencies": {
         "@jridgewell/source-map": "^0.3.3",
         "acorn": "^8.8.2",
@@ -14752,16 +14040,17 @@
       }
     },
     "node_modules/terser-webpack-plugin": {
-      "version": "5.3.10",
-      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
-      "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==",
+      "version": "5.3.11",
+      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz",
+      "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@jridgewell/trace-mapping": "^0.3.20",
+        "@jridgewell/trace-mapping": "^0.3.25",
         "jest-worker": "^27.4.5",
-        "schema-utils": "^3.1.1",
-        "serialize-javascript": "^6.0.1",
-        "terser": "^5.26.0"
+        "schema-utils": "^4.3.0",
+        "serialize-javascript": "^6.0.2",
+        "terser": "^5.31.1"
       },
       "engines": {
         "node": ">= 10.13.0"
@@ -14785,46 +14074,76 @@
         }
       }
     },
-    "node_modules/terser-webpack-plugin/node_modules/jest-worker": {
-      "version": "27.5.1",
-      "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
-      "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
+    "node_modules/terser-webpack-plugin/node_modules/ajv": {
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+      "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@types/node": "*",
-        "merge-stream": "^2.0.0",
-        "supports-color": "^8.0.0"
+        "fast-deep-equal": "^3.1.3",
+        "fast-uri": "^3.0.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+      "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3"
+      },
+      "peerDependencies": {
+        "ajv": "^8.8.2"
+      }
+    },
+    "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/terser-webpack-plugin/node_modules/schema-utils": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz",
+      "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/json-schema": "^7.0.9",
+        "ajv": "^8.9.0",
+        "ajv-formats": "^2.1.1",
+        "ajv-keywords": "^5.1.0"
       },
       "engines": {
         "node": ">= 10.13.0"
-      }
-    },
-    "node_modules/terser-webpack-plugin/node_modules/supports-color": {
-      "version": "8.1.1",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
-      "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
-      "dev": true,
-      "dependencies": {
-        "has-flag": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
       },
       "funding": {
-        "url": "https://github.com/chalk/supports-color?sponsor=1"
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
       }
     },
     "node_modules/terser/node_modules/commander": {
       "version": "2.20.3",
       "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
       "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/test-exclude": {
       "version": "7.0.1",
       "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz",
       "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "@istanbuljs/schema": "^0.1.2",
         "glob": "^10.4.1",
@@ -14839,6 +14158,7 @@
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
       "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "balanced-match": "^1.0.0"
       }
@@ -14848,6 +14168,7 @@
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
       "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "brace-expansion": "^2.0.1"
       },
@@ -14859,10 +14180,11 @@
       }
     },
     "node_modules/testcontainers": {
-      "version": "10.13.0",
-      "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.13.0.tgz",
-      "integrity": "sha512-SDblQvirbJw1ZpenxaAairGtAesw5XMOCHLbRhTTUBJtBkZJGce8Vx/I8lXQxWIM8HRXsg3HILTHGQvYo4x7wQ==",
+      "version": "10.16.0",
+      "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.16.0.tgz",
+      "integrity": "sha512-oxPLuOtrRWS11A+Yn0+zXB7GkmNarflWqmy6CQJk8KJ75LZs2/zlUXDpizTbPpCGtk4kE2EQYwFZjrE967F8Wg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@balena/dockerignore": "^1.0.2",
         "@types/dockerode": "^3.3.29",
@@ -14886,14 +14208,16 @@
       "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
       "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=14.14"
       }
     },
     "node_modules/text-decoder": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz",
-      "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==",
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
+      "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
+      "license": "Apache-2.0",
       "dependencies": {
         "b4a": "^1.6.4"
       }
@@ -14902,18 +14226,14 @@
       "version": "0.6.4",
       "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz",
       "integrity": "sha512-hJnc6Qg3dWoOMkqP53F0dzRIgtmsAge09kxUIqGrEUS4qr5rWLckGYaQAVr+opBrIMRErGgy6f5aPnyPpyGRfg==",
-      "deprecated": "no longer maintained"
-    },
-    "node_modules/text-table": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
-      "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
-      "dev": true
+      "deprecated": "no longer maintained",
+      "license": "Unlicense"
     },
     "node_modules/thenify": {
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
       "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+      "license": "MIT",
       "dependencies": {
         "any-promise": "^1.0.0"
       }
@@ -14922,6 +14242,7 @@
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
       "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+      "license": "MIT",
       "dependencies": {
         "thenify": ">= 3.1.0 < 4"
       },
@@ -14932,24 +14253,35 @@
     "node_modules/through": {
       "version": "2.3.8",
       "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
-      "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
+      "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
+      "license": "MIT"
     },
     "node_modules/thumbhash": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/thumbhash/-/thumbhash-0.1.1.tgz",
-      "integrity": "sha512-kH5pKeIIBPQXAOni2AiY/Cu/NKdkFREdpH+TLdM0g6WA7RriCv0kPLgP731ady67MhTAqrVG/4mnEeibVuCJcg=="
+      "integrity": "sha512-kH5pKeIIBPQXAOni2AiY/Cu/NKdkFREdpH+TLdM0g6WA7RriCv0kPLgP731ady67MhTAqrVG/4mnEeibVuCJcg==",
+      "license": "MIT"
     },
     "node_modules/tinybench": {
-      "version": "2.8.0",
-      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz",
-      "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==",
-      "dev": true
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+      "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tinyexec": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz",
+      "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/tinypool": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz",
-      "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz",
+      "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": "^18.0.0 || >=20.0.0"
       }
@@ -14959,15 +14291,17 @@
       "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
       "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=14.0.0"
       }
     },
     "node_modules/tinyspy": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz",
-      "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==",
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
+      "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=14.0.0"
       }
@@ -14976,6 +14310,7 @@
       "version": "0.0.33",
       "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
       "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+      "license": "MIT",
       "dependencies": {
         "os-tmpdir": "~1.0.2"
       },
@@ -14983,18 +14318,11 @@
         "node": ">=0.6.0"
       }
     },
-    "node_modules/to-fast-properties": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
-      "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/to-regex-range": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
       "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "license": "MIT",
       "dependencies": {
         "is-number": "^7.0.0"
       },
@@ -15006,6 +14334,7 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
       "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "license": "MIT",
       "engines": {
         "node": ">=0.6"
       }
@@ -15014,6 +14343,7 @@
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
       "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
@@ -15021,13 +14351,15 @@
     "node_modules/tr46": {
       "version": "0.0.3",
       "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
-      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+      "license": "MIT"
     },
     "node_modules/tree-kill": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
       "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
       "dev": true,
+      "license": "MIT",
       "bin": {
         "tree-kill": "cli.js"
       }
@@ -15036,15 +14368,17 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
       "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==",
+      "license": "WTFPL",
       "dependencies": {
         "utf8-byte-length": "^1.0.1"
       }
     },
     "node_modules/ts-api-utils": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
-      "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
+      "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=16"
       },
@@ -15056,57 +14390,15 @@
       "version": "0.1.13",
       "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
       "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+      "license": "Apache-2.0",
       "peer": true
     },
-    "node_modules/ts-node": {
-      "version": "10.9.2",
-      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
-      "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
-      "optional": true,
-      "peer": true,
-      "dependencies": {
-        "@cspotcode/source-map-support": "^0.8.0",
-        "@tsconfig/node10": "^1.0.7",
-        "@tsconfig/node12": "^1.0.7",
-        "@tsconfig/node14": "^1.0.0",
-        "@tsconfig/node16": "^1.0.2",
-        "acorn": "^8.4.1",
-        "acorn-walk": "^8.1.1",
-        "arg": "^4.1.0",
-        "create-require": "^1.1.0",
-        "diff": "^4.0.1",
-        "make-error": "^1.1.1",
-        "v8-compile-cache-lib": "^3.0.1",
-        "yn": "3.1.1"
-      },
-      "bin": {
-        "ts-node": "dist/bin.js",
-        "ts-node-cwd": "dist/bin-cwd.js",
-        "ts-node-esm": "dist/bin-esm.js",
-        "ts-node-script": "dist/bin-script.js",
-        "ts-node-transpile-only": "dist/bin-transpile.js",
-        "ts-script": "dist/bin-script-deprecated.js"
-      },
-      "peerDependencies": {
-        "@swc/core": ">=1.2.50",
-        "@swc/wasm": ">=1.2.50",
-        "@types/node": "*",
-        "typescript": ">=2.7"
-      },
-      "peerDependenciesMeta": {
-        "@swc/core": {
-          "optional": true
-        },
-        "@swc/wasm": {
-          "optional": true
-        }
-      }
-    },
     "node_modules/tsconfck": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.1.tgz",
-      "integrity": "sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==",
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.4.tgz",
+      "integrity": "sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==",
       "dev": true,
+      "license": "MIT",
       "bin": {
         "tsconfck": "bin/tsconfck.js"
       },
@@ -15127,6 +14419,7 @@
       "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
       "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "json5": "^2.2.2",
         "minimist": "^1.2.6",
@@ -15137,44 +14430,40 @@
       }
     },
     "node_modules/tsconfig-paths-webpack-plugin": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz",
-      "integrity": "sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz",
+      "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "chalk": "^4.1.0",
         "enhanced-resolve": "^5.7.0",
+        "tapable": "^2.2.1",
         "tsconfig-paths": "^4.1.2"
       },
       "engines": {
         "node": ">=10.13.0"
       }
     },
-    "node_modules/tsconfig-paths/node_modules/strip-bom": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
-      "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/tslib": {
-      "version": "2.6.3",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
-      "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "license": "0BSD"
     },
     "node_modules/tweetnacl": {
       "version": "0.14.5",
       "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
       "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
-      "dev": true
+      "dev": true,
+      "license": "Unlicense"
     },
     "node_modules/type-check": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
       "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "prelude-ls": "^1.2.1"
       },
@@ -15182,10 +14471,23 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/type-fest": {
+      "version": "0.21.3",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+      "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/type-is": {
       "version": "1.6.18",
       "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
       "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "license": "MIT",
       "dependencies": {
         "media-typer": "0.3.0",
         "mime-types": "~2.1.24"
@@ -15197,12 +14499,14 @@
     "node_modules/typedarray": {
       "version": "0.0.6",
       "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
-      "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
+      "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
+      "license": "MIT"
     },
     "node_modules/typeorm": {
       "version": "0.3.20",
       "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.20.tgz",
       "integrity": "sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==",
+      "license": "MIT",
       "dependencies": {
         "@sqltools/formatter": "^1.2.5",
         "app-root-path": "^3.1.0",
@@ -15322,28 +14626,17 @@
           "url": "https://feross.org/support"
         }
       ],
+      "license": "MIT",
       "dependencies": {
         "base64-js": "^1.3.1",
         "ieee754": "^1.2.1"
       }
     },
-    "node_modules/typeorm/node_modules/cliui": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
-      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
-      "dependencies": {
-        "string-width": "^4.2.0",
-        "strip-ansi": "^6.0.1",
-        "wrap-ansi": "^7.0.0"
-      },
-      "engines": {
-        "node": ">=12"
-      }
-    },
     "node_modules/typeorm/node_modules/mkdirp": {
       "version": "2.1.6",
       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz",
       "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==",
+      "license": "MIT",
       "bin": {
         "mkdirp": "dist/cjs/src/bin.js"
       },
@@ -15354,44 +14647,25 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
-    "node_modules/typeorm/node_modules/wrap-ansi": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
-      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-      "dependencies": {
-        "ansi-styles": "^4.0.0",
-        "string-width": "^4.1.0",
-        "strip-ansi": "^6.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
-      }
-    },
-    "node_modules/typeorm/node_modules/yargs": {
-      "version": "17.7.2",
-      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
-      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
-      "dependencies": {
-        "cliui": "^8.0.1",
-        "escalade": "^3.1.1",
-        "get-caller-file": "^2.0.5",
-        "require-directory": "^2.1.1",
-        "string-width": "^4.2.3",
-        "y18n": "^5.0.5",
-        "yargs-parser": "^21.1.1"
-      },
-      "engines": {
-        "node": ">=12"
+    "node_modules/typeorm/node_modules/uuid": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+      "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
       }
     },
     "node_modules/typescript": {
-      "version": "5.5.4",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
-      "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
+      "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
       "devOptional": true,
+      "license": "Apache-2.0",
       "bin": {
         "tsc": "bin/tsc",
         "tsserver": "bin/tsserver"
@@ -15401,9 +14675,9 @@
       }
     },
     "node_modules/ua-parser-js": {
-      "version": "1.0.38",
-      "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.38.tgz",
-      "integrity": "sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==",
+      "version": "1.0.39",
+      "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.39.tgz",
+      "integrity": "sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw==",
       "funding": [
         {
           "type": "opencollective",
@@ -15418,14 +14692,19 @@
           "url": "https://github.com/sponsors/faisalman"
         }
       ],
+      "license": "MIT",
+      "bin": {
+        "ua-parser-js": "script/cli.js"
+      },
       "engines": {
         "node": "*"
       }
     },
     "node_modules/uglify-js": {
-      "version": "3.17.4",
-      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
-      "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==",
+      "version": "3.19.3",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
+      "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
+      "license": "BSD-2-Clause",
       "optional": true,
       "bin": {
         "uglifyjs": "bin/uglifyjs"
@@ -15438,6 +14717,7 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz",
       "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==",
+      "license": "MIT",
       "dependencies": {
         "@lukeed/csprng": "^1.0.0"
       },
@@ -15449,6 +14729,7 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/uid2/-/uid2-1.0.0.tgz",
       "integrity": "sha512-+I6aJUv63YAcY9n4mQreLUt0d4lvwkkopDNmpomkAUz0fAkEMV9pRWxN0EjhW1YfRhcuyHg2v3mwddCDW1+LFQ==",
+      "license": "MIT",
       "engines": {
         "node": ">= 4.0.0"
       }
@@ -15458,6 +14739,7 @@
       "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
       "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@fastify/busboy": "^2.0.0"
       },
@@ -15466,15 +14748,17 @@
       }
     },
     "node_modules/undici-types": {
-      "version": "6.19.8",
-      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
-      "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
+      "version": "6.20.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
+      "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+      "license": "MIT"
     },
     "node_modules/universalify": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
-      "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+      "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">= 10.0.0"
       }
@@ -15483,20 +14767,20 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
       "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "license": "MIT",
       "engines": {
         "node": ">= 0.8"
       }
     },
     "node_modules/unplugin": {
-      "version": "1.11.0",
-      "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.11.0.tgz",
-      "integrity": "sha512-3r7VWZ/webh0SGgJScpWl2/MRCZK5d3ZYFcNaeci/GQ7Teop7zf0Nl2pUuz7G21BwPd9pcUPOC5KmJ2L3WgC5g==",
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.0.tgz",
+      "integrity": "sha512-5liCNPuJW8dqh3+DM6uNM2EI3MLLpCKp/KY+9pB5M2S2SR2qvvDHhKgBOaTWEbZTAws3CXfB0rKTIolWKL05VQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "acorn": "^8.11.3",
-        "chokidar": "^3.6.0",
-        "webpack-sources": "^3.2.3",
-        "webpack-virtual-modules": "^0.6.1"
+        "acorn": "^8.14.0",
+        "webpack-virtual-modules": "^0.6.2"
       },
       "engines": {
         "node": ">=14.0.0"
@@ -15507,6 +14791,7 @@
       "resolved": "https://registry.npmjs.org/unplugin-swc/-/unplugin-swc-1.5.1.tgz",
       "integrity": "sha512-/ZLrPNjChhGx3Z95pxJ4tQgfI6rWqukgYHKflrNB4zAV1izOQuDhkTn55JWeivpBxDCoK7M/TStb2aS/14PS/g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@rollup/pluginutils": "^5.1.0",
         "load-tsconfig": "^0.2.5",
@@ -15517,9 +14802,9 @@
       }
     },
     "node_modules/update-browserslist-db": {
-      "version": "1.0.13",
-      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
-      "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
+      "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
       "funding": [
         {
           "type": "opencollective",
@@ -15534,9 +14819,10 @@
           "url": "https://github.com/sponsors/ai"
         }
       ],
+      "license": "MIT",
       "dependencies": {
-        "escalade": "^3.1.1",
-        "picocolors": "^1.0.0"
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.0"
       },
       "bin": {
         "update-browserslist-db": "cli.js"
@@ -15550,24 +14836,28 @@
       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
       "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
       "dev": true,
+      "license": "BSD-2-Clause",
       "dependencies": {
         "punycode": "^2.1.0"
       }
     },
     "node_modules/utf8-byte-length": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz",
-      "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA=="
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
+      "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==",
+      "license": "(WTFPL OR MIT)"
     },
     "node_modules/util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
-      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "license": "MIT"
     },
     "node_modules/utils-merge": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
       "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+      "license": "MIT",
       "engines": {
         "node": ">= 0.4.0"
       }
@@ -15578,6 +14868,7 @@
       "integrity": "sha512-6S5mCapmzcxetOD/2UEjL0GF5e4+gB07Dh8qs63xylw5ay4XuyW6iQs70FOJo/puf10LCkvhp4jYMQSDUBYEFg==",
       "dev": true,
       "hasInstallScript": true,
+      "license": "MIT",
       "dependencies": {
         "@mapbox/node-pre-gyp": "^1.0.11",
         "node-addon-api": "^4.3.0"
@@ -15590,41 +14881,38 @@
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
       "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/uuid": {
-      "version": "9.0.1",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
-      "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+      "version": "11.0.3",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz",
+      "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==",
       "funding": [
         "https://github.com/sponsors/broofa",
         "https://github.com/sponsors/ctavan"
       ],
+      "license": "MIT",
       "bin": {
-        "uuid": "dist/bin/uuid"
+        "uuid": "dist/esm/bin/uuid"
       }
     },
-    "node_modules/v8-compile-cache-lib": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
-      "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
-      "optional": true,
-      "peer": true
-    },
     "node_modules/validate-npm-package-license": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
       "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
       "dev": true,
+      "license": "Apache-2.0",
       "dependencies": {
         "spdx-correct": "^3.0.0",
         "spdx-expression-parse": "^3.0.0"
       }
     },
     "node_modules/validator": {
-      "version": "13.11.0",
-      "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz",
-      "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==",
+      "version": "13.12.0",
+      "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
+      "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
+      "license": "MIT",
       "engines": {
         "node": ">= 0.10"
       }
@@ -15633,19 +14921,21 @@
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
       "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "license": "MIT",
       "engines": {
         "node": ">= 0.8"
       }
     },
     "node_modules/vite": {
-      "version": "5.2.11",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz",
-      "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==",
+      "version": "5.4.11",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
+      "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "esbuild": "^0.20.1",
-        "postcss": "^8.4.38",
-        "rollup": "^4.13.0"
+        "esbuild": "^0.21.3",
+        "postcss": "^8.4.43",
+        "rollup": "^4.20.0"
       },
       "bin": {
         "vite": "bin/vite.js"
@@ -15664,6 +14954,7 @@
         "less": "*",
         "lightningcss": "^1.21.0",
         "sass": "*",
+        "sass-embedded": "*",
         "stylus": "*",
         "sugarss": "*",
         "terser": "^5.4.0"
@@ -15681,6 +14972,9 @@
         "sass": {
           "optional": true
         },
+        "sass-embedded": {
+          "optional": true
+        },
         "stylus": {
           "optional": true
         },
@@ -15693,15 +14987,16 @@
       }
     },
     "node_modules/vite-node": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz",
-      "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==",
+      "version": "2.1.8",
+      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz",
+      "integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "cac": "^6.7.14",
-        "debug": "^4.3.5",
+        "debug": "^4.3.7",
+        "es-module-lexer": "^1.5.4",
         "pathe": "^1.1.2",
-        "tinyrainbow": "^1.2.0",
         "vite": "^5.0.0"
       },
       "bin": {
@@ -15715,10 +15010,11 @@
       }
     },
     "node_modules/vite-tsconfig-paths": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.0.1.tgz",
-      "integrity": "sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==",
+      "version": "5.1.4",
+      "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz",
+      "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "debug": "^4.1.1",
         "globrex": "^0.1.2",
@@ -15733,30 +15029,491 @@
         }
       }
     },
-    "node_modules/vitest": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz",
-      "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==",
+    "node_modules/vite/node_modules/@esbuild/aix-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+      "cpu": [
+        "ppc64"
+      ],
       "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/android-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+      "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/android-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+      "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/android-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+      "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/darwin-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+      "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/darwin-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+      "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+      "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/freebsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+      "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/linux-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+      "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/linux-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+      "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/linux-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+      "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/linux-loong64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+      "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/linux-mips64el": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+      "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/linux-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/linux-riscv64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+      "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/linux-s390x": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+      "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/linux-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+      "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/netbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/openbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/sunos-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+      "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/win32-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+      "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/win32-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+      "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/win32-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+      "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/esbuild": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+      "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.21.5",
+        "@esbuild/android-arm": "0.21.5",
+        "@esbuild/android-arm64": "0.21.5",
+        "@esbuild/android-x64": "0.21.5",
+        "@esbuild/darwin-arm64": "0.21.5",
+        "@esbuild/darwin-x64": "0.21.5",
+        "@esbuild/freebsd-arm64": "0.21.5",
+        "@esbuild/freebsd-x64": "0.21.5",
+        "@esbuild/linux-arm": "0.21.5",
+        "@esbuild/linux-arm64": "0.21.5",
+        "@esbuild/linux-ia32": "0.21.5",
+        "@esbuild/linux-loong64": "0.21.5",
+        "@esbuild/linux-mips64el": "0.21.5",
+        "@esbuild/linux-ppc64": "0.21.5",
+        "@esbuild/linux-riscv64": "0.21.5",
+        "@esbuild/linux-s390x": "0.21.5",
+        "@esbuild/linux-x64": "0.21.5",
+        "@esbuild/netbsd-x64": "0.21.5",
+        "@esbuild/openbsd-x64": "0.21.5",
+        "@esbuild/sunos-x64": "0.21.5",
+        "@esbuild/win32-arm64": "0.21.5",
+        "@esbuild/win32-ia32": "0.21.5",
+        "@esbuild/win32-x64": "0.21.5"
+      }
+    },
+    "node_modules/vite/node_modules/postcss": {
+      "version": "8.4.49",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+      "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
       "dependencies": {
-        "@ampproject/remapping": "^2.3.0",
-        "@vitest/expect": "2.0.5",
-        "@vitest/pretty-format": "^2.0.5",
-        "@vitest/runner": "2.0.5",
-        "@vitest/snapshot": "2.0.5",
-        "@vitest/spy": "2.0.5",
-        "@vitest/utils": "2.0.5",
-        "chai": "^5.1.1",
-        "debug": "^4.3.5",
-        "execa": "^8.0.1",
-        "magic-string": "^0.30.10",
+        "nanoid": "^3.3.7",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/vitest": {
+      "version": "2.1.8",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz",
+      "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/expect": "2.1.8",
+        "@vitest/mocker": "2.1.8",
+        "@vitest/pretty-format": "^2.1.8",
+        "@vitest/runner": "2.1.8",
+        "@vitest/snapshot": "2.1.8",
+        "@vitest/spy": "2.1.8",
+        "@vitest/utils": "2.1.8",
+        "chai": "^5.1.2",
+        "debug": "^4.3.7",
+        "expect-type": "^1.1.0",
+        "magic-string": "^0.30.12",
         "pathe": "^1.1.2",
-        "std-env": "^3.7.0",
-        "tinybench": "^2.8.0",
-        "tinypool": "^1.0.0",
+        "std-env": "^3.8.0",
+        "tinybench": "^2.9.0",
+        "tinyexec": "^0.3.1",
+        "tinypool": "^1.0.1",
         "tinyrainbow": "^1.2.0",
         "vite": "^5.0.0",
-        "vite-node": "2.0.5",
+        "vite-node": "2.1.8",
         "why-is-node-running": "^2.3.0"
       },
       "bin": {
@@ -15771,8 +15528,8 @@
       "peerDependencies": {
         "@edge-runtime/vm": "*",
         "@types/node": "^18.0.0 || >=20.0.0",
-        "@vitest/browser": "2.0.5",
-        "@vitest/ui": "2.0.5",
+        "@vitest/browser": "2.1.8",
+        "@vitest/ui": "2.1.8",
         "happy-dom": "*",
         "jsdom": "*"
       },
@@ -15798,19 +15555,21 @@
       }
     },
     "node_modules/vitest/node_modules/magic-string": {
-      "version": "0.30.11",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
-      "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
+      "version": "0.30.17",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+      "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.5.0"
       }
     },
     "node_modules/watchpack": {
-      "version": "2.4.1",
-      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
-      "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==",
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
+      "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "glob-to-regexp": "^0.4.1",
         "graceful-fs": "^4.1.2"
@@ -15823,6 +15582,7 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
       "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
+      "license": "MIT",
       "dependencies": {
         "defaults": "^1.0.3"
       }
@@ -15830,24 +15590,25 @@
     "node_modules/webidl-conversions": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
-      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+      "license": "BSD-2-Clause"
     },
     "node_modules/webpack": {
-      "version": "5.93.0",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz",
-      "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==",
+      "version": "5.97.1",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz",
+      "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@types/eslint-scope": "^3.7.3",
-        "@types/estree": "^1.0.5",
-        "@webassemblyjs/ast": "^1.12.1",
-        "@webassemblyjs/wasm-edit": "^1.12.1",
-        "@webassemblyjs/wasm-parser": "^1.12.1",
-        "acorn": "^8.7.1",
-        "acorn-import-attributes": "^1.9.5",
-        "browserslist": "^4.21.10",
+        "@types/eslint-scope": "^3.7.7",
+        "@types/estree": "^1.0.6",
+        "@webassemblyjs/ast": "^1.14.1",
+        "@webassemblyjs/wasm-edit": "^1.14.1",
+        "@webassemblyjs/wasm-parser": "^1.14.1",
+        "acorn": "^8.14.0",
+        "browserslist": "^4.24.0",
         "chrome-trace-event": "^1.0.2",
-        "enhanced-resolve": "^5.17.0",
+        "enhanced-resolve": "^5.17.1",
         "es-module-lexer": "^1.2.1",
         "eslint-scope": "5.1.1",
         "events": "^3.2.0",
@@ -15884,6 +15645,7 @@
       "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz",
       "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
@@ -15893,21 +15655,24 @@
       "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
       "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=10.13.0"
       }
     },
     "node_modules/webpack-virtual-modules": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.1.tgz",
-      "integrity": "sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg==",
-      "dev": true
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
+      "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/webpack/node_modules/eslint-scope": {
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
       "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
       "dev": true,
+      "license": "BSD-2-Clause",
       "dependencies": {
         "esrecurse": "^4.3.0",
         "estraverse": "^4.1.1"
@@ -15921,6 +15686,7 @@
       "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
       "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
       "dev": true,
+      "license": "BSD-2-Clause",
       "engines": {
         "node": ">=4.0"
       }
@@ -15929,6 +15695,7 @@
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
       "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+      "license": "MIT",
       "dependencies": {
         "tr46": "~0.0.3",
         "webidl-conversions": "^3.0.0"
@@ -15938,6 +15705,7 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
       "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "license": "ISC",
       "dependencies": {
         "isexe": "^2.0.0"
       },
@@ -15953,6 +15721,7 @@
       "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
       "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "siginfo": "^2.0.0",
         "stackback": "0.0.2"
@@ -15968,19 +15737,32 @@
       "version": "1.1.5",
       "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
       "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
+      "license": "ISC",
       "dependencies": {
         "string-width": "^1.0.2 || 2 || 3 || 4"
       }
     },
+    "node_modules/word-wrap": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+      "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/wordwrap": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
-      "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="
+      "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
+      "license": "MIT"
     },
     "node_modules/wrap-ansi": {
       "version": "6.2.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
       "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+      "license": "MIT",
       "dependencies": {
         "ansi-styles": "^4.0.0",
         "string-width": "^4.1.0",
@@ -15995,6 +15777,7 @@
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
       "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "license": "MIT",
       "dependencies": {
         "ansi-styles": "^4.0.0",
         "string-width": "^4.1.0",
@@ -16010,18 +15793,20 @@
     "node_modules/wrappy": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "license": "ISC"
     },
     "node_modules/ws": {
-      "version": "8.11.0",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
-      "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+      "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+      "license": "MIT",
       "engines": {
         "node": ">=10.0.0"
       },
       "peerDependencies": {
         "bufferutil": "^4.0.1",
-        "utf-8-validate": "^5.0.2"
+        "utf-8-validate": ">=5.0.2"
       },
       "peerDependenciesMeta": {
         "bufferutil": {
@@ -16036,6 +15821,7 @@
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
       "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "license": "MIT",
       "engines": {
         "node": ">=0.4"
       }
@@ -16044,71 +15830,62 @@
       "version": "5.0.8",
       "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
       "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+      "license": "ISC",
       "engines": {
         "node": ">=10"
       }
     },
     "node_modules/yallist": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
-      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+      "license": "ISC"
     },
     "node_modules/yaml": {
-      "version": "2.3.4",
-      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz",
-      "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==",
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz",
+      "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==",
+      "license": "ISC",
+      "bin": {
+        "yaml": "bin.mjs"
+      },
       "engines": {
         "node": ">= 14"
       }
     },
     "node_modules/yargs": {
-      "version": "16.2.0",
-      "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
-      "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+      "version": "17.7.2",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+      "license": "MIT",
       "dependencies": {
-        "cliui": "^7.0.2",
+        "cliui": "^8.0.1",
         "escalade": "^3.1.1",
         "get-caller-file": "^2.0.5",
         "require-directory": "^2.1.1",
-        "string-width": "^4.2.0",
+        "string-width": "^4.2.3",
         "y18n": "^5.0.5",
-        "yargs-parser": "^20.2.2"
+        "yargs-parser": "^21.1.1"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">=12"
       }
     },
     "node_modules/yargs-parser": {
       "version": "21.1.1",
       "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
       "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+      "license": "ISC",
       "engines": {
         "node": ">=12"
       }
     },
-    "node_modules/yargs/node_modules/yargs-parser": {
-      "version": "20.2.9",
-      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
-      "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/yn": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
-      "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
-      "optional": true,
-      "peer": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
     "node_modules/yocto-queue": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
       "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=10"
       },
@@ -16120,6 +15897,7 @@
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
       "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==",
+      "license": "MIT",
       "dependencies": {
         "archiver-utils": "^5.0.0",
         "compress-commons": "^6.0.2",
@@ -16128,11012 +15906,6 @@
       "engines": {
         "node": ">= 14"
       }
-    },
-    "node_modules/zip-stream/node_modules/buffer": {
-      "version": "6.0.3",
-      "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
-      "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "dependencies": {
-        "base64-js": "^1.3.1",
-        "ieee754": "^1.2.1"
-      }
-    },
-    "node_modules/zip-stream/node_modules/readable-stream": {
-      "version": "4.5.2",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
-      "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
-      "dependencies": {
-        "abort-controller": "^3.0.0",
-        "buffer": "^6.0.3",
-        "events": "^3.3.0",
-        "process": "^0.11.10",
-        "string_decoder": "^1.3.0"
-      },
-      "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      }
-    }
-  },
-  "dependencies": {
-    "@aashutoshrathi/word-wrap": {
-      "version": "1.2.6",
-      "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
-      "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
-      "dev": true
-    },
-    "@alloc/quick-lru": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
-      "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
-      "peer": true
-    },
-    "@ampproject/remapping": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
-      "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
-      "requires": {
-        "@jridgewell/gen-mapping": "^0.3.5",
-        "@jridgewell/trace-mapping": "^0.3.24"
-      }
-    },
-    "@angular-devkit/core": {
-      "version": "17.3.8",
-      "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.8.tgz",
-      "integrity": "sha512-Q8q0voCGudbdCgJ7lXdnyaxKHbNQBARH68zPQV72WT8NWy+Gw/tys870i6L58NWbBaCJEUcIj/kb6KoakSRu+Q==",
-      "dev": true,
-      "requires": {
-        "ajv": "8.12.0",
-        "ajv-formats": "2.1.1",
-        "jsonc-parser": "3.2.1",
-        "picomatch": "4.0.1",
-        "rxjs": "7.8.1",
-        "source-map": "0.7.4"
-      },
-      "dependencies": {
-        "picomatch": {
-          "version": "4.0.1",
-          "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz",
-          "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==",
-          "dev": true
-        }
-      }
-    },
-    "@angular-devkit/schematics": {
-      "version": "17.3.8",
-      "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.8.tgz",
-      "integrity": "sha512-QRVEYpIfgkprNHc916JlPuNbLzOgrm9DZalHasnLUz4P6g7pR21olb8YCyM2OTJjombNhya9ZpckcADU5Qyvlg==",
-      "dev": true,
-      "requires": {
-        "@angular-devkit/core": "17.3.8",
-        "jsonc-parser": "3.2.1",
-        "magic-string": "0.30.8",
-        "ora": "5.4.1",
-        "rxjs": "7.8.1"
-      }
-    },
-    "@angular-devkit/schematics-cli": {
-      "version": "17.3.8",
-      "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.3.8.tgz",
-      "integrity": "sha512-TjmiwWJarX7oqvNiRAroQ5/LeKUatxBOCNEuKXO/PV8e7pn/Hr/BqfFm+UcYrQoFdZplmtNAfqmbqgVziKvCpA==",
-      "dev": true,
-      "requires": {
-        "@angular-devkit/core": "17.3.8",
-        "@angular-devkit/schematics": "17.3.8",
-        "ansi-colors": "4.1.3",
-        "inquirer": "9.2.15",
-        "symbol-observable": "4.0.0",
-        "yargs-parser": "21.1.1"
-      },
-      "dependencies": {
-        "chalk": {
-          "version": "5.3.0",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
-          "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
-          "dev": true
-        },
-        "cli-width": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
-          "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
-          "dev": true
-        },
-        "inquirer": {
-          "version": "9.2.15",
-          "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz",
-          "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==",
-          "dev": true,
-          "requires": {
-            "@ljharb/through": "^2.3.12",
-            "ansi-escapes": "^4.3.2",
-            "chalk": "^5.3.0",
-            "cli-cursor": "^3.1.0",
-            "cli-width": "^4.1.0",
-            "external-editor": "^3.1.0",
-            "figures": "^3.2.0",
-            "lodash": "^4.17.21",
-            "mute-stream": "1.0.0",
-            "ora": "^5.4.1",
-            "run-async": "^3.0.0",
-            "rxjs": "^7.8.1",
-            "string-width": "^4.2.3",
-            "strip-ansi": "^6.0.1",
-            "wrap-ansi": "^6.2.0"
-          }
-        },
-        "mute-stream": {
-          "version": "1.0.0",
-          "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
-          "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==",
-          "dev": true
-        },
-        "run-async": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz",
-          "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==",
-          "dev": true
-        }
-      }
-    },
-    "@babel/code-frame": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz",
-      "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==",
-      "requires": {
-        "@babel/highlight": "^7.24.6",
-        "picocolors": "^1.0.0"
-      }
-    },
-    "@babel/compat-data": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.6.tgz",
-      "integrity": "sha512-aC2DGhBq5eEdyXWqrDInSqQjO0k8xtPRf5YylULqx8MCd6jBtzqfta/3ETMRpuKIc5hyswfO80ObyA1MvkCcUQ=="
-    },
-    "@babel/core": {
-      "version": "7.24.5",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz",
-      "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==",
-      "requires": {
-        "@ampproject/remapping": "^2.2.0",
-        "@babel/code-frame": "^7.24.2",
-        "@babel/generator": "^7.24.5",
-        "@babel/helper-compilation-targets": "^7.23.6",
-        "@babel/helper-module-transforms": "^7.24.5",
-        "@babel/helpers": "^7.24.5",
-        "@babel/parser": "^7.24.5",
-        "@babel/template": "^7.24.0",
-        "@babel/traverse": "^7.24.5",
-        "@babel/types": "^7.24.5",
-        "convert-source-map": "^2.0.0",
-        "debug": "^4.1.0",
-        "gensync": "^1.0.0-beta.2",
-        "json5": "^2.2.3",
-        "semver": "^6.3.1"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.1",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-          "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
-        }
-      }
-    },
-    "@babel/generator": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.6.tgz",
-      "integrity": "sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg==",
-      "requires": {
-        "@babel/types": "^7.24.6",
-        "@jridgewell/gen-mapping": "^0.3.5",
-        "@jridgewell/trace-mapping": "^0.3.25",
-        "jsesc": "^2.5.1"
-      },
-      "dependencies": {
-        "jsesc": {
-          "version": "2.5.2",
-          "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
-          "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="
-        }
-      }
-    },
-    "@babel/helper-compilation-targets": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz",
-      "integrity": "sha512-VZQ57UsDGlX/5fFA7GkVPplZhHsVc+vuErWgdOiysI9Ksnw0Pbbd6pnPiR/mmJyKHgyIW0c7KT32gmhiF+cirg==",
-      "requires": {
-        "@babel/compat-data": "^7.24.6",
-        "@babel/helper-validator-option": "^7.24.6",
-        "browserslist": "^4.22.2",
-        "lru-cache": "^5.1.1",
-        "semver": "^6.3.1"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.1",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-          "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
-        }
-      }
-    },
-    "@babel/helper-environment-visitor": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz",
-      "integrity": "sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g=="
-    },
-    "@babel/helper-function-name": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz",
-      "integrity": "sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==",
-      "requires": {
-        "@babel/template": "^7.24.6",
-        "@babel/types": "^7.24.6"
-      }
-    },
-    "@babel/helper-hoist-variables": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.6.tgz",
-      "integrity": "sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==",
-      "requires": {
-        "@babel/types": "^7.24.6"
-      }
-    },
-    "@babel/helper-module-imports": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz",
-      "integrity": "sha512-a26dmxFJBF62rRO9mmpgrfTLsAuyHk4e1hKTUkD/fcMfynt8gvEKwQPQDVxWhca8dHoDck+55DFt42zV0QMw5g==",
-      "requires": {
-        "@babel/types": "^7.24.6"
-      }
-    },
-    "@babel/helper-module-transforms": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.6.tgz",
-      "integrity": "sha512-Y/YMPm83mV2HJTbX1Qh2sjgjqcacvOlhbzdCCsSlblOKjSYmQqEbO6rUniWQyRo9ncyfjT8hnUjlG06RXDEmcA==",
-      "requires": {
-        "@babel/helper-environment-visitor": "^7.24.6",
-        "@babel/helper-module-imports": "^7.24.6",
-        "@babel/helper-simple-access": "^7.24.6",
-        "@babel/helper-split-export-declaration": "^7.24.6",
-        "@babel/helper-validator-identifier": "^7.24.6"
-      }
-    },
-    "@babel/helper-simple-access": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.6.tgz",
-      "integrity": "sha512-nZzcMMD4ZhmB35MOOzQuiGO5RzL6tJbsT37Zx8M5L/i9KSrukGXWTjLe1knIbb/RmxoJE9GON9soq0c0VEMM5g==",
-      "requires": {
-        "@babel/types": "^7.24.6"
-      }
-    },
-    "@babel/helper-split-export-declaration": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz",
-      "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==",
-      "requires": {
-        "@babel/types": "^7.24.6"
-      }
-    },
-    "@babel/helper-string-parser": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz",
-      "integrity": "sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q=="
-    },
-    "@babel/helper-validator-identifier": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz",
-      "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw=="
-    },
-    "@babel/helper-validator-option": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.6.tgz",
-      "integrity": "sha512-Jktc8KkF3zIkePb48QO+IapbXlSapOW9S+ogZZkcO6bABgYAxtZcjZ/O005111YLf+j4M84uEgwYoidDkXbCkQ=="
-    },
-    "@babel/helpers": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.6.tgz",
-      "integrity": "sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA==",
-      "requires": {
-        "@babel/template": "^7.24.6",
-        "@babel/types": "^7.24.6"
-      }
-    },
-    "@babel/highlight": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz",
-      "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==",
-      "requires": {
-        "@babel/helper-validator-identifier": "^7.24.6",
-        "chalk": "^2.4.2",
-        "js-tokens": "^4.0.0",
-        "picocolors": "^1.0.0"
-      },
-      "dependencies": {
-        "ansi-styles": {
-          "version": "3.2.1",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-          "requires": {
-            "color-convert": "^1.9.0"
-          }
-        },
-        "chalk": {
-          "version": "2.4.2",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-          "requires": {
-            "ansi-styles": "^3.2.1",
-            "escape-string-regexp": "^1.0.5",
-            "supports-color": "^5.3.0"
-          }
-        },
-        "color-convert": {
-          "version": "1.9.3",
-          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-          "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-          "requires": {
-            "color-name": "1.1.3"
-          }
-        },
-        "color-name": {
-          "version": "1.1.3",
-          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-          "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
-        },
-        "escape-string-regexp": {
-          "version": "1.0.5",
-          "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-          "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="
-        },
-        "has-flag": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-          "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="
-        },
-        "supports-color": {
-          "version": "5.5.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-          "requires": {
-            "has-flag": "^3.0.0"
-          }
-        }
-      }
-    },
-    "@babel/parser": {
-      "version": "7.24.5",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz",
-      "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg=="
-    },
-    "@babel/template": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz",
-      "integrity": "sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==",
-      "requires": {
-        "@babel/code-frame": "^7.24.6",
-        "@babel/parser": "^7.24.6",
-        "@babel/types": "^7.24.6"
-      },
-      "dependencies": {
-        "@babel/parser": {
-          "version": "7.24.6",
-          "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz",
-          "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q=="
-        }
-      }
-    },
-    "@babel/traverse": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.6.tgz",
-      "integrity": "sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw==",
-      "requires": {
-        "@babel/code-frame": "^7.24.6",
-        "@babel/generator": "^7.24.6",
-        "@babel/helper-environment-visitor": "^7.24.6",
-        "@babel/helper-function-name": "^7.24.6",
-        "@babel/helper-hoist-variables": "^7.24.6",
-        "@babel/helper-split-export-declaration": "^7.24.6",
-        "@babel/parser": "^7.24.6",
-        "@babel/types": "^7.24.6",
-        "debug": "^4.3.1",
-        "globals": "^11.1.0"
-      },
-      "dependencies": {
-        "@babel/parser": {
-          "version": "7.24.6",
-          "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz",
-          "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q=="
-        },
-        "globals": {
-          "version": "11.12.0",
-          "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
-          "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
-        }
-      }
-    },
-    "@babel/types": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz",
-      "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==",
-      "requires": {
-        "@babel/helper-string-parser": "^7.24.6",
-        "@babel/helper-validator-identifier": "^7.24.6",
-        "to-fast-properties": "^2.0.0"
-      }
-    },
-    "@balena/dockerignore": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz",
-      "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==",
-      "dev": true
-    },
-    "@bcoe/v8-coverage": {
-      "version": "0.2.3",
-      "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
-      "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
-      "dev": true
-    },
-    "@colors/colors": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
-      "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
-      "dev": true,
-      "optional": true
-    },
-    "@cspotcode/source-map-support": {
-      "version": "0.8.1",
-      "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
-      "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
-      "optional": true,
-      "peer": true,
-      "requires": {
-        "@jridgewell/trace-mapping": "0.3.9"
-      },
-      "dependencies": {
-        "@jridgewell/trace-mapping": {
-          "version": "0.3.9",
-          "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
-          "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
-          "optional": true,
-          "peer": true,
-          "requires": {
-            "@jridgewell/resolve-uri": "^3.0.3",
-            "@jridgewell/sourcemap-codec": "^1.4.10"
-          }
-        }
-      }
-    },
-    "@emnapi/runtime": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz",
-      "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==",
-      "optional": true,
-      "requires": {
-        "tslib": "^2.4.0"
-      }
-    },
-    "@esbuild/aix-ppc64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
-      "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/android-arm": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
-      "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/android-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
-      "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/android-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
-      "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/darwin-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
-      "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/darwin-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
-      "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/freebsd-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
-      "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/freebsd-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
-      "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/linux-arm": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
-      "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/linux-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
-      "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/linux-ia32": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
-      "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/linux-loong64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
-      "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/linux-mips64el": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
-      "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/linux-ppc64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
-      "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/linux-riscv64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
-      "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/linux-s390x": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
-      "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/linux-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
-      "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/netbsd-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
-      "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/openbsd-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
-      "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/sunos-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
-      "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/win32-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
-      "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/win32-ia32": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
-      "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/win32-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
-      "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
-      "dev": true,
-      "optional": true
-    },
-    "@eslint-community/eslint-utils": {
-      "version": "4.4.0",
-      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
-      "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
-      "dev": true,
-      "requires": {
-        "eslint-visitor-keys": "^3.3.0"
-      }
-    },
-    "@eslint-community/regexpp": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz",
-      "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==",
-      "dev": true
-    },
-    "@eslint/config-array": {
-      "version": "0.18.0",
-      "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz",
-      "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==",
-      "dev": true,
-      "requires": {
-        "@eslint/object-schema": "^2.1.4",
-        "debug": "^4.3.1",
-        "minimatch": "^3.1.2"
-      }
-    },
-    "@eslint/eslintrc": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
-      "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
-      "dev": true,
-      "requires": {
-        "ajv": "^6.12.4",
-        "debug": "^4.3.2",
-        "espree": "^10.0.1",
-        "globals": "^14.0.0",
-        "ignore": "^5.2.0",
-        "import-fresh": "^3.2.1",
-        "js-yaml": "^4.1.0",
-        "minimatch": "^3.1.2",
-        "strip-json-comments": "^3.1.1"
-      },
-      "dependencies": {
-        "ajv": {
-          "version": "6.12.6",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-          "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-          "dev": true,
-          "requires": {
-            "fast-deep-equal": "^3.1.1",
-            "fast-json-stable-stringify": "^2.0.0",
-            "json-schema-traverse": "^0.4.1",
-            "uri-js": "^4.2.2"
-          }
-        },
-        "globals": {
-          "version": "14.0.0",
-          "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
-          "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
-          "dev": true
-        },
-        "json-schema-traverse": {
-          "version": "0.4.1",
-          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-          "dev": true
-        }
-      }
-    },
-    "@eslint/js": {
-      "version": "9.9.1",
-      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz",
-      "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==",
-      "dev": true
-    },
-    "@eslint/object-schema": {
-      "version": "2.1.4",
-      "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz",
-      "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==",
-      "dev": true
-    },
-    "@fastify/busboy": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
-      "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
-      "dev": true
-    },
-    "@golevelup/nestjs-discovery": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-4.0.1.tgz",
-      "integrity": "sha512-HFXBJayEkYcU/bbxOztozONdWaZR34ZeJ2zRbZIWY8d5K26oPZQTvJ4L0STW3XVRGWtoE0WBpmx2YPNgYvcmJQ==",
-      "requires": {
-        "lodash": "^4.17.21"
-      }
-    },
-    "@grpc/grpc-js": {
-      "version": "1.10.10",
-      "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.10.tgz",
-      "integrity": "sha512-HPa/K5NX6ahMoeBv15njAc/sfF4/jmiXLar9UlC2UfHFKZzsCVLc3wbe7+7qua7w9VPh2/L6EBxyAV7/E8Wftg==",
-      "requires": {
-        "@grpc/proto-loader": "^0.7.13",
-        "@js-sdsl/ordered-map": "^4.4.2"
-      }
-    },
-    "@grpc/proto-loader": {
-      "version": "0.7.13",
-      "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz",
-      "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==",
-      "requires": {
-        "lodash.camelcase": "^4.3.0",
-        "long": "^5.0.0",
-        "protobufjs": "^7.2.5",
-        "yargs": "^17.7.2"
-      },
-      "dependencies": {
-        "cliui": {
-          "version": "8.0.1",
-          "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
-          "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
-          "requires": {
-            "string-width": "^4.2.0",
-            "strip-ansi": "^6.0.1",
-            "wrap-ansi": "^7.0.0"
-          }
-        },
-        "wrap-ansi": {
-          "version": "7.0.0",
-          "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
-          "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-          "requires": {
-            "ansi-styles": "^4.0.0",
-            "string-width": "^4.1.0",
-            "strip-ansi": "^6.0.0"
-          }
-        },
-        "yargs": {
-          "version": "17.7.2",
-          "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
-          "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
-          "requires": {
-            "cliui": "^8.0.1",
-            "escalade": "^3.1.1",
-            "get-caller-file": "^2.0.5",
-            "require-directory": "^2.1.1",
-            "string-width": "^4.2.3",
-            "y18n": "^5.0.5",
-            "yargs-parser": "^21.1.1"
-          }
-        }
-      }
-    },
-    "@hapi/hoek": {
-      "version": "9.3.0",
-      "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
-      "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="
-    },
-    "@hapi/topo": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
-      "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
-      "requires": {
-        "@hapi/hoek": "^9.0.0"
-      }
-    },
-    "@humanwhocodes/module-importer": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
-      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
-      "dev": true
-    },
-    "@humanwhocodes/retry": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz",
-      "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==",
-      "dev": true
-    },
-    "@img/sharp-darwin-arm64": {
-      "version": "0.33.5",
-      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
-      "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
-      "optional": true,
-      "requires": {
-        "@img/sharp-libvips-darwin-arm64": "1.0.4"
-      }
-    },
-    "@img/sharp-darwin-x64": {
-      "version": "0.33.5",
-      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
-      "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
-      "optional": true,
-      "requires": {
-        "@img/sharp-libvips-darwin-x64": "1.0.4"
-      }
-    },
-    "@img/sharp-libvips-darwin-arm64": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
-      "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
-      "optional": true
-    },
-    "@img/sharp-libvips-darwin-x64": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
-      "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
-      "optional": true
-    },
-    "@img/sharp-libvips-linux-arm": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
-      "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
-      "optional": true
-    },
-    "@img/sharp-libvips-linux-arm64": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
-      "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
-      "optional": true
-    },
-    "@img/sharp-libvips-linux-s390x": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
-      "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
-      "optional": true
-    },
-    "@img/sharp-libvips-linux-x64": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
-      "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
-      "optional": true
-    },
-    "@img/sharp-libvips-linuxmusl-arm64": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
-      "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
-      "optional": true
-    },
-    "@img/sharp-libvips-linuxmusl-x64": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
-      "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
-      "optional": true
-    },
-    "@img/sharp-linux-arm": {
-      "version": "0.33.5",
-      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
-      "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
-      "optional": true,
-      "requires": {
-        "@img/sharp-libvips-linux-arm": "1.0.5"
-      }
-    },
-    "@img/sharp-linux-arm64": {
-      "version": "0.33.5",
-      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
-      "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
-      "optional": true,
-      "requires": {
-        "@img/sharp-libvips-linux-arm64": "1.0.4"
-      }
-    },
-    "@img/sharp-linux-s390x": {
-      "version": "0.33.5",
-      "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
-      "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
-      "optional": true,
-      "requires": {
-        "@img/sharp-libvips-linux-s390x": "1.0.4"
-      }
-    },
-    "@img/sharp-linux-x64": {
-      "version": "0.33.5",
-      "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
-      "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
-      "optional": true,
-      "requires": {
-        "@img/sharp-libvips-linux-x64": "1.0.4"
-      }
-    },
-    "@img/sharp-linuxmusl-arm64": {
-      "version": "0.33.5",
-      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
-      "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
-      "optional": true,
-      "requires": {
-        "@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
-      }
-    },
-    "@img/sharp-linuxmusl-x64": {
-      "version": "0.33.5",
-      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
-      "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
-      "optional": true,
-      "requires": {
-        "@img/sharp-libvips-linuxmusl-x64": "1.0.4"
-      }
-    },
-    "@img/sharp-wasm32": {
-      "version": "0.33.5",
-      "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
-      "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
-      "optional": true,
-      "requires": {
-        "@emnapi/runtime": "^1.2.0"
-      }
-    },
-    "@img/sharp-win32-ia32": {
-      "version": "0.33.5",
-      "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
-      "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
-      "optional": true
-    },
-    "@img/sharp-win32-x64": {
-      "version": "0.33.5",
-      "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
-      "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
-      "optional": true
-    },
-    "@ioredis/commands": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
-      "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
-    },
-    "@isaacs/cliui": {
-      "version": "8.0.2",
-      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
-      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
-      "requires": {
-        "string-width": "^5.1.2",
-        "string-width-cjs": "npm:string-width@^4.2.0",
-        "strip-ansi": "^7.0.1",
-        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
-        "wrap-ansi": "^8.1.0",
-        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
-      },
-      "dependencies": {
-        "ansi-regex": {
-          "version": "6.0.1",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
-          "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA=="
-        },
-        "ansi-styles": {
-          "version": "6.2.1",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
-          "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="
-        },
-        "emoji-regex": {
-          "version": "9.2.2",
-          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
-          "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
-        },
-        "string-width": {
-          "version": "5.1.2",
-          "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
-          "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
-          "requires": {
-            "eastasianwidth": "^0.2.0",
-            "emoji-regex": "^9.2.2",
-            "strip-ansi": "^7.0.1"
-          }
-        },
-        "strip-ansi": {
-          "version": "7.1.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
-          "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
-          "requires": {
-            "ansi-regex": "^6.0.1"
-          }
-        },
-        "wrap-ansi": {
-          "version": "8.1.0",
-          "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
-          "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
-          "requires": {
-            "ansi-styles": "^6.1.0",
-            "string-width": "^5.0.1",
-            "strip-ansi": "^7.0.1"
-          }
-        }
-      }
-    },
-    "@istanbuljs/schema": {
-      "version": "0.1.3",
-      "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
-      "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
-      "dev": true
-    },
-    "@jridgewell/gen-mapping": {
-      "version": "0.3.5",
-      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
-      "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
-      "requires": {
-        "@jridgewell/set-array": "^1.2.1",
-        "@jridgewell/sourcemap-codec": "^1.4.10",
-        "@jridgewell/trace-mapping": "^0.3.24"
-      }
-    },
-    "@jridgewell/resolve-uri": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
-      "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA=="
-    },
-    "@jridgewell/set-array": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
-      "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="
-    },
-    "@jridgewell/source-map": {
-      "version": "0.3.5",
-      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
-      "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
-      "dev": true,
-      "requires": {
-        "@jridgewell/gen-mapping": "^0.3.0",
-        "@jridgewell/trace-mapping": "^0.3.9"
-      }
-    },
-    "@jridgewell/sourcemap-codec": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
-      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
-    },
-    "@jridgewell/trace-mapping": {
-      "version": "0.3.25",
-      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
-      "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
-      "requires": {
-        "@jridgewell/resolve-uri": "^3.1.0",
-        "@jridgewell/sourcemap-codec": "^1.4.14"
-      }
-    },
-    "@js-sdsl/ordered-map": {
-      "version": "4.4.2",
-      "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
-      "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="
-    },
-    "@ljharb/through": {
-      "version": "2.3.13",
-      "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz",
-      "integrity": "sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==",
-      "dev": true,
-      "requires": {
-        "call-bind": "^1.0.7"
-      }
-    },
-    "@lukeed/csprng": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz",
-      "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA=="
-    },
-    "@mapbox/node-pre-gyp": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
-      "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
-      "requires": {
-        "detect-libc": "^2.0.0",
-        "https-proxy-agent": "^5.0.0",
-        "make-dir": "^3.1.0",
-        "node-fetch": "^2.6.7",
-        "nopt": "^5.0.0",
-        "npmlog": "^5.0.1",
-        "rimraf": "^3.0.2",
-        "semver": "^7.3.5",
-        "tar": "^6.1.11"
-      },
-      "dependencies": {
-        "glob": {
-          "version": "7.2.3",
-          "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
-          "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
-          "requires": {
-            "fs.realpath": "^1.0.0",
-            "inflight": "^1.0.4",
-            "inherits": "2",
-            "minimatch": "^3.1.1",
-            "once": "^1.3.0",
-            "path-is-absolute": "^1.0.0"
-          }
-        },
-        "rimraf": {
-          "version": "3.0.2",
-          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
-          "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
-          "requires": {
-            "glob": "^7.1.3"
-          }
-        }
-      }
-    },
-    "@microsoft/tsdoc": {
-      "version": "0.15.0",
-      "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz",
-      "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA=="
-    },
-    "@msgpackr-extract/msgpackr-extract-darwin-arm64": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz",
-      "integrity": "sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==",
-      "optional": true
-    },
-    "@nestjs/bull-shared": {
-      "version": "10.2.1",
-      "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.2.1.tgz",
-      "integrity": "sha512-zvnTvSq6OJ92omcsFUwaUmPbM3PRgWkIusHPB5TE3IFS7nNdM3OwF+kfe56sgKjMtQQMe/56lok0S04OtPMX5Q==",
-      "requires": {
-        "tslib": "2.6.3"
-      }
-    },
-    "@nestjs/bullmq": {
-      "version": "10.2.1",
-      "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.2.1.tgz",
-      "integrity": "sha512-nDR0hDabmtXt5gsb5R786BJsGIJoWh/79sVmRETXf4S45+fvdqG1XkCKAeHF9TO9USodw9m+XBNKysTnkY41gw==",
-      "requires": {
-        "@nestjs/bull-shared": "^10.2.1",
-        "tslib": "2.6.3"
-      }
-    },
-    "@nestjs/cli": {
-      "version": "10.4.4",
-      "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.4.tgz",
-      "integrity": "sha512-WKERbSZJGof0+9XeeMmWnb/9FpNxogcB5eTJTHjc9no0ymdTw3jTzT+KZL9iC/hGqBpuomDLaNFCYbAOt29nBw==",
-      "dev": true,
-      "requires": {
-        "@angular-devkit/core": "17.3.8",
-        "@angular-devkit/schematics": "17.3.8",
-        "@angular-devkit/schematics-cli": "17.3.8",
-        "@nestjs/schematics": "^10.0.1",
-        "chalk": "4.1.2",
-        "chokidar": "3.6.0",
-        "cli-table3": "0.6.5",
-        "commander": "4.1.1",
-        "fork-ts-checker-webpack-plugin": "9.0.2",
-        "glob": "10.4.2",
-        "inquirer": "8.2.6",
-        "node-emoji": "1.11.0",
-        "ora": "5.4.1",
-        "tree-kill": "1.2.2",
-        "tsconfig-paths": "4.2.0",
-        "tsconfig-paths-webpack-plugin": "4.1.0",
-        "typescript": "5.3.3",
-        "webpack": "5.93.0",
-        "webpack-node-externals": "3.0.0"
-      },
-      "dependencies": {
-        "typescript": {
-          "version": "5.3.3",
-          "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
-          "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
-          "dev": true
-        }
-      }
-    },
-    "@nestjs/common": {
-      "version": "10.4.1",
-      "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.1.tgz",
-      "integrity": "sha512-4CkrDx0s4XuWqFjX8WvOFV7Y6RGJd0P2OBblkhZS7nwoctoSuW5pyEa8SWak6YHNGrHRpFb6ymm5Ai4LncwRVA==",
-      "requires": {
-        "iterare": "1.2.1",
-        "tslib": "2.6.3",
-        "uid": "2.0.2"
-      }
-    },
-    "@nestjs/config": {
-      "version": "3.2.3",
-      "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.3.tgz",
-      "integrity": "sha512-p6yv/CvoBewJ72mBq4NXgOAi2rSQNWx3a+IMJLVKS2uiwFCOQQuiIatGwq6MRjXV3Jr+B41iUO8FIf4xBrZ4/w==",
-      "requires": {
-        "dotenv": "16.4.5",
-        "dotenv-expand": "10.0.0",
-        "lodash": "4.17.21"
-      }
-    },
-    "@nestjs/core": {
-      "version": "10.4.1",
-      "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.1.tgz",
-      "integrity": "sha512-9I1WdfOBCCHdUm+ClBJupOuZQS6UxzIWHIq6Vp1brAA5ZKl/Wq6BVwSsbnUJGBy3J3PM2XHmR0EQ4fwX3nR7lA==",
-      "requires": {
-        "@nuxtjs/opencollective": "0.3.2",
-        "fast-safe-stringify": "2.1.1",
-        "iterare": "1.2.1",
-        "path-to-regexp": "3.2.0",
-        "tslib": "2.6.3",
-        "uid": "2.0.2"
-      }
-    },
-    "@nestjs/event-emitter": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-2.0.4.tgz",
-      "integrity": "sha512-quMiw8yOwoSul0pp3mOonGz8EyXWHSBTqBy8B0TbYYgpnG1Ix2wGUnuTksLWaaBiiOTDhciaZ41Y5fJZsSJE1Q==",
-      "requires": {
-        "eventemitter2": "6.4.9"
-      }
-    },
-    "@nestjs/mapped-types": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz",
-      "integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==",
-      "requires": {}
-    },
-    "@nestjs/platform-express": {
-      "version": "10.4.1",
-      "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.1.tgz",
-      "integrity": "sha512-ccfqIDAq/bg1ShLI5KGtaLaYGykuAdvCi57ohewH7eKJSIpWY1DQjbgKlFfXokALYUq1YOMGqjeZ244OWHfDQg==",
-      "requires": {
-        "body-parser": "1.20.2",
-        "cors": "2.8.5",
-        "express": "4.19.2",
-        "multer": "1.4.4-lts.1",
-        "tslib": "2.6.3"
-      }
-    },
-    "@nestjs/platform-socket.io": {
-      "version": "10.4.1",
-      "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.1.tgz",
-      "integrity": "sha512-cxn5vKBAbqtEVPl0qVcJpR4sC12+hzcY/mYXGW6ippOKQDBNc2OF8oZXu6V3O1MvAl+VM7eNNEsLmP9DRKQlnw==",
-      "requires": {
-        "socket.io": "4.7.5",
-        "tslib": "2.6.3"
-      }
-    },
-    "@nestjs/schedule": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.0.tgz",
-      "integrity": "sha512-WEc96WTXZW+VI/Ng+uBpiBUwm6TWtAbQ4RKWkfbmzKvmbRGzA/9k/UyAWDS9k0pp+ZcbC+MaZQtt7TjQHrwX6g==",
-      "requires": {
-        "cron": "3.1.7",
-        "uuid": "10.0.0"
-      },
-      "dependencies": {
-        "uuid": {
-          "version": "10.0.0",
-          "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
-          "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="
-        }
-      }
-    },
-    "@nestjs/schematics": {
-      "version": "10.1.4",
-      "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.4.tgz",
-      "integrity": "sha512-QpY8ez9cTvXXPr3/KBrtSgXQHMSV6BkOUYy2c2TTe6cBqriEdGnCYqGl8cnfrQl3632q3lveQPaZ/c127dHsEw==",
-      "dev": true,
-      "requires": {
-        "@angular-devkit/core": "17.3.8",
-        "@angular-devkit/schematics": "17.3.8",
-        "comment-json": "4.2.3",
-        "jsonc-parser": "3.3.1",
-        "pluralize": "8.0.0"
-      },
-      "dependencies": {
-        "jsonc-parser": {
-          "version": "3.3.1",
-          "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
-          "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
-          "dev": true
-        }
-      }
-    },
-    "@nestjs/swagger": {
-      "version": "7.4.0",
-      "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.0.tgz",
-      "integrity": "sha512-dCiwKkRxcR7dZs5jtrGspBAe/nqJd1AYzOBTzw9iCdbq3BGrLpwokelk6lFZPe4twpTsPQqzNKBwKzVbI6AR/g==",
-      "requires": {
-        "@microsoft/tsdoc": "^0.15.0",
-        "@nestjs/mapped-types": "2.0.5",
-        "js-yaml": "4.1.0",
-        "lodash": "4.17.21",
-        "path-to-regexp": "3.2.0",
-        "swagger-ui-dist": "5.17.14"
-      }
-    },
-    "@nestjs/testing": {
-      "version": "10.4.1",
-      "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.1.tgz",
-      "integrity": "sha512-pR+su5+YGqCLH0RhhVkPowQK7FCORU0/PWAywPK7LScAOtD67ZoviZ7hAU4vnGdwkg4HCB0D7W8Bkg19CGU8Xw==",
-      "dev": true,
-      "requires": {
-        "tslib": "2.6.3"
-      }
-    },
-    "@nestjs/typeorm": {
-      "version": "10.0.2",
-      "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.2.tgz",
-      "integrity": "sha512-H738bJyydK4SQkRCTeh1aFBxoO1E9xdL/HaLGThwrqN95os5mEyAtK7BLADOS+vldP4jDZ2VQPLj4epWwRqCeQ==",
-      "requires": {
-        "uuid": "9.0.1"
-      }
-    },
-    "@nestjs/websockets": {
-      "version": "10.4.1",
-      "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.1.tgz",
-      "integrity": "sha512-p0Eq94WneczV2bnLEu9hl24iCIfH5eUCGgBuYOkVDySBwvya5L+gD4wUoqIqGoX1c6rkhQa+pMR7pi1EY4t93w==",
-      "requires": {
-        "iterare": "1.2.1",
-        "object-hash": "3.0.0",
-        "tslib": "2.6.3"
-      }
-    },
-    "@next/env": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz",
-      "integrity": "sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA=="
-    },
-    "@next/swc-darwin-arm64": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.3.tgz",
-      "integrity": "sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==",
-      "optional": true
-    },
-    "@next/swc-darwin-x64": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz",
-      "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==",
-      "optional": true
-    },
-    "@next/swc-linux-arm64-gnu": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz",
-      "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==",
-      "optional": true
-    },
-    "@next/swc-linux-arm64-musl": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz",
-      "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==",
-      "optional": true
-    },
-    "@next/swc-linux-x64-gnu": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz",
-      "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==",
-      "optional": true
-    },
-    "@next/swc-linux-x64-musl": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz",
-      "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==",
-      "optional": true
-    },
-    "@next/swc-win32-arm64-msvc": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz",
-      "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==",
-      "optional": true
-    },
-    "@next/swc-win32-ia32-msvc": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz",
-      "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==",
-      "optional": true
-    },
-    "@next/swc-win32-x64-msvc": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz",
-      "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==",
-      "optional": true
-    },
-    "@nodelib/fs.scandir": {
-      "version": "2.1.5",
-      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
-      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
-      "requires": {
-        "@nodelib/fs.stat": "2.0.5",
-        "run-parallel": "^1.1.9"
-      }
-    },
-    "@nodelib/fs.stat": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
-      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="
-    },
-    "@nodelib/fs.walk": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
-      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
-      "requires": {
-        "@nodelib/fs.scandir": "2.1.5",
-        "fastq": "^1.6.0"
-      }
-    },
-    "@nuxtjs/opencollective": {
-      "version": "0.3.2",
-      "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz",
-      "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==",
-      "requires": {
-        "chalk": "^4.1.0",
-        "consola": "^2.15.0",
-        "node-fetch": "^2.6.1"
-      }
-    },
-    "@one-ini/wasm": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
-      "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="
-    },
-    "@opentelemetry/api": {
-      "version": "1.8.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz",
-      "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w=="
-    },
-    "@opentelemetry/api-logs": {
-      "version": "0.52.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.0.tgz",
-      "integrity": "sha512-HxjD7xH9iAE4OyhNaaSec65i1H6QZYBWSwWkowFfsc5YAcDvJG30/J1sRKXEQqdmUcKTXEAnA66UciqZha/4+Q==",
-      "requires": {
-        "@opentelemetry/api": "^1.0.0"
-      }
-    },
-    "@opentelemetry/auto-instrumentations-node": {
-      "version": "0.49.2",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.49.2.tgz",
-      "integrity": "sha512-xtETEPmAby/3MMmedv8Z/873sdLTWg+Vq98rtm4wbwvAiXBB/ao8qRyzRlvR2MR6puEr+vIB/CXeyJnzNA3cyw==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/instrumentation-amqplib": "^0.41.0",
-        "@opentelemetry/instrumentation-aws-lambda": "^0.43.0",
-        "@opentelemetry/instrumentation-aws-sdk": "^0.43.1",
-        "@opentelemetry/instrumentation-bunyan": "^0.40.0",
-        "@opentelemetry/instrumentation-cassandra-driver": "^0.40.0",
-        "@opentelemetry/instrumentation-connect": "^0.38.0",
-        "@opentelemetry/instrumentation-cucumber": "^0.8.0",
-        "@opentelemetry/instrumentation-dataloader": "^0.11.0",
-        "@opentelemetry/instrumentation-dns": "^0.38.0",
-        "@opentelemetry/instrumentation-express": "^0.41.1",
-        "@opentelemetry/instrumentation-fastify": "^0.38.0",
-        "@opentelemetry/instrumentation-fs": "^0.14.0",
-        "@opentelemetry/instrumentation-generic-pool": "^0.38.1",
-        "@opentelemetry/instrumentation-graphql": "^0.42.0",
-        "@opentelemetry/instrumentation-grpc": "^0.52.0",
-        "@opentelemetry/instrumentation-hapi": "^0.40.0",
-        "@opentelemetry/instrumentation-http": "^0.52.0",
-        "@opentelemetry/instrumentation-ioredis": "^0.42.0",
-        "@opentelemetry/instrumentation-kafkajs": "^0.2.0",
-        "@opentelemetry/instrumentation-knex": "^0.39.0",
-        "@opentelemetry/instrumentation-koa": "^0.42.0",
-        "@opentelemetry/instrumentation-lru-memoizer": "^0.39.0",
-        "@opentelemetry/instrumentation-memcached": "^0.38.0",
-        "@opentelemetry/instrumentation-mongodb": "^0.46.0",
-        "@opentelemetry/instrumentation-mongoose": "^0.41.0",
-        "@opentelemetry/instrumentation-mysql": "^0.40.0",
-        "@opentelemetry/instrumentation-mysql2": "^0.40.0",
-        "@opentelemetry/instrumentation-nestjs-core": "^0.39.0",
-        "@opentelemetry/instrumentation-net": "^0.38.0",
-        "@opentelemetry/instrumentation-pg": "^0.43.0",
-        "@opentelemetry/instrumentation-pino": "^0.41.0",
-        "@opentelemetry/instrumentation-redis": "^0.41.0",
-        "@opentelemetry/instrumentation-redis-4": "^0.41.1",
-        "@opentelemetry/instrumentation-restify": "^0.40.0",
-        "@opentelemetry/instrumentation-router": "^0.39.0",
-        "@opentelemetry/instrumentation-socket.io": "^0.41.0",
-        "@opentelemetry/instrumentation-tedious": "^0.13.0",
-        "@opentelemetry/instrumentation-undici": "^0.5.0",
-        "@opentelemetry/instrumentation-winston": "^0.39.0",
-        "@opentelemetry/resource-detector-alibaba-cloud": "^0.29.0",
-        "@opentelemetry/resource-detector-aws": "^1.6.0",
-        "@opentelemetry/resource-detector-azure": "^0.2.10",
-        "@opentelemetry/resource-detector-container": "^0.4.0",
-        "@opentelemetry/resource-detector-gcp": "^0.29.10",
-        "@opentelemetry/resources": "^1.24.0",
-        "@opentelemetry/sdk-node": "^0.52.0"
-      },
-      "dependencies": {
-        "@opentelemetry/api-logs": {
-          "version": "0.52.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz",
-          "integrity": "sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==",
-          "requires": {
-            "@opentelemetry/api": "^1.0.0"
-          }
-        },
-        "@opentelemetry/core": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-          "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.25.1"
-          }
-        },
-        "@opentelemetry/instrumentation": {
-          "version": "0.52.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz",
-          "integrity": "sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==",
-          "requires": {
-            "@opentelemetry/api-logs": "0.52.1",
-            "@types/shimmer": "^1.0.2",
-            "import-in-the-middle": "^1.8.1",
-            "require-in-the-middle": "^7.1.1",
-            "semver": "^7.5.2",
-            "shimmer": "^1.2.1"
-          }
-        },
-        "@opentelemetry/sdk-node": {
-          "version": "0.52.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.52.1.tgz",
-          "integrity": "sha512-uEG+gtEr6eKd8CVWeKMhH2olcCHM9dEK68pe0qE0be32BcCRsvYURhHaD1Srngh1SQcnQzZ4TP324euxqtBOJA==",
-          "requires": {
-            "@opentelemetry/api-logs": "0.52.1",
-            "@opentelemetry/core": "1.25.1",
-            "@opentelemetry/exporter-trace-otlp-grpc": "0.52.1",
-            "@opentelemetry/exporter-trace-otlp-http": "0.52.1",
-            "@opentelemetry/exporter-trace-otlp-proto": "0.52.1",
-            "@opentelemetry/exporter-zipkin": "1.25.1",
-            "@opentelemetry/instrumentation": "0.52.1",
-            "@opentelemetry/resources": "1.25.1",
-            "@opentelemetry/sdk-logs": "0.52.1",
-            "@opentelemetry/sdk-metrics": "1.25.1",
-            "@opentelemetry/sdk-trace-base": "1.25.1",
-            "@opentelemetry/sdk-trace-node": "1.25.1",
-            "@opentelemetry/semantic-conventions": "1.25.1"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-          "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ=="
-        },
-        "import-in-the-middle": {
-          "version": "1.11.0",
-          "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz",
-          "integrity": "sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q==",
-          "requires": {
-            "acorn": "^8.8.2",
-            "acorn-import-attributes": "^1.9.5",
-            "cjs-module-lexer": "^1.2.2",
-            "module-details-from-path": "^1.0.3"
-          }
-        }
-      }
-    },
-    "@opentelemetry/context-async-hooks": {
-      "version": "1.26.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.26.0.tgz",
-      "integrity": "sha512-HedpXXYzzbaoutw6DFLWLDket2FwLkLpil4hGCZ1xYEIMTcivdfwEOISgdbLEWyG3HW52gTq2V9mOVJrONgiwg==",
-      "requires": {}
-    },
-    "@opentelemetry/core": {
-      "version": "1.25.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.0.tgz",
-      "integrity": "sha512-n0B3s8rrqGrasTgNkXLKXzN0fXo+6IYP7M5b7AMsrZM33f/y6DS6kJ0Btd7SespASWq8bgL3taLo0oe0vB52IQ==",
-      "requires": {
-        "@opentelemetry/semantic-conventions": "1.25.0"
-      }
-    },
-    "@opentelemetry/exporter-logs-otlp-grpc": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.53.0.tgz",
-      "integrity": "sha512-x5ygAQgWAQOI+UOhyV3z9eW7QU2dCfnfOuIBiyYmC2AWr74f6x/3JBnP27IAcEx6aihpqBYWKnpoUTztkVPAZw==",
-      "requires": {
-        "@grpc/grpc-js": "^1.7.1",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/otlp-grpc-exporter-base": "0.53.0",
-        "@opentelemetry/otlp-transformer": "0.53.0",
-        "@opentelemetry/sdk-logs": "0.53.0"
-      },
-      "dependencies": {
-        "@opentelemetry/api-logs": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz",
-          "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==",
-          "requires": {
-            "@opentelemetry/api": "^1.0.0"
-          }
-        },
-        "@opentelemetry/core": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz",
-          "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.27.0"
-          }
-        },
-        "@opentelemetry/otlp-exporter-base": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz",
-          "integrity": "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/otlp-transformer": "0.53.0"
-          }
-        },
-        "@opentelemetry/otlp-grpc-exporter-base": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.53.0.tgz",
-          "integrity": "sha512-F7RCN8VN+lzSa4fGjewit8Z5fEUpY/lmMVy5EWn2ZpbAabg3EE3sCLuTNfOiooNGnmvzimUPruoeqeko/5/TzQ==",
-          "requires": {
-            "@grpc/grpc-js": "^1.7.1",
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/otlp-exporter-base": "0.53.0",
-            "@opentelemetry/otlp-transformer": "0.53.0"
-          }
-        },
-        "@opentelemetry/otlp-transformer": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.53.0.tgz",
-          "integrity": "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==",
-          "requires": {
-            "@opentelemetry/api-logs": "0.53.0",
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/resources": "1.26.0",
-            "@opentelemetry/sdk-logs": "0.53.0",
-            "@opentelemetry/sdk-metrics": "1.26.0",
-            "@opentelemetry/sdk-trace-base": "1.26.0",
-            "protobufjs": "^7.3.0"
-          }
-        },
-        "@opentelemetry/resources": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz",
-          "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/semantic-conventions": "1.27.0"
-          }
-        },
-        "@opentelemetry/sdk-logs": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.53.0.tgz",
-          "integrity": "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==",
-          "requires": {
-            "@opentelemetry/api-logs": "0.53.0",
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/resources": "1.26.0"
-          }
-        },
-        "@opentelemetry/sdk-metrics": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz",
-          "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/resources": "1.26.0"
-          }
-        },
-        "@opentelemetry/sdk-trace-base": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz",
-          "integrity": "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/resources": "1.26.0",
-            "@opentelemetry/semantic-conventions": "1.27.0"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.27.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
-          "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="
-        }
-      }
-    },
-    "@opentelemetry/exporter-logs-otlp-http": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.53.0.tgz",
-      "integrity": "sha512-cSRKgD/n8rb+Yd+Cif6EnHEL/VZg1o8lEcEwFji1lwene6BdH51Zh3feAD9p2TyVoBKrl6Q9Zm2WltSp2k9gWQ==",
-      "requires": {
-        "@opentelemetry/api-logs": "0.53.0",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/otlp-exporter-base": "0.53.0",
-        "@opentelemetry/otlp-transformer": "0.53.0",
-        "@opentelemetry/sdk-logs": "0.53.0"
-      },
-      "dependencies": {
-        "@opentelemetry/api-logs": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz",
-          "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==",
-          "requires": {
-            "@opentelemetry/api": "^1.0.0"
-          }
-        },
-        "@opentelemetry/core": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz",
-          "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.27.0"
-          }
-        },
-        "@opentelemetry/otlp-exporter-base": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz",
-          "integrity": "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/otlp-transformer": "0.53.0"
-          }
-        },
-        "@opentelemetry/otlp-transformer": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.53.0.tgz",
-          "integrity": "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==",
-          "requires": {
-            "@opentelemetry/api-logs": "0.53.0",
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/resources": "1.26.0",
-            "@opentelemetry/sdk-logs": "0.53.0",
-            "@opentelemetry/sdk-metrics": "1.26.0",
-            "@opentelemetry/sdk-trace-base": "1.26.0",
-            "protobufjs": "^7.3.0"
-          }
-        },
-        "@opentelemetry/resources": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz",
-          "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/semantic-conventions": "1.27.0"
-          }
-        },
-        "@opentelemetry/sdk-logs": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.53.0.tgz",
-          "integrity": "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==",
-          "requires": {
-            "@opentelemetry/api-logs": "0.53.0",
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/resources": "1.26.0"
-          }
-        },
-        "@opentelemetry/sdk-metrics": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz",
-          "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/resources": "1.26.0"
-          }
-        },
-        "@opentelemetry/sdk-trace-base": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz",
-          "integrity": "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/resources": "1.26.0",
-            "@opentelemetry/semantic-conventions": "1.27.0"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.27.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
-          "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="
-        }
-      }
-    },
-    "@opentelemetry/exporter-logs-otlp-proto": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.53.0.tgz",
-      "integrity": "sha512-jhEcVL1deeWNmTUP05UZMriZPSWUBcfg94ng7JuBb1q2NExgnADQFl1VQQ+xo62/JepK+MxQe4xAwlsDQFbISA==",
-      "requires": {
-        "@opentelemetry/api-logs": "0.53.0",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/otlp-exporter-base": "0.53.0",
-        "@opentelemetry/otlp-transformer": "0.53.0",
-        "@opentelemetry/resources": "1.26.0",
-        "@opentelemetry/sdk-logs": "0.53.0",
-        "@opentelemetry/sdk-trace-base": "1.26.0"
-      },
-      "dependencies": {
-        "@opentelemetry/api-logs": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz",
-          "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==",
-          "requires": {
-            "@opentelemetry/api": "^1.0.0"
-          }
-        },
-        "@opentelemetry/core": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz",
-          "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.27.0"
-          }
-        },
-        "@opentelemetry/otlp-exporter-base": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz",
-          "integrity": "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/otlp-transformer": "0.53.0"
-          }
-        },
-        "@opentelemetry/otlp-transformer": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.53.0.tgz",
-          "integrity": "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==",
-          "requires": {
-            "@opentelemetry/api-logs": "0.53.0",
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/resources": "1.26.0",
-            "@opentelemetry/sdk-logs": "0.53.0",
-            "@opentelemetry/sdk-metrics": "1.26.0",
-            "@opentelemetry/sdk-trace-base": "1.26.0",
-            "protobufjs": "^7.3.0"
-          }
-        },
-        "@opentelemetry/resources": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz",
-          "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/semantic-conventions": "1.27.0"
-          }
-        },
-        "@opentelemetry/sdk-logs": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.53.0.tgz",
-          "integrity": "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==",
-          "requires": {
-            "@opentelemetry/api-logs": "0.53.0",
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/resources": "1.26.0"
-          }
-        },
-        "@opentelemetry/sdk-metrics": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz",
-          "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/resources": "1.26.0"
-          }
-        },
-        "@opentelemetry/sdk-trace-base": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz",
-          "integrity": "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/resources": "1.26.0",
-            "@opentelemetry/semantic-conventions": "1.27.0"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.27.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
-          "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="
-        }
-      }
-    },
-    "@opentelemetry/exporter-prometheus": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.53.0.tgz",
-      "integrity": "sha512-STP2FZQOykUByPnibbouTirNxnG69Ph8TiMXDsaZuWxGDJ7wsYsRQydJkAVpvG+p0hTMP/hIfZp9zT/1iHpIkQ==",
-      "requires": {
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/resources": "1.26.0",
-        "@opentelemetry/sdk-metrics": "1.26.0"
-      },
-      "dependencies": {
-        "@opentelemetry/core": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz",
-          "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.27.0"
-          }
-        },
-        "@opentelemetry/resources": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz",
-          "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/semantic-conventions": "1.27.0"
-          }
-        },
-        "@opentelemetry/sdk-metrics": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz",
-          "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/resources": "1.26.0"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.27.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
-          "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="
-        }
-      }
-    },
-    "@opentelemetry/exporter-trace-otlp-grpc": {
-      "version": "0.52.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.52.1.tgz",
-      "integrity": "sha512-pVkSH20crBwMTqB3nIN4jpQKUEoB0Z94drIHpYyEqs7UBr+I0cpYyOR3bqjA/UasQUMROb3GX8ZX4/9cVRqGBQ==",
-      "requires": {
-        "@grpc/grpc-js": "^1.7.1",
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/otlp-grpc-exporter-base": "0.52.1",
-        "@opentelemetry/otlp-transformer": "0.52.1",
-        "@opentelemetry/resources": "1.25.1",
-        "@opentelemetry/sdk-trace-base": "1.25.1"
-      },
-      "dependencies": {
-        "@opentelemetry/core": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-          "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.25.1"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-          "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ=="
-        }
-      }
-    },
-    "@opentelemetry/exporter-trace-otlp-http": {
-      "version": "0.52.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.52.1.tgz",
-      "integrity": "sha512-05HcNizx0BxcFKKnS5rwOV+2GevLTVIRA0tRgWYyw4yCgR53Ic/xk83toYKts7kbzcI+dswInUg/4s8oyA+tqg==",
-      "requires": {
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/otlp-exporter-base": "0.52.1",
-        "@opentelemetry/otlp-transformer": "0.52.1",
-        "@opentelemetry/resources": "1.25.1",
-        "@opentelemetry/sdk-trace-base": "1.25.1"
-      },
-      "dependencies": {
-        "@opentelemetry/core": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-          "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.25.1"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-          "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ=="
-        }
-      }
-    },
-    "@opentelemetry/exporter-trace-otlp-proto": {
-      "version": "0.52.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.52.1.tgz",
-      "integrity": "sha512-pt6uX0noTQReHXNeEslQv7x311/F1gJzMnp1HD2qgypLRPbXDeMzzeTngRTUaUbP6hqWNtPxuLr4DEoZG+TcEQ==",
-      "requires": {
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/otlp-exporter-base": "0.52.1",
-        "@opentelemetry/otlp-transformer": "0.52.1",
-        "@opentelemetry/resources": "1.25.1",
-        "@opentelemetry/sdk-trace-base": "1.25.1"
-      },
-      "dependencies": {
-        "@opentelemetry/core": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-          "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.25.1"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-          "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ=="
-        }
-      }
-    },
-    "@opentelemetry/exporter-zipkin": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.25.1.tgz",
-      "integrity": "sha512-RmOwSvkimg7ETwJbUOPTMhJm9A9bG1U8s7Zo3ajDh4zM7eYcycQ0dM7FbLD6NXWbI2yj7UY4q8BKinKYBQksyw==",
-      "requires": {
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/resources": "1.25.1",
-        "@opentelemetry/sdk-trace-base": "1.25.1",
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "dependencies": {
-        "@opentelemetry/core": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-          "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.25.1"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-          "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ=="
-        }
-      }
-    },
-    "@opentelemetry/host-metrics": {
-      "version": "0.35.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/host-metrics/-/host-metrics-0.35.1.tgz",
-      "integrity": "sha512-d49/Un/pzqUSsGLeO8PvrX2bLxVAORcaoL3nxjJCzGikXA6gjWXxGOfT8D4qePlgnocozppWszefMHoRFS2MsA==",
-      "requires": {
-        "@opentelemetry/sdk-metrics": "^1.8.0",
-        "systeminformation": "^5.21.20"
-      }
-    },
-    "@opentelemetry/instrumentation": {
-      "version": "0.52.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.52.0.tgz",
-      "integrity": "sha512-LPwSIrw+60cheWaXsfGL8stBap/AppKQJFE+qqRvzYrgttXFH2ofoIMxWadeqPTq4BYOXM/C7Bdh/T+B60xnlQ==",
-      "requires": {
-        "@opentelemetry/api-logs": "0.52.0",
-        "@types/shimmer": "^1.0.2",
-        "import-in-the-middle": "1.8.0",
-        "require-in-the-middle": "^7.1.1",
-        "semver": "^7.5.2",
-        "shimmer": "^1.2.1"
-      }
-    },
-    "@opentelemetry/instrumentation-amqplib": {
-      "version": "0.41.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.41.0.tgz",
-      "integrity": "sha512-00Oi6N20BxJVcqETjgNzCmVKN+I5bJH/61IlHiIWd00snj1FdgiIKlpE4hYVacTB2sjIBB3nTbHskttdZEE2eg==",
-      "requires": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      }
-    },
-    "@opentelemetry/instrumentation-aws-lambda": {
-      "version": "0.43.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.43.0.tgz",
-      "integrity": "sha512-pSxcWlsE/pCWQRIw92sV2C+LmKXelYkjkA7C5s39iPUi4pZ2lA1nIiw+1R/y2pDEhUHcaKkNyljQr3cx9ZpVlQ==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/propagator-aws-xray": "^1.3.1",
-        "@opentelemetry/resources": "^1.8.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0",
-        "@types/aws-lambda": "8.10.122"
-      }
-    },
-    "@opentelemetry/instrumentation-aws-sdk": {
-      "version": "0.43.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.43.1.tgz",
-      "integrity": "sha512-qLT2cCniJ5W+6PFzKbksnoIQuq9pS83nmgaExfUwXVvlwi0ILc50dea0tWBHZMkdIDa/zZdcuFrJ7+fUcSnRow==",
-      "requires": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/propagation-utils": "^0.30.10",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      }
-    },
-    "@opentelemetry/instrumentation-bunyan": {
-      "version": "0.40.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.40.0.tgz",
-      "integrity": "sha512-aZ4cXaGWwj79ZXSYrgFVsrDlE4mmf2wfvP9bViwRc0j75A6eN6GaHYHqufFGMTCqASQn5pIjjP+Bx+PWTGiofw==",
-      "requires": {
-        "@opentelemetry/api-logs": "^0.52.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@types/bunyan": "1.8.9"
-      }
-    },
-    "@opentelemetry/instrumentation-cassandra-driver": {
-      "version": "0.40.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.40.0.tgz",
-      "integrity": "sha512-JxbM39JU7HxE9MTKKwi6y5Z3mokjZB2BjwfqYi4B3Y29YO3I42Z7eopG6qq06yWZc+nQli386UDQe0d9xKmw0A==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      }
-    },
-    "@opentelemetry/instrumentation-connect": {
-      "version": "0.38.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.38.0.tgz",
-      "integrity": "sha512-2/nRnx3pjYEmdPIaBwtgtSviTKHWnDZN3R+TkRUnhIVrvBKVcq+I5B2rtd6mr6Fe9cHlZ9Ojcuh7pkNh/xdWWg==",
-      "requires": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0",
-        "@types/connect": "3.4.36"
-      }
-    },
-    "@opentelemetry/instrumentation-cucumber": {
-      "version": "0.8.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cucumber/-/instrumentation-cucumber-0.8.0.tgz",
-      "integrity": "sha512-ieTm4RBIlZt2brPwtX5aEZYtYnkyqhAVXJI9RIohiBVMe5DxiwCwt+2Exep/nDVqGPX8zRBZUl4AEw423OxJig==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      }
-    },
-    "@opentelemetry/instrumentation-dataloader": {
-      "version": "0.11.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.11.0.tgz",
-      "integrity": "sha512-27urJmwkH4KDaMJtEv1uy2S7Apk4XbN4AgWMdfMJbi7DnOduJmeuA+DpJCwXB72tEWXo89z5T3hUVJIDiSNmNw==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0"
-      }
-    },
-    "@opentelemetry/instrumentation-dns": {
-      "version": "0.38.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dns/-/instrumentation-dns-0.38.0.tgz",
-      "integrity": "sha512-Um07I0TQXDWa+ZbEAKDFUxFH40dLtejtExDOMLNJ1CL8VmOmA71qx93Qi/QG4tGkiI1XWqr7gF/oiMCJ4m8buQ==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "semver": "^7.5.4"
-      }
-    },
-    "@opentelemetry/instrumentation-express": {
-      "version": "0.41.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.41.1.tgz",
-      "integrity": "sha512-uRx0V3LPGzjn2bxAnV8eUsDT82vT7NTwI0ezEuPMBOTOsnPpGhWdhcdNdhH80sM4TrWrOfXm9HGEdfWE3TRIww==",
-      "requires": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      }
-    },
-    "@opentelemetry/instrumentation-fastify": {
-      "version": "0.38.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.38.0.tgz",
-      "integrity": "sha512-HBVLpTSYpkQZ87/Df3N0gAw7VzYZV3n28THIBrJWfuqw3Or7UqdhnjeuMIPQ04BKk3aZc0cWn2naSQObbh5vXw==",
-      "requires": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      }
-    },
-    "@opentelemetry/instrumentation-fs": {
-      "version": "0.14.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.14.0.tgz",
-      "integrity": "sha512-pVc8P5AgliC1DphyyBUgsxXlm2XaPH4BpYvt7rAZDMIqUpRk8gs19SioABtKqqxvFzg5jPtgJfJsdxq0Y+maLw==",
-      "requires": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0"
-      }
-    },
-    "@opentelemetry/instrumentation-generic-pool": {
-      "version": "0.38.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.38.1.tgz",
-      "integrity": "sha512-WvssuKCuavu/hlq661u82UWkc248cyI/sT+c2dEIj6yCk0BUkErY1D+9XOO+PmHdJNE+76i2NdcvQX5rJrOe/w==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0"
-      }
-    },
-    "@opentelemetry/instrumentation-graphql": {
-      "version": "0.42.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.42.0.tgz",
-      "integrity": "sha512-N8SOwoKL9KQSX7z3gOaw5UaTeVQcfDO1c21csVHnmnmGUoqsXbArK2B8VuwPWcv6/BC/i3io+xTo7QGRZ/z28Q==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0"
-      }
-    },
-    "@opentelemetry/instrumentation-grpc": {
-      "version": "0.52.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.52.0.tgz",
-      "integrity": "sha512-YYhA2pbhMWgF5Hp6eR7AHp1utzZQ3Y0VB8GIwd8zJoLtAuQRZa1N29DUtZ+t/pGRJF+xGPVI+vP+7ugHgeN0zQ==",
-      "requires": {
-        "@opentelemetry/instrumentation": "0.52.0",
-        "@opentelemetry/semantic-conventions": "1.25.0"
-      }
-    },
-    "@opentelemetry/instrumentation-hapi": {
-      "version": "0.40.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.40.0.tgz",
-      "integrity": "sha512-8U/w7Ifumtd2bSN1OLaSwAAFhb9FyqWUki3lMMB0ds+1+HdSxYBe9aspEJEgvxAqOkrQnVniAPTEGf1pGM7SOw==",
-      "requires": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      }
-    },
-    "@opentelemetry/instrumentation-http": {
-      "version": "0.52.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.52.0.tgz",
-      "integrity": "sha512-E6ywZuxTa4LnVXZGwL1oj3e2Eog1yIaNqa8KjKXoGkDNKte9/SjQnePXOmhQYI0A9nf0UyFbP9aKd+yHrkJXUA==",
-      "requires": {
-        "@opentelemetry/core": "1.25.0",
-        "@opentelemetry/instrumentation": "0.52.0",
-        "@opentelemetry/semantic-conventions": "1.25.0",
-        "semver": "^7.5.2"
-      }
-    },
-    "@opentelemetry/instrumentation-ioredis": {
-      "version": "0.42.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.42.0.tgz",
-      "integrity": "sha512-P11H168EKvBB9TUSasNDOGJCSkpT44XgoM6d3gRIWAa9ghLpYhl0uRkS8//MqPzcJVHr3h3RmfXIpiYLjyIZTw==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/redis-common": "^0.36.2",
-        "@opentelemetry/semantic-conventions": "^1.23.0"
-      }
-    },
-    "@opentelemetry/instrumentation-kafkajs": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.2.0.tgz",
-      "integrity": "sha512-uKKmhEFd0zR280tJovuiBG7cfnNZT4kvVTvqtHPxQP7nOmRbJstCYHFH13YzjVcKjkmoArmxiSulmZmF7SLIlg==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.24.0"
-      }
-    },
-    "@opentelemetry/instrumentation-knex": {
-      "version": "0.39.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.39.0.tgz",
-      "integrity": "sha512-lRwTqIKQecPWDkH1KEcAUcFhCaNssbKSpxf4sxRTAROCwrCEnYkjOuqJHV+q1/CApjMTaKu0Er4LBv/6bDpoxA==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      }
-    },
-    "@opentelemetry/instrumentation-koa": {
-      "version": "0.42.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.42.0.tgz",
-      "integrity": "sha512-H1BEmnMhho8o8HuNRq5zEI4+SIHDIglNB7BPKohZyWG4fWNuR7yM4GTlR01Syq21vODAS7z5omblScJD/eZdKw==",
-      "requires": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      }
-    },
-    "@opentelemetry/instrumentation-lru-memoizer": {
-      "version": "0.39.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.39.0.tgz",
-      "integrity": "sha512-eU1Wx1RRTR/2wYXFzH9gcpB8EPmhYlNDIUHzUXjyUE0CAXEJhBLkYNlzdaVCoQDw2neDqS+Woshiia6+emWK9A==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0"
-      }
-    },
-    "@opentelemetry/instrumentation-memcached": {
-      "version": "0.38.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-memcached/-/instrumentation-memcached-0.38.0.tgz",
-      "integrity": "sha512-tPmyqQEZNyrvg6G+iItdlguQEcGzfE+bJkpQifmBXmWBnoS5oU3UxqtyYuXGL2zI9qQM5yMBHH4nRXWALzy7WA==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.23.0",
-        "@types/memcached": "^2.2.6"
-      }
-    },
-    "@opentelemetry/instrumentation-mongodb": {
-      "version": "0.46.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.46.0.tgz",
-      "integrity": "sha512-VF/MicZ5UOBiXrqBslzwxhN7TVqzu1/LN/QDpkskqM0Zm0aZ4CVRbUygL8d7lrjLn15x5kGIe8VsSphMfPJzlA==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/sdk-metrics": "^1.9.1",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      }
-    },
-    "@opentelemetry/instrumentation-mongoose": {
-      "version": "0.41.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.41.0.tgz",
-      "integrity": "sha512-ivJg4QnnabFxxoI7K8D+in7hfikjte38sYzJB9v1641xJk9Esa7jM3hmbPB7lxwcgWJLVEDvfPwobt1if0tXxA==",
-      "requires": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      }
-    },
-    "@opentelemetry/instrumentation-mysql": {
-      "version": "0.40.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.40.0.tgz",
-      "integrity": "sha512-d7ja8yizsOCNMYIJt5PH/fKZXjb/mS48zLROO4BzZTtDfhNCl2UM/9VIomP2qkGIFVouSJrGr/T00EzY7bPtKA==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0",
-        "@types/mysql": "2.15.22"
-      }
-    },
-    "@opentelemetry/instrumentation-mysql2": {
-      "version": "0.40.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.40.0.tgz",
-      "integrity": "sha512-0xfS1xcqUmY7WE1uWjlmI67Xg3QsSUlNT+AcXHeA4BDUPwZtWqF4ezIwLgpVZfHOnkAEheqGfNSWd1PIu3Wnfg==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0",
-        "@opentelemetry/sql-common": "^0.40.1"
-      }
-    },
-    "@opentelemetry/instrumentation-nestjs-core": {
-      "version": "0.39.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.39.0.tgz",
-      "integrity": "sha512-mewVhEXdikyvIZoMIUry8eb8l3HUjuQjSjVbmLVTt4NQi35tkpnHQrG9bTRBrl3403LoWZ2njMPJyg4l6HfKvA==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.23.0"
-      }
-    },
-    "@opentelemetry/instrumentation-net": {
-      "version": "0.38.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-net/-/instrumentation-net-0.38.0.tgz",
-      "integrity": "sha512-stjow1PijcmUquSmRD/fSihm/H61DbjPlJuJhWUe7P22LFPjFhsrSeiB5vGj3vn+QGceNAs+kioUTzMGPbNxtg==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.23.0"
-      }
-    },
-    "@opentelemetry/instrumentation-pg": {
-      "version": "0.43.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.43.0.tgz",
-      "integrity": "sha512-og23KLyoxdnAeFs1UWqzSonuCkePUzCX30keSYigIzJe/6WSYA8rnEI5lobcxPEzg+GcU06J7jzokuEHbjVJNw==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0",
-        "@opentelemetry/sql-common": "^0.40.1",
-        "@types/pg": "8.6.1",
-        "@types/pg-pool": "2.0.4"
-      },
-      "dependencies": {
-        "@types/pg": {
-          "version": "8.6.1",
-          "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz",
-          "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==",
-          "requires": {
-            "@types/node": "*",
-            "pg-protocol": "*",
-            "pg-types": "^2.2.0"
-          }
-        }
-      }
-    },
-    "@opentelemetry/instrumentation-pino": {
-      "version": "0.41.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.41.0.tgz",
-      "integrity": "sha512-Kpv0fJRk/8iMzMk5Ue5BsUJfHkBJ2wQoIi/qduU1a1Wjx9GLj6J2G17PHjPK5mnZjPNzkFOXFADZMfgDioliQw==",
-      "requires": {
-        "@opentelemetry/api-logs": "^0.52.0",
-        "@opentelemetry/core": "^1.25.0",
-        "@opentelemetry/instrumentation": "^0.52.0"
-      }
-    },
-    "@opentelemetry/instrumentation-redis": {
-      "version": "0.41.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.41.0.tgz",
-      "integrity": "sha512-RJ1pwI3btykp67ts+5qZbaFSAAzacucwBet5/5EsKYtWBpHbWwV/qbGN/kIBzXg5WEZBhXLrR/RUq0EpEUpL3A==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/redis-common": "^0.36.2",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      }
-    },
-    "@opentelemetry/instrumentation-redis-4": {
-      "version": "0.41.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.41.1.tgz",
-      "integrity": "sha512-UqJAbxraBk7s7pQTlFi5ND4sAUs4r/Ai7gsAVZTQDbHl2kSsOp7gpHcpIuN5dpcI2xnuhM2tkH4SmEhbrv2S6Q==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/redis-common": "^0.36.2",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      }
-    },
-    "@opentelemetry/instrumentation-restify": {
-      "version": "0.40.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-restify/-/instrumentation-restify-0.40.0.tgz",
-      "integrity": "sha512-sm/rH/GysY/KOEvZqYBZSLYFeXlBkHCgqPDgWc07tz+bHCN6mPs9P3otGOSTe7o3KAIM8Nc6ncCO59vL+jb2cA==",
-      "requires": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      }
-    },
-    "@opentelemetry/instrumentation-router": {
-      "version": "0.39.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-router/-/instrumentation-router-0.39.0.tgz",
-      "integrity": "sha512-LaXnVmD69WPC4hNeLzKexCCS19hRLrUw3xicneAMkzJSzNJvPyk7G6I7lz7VjQh1cooObPBt9gNyd3hhTCUrag==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      }
-    },
-    "@opentelemetry/instrumentation-socket.io": {
-      "version": "0.41.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.41.0.tgz",
-      "integrity": "sha512-7fzDe9/FpO6NFizC/wnzXXX7bF9oRchsD//wFqy5g5hVEgXZCQ70IhxjrKdBvgjyIejR9T9zTvfQ6PfVKfkCAw==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      }
-    },
-    "@opentelemetry/instrumentation-tedious": {
-      "version": "0.13.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.13.0.tgz",
-      "integrity": "sha512-Pob0+0R62AqXH50pjazTeGBy/1+SK4CYpFUBV5t7xpbpeuQezkkgVGvLca84QqjBqQizcXedjpUJLgHQDixPQg==",
-      "requires": {
-        "@opentelemetry/instrumentation": "^0.52.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0",
-        "@types/tedious": "^4.0.14"
-      }
-    },
-    "@opentelemetry/instrumentation-undici": {
-      "version": "0.5.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.5.0.tgz",
-      "integrity": "sha512-aNTeSrFAVcM9qco5DfZ9DNXu6hpMRe8Kt8nCDHfMWDB3pwgGVUE76jTdohc+H/7eLRqh4L7jqs5NSQoHw7S6ww==",
-      "requires": {
-        "@opentelemetry/core": "^1.8.0",
-        "@opentelemetry/instrumentation": "^0.52.0"
-      }
-    },
-    "@opentelemetry/instrumentation-winston": {
-      "version": "0.39.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.39.0.tgz",
-      "integrity": "sha512-v/1xziLJ9CyB3YDjBSBzbB70Qd0JwWTo36EqWK5m3AR0CzsyMQQmf3ZIZM6sgx7hXMcRQ0pnEYhg6nhrUQPm9A==",
-      "requires": {
-        "@opentelemetry/api-logs": "^0.52.0",
-        "@opentelemetry/instrumentation": "^0.52.0"
-      }
-    },
-    "@opentelemetry/otlp-exporter-base": {
-      "version": "0.52.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.52.1.tgz",
-      "integrity": "sha512-z175NXOtX5ihdlshtYBe5RpGeBoTXVCKPPLiQlD6FHvpM4Ch+p2B0yWKYSrBfLH24H9zjJiBdTrtD+hLlfnXEQ==",
-      "requires": {
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/otlp-transformer": "0.52.1"
-      },
-      "dependencies": {
-        "@opentelemetry/core": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-          "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.25.1"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-          "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ=="
-        }
-      }
-    },
-    "@opentelemetry/otlp-grpc-exporter-base": {
-      "version": "0.52.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.52.1.tgz",
-      "integrity": "sha512-zo/YrSDmKMjG+vPeA9aBBrsQM9Q/f2zo6N04WMB3yNldJRsgpRBeLLwvAt/Ba7dpehDLOEFBd1i2JCoaFtpCoQ==",
-      "requires": {
-        "@grpc/grpc-js": "^1.7.1",
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/otlp-exporter-base": "0.52.1",
-        "@opentelemetry/otlp-transformer": "0.52.1"
-      },
-      "dependencies": {
-        "@opentelemetry/core": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-          "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.25.1"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-          "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ=="
-        }
-      }
-    },
-    "@opentelemetry/otlp-transformer": {
-      "version": "0.52.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.52.1.tgz",
-      "integrity": "sha512-I88uCZSZZtVa0XniRqQWKbjAUm73I8tpEy/uJYPPYw5d7BRdVk0RfTBQw8kSUl01oVWEuqxLDa802222MYyWHg==",
-      "requires": {
-        "@opentelemetry/api-logs": "0.52.1",
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/resources": "1.25.1",
-        "@opentelemetry/sdk-logs": "0.52.1",
-        "@opentelemetry/sdk-metrics": "1.25.1",
-        "@opentelemetry/sdk-trace-base": "1.25.1",
-        "protobufjs": "^7.3.0"
-      },
-      "dependencies": {
-        "@opentelemetry/api-logs": {
-          "version": "0.52.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz",
-          "integrity": "sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==",
-          "requires": {
-            "@opentelemetry/api": "^1.0.0"
-          }
-        },
-        "@opentelemetry/core": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-          "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.25.1"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-          "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ=="
-        }
-      }
-    },
-    "@opentelemetry/propagation-utils": {
-      "version": "0.30.10",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/propagation-utils/-/propagation-utils-0.30.10.tgz",
-      "integrity": "sha512-hhTW8pFp9PSyosYzzuUL9rdm7HF97w3OCyElufFHyUnYnKkCBbu8ne2LyF/KSdI/xZ81ubxWZs78hX4S7pLq5g==",
-      "requires": {}
-    },
-    "@opentelemetry/propagator-aws-xray": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-aws-xray/-/propagator-aws-xray-1.3.1.tgz",
-      "integrity": "sha512-6fDMzFlt5r6VWv7MUd0eOpglXPFqykW8CnOuUxJ1VZyLy6mV1bzBlzpsqEmhx1bjvZYvH93vhGkQZqrm95mlrQ==",
-      "requires": {
-        "@opentelemetry/core": "^1.0.0"
-      }
-    },
-    "@opentelemetry/propagator-b3": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.25.1.tgz",
-      "integrity": "sha512-p6HFscpjrv7//kE+7L+3Vn00VEDUJB0n6ZrjkTYHrJ58QZ8B3ajSJhRbCcY6guQ3PDjTbxWklyvIN2ojVbIb1A==",
-      "requires": {
-        "@opentelemetry/core": "1.25.1"
-      },
-      "dependencies": {
-        "@opentelemetry/core": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-          "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.25.1"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-          "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ=="
-        }
-      }
-    },
-    "@opentelemetry/propagator-jaeger": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.25.1.tgz",
-      "integrity": "sha512-nBprRf0+jlgxks78G/xq72PipVK+4or9Ypntw0gVZYNTCSK8rg5SeaGV19tV920CMqBD/9UIOiFr23Li/Q8tiA==",
-      "requires": {
-        "@opentelemetry/core": "1.25.1"
-      },
-      "dependencies": {
-        "@opentelemetry/core": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-          "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.25.1"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-          "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ=="
-        }
-      }
-    },
-    "@opentelemetry/redis-common": {
-      "version": "0.36.2",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz",
-      "integrity": "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g=="
-    },
-    "@opentelemetry/resource-detector-alibaba-cloud": {
-      "version": "0.29.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.29.0.tgz",
-      "integrity": "sha512-cYL1DfBwszTQcpzjiezzFkZp1bzevXjaVJ+VClrufHzH17S0RADcaLRQcLq4GqbWCGfvkJKUqBNz6f1SgfePgw==",
-      "requires": {
-        "@opentelemetry/resources": "^1.0.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0"
-      }
-    },
-    "@opentelemetry/resource-detector-aws": {
-      "version": "1.6.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-aws/-/resource-detector-aws-1.6.1.tgz",
-      "integrity": "sha512-A/3lqx9xoew7sFi+AVUUVr6VgB7UJ5qqddkKR3gQk9hWLm1R7HUXVJG09cLcZ7DMNpX13DohPRGmHE/vp1vafw==",
-      "requires": {
-        "@opentelemetry/core": "^1.0.0",
-        "@opentelemetry/resources": "^1.10.0",
-        "@opentelemetry/semantic-conventions": "^1.27.0"
-      },
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.27.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
-          "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="
-        }
-      }
-    },
-    "@opentelemetry/resource-detector-azure": {
-      "version": "0.2.11",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.2.11.tgz",
-      "integrity": "sha512-XepvQfTXWyHAoAziCfXGwYbSZL0LHtFk5iuKKN2VE2vzcoiw5Tepi0Qafuwb7CCtpQRReao4H7E29MFbCmh47g==",
-      "requires": {
-        "@opentelemetry/core": "^1.25.1",
-        "@opentelemetry/resources": "^1.10.1",
-        "@opentelemetry/semantic-conventions": "^1.27.0"
-      },
-      "dependencies": {
-        "@opentelemetry/core": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz",
-          "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.27.0"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.27.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
-          "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="
-        }
-      }
-    },
-    "@opentelemetry/resource-detector-container": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.4.1.tgz",
-      "integrity": "sha512-v0bvO6RxYtbxvY/HwqrPQnZ4UtP4nBq4AOyS30iqV2vEtiLTY1gNTbNvTF1lwN/gg/g5CY1tRSrHcYODDOv0vw==",
-      "requires": {
-        "@opentelemetry/resources": "^1.10.0",
-        "@opentelemetry/semantic-conventions": "^1.27.0"
-      },
-      "dependencies": {
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.27.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
-          "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="
-        }
-      }
-    },
-    "@opentelemetry/resource-detector-gcp": {
-      "version": "0.29.10",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.29.10.tgz",
-      "integrity": "sha512-rm2HKJ9lsdoVvrbmkr9dkOzg3Uk0FksXNxvNBgrCprM1XhMoJwThI5i0h/5sJypISUAJlEeJS6gn6nROj/NpkQ==",
-      "requires": {
-        "@opentelemetry/core": "^1.0.0",
-        "@opentelemetry/resources": "^1.0.0",
-        "@opentelemetry/semantic-conventions": "^1.22.0",
-        "gcp-metadata": "^6.0.0"
-      }
-    },
-    "@opentelemetry/resources": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.25.1.tgz",
-      "integrity": "sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ==",
-      "requires": {
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "dependencies": {
-        "@opentelemetry/core": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-          "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.25.1"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-          "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ=="
-        }
-      }
-    },
-    "@opentelemetry/sdk-logs": {
-      "version": "0.52.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.52.1.tgz",
-      "integrity": "sha512-MBYh+WcPPsN8YpRHRmK1Hsca9pVlyyKd4BxOC4SsgHACnl/bPp4Cri9hWhVm5+2tiQ9Zf4qSc1Jshw9tOLGWQA==",
-      "requires": {
-        "@opentelemetry/api-logs": "0.52.1",
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/resources": "1.25.1"
-      },
-      "dependencies": {
-        "@opentelemetry/api-logs": {
-          "version": "0.52.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz",
-          "integrity": "sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==",
-          "requires": {
-            "@opentelemetry/api": "^1.0.0"
-          }
-        },
-        "@opentelemetry/core": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-          "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.25.1"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-          "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ=="
-        }
-      }
-    },
-    "@opentelemetry/sdk-metrics": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.25.1.tgz",
-      "integrity": "sha512-9Mb7q5ioFL4E4dDrc4wC/A3NTHDat44v4I3p2pLPSxRvqUbDIQyMVr9uK+EU69+HWhlET1VaSrRzwdckWqY15Q==",
-      "requires": {
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/resources": "1.25.1",
-        "lodash.merge": "^4.6.2"
-      },
-      "dependencies": {
-        "@opentelemetry/core": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-          "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.25.1"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-          "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ=="
-        }
-      }
-    },
-    "@opentelemetry/sdk-node": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.53.0.tgz",
-      "integrity": "sha512-0hsxfq3BKy05xGktwG8YdGdxV978++x40EAKyKr1CaHZRh8uqVlXnclnl7OMi9xLMJEcXUw7lGhiRlArFcovyg==",
-      "requires": {
-        "@opentelemetry/api-logs": "0.53.0",
-        "@opentelemetry/core": "1.26.0",
-        "@opentelemetry/exporter-logs-otlp-grpc": "0.53.0",
-        "@opentelemetry/exporter-logs-otlp-http": "0.53.0",
-        "@opentelemetry/exporter-logs-otlp-proto": "0.53.0",
-        "@opentelemetry/exporter-trace-otlp-grpc": "0.53.0",
-        "@opentelemetry/exporter-trace-otlp-http": "0.53.0",
-        "@opentelemetry/exporter-trace-otlp-proto": "0.53.0",
-        "@opentelemetry/exporter-zipkin": "1.26.0",
-        "@opentelemetry/instrumentation": "0.53.0",
-        "@opentelemetry/resources": "1.26.0",
-        "@opentelemetry/sdk-logs": "0.53.0",
-        "@opentelemetry/sdk-metrics": "1.26.0",
-        "@opentelemetry/sdk-trace-base": "1.26.0",
-        "@opentelemetry/sdk-trace-node": "1.26.0",
-        "@opentelemetry/semantic-conventions": "1.27.0"
-      },
-      "dependencies": {
-        "@opentelemetry/api-logs": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz",
-          "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==",
-          "requires": {
-            "@opentelemetry/api": "^1.0.0"
-          }
-        },
-        "@opentelemetry/core": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz",
-          "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.27.0"
-          }
-        },
-        "@opentelemetry/exporter-trace-otlp-grpc": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.53.0.tgz",
-          "integrity": "sha512-m6KSh6OBDwfDjpzPVbuJbMgMbkoZfpxYH2r262KckgX9cMYvooWXEKzlJYsNDC6ADr28A1rtRoUVRwNfIN4tUg==",
-          "requires": {
-            "@grpc/grpc-js": "^1.7.1",
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/otlp-grpc-exporter-base": "0.53.0",
-            "@opentelemetry/otlp-transformer": "0.53.0",
-            "@opentelemetry/resources": "1.26.0",
-            "@opentelemetry/sdk-trace-base": "1.26.0"
-          }
-        },
-        "@opentelemetry/exporter-trace-otlp-http": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.53.0.tgz",
-          "integrity": "sha512-m7F5ZTq+V9mKGWYpX8EnZ7NjoqAU7VemQ1E2HAG+W/u0wpY1x0OmbxAXfGKFHCspdJk8UKlwPGrpcB8nay3P8A==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/otlp-exporter-base": "0.53.0",
-            "@opentelemetry/otlp-transformer": "0.53.0",
-            "@opentelemetry/resources": "1.26.0",
-            "@opentelemetry/sdk-trace-base": "1.26.0"
-          }
-        },
-        "@opentelemetry/exporter-trace-otlp-proto": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.53.0.tgz",
-          "integrity": "sha512-T/bdXslwRKj23S96qbvGtaYOdfyew3TjPEKOk5mHjkCmkVl1O9C/YMdejwSsdLdOq2YW30KjR9kVi0YMxZushQ==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/otlp-exporter-base": "0.53.0",
-            "@opentelemetry/otlp-transformer": "0.53.0",
-            "@opentelemetry/resources": "1.26.0",
-            "@opentelemetry/sdk-trace-base": "1.26.0"
-          }
-        },
-        "@opentelemetry/exporter-zipkin": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.26.0.tgz",
-          "integrity": "sha512-PW5R34n3SJHO4t0UetyHKiXL6LixIqWN6lWncg3eRXhKuT30x+b7m5sDJS0kEWRfHeS+kG7uCw2vBzmB2lk3Dw==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/resources": "1.26.0",
-            "@opentelemetry/sdk-trace-base": "1.26.0",
-            "@opentelemetry/semantic-conventions": "1.27.0"
-          }
-        },
-        "@opentelemetry/instrumentation": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz",
-          "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==",
-          "requires": {
-            "@opentelemetry/api-logs": "0.53.0",
-            "@types/shimmer": "^1.2.0",
-            "import-in-the-middle": "^1.8.1",
-            "require-in-the-middle": "^7.1.1",
-            "semver": "^7.5.2",
-            "shimmer": "^1.2.1"
-          }
-        },
-        "@opentelemetry/otlp-exporter-base": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz",
-          "integrity": "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/otlp-transformer": "0.53.0"
-          }
-        },
-        "@opentelemetry/otlp-grpc-exporter-base": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.53.0.tgz",
-          "integrity": "sha512-F7RCN8VN+lzSa4fGjewit8Z5fEUpY/lmMVy5EWn2ZpbAabg3EE3sCLuTNfOiooNGnmvzimUPruoeqeko/5/TzQ==",
-          "requires": {
-            "@grpc/grpc-js": "^1.7.1",
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/otlp-exporter-base": "0.53.0",
-            "@opentelemetry/otlp-transformer": "0.53.0"
-          }
-        },
-        "@opentelemetry/otlp-transformer": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.53.0.tgz",
-          "integrity": "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==",
-          "requires": {
-            "@opentelemetry/api-logs": "0.53.0",
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/resources": "1.26.0",
-            "@opentelemetry/sdk-logs": "0.53.0",
-            "@opentelemetry/sdk-metrics": "1.26.0",
-            "@opentelemetry/sdk-trace-base": "1.26.0",
-            "protobufjs": "^7.3.0"
-          }
-        },
-        "@opentelemetry/propagator-b3": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.26.0.tgz",
-          "integrity": "sha512-vvVkQLQ/lGGyEy9GT8uFnI047pajSOVnZI2poJqVGD3nJ+B9sFGdlHNnQKophE3lHfnIH0pw2ubrCTjZCgIj+Q==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0"
-          }
-        },
-        "@opentelemetry/propagator-jaeger": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.26.0.tgz",
-          "integrity": "sha512-DelFGkCdaxA1C/QA0Xilszfr0t4YbGd3DjxiCDPh34lfnFr+VkkrjV9S8ZTJvAzfdKERXhfOxIKBoGPJwoSz7Q==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0"
-          }
-        },
-        "@opentelemetry/resources": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz",
-          "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/semantic-conventions": "1.27.0"
-          }
-        },
-        "@opentelemetry/sdk-logs": {
-          "version": "0.53.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.53.0.tgz",
-          "integrity": "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==",
-          "requires": {
-            "@opentelemetry/api-logs": "0.53.0",
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/resources": "1.26.0"
-          }
-        },
-        "@opentelemetry/sdk-metrics": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz",
-          "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/resources": "1.26.0"
-          }
-        },
-        "@opentelemetry/sdk-trace-base": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz",
-          "integrity": "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==",
-          "requires": {
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/resources": "1.26.0",
-            "@opentelemetry/semantic-conventions": "1.27.0"
-          }
-        },
-        "@opentelemetry/sdk-trace-node": {
-          "version": "1.26.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.26.0.tgz",
-          "integrity": "sha512-Fj5IVKrj0yeUwlewCRwzOVcr5avTuNnMHWf7GPc1t6WaT78J6CJyF3saZ/0RkZfdeNO8IcBl/bNcWMVZBMRW8Q==",
-          "requires": {
-            "@opentelemetry/context-async-hooks": "1.26.0",
-            "@opentelemetry/core": "1.26.0",
-            "@opentelemetry/propagator-b3": "1.26.0",
-            "@opentelemetry/propagator-jaeger": "1.26.0",
-            "@opentelemetry/sdk-trace-base": "1.26.0",
-            "semver": "^7.5.2"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.27.0",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
-          "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="
-        },
-        "import-in-the-middle": {
-          "version": "1.11.0",
-          "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz",
-          "integrity": "sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q==",
-          "requires": {
-            "acorn": "^8.8.2",
-            "acorn-import-attributes": "^1.9.5",
-            "cjs-module-lexer": "^1.2.2",
-            "module-details-from-path": "^1.0.3"
-          }
-        }
-      }
-    },
-    "@opentelemetry/sdk-trace-base": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz",
-      "integrity": "sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw==",
-      "requires": {
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/resources": "1.25.1",
-        "@opentelemetry/semantic-conventions": "1.25.1"
-      },
-      "dependencies": {
-        "@opentelemetry/core": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-          "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.25.1"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-          "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ=="
-        }
-      }
-    },
-    "@opentelemetry/sdk-trace-node": {
-      "version": "1.25.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.25.1.tgz",
-      "integrity": "sha512-nMcjFIKxnFqoez4gUmihdBrbpsEnAX/Xj16sGvZm+guceYE0NE00vLhpDVK6f3q8Q4VFI5xG8JjlXKMB/SkTTQ==",
-      "requires": {
-        "@opentelemetry/context-async-hooks": "1.25.1",
-        "@opentelemetry/core": "1.25.1",
-        "@opentelemetry/propagator-b3": "1.25.1",
-        "@opentelemetry/propagator-jaeger": "1.25.1",
-        "@opentelemetry/sdk-trace-base": "1.25.1",
-        "semver": "^7.5.2"
-      },
-      "dependencies": {
-        "@opentelemetry/context-async-hooks": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.25.1.tgz",
-          "integrity": "sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ==",
-          "requires": {}
-        },
-        "@opentelemetry/core": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
-          "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
-          "requires": {
-            "@opentelemetry/semantic-conventions": "1.25.1"
-          }
-        },
-        "@opentelemetry/semantic-conventions": {
-          "version": "1.25.1",
-          "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
-          "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ=="
-        }
-      }
-    },
-    "@opentelemetry/semantic-conventions": {
-      "version": "1.25.0",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.0.tgz",
-      "integrity": "sha512-M+kkXKRAIAiAP6qYyesfrC5TOmDpDVtsxuGfPcqd9B/iBrac+E14jYwrgm0yZBUIbIP2OnqC3j+UgkXLm1vxUQ=="
-    },
-    "@opentelemetry/sql-common": {
-      "version": "0.40.1",
-      "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz",
-      "integrity": "sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==",
-      "requires": {
-        "@opentelemetry/core": "^1.1.0"
-      }
-    },
-    "@photostructure/tz-lookup": {
-      "version": "10.0.0",
-      "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-10.0.0.tgz",
-      "integrity": "sha512-8ZAjoj/irCuvUlyEinQ/HB6A8hP3bD1dgTOZvfl1b9nAwqniutFDHOQRcGM6Crea68bOwPj010f0Z4KkmuLHEA=="
-    },
-    "@pkgjs/parseargs": {
-      "version": "0.11.0",
-      "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
-      "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
-      "optional": true
-    },
-    "@pkgr/core": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.0.tgz",
-      "integrity": "sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ==",
-      "dev": true
-    },
-    "@polka/url": {
-      "version": "1.0.0-next.25",
-      "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz",
-      "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ=="
-    },
-    "@protobufjs/aspromise": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
-      "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
-    },
-    "@protobufjs/base64": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
-      "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
-    },
-    "@protobufjs/codegen": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
-      "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
-    },
-    "@protobufjs/eventemitter": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
-      "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
-    },
-    "@protobufjs/fetch": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
-      "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
-      "requires": {
-        "@protobufjs/aspromise": "^1.1.1",
-        "@protobufjs/inquire": "^1.1.0"
-      }
-    },
-    "@protobufjs/float": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
-      "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
-    },
-    "@protobufjs/inquire": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
-      "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
-    },
-    "@protobufjs/path": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
-      "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
-    },
-    "@protobufjs/pool": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
-      "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
-    },
-    "@protobufjs/utf8": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
-      "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
-    },
-    "@react-email/body": {
-      "version": "0.0.10",
-      "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.10.tgz",
-      "integrity": "sha512-dMJyL9aU25ieatdPtVjCyQ/WHZYHwNc+Hy/XpF8Cc18gu21cUynVEeYQzFSeigDRMeBQ3PGAyjVDPIob7YlGwA==",
-      "requires": {}
-    },
-    "@react-email/button": {
-      "version": "0.0.17",
-      "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.17.tgz",
-      "integrity": "sha512-ioHdsk+BpGS/PqjU6JS7tUrVy9yvbUx92Z+Cem2+MbYp55oEwQ9VHf7u4f5NoM0gdhfKSehBwRdYlHt/frEMcg==",
-      "requires": {}
-    },
-    "@react-email/code-block": {
-      "version": "0.0.8",
-      "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.8.tgz",
-      "integrity": "sha512-WbuAEpTnB262i9C3SGPmmErgZ4iU5KIpqLUjr7uBJijqldLqZc5x39e8wPWaRdF7NLcShmrc/+G7GJgI1bdC5w==",
-      "requires": {
-        "prismjs": "1.29.0"
-      }
-    },
-    "@react-email/code-inline": {
-      "version": "0.0.4",
-      "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.4.tgz",
-      "integrity": "sha512-zj3oMQiiUCZbddSNt3k0zNfIBFK0ZNDIzzDyBaJKy6ZASTtWfB+1WFX0cpTX8q0gUiYK+A94rk5Qp68L6YXjXQ==",
-      "requires": {}
-    },
-    "@react-email/column": {
-      "version": "0.0.12",
-      "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.12.tgz",
-      "integrity": "sha512-Rsl7iSdDaeHZO938xb+0wR5ud0Z3MVfdtPbNKJNojZi2hApwLAQXmDrnn/AcPDM5Lpl331ZljJS8vHTWxxkvKw==",
-      "requires": {}
-    },
-    "@react-email/components": {
-      "version": "0.0.24",
-      "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.24.tgz",
-      "integrity": "sha512-/DNmfTREaT59UFdkHoIK3BewJ214LfRxmduiil3m7POj+gougkItANu1+BMmgbUATxjf7jH1WoBxo9x/rhFEFw==",
-      "requires": {
-        "@react-email/body": "0.0.10",
-        "@react-email/button": "0.0.17",
-        "@react-email/code-block": "0.0.8",
-        "@react-email/code-inline": "0.0.4",
-        "@react-email/column": "0.0.12",
-        "@react-email/container": "0.0.14",
-        "@react-email/font": "0.0.8",
-        "@react-email/head": "0.0.11",
-        "@react-email/heading": "0.0.14",
-        "@react-email/hr": "0.0.10",
-        "@react-email/html": "0.0.10",
-        "@react-email/img": "0.0.10",
-        "@react-email/link": "0.0.10",
-        "@react-email/markdown": "0.0.12",
-        "@react-email/preview": "0.0.11",
-        "@react-email/render": "1.0.1",
-        "@react-email/row": "0.0.10",
-        "@react-email/section": "0.0.14",
-        "@react-email/tailwind": "0.1.0",
-        "@react-email/text": "0.0.10"
-      }
-    },
-    "@react-email/container": {
-      "version": "0.0.14",
-      "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.14.tgz",
-      "integrity": "sha512-NgoaJJd9tTtsrveL86Ocr/AYLkGyN3prdXKd/zm5fQpfDhy/NXezyT3iF6VlwAOEUIu64ErHpAJd+P6ygR+vjg==",
-      "requires": {}
-    },
-    "@react-email/font": {
-      "version": "0.0.8",
-      "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.8.tgz",
-      "integrity": "sha512-fSBEqYyVPAyyACBBHcs3wEYzNknpHMuwcSAAKE8fOoDfGqURr/vSxKPdh4tOa9z7G4hlcEfgGrCYEa2iPT22cw==",
-      "requires": {}
-    },
-    "@react-email/head": {
-      "version": "0.0.11",
-      "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.11.tgz",
-      "integrity": "sha512-skw5FUgyamIMK+LN+fZQ5WIKQYf0dPiRAvsUAUR2eYoZp9oRsfkIpFHr0GWPkKAYjFEj+uJjaxQ/0VzQH7svVg==",
-      "requires": {}
-    },
-    "@react-email/heading": {
-      "version": "0.0.14",
-      "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.14.tgz",
-      "integrity": "sha512-jZM7IVuZOXa0G110ES8OkxajPTypIKlzlO1K1RIe1auk76ukQRiCg1IRV4HZlWk1GGUbec5hNxsvZa2kU8cb9w==",
-      "requires": {}
-    },
-    "@react-email/hr": {
-      "version": "0.0.10",
-      "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.10.tgz",
-      "integrity": "sha512-3AA4Yjgl3zEid/KVx6uf6TuLJHVZvUc2cG9Wm9ZpWeAX4ODA+8g9HyuC0tfnjbRsVMhMcCGiECuWWXINi+60vA==",
-      "requires": {}
-    },
-    "@react-email/html": {
-      "version": "0.0.10",
-      "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.10.tgz",
-      "integrity": "sha512-06uiuSKJBWQJfhCKv4MPupELei4Lepyz9Sth7Yq7Fq29CAeB1ejLgKkGqn1I+FZ72hQxPLdYF4iq4yloKv3JCg==",
-      "requires": {}
-    },
-    "@react-email/img": {
-      "version": "0.0.10",
-      "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.10.tgz",
-      "integrity": "sha512-pJ8glJjDNaJ53qoM95pvX9SK05yh0bNQY/oyBKmxlBDdUII6ixuMc3SCwYXPMl+tgkQUyDgwEBpSTrLAnjL3hA==",
-      "requires": {}
-    },
-    "@react-email/link": {
-      "version": "0.0.10",
-      "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.10.tgz",
-      "integrity": "sha512-tva3wvAWSR10lMJa9fVA09yRn7pbEki0ZZpHE6GD1jKbFhmzt38VgLO9B797/prqoDZdAr4rVK7LJFcdPx3GwA==",
-      "requires": {}
-    },
-    "@react-email/markdown": {
-      "version": "0.0.12",
-      "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.12.tgz",
-      "integrity": "sha512-wsuvj1XAb6O63aizCLNEeqVgKR3oFjAwt9vjfg2y2oh4G1dZeo8zonZM2x1fmkEkBZhzwSHraNi70jSXhA3A9w==",
-      "requires": {
-        "md-to-react-email": "5.0.2"
-      }
-    },
-    "@react-email/preview": {
-      "version": "0.0.11",
-      "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.11.tgz",
-      "integrity": "sha512-7O/CT4b16YlSGrj18htTPx3Vbhu2suCGv/cSe5c+fuSrIM/nMiBSZ3Js16Vj0XJbAmmmlVmYFZw9L20wXJ+LjQ==",
-      "requires": {}
-    },
-    "@react-email/render": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.1.tgz",
-      "integrity": "sha512-W3gTrcmLOVYnG80QuUp22ReIT/xfLsVJ+n7ghSlG2BITB8evNABn1AO2rGQoXuK84zKtDAlxCdm3hRyIpZdGSA==",
-      "requires": {
-        "html-to-text": "9.0.5",
-        "js-beautify": "^1.14.11",
-        "react-promise-suspense": "0.3.4"
-      }
-    },
-    "@react-email/row": {
-      "version": "0.0.10",
-      "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.10.tgz",
-      "integrity": "sha512-jPyEhG3gsLX+Eb9U+A30fh0gK6hXJwF4ghJ+ZtFQtlKAKqHX+eCpWlqB3Xschd/ARJLod8WAswg0FB+JD9d0/A==",
-      "requires": {}
-    },
-    "@react-email/section": {
-      "version": "0.0.14",
-      "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.14.tgz",
-      "integrity": "sha512-+fYWLb4tPU1A/+GE5J1+SEMA7/wR3V30lQ+OR9t2kAJqNrARDbMx0bLnYnR1QL5TiFRz0pCF05SQUobk6gHEDQ==",
-      "requires": {}
-    },
-    "@react-email/tailwind": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-0.1.0.tgz",
-      "integrity": "sha512-qysVUEY+M3SKUvu35XDpzn7yokhqFOT3tPU6Mj/pgc62TL5tQFj6msEbBtwoKs2qO3WZvai0DIHdLhaOxBQSow==",
-      "requires": {}
-    },
-    "@react-email/text": {
-      "version": "0.0.10",
-      "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.10.tgz",
-      "integrity": "sha512-wNAnxeEAiFs6N+SxS0y6wTJWfewEzUETuyS2aZmT00xk50VijwyFRuhm4sYSjusMyshevomFwz5jNISCxRsGWw==",
-      "requires": {}
-    },
-    "@rollup/pluginutils": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
-      "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==",
-      "dev": true,
-      "requires": {
-        "@types/estree": "^1.0.0",
-        "estree-walker": "^2.0.2",
-        "picomatch": "^2.3.1"
-      },
-      "dependencies": {
-        "estree-walker": {
-          "version": "2.0.2",
-          "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
-          "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
-          "dev": true
-        },
-        "picomatch": {
-          "version": "2.3.1",
-          "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-          "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
-          "dev": true
-        }
-      }
-    },
-    "@rollup/rollup-android-arm-eabi": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.3.tgz",
-      "integrity": "sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g==",
-      "dev": true,
-      "optional": true
-    },
-    "@rollup/rollup-android-arm64": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.3.tgz",
-      "integrity": "sha512-eQK5JIi+POhFpzk+LnjKIy4Ks+pwJ+NXmPxOCSvOKSNRPONzKuUvWE+P9JxGZVxrtzm6BAYMaL50FFuPe0oWMQ==",
-      "dev": true,
-      "optional": true
-    },
-    "@rollup/rollup-darwin-arm64": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.3.tgz",
-      "integrity": "sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA==",
-      "dev": true,
-      "optional": true
-    },
-    "@rollup/rollup-darwin-x64": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.3.tgz",
-      "integrity": "sha512-0IMAO21axJeNIrvS9lSe/PGthc8ZUS+zC53O0VhF5gMxfmcKAP4ESkKOCwEi6u2asUrt4mQv2rjY8QseIEb1aw==",
-      "dev": true,
-      "optional": true
-    },
-    "@rollup/rollup-linux-arm-gnueabihf": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.3.tgz",
-      "integrity": "sha512-ge2DC7tHRHa3caVEoSbPRJpq7azhG+xYsd6u2MEnJ6XzPSzQsTKyXvh6iWjXRf7Rt9ykIUWHtl0Uz3T6yXPpKw==",
-      "dev": true,
-      "optional": true
-    },
-    "@rollup/rollup-linux-arm-musleabihf": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.14.3.tgz",
-      "integrity": "sha512-ljcuiDI4V3ySuc7eSk4lQ9wU8J8r8KrOUvB2U+TtK0TiW6OFDmJ+DdIjjwZHIw9CNxzbmXY39wwpzYuFDwNXuw==",
-      "dev": true,
-      "optional": true
-    },
-    "@rollup/rollup-linux-arm64-gnu": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.3.tgz",
-      "integrity": "sha512-Eci2us9VTHm1eSyn5/eEpaC7eP/mp5n46gTRB3Aar3BgSvDQGJZuicyq6TsH4HngNBgVqC5sDYxOzTExSU+NjA==",
-      "dev": true,
-      "optional": true
-    },
-    "@rollup/rollup-linux-arm64-musl": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.3.tgz",
-      "integrity": "sha512-UrBoMLCq4E92/LCqlh+blpqMz5h1tJttPIniwUgOFJyjWI1qrtrDhhpHPuFxULlUmjFHfloWdixtDhSxJt5iKw==",
-      "dev": true,
-      "optional": true
-    },
-    "@rollup/rollup-linux-powerpc64le-gnu": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.3.tgz",
-      "integrity": "sha512-5aRjvsS8q1nWN8AoRfrq5+9IflC3P1leMoy4r2WjXyFqf3qcqsxRCfxtZIV58tCxd+Yv7WELPcO9mY9aeQyAmw==",
-      "dev": true,
-      "optional": true
-    },
-    "@rollup/rollup-linux-riscv64-gnu": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.3.tgz",
-      "integrity": "sha512-sk/Qh1j2/RJSX7FhEpJn8n0ndxy/uf0kI/9Zc4b1ELhqULVdTfN6HL31CDaTChiBAOgLcsJ1sgVZjWv8XNEsAQ==",
-      "dev": true,
-      "optional": true
-    },
-    "@rollup/rollup-linux-s390x-gnu": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.3.tgz",
-      "integrity": "sha512-jOO/PEaDitOmY9TgkxF/TQIjXySQe5KVYB57H/8LRP/ux0ZoO8cSHCX17asMSv3ruwslXW/TLBcxyaUzGRHcqg==",
-      "dev": true,
-      "optional": true
-    },
-    "@rollup/rollup-linux-x64-gnu": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.3.tgz",
-      "integrity": "sha512-8ybV4Xjy59xLMyWo3GCfEGqtKV5M5gCSrZlxkPGvEPCGDLNla7v48S662HSGwRd6/2cSneMQWiv+QzcttLrrOA==",
-      "dev": true,
-      "optional": true
-    },
-    "@rollup/rollup-linux-x64-musl": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.3.tgz",
-      "integrity": "sha512-s+xf1I46trOY10OqAtZ5Rm6lzHre/UiLA1J2uOhCFXWkbZrJRkYBPO6FhvGfHmdtQ3Bx793MNa7LvoWFAm93bg==",
-      "dev": true,
-      "optional": true
-    },
-    "@rollup/rollup-win32-arm64-msvc": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.3.tgz",
-      "integrity": "sha512-+4h2WrGOYsOumDQ5S2sYNyhVfrue+9tc9XcLWLh+Kw3UOxAvrfOrSMFon60KspcDdytkNDh7K2Vs6eMaYImAZg==",
-      "dev": true,
-      "optional": true
-    },
-    "@rollup/rollup-win32-ia32-msvc": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.3.tgz",
-      "integrity": "sha512-T1l7y/bCeL/kUwh9OD4PQT4aM7Bq43vX05htPJJ46RTI4r5KNt6qJRzAfNfM+OYMNEVBWQzR2Gyk+FXLZfogGw==",
-      "dev": true,
-      "optional": true
-    },
-    "@rollup/rollup-win32-x64-msvc": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.3.tgz",
-      "integrity": "sha512-/BypzV0H1y1HzgYpxqRaXGBRqfodgoBBCcsrujT6QRcakDQdfU+Lq9PENPh5jB4I44YWq+0C2eHsHya+nZY1sA==",
-      "dev": true,
-      "optional": true
-    },
-    "@selderee/plugin-htmlparser2": {
-      "version": "0.11.0",
-      "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
-      "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==",
-      "requires": {
-        "domhandler": "^5.0.3",
-        "selderee": "^0.11.0"
-      }
-    },
-    "@sideway/address": {
-      "version": "4.1.5",
-      "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
-      "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
-      "requires": {
-        "@hapi/hoek": "^9.0.0"
-      }
-    },
-    "@sideway/formula": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
-      "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
-    },
-    "@sideway/pinpoint": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
-      "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
-    },
-    "@socket.io/component-emitter": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
-      "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
-    },
-    "@socket.io/redis-adapter": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@socket.io/redis-adapter/-/redis-adapter-8.3.0.tgz",
-      "integrity": "sha512-ly0cra+48hDmChxmIpnESKrc94LjRL80TEmZVscuQ/WWkRP81nNj8W8cCGMqbI4L6NCuAaPRSzZF1a9GlAxxnA==",
-      "requires": {
-        "debug": "~4.3.1",
-        "notepack.io": "~3.0.1",
-        "uid2": "1.0.0"
-      }
-    },
-    "@sqltools/formatter": {
-      "version": "1.2.5",
-      "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz",
-      "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw=="
-    },
-    "@swc/core": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.21.tgz",
-      "integrity": "sha512-7/cN0SZ+y2V6e0hsDD8koGR0QVh7Jl3r756bwaHLLSN+kReoUb/yVcLsA8iTn90JLME3DkQK4CPjxDCQiyMXNg==",
-      "devOptional": true,
-      "requires": {
-        "@swc/core-darwin-arm64": "1.7.21",
-        "@swc/core-darwin-x64": "1.7.21",
-        "@swc/core-linux-arm-gnueabihf": "1.7.21",
-        "@swc/core-linux-arm64-gnu": "1.7.21",
-        "@swc/core-linux-arm64-musl": "1.7.21",
-        "@swc/core-linux-x64-gnu": "1.7.21",
-        "@swc/core-linux-x64-musl": "1.7.21",
-        "@swc/core-win32-arm64-msvc": "1.7.21",
-        "@swc/core-win32-ia32-msvc": "1.7.21",
-        "@swc/core-win32-x64-msvc": "1.7.21",
-        "@swc/counter": "^0.1.3",
-        "@swc/types": "^0.1.12"
-      }
-    },
-    "@swc/core-darwin-arm64": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.21.tgz",
-      "integrity": "sha512-hh5uOZ7jWF66z2TRMhhXtWMQkssuPCSIZPy9VHf5KvZ46cX+5UeECDthchYklEVZQyy4Qr6oxfh4qff/5spoMA==",
-      "dev": true,
-      "optional": true
-    },
-    "@swc/core-darwin-x64": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.21.tgz",
-      "integrity": "sha512-lTsPquqSierQ6jWiWM7NnYXXZGk9zx3NGkPLHjPbcH5BmyiauX0CC/YJYJx7YmS2InRLyALlGmidHkaF4JY28A==",
-      "dev": true,
-      "optional": true
-    },
-    "@swc/core-linux-arm-gnueabihf": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.21.tgz",
-      "integrity": "sha512-AgSd0fnSzAqCvWpzzZCq75z62JVGUkkXEOpfdi99jj/tryPy38KdXJtkVWJmufPXlRHokGTBitalk33WDJwsbA==",
-      "dev": true,
-      "optional": true
-    },
-    "@swc/core-linux-arm64-gnu": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.21.tgz",
-      "integrity": "sha512-l+jw6RQ4Y43/8dIst0c73uQE+W3kCWrCFqMqC/xIuE/iqHOnvYK6YbA1ffOct2dImkHzNiKuoehGqtQAc6cNaQ==",
-      "dev": true,
-      "optional": true
-    },
-    "@swc/core-linux-arm64-musl": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.21.tgz",
-      "integrity": "sha512-29KKZXrTo/c9F1JFL9WsNvCa6UCdIVhHP5EfuYhlKbn5/YmSsNFkuHdUtZFEd5U4+jiShXDmgGCtLW2d08LIwg==",
-      "dev": true,
-      "optional": true
-    },
-    "@swc/core-linux-x64-gnu": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.21.tgz",
-      "integrity": "sha512-HsP3JwddvQj5HvnjmOr+Bd5plEm6ccpfP5wUlm3hywzvdVkj+yR29bmD7UwpV/1zCQ60Ry35a7mXhKI6HQxFgw==",
-      "dev": true,
-      "optional": true
-    },
-    "@swc/core-linux-x64-musl": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.21.tgz",
-      "integrity": "sha512-hYKLVeUTHqvFK628DFJEwxoX6p42T3HaQ4QjNtf3oKhiJWFh9iTRUrN/oCB5YI3R9WMkFkKh+99gZ/Dd0T5lsg==",
-      "dev": true,
-      "optional": true
-    },
-    "@swc/core-win32-arm64-msvc": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.21.tgz",
-      "integrity": "sha512-qyWAKW10aMBe6iUqeZ7NAJIswjfggVTUpDINpQGUJhz+pR71YZDidXgZXpaDB84YyDB2JAlRqd1YrLkl7CMiIw==",
-      "dev": true,
-      "optional": true
-    },
-    "@swc/core-win32-ia32-msvc": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.21.tgz",
-      "integrity": "sha512-cy61wS3wgH5mEwBiQ5w6/FnQrchBDAdPsSh0dKSzNmI+4K8hDxS8uzdBycWqJXO0cc+mA77SIlwZC3hP3Kum2g==",
-      "dev": true,
-      "optional": true
-    },
-    "@swc/core-win32-x64-msvc": {
-      "version": "1.7.21",
-      "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.21.tgz",
-      "integrity": "sha512-/rexGItJURNJOdae+a48M+loT74nsEU+PyRRVAkZMKNRtLoYFAr0cpDlS5FodIgGunp/nqM0bst4H2w6Y05IKA==",
-      "dev": true,
-      "optional": true
-    },
-    "@swc/counter": {
-      "version": "0.1.3",
-      "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
-      "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="
-    },
-    "@swc/helpers": {
-      "version": "0.5.5",
-      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
-      "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
-      "requires": {
-        "@swc/counter": "^0.1.3",
-        "tslib": "^2.4.0"
-      }
-    },
-    "@swc/types": {
-      "version": "0.1.12",
-      "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz",
-      "integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==",
-      "devOptional": true,
-      "requires": {
-        "@swc/counter": "^0.1.3"
-      }
-    },
-    "@testcontainers/postgresql": {
-      "version": "10.12.0",
-      "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.12.0.tgz",
-      "integrity": "sha512-n0Q0Btx0R923CDgm6KBXbesPH10CewpuuCPcnmEZzon3IneMzdk4UqVhhNNOeJFDGhtFrZBOoJ1o7CUI4J0vQw==",
-      "dev": true,
-      "requires": {
-        "testcontainers": "^10.12.0"
-      }
-    },
-    "@tsconfig/node10": {
-      "version": "1.0.9",
-      "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
-      "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
-      "optional": true,
-      "peer": true
-    },
-    "@tsconfig/node12": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
-      "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
-      "optional": true,
-      "peer": true
-    },
-    "@tsconfig/node14": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
-      "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
-      "optional": true,
-      "peer": true
-    },
-    "@tsconfig/node16": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
-      "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
-      "optional": true,
-      "peer": true
-    },
-    "@turf/boolean-point-in-polygon": {
-      "version": "6.5.0",
-      "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz",
-      "integrity": "sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A==",
-      "requires": {
-        "@turf/helpers": "^6.5.0",
-        "@turf/invariant": "^6.5.0"
-      }
-    },
-    "@turf/helpers": {
-      "version": "6.5.0",
-      "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
-      "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw=="
-    },
-    "@turf/invariant": {
-      "version": "6.5.0",
-      "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz",
-      "integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==",
-      "requires": {
-        "@turf/helpers": "^6.5.0"
-      }
-    },
-    "@types/archiver": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.2.tgz",
-      "integrity": "sha512-KmROQqbQzKGuaAbmK+ZcytkJ51+YqDa7NmbXjmtC5YBLSyQYo21YaUnQ3HbaPFKL1ooo6RQ6OPYPIDyxfpDDXw==",
-      "dev": true,
-      "requires": {
-        "@types/readdir-glob": "*"
-      }
-    },
-    "@types/async-lock": {
-      "version": "1.4.2",
-      "resolved": "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.4.2.tgz",
-      "integrity": "sha512-HlZ6Dcr205BmNhwkdXqrg2vkFMN2PluI7Lgr8In3B3wE5PiQHhjRqtW/lGdVU9gw+sM0JcIDx2AN+cW8oSWIcw==",
-      "dev": true
-    },
-    "@types/aws-lambda": {
-      "version": "8.10.122",
-      "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.122.tgz",
-      "integrity": "sha512-vBkIh9AY22kVOCEKo5CJlyCgmSWvasC+SWUxL/x/vOwRobMpI/HG1xp/Ae3AqmSiZeLUbOhW0FCD3ZjqqUxmXw=="
-    },
-    "@types/bcrypt": {
-      "version": "5.0.2",
-      "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz",
-      "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==",
-      "dev": true,
-      "requires": {
-        "@types/node": "*"
-      }
-    },
-    "@types/body-parser": {
-      "version": "1.19.3",
-      "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz",
-      "integrity": "sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==",
-      "dev": true,
-      "requires": {
-        "@types/connect": "*",
-        "@types/node": "*"
-      }
-    },
-    "@types/bunyan": {
-      "version": "1.8.9",
-      "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.9.tgz",
-      "integrity": "sha512-ZqS9JGpBxVOvsawzmVt30sP++gSQMTejCkIAQ3VdadOcRE8izTyW66hufvwLeH+YEGP6Js2AW7Gz+RMyvrEbmw==",
-      "requires": {
-        "@types/node": "*"
-      }
-    },
-    "@types/connect": {
-      "version": "3.4.36",
-      "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz",
-      "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==",
-      "requires": {
-        "@types/node": "*"
-      }
-    },
-    "@types/cookie": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
-      "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
-    },
-    "@types/cookie-parser": {
-      "version": "1.4.7",
-      "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.7.tgz",
-      "integrity": "sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==",
-      "dev": true,
-      "requires": {
-        "@types/express": "*"
-      }
-    },
-    "@types/cookiejar": {
-      "version": "2.1.5",
-      "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
-      "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
-      "dev": true
-    },
-    "@types/cors": {
-      "version": "2.8.14",
-      "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.14.tgz",
-      "integrity": "sha512-RXHUvNWYICtbP6s18PnOCaqToK8y14DnLd75c6HfyKf228dxy7pHNOQkxPtvXKp/hINFMDjbYzsj63nnpPMSRQ==",
-      "requires": {
-        "@types/node": "*"
-      }
-    },
-    "@types/docker-modem": {
-      "version": "3.0.6",
-      "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz",
-      "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==",
-      "dev": true,
-      "requires": {
-        "@types/node": "*",
-        "@types/ssh2": "*"
-      }
-    },
-    "@types/dockerode": {
-      "version": "3.3.29",
-      "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.29.tgz",
-      "integrity": "sha512-5PRRq/yt5OT/Jf77ltIdz4EiR9+VLnPF+HpU4xGFwUqmV24Co2HKBNW3w+slqZ1CYchbcDeqJASHDYWzZCcMiQ==",
-      "dev": true,
-      "requires": {
-        "@types/docker-modem": "*",
-        "@types/node": "*",
-        "@types/ssh2": "*"
-      }
-    },
-    "@types/eslint": {
-      "version": "8.44.3",
-      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz",
-      "integrity": "sha512-iM/WfkwAhwmPff3wZuPLYiHX18HI24jU8k1ZSH7P8FHwxTjZ2P6CoX2wnF43oprR+YXJM6UUxATkNvyv/JHd+g==",
-      "dev": true,
-      "requires": {
-        "@types/estree": "*",
-        "@types/json-schema": "*"
-      }
-    },
-    "@types/eslint-scope": {
-      "version": "3.7.5",
-      "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.5.tgz",
-      "integrity": "sha512-JNvhIEyxVW6EoMIFIvj93ZOywYFatlpu9deeH6eSx6PE3WHYvHaQtmHmQeNw7aA81bYGBPPQqdtBm6b1SsQMmA==",
-      "dev": true,
-      "requires": {
-        "@types/eslint": "*",
-        "@types/estree": "*"
-      }
-    },
-    "@types/estree": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
-      "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
-      "dev": true
-    },
-    "@types/express": {
-      "version": "4.17.21",
-      "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
-      "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
-      "dev": true,
-      "requires": {
-        "@types/body-parser": "*",
-        "@types/express-serve-static-core": "^4.17.33",
-        "@types/qs": "*",
-        "@types/serve-static": "*"
-      }
-    },
-    "@types/express-serve-static-core": {
-      "version": "4.17.37",
-      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.37.tgz",
-      "integrity": "sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==",
-      "dev": true,
-      "requires": {
-        "@types/node": "*",
-        "@types/qs": "*",
-        "@types/range-parser": "*",
-        "@types/send": "*"
-      }
-    },
-    "@types/fluent-ffmpeg": {
-      "version": "2.1.26",
-      "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.26.tgz",
-      "integrity": "sha512-0JVF3wdQG+pN0ImwWD0bNgJiKF2OHg/7CDBHw5UIbRTvlnkgGHK6V5doE54ltvhud4o31/dEiHm23CAlxFiUQg==",
-      "dev": true,
-      "requires": {
-        "@types/node": "*"
-      }
-    },
-    "@types/http-errors": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz",
-      "integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==",
-      "dev": true
-    },
-    "@types/inquirer": {
-      "version": "8.2.6",
-      "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.6.tgz",
-      "integrity": "sha512-3uT88kxg8lNzY8ay2ZjP44DKcRaTGztqeIvN2zHvhzIBH/uAPaL75aBtdNRKbA7xXoMbBt5kX0M00VKAnfOYlA==",
-      "peer": true,
-      "requires": {
-        "@types/through": "*",
-        "rxjs": "^7.2.0"
-      }
-    },
-    "@types/js-yaml": {
-      "version": "4.0.9",
-      "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
-      "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
-      "dev": true
-    },
-    "@types/json-schema": {
-      "version": "7.0.15",
-      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
-      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
-      "dev": true
-    },
-    "@types/lodash": {
-      "version": "4.17.7",
-      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz",
-      "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==",
-      "dev": true
-    },
-    "@types/luxon": {
-      "version": "3.4.2",
-      "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
-      "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA=="
-    },
-    "@types/memcached": {
-      "version": "2.2.10",
-      "resolved": "https://registry.npmjs.org/@types/memcached/-/memcached-2.2.10.tgz",
-      "integrity": "sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==",
-      "requires": {
-        "@types/node": "*"
-      }
-    },
-    "@types/methods": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
-      "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==",
-      "dev": true
-    },
-    "@types/mime": {
-      "version": "1.3.3",
-      "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz",
-      "integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==",
-      "dev": true
-    },
-    "@types/mock-fs": {
-      "version": "4.13.4",
-      "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz",
-      "integrity": "sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==",
-      "dev": true,
-      "requires": {
-        "@types/node": "*"
-      }
-    },
-    "@types/multer": {
-      "version": "1.4.12",
-      "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz",
-      "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==",
-      "dev": true,
-      "requires": {
-        "@types/express": "*"
-      }
-    },
-    "@types/mysql": {
-      "version": "2.15.22",
-      "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.22.tgz",
-      "integrity": "sha512-wK1pzsJVVAjYCSZWQoWHziQZbNggXFDUEIGf54g4ZM/ERuP86uGdWeKZWMYlqTPMZfHJJvLPyogXGvCOg87yLQ==",
-      "requires": {
-        "@types/node": "*"
-      }
-    },
-    "@types/node": {
-      "version": "20.16.5",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz",
-      "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==",
-      "requires": {
-        "undici-types": "~6.19.2"
-      }
-    },
-    "@types/nodemailer": {
-      "version": "6.4.15",
-      "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.15.tgz",
-      "integrity": "sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==",
-      "dev": true,
-      "requires": {
-        "@types/node": "*"
-      }
-    },
-    "@types/normalize-package-data": {
-      "version": "2.4.4",
-      "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
-      "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
-      "dev": true
-    },
-    "@types/pg": {
-      "version": "8.10.9",
-      "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.9.tgz",
-      "integrity": "sha512-UksbANNE/f8w0wOMxVKKIrLCbEMV+oM1uKejmwXr39olg4xqcfBDbXxObJAt6XxHbDa4XTKOlUEcEltXDX+XLQ==",
-      "requires": {
-        "@types/node": "*",
-        "pg-protocol": "*",
-        "pg-types": "^4.0.1"
-      },
-      "dependencies": {
-        "pg-types": {
-          "version": "4.0.1",
-          "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.1.tgz",
-          "integrity": "sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==",
-          "requires": {
-            "pg-int8": "1.0.1",
-            "pg-numeric": "1.0.2",
-            "postgres-array": "~3.0.1",
-            "postgres-bytea": "~3.0.0",
-            "postgres-date": "~2.0.1",
-            "postgres-interval": "^3.0.0",
-            "postgres-range": "^1.1.1"
-          }
-        },
-        "postgres-array": {
-          "version": "3.0.2",
-          "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz",
-          "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog=="
-        },
-        "postgres-bytea": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz",
-          "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==",
-          "requires": {
-            "obuf": "~1.1.2"
-          }
-        },
-        "postgres-date": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.0.1.tgz",
-          "integrity": "sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw=="
-        },
-        "postgres-interval": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz",
-          "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="
-        }
-      }
-    },
-    "@types/pg-pool": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.4.tgz",
-      "integrity": "sha512-qZAvkv1K3QbmHHFYSNRYPkRjOWRLBYrL4B9c+wG0GSVGBw0NtJwPcgx/DSddeDJvRGMHCEQ4VMEVfuJ/0gZ3XQ==",
-      "requires": {
-        "@types/pg": "*"
-      }
-    },
-    "@types/picomatch": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-3.0.1.tgz",
-      "integrity": "sha512-1MRgzpzY0hOp9pW/kLRxeQhUWwil6gnrUYd3oEpeYBqp/FexhaCPv3F8LsYr47gtUU45fO2cm1dbwkSrHEo8Uw==",
-      "dev": true
-    },
-    "@types/prop-types": {
-      "version": "15.7.12",
-      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
-      "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
-      "dev": true
-    },
-    "@types/qs": {
-      "version": "6.9.8",
-      "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz",
-      "integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==",
-      "dev": true
-    },
-    "@types/range-parser": {
-      "version": "1.2.5",
-      "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz",
-      "integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==",
-      "dev": true
-    },
-    "@types/react": {
-      "version": "18.3.4",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz",
-      "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==",
-      "dev": true,
-      "requires": {
-        "@types/prop-types": "*",
-        "csstype": "^3.0.2"
-      }
-    },
-    "@types/readdir-glob": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.2.tgz",
-      "integrity": "sha512-vwAYrNN/8yhp/FJRU6HUSD0yk6xfoOS8HrZa8ZL7j+X8hJpaC1hTcAiXX2IxaAkkvrz9mLyoEhYZTE3cEYvA9Q==",
-      "dev": true,
-      "requires": {
-        "@types/node": "*"
-      }
-    },
-    "@types/semver": {
-      "version": "7.5.8",
-      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
-      "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
-      "dev": true
-    },
-    "@types/send": {
-      "version": "0.17.2",
-      "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.2.tgz",
-      "integrity": "sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==",
-      "dev": true,
-      "requires": {
-        "@types/mime": "^1",
-        "@types/node": "*"
-      }
-    },
-    "@types/serve-static": {
-      "version": "1.15.3",
-      "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.3.tgz",
-      "integrity": "sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==",
-      "dev": true,
-      "requires": {
-        "@types/http-errors": "*",
-        "@types/mime": "*",
-        "@types/node": "*"
-      }
-    },
-    "@types/shimmer": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
-      "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="
-    },
-    "@types/ssh2": {
-      "version": "0.5.52",
-      "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz",
-      "integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==",
-      "dev": true,
-      "requires": {
-        "@types/node": "*",
-        "@types/ssh2-streams": "*"
-      }
-    },
-    "@types/ssh2-streams": {
-      "version": "0.1.10",
-      "resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.10.tgz",
-      "integrity": "sha512-r3HYPL0kPxRwk7Nk1P4JxaWPyJ2Mfnfm5efuK0vYgYZu16RerZUTyun6Yqu5KEfc3AR7BvTL1x+nzf7+kbKftQ==",
-      "dev": true,
-      "requires": {
-        "@types/node": "*"
-      }
-    },
-    "@types/superagent": {
-      "version": "8.1.6",
-      "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.6.tgz",
-      "integrity": "sha512-yzBOv+6meEHSzV2NThYYOA6RtqvPr3Hbob9ZLp3i07SH27CrYVfm8CrF7ydTmidtelsFiKx2I4gZAiAOamGgvQ==",
-      "dev": true,
-      "requires": {
-        "@types/cookiejar": "^2.1.5",
-        "@types/methods": "^1.1.4",
-        "@types/node": "*"
-      }
-    },
-    "@types/supertest": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz",
-      "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==",
-      "dev": true,
-      "requires": {
-        "@types/methods": "^1.1.4",
-        "@types/superagent": "^8.1.0"
-      }
-    },
-    "@types/tedious": {
-      "version": "4.0.14",
-      "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz",
-      "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==",
-      "requires": {
-        "@types/node": "*"
-      }
-    },
-    "@types/through": {
-      "version": "0.0.31",
-      "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.31.tgz",
-      "integrity": "sha512-LpKpmb7FGevYgXnBXYs6HWnmiFyVG07Pt1cnbgM1IhEacITTiUaBXXvOR3Y50ksaJWGSfhbEvQFivQEFGCC55w==",
-      "peer": true,
-      "requires": {
-        "@types/node": "*"
-      }
-    },
-    "@types/ua-parser-js": {
-      "version": "0.7.39",
-      "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz",
-      "integrity": "sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==",
-      "dev": true
-    },
-    "@types/validator": {
-      "version": "13.11.8",
-      "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.8.tgz",
-      "integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ=="
-    },
-    "@typescript-eslint/eslint-plugin": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz",
-      "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==",
-      "dev": true,
-      "requires": {
-        "@eslint-community/regexpp": "^4.10.0",
-        "@typescript-eslint/scope-manager": "8.3.0",
-        "@typescript-eslint/type-utils": "8.3.0",
-        "@typescript-eslint/utils": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0",
-        "graphemer": "^1.4.0",
-        "ignore": "^5.3.1",
-        "natural-compare": "^1.4.0",
-        "ts-api-utils": "^1.3.0"
-      }
-    },
-    "@typescript-eslint/parser": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz",
-      "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==",
-      "dev": true,
-      "requires": {
-        "@typescript-eslint/scope-manager": "8.3.0",
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/typescript-estree": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0",
-        "debug": "^4.3.4"
-      }
-    },
-    "@typescript-eslint/scope-manager": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz",
-      "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==",
-      "dev": true,
-      "requires": {
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0"
-      }
-    },
-    "@typescript-eslint/type-utils": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz",
-      "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==",
-      "dev": true,
-      "requires": {
-        "@typescript-eslint/typescript-estree": "8.3.0",
-        "@typescript-eslint/utils": "8.3.0",
-        "debug": "^4.3.4",
-        "ts-api-utils": "^1.3.0"
-      }
-    },
-    "@typescript-eslint/types": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz",
-      "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==",
-      "dev": true
-    },
-    "@typescript-eslint/typescript-estree": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz",
-      "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==",
-      "dev": true,
-      "requires": {
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0",
-        "debug": "^4.3.4",
-        "fast-glob": "^3.3.2",
-        "is-glob": "^4.0.3",
-        "minimatch": "^9.0.4",
-        "semver": "^7.6.0",
-        "ts-api-utils": "^1.3.0"
-      },
-      "dependencies": {
-        "brace-expansion": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
-          "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-          "dev": true,
-          "requires": {
-            "balanced-match": "^1.0.0"
-          }
-        },
-        "minimatch": {
-          "version": "9.0.5",
-          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
-          "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
-          "dev": true,
-          "requires": {
-            "brace-expansion": "^2.0.1"
-          }
-        }
-      }
-    },
-    "@typescript-eslint/utils": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz",
-      "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==",
-      "dev": true,
-      "requires": {
-        "@eslint-community/eslint-utils": "^4.4.0",
-        "@typescript-eslint/scope-manager": "8.3.0",
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/typescript-estree": "8.3.0"
-      }
-    },
-    "@typescript-eslint/visitor-keys": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz",
-      "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==",
-      "dev": true,
-      "requires": {
-        "@typescript-eslint/types": "8.3.0",
-        "eslint-visitor-keys": "^3.4.3"
-      }
-    },
-    "@vitest/coverage-v8": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz",
-      "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==",
-      "dev": true,
-      "requires": {
-        "@ampproject/remapping": "^2.3.0",
-        "@bcoe/v8-coverage": "^0.2.3",
-        "debug": "^4.3.5",
-        "istanbul-lib-coverage": "^3.2.2",
-        "istanbul-lib-report": "^3.0.1",
-        "istanbul-lib-source-maps": "^5.0.6",
-        "istanbul-reports": "^3.1.7",
-        "magic-string": "^0.30.10",
-        "magicast": "^0.3.4",
-        "std-env": "^3.7.0",
-        "test-exclude": "^7.0.1",
-        "tinyrainbow": "^1.2.0"
-      },
-      "dependencies": {
-        "magic-string": {
-          "version": "0.30.11",
-          "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
-          "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
-          "dev": true,
-          "requires": {
-            "@jridgewell/sourcemap-codec": "^1.5.0"
-          }
-        }
-      }
-    },
-    "@vitest/expect": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz",
-      "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==",
-      "dev": true,
-      "requires": {
-        "@vitest/spy": "2.0.5",
-        "@vitest/utils": "2.0.5",
-        "chai": "^5.1.1",
-        "tinyrainbow": "^1.2.0"
-      }
-    },
-    "@vitest/pretty-format": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz",
-      "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==",
-      "dev": true,
-      "requires": {
-        "tinyrainbow": "^1.2.0"
-      }
-    },
-    "@vitest/runner": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz",
-      "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==",
-      "dev": true,
-      "requires": {
-        "@vitest/utils": "2.0.5",
-        "pathe": "^1.1.2"
-      }
-    },
-    "@vitest/snapshot": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz",
-      "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==",
-      "dev": true,
-      "requires": {
-        "@vitest/pretty-format": "2.0.5",
-        "magic-string": "^0.30.10",
-        "pathe": "^1.1.2"
-      },
-      "dependencies": {
-        "magic-string": {
-          "version": "0.30.11",
-          "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
-          "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
-          "dev": true,
-          "requires": {
-            "@jridgewell/sourcemap-codec": "^1.5.0"
-          }
-        }
-      }
-    },
-    "@vitest/spy": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz",
-      "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==",
-      "dev": true,
-      "requires": {
-        "tinyspy": "^3.0.0"
-      }
-    },
-    "@vitest/utils": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz",
-      "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==",
-      "dev": true,
-      "requires": {
-        "@vitest/pretty-format": "2.0.5",
-        "estree-walker": "^3.0.3",
-        "loupe": "^3.1.1",
-        "tinyrainbow": "^1.2.0"
-      }
-    },
-    "@webassemblyjs/ast": {
-      "version": "1.12.1",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
-      "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==",
-      "dev": true,
-      "requires": {
-        "@webassemblyjs/helper-numbers": "1.11.6",
-        "@webassemblyjs/helper-wasm-bytecode": "1.11.6"
-      }
-    },
-    "@webassemblyjs/floating-point-hex-parser": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz",
-      "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==",
-      "dev": true
-    },
-    "@webassemblyjs/helper-api-error": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz",
-      "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==",
-      "dev": true
-    },
-    "@webassemblyjs/helper-buffer": {
-      "version": "1.12.1",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz",
-      "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==",
-      "dev": true
-    },
-    "@webassemblyjs/helper-numbers": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz",
-      "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==",
-      "dev": true,
-      "requires": {
-        "@webassemblyjs/floating-point-hex-parser": "1.11.6",
-        "@webassemblyjs/helper-api-error": "1.11.6",
-        "@xtuc/long": "4.2.2"
-      }
-    },
-    "@webassemblyjs/helper-wasm-bytecode": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz",
-      "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==",
-      "dev": true
-    },
-    "@webassemblyjs/helper-wasm-section": {
-      "version": "1.12.1",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz",
-      "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==",
-      "dev": true,
-      "requires": {
-        "@webassemblyjs/ast": "1.12.1",
-        "@webassemblyjs/helper-buffer": "1.12.1",
-        "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
-        "@webassemblyjs/wasm-gen": "1.12.1"
-      }
-    },
-    "@webassemblyjs/ieee754": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz",
-      "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==",
-      "dev": true,
-      "requires": {
-        "@xtuc/ieee754": "^1.2.0"
-      }
-    },
-    "@webassemblyjs/leb128": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz",
-      "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==",
-      "dev": true,
-      "requires": {
-        "@xtuc/long": "4.2.2"
-      }
-    },
-    "@webassemblyjs/utf8": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz",
-      "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==",
-      "dev": true
-    },
-    "@webassemblyjs/wasm-edit": {
-      "version": "1.12.1",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz",
-      "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==",
-      "dev": true,
-      "requires": {
-        "@webassemblyjs/ast": "1.12.1",
-        "@webassemblyjs/helper-buffer": "1.12.1",
-        "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
-        "@webassemblyjs/helper-wasm-section": "1.12.1",
-        "@webassemblyjs/wasm-gen": "1.12.1",
-        "@webassemblyjs/wasm-opt": "1.12.1",
-        "@webassemblyjs/wasm-parser": "1.12.1",
-        "@webassemblyjs/wast-printer": "1.12.1"
-      }
-    },
-    "@webassemblyjs/wasm-gen": {
-      "version": "1.12.1",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz",
-      "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==",
-      "dev": true,
-      "requires": {
-        "@webassemblyjs/ast": "1.12.1",
-        "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
-        "@webassemblyjs/ieee754": "1.11.6",
-        "@webassemblyjs/leb128": "1.11.6",
-        "@webassemblyjs/utf8": "1.11.6"
-      }
-    },
-    "@webassemblyjs/wasm-opt": {
-      "version": "1.12.1",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz",
-      "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==",
-      "dev": true,
-      "requires": {
-        "@webassemblyjs/ast": "1.12.1",
-        "@webassemblyjs/helper-buffer": "1.12.1",
-        "@webassemblyjs/wasm-gen": "1.12.1",
-        "@webassemblyjs/wasm-parser": "1.12.1"
-      }
-    },
-    "@webassemblyjs/wasm-parser": {
-      "version": "1.12.1",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz",
-      "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==",
-      "dev": true,
-      "requires": {
-        "@webassemblyjs/ast": "1.12.1",
-        "@webassemblyjs/helper-api-error": "1.11.6",
-        "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
-        "@webassemblyjs/ieee754": "1.11.6",
-        "@webassemblyjs/leb128": "1.11.6",
-        "@webassemblyjs/utf8": "1.11.6"
-      }
-    },
-    "@webassemblyjs/wast-printer": {
-      "version": "1.12.1",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz",
-      "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==",
-      "dev": true,
-      "requires": {
-        "@webassemblyjs/ast": "1.12.1",
-        "@xtuc/long": "4.2.2"
-      }
-    },
-    "@xtuc/ieee754": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
-      "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
-      "dev": true
-    },
-    "@xtuc/long": {
-      "version": "4.2.2",
-      "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
-      "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
-      "dev": true
-    },
-    "abbrev": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
-      "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
-    },
-    "abort-controller": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
-      "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
-      "requires": {
-        "event-target-shim": "^5.0.0"
-      }
-    },
-    "accepts": {
-      "version": "1.3.8",
-      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
-      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
-      "requires": {
-        "mime-types": "~2.1.34",
-        "negotiator": "0.6.3"
-      }
-    },
-    "acorn": {
-      "version": "8.12.0",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz",
-      "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw=="
-    },
-    "acorn-import-attributes": {
-      "version": "1.9.5",
-      "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
-      "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
-      "requires": {}
-    },
-    "acorn-jsx": {
-      "version": "5.3.2",
-      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
-      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
-      "dev": true,
-      "requires": {}
-    },
-    "acorn-walk": {
-      "version": "8.3.2",
-      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
-      "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
-      "optional": true,
-      "peer": true
-    },
-    "agent-base": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
-      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
-      "requires": {
-        "debug": "4"
-      }
-    },
-    "ajv": {
-      "version": "8.12.0",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
-      "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
-      "dev": true,
-      "requires": {
-        "fast-deep-equal": "^3.1.1",
-        "json-schema-traverse": "^1.0.0",
-        "require-from-string": "^2.0.2",
-        "uri-js": "^4.2.2"
-      }
-    },
-    "ajv-formats": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
-      "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
-      "dev": true,
-      "requires": {
-        "ajv": "^8.0.0"
-      }
-    },
-    "ansi-colors": {
-      "version": "4.1.3",
-      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
-      "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
-      "dev": true
-    },
-    "ansi-escapes": {
-      "version": "4.3.2",
-      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
-      "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
-      "requires": {
-        "type-fest": "^0.21.3"
-      },
-      "dependencies": {
-        "type-fest": {
-          "version": "0.21.3",
-          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
-          "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="
-        }
-      }
-    },
-    "ansi-regex": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
-      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
-    },
-    "ansi-styles": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-      "requires": {
-        "color-convert": "^2.0.1"
-      }
-    },
-    "any-promise": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
-      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
-    },
-    "anymatch": {
-      "version": "3.1.3",
-      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
-      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
-      "requires": {
-        "normalize-path": "^3.0.0",
-        "picomatch": "^2.0.4"
-      },
-      "dependencies": {
-        "picomatch": {
-          "version": "2.3.1",
-          "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-          "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
-        }
-      }
-    },
-    "app-root-path": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz",
-      "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA=="
-    },
-    "append-field": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
-      "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="
-    },
-    "aproba": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
-      "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
-    },
-    "archiver": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz",
-      "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==",
-      "requires": {
-        "archiver-utils": "^5.0.2",
-        "async": "^3.2.4",
-        "buffer-crc32": "^1.0.0",
-        "readable-stream": "^4.0.0",
-        "readdir-glob": "^1.1.2",
-        "tar-stream": "^3.0.0",
-        "zip-stream": "^6.0.1"
-      },
-      "dependencies": {
-        "buffer": {
-          "version": "6.0.3",
-          "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
-          "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
-          "requires": {
-            "base64-js": "^1.3.1",
-            "ieee754": "^1.2.1"
-          }
-        },
-        "buffer-crc32": {
-          "version": "1.0.0",
-          "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
-          "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="
-        },
-        "readable-stream": {
-          "version": "4.5.2",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
-          "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
-          "requires": {
-            "abort-controller": "^3.0.0",
-            "buffer": "^6.0.3",
-            "events": "^3.3.0",
-            "process": "^0.11.10",
-            "string_decoder": "^1.3.0"
-          }
-        }
-      }
-    },
-    "archiver-utils": {
-      "version": "5.0.2",
-      "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz",
-      "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==",
-      "requires": {
-        "glob": "^10.0.0",
-        "graceful-fs": "^4.2.0",
-        "is-stream": "^2.0.1",
-        "lazystream": "^1.0.0",
-        "lodash": "^4.17.15",
-        "normalize-path": "^3.0.0",
-        "readable-stream": "^4.0.0"
-      },
-      "dependencies": {
-        "buffer": {
-          "version": "6.0.3",
-          "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
-          "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
-          "requires": {
-            "base64-js": "^1.3.1",
-            "ieee754": "^1.2.1"
-          }
-        },
-        "readable-stream": {
-          "version": "4.5.2",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
-          "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
-          "requires": {
-            "abort-controller": "^3.0.0",
-            "buffer": "^6.0.3",
-            "events": "^3.3.0",
-            "process": "^0.11.10",
-            "string_decoder": "^1.3.0"
-          }
-        }
-      }
-    },
-    "are-we-there-yet": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
-      "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
-      "requires": {
-        "delegates": "^1.0.0",
-        "readable-stream": "^3.6.0"
-      }
-    },
-    "arg": {
-      "version": "4.1.3",
-      "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
-      "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
-      "optional": true,
-      "peer": true
-    },
-    "argparse": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
-      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
-    },
-    "array-flatten": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
-      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
-    },
-    "array-source": {
-      "version": "0.0.4",
-      "resolved": "https://registry.npmjs.org/array-source/-/array-source-0.0.4.tgz",
-      "integrity": "sha512-frNdc+zBn80vipY+GdcJkLEbMWj3xmzArYApmUGxoiV8uAu/ygcs9icPdsGdA26h0MkHUMW6EN2piIvVx+M5Mw=="
-    },
-    "array-timsort": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz",
-      "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==",
-      "dev": true
-    },
-    "asn1": {
-      "version": "0.2.6",
-      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
-      "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
-      "dev": true,
-      "requires": {
-        "safer-buffer": "~2.1.0"
-      }
-    },
-    "assertion-error": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
-      "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
-      "dev": true
-    },
-    "async": {
-      "version": "3.2.4",
-      "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
-      "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
-    },
-    "async-lock": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz",
-      "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="
-    },
-    "b4a": {
-      "version": "1.6.4",
-      "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
-      "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw=="
-    },
-    "balanced-match": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
-      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
-    },
-    "bare-events": {
-      "version": "2.4.2",
-      "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz",
-      "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==",
-      "optional": true
-    },
-    "bare-fs": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.1.tgz",
-      "integrity": "sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==",
-      "dev": true,
-      "optional": true,
-      "requires": {
-        "bare-events": "^2.0.0",
-        "bare-path": "^2.0.0",
-        "bare-stream": "^2.0.0"
-      }
-    },
-    "bare-os": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.0.tgz",
-      "integrity": "sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNg==",
-      "dev": true,
-      "optional": true
-    },
-    "bare-path": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz",
-      "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==",
-      "dev": true,
-      "optional": true,
-      "requires": {
-        "bare-os": "^2.1.0"
-      }
-    },
-    "bare-stream": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.1.3.tgz",
-      "integrity": "sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ==",
-      "dev": true,
-      "optional": true,
-      "requires": {
-        "streamx": "^2.18.0"
-      }
-    },
-    "base64-js": {
-      "version": "1.5.1",
-      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
-      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
-    },
-    "base64id": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
-      "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
-    },
-    "batch-cluster": {
-      "version": "13.0.0",
-      "resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-13.0.0.tgz",
-      "integrity": "sha512-EreW0Vi8TwovhYUHBXXRA5tthuU2ynGsZFlboyMJHCCUXYa2AjgwnE3ubBOJs2xJLcuXFJbi6c/8pH5+FVj8Og=="
-    },
-    "bcrypt": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
-      "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
-      "requires": {
-        "@mapbox/node-pre-gyp": "^1.0.11",
-        "node-addon-api": "^5.0.0"
-      },
-      "dependencies": {
-        "node-addon-api": {
-          "version": "5.1.0",
-          "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
-          "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
-        }
-      }
-    },
-    "bcrypt-pbkdf": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
-      "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
-      "dev": true,
-      "requires": {
-        "tweetnacl": "^0.14.3"
-      }
-    },
-    "bignumber.js": {
-      "version": "9.1.2",
-      "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
-      "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug=="
-    },
-    "binary-extensions": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
-      "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA=="
-    },
-    "bl": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
-      "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
-      "requires": {
-        "buffer": "^5.5.0",
-        "inherits": "^2.0.4",
-        "readable-stream": "^3.4.0"
-      }
-    },
-    "body-parser": {
-      "version": "1.20.2",
-      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
-      "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
-      "requires": {
-        "bytes": "3.1.2",
-        "content-type": "~1.0.5",
-        "debug": "2.6.9",
-        "depd": "2.0.0",
-        "destroy": "1.2.0",
-        "http-errors": "2.0.0",
-        "iconv-lite": "0.4.24",
-        "on-finished": "2.4.1",
-        "qs": "6.11.0",
-        "raw-body": "2.5.2",
-        "type-is": "~1.6.18",
-        "unpipe": "1.0.0"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "2.6.9",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
-          "requires": {
-            "ms": "2.0.0"
-          }
-        },
-        "ms": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-          "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
-        }
-      }
-    },
-    "brace-expansion": {
-      "version": "1.1.11",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-      "requires": {
-        "balanced-match": "^1.0.0",
-        "concat-map": "0.0.1"
-      }
-    },
-    "braces": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
-      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
-      "requires": {
-        "fill-range": "^7.1.1"
-      }
-    },
-    "browserslist": {
-      "version": "4.23.0",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
-      "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
-      "requires": {
-        "caniuse-lite": "^1.0.30001587",
-        "electron-to-chromium": "^1.4.668",
-        "node-releases": "^2.0.14",
-        "update-browserslist-db": "^1.0.13"
-      }
-    },
-    "buffer": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
-      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
-      "requires": {
-        "base64-js": "^1.3.1",
-        "ieee754": "^1.1.13"
-      }
-    },
-    "buffer-from": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
-      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
-    },
-    "buildcheck": {
-      "version": "0.0.6",
-      "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
-      "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==",
-      "dev": true,
-      "optional": true
-    },
-    "builtin-modules": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
-      "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
-      "dev": true
-    },
-    "bullmq": {
-      "version": "4.17.0",
-      "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.17.0.tgz",
-      "integrity": "sha512-URnHgB01rlCP8RTpmW3kFnvv3vdd2aI1OcBMYQwnqODxGiJUlz9MibDVXE83mq7ee1eS1IvD9lMQqGszX6E5Pw==",
-      "requires": {
-        "cron-parser": "^4.6.0",
-        "glob": "^8.0.3",
-        "ioredis": "^5.3.2",
-        "lodash": "^4.17.21",
-        "msgpackr": "^1.10.1",
-        "node-abort-controller": "^3.1.1",
-        "semver": "^7.5.4",
-        "tslib": "^2.0.0",
-        "uuid": "^9.0.0"
-      },
-      "dependencies": {
-        "brace-expansion": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
-          "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-          "requires": {
-            "balanced-match": "^1.0.0"
-          }
-        },
-        "glob": {
-          "version": "8.1.0",
-          "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
-          "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
-          "requires": {
-            "fs.realpath": "^1.0.0",
-            "inflight": "^1.0.4",
-            "inherits": "2",
-            "minimatch": "^5.0.1",
-            "once": "^1.3.0"
-          }
-        },
-        "minimatch": {
-          "version": "5.1.6",
-          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
-          "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
-          "requires": {
-            "brace-expansion": "^2.0.1"
-          }
-        }
-      }
-    },
-    "busboy": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
-      "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
-      "requires": {
-        "streamsearch": "^1.1.0"
-      }
-    },
-    "byline": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz",
-      "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==",
-      "dev": true
-    },
-    "bytes": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
-      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
-    },
-    "cac": {
-      "version": "6.7.14",
-      "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
-      "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
-      "dev": true
-    },
-    "call-bind": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
-      "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
-      "requires": {
-        "es-define-property": "^1.0.0",
-        "es-errors": "^1.3.0",
-        "function-bind": "^1.1.2",
-        "get-intrinsic": "^1.2.4",
-        "set-function-length": "^1.2.1"
-      }
-    },
-    "callsites": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
-      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
-    },
-    "camelcase-css": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
-      "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
-      "peer": true
-    },
-    "caniuse-lite": {
-      "version": "1.0.30001618",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001618.tgz",
-      "integrity": "sha512-p407+D1tIkDvsEAPS22lJxLQQaG8OTBEqo0KhzfABGk0TU4juBNDSfH0hyAp/HRyx+M8L17z/ltyhxh27FTfQg=="
-    },
-    "chai": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
-      "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
-      "dev": true,
-      "requires": {
-        "assertion-error": "^2.0.1",
-        "check-error": "^2.1.1",
-        "deep-eql": "^5.0.1",
-        "loupe": "^3.1.0",
-        "pathval": "^2.0.0"
-      }
-    },
-    "chalk": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-      "requires": {
-        "ansi-styles": "^4.1.0",
-        "supports-color": "^7.1.0"
-      }
-    },
-    "chardet": {
-      "version": "0.7.0",
-      "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
-      "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
-    },
-    "check-error": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
-      "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
-      "dev": true
-    },
-    "chokidar": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
-      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
-      "requires": {
-        "anymatch": "~3.1.2",
-        "braces": "~3.0.2",
-        "fsevents": "~2.3.2",
-        "glob-parent": "~5.1.2",
-        "is-binary-path": "~2.1.0",
-        "is-glob": "~4.0.1",
-        "normalize-path": "~3.0.0",
-        "readdirp": "~3.6.0"
-      }
-    },
-    "chownr": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
-      "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="
-    },
-    "chrome-trace-event": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
-      "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==",
-      "dev": true
-    },
-    "ci-info": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz",
-      "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==",
-      "dev": true
-    },
-    "cjs-module-lexer": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz",
-      "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q=="
-    },
-    "class-transformer": {
-      "version": "0.5.1",
-      "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
-      "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw=="
-    },
-    "class-validator": {
-      "version": "0.14.1",
-      "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz",
-      "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==",
-      "requires": {
-        "@types/validator": "^13.11.8",
-        "libphonenumber-js": "^1.10.53",
-        "validator": "^13.9.0"
-      }
-    },
-    "clean-regexp": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz",
-      "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==",
-      "dev": true,
-      "requires": {
-        "escape-string-regexp": "^1.0.5"
-      },
-      "dependencies": {
-        "escape-string-regexp": {
-          "version": "1.0.5",
-          "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-          "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-          "dev": true
-        }
-      }
-    },
-    "cli-cursor": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
-      "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
-      "requires": {
-        "restore-cursor": "^3.1.0"
-      }
-    },
-    "cli-highlight": {
-      "version": "2.1.11",
-      "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz",
-      "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==",
-      "requires": {
-        "chalk": "^4.0.0",
-        "highlight.js": "^10.7.1",
-        "mz": "^2.4.0",
-        "parse5": "^5.1.1",
-        "parse5-htmlparser2-tree-adapter": "^6.0.0",
-        "yargs": "^16.0.0"
-      }
-    },
-    "cli-spinners": {
-      "version": "2.9.1",
-      "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.1.tgz",
-      "integrity": "sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ=="
-    },
-    "cli-table3": {
-      "version": "0.6.5",
-      "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz",
-      "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==",
-      "dev": true,
-      "requires": {
-        "@colors/colors": "1.5.0",
-        "string-width": "^4.2.0"
-      }
-    },
-    "cli-width": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
-      "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw=="
-    },
-    "client-only": {
-      "version": "0.0.1",
-      "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
-      "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
-    },
-    "cliui": {
-      "version": "7.0.4",
-      "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
-      "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
-      "requires": {
-        "string-width": "^4.2.0",
-        "strip-ansi": "^6.0.0",
-        "wrap-ansi": "^7.0.0"
-      },
-      "dependencies": {
-        "wrap-ansi": {
-          "version": "7.0.0",
-          "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
-          "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-          "requires": {
-            "ansi-styles": "^4.0.0",
-            "string-width": "^4.1.0",
-            "strip-ansi": "^6.0.0"
-          }
-        }
-      }
-    },
-    "clone": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
-      "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="
-    },
-    "cluster-key-slot": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
-      "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="
-    },
-    "color": {
-      "version": "4.2.3",
-      "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
-      "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
-      "requires": {
-        "color-convert": "^2.0.1",
-        "color-string": "^1.9.0"
-      }
-    },
-    "color-convert": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-      "requires": {
-        "color-name": "~1.1.4"
-      }
-    },
-    "color-name": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
-    },
-    "color-string": {
-      "version": "1.9.1",
-      "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
-      "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
-      "requires": {
-        "color-name": "^1.0.0",
-        "simple-swizzle": "^0.2.2"
-      }
-    },
-    "color-support": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
-      "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="
-    },
-    "commander": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
-      "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="
-    },
-    "comment-json": {
-      "version": "4.2.3",
-      "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.3.tgz",
-      "integrity": "sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==",
-      "dev": true,
-      "requires": {
-        "array-timsort": "^1.0.3",
-        "core-util-is": "^1.0.3",
-        "esprima": "^4.0.1",
-        "has-own-prop": "^2.0.0",
-        "repeat-string": "^1.6.1"
-      }
-    },
-    "compress-commons": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
-      "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==",
-      "requires": {
-        "crc-32": "^1.2.0",
-        "crc32-stream": "^6.0.0",
-        "is-stream": "^2.0.1",
-        "normalize-path": "^3.0.0",
-        "readable-stream": "^4.0.0"
-      },
-      "dependencies": {
-        "buffer": {
-          "version": "6.0.3",
-          "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
-          "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
-          "requires": {
-            "base64-js": "^1.3.1",
-            "ieee754": "^1.2.1"
-          }
-        },
-        "readable-stream": {
-          "version": "4.5.2",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
-          "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
-          "requires": {
-            "abort-controller": "^3.0.0",
-            "buffer": "^6.0.3",
-            "events": "^3.3.0",
-            "process": "^0.11.10",
-            "string_decoder": "^1.3.0"
-          }
-        }
-      }
-    },
-    "concat-map": {
-      "version": "0.0.1",
-      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
-      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
-    },
-    "concat-stream": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
-      "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
-      "requires": {
-        "buffer-from": "^1.0.0",
-        "inherits": "^2.0.3",
-        "readable-stream": "^3.0.2",
-        "typedarray": "^0.0.6"
-      }
-    },
-    "config-chain": {
-      "version": "1.1.13",
-      "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
-      "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
-      "requires": {
-        "ini": "^1.3.4",
-        "proto-list": "~1.2.1"
-      }
-    },
-    "consola": {
-      "version": "2.15.3",
-      "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz",
-      "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw=="
-    },
-    "console-control-strings": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
-      "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="
-    },
-    "content-disposition": {
-      "version": "0.5.4",
-      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
-      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
-      "requires": {
-        "safe-buffer": "5.2.1"
-      }
-    },
-    "content-type": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
-      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="
-    },
-    "convert-source-map": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
-      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
-    },
-    "cookie": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
-      "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
-    },
-    "cookie-parser": {
-      "version": "1.4.6",
-      "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
-      "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
-      "requires": {
-        "cookie": "0.4.1",
-        "cookie-signature": "1.0.6"
-      }
-    },
-    "cookie-signature": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
-      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
-    },
-    "core-js-compat": {
-      "version": "3.37.1",
-      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
-      "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
-      "dev": true,
-      "requires": {
-        "browserslist": "^4.23.0"
-      }
-    },
-    "core-util-is": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
-      "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
-    },
-    "cors": {
-      "version": "2.8.5",
-      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
-      "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
-      "requires": {
-        "object-assign": "^4",
-        "vary": "^1"
-      }
-    },
-    "cosmiconfig": {
-      "version": "8.3.6",
-      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
-      "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
-      "requires": {
-        "import-fresh": "^3.3.0",
-        "js-yaml": "^4.1.0",
-        "parse-json": "^5.2.0",
-        "path-type": "^4.0.0"
-      }
-    },
-    "cpu-features": {
-      "version": "0.0.9",
-      "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.9.tgz",
-      "integrity": "sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ==",
-      "dev": true,
-      "optional": true,
-      "requires": {
-        "buildcheck": "~0.0.6",
-        "nan": "^2.17.0"
-      }
-    },
-    "crc-32": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
-      "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="
-    },
-    "crc32-stream": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz",
-      "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==",
-      "requires": {
-        "crc-32": "^1.2.0",
-        "readable-stream": "^4.0.0"
-      },
-      "dependencies": {
-        "buffer": {
-          "version": "6.0.3",
-          "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
-          "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
-          "requires": {
-            "base64-js": "^1.3.1",
-            "ieee754": "^1.2.1"
-          }
-        },
-        "readable-stream": {
-          "version": "4.5.2",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
-          "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
-          "requires": {
-            "abort-controller": "^3.0.0",
-            "buffer": "^6.0.3",
-            "events": "^3.3.0",
-            "process": "^0.11.10",
-            "string_decoder": "^1.3.0"
-          }
-        }
-      }
-    },
-    "create-require": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
-      "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
-      "optional": true,
-      "peer": true
-    },
-    "cron": {
-      "version": "3.1.7",
-      "resolved": "https://registry.npmjs.org/cron/-/cron-3.1.7.tgz",
-      "integrity": "sha512-tlBg7ARsAMQLzgwqVxy8AZl/qlTc5nibqYwtNGoCrd+cV+ugI+tvZC1oT/8dFH8W455YrywGykx/KMmAqOr7Jw==",
-      "requires": {
-        "@types/luxon": "~3.4.0",
-        "luxon": "~3.4.0"
-      },
-      "dependencies": {
-        "luxon": {
-          "version": "3.4.4",
-          "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
-          "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA=="
-        }
-      }
-    },
-    "cron-parser": {
-      "version": "4.9.0",
-      "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
-      "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==",
-      "requires": {
-        "luxon": "^3.2.1"
-      }
-    },
-    "cross-spawn": {
-      "version": "7.0.3",
-      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
-      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
-      "requires": {
-        "path-key": "^3.1.0",
-        "shebang-command": "^2.0.0",
-        "which": "^2.0.1"
-      }
-    },
-    "cssesc": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
-      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
-      "peer": true
-    },
-    "csstype": {
-      "version": "3.1.3",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
-      "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
-      "dev": true
-    },
-    "dayjs": {
-      "version": "1.11.10",
-      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
-      "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
-    },
-    "debounce": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.0.0.tgz",
-      "integrity": "sha512-xRetU6gL1VJbs85Mc4FoEGSjQxzpdxRyFhe3lmWFyy2EzydIcD4xzUvRJMD+NPDfMwKNhxa3PvsIOU32luIWeA=="
-    },
-    "debug": {
-      "version": "4.3.5",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
-      "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
-      "requires": {
-        "ms": "2.1.2"
-      }
-    },
-    "deep-eql": {
-      "version": "5.0.2",
-      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
-      "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
-      "dev": true
-    },
-    "deep-is": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
-      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
-      "dev": true
-    },
-    "deepmerge": {
-      "version": "4.3.1",
-      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
-      "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="
-    },
-    "defaults": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
-      "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
-      "requires": {
-        "clone": "^1.0.2"
-      }
-    },
-    "define-data-property": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.3.tgz",
-      "integrity": "sha512-h3GBouC+RPtNX2N0hHVLo2ZwPYurq8mLmXpOLTsw71gr7lHt5VaI4vVkDUNOfiWmm48JEXe3VM7PmLX45AMmmg==",
-      "requires": {
-        "es-errors": "^1.3.0",
-        "get-intrinsic": "^1.2.4",
-        "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.1"
-      }
-    },
-    "delegates": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
-      "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="
-    },
-    "denque": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
-      "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="
-    },
-    "depd": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
-      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
-    },
-    "destroy": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
-      "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
-    },
-    "detect-libc": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
-      "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="
-    },
-    "diacritics": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
-      "integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA=="
-    },
-    "didyoumean": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
-      "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
-      "peer": true
-    },
-    "diff": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
-      "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
-      "optional": true,
-      "peer": true
-    },
-    "discontinuous-range": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz",
-      "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==",
-      "dev": true
-    },
-    "dlv": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
-      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
-      "peer": true
-    },
-    "docker-compose": {
-      "version": "0.24.8",
-      "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.8.tgz",
-      "integrity": "sha512-plizRs/Vf15H+GCVxq2EUvyPK7ei9b/cVesHvjnX4xaXjM9spHe2Ytq0BitndFgvTJ3E3NljPNUEl7BAN43iZw==",
-      "dev": true,
-      "requires": {
-        "yaml": "^2.2.2"
-      }
-    },
-    "docker-modem": {
-      "version": "3.0.8",
-      "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.8.tgz",
-      "integrity": "sha512-f0ReSURdM3pcKPNS30mxOHSbaFLcknGmQjwSfmbcdOw1XWKXVhukM3NJHhr7NpY9BIyyWQb0EBo3KQvvuU5egQ==",
-      "dev": true,
-      "requires": {
-        "debug": "^4.1.1",
-        "readable-stream": "^3.5.0",
-        "split-ca": "^1.0.1",
-        "ssh2": "^1.11.0"
-      }
-    },
-    "dockerode": {
-      "version": "3.3.5",
-      "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.3.5.tgz",
-      "integrity": "sha512-/0YNa3ZDNeLr/tSckmD69+Gq+qVNhvKfAHNeZJBnp7EOP6RGKV8ORrJHkUn20So5wU+xxT7+1n5u8PjHbfjbSA==",
-      "dev": true,
-      "requires": {
-        "@balena/dockerignore": "^1.0.2",
-        "docker-modem": "^3.0.0",
-        "tar-fs": "~2.0.1"
-      },
-      "dependencies": {
-        "chownr": {
-          "version": "1.1.4",
-          "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
-          "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
-          "dev": true
-        },
-        "tar-fs": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz",
-          "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==",
-          "dev": true,
-          "requires": {
-            "chownr": "^1.1.1",
-            "mkdirp-classic": "^0.5.2",
-            "pump": "^3.0.0",
-            "tar-stream": "^2.0.0"
-          }
-        },
-        "tar-stream": {
-          "version": "2.2.0",
-          "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
-          "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
-          "dev": true,
-          "requires": {
-            "bl": "^4.0.3",
-            "end-of-stream": "^1.4.1",
-            "fs-constants": "^1.0.0",
-            "inherits": "^2.0.3",
-            "readable-stream": "^3.1.1"
-          }
-        }
-      }
-    },
-    "dom-serializer": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
-      "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
-      "requires": {
-        "domelementtype": "^2.3.0",
-        "domhandler": "^5.0.2",
-        "entities": "^4.2.0"
-      }
-    },
-    "domelementtype": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
-      "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="
-    },
-    "domhandler": {
-      "version": "5.0.3",
-      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
-      "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
-      "requires": {
-        "domelementtype": "^2.3.0"
-      }
-    },
-    "domutils": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
-      "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
-      "requires": {
-        "dom-serializer": "^2.0.0",
-        "domelementtype": "^2.3.0",
-        "domhandler": "^5.0.3"
-      }
-    },
-    "dotenv": {
-      "version": "16.4.5",
-      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
-      "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg=="
-    },
-    "dotenv-expand": {
-      "version": "10.0.0",
-      "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz",
-      "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A=="
-    },
-    "eastasianwidth": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
-      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
-    },
-    "editorconfig": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
-      "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
-      "requires": {
-        "@one-ini/wasm": "0.1.1",
-        "commander": "^10.0.0",
-        "minimatch": "9.0.1",
-        "semver": "^7.5.3"
-      },
-      "dependencies": {
-        "brace-expansion": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
-          "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-          "requires": {
-            "balanced-match": "^1.0.0"
-          }
-        },
-        "commander": {
-          "version": "10.0.1",
-          "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
-          "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="
-        },
-        "minimatch": {
-          "version": "9.0.1",
-          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
-          "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
-          "requires": {
-            "brace-expansion": "^2.0.1"
-          }
-        }
-      }
-    },
-    "ee-first": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
-      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
-    },
-    "electron-to-chromium": {
-      "version": "1.4.769",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.769.tgz",
-      "integrity": "sha512-bZu7p623NEA2rHTc9K1vykl57ektSPQYFFqQir8BOYf6EKOB+yIsbFB9Kpm7Cgt6tsLr9sRkqfqSZUw7LP1XxQ=="
-    },
-    "emoji-regex": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
-    },
-    "encodeurl": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
-      "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
-    },
-    "end-of-stream": {
-      "version": "1.4.4",
-      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
-      "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
-      "dev": true,
-      "requires": {
-        "once": "^1.4.0"
-      }
-    },
-    "engine.io": {
-      "version": "6.5.2",
-      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.2.tgz",
-      "integrity": "sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA==",
-      "requires": {
-        "@types/cookie": "^0.4.1",
-        "@types/cors": "^2.8.12",
-        "@types/node": ">=10.0.0",
-        "accepts": "~1.3.4",
-        "base64id": "2.0.0",
-        "cookie": "~0.4.1",
-        "cors": "~2.8.5",
-        "debug": "~4.3.1",
-        "engine.io-parser": "~5.2.1",
-        "ws": "~8.11.0"
-      }
-    },
-    "engine.io-parser": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
-      "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ=="
-    },
-    "enhanced-resolve": {
-      "version": "5.17.0",
-      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz",
-      "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==",
-      "dev": true,
-      "requires": {
-        "graceful-fs": "^4.2.4",
-        "tapable": "^2.2.0"
-      }
-    },
-    "entities": {
-      "version": "4.5.0",
-      "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
-      "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
-    },
-    "error-ex": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
-      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
-      "requires": {
-        "is-arrayish": "^0.2.1"
-      }
-    },
-    "es-define-property": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
-      "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
-      "requires": {
-        "get-intrinsic": "^1.2.4"
-      }
-    },
-    "es-errors": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
-      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
-    },
-    "es-module-lexer": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz",
-      "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==",
-      "dev": true
-    },
-    "esbuild": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
-      "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
-      "dev": true,
-      "requires": {
-        "@esbuild/aix-ppc64": "0.20.2",
-        "@esbuild/android-arm": "0.20.2",
-        "@esbuild/android-arm64": "0.20.2",
-        "@esbuild/android-x64": "0.20.2",
-        "@esbuild/darwin-arm64": "0.20.2",
-        "@esbuild/darwin-x64": "0.20.2",
-        "@esbuild/freebsd-arm64": "0.20.2",
-        "@esbuild/freebsd-x64": "0.20.2",
-        "@esbuild/linux-arm": "0.20.2",
-        "@esbuild/linux-arm64": "0.20.2",
-        "@esbuild/linux-ia32": "0.20.2",
-        "@esbuild/linux-loong64": "0.20.2",
-        "@esbuild/linux-mips64el": "0.20.2",
-        "@esbuild/linux-ppc64": "0.20.2",
-        "@esbuild/linux-riscv64": "0.20.2",
-        "@esbuild/linux-s390x": "0.20.2",
-        "@esbuild/linux-x64": "0.20.2",
-        "@esbuild/netbsd-x64": "0.20.2",
-        "@esbuild/openbsd-x64": "0.20.2",
-        "@esbuild/sunos-x64": "0.20.2",
-        "@esbuild/win32-arm64": "0.20.2",
-        "@esbuild/win32-ia32": "0.20.2",
-        "@esbuild/win32-x64": "0.20.2"
-      }
-    },
-    "escalade": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
-      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
-    },
-    "escape-html": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
-      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
-    },
-    "escape-string-regexp": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
-      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
-      "dev": true
-    },
-    "eslint": {
-      "version": "9.9.1",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz",
-      "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==",
-      "dev": true,
-      "requires": {
-        "@eslint-community/eslint-utils": "^4.2.0",
-        "@eslint-community/regexpp": "^4.11.0",
-        "@eslint/config-array": "^0.18.0",
-        "@eslint/eslintrc": "^3.1.0",
-        "@eslint/js": "9.9.1",
-        "@humanwhocodes/module-importer": "^1.0.1",
-        "@humanwhocodes/retry": "^0.3.0",
-        "@nodelib/fs.walk": "^1.2.8",
-        "ajv": "^6.12.4",
-        "chalk": "^4.0.0",
-        "cross-spawn": "^7.0.2",
-        "debug": "^4.3.2",
-        "escape-string-regexp": "^4.0.0",
-        "eslint-scope": "^8.0.2",
-        "eslint-visitor-keys": "^4.0.0",
-        "espree": "^10.1.0",
-        "esquery": "^1.5.0",
-        "esutils": "^2.0.2",
-        "fast-deep-equal": "^3.1.3",
-        "file-entry-cache": "^8.0.0",
-        "find-up": "^5.0.0",
-        "glob-parent": "^6.0.2",
-        "ignore": "^5.2.0",
-        "imurmurhash": "^0.1.4",
-        "is-glob": "^4.0.0",
-        "is-path-inside": "^3.0.3",
-        "json-stable-stringify-without-jsonify": "^1.0.1",
-        "levn": "^0.4.1",
-        "lodash.merge": "^4.6.2",
-        "minimatch": "^3.1.2",
-        "natural-compare": "^1.4.0",
-        "optionator": "^0.9.3",
-        "strip-ansi": "^6.0.1",
-        "text-table": "^0.2.0"
-      },
-      "dependencies": {
-        "ajv": {
-          "version": "6.12.6",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-          "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-          "dev": true,
-          "requires": {
-            "fast-deep-equal": "^3.1.1",
-            "fast-json-stable-stringify": "^2.0.0",
-            "json-schema-traverse": "^0.4.1",
-            "uri-js": "^4.2.2"
-          }
-        },
-        "eslint-visitor-keys": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
-          "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
-          "dev": true
-        },
-        "glob-parent": {
-          "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
-          "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
-          "dev": true,
-          "requires": {
-            "is-glob": "^4.0.3"
-          }
-        },
-        "json-schema-traverse": {
-          "version": "0.4.1",
-          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-          "dev": true
-        }
-      }
-    },
-    "eslint-config-prettier": {
-      "version": "9.1.0",
-      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
-      "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
-      "dev": true,
-      "requires": {}
-    },
-    "eslint-plugin-prettier": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz",
-      "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==",
-      "dev": true,
-      "requires": {
-        "prettier-linter-helpers": "^1.0.0",
-        "synckit": "^0.9.1"
-      }
-    },
-    "eslint-plugin-unicorn": {
-      "version": "55.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz",
-      "integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-validator-identifier": "^7.24.5",
-        "@eslint-community/eslint-utils": "^4.4.0",
-        "ci-info": "^4.0.0",
-        "clean-regexp": "^1.0.0",
-        "core-js-compat": "^3.37.0",
-        "esquery": "^1.5.0",
-        "globals": "^15.7.0",
-        "indent-string": "^4.0.0",
-        "is-builtin-module": "^3.2.1",
-        "jsesc": "^3.0.2",
-        "pluralize": "^8.0.0",
-        "read-pkg-up": "^7.0.1",
-        "regexp-tree": "^0.1.27",
-        "regjsparser": "^0.10.0",
-        "semver": "^7.6.1",
-        "strip-indent": "^3.0.0"
-      }
-    },
-    "eslint-scope": {
-      "version": "8.0.2",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz",
-      "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==",
-      "dev": true,
-      "requires": {
-        "esrecurse": "^4.3.0",
-        "estraverse": "^5.2.0"
-      }
-    },
-    "eslint-visitor-keys": {
-      "version": "3.4.3",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
-      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
-      "dev": true
-    },
-    "espree": {
-      "version": "10.1.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz",
-      "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==",
-      "dev": true,
-      "requires": {
-        "acorn": "^8.12.0",
-        "acorn-jsx": "^5.3.2",
-        "eslint-visitor-keys": "^4.0.0"
-      },
-      "dependencies": {
-        "eslint-visitor-keys": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
-          "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
-          "dev": true
-        }
-      }
-    },
-    "esprima": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
-      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
-      "dev": true
-    },
-    "esquery": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
-      "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
-      "dev": true,
-      "requires": {
-        "estraverse": "^5.1.0"
-      }
-    },
-    "esrecurse": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
-      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
-      "dev": true,
-      "requires": {
-        "estraverse": "^5.2.0"
-      }
-    },
-    "estraverse": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
-      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
-      "dev": true
-    },
-    "estree-walker": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
-      "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
-      "dev": true,
-      "requires": {
-        "@types/estree": "^1.0.0"
-      }
-    },
-    "esutils": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
-      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
-      "dev": true
-    },
-    "etag": {
-      "version": "1.8.1",
-      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
-      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
-    },
-    "event-target-shim": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
-      "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
-    },
-    "eventemitter2": {
-      "version": "6.4.9",
-      "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
-      "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg=="
-    },
-    "events": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
-      "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
-    },
-    "execa": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
-      "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
-      "dev": true,
-      "requires": {
-        "cross-spawn": "^7.0.3",
-        "get-stream": "^8.0.1",
-        "human-signals": "^5.0.0",
-        "is-stream": "^3.0.0",
-        "merge-stream": "^2.0.0",
-        "npm-run-path": "^5.1.0",
-        "onetime": "^6.0.0",
-        "signal-exit": "^4.1.0",
-        "strip-final-newline": "^3.0.0"
-      },
-      "dependencies": {
-        "is-stream": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
-          "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
-          "dev": true
-        },
-        "mimic-fn": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
-          "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
-          "dev": true
-        },
-        "onetime": {
-          "version": "6.0.0",
-          "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
-          "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
-          "dev": true,
-          "requires": {
-            "mimic-fn": "^4.0.0"
-          }
-        }
-      }
-    },
-    "exiftool-vendored": {
-      "version": "28.2.1",
-      "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.1.tgz",
-      "integrity": "sha512-D3YsKErr3BbjKeJzUVsv6CVZ+SQNgAJKPFWVLXu0CBtr24FNuE3CZBXWKWysGu0MjzeDCNwQrQI5+bXUFeiYVA==",
-      "requires": {
-        "@photostructure/tz-lookup": "^10.0.0",
-        "@types/luxon": "^3.4.2",
-        "batch-cluster": "^13.0.0",
-        "exiftool-vendored.exe": "12.91.0",
-        "exiftool-vendored.pl": "12.91.0",
-        "he": "^1.2.0",
-        "luxon": "^3.5.0"
-      }
-    },
-    "exiftool-vendored.exe": {
-      "version": "12.91.0",
-      "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.91.0.tgz",
-      "integrity": "sha512-nxcoGBaJL/D+Wb0jVe8qwyV8QZpRcCzU0aCKhG0S1XNGWGjJJJ4QV851aobcfDwI4NluFOdqkjTSf32pVijvHg==",
-      "optional": true
-    },
-    "exiftool-vendored.pl": {
-      "version": "12.91.0",
-      "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.91.0.tgz",
-      "integrity": "sha512-GZMy9+Jiv8/C7R4uYe1kWtXsAaJdgVezTwYa+wDeoqvReHiX2t5uzkCrzWdjo4LGl5mPQkyKhN7/uPLYk5Ak6w==",
-      "optional": true
-    },
-    "express": {
-      "version": "4.19.2",
-      "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
-      "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
-      "requires": {
-        "accepts": "~1.3.8",
-        "array-flatten": "1.1.1",
-        "body-parser": "1.20.2",
-        "content-disposition": "0.5.4",
-        "content-type": "~1.0.4",
-        "cookie": "0.6.0",
-        "cookie-signature": "1.0.6",
-        "debug": "2.6.9",
-        "depd": "2.0.0",
-        "encodeurl": "~1.0.2",
-        "escape-html": "~1.0.3",
-        "etag": "~1.8.1",
-        "finalhandler": "1.2.0",
-        "fresh": "0.5.2",
-        "http-errors": "2.0.0",
-        "merge-descriptors": "1.0.1",
-        "methods": "~1.1.2",
-        "on-finished": "2.4.1",
-        "parseurl": "~1.3.3",
-        "path-to-regexp": "0.1.7",
-        "proxy-addr": "~2.0.7",
-        "qs": "6.11.0",
-        "range-parser": "~1.2.1",
-        "safe-buffer": "5.2.1",
-        "send": "0.18.0",
-        "serve-static": "1.15.0",
-        "setprototypeof": "1.2.0",
-        "statuses": "2.0.1",
-        "type-is": "~1.6.18",
-        "utils-merge": "1.0.1",
-        "vary": "~1.1.2"
-      },
-      "dependencies": {
-        "cookie": {
-          "version": "0.6.0",
-          "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
-          "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
-        },
-        "debug": {
-          "version": "2.6.9",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
-          "requires": {
-            "ms": "2.0.0"
-          }
-        },
-        "ms": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-          "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
-        },
-        "path-to-regexp": {
-          "version": "0.1.7",
-          "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
-          "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
-        }
-      }
-    },
-    "extend": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
-      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
-    },
-    "external-editor": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
-      "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
-      "requires": {
-        "chardet": "^0.7.0",
-        "iconv-lite": "^0.4.24",
-        "tmp": "^0.0.33"
-      }
-    },
-    "fast-deep-equal": {
-      "version": "3.1.3",
-      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
-      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
-      "dev": true
-    },
-    "fast-diff": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
-      "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
-      "dev": true
-    },
-    "fast-fifo": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
-      "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="
-    },
-    "fast-glob": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
-      "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
-      "requires": {
-        "@nodelib/fs.stat": "^2.0.2",
-        "@nodelib/fs.walk": "^1.2.3",
-        "glob-parent": "^5.1.2",
-        "merge2": "^1.3.0",
-        "micromatch": "^4.0.4"
-      }
-    },
-    "fast-json-stable-stringify": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
-      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
-      "dev": true
-    },
-    "fast-levenshtein": {
-      "version": "2.0.6",
-      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
-      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
-      "dev": true
-    },
-    "fast-safe-stringify": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
-      "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="
-    },
-    "fastq": {
-      "version": "1.15.0",
-      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
-      "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
-      "requires": {
-        "reusify": "^1.0.4"
-      }
-    },
-    "figures": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
-      "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
-      "requires": {
-        "escape-string-regexp": "^1.0.5"
-      },
-      "dependencies": {
-        "escape-string-regexp": {
-          "version": "1.0.5",
-          "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-          "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="
-        }
-      }
-    },
-    "file-entry-cache": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
-      "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
-      "dev": true,
-      "requires": {
-        "flat-cache": "^4.0.0"
-      }
-    },
-    "file-source": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/file-source/-/file-source-0.6.1.tgz",
-      "integrity": "sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA==",
-      "requires": {
-        "stream-source": "0.3"
-      }
-    },
-    "fill-range": {
-      "version": "7.1.1",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
-      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
-      "requires": {
-        "to-regex-range": "^5.0.1"
-      }
-    },
-    "finalhandler": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
-      "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
-      "requires": {
-        "debug": "2.6.9",
-        "encodeurl": "~1.0.2",
-        "escape-html": "~1.0.3",
-        "on-finished": "2.4.1",
-        "parseurl": "~1.3.3",
-        "statuses": "2.0.1",
-        "unpipe": "~1.0.0"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "2.6.9",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
-          "requires": {
-            "ms": "2.0.0"
-          }
-        },
-        "ms": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-          "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
-        }
-      }
-    },
-    "find-up": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
-      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
-      "dev": true,
-      "requires": {
-        "locate-path": "^6.0.0",
-        "path-exists": "^4.0.0"
-      }
-    },
-    "flat-cache": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
-      "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
-      "dev": true,
-      "requires": {
-        "flatted": "^3.2.9",
-        "keyv": "^4.5.4"
-      }
-    },
-    "flatted": {
-      "version": "3.3.1",
-      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
-      "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
-      "dev": true
-    },
-    "fluent-ffmpeg": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz",
-      "integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==",
-      "requires": {
-        "async": "^0.2.9",
-        "which": "^1.1.1"
-      },
-      "dependencies": {
-        "async": {
-          "version": "0.2.10",
-          "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
-          "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
-        },
-        "which": {
-          "version": "1.3.1",
-          "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
-          "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
-          "requires": {
-            "isexe": "^2.0.0"
-          }
-        }
-      }
-    },
-    "foreground-child": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
-      "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
-      "requires": {
-        "cross-spawn": "^7.0.0",
-        "signal-exit": "^4.0.1"
-      }
-    },
-    "fork-ts-checker-webpack-plugin": {
-      "version": "9.0.2",
-      "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz",
-      "integrity": "sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==",
-      "dev": true,
-      "requires": {
-        "@babel/code-frame": "^7.16.7",
-        "chalk": "^4.1.2",
-        "chokidar": "^3.5.3",
-        "cosmiconfig": "^8.2.0",
-        "deepmerge": "^4.2.2",
-        "fs-extra": "^10.0.0",
-        "memfs": "^3.4.1",
-        "minimatch": "^3.0.4",
-        "node-abort-controller": "^3.0.1",
-        "schema-utils": "^3.1.1",
-        "semver": "^7.3.5",
-        "tapable": "^2.2.1"
-      }
-    },
-    "forwarded": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
-      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
-    },
-    "fresh": {
-      "version": "0.5.2",
-      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
-      "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
-    },
-    "fs-constants": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
-      "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
-      "dev": true
-    },
-    "fs-extra": {
-      "version": "10.1.0",
-      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
-      "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
-      "dev": true,
-      "requires": {
-        "graceful-fs": "^4.2.0",
-        "jsonfile": "^6.0.1",
-        "universalify": "^2.0.0"
-      }
-    },
-    "fs-minipass": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
-      "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
-      "requires": {
-        "minipass": "^3.0.0"
-      },
-      "dependencies": {
-        "minipass": {
-          "version": "3.3.6",
-          "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
-          "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
-          "requires": {
-            "yallist": "^4.0.0"
-          }
-        },
-        "yallist": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
-        }
-      }
-    },
-    "fs-monkey": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz",
-      "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==",
-      "dev": true
-    },
-    "fs.realpath": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
-      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
-    },
-    "fsevents": {
-      "version": "2.3.3",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
-      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
-      "optional": true
-    },
-    "function-bind": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
-      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
-    },
-    "gauge": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
-      "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
-      "requires": {
-        "aproba": "^1.0.3 || ^2.0.0",
-        "color-support": "^1.1.2",
-        "console-control-strings": "^1.0.0",
-        "has-unicode": "^2.0.1",
-        "object-assign": "^4.1.1",
-        "signal-exit": "^3.0.0",
-        "string-width": "^4.2.3",
-        "strip-ansi": "^6.0.1",
-        "wide-align": "^1.1.2"
-      },
-      "dependencies": {
-        "signal-exit": {
-          "version": "3.0.7",
-          "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
-          "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
-        }
-      }
-    },
-    "gaxios": {
-      "version": "6.2.0",
-      "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.2.0.tgz",
-      "integrity": "sha512-H6+bHeoEAU5D6XNc6mPKeN5dLZqEDs9Gpk6I+SZBEzK5So58JVrHPmevNi35fRl1J9Y5TaeLW0kYx3pCJ1U2mQ==",
-      "requires": {
-        "extend": "^3.0.2",
-        "https-proxy-agent": "^7.0.1",
-        "is-stream": "^2.0.0",
-        "node-fetch": "^2.6.9"
-      },
-      "dependencies": {
-        "agent-base": {
-          "version": "7.1.0",
-          "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
-          "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==",
-          "requires": {
-            "debug": "^4.3.4"
-          }
-        },
-        "https-proxy-agent": {
-          "version": "7.0.4",
-          "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz",
-          "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==",
-          "requires": {
-            "agent-base": "^7.0.2",
-            "debug": "4"
-          }
-        }
-      }
-    },
-    "gcp-metadata": {
-      "version": "6.1.0",
-      "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz",
-      "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==",
-      "requires": {
-        "gaxios": "^6.0.0",
-        "json-bigint": "^1.0.0"
-      }
-    },
-    "gensync": {
-      "version": "1.0.0-beta.2",
-      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
-      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="
-    },
-    "geo-tz": {
-      "version": "8.0.2",
-      "resolved": "https://registry.npmjs.org/geo-tz/-/geo-tz-8.0.2.tgz",
-      "integrity": "sha512-NjEzJBzaMhO9C7lFZIsWDkVED7aLxcES3iEZOWJ97dhnDUGhEB8vhW7MaWR+2y4aWvtFV/VyuDi8Y0rUHvm4tw==",
-      "requires": {
-        "@turf/boolean-point-in-polygon": "^6.5.0",
-        "@turf/helpers": "^6.5.0",
-        "geobuf": "^3.0.2",
-        "pbf": "^3.2.1"
-      }
-    },
-    "geobuf": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/geobuf/-/geobuf-3.0.2.tgz",
-      "integrity": "sha512-ASgKwEAQQRnyNFHNvpd5uAwstbVYmiTW0Caw3fBb509tNTqXyAAPMyFs5NNihsLZhLxU1j/kjFhkhLWA9djuVg==",
-      "requires": {
-        "concat-stream": "^2.0.0",
-        "pbf": "^3.2.1",
-        "shapefile": "~0.6.6"
-      }
-    },
-    "get-caller-file": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
-      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
-    },
-    "get-func-name": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
-      "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
-      "dev": true
-    },
-    "get-intrinsic": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
-      "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
-      "requires": {
-        "es-errors": "^1.3.0",
-        "function-bind": "^1.1.2",
-        "has-proto": "^1.0.1",
-        "has-symbols": "^1.0.3",
-        "hasown": "^2.0.0"
-      }
-    },
-    "get-port": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz",
-      "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==",
-      "dev": true
-    },
-    "get-stdin": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
-      "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==",
-      "dev": true
-    },
-    "get-stream": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
-      "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
-      "dev": true
-    },
-    "glob": {
-      "version": "10.4.2",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz",
-      "integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==",
-      "requires": {
-        "foreground-child": "^3.1.0",
-        "jackspeak": "^3.1.2",
-        "minimatch": "^9.0.4",
-        "minipass": "^7.1.2",
-        "package-json-from-dist": "^1.0.0",
-        "path-scurry": "^1.11.1"
-      },
-      "dependencies": {
-        "brace-expansion": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
-          "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-          "requires": {
-            "balanced-match": "^1.0.0"
-          }
-        },
-        "jackspeak": {
-          "version": "3.4.2",
-          "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.2.tgz",
-          "integrity": "sha512-qH3nOSj8q/8+Eg8LUPOq3C+6HWkpUioIjDsq1+D4zY91oZvpPttw8GwtF1nReRYKXl+1AORyFqtm2f5Q1SB6/Q==",
-          "requires": {
-            "@isaacs/cliui": "^8.0.2",
-            "@pkgjs/parseargs": "^0.11.0"
-          }
-        },
-        "minimatch": {
-          "version": "9.0.4",
-          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
-          "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
-          "requires": {
-            "brace-expansion": "^2.0.1"
-          }
-        }
-      }
-    },
-    "glob-parent": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
-      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
-      "requires": {
-        "is-glob": "^4.0.1"
-      }
-    },
-    "glob-to-regexp": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
-      "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
-      "dev": true
-    },
-    "globals": {
-      "version": "15.9.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz",
-      "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==",
-      "dev": true
-    },
-    "globrex": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
-      "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
-      "dev": true
-    },
-    "gopd": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
-      "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
-      "requires": {
-        "get-intrinsic": "^1.1.3"
-      }
-    },
-    "graceful-fs": {
-      "version": "4.2.11",
-      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
-      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
-    },
-    "graphemer": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
-      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
-      "dev": true
-    },
-    "handlebars": {
-      "version": "4.7.8",
-      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
-      "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
-      "requires": {
-        "minimist": "^1.2.5",
-        "neo-async": "^2.6.2",
-        "source-map": "^0.6.1",
-        "uglify-js": "^3.1.4",
-        "wordwrap": "^1.0.0"
-      },
-      "dependencies": {
-        "source-map": {
-          "version": "0.6.1",
-          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
-        }
-      }
-    },
-    "has": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
-      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
-      "requires": {
-        "function-bind": "^1.1.1"
-      }
-    },
-    "has-flag": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
-    },
-    "has-own-prop": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz",
-      "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==",
-      "dev": true
-    },
-    "has-property-descriptors": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
-      "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
-      "requires": {
-        "get-intrinsic": "^1.2.2"
-      }
-    },
-    "has-proto": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
-      "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg=="
-    },
-    "has-symbols": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
-      "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
-    },
-    "has-unicode": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
-      "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
-    },
-    "hasown": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
-      "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
-      "requires": {
-        "function-bind": "^1.1.2"
-      }
-    },
-    "he": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
-      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
-    },
-    "highlight.js": {
-      "version": "10.7.3",
-      "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
-      "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="
-    },
-    "hosted-git-info": {
-      "version": "2.8.9",
-      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
-      "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
-      "dev": true
-    },
-    "html-escaper": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
-      "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
-      "dev": true
-    },
-    "html-to-text": {
-      "version": "9.0.5",
-      "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
-      "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==",
-      "requires": {
-        "@selderee/plugin-htmlparser2": "^0.11.0",
-        "deepmerge": "^4.3.1",
-        "dom-serializer": "^2.0.0",
-        "htmlparser2": "^8.0.2",
-        "selderee": "^0.11.0"
-      }
-    },
-    "htmlparser2": {
-      "version": "8.0.2",
-      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
-      "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
-      "requires": {
-        "domelementtype": "^2.3.0",
-        "domhandler": "^5.0.3",
-        "domutils": "^3.0.1",
-        "entities": "^4.4.0"
-      }
-    },
-    "http-errors": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
-      "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
-      "requires": {
-        "depd": "2.0.0",
-        "inherits": "2.0.4",
-        "setprototypeof": "1.2.0",
-        "statuses": "2.0.1",
-        "toidentifier": "1.0.1"
-      }
-    },
-    "https-proxy-agent": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
-      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
-      "requires": {
-        "agent-base": "6",
-        "debug": "4"
-      }
-    },
-    "human-signals": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
-      "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
-      "dev": true
-    },
-    "i18n-iso-countries": {
-      "version": "7.11.3",
-      "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.11.3.tgz",
-      "integrity": "sha512-yxQVzNvxEaspSqNnCbqLvwTZNXXkGydWcSxytJYZYb0KH5pn13fdywuX0vFxmOg57Z8ff416AuKDx6Oqnx+j9w==",
-      "requires": {
-        "diacritics": "1.3.0"
-      }
-    },
-    "iconv-lite": {
-      "version": "0.4.24",
-      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
-      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
-      "requires": {
-        "safer-buffer": ">= 2.1.2 < 3"
-      }
-    },
-    "ieee754": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
-      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
-    },
-    "ignore": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
-      "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
-      "dev": true
-    },
-    "import-fresh": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
-      "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
-      "requires": {
-        "parent-module": "^1.0.0",
-        "resolve-from": "^4.0.0"
-      }
-    },
-    "import-in-the-middle": {
-      "version": "1.8.0",
-      "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.8.0.tgz",
-      "integrity": "sha512-/xQjze8szLNnJ5rvHSzn+dcVXqCAU6Plbk4P24U/jwPmg1wy7IIp9OjKIO5tYue8GSPhDpPDiApQjvBUmWwhsQ==",
-      "requires": {
-        "acorn": "^8.8.2",
-        "acorn-import-attributes": "^1.9.5",
-        "cjs-module-lexer": "^1.2.2",
-        "module-details-from-path": "^1.0.3"
-      }
-    },
-    "imurmurhash": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
-      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
-      "dev": true
-    },
-    "indent-string": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
-      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
-      "dev": true
-    },
-    "inflight": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
-      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
-      "requires": {
-        "once": "^1.3.0",
-        "wrappy": "1"
-      }
-    },
-    "inherits": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
-      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
-    },
-    "ini": {
-      "version": "1.3.8",
-      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
-      "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
-    },
-    "inquirer": {
-      "version": "8.2.6",
-      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz",
-      "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==",
-      "requires": {
-        "ansi-escapes": "^4.2.1",
-        "chalk": "^4.1.1",
-        "cli-cursor": "^3.1.0",
-        "cli-width": "^3.0.0",
-        "external-editor": "^3.0.3",
-        "figures": "^3.0.0",
-        "lodash": "^4.17.21",
-        "mute-stream": "0.0.8",
-        "ora": "^5.4.1",
-        "run-async": "^2.4.0",
-        "rxjs": "^7.5.5",
-        "string-width": "^4.1.0",
-        "strip-ansi": "^6.0.0",
-        "through": "^2.3.6",
-        "wrap-ansi": "^6.0.1"
-      }
-    },
-    "ioredis": {
-      "version": "5.4.1",
-      "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz",
-      "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==",
-      "requires": {
-        "@ioredis/commands": "^1.1.1",
-        "cluster-key-slot": "^1.1.0",
-        "debug": "^4.3.4",
-        "denque": "^2.1.0",
-        "lodash.defaults": "^4.2.0",
-        "lodash.isarguments": "^3.1.0",
-        "redis-errors": "^1.2.0",
-        "redis-parser": "^3.0.0",
-        "standard-as-callback": "^2.1.0"
-      }
-    },
-    "ipaddr.js": {
-      "version": "1.9.1",
-      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
-      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
-    },
-    "is-arrayish": {
-      "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
-      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
-    },
-    "is-binary-path": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
-      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
-      "requires": {
-        "binary-extensions": "^2.0.0"
-      }
-    },
-    "is-builtin-module": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
-      "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
-      "dev": true,
-      "requires": {
-        "builtin-modules": "^3.3.0"
-      }
-    },
-    "is-core-module": {
-      "version": "2.13.0",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
-      "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==",
-      "requires": {
-        "has": "^1.0.3"
-      }
-    },
-    "is-extglob": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
-    },
-    "is-fullwidth-code-point": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
-    },
-    "is-glob": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
-      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
-      "requires": {
-        "is-extglob": "^2.1.1"
-      }
-    },
-    "is-interactive": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
-      "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="
-    },
-    "is-number": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
-      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
-    },
-    "is-path-inside": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
-      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
-      "dev": true
-    },
-    "is-stream": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
-      "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="
-    },
-    "is-unicode-supported": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
-      "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="
-    },
-    "isarray": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
-      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
-    },
-    "isexe": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
-    },
-    "istanbul-lib-coverage": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
-      "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
-      "dev": true
-    },
-    "istanbul-lib-report": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
-      "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
-      "dev": true,
-      "requires": {
-        "istanbul-lib-coverage": "^3.0.0",
-        "make-dir": "^4.0.0",
-        "supports-color": "^7.1.0"
-      },
-      "dependencies": {
-        "make-dir": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
-          "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
-          "dev": true,
-          "requires": {
-            "semver": "^7.5.3"
-          }
-        }
-      }
-    },
-    "istanbul-lib-source-maps": {
-      "version": "5.0.6",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
-      "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
-      "dev": true,
-      "requires": {
-        "@jridgewell/trace-mapping": "^0.3.23",
-        "debug": "^4.1.1",
-        "istanbul-lib-coverage": "^3.0.0"
-      }
-    },
-    "istanbul-reports": {
-      "version": "3.1.7",
-      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
-      "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
-      "dev": true,
-      "requires": {
-        "html-escaper": "^2.0.0",
-        "istanbul-lib-report": "^3.0.0"
-      }
-    },
-    "iterare": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz",
-      "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q=="
-    },
-    "jackspeak": {
-      "version": "2.3.6",
-      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
-      "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
-      "requires": {
-        "@isaacs/cliui": "^8.0.2",
-        "@pkgjs/parseargs": "^0.11.0"
-      }
-    },
-    "jiti": {
-      "version": "1.21.0",
-      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
-      "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
-      "peer": true
-    },
-    "joi": {
-      "version": "17.13.3",
-      "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
-      "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
-      "requires": {
-        "@hapi/hoek": "^9.3.0",
-        "@hapi/topo": "^5.1.0",
-        "@sideway/address": "^4.1.5",
-        "@sideway/formula": "^3.0.1",
-        "@sideway/pinpoint": "^2.0.0"
-      }
-    },
-    "jose": {
-      "version": "4.15.5",
-      "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz",
-      "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg=="
-    },
-    "js-beautify": {
-      "version": "1.15.1",
-      "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz",
-      "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==",
-      "requires": {
-        "config-chain": "^1.1.13",
-        "editorconfig": "^1.0.4",
-        "glob": "^10.3.3",
-        "js-cookie": "^3.0.5",
-        "nopt": "^7.2.0"
-      },
-      "dependencies": {
-        "abbrev": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
-          "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ=="
-        },
-        "nopt": {
-          "version": "7.2.0",
-          "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz",
-          "integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==",
-          "requires": {
-            "abbrev": "^2.0.0"
-          }
-        }
-      }
-    },
-    "js-cookie": {
-      "version": "3.0.5",
-      "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
-      "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="
-    },
-    "js-tokens": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
-      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
-    },
-    "js-yaml": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
-      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
-      "requires": {
-        "argparse": "^2.0.1"
-      }
-    },
-    "jsesc": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
-      "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
-      "dev": true
-    },
-    "json-bigint": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
-      "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
-      "requires": {
-        "bignumber.js": "^9.0.0"
-      }
-    },
-    "json-buffer": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
-      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
-      "dev": true
-    },
-    "json-parse-even-better-errors": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
-      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
-    },
-    "json-schema-traverse": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
-      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
-      "dev": true
-    },
-    "json-stable-stringify-without-jsonify": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
-      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
-      "dev": true
-    },
-    "json5": {
-      "version": "2.2.3",
-      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
-      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
-    },
-    "jsonc-parser": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz",
-      "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==",
-      "dev": true
-    },
-    "jsonfile": {
-      "version": "6.1.0",
-      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
-      "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
-      "dev": true,
-      "requires": {
-        "graceful-fs": "^4.1.6",
-        "universalify": "^2.0.0"
-      }
-    },
-    "keyv": {
-      "version": "4.5.4",
-      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
-      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
-      "dev": true,
-      "requires": {
-        "json-buffer": "3.0.1"
-      }
-    },
-    "lazystream": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
-      "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
-      "requires": {
-        "readable-stream": "^2.0.5"
-      },
-      "dependencies": {
-        "readable-stream": {
-          "version": "2.3.8",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
-          "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
-          "requires": {
-            "core-util-is": "~1.0.0",
-            "inherits": "~2.0.3",
-            "isarray": "~1.0.0",
-            "process-nextick-args": "~2.0.0",
-            "safe-buffer": "~5.1.1",
-            "string_decoder": "~1.1.1",
-            "util-deprecate": "~1.0.1"
-          }
-        },
-        "safe-buffer": {
-          "version": "5.1.2",
-          "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
-          "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
-        },
-        "string_decoder": {
-          "version": "1.1.1",
-          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
-          "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
-          "requires": {
-            "safe-buffer": "~5.1.0"
-          }
-        }
-      }
-    },
-    "leac": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
-      "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="
-    },
-    "levn": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
-      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
-      "dev": true,
-      "requires": {
-        "prelude-ls": "^1.2.1",
-        "type-check": "~0.4.0"
-      }
-    },
-    "libphonenumber-js": {
-      "version": "1.10.53",
-      "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.53.tgz",
-      "integrity": "sha512-sDTnnqlWK4vH4AlDQuswz3n4Hx7bIQWTpIcScJX+Sp7St3LXHmfiax/ZFfyYxHmkdCvydOLSuvtAO/XpXiSySw=="
-    },
-    "lilconfig": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
-      "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
-      "peer": true
-    },
-    "lines-and-columns": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
-      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
-    },
-    "load-tsconfig": {
-      "version": "0.2.5",
-      "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz",
-      "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==",
-      "dev": true
-    },
-    "loader-runner": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
-      "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
-      "dev": true
-    },
-    "locate-path": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
-      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
-      "dev": true,
-      "requires": {
-        "p-locate": "^5.0.0"
-      }
-    },
-    "lodash": {
-      "version": "4.17.21",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
-      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
-    },
-    "lodash.camelcase": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
-      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
-    },
-    "lodash.defaults": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
-      "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
-    },
-    "lodash.isarguments": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
-      "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="
-    },
-    "lodash.merge": {
-      "version": "4.6.2",
-      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
-      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
-    },
-    "log-symbols": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
-      "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
-      "requires": {
-        "chalk": "^4.1.0",
-        "is-unicode-supported": "^0.1.0"
-      }
-    },
-    "long": {
-      "version": "5.2.3",
-      "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
-      "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
-    },
-    "loose-envify": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
-      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
-      "requires": {
-        "js-tokens": "^3.0.0 || ^4.0.0"
-      }
-    },
-    "loupe": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz",
-      "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==",
-      "dev": true,
-      "requires": {
-        "get-func-name": "^2.0.1"
-      }
-    },
-    "lru-cache": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
-      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
-      "requires": {
-        "yallist": "^3.0.2"
-      }
-    },
-    "luxon": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
-      "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ=="
-    },
-    "magic-string": {
-      "version": "0.30.8",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
-      "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
-      "dev": true,
-      "requires": {
-        "@jridgewell/sourcemap-codec": "^1.4.15"
-      }
-    },
-    "magicast": {
-      "version": "0.3.4",
-      "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz",
-      "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==",
-      "dev": true,
-      "requires": {
-        "@babel/parser": "^7.24.4",
-        "@babel/types": "^7.24.0",
-        "source-map-js": "^1.2.0"
-      }
-    },
-    "make-dir": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
-      "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
-      "requires": {
-        "semver": "^6.0.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.1",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-          "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
-        }
-      }
-    },
-    "make-error": {
-      "version": "1.3.6",
-      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
-      "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
-      "optional": true,
-      "peer": true
-    },
-    "marked": {
-      "version": "7.0.4",
-      "resolved": "https://registry.npmjs.org/marked/-/marked-7.0.4.tgz",
-      "integrity": "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="
-    },
-    "md-to-react-email": {
-      "version": "5.0.2",
-      "resolved": "https://registry.npmjs.org/md-to-react-email/-/md-to-react-email-5.0.2.tgz",
-      "integrity": "sha512-x6kkpdzIzUhecda/yahltfEl53mH26QdWu4abUF9+S0Jgam8P//Ciro8cdhyMHnT5MQUJYrIbO6ORM2UxPiNNA==",
-      "requires": {
-        "marked": "7.0.4"
-      }
-    },
-    "media-typer": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
-      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
-    },
-    "memfs": {
-      "version": "3.5.3",
-      "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz",
-      "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==",
-      "dev": true,
-      "requires": {
-        "fs-monkey": "^1.0.4"
-      }
-    },
-    "merge-descriptors": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
-      "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
-    },
-    "merge-stream": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
-      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
-      "dev": true
-    },
-    "merge2": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
-      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="
-    },
-    "methods": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
-      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
-    },
-    "micromatch": {
-      "version": "4.0.5",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
-      "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
-      "requires": {
-        "braces": "^3.0.2",
-        "picomatch": "^2.3.1"
-      },
-      "dependencies": {
-        "picomatch": {
-          "version": "2.3.1",
-          "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-          "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
-        }
-      }
-    },
-    "mime": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
-      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
-    },
-    "mime-db": {
-      "version": "1.52.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
-      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
-    },
-    "mime-types": {
-      "version": "2.1.35",
-      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
-      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
-      "requires": {
-        "mime-db": "1.52.0"
-      }
-    },
-    "mimic-fn": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
-      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
-    },
-    "min-indent": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
-      "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
-      "dev": true
-    },
-    "minimatch": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-      "requires": {
-        "brace-expansion": "^1.1.7"
-      }
-    },
-    "minimist": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
-      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
-    },
-    "minipass": {
-      "version": "7.1.2",
-      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
-      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="
-    },
-    "minizlib": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
-      "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
-      "requires": {
-        "minipass": "^3.0.0",
-        "yallist": "^4.0.0"
-      },
-      "dependencies": {
-        "minipass": {
-          "version": "3.3.6",
-          "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
-          "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
-          "requires": {
-            "yallist": "^4.0.0"
-          }
-        },
-        "yallist": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
-        }
-      }
-    },
-    "mkdirp": {
-      "version": "0.5.6",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
-      "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
-      "requires": {
-        "minimist": "^1.2.6"
-      }
-    },
-    "mkdirp-classic": {
-      "version": "0.5.3",
-      "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
-      "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
-      "dev": true
-    },
-    "mock-fs": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.2.0.tgz",
-      "integrity": "sha512-2dF2R6YMSZbpip1V1WHKGLNjr/k48uQClqMVb5H3MOvwc9qhYis3/IWbj02qIg/Y8MDXKFF4c5v0rxx2o6xTZw==",
-      "dev": true
-    },
-    "module-details-from-path": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz",
-      "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A=="
-    },
-    "moo": {
-      "version": "0.5.2",
-      "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
-      "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==",
-      "dev": true
-    },
-    "mrmime": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
-      "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw=="
-    },
-    "ms": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
-    },
-    "msgpackr": {
-      "version": "1.10.1",
-      "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.10.1.tgz",
-      "integrity": "sha512-r5VRLv9qouXuLiIBrLpl2d5ZvPt8svdQTl5/vMvE4nzDMyEX4sgW5yWhuBBj5UmgwOTWj8CIdSXn5sAfsHAWIQ==",
-      "requires": {
-        "msgpackr-extract": "^3.0.2"
-      }
-    },
-    "msgpackr-extract": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz",
-      "integrity": "sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==",
-      "optional": true,
-      "requires": {
-        "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.2",
-        "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.2",
-        "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.2",
-        "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.2",
-        "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.2",
-        "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2",
-        "node-gyp-build-optional-packages": "5.0.7"
-      }
-    },
-    "multer": {
-      "version": "1.4.4-lts.1",
-      "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz",
-      "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==",
-      "requires": {
-        "append-field": "^1.0.0",
-        "busboy": "^1.0.0",
-        "concat-stream": "^1.5.2",
-        "mkdirp": "^0.5.4",
-        "object-assign": "^4.1.1",
-        "type-is": "^1.6.4",
-        "xtend": "^4.0.0"
-      },
-      "dependencies": {
-        "concat-stream": {
-          "version": "1.6.2",
-          "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
-          "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
-          "requires": {
-            "buffer-from": "^1.0.0",
-            "inherits": "^2.0.3",
-            "readable-stream": "^2.2.2",
-            "typedarray": "^0.0.6"
-          }
-        },
-        "readable-stream": {
-          "version": "2.3.8",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
-          "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
-          "requires": {
-            "core-util-is": "~1.0.0",
-            "inherits": "~2.0.3",
-            "isarray": "~1.0.0",
-            "process-nextick-args": "~2.0.0",
-            "safe-buffer": "~5.1.1",
-            "string_decoder": "~1.1.1",
-            "util-deprecate": "~1.0.1"
-          }
-        },
-        "safe-buffer": {
-          "version": "5.1.2",
-          "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
-          "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
-        },
-        "string_decoder": {
-          "version": "1.1.1",
-          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
-          "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
-          "requires": {
-            "safe-buffer": "~5.1.0"
-          }
-        }
-      }
-    },
-    "mute-stream": {
-      "version": "0.0.8",
-      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
-      "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
-    },
-    "mz": {
-      "version": "2.7.0",
-      "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
-      "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
-      "requires": {
-        "any-promise": "^1.0.0",
-        "object-assign": "^4.0.1",
-        "thenify-all": "^1.0.0"
-      }
-    },
-    "nan": {
-      "version": "2.18.0",
-      "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
-      "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==",
-      "dev": true,
-      "optional": true
-    },
-    "nanoid": {
-      "version": "3.3.7",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
-      "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="
-    },
-    "natural-compare": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
-      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
-      "dev": true
-    },
-    "nearley": {
-      "version": "2.20.1",
-      "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz",
-      "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==",
-      "dev": true,
-      "requires": {
-        "commander": "^2.19.0",
-        "moo": "^0.5.0",
-        "railroad-diagrams": "^1.0.0",
-        "randexp": "0.4.6"
-      },
-      "dependencies": {
-        "commander": {
-          "version": "2.20.3",
-          "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
-          "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
-          "dev": true
-        }
-      }
-    },
-    "negotiator": {
-      "version": "0.6.3",
-      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
-      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
-    },
-    "neo-async": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
-      "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
-    },
-    "nest-commander": {
-      "version": "3.15.0",
-      "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.15.0.tgz",
-      "integrity": "sha512-o9VEfFj/w2nm+hQi6fnkxL1GAFZW/KmuGcIE7/B/TX0gwm0QVy8svAF75EQm8wrDjcvWS7Cx/ArnkFn2C+iM2w==",
-      "requires": {
-        "@fig/complete-commander": "^3.0.0",
-        "@golevelup/nestjs-discovery": "4.0.1",
-        "commander": "11.1.0",
-        "cosmiconfig": "8.3.6",
-        "inquirer": "8.2.6"
-      },
-      "dependencies": {
-        "@fig/complete-commander": {
-          "version": "3.2.0",
-          "resolved": "https://registry.npmjs.org/@fig/complete-commander/-/complete-commander-3.2.0.tgz",
-          "integrity": "sha512-1Holl3XtRiANVKURZwgpjCnPuV4RsHp+XC0MhgvyAX/avQwj7F2HUItYOvGi/bXjJCkEzgBZmVfCr0HBA+q+Bw==",
-          "requires": {
-            "prettier": "^3.2.5"
-          }
-        },
-        "commander": {
-          "version": "11.1.0",
-          "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
-          "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="
-        }
-      }
-    },
-    "nestjs-cls": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-4.4.1.tgz",
-      "integrity": "sha512-4yhldwm/cJ02lQ8ZAdM8KQ7gMfjAc1z3fo5QAQgXNyN4N6X5So9BCwv+BTLRugDCkELUo3qtzQHnKhGYL/ftPg==",
-      "requires": {}
-    },
-    "nestjs-otel": {
-      "version": "6.1.1",
-      "resolved": "https://registry.npmjs.org/nestjs-otel/-/nestjs-otel-6.1.1.tgz",
-      "integrity": "sha512-hWuhDYkkaZrBXpHmi2v0jGqKa61uPqzu2YsVhww8/s+v9SaDILylR7ZdoOiygCQisgHG9rw5odP12GfsMS8cBA==",
-      "requires": {
-        "@opentelemetry/api": "^1.8.0",
-        "@opentelemetry/host-metrics": "^0.35.1",
-        "response-time": "^2.3.2"
-      }
-    },
-    "next": {
-      "version": "14.2.3",
-      "resolved": "https://registry.npmjs.org/next/-/next-14.2.3.tgz",
-      "integrity": "sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==",
-      "requires": {
-        "@next/env": "14.2.3",
-        "@next/swc-darwin-arm64": "14.2.3",
-        "@next/swc-darwin-x64": "14.2.3",
-        "@next/swc-linux-arm64-gnu": "14.2.3",
-        "@next/swc-linux-arm64-musl": "14.2.3",
-        "@next/swc-linux-x64-gnu": "14.2.3",
-        "@next/swc-linux-x64-musl": "14.2.3",
-        "@next/swc-win32-arm64-msvc": "14.2.3",
-        "@next/swc-win32-ia32-msvc": "14.2.3",
-        "@next/swc-win32-x64-msvc": "14.2.3",
-        "@swc/helpers": "0.5.5",
-        "busboy": "1.6.0",
-        "caniuse-lite": "^1.0.30001579",
-        "graceful-fs": "^4.2.11",
-        "postcss": "8.4.31",
-        "styled-jsx": "5.1.1"
-      },
-      "dependencies": {
-        "postcss": {
-          "version": "8.4.31",
-          "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
-          "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
-          "requires": {
-            "nanoid": "^3.3.6",
-            "picocolors": "^1.0.0",
-            "source-map-js": "^1.0.2"
-          }
-        }
-      }
-    },
-    "node-abort-controller": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
-      "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="
-    },
-    "node-emoji": {
-      "version": "1.11.0",
-      "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
-      "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==",
-      "dev": true,
-      "requires": {
-        "lodash": "^4.17.21"
-      }
-    },
-    "node-fetch": {
-      "version": "2.7.0",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
-      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
-      "requires": {
-        "whatwg-url": "^5.0.0"
-      }
-    },
-    "node-gyp-build-optional-packages": {
-      "version": "5.0.7",
-      "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz",
-      "integrity": "sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==",
-      "optional": true
-    },
-    "node-releases": {
-      "version": "2.0.14",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
-      "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
-    },
-    "nodemailer": {
-      "version": "6.9.14",
-      "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz",
-      "integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA=="
-    },
-    "nopt": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
-      "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
-      "requires": {
-        "abbrev": "1"
-      }
-    },
-    "normalize-package-data": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
-      "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
-      "dev": true,
-      "requires": {
-        "hosted-git-info": "^2.1.4",
-        "resolve": "^1.10.0",
-        "semver": "2 || 3 || 4 || 5",
-        "validate-npm-package-license": "^3.0.1"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "5.7.2",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
-          "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
-          "dev": true
-        }
-      }
-    },
-    "normalize-path": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
-      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
-    },
-    "notepack.io": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-3.0.1.tgz",
-      "integrity": "sha512-TKC/8zH5pXIAMVQio2TvVDTtPRX+DJPHDqjRbxogtFiByHyzKmy96RA0JtCQJ+WouyyL4A10xomQzgbUT+1jCg=="
-    },
-    "npm-run-path": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
-      "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
-      "dev": true,
-      "requires": {
-        "path-key": "^4.0.0"
-      },
-      "dependencies": {
-        "path-key": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
-          "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
-          "dev": true
-        }
-      }
-    },
-    "npmlog": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
-      "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
-      "requires": {
-        "are-we-there-yet": "^2.0.0",
-        "console-control-strings": "^1.1.0",
-        "gauge": "^3.0.0",
-        "set-blocking": "^2.0.0"
-      }
-    },
-    "object-assign": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
-      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
-    },
-    "object-hash": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
-      "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="
-    },
-    "object-inspect": {
-      "version": "1.12.3",
-      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
-      "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g=="
-    },
-    "obuf": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
-      "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
-    },
-    "oidc-token-hash": {
-      "version": "5.0.3",
-      "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",
-      "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw=="
-    },
-    "on-finished": {
-      "version": "2.4.1",
-      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
-      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
-      "requires": {
-        "ee-first": "1.1.1"
-      }
-    },
-    "on-headers": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
-      "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
-    },
-    "once": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
-      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
-      "requires": {
-        "wrappy": "1"
-      }
-    },
-    "onetime": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
-      "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
-      "requires": {
-        "mimic-fn": "^2.1.0"
-      }
-    },
-    "openid-client": {
-      "version": "5.6.5",
-      "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz",
-      "integrity": "sha512-5P4qO9nGJzB5PI0LFlhj4Dzg3m4odt0qsJTfyEtZyOlkgpILwEioOhVVJOrS1iVH494S4Ee5OCjjg6Bf5WOj3w==",
-      "requires": {
-        "jose": "^4.15.5",
-        "lru-cache": "^6.0.0",
-        "object-hash": "^2.2.0",
-        "oidc-token-hash": "^5.0.3"
-      },
-      "dependencies": {
-        "lru-cache": {
-          "version": "6.0.0",
-          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-          "requires": {
-            "yallist": "^4.0.0"
-          }
-        },
-        "object-hash": {
-          "version": "2.2.0",
-          "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
-          "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="
-        },
-        "yallist": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
-        }
-      }
-    },
-    "optionator": {
-      "version": "0.9.3",
-      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
-      "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
-      "dev": true,
-      "requires": {
-        "@aashutoshrathi/word-wrap": "^1.2.3",
-        "deep-is": "^0.1.3",
-        "fast-levenshtein": "^2.0.6",
-        "levn": "^0.4.1",
-        "prelude-ls": "^1.2.1",
-        "type-check": "^0.4.0"
-      }
-    },
-    "ora": {
-      "version": "5.4.1",
-      "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
-      "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
-      "requires": {
-        "bl": "^4.1.0",
-        "chalk": "^4.1.0",
-        "cli-cursor": "^3.1.0",
-        "cli-spinners": "^2.5.0",
-        "is-interactive": "^1.0.0",
-        "is-unicode-supported": "^0.1.0",
-        "log-symbols": "^4.1.0",
-        "strip-ansi": "^6.0.0",
-        "wcwidth": "^1.0.1"
-      }
-    },
-    "os-tmpdir": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
-      "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="
-    },
-    "p-limit": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
-      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
-      "dev": true,
-      "requires": {
-        "yocto-queue": "^0.1.0"
-      }
-    },
-    "p-locate": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
-      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
-      "dev": true,
-      "requires": {
-        "p-limit": "^3.0.2"
-      }
-    },
-    "p-try": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
-      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
-      "dev": true
-    },
-    "package-json-from-dist": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
-      "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw=="
-    },
-    "parent-module": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
-      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
-      "requires": {
-        "callsites": "^3.0.0"
-      }
-    },
-    "parse-json": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
-      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
-      "requires": {
-        "@babel/code-frame": "^7.0.0",
-        "error-ex": "^1.3.1",
-        "json-parse-even-better-errors": "^2.3.0",
-        "lines-and-columns": "^1.1.6"
-      }
-    },
-    "parse5": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
-      "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="
-    },
-    "parse5-htmlparser2-tree-adapter": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
-      "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==",
-      "requires": {
-        "parse5": "^6.0.1"
-      },
-      "dependencies": {
-        "parse5": {
-          "version": "6.0.1",
-          "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
-          "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
-        }
-      }
-    },
-    "parseley": {
-      "version": "0.12.1",
-      "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
-      "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==",
-      "requires": {
-        "leac": "^0.6.0",
-        "peberminta": "^0.9.0"
-      }
-    },
-    "parseurl": {
-      "version": "1.3.3",
-      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
-      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
-    },
-    "path-exists": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
-      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
-      "dev": true
-    },
-    "path-is-absolute": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
-      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="
-    },
-    "path-key": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
-      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
-    },
-    "path-parse": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
-      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
-    },
-    "path-scurry": {
-      "version": "1.11.1",
-      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
-      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
-      "requires": {
-        "lru-cache": "^10.2.0",
-        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
-      },
-      "dependencies": {
-        "lru-cache": {
-          "version": "10.2.0",
-          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
-          "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q=="
-        }
-      }
-    },
-    "path-source": {
-      "version": "0.1.3",
-      "resolved": "https://registry.npmjs.org/path-source/-/path-source-0.1.3.tgz",
-      "integrity": "sha512-dWRHm5mIw5kw0cs3QZLNmpUWty48f5+5v9nWD2dw3Y0Hf+s01Ag8iJEWV0Sm0kocE8kK27DrIowha03e1YR+Qw==",
-      "requires": {
-        "array-source": "0.0",
-        "file-source": "0.6"
-      }
-    },
-    "path-to-regexp": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz",
-      "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA=="
-    },
-    "path-type": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
-      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="
-    },
-    "pathe": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
-      "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
-      "dev": true
-    },
-    "pathval": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
-      "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
-      "dev": true
-    },
-    "pbf": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz",
-      "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==",
-      "requires": {
-        "ieee754": "^1.1.12",
-        "resolve-protobuf-schema": "^2.1.0"
-      }
-    },
-    "peberminta": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
-      "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="
-    },
-    "pg": {
-      "version": "8.12.0",
-      "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz",
-      "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==",
-      "requires": {
-        "pg-cloudflare": "^1.1.1",
-        "pg-connection-string": "^2.6.4",
-        "pg-pool": "^3.6.2",
-        "pg-protocol": "^1.6.1",
-        "pg-types": "^2.1.0",
-        "pgpass": "1.x"
-      }
-    },
-    "pg-cloudflare": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
-      "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
-      "optional": true
-    },
-    "pg-connection-string": {
-      "version": "2.6.4",
-      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz",
-      "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA=="
-    },
-    "pg-int8": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
-      "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="
-    },
-    "pg-numeric": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz",
-      "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw=="
-    },
-    "pg-pool": {
-      "version": "3.6.2",
-      "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz",
-      "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==",
-      "requires": {}
-    },
-    "pg-protocol": {
-      "version": "1.6.1",
-      "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz",
-      "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg=="
-    },
-    "pg-types": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
-      "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
-      "requires": {
-        "pg-int8": "1.0.1",
-        "postgres-array": "~2.0.0",
-        "postgres-bytea": "~1.0.0",
-        "postgres-date": "~1.0.4",
-        "postgres-interval": "^1.1.0"
-      }
-    },
-    "pgpass": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
-      "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
-      "requires": {
-        "split2": "^4.1.0"
-      }
-    },
-    "picocolors": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
-      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
-    },
-    "picomatch": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
-      "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="
-    },
-    "pify": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
-      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
-      "peer": true
-    },
-    "pirates": {
-      "version": "4.0.6",
-      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
-      "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
-      "peer": true
-    },
-    "pluralize": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
-      "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
-      "dev": true
-    },
-    "postcss": {
-      "version": "8.4.38",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
-      "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
-      "requires": {
-        "nanoid": "^3.3.7",
-        "picocolors": "^1.0.0",
-        "source-map-js": "^1.2.0"
-      }
-    },
-    "postcss-import": {
-      "version": "15.1.0",
-      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
-      "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
-      "peer": true,
-      "requires": {
-        "postcss-value-parser": "^4.0.0",
-        "read-cache": "^1.0.0",
-        "resolve": "^1.1.7"
-      }
-    },
-    "postcss-js": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
-      "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
-      "peer": true,
-      "requires": {
-        "camelcase-css": "^2.0.1"
-      }
-    },
-    "postcss-load-config": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
-      "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
-      "peer": true,
-      "requires": {
-        "lilconfig": "^3.0.0",
-        "yaml": "^2.3.4"
-      },
-      "dependencies": {
-        "lilconfig": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz",
-          "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==",
-          "peer": true
-        }
-      }
-    },
-    "postcss-nested": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
-      "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
-      "peer": true,
-      "requires": {
-        "postcss-selector-parser": "^6.0.11"
-      }
-    },
-    "postcss-selector-parser": {
-      "version": "6.0.16",
-      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz",
-      "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==",
-      "peer": true,
-      "requires": {
-        "cssesc": "^3.0.0",
-        "util-deprecate": "^1.0.2"
-      }
-    },
-    "postcss-value-parser": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
-      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
-      "peer": true
-    },
-    "postgres-array": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
-      "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="
-    },
-    "postgres-bytea": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
-      "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="
-    },
-    "postgres-date": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
-      "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="
-    },
-    "postgres-interval": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
-      "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
-      "requires": {
-        "xtend": "^4.0.0"
-      }
-    },
-    "postgres-range": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.3.tgz",
-      "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g=="
-    },
-    "prelude-ls": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
-      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
-      "dev": true
-    },
-    "prettier": {
-      "version": "3.3.3",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
-      "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew=="
-    },
-    "prettier-linter-helpers": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
-      "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
-      "dev": true,
-      "requires": {
-        "fast-diff": "^1.1.2"
-      }
-    },
-    "prettier-plugin-organize-imports": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
-      "integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
-      "dev": true,
-      "requires": {}
-    },
-    "prismjs": {
-      "version": "1.29.0",
-      "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
-      "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q=="
-    },
-    "process": {
-      "version": "0.11.10",
-      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
-      "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="
-    },
-    "process-nextick-args": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
-      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
-    },
-    "proper-lockfile": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
-      "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
-      "dev": true,
-      "requires": {
-        "graceful-fs": "^4.2.4",
-        "retry": "^0.12.0",
-        "signal-exit": "^3.0.2"
-      },
-      "dependencies": {
-        "signal-exit": {
-          "version": "3.0.7",
-          "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
-          "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
-          "dev": true
-        }
-      }
-    },
-    "properties-reader": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/properties-reader/-/properties-reader-2.3.0.tgz",
-      "integrity": "sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==",
-      "dev": true,
-      "requires": {
-        "mkdirp": "^1.0.4"
-      },
-      "dependencies": {
-        "mkdirp": {
-          "version": "1.0.4",
-          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
-          "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
-          "dev": true
-        }
-      }
-    },
-    "proto-list": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
-      "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="
-    },
-    "protobufjs": {
-      "version": "7.3.2",
-      "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.2.tgz",
-      "integrity": "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==",
-      "requires": {
-        "@protobufjs/aspromise": "^1.1.2",
-        "@protobufjs/base64": "^1.1.2",
-        "@protobufjs/codegen": "^2.0.4",
-        "@protobufjs/eventemitter": "^1.1.0",
-        "@protobufjs/fetch": "^1.1.0",
-        "@protobufjs/float": "^1.0.2",
-        "@protobufjs/inquire": "^1.1.0",
-        "@protobufjs/path": "^1.1.2",
-        "@protobufjs/pool": "^1.1.0",
-        "@protobufjs/utf8": "^1.1.0",
-        "@types/node": ">=13.7.0",
-        "long": "^5.0.0"
-      }
-    },
-    "protocol-buffers-schema": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
-      "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="
-    },
-    "proxy-addr": {
-      "version": "2.0.7",
-      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
-      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
-      "requires": {
-        "forwarded": "0.2.0",
-        "ipaddr.js": "1.9.1"
-      }
-    },
-    "pump": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
-      "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
-      "dev": true,
-      "requires": {
-        "end-of-stream": "^1.1.0",
-        "once": "^1.3.1"
-      }
-    },
-    "punycode": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
-      "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
-      "dev": true
-    },
-    "qs": {
-      "version": "6.11.0",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
-      "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
-      "requires": {
-        "side-channel": "^1.0.4"
-      }
-    },
-    "queue-microtask": {
-      "version": "1.2.3",
-      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
-      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
-    },
-    "queue-tick": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
-      "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="
-    },
-    "railroad-diagrams": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
-      "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==",
-      "dev": true
-    },
-    "randexp": {
-      "version": "0.4.6",
-      "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz",
-      "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==",
-      "dev": true,
-      "requires": {
-        "discontinuous-range": "1.0.0",
-        "ret": "~0.1.10"
-      }
-    },
-    "randombytes": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
-      "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
-      "dev": true,
-      "requires": {
-        "safe-buffer": "^5.1.0"
-      }
-    },
-    "range-parser": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
-      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
-    },
-    "raw-body": {
-      "version": "2.5.2",
-      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
-      "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
-      "requires": {
-        "bytes": "3.1.2",
-        "http-errors": "2.0.0",
-        "iconv-lite": "0.4.24",
-        "unpipe": "1.0.0"
-      }
-    },
-    "react": {
-      "version": "18.3.1",
-      "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
-      "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
-      "requires": {
-        "loose-envify": "^1.1.0"
-      }
-    },
-    "react-dom": {
-      "version": "18.3.1",
-      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
-      "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
-      "peer": true,
-      "requires": {
-        "loose-envify": "^1.1.0",
-        "scheduler": "^0.23.2"
-      }
-    },
-    "react-email": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/react-email/-/react-email-3.0.1.tgz",
-      "integrity": "sha512-G4Bkx2ULIScy/0Z8nnWywHt0W1iTkaYCdh9rWNuQ3eVZ6B3ttTUDE9uUy3VNQ8dtQbmG0cpt8+XmImw7mMBW6Q==",
-      "requires": {
-        "@babel/core": "7.24.5",
-        "@babel/parser": "7.24.5",
-        "chalk": "4.1.2",
-        "chokidar": "3.6.0",
-        "commander": "11.1.0",
-        "debounce": "2.0.0",
-        "esbuild": "0.19.11",
-        "glob": "10.3.4",
-        "log-symbols": "4.1.0",
-        "mime-types": "2.1.35",
-        "next": "14.2.3",
-        "normalize-path": "3.0.0",
-        "ora": "5.4.1",
-        "socket.io": "4.7.5"
-      },
-      "dependencies": {
-        "@esbuild/aix-ppc64": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz",
-          "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==",
-          "optional": true
-        },
-        "@esbuild/android-arm": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz",
-          "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==",
-          "optional": true
-        },
-        "@esbuild/android-arm64": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz",
-          "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==",
-          "optional": true
-        },
-        "@esbuild/android-x64": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz",
-          "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==",
-          "optional": true
-        },
-        "@esbuild/darwin-arm64": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz",
-          "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==",
-          "optional": true
-        },
-        "@esbuild/darwin-x64": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz",
-          "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==",
-          "optional": true
-        },
-        "@esbuild/freebsd-arm64": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz",
-          "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==",
-          "optional": true
-        },
-        "@esbuild/freebsd-x64": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz",
-          "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==",
-          "optional": true
-        },
-        "@esbuild/linux-arm": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz",
-          "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==",
-          "optional": true
-        },
-        "@esbuild/linux-arm64": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz",
-          "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==",
-          "optional": true
-        },
-        "@esbuild/linux-ia32": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz",
-          "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==",
-          "optional": true
-        },
-        "@esbuild/linux-loong64": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz",
-          "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==",
-          "optional": true
-        },
-        "@esbuild/linux-mips64el": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz",
-          "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==",
-          "optional": true
-        },
-        "@esbuild/linux-ppc64": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz",
-          "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==",
-          "optional": true
-        },
-        "@esbuild/linux-riscv64": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz",
-          "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==",
-          "optional": true
-        },
-        "@esbuild/linux-s390x": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz",
-          "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==",
-          "optional": true
-        },
-        "@esbuild/linux-x64": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz",
-          "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==",
-          "optional": true
-        },
-        "@esbuild/netbsd-x64": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz",
-          "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==",
-          "optional": true
-        },
-        "@esbuild/openbsd-x64": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz",
-          "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==",
-          "optional": true
-        },
-        "@esbuild/sunos-x64": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz",
-          "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==",
-          "optional": true
-        },
-        "@esbuild/win32-arm64": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz",
-          "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==",
-          "optional": true
-        },
-        "@esbuild/win32-ia32": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz",
-          "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==",
-          "optional": true
-        },
-        "@esbuild/win32-x64": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz",
-          "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==",
-          "optional": true
-        },
-        "brace-expansion": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
-          "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-          "requires": {
-            "balanced-match": "^1.0.0"
-          }
-        },
-        "commander": {
-          "version": "11.1.0",
-          "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
-          "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="
-        },
-        "esbuild": {
-          "version": "0.19.11",
-          "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz",
-          "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==",
-          "requires": {
-            "@esbuild/aix-ppc64": "0.19.11",
-            "@esbuild/android-arm": "0.19.11",
-            "@esbuild/android-arm64": "0.19.11",
-            "@esbuild/android-x64": "0.19.11",
-            "@esbuild/darwin-arm64": "0.19.11",
-            "@esbuild/darwin-x64": "0.19.11",
-            "@esbuild/freebsd-arm64": "0.19.11",
-            "@esbuild/freebsd-x64": "0.19.11",
-            "@esbuild/linux-arm": "0.19.11",
-            "@esbuild/linux-arm64": "0.19.11",
-            "@esbuild/linux-ia32": "0.19.11",
-            "@esbuild/linux-loong64": "0.19.11",
-            "@esbuild/linux-mips64el": "0.19.11",
-            "@esbuild/linux-ppc64": "0.19.11",
-            "@esbuild/linux-riscv64": "0.19.11",
-            "@esbuild/linux-s390x": "0.19.11",
-            "@esbuild/linux-x64": "0.19.11",
-            "@esbuild/netbsd-x64": "0.19.11",
-            "@esbuild/openbsd-x64": "0.19.11",
-            "@esbuild/sunos-x64": "0.19.11",
-            "@esbuild/win32-arm64": "0.19.11",
-            "@esbuild/win32-ia32": "0.19.11",
-            "@esbuild/win32-x64": "0.19.11"
-          }
-        },
-        "glob": {
-          "version": "10.3.4",
-          "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz",
-          "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==",
-          "requires": {
-            "foreground-child": "^3.1.0",
-            "jackspeak": "^2.0.3",
-            "minimatch": "^9.0.1",
-            "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
-            "path-scurry": "^1.10.1"
-          }
-        },
-        "minimatch": {
-          "version": "9.0.5",
-          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
-          "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
-          "requires": {
-            "brace-expansion": "^2.0.1"
-          }
-        }
-      }
-    },
-    "react-promise-suspense": {
-      "version": "0.3.4",
-      "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz",
-      "integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==",
-      "requires": {
-        "fast-deep-equal": "^2.0.1"
-      },
-      "dependencies": {
-        "fast-deep-equal": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
-          "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="
-        }
-      }
-    },
-    "read-cache": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
-      "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
-      "peer": true,
-      "requires": {
-        "pify": "^2.3.0"
-      }
-    },
-    "read-pkg": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
-      "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
-      "dev": true,
-      "requires": {
-        "@types/normalize-package-data": "^2.4.0",
-        "normalize-package-data": "^2.5.0",
-        "parse-json": "^5.0.0",
-        "type-fest": "^0.6.0"
-      },
-      "dependencies": {
-        "type-fest": {
-          "version": "0.6.0",
-          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
-          "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
-          "dev": true
-        }
-      }
-    },
-    "read-pkg-up": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz",
-      "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==",
-      "dev": true,
-      "requires": {
-        "find-up": "^4.1.0",
-        "read-pkg": "^5.2.0",
-        "type-fest": "^0.8.1"
-      },
-      "dependencies": {
-        "find-up": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
-          "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
-          "dev": true,
-          "requires": {
-            "locate-path": "^5.0.0",
-            "path-exists": "^4.0.0"
-          }
-        },
-        "locate-path": {
-          "version": "5.0.0",
-          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
-          "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
-          "dev": true,
-          "requires": {
-            "p-locate": "^4.1.0"
-          }
-        },
-        "p-limit": {
-          "version": "2.3.0",
-          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
-          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
-          "dev": true,
-          "requires": {
-            "p-try": "^2.0.0"
-          }
-        },
-        "p-locate": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
-          "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
-          "dev": true,
-          "requires": {
-            "p-limit": "^2.2.0"
-          }
-        },
-        "type-fest": {
-          "version": "0.8.1",
-          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
-          "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
-          "dev": true
-        }
-      }
-    },
-    "readable-stream": {
-      "version": "3.6.2",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
-      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
-      "requires": {
-        "inherits": "^2.0.3",
-        "string_decoder": "^1.1.1",
-        "util-deprecate": "^1.0.1"
-      }
-    },
-    "readdir-glob": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
-      "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
-      "requires": {
-        "minimatch": "^5.1.0"
-      },
-      "dependencies": {
-        "brace-expansion": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
-          "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-          "requires": {
-            "balanced-match": "^1.0.0"
-          }
-        },
-        "minimatch": {
-          "version": "5.1.6",
-          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
-          "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
-          "requires": {
-            "brace-expansion": "^2.0.1"
-          }
-        }
-      }
-    },
-    "readdirp": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
-      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
-      "requires": {
-        "picomatch": "^2.2.1"
-      },
-      "dependencies": {
-        "picomatch": {
-          "version": "2.3.1",
-          "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-          "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
-        }
-      }
-    },
-    "redis-errors": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
-      "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="
-    },
-    "redis-parser": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
-      "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
-      "requires": {
-        "redis-errors": "^1.0.0"
-      }
-    },
-    "reflect-metadata": {
-      "version": "0.2.2",
-      "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
-      "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="
-    },
-    "regexp-tree": {
-      "version": "0.1.27",
-      "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
-      "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
-      "dev": true
-    },
-    "regjsparser": {
-      "version": "0.10.0",
-      "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz",
-      "integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==",
-      "dev": true,
-      "requires": {
-        "jsesc": "~0.5.0"
-      },
-      "dependencies": {
-        "jsesc": {
-          "version": "0.5.0",
-          "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
-          "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==",
-          "dev": true
-        }
-      }
-    },
-    "repeat-string": {
-      "version": "1.6.1",
-      "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
-      "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
-      "dev": true
-    },
-    "require-directory": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
-      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="
-    },
-    "require-from-string": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
-      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
-      "dev": true
-    },
-    "require-in-the-middle": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz",
-      "integrity": "sha512-3TLx5TGyAY6AOqLBoXmHkNql0HIf2RGbuMgCDT2WO/uGVAPJs6h7Kl+bN6TIZGd9bWhWPwnDnTHGtW8Iu77sdw==",
-      "requires": {
-        "debug": "^4.1.1",
-        "module-details-from-path": "^1.0.3",
-        "resolve": "^1.22.1"
-      }
-    },
-    "resolve": {
-      "version": "1.22.6",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz",
-      "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==",
-      "requires": {
-        "is-core-module": "^2.13.0",
-        "path-parse": "^1.0.7",
-        "supports-preserve-symlinks-flag": "^1.0.0"
-      }
-    },
-    "resolve-from": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
-      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
-    },
-    "resolve-protobuf-schema": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
-      "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
-      "requires": {
-        "protocol-buffers-schema": "^3.3.1"
-      }
-    },
-    "response-time": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmjs.org/response-time/-/response-time-2.3.2.tgz",
-      "integrity": "sha512-MUIDaDQf+CVqflfTdQ5yam+aYCkXj1PY8fjlPDQ6ppxJlmgZb864pHtA750mayywNg8tx4rS7qH9JXd/OF+3gw==",
-      "requires": {
-        "depd": "~1.1.0",
-        "on-headers": "~1.0.1"
-      },
-      "dependencies": {
-        "depd": {
-          "version": "1.1.2",
-          "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
-          "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="
-        }
-      }
-    },
-    "restore-cursor": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
-      "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
-      "requires": {
-        "onetime": "^5.1.0",
-        "signal-exit": "^3.0.2"
-      },
-      "dependencies": {
-        "signal-exit": {
-          "version": "3.0.7",
-          "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
-          "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
-        }
-      }
-    },
-    "ret": {
-      "version": "0.1.15",
-      "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
-      "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
-      "dev": true
-    },
-    "retry": {
-      "version": "0.12.0",
-      "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
-      "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
-      "dev": true
-    },
-    "reusify": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
-      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="
-    },
-    "rimraf": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz",
-      "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==",
-      "dev": true,
-      "requires": {
-        "glob": "^11.0.0",
-        "package-json-from-dist": "^1.0.0"
-      },
-      "dependencies": {
-        "brace-expansion": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
-          "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-          "dev": true,
-          "requires": {
-            "balanced-match": "^1.0.0"
-          }
-        },
-        "glob": {
-          "version": "11.0.0",
-          "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz",
-          "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==",
-          "dev": true,
-          "requires": {
-            "foreground-child": "^3.1.0",
-            "jackspeak": "^4.0.1",
-            "minimatch": "^10.0.0",
-            "minipass": "^7.1.2",
-            "package-json-from-dist": "^1.0.0",
-            "path-scurry": "^2.0.0"
-          }
-        },
-        "jackspeak": {
-          "version": "4.0.1",
-          "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz",
-          "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==",
-          "dev": true,
-          "requires": {
-            "@isaacs/cliui": "^8.0.2",
-            "@pkgjs/parseargs": "^0.11.0"
-          }
-        },
-        "lru-cache": {
-          "version": "11.0.0",
-          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz",
-          "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==",
-          "dev": true
-        },
-        "minimatch": {
-          "version": "10.0.1",
-          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
-          "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
-          "dev": true,
-          "requires": {
-            "brace-expansion": "^2.0.1"
-          }
-        },
-        "path-scurry": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
-          "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
-          "dev": true,
-          "requires": {
-            "lru-cache": "^11.0.0",
-            "minipass": "^7.1.2"
-          }
-        }
-      }
-    },
-    "rollup": {
-      "version": "4.14.3",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.3.tgz",
-      "integrity": "sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==",
-      "dev": true,
-      "requires": {
-        "@rollup/rollup-android-arm-eabi": "4.14.3",
-        "@rollup/rollup-android-arm64": "4.14.3",
-        "@rollup/rollup-darwin-arm64": "4.14.3",
-        "@rollup/rollup-darwin-x64": "4.14.3",
-        "@rollup/rollup-linux-arm-gnueabihf": "4.14.3",
-        "@rollup/rollup-linux-arm-musleabihf": "4.14.3",
-        "@rollup/rollup-linux-arm64-gnu": "4.14.3",
-        "@rollup/rollup-linux-arm64-musl": "4.14.3",
-        "@rollup/rollup-linux-powerpc64le-gnu": "4.14.3",
-        "@rollup/rollup-linux-riscv64-gnu": "4.14.3",
-        "@rollup/rollup-linux-s390x-gnu": "4.14.3",
-        "@rollup/rollup-linux-x64-gnu": "4.14.3",
-        "@rollup/rollup-linux-x64-musl": "4.14.3",
-        "@rollup/rollup-win32-arm64-msvc": "4.14.3",
-        "@rollup/rollup-win32-ia32-msvc": "4.14.3",
-        "@rollup/rollup-win32-x64-msvc": "4.14.3",
-        "@types/estree": "1.0.5",
-        "fsevents": "~2.3.2"
-      }
-    },
-    "run-async": {
-      "version": "2.4.1",
-      "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
-      "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ=="
-    },
-    "run-parallel": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
-      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
-      "requires": {
-        "queue-microtask": "^1.2.2"
-      }
-    },
-    "rxjs": {
-      "version": "7.8.1",
-      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
-      "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
-      "requires": {
-        "tslib": "^2.1.0"
-      }
-    },
-    "safe-buffer": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
-      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
-    },
-    "safer-buffer": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
-      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
-    },
-    "sanitize-filename": {
-      "version": "1.6.3",
-      "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
-      "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
-      "requires": {
-        "truncate-utf8-bytes": "^1.0.0"
-      }
-    },
-    "scheduler": {
-      "version": "0.23.2",
-      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
-      "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
-      "peer": true,
-      "requires": {
-        "loose-envify": "^1.1.0"
-      }
-    },
-    "schema-utils": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
-      "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
-      "dev": true,
-      "requires": {
-        "@types/json-schema": "^7.0.8",
-        "ajv": "^6.12.5",
-        "ajv-keywords": "^3.5.2"
-      },
-      "dependencies": {
-        "ajv": {
-          "version": "6.12.6",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-          "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-          "dev": true,
-          "requires": {
-            "fast-deep-equal": "^3.1.1",
-            "fast-json-stable-stringify": "^2.0.0",
-            "json-schema-traverse": "^0.4.1",
-            "uri-js": "^4.2.2"
-          }
-        },
-        "ajv-keywords": {
-          "version": "3.5.2",
-          "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
-          "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
-          "dev": true,
-          "requires": {}
-        },
-        "json-schema-traverse": {
-          "version": "0.4.1",
-          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-          "dev": true
-        }
-      }
-    },
-    "selderee": {
-      "version": "0.11.0",
-      "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
-      "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==",
-      "requires": {
-        "parseley": "^0.12.0"
-      }
-    },
-    "semver": {
-      "version": "7.6.3",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
-      "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="
-    },
-    "send": {
-      "version": "0.18.0",
-      "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
-      "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
-      "requires": {
-        "debug": "2.6.9",
-        "depd": "2.0.0",
-        "destroy": "1.2.0",
-        "encodeurl": "~1.0.2",
-        "escape-html": "~1.0.3",
-        "etag": "~1.8.1",
-        "fresh": "0.5.2",
-        "http-errors": "2.0.0",
-        "mime": "1.6.0",
-        "ms": "2.1.3",
-        "on-finished": "2.4.1",
-        "range-parser": "~1.2.1",
-        "statuses": "2.0.1"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "2.6.9",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
-          "requires": {
-            "ms": "2.0.0"
-          },
-          "dependencies": {
-            "ms": {
-              "version": "2.0.0",
-              "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-              "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
-            }
-          }
-        },
-        "ms": {
-          "version": "2.1.3",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
-          "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
-        }
-      }
-    },
-    "serialize-javascript": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
-      "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
-      "dev": true,
-      "requires": {
-        "randombytes": "^2.1.0"
-      }
-    },
-    "serve-static": {
-      "version": "1.15.0",
-      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
-      "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
-      "requires": {
-        "encodeurl": "~1.0.2",
-        "escape-html": "~1.0.3",
-        "parseurl": "~1.3.3",
-        "send": "0.18.0"
-      }
-    },
-    "set-blocking": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
-      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
-    },
-    "set-function-length": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
-      "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==",
-      "requires": {
-        "define-data-property": "^1.1.2",
-        "es-errors": "^1.3.0",
-        "function-bind": "^1.1.2",
-        "get-intrinsic": "^1.2.3",
-        "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.1"
-      }
-    },
-    "setprototypeof": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
-      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
-    },
-    "sha.js": {
-      "version": "2.4.11",
-      "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
-      "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
-      "requires": {
-        "inherits": "^2.0.1",
-        "safe-buffer": "^5.0.1"
-      }
-    },
-    "shapefile": {
-      "version": "0.6.6",
-      "resolved": "https://registry.npmjs.org/shapefile/-/shapefile-0.6.6.tgz",
-      "integrity": "sha512-rLGSWeK2ufzCVx05wYd+xrWnOOdSV7xNUW5/XFgx3Bc02hBkpMlrd2F1dDII7/jhWzv0MSyBFh5uJIy9hLdfuw==",
-      "requires": {
-        "array-source": "0.0",
-        "commander": "2",
-        "path-source": "0.1",
-        "slice-source": "0.4",
-        "stream-source": "0.3",
-        "text-encoding": "^0.6.4"
-      },
-      "dependencies": {
-        "commander": {
-          "version": "2.20.3",
-          "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
-          "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
-        }
-      }
-    },
-    "sharp": {
-      "version": "0.33.5",
-      "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
-      "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
-      "requires": {
-        "@img/sharp-darwin-arm64": "0.33.5",
-        "@img/sharp-darwin-x64": "0.33.5",
-        "@img/sharp-libvips-darwin-arm64": "1.0.4",
-        "@img/sharp-libvips-darwin-x64": "1.0.4",
-        "@img/sharp-libvips-linux-arm": "1.0.5",
-        "@img/sharp-libvips-linux-arm64": "1.0.4",
-        "@img/sharp-libvips-linux-s390x": "1.0.4",
-        "@img/sharp-libvips-linux-x64": "1.0.4",
-        "@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
-        "@img/sharp-libvips-linuxmusl-x64": "1.0.4",
-        "@img/sharp-linux-arm": "0.33.5",
-        "@img/sharp-linux-arm64": "0.33.5",
-        "@img/sharp-linux-s390x": "0.33.5",
-        "@img/sharp-linux-x64": "0.33.5",
-        "@img/sharp-linuxmusl-arm64": "0.33.5",
-        "@img/sharp-linuxmusl-x64": "0.33.5",
-        "@img/sharp-wasm32": "0.33.5",
-        "@img/sharp-win32-ia32": "0.33.5",
-        "@img/sharp-win32-x64": "0.33.5",
-        "color": "^4.2.3",
-        "detect-libc": "^2.0.3",
-        "semver": "^7.6.3"
-      }
-    },
-    "shebang-command": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
-      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
-      "requires": {
-        "shebang-regex": "^3.0.0"
-      }
-    },
-    "shebang-regex": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
-      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
-    },
-    "shimmer": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
-      "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="
-    },
-    "side-channel": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
-      "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
-      "requires": {
-        "call-bind": "^1.0.0",
-        "get-intrinsic": "^1.0.2",
-        "object-inspect": "^1.9.0"
-      }
-    },
-    "siginfo": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
-      "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
-      "dev": true
-    },
-    "signal-exit": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
-      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="
-    },
-    "simple-swizzle": {
-      "version": "0.2.2",
-      "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
-      "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
-      "requires": {
-        "is-arrayish": "^0.3.1"
-      },
-      "dependencies": {
-        "is-arrayish": {
-          "version": "0.3.2",
-          "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
-          "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
-        }
-      }
-    },
-    "sirv": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
-      "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==",
-      "requires": {
-        "@polka/url": "^1.0.0-next.24",
-        "mrmime": "^2.0.0",
-        "totalist": "^3.0.0"
-      }
-    },
-    "slice-source": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/slice-source/-/slice-source-0.4.1.tgz",
-      "integrity": "sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg=="
-    },
-    "socket.io": {
-      "version": "4.7.5",
-      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz",
-      "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==",
-      "requires": {
-        "accepts": "~1.3.4",
-        "base64id": "~2.0.0",
-        "cors": "~2.8.5",
-        "debug": "~4.3.2",
-        "engine.io": "~6.5.2",
-        "socket.io-adapter": "~2.5.2",
-        "socket.io-parser": "~4.2.4"
-      }
-    },
-    "socket.io-adapter": {
-      "version": "2.5.5",
-      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
-      "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
-      "requires": {
-        "debug": "~4.3.4",
-        "ws": "~8.17.1"
-      },
-      "dependencies": {
-        "ws": {
-          "version": "8.17.1",
-          "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
-          "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
-          "requires": {}
-        }
-      }
-    },
-    "socket.io-parser": {
-      "version": "4.2.4",
-      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
-      "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
-      "requires": {
-        "@socket.io/component-emitter": "~3.1.0",
-        "debug": "~4.3.1"
-      }
-    },
-    "source-map": {
-      "version": "0.7.4",
-      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
-      "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
-      "dev": true
-    },
-    "source-map-js": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
-      "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg=="
-    },
-    "source-map-support": {
-      "version": "0.5.21",
-      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
-      "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
-      "dev": true,
-      "requires": {
-        "buffer-from": "^1.0.0",
-        "source-map": "^0.6.0"
-      },
-      "dependencies": {
-        "source-map": {
-          "version": "0.6.1",
-          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-          "dev": true
-        }
-      }
-    },
-    "spdx-correct": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
-      "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
-      "dev": true,
-      "requires": {
-        "spdx-expression-parse": "^3.0.0",
-        "spdx-license-ids": "^3.0.0"
-      }
-    },
-    "spdx-exceptions": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz",
-      "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==",
-      "dev": true
-    },
-    "spdx-expression-parse": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
-      "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
-      "dev": true,
-      "requires": {
-        "spdx-exceptions": "^2.1.0",
-        "spdx-license-ids": "^3.0.0"
-      }
-    },
-    "spdx-license-ids": {
-      "version": "3.0.16",
-      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz",
-      "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==",
-      "dev": true
-    },
-    "split-ca": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
-      "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==",
-      "dev": true
-    },
-    "split2": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
-      "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="
-    },
-    "sql-formatter": {
-      "version": "15.4.1",
-      "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.4.1.tgz",
-      "integrity": "sha512-lw/G/emIJ+tVspOtOFzfD2YFFMN3MFPxGnbWl1DlJLB+fsX7X7zMqSRM1SLSn2YuaRJ0lTe7AMknHDqmIW1Y8w==",
-      "dev": true,
-      "requires": {
-        "argparse": "^2.0.1",
-        "get-stdin": "=8.0.0",
-        "nearley": "^2.20.1"
-      }
-    },
-    "ssh-remote-port-forward": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz",
-      "integrity": "sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==",
-      "dev": true,
-      "requires": {
-        "@types/ssh2": "^0.5.48",
-        "ssh2": "^1.4.0"
-      }
-    },
-    "ssh2": {
-      "version": "1.14.0",
-      "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.14.0.tgz",
-      "integrity": "sha512-AqzD1UCqit8tbOKoj6ztDDi1ffJZ2rV2SwlgrVVrHPkV5vWqGJOVp5pmtj18PunkPJAuKQsnInyKV+/Nb2bUnA==",
-      "dev": true,
-      "requires": {
-        "asn1": "^0.2.6",
-        "bcrypt-pbkdf": "^1.0.2",
-        "cpu-features": "~0.0.8",
-        "nan": "^2.17.0"
-      }
-    },
-    "stackback": {
-      "version": "0.0.2",
-      "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
-      "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
-      "dev": true
-    },
-    "standard-as-callback": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
-      "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="
-    },
-    "statuses": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
-      "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
-    },
-    "std-env": {
-      "version": "3.7.0",
-      "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
-      "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==",
-      "dev": true
-    },
-    "stream-source": {
-      "version": "0.3.5",
-      "resolved": "https://registry.npmjs.org/stream-source/-/stream-source-0.3.5.tgz",
-      "integrity": "sha512-ZuEDP9sgjiAwUVoDModftG0JtYiLUV8K4ljYD1VyUMRWtbVf92474o4kuuul43iZ8t/hRuiDAx1dIJSvirrK/g=="
-    },
-    "streamsearch": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
-      "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="
-    },
-    "streamx": {
-      "version": "2.18.0",
-      "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz",
-      "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==",
-      "requires": {
-        "bare-events": "^2.2.0",
-        "fast-fifo": "^1.3.2",
-        "queue-tick": "^1.0.1",
-        "text-decoder": "^1.1.0"
-      }
-    },
-    "string_decoder": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
-      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
-      "requires": {
-        "safe-buffer": "~5.2.0"
-      }
-    },
-    "string-width": {
-      "version": "4.2.3",
-      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
-      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-      "requires": {
-        "emoji-regex": "^8.0.0",
-        "is-fullwidth-code-point": "^3.0.0",
-        "strip-ansi": "^6.0.1"
-      }
-    },
-    "string-width-cjs": {
-      "version": "npm:string-width@4.2.3",
-      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
-      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-      "requires": {
-        "emoji-regex": "^8.0.0",
-        "is-fullwidth-code-point": "^3.0.0",
-        "strip-ansi": "^6.0.1"
-      }
-    },
-    "strip-ansi": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
-      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-      "requires": {
-        "ansi-regex": "^5.0.1"
-      }
-    },
-    "strip-ansi-cjs": {
-      "version": "npm:strip-ansi@6.0.1",
-      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
-      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-      "requires": {
-        "ansi-regex": "^5.0.1"
-      }
-    },
-    "strip-final-newline": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
-      "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
-      "dev": true
-    },
-    "strip-indent": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
-      "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
-      "dev": true,
-      "requires": {
-        "min-indent": "^1.0.0"
-      }
-    },
-    "strip-json-comments": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
-      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
-      "dev": true
-    },
-    "styled-jsx": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
-      "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
-      "requires": {
-        "client-only": "0.0.1"
-      }
-    },
-    "sucrase": {
-      "version": "3.35.0",
-      "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
-      "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
-      "peer": true,
-      "requires": {
-        "@jridgewell/gen-mapping": "^0.3.2",
-        "commander": "^4.0.0",
-        "glob": "^10.3.10",
-        "lines-and-columns": "^1.1.6",
-        "mz": "^2.7.0",
-        "pirates": "^4.0.1",
-        "ts-interface-checker": "^0.1.9"
-      }
-    },
-    "supports-color": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-      "requires": {
-        "has-flag": "^4.0.0"
-      }
-    },
-    "supports-preserve-symlinks-flag": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
-      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
-    },
-    "swagger-ui-dist": {
-      "version": "5.17.14",
-      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz",
-      "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw=="
-    },
-    "symbol-observable": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
-      "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==",
-      "dev": true
-    },
-    "synckit": {
-      "version": "0.9.1",
-      "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz",
-      "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==",
-      "dev": true,
-      "requires": {
-        "@pkgr/core": "^0.1.0",
-        "tslib": "^2.6.2"
-      }
-    },
-    "systeminformation": {
-      "version": "5.22.0",
-      "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.22.0.tgz",
-      "integrity": "sha512-oAP80ymt8ssrAzjX8k3frbL7ys6AotqC35oikG6/SG15wBw+tG9nCk4oPaXIhEaAOAZ8XngxUv3ORq2IuR3r4Q=="
-    },
-    "tailwindcss": {
-      "version": "3.4.6",
-      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz",
-      "integrity": "sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==",
-      "peer": true,
-      "requires": {
-        "@alloc/quick-lru": "^5.2.0",
-        "arg": "^5.0.2",
-        "chokidar": "^3.5.3",
-        "didyoumean": "^1.2.2",
-        "dlv": "^1.1.3",
-        "fast-glob": "^3.3.0",
-        "glob-parent": "^6.0.2",
-        "is-glob": "^4.0.3",
-        "jiti": "^1.21.0",
-        "lilconfig": "^2.1.0",
-        "micromatch": "^4.0.5",
-        "normalize-path": "^3.0.0",
-        "object-hash": "^3.0.0",
-        "picocolors": "^1.0.0",
-        "postcss": "^8.4.23",
-        "postcss-import": "^15.1.0",
-        "postcss-js": "^4.0.1",
-        "postcss-load-config": "^4.0.1",
-        "postcss-nested": "^6.0.1",
-        "postcss-selector-parser": "^6.0.11",
-        "resolve": "^1.22.2",
-        "sucrase": "^3.32.0"
-      },
-      "dependencies": {
-        "arg": {
-          "version": "5.0.2",
-          "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
-          "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
-          "peer": true
-        },
-        "glob-parent": {
-          "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
-          "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
-          "peer": true,
-          "requires": {
-            "is-glob": "^4.0.3"
-          }
-        }
-      }
-    },
-    "tailwindcss-email-variants": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/tailwindcss-email-variants/-/tailwindcss-email-variants-3.0.1.tgz",
-      "integrity": "sha512-bRk4R2jnfaW7BBaL2kDgOdBl0SpVP/JPDE/yCkZb1n3YrPK9ZQyQGZoVX3OX06GxjMOrNO3wZACVdHJce7dm8w==",
-      "requires": {}
-    },
-    "tailwindcss-mso": {
-      "version": "1.4.3",
-      "resolved": "https://registry.npmjs.org/tailwindcss-mso/-/tailwindcss-mso-1.4.3.tgz",
-      "integrity": "sha512-8YfZ4xnIComDrhoSr8FUwm7EGz1FkxsZy07Fs4Jm/JxHrFiubdiZjyxLuHMc3S8o02+U4fjRGHPOzoVXRus10A==",
-      "requires": {}
-    },
-    "tailwindcss-preset-email": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/tailwindcss-preset-email/-/tailwindcss-preset-email-1.3.2.tgz",
-      "integrity": "sha512-kSPNZM5+tSi+uhCb4rk1XF9Q6zp8lhoNLCa3GQqe6gKmfI/nTqY8Y+5/DYNpwqhmUPCSHULlyI/LUCaF/q8sLg==",
-      "requires": {
-        "tailwindcss-email-variants": "^3.0.0",
-        "tailwindcss-mso": "^1.4.3"
-      }
-    },
-    "tapable": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
-      "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
-      "dev": true
-    },
-    "tar": {
-      "version": "6.2.0",
-      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
-      "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
-      "requires": {
-        "chownr": "^2.0.0",
-        "fs-minipass": "^2.0.0",
-        "minipass": "^5.0.0",
-        "minizlib": "^2.1.1",
-        "mkdirp": "^1.0.3",
-        "yallist": "^4.0.0"
-      },
-      "dependencies": {
-        "minipass": {
-          "version": "5.0.0",
-          "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
-          "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="
-        },
-        "mkdirp": {
-          "version": "1.0.4",
-          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
-          "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
-        },
-        "yallist": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
-        }
-      }
-    },
-    "tar-fs": {
-      "version": "3.0.6",
-      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz",
-      "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==",
-      "dev": true,
-      "requires": {
-        "bare-fs": "^2.1.1",
-        "bare-path": "^2.1.0",
-        "pump": "^3.0.0",
-        "tar-stream": "^3.1.5"
-      }
-    },
-    "tar-stream": {
-      "version": "3.1.6",
-      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz",
-      "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==",
-      "requires": {
-        "b4a": "^1.6.4",
-        "fast-fifo": "^1.2.0",
-        "streamx": "^2.15.0"
-      }
-    },
-    "terser": {
-      "version": "5.27.0",
-      "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz",
-      "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==",
-      "dev": true,
-      "requires": {
-        "@jridgewell/source-map": "^0.3.3",
-        "acorn": "^8.8.2",
-        "commander": "^2.20.0",
-        "source-map-support": "~0.5.20"
-      },
-      "dependencies": {
-        "commander": {
-          "version": "2.20.3",
-          "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
-          "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
-          "dev": true
-        }
-      }
-    },
-    "terser-webpack-plugin": {
-      "version": "5.3.10",
-      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
-      "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==",
-      "dev": true,
-      "requires": {
-        "@jridgewell/trace-mapping": "^0.3.20",
-        "jest-worker": "^27.4.5",
-        "schema-utils": "^3.1.1",
-        "serialize-javascript": "^6.0.1",
-        "terser": "^5.26.0"
-      },
-      "dependencies": {
-        "jest-worker": {
-          "version": "27.5.1",
-          "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
-          "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
-          "dev": true,
-          "requires": {
-            "@types/node": "*",
-            "merge-stream": "^2.0.0",
-            "supports-color": "^8.0.0"
-          }
-        },
-        "supports-color": {
-          "version": "8.1.1",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
-          "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
-          "dev": true,
-          "requires": {
-            "has-flag": "^4.0.0"
-          }
-        }
-      }
-    },
-    "test-exclude": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz",
-      "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==",
-      "dev": true,
-      "requires": {
-        "@istanbuljs/schema": "^0.1.2",
-        "glob": "^10.4.1",
-        "minimatch": "^9.0.4"
-      },
-      "dependencies": {
-        "brace-expansion": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
-          "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-          "dev": true,
-          "requires": {
-            "balanced-match": "^1.0.0"
-          }
-        },
-        "minimatch": {
-          "version": "9.0.5",
-          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
-          "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
-          "dev": true,
-          "requires": {
-            "brace-expansion": "^2.0.1"
-          }
-        }
-      }
-    },
-    "testcontainers": {
-      "version": "10.13.0",
-      "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.13.0.tgz",
-      "integrity": "sha512-SDblQvirbJw1ZpenxaAairGtAesw5XMOCHLbRhTTUBJtBkZJGce8Vx/I8lXQxWIM8HRXsg3HILTHGQvYo4x7wQ==",
-      "dev": true,
-      "requires": {
-        "@balena/dockerignore": "^1.0.2",
-        "@types/dockerode": "^3.3.29",
-        "archiver": "^7.0.1",
-        "async-lock": "^1.4.1",
-        "byline": "^5.0.0",
-        "debug": "^4.3.5",
-        "docker-compose": "^0.24.8",
-        "dockerode": "^3.3.5",
-        "get-port": "^5.1.1",
-        "proper-lockfile": "^4.1.2",
-        "properties-reader": "^2.3.0",
-        "ssh-remote-port-forward": "^1.0.4",
-        "tar-fs": "^3.0.6",
-        "tmp": "^0.2.3",
-        "undici": "^5.28.4"
-      },
-      "dependencies": {
-        "tmp": {
-          "version": "0.2.3",
-          "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
-          "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
-          "dev": true
-        }
-      }
-    },
-    "text-decoder": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz",
-      "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==",
-      "requires": {
-        "b4a": "^1.6.4"
-      }
-    },
-    "text-encoding": {
-      "version": "0.6.4",
-      "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz",
-      "integrity": "sha512-hJnc6Qg3dWoOMkqP53F0dzRIgtmsAge09kxUIqGrEUS4qr5rWLckGYaQAVr+opBrIMRErGgy6f5aPnyPpyGRfg=="
-    },
-    "text-table": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
-      "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
-      "dev": true
-    },
-    "thenify": {
-      "version": "3.3.1",
-      "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
-      "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
-      "requires": {
-        "any-promise": "^1.0.0"
-      }
-    },
-    "thenify-all": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
-      "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
-      "requires": {
-        "thenify": ">= 3.1.0 < 4"
-      }
-    },
-    "through": {
-      "version": "2.3.8",
-      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
-      "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
-    },
-    "thumbhash": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/thumbhash/-/thumbhash-0.1.1.tgz",
-      "integrity": "sha512-kH5pKeIIBPQXAOni2AiY/Cu/NKdkFREdpH+TLdM0g6WA7RriCv0kPLgP731ady67MhTAqrVG/4mnEeibVuCJcg=="
-    },
-    "tinybench": {
-      "version": "2.8.0",
-      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz",
-      "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==",
-      "dev": true
-    },
-    "tinypool": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz",
-      "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==",
-      "dev": true
-    },
-    "tinyrainbow": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
-      "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==",
-      "dev": true
-    },
-    "tinyspy": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz",
-      "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==",
-      "dev": true
-    },
-    "tmp": {
-      "version": "0.0.33",
-      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
-      "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
-      "requires": {
-        "os-tmpdir": "~1.0.2"
-      }
-    },
-    "to-fast-properties": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
-      "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="
-    },
-    "to-regex-range": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
-      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-      "requires": {
-        "is-number": "^7.0.0"
-      }
-    },
-    "toidentifier": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
-      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
-    },
-    "totalist": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
-      "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="
-    },
-    "tr46": {
-      "version": "0.0.3",
-      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
-      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
-    },
-    "tree-kill": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
-      "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
-      "dev": true
-    },
-    "truncate-utf8-bytes": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
-      "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==",
-      "requires": {
-        "utf8-byte-length": "^1.0.1"
-      }
-    },
-    "ts-api-utils": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
-      "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
-      "dev": true,
-      "requires": {}
-    },
-    "ts-interface-checker": {
-      "version": "0.1.13",
-      "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
-      "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
-      "peer": true
-    },
-    "ts-node": {
-      "version": "10.9.2",
-      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
-      "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
-      "optional": true,
-      "peer": true,
-      "requires": {
-        "@cspotcode/source-map-support": "^0.8.0",
-        "@tsconfig/node10": "^1.0.7",
-        "@tsconfig/node12": "^1.0.7",
-        "@tsconfig/node14": "^1.0.0",
-        "@tsconfig/node16": "^1.0.2",
-        "acorn": "^8.4.1",
-        "acorn-walk": "^8.1.1",
-        "arg": "^4.1.0",
-        "create-require": "^1.1.0",
-        "diff": "^4.0.1",
-        "make-error": "^1.1.1",
-        "v8-compile-cache-lib": "^3.0.1",
-        "yn": "3.1.1"
-      }
-    },
-    "tsconfck": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.1.tgz",
-      "integrity": "sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==",
-      "dev": true,
-      "requires": {}
-    },
-    "tsconfig-paths": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
-      "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
-      "dev": true,
-      "requires": {
-        "json5": "^2.2.2",
-        "minimist": "^1.2.6",
-        "strip-bom": "^3.0.0"
-      },
-      "dependencies": {
-        "strip-bom": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
-          "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
-          "dev": true
-        }
-      }
-    },
-    "tsconfig-paths-webpack-plugin": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz",
-      "integrity": "sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==",
-      "dev": true,
-      "requires": {
-        "chalk": "^4.1.0",
-        "enhanced-resolve": "^5.7.0",
-        "tsconfig-paths": "^4.1.2"
-      }
-    },
-    "tslib": {
-      "version": "2.6.3",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
-      "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
-    },
-    "tweetnacl": {
-      "version": "0.14.5",
-      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
-      "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
-      "dev": true
-    },
-    "type-check": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
-      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
-      "dev": true,
-      "requires": {
-        "prelude-ls": "^1.2.1"
-      }
-    },
-    "type-is": {
-      "version": "1.6.18",
-      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
-      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
-      "requires": {
-        "media-typer": "0.3.0",
-        "mime-types": "~2.1.24"
-      }
-    },
-    "typedarray": {
-      "version": "0.0.6",
-      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
-      "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
-    },
-    "typeorm": {
-      "version": "0.3.20",
-      "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.20.tgz",
-      "integrity": "sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==",
-      "requires": {
-        "@sqltools/formatter": "^1.2.5",
-        "app-root-path": "^3.1.0",
-        "buffer": "^6.0.3",
-        "chalk": "^4.1.2",
-        "cli-highlight": "^2.1.11",
-        "dayjs": "^1.11.9",
-        "debug": "^4.3.4",
-        "dotenv": "^16.0.3",
-        "glob": "^10.3.10",
-        "mkdirp": "^2.1.3",
-        "reflect-metadata": "^0.2.1",
-        "sha.js": "^2.4.11",
-        "tslib": "^2.5.0",
-        "uuid": "^9.0.0",
-        "yargs": "^17.6.2"
-      },
-      "dependencies": {
-        "buffer": {
-          "version": "6.0.3",
-          "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
-          "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
-          "requires": {
-            "base64-js": "^1.3.1",
-            "ieee754": "^1.2.1"
-          }
-        },
-        "cliui": {
-          "version": "8.0.1",
-          "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
-          "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
-          "requires": {
-            "string-width": "^4.2.0",
-            "strip-ansi": "^6.0.1",
-            "wrap-ansi": "^7.0.0"
-          }
-        },
-        "mkdirp": {
-          "version": "2.1.6",
-          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz",
-          "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A=="
-        },
-        "wrap-ansi": {
-          "version": "7.0.0",
-          "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
-          "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-          "requires": {
-            "ansi-styles": "^4.0.0",
-            "string-width": "^4.1.0",
-            "strip-ansi": "^6.0.0"
-          }
-        },
-        "yargs": {
-          "version": "17.7.2",
-          "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
-          "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
-          "requires": {
-            "cliui": "^8.0.1",
-            "escalade": "^3.1.1",
-            "get-caller-file": "^2.0.5",
-            "require-directory": "^2.1.1",
-            "string-width": "^4.2.3",
-            "y18n": "^5.0.5",
-            "yargs-parser": "^21.1.1"
-          }
-        }
-      }
-    },
-    "typescript": {
-      "version": "5.5.4",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
-      "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
-      "devOptional": true
-    },
-    "ua-parser-js": {
-      "version": "1.0.38",
-      "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.38.tgz",
-      "integrity": "sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ=="
-    },
-    "uglify-js": {
-      "version": "3.17.4",
-      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
-      "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==",
-      "optional": true
-    },
-    "uid": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz",
-      "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==",
-      "requires": {
-        "@lukeed/csprng": "^1.0.0"
-      }
-    },
-    "uid2": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/uid2/-/uid2-1.0.0.tgz",
-      "integrity": "sha512-+I6aJUv63YAcY9n4mQreLUt0d4lvwkkopDNmpomkAUz0fAkEMV9pRWxN0EjhW1YfRhcuyHg2v3mwddCDW1+LFQ=="
-    },
-    "undici": {
-      "version": "5.28.4",
-      "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
-      "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
-      "dev": true,
-      "requires": {
-        "@fastify/busboy": "^2.0.0"
-      }
-    },
-    "undici-types": {
-      "version": "6.19.8",
-      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
-      "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
-    },
-    "universalify": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
-      "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
-      "dev": true
-    },
-    "unpipe": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
-      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
-    },
-    "unplugin": {
-      "version": "1.11.0",
-      "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.11.0.tgz",
-      "integrity": "sha512-3r7VWZ/webh0SGgJScpWl2/MRCZK5d3ZYFcNaeci/GQ7Teop7zf0Nl2pUuz7G21BwPd9pcUPOC5KmJ2L3WgC5g==",
-      "dev": true,
-      "requires": {
-        "acorn": "^8.11.3",
-        "chokidar": "^3.6.0",
-        "webpack-sources": "^3.2.3",
-        "webpack-virtual-modules": "^0.6.1"
-      }
-    },
-    "unplugin-swc": {
-      "version": "1.5.1",
-      "resolved": "https://registry.npmjs.org/unplugin-swc/-/unplugin-swc-1.5.1.tgz",
-      "integrity": "sha512-/ZLrPNjChhGx3Z95pxJ4tQgfI6rWqukgYHKflrNB4zAV1izOQuDhkTn55JWeivpBxDCoK7M/TStb2aS/14PS/g==",
-      "dev": true,
-      "requires": {
-        "@rollup/pluginutils": "^5.1.0",
-        "load-tsconfig": "^0.2.5",
-        "unplugin": "^1.11.0"
-      }
-    },
-    "update-browserslist-db": {
-      "version": "1.0.13",
-      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
-      "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
-      "requires": {
-        "escalade": "^3.1.1",
-        "picocolors": "^1.0.0"
-      }
-    },
-    "uri-js": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
-      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
-      "dev": true,
-      "requires": {
-        "punycode": "^2.1.0"
-      }
-    },
-    "utf8-byte-length": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz",
-      "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA=="
-    },
-    "util-deprecate": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
-      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
-    },
-    "utils-merge": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
-      "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="
-    },
-    "utimes": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/utimes/-/utimes-5.2.1.tgz",
-      "integrity": "sha512-6S5mCapmzcxetOD/2UEjL0GF5e4+gB07Dh8qs63xylw5ay4XuyW6iQs70FOJo/puf10LCkvhp4jYMQSDUBYEFg==",
-      "dev": true,
-      "requires": {
-        "@mapbox/node-pre-gyp": "^1.0.11",
-        "node-addon-api": "^4.3.0"
-      },
-      "dependencies": {
-        "node-addon-api": {
-          "version": "4.3.0",
-          "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
-          "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==",
-          "dev": true
-        }
-      }
-    },
-    "uuid": {
-      "version": "9.0.1",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
-      "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="
-    },
-    "v8-compile-cache-lib": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
-      "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
-      "optional": true,
-      "peer": true
-    },
-    "validate-npm-package-license": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
-      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
-      "dev": true,
-      "requires": {
-        "spdx-correct": "^3.0.0",
-        "spdx-expression-parse": "^3.0.0"
-      }
-    },
-    "validator": {
-      "version": "13.11.0",
-      "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz",
-      "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ=="
-    },
-    "vary": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
-      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
-    },
-    "vite": {
-      "version": "5.2.11",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz",
-      "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==",
-      "dev": true,
-      "requires": {
-        "esbuild": "^0.20.1",
-        "fsevents": "~2.3.3",
-        "postcss": "^8.4.38",
-        "rollup": "^4.13.0"
-      }
-    },
-    "vite-node": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz",
-      "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==",
-      "dev": true,
-      "requires": {
-        "cac": "^6.7.14",
-        "debug": "^4.3.5",
-        "pathe": "^1.1.2",
-        "tinyrainbow": "^1.2.0",
-        "vite": "^5.0.0"
-      }
-    },
-    "vite-tsconfig-paths": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.0.1.tgz",
-      "integrity": "sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==",
-      "dev": true,
-      "requires": {
-        "debug": "^4.1.1",
-        "globrex": "^0.1.2",
-        "tsconfck": "^3.0.3"
-      }
-    },
-    "vitest": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz",
-      "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==",
-      "dev": true,
-      "requires": {
-        "@ampproject/remapping": "^2.3.0",
-        "@vitest/expect": "2.0.5",
-        "@vitest/pretty-format": "^2.0.5",
-        "@vitest/runner": "2.0.5",
-        "@vitest/snapshot": "2.0.5",
-        "@vitest/spy": "2.0.5",
-        "@vitest/utils": "2.0.5",
-        "chai": "^5.1.1",
-        "debug": "^4.3.5",
-        "execa": "^8.0.1",
-        "magic-string": "^0.30.10",
-        "pathe": "^1.1.2",
-        "std-env": "^3.7.0",
-        "tinybench": "^2.8.0",
-        "tinypool": "^1.0.0",
-        "tinyrainbow": "^1.2.0",
-        "vite": "^5.0.0",
-        "vite-node": "2.0.5",
-        "why-is-node-running": "^2.3.0"
-      },
-      "dependencies": {
-        "magic-string": {
-          "version": "0.30.11",
-          "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
-          "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
-          "dev": true,
-          "requires": {
-            "@jridgewell/sourcemap-codec": "^1.5.0"
-          }
-        }
-      }
-    },
-    "watchpack": {
-      "version": "2.4.1",
-      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
-      "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==",
-      "dev": true,
-      "requires": {
-        "glob-to-regexp": "^0.4.1",
-        "graceful-fs": "^4.1.2"
-      }
-    },
-    "wcwidth": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
-      "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
-      "requires": {
-        "defaults": "^1.0.3"
-      }
-    },
-    "webidl-conversions": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
-      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
-    },
-    "webpack": {
-      "version": "5.93.0",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz",
-      "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==",
-      "dev": true,
-      "requires": {
-        "@types/eslint-scope": "^3.7.3",
-        "@types/estree": "^1.0.5",
-        "@webassemblyjs/ast": "^1.12.1",
-        "@webassemblyjs/wasm-edit": "^1.12.1",
-        "@webassemblyjs/wasm-parser": "^1.12.1",
-        "acorn": "^8.7.1",
-        "acorn-import-attributes": "^1.9.5",
-        "browserslist": "^4.21.10",
-        "chrome-trace-event": "^1.0.2",
-        "enhanced-resolve": "^5.17.0",
-        "es-module-lexer": "^1.2.1",
-        "eslint-scope": "5.1.1",
-        "events": "^3.2.0",
-        "glob-to-regexp": "^0.4.1",
-        "graceful-fs": "^4.2.11",
-        "json-parse-even-better-errors": "^2.3.1",
-        "loader-runner": "^4.2.0",
-        "mime-types": "^2.1.27",
-        "neo-async": "^2.6.2",
-        "schema-utils": "^3.2.0",
-        "tapable": "^2.1.1",
-        "terser-webpack-plugin": "^5.3.10",
-        "watchpack": "^2.4.1",
-        "webpack-sources": "^3.2.3"
-      },
-      "dependencies": {
-        "eslint-scope": {
-          "version": "5.1.1",
-          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
-          "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
-          "dev": true,
-          "requires": {
-            "esrecurse": "^4.3.0",
-            "estraverse": "^4.1.1"
-          }
-        },
-        "estraverse": {
-          "version": "4.3.0",
-          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
-          "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
-          "dev": true
-        }
-      }
-    },
-    "webpack-node-externals": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz",
-      "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==",
-      "dev": true
-    },
-    "webpack-sources": {
-      "version": "3.2.3",
-      "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
-      "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
-      "dev": true
-    },
-    "webpack-virtual-modules": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.1.tgz",
-      "integrity": "sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg==",
-      "dev": true
-    },
-    "whatwg-url": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
-      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
-      "requires": {
-        "tr46": "~0.0.3",
-        "webidl-conversions": "^3.0.0"
-      }
-    },
-    "which": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
-      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
-      "requires": {
-        "isexe": "^2.0.0"
-      }
-    },
-    "why-is-node-running": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
-      "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
-      "dev": true,
-      "requires": {
-        "siginfo": "^2.0.0",
-        "stackback": "0.0.2"
-      }
-    },
-    "wide-align": {
-      "version": "1.1.5",
-      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
-      "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
-      "requires": {
-        "string-width": "^1.0.2 || 2 || 3 || 4"
-      }
-    },
-    "wordwrap": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
-      "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="
-    },
-    "wrap-ansi": {
-      "version": "6.2.0",
-      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
-      "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
-      "requires": {
-        "ansi-styles": "^4.0.0",
-        "string-width": "^4.1.0",
-        "strip-ansi": "^6.0.0"
-      }
-    },
-    "wrap-ansi-cjs": {
-      "version": "npm:wrap-ansi@7.0.0",
-      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
-      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-      "requires": {
-        "ansi-styles": "^4.0.0",
-        "string-width": "^4.1.0",
-        "strip-ansi": "^6.0.0"
-      }
-    },
-    "wrappy": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
-    },
-    "ws": {
-      "version": "8.11.0",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
-      "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
-      "requires": {}
-    },
-    "xtend": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
-      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
-    },
-    "y18n": {
-      "version": "5.0.8",
-      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
-      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="
-    },
-    "yallist": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
-      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
-    },
-    "yaml": {
-      "version": "2.3.4",
-      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz",
-      "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA=="
-    },
-    "yargs": {
-      "version": "16.2.0",
-      "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
-      "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
-      "requires": {
-        "cliui": "^7.0.2",
-        "escalade": "^3.1.1",
-        "get-caller-file": "^2.0.5",
-        "require-directory": "^2.1.1",
-        "string-width": "^4.2.0",
-        "y18n": "^5.0.5",
-        "yargs-parser": "^20.2.2"
-      },
-      "dependencies": {
-        "yargs-parser": {
-          "version": "20.2.9",
-          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
-          "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="
-        }
-      }
-    },
-    "yargs-parser": {
-      "version": "21.1.1",
-      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
-      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="
-    },
-    "yn": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
-      "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
-      "optional": true,
-      "peer": true
-    },
-    "yocto-queue": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
-      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
-      "dev": true
-    },
-    "zip-stream": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
-      "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==",
-      "requires": {
-        "archiver-utils": "^5.0.0",
-        "compress-commons": "^6.0.2",
-        "readable-stream": "^4.0.0"
-      },
-      "dependencies": {
-        "buffer": {
-          "version": "6.0.3",
-          "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
-          "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
-          "requires": {
-            "base64-js": "^1.3.1",
-            "ieee754": "^1.2.1"
-          }
-        },
-        "readable-stream": {
-          "version": "4.5.2",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
-          "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
-          "requires": {
-            "abort-controller": "^3.0.0",
-            "buffer": "^6.0.3",
-            "events": "^3.3.0",
-            "process": "^0.11.10",
-            "string_decoder": "^1.3.0"
-          }
-        }
-      }
     }
   }
 }
diff --git a/server/package.json b/server/package.json
index a1b5a6b269..074dafa5d3 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,6 +1,6 @@
 {
   "name": "immich",
-  "version": "1.115.0",
+  "version": "1.123.0",
   "description": "",
   "author": "",
   "private": true,
@@ -18,17 +18,16 @@
     "check": "tsc --noEmit",
     "check:code": "npm run format && npm run lint && npm run check",
     "check:all": "npm run check:code && npm run test:cov",
-    "healthcheck": "node ./dist/utils/healthcheck.js",
     "test": "vitest",
-    "test:watch": "vitest --watch",
     "test:cov": "vitest --coverage",
+    "test:medium": "vitest --config vitest.config.medium.mjs",
     "typeorm": "typeorm",
     "lifecycle": "node ./dist/utils/lifecycle.js",
     "typeorm:migrations:create": "typeorm migration:create",
-    "typeorm:migrations:generate": "typeorm migration:generate -d ./dist/database.config.js",
-    "typeorm:migrations:run": "typeorm migration:run -d ./dist/database.config.js",
-    "typeorm:migrations:revert": "typeorm migration:revert -d ./dist/database.config.js",
-    "typeorm:schema:drop": "typeorm query -d ./dist/database.config.js 'DROP schema public cascade; CREATE schema public;'",
+    "typeorm:migrations:generate": "typeorm migration:generate -d ./dist/bin/database.js",
+    "typeorm:migrations:run": "typeorm migration:run -d ./dist/bin/database.js",
+    "typeorm:migrations:revert": "typeorm migration:revert -d ./dist/bin/database.js",
+    "typeorm:schema:drop": "typeorm query -d ./dist/bin/database.js 'DROP schema public cascade; CREATE schema public;'",
     "typeorm:schema:reset": "npm run typeorm:schema:drop && npm run typeorm:migrations:run",
     "sync:open-api": "node ./dist/bin/sync-open-api.js",
     "sync:sql": "node ./dist/bin/sync-sql.js",
@@ -37,20 +36,19 @@
   "dependencies": {
     "@nestjs/bullmq": "^10.0.1",
     "@nestjs/common": "^10.2.2",
-    "@nestjs/config": "^3.0.0",
     "@nestjs/core": "^10.2.2",
     "@nestjs/event-emitter": "^2.0.4",
     "@nestjs/platform-express": "^10.2.2",
     "@nestjs/platform-socket.io": "^10.2.2",
     "@nestjs/schedule": "^4.0.0",
-    "@nestjs/swagger": "^7.1.8",
+    "@nestjs/swagger": "^8.0.0",
     "@nestjs/typeorm": "^10.0.0",
     "@nestjs/websockets": "^10.2.2",
-    "@opentelemetry/auto-instrumentations-node": "^0.49.0",
+    "@opentelemetry/auto-instrumentations-node": "^0.54.0",
     "@opentelemetry/context-async-hooks": "^1.24.0",
-    "@opentelemetry/exporter-prometheus": "^0.53.0",
-    "@opentelemetry/sdk-node": "^0.53.0",
-    "@react-email/components": "^0.0.24",
+    "@opentelemetry/exporter-prometheus": "^0.56.0",
+    "@opentelemetry/sdk-node": "^0.56.0",
+    "@react-email/components": "^0.0.31",
     "@socket.io/redis-adapter": "^8.3.0",
     "archiver": "^7.0.0",
     "async-lock": "^1.4.0",
@@ -60,7 +58,7 @@
     "class-transformer": "^0.5.1",
     "class-validator": "^0.14.0",
     "cookie-parser": "^1.4.6",
-    "exiftool-vendored": "^28.1.0",
+    "exiftool-vendored": "^28.3.1",
     "fast-glob": "^3.3.2",
     "fluent-ffmpeg": "^2.1.2",
     "geo-tz": "^8.0.0",
@@ -78,18 +76,19 @@
     "openid-client": "^5.4.3",
     "pg": "^8.11.3",
     "picomatch": "^4.0.2",
-    "react": "^18.3.1",
-    "react-email": "^3.0.0",
+    "react": "^19.0.0",
+    "react-email": "^3.0.4",
     "reflect-metadata": "^0.2.0",
     "rxjs": "^7.8.1",
     "sanitize-filename": "^1.6.3",
     "semver": "^7.6.2",
     "sharp": "^0.33.0",
-    "sirv": "^2.0.4",
+    "sirv": "^3.0.0",
     "tailwindcss-preset-email": "^1.3.2",
     "thumbhash": "^0.1.1",
     "typeorm": "^0.3.17",
-    "ua-parser-js": "^1.0.35"
+    "ua-parser-js": "^1.0.35",
+    "validator": "^13.12.0"
   },
   "devDependencies": {
     "@eslint/eslintrc": "^3.1.0",
@@ -109,22 +108,24 @@
     "@types/lodash": "^4.14.197",
     "@types/mock-fs": "^4.13.1",
     "@types/multer": "^1.4.7",
-    "@types/node": "^20.16.5",
+    "@types/node": "^22.10.2",
     "@types/nodemailer": "^6.4.14",
     "@types/picomatch": "^3.0.0",
-    "@types/react": "^18.3.4",
+    "@types/pngjs": "^6.0.5",
+    "@types/react": "^19.0.0",
     "@types/semver": "^7.5.8",
     "@types/supertest": "^6.0.0",
     "@types/ua-parser-js": "^0.7.36",
-    "@typescript-eslint/eslint-plugin": "^8.0.0",
-    "@typescript-eslint/parser": "^8.0.0",
+    "@typescript-eslint/eslint-plugin": "^8.15.0",
+    "@typescript-eslint/parser": "^8.15.0",
     "@vitest/coverage-v8": "^2.0.5",
-    "eslint": "^9.0.0",
+    "eslint": "^9.14.0",
     "eslint-config-prettier": "^9.1.0",
     "eslint-plugin-prettier": "^5.1.3",
-    "eslint-plugin-unicorn": "^55.0.0",
+    "eslint-plugin-unicorn": "^56.0.1",
     "globals": "^15.9.0",
     "mock-fs": "^5.2.0",
+    "pngjs": "^7.0.0",
     "prettier": "^3.0.2",
     "prettier-plugin-organize-imports": "^4.0.0",
     "rimraf": "^6.0.0",
@@ -138,6 +139,6 @@
     "vitest": "^2.0.5"
   },
   "volta": {
-    "node": "20.17.0"
+    "node": "22.12.0"
   }
 }
diff --git a/server/src/app.module.ts b/server/src/app.module.ts
index 9446010127..da8fa55606 100644
--- a/server/src/app.module.ts
+++ b/server/src/app.module.ts
@@ -1,30 +1,29 @@
 import { BullModule } from '@nestjs/bullmq';
 import { Inject, Module, OnModuleDestroy, OnModuleInit, ValidationPipe } from '@nestjs/common';
-import { ConfigModule } from '@nestjs/config';
 import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE, ModuleRef } from '@nestjs/core';
-import { EventEmitterModule } from '@nestjs/event-emitter';
 import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule';
 import { TypeOrmModule } from '@nestjs/typeorm';
-import _ from 'lodash';
 import { ClsModule } from 'nestjs-cls';
 import { OpenTelemetryModule } from 'nestjs-otel';
 import { commands } from 'src/commands';
-import { bullConfig, bullQueues, clsConfig, immichAppConfig } from 'src/config';
+import { IWorker } from 'src/constants';
 import { controllers } from 'src/controllers';
-import { databaseConfig } from 'src/database.config';
 import { entities } from 'src/entities';
+import { ImmichWorker } from 'src/enum';
 import { IEventRepository } from 'src/interfaces/event.interface';
+import { IJobRepository } from 'src/interfaces/job.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
+import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
 import { AuthGuard } from 'src/middleware/auth.guard';
 import { ErrorInterceptor } from 'src/middleware/error.interceptor';
 import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
 import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter';
 import { LoggingInterceptor } from 'src/middleware/logging.interceptor';
 import { repositories } from 'src/repositories';
+import { ConfigRepository } from 'src/repositories/config.repository';
+import { teardownTelemetry } from 'src/repositories/telemetry.repository';
 import { services } from 'src/services';
 import { DatabaseService } from 'src/services/database.service';
-import { setupEventHandlers } from 'src/utils/events';
-import { otelConfig } from 'src/utils/instrumentation';
 
 const common = [...services, ...repositories];
 
@@ -37,18 +36,19 @@ const middleware = [
   { provide: APP_GUARD, useClass: AuthGuard },
 ];
 
+const configRepository = new ConfigRepository();
+const { bull, cls, database, otel } = configRepository.getEnv();
+
 const imports = [
-  BullModule.forRoot(bullConfig),
-  BullModule.registerQueue(...bullQueues),
-  ClsModule.forRoot(clsConfig),
-  ConfigModule.forRoot(immichAppConfig),
-  EventEmitterModule.forRoot(),
-  OpenTelemetryModule.forRoot(otelConfig),
+  BullModule.forRoot(bull.config),
+  BullModule.registerQueue(...bull.queues),
+  ClsModule.forRoot(cls.config),
+  OpenTelemetryModule.forRoot(otel),
   TypeOrmModule.forRootAsync({
     inject: [ModuleRef],
     useFactory: (moduleRef: ModuleRef) => {
       return {
-        ...databaseConfig,
+        ...database.config,
         poolErrorHandler: (error) => {
           moduleRef.get(DatabaseService, { strict: false }).handleConnectionError(error);
         },
@@ -58,72 +58,50 @@ const imports = [
   TypeOrmModule.forFeature(entities),
 ];
 
+class BaseModule implements OnModuleInit, OnModuleDestroy {
+  constructor(
+    @Inject(IWorker) private worker: ImmichWorker,
+    @Inject(ILoggerRepository) logger: ILoggerRepository,
+    @Inject(IEventRepository) private eventRepository: IEventRepository,
+    @Inject(IJobRepository) private jobRepository: IJobRepository,
+    @Inject(ITelemetryRepository) private telemetryRepository: ITelemetryRepository,
+  ) {
+    logger.setAppName(this.worker);
+  }
+
+  async onModuleInit() {
+    this.telemetryRepository.setup({ repositories: repositories.map(({ useClass }) => useClass) });
+
+    this.jobRepository.setup({ services });
+    if (this.worker === ImmichWorker.MICROSERVICES) {
+      this.jobRepository.startWorkers();
+    }
+
+    this.eventRepository.setup({ services });
+    await this.eventRepository.emit('app.bootstrap');
+  }
+
+  async onModuleDestroy() {
+    await this.eventRepository.emit('app.shutdown');
+    await teardownTelemetry();
+  }
+}
+
 @Module({
   imports: [...imports, ScheduleModule.forRoot()],
   controllers: [...controllers],
-  providers: [...common, ...middleware],
+  providers: [...common, ...middleware, { provide: IWorker, useValue: ImmichWorker.API }],
 })
-export class ApiModule implements OnModuleInit, OnModuleDestroy {
-  constructor(
-    private moduleRef: ModuleRef,
-    @Inject(IEventRepository) private eventRepository: IEventRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-  ) {}
-
-  async onModuleInit() {
-    const items = setupEventHandlers(this.moduleRef);
-
-    await this.eventRepository.emit('app.bootstrap', 'api');
-
-    this.logger.setContext('EventLoader');
-    const eventMap = _.groupBy(items, 'event');
-    for (const [event, handlers] of Object.entries(eventMap)) {
-      for (const { priority, label } of handlers) {
-        this.logger.verbose(`Added ${event} {${label}${priority ? '' : ', ' + priority}} event`);
-      }
-    }
-  }
-
-  async onModuleDestroy() {
-    await this.eventRepository.emit('app.shutdown');
-  }
-}
+export class ApiModule extends BaseModule {}
 
 @Module({
   imports: [...imports],
-  providers: [...common, SchedulerRegistry],
+  providers: [...common, { provide: IWorker, useValue: ImmichWorker.MICROSERVICES }, SchedulerRegistry],
 })
-export class MicroservicesModule implements OnModuleInit, OnModuleDestroy {
-  constructor(
-    private moduleRef: ModuleRef,
-    @Inject(IEventRepository) private eventRepository: IEventRepository,
-  ) {}
-
-  async onModuleInit() {
-    setupEventHandlers(this.moduleRef);
-    await this.eventRepository.emit('app.bootstrap', 'microservices');
-  }
-
-  async onModuleDestroy() {
-    await this.eventRepository.emit('app.shutdown');
-  }
-}
+export class MicroservicesModule extends BaseModule {}
 
 @Module({
   imports: [...imports],
   providers: [...common, ...commands, SchedulerRegistry],
 })
 export class ImmichAdminModule {}
-
-@Module({
-  imports: [
-    ConfigModule.forRoot(immichAppConfig),
-    EventEmitterModule.forRoot(),
-    TypeOrmModule.forRoot(databaseConfig),
-    TypeOrmModule.forFeature(entities),
-    OpenTelemetryModule.forRoot(otelConfig),
-  ],
-  controllers: [...controllers],
-  providers: [...common, ...middleware, SchedulerRegistry],
-})
-export class AppTestModule {}
diff --git a/server/src/bin/database.ts b/server/src/bin/database.ts
new file mode 100644
index 0000000000..c861902b4e
--- /dev/null
+++ b/server/src/bin/database.ts
@@ -0,0 +1,11 @@
+import { ConfigRepository } from 'src/repositories/config.repository';
+import { DataSource } from 'typeorm';
+
+const { database } = new ConfigRepository().getEnv();
+
+/**
+ * @deprecated - DO NOT USE THIS
+ *
+ * this export is ONLY to be used for TypeORM commands in package.json#scripts
+ */
+export const dataSource = new DataSource({ ...database.config, host: 'localhost' });
diff --git a/server/src/bin/sync-open-api.ts b/server/src/bin/sync-open-api.ts
index 70e2bb8c35..d5316b34cf 100644
--- a/server/src/bin/sync-open-api.ts
+++ b/server/src/bin/sync-open-api.ts
@@ -7,7 +7,7 @@ import { useSwagger } from 'src/utils/misc';
 
 const sync = async () => {
   const app = await NestFactory.create<NestExpressApplication>(ApiModule, { preview: true });
-  useSwagger(app, true);
+  useSwagger(app, { write: true });
   await app.close();
 };
 
diff --git a/server/src/bin/sync-sql.ts b/server/src/bin/sync-sql.ts
index 6bf85d1553..98f26d879a 100644
--- a/server/src/bin/sync-sql.ts
+++ b/server/src/bin/sync-sql.ts
@@ -1,7 +1,6 @@
 #!/usr/bin/env node
 import { INestApplication } from '@nestjs/common';
 import { Reflector } from '@nestjs/core';
-import { EventEmitterModule } from '@nestjs/event-emitter';
 import { SchedulerRegistry } from '@nestjs/schedule';
 import { Test } from '@nestjs/testing';
 import { TypeOrmModule } from '@nestjs/typeorm';
@@ -9,14 +8,13 @@ import { OpenTelemetryModule } from 'nestjs-otel';
 import { mkdir, rm, writeFile } from 'node:fs/promises';
 import { join } from 'node:path';
 import { format } from 'sql-formatter';
-import { databaseConfig } from 'src/database.config';
 import { GENERATE_SQL_KEY, GenerateSqlQueries } from 'src/decorators';
 import { entities } from 'src/entities';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { repositories } from 'src/repositories';
 import { AccessRepository } from 'src/repositories/access.repository';
+import { ConfigRepository } from 'src/repositories/config.repository';
 import { AuthService } from 'src/services/auth.service';
-import { otelConfig } from 'src/utils/instrumentation';
 import { Logger } from 'typeorm';
 
 export class SqlLogger implements Logger {
@@ -75,18 +73,19 @@ class SqlGenerator {
     await rm(this.options.targetDir, { force: true, recursive: true });
     await mkdir(this.options.targetDir);
 
+    const { database, otel } = new ConfigRepository().getEnv();
+
     const moduleFixture = await Test.createTestingModule({
       imports: [
         TypeOrmModule.forRoot({
-          ...databaseConfig,
+          ...database.config,
           host: 'localhost',
           entities,
           logging: ['query'],
           logger: this.sqlLogger,
         }),
         TypeOrmModule.forFeature(entities),
-        EventEmitterModule.forRoot(),
-        OpenTelemetryModule.forRoot(otelConfig),
+        OpenTelemetryModule.forRoot(otel),
       ],
       providers: [...repositories, AuthService, SchedulerRegistry],
     }).compile();
diff --git a/server/src/config.ts b/server/src/config.ts
index 057c9a69e2..2658974200 100644
--- a/server/src/config.ts
+++ b/server/src/config.ts
@@ -1,90 +1,27 @@
-import { RegisterQueueOptions } from '@nestjs/bullmq';
-import { ConfigModuleOptions } from '@nestjs/config';
 import { CronExpression } from '@nestjs/schedule';
-import { QueueOptions } from 'bullmq';
-import { Request, Response } from 'express';
-import { RedisOptions } from 'ioredis';
-import Joi, { Root } from 'joi';
-import { CLS_ID, ClsModuleOptions } from 'nestjs-cls';
-import { ImmichHeader } from 'src/dtos/auth.dto';
+import {
+  AudioCodec,
+  Colorspace,
+  CQMode,
+  ImageFormat,
+  LogLevel,
+  ToneMapping,
+  TranscodeHWAccel,
+  TranscodePolicy,
+  VideoCodec,
+  VideoContainer,
+} from 'src/enum';
 import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
-
-export enum TranscodePolicy {
-  ALL = 'all',
-  OPTIMAL = 'optimal',
-  BITRATE = 'bitrate',
-  REQUIRED = 'required',
-  DISABLED = 'disabled',
-}
-
-export enum TranscodeTarget {
-  NONE,
-  AUDIO,
-  VIDEO,
-  ALL,
-}
-
-export enum VideoCodec {
-  H264 = 'h264',
-  HEVC = 'hevc',
-  VP9 = 'vp9',
-  AV1 = 'av1',
-}
-
-export enum AudioCodec {
-  MP3 = 'mp3',
-  AAC = 'aac',
-  LIBOPUS = 'libopus',
-}
-
-export enum VideoContainer {
-  MOV = 'mov',
-  MP4 = 'mp4',
-  OGG = 'ogg',
-  WEBM = 'webm',
-}
-
-export enum TranscodeHWAccel {
-  NVENC = 'nvenc',
-  QSV = 'qsv',
-  VAAPI = 'vaapi',
-  RKMPP = 'rkmpp',
-  DISABLED = 'disabled',
-}
-
-export enum ToneMapping {
-  HABLE = 'hable',
-  MOBIUS = 'mobius',
-  REINHARD = 'reinhard',
-  DISABLED = 'disabled',
-}
-
-export enum CQMode {
-  AUTO = 'auto',
-  CQP = 'cqp',
-  ICQ = 'icq',
-}
-
-export enum Colorspace {
-  SRGB = 'srgb',
-  P3 = 'p3',
-}
-
-export enum ImageFormat {
-  JPEG = 'jpeg',
-  WEBP = 'webp',
-}
-
-export enum LogLevel {
-  VERBOSE = 'verbose',
-  DEBUG = 'debug',
-  LOG = 'log',
-  WARN = 'warn',
-  ERROR = 'error',
-  FATAL = 'fatal',
-}
+import { ImageOptions } from 'src/interfaces/media.interface';
 
 export interface SystemConfig {
+  backup: {
+    database: {
+      enabled: boolean;
+      cronExpression: string;
+      keepLastAmount: number;
+    };
+  };
   ffmpeg: {
     crf: number;
     threads: number;
@@ -99,7 +36,6 @@ export interface SystemConfig {
     bframes: number;
     refs: number;
     gopSize: number;
-    npl: number;
     temporalAQ: boolean;
     cqMode: CQMode;
     twoPass: boolean;
@@ -116,7 +52,7 @@ export interface SystemConfig {
   };
   machineLearning: {
     enabled: boolean;
-    url: string;
+    urls: string[];
     clip: {
       enabled: boolean;
       modelName: string;
@@ -172,11 +108,8 @@ export interface SystemConfig {
     template: string;
   };
   image: {
-    thumbnailFormat: ImageFormat;
-    thumbnailSize: number;
-    previewFormat: ImageFormat;
-    previewSize: number;
-    quality: number;
+    thumbnail: ImageOptions;
+    preview: ImageOptions;
     colorspace: Colorspace;
     extractEmbedded: boolean;
   };
@@ -213,9 +146,17 @@ export interface SystemConfig {
       };
     };
   };
+  templates: {
+    email: {
+      welcomeTemplate: string;
+      albumInviteTemplate: string;
+      albumUpdateTemplate: string;
+    };
+  };
   server: {
     externalDomain: string;
     loginPageMessage: string;
+    publicUsers: boolean;
   };
   user: {
     deleteDelay: number;
@@ -223,6 +164,13 @@ export interface SystemConfig {
 }
 
 export const defaults = Object.freeze<SystemConfig>({
+  backup: {
+    database: {
+      enabled: true,
+      cronExpression: CronExpression.EVERY_DAY_AT_2AM,
+      keepLastAmount: 14,
+    },
+  },
   ffmpeg: {
     crf: 23,
     threads: 0,
@@ -230,14 +178,13 @@ export const defaults = Object.freeze<SystemConfig>({
     targetVideoCodec: VideoCodec.H264,
     acceptedVideoCodecs: [VideoCodec.H264],
     targetAudioCodec: AudioCodec.AAC,
-    acceptedAudioCodecs: [AudioCodec.AAC, AudioCodec.MP3, AudioCodec.LIBOPUS],
+    acceptedAudioCodecs: [AudioCodec.AAC, AudioCodec.MP3, AudioCodec.LIBOPUS, AudioCodec.PCMS16LE],
     acceptedContainers: [VideoContainer.MOV, VideoContainer.OGG, VideoContainer.WEBM],
     targetResolution: '720',
     maxBitrate: '0',
     bframes: -1,
     refs: 0,
     gopSize: 0,
-    npl: 0,
     temporalAQ: false,
     cqMode: CQMode.AUTO,
     twoPass: false,
@@ -266,7 +213,7 @@ export const defaults = Object.freeze<SystemConfig>({
   },
   machineLearning: {
     enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false',
-    url: process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003',
+    urls: [process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003'],
     clip: {
       enabled: true,
       modelName: 'ViT-B-32__openai',
@@ -285,8 +232,8 @@ export const defaults = Object.freeze<SystemConfig>({
   },
   map: {
     enabled: true,
-    lightStyle: '',
-    darkStyle: '',
+    lightStyle: 'https://tiles.immich.cloud/v1/style/light.json',
+    darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json',
   },
   reverseGeocoding: {
     enabled: true,
@@ -322,11 +269,16 @@ export const defaults = Object.freeze<SystemConfig>({
     template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
   },
   image: {
-    thumbnailFormat: ImageFormat.WEBP,
-    thumbnailSize: 250,
-    previewFormat: ImageFormat.JPEG,
-    previewSize: 1440,
-    quality: 80,
+    thumbnail: {
+      format: ImageFormat.WEBP,
+      size: 250,
+      quality: 80,
+    },
+    preview: {
+      format: ImageFormat.JPEG,
+      size: 1440,
+      quality: 80,
+    },
     colorspace: Colorspace.P3,
     extractEmbedded: false,
   },
@@ -352,6 +304,7 @@ export const defaults = Object.freeze<SystemConfig>({
   server: {
     externalDomain: '',
     loginPageMessage: '',
+    publicUsers: true,
   },
   notifications: {
     smtp: {
@@ -367,138 +320,14 @@ export const defaults = Object.freeze<SystemConfig>({
       },
     },
   },
+  templates: {
+    email: {
+      welcomeTemplate: '',
+      albumInviteTemplate: '',
+      albumUpdateTemplate: '',
+    },
+  },
   user: {
     deleteDelay: 7,
   },
 });
-
-const WHEN_DB_URL_SET = Joi.when('DB_URL', {
-  is: Joi.exist(),
-  then: Joi.string().optional(),
-  otherwise: Joi.string().required(),
-});
-
-export const immichAppConfig: ConfigModuleOptions = {
-  envFilePath: '.env',
-  isGlobal: true,
-  validationSchema: Joi.object({
-    IMMICH_ENV: Joi.string().optional().valid('development', 'testing', 'production').default('production'),
-    IMMICH_LOG_LEVEL: Joi.string()
-      .optional()
-      .valid(...Object.values(LogLevel)),
-
-    DB_USERNAME: WHEN_DB_URL_SET,
-    DB_PASSWORD: WHEN_DB_URL_SET,
-    DB_DATABASE_NAME: WHEN_DB_URL_SET,
-    DB_URL: Joi.string().optional(),
-    DB_VECTOR_EXTENSION: Joi.string().optional().valid('pgvector', 'pgvecto.rs').default('pgvecto.rs'),
-    DB_SKIP_MIGRATIONS: Joi.boolean().optional().default(false),
-
-    IMMICH_PORT: Joi.number().optional(),
-    IMMICH_API_METRICS_PORT: Joi.number().optional(),
-    IMMICH_MICROSERVICES_METRICS_PORT: Joi.number().optional(),
-
-    IMMICH_TRUSTED_PROXIES: Joi.extend((joi: Root) => ({
-      type: 'stringArray',
-      base: joi.array(),
-      coerce: (value) => (value.split ? value.split(',') : value),
-    }))
-      .stringArray()
-      .single()
-      .items(
-        Joi.string().ip({
-          version: ['ipv4', 'ipv6'],
-          cidr: 'optional',
-        }),
-      ),
-
-    IMMICH_METRICS: Joi.boolean().optional().default(false),
-    IMMICH_HOST_METRICS: Joi.boolean().optional().default(false),
-    IMMICH_API_METRICS: Joi.boolean().optional().default(false),
-    IMMICH_IO_METRICS: Joi.boolean().optional().default(false),
-  }),
-};
-
-export function parseRedisConfig(): RedisOptions {
-  const redisUrl = process.env.REDIS_URL;
-  if (redisUrl && redisUrl.startsWith('ioredis://')) {
-    try {
-      const decodedString = Buffer.from(redisUrl.slice(10), 'base64').toString();
-      return JSON.parse(decodedString);
-    } catch (error) {
-      throw new Error(`Failed to decode redis options: ${error}`);
-    }
-  }
-  return {
-    host: process.env.REDIS_HOSTNAME || 'redis',
-    port: Number.parseInt(process.env.REDIS_PORT || '6379'),
-    db: Number.parseInt(process.env.REDIS_DBINDEX || '0'),
-    username: process.env.REDIS_USERNAME || undefined,
-    password: process.env.REDIS_PASSWORD || undefined,
-    path: process.env.REDIS_SOCKET || undefined,
-  };
-}
-
-export const bullConfig: QueueOptions = {
-  prefix: 'immich_bull',
-  connection: parseRedisConfig(),
-  defaultJobOptions: {
-    attempts: 3,
-    removeOnComplete: true,
-    removeOnFail: false,
-  },
-};
-
-export const bullQueues: RegisterQueueOptions[] = Object.values(QueueName).map((name) => ({ name }));
-
-export const clsConfig: ClsModuleOptions = {
-  middleware: {
-    mount: true,
-    generateId: true,
-    setup: (cls, req: Request, res: Response) => {
-      const headerValues = req.headers[ImmichHeader.CID];
-      const headerValue = Array.isArray(headerValues) ? headerValues[0] : headerValues;
-      const cid = headerValue || cls.get(CLS_ID);
-      cls.set(CLS_ID, cid);
-      res.header(ImmichHeader.CID, cid);
-    },
-  },
-};
-
-export const getBuildMetadata = () => ({
-  build: process.env.IMMICH_BUILD,
-  buildUrl: process.env.IMMICH_BUILD_URL,
-  buildImage: process.env.IMMICH_BUILD_IMAGE,
-  buildImageUrl: process.env.IMMICH_BUILD_IMAGE_URL,
-  repository: process.env.IMMICH_REPOSITORY,
-  repositoryUrl: process.env.IMMICH_REPOSITORY_URL,
-  sourceRef: process.env.IMMICH_SOURCE_REF,
-  sourceCommit: process.env.IMMICH_SOURCE_COMMIT,
-  sourceUrl: process.env.IMMICH_SOURCE_URL,
-});
-
-const clientLicensePublicKeyProd =
-  'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF2LzdTMzJjUkE1KysxTm5WRHNDTQpzcFAvakpISU1xT0pYRm5oNE53QTJPcHorUk1mZGNvOTJQc09naCt3d1FlRXYxVTJjMnBqelRpUS8ybHJLcS9rCnpKUmxYd2M0Y1Vlc1FETUpPRitQMnFPTlBiQUprWHZDWFlCVUxpdENJa29Md2ZoU0dOanlJS2FSRGhkL3ROeU4KOCtoTlJabllUMWhTSWo5U0NrS3hVQ096YXRQVjRtQ0RlclMrYkUrZ0VVZVdwOTlWOWF6dkYwRkltblRXcFFTdwpjOHdFWmdPTWg0c3ZoNmFpY3dkemtQQ3dFTGFrMFZhQkgzMUJFVUNRTGI5K0FJdEhBVXRKQ0t4aGI1V2pzMXM5CmJyWGZpMHZycGdjWi82RGFuWTJxZlNQem5PbXZEMkZycmxTMXE0SkpOM1ZvN1d3LzBZeS95TWNtelRXWmhHdWgKVVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo=';
-
-const clientLicensePublicKeyStaging =
-  'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFuSUNyTm5jbGpPSC9JdTNtWVVaRQp0dGJLV1c3OGRuajl5M0U2ekk3dU1NUndEckdYWFhkTGhkUDFxSWtlZHh0clVVeUpCMWR4R04yQW91S082MlNGCldrbU9PTmNGQlRBWFZTdjhUNVY0S0VwWnFQYWEwaXpNaGxMaE5sRXEvY1ZKdllrWlh1Z2x6b1o3cG1nbzFSdHgKam1iRm5NNzhrYTFRUUJqOVdLaEw2eWpWRUl2MDdVS0lKWHBNTnNuS2g1V083MjZhYmMzSE9udTlETjY5VnFFRQo3dGZrUnRWNmx2U1NzMkFVMngzT255cHA4ek53b0lPTWRibGsyb09aWWROZzY0Y3l2SzJoU0FlU3NVMFRyOVc5Ckgra0Y5QlNCNlk0QXl0QlVkSmkrK2pMSW5HM2Q5cU9ieFVzTlYrN05mRkF5NjJkL0xNR0xSOC9OUFc0U0s3c0MKRlFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo=';
-
-export const getClientLicensePublicKey = (): string => {
-  if (process.env.IMMICH_ENV === 'production') {
-    return clientLicensePublicKeyProd;
-  }
-  return clientLicensePublicKeyStaging;
-};
-
-const serverLicensePublicKeyProd =
-  'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFvcG5ZRGEwYS9kVTVJZUc3NGlFRQpNd2RBS2pzTmN6TGRDcVJkMVo5eTVUMndqTzdlWUlPZUpUc2wzNTBzUjBwNEtmU1VEU1h2QzlOcERwYzF0T0tsCjVzaEMvQXhwdlFBTENva0Y0anQ4dnJyZDlmQ2FYYzFUcVJiT21uaGl1Z0Q2dmtyME8vRmIzVURpM1UwVHZoUFAKbFBkdlNhd3pMcldaUExmbUhWVnJiclNLbW45SWVTZ3kwN3VrV1RJeUxzY2lOcnZuQnl3c0phUmVEdW9OV1BCSApVL21vMm1YYThtNHdNV2hpWGVoaUlPUXFNdVNVZ1BlQ3NXajhVVngxQ0dsUnpQREEwYlZOUXZlS1hXVnhjRUk2ClVMRWdKeTJGNDlsSDArYVlDbUJmN05FcjZWUTJXQjk1ZXZUS1hLdm4wcUlNN25nRmxjVUF3NmZ1VjFjTkNUSlMKNndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo=';
-
-const serverLicensePublicKeyStaging =
-  'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUE3Sy8yd3ZLUS9NdU8ydi9MUm5saAoyUy9zTHhDOGJiTEw1UUlKOGowQ3BVZW40YURlY2dYMUpKUmtGNlpUVUtpNTdTbEhtS3RSM2JOTzJmdTBUUVg5Ck5WMEJzVzllZVB0MmlTMWl4VVFmTzRObjdvTjZzbEtac01qd29RNGtGRGFmM3VHTlZJc0dMb3UxVWRLUVhpeDEKUlRHcXVTb3NZVjNWRlk3Q1hGYTVWaENBL3poVXNsNGFuVXp3eEF6M01jUFVlTXBaenYvbVZiQlRKVzBPSytWZgpWQUJvMXdYMkVBanpBekVHVzQ3Vko4czhnMnQrNHNPaHFBNStMQjBKVzlORUg5QUpweGZzWE4zSzVtM00yNUJVClZXcTlRYStIdHRENnJ0bnAvcUFweXVkWUdwZk9HYTRCUlZTR1MxMURZM0xrb2FlRzYwUEU5NHpoYjduOHpMWkgKelFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo=';
-
-export const getServerLicensePublicKey = (): string => {
-  if (process.env.IMMICH_ENV === 'production') {
-    return serverLicensePublicKeyProd;
-  }
-  return serverLicensePublicKeyStaging;
-};
diff --git a/server/src/constants.ts b/server/src/constants.ts
index 6cfcc41d89..fc2442130e 100644
--- a/server/src/constants.ts
+++ b/server/src/constants.ts
@@ -1,7 +1,7 @@
 import { Duration } from 'luxon';
 import { readFileSync } from 'node:fs';
-import { join } from 'node:path';
 import { SemVer } from 'semver';
+import { ExifOrientation } from 'src/enum';
 
 export const POSTGRES_VERSION_RANGE = '>=14.0.0';
 export const VECTORS_VERSION_RANGE = '>=0.2 <0.4';
@@ -14,84 +14,25 @@ export const ADDED_IN_PREFIX = 'This property was added in ';
 
 export const SALT_ROUNDS = 10;
 
+export const IWorker = 'IWorker';
+
 const { version } = JSON.parse(readFileSync('./package.json', 'utf8'));
 export const serverVersion = new SemVer(version);
 
 export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 });
 export const ONE_HOUR = Duration.fromObject({ hours: 1 });
 
-export const envName = (process.env.IMMICH_ENV || 'production').toUpperCase();
-export const isDev = () => process.env.IMMICH_ENV === 'development';
 export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload';
-export const WEB_ROOT = process.env.IMMICH_WEB_ROOT || '/usr/src/app/www';
-const HOST_SERVER_PORT = process.env.IMMICH_PORT || '2283';
-export const DEFAULT_EXTERNAL_DOMAIN = 'http://localhost:' + HOST_SERVER_PORT;
 
 export const citiesFile = 'cities500.txt';
 
-const buildFolder = process.env.IMMICH_BUILD_DATA || '/build';
-
-const folders = {
-  geodata: join(buildFolder, 'geodata'),
-  web: join(buildFolder, 'www'),
-};
-
-export const resourcePaths = {
-  lockFile: join(buildFolder, 'build-lock.json'),
-  geodata: {
-    dateFile: join(folders.geodata, 'geodata-date.txt'),
-    admin1: join(folders.geodata, 'admin1CodesASCII.txt'),
-    admin2: join(folders.geodata, 'admin2Codes.txt'),
-    cities500: join(folders.geodata, citiesFile),
-    naturalEarthCountriesPath: join(folders.geodata, 'ne_10m_admin_0_countries.geojson'),
-  },
-  web: {
-    root: folders.web,
-    indexHtml: join(folders.web, 'index.html'),
-  },
-};
-
 export const MOBILE_REDIRECT = 'app.immich:///oauth-callback';
 export const LOGIN_URL = '/auth/login?autoLaunch=0';
 
-export enum AuthType {
-  PASSWORD = 'password',
-  OAUTH = 'oauth',
-}
-
 export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico'];
 
 export const FACE_THUMBNAIL_SIZE = 250;
 
-export const supportedYearTokens = ['y', 'yy'];
-export const supportedMonthTokens = ['M', 'MM', 'MMM', 'MMMM'];
-export const supportedWeekTokens = ['W', 'WW'];
-export const supportedDayTokens = ['d', 'dd'];
-export const supportedHourTokens = ['h', 'hh', 'H', 'HH'];
-export const supportedMinuteTokens = ['m', 'mm'];
-export const supportedSecondTokens = ['s', 'ss', 'SSS'];
-export const supportedPresetTokens = [
-  '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
-  '{{y}}/{{MM}}-{{dd}}/{{filename}}',
-  '{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
-  '{{y}}/{{MM}}/{{filename}}',
-  '{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}',
-  '{{y}}/{{MMM}}/{{filename}}',
-  '{{y}}/{{MMMM}}/{{filename}}',
-  '{{y}}/{{MM}}/{{dd}}/{{filename}}',
-  '{{y}}/{{MMMM}}/{{dd}}/{{filename}}',
-  '{{y}}/{{y}}-{{MM}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
-  '{{y}}-{{MM}}-{{dd}}/{{filename}}',
-  '{{y}}-{{MMM}}-{{dd}}/{{filename}}',
-  '{{y}}-{{MMMM}}-{{dd}}/{{filename}}',
-  '{{y}}/{{y}}-{{MM}}/{{filename}}',
-  '{{y}}/{{y}}-{{WW}}/{{filename}}',
-  '{{y}}/{{y}}-{{MM}}-{{dd}}/{{assetId}}',
-  '{{y}}/{{y}}-{{MM}}/{{assetId}}',
-  '{{y}}/{{y}}-{{WW}}/{{assetId}}',
-  '{{album}}/{{filename}}',
-];
-
 type ModelInfo = { dimSize: number };
 export const CLIP_MODEL_INFO: Record<string, ModelInfo> = {
   RN101__openai: { dimSize: 512 },
@@ -141,3 +82,19 @@ export const CLIP_MODEL_INFO: Record<string, ModelInfo> = {
   'nllb-clip-large-siglip__mrl': { dimSize: 1152 },
   'nllb-clip-large-siglip__v1': { dimSize: 1152 },
 };
+
+type SharpRotationData = {
+  angle?: number;
+  flip?: boolean;
+  flop?: boolean;
+};
+export const ORIENTATION_TO_SHARP_ROTATION: Record<ExifOrientation, SharpRotationData> = {
+  [ExifOrientation.Horizontal]: { angle: 0 },
+  [ExifOrientation.MirrorHorizontal]: { angle: 0, flop: true },
+  [ExifOrientation.Rotate180]: { angle: 180 },
+  [ExifOrientation.MirrorVertical]: { angle: 180, flop: true },
+  [ExifOrientation.MirrorHorizontalRotate270CW]: { angle: 270, flip: true },
+  [ExifOrientation.Rotate90CW]: { angle: 90 },
+  [ExifOrientation.MirrorHorizontalRotate90CW]: { angle: 90, flip: true },
+  [ExifOrientation.Rotate270CW]: { angle: 270 },
+} as const;
diff --git a/server/src/controllers/asset-media.controller.ts b/server/src/controllers/asset-media.controller.ts
index fb5ec58f25..56e793975a 100644
--- a/server/src/controllers/asset-media.controller.ts
+++ b/server/src/controllers/asset-media.controller.ts
@@ -32,17 +32,18 @@ import {
   CheckExistingAssetsDto,
   UploadFieldName,
 } from 'src/dtos/asset-media.dto';
-import { AuthDto, ImmichHeader } from 'src/dtos/auth.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { ImmichHeader, RouteKey } from 'src/enum';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor';
 import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
-import { FileUploadInterceptor, Route, UploadFiles, getFiles } from 'src/middleware/file-upload.interceptor';
+import { FileUploadInterceptor, UploadFiles, getFiles } from 'src/middleware/file-upload.interceptor';
 import { AssetMediaService } from 'src/services/asset-media.service';
 import { sendFile } from 'src/utils/file';
 import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation';
 
 @ApiTags('Assets')
-@Controller(Route.ASSET)
+@Controller(RouteKey.ASSET)
 export class AssetMediaController {
   constructor(
     @Inject(ILoggerRepository) private logger: ILoggerRepository,
diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts
index c6fdac1710..8a5b5fb0b6 100644
--- a/server/src/controllers/asset.controller.ts
+++ b/server/src/controllers/asset.controller.ts
@@ -1,5 +1,6 @@
 import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
+import { EndpointLifecycle } from 'src/decorators';
 import { AssetResponseDto, MemoryLaneResponseDto } from 'src/dtos/asset-response.dto';
 import {
   AssetBulkDeleteDto,
@@ -13,13 +14,13 @@ import {
 } from 'src/dtos/asset.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
 import { MemoryLaneDto } from 'src/dtos/search.dto';
+import { RouteKey } from 'src/enum';
 import { Auth, Authenticated } from 'src/middleware/auth.guard';
-import { Route } from 'src/middleware/file-upload.interceptor';
 import { AssetService } from 'src/services/asset.service';
 import { UUIDParamDto } from 'src/validation';
 
 @ApiTags('Assets')
-@Controller(Route.ASSET)
+@Controller(RouteKey.ASSET)
 export class AssetController {
   constructor(private service: AssetService) {}
 
@@ -31,6 +32,7 @@ export class AssetController {
 
   @Get('random')
   @Authenticated()
+  @EndpointLifecycle({ deprecatedAt: 'v1.116.0' })
   getRandom(@Auth() auth: AuthDto, @Query() dto: RandomAssetsDto): Promise<AssetResponseDto[]> {
     return this.service.getRandom(auth, dto.count ?? 1);
   }
diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts
index 7dcef9df5f..92fa59f6bf 100644
--- a/server/src/controllers/auth.controller.ts
+++ b/server/src/controllers/auth.controller.ts
@@ -1,11 +1,9 @@
 import { Body, Controller, HttpCode, HttpStatus, Post, Req, Res } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
 import { Request, Response } from 'express';
-import { AuthType } from 'src/constants';
 import {
   AuthDto,
   ChangePasswordDto,
-  ImmichCookie,
   LoginCredentialDto,
   LoginResponseDto,
   LogoutResponseDto,
@@ -13,6 +11,7 @@ import {
   ValidateAccessTokenResponseDto,
 } from 'src/dtos/auth.dto';
 import { UserAdminResponseDto } from 'src/dtos/user.dto';
+import { AuthType, ImmichCookie } from 'src/enum';
 import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
 import { AuthService, LoginDetails } from 'src/services/auth.service';
 import { respondWithCookie, respondWithoutCookie } from 'src/utils/response';
diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts
index ab569d7434..f10bf601b4 100644
--- a/server/src/controllers/index.ts
+++ b/server/src/controllers/index.ts
@@ -19,7 +19,6 @@ import { OAuthController } from 'src/controllers/oauth.controller';
 import { PartnerController } from 'src/controllers/partner.controller';
 import { PersonController } from 'src/controllers/person.controller';
 import { SearchController } from 'src/controllers/search.controller';
-import { ServerInfoController } from 'src/controllers/server-info.controller';
 import { ServerController } from 'src/controllers/server.controller';
 import { SessionController } from 'src/controllers/session.controller';
 import { SharedLinkController } from 'src/controllers/shared-link.controller';
@@ -57,7 +56,6 @@ export const controllers = [
   ReportController,
   SearchController,
   ServerController,
-  ServerInfoController,
   SessionController,
   SharedLinkController,
   StackController,
diff --git a/server/src/controllers/job.controller.ts b/server/src/controllers/job.controller.ts
index 2aa5920fab..7da19e207f 100644
--- a/server/src/controllers/job.controller.ts
+++ b/server/src/controllers/job.controller.ts
@@ -1,6 +1,6 @@
-import { Body, Controller, Get, Param, Put } from '@nestjs/common';
+import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
-import { AllJobStatusResponseDto, JobCommandDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto';
+import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto';
 import { Authenticated } from 'src/middleware/auth.guard';
 import { JobService } from 'src/services/job.service';
 
@@ -15,6 +15,12 @@ export class JobController {
     return this.service.getAllJobsStatus();
   }
 
+  @Post()
+  @Authenticated({ admin: true })
+  createJob(@Body() dto: JobCreateDto): Promise<void> {
+    return this.service.create(dto);
+  }
+
   @Put(':id')
   @Authenticated({ admin: true })
   sendJobCommand(@Param() { id }: JobIdParamDto, @Body() dto: JobCommandDto): Promise<JobStatusDto> {
diff --git a/server/src/controllers/library.controller.ts b/server/src/controllers/library.controller.ts
index a45617fc2a..b8959ca288 100644
--- a/server/src/controllers/library.controller.ts
+++ b/server/src/controllers/library.controller.ts
@@ -4,7 +4,6 @@ import {
   CreateLibraryDto,
   LibraryResponseDto,
   LibraryStatsResponseDto,
-  ScanLibraryDto,
   UpdateLibraryDto,
   ValidateLibraryDto,
   ValidateLibraryResponseDto,
@@ -43,6 +42,13 @@ export class LibraryController {
     return this.service.update(id, dto);
   }
 
+  @Delete(':id')
+  @HttpCode(HttpStatus.NO_CONTENT)
+  @Authenticated({ permission: Permission.LIBRARY_DELETE, admin: true })
+  deleteLibrary(@Param() { id }: UUIDParamDto): Promise<void> {
+    return this.service.delete(id);
+  }
+
   @Post(':id/validate')
   @HttpCode(200)
   @Authenticated({ admin: true })
@@ -51,13 +57,6 @@ export class LibraryController {
     return this.service.validate(id, dto);
   }
 
-  @Delete(':id')
-  @HttpCode(HttpStatus.NO_CONTENT)
-  @Authenticated({ permission: Permission.LIBRARY_DELETE, admin: true })
-  deleteLibrary(@Param() { id }: UUIDParamDto): Promise<void> {
-    return this.service.delete(id);
-  }
-
   @Get(':id/statistics')
   @Authenticated({ permission: Permission.LIBRARY_STATISTICS, admin: true })
   getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise<LibraryStatsResponseDto> {
@@ -66,15 +65,8 @@ export class LibraryController {
 
   @Post(':id/scan')
   @HttpCode(HttpStatus.NO_CONTENT)
-  @Authenticated({ admin: true })
-  scanLibrary(@Param() { id }: UUIDParamDto, @Body() dto: ScanLibraryDto) {
-    return this.service.queueScan(id, dto);
-  }
-
-  @Post(':id/removeOffline')
-  @HttpCode(HttpStatus.NO_CONTENT)
-  @Authenticated({ admin: true })
-  removeOfflineFiles(@Param() { id }: UUIDParamDto) {
-    return this.service.queueRemoveOffline(id);
+  @Authenticated({ permission: Permission.LIBRARY_UPDATE, admin: true })
+  scanLibrary(@Param() { id }: UUIDParamDto) {
+    return this.service.queueScan(id);
   }
 }
diff --git a/server/src/controllers/map.controller.ts b/server/src/controllers/map.controller.ts
index d6c26c58a0..88104e6b58 100644
--- a/server/src/controllers/map.controller.ts
+++ b/server/src/controllers/map.controller.ts
@@ -7,7 +7,6 @@ import {
   MapReverseGeocodeDto,
   MapReverseGeocodeResponseDto,
 } from 'src/dtos/map.dto';
-import { MapThemeDto } from 'src/dtos/system-config.dto';
 import { Auth, Authenticated } from 'src/middleware/auth.guard';
 import { MapService } from 'src/services/map.service';
 
@@ -22,12 +21,6 @@ export class MapController {
     return this.service.getMapMarkers(auth, options);
   }
 
-  @Authenticated({ sharedLink: true })
-  @Get('style.json')
-  getMapStyle(@Query() dto: MapThemeDto) {
-    return this.service.getMapStyle(dto.theme);
-  }
-
   @Authenticated()
   @Get('reverse-geocode')
   @HttpCode(HttpStatus.OK)
diff --git a/server/src/controllers/notification.controller.ts b/server/src/controllers/notification.controller.ts
index 2772e93b5d..27034fd63a 100644
--- a/server/src/controllers/notification.controller.ts
+++ b/server/src/controllers/notification.controller.ts
@@ -1,7 +1,9 @@
-import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
+import { Body, Controller, HttpCode, HttpStatus, Param, Post } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
 import { AuthDto } from 'src/dtos/auth.dto';
+import { TemplateDto, TemplateResponseDto, TestEmailResponseDto } from 'src/dtos/notification.dto';
 import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
+import { EmailTemplate } from 'src/interfaces/notification.interface';
 import { Auth, Authenticated } from 'src/middleware/auth.guard';
 import { NotificationService } from 'src/services/notification.service';
 
@@ -13,7 +15,18 @@ export class NotificationController {
   @Post('test-email')
   @HttpCode(HttpStatus.OK)
   @Authenticated({ admin: true })
-  sendTestEmail(@Auth() auth: AuthDto, @Body() dto: SystemConfigSmtpDto) {
+  sendTestEmail(@Auth() auth: AuthDto, @Body() dto: SystemConfigSmtpDto): Promise<TestEmailResponseDto> {
     return this.service.sendTestEmail(auth.user.id, dto);
   }
+
+  @Post('templates/:name')
+  @HttpCode(HttpStatus.OK)
+  @Authenticated({ admin: true })
+  getNotificationTemplate(
+    @Auth() auth: AuthDto,
+    @Param('name') name: EmailTemplate,
+    @Body() dto: TemplateDto,
+  ): Promise<TemplateResponseDto> {
+    return this.service.getTemplate(name, dto.template);
+  }
 }
diff --git a/server/src/controllers/oauth.controller.ts b/server/src/controllers/oauth.controller.ts
index b733dc612b..b5b94030f2 100644
--- a/server/src/controllers/oauth.controller.ts
+++ b/server/src/controllers/oauth.controller.ts
@@ -1,16 +1,15 @@
 import { Body, Controller, Get, HttpCode, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
 import { Request, Response } from 'express';
-import { AuthType } from 'src/constants';
 import {
   AuthDto,
-  ImmichCookie,
   LoginResponseDto,
   OAuthAuthorizeResponseDto,
   OAuthCallbackDto,
   OAuthConfigDto,
 } from 'src/dtos/auth.dto';
 import { UserAdminResponseDto } from 'src/dtos/user.dto';
+import { AuthType, ImmichCookie } from 'src/enum';
 import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
 import { AuthService, LoginDetails } from 'src/services/auth.service';
 import { respondWithCookie } from 'src/utils/response';
diff --git a/server/src/controllers/person.controller.ts b/server/src/controllers/person.controller.ts
index 5462305d9f..c8faf87e62 100644
--- a/server/src/controllers/person.controller.ts
+++ b/server/src/controllers/person.controller.ts
@@ -1,9 +1,7 @@
 import { Body, Controller, Get, Inject, Next, Param, Post, Put, Query, Res } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
 import { NextFunction, Response } from 'express';
-import { EndpointLifecycle } from 'src/decorators';
 import { BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
-import { AssetResponseDto } from 'src/dtos/asset-response.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
 import {
   AssetFaceUpdateDto,
@@ -33,8 +31,8 @@ export class PersonController {
 
   @Get()
   @Authenticated({ permission: Permission.PERSON_READ })
-  getAllPeople(@Auth() auth: AuthDto, @Query() withHidden: PersonSearchDto): Promise<PeopleResponseDto> {
-    return this.service.getAll(auth, withHidden);
+  getAllPeople(@Auth() auth: AuthDto, @Query() options: PersonSearchDto): Promise<PeopleResponseDto> {
+    return this.service.getAll(auth, options);
   }
 
   @Post()
@@ -83,13 +81,6 @@ export class PersonController {
     await sendFile(res, next, () => this.service.getThumbnail(auth, id), this.logger);
   }
 
-  @EndpointLifecycle({ deprecatedAt: 'v1.113.0' })
-  @Get(':id/assets')
-  @Authenticated()
-  getPersonAssets(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto[]> {
-    return this.service.getAssets(auth, id);
-  }
-
   @Put(':id/reassign')
   @Authenticated({ permission: Permission.PERSON_REASSIGN })
   reassignFaces(
diff --git a/server/src/controllers/search.controller.ts b/server/src/controllers/search.controller.ts
index 5b8c1eeece..367c39dae9 100644
--- a/server/src/controllers/search.controller.ts
+++ b/server/src/controllers/search.controller.ts
@@ -6,6 +6,7 @@ import { PersonResponseDto } from 'src/dtos/person.dto';
 import {
   MetadataSearchDto,
   PlacesResponseDto,
+  RandomSearchDto,
   SearchExploreResponseDto,
   SearchPeopleDto,
   SearchPlacesDto,
@@ -24,10 +25,17 @@ export class SearchController {
   @Post('metadata')
   @HttpCode(HttpStatus.OK)
   @Authenticated()
-  searchMetadata(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise<SearchResponseDto> {
+  searchAssets(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise<SearchResponseDto> {
     return this.service.searchMetadata(auth, dto);
   }
 
+  @Post('random')
+  @HttpCode(HttpStatus.OK)
+  @Authenticated()
+  searchRandom(@Auth() auth: AuthDto, @Body() dto: RandomSearchDto): Promise<AssetResponseDto[]> {
+    return this.service.searchRandom(auth, dto);
+  }
+
   @Post('smart')
   @HttpCode(HttpStatus.OK)
   @Authenticated()
diff --git a/server/src/controllers/server-info.controller.ts b/server/src/controllers/server-info.controller.ts
deleted file mode 100644
index 245bbbd347..0000000000
--- a/server/src/controllers/server-info.controller.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import { Controller, Get } from '@nestjs/common';
-import { ApiExcludeController, ApiTags } from '@nestjs/swagger';
-import { EndpointLifecycle } from 'src/decorators';
-import {
-  ServerAboutResponseDto,
-  ServerConfigDto,
-  ServerFeaturesDto,
-  ServerMediaTypesResponseDto,
-  ServerPingResponse,
-  ServerStatsResponseDto,
-  ServerStorageResponseDto,
-  ServerThemeDto,
-  ServerVersionResponseDto,
-} from 'src/dtos/server.dto';
-import { Authenticated } from 'src/middleware/auth.guard';
-import { ServerService } from 'src/services/server.service';
-import { VersionService } from 'src/services/version.service';
-
-@ApiExcludeController()
-@ApiTags('Server Info')
-@Controller('server-info')
-export class ServerInfoController {
-  constructor(
-    private service: ServerService,
-    private versionService: VersionService,
-  ) {}
-
-  @Get('about')
-  @EndpointLifecycle({ deprecatedAt: 'v1.107.0' })
-  @Authenticated()
-  getAboutInfo(): Promise<ServerAboutResponseDto> {
-    return this.service.getAboutInfo();
-  }
-
-  @Get('storage')
-  @EndpointLifecycle({ deprecatedAt: 'v1.107.0' })
-  @Authenticated()
-  getStorage(): Promise<ServerStorageResponseDto> {
-    return this.service.getStorage();
-  }
-
-  @Get('ping')
-  @EndpointLifecycle({ deprecatedAt: 'v1.107.0' })
-  pingServer(): ServerPingResponse {
-    return this.service.ping();
-  }
-
-  @Get('version')
-  @EndpointLifecycle({ deprecatedAt: 'v1.107.0' })
-  getServerVersion(): ServerVersionResponseDto {
-    return this.versionService.getVersion();
-  }
-
-  @Get('features')
-  @EndpointLifecycle({ deprecatedAt: 'v1.107.0' })
-  getServerFeatures(): Promise<ServerFeaturesDto> {
-    return this.service.getFeatures();
-  }
-
-  @Get('theme')
-  @EndpointLifecycle({ deprecatedAt: 'v1.107.0' })
-  getTheme(): Promise<ServerThemeDto> {
-    return this.service.getTheme();
-  }
-
-  @Get('config')
-  @EndpointLifecycle({ deprecatedAt: 'v1.107.0' })
-  getServerConfig(): Promise<ServerConfigDto> {
-    return this.service.getConfig();
-  }
-
-  @Get('statistics')
-  @EndpointLifecycle({ deprecatedAt: 'v1.107.0' })
-  @Authenticated({ admin: true })
-  getServerStatistics(): Promise<ServerStatsResponseDto> {
-    return this.service.getStatistics();
-  }
-
-  @Get('media-types')
-  @EndpointLifecycle({ deprecatedAt: 'v1.107.0' })
-  getSupportedMediaTypes(): ServerMediaTypesResponseDto {
-    return this.service.getSupportedMediaTypes();
-  }
-}
diff --git a/server/src/controllers/server.controller.ts b/server/src/controllers/server.controller.ts
index 75becfe341..8327ff6d1d 100644
--- a/server/src/controllers/server.controller.ts
+++ b/server/src/controllers/server.controller.ts
@@ -10,6 +10,7 @@ import {
   ServerStatsResponseDto,
   ServerStorageResponseDto,
   ServerThemeDto,
+  ServerVersionHistoryResponseDto,
   ServerVersionResponseDto,
 } from 'src/dtos/server.dto';
 import { Authenticated } from 'src/middleware/auth.guard';
@@ -46,6 +47,11 @@ export class ServerController {
     return this.versionService.getVersion();
   }
 
+  @Get('version-history')
+  getVersionHistory(): Promise<ServerVersionHistoryResponseDto[]> {
+    return this.versionService.getVersionHistory();
+  }
+
   @Get('features')
   getServerFeatures(): Promise<ServerFeaturesDto> {
     return this.service.getFeatures();
@@ -58,7 +64,7 @@ export class ServerController {
 
   @Get('config')
   getServerConfig(): Promise<ServerConfigDto> {
-    return this.service.getConfig();
+    return this.service.getSystemConfig();
   }
 
   @Get('statistics')
diff --git a/server/src/controllers/shared-link.controller.ts b/server/src/controllers/shared-link.controller.ts
index 065e578ec5..59f81068d8 100644
--- a/server/src/controllers/shared-link.controller.ts
+++ b/server/src/controllers/shared-link.controller.ts
@@ -3,14 +3,14 @@ import { ApiTags } from '@nestjs/swagger';
 import { Request, Response } from 'express';
 import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
 import { AssetIdsDto } from 'src/dtos/asset.dto';
-import { AuthDto, ImmichCookie } from 'src/dtos/auth.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
 import {
   SharedLinkCreateDto,
   SharedLinkEditDto,
   SharedLinkPasswordDto,
   SharedLinkResponseDto,
 } from 'src/dtos/shared-link.dto';
-import { Permission } from 'src/enum';
+import { ImmichCookie, Permission } from 'src/enum';
 import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
 import { LoginDetails } from 'src/services/auth.service';
 import { SharedLinkService } from 'src/services/shared-link.service';
diff --git a/server/src/controllers/system-config.controller.ts b/server/src/controllers/system-config.controller.ts
index 804c19500f..58e8bde87b 100644
--- a/server/src/controllers/system-config.controller.ts
+++ b/server/src/controllers/system-config.controller.ts
@@ -3,17 +3,21 @@ import { ApiTags } from '@nestjs/swagger';
 import { SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto';
 import { Permission } from 'src/enum';
 import { Authenticated } from 'src/middleware/auth.guard';
+import { StorageTemplateService } from 'src/services/storage-template.service';
 import { SystemConfigService } from 'src/services/system-config.service';
 
 @ApiTags('System Config')
 @Controller('system-config')
 export class SystemConfigController {
-  constructor(private service: SystemConfigService) {}
+  constructor(
+    private service: SystemConfigService,
+    private storageTemplateService: StorageTemplateService,
+  ) {}
 
   @Get()
   @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true })
   getConfig(): Promise<SystemConfigDto> {
-    return this.service.getConfig();
+    return this.service.getSystemConfig();
   }
 
   @Get('defaults')
@@ -25,12 +29,12 @@ export class SystemConfigController {
   @Put()
   @Authenticated({ permission: Permission.SYSTEM_CONFIG_UPDATE, admin: true })
   updateConfig(@Body() dto: SystemConfigDto): Promise<SystemConfigDto> {
-    return this.service.updateConfig(dto);
+    return this.service.updateSystemConfig(dto);
   }
 
   @Get('storage-template-options')
   @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true })
   getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
-    return this.service.getStorageTemplateOptions();
+    return this.storageTemplateService.getStorageTemplateOptions();
   }
 }
diff --git a/server/src/controllers/trash.controller.ts b/server/src/controllers/trash.controller.ts
index 20adbb11bb..dfcdfa6ba2 100644
--- a/server/src/controllers/trash.controller.ts
+++ b/server/src/controllers/trash.controller.ts
@@ -2,6 +2,7 @@ import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
 import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
+import { TrashResponseDto } from 'src/dtos/trash.dto';
 import { Permission } from 'src/enum';
 import { Auth, Authenticated } from 'src/middleware/auth.guard';
 import { TrashService } from 'src/services/trash.service';
@@ -12,23 +13,23 @@ export class TrashController {
   constructor(private service: TrashService) {}
 
   @Post('empty')
-  @HttpCode(HttpStatus.NO_CONTENT)
+  @HttpCode(HttpStatus.OK)
   @Authenticated({ permission: Permission.ASSET_DELETE })
-  emptyTrash(@Auth() auth: AuthDto): Promise<void> {
+  emptyTrash(@Auth() auth: AuthDto): Promise<TrashResponseDto> {
     return this.service.empty(auth);
   }
 
   @Post('restore')
-  @HttpCode(HttpStatus.NO_CONTENT)
+  @HttpCode(HttpStatus.OK)
   @Authenticated({ permission: Permission.ASSET_DELETE })
-  restoreTrash(@Auth() auth: AuthDto): Promise<void> {
+  restoreTrash(@Auth() auth: AuthDto): Promise<TrashResponseDto> {
     return this.service.restore(auth);
   }
 
   @Post('restore/assets')
-  @HttpCode(HttpStatus.NO_CONTENT)
+  @HttpCode(HttpStatus.OK)
   @Authenticated({ permission: Permission.ASSET_DELETE })
-  restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
+  restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<TrashResponseDto> {
     return this.service.restoreAssets(auth, dto);
   }
 }
diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts
index 01b2258390..15bb1913db 100644
--- a/server/src/controllers/user.controller.ts
+++ b/server/src/controllers/user.controller.ts
@@ -21,15 +21,16 @@ import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
 import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
 import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
 import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto';
+import { RouteKey } from 'src/enum';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
-import { FileUploadInterceptor, Route } from 'src/middleware/file-upload.interceptor';
+import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
 import { UserService } from 'src/services/user.service';
 import { sendFile } from 'src/utils/file';
 import { UUIDParamDto } from 'src/validation';
 
 @ApiTags('Users')
-@Controller(Route.USER)
+@Controller(RouteKey.USER)
 export class UserController {
   constructor(
     private service: UserService,
@@ -38,8 +39,8 @@ export class UserController {
 
   @Get()
   @Authenticated()
-  searchUsers(): Promise<UserResponseDto[]> {
-    return this.service.search();
+  searchUsers(@Auth() auth: AuthDto): Promise<UserResponseDto[]> {
+    return this.service.search(auth);
   }
 
   @Get('me')
diff --git a/server/src/cores/storage.core.spec.ts b/server/src/cores/storage.core.spec.ts
index 6ff6ca61bf..a663673306 100644
--- a/server/src/cores/storage.core.spec.ts
+++ b/server/src/cores/storage.core.spec.ts
@@ -3,6 +3,8 @@ import { vitest } from 'vitest';
 
 vitest.mock('src/constants', () => ({
   APP_MEDIA_LOCATION: '/photos',
+  ADDED_IN_PREFIX: 'This property was added in ',
+  DEPRECATED_IN_PREFIX: 'This property was deprecated in ',
 }));
 
 describe('StorageCore', () => {
diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts
index e20a0c658d..c49175172d 100644
--- a/server/src/cores/storage.core.ts
+++ b/server/src/cores/storage.core.ts
@@ -1,13 +1,11 @@
 import { randomUUID } from 'node:crypto';
 import { dirname, join, resolve } from 'node:path';
-import { ImageFormat } from 'src/config';
 import { APP_MEDIA_LOCATION } from 'src/constants';
-import { SystemConfigCore } from 'src/cores/system-config.core';
 import { AssetEntity } from 'src/entities/asset.entity';
-import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity';
 import { PersonEntity } from 'src/entities/person.entity';
-import { AssetFileType } from 'src/enum';
+import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
 import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { IConfigRepository } from 'src/interfaces/config.interface';
 import { ICryptoRepository } from 'src/interfaces/crypto.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { IMoveRepository } from 'src/interfaces/move.interface';
@@ -15,17 +13,7 @@ import { IPersonRepository } from 'src/interfaces/person.interface';
 import { IStorageRepository } from 'src/interfaces/storage.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { getAssetFiles } from 'src/utils/asset.util';
-
-export enum StorageFolder {
-  ENCODED_VIDEO = 'encoded-video',
-  LIBRARY = 'library',
-  UPLOAD = 'upload',
-  PROFILE = 'profile',
-  THUMBNAILS = 'thumbs',
-}
-
-export const THUMBNAIL_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.THUMBNAILS));
-export const ENCODED_VIDEO_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.ENCODED_VIDEO));
+import { getConfig } from 'src/utils/config';
 
 export interface MoveRequest {
   entityId: string;
@@ -44,21 +32,20 @@ export type GeneratedAssetType = GeneratedImageType | AssetPathType.ENCODED_VIDE
 let instance: StorageCore | null;
 
 export class StorageCore {
-  private configCore;
   private constructor(
     private assetRepository: IAssetRepository,
+    private configRepository: IConfigRepository,
     private cryptoRepository: ICryptoRepository,
     private moveRepository: IMoveRepository,
     private personRepository: IPersonRepository,
     private storageRepository: IStorageRepository,
-    systemMetadataRepository: ISystemMetadataRepository,
+    private systemMetadataRepository: ISystemMetadataRepository,
     private logger: ILoggerRepository,
-  ) {
-    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
-  }
+  ) {}
 
   static create(
     assetRepository: IAssetRepository,
+    configRepository: IConfigRepository,
     cryptoRepository: ICryptoRepository,
     moveRepository: IMoveRepository,
     personRepository: IPersonRepository,
@@ -69,6 +56,7 @@ export class StorageCore {
     if (!instance) {
       instance = new StorageCore(
         assetRepository,
+        configRepository,
         cryptoRepository,
         moveRepository,
         personRepository,
@@ -127,10 +115,6 @@ export class StorageCore {
     return normalizedPath.startsWith(normalizedAppMediaLocation);
   }
 
-  static isGeneratedAsset(path: string) {
-    return path.startsWith(THUMBNAIL_DIR) || path.startsWith(ENCODED_VIDEO_DIR);
-  }
-
   async moveAssetImage(asset: AssetEntity, pathType: GeneratedImageType, format: ImageFormat) {
     const { id: entityId, files } = asset;
     const { thumbnailFile, previewFile } = getAssetFiles(files);
@@ -258,7 +242,12 @@ export class StorageCore {
       this.logger.warn(`Unable to complete move. File size mismatch: ${newPathSize} !== ${oldPathSize}`);
       return false;
     }
-    const config = await this.configCore.getConfig({ withCache: true });
+    const repos = {
+      configRepo: this.configRepository,
+      metadataRepo: this.systemMetadataRepository,
+      logger: this.logger,
+    };
+    const config = await getConfig(repos, { withCache: true });
     if (assetInfo && config.storageTemplate.hashVerificationEnabled) {
       const { checksum } = assetInfo;
       const newChecksum = await this.cryptoRepository.hashFile(newPath);
diff --git a/server/src/cores/system-config.core.ts b/server/src/cores/system-config.core.ts
deleted file mode 100644
index 7c1434004a..0000000000
--- a/server/src/cores/system-config.core.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-import { Injectable } from '@nestjs/common';
-import AsyncLock from 'async-lock';
-import { plainToInstance } from 'class-transformer';
-import { validate } from 'class-validator';
-import { load as loadYaml } from 'js-yaml';
-import * as _ from 'lodash';
-import { Subject } from 'rxjs';
-import { SystemConfig, defaults } from 'src/config';
-import { SystemConfigDto } from 'src/dtos/system-config.dto';
-import { SystemMetadataKey } from 'src/enum';
-import { DatabaseLock } from 'src/interfaces/database.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
-import { getKeysDeep, unsetDeep } from 'src/utils/misc';
-import { DeepPartial } from 'typeorm';
-
-export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise<void>;
-
-let instance: SystemConfigCore | null;
-
-@Injectable()
-export class SystemConfigCore {
-  private readonly asyncLock = new AsyncLock();
-  private config: SystemConfig | null = null;
-  private lastUpdated: number | null = null;
-
-  config$ = new Subject<SystemConfig>();
-
-  private constructor(
-    private repository: ISystemMetadataRepository,
-    private logger: ILoggerRepository,
-  ) {}
-
-  static create(repository: ISystemMetadataRepository, logger: ILoggerRepository) {
-    if (!instance) {
-      instance = new SystemConfigCore(repository, logger);
-    }
-    return instance;
-  }
-
-  static reset() {
-    instance = null;
-  }
-
-  async getConfig({ withCache }: { withCache: boolean }): Promise<SystemConfig> {
-    if (!withCache || !this.config) {
-      const lastUpdated = this.lastUpdated;
-      await this.asyncLock.acquire(DatabaseLock[DatabaseLock.GetSystemConfig], async () => {
-        if (lastUpdated === this.lastUpdated) {
-          this.config = await this.buildConfig();
-          this.lastUpdated = Date.now();
-        }
-      });
-    }
-
-    return this.config!;
-  }
-
-  async updateConfig(newConfig: SystemConfig): Promise<SystemConfig> {
-    // get the difference between the new config and the default config
-    const partialConfig: DeepPartial<SystemConfig> = {};
-    for (const property of getKeysDeep(defaults)) {
-      const newValue = _.get(newConfig, property);
-      const isEmpty = newValue === undefined || newValue === null || newValue === '';
-      const defaultValue = _.get(defaults, property);
-      const isEqual = newValue === defaultValue || _.isEqual(newValue, defaultValue);
-
-      if (isEmpty || isEqual) {
-        continue;
-      }
-
-      _.set(partialConfig, property, newValue);
-    }
-
-    await this.repository.set(SystemMetadataKey.SYSTEM_CONFIG, partialConfig);
-
-    const config = await this.getConfig({ withCache: false });
-    this.config$.next(config);
-    return config;
-  }
-
-  async refreshConfig() {
-    const newConfig = await this.getConfig({ withCache: false });
-    this.config$.next(newConfig);
-  }
-
-  isUsingConfigFile() {
-    return !!process.env.IMMICH_CONFIG_FILE;
-  }
-
-  private async buildConfig() {
-    // load partial
-    const partial = this.isUsingConfigFile()
-      ? await this.loadFromFile(process.env.IMMICH_CONFIG_FILE as string)
-      : await this.repository.get(SystemMetadataKey.SYSTEM_CONFIG);
-
-    // merge with defaults
-    const config = _.cloneDeep(defaults);
-    for (const property of getKeysDeep(partial)) {
-      _.set(config, property, _.get(partial, property));
-    }
-
-    // check for extra properties
-    const unknownKeys = _.cloneDeep(config);
-    for (const property of getKeysDeep(defaults)) {
-      unsetDeep(unknownKeys, property);
-    }
-
-    if (!_.isEmpty(unknownKeys)) {
-      this.logger.warn(`Unknown keys found: ${JSON.stringify(unknownKeys, null, 2)}`);
-    }
-
-    // validate full config
-    const errors = await validate(plainToInstance(SystemConfigDto, config));
-    if (errors.length > 0) {
-      if (this.isUsingConfigFile()) {
-        throw new Error(`Invalid value(s) in file: ${errors}`);
-      } else {
-        this.logger.error('Validation error', errors);
-      }
-    }
-
-    if (!config.ffmpeg.acceptedVideoCodecs.includes(config.ffmpeg.targetVideoCodec)) {
-      config.ffmpeg.acceptedVideoCodecs.push(config.ffmpeg.targetVideoCodec);
-    }
-
-    if (!config.ffmpeg.acceptedAudioCodecs.includes(config.ffmpeg.targetAudioCodec)) {
-      config.ffmpeg.acceptedAudioCodecs.push(config.ffmpeg.targetAudioCodec);
-    }
-
-    return config;
-  }
-
-  private async loadFromFile(filepath: string) {
-    try {
-      const file = await this.repository.readFile(filepath);
-      return loadYaml(file.toString()) as unknown;
-    } catch (error: Error | any) {
-      this.logger.error(`Unable to load configuration file: ${filepath}`);
-      this.logger.error(error);
-      throw error;
-    }
-  }
-}
diff --git a/server/src/cores/user.core.ts b/server/src/cores/user.core.ts
deleted file mode 100644
index 153463a9cc..0000000000
--- a/server/src/cores/user.core.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { BadRequestException } from '@nestjs/common';
-import sanitize from 'sanitize-filename';
-import { SALT_ROUNDS } from 'src/constants';
-import { UserEntity } from 'src/entities/user.entity';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import { IUserRepository } from 'src/interfaces/user.interface';
-
-let instance: UserCore | null;
-
-export class UserCore {
-  private constructor(
-    private cryptoRepository: ICryptoRepository,
-    private userRepository: IUserRepository,
-  ) {}
-
-  static create(cryptoRepository: ICryptoRepository, userRepository: IUserRepository) {
-    if (!instance) {
-      instance = new UserCore(cryptoRepository, userRepository);
-    }
-
-    return instance;
-  }
-
-  static reset() {
-    instance = null;
-  }
-
-  async createUser(dto: Partial<UserEntity> & { email: string }): Promise<UserEntity> {
-    const user = await this.userRepository.getByEmail(dto.email);
-    if (user) {
-      throw new BadRequestException('User exists');
-    }
-
-    if (!dto.isAdmin) {
-      const localAdmin = await this.userRepository.getAdmin();
-      if (!localAdmin) {
-        throw new BadRequestException('The first registered account must the administrator.');
-      }
-    }
-
-    const payload: Partial<UserEntity> = { ...dto };
-    if (payload.password) {
-      payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS);
-    }
-    if (payload.storageLabel) {
-      payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', ''));
-    }
-
-    return this.userRepository.create(payload);
-  }
-}
diff --git a/server/src/database.config.ts b/server/src/database.config.ts
deleted file mode 100644
index 9cc317a734..0000000000
--- a/server/src/database.config.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { DatabaseExtension } from 'src/interfaces/database.interface';
-import { DataSource } from 'typeorm';
-import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js';
-
-const url = process.env.DB_URL;
-const urlOrParts = url
-  ? { url }
-  : {
-      host: process.env.DB_HOSTNAME || 'database',
-      port: Number.parseInt(process.env.DB_PORT || '5432'),
-      username: process.env.DB_USERNAME || 'postgres',
-      password: process.env.DB_PASSWORD || 'postgres',
-      database: process.env.DB_DATABASE_NAME || 'immich',
-    };
-
-/* eslint unicorn/prefer-module: "off" -- We can fix this when migrating to ESM*/
-export const databaseConfig: PostgresConnectionOptions = {
-  type: 'postgres',
-  entities: [__dirname + '/entities/*.entity.{js,ts}'],
-  migrations: [__dirname + '/migrations/*.{js,ts}'],
-  subscribers: [__dirname + '/subscribers/*.{js,ts}'],
-  migrationsRun: false,
-  synchronize: false,
-  connectTimeoutMS: 10_000, // 10 seconds
-  parseInt8: true,
-  ...urlOrParts,
-};
-
-/**
- * @deprecated - DO NOT USE THIS
- *
- * this export is ONLY to be used for TypeORM commands in package.json#scripts
- */
-export const dataSource = new DataSource({ ...databaseConfig, host: 'localhost' });
-
-export const getVectorExtension = () =>
-  process.env.DB_VECTOR_EXTENSION === 'pgvector' ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS;
diff --git a/server/src/decorators.ts b/server/src/decorators.ts
index 2316e114e8..c2bbe19b28 100644
--- a/server/src/decorators.ts
+++ b/server/src/decorators.ts
@@ -1,11 +1,10 @@
 import { SetMetadata, applyDecorators } from '@nestjs/common';
-import { OnEvent } from '@nestjs/event-emitter';
-import { OnEventOptions } from '@nestjs/event-emitter/dist/interfaces';
 import { ApiExtension, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
 import _ from 'lodash';
 import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
-import { EmitEvent, ServerEvent } from 'src/interfaces/event.interface';
-import { Metadata } from 'src/middleware/auth.guard';
+import { ImmichWorker, MetadataKey } from 'src/enum';
+import { EmitEvent } from 'src/interfaces/event.interface';
+import { JobName, QueueName } from 'src/interfaces/job.interface';
 import { setUnion } from 'src/utils/set';
 
 // PostgreSQL uses a 16-bit integer to indicate the number of bound parameters. This means that the
@@ -88,27 +87,6 @@ export function ChunkedSet(options?: { paramIndex?: number }): MethodDecorator {
   return Chunked({ ...options, mergeFn: setUnion });
 }
 
-// https://stackoverflow.com/a/74898678
-export function DecorateAll(
-  decorator: <T>(
-    target: any,
-    propertyKey: string,
-    descriptor: TypedPropertyDescriptor<T>,
-  ) => TypedPropertyDescriptor<T> | void,
-) {
-  return (target: any) => {
-    const descriptors = Object.getOwnPropertyDescriptors(target.prototype);
-    for (const [propName, descriptor] of Object.entries(descriptors)) {
-      const isMethod = typeof descriptor.value == 'function' && propName !== 'constructor';
-      if (!isMethod) {
-        continue;
-      }
-      decorator({ ...target, constructor: { ...target.constructor, name: target.name } as any }, propName, descriptor);
-      Object.defineProperty(target.prototype, propName, descriptor);
-    }
-  };
-}
-
 const UUID = '00000000-0000-4000-a000-000000000000';
 
 export const DummyValue = {
@@ -130,18 +108,28 @@ export interface GenerateSqlQueries {
   params: unknown[];
 }
 
+export const Telemetry = (options: { enabled?: boolean }) =>
+  SetMetadata(MetadataKey.TELEMETRY_ENABLED, options?.enabled ?? true);
+
 /** Decorator to enable versioning/tracking of generated Sql */
 export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GENERATE_SQL_KEY, options);
 
-export const OnServerEvent = (event: ServerEvent, options?: OnEventOptions) =>
-  OnEvent(event, { suppressErrors: false, ...options });
-
-export type EmitConfig = {
-  event: EmitEvent;
+export type EventConfig = {
+  name: EmitEvent;
+  /** handle socket.io server events as well  */
+  server?: boolean;
   /** lower value has higher priority, defaults to 0 */
   priority?: number;
+  /** register events for these workers, defaults to all workers */
+  workers?: ImmichWorker[];
 };
-export const OnEmit = (config: EmitConfig) => SetMetadata(Metadata.ON_EMIT_CONFIG, config);
+export const OnEvent = (config: EventConfig) => SetMetadata(MetadataKey.EVENT_CONFIG, config);
+
+export type JobConfig = {
+  name: JobName;
+  queue: QueueName;
+};
+export const OnJob = (config: JobConfig) => SetMetadata(MetadataKey.JOB_CONFIG, config);
 
 type LifecycleRelease = 'NEXT_RELEASE' | string;
 type LifecycleMetadata = {
diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts
index b12847ee62..76f4fdfc98 100644
--- a/server/src/dtos/album.dto.ts
+++ b/server/src/dtos/album.dto.ts
@@ -7,6 +7,7 @@ import { AuthDto } from 'src/dtos/auth.dto';
 import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
 import { AlbumEntity } from 'src/entities/album.entity';
 import { AlbumUserRole, AssetOrder } from 'src/enum';
+import { getAssetDateTime } from 'src/utils/date-time';
 import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
 
 export class AlbumInfoDto {
@@ -164,8 +165,8 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt
   const hasSharedLink = entity.sharedLinks?.length > 0;
   const hasSharedUser = sharedUsers.length > 0;
 
-  let startDate = assets.at(0)?.fileCreatedAt || undefined;
-  let endDate = assets.at(-1)?.fileCreatedAt || undefined;
+  let startDate = getAssetDateTime(assets.at(0));
+  let endDate = getAssetDateTime(assets.at(-1));
   // Swap dates if start date is greater than end date.
   if (startDate && endDate && startDate > endDate) {
     [startDate, endDate] = [endDate, startDate];
diff --git a/server/src/dtos/asset-media.dto.ts b/server/src/dtos/asset-media.dto.ts
index e9e346c4cb..c62857da65 100644
--- a/server/src/dtos/asset-media.dto.ts
+++ b/server/src/dtos/asset-media.dto.ts
@@ -56,9 +56,6 @@ export class AssetMediaCreateDto extends AssetMediaBase {
   @ValidateBoolean({ optional: true })
   isVisible?: boolean;
 
-  @ValidateBoolean({ optional: true })
-  isOffline?: boolean;
-
   @ValidateUUID({ optional: true })
   livePhotoVideoId?: string;
 
diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts
index ed92208182..a255ac103b 100644
--- a/server/src/dtos/asset-response.dto.ts
+++ b/server/src/dtos/asset-response.dto.ts
@@ -12,7 +12,6 @@ import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
 import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
 import { AssetFaceEntity } from 'src/entities/asset-face.entity';
 import { AssetEntity } from 'src/entities/asset.entity';
-import { SmartInfoEntity } from 'src/entities/smart-info.entity';
 import { AssetType } from 'src/enum';
 import { mimeTypes } from 'src/utils/mime-types';
 
@@ -45,7 +44,6 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
   isTrashed!: boolean;
   isOffline!: boolean;
   exifInfo?: ExifResponseDto;
-  smartInfo?: SmartInfoResponseDto;
   tags?: TagResponseDto[];
   people?: PersonWithFacesResponseDto[];
   unassignedFaces?: AssetFaceWithoutPersonResponseDto[];
@@ -141,7 +139,6 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
     isTrashed: !!entity.deletedAt,
     duration: entity.duration ?? '0:00:00.00000',
     exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
-    smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
     livePhotoVideoId: entity.livePhotoVideoId,
     tags: entity.tags?.map((tag) => mapTag(tag)),
     people: peopleWithFaces(entity.faces),
@@ -161,15 +158,3 @@ export class MemoryLaneResponseDto {
 
   assets!: AssetResponseDto[];
 }
-
-export class SmartInfoResponseDto {
-  tags?: string[] | null;
-  objects?: string[] | null;
-}
-
-export function mapSmartInfo(entity: SmartInfoEntity): SmartInfoResponseDto {
-  return {
-    tags: entity.tags,
-    objects: entity.objects,
-  };
-}
diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts
index 703b1ccfe3..42d6d7d745 100644
--- a/server/src/dtos/asset.dto.ts
+++ b/server/src/dtos/asset.dto.ts
@@ -92,8 +92,9 @@ export class AssetIdsDto {
 }
 
 export enum AssetJobName {
-  REGENERATE_THUMBNAIL = 'regenerate-thumbnail',
+  REFRESH_FACES = 'refresh-faces',
   REFRESH_METADATA = 'refresh-metadata',
+  REGENERATE_THUMBNAIL = 'regenerate-thumbnail',
   TRANSCODE_VIDEO = 'transcode-video',
 }
 
diff --git a/server/src/dtos/audit.dto.ts b/server/src/dtos/audit.dto.ts
index dcace5a551..434da46eba 100644
--- a/server/src/dtos/audit.dto.ts
+++ b/server/src/dtos/audit.dto.ts
@@ -1,8 +1,7 @@
 import { ApiProperty } from '@nestjs/swagger';
 import { Type } from 'class-transformer';
 import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator';
-import { AssetPathType, PathType, PersonPathType, UserPathType } from 'src/entities/move.entity';
-import { EntityType } from 'src/enum';
+import { AssetPathType, EntityType, PathType, PersonPathType, UserPathType } from 'src/enum';
 import { Optional, ValidateDate, ValidateUUID } from 'src/validation';
 
 const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType });
diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts
index f2d5bd2324..b2bf1b8bcc 100644
--- a/server/src/dtos/auth.dto.ts
+++ b/server/src/dtos/auth.dto.ts
@@ -5,30 +5,9 @@ import { APIKeyEntity } from 'src/entities/api-key.entity';
 import { SessionEntity } from 'src/entities/session.entity';
 import { SharedLinkEntity } from 'src/entities/shared-link.entity';
 import { UserEntity } from 'src/entities/user.entity';
+import { ImmichCookie } from 'src/enum';
 import { toEmail } from 'src/validation';
 
-export enum ImmichCookie {
-  ACCESS_TOKEN = 'immich_access_token',
-  AUTH_TYPE = 'immich_auth_type',
-  IS_AUTHENTICATED = 'immich_is_authenticated',
-  SHARED_LINK_TOKEN = 'immich_shared_link_token',
-}
-
-export enum ImmichHeader {
-  API_KEY = 'x-api-key',
-  USER_TOKEN = 'x-immich-user-token',
-  SESSION_TOKEN = 'x-immich-session-token',
-  SHARED_LINK_KEY = 'x-immich-share-key',
-  CHECKSUM = 'x-immich-checksum',
-  CID = 'x-immich-cid',
-}
-
-export enum ImmichQuery {
-  SHARED_LINK_KEY = 'key',
-  API_KEY = 'apiKey',
-  SESSION_KEY = 'sessionKey',
-}
-
 export type CookieResponse = {
   isSecure: boolean;
   values: Array<{ key: ImmichCookie; value: string }>;
diff --git a/server/src/dtos/env.dto.ts b/server/src/dtos/env.dto.ts
new file mode 100644
index 0000000000..6c238252a6
--- /dev/null
+++ b/server/src/dtos/env.dto.ts
@@ -0,0 +1,190 @@
+import { Transform, Type } from 'class-transformer';
+import { IsEnum, IsInt, IsString } from 'class-validator';
+import { ImmichEnvironment, LogLevel } from 'src/enum';
+import { IsIPRange, Optional, ValidateBoolean } from 'src/validation';
+
+export class EnvDto {
+  @IsInt()
+  @Optional()
+  @Type(() => Number)
+  IMMICH_API_METRICS_PORT?: number;
+
+  @IsString()
+  @Optional()
+  IMMICH_BUILD_DATA?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_BUILD?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_BUILD_URL?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_BUILD_IMAGE?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_BUILD_IMAGE_URL?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_CONFIG_FILE?: string;
+
+  @IsEnum(ImmichEnvironment)
+  @Optional()
+  IMMICH_ENV?: ImmichEnvironment;
+
+  @IsString()
+  @Optional()
+  IMMICH_HOST?: string;
+
+  @ValidateBoolean({ optional: true })
+  IMMICH_IGNORE_MOUNT_CHECK_ERRORS?: boolean;
+
+  @IsEnum(LogLevel)
+  @Optional()
+  IMMICH_LOG_LEVEL?: LogLevel;
+
+  @IsInt()
+  @Optional()
+  @Type(() => Number)
+  IMMICH_MICROSERVICES_METRICS_PORT?: number;
+
+  @IsInt()
+  @Optional()
+  @Type(() => Number)
+  IMMICH_PORT?: number;
+
+  @IsString()
+  @Optional()
+  IMMICH_REPOSITORY?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_REPOSITORY_URL?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_SOURCE_REF?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_SOURCE_COMMIT?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_SOURCE_URL?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_TELEMETRY_INCLUDE?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_TELEMETRY_EXCLUDE?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_THIRD_PARTY_SOURCE_URL?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_THIRD_PARTY_BUG_FEATURE_URL?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_THIRD_PARTY_DOCUMENTATION_URL?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_THIRD_PARTY_SUPPORT_URL?: string;
+
+  @IsIPRange({ requireCIDR: false }, { each: true })
+  @Transform(({ value }) =>
+    value && typeof value === 'string'
+      ? value
+          .split(',')
+          .map((value) => value.trim())
+          .filter(Boolean)
+      : value,
+  )
+  @Optional()
+  IMMICH_TRUSTED_PROXIES?: string[];
+
+  @IsString()
+  @Optional()
+  IMMICH_WORKERS_INCLUDE?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_WORKERS_EXCLUDE?: string;
+
+  @IsString()
+  @Optional()
+  DB_DATABASE_NAME?: string;
+
+  @IsString()
+  @Optional()
+  DB_HOSTNAME?: string;
+
+  @IsString()
+  @Optional()
+  DB_PASSWORD?: string;
+
+  @IsInt()
+  @Optional()
+  @Type(() => Number)
+  DB_PORT?: number;
+
+  @ValidateBoolean({ optional: true })
+  DB_SKIP_MIGRATIONS?: boolean;
+
+  @IsString()
+  @Optional()
+  DB_URL?: string;
+
+  @IsString()
+  @Optional()
+  DB_USERNAME?: string;
+
+  @IsEnum(['pgvector', 'pgvecto.rs'])
+  @Optional()
+  DB_VECTOR_EXTENSION?: 'pgvector' | 'pgvecto.rs';
+
+  @IsString()
+  @Optional()
+  NO_COLOR?: string;
+
+  @IsString()
+  @Optional()
+  REDIS_HOSTNAME?: string;
+
+  @IsInt()
+  @Optional()
+  @Type(() => Number)
+  REDIS_PORT?: number;
+
+  @IsInt()
+  @Optional()
+  @Type(() => Number)
+  REDIS_DBINDEX?: number;
+
+  @IsString()
+  @Optional()
+  REDIS_USERNAME?: string;
+
+  @IsString()
+  @Optional()
+  REDIS_PASSWORD?: string;
+
+  @IsString()
+  @Optional()
+  REDIS_SOCKET?: string;
+
+  @IsString()
+  @Optional()
+  REDIS_URL?: string;
+}
diff --git a/server/src/dtos/job.dto.ts b/server/src/dtos/job.dto.ts
index b7d8cf59bf..31612bd8a4 100644
--- a/server/src/dtos/job.dto.ts
+++ b/server/src/dtos/job.dto.ts
@@ -1,5 +1,6 @@
 import { ApiProperty } from '@nestjs/swagger';
 import { IsEnum, IsNotEmpty } from 'class-validator';
+import { ManualJobName } from 'src/enum';
 import { JobCommand, QueueName } from 'src/interfaces/job.interface';
 import { ValidateBoolean } from 'src/validation';
 
@@ -17,7 +18,13 @@ export class JobCommandDto {
   command!: JobCommand;
 
   @ValidateBoolean({ optional: true })
-  force!: boolean;
+  force?: boolean; // TODO: this uses undefined as a third state, which should be refactored to be more explicit
+}
+
+export class JobCreateDto {
+  @IsEnum(ManualJobName)
+  @ApiProperty({ type: 'string', enum: ManualJobName, enumName: 'ManualJobName' })
+  name!: ManualJobName;
 }
 
 export class JobCountsDto {
@@ -90,4 +97,7 @@ export class AllJobStatusResponseDto implements Record<QueueName, JobStatusDto>
 
   @ApiProperty({ type: JobStatusDto })
   [QueueName.NOTIFICATION]!: JobStatusDto;
+
+  @ApiProperty({ type: JobStatusDto })
+  [QueueName.BACKUP_DATABASE]!: JobStatusDto;
 }
diff --git a/server/src/dtos/library.dto.ts b/server/src/dtos/library.dto.ts
index c2c3ac9d27..7fb363dd9a 100644
--- a/server/src/dtos/library.dto.ts
+++ b/server/src/dtos/library.dto.ts
@@ -1,7 +1,7 @@
 import { ApiProperty } from '@nestjs/swagger';
 import { ArrayMaxSize, ArrayUnique, IsNotEmpty, IsString } from 'class-validator';
 import { LibraryEntity } from 'src/entities/library.entity';
-import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
+import { Optional, ValidateUUID } from 'src/validation';
 
 export class CreateLibraryDto {
   @ValidateUUID()
@@ -89,14 +89,6 @@ export class LibrarySearchDto {
   userId?: string;
 }
 
-export class ScanLibraryDto {
-  @ValidateBoolean({ optional: true })
-  refreshModifiedFiles?: boolean;
-
-  @ValidateBoolean({ optional: true })
-  refreshAllFiles?: boolean;
-}
-
 export class LibraryResponseDto {
   id!: string;
   ownerId!: string;
diff --git a/server/src/dtos/model-config.dto.ts b/server/src/dtos/model-config.dto.ts
index dffacc793d..f8b9e2043f 100644
--- a/server/src/dtos/model-config.dto.ts
+++ b/server/src/dtos/model-config.dto.ts
@@ -27,14 +27,14 @@ export class DuplicateDetectionConfig extends TaskConfig {
 
 export class FacialRecognitionConfig extends ModelConfig {
   @IsNumber()
-  @Min(0)
+  @Min(0.1)
   @Max(1)
   @Type(() => Number)
   @ApiProperty({ type: 'number', format: 'double' })
   minScore!: number;
 
   @IsNumber()
-  @Min(0)
+  @Min(0.1)
   @Max(2)
   @Type(() => Number)
   @ApiProperty({ type: 'number', format: 'double' })
diff --git a/server/src/dtos/notification.dto.ts b/server/src/dtos/notification.dto.ts
new file mode 100644
index 0000000000..c1a09c801c
--- /dev/null
+++ b/server/src/dtos/notification.dto.ts
@@ -0,0 +1,13 @@
+import { IsString } from 'class-validator';
+
+export class TestEmailResponseDto {
+  messageId!: string;
+}
+export class TemplateResponseDto {
+  name!: string;
+  html!: string;
+}
+export class TemplateDto {
+  @IsString()
+  template!: string;
+}
diff --git a/server/src/dtos/person.dto.ts b/server/src/dtos/person.dto.ts
index 94ee52d916..047ef600b8 100644
--- a/server/src/dtos/person.dto.ts
+++ b/server/src/dtos/person.dto.ts
@@ -67,6 +67,10 @@ export class MergePersonDto {
 export class PersonSearchDto {
   @ValidateBoolean({ optional: true })
   withHidden?: boolean;
+  @ValidateUUID({ optional: true })
+  closestPersonId?: string;
+  @ValidateUUID({ optional: true })
+  closestAssetId?: string;
 
   /** Page number for pagination */
   @ApiPropertyOptional()
diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts
index 9e36cfee80..5c5dce1a11 100644
--- a/server/src/dtos/search.dto.ts
+++ b/server/src/dtos/search.dto.ts
@@ -99,12 +99,6 @@ class BaseSearchDto {
   @Optional({ nullable: true, emptyToNull: true })
   lensModel?: string | null;
 
-  @IsInt()
-  @Min(1)
-  @Type(() => Number)
-  @Optional()
-  page?: number;
-
   @IsInt()
   @Min(1)
   @Max(1000)
@@ -119,7 +113,15 @@ class BaseSearchDto {
   personIds?: string[];
 }
 
-export class MetadataSearchDto extends BaseSearchDto {
+export class RandomSearchDto extends BaseSearchDto {
+  @ValidateBoolean({ optional: true })
+  withStacked?: boolean;
+
+  @ValidateBoolean({ optional: true })
+  withPeople?: boolean;
+}
+
+export class MetadataSearchDto extends RandomSearchDto {
   @ValidateUUID({ optional: true })
   id?: string;
 
@@ -133,12 +135,6 @@ export class MetadataSearchDto extends BaseSearchDto {
   @Optional()
   checksum?: string;
 
-  @ValidateBoolean({ optional: true })
-  withStacked?: boolean;
-
-  @ValidateBoolean({ optional: true })
-  withPeople?: boolean;
-
   @IsString()
   @IsNotEmpty()
   @Optional()
@@ -168,12 +164,24 @@ export class MetadataSearchDto extends BaseSearchDto {
   @Optional()
   @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
   order?: AssetOrder;
+
+  @IsInt()
+  @Min(1)
+  @Type(() => Number)
+  @Optional()
+  page?: number;
 }
 
 export class SmartSearchDto extends BaseSearchDto {
   @IsString()
   @IsNotEmpty()
   query!: string;
+
+  @IsInt()
+  @Min(1)
+  @Type(() => Number)
+  @Optional()
+  page?: number;
 }
 
 export class SearchPlacesDto {
diff --git a/server/src/dtos/server.dto.ts b/server/src/dtos/server.dto.ts
index 78e59e4d1a..e1f94dbaa5 100644
--- a/server/src/dtos/server.dto.ts
+++ b/server/src/dtos/server.dto.ts
@@ -30,6 +30,11 @@ export class ServerAboutResponseDto {
   exiftool?: string;
 
   licensed!: boolean;
+
+  thirdPartySourceUrl?: string;
+  thirdPartyBugFeatureUrl?: string;
+  thirdPartyDocumentationUrl?: string;
+  thirdPartySupportUrl?: string;
 }
 
 export class ServerStorageResponseDto {
@@ -63,6 +68,12 @@ export class ServerVersionResponseDto {
   }
 }
 
+export class ServerVersionHistoryResponseDto {
+  id!: string;
+  createdAt!: Date;
+  version!: string;
+}
+
 export class UsageByUserDto {
   @ApiProperty({ type: 'string' })
   userId!: string;
@@ -75,6 +86,10 @@ export class UsageByUserDto {
   @ApiProperty({ type: 'integer', format: 'int64' })
   usage!: number;
   @ApiProperty({ type: 'integer', format: 'int64' })
+  usagePhotos!: number;
+  @ApiProperty({ type: 'integer', format: 'int64' })
+  usageVideos!: number;
+  @ApiProperty({ type: 'integer', format: 'int64' })
   quotaSizeInBytes!: number | null;
 }
 
@@ -88,6 +103,12 @@ export class ServerStatsResponseDto {
   @ApiProperty({ type: 'integer', format: 'int64' })
   usage = 0;
 
+  @ApiProperty({ type: 'integer', format: 'int64' })
+  usagePhotos = 0;
+
+  @ApiProperty({ type: 'integer', format: 'int64' })
+  usageVideos = 0;
+
   @ApiProperty({
     isArray: true,
     type: UsageByUserDto,
@@ -96,7 +117,9 @@ export class ServerStatsResponseDto {
       {
         photos: 1,
         videos: 1,
-        diskUsageRaw: 1,
+        diskUsageRaw: 2,
+        usagePhotos: 1,
+        usageVideos: 1,
       },
     ],
   })
@@ -121,6 +144,9 @@ export class ServerConfigDto {
   isInitialized!: boolean;
   isOnboarded!: boolean;
   externalDomain!: string;
+  publicUsers!: boolean;
+  mapDarkStyleUrl!: string;
+  mapLightStyleUrl!: string;
 }
 
 export class ServerFeaturesDto {
diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts
index 14027aa16a..3509182545 100644
--- a/server/src/dtos/system-config.dto.ts
+++ b/server/src/dtos/system-config.dto.ts
@@ -1,6 +1,7 @@
 import { ApiProperty } from '@nestjs/swagger';
-import { Type } from 'class-transformer';
+import { Exclude, Transform, Type } from 'class-transformer';
 import {
+  ArrayMinSize,
   IsBoolean,
   IsEnum,
   IsInt,
@@ -12,40 +13,55 @@ import {
   IsUrl,
   Max,
   Min,
-  Validate,
   ValidateIf,
   ValidateNested,
-  ValidatorConstraint,
-  ValidatorConstraintInterface,
 } from 'class-validator';
+import { SystemConfig } from 'src/config';
+import { PropertyLifecycle } from 'src/decorators';
+import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto';
 import {
   AudioCodec,
   CQMode,
   Colorspace,
   ImageFormat,
   LogLevel,
-  SystemConfig,
   ToneMapping,
   TranscodeHWAccel,
   TranscodePolicy,
   VideoCodec,
   VideoContainer,
-} from 'src/config';
-import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto';
+} from 'src/enum';
 import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
-import { ValidateBoolean, validateCronExpression } from 'src/validation';
-
-@ValidatorConstraint({ name: 'cronValidator' })
-class CronValidator implements ValidatorConstraintInterface {
-  validate(expression: string): boolean {
-    return validateCronExpression(expression);
-  }
-}
+import { IsCronExpression, ValidateBoolean } from 'src/validation';
 
 const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled;
 const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled;
 const isOAuthOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrideEnabled;
 const isEmailNotificationEnabled = (config: SystemConfigSmtpDto) => config.enabled;
+const isDatabaseBackupEnabled = (config: DatabaseBackupConfig) => config.enabled;
+
+export class DatabaseBackupConfig {
+  @ValidateBoolean()
+  enabled!: boolean;
+
+  @ValidateIf(isDatabaseBackupEnabled)
+  @IsNotEmpty()
+  @IsCronExpression()
+  @IsString()
+  cronExpression!: string;
+
+  @IsInt()
+  @IsPositive()
+  @IsNotEmpty()
+  keepLastAmount!: number;
+}
+
+export class SystemConfigBackupsDto {
+  @Type(() => DatabaseBackupConfig)
+  @ValidateNested()
+  @IsObject()
+  database!: DatabaseBackupConfig;
+}
 
 export class SystemConfigFFmpegDto {
   @IsInt()
@@ -110,12 +126,6 @@ export class SystemConfigFFmpegDto {
   @ApiProperty({ type: 'integer' })
   gopSize!: number;
 
-  @IsInt()
-  @Min(0)
-  @Type(() => Number)
-  @ApiProperty({ type: 'integer' })
-  npl!: number;
-
   @ValidateBoolean()
   temporalAQ!: boolean;
 
@@ -226,7 +236,7 @@ class SystemConfigLibraryScanDto {
 
   @ValidateIf(isLibraryScanEnabled)
   @IsNotEmpty()
-  @Validate(CronValidator, { message: 'Invalid cron expression' })
+  @IsCronExpression()
   @IsString()
   cronExpression!: string;
 }
@@ -261,9 +271,16 @@ class SystemConfigMachineLearningDto {
   @ValidateBoolean()
   enabled!: boolean;
 
-  @IsUrl({ require_tld: false, allow_underscores: true })
+  @PropertyLifecycle({ deprecatedAt: 'v1.122.0' })
+  @Exclude()
+  url?: string;
+
+  @IsUrl({ require_tld: false, allow_underscores: true }, { each: true })
+  @ArrayMinSize(1)
+  @Transform(({ obj, value }) => (obj.url ? [obj.url] : value))
   @ValidateIf((dto) => dto.enabled)
-  url!: string;
+  @ApiProperty({ type: 'array', items: { type: 'string', format: 'uri' }, minItems: 1 })
+  urls!: string[];
 
   @Type(() => CLIPConfig)
   @ValidateNested()
@@ -296,10 +313,12 @@ class SystemConfigMapDto {
   @ValidateBoolean()
   enabled!: boolean;
 
-  @IsString()
+  @IsNotEmpty()
+  @IsUrl()
   lightStyle!: string;
 
-  @IsString()
+  @IsNotEmpty()
+  @IsUrl()
   darkStyle!: string;
 }
 
@@ -394,6 +413,9 @@ class SystemConfigServerDto {
 
   @IsString()
   loginPageMessage!: string;
+
+  @IsBoolean()
+  publicUsers!: boolean;
 }
 
 class SystemConfigSmtpTransportDto {
@@ -443,6 +465,24 @@ class SystemConfigNotificationsDto {
   smtp!: SystemConfigSmtpDto;
 }
 
+class SystemConfigTemplateEmailsDto {
+  @IsString()
+  albumInviteTemplate!: string;
+
+  @IsString()
+  welcomeTemplate!: string;
+
+  @IsString()
+  albumUpdateTemplate!: string;
+}
+
+class SystemConfigTemplatesDto {
+  @Type(() => SystemConfigTemplateEmailsDto)
+  @ValidateNested()
+  @IsObject()
+  email!: SystemConfigTemplateEmailsDto;
+}
+
 class SystemConfigStorageTemplateDto {
   @ValidateBoolean()
   enabled!: boolean;
@@ -471,26 +511,10 @@ export class SystemConfigThemeDto {
   customCss!: string;
 }
 
-class SystemConfigImageDto {
+class SystemConfigGeneratedImageDto {
   @IsEnum(ImageFormat)
   @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat })
-  thumbnailFormat!: ImageFormat;
-
-  @IsInt()
-  @Min(1)
-  @Type(() => Number)
-  @ApiProperty({ type: 'integer' })
-  thumbnailSize!: number;
-
-  @IsEnum(ImageFormat)
-  @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat })
-  previewFormat!: ImageFormat;
-
-  @IsInt()
-  @Min(1)
-  @Type(() => Number)
-  @ApiProperty({ type: 'integer' })
-  previewSize!: number;
+  format!: ImageFormat;
 
   @IsInt()
   @Min(1)
@@ -499,6 +523,24 @@ class SystemConfigImageDto {
   @ApiProperty({ type: 'integer' })
   quality!: number;
 
+  @IsInt()
+  @Min(1)
+  @Type(() => Number)
+  @ApiProperty({ type: 'integer' })
+  size!: number;
+}
+
+export class SystemConfigImageDto {
+  @Type(() => SystemConfigGeneratedImageDto)
+  @ValidateNested()
+  @IsObject()
+  thumbnail!: SystemConfigGeneratedImageDto;
+
+  @Type(() => SystemConfigGeneratedImageDto)
+  @ValidateNested()
+  @IsObject()
+  preview!: SystemConfigGeneratedImageDto;
+
   @IsEnum(Colorspace)
   @ApiProperty({ enumName: 'Colorspace', enum: Colorspace })
   colorspace!: Colorspace;
@@ -527,6 +569,11 @@ class SystemConfigUserDto {
 }
 
 export class SystemConfigDto implements SystemConfig {
+  @Type(() => SystemConfigBackupsDto)
+  @ValidateNested()
+  @IsObject()
+  backup!: SystemConfigBackupsDto;
+
   @Type(() => SystemConfigFFmpegDto)
   @ValidateNested()
   @IsObject()
@@ -607,6 +654,11 @@ export class SystemConfigDto implements SystemConfig {
   @IsObject()
   notifications!: SystemConfigNotificationsDto;
 
+  @Type(() => SystemConfigTemplatesDto)
+  @ValidateNested()
+  @IsObject()
+  templates!: SystemConfigTemplatesDto;
+
   @Type(() => SystemConfigServerDto)
   @ValidateNested()
   @IsObject()
diff --git a/server/src/dtos/trash.dto.ts b/server/src/dtos/trash.dto.ts
new file mode 100644
index 0000000000..d8e139bff2
--- /dev/null
+++ b/server/src/dtos/trash.dto.ts
@@ -0,0 +1,6 @@
+import { ApiProperty } from '@nestjs/swagger';
+
+export class TrashResponseDto {
+  @ApiProperty({ type: 'integer' })
+  count!: number;
+}
diff --git a/server/src/dtos/user-profile.dto.ts b/server/src/dtos/user-profile.dto.ts
index 9659fa3965..16eea373e3 100644
--- a/server/src/dtos/user-profile.dto.ts
+++ b/server/src/dtos/user-profile.dto.ts
@@ -8,12 +8,6 @@ export class CreateProfileImageDto {
 
 export class CreateProfileImageResponseDto {
   userId!: string;
+  profileChangedAt!: Date;
   profileImagePath!: string;
 }
-
-export function mapCreateProfileImageResponse(userId: string, profileImagePath: string): CreateProfileImageResponseDto {
-  return {
-    userId,
-    profileImagePath,
-  };
-}
diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts
index f7cd70ee74..593a7934bc 100644
--- a/server/src/dtos/user.dto.ts
+++ b/server/src/dtos/user.dto.ts
@@ -32,6 +32,7 @@ export class UserResponseDto {
   profileImagePath!: string;
   @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
   avatarColor!: UserAvatarColor;
+  profileChangedAt!: Date;
 }
 
 export class UserLicense {
@@ -47,6 +48,7 @@ export const mapUser = (entity: UserEntity): UserResponseDto => {
     name: entity.name,
     profileImagePath: entity.profileImagePath,
     avatarColor: getPreferences(entity).avatar.color,
+    profileChangedAt: entity.profileChangedAt,
   };
 };
 
@@ -60,7 +62,6 @@ export class UserAdminCreateDto {
   @Transform(toEmail)
   email!: string;
 
-  @IsNotEmpty()
   @IsString()
   password!: string;
 
diff --git a/server/src/emails/album-invite.email.tsx b/server/src/emails/album-invite.email.tsx
index 232ef5290d..0b3819b332 100644
--- a/server/src/emails/album-invite.email.tsx
+++ b/server/src/emails/album-invite.email.tsx
@@ -3,6 +3,7 @@ import * as React from 'react';
 import { ImmichButton } from 'src/emails/components/button.component';
 import ImmichLayout from 'src/emails/components/immich.layout';
 import { AlbumInviteEmailProps } from 'src/interfaces/notification.interface';
+import { replaceTemplateTags } from 'src/utils/replace-template-tags';
 
 export const AlbumInviteEmail = ({
   baseUrl,
@@ -11,39 +12,64 @@ export const AlbumInviteEmail = ({
   senderName,
   albumId,
   cid,
-}: AlbumInviteEmailProps) => (
-  <ImmichLayout preview="You have been added to a shared album.">
-    <Text className="m-0">
-      Hey <strong>{recipientName}</strong>!
-    </Text>
+  customTemplate,
+}: AlbumInviteEmailProps) => {
+  const variables = {
+    albumName,
+    recipientName,
+    senderName,
+    albumId,
+    baseUrl,
+  };
 
-    <Text>
-      {senderName} has added you to the album <strong>{albumName}</strong>.
-    </Text>
+  const emailContent = customTemplate ? (
+    replaceTemplateTags(customTemplate, variables)
+  ) : (
+    <>
+      <Text className="m-0">
+        Hey <strong>{recipientName}</strong>!
+      </Text>
 
-    {cid && (
-      <Section className="flex justify-center my-0">
-        <Img
-          className="max-w-[300px] w-full rounded-lg"
-          src={`cid:${cid}`}
-          style={{
-            boxShadow: 'rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px',
-          }}
-        />
+      <Text>
+        {senderName} has added you to the album <strong>{albumName}</strong>.
+      </Text>
+    </>
+  );
+
+  return (
+    <ImmichLayout preview={customTemplate ? emailContent.toString() : 'You have been added to a shared album.'}>
+      {customTemplate && (
+        <Text className="m-0">
+          <div dangerouslySetInnerHTML={{ __html: emailContent }}></div>
+        </Text>
+      )}
+
+      {!customTemplate && emailContent}
+
+      {cid && (
+        <Section className="flex justify-center my-0">
+          <Img
+            className="max-w-[300px] w-full rounded-lg"
+            src={`cid:${cid}`}
+            style={{
+              boxShadow: 'rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px',
+            }}
+          />
+        </Section>
+      )}
+
+      <Section className="flex justify-center my-6">
+        <ImmichButton href={`${baseUrl}/albums/${albumId}`}>View Album</ImmichButton>
       </Section>
-    )}
 
-    <Section className="flex justify-center my-6">
-      <ImmichButton href={`${baseUrl}/albums/${albumId}`}>View Album</ImmichButton>
-    </Section>
-
-    <Text className="text-xs">
-      If you cannot click the button use the link below to view the album.
-      <br />
-      <Link href={`${baseUrl}/albums/${albumId}`}>{`${baseUrl}/albums/${albumId}`}</Link>
-    </Text>
-  </ImmichLayout>
-);
+      <Text className="text-xs">
+        If you cannot click the button use the link below to view the album.
+        <br />
+        <Link href={`${baseUrl}/albums/${albumId}`}>{`${baseUrl}/albums/${albumId}`}</Link>
+      </Text>
+    </ImmichLayout>
+  );
+};
 
 AlbumInviteEmail.PreviewProps = {
   baseUrl: 'https://demo.immich.app',
diff --git a/server/src/emails/album-update.email.tsx b/server/src/emails/album-update.email.tsx
index 0fb5ad931c..9dcd858e93 100644
--- a/server/src/emails/album-update.email.tsx
+++ b/server/src/emails/album-update.email.tsx
@@ -3,47 +3,80 @@ import * as React from 'react';
 import { ImmichButton } from 'src/emails/components/button.component';
 import ImmichLayout from 'src/emails/components/immich.layout';
 import { AlbumUpdateEmailProps } from 'src/interfaces/notification.interface';
+import { replaceTemplateTags } from 'src/utils/replace-template-tags';
 
-export const AlbumUpdateEmail = ({ baseUrl, albumName, recipientName, albumId, cid }: AlbumUpdateEmailProps) => (
-  <ImmichLayout preview="New media has been added to a shared album.">
-    <Text className="m-0">
-      Hey <strong>{recipientName}</strong>!
-    </Text>
+export const AlbumUpdateEmail = ({
+  baseUrl,
+  albumName,
+  recipientName,
+  albumId,
+  cid,
+  customTemplate,
+}: AlbumUpdateEmailProps) => {
+  const usableTemplateVariables = {
+    albumName,
+    recipientName,
+    albumId,
+    baseUrl,
+  };
 
-    <Text>
-      New media has been added to <strong>{albumName}</strong>,
-      <br /> check it out!
-    </Text>
+  const emailContent = customTemplate ? (
+    replaceTemplateTags(customTemplate, usableTemplateVariables)
+  ) : (
+    <>
+      <Text className="m-0">
+        Hey <strong>{recipientName}</strong>!
+      </Text>
 
-    {cid && (
-      <Section className="flex justify-center my-0">
-        <Img
-          className="max-w-[300px] w-full rounded-lg"
-          src={`cid:${cid}`}
-          style={{
-            boxShadow: 'rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px',
-          }}
-        />
+      <Text>
+        New media has been added to <strong>{albumName}</strong>,
+        <br /> check it out!
+      </Text>
+    </>
+  );
+
+  return (
+    <ImmichLayout preview={customTemplate ? emailContent.toString() : 'New media has been added to a shared album.'}>
+      {customTemplate && (
+        <Text className="m-0">
+          <div dangerouslySetInnerHTML={{ __html: emailContent }}></div>
+        </Text>
+      )}
+
+      {!customTemplate && emailContent}
+
+      {cid && (
+        <Section className="flex justify-center my-0">
+          <Img
+            className="max-w-[300px] w-full rounded-lg"
+            src={`cid:${cid}`}
+            style={{
+              boxShadow: 'rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px',
+            }}
+          />
+        </Section>
+      )}
+
+      <Section className="flex justify-center my-6">
+        <ImmichButton href={`${baseUrl}/albums/${albumId}`}>View Album</ImmichButton>
       </Section>
-    )}
 
-    <Section className="flex justify-center my-6">
-      <ImmichButton href={`${baseUrl}/albums/${albumId}`}>View Album</ImmichButton>
-    </Section>
-
-    <Text className="text-xs">
-      If you cannot click the button use the link below to view the album.
-      <br />
-      <Link href={`${baseUrl}/albums/${albumId}`}>{`${baseUrl}/albums/${albumId}`}</Link>
-    </Text>
-  </ImmichLayout>
-);
+      <Text className="text-xs">
+        If you cannot click the button use the link below to view the album.
+        <br />
+        <Link href={`${baseUrl}/albums/${albumId}`}>{`${baseUrl}/albums/${albumId}`}</Link>
+      </Text>
+    </ImmichLayout>
+  );
+};
 
 AlbumUpdateEmail.PreviewProps = {
   baseUrl: 'https://demo.immich.app',
   albumName: 'Trip to Europe',
   albumId: 'b63f6dae-e1c9-401b-9a85-9dbbf5612539',
   recipientName: 'Alan Turing',
+  cid: '',
+  customTemplate: '',
 } as AlbumUpdateEmailProps;
 
 export default AlbumUpdateEmail;
diff --git a/server/src/emails/components/footer.template.tsx b/server/src/emails/components/footer.template.tsx
index 7c41a7196d..c84246bf87 100644
--- a/server/src/emails/components/footer.template.tsx
+++ b/server/src/emails/components/footer.template.tsx
@@ -5,12 +5,14 @@ export const ImmichFooter = () => (
   <>
     <Row className="h-18 w-full">
       <Column align="center" className="w-6/12 sm:w-full">
-        <Link href="https://play.google.com/store/apps/details?id=app.alextran.immich">
-          <Img className="h-full max-w-full" src={`https://immich.app/img/google-play-badge.png`} />
-        </Link>
+        <div>
+          <Link href="https://play.google.com/store/apps/details?id=app.alextran.immich" className="object-contain">
+            <Img className="max-w-full" src={`https://immich.app/img/google-play-badge.png`} />
+          </Link>
+        </div>
       </Column>
       <Column align="center" className="w-6/12 sm:w-full">
-        <div className="h-full p-3">
+        <div className="h-full p-6">
           <Link href="https://apps.apple.com/sg/app/immich/id1613945652">
             <Img src={`https://immich.app/img/ios-app-store-badge.png`} alt="Immich" className="max-w-full" />
           </Link>
diff --git a/server/src/emails/welcome.email.tsx b/server/src/emails/welcome.email.tsx
index e031ac6b97..ced0b77698 100644
--- a/server/src/emails/welcome.email.tsx
+++ b/server/src/emails/welcome.email.tsx
@@ -3,36 +3,62 @@ import * as React from 'react';
 import { ImmichButton } from 'src/emails/components/button.component';
 import ImmichLayout from 'src/emails/components/immich.layout';
 import { WelcomeEmailProps } from 'src/interfaces/notification.interface';
+import { replaceTemplateTags } from 'src/utils/replace-template-tags';
 
-export const WelcomeEmail = ({ baseUrl, displayName, username, password }: WelcomeEmailProps) => (
-  <ImmichLayout preview="You have been invited to a new Immich instance.">
-    <Text className="m-0">
-      Hey <strong>{displayName}</strong>!
-    </Text>
+export const WelcomeEmail = ({ baseUrl, displayName, username, password, customTemplate }: WelcomeEmailProps) => {
+  const usableTemplateVariables = {
+    displayName,
+    username,
+    password,
+    baseUrl,
+  };
 
-    <Text>A new account has been created for you.</Text>
+  const emailContent = customTemplate ? (
+    replaceTemplateTags(customTemplate, usableTemplateVariables)
+  ) : (
+    <>
+      <Text className="m-0">
+        Hey <strong>{displayName}</strong>!
+      </Text>
 
-    <Text>
-      <strong>Username</strong>: {username}
-      {password && (
-        <>
-          <br />
-          <strong>Password</strong>: {password}
-        </>
+      <Text>A new account has been created for you.</Text>
+
+      <Text>
+        <strong>Username</strong>: {username}
+        {password && (
+          <>
+            <br />
+            <strong>Password</strong>: {password}
+          </>
+        )}
+      </Text>
+    </>
+  );
+
+  return (
+    <ImmichLayout
+      preview={customTemplate ? emailContent.toString() : 'You have been invited to a new Immich instance.'}
+    >
+      {customTemplate && (
+        <Text className="m-0">
+          <div dangerouslySetInnerHTML={{ __html: emailContent }}></div>
+        </Text>
       )}
-    </Text>
 
-    <Section className="flex justify-center my-6">
-      <ImmichButton href={`${baseUrl}/auth/login`}>Login</ImmichButton>
-    </Section>
+      {!customTemplate && emailContent}
 
-    <Text className="text-xs">
-      If you cannot click the button use the link below to proceed with first login.
-      <br />
-      <Link href={baseUrl}>{baseUrl}</Link>
-    </Text>
-  </ImmichLayout>
-);
+      <Section className="flex justify-center my-6">
+        <ImmichButton href={`${baseUrl}/auth/login`}>Login</ImmichButton>
+      </Section>
+
+      <Text className="text-xs">
+        If you cannot click the button use the link below to proceed with first login.
+        <br />
+        <Link href={baseUrl}>{baseUrl}</Link>
+      </Text>
+    </ImmichLayout>
+  );
+};
 
 WelcomeEmail.PreviewProps = {
   baseUrl: 'https://demo.immich.app/auth/login',
diff --git a/server/src/entities/album.entity.ts b/server/src/entities/album.entity.ts
index e5d2c98814..5aec5a0f47 100644
--- a/server/src/entities/album.entity.ts
+++ b/server/src/entities/album.entity.ts
@@ -52,7 +52,7 @@ export class AlbumEntity {
   albumUsers!: AlbumUserEntity[];
 
   @ManyToMany(() => AssetEntity, (asset) => asset.albums)
-  @JoinTable()
+  @JoinTable({ synchronize: false })
   assets!: AssetEntity[];
 
   @OneToMany(() => SharedLinkEntity, (link) => link.album)
diff --git a/server/src/entities/asset.entity.ts b/server/src/entities/asset.entity.ts
index 9ebf9364d1..f9e5c5e981 100644
--- a/server/src/entities/asset.entity.ts
+++ b/server/src/entities/asset.entity.ts
@@ -5,12 +5,11 @@ import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
 import { ExifEntity } from 'src/entities/exif.entity';
 import { LibraryEntity } from 'src/entities/library.entity';
 import { SharedLinkEntity } from 'src/entities/shared-link.entity';
-import { SmartInfoEntity } from 'src/entities/smart-info.entity';
 import { SmartSearchEntity } from 'src/entities/smart-search.entity';
 import { StackEntity } from 'src/entities/stack.entity';
 import { TagEntity } from 'src/entities/tag.entity';
 import { UserEntity } from 'src/entities/user.entity';
-import { AssetType } from 'src/enum';
+import { AssetStatus, AssetType } from 'src/enum';
 import {
   Column,
   CreateDateColumn,
@@ -70,6 +69,9 @@ export class AssetEntity {
   @Column()
   type!: AssetType;
 
+  @Column({ type: 'enum', enum: AssetStatus, default: AssetStatus.ACTIVE })
+  status!: AssetStatus;
+
   @Column()
   originalPath!: string;
 
@@ -140,9 +142,6 @@ export class AssetEntity {
   @OneToOne(() => ExifEntity, (exifEntity) => exifEntity.asset)
   exifInfo?: ExifEntity;
 
-  @OneToOne(() => SmartInfoEntity, (smartInfoEntity) => smartInfoEntity.asset)
-  smartInfo?: SmartInfoEntity;
-
   @OneToOne(() => SmartSearchEntity, (smartSearchEntity) => smartSearchEntity.asset)
   smartSearch?: SmartSearchEntity;
 
diff --git a/server/src/entities/geodata-places.entity.ts b/server/src/entities/geodata-places.entity.ts
index 966a50d5c9..eb32d1b99b 100644
--- a/server/src/entities/geodata-places.entity.ts
+++ b/server/src/entities/geodata-places.entity.ts
@@ -14,13 +14,42 @@ export class GeodataPlacesEntity {
   @Column({ type: 'float' })
   latitude!: number;
 
-  // @Column({
-  //   generatedType: 'STORED',
-  //   asExpression: 'll_to_earth((latitude)::double precision, (longitude)::double precision)',
-  //   type: 'earth',
-  // })
-  // earthCoord!: unknown;
-
+  @Column({ type: 'char', length: 2 })
+  countryCode!: string;
+
+  @Column({ type: 'varchar', length: 20, nullable: true })
+  admin1Code!: string;
+
+  @Column({ type: 'varchar', length: 80, nullable: true })
+  admin2Code!: string;
+
+  @Column({ type: 'varchar', nullable: true })
+  admin1Name!: string;
+
+  @Column({ type: 'varchar', nullable: true })
+  admin2Name!: string;
+
+  @Column({ type: 'varchar', nullable: true })
+  alternateNames!: string;
+
+  @Column({ type: 'date' })
+  modificationDate!: Date;
+}
+
+@Entity('geodata_places_tmp', { synchronize: false })
+export class GeodataPlacesTempEntity {
+  @PrimaryColumn({ type: 'integer' })
+  id!: number;
+
+  @Column({ type: 'varchar', length: 200 })
+  name!: string;
+
+  @Column({ type: 'float' })
+  longitude!: number;
+
+  @Column({ type: 'float' })
+  latitude!: number;
+
   @Column({ type: 'char', length: 2 })
   countryCode!: string;
 
diff --git a/server/src/entities/index.ts b/server/src/entities/index.ts
index 0b7ca8c3bd..75e92038ac 100644
--- a/server/src/entities/index.ts
+++ b/server/src/entities/index.ts
@@ -18,13 +18,13 @@ import { PartnerEntity } from 'src/entities/partner.entity';
 import { PersonEntity } from 'src/entities/person.entity';
 import { SessionEntity } from 'src/entities/session.entity';
 import { SharedLinkEntity } from 'src/entities/shared-link.entity';
-import { SmartInfoEntity } from 'src/entities/smart-info.entity';
 import { SmartSearchEntity } from 'src/entities/smart-search.entity';
 import { StackEntity } from 'src/entities/stack.entity';
 import { SystemMetadataEntity } from 'src/entities/system-metadata.entity';
 import { TagEntity } from 'src/entities/tag.entity';
 import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
 import { UserEntity } from 'src/entities/user.entity';
+import { VersionHistoryEntity } from 'src/entities/version-history.entity';
 
 export const entities = [
   ActivityEntity,
@@ -45,7 +45,6 @@ export const entities = [
   PartnerEntity,
   PersonEntity,
   SharedLinkEntity,
-  SmartInfoEntity,
   SmartSearchEntity,
   StackEntity,
   SystemMetadataEntity,
@@ -54,4 +53,5 @@ export const entities = [
   UserMetadataEntity,
   SessionEntity,
   LibraryEntity,
+  VersionHistoryEntity,
 ];
diff --git a/server/src/entities/move.entity.ts b/server/src/entities/move.entity.ts
index f3dad6b280..5cdef5d22e 100644
--- a/server/src/entities/move.entity.ts
+++ b/server/src/entities/move.entity.ts
@@ -1,3 +1,4 @@
+import { PathType } from 'src/enum';
 import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
 
 @Entity('move_history')
@@ -21,21 +22,3 @@ export class MoveEntity {
   @Column({ type: 'varchar' })
   newPath!: string;
 }
-
-export enum AssetPathType {
-  ORIGINAL = 'original',
-  PREVIEW = 'preview',
-  THUMBNAIL = 'thumbnail',
-  ENCODED_VIDEO = 'encoded_video',
-  SIDECAR = 'sidecar',
-}
-
-export enum PersonPathType {
-  FACE = 'face',
-}
-
-export enum UserPathType {
-  PROFILE = 'profile',
-}
-
-export type PathType = AssetPathType | PersonPathType | UserPathType;
diff --git a/server/src/entities/natural-earth-countries.entity.ts b/server/src/entities/natural-earth-countries.entity.ts
index 19a12fa07b..0f97132045 100644
--- a/server/src/entities/natural-earth-countries.entity.ts
+++ b/server/src/entities/natural-earth-countries.entity.ts
@@ -2,7 +2,25 @@ import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
 
 @Entity('naturalearth_countries', { synchronize: false })
 export class NaturalEarthCountriesEntity {
-  @PrimaryGeneratedColumn()
+  @PrimaryGeneratedColumn('identity', { generatedIdentity: 'ALWAYS' })
+  id!: number;
+
+  @Column({ type: 'varchar', length: 50 })
+  admin!: string;
+
+  @Column({ type: 'varchar', length: 3 })
+  admin_a3!: string;
+
+  @Column({ type: 'varchar', length: 50 })
+  type!: string;
+
+  @Column({ type: 'polygon' })
+  coordinates!: string;
+}
+
+@Entity('naturalearth_countries_tmp', { synchronize: false })
+export class NaturalEarthCountriesTempEntity {
+  @PrimaryGeneratedColumn('identity', { generatedIdentity: 'ALWAYS' })
   id!: number;
 
   @Column({ type: 'varchar', length: 50 })
diff --git a/server/src/entities/smart-info.entity.ts b/server/src/entities/smart-info.entity.ts
deleted file mode 100644
index 86190c174d..0000000000
--- a/server/src/entities/smart-info.entity.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { AssetEntity } from 'src/entities/asset.entity';
-import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm';
-
-@Entity('smart_info', { synchronize: false })
-export class SmartInfoEntity {
-  @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
-  @JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
-  asset?: AssetEntity;
-
-  @PrimaryColumn()
-  assetId!: string;
-
-  @Column({ type: 'text', array: true, nullable: true })
-  tags!: string[] | null;
-
-  @Column({ type: 'text', array: true, nullable: true })
-  objects!: string[] | null;
-}
diff --git a/server/src/entities/system-metadata.entity.ts b/server/src/entities/system-metadata.entity.ts
index 0a238e1da5..0a03a55403 100644
--- a/server/src/entities/system-metadata.entity.ts
+++ b/server/src/entities/system-metadata.entity.ts
@@ -1,5 +1,5 @@
 import { SystemConfig } from 'src/config';
-import { SystemMetadataKey } from 'src/enum';
+import { StorageFolder, SystemMetadataKey } from 'src/enum';
 import { Column, DeepPartial, Entity, PrimaryColumn } from 'typeorm';
 
 @Entity('system_metadata')
@@ -12,7 +12,7 @@ export class SystemMetadataEntity<T extends keyof SystemMetadata = SystemMetadat
 }
 
 export type VersionCheckMetadata = { checkedAt: string; releaseVersion: string };
-export type SystemFlags = { mountFiles: boolean };
+export type SystemFlags = { mountChecks: Record<StorageFolder, boolean> };
 
 export interface SystemMetadata extends Record<SystemMetadataKey, Record<string, any>> {
   [SystemMetadataKey.ADMIN_ONBOARDING]: { isOnboarded: boolean };
@@ -20,6 +20,6 @@ export interface SystemMetadata extends Record<SystemMetadataKey, Record<string,
   [SystemMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: Date };
   [SystemMetadataKey.REVERSE_GEOCODING_STATE]: { lastUpdate?: string; lastImportFileName?: string };
   [SystemMetadataKey.SYSTEM_CONFIG]: DeepPartial<SystemConfig>;
-  [SystemMetadataKey.SYSTEM_FLAGS]: SystemFlags;
+  [SystemMetadataKey.SYSTEM_FLAGS]: DeepPartial<SystemFlags>;
   [SystemMetadataKey.VERSION_CHECK_STATE]: VersionCheckMetadata;
 }
diff --git a/server/src/entities/user.entity.ts b/server/src/entities/user.entity.ts
index 9cacad315b..ea446be390 100644
--- a/server/src/entities/user.entity.ts
+++ b/server/src/entities/user.entity.ts
@@ -67,4 +67,7 @@ export class UserEntity {
 
   @OneToMany(() => UserMetadataEntity, (metadata) => metadata.user)
   metadata!: UserMetadataEntity[];
+
+  @Column({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
+  profileChangedAt!: Date;
 }
diff --git a/server/src/entities/version-history.entity.ts b/server/src/entities/version-history.entity.ts
new file mode 100644
index 0000000000..edccd9aed6
--- /dev/null
+++ b/server/src/entities/version-history.entity.ts
@@ -0,0 +1,13 @@
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm';
+
+@Entity('version_history')
+export class VersionHistoryEntity {
+  @PrimaryGeneratedColumn('uuid')
+  id!: string;
+
+  @CreateDateColumn({ type: 'timestamptz' })
+  createdAt!: Date;
+
+  @Column()
+  version!: string;
+}
diff --git a/server/src/enum.ts b/server/src/enum.ts
index 32254854e4..3440d45cee 100644
--- a/server/src/enum.ts
+++ b/server/src/enum.ts
@@ -1,3 +1,30 @@
+export enum AuthType {
+  PASSWORD = 'password',
+  OAUTH = 'oauth',
+}
+
+export enum ImmichCookie {
+  ACCESS_TOKEN = 'immich_access_token',
+  AUTH_TYPE = 'immich_auth_type',
+  IS_AUTHENTICATED = 'immich_is_authenticated',
+  SHARED_LINK_TOKEN = 'immich_shared_link_token',
+}
+
+export enum ImmichHeader {
+  API_KEY = 'x-api-key',
+  USER_TOKEN = 'x-immich-user-token',
+  SESSION_TOKEN = 'x-immich-session-token',
+  SHARED_LINK_KEY = 'x-immich-share-key',
+  CHECKSUM = 'x-immich-checksum',
+  CID = 'x-immich-cid',
+}
+
+export enum ImmichQuery {
+  SHARED_LINK_KEY = 'key',
+  API_KEY = 'apiKey',
+  SESSION_KEY = 'sessionKey',
+}
+
 export enum AssetType {
   IMAGE = 'IMAGE',
   VIDEO = 'VIDEO',
@@ -148,6 +175,15 @@ export enum SharedLinkType {
   INDIVIDUAL = 'INDIVIDUAL',
 }
 
+export enum StorageFolder {
+  ENCODED_VIDEO = 'encoded-video',
+  LIBRARY = 'library',
+  UPLOAD = 'upload',
+  PROFILE = 'profile',
+  THUMBNAILS = 'thumbs',
+  BACKUPS = 'backups',
+}
+
 export enum SystemMetadataKey {
   REVERSE_GEOCODING_STATE = 'reverse-geocoding-state',
   FACIAL_RECOGNITION_STATE = 'facial-recognition-state',
@@ -182,7 +218,169 @@ export enum UserStatus {
   DELETED = 'deleted',
 }
 
+export enum AssetStatus {
+  ACTIVE = 'active',
+  TRASHED = 'trashed',
+  DELETED = 'deleted',
+}
+
 export enum SourceType {
   MACHINE_LEARNING = 'machine-learning',
   EXIF = 'exif',
 }
+
+export enum ManualJobName {
+  PERSON_CLEANUP = 'person-cleanup',
+  TAG_CLEANUP = 'tag-cleanup',
+  USER_CLEANUP = 'user-cleanup',
+}
+
+export enum AssetPathType {
+  ORIGINAL = 'original',
+  PREVIEW = 'preview',
+  THUMBNAIL = 'thumbnail',
+  ENCODED_VIDEO = 'encoded_video',
+  SIDECAR = 'sidecar',
+}
+
+export enum PersonPathType {
+  FACE = 'face',
+}
+
+export enum UserPathType {
+  PROFILE = 'profile',
+}
+
+export type PathType = AssetPathType | PersonPathType | UserPathType;
+
+export enum TranscodePolicy {
+  ALL = 'all',
+  OPTIMAL = 'optimal',
+  BITRATE = 'bitrate',
+  REQUIRED = 'required',
+  DISABLED = 'disabled',
+}
+
+export enum TranscodeTarget {
+  NONE,
+  AUDIO,
+  VIDEO,
+  ALL,
+}
+
+export enum VideoCodec {
+  H264 = 'h264',
+  HEVC = 'hevc',
+  VP9 = 'vp9',
+  AV1 = 'av1',
+}
+
+export enum AudioCodec {
+  MP3 = 'mp3',
+  AAC = 'aac',
+  LIBOPUS = 'libopus',
+  PCMS16LE = 'pcm_s16le',
+}
+
+export enum VideoContainer {
+  MOV = 'mov',
+  MP4 = 'mp4',
+  OGG = 'ogg',
+  WEBM = 'webm',
+}
+
+export enum TranscodeHWAccel {
+  NVENC = 'nvenc',
+  QSV = 'qsv',
+  VAAPI = 'vaapi',
+  RKMPP = 'rkmpp',
+  DISABLED = 'disabled',
+}
+
+export enum ToneMapping {
+  HABLE = 'hable',
+  MOBIUS = 'mobius',
+  REINHARD = 'reinhard',
+  DISABLED = 'disabled',
+}
+
+export enum CQMode {
+  AUTO = 'auto',
+  CQP = 'cqp',
+  ICQ = 'icq',
+}
+
+export enum Colorspace {
+  SRGB = 'srgb',
+  P3 = 'p3',
+}
+
+export enum ImageFormat {
+  JPEG = 'jpeg',
+  WEBP = 'webp',
+}
+
+export enum LogLevel {
+  VERBOSE = 'verbose',
+  DEBUG = 'debug',
+  LOG = 'log',
+  WARN = 'warn',
+  ERROR = 'error',
+  FATAL = 'fatal',
+}
+
+export enum MetadataKey {
+  AUTH_ROUTE = 'auth_route',
+  ADMIN_ROUTE = 'admin_route',
+  SHARED_ROUTE = 'shared_route',
+  API_KEY_SECURITY = 'api_key',
+  EVENT_CONFIG = 'event_config',
+  JOB_CONFIG = 'job_config',
+  TELEMETRY_ENABLED = 'telemetry_enabled',
+}
+
+export enum RouteKey {
+  ASSET = 'assets',
+  USER = 'users',
+}
+
+export enum CacheControl {
+  PRIVATE_WITH_CACHE = 'private_with_cache',
+  PRIVATE_WITHOUT_CACHE = 'private_without_cache',
+  NONE = 'none',
+}
+
+export enum PaginationMode {
+  LIMIT_OFFSET = 'limit-offset',
+  SKIP_TAKE = 'skip-take',
+}
+
+export enum ImmichEnvironment {
+  DEVELOPMENT = 'development',
+  TESTING = 'testing',
+  PRODUCTION = 'production',
+}
+
+export enum ImmichWorker {
+  API = 'api',
+  MICROSERVICES = 'microservices',
+}
+
+export enum ImmichTelemetry {
+  HOST = 'host',
+  API = 'api',
+  IO = 'io',
+  REPO = 'repo',
+  JOB = 'job',
+}
+
+export enum ExifOrientation {
+  Horizontal = 1,
+  MirrorHorizontal = 2,
+  Rotate180 = 3,
+  MirrorVertical = 4,
+  MirrorHorizontalRotate270CW = 5,
+  Rotate90CW = 6,
+  MirrorHorizontalRotate90CW = 7,
+  Rotate270CW = 8,
+}
diff --git a/server/src/interfaces/album.interface.ts b/server/src/interfaces/album.interface.ts
index 091442ff05..24c64bdc9d 100644
--- a/server/src/interfaces/album.interface.ts
+++ b/server/src/interfaces/album.interface.ts
@@ -16,18 +16,15 @@ export interface AlbumInfoOptions {
 
 export interface IAlbumRepository extends IBulkAsset {
   getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null>;
-  getByIds(ids: string[]): Promise<AlbumEntity[]>;
   getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>;
   removeAsset(assetId: string): Promise<void>;
   getMetadataForIds(ids: string[]): Promise<AlbumAssetCount[]>;
-  getInvalidThumbnail(): Promise<string[]>;
   getOwned(ownerId: string): Promise<AlbumEntity[]>;
   getShared(ownerId: string): Promise<AlbumEntity[]>;
   getNotShared(ownerId: string): Promise<AlbumEntity[]>;
   restoreAll(userId: string): Promise<void>;
   softDeleteAll(userId: string): Promise<void>;
   deleteAll(userId: string): Promise<void>;
-  getAll(): Promise<AlbumEntity[]>;
   create(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
   update(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
   delete(id: string): Promise<void>;
diff --git a/server/src/interfaces/asset.interface.ts b/server/src/interfaces/asset.interface.ts
index 0d37b64ebb..b25e42ba0e 100644
--- a/server/src/interfaces/asset.interface.ts
+++ b/server/src/interfaces/asset.interface.ts
@@ -1,7 +1,7 @@
 import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { ExifEntity } from 'src/entities/exif.entity';
-import { AssetFileType, AssetOrder, AssetType } from 'src/enum';
+import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
 import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
 import { Paginated, PaginationOptions } from 'src/utils/pagination';
 import { FindOptionsOrder, FindOptionsRelations, FindOptionsSelect } from 'typeorm';
@@ -28,16 +28,12 @@ export enum WithoutProperty {
   EXIF = 'exif',
   SMART_SEARCH = 'smart-search',
   DUPLICATE = 'duplicate',
-  OBJECT_TAGS = 'object-tags',
   FACES = 'faces',
-  PERSON = 'person',
   SIDECAR = 'sidecar',
 }
 
 export enum WithProperty {
   SIDECAR = 'sidecar',
-  IS_ONLINE = 'isOnline',
-  IS_OFFLINE = 'isOffline',
 }
 
 export enum TimeBucketSize {
@@ -56,6 +52,7 @@ export interface AssetBuilderOptions {
   userIds?: string[];
   withStacked?: boolean;
   exifInfo?: boolean;
+  status?: AssetStatus;
   assetType?: AssetType;
 }
 
@@ -95,7 +92,6 @@ export type AssetWithoutRelations = Omit<
   | 'library'
   | 'exifInfo'
   | 'sharedLinks'
-  | 'smartInfo'
   | 'smartSearch'
   | 'tags'
 >;
@@ -142,13 +138,22 @@ export interface AssetUpdateDuplicateOptions {
   duplicateIds: string[];
 }
 
+export interface UpsertFileOptions {
+  assetId: string;
+  type: AssetFileType;
+  path: string;
+}
+
 export type AssetPathEntity = Pick<AssetEntity, 'id' | 'originalPath' | 'isOffline'>;
 
+export interface DayOfYearAssets {
+  yearsAgo: number;
+  assets: AssetEntity[];
+}
+
 export const IAssetRepository = 'IAssetRepository';
 
 export interface IAssetRepository {
-  getAssetsByOriginalPath(userId: string, partialPath: string): Promise<AssetEntity[]>;
-  getUniqueOriginalPaths(userId: string): Promise<string[]>;
   create(asset: AssetCreate): Promise<AssetEntity>;
   getByIds(
     ids: string[],
@@ -156,7 +161,7 @@ export interface IAssetRepository {
     select?: FindOptionsSelect<AssetEntity>,
   ): Promise<AssetEntity[]>;
   getByIdsWithAllRelations(ids: string[]): Promise<AssetEntity[]>;
-  getByDayOfYear(ownerIds: string[], monthDay: MonthDay): Promise<AssetEntity[]>;
+  getByDayOfYear(ownerIds: string[], monthDay: MonthDay): Promise<DayOfYearAssets[]>;
   getByChecksum(options: { ownerId: string; checksum: Buffer; libraryId?: string }): Promise<AssetEntity | null>;
   getByChecksums(userId: string, checksums: Buffer[]): Promise<AssetEntity[]>;
   getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise<string | undefined>;
@@ -169,16 +174,8 @@ export interface IAssetRepository {
     order?: FindOptionsOrder<AssetEntity>,
   ): Promise<AssetEntity | null>;
   getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>;
-  getWith(
-    pagination: PaginationOptions,
-    property: WithProperty,
-    libraryId?: string,
-    withDeleted?: boolean,
-  ): Paginated<AssetEntity>;
   getRandom(userIds: string[], count: number): Promise<AssetEntity[]>;
-  getFirstAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
   getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
-  getExternalLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated<AssetPathEntity>;
   getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise<AssetEntity | null>;
   deleteAll(ownerId: string): Promise<void>;
   getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>;
@@ -188,8 +185,6 @@ export interface IAssetRepository {
   updateDuplicates(options: AssetUpdateDuplicateOptions): Promise<void>;
   update(asset: AssetUpdateOptions): Promise<void>;
   remove(asset: AssetEntity): Promise<void>;
-  softDeleteAll(ids: string[]): Promise<void>;
-  restoreAll(ids: string[]): Promise<void>;
   findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
   getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
   getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]>;
@@ -197,9 +192,9 @@ export interface IAssetRepository {
   upsertExif(exif: Partial<ExifEntity>): Promise<void>;
   upsertJobStatus(...jobStatus: Partial<AssetJobStatusEntity>[]): Promise<void>;
   getAssetIdByCity(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
-  getAssetIdByTag(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
   getDuplicates(options: AssetBuilderOptions): Promise<AssetEntity[]>;
   getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]>;
   getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]>;
-  upsertFile(options: { assetId: string; type: AssetFileType; path: string }): Promise<void>;
+  upsertFile(file: UpsertFileOptions): Promise<void>;
+  upsertFiles(files: UpsertFileOptions[]): Promise<void>;
 }
diff --git a/server/src/interfaces/config.interface.ts b/server/src/interfaces/config.interface.ts
new file mode 100644
index 0000000000..300b55f27b
--- /dev/null
+++ b/server/src/interfaces/config.interface.ts
@@ -0,0 +1,97 @@
+import { RegisterQueueOptions } from '@nestjs/bullmq';
+import { QueueOptions } from 'bullmq';
+import { RedisOptions } from 'ioredis';
+import { ClsModuleOptions } from 'nestjs-cls';
+import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces';
+import { ImmichEnvironment, ImmichTelemetry, ImmichWorker, LogLevel } from 'src/enum';
+import { DatabaseConnectionParams, VectorExtension } from 'src/interfaces/database.interface';
+import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js';
+
+export const IConfigRepository = 'IConfigRepository';
+
+export interface EnvData {
+  host?: string;
+  port: number;
+  environment: ImmichEnvironment;
+  configFile?: string;
+  logLevel?: LogLevel;
+
+  buildMetadata: {
+    build?: string;
+    buildUrl?: string;
+    buildImage?: string;
+    buildImageUrl?: string;
+    repository?: string;
+    repositoryUrl?: string;
+    sourceRef?: string;
+    sourceCommit?: string;
+    sourceUrl?: string;
+    thirdPartySourceUrl?: string;
+    thirdPartyBugFeatureUrl?: string;
+    thirdPartyDocumentationUrl?: string;
+    thirdPartySupportUrl?: string;
+  };
+
+  bull: {
+    config: QueueOptions;
+    queues: RegisterQueueOptions[];
+  };
+
+  cls: {
+    config: ClsModuleOptions;
+  };
+
+  database: {
+    config: PostgresConnectionOptions & DatabaseConnectionParams;
+    skipMigrations: boolean;
+    vectorExtension: VectorExtension;
+  };
+
+  licensePublicKey: {
+    client: string;
+    server: string;
+  };
+
+  network: {
+    trustedProxies: string[];
+  };
+
+  otel: OpenTelemetryModuleOptions;
+
+  resourcePaths: {
+    lockFile: string;
+    geodata: {
+      dateFile: string;
+      admin1: string;
+      admin2: string;
+      cities500: string;
+      naturalEarthCountriesPath: string;
+    };
+    web: {
+      root: string;
+      indexHtml: string;
+    };
+  };
+
+  redis: RedisOptions;
+
+  telemetry: {
+    apiPort: number;
+    microservicesPort: number;
+    metrics: Set<ImmichTelemetry>;
+  };
+
+  storage: {
+    ignoreMountCheckErrors: boolean;
+  };
+
+  workers: ImmichWorker[];
+
+  noColor: boolean;
+  nodeVersion?: string;
+}
+
+export interface IConfigRepository {
+  getEnv(): EnvData;
+  getWorker(): ImmichWorker | undefined;
+}
diff --git a/server/src/interfaces/cron.interface.ts b/server/src/interfaces/cron.interface.ts
new file mode 100644
index 0000000000..ceb554864a
--- /dev/null
+++ b/server/src/interfaces/cron.interface.ts
@@ -0,0 +1,20 @@
+export const ICronRepository = 'ICronRepository';
+
+type CronBase = {
+  name: string;
+  start?: boolean;
+};
+
+export type CronCreate = CronBase & {
+  expression: string;
+  onTick: () => void;
+};
+
+export type CronUpdate = CronBase & {
+  expression?: string;
+};
+
+export interface ICronRepository {
+  create(cron: CronCreate): void;
+  update(cron: CronUpdate): void;
+}
diff --git a/server/src/interfaces/database.interface.ts b/server/src/interfaces/database.interface.ts
index 51b39b95a8..6a10a92f31 100644
--- a/server/src/interfaces/database.interface.ts
+++ b/server/src/interfaces/database.interface.ts
@@ -7,6 +7,22 @@ export enum DatabaseExtension {
 
 export type VectorExtension = DatabaseExtension.VECTOR | DatabaseExtension.VECTORS;
 
+export type DatabaseConnectionURL = {
+  connectionType: 'url';
+  url: string;
+};
+
+export type DatabaseConnectionParts = {
+  connectionType: 'parts';
+  host: string;
+  port: number;
+  username: string;
+  password: string;
+  database: string;
+};
+
+export type DatabaseConnectionParams = DatabaseConnectionURL | DatabaseConnectionParts;
+
 export enum VectorIndex {
   CLIP = 'clip_index',
   FACE = 'face_index',
@@ -17,9 +33,11 @@ export enum DatabaseLock {
   Migrations = 200,
   SystemFileMounts = 300,
   StorageTemplateMigration = 420,
+  VersionHistory = 500,
   CLIPDimSize = 512,
-  LibraryWatch = 1337,
+  Library = 1337,
   GetSystemConfig = 69,
+  BackupDatabase = 42,
 }
 
 export const EXTENSION_NAMES: Record<DatabaseExtension, string> = {
@@ -47,7 +65,6 @@ export interface IDatabaseRepository {
   getPostgresVersion(): Promise<string>;
   getPostgresVersionRange(): string;
   createExtension(extension: DatabaseExtension): Promise<void>;
-  updateExtension(extension: DatabaseExtension, version?: string): Promise<void>;
   updateVectorExtension(extension: VectorExtension, version?: string): Promise<VectorUpdateResult>;
   reindex(index: VectorIndex): Promise<void>;
   shouldReindex(name: VectorIndex): Promise<boolean>;
diff --git a/server/src/interfaces/event.interface.ts b/server/src/interfaces/event.interface.ts
index eced261dbe..9a9e23cca0 100644
--- a/server/src/interfaces/event.interface.ts
+++ b/server/src/interfaces/event.interface.ts
@@ -1,20 +1,28 @@
+import { ClassConstructor } from 'class-transformer';
 import { SystemConfig } from 'src/config';
 import { AssetResponseDto } from 'src/dtos/asset-response.dto';
 import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
+import { JobItem, QueueName } from 'src/interfaces/job.interface';
 
 export const IEventRepository = 'IEventRepository';
 
-type EmitEventMap = {
+type EventMap = {
   // app events
-  'app.bootstrap': ['api' | 'microservices'];
+  'app.bootstrap': [];
   'app.shutdown': [];
 
+  'config.init': [{ newConfig: SystemConfig }];
   // config events
-  'config.update': [{ newConfig: SystemConfig; oldConfig: SystemConfig }];
+  'config.update': [
+    {
+      newConfig: SystemConfig;
+      oldConfig: SystemConfig;
+    },
+  ];
   'config.validate': [{ newConfig: SystemConfig; oldConfig: SystemConfig }];
 
   // album events
-  'album.update': [{ id: string; updatedBy: string }];
+  'album.update': [{ id: string; recipientIds: string[] }];
   'album.invite': [{ id: string; userId: string }];
 
   // asset events
@@ -27,8 +35,11 @@ type EmitEventMap = {
 
   // asset bulk events
   'assets.trash': [{ assetIds: string[]; userId: string }];
+  'assets.delete': [{ assetIds: string[]; userId: string }];
   'assets.restore': [{ assetIds: string[]; userId: string }];
 
+  'job.start': [QueueName, JobItem];
+
   // session events
   'session.delete': [{ sessionId: string }];
 
@@ -42,69 +53,62 @@ type EmitEventMap = {
 
   // user events
   'user.signup': [{ notify: boolean; id: string; tempPassword?: string }];
+
+  // websocket events
+  'websocket.connect': [{ userId: string }];
 };
 
-export type EmitEvent = keyof EmitEventMap;
-export type EmitHandler<T extends EmitEvent> = (...args: ArgsOf<T>) => Promise<void> | void;
-export type ArgOf<T extends EmitEvent> = EmitEventMap[T][0];
-export type ArgsOf<T extends EmitEvent> = EmitEventMap[T];
+export const serverEvents = ['config.update'] as const;
+export type ServerEvents = (typeof serverEvents)[number];
 
-export enum ClientEvent {
-  UPLOAD_SUCCESS = 'on_upload_success',
-  USER_DELETE = 'on_user_delete',
-  ASSET_DELETE = 'on_asset_delete',
-  ASSET_TRASH = 'on_asset_trash',
-  ASSET_UPDATE = 'on_asset_update',
-  ASSET_HIDDEN = 'on_asset_hidden',
-  ASSET_RESTORE = 'on_asset_restore',
-  ASSET_STACK_UPDATE = 'on_asset_stack_update',
-  PERSON_THUMBNAIL = 'on_person_thumbnail',
-  SERVER_VERSION = 'on_server_version',
-  CONFIG_UPDATE = 'on_config_update',
-  NEW_RELEASE = 'on_new_release',
-  SESSION_DELETE = 'on_session_delete',
-}
+export type EmitEvent = keyof EventMap;
+export type EmitHandler<T extends EmitEvent> = (...args: ArgsOf<T>) => Promise<void> | void;
+export type ArgOf<T extends EmitEvent> = EventMap[T][0];
+export type ArgsOf<T extends EmitEvent> = EventMap[T];
 
 export interface ClientEventMap {
-  [ClientEvent.UPLOAD_SUCCESS]: AssetResponseDto;
-  [ClientEvent.USER_DELETE]: string;
-  [ClientEvent.ASSET_DELETE]: string;
-  [ClientEvent.ASSET_TRASH]: string[];
-  [ClientEvent.ASSET_UPDATE]: AssetResponseDto;
-  [ClientEvent.ASSET_HIDDEN]: string;
-  [ClientEvent.ASSET_RESTORE]: string[];
-  [ClientEvent.ASSET_STACK_UPDATE]: string[];
-  [ClientEvent.PERSON_THUMBNAIL]: string;
-  [ClientEvent.SERVER_VERSION]: ServerVersionResponseDto;
-  [ClientEvent.CONFIG_UPDATE]: Record<string, never>;
-  [ClientEvent.NEW_RELEASE]: ReleaseNotification;
-  [ClientEvent.SESSION_DELETE]: string;
+  on_upload_success: [AssetResponseDto];
+  on_user_delete: [string];
+  on_asset_delete: [string];
+  on_asset_trash: [string[]];
+  on_asset_update: [AssetResponseDto];
+  on_asset_hidden: [string];
+  on_asset_restore: [string[]];
+  on_asset_stack_update: string[];
+  on_person_thumbnail: [string];
+  on_server_version: [ServerVersionResponseDto];
+  on_config_update: [];
+  on_new_release: [ReleaseNotification];
+  on_session_delete: [string];
 }
 
-export enum ServerEvent {
-  CONFIG_UPDATE = 'config.update',
-  WEBSOCKET_CONNECT = 'websocket.connect',
-}
+export type EventItem<T extends EmitEvent> = {
+  event: T;
+  handler: EmitHandler<T>;
+  server: boolean;
+};
 
-export interface ServerEventMap {
-  [ServerEvent.CONFIG_UPDATE]: null;
-  [ServerEvent.WEBSOCKET_CONNECT]: { userId: string };
+export enum BootstrapEventPriority {
+  // Database service should be initialized before anything else, most other services need database access
+  DatabaseService = -200,
+  // Initialise config after other bootstrap services, stop other services from using config on bootstrap
+  SystemConfig = 100,
 }
 
 export interface IEventRepository {
-  on<T extends keyof EmitEventMap>(event: T, handler: EmitHandler<T>): void;
-  emit<T extends keyof EmitEventMap>(event: T, ...args: ArgsOf<T>): Promise<void>;
+  setup(options: { services: ClassConstructor<unknown>[] }): void;
+  emit<T extends keyof EventMap>(event: T, ...args: ArgsOf<T>): Promise<void>;
 
   /**
    * Send to connected clients for a specific user
    */
-  clientSend<E extends keyof ClientEventMap>(event: E, room: string, data: ClientEventMap[E]): void;
+  clientSend<E extends keyof ClientEventMap>(event: E, room: string, ...data: ClientEventMap[E]): void;
   /**
    * Send to all connected clients
    */
-  clientBroadcast<E extends keyof ClientEventMap>(event: E, data: ClientEventMap[E]): void;
+  clientBroadcast<E extends keyof ClientEventMap>(event: E, ...data: ClientEventMap[E]): void;
   /**
-   * Notify listeners in this and connected processes. Subscribe to an event with `@OnServerEvent`
+   * Send to all connected servers
    */
-  serverSend<E extends keyof ServerEventMap>(event: E, data: ServerEventMap[E]): boolean;
+  serverSend<T extends ServerEvents>(event: T, ...args: ArgsOf<T>): void;
 }
diff --git a/server/src/interfaces/job.interface.ts b/server/src/interfaces/job.interface.ts
index a0533fa63f..7976f81302 100644
--- a/server/src/interfaces/job.interface.ts
+++ b/server/src/interfaces/job.interface.ts
@@ -1,3 +1,4 @@
+import { ClassConstructor } from 'class-transformer';
 import { EmailImageAttachment } from 'src/interfaces/notification.interface';
 
 export enum QueueName {
@@ -15,11 +16,15 @@ export enum QueueName {
   SIDECAR = 'sidecar',
   LIBRARY = 'library',
   NOTIFICATION = 'notifications',
+  BACKUP_DATABASE = 'backupDatabase',
 }
 
 export type ConcurrentQueueName = Exclude<
   QueueName,
-  QueueName.STORAGE_TEMPLATE_MIGRATION | QueueName.FACIAL_RECOGNITION | QueueName.DUPLICATE_DETECTION
+  | QueueName.STORAGE_TEMPLATE_MIGRATION
+  | QueueName.FACIAL_RECOGNITION
+  | QueueName.DUPLICATE_DETECTION
+  | QueueName.BACKUP_DATABASE
 >;
 
 export enum JobCommand {
@@ -31,15 +36,16 @@ export enum JobCommand {
 }
 
 export enum JobName {
+  //backups
+  BACKUP_DATABASE = 'database-backup',
+
   // conversion
   QUEUE_VIDEO_CONVERSION = 'queue-video-conversion',
   VIDEO_CONVERSION = 'video-conversion',
 
   // thumbnails
   QUEUE_GENERATE_THUMBNAILS = 'queue-generate-thumbnails',
-  GENERATE_PREVIEW = 'generate-preview',
-  GENERATE_THUMBNAIL = 'generate-thumbnail',
-  GENERATE_THUMBHASH = 'generate-thumbhash',
+  GENERATE_THUMBNAILS = 'generate-thumbnails',
   GENERATE_PERSON_THUMBNAIL = 'generate-person-thumbnail',
 
   // metadata
@@ -60,6 +66,9 @@ export enum JobName {
   STORAGE_TEMPLATE_MIGRATION = 'storage-template-migration',
   STORAGE_TEMPLATE_MIGRATION_SINGLE = 'storage-template-migration-single',
 
+  // tags
+  TAG_CLEANUP = 'tag-cleanup',
+
   // migration
   QUEUE_MIGRATION = 'queue-migration',
   MIGRATE_ASSET = 'migrate-asset',
@@ -73,12 +82,12 @@ export enum JobName {
   FACIAL_RECOGNITION = 'facial-recognition',
 
   // library management
-  LIBRARY_SCAN = 'library-refresh',
-  LIBRARY_SCAN_ASSET = 'library-refresh-asset',
-  LIBRARY_REMOVE_OFFLINE = 'library-remove-offline',
-  LIBRARY_CHECK_OFFLINE = 'library-check-offline',
+  LIBRARY_QUEUE_SYNC_FILES = 'library-queue-sync-files',
+  LIBRARY_QUEUE_SYNC_ASSETS = 'library-queue-sync-assets',
+  LIBRARY_SYNC_FILE = 'library-sync-file',
+  LIBRARY_SYNC_ASSET = 'library-sync-asset',
   LIBRARY_DELETE = 'library-delete',
-  LIBRARY_QUEUE_SCAN_ALL = 'library-queue-all-refresh',
+  LIBRARY_QUEUE_SYNC_ALL = 'library-queue-sync-all',
   LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup',
 
   // cleanup
@@ -90,6 +99,8 @@ export enum JobName {
   QUEUE_SMART_SEARCH = 'queue-smart-search',
   SMART_SEARCH = 'smart-search',
 
+  QUEUE_TRASH_EMPTY = 'queue-trash-empty',
+
   // duplicate detection
   QUEUE_DUPLICATE_DETECTION = 'queue-duplicate-detection',
   DUPLICATE_DETECTION = 'duplicate-detection',
@@ -111,12 +122,17 @@ export enum JobName {
 }
 
 export const JOBS_ASSET_PAGINATION_SIZE = 1000;
-export const JOBS_LIBRARY_PAGINATION_SIZE = 100_000;
+export const JOBS_LIBRARY_PAGINATION_SIZE = 10_000;
 
 export interface IBaseJob {
   force?: boolean;
 }
 
+export interface IDelayedJob extends IBaseJob {
+  /** The minimum time to wait to execute this job, in milliseconds. */
+  delay?: number;
+}
+
 export interface IEntityJob extends IBaseJob {
   id: string;
   source?: 'upload' | 'sidecar-write' | 'copy';
@@ -132,16 +148,11 @@ export interface ILibraryFileJob extends IEntityJob {
   assetPath: string;
 }
 
-export interface ILibraryOfflineJob extends IEntityJob {
+export interface ILibraryAssetJob extends IEntityJob {
   importPaths: string[];
   exclusionPatterns: string[];
 }
 
-export interface ILibraryRefreshJob extends IEntityJob {
-  refreshModifiedFiles: boolean;
-  refreshAllFiles: boolean;
-}
-
 export interface IBulkEntityJob extends IBaseJob {
   ids: string[];
 }
@@ -183,8 +194,8 @@ export interface INotifyAlbumInviteJob extends IEntityJob {
   recipientId: string;
 }
 
-export interface INotifyAlbumUpdateJob extends IEntityJob {
-  senderId: string;
+export interface INotifyAlbumUpdateJob extends IEntityJob, IDelayedJob {
+  recipientIds: string[];
 }
 
 export interface JobCounts {
@@ -206,15 +217,16 @@ export enum QueueCleanType {
 }
 
 export type JobItem =
+  // Backups
+  | { name: JobName.BACKUP_DATABASE; data?: IBaseJob }
+
   // Transcoding
   | { name: JobName.QUEUE_VIDEO_CONVERSION; data: IBaseJob }
   | { name: JobName.VIDEO_CONVERSION; data: IEntityJob }
 
   // Thumbnails
   | { name: JobName.QUEUE_GENERATE_THUMBNAILS; data: IBaseJob }
-  | { name: JobName.GENERATE_PREVIEW; data: IEntityJob }
-  | { name: JobName.GENERATE_THUMBNAIL; data: IEntityJob }
-  | { name: JobName.GENERATE_THUMBHASH; data: IEntityJob }
+  | { name: JobName.GENERATE_THUMBNAILS; data: IEntityJob }
 
   // User
   | { name: JobName.USER_DELETE_CHECK; data?: IBaseJob }
@@ -227,8 +239,8 @@ export type JobItem =
 
   // Migration
   | { name: JobName.QUEUE_MIGRATION; data?: IBaseJob }
-  | { name: JobName.MIGRATE_ASSET; data?: IEntityJob }
-  | { name: JobName.MIGRATE_PERSON; data?: IEntityJob }
+  | { name: JobName.MIGRATE_ASSET; data: IEntityJob }
+  | { name: JobName.MIGRATE_PERSON; data: IEntityJob }
 
   // Metadata Extraction
   | { name: JobName.QUEUE_METADATA_EXTRACTION; data: IBaseJob }
@@ -250,6 +262,7 @@ export type JobItem =
   // Smart Search
   | { name: JobName.QUEUE_SMART_SEARCH; data: IBaseJob }
   | { name: JobName.SMART_SEARCH; data: IEntityJob }
+  | { name: JobName.QUEUE_TRASH_EMPTY; data?: IBaseJob }
 
   // Duplicate Detection
   | { name: JobName.QUEUE_DUPLICATE_DETECTION; data: IBaseJob }
@@ -262,18 +275,21 @@ export type JobItem =
   | { name: JobName.CLEAN_OLD_AUDIT_LOGS; data?: IBaseJob }
   | { name: JobName.CLEAN_OLD_SESSION_TOKENS; data?: IBaseJob }
 
+  // Tags
+  | { name: JobName.TAG_CLEANUP; data?: IBaseJob }
+
   // Asset Deletion
   | { name: JobName.PERSON_CLEANUP; data?: IBaseJob }
   | { name: JobName.ASSET_DELETION; data: IAssetDeleteJob }
   | { name: JobName.ASSET_DELETION_CHECK; data?: IBaseJob }
 
   // Library Management
-  | { name: JobName.LIBRARY_SCAN_ASSET; data: ILibraryFileJob }
-  | { name: JobName.LIBRARY_SCAN; data: ILibraryRefreshJob }
-  | { name: JobName.LIBRARY_REMOVE_OFFLINE; data: IEntityJob }
+  | { name: JobName.LIBRARY_SYNC_FILE; data: ILibraryFileJob }
+  | { name: JobName.LIBRARY_QUEUE_SYNC_FILES; data: IEntityJob }
+  | { name: JobName.LIBRARY_QUEUE_SYNC_ASSETS; data: IEntityJob }
+  | { name: JobName.LIBRARY_SYNC_ASSET; data: ILibraryAssetJob }
   | { name: JobName.LIBRARY_DELETE; data: IEntityJob }
-  | { name: JobName.LIBRARY_QUEUE_SCAN_ALL; data: IBaseJob }
-  | { name: JobName.LIBRARY_CHECK_OFFLINE; data: IEntityJob }
+  | { name: JobName.LIBRARY_QUEUE_SYNC_ALL; data?: IBaseJob }
   | { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob }
 
   // Notification
@@ -290,17 +306,15 @@ export enum JobStatus {
   FAILED = 'failed',
   SKIPPED = 'skipped',
 }
-
-export type JobHandler<T = any> = (data: T) => Promise<JobStatus>;
-export type JobItemHandler = (item: JobItem) => Promise<void>;
+export type Jobs = { [K in JobItem['name']]: (JobItem & { name: K })['data'] };
+export type JobOf<T extends JobName> = Jobs[T];
 
 export const IJobRepository = 'IJobRepository';
 
 export interface IJobRepository {
-  addHandler(queueName: QueueName, concurrency: number, handler: JobItemHandler): void;
-  addCronJob(name: string, expression: string, onTick: () => void, start?: boolean): void;
-  updateCronJob(name: string, expression?: string, start?: boolean): void;
-  deleteCronJob(name: string): void;
+  setup(options: { services: ClassConstructor<unknown>[] }): void;
+  startWorkers(): void;
+  run(job: JobItem): Promise<JobStatus>;
   setConcurrency(queueName: QueueName, concurrency: number): void;
   queue(item: JobItem): Promise<void>;
   queueAll(items: JobItem[]): Promise<void>;
@@ -311,4 +325,5 @@ export interface IJobRepository {
   getQueueStatus(name: QueueName): Promise<QueueStatus>;
   getJobCounts(name: QueueName): Promise<JobCounts>;
   waitForQueueCompletion(...queues: QueueName[]): Promise<void>;
+  removeJob(jobId: string, name: JobName): Promise<IEntityJob | undefined>;
 }
diff --git a/server/src/interfaces/logger.interface.ts b/server/src/interfaces/logger.interface.ts
index f0afdce2a5..92984bf8e1 100644
--- a/server/src/interfaces/logger.interface.ts
+++ b/server/src/interfaces/logger.interface.ts
@@ -1,11 +1,12 @@
-import { LogLevel } from 'src/config';
+import { ImmichWorker, LogLevel } from 'src/enum';
 
 export const ILoggerRepository = 'ILoggerRepository';
 
 export interface ILoggerRepository {
-  setAppName(name: string): void;
+  setAppName(name: ImmichWorker): void;
   setContext(message: string): void;
-  setLogLevel(level: LogLevel): void;
+  setLogLevel(level: LogLevel | false): void;
+  isLevelEnabled(level: LogLevel): boolean;
 
   verbose(message: any, ...args: any): void;
   debug(message: any, ...args: any): void;
diff --git a/server/src/interfaces/machine-learning.interface.ts b/server/src/interfaces/machine-learning.interface.ts
index 5342030c8f..372aa0c7cd 100644
--- a/server/src/interfaces/machine-learning.interface.ts
+++ b/server/src/interfaces/machine-learning.interface.ts
@@ -51,7 +51,7 @@ export type DetectedFaces = { faces: Face[] } & VisualResponse;
 export type MachineLearningRequest = ClipVisualRequest | ClipTextualRequest | FacialRecognitionRequest;
 
 export interface IMachineLearningRepository {
-  encodeImage(url: string, imagePath: string, config: ModelOptions): Promise<number[]>;
-  encodeText(url: string, text: string, config: ModelOptions): Promise<number[]>;
-  detectFaces(url: string, imagePath: string, config: FaceDetectionOptions): Promise<DetectedFaces>;
+  encodeImage(urls: string[], imagePath: string, config: ModelOptions): Promise<number[]>;
+  encodeText(urls: string[], text: string, config: ModelOptions): Promise<number[]>;
+  detectFaces(urls: string[], imagePath: string, config: FaceDetectionOptions): Promise<DetectedFaces>;
 }
diff --git a/server/src/interfaces/map.interface.ts b/server/src/interfaces/map.interface.ts
index 80b37c3a5f..0a04840a96 100644
--- a/server/src/interfaces/map.interface.ts
+++ b/server/src/interfaces/map.interface.ts
@@ -28,5 +28,4 @@ export interface IMapRepository {
   init(): Promise<void>;
   reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult>;
   getMapMarkers(ownerIds: string[], albumIds: string[], options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
-  fetchStyle(url: string): Promise<any>;
 }
diff --git a/server/src/interfaces/media.interface.ts b/server/src/interfaces/media.interface.ts
index f7389d3d06..b90dfb483c 100644
--- a/server/src/interfaces/media.interface.ts
+++ b/server/src/interfaces/media.interface.ts
@@ -1,5 +1,5 @@
 import { Writable } from 'node:stream';
-import { ImageFormat, TranscodeTarget, VideoCodec } from 'src/config';
+import { ExifOrientation, ImageFormat, TranscodeTarget, VideoCodec } from 'src/enum';
 
 export const IMediaRepository = 'IMediaRepository';
 
@@ -10,13 +10,45 @@ export interface CropOptions {
   height: number;
 }
 
-export interface ThumbnailOptions {
-  size: number;
+export interface ImageOptions {
   format: ImageFormat;
-  colorspace: string;
   quality: number;
+  size: number;
+}
+
+export interface RawImageInfo {
+  width: number;
+  height: number;
+  channels: 1 | 2 | 3 | 4;
+}
+
+interface DecodeImageOptions {
+  colorspace: string;
   crop?: CropOptions;
   processInvalidImages: boolean;
+  raw?: RawImageInfo;
+}
+
+export interface DecodeToBufferOptions extends DecodeImageOptions {
+  size: number;
+  orientation?: ExifOrientation;
+}
+
+export type GenerateThumbnailOptions = ImageOptions & DecodeImageOptions;
+
+export type GenerateThumbnailFromBufferOptions = GenerateThumbnailOptions & { raw: RawImageInfo };
+
+export type GenerateThumbhashOptions = DecodeImageOptions;
+
+export type GenerateThumbhashFromBufferOptions = GenerateThumbhashOptions & { raw: RawImageInfo };
+
+export interface GenerateThumbnailsOptions {
+  colorspace: string;
+  crop?: CropOptions;
+  preview?: ImageOptions;
+  processInvalidImages: boolean;
+  thumbhash?: boolean;
+  thumbnail?: ImageOptions;
 }
 
 export interface VideoStreamInfo {
@@ -28,6 +60,7 @@ export interface VideoStreamInfo {
   frameCount: number;
   isHDR: boolean;
   bitrate: number;
+  pixelFormat: string;
 }
 
 export interface AudioStreamInfo {
@@ -62,6 +95,10 @@ export interface TranscodeCommand {
   inputOptions: string[];
   outputOptions: string[];
   twoPass: boolean;
+  progress: {
+    frameCount: number;
+    percentInterval: number;
+  };
 }
 
 export interface BitrateDistribution {
@@ -71,22 +108,44 @@ export interface BitrateDistribution {
   unit: string;
 }
 
+export interface ImageBuffer {
+  data: Buffer;
+  info: RawImageInfo;
+}
+
 export interface VideoCodecSWConfig {
-  getCommand(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream: AudioStreamInfo): TranscodeCommand;
+  getCommand(
+    target: TranscodeTarget,
+    videoStream: VideoStreamInfo,
+    audioStream: AudioStreamInfo,
+    format?: VideoFormat,
+  ): TranscodeCommand;
 }
 
 export interface VideoCodecHWConfig extends VideoCodecSWConfig {
   getSupportedCodecs(): Array<VideoCodec>;
 }
 
+export interface ProbeOptions {
+  countFrames: boolean;
+}
+
+export interface VideoInterfaces {
+  dri: string[];
+  mali: boolean;
+}
+
 export interface IMediaRepository {
   // image
   extract(input: string, output: string): Promise<boolean>;
-  generateThumbnail(input: string | Buffer, output: string, options: ThumbnailOptions): Promise<void>;
-  generateThumbhash(imagePath: string): Promise<Buffer>;
+  decodeImage(input: string, options: DecodeToBufferOptions): Promise<ImageBuffer>;
+  generateThumbnail(input: string, options: GenerateThumbnailOptions, outputFile: string): Promise<void>;
+  generateThumbnail(input: Buffer, options: GenerateThumbnailFromBufferOptions, outputFile: string): Promise<void>;
+  generateThumbhash(input: string, options: GenerateThumbhashOptions): Promise<Buffer>;
+  generateThumbhash(input: Buffer, options: GenerateThumbhashFromBufferOptions): Promise<Buffer>;
   getImageDimensions(input: string): Promise<ImageDimensions>;
 
   // video
-  probe(input: string): Promise<VideoInfo>;
+  probe(input: string, options?: ProbeOptions): Promise<VideoInfo>;
   transcode(input: string, output: string | Writable, command: TranscodeCommand): Promise<void>;
 }
diff --git a/server/src/interfaces/metadata.interface.ts b/server/src/interfaces/metadata.interface.ts
index 39ff6ab4af..574420e27a 100644
--- a/server/src/interfaces/metadata.interface.ts
+++ b/server/src/interfaces/metadata.interface.ts
@@ -7,7 +7,18 @@ export interface ExifDuration {
   Scale?: number;
 }
 
-type TagsWithWrongTypes = 'FocalLength' | 'Duration' | 'Description' | 'ImageDescription' | 'RegionInfo';
+type StringOrNumber = string | number;
+
+type TagsWithWrongTypes =
+  | 'FocalLength'
+  | 'Duration'
+  | 'Description'
+  | 'ImageDescription'
+  | 'RegionInfo'
+  | 'TagsList'
+  | 'Keywords'
+  | 'HierarchicalSubject'
+  | 'ISO';
 export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
   ContentIdentifier?: string;
   MotionPhoto?: number;
@@ -20,10 +31,14 @@ export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
   EmbeddedVideoType?: string;
   EmbeddedVideoFile?: BinaryField;
   MotionPhotoVideo?: BinaryField;
+  TagsList?: StringOrNumber[];
+  HierarchicalSubject?: StringOrNumber[];
+  Keywords?: StringOrNumber | StringOrNumber[];
+  ISO?: number | number[];
 
   // Type is wrong, can also be number.
-  Description?: string | number;
-  ImageDescription?: string | number;
+  Description?: StringOrNumber;
+  ImageDescription?: StringOrNumber;
 
   // Extended properties for image regions, such as faces
   RegionInfo?: {
@@ -53,9 +68,4 @@ export interface IMetadataRepository {
   readTags(path: string): Promise<ImmichTags>;
   writeTags(path: string, tags: Partial<Tags>): Promise<void>;
   extractBinaryTag(tagName: string, path: string): Promise<Buffer>;
-  getCountries(userIds: string[]): Promise<Array<string | null>>;
-  getStates(userIds: string[], country?: string): Promise<Array<string | null>>;
-  getCities(userIds: string[], country?: string, state?: string): Promise<Array<string | null>>;
-  getCameraMakes(userIds: string[], model?: string): Promise<Array<string | null>>;
-  getCameraModels(userIds: string[], make?: string): Promise<Array<string | null>>;
 }
diff --git a/server/src/interfaces/move.interface.ts b/server/src/interfaces/move.interface.ts
index c9d39e78cf..0e79cfcadc 100644
--- a/server/src/interfaces/move.interface.ts
+++ b/server/src/interfaces/move.interface.ts
@@ -1,4 +1,5 @@
-import { MoveEntity, PathType } from 'src/entities/move.entity';
+import { MoveEntity } from 'src/entities/move.entity';
+import { PathType } from 'src/enum';
 
 export const IMoveRepository = 'IMoveRepository';
 
diff --git a/server/src/interfaces/notification.interface.ts b/server/src/interfaces/notification.interface.ts
index ec0ecc534b..b20b3c50ae 100644
--- a/server/src/interfaces/notification.interface.ts
+++ b/server/src/interfaces/notification.interface.ts
@@ -39,6 +39,7 @@ export enum EmailTemplate {
 
 interface BaseEmailProps {
   baseUrl: string;
+  customTemplate?: string;
 }
 
 export interface TestEmailProps extends BaseEmailProps {
@@ -70,18 +71,22 @@ export type EmailRenderRequest =
   | {
       template: EmailTemplate.TEST_EMAIL;
       data: TestEmailProps;
+      customTemplate: string;
     }
   | {
       template: EmailTemplate.WELCOME;
       data: WelcomeEmailProps;
+      customTemplate: string;
     }
   | {
       template: EmailTemplate.ALBUM_INVITE;
       data: AlbumInviteEmailProps;
+      customTemplate: string;
     }
   | {
       template: EmailTemplate.ALBUM_UPDATE;
       data: AlbumUpdateEmailProps;
+      customTemplate: string;
     };
 
 export type SendEmailResponse = {
diff --git a/server/src/interfaces/oauth.interface.ts b/server/src/interfaces/oauth.interface.ts
new file mode 100644
index 0000000000..5e629726a0
--- /dev/null
+++ b/server/src/interfaces/oauth.interface.ts
@@ -0,0 +1,22 @@
+import { UserinfoResponse } from 'openid-client';
+
+export const IOAuthRepository = 'IOAuthRepository';
+
+export type OAuthConfig = {
+  clientId: string;
+  clientSecret: string;
+  issuerUrl: string;
+  mobileOverrideEnabled: boolean;
+  mobileRedirectUri: string;
+  profileSigningAlgorithm: string;
+  scope: string;
+  signingAlgorithm: string;
+};
+export type OAuthProfile = UserinfoResponse;
+
+export interface IOAuthRepository {
+  init(): void;
+  authorize(config: OAuthConfig, redirectUrl: string): Promise<string>;
+  getLogoutEndpoint(config: OAuthConfig): Promise<string | undefined>;
+  getProfile(config: OAuthConfig, url: string, redirectUrl: string): Promise<OAuthProfile>;
+}
diff --git a/server/src/interfaces/person.interface.ts b/server/src/interfaces/person.interface.ts
index 5708274a6e..dc89f5c1b0 100644
--- a/server/src/interfaces/person.interface.ts
+++ b/server/src/interfaces/person.interface.ts
@@ -1,6 +1,7 @@
 import { AssetFaceEntity } from 'src/entities/asset-face.entity';
-import { AssetEntity } from 'src/entities/asset.entity';
+import { FaceSearchEntity } from 'src/entities/face-search.entity';
 import { PersonEntity } from 'src/entities/person.entity';
+import { SourceType } from 'src/enum';
 import { Paginated, PaginationOptions } from 'src/utils/pagination';
 import { FindManyOptions, FindOptionsRelations, FindOptionsSelect } from 'typeorm';
 
@@ -9,6 +10,7 @@ export const IPersonRepository = 'IPersonRepository';
 export interface PersonSearchOptions {
   minimumFaceCount: number;
   withHidden: boolean;
+  closestFaceAssetId?: string;
 }
 
 export interface PersonNameSearchOptions {
@@ -40,10 +42,12 @@ export interface PeopleStatistics {
   hidden: number;
 }
 
-export interface DeleteAllFacesOptions {
-  sourceType?: string;
+export interface DeleteFacesOptions {
+  sourceType: SourceType;
 }
 
+export type UnassignFacesOptions = DeleteFacesOptions;
+
 export interface IPersonRepository {
   getAll(pagination: PaginationOptions, options?: FindManyOptions<PersonEntity>): Paginated<PersonEntity>;
   getAllForUser(pagination: PaginationOptions, userId: string, options: PersonSearchOptions): Paginated<PersonEntity>;
@@ -52,15 +56,15 @@ export interface IPersonRepository {
   getByName(userId: string, personName: string, options: PersonNameSearchOptions): Promise<PersonEntity[]>;
   getDistinctNames(userId: string, options: PersonNameSearchOptions): Promise<PersonNameResponse[]>;
 
-  getAssets(personId: string): Promise<AssetEntity[]>;
-
   create(person: Partial<PersonEntity>): Promise<PersonEntity>;
   createAll(people: Partial<PersonEntity>[]): Promise<string[]>;
-  createFaces(entities: Partial<AssetFaceEntity>[]): Promise<string[]>;
   delete(entities: PersonEntity[]): Promise<void>;
-  deleteAll(): Promise<void>;
-  deleteAllFaces(options: DeleteAllFacesOptions): Promise<void>;
-  replaceFaces(assetId: string, entities: Partial<AssetFaceEntity>[], sourceType?: string): Promise<string[]>;
+  deleteFaces(options: DeleteFacesOptions): Promise<void>;
+  refreshFaces(
+    facesToAdd: Partial<AssetFaceEntity>[],
+    faceIdsToRemove: string[],
+    embeddingsToAdd?: FaceSearchEntity[],
+  ): Promise<void>;
   getAllFaces(pagination: PaginationOptions, options?: FindManyOptions<AssetFaceEntity>): Paginated<AssetFaceEntity>;
   getFaceById(id: string): Promise<AssetFaceEntity>;
   getFaceByIdWithAssets(
@@ -75,6 +79,7 @@ export interface IPersonRepository {
   reassignFace(assetFaceId: string, newPersonId: string): Promise<number>;
   getNumberOfPeople(userId: string): Promise<PeopleStatistics>;
   reassignFaces(data: UpdateFacesData): Promise<number>;
+  unassignFaces(options: UnassignFacesOptions): Promise<void>;
   update(person: Partial<PersonEntity>): Promise<PersonEntity>;
   updateAll(people: Partial<PersonEntity>[]): Promise<void>;
   getLatestFaceDate(): Promise<string | undefined>;
diff --git a/server/src/interfaces/process.interface.ts b/server/src/interfaces/process.interface.ts
new file mode 100644
index 0000000000..14a8c1ff33
--- /dev/null
+++ b/server/src/interfaces/process.interface.ts
@@ -0,0 +1,25 @@
+import { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio } from 'node:child_process';
+import { Readable } from 'node:stream';
+
+export interface ImmichReadStream {
+  stream: Readable;
+  type?: string;
+  length?: number;
+}
+
+export interface ImmichZipStream extends ImmichReadStream {
+  addFile: (inputPath: string, filename: string) => void;
+  finalize: () => Promise<void>;
+}
+
+export interface DiskUsage {
+  available: number;
+  free: number;
+  total: number;
+}
+
+export const IProcessRepository = 'IProcessRepository';
+
+export interface IProcessRepository {
+  spawn(command: string, args?: readonly string[], options?: SpawnOptionsWithoutStdio): ChildProcessWithoutNullStreams;
+}
diff --git a/server/src/interfaces/search.interface.ts b/server/src/interfaces/search.interface.ts
index 0226e3663c..d59291c883 100644
--- a/server/src/interfaces/search.interface.ts
+++ b/server/src/interfaces/search.interface.ts
@@ -1,7 +1,7 @@
 import { AssetFaceEntity } from 'src/entities/asset-face.entity';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
-import { AssetType } from 'src/enum';
+import { AssetStatus, AssetType } from 'src/enum';
 import { Paginated } from 'src/utils/pagination';
 
 export const ISearchRepository = 'ISearchRepository';
@@ -61,13 +61,13 @@ export interface SearchStatusOptions {
   isVisible?: boolean;
   isNotInAlbum?: boolean;
   type?: AssetType;
+  status?: AssetStatus;
   withArchived?: boolean;
   withDeleted?: boolean;
 }
 
 export interface SearchOneToOneRelationOptions {
   withExif?: boolean;
-  withSmartInfo?: boolean;
   withStacked?: boolean;
 }
 
@@ -170,15 +170,37 @@ export interface AssetDuplicateResult {
   distance: number;
 }
 
+export interface GetStatesOptions {
+  country?: string;
+}
+
+export interface GetCitiesOptions extends GetStatesOptions {
+  state?: string;
+}
+
+export interface GetCameraModelsOptions {
+  make?: string;
+}
+
+export interface GetCameraMakesOptions {
+  model?: string;
+}
+
 export interface ISearchRepository {
   searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated<AssetEntity>;
   searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated<AssetEntity>;
   searchDuplicates(options: AssetDuplicateSearch): Promise<AssetDuplicateResult[]>;
   searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
+  searchRandom(size: number, options: AssetSearchOptions): Promise<AssetEntity[]>;
   upsert(assetId: string, embedding: number[]): Promise<void>;
   searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]>;
   getAssetsByCity(userIds: string[]): Promise<AssetEntity[]>;
   deleteAllSearchEmbeddings(): Promise<void>;
   getDimensionSize(): Promise<number>;
   setDimensionSize(dimSize: number): Promise<void>;
+  getCountries(userIds: string[]): Promise<Array<string | null>>;
+  getStates(userIds: string[], options: GetStatesOptions): Promise<Array<string | null>>;
+  getCities(userIds: string[], options: GetCitiesOptions): Promise<Array<string | null>>;
+  getCameraMakes(userIds: string[], options: GetCameraMakesOptions): Promise<Array<string | null>>;
+  getCameraModels(userIds: string[], options: GetCameraModelsOptions): Promise<Array<string | null>>;
 }
diff --git a/server/src/interfaces/storage.interface.ts b/server/src/interfaces/storage.interface.ts
index fec3d66dd5..b304d94fef 100644
--- a/server/src/interfaces/storage.interface.ts
+++ b/server/src/interfaces/storage.interface.ts
@@ -1,7 +1,7 @@
 import { WatchOptions } from 'chokidar';
 import { Stats } from 'node:fs';
 import { FileReadOptions } from 'node:fs/promises';
-import { Readable } from 'node:stream';
+import { Readable, Writable } from 'node:stream';
 import { CrawlOptionsDto, WalkOptionsDto } from 'src/dtos/library.dto';
 
 export interface ImmichReadStream {
@@ -35,7 +35,10 @@ export interface IStorageRepository {
   createZipStream(): ImmichZipStream;
   createReadStream(filepath: string, mimeType?: string | null): Promise<ImmichReadStream>;
   readFile(filepath: string, options?: FileReadOptions<Buffer>): Promise<Buffer>;
-  writeFile(filepath: string, buffer: Buffer): Promise<void>;
+  createFile(filepath: string, buffer: Buffer): Promise<void>;
+  createWriteStream(filepath: string): Writable;
+  createOrOverwriteFile(filepath: string, buffer: Buffer): Promise<void>;
+  overwriteFile(filepath: string, buffer: Buffer): Promise<void>;
   realpath(filepath: string): Promise<string>;
   unlink(filepath: string): Promise<void>;
   unlinkDir(folder: string, options?: { recursive?: boolean; force?: boolean }): Promise<void>;
diff --git a/server/src/interfaces/tag.interface.ts b/server/src/interfaces/tag.interface.ts
index aca9c223d5..16a34d6ac4 100644
--- a/server/src/interfaces/tag.interface.ts
+++ b/server/src/interfaces/tag.interface.ts
@@ -17,4 +17,5 @@ export interface ITagRepository extends IBulkAsset {
 
   upsertAssetTags({ assetId, tagIds }: { assetId: string; tagIds: string[] }): Promise<void>;
   upsertAssetIds(items: AssetTagItem[]): Promise<AssetTagItem[]>;
+  deleteEmptyTags(): Promise<void>;
 }
diff --git a/server/src/interfaces/metric.interface.ts b/server/src/interfaces/telemetry.interface.ts
similarity index 71%
rename from server/src/interfaces/metric.interface.ts
rename to server/src/interfaces/telemetry.interface.ts
index a87a849833..688e52c21e 100644
--- a/server/src/interfaces/metric.interface.ts
+++ b/server/src/interfaces/telemetry.interface.ts
@@ -1,6 +1,7 @@
 import { MetricOptions } from '@opentelemetry/api';
+import { ClassConstructor } from 'class-transformer';
 
-export const IMetricRepository = 'IMetricRepository';
+export const ITelemetryRepository = 'ITelemetryRepository';
 
 export interface MetricGroupOptions {
   enabled: boolean;
@@ -13,7 +14,8 @@ export interface IMetricGroupRepository {
   configure(options: MetricGroupOptions): this;
 }
 
-export interface IMetricRepository {
+export interface ITelemetryRepository {
+  setup(options: { repositories: ClassConstructor<unknown>[] }): void;
   api: IMetricGroupRepository;
   host: IMetricGroupRepository;
   jobs: IMetricGroupRepository;
diff --git a/server/src/interfaces/trash.interface.ts b/server/src/interfaces/trash.interface.ts
new file mode 100644
index 0000000000..96c2322d8a
--- /dev/null
+++ b/server/src/interfaces/trash.interface.ts
@@ -0,0 +1,10 @@
+import { Paginated, PaginationOptions } from 'src/utils/pagination';
+
+export const ITrashRepository = 'ITrashRepository';
+
+export interface ITrashRepository {
+  empty(userId: string): Promise<number>;
+  restore(userId: string): Promise<number>;
+  restoreAll(assetIds: string[]): Promise<number>;
+  getDeletedIds(pagination: PaginationOptions): Paginated<string>;
+}
diff --git a/server/src/interfaces/user.interface.ts b/server/src/interfaces/user.interface.ts
index 3353d45dce..385a4d3d50 100644
--- a/server/src/interfaces/user.interface.ts
+++ b/server/src/interfaces/user.interface.ts
@@ -11,6 +11,8 @@ export interface UserStatsQueryResponse {
   photos: number;
   videos: number;
   usage: number;
+  usagePhotos: number;
+  usageVideos: number;
   quotaSizeInBytes: number | null;
 }
 
diff --git a/server/src/interfaces/version-history.interface.ts b/server/src/interfaces/version-history.interface.ts
new file mode 100644
index 0000000000..6733706220
--- /dev/null
+++ b/server/src/interfaces/version-history.interface.ts
@@ -0,0 +1,9 @@
+import { VersionHistoryEntity } from 'src/entities/version-history.entity';
+
+export const IVersionHistoryRepository = 'IVersionHistoryRepository';
+
+export interface IVersionHistoryRepository {
+  create(version: Omit<VersionHistoryEntity, 'id' | 'createdAt'>): Promise<VersionHistoryEntity>;
+  getAll(): Promise<VersionHistoryEntity[]>;
+  getLatest(): Promise<VersionHistoryEntity | null>;
+}
diff --git a/server/src/interfaces/view.interface.ts b/server/src/interfaces/view.interface.ts
new file mode 100644
index 0000000000..f819160002
--- /dev/null
+++ b/server/src/interfaces/view.interface.ts
@@ -0,0 +1,8 @@
+import { AssetEntity } from 'src/entities/asset.entity';
+
+export const IViewRepository = 'IViewRepository';
+
+export interface IViewRepository {
+  getAssetsByOriginalPath(userId: string, partialPath: string): Promise<AssetEntity[]>;
+  getUniqueOriginalPaths(userId: string): Promise<string[]>;
+}
diff --git a/server/src/main.ts b/server/src/main.ts
index ee4de1a259..3097eee69b 100644
--- a/server/src/main.ts
+++ b/server/src/main.ts
@@ -1,58 +1,74 @@
 import { CommandFactory } from 'nest-commander';
-import { fork } from 'node:child_process';
+import { ChildProcess, fork } from 'node:child_process';
 import { Worker } from 'node:worker_threads';
 import { ImmichAdminModule } from 'src/app.module';
-import { LogLevel } from 'src/config';
-import { getWorkers } from 'src/utils/workers';
-const immichApp = process.argv[2] || process.env.IMMICH_APP;
+import { ImmichWorker, LogLevel } from 'src/enum';
+import { ConfigRepository } from 'src/repositories/config.repository';
 
-if (process.argv[2] === immichApp) {
+const immichApp = process.argv[2];
+if (immichApp) {
   process.argv.splice(2, 1);
 }
 
-async function bootstrapImmichAdmin() {
-  process.env.IMMICH_LOG_LEVEL = LogLevel.WARN;
-  await CommandFactory.run(ImmichAdminModule);
-}
+let apiProcess: ChildProcess | undefined;
 
-function bootstrapWorker(name: string) {
+const onError = (name: string, error: Error) => {
+  console.error(`${name} worker error: ${error}`);
+};
+
+const onExit = (name: string, exitCode: number | null) => {
+  if (exitCode !== 0) {
+    console.error(`${name} worker exited with code ${exitCode}`);
+
+    if (apiProcess && name !== ImmichWorker.API) {
+      console.error('Killing api process');
+      apiProcess.kill('SIGTERM');
+      apiProcess = undefined;
+    }
+  }
+
+  process.exit(exitCode);
+};
+
+function bootstrapWorker(name: ImmichWorker) {
   console.log(`Starting ${name} worker`);
 
-  const worker = name === 'api' ? fork(`./dist/workers/${name}.js`) : new Worker(`./dist/workers/${name}.js`);
+  let worker: Worker | ChildProcess;
+  if (name === ImmichWorker.API) {
+    worker = fork(`./dist/workers/${name}.js`, [], {
+      execArgv: process.execArgv.map((arg) => (arg.startsWith('--inspect') ? '--inspect=0.0.0.0:9231' : arg)),
+    });
+    apiProcess = worker;
+  } else {
+    worker = new Worker(`./dist/workers/${name}.js`);
+  }
 
-  worker.on('error', (error) => {
-    console.error(`${name} worker error: ${error}`);
-  });
-
-  worker.on('exit', (exitCode) => {
-    if (exitCode !== 0) {
-      console.error(`${name} worker exited with code ${exitCode}`);
-      process.exit(exitCode);
-    }
-  });
+  worker.on('error', (error) => onError(name, error));
+  worker.on('exit', (exitCode) => onExit(name, exitCode));
 }
 
 function bootstrap() {
-  switch (immichApp) {
-    case 'immich-admin': {
-      process.title = 'immich_admin_cli';
-      return bootstrapImmichAdmin();
-    }
-    case 'immich': {
-      if (!process.env.IMMICH_WORKERS_INCLUDE) {
-        process.env.IMMICH_WORKERS_INCLUDE = 'api';
-      }
-      break;
-    }
-    case 'microservices': {
-      if (!process.env.IMMICH_WORKERS_INCLUDE) {
-        process.env.IMMICH_WORKERS_INCLUDE = 'microservices';
-      }
-      break;
-    }
+  if (immichApp === 'immich-admin') {
+    process.title = 'immich_admin_cli';
+    process.env.IMMICH_LOG_LEVEL = LogLevel.WARN;
+    return CommandFactory.run(ImmichAdminModule);
   }
+
+  if (immichApp === 'immich' || immichApp === 'microservices') {
+    console.error(
+      `Using "start.sh ${immichApp}" has been deprecated. See https://github.com/immich-app/immich/releases/tag/v1.118.0 for more information.`,
+    );
+    process.exit(1);
+  }
+
+  if (immichApp) {
+    console.error(`Unknown command: "${immichApp}"`);
+    process.exit(1);
+  }
+
   process.title = 'immich';
-  for (const worker of getWorkers()) {
+  const { workers } = new ConfigRepository().getEnv();
+  for (const worker of workers) {
     bootstrapWorker(worker);
   }
 }
diff --git a/server/src/middleware/asset-upload.interceptor.ts b/server/src/middleware/asset-upload.interceptor.ts
index 0f38c34259..bc403ee562 100644
--- a/server/src/middleware/asset-upload.interceptor.ts
+++ b/server/src/middleware/asset-upload.interceptor.ts
@@ -2,7 +2,7 @@ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nes
 import { Response } from 'express';
 import { of } from 'rxjs';
 import { AssetMediaResponseDto, AssetMediaStatus } from 'src/dtos/asset-media-response.dto';
-import { ImmichHeader } from 'src/dtos/auth.dto';
+import { ImmichHeader } from 'src/enum';
 import { AuthenticatedRequest } from 'src/middleware/auth.guard';
 import { AssetMediaService } from 'src/services/asset-media.service';
 import { fromMaybeArray } from 'src/utils/request';
diff --git a/server/src/middleware/auth.guard.ts b/server/src/middleware/auth.guard.ts
index d6138f2d3a..e05dba907b 100644
--- a/server/src/middleware/auth.guard.ts
+++ b/server/src/middleware/auth.guard.ts
@@ -10,20 +10,12 @@ import {
 import { Reflector } from '@nestjs/core';
 import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger';
 import { Request } from 'express';
-import { AuthDto, ImmichQuery } from 'src/dtos/auth.dto';
-import { Permission } from 'src/enum';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { ImmichQuery, MetadataKey, Permission } from 'src/enum';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { AuthService, LoginDetails } from 'src/services/auth.service';
 import { UAParser } from 'ua-parser-js';
 
-export enum Metadata {
-  AUTH_ROUTE = 'auth_route',
-  ADMIN_ROUTE = 'admin_route',
-  SHARED_ROUTE = 'shared_route',
-  API_KEY_SECURITY = 'api_key',
-  ON_EMIT_CONFIG = 'on_emit_config',
-}
-
 type AdminRoute = { admin?: true };
 type SharedLinkRoute = { sharedLink?: true };
 type AuthenticatedOptions = { permission?: Permission } & (AdminRoute | SharedLinkRoute);
@@ -32,8 +24,8 @@ export const Authenticated = (options?: AuthenticatedOptions): MethodDecorator =
   const decorators: MethodDecorator[] = [
     ApiBearerAuth(),
     ApiCookieAuth(),
-    ApiSecurity(Metadata.API_KEY_SECURITY),
-    SetMetadata(Metadata.AUTH_ROUTE, options || {}),
+    ApiSecurity(MetadataKey.API_KEY_SECURITY),
+    SetMetadata(MetadataKey.AUTH_ROUTE, options || {}),
   ];
 
   if ((options as SharedLinkRoute)?.sharedLink) {
@@ -57,7 +49,7 @@ export const GetLoginDetails = createParamDecorator((data, context: ExecutionCon
   const userAgent = UAParser(request.headers['user-agent']);
 
   return {
-    clientIp: request.ip,
+    clientIp: request.ip ?? '',
     isSecure: request.secure,
     deviceType: userAgent.browser.name || userAgent.device.type || (request.headers.devicemodel as string) || '',
     deviceOS: userAgent.os.name || (request.headers.devicetype as string) || '',
@@ -85,7 +77,7 @@ export class AuthGuard implements CanActivate {
   async canActivate(context: ExecutionContext): Promise<boolean> {
     const targets = [context.getHandler()];
 
-    const options = this.reflector.getAllAndOverride<AuthenticatedOptions | undefined>(Metadata.AUTH_ROUTE, targets);
+    const options = this.reflector.getAllAndOverride<AuthenticatedOptions | undefined>(MetadataKey.AUTH_ROUTE, targets);
     if (!options) {
       return true;
     }
diff --git a/server/src/middleware/file-upload.interceptor.ts b/server/src/middleware/file-upload.interceptor.ts
index 6ec8b401ef..108a187e67 100644
--- a/server/src/middleware/file-upload.interceptor.ts
+++ b/server/src/middleware/file-upload.interceptor.ts
@@ -7,9 +7,11 @@ import multer, { StorageEngine, diskStorage } from 'multer';
 import { createHash, randomUUID } from 'node:crypto';
 import { Observable } from 'rxjs';
 import { UploadFieldName } from 'src/dtos/asset-media.dto';
+import { RouteKey } from 'src/enum';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { AuthRequest } from 'src/middleware/auth.guard';
 import { AssetMediaService, UploadFile } from 'src/services/asset-media.service';
+import { asRequest, mapToUploadFile } from 'src/utils/asset.util';
 
 export interface UploadFiles {
   assetData: ImmichFile[];
@@ -28,27 +30,12 @@ export function getFiles(files: UploadFiles) {
   };
 }
 
-export enum Route {
-  ASSET = 'assets',
-  USER = 'users',
-}
-
 export interface ImmichFile extends Express.Multer.File {
   /** sha1 hash of file */
   uuid: string;
   checksum: Buffer;
 }
 
-export function mapToUploadFile(file: ImmichFile): UploadFile {
-  return {
-    uuid: file.uuid,
-    checksum: file.checksum,
-    originalPath: file.path,
-    originalName: Buffer.from(file.originalname, 'latin1').toString('utf8'),
-    size: file.size,
-  };
-}
-
 type DiskStorageCallback = (error: Error | null, result: string) => void;
 
 type ImmichMulterFile = Express.Multer.File & { uuid: string };
@@ -66,14 +53,6 @@ const callbackify = <T>(target: (...arguments_: any[]) => T, callback: Callback<
   }
 };
 
-const asRequest = (request: AuthRequest, file: Express.Multer.File) => {
-  return {
-    auth: request.user || null,
-    fieldName: file.fieldname as UploadFieldName,
-    file: mapToUploadFile(file as ImmichFile),
-  };
-};
-
 @Injectable()
 export class FileUploadInterceptor implements NestInterceptor {
   private handlers: {
@@ -115,7 +94,7 @@ export class FileUploadInterceptor implements NestInterceptor {
     const context_ = context.switchToHttp();
     const route = this.reflect.get<string>(PATH_METADATA, context.getClass());
 
-    const handler: RequestHandler | null = this.getHandler(route as Route);
+    const handler: RequestHandler | null = this.getHandler(route as RouteKey);
     if (handler) {
       await new Promise<void>((resolve, reject) => {
         const next: NextFunction = (error) => (error ? reject(transformException(error)) : resolve());
@@ -145,6 +124,12 @@ export class FileUploadInterceptor implements NestInterceptor {
 
   private handleFile(request: AuthRequest, file: Express.Multer.File, callback: Callback<Partial<ImmichFile>>) {
     (file as ImmichMulterFile).uuid = randomUUID();
+
+    request.on('error', (error) => {
+      this.logger.warn('Request error while uploading file, cleaning up', error);
+      this.assetService.onUploadError(request, file).catch(this.logger.error);
+    });
+
     if (!this.isAssetUploadFile(file)) {
       this.defaultStorage._handleFile(request, file, callback);
       return;
@@ -176,13 +161,13 @@ export class FileUploadInterceptor implements NestInterceptor {
     return false;
   }
 
-  private getHandler(route: Route) {
+  private getHandler(route: RouteKey) {
     switch (route) {
-      case Route.ASSET: {
+      case RouteKey.ASSET: {
         return this.handlers.assetUpload;
       }
 
-      case Route.USER: {
+      case RouteKey.USER: {
         return this.handlers.userProfile;
       }
 
diff --git a/server/src/middleware/websocket.adapter.ts b/server/src/middleware/websocket.adapter.ts
index 4978b16102..da5e5e9816 100644
--- a/server/src/middleware/websocket.adapter.ts
+++ b/server/src/middleware/websocket.adapter.ts
@@ -3,7 +3,7 @@ import { IoAdapter } from '@nestjs/platform-socket.io';
 import { createAdapter } from '@socket.io/redis-adapter';
 import { Redis } from 'ioredis';
 import { ServerOptions } from 'socket.io';
-import { parseRedisConfig } from 'src/config';
+import { IConfigRepository } from 'src/interfaces/config.interface';
 
 export class WebSocketAdapter extends IoAdapter {
   constructor(private app: INestApplicationContext) {
@@ -11,8 +11,9 @@ export class WebSocketAdapter extends IoAdapter {
   }
 
   createIOServer(port: number, options?: ServerOptions): any {
+    const { redis } = this.app.get<IConfigRepository>(IConfigRepository).getEnv();
     const server = super.createIOServer(port, options);
-    const pubClient = new Redis(parseRedisConfig());
+    const pubClient = new Redis(redis);
     const subClient = pubClient.duplicate();
     server.adapter(createAdapter(pubClient, subClient));
     return server;
diff --git a/server/src/migrations/1700713871511-UsePgVectors.ts b/server/src/migrations/1700713871511-UsePgVectors.ts
index 033e2ba9ad..e67c7275a7 100644
--- a/server/src/migrations/1700713871511-UsePgVectors.ts
+++ b/server/src/migrations/1700713871511-UsePgVectors.ts
@@ -1,13 +1,15 @@
-import { getVectorExtension } from 'src/database.config';
+import { ConfigRepository } from 'src/repositories/config.repository';
 import { getCLIPModelInfo } from 'src/utils/misc';
 import { MigrationInterface, QueryRunner } from 'typeorm';
 
+const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension;
+
 export class UsePgVectors1700713871511 implements MigrationInterface {
   name = 'UsePgVectors1700713871511';
 
   public async up(queryRunner: QueryRunner): Promise<void> {
     await queryRunner.query(`SET search_path TO "$user", public, vectors`);
-    await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS ${getVectorExtension()}`);
+    await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS ${vectorExtension}`);
     const faceDimQuery = await queryRunner.query(`
         SELECT CARDINALITY(embedding::real[]) as dimsize
         FROM asset_faces
diff --git a/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts b/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts
index e325f270fd..f9ea5a0dc3 100644
--- a/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts
+++ b/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts
@@ -1,12 +1,14 @@
-import { getVectorExtension } from 'src/database.config';
 import { DatabaseExtension } from 'src/interfaces/database.interface';
+import { ConfigRepository } from 'src/repositories/config.repository';
 import { MigrationInterface, QueryRunner } from 'typeorm';
 
+const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension;
+
 export class AddCLIPEmbeddingIndex1700713994428 implements MigrationInterface {
   name = 'AddCLIPEmbeddingIndex1700713994428';
 
   public async up(queryRunner: QueryRunner): Promise<void> {
-    if (getVectorExtension() === DatabaseExtension.VECTORS) {
+    if (vectorExtension === DatabaseExtension.VECTORS) {
       await queryRunner.query(`SET vectors.pgvector_compatibility=on`);
     }
     await queryRunner.query(`SET search_path TO "$user", public, vectors`);
diff --git a/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts b/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts
index bc6bad6dbd..d11e7b921e 100644
--- a/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts
+++ b/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts
@@ -1,12 +1,14 @@
-import { getVectorExtension } from 'src/database.config';
 import { DatabaseExtension } from 'src/interfaces/database.interface';
+import { ConfigRepository } from 'src/repositories/config.repository';
 import { MigrationInterface, QueryRunner } from 'typeorm';
 
+const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension;
+
 export class AddFaceEmbeddingIndex1700714033632 implements MigrationInterface {
   name = 'AddFaceEmbeddingIndex1700714033632';
 
   public async up(queryRunner: QueryRunner): Promise<void> {
-    if (getVectorExtension() === DatabaseExtension.VECTORS) {
+    if (vectorExtension === DatabaseExtension.VECTORS) {
       await queryRunner.query(`SET vectors.pgvector_compatibility=on`);
     }
     await queryRunner.query(`SET search_path TO "$user", public, vectors`);
diff --git a/server/src/migrations/1718486162779-AddFaceSearchRelation.ts b/server/src/migrations/1718486162779-AddFaceSearchRelation.ts
index c8e02ec0c5..ae6d752c65 100644
--- a/server/src/migrations/1718486162779-AddFaceSearchRelation.ts
+++ b/server/src/migrations/1718486162779-AddFaceSearchRelation.ts
@@ -1,10 +1,12 @@
-import { getVectorExtension } from 'src/database.config';
 import { DatabaseExtension } from 'src/interfaces/database.interface';
+import { ConfigRepository } from 'src/repositories/config.repository';
 import { MigrationInterface, QueryRunner } from 'typeorm';
 
+const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension;
+
 export class AddFaceSearchRelation1718486162779 implements MigrationInterface {
   public async up(queryRunner: QueryRunner): Promise<void> {
-    if (getVectorExtension() === DatabaseExtension.VECTORS) {
+    if (vectorExtension === DatabaseExtension.VECTORS) {
       await queryRunner.query(`SET search_path TO "$user", public, vectors`);
       await queryRunner.query(`SET vectors.pgvector_compatibility=on`);
     }
@@ -13,9 +15,10 @@ export class AddFaceSearchRelation1718486162779 implements MigrationInterface {
       const columns = await queryRunner.query(
         `SELECT column_name as name
         FROM information_schema.columns
-        WHERE table_name = '${tableName}'`);
+        WHERE table_name = '${tableName}'`,
+      );
       return columns.some((column: { name: string }) => column.name === 'embedding');
-    }
+    };
 
     const hasAssetEmbeddings = await hasEmbeddings('smart_search');
     if (!hasAssetEmbeddings) {
@@ -31,7 +34,7 @@ export class AddFaceSearchRelation1718486162779 implements MigrationInterface {
     await queryRunner.query(`ALTER TABLE face_search ALTER COLUMN embedding SET STORAGE EXTERNAL`);
     await queryRunner.query(`ALTER TABLE smart_search ALTER COLUMN embedding SET STORAGE EXTERNAL`);
 
-    const hasFaceEmbeddings = await hasEmbeddings('asset_faces')
+    const hasFaceEmbeddings = await hasEmbeddings('asset_faces');
     if (hasFaceEmbeddings) {
       await queryRunner.query(`
         INSERT INTO face_search("faceId", embedding)
@@ -56,7 +59,7 @@ export class AddFaceSearchRelation1718486162779 implements MigrationInterface {
   }
 
   public async down(queryRunner: QueryRunner): Promise<void> {
-    if (getVectorExtension() === DatabaseExtension.VECTORS) {
+    if (vectorExtension === DatabaseExtension.VECTORS) {
       await queryRunner.query(`SET search_path TO "$user", public, vectors`);
       await queryRunner.query(`SET vectors.pgvector_compatibility=on`);
     }
diff --git a/server/src/migrations/1726491047923-AddprofileChangedAt.ts b/server/src/migrations/1726491047923-AddprofileChangedAt.ts
new file mode 100644
index 0000000000..bcf568426a
--- /dev/null
+++ b/server/src/migrations/1726491047923-AddprofileChangedAt.ts
@@ -0,0 +1,14 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class AddprofileChangedAt1726491047923 implements MigrationInterface {
+    name = 'AddprofileChangedAt1726491047923'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "users" ADD "profileChangedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "profileChangedAt"`);
+    }
+
+}
diff --git a/server/src/migrations/1726593009549-AddAssetStatus.ts b/server/src/migrations/1726593009549-AddAssetStatus.ts
new file mode 100644
index 0000000000..5b243b05b5
--- /dev/null
+++ b/server/src/migrations/1726593009549-AddAssetStatus.ts
@@ -0,0 +1,16 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class AddAssetStatus1726593009549 implements MigrationInterface {
+    name = 'AddAssetStatus1726593009549'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`CREATE TYPE "assets_status_enum" AS ENUM('active', 'trashed', 'deleted')`);
+        await queryRunner.query(`ALTER TABLE "assets" ADD "status" "assets_status_enum" NOT NULL DEFAULT 'active'`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "status"`);
+        await queryRunner.query(`DROP TYPE "assets_status_enum"`);
+    }
+
+}
diff --git a/server/src/migrations/1727471863507-SeparateQualityForThumbnailAndPreview.ts b/server/src/migrations/1727471863507-SeparateQualityForThumbnailAndPreview.ts
new file mode 100644
index 0000000000..e02203997f
--- /dev/null
+++ b/server/src/migrations/1727471863507-SeparateQualityForThumbnailAndPreview.ts
@@ -0,0 +1,37 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class SeparateQualityForThumbnailAndPreview1727471863507 implements MigrationInterface {
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(`
+      update system_metadata
+      set value = jsonb_set(value, '{image}', jsonb_strip_nulls(
+        jsonb_build_object(
+          'preview', jsonb_build_object(
+            'format', value->'image'->'previewFormat',
+            'quality', value->'image'->'quality',
+            'size', value->'image'->'previewSize'),
+          'thumbnail', jsonb_build_object(
+            'format', value->'image'->'thumbnailFormat',
+            'quality', value->'image'->'quality',
+            'size', value->'image'->'thumbnailSize'),
+          'extractEmbedded', value->'extractEmbedded',
+          'colorspace', value->'colorspace'
+        )))
+        where key = 'system-config'`);
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(`
+      update system_metadata
+        set value = jsonb_set(value, '{image}', jsonb_strip_nulls(jsonb_build_object(
+          'previewFormat', value->'image'->'preview'->'format',
+          'previewSize', value->'image'->'preview'->'size',
+          'thumbnailFormat', value->'image'->'thumbnail'->'format',
+          'thumbnailSize', value->'image'->'thumbnail'->'size',
+          'extractEmbedded', value->'extractEmbedded',
+          'colorspace', value->'colorspace',
+          'quality', value->'image'->'preview'->'quality'
+        )))
+        where key = 'system-config'`);
+  }
+}
diff --git a/server/src/migrations/1727781844613-IsOfflineSetDeletedAt.ts b/server/src/migrations/1727781844613-IsOfflineSetDeletedAt.ts
new file mode 100644
index 0000000000..050e9a93cf
--- /dev/null
+++ b/server/src/migrations/1727781844613-IsOfflineSetDeletedAt.ts
@@ -0,0 +1,16 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class IsOfflineSetDeletedAt1727781844613 implements MigrationInterface {
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+      await queryRunner.query(
+        `UPDATE assets SET "deletedAt" = now() WHERE "isOffline" = true AND "deletedAt" IS NULL`,
+      );
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+      await queryRunner.query(
+        `UPDATE assets SET "deletedAt" = null WHERE "isOffline" = true`,
+      );
+    }
+}
diff --git a/server/src/migrations/1727797340951-AddVersionHistory.ts b/server/src/migrations/1727797340951-AddVersionHistory.ts
new file mode 100644
index 0000000000..7eb731d1a3
--- /dev/null
+++ b/server/src/migrations/1727797340951-AddVersionHistory.ts
@@ -0,0 +1,14 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class AddVersionHistory1727797340951 implements MigrationInterface {
+    name = 'AddVersionHistory1727797340951'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`CREATE TABLE "version_history" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "version" character varying NOT NULL, CONSTRAINT "PK_5db259cbb09ce82c0d13cfd1b23" PRIMARY KEY ("id"))`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`DROP TABLE "version_history"`);
+    }
+
+}
diff --git a/server/src/migrations/1729793521993-AddAlbumAssetCreatedAt.ts b/server/src/migrations/1729793521993-AddAlbumAssetCreatedAt.ts
new file mode 100644
index 0000000000..280b34890d
--- /dev/null
+++ b/server/src/migrations/1729793521993-AddAlbumAssetCreatedAt.ts
@@ -0,0 +1,13 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class AddAlbumAssetCreatedAt1729793521993 implements MigrationInterface {
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(
+      `ALTER TABLE "albums_assets_assets" ADD COLUMN "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`,
+    );
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(`ALTER TABLE "albums_assets_assets" DROP COLUMN "createdAt"`);
+  }
+}
diff --git a/server/src/migrations/1730227312171-RemoveNplFromSystemConfig.ts b/server/src/migrations/1730227312171-RemoveNplFromSystemConfig.ts
new file mode 100644
index 0000000000..2c929191dd
--- /dev/null
+++ b/server/src/migrations/1730227312171-RemoveNplFromSystemConfig.ts
@@ -0,0 +1,12 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class RemoveNplFromSystemConfig1730227312171 implements MigrationInterface {
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(`
+        update system_metadata
+        set value = value #- '{ffmpeg,npl}'
+        where key = 'system-config' and value->'ffmpeg'->'npl' is not null`);
+  }
+
+  public async down(): Promise<void> {}
+}
diff --git a/server/src/migrations/1730989238718-DropSmartInfoTable.ts b/server/src/migrations/1730989238718-DropSmartInfoTable.ts
new file mode 100644
index 0000000000..a4de2652d6
--- /dev/null
+++ b/server/src/migrations/1730989238718-DropSmartInfoTable.ts
@@ -0,0 +1,11 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class DropSmartInfoTable1730989238718 implements MigrationInterface {
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(`DROP TABLE smart_info`);
+  }
+
+  public async down(): Promise<void> {
+    // not implemented
+  }
+}
diff --git a/server/src/migrations/1732072134943-NaturalEarthCountriesIdentityColumn.ts b/server/src/migrations/1732072134943-NaturalEarthCountriesIdentityColumn.ts
new file mode 100644
index 0000000000..3ebe8108cb
--- /dev/null
+++ b/server/src/migrations/1732072134943-NaturalEarthCountriesIdentityColumn.ts
@@ -0,0 +1,29 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class NaturalEarthCountriesIdentityColumn1732072134943 implements MigrationInterface {
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(`ALTER TABLE naturalearth_countries ALTER id DROP DEFAULT`);
+    await queryRunner.query(`DROP SEQUENCE naturalearth_countries_id_seq`);
+    await queryRunner.query(`ALTER TABLE naturalearth_countries ALTER id ADD GENERATED ALWAYS AS IDENTITY`);
+
+    // same as ll_to_earth, but with explicit schema to avoid weirdness and allow it to work in expression indices
+    await queryRunner.query(`
+      CREATE FUNCTION ll_to_earth_public(latitude double precision, longitude double precision) RETURNS public.earth PARALLEL SAFE IMMUTABLE STRICT LANGUAGE SQL AS $$
+        SELECT public.cube(public.cube(public.cube(public.earth()*cos(radians(latitude))*cos(radians(longitude))),public.earth()*cos(radians(latitude))*sin(radians(longitude))),public.earth()*sin(radians(latitude)))::public.earth
+    $$`);
+
+    await queryRunner.query(`ALTER TABLE geodata_places DROP COLUMN "earthCoord"`);
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(`ALTER TABLE naturalearth_countries ALTER id DROP GENERATED`);
+    await queryRunner.query(`CREATE SEQUENCE naturalearth_countries_id_seq`);
+    await queryRunner.query(
+      `ALTER TABLE naturalearth_countries ALTER id SET DEFAULT nextval('naturalearth_countries_id_seq'::regclass)`,
+    );
+    await queryRunner.query(`DROP FUNCTION ll_to_earth_public`);
+    await queryRunner.query(
+      `ALTER TABLE "geodata_places" ADD "earthCoord" earth GENERATED ALWAYS AS (ll_to_earth(latitude, longitude)) STORED`,
+    );
+  }
+}
diff --git a/server/src/migrations/1733339482860-RenameMachineLearningUrlToUrls.ts b/server/src/migrations/1733339482860-RenameMachineLearningUrlToUrls.ts
new file mode 100644
index 0000000000..65bb02c8e2
--- /dev/null
+++ b/server/src/migrations/1733339482860-RenameMachineLearningUrlToUrls.ts
@@ -0,0 +1,19 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class RenameMachineLearningUrlToUrls1733339482860 implements MigrationInterface {
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(`
+        UPDATE system_metadata
+        SET value = jsonb_insert(value #- '{machineLearning,url}', '{machineLearning,urls}'::text[], jsonb_build_array(value->'machineLearning'->'url'))
+        WHERE key = 'system-config' AND value->'machineLearning'->'url' IS NOT NULL
+    `);
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(`
+        UPDATE system_metadata
+        SET value = jsonb_insert(value #- '{machineLearning,urls}', '{machineLearning,url}'::text[], to_jsonb(value->'machineLearning'->'urls'->>0))
+        WHERE key = 'system-config' AND value->'machineLearning'->'urls' IS NOT NULL AND jsonb_array_length(value->'machineLearning'->'urls') >= 1
+    `);
+  }
+}
diff --git a/server/src/queries/activity.repository.sql b/server/src/queries/activity.repository.sql
index 3f3e04140c..44042c0e6d 100644
--- a/server/src/queries/activity.repository.sql
+++ b/server/src/queries/activity.repository.sql
@@ -23,7 +23,8 @@ SELECT
   "ActivityEntity__ActivityEntity_user"."status" AS "ActivityEntity__ActivityEntity_user_status",
   "ActivityEntity__ActivityEntity_user"."updatedAt" AS "ActivityEntity__ActivityEntity_user_updatedAt",
   "ActivityEntity__ActivityEntity_user"."quotaSizeInBytes" AS "ActivityEntity__ActivityEntity_user_quotaSizeInBytes",
-  "ActivityEntity__ActivityEntity_user"."quotaUsageInBytes" AS "ActivityEntity__ActivityEntity_user_quotaUsageInBytes"
+  "ActivityEntity__ActivityEntity_user"."quotaUsageInBytes" AS "ActivityEntity__ActivityEntity_user_quotaUsageInBytes",
+  "ActivityEntity__ActivityEntity_user"."profileChangedAt" AS "ActivityEntity__ActivityEntity_user_profileChangedAt"
 FROM
   "activity" "ActivityEntity"
   LEFT JOIN "users" "ActivityEntity__ActivityEntity_user" ON "ActivityEntity__ActivityEntity_user"."id" = "ActivityEntity"."userId"
diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql
index 729f7c7f20..c4f6fbdd32 100644
--- a/server/src/queries/album.repository.sql
+++ b/server/src/queries/album.repository.sql
@@ -30,6 +30,7 @@ FROM
       "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
       "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
       "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
+      "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt",
       "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
       "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
       "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
@@ -47,6 +48,7 @@ FROM
       "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
       "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
       "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
+      "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt",
       "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
       "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
       "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
@@ -80,64 +82,6 @@ ORDER BY
 LIMIT
   1
 
--- AlbumRepository.getByIds
-SELECT
-  "AlbumEntity"."id" AS "AlbumEntity_id",
-  "AlbumEntity"."ownerId" AS "AlbumEntity_ownerId",
-  "AlbumEntity"."albumName" AS "AlbumEntity_albumName",
-  "AlbumEntity"."description" AS "AlbumEntity_description",
-  "AlbumEntity"."createdAt" AS "AlbumEntity_createdAt",
-  "AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt",
-  "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
-  "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
-  "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
-  "AlbumEntity"."order" AS "AlbumEntity_order",
-  "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
-  "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
-  "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
-  "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
-  "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
-  "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
-  "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
-  "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
-  "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
-  "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
-  "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
-  "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
-  "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
-  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
-  "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
-  "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
-  "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
-  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
-  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
-  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
-  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
-  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
-  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId",
-  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath",
-  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword",
-  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt",
-  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
-  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
-  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
-  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
-  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes"
-FROM
-  "albums" "AlbumEntity"
-  LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
-  AND (
-    "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL
-  )
-  LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
-  LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId"
-  AND (
-    "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL
-  )
-WHERE
-  ((("AlbumEntity"."id" IN ($1))))
-  AND ("AlbumEntity"."deletedAt" IS NULL)
-
 -- AlbumRepository.getByAssetId
 SELECT
   "AlbumEntity"."id" AS "AlbumEntity_id",
@@ -164,6 +108,7 @@ SELECT
   "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
   "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
   "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
+  "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt",
   "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
   "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
   "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
@@ -180,7 +125,8 @@ SELECT
   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
-  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes"
+  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
+  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt"
 FROM
   "albums" "AlbumEntity"
   LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
@@ -241,35 +187,6 @@ WHERE
 GROUP BY
   "album"."id"
 
--- AlbumRepository.getInvalidThumbnail
-SELECT
-  "albums"."id" AS "albums_id"
-FROM
-  "albums" "albums"
-WHERE
-  (
-    "albums"."albumThumbnailAssetId" IS NULL
-    AND EXISTS (
-      SELECT
-        1
-      FROM
-        "albums_assets_assets" "albums_assets"
-      WHERE
-        "albums"."id" = "albums_assets"."albumsId"
-    )
-    OR "albums"."albumThumbnailAssetId" IS NOT NULL
-    AND NOT EXISTS (
-      SELECT
-        1
-      FROM
-        "albums_assets_assets" "albums_assets"
-      WHERE
-        "albums"."id" = "albums_assets"."albumsId"
-        AND "albums"."albumThumbnailAssetId" = "albums_assets"."assetsId"
-    )
-  )
-  AND ("albums"."deletedAt" IS NULL)
-
 -- AlbumRepository.getOwned
 SELECT
   "AlbumEntity"."id" AS "AlbumEntity_id",
@@ -299,6 +216,7 @@ SELECT
   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
+  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt",
   "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
   "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
   "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
@@ -324,7 +242,8 @@ SELECT
   "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
   "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
   "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
-  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
+  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
+  "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt"
 FROM
   "albums" "AlbumEntity"
   LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
@@ -372,6 +291,7 @@ SELECT
   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
+  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt",
   "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
   "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
   "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
@@ -397,7 +317,8 @@ SELECT
   "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
   "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
   "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
-  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
+  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
+  "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt"
 FROM
   "albums" "AlbumEntity"
   LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
@@ -495,7 +416,8 @@ SELECT
   "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
   "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
   "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
-  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
+  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
+  "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt"
 FROM
   "albums" "AlbumEntity"
   LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
@@ -528,41 +450,6 @@ WHERE
 ORDER BY
   "AlbumEntity"."createdAt" DESC
 
--- AlbumRepository.getAll
-SELECT
-  "AlbumEntity"."id" AS "AlbumEntity_id",
-  "AlbumEntity"."ownerId" AS "AlbumEntity_ownerId",
-  "AlbumEntity"."albumName" AS "AlbumEntity_albumName",
-  "AlbumEntity"."description" AS "AlbumEntity_description",
-  "AlbumEntity"."createdAt" AS "AlbumEntity_createdAt",
-  "AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt",
-  "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
-  "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
-  "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
-  "AlbumEntity"."order" AS "AlbumEntity_order",
-  "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
-  "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
-  "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
-  "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
-  "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
-  "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
-  "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
-  "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
-  "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
-  "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
-  "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
-  "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
-  "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
-  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
-FROM
-  "albums" "AlbumEntity"
-  LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
-  AND (
-    "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL
-  )
-WHERE
-  "AlbumEntity"."deletedAt" IS NULL
-
 -- AlbumRepository.removeAsset
 DELETE FROM "albums_assets_assets"
 WHERE
@@ -596,16 +483,13 @@ UPDATE "albums"
 SET
   "albumThumbnailAssetId" = (
     SELECT
-      "albums_assets2"."assetsId"
+      "album_assets"."assetsId"
     FROM
-      "assets" "assets",
-      "albums_assets_assets" "albums_assets2"
+      "albums_assets_assets" "album_assets"
+      INNER JOIN "assets" "assets" ON "album_assets"."assetsId" = "assets"."id"
+      AND "assets"."deletedAt" IS NULL
     WHERE
-      (
-        "albums_assets2"."assetsId" = "assets"."id"
-        AND "albums_assets2"."albumsId" = "albums"."id"
-      )
-      AND ("assets"."deletedAt" IS NULL)
+      "album_assets"."albumsId" = "albums"."id"
     ORDER BY
       "assets"."fileCreatedAt" DESC
     LIMIT
@@ -618,17 +502,21 @@ WHERE
     SELECT
       1
     FROM
-      "albums_assets_assets" "albums_assets"
+      "albums_assets_assets" "album_assets"
+      INNER JOIN "assets" "assets" ON "album_assets"."assetsId" = "assets"."id"
+      AND "assets"."deletedAt" IS NULL
     WHERE
-      "albums"."id" = "albums_assets"."albumsId"
+      "album_assets"."albumsId" = "albums"."id"
   )
   OR "albums"."albumThumbnailAssetId" IS NOT NULL
   AND NOT EXISTS (
     SELECT
       1
     FROM
-      "albums_assets_assets" "albums_assets"
+      "albums_assets_assets" "album_assets"
+      INNER JOIN "assets" "assets" ON "album_assets"."assetsId" = "assets"."id"
+      AND "assets"."deletedAt" IS NULL
     WHERE
-      "albums"."id" = "albums_assets"."albumsId"
-      AND "albums"."albumThumbnailAssetId" = "albums_assets"."assetsId"
+      "album_assets"."albumsId" = "albums"."id"
+      AND "albums"."albumThumbnailAssetId" = "album_assets"."assetsId"
   )
diff --git a/server/src/queries/api.key.repository.sql b/server/src/queries/api.key.repository.sql
index e5f389ac4d..f4989d355e 100644
--- a/server/src/queries/api.key.repository.sql
+++ b/server/src/queries/api.key.repository.sql
@@ -24,6 +24,7 @@ FROM
       "APIKeyEntity__APIKeyEntity_user"."updatedAt" AS "APIKeyEntity__APIKeyEntity_user_updatedAt",
       "APIKeyEntity__APIKeyEntity_user"."quotaSizeInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaSizeInBytes",
       "APIKeyEntity__APIKeyEntity_user"."quotaUsageInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaUsageInBytes",
+      "APIKeyEntity__APIKeyEntity_user"."profileChangedAt" AS "APIKeyEntity__APIKeyEntity_user_profileChangedAt",
       "7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."userId" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_userId",
       "7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."key" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_key",
       "7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."value" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_value"
diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql
index da5ec1d4d1..4694cd20fc 100644
--- a/server/src/queries/asset.repository.sql
+++ b/server/src/queries/asset.repository.sql
@@ -8,6 +8,7 @@ SELECT
   "entity"."libraryId" AS "entity_libraryId",
   "entity"."deviceId" AS "entity_deviceId",
   "entity"."type" AS "entity_type",
+  "entity"."status" AS "entity_status",
   "entity"."originalPath" AS "entity_originalPath",
   "entity"."thumbhash" AS "entity_thumbhash",
   "entity"."encodedVideoPath" AS "entity_encodedVideoPath",
@@ -67,7 +68,7 @@ SELECT
 FROM
   "assets" "entity"
   LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "entity"."id"
-  LEFT JOIN "asset_files" "files" ON "files"."assetId" = "entity"."id"
+  INNER JOIN "asset_files" "files" ON "files"."assetId" = "entity"."id"
 WHERE
   (
     "entity"."ownerId" IN ($1)
@@ -83,10 +84,20 @@ WHERE
       FROM
         "entity"."localDateTime" AT TIME ZONE 'UTC'
     ) = $3
+    AND "files"."type" = $4
+    AND EXTRACT(
+      YEAR
+      FROM
+        CURRENT_DATE AT TIME ZONE 'UTC'
+    ) - EXTRACT(
+      YEAR
+      FROM
+        "entity"."localDateTime" AT TIME ZONE 'UTC'
+    ) > 0
   )
   AND ("entity"."deletedAt" IS NULL)
 ORDER BY
-  "entity"."localDateTime" ASC
+  "entity"."fileCreatedAt" ASC
 
 -- AssetRepository.getByIds
 SELECT
@@ -96,6 +107,7 @@ SELECT
   "AssetEntity"."libraryId" AS "AssetEntity_libraryId",
   "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
   "AssetEntity"."type" AS "AssetEntity_type",
+  "AssetEntity"."status" AS "AssetEntity_status",
   "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
   "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
   "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
@@ -130,6 +142,7 @@ SELECT
   "AssetEntity"."libraryId" AS "AssetEntity_libraryId",
   "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
   "AssetEntity"."type" AS "AssetEntity_type",
+  "AssetEntity"."status" AS "AssetEntity_status",
   "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
   "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
   "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
@@ -180,9 +193,6 @@ SELECT
   "AssetEntity__AssetEntity_exifInfo"."bitsPerSample" AS "AssetEntity__AssetEntity_exifInfo_bitsPerSample",
   "AssetEntity__AssetEntity_exifInfo"."rating" AS "AssetEntity__AssetEntity_exifInfo_rating",
   "AssetEntity__AssetEntity_exifInfo"."fps" AS "AssetEntity__AssetEntity_exifInfo_fps",
-  "AssetEntity__AssetEntity_smartInfo"."assetId" AS "AssetEntity__AssetEntity_smartInfo_assetId",
-  "AssetEntity__AssetEntity_smartInfo"."tags" AS "AssetEntity__AssetEntity_smartInfo_tags",
-  "AssetEntity__AssetEntity_smartInfo"."objects" AS "AssetEntity__AssetEntity_smartInfo_objects",
   "AssetEntity__AssetEntity_tags"."id" AS "AssetEntity__AssetEntity_tags_id",
   "AssetEntity__AssetEntity_tags"."value" AS "AssetEntity__AssetEntity_tags_value",
   "AssetEntity__AssetEntity_tags"."createdAt" AS "AssetEntity__AssetEntity_tags_createdAt",
@@ -218,6 +228,7 @@ SELECT
   "bd93d5747511a4dad4923546c51365bf1a803774"."libraryId" AS "bd93d5747511a4dad4923546c51365bf1a803774_libraryId",
   "bd93d5747511a4dad4923546c51365bf1a803774"."deviceId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceId",
   "bd93d5747511a4dad4923546c51365bf1a803774"."type" AS "bd93d5747511a4dad4923546c51365bf1a803774_type",
+  "bd93d5747511a4dad4923546c51365bf1a803774"."status" AS "bd93d5747511a4dad4923546c51365bf1a803774_status",
   "bd93d5747511a4dad4923546c51365bf1a803774"."originalPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalPath",
   "bd93d5747511a4dad4923546c51365bf1a803774"."thumbhash" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbhash",
   "bd93d5747511a4dad4923546c51365bf1a803774"."encodedVideoPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_encodedVideoPath",
@@ -248,7 +259,6 @@ SELECT
 FROM
   "assets" "AssetEntity"
   LEFT JOIN "exif" "AssetEntity__AssetEntity_exifInfo" ON "AssetEntity__AssetEntity_exifInfo"."assetId" = "AssetEntity"."id"
-  LEFT JOIN "smart_info" "AssetEntity__AssetEntity_smartInfo" ON "AssetEntity__AssetEntity_smartInfo"."assetId" = "AssetEntity"."id"
   LEFT JOIN "tag_asset" "AssetEntity_AssetEntity__AssetEntity_tags" ON "AssetEntity_AssetEntity__AssetEntity_tags"."assetsId" = "AssetEntity"."id"
   LEFT JOIN "tags" "AssetEntity__AssetEntity_tags" ON "AssetEntity__AssetEntity_tags"."id" = "AssetEntity_AssetEntity__AssetEntity_tags"."tagsId"
   LEFT JOIN "asset_faces" "AssetEntity__AssetEntity_faces" ON "AssetEntity__AssetEntity_faces"."assetId" = "AssetEntity"."id"
@@ -264,35 +274,6 @@ DELETE FROM "assets"
 WHERE
   "ownerId" = $1
 
--- AssetRepository.getExternalLibraryAssetPaths
-SELECT DISTINCT
-  "distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id"
-FROM
-  (
-    SELECT
-      "AssetEntity"."id" AS "AssetEntity_id",
-      "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
-      "AssetEntity"."isOffline" AS "AssetEntity_isOffline"
-    FROM
-      "assets" "AssetEntity"
-      LEFT JOIN "libraries" "AssetEntity__AssetEntity_library" ON "AssetEntity__AssetEntity_library"."id" = "AssetEntity"."libraryId"
-      AND (
-        "AssetEntity__AssetEntity_library"."deletedAt" IS NULL
-      )
-    WHERE
-      (
-        (
-          ((("AssetEntity__AssetEntity_library"."id" = $1)))
-          AND ("AssetEntity"."isExternal" = $2)
-        )
-      )
-      AND ("AssetEntity"."deletedAt" IS NULL)
-  ) "distinctAlias"
-ORDER BY
-  "AssetEntity_id" ASC
-LIMIT
-  2
-
 -- AssetRepository.getByLibraryIdAndOriginalPath
 SELECT DISTINCT
   "distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id"
@@ -305,6 +286,7 @@ FROM
       "AssetEntity"."libraryId" AS "AssetEntity_libraryId",
       "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
       "AssetEntity"."type" AS "AssetEntity_type",
+      "AssetEntity"."status" AS "AssetEntity_status",
       "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
       "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
       "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
@@ -361,18 +343,6 @@ WHERE
       AND "originalPath" = path
   );
 
--- AssetRepository.updateOfflineLibraryAssets
-UPDATE "assets"
-SET
-  "isOffline" = $1,
-  "updatedAt" = CURRENT_TIMESTAMP
-WHERE
-  (
-    "libraryId" = $2
-    AND NOT ("originalPath" IN ($3))
-    AND "isOffline" = $4
-  )
-
 -- AssetRepository.getAllByDeviceId
 SELECT
   "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId",
@@ -402,6 +372,7 @@ SELECT
   "AssetEntity"."libraryId" AS "AssetEntity_libraryId",
   "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
   "AssetEntity"."type" AS "AssetEntity_type",
+  "AssetEntity"."status" AS "AssetEntity_status",
   "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
   "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
   "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
@@ -455,6 +426,7 @@ SELECT
   "AssetEntity"."libraryId" AS "AssetEntity_libraryId",
   "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
   "AssetEntity"."type" AS "AssetEntity_type",
+  "AssetEntity"."status" AS "AssetEntity_status",
   "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
   "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
   "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
@@ -527,6 +499,7 @@ SELECT
   "AssetEntity"."libraryId" AS "AssetEntity_libraryId",
   "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
   "AssetEntity"."type" AS "AssetEntity_type",
+  "AssetEntity"."status" AS "AssetEntity_status",
   "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
   "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
   "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
@@ -581,6 +554,7 @@ SELECT
   "asset"."libraryId" AS "asset_libraryId",
   "asset"."deviceId" AS "asset_deviceId",
   "asset"."type" AS "asset_type",
+  "asset"."status" AS "asset_status",
   "asset"."originalPath" AS "asset_originalPath",
   "asset"."thumbhash" AS "asset_thumbhash",
   "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
@@ -640,6 +614,7 @@ SELECT
   "stackedAssets"."libraryId" AS "stackedAssets_libraryId",
   "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
   "stackedAssets"."type" AS "stackedAssets_type",
+  "stackedAssets"."status" AS "stackedAssets_status",
   "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
   "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
   "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
@@ -719,6 +694,7 @@ SELECT
   "asset"."libraryId" AS "asset_libraryId",
   "asset"."deviceId" AS "asset_deviceId",
   "asset"."type" AS "asset_type",
+  "asset"."status" AS "asset_status",
   "asset"."originalPath" AS "asset_originalPath",
   "asset"."thumbhash" AS "asset_thumbhash",
   "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
@@ -778,6 +754,7 @@ SELECT
   "stackedAssets"."libraryId" AS "stackedAssets_libraryId",
   "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
   "stackedAssets"."type" AS "stackedAssets_type",
+  "stackedAssets"."status" AS "stackedAssets_status",
   "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
   "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
   "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
@@ -833,6 +810,7 @@ SELECT
   "asset"."libraryId" AS "asset_libraryId",
   "asset"."deviceId" AS "asset_deviceId",
   "asset"."type" AS "asset_type",
+  "asset"."status" AS "asset_status",
   "asset"."originalPath" AS "asset_originalPath",
   "asset"."thumbhash" AS "asset_thumbhash",
   "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
@@ -892,6 +870,7 @@ SELECT
   "stackedAssets"."libraryId" AS "stackedAssets_libraryId",
   "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
   "stackedAssets"."type" AS "stackedAssets_type",
+  "stackedAssets"."status" AS "stackedAssets_status",
   "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
   "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
   "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
@@ -959,36 +938,6 @@ WHERE
 LIMIT
   12
 
--- AssetRepository.getAssetIdByTag
-WITH
-  "random_tags" AS (
-    SELECT
-      unnest(tags) AS "tag"
-    FROM
-      "smart_info" "si"
-    GROUP BY
-      tag
-    HAVING
-      count(*) >= $1
-  )
-SELECT DISTINCT
-  ON (unnest("si"."tags")) "asset"."id" AS "data",
-  unnest("si"."tags") AS "value"
-FROM
-  "assets" "asset"
-  INNER JOIN "smart_info" "si" ON "asset"."id" = si."assetId"
-  INNER JOIN "random_tags" "t" ON "si"."tags" @> ARRAY[t.tag]
-WHERE
-  (
-    "asset"."isVisible" = true
-    AND "asset"."type" = $2
-    AND "asset"."ownerId" IN ($3)
-    AND "asset"."isArchived" = $4
-  )
-  AND ("asset"."deletedAt" IS NULL)
-LIMIT
-  12
-
 -- AssetRepository.getAllForUserFullSync
 SELECT
   "asset"."id" AS "asset_id",
@@ -997,6 +946,7 @@ SELECT
   "asset"."libraryId" AS "asset_libraryId",
   "asset"."deviceId" AS "asset_deviceId",
   "asset"."type" AS "asset_type",
+  "asset"."status" AS "asset_status",
   "asset"."originalPath" AS "asset_originalPath",
   "asset"."thumbhash" AS "asset_thumbhash",
   "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
@@ -1072,6 +1022,7 @@ SELECT
   "asset"."libraryId" AS "asset_libraryId",
   "asset"."deviceId" AS "asset_deviceId",
   "asset"."type" AS "asset_type",
+  "asset"."status" AS "asset_status",
   "asset"."originalPath" AS "asset_originalPath",
   "asset"."thumbhash" AS "asset_thumbhash",
   "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
@@ -1134,109 +1085,6 @@ WHERE
   AND "asset"."ownerId" IN ($1)
   AND "asset"."updatedAt" > $2
 
--- AssetRepository.getAssetsByOriginalPath
-SELECT
-  "asset"."id" AS "asset_id",
-  "asset"."deviceAssetId" AS "asset_deviceAssetId",
-  "asset"."ownerId" AS "asset_ownerId",
-  "asset"."libraryId" AS "asset_libraryId",
-  "asset"."deviceId" AS "asset_deviceId",
-  "asset"."type" AS "asset_type",
-  "asset"."originalPath" AS "asset_originalPath",
-  "asset"."thumbhash" AS "asset_thumbhash",
-  "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
-  "asset"."createdAt" AS "asset_createdAt",
-  "asset"."updatedAt" AS "asset_updatedAt",
-  "asset"."deletedAt" AS "asset_deletedAt",
-  "asset"."fileCreatedAt" AS "asset_fileCreatedAt",
-  "asset"."localDateTime" AS "asset_localDateTime",
-  "asset"."fileModifiedAt" AS "asset_fileModifiedAt",
-  "asset"."isFavorite" AS "asset_isFavorite",
-  "asset"."isArchived" AS "asset_isArchived",
-  "asset"."isExternal" AS "asset_isExternal",
-  "asset"."isOffline" AS "asset_isOffline",
-  "asset"."checksum" AS "asset_checksum",
-  "asset"."duration" AS "asset_duration",
-  "asset"."isVisible" AS "asset_isVisible",
-  "asset"."livePhotoVideoId" AS "asset_livePhotoVideoId",
-  "asset"."originalFileName" AS "asset_originalFileName",
-  "asset"."sidecarPath" AS "asset_sidecarPath",
-  "asset"."stackId" AS "asset_stackId",
-  "asset"."duplicateId" AS "asset_duplicateId",
-  "exifInfo"."assetId" AS "exifInfo_assetId",
-  "exifInfo"."description" AS "exifInfo_description",
-  "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
-  "exifInfo"."exifImageHeight" AS "exifInfo_exifImageHeight",
-  "exifInfo"."fileSizeInByte" AS "exifInfo_fileSizeInByte",
-  "exifInfo"."orientation" AS "exifInfo_orientation",
-  "exifInfo"."dateTimeOriginal" AS "exifInfo_dateTimeOriginal",
-  "exifInfo"."modifyDate" AS "exifInfo_modifyDate",
-  "exifInfo"."timeZone" AS "exifInfo_timeZone",
-  "exifInfo"."latitude" AS "exifInfo_latitude",
-  "exifInfo"."longitude" AS "exifInfo_longitude",
-  "exifInfo"."projectionType" AS "exifInfo_projectionType",
-  "exifInfo"."city" AS "exifInfo_city",
-  "exifInfo"."livePhotoCID" AS "exifInfo_livePhotoCID",
-  "exifInfo"."autoStackId" AS "exifInfo_autoStackId",
-  "exifInfo"."state" AS "exifInfo_state",
-  "exifInfo"."country" AS "exifInfo_country",
-  "exifInfo"."make" AS "exifInfo_make",
-  "exifInfo"."model" AS "exifInfo_model",
-  "exifInfo"."lensModel" AS "exifInfo_lensModel",
-  "exifInfo"."fNumber" AS "exifInfo_fNumber",
-  "exifInfo"."focalLength" AS "exifInfo_focalLength",
-  "exifInfo"."iso" AS "exifInfo_iso",
-  "exifInfo"."exposureTime" AS "exifInfo_exposureTime",
-  "exifInfo"."profileDescription" AS "exifInfo_profileDescription",
-  "exifInfo"."colorspace" AS "exifInfo_colorspace",
-  "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
-  "exifInfo"."rating" AS "exifInfo_rating",
-  "exifInfo"."fps" AS "exifInfo_fps",
-  "stack"."id" AS "stack_id",
-  "stack"."ownerId" AS "stack_ownerId",
-  "stack"."primaryAssetId" AS "stack_primaryAssetId",
-  "stackedAssets"."id" AS "stackedAssets_id",
-  "stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
-  "stackedAssets"."ownerId" AS "stackedAssets_ownerId",
-  "stackedAssets"."libraryId" AS "stackedAssets_libraryId",
-  "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
-  "stackedAssets"."type" AS "stackedAssets_type",
-  "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
-  "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
-  "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
-  "stackedAssets"."createdAt" AS "stackedAssets_createdAt",
-  "stackedAssets"."updatedAt" AS "stackedAssets_updatedAt",
-  "stackedAssets"."deletedAt" AS "stackedAssets_deletedAt",
-  "stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt",
-  "stackedAssets"."localDateTime" AS "stackedAssets_localDateTime",
-  "stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt",
-  "stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
-  "stackedAssets"."isArchived" AS "stackedAssets_isArchived",
-  "stackedAssets"."isExternal" AS "stackedAssets_isExternal",
-  "stackedAssets"."isOffline" AS "stackedAssets_isOffline",
-  "stackedAssets"."checksum" AS "stackedAssets_checksum",
-  "stackedAssets"."duration" AS "stackedAssets_duration",
-  "stackedAssets"."isVisible" AS "stackedAssets_isVisible",
-  "stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
-  "stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
-  "stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
-  "stackedAssets"."stackId" AS "stackedAssets_stackId",
-  "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
-FROM
-  "assets" "asset"
-  LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
-  LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
-  LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
-  AND ("stackedAssets"."deletedAt" IS NULL)
-WHERE
-  "asset"."ownerId" = $1
-  AND (
-    "asset"."originalPath" LIKE $2
-    AND "asset"."originalPath" NOT LIKE $3
-  )
-ORDER BY
-  regexp_replace("asset"."originalPath", '.*/(.+)', '\1') ASC
-
 -- AssetRepository.upsertFile
 INSERT INTO
   "asset_files" (
@@ -1260,3 +1108,27 @@ RETURNING
   "id",
   "createdAt",
   "updatedAt"
+
+-- AssetRepository.upsertFiles
+INSERT INTO
+  "asset_files" (
+    "id",
+    "assetId",
+    "createdAt",
+    "updatedAt",
+    "type",
+    "path"
+  )
+VALUES
+  (DEFAULT, $1, DEFAULT, DEFAULT, $2, $3)
+ON CONFLICT ("assetId", "type") DO
+UPDATE
+SET
+  "assetId" = EXCLUDED."assetId",
+  "type" = EXCLUDED."type",
+  "path" = EXCLUDED."path",
+  "updatedAt" = DEFAULT
+RETURNING
+  "id",
+  "createdAt",
+  "updatedAt"
diff --git a/server/src/queries/library.repository.sql b/server/src/queries/library.repository.sql
index 5dd32ce365..a5d6ba05db 100644
--- a/server/src/queries/library.repository.sql
+++ b/server/src/queries/library.repository.sql
@@ -28,7 +28,8 @@ FROM
       "LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status",
       "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
       "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
-      "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes"
+      "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes",
+      "LibraryEntity__LibraryEntity_owner"."profileChangedAt" AS "LibraryEntity__LibraryEntity_owner_profileChangedAt"
     FROM
       "libraries" "LibraryEntity"
       LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId"
@@ -68,7 +69,8 @@ SELECT
   "LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status",
   "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
   "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
-  "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes"
+  "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes",
+  "LibraryEntity__LibraryEntity_owner"."profileChangedAt" AS "LibraryEntity__LibraryEntity_owner_profileChangedAt"
 FROM
   "libraries" "LibraryEntity"
   LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId"
@@ -104,7 +106,8 @@ SELECT
   "LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status",
   "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
   "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
-  "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes"
+  "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes",
+  "LibraryEntity__LibraryEntity_owner"."profileChangedAt" AS "LibraryEntity__LibraryEntity_owner_profileChangedAt"
 FROM
   "libraries" "LibraryEntity"
   LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId"
diff --git a/server/src/queries/metadata.repository.sql b/server/src/queries/metadata.repository.sql
deleted file mode 100644
index 2125274320..0000000000
--- a/server/src/queries/metadata.repository.sql
+++ /dev/null
@@ -1,56 +0,0 @@
--- NOTE: This file is auto generated by ./sql-generator
-
--- MetadataRepository.getCountries
-SELECT DISTINCT
-  ON ("exif"."country") "exif"."country" AS "country"
-FROM
-  "exif" "exif"
-  LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
-  AND ("asset"."deletedAt" IS NULL)
-WHERE
-  "asset"."ownerId" IN ($1)
-
--- MetadataRepository.getStates
-SELECT DISTINCT
-  ON ("exif"."state") "exif"."state" AS "state"
-FROM
-  "exif" "exif"
-  LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
-  AND ("asset"."deletedAt" IS NULL)
-WHERE
-  "asset"."ownerId" IN ($1)
-  AND "exif"."country" = $2
-
--- MetadataRepository.getCities
-SELECT DISTINCT
-  ON ("exif"."city") "exif"."city" AS "city"
-FROM
-  "exif" "exif"
-  LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
-  AND ("asset"."deletedAt" IS NULL)
-WHERE
-  "asset"."ownerId" IN ($1)
-  AND "exif"."country" = $2
-  AND "exif"."state" = $3
-
--- MetadataRepository.getCameraMakes
-SELECT DISTINCT
-  ON ("exif"."make") "exif"."make" AS "make"
-FROM
-  "exif" "exif"
-  LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
-  AND ("asset"."deletedAt" IS NULL)
-WHERE
-  "asset"."ownerId" IN ($1)
-  AND "exif"."model" = $2
-
--- MetadataRepository.getCameraModels
-SELECT DISTINCT
-  ON ("exif"."model") "exif"."model" AS "model"
-FROM
-  "exif" "exif"
-  LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
-  AND ("asset"."deletedAt" IS NULL)
-WHERE
-  "asset"."ownerId" IN ($1)
-  AND "exif"."make" = $2
diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql
index 95374b136d..a7e683fca1 100644
--- a/server/src/queries/person.repository.sql
+++ b/server/src/queries/person.repository.sql
@@ -20,13 +20,12 @@ SELECT
   "person"."isHidden" AS "person_isHidden"
 FROM
   "person" "person"
-  LEFT JOIN "asset_faces" "face" ON "face"."personId" = "person"."id"
+  INNER JOIN "asset_faces" "face" ON "face"."personId" = "person"."id"
   INNER JOIN "assets" "asset" ON "asset"."id" = "face"."assetId"
   AND ("asset"."deletedAt" IS NULL)
 WHERE
   "person"."ownerId" = $1
   AND "asset"."isArchived" = false
-  AND "person"."thumbnailPath" != ''
   AND "person"."isHidden" = false
 GROUP BY
   "person"."id"
@@ -159,6 +158,7 @@ FROM
       "AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId",
       "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId",
       "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type",
+      "AssetFaceEntity__AssetFaceEntity_asset"."status" AS "AssetFaceEntity__AssetFaceEntity_asset_status",
       "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath",
       "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash",
       "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath",
@@ -247,113 +247,6 @@ WHERE
   AND "asset"."deletedAt" IS NULL
   AND "asset"."livePhotoVideoId" IS NULL
 
--- PersonRepository.getAssets
-SELECT DISTINCT
-  "distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id",
-  "distinctAlias"."AssetEntity_fileCreatedAt"
-FROM
-  (
-    SELECT
-      "AssetEntity"."id" AS "AssetEntity_id",
-      "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId",
-      "AssetEntity"."ownerId" AS "AssetEntity_ownerId",
-      "AssetEntity"."libraryId" AS "AssetEntity_libraryId",
-      "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
-      "AssetEntity"."type" AS "AssetEntity_type",
-      "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
-      "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
-      "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
-      "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
-      "AssetEntity"."updatedAt" AS "AssetEntity_updatedAt",
-      "AssetEntity"."deletedAt" AS "AssetEntity_deletedAt",
-      "AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt",
-      "AssetEntity"."localDateTime" AS "AssetEntity_localDateTime",
-      "AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt",
-      "AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
-      "AssetEntity"."isArchived" AS "AssetEntity_isArchived",
-      "AssetEntity"."isExternal" AS "AssetEntity_isExternal",
-      "AssetEntity"."isOffline" AS "AssetEntity_isOffline",
-      "AssetEntity"."checksum" AS "AssetEntity_checksum",
-      "AssetEntity"."duration" AS "AssetEntity_duration",
-      "AssetEntity"."isVisible" AS "AssetEntity_isVisible",
-      "AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
-      "AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
-      "AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
-      "AssetEntity"."stackId" AS "AssetEntity_stackId",
-      "AssetEntity"."duplicateId" AS "AssetEntity_duplicateId",
-      "AssetEntity__AssetEntity_faces"."id" AS "AssetEntity__AssetEntity_faces_id",
-      "AssetEntity__AssetEntity_faces"."assetId" AS "AssetEntity__AssetEntity_faces_assetId",
-      "AssetEntity__AssetEntity_faces"."personId" AS "AssetEntity__AssetEntity_faces_personId",
-      "AssetEntity__AssetEntity_faces"."imageWidth" AS "AssetEntity__AssetEntity_faces_imageWidth",
-      "AssetEntity__AssetEntity_faces"."imageHeight" AS "AssetEntity__AssetEntity_faces_imageHeight",
-      "AssetEntity__AssetEntity_faces"."boundingBoxX1" AS "AssetEntity__AssetEntity_faces_boundingBoxX1",
-      "AssetEntity__AssetEntity_faces"."boundingBoxY1" AS "AssetEntity__AssetEntity_faces_boundingBoxY1",
-      "AssetEntity__AssetEntity_faces"."boundingBoxX2" AS "AssetEntity__AssetEntity_faces_boundingBoxX2",
-      "AssetEntity__AssetEntity_faces"."boundingBoxY2" AS "AssetEntity__AssetEntity_faces_boundingBoxY2",
-      "AssetEntity__AssetEntity_faces"."sourceType" AS "AssetEntity__AssetEntity_faces_sourceType",
-      "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_id",
-      "8258e303a73a72cf6abb13d73fb592dde0d68280"."createdAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_createdAt",
-      "8258e303a73a72cf6abb13d73fb592dde0d68280"."updatedAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_updatedAt",
-      "8258e303a73a72cf6abb13d73fb592dde0d68280"."ownerId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_ownerId",
-      "8258e303a73a72cf6abb13d73fb592dde0d68280"."name" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_name",
-      "8258e303a73a72cf6abb13d73fb592dde0d68280"."birthDate" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_birthDate",
-      "8258e303a73a72cf6abb13d73fb592dde0d68280"."thumbnailPath" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_thumbnailPath",
-      "8258e303a73a72cf6abb13d73fb592dde0d68280"."faceAssetId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_faceAssetId",
-      "8258e303a73a72cf6abb13d73fb592dde0d68280"."isHidden" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_isHidden",
-      "AssetEntity__AssetEntity_exifInfo"."assetId" AS "AssetEntity__AssetEntity_exifInfo_assetId",
-      "AssetEntity__AssetEntity_exifInfo"."description" AS "AssetEntity__AssetEntity_exifInfo_description",
-      "AssetEntity__AssetEntity_exifInfo"."exifImageWidth" AS "AssetEntity__AssetEntity_exifInfo_exifImageWidth",
-      "AssetEntity__AssetEntity_exifInfo"."exifImageHeight" AS "AssetEntity__AssetEntity_exifInfo_exifImageHeight",
-      "AssetEntity__AssetEntity_exifInfo"."fileSizeInByte" AS "AssetEntity__AssetEntity_exifInfo_fileSizeInByte",
-      "AssetEntity__AssetEntity_exifInfo"."orientation" AS "AssetEntity__AssetEntity_exifInfo_orientation",
-      "AssetEntity__AssetEntity_exifInfo"."dateTimeOriginal" AS "AssetEntity__AssetEntity_exifInfo_dateTimeOriginal",
-      "AssetEntity__AssetEntity_exifInfo"."modifyDate" AS "AssetEntity__AssetEntity_exifInfo_modifyDate",
-      "AssetEntity__AssetEntity_exifInfo"."timeZone" AS "AssetEntity__AssetEntity_exifInfo_timeZone",
-      "AssetEntity__AssetEntity_exifInfo"."latitude" AS "AssetEntity__AssetEntity_exifInfo_latitude",
-      "AssetEntity__AssetEntity_exifInfo"."longitude" AS "AssetEntity__AssetEntity_exifInfo_longitude",
-      "AssetEntity__AssetEntity_exifInfo"."projectionType" AS "AssetEntity__AssetEntity_exifInfo_projectionType",
-      "AssetEntity__AssetEntity_exifInfo"."city" AS "AssetEntity__AssetEntity_exifInfo_city",
-      "AssetEntity__AssetEntity_exifInfo"."livePhotoCID" AS "AssetEntity__AssetEntity_exifInfo_livePhotoCID",
-      "AssetEntity__AssetEntity_exifInfo"."autoStackId" AS "AssetEntity__AssetEntity_exifInfo_autoStackId",
-      "AssetEntity__AssetEntity_exifInfo"."state" AS "AssetEntity__AssetEntity_exifInfo_state",
-      "AssetEntity__AssetEntity_exifInfo"."country" AS "AssetEntity__AssetEntity_exifInfo_country",
-      "AssetEntity__AssetEntity_exifInfo"."make" AS "AssetEntity__AssetEntity_exifInfo_make",
-      "AssetEntity__AssetEntity_exifInfo"."model" AS "AssetEntity__AssetEntity_exifInfo_model",
-      "AssetEntity__AssetEntity_exifInfo"."lensModel" AS "AssetEntity__AssetEntity_exifInfo_lensModel",
-      "AssetEntity__AssetEntity_exifInfo"."fNumber" AS "AssetEntity__AssetEntity_exifInfo_fNumber",
-      "AssetEntity__AssetEntity_exifInfo"."focalLength" AS "AssetEntity__AssetEntity_exifInfo_focalLength",
-      "AssetEntity__AssetEntity_exifInfo"."iso" AS "AssetEntity__AssetEntity_exifInfo_iso",
-      "AssetEntity__AssetEntity_exifInfo"."exposureTime" AS "AssetEntity__AssetEntity_exifInfo_exposureTime",
-      "AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription",
-      "AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace",
-      "AssetEntity__AssetEntity_exifInfo"."bitsPerSample" AS "AssetEntity__AssetEntity_exifInfo_bitsPerSample",
-      "AssetEntity__AssetEntity_exifInfo"."rating" AS "AssetEntity__AssetEntity_exifInfo_rating",
-      "AssetEntity__AssetEntity_exifInfo"."fps" AS "AssetEntity__AssetEntity_exifInfo_fps"
-    FROM
-      "assets" "AssetEntity"
-      LEFT JOIN "asset_faces" "AssetEntity__AssetEntity_faces" ON "AssetEntity__AssetEntity_faces"."assetId" = "AssetEntity"."id"
-      LEFT JOIN "person" "8258e303a73a72cf6abb13d73fb592dde0d68280" ON "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" = "AssetEntity__AssetEntity_faces"."personId"
-      LEFT JOIN "exif" "AssetEntity__AssetEntity_exifInfo" ON "AssetEntity__AssetEntity_exifInfo"."assetId" = "AssetEntity"."id"
-    WHERE
-      (
-        (
-          (
-            (
-              ("AssetEntity__AssetEntity_faces"."personId" = $1)
-            )
-          )
-          AND ("AssetEntity"."isVisible" = $2)
-          AND ("AssetEntity"."isArchived" = $3)
-        )
-      )
-      AND ("AssetEntity"."deletedAt" IS NULL)
-  ) "distinctAlias"
-ORDER BY
-  "distinctAlias"."AssetEntity_fileCreatedAt" DESC,
-  "AssetEntity_id" ASC
-LIMIT
-  1000
-
 -- PersonRepository.getNumberOfPeople
 SELECT
   COUNT(DISTINCT ("person"."id")) AS "total",
@@ -363,15 +256,12 @@ SELECT
   ) AS "hidden"
 FROM
   "person" "person"
-  LEFT JOIN "asset_faces" "face" ON "face"."personId" = "person"."id"
+  INNER JOIN "asset_faces" "face" ON "face"."personId" = "person"."id"
   INNER JOIN "assets" "asset" ON "asset"."id" = "face"."assetId"
   AND ("asset"."deletedAt" IS NULL)
 WHERE
   "person"."ownerId" = $1
   AND "asset"."isArchived" = false
-  AND "person"."thumbnailPath" != ''
-HAVING
-  COUNT("face"."assetId") != 0
 
 -- PersonRepository.getFacesByIds
 SELECT
@@ -391,6 +281,7 @@ SELECT
   "AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId",
   "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId",
   "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type",
+  "AssetFaceEntity__AssetFaceEntity_asset"."status" AS "AssetFaceEntity__AssetFaceEntity_asset_status",
   "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath",
   "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash",
   "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath",
diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql
index dd2e3ae75c..1084375059 100644
--- a/server/src/queries/search.repository.sql
+++ b/server/src/queries/search.repository.sql
@@ -13,6 +13,7 @@ FROM
       "asset"."libraryId" AS "asset_libraryId",
       "asset"."deviceId" AS "asset_deviceId",
       "asset"."type" AS "asset_type",
+      "asset"."status" AS "asset_status",
       "asset"."originalPath" AS "asset_originalPath",
       "asset"."thumbhash" AS "asset_thumbhash",
       "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
@@ -43,6 +44,7 @@ FROM
       "stackedAssets"."libraryId" AS "stackedAssets_libraryId",
       "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
       "stackedAssets"."type" AS "stackedAssets_type",
+      "stackedAssets"."status" AS "stackedAssets_status",
       "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
       "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
       "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
@@ -75,10 +77,11 @@ FROM
         "asset"."fileCreatedAt" >= $1
         AND "exifInfo"."lensModel" = $2
         AND 1 = 1
+        AND "asset"."ownerId" IN ($3)
         AND 1 = 1
         AND (
-          "asset"."isFavorite" = $3
-          AND "asset"."isArchived" = $4
+          "asset"."isFavorite" = $4
+          AND "asset"."isArchived" = $5
         )
       )
       AND ("asset"."deletedAt" IS NULL)
@@ -89,16 +92,194 @@ ORDER BY
 LIMIT
   101
 
+-- SearchRepository.searchRandom
+SELECT DISTINCT
+  "distinctAlias"."asset_id" AS "ids_asset_id",
+  "distinctAlias"."asset_id"
+FROM
+  (
+    SELECT
+      "asset"."id" AS "asset_id",
+      "asset"."deviceAssetId" AS "asset_deviceAssetId",
+      "asset"."ownerId" AS "asset_ownerId",
+      "asset"."libraryId" AS "asset_libraryId",
+      "asset"."deviceId" AS "asset_deviceId",
+      "asset"."type" AS "asset_type",
+      "asset"."status" AS "asset_status",
+      "asset"."originalPath" AS "asset_originalPath",
+      "asset"."thumbhash" AS "asset_thumbhash",
+      "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
+      "asset"."createdAt" AS "asset_createdAt",
+      "asset"."updatedAt" AS "asset_updatedAt",
+      "asset"."deletedAt" AS "asset_deletedAt",
+      "asset"."fileCreatedAt" AS "asset_fileCreatedAt",
+      "asset"."localDateTime" AS "asset_localDateTime",
+      "asset"."fileModifiedAt" AS "asset_fileModifiedAt",
+      "asset"."isFavorite" AS "asset_isFavorite",
+      "asset"."isArchived" AS "asset_isArchived",
+      "asset"."isExternal" AS "asset_isExternal",
+      "asset"."isOffline" AS "asset_isOffline",
+      "asset"."checksum" AS "asset_checksum",
+      "asset"."duration" AS "asset_duration",
+      "asset"."isVisible" AS "asset_isVisible",
+      "asset"."livePhotoVideoId" AS "asset_livePhotoVideoId",
+      "asset"."originalFileName" AS "asset_originalFileName",
+      "asset"."sidecarPath" AS "asset_sidecarPath",
+      "asset"."stackId" AS "asset_stackId",
+      "asset"."duplicateId" AS "asset_duplicateId",
+      "stack"."id" AS "stack_id",
+      "stack"."ownerId" AS "stack_ownerId",
+      "stack"."primaryAssetId" AS "stack_primaryAssetId",
+      "stackedAssets"."id" AS "stackedAssets_id",
+      "stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
+      "stackedAssets"."ownerId" AS "stackedAssets_ownerId",
+      "stackedAssets"."libraryId" AS "stackedAssets_libraryId",
+      "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
+      "stackedAssets"."type" AS "stackedAssets_type",
+      "stackedAssets"."status" AS "stackedAssets_status",
+      "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
+      "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
+      "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
+      "stackedAssets"."createdAt" AS "stackedAssets_createdAt",
+      "stackedAssets"."updatedAt" AS "stackedAssets_updatedAt",
+      "stackedAssets"."deletedAt" AS "stackedAssets_deletedAt",
+      "stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt",
+      "stackedAssets"."localDateTime" AS "stackedAssets_localDateTime",
+      "stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt",
+      "stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
+      "stackedAssets"."isArchived" AS "stackedAssets_isArchived",
+      "stackedAssets"."isExternal" AS "stackedAssets_isExternal",
+      "stackedAssets"."isOffline" AS "stackedAssets_isOffline",
+      "stackedAssets"."checksum" AS "stackedAssets_checksum",
+      "stackedAssets"."duration" AS "stackedAssets_duration",
+      "stackedAssets"."isVisible" AS "stackedAssets_isVisible",
+      "stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
+      "stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
+      "stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
+      "stackedAssets"."stackId" AS "stackedAssets_stackId",
+      "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
+    FROM
+      "assets" "asset"
+      LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
+      LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
+      LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
+      AND ("stackedAssets"."deletedAt" IS NULL)
+    WHERE
+      (
+        "asset"."fileCreatedAt" >= $1
+        AND "exifInfo"."lensModel" = $2
+        AND 1 = 1
+        AND "asset"."ownerId" IN ($3)
+        AND 1 = 1
+        AND (
+          "asset"."isFavorite" = $4
+          AND "asset"."isArchived" = $5
+        )
+        AND "asset"."id" > $6
+      )
+      AND ("asset"."deletedAt" IS NULL)
+  ) "distinctAlias"
+ORDER BY
+  "distinctAlias"."asset_id" ASC,
+  "asset_id" ASC
+LIMIT
+  100
+SELECT DISTINCT
+  "distinctAlias"."asset_id" AS "ids_asset_id",
+  "distinctAlias"."asset_id"
+FROM
+  (
+    SELECT
+      "asset"."id" AS "asset_id",
+      "asset"."deviceAssetId" AS "asset_deviceAssetId",
+      "asset"."ownerId" AS "asset_ownerId",
+      "asset"."libraryId" AS "asset_libraryId",
+      "asset"."deviceId" AS "asset_deviceId",
+      "asset"."type" AS "asset_type",
+      "asset"."status" AS "asset_status",
+      "asset"."originalPath" AS "asset_originalPath",
+      "asset"."thumbhash" AS "asset_thumbhash",
+      "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
+      "asset"."createdAt" AS "asset_createdAt",
+      "asset"."updatedAt" AS "asset_updatedAt",
+      "asset"."deletedAt" AS "asset_deletedAt",
+      "asset"."fileCreatedAt" AS "asset_fileCreatedAt",
+      "asset"."localDateTime" AS "asset_localDateTime",
+      "asset"."fileModifiedAt" AS "asset_fileModifiedAt",
+      "asset"."isFavorite" AS "asset_isFavorite",
+      "asset"."isArchived" AS "asset_isArchived",
+      "asset"."isExternal" AS "asset_isExternal",
+      "asset"."isOffline" AS "asset_isOffline",
+      "asset"."checksum" AS "asset_checksum",
+      "asset"."duration" AS "asset_duration",
+      "asset"."isVisible" AS "asset_isVisible",
+      "asset"."livePhotoVideoId" AS "asset_livePhotoVideoId",
+      "asset"."originalFileName" AS "asset_originalFileName",
+      "asset"."sidecarPath" AS "asset_sidecarPath",
+      "asset"."stackId" AS "asset_stackId",
+      "asset"."duplicateId" AS "asset_duplicateId",
+      "stack"."id" AS "stack_id",
+      "stack"."ownerId" AS "stack_ownerId",
+      "stack"."primaryAssetId" AS "stack_primaryAssetId",
+      "stackedAssets"."id" AS "stackedAssets_id",
+      "stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
+      "stackedAssets"."ownerId" AS "stackedAssets_ownerId",
+      "stackedAssets"."libraryId" AS "stackedAssets_libraryId",
+      "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
+      "stackedAssets"."type" AS "stackedAssets_type",
+      "stackedAssets"."status" AS "stackedAssets_status",
+      "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
+      "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
+      "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
+      "stackedAssets"."createdAt" AS "stackedAssets_createdAt",
+      "stackedAssets"."updatedAt" AS "stackedAssets_updatedAt",
+      "stackedAssets"."deletedAt" AS "stackedAssets_deletedAt",
+      "stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt",
+      "stackedAssets"."localDateTime" AS "stackedAssets_localDateTime",
+      "stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt",
+      "stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
+      "stackedAssets"."isArchived" AS "stackedAssets_isArchived",
+      "stackedAssets"."isExternal" AS "stackedAssets_isExternal",
+      "stackedAssets"."isOffline" AS "stackedAssets_isOffline",
+      "stackedAssets"."checksum" AS "stackedAssets_checksum",
+      "stackedAssets"."duration" AS "stackedAssets_duration",
+      "stackedAssets"."isVisible" AS "stackedAssets_isVisible",
+      "stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
+      "stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
+      "stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
+      "stackedAssets"."stackId" AS "stackedAssets_stackId",
+      "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
+    FROM
+      "assets" "asset"
+      LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
+      LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
+      LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
+      AND ("stackedAssets"."deletedAt" IS NULL)
+    WHERE
+      (
+        "asset"."fileCreatedAt" >= $1
+        AND "exifInfo"."lensModel" = $2
+        AND 1 = 1
+        AND "asset"."ownerId" IN ($3)
+        AND 1 = 1
+        AND (
+          "asset"."isFavorite" = $4
+          AND "asset"."isArchived" = $5
+        )
+        AND "asset"."id" < $6
+      )
+      AND ("asset"."deletedAt" IS NULL)
+  ) "distinctAlias"
+ORDER BY
+  "distinctAlias"."asset_id" ASC,
+  "asset_id" ASC
+LIMIT
+  100
+
 -- SearchRepository.searchSmart
 START TRANSACTION
 SET
-  LOCAL vectors.enable_prefilter = on;
-
-SET
-  LOCAL vectors.search_mode = vbase;
-
-SET
-  LOCAL vectors.hnsw_ef_search = 100;
+  LOCAL vectors.hnsw_ef_search = 200;
 SELECT
   "asset"."id" AS "asset_id",
   "asset"."deviceAssetId" AS "asset_deviceAssetId",
@@ -106,6 +287,7 @@ SELECT
   "asset"."libraryId" AS "asset_libraryId",
   "asset"."deviceId" AS "asset_deviceId",
   "asset"."type" AS "asset_type",
+  "asset"."status" AS "asset_status",
   "asset"."originalPath" AS "asset_originalPath",
   "asset"."thumbhash" AS "asset_thumbhash",
   "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
@@ -136,6 +318,7 @@ SELECT
   "stackedAssets"."libraryId" AS "stackedAssets_libraryId",
   "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
   "stackedAssets"."type" AS "stackedAssets_type",
+  "stackedAssets"."status" AS "stackedAssets_status",
   "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
   "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
   "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
@@ -180,7 +363,7 @@ WHERE
 ORDER BY
   "search"."embedding" <= > $6 ASC
 LIMIT
-  101
+  201
 COMMIT
 
 -- SearchRepository.searchDuplicates
@@ -215,12 +398,6 @@ WHERE
 
 -- SearchRepository.searchFaces
 START TRANSACTION
-SET
-  LOCAL vectors.enable_prefilter = on;
-
-SET
-  LOCAL vectors.search_mode = vbase;
-
 SET
   LOCAL vectors.hnsw_ef_search = 100;
 WITH
@@ -247,7 +424,7 @@ WITH
     ORDER BY
       "search"."embedding" <= > $1 ASC
     LIMIT
-      100
+      64
   )
 SELECT
   res.*
@@ -345,6 +522,7 @@ SELECT
   "asset"."libraryId" AS "asset_libraryId",
   "asset"."deviceId" AS "asset_deviceId",
   "asset"."type" AS "asset_type",
+  "asset"."status" AS "asset_status",
   "asset"."originalPath" AS "asset_originalPath",
   "asset"."thumbhash" AS "asset_thumbhash",
   "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
@@ -401,3 +579,63 @@ FROM
   INNER JOIN cte ON asset.id = cte."assetId"
 ORDER BY
   exif.city
+
+-- SearchRepository.getCountries
+SELECT DISTINCT
+  ON ("exif"."country") "exif"."country" AS "country"
+FROM
+  "exif" "exif"
+  INNER JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
+  AND ("asset"."deletedAt" IS NULL)
+WHERE
+  "asset"."ownerId" IN ($1)
+  AND "exif"."country" != ''
+  AND "exif"."country" IS NOT NULL
+
+-- SearchRepository.getStates
+SELECT DISTINCT
+  ON ("exif"."state") "exif"."state" AS "state"
+FROM
+  "exif" "exif"
+  INNER JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
+  AND ("asset"."deletedAt" IS NULL)
+WHERE
+  "asset"."ownerId" IN ($1)
+  AND "exif"."state" != ''
+  AND "exif"."state" IS NOT NULL
+
+-- SearchRepository.getCities
+SELECT DISTINCT
+  ON ("exif"."city") "exif"."city" AS "city"
+FROM
+  "exif" "exif"
+  INNER JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
+  AND ("asset"."deletedAt" IS NULL)
+WHERE
+  "asset"."ownerId" IN ($1)
+  AND "exif"."city" != ''
+  AND "exif"."city" IS NOT NULL
+
+-- SearchRepository.getCameraMakes
+SELECT DISTINCT
+  ON ("exif"."make") "exif"."make" AS "make"
+FROM
+  "exif" "exif"
+  INNER JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
+  AND ("asset"."deletedAt" IS NULL)
+WHERE
+  "asset"."ownerId" IN ($1)
+  AND "exif"."make" != ''
+  AND "exif"."make" IS NOT NULL
+
+-- SearchRepository.getCameraModels
+SELECT DISTINCT
+  ON ("exif"."model") "exif"."model" AS "model"
+FROM
+  "exif" "exif"
+  INNER JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
+  AND ("asset"."deletedAt" IS NULL)
+WHERE
+  "asset"."ownerId" IN ($1)
+  AND "exif"."model" != ''
+  AND "exif"."model" IS NOT NULL
diff --git a/server/src/queries/session.repository.sql b/server/src/queries/session.repository.sql
index 17fff94f42..2f0613b4d0 100644
--- a/server/src/queries/session.repository.sql
+++ b/server/src/queries/session.repository.sql
@@ -39,6 +39,7 @@ FROM
       "SessionEntity__SessionEntity_user"."updatedAt" AS "SessionEntity__SessionEntity_user_updatedAt",
       "SessionEntity__SessionEntity_user"."quotaSizeInBytes" AS "SessionEntity__SessionEntity_user_quotaSizeInBytes",
       "SessionEntity__SessionEntity_user"."quotaUsageInBytes" AS "SessionEntity__SessionEntity_user_quotaUsageInBytes",
+      "SessionEntity__SessionEntity_user"."profileChangedAt" AS "SessionEntity__SessionEntity_user_profileChangedAt",
       "469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."userId" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_userId",
       "469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."key" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_key",
       "469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."value" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_value"
diff --git a/server/src/queries/shared.link.repository.sql b/server/src/queries/shared.link.repository.sql
index 10af8d17db..a19b698f76 100644
--- a/server/src/queries/shared.link.repository.sql
+++ b/server/src/queries/shared.link.repository.sql
@@ -27,6 +27,7 @@ FROM
       "SharedLinkEntity__SharedLinkEntity_assets"."libraryId" AS "SharedLinkEntity__SharedLinkEntity_assets_libraryId",
       "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId",
       "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type",
+      "SharedLinkEntity__SharedLinkEntity_assets"."status" AS "SharedLinkEntity__SharedLinkEntity_assets_status",
       "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath",
       "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash",
       "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath",
@@ -93,6 +94,7 @@ FROM
       "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."libraryId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_libraryId",
       "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceId",
       "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."type" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_type",
+      "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."status" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_status",
       "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalPath",
       "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbhash" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbhash",
       "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."encodedVideoPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_encodedVideoPath",
@@ -156,7 +158,8 @@ FROM
       "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."status" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_status",
       "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt",
       "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes",
-      "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes"
+      "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes",
+      "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileChangedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileChangedAt"
     FROM
       "shared_links" "SharedLinkEntity"
       LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id"
@@ -213,6 +216,7 @@ SELECT
   "SharedLinkEntity__SharedLinkEntity_assets"."libraryId" AS "SharedLinkEntity__SharedLinkEntity_assets_libraryId",
   "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId",
   "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type",
+  "SharedLinkEntity__SharedLinkEntity_assets"."status" AS "SharedLinkEntity__SharedLinkEntity_assets_status",
   "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath",
   "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash",
   "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath",
@@ -257,7 +261,8 @@ SELECT
   "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."status" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_status",
   "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt",
   "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes",
-  "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes"
+  "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes",
+  "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileChangedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileChangedAt"
 FROM
   "shared_links" "SharedLinkEntity"
   LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id"
@@ -309,7 +314,8 @@ FROM
       "SharedLinkEntity__SharedLinkEntity_user"."status" AS "SharedLinkEntity__SharedLinkEntity_user_status",
       "SharedLinkEntity__SharedLinkEntity_user"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_user_updatedAt",
       "SharedLinkEntity__SharedLinkEntity_user"."quotaSizeInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaSizeInBytes",
-      "SharedLinkEntity__SharedLinkEntity_user"."quotaUsageInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaUsageInBytes"
+      "SharedLinkEntity__SharedLinkEntity_user"."quotaUsageInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaUsageInBytes",
+      "SharedLinkEntity__SharedLinkEntity_user"."profileChangedAt" AS "SharedLinkEntity__SharedLinkEntity_user_profileChangedAt"
     FROM
       "shared_links" "SharedLinkEntity"
       LEFT JOIN "users" "SharedLinkEntity__SharedLinkEntity_user" ON "SharedLinkEntity__SharedLinkEntity_user"."id" = "SharedLinkEntity"."userId"
diff --git a/server/src/queries/user.repository.sql b/server/src/queries/user.repository.sql
index 2c75786f97..c35dc540ce 100644
--- a/server/src/queries/user.repository.sql
+++ b/server/src/queries/user.repository.sql
@@ -15,7 +15,8 @@ SELECT
   "UserEntity"."status" AS "UserEntity_status",
   "UserEntity"."updatedAt" AS "UserEntity_updatedAt",
   "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes",
-  "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes"
+  "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes",
+  "UserEntity"."profileChangedAt" AS "UserEntity_profileChangedAt"
 FROM
   "users" "UserEntity"
 WHERE
@@ -60,7 +61,8 @@ SELECT
   "user"."status" AS "user_status",
   "user"."updatedAt" AS "user_updatedAt",
   "user"."quotaSizeInBytes" AS "user_quotaSizeInBytes",
-  "user"."quotaUsageInBytes" AS "user_quotaUsageInBytes"
+  "user"."quotaUsageInBytes" AS "user_quotaUsageInBytes",
+  "user"."profileChangedAt" AS "user_profileChangedAt"
 FROM
   "users" "user"
 WHERE
@@ -82,7 +84,8 @@ SELECT
   "UserEntity"."status" AS "UserEntity_status",
   "UserEntity"."updatedAt" AS "UserEntity_updatedAt",
   "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes",
-  "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes"
+  "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes",
+  "UserEntity"."profileChangedAt" AS "UserEntity_profileChangedAt"
 FROM
   "users" "UserEntity"
 WHERE
@@ -106,7 +109,8 @@ SELECT
   "UserEntity"."status" AS "UserEntity_status",
   "UserEntity"."updatedAt" AS "UserEntity_updatedAt",
   "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes",
-  "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes"
+  "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes",
+  "UserEntity"."profileChangedAt" AS "UserEntity_profileChangedAt"
 FROM
   "users" "UserEntity"
 WHERE
@@ -136,7 +140,23 @@ SELECT
         "assets"."libraryId" IS NULL
     ),
     0
-  ) AS "usage"
+  ) AS "usage",
+  COALESCE(
+    SUM("exif"."fileSizeInByte") FILTER (
+      WHERE
+        "assets"."libraryId" IS NULL
+        AND "assets"."type" = 'IMAGE'
+    ),
+    0
+  ) AS "usagePhotos",
+  COALESCE(
+    SUM("exif"."fileSizeInByte") FILTER (
+      WHERE
+        "assets"."libraryId" IS NULL
+        AND "assets"."type" = 'VIDEO'
+    ),
+    0
+  ) AS "usageVideos"
 FROM
   "users" "users"
   LEFT JOIN "assets" "assets" ON "assets"."ownerId" = "users"."id"
diff --git a/server/src/queries/view.repository.sql b/server/src/queries/view.repository.sql
new file mode 100644
index 0000000000..e5b88ffef9
--- /dev/null
+++ b/server/src/queries/view.repository.sql
@@ -0,0 +1,79 @@
+-- NOTE: This file is auto generated by ./sql-generator
+
+-- ViewRepository.getAssetsByOriginalPath
+SELECT
+  "asset"."id" AS "asset_id",
+  "asset"."deviceAssetId" AS "asset_deviceAssetId",
+  "asset"."ownerId" AS "asset_ownerId",
+  "asset"."libraryId" AS "asset_libraryId",
+  "asset"."deviceId" AS "asset_deviceId",
+  "asset"."type" AS "asset_type",
+  "asset"."status" AS "asset_status",
+  "asset"."originalPath" AS "asset_originalPath",
+  "asset"."thumbhash" AS "asset_thumbhash",
+  "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
+  "asset"."createdAt" AS "asset_createdAt",
+  "asset"."updatedAt" AS "asset_updatedAt",
+  "asset"."deletedAt" AS "asset_deletedAt",
+  "asset"."fileCreatedAt" AS "asset_fileCreatedAt",
+  "asset"."localDateTime" AS "asset_localDateTime",
+  "asset"."fileModifiedAt" AS "asset_fileModifiedAt",
+  "asset"."isFavorite" AS "asset_isFavorite",
+  "asset"."isArchived" AS "asset_isArchived",
+  "asset"."isExternal" AS "asset_isExternal",
+  "asset"."isOffline" AS "asset_isOffline",
+  "asset"."checksum" AS "asset_checksum",
+  "asset"."duration" AS "asset_duration",
+  "asset"."isVisible" AS "asset_isVisible",
+  "asset"."livePhotoVideoId" AS "asset_livePhotoVideoId",
+  "asset"."originalFileName" AS "asset_originalFileName",
+  "asset"."sidecarPath" AS "asset_sidecarPath",
+  "asset"."stackId" AS "asset_stackId",
+  "asset"."duplicateId" AS "asset_duplicateId",
+  "exifInfo"."assetId" AS "exifInfo_assetId",
+  "exifInfo"."description" AS "exifInfo_description",
+  "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
+  "exifInfo"."exifImageHeight" AS "exifInfo_exifImageHeight",
+  "exifInfo"."fileSizeInByte" AS "exifInfo_fileSizeInByte",
+  "exifInfo"."orientation" AS "exifInfo_orientation",
+  "exifInfo"."dateTimeOriginal" AS "exifInfo_dateTimeOriginal",
+  "exifInfo"."modifyDate" AS "exifInfo_modifyDate",
+  "exifInfo"."timeZone" AS "exifInfo_timeZone",
+  "exifInfo"."latitude" AS "exifInfo_latitude",
+  "exifInfo"."longitude" AS "exifInfo_longitude",
+  "exifInfo"."projectionType" AS "exifInfo_projectionType",
+  "exifInfo"."city" AS "exifInfo_city",
+  "exifInfo"."livePhotoCID" AS "exifInfo_livePhotoCID",
+  "exifInfo"."autoStackId" AS "exifInfo_autoStackId",
+  "exifInfo"."state" AS "exifInfo_state",
+  "exifInfo"."country" AS "exifInfo_country",
+  "exifInfo"."make" AS "exifInfo_make",
+  "exifInfo"."model" AS "exifInfo_model",
+  "exifInfo"."lensModel" AS "exifInfo_lensModel",
+  "exifInfo"."fNumber" AS "exifInfo_fNumber",
+  "exifInfo"."focalLength" AS "exifInfo_focalLength",
+  "exifInfo"."iso" AS "exifInfo_iso",
+  "exifInfo"."exposureTime" AS "exifInfo_exposureTime",
+  "exifInfo"."profileDescription" AS "exifInfo_profileDescription",
+  "exifInfo"."colorspace" AS "exifInfo_colorspace",
+  "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
+  "exifInfo"."rating" AS "exifInfo_rating",
+  "exifInfo"."fps" AS "exifInfo_fps"
+FROM
+  "assets" "asset"
+  LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
+WHERE
+  (
+    (
+      "asset"."isVisible" = $1
+      AND "asset"."isArchived" = $2
+      AND "asset"."ownerId" = $3
+    )
+    AND (
+      "asset"."originalPath" LIKE $4
+      AND "asset"."originalPath" NOT LIKE $5
+    )
+  )
+  AND ("asset"."deletedAt" IS NULL)
+ORDER BY
+  regexp_replace("asset"."originalPath", '.*/(.+)', '\1') ASC
diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts
index f6921ffe27..f3cbf392db 100644
--- a/server/src/repositories/access.repository.ts
+++ b/server/src/repositories/access.repository.ts
@@ -15,7 +15,6 @@ import { StackEntity } from 'src/entities/stack.entity';
 import { TagEntity } from 'src/entities/tag.entity';
 import { AlbumUserRole } from 'src/enum';
 import { IAccessRepository } from 'src/interfaces/access.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 import { Brackets, In, Repository } from 'typeorm';
 
 type IActivityAccess = IAccessRepository['activity'];
@@ -29,7 +28,6 @@ type IStackAccess = IAccessRepository['stack'];
 type ITagAccess = IAccessRepository['tag'];
 type ITimelineAccess = IAccessRepository['timeline'];
 
-@Instrumentation()
 @Injectable()
 class ActivityAccess implements IActivityAccess {
   constructor(
diff --git a/server/src/repositories/activity.repository.ts b/server/src/repositories/activity.repository.ts
index e21f746483..0f0a0cb60e 100644
--- a/server/src/repositories/activity.repository.ts
+++ b/server/src/repositories/activity.repository.ts
@@ -3,7 +3,6 @@ import { InjectRepository } from '@nestjs/typeorm';
 import { DummyValue, GenerateSql } from 'src/decorators';
 import { ActivityEntity } from 'src/entities/activity.entity';
 import { IActivityRepository } from 'src/interfaces/activity.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 import { IsNull, Repository } from 'typeorm';
 
 export interface ActivitySearch {
@@ -13,7 +12,6 @@ export interface ActivitySearch {
   isLiked?: boolean;
 }
 
-@Instrumentation()
 @Injectable()
 export class ActivityRepository implements IActivityRepository {
   constructor(@InjectRepository(ActivityEntity) private repository: Repository<ActivityEntity>) {}
diff --git a/server/src/repositories/album-user.repository.ts b/server/src/repositories/album-user.repository.ts
index 7fd18711aa..9328ea8cfc 100644
--- a/server/src/repositories/album-user.repository.ts
+++ b/server/src/repositories/album-user.repository.ts
@@ -2,10 +2,8 @@ import { Injectable } from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
 import { AlbumUserEntity } from 'src/entities/album-user.entity';
 import { AlbumPermissionId, IAlbumUserRepository } from 'src/interfaces/album-user.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 import { Repository } from 'typeorm';
 
-@Instrumentation()
 @Injectable()
 export class AlbumUserRepository implements IAlbumUserRepository {
   constructor(@InjectRepository(AlbumUserEntity) private repository: Repository<AlbumUserEntity>) {}
diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts
index fd3a89993a..8b7565e318 100644
--- a/server/src/repositories/album.repository.ts
+++ b/server/src/repositories/album.repository.ts
@@ -4,7 +4,6 @@ import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/
 import { AlbumEntity } from 'src/entities/album.entity';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 import {
   DataSource,
   EntityManager,
@@ -23,7 +22,6 @@ const withoutDeletedUsers = <T extends AlbumEntity | null>(album: T) => {
   return album;
 };
 
-@Instrumentation()
 @Injectable()
 export class AlbumRepository implements IAlbumRepository {
   constructor(
@@ -57,22 +55,6 @@ export class AlbumRepository implements IAlbumRepository {
     return withoutDeletedUsers(album);
   }
 
-  @GenerateSql({ params: [[DummyValue.UUID]] })
-  @ChunkedArray()
-  async getByIds(ids: string[]): Promise<AlbumEntity[]> {
-    const albums = await this.repository.find({
-      where: {
-        id: In(ids),
-      },
-      relations: {
-        owner: true,
-        albumUsers: { user: true },
-      },
-    });
-
-    return albums.map((album) => withoutDeletedUsers(album));
-  }
-
   @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
   async getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]> {
     const albums = await this.repository.find({
@@ -116,34 +98,6 @@ export class AlbumRepository implements IAlbumRepository {
     }));
   }
 
-  /**
-   * Returns the album IDs that have an invalid thumbnail, when:
-   *  - Thumbnail references an asset outside the album
-   *  - Empty album still has a thumbnail set
-   */
-  @GenerateSql()
-  async getInvalidThumbnail(): Promise<string[]> {
-    // Using dataSource, because there is no direct access to albums_assets_assets.
-    const albumHasAssets = this.dataSource
-      .createQueryBuilder()
-      .select('1')
-      .from('albums_assets_assets', 'albums_assets')
-      .where('"albums"."id" = "albums_assets"."albumsId"');
-
-    const albumContainsThumbnail = albumHasAssets
-      .clone()
-      .andWhere('"albums"."albumThumbnailAssetId" = "albums_assets"."assetsId"');
-
-    const albums = await this.repository
-      .createQueryBuilder('albums')
-      .select('albums.id')
-      .where(`"albums"."albumThumbnailAssetId" IS NULL AND EXISTS (${albumHasAssets.getQuery()})`)
-      .orWhere(`"albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS (${albumContainsThumbnail.getQuery()})`)
-      .getMany();
-
-    return albums.map((album) => album.id);
-  }
-
   @GenerateSql({ params: [DummyValue.UUID] })
   async getOwned(ownerId: string): Promise<AlbumEntity[]> {
     const albums = await this.repository.find({
@@ -199,15 +153,6 @@ export class AlbumRepository implements IAlbumRepository {
     await this.repository.delete({ ownerId: userId });
   }
 
-  @GenerateSql()
-  getAll(): Promise<AlbumEntity[]> {
-    return this.repository.find({
-      relations: {
-        owner: true,
-      },
-    });
-  }
-
   @GenerateSql({ params: [DummyValue.UUID] })
   async removeAsset(assetId: string): Promise<void> {
     // Using dataSource, because there is no direct access to albums_assets_assets.
@@ -330,32 +275,26 @@ export class AlbumRepository implements IAlbumRepository {
   @GenerateSql()
   async updateThumbnails(): Promise<number | undefined> {
     // Subquery for getting a new thumbnail.
-    const newThumbnail = this.assetRepository
-      .createQueryBuilder('assets')
-      .select('albums_assets2.assetsId')
-      .addFrom('albums_assets_assets', 'albums_assets2')
-      .where('albums_assets2.assetsId = assets.id')
-      .andWhere('albums_assets2.albumsId = "albums"."id"') // Reference to albums.id outside this query
-      .orderBy('assets.fileCreatedAt', 'DESC')
-      .limit(1);
 
-    // Using dataSource, because there is no direct access to albums_assets_assets.
-    const albumHasAssets = this.dataSource
-      .createQueryBuilder()
-      .select('1')
-      .from('albums_assets_assets', 'albums_assets')
-      .where('"albums"."id" = "albums_assets"."albumsId"');
+    const builder = this.dataSource
+      .createQueryBuilder('albums_assets_assets', 'album_assets')
+      .innerJoin('assets', 'assets', '"album_assets"."assetsId" = "assets"."id"')
+      .where('"album_assets"."albumsId" = "albums"."id"');
 
-    const albumContainsThumbnail = albumHasAssets
+    const newThumbnail = builder
       .clone()
-      .andWhere('"albums"."albumThumbnailAssetId" = "albums_assets"."assetsId"');
+      .select('"album_assets"."assetsId"')
+      .orderBy('"assets"."fileCreatedAt"', 'DESC')
+      .limit(1);
+    const hasAssets = builder.clone().select('1');
+    const hasInvalidAsset = hasAssets.clone().andWhere('"albums"."albumThumbnailAssetId" = "album_assets"."assetsId"');
 
     const updateAlbums = this.repository
       .createQueryBuilder('albums')
       .update(AlbumEntity)
       .set({ albumThumbnailAssetId: () => `(${newThumbnail.getQuery()})` })
-      .where(`"albums"."albumThumbnailAssetId" IS NULL AND EXISTS (${albumHasAssets.getQuery()})`)
-      .orWhere(`"albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS (${albumContainsThumbnail.getQuery()})`);
+      .where(`"albums"."albumThumbnailAssetId" IS NULL AND EXISTS (${hasAssets.getQuery()})`)
+      .orWhere(`"albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS (${hasInvalidAsset.getQuery()})`);
 
     const result = await updateAlbums.execute();
 
diff --git a/server/src/repositories/api-key.repository.ts b/server/src/repositories/api-key.repository.ts
index 5178039177..bb37390de1 100644
--- a/server/src/repositories/api-key.repository.ts
+++ b/server/src/repositories/api-key.repository.ts
@@ -3,10 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
 import { DummyValue, GenerateSql } from 'src/decorators';
 import { APIKeyEntity } from 'src/entities/api-key.entity';
 import { IKeyRepository } from 'src/interfaces/api-key.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 import { Repository } from 'typeorm';
 
-@Instrumentation()
 @Injectable()
 export class ApiKeyRepository implements IKeyRepository {
   constructor(@InjectRepository(APIKeyEntity) private repository: Repository<APIKeyEntity>) {}
diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts
index ac9dab6fbc..33d1e2457e 100644
--- a/server/src/repositories/asset.repository.ts
+++ b/server/src/repositories/asset.repository.ts
@@ -5,20 +5,19 @@ import { AssetFileEntity } from 'src/entities/asset-files.entity';
 import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { ExifEntity } from 'src/entities/exif.entity';
-import { SmartInfoEntity } from 'src/entities/smart-info.entity';
-import { AssetFileType, AssetOrder, AssetType } from 'src/enum';
+import { AssetFileType, AssetOrder, AssetStatus, AssetType, PaginationMode } from 'src/enum';
 import {
   AssetBuilderOptions,
   AssetCreate,
   AssetDeltaSyncOptions,
   AssetExploreFieldOptions,
   AssetFullSyncOptions,
-  AssetPathEntity,
   AssetStats,
   AssetStatsOptions,
   AssetUpdateAllOptions,
   AssetUpdateDuplicateOptions,
   AssetUpdateOptions,
+  DayOfYearAssets,
   IAssetRepository,
   LivePhotoSearchOptions,
   MonthDay,
@@ -30,8 +29,7 @@ import {
 } from 'src/interfaces/asset.interface';
 import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
 import { searchAssetBuilder } from 'src/utils/database';
-import { Instrumentation } from 'src/utils/instrumentation';
-import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
+import { Paginated, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
 import {
   Brackets,
   FindOptionsOrder,
@@ -55,7 +53,6 @@ const dateTrunc = (options: TimeBucketOptions) =>
     truncateMap[options.size]
   }', (asset."localDateTime" at time zone 'UTC')) at time zone 'UTC')::timestamptz`;
 
-@Instrumentation()
 @Injectable()
 export class AssetRepository implements IAssetRepository {
   constructor(
@@ -63,7 +60,6 @@ export class AssetRepository implements IAssetRepository {
     @InjectRepository(AssetFileEntity) private fileRepository: Repository<AssetFileEntity>,
     @InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
     @InjectRepository(AssetJobStatusEntity) private jobStatusRepository: Repository<AssetJobStatusEntity>,
-    @InjectRepository(SmartInfoEntity) private smartInfoRepository: Repository<SmartInfoEntity>,
   ) {}
 
   async upsertExif(exif: Partial<ExifEntity>): Promise<void> {
@@ -79,8 +75,8 @@ export class AssetRepository implements IAssetRepository {
   }
 
   @GenerateSql({ params: [[DummyValue.UUID], { day: 1, month: 1 }] })
-  getByDayOfYear(ownerIds: string[], { day, month }: MonthDay): Promise<AssetEntity[]> {
-    return this.repository
+  async getByDayOfYear(ownerIds: string[], { day, month }: MonthDay): Promise<DayOfYearAssets[]> {
+    const assets = await this.repository
       .createQueryBuilder('entity')
       .where(
         `entity.ownerId IN (:...ownerIds)
@@ -95,9 +91,25 @@ export class AssetRepository implements IAssetRepository {
         },
       )
       .leftJoinAndSelect('entity.exifInfo', 'exifInfo')
-      .leftJoinAndSelect('entity.files', 'files')
-      .orderBy('entity.localDateTime', 'ASC')
+      .innerJoinAndSelect('entity.files', 'files')
+      .andWhere('files.type = :type', { type: AssetFileType.THUMBNAIL })
+      .andWhere(
+        `EXTRACT(YEAR FROM CURRENT_DATE AT TIME ZONE 'UTC') - EXTRACT(YEAR FROM entity.localDateTime AT TIME ZONE 'UTC') > 0`,
+      )
+      .orderBy('entity.fileCreatedAt', 'ASC')
       .getMany();
+
+    const groups: Record<number, DayOfYearAssets> = {};
+    const currentYear = new Date().getFullYear();
+    for (const asset of assets) {
+      const yearsAgo = currentYear - asset.localDateTime.getFullYear();
+      if (!groups[yearsAgo]) {
+        groups[yearsAgo] = { yearsAgo, assets: [] };
+      }
+      groups[yearsAgo].assets.push(asset);
+    }
+
+    return Object.values(groups);
   }
 
   @GenerateSql({ params: [[DummyValue.UUID]] })
@@ -122,7 +134,6 @@ export class AssetRepository implements IAssetRepository {
       where: { id: In(ids) },
       relations: {
         exifInfo: true,
-        smartInfo: true,
         tags: true,
         faces: {
           person: true,
@@ -177,14 +188,6 @@ export class AssetRepository implements IAssetRepository {
     return this.getAll(pagination, { ...options, userIds: [userId] });
   }
 
-  @GenerateSql({ params: [{ take: 1, skip: 0 }, DummyValue.UUID] })
-  getExternalLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated<AssetPathEntity> {
-    return paginate(this.repository, pagination, {
-      select: { id: true, originalPath: true, isOffline: true },
-      where: { library: { id: libraryId }, isExternal: true },
-    });
-  }
-
   @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
   getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise<AssetEntity | null> {
     return this.repository.findOne({
@@ -198,24 +201,16 @@ export class AssetRepository implements IAssetRepository {
   async getPathsNotInLibrary(libraryId: string, originalPaths: string[]): Promise<string[]> {
     const result = await this.repository.query(
       `
-      WITH paths AS (SELECT unnest($2::text[]) AS path)
-      SELECT path FROM paths
-      WHERE NOT EXISTS (SELECT 1 FROM assets WHERE "libraryId" = $1 AND "originalPath" = path);
-    `,
+        WITH paths AS (SELECT unnest($2::text[]) AS path)
+        SELECT path
+        FROM paths
+        WHERE NOT EXISTS (SELECT 1 FROM assets WHERE "libraryId" = $1 AND "originalPath" = path);
+      `,
       [libraryId, originalPaths],
     );
     return result.map((row: { path: string }) => row.path);
   }
 
-  @GenerateSql({ params: [DummyValue.UUID, [DummyValue.STRING]] })
-  @ChunkedArray({ paramIndex: 1 })
-  async updateOfflineLibraryAssets(libraryId: string, originalPaths: string[]): Promise<void> {
-    await this.repository.update(
-      { library: { id: libraryId }, originalPath: Not(In(originalPaths)), isOffline: false },
-      { isOffline: true },
-    );
-  }
-
   getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated<AssetEntity> {
     let builder = this.repository.createQueryBuilder('asset').leftJoinAndSelect('asset.files', 'files');
     builder = searchAssetBuilder(builder, options);
@@ -295,16 +290,6 @@ export class AssetRepository implements IAssetRepository {
       .execute();
   }
 
-  @Chunked()
-  async softDeleteAll(ids: string[]): Promise<void> {
-    await this.repository.softDelete({ id: In(ids) });
-  }
-
-  @Chunked()
-  async restoreAll(ids: string[]): Promise<void> {
-    await this.repository.restore({ id: In(ids) });
-  }
-
   async update(asset: AssetUpdateOptions): Promise<void> {
     await this.repository.update(asset.id, asset);
   }
@@ -383,12 +368,10 @@ export class AssetRepository implements IAssetRepository {
   }
 
   @GenerateSql(
-    ...Object.values(WithProperty)
-      .filter((property) => property !== WithProperty.IS_OFFLINE && property !== WithProperty.IS_ONLINE)
-      .map((property) => ({
-        name: property,
-        params: [DummyValue.PAGINATION, property],
-      })),
+    ...Object.values(WithProperty).map((property) => ({
+      name: property,
+      params: [DummyValue.PAGINATION, property],
+    })),
   )
   getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity> {
     let relations: FindOptionsRelations<AssetEntity> = {};
@@ -453,22 +436,6 @@ export class AssetRepository implements IAssetRepository {
         break;
       }
 
-      case WithoutProperty.OBJECT_TAGS: {
-        relations = {
-          smartInfo: true,
-        };
-        where = {
-          jobStatus: {
-            previewAt: Not(IsNull()),
-          },
-          isVisible: true,
-          smartInfo: {
-            tags: IsNull(),
-          },
-        };
-        break;
-      }
-
       case WithoutProperty.FACES: {
         relations = {
           faces: true,
@@ -488,23 +455,6 @@ export class AssetRepository implements IAssetRepository {
         break;
       }
 
-      case WithoutProperty.PERSON: {
-        relations = {
-          faces: true,
-        };
-        where = {
-          jobStatus: {
-            previewAt: Not(IsNull()),
-          },
-          isVisible: true,
-          faces: {
-            assetId: Not(IsNull()),
-            personId: IsNull(),
-          },
-        };
-        break;
-      }
-
       case WithoutProperty.SIDECAR: {
         where = [
           { sidecarPath: IsNull(), isVisible: true },
@@ -528,56 +478,6 @@ export class AssetRepository implements IAssetRepository {
     });
   }
 
-  getWith(
-    pagination: PaginationOptions,
-    property: WithProperty,
-    libraryId?: string,
-    withDeleted = false,
-  ): Paginated<AssetEntity> {
-    let where: FindOptionsWhere<AssetEntity> | FindOptionsWhere<AssetEntity>[] = {};
-
-    switch (property) {
-      case WithProperty.SIDECAR: {
-        where = [{ sidecarPath: Not(IsNull()), isVisible: true }];
-        break;
-      }
-      case WithProperty.IS_OFFLINE: {
-        if (!libraryId) {
-          throw new Error('Library id is required when finding offline assets');
-        }
-        where = [{ isOffline: true, libraryId }];
-        break;
-      }
-      case WithProperty.IS_ONLINE: {
-        if (!libraryId) {
-          throw new Error('Library id is required when finding online assets');
-        }
-        where = [{ isOffline: false, libraryId }];
-        break;
-      }
-
-      default: {
-        throw new Error(`Invalid getWith property: ${property}`);
-      }
-    }
-
-    return paginate(this.repository, pagination, {
-      where,
-      withDeleted,
-      order: {
-        // Ensures correct order when paginating
-        createdAt: 'ASC',
-      },
-    });
-  }
-
-  getFirstAssetForAlbumId(albumId: string): Promise<AssetEntity | null> {
-    return this.repository.findOne({
-      where: { albums: { id: albumId } },
-      order: { fileCreatedAt: 'DESC' },
-    });
-  }
-
   getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null> {
     return this.repository.findOne({
       where: { albums: { id: albumId } },
@@ -604,7 +504,10 @@ export class AssetRepository implements IAssetRepository {
     }
 
     if (isTrashed !== undefined) {
-      builder.withDeleted().andWhere(`asset.deletedAt is not null`);
+      builder
+        .withDeleted()
+        .andWhere(`asset.deletedAt is not null`)
+        .andWhere('asset.status = :status', { status: AssetStatus.TRASHED });
     }
 
     const items = await builder.getRawMany();
@@ -689,35 +592,6 @@ export class AssetRepository implements IAssetRepository {
     return { fieldName: 'exifInfo.city', items };
   }
 
-  @GenerateSql({ params: [DummyValue.UUID, { minAssetsPerField: 5, maxFields: 12 }] })
-  async getAssetIdByTag(
-    ownerId: string,
-    { minAssetsPerField, maxFields }: AssetExploreFieldOptions,
-  ): Promise<SearchExploreItem<string>> {
-    const cte = this.smartInfoRepository
-      .createQueryBuilder('si')
-      .select('unnest(tags)', 'tag')
-      .groupBy('tag')
-      .having('count(*) >= :minAssetsPerField', { minAssetsPerField });
-
-    const items = await this.getBuilder({
-      userIds: [ownerId],
-      exifInfo: false,
-      assetType: AssetType.IMAGE,
-      isArchived: false,
-    })
-      .select('unnest(si.tags)', 'value')
-      .addSelect('asset.id', 'data')
-      .distinctOn(['unnest(si.tags)'])
-      .innerJoin('smart_info', 'si', 'asset.id = si."assetId"')
-      .addCommonTableExpression(cte, 'random_tags')
-      .innerJoin('random_tags', 't', 'si.tags @> ARRAY[t.tag]')
-      .limit(maxFields)
-      .getRawMany();
-
-    return { fieldName: 'smartInfo.tags', items };
-  }
-
   private getBuilder(options: AssetBuilderOptions) {
     const builder = this.repository.createQueryBuilder('asset').where('asset.isVisible = true');
 
@@ -762,6 +636,13 @@ export class AssetRepository implements IAssetRepository {
 
     if (options.isTrashed !== undefined) {
       builder.andWhere(`asset.deletedAt ${options.isTrashed ? 'IS NOT NULL' : 'IS NULL'}`).withDeleted();
+
+      if (options.isTrashed) {
+        // TODO: Temporarily inverted to support showing offline assets in the trash queries.
+        // Once offline assets are handled in a separate screen, this should be set back to status = TRASHED
+        // and the offline screens should use a separate isOffline = true parameter in the timeline query.
+        builder.andWhere('asset.status != :status', { status: AssetStatus.DELETED });
+      }
     }
 
     if (options.isDuplicate !== undefined) {
@@ -836,52 +717,13 @@ export class AssetRepository implements IAssetRepository {
     return builder.getMany();
   }
 
-  async getUniqueOriginalPaths(userId: string): Promise<string[]> {
-    const builder = this.getBuilder({
-      userIds: [userId],
-      exifInfo: false,
-      withStacked: false,
-      isArchived: false,
-      isTrashed: false,
-    });
-
-    const results = await builder
-      .select("DISTINCT substring(asset.originalPath FROM '^(.*/)[^/]*$')", 'directoryPath')
-      .getRawMany();
-
-    return results.map((row: { directoryPath: string }) => row.directoryPath.replaceAll(/^\/|\/$/g, ''));
-  }
-
-  @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
-  async getAssetsByOriginalPath(userId: string, partialPath: string): Promise<AssetEntity[]> {
-    const normalizedPath = partialPath.replaceAll(/^\/|\/$/g, '');
-
-    const builder = this.getBuilder({
-      userIds: [userId],
-      exifInfo: true,
-      withStacked: false,
-      isArchived: false,
-      isTrashed: false,
-    });
-
-    const assets = await builder
-      .where('asset.ownerId = :userId', { userId })
-      .andWhere(
-        new Brackets((qb) => {
-          qb.where('asset.originalPath LIKE :likePath', { likePath: `%${normalizedPath}/%` }).andWhere(
-            'asset.originalPath NOT LIKE :notLikePath',
-            { notLikePath: `%${normalizedPath}/%/%` },
-          );
-        }),
-      )
-      .orderBy(String.raw`regexp_replace(asset.originalPath, '.*/(.+)', '\1')`, 'ASC')
-      .getMany();
-
-    return assets;
+  @GenerateSql({ params: [{ assetId: DummyValue.UUID, type: AssetFileType.PREVIEW, path: '/path/to/file' }] })
+  async upsertFile(file: { assetId: string; type: AssetFileType; path: string }): Promise<void> {
+    await this.fileRepository.upsert(file, { conflictPaths: ['assetId', 'type'] });
   }
 
   @GenerateSql({ params: [{ assetId: DummyValue.UUID, type: AssetFileType.PREVIEW, path: '/path/to/file' }] })
-  async upsertFile({ assetId, type, path }: { assetId: string; type: AssetFileType; path: string }): Promise<void> {
-    await this.fileRepository.upsert({ assetId, type, path }, { conflictPaths: ['assetId', 'type'] });
+  async upsertFiles(files: { assetId: string; type: AssetFileType; path: string }[]): Promise<void> {
+    await this.fileRepository.upsert(files, { conflictPaths: ['assetId', 'type'] });
   }
 }
diff --git a/server/src/repositories/audit.repository.ts b/server/src/repositories/audit.repository.ts
index deb0d0f6f1..ac73c3a8b9 100644
--- a/server/src/repositories/audit.repository.ts
+++ b/server/src/repositories/audit.repository.ts
@@ -2,10 +2,8 @@ import { Injectable } from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
 import { AuditEntity } from 'src/entities/audit.entity';
 import { AuditSearch, IAuditRepository } from 'src/interfaces/audit.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 import { In, LessThan, MoreThan, Repository } from 'typeorm';
 
-@Instrumentation()
 @Injectable()
 export class AuditRepository implements IAuditRepository {
   constructor(@InjectRepository(AuditEntity) private repository: Repository<AuditEntity>) {}
diff --git a/server/src/repositories/config.repository.spec.ts b/server/src/repositories/config.repository.spec.ts
new file mode 100644
index 0000000000..aa7fb87ac5
--- /dev/null
+++ b/server/src/repositories/config.repository.spec.ts
@@ -0,0 +1,267 @@
+import { ImmichTelemetry } from 'src/enum';
+import { clearEnvCache, ConfigRepository } from 'src/repositories/config.repository';
+
+const getEnv = () => {
+  clearEnvCache();
+  return new ConfigRepository().getEnv();
+};
+
+const resetEnv = () => {
+  for (const env of [
+    'IMMICH_ENV',
+    'IMMICH_WORKERS_INCLUDE',
+    'IMMICH_WORKERS_EXCLUDE',
+    'IMMICH_TRUSTED_PROXIES',
+    'IMMICH_API_METRICS_PORT',
+    'IMMICH_MICROSERVICES_METRICS_PORT',
+    'IMMICH_TELEMETRY_INCLUDE',
+    'IMMICH_TELEMETRY_EXCLUDE',
+
+    'DB_URL',
+    'DB_HOSTNAME',
+    'DB_PORT',
+    'DB_USERNAME',
+    'DB_PASSWORD',
+    'DB_DATABASE_NAME',
+    'DB_SKIP_MIGRATIONS',
+    'DB_VECTOR_EXTENSION',
+
+    'REDIS_HOSTNAME',
+    'REDIS_PORT',
+    'REDIS_DBINDEX',
+    'REDIS_USERNAME',
+    'REDIS_PASSWORD',
+    'REDIS_SOCKET',
+    'REDIS_URL',
+
+    'NO_COLOR',
+  ]) {
+    delete process.env[env];
+  }
+};
+
+const sentinelConfig = {
+  sentinels: [
+    {
+      host: 'redis-sentinel-node-0',
+      port: 26_379,
+    },
+    {
+      host: 'redis-sentinel-node-1',
+      port: 26_379,
+    },
+    {
+      host: 'redis-sentinel-node-2',
+      port: 26_379,
+    },
+  ],
+  name: 'redis-sentinel',
+};
+
+describe('getEnv', () => {
+  beforeEach(() => {
+    resetEnv();
+  });
+
+  it('should use defaults', () => {
+    const config = getEnv();
+
+    expect(config).toMatchObject({
+      host: undefined,
+      port: 2283,
+      environment: 'production',
+      configFile: undefined,
+      logLevel: undefined,
+    });
+  });
+
+  describe('database', () => {
+    it('should use defaults', () => {
+      const { database } = getEnv();
+      expect(database).toEqual({
+        config: expect.objectContaining({
+          type: 'postgres',
+          host: 'database',
+          port: 5432,
+          database: 'immich',
+          username: 'postgres',
+          password: 'postgres',
+        }),
+        skipMigrations: false,
+        vectorExtension: 'vectors',
+      });
+    });
+
+    it('should allow skipping migrations', () => {
+      process.env.DB_SKIP_MIGRATIONS = 'true';
+      const { database } = getEnv();
+      expect(database).toMatchObject({ skipMigrations: true });
+    });
+  });
+
+  describe('redis', () => {
+    it('should use defaults', () => {
+      const { redis } = getEnv();
+      expect(redis).toEqual({
+        host: 'redis',
+        port: 6379,
+        db: 0,
+        username: undefined,
+        password: undefined,
+        path: undefined,
+      });
+    });
+
+    it('should parse base64 encoded config, ignore other env', () => {
+      process.env.REDIS_URL = `ioredis://${Buffer.from(JSON.stringify(sentinelConfig)).toString('base64')}`;
+      process.env.REDIS_HOSTNAME = 'redis-host';
+      process.env.REDIS_USERNAME = 'redis-user';
+      process.env.REDIS_PASSWORD = 'redis-password';
+      const { redis } = getEnv();
+      expect(redis).toEqual(sentinelConfig);
+    });
+
+    it('should reject invalid json', () => {
+      process.env.REDIS_URL = `ioredis://${Buffer.from('{ "invalid json"').toString('base64')}`;
+      expect(() => getEnv()).toThrowError('Failed to decode redis options');
+    });
+  });
+
+  describe('noColor', () => {
+    beforeEach(() => {
+      delete process.env.NO_COLOR;
+    });
+
+    it('should default noColor to false', () => {
+      const { noColor } = getEnv();
+      expect(noColor).toBe(false);
+    });
+
+    it('should map NO_COLOR=1 to true', () => {
+      process.env.NO_COLOR = '1';
+      const { noColor } = getEnv();
+      expect(noColor).toBe(true);
+    });
+
+    it('should map NO_COLOR=true to true', () => {
+      process.env.NO_COLOR = 'true';
+      const { noColor } = getEnv();
+      expect(noColor).toBe(true);
+    });
+  });
+
+  describe('workers', () => {
+    it('should return default workers', () => {
+      const { workers } = getEnv();
+      expect(workers).toEqual(['api', 'microservices']);
+    });
+
+    it('should return included workers', () => {
+      process.env.IMMICH_WORKERS_INCLUDE = 'api';
+      const { workers } = getEnv();
+      expect(workers).toEqual(['api']);
+    });
+
+    it('should excluded workers from defaults', () => {
+      process.env.IMMICH_WORKERS_EXCLUDE = 'api';
+      const { workers } = getEnv();
+      expect(workers).toEqual(['microservices']);
+    });
+
+    it('should exclude workers from include list', () => {
+      process.env.IMMICH_WORKERS_INCLUDE = 'api,microservices,randomservice';
+      process.env.IMMICH_WORKERS_EXCLUDE = 'randomservice,microservices';
+      const { workers } = getEnv();
+      expect(workers).toEqual(['api']);
+    });
+
+    it('should remove whitespace from included workers before parsing', () => {
+      process.env.IMMICH_WORKERS_INCLUDE = 'api, microservices';
+      const { workers } = getEnv();
+      expect(workers).toEqual(['api', 'microservices']);
+    });
+
+    it('should remove whitespace from excluded workers before parsing', () => {
+      process.env.IMMICH_WORKERS_EXCLUDE = 'api, microservices';
+      const { workers } = getEnv();
+      expect(workers).toEqual([]);
+    });
+
+    it('should remove whitespace from included and excluded workers before parsing', () => {
+      process.env.IMMICH_WORKERS_INCLUDE = 'api, microservices, randomservice,randomservice2';
+      process.env.IMMICH_WORKERS_EXCLUDE = 'randomservice,microservices, randomservice2';
+      const { workers } = getEnv();
+      expect(workers).toEqual(['api']);
+    });
+
+    it('should throw error for invalid workers', () => {
+      process.env.IMMICH_WORKERS_INCLUDE = 'api,microservices,randomservice';
+      expect(getEnv).toThrowError('Invalid worker(s) found: api,microservices,randomservice');
+    });
+  });
+
+  describe('network', () => {
+    it('should return default network options', () => {
+      const { network } = getEnv();
+      expect(network).toEqual({
+        trustedProxies: ['linklocal', 'uniquelocal'],
+      });
+    });
+
+    it('should parse trusted proxies', () => {
+      process.env.IMMICH_TRUSTED_PROXIES = '10.1.0.0,10.2.0.0, 169.254.0.0/16';
+      const { network } = getEnv();
+      expect(network).toEqual({
+        trustedProxies: ['10.1.0.0', '10.2.0.0', '169.254.0.0/16'],
+      });
+    });
+
+    it('should reject invalid trusted proxies', () => {
+      process.env.IMMICH_TRUSTED_PROXIES = '10.1';
+      expect(() => getEnv()).toThrowError('Invalid environment variables: IMMICH_TRUSTED_PROXIES');
+    });
+  });
+
+  describe('telemetry', () => {
+    it('should have default values', () => {
+      const { telemetry } = getEnv();
+      expect(telemetry).toEqual({
+        apiPort: 8081,
+        microservicesPort: 8082,
+        metrics: new Set([]),
+      });
+    });
+
+    it('should parse custom ports', () => {
+      process.env.IMMICH_API_METRICS_PORT = '2001';
+      process.env.IMMICH_MICROSERVICES_METRICS_PORT = '2002';
+      const { telemetry } = getEnv();
+      expect(telemetry).toMatchObject({
+        apiPort: 2001,
+        microservicesPort: 2002,
+        metrics: expect.any(Set),
+      });
+    });
+
+    it('should run with telemetry enabled', () => {
+      process.env.IMMICH_TELEMETRY_INCLUDE = 'all';
+      const { telemetry } = getEnv();
+      expect(telemetry.metrics).toEqual(new Set(Object.values(ImmichTelemetry)));
+    });
+
+    it('should run with telemetry enabled and jobs disabled', () => {
+      process.env.IMMICH_TELEMETRY_INCLUDE = 'all';
+      process.env.IMMICH_TELEMETRY_EXCLUDE = 'job';
+      const { telemetry } = getEnv();
+      expect(telemetry.metrics).toEqual(
+        new Set([ImmichTelemetry.API, ImmichTelemetry.HOST, ImmichTelemetry.IO, ImmichTelemetry.REPO]),
+      );
+    });
+
+    it('should run with specific telemetry metrics', () => {
+      process.env.IMMICH_TELEMETRY_INCLUDE = 'io, host, api';
+      const { telemetry } = getEnv();
+      expect(telemetry.metrics).toEqual(new Set([ImmichTelemetry.API, ImmichTelemetry.HOST, ImmichTelemetry.IO]));
+    });
+  });
+});
diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts
new file mode 100644
index 0000000000..cc05fd927c
--- /dev/null
+++ b/server/src/repositories/config.repository.ts
@@ -0,0 +1,246 @@
+import { Inject, Injectable, Optional } from '@nestjs/common';
+import { plainToInstance } from 'class-transformer';
+import { validateSync } from 'class-validator';
+import { Request, Response } from 'express';
+import { CLS_ID } from 'nestjs-cls';
+import { join, resolve } from 'node:path';
+import { citiesFile, excludePaths, IWorker } from 'src/constants';
+import { Telemetry } from 'src/decorators';
+import { EnvDto } from 'src/dtos/env.dto';
+import { ImmichEnvironment, ImmichHeader, ImmichTelemetry, ImmichWorker } from 'src/enum';
+import { EnvData, IConfigRepository } from 'src/interfaces/config.interface';
+import { DatabaseExtension } from 'src/interfaces/database.interface';
+import { QueueName } from 'src/interfaces/job.interface';
+import { setDifference } from 'src/utils/set';
+
+const productionKeys = {
+  client:
+    'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF2LzdTMzJjUkE1KysxTm5WRHNDTQpzcFAvakpISU1xT0pYRm5oNE53QTJPcHorUk1mZGNvOTJQc09naCt3d1FlRXYxVTJjMnBqelRpUS8ybHJLcS9rCnpKUmxYd2M0Y1Vlc1FETUpPRitQMnFPTlBiQUprWHZDWFlCVUxpdENJa29Md2ZoU0dOanlJS2FSRGhkL3ROeU4KOCtoTlJabllUMWhTSWo5U0NrS3hVQ096YXRQVjRtQ0RlclMrYkUrZ0VVZVdwOTlWOWF6dkYwRkltblRXcFFTdwpjOHdFWmdPTWg0c3ZoNmFpY3dkemtQQ3dFTGFrMFZhQkgzMUJFVUNRTGI5K0FJdEhBVXRKQ0t4aGI1V2pzMXM5CmJyWGZpMHZycGdjWi82RGFuWTJxZlNQem5PbXZEMkZycmxTMXE0SkpOM1ZvN1d3LzBZeS95TWNtelRXWmhHdWgKVVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo=',
+  server:
+    'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFvcG5ZRGEwYS9kVTVJZUc3NGlFRQpNd2RBS2pzTmN6TGRDcVJkMVo5eTVUMndqTzdlWUlPZUpUc2wzNTBzUjBwNEtmU1VEU1h2QzlOcERwYzF0T0tsCjVzaEMvQXhwdlFBTENva0Y0anQ4dnJyZDlmQ2FYYzFUcVJiT21uaGl1Z0Q2dmtyME8vRmIzVURpM1UwVHZoUFAKbFBkdlNhd3pMcldaUExmbUhWVnJiclNLbW45SWVTZ3kwN3VrV1RJeUxzY2lOcnZuQnl3c0phUmVEdW9OV1BCSApVL21vMm1YYThtNHdNV2hpWGVoaUlPUXFNdVNVZ1BlQ3NXajhVVngxQ0dsUnpQREEwYlZOUXZlS1hXVnhjRUk2ClVMRWdKeTJGNDlsSDArYVlDbUJmN05FcjZWUTJXQjk1ZXZUS1hLdm4wcUlNN25nRmxjVUF3NmZ1VjFjTkNUSlMKNndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo=',
+};
+
+const stagingKeys = {
+  client:
+    'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFuSUNyTm5jbGpPSC9JdTNtWVVaRQp0dGJLV1c3OGRuajl5M0U2ekk3dU1NUndEckdYWFhkTGhkUDFxSWtlZHh0clVVeUpCMWR4R04yQW91S082MlNGCldrbU9PTmNGQlRBWFZTdjhUNVY0S0VwWnFQYWEwaXpNaGxMaE5sRXEvY1ZKdllrWlh1Z2x6b1o3cG1nbzFSdHgKam1iRm5NNzhrYTFRUUJqOVdLaEw2eWpWRUl2MDdVS0lKWHBNTnNuS2g1V083MjZhYmMzSE9udTlETjY5VnFFRQo3dGZrUnRWNmx2U1NzMkFVMngzT255cHA4ek53b0lPTWRibGsyb09aWWROZzY0Y3l2SzJoU0FlU3NVMFRyOVc5Ckgra0Y5QlNCNlk0QXl0QlVkSmkrK2pMSW5HM2Q5cU9ieFVzTlYrN05mRkF5NjJkL0xNR0xSOC9OUFc0U0s3c0MKRlFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo=',
+  server:
+    'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUE3Sy8yd3ZLUS9NdU8ydi9MUm5saAoyUy9zTHhDOGJiTEw1UUlKOGowQ3BVZW40YURlY2dYMUpKUmtGNlpUVUtpNTdTbEhtS3RSM2JOTzJmdTBUUVg5Ck5WMEJzVzllZVB0MmlTMWl4VVFmTzRObjdvTjZzbEtac01qd29RNGtGRGFmM3VHTlZJc0dMb3UxVWRLUVhpeDEKUlRHcXVTb3NZVjNWRlk3Q1hGYTVWaENBL3poVXNsNGFuVXp3eEF6M01jUFVlTXBaenYvbVZiQlRKVzBPSytWZgpWQUJvMXdYMkVBanpBekVHVzQ3Vko4czhnMnQrNHNPaHFBNStMQjBKVzlORUg5QUpweGZzWE4zSzVtM00yNUJVClZXcTlRYStIdHRENnJ0bnAvcUFweXVkWUdwZk9HYTRCUlZTR1MxMURZM0xrb2FlRzYwUEU5NHpoYjduOHpMWkgKelFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo=',
+};
+
+const WORKER_TYPES = new Set(Object.values(ImmichWorker));
+const TELEMETRY_TYPES = new Set(Object.values(ImmichTelemetry));
+
+const asSet = <T>(value: string | undefined, defaults: T[]) => {
+  const values = (value || '').replaceAll(/\s/g, '').split(',').filter(Boolean);
+  return new Set(values.length === 0 ? defaults : (values as T[]));
+};
+
+const getEnv = (): EnvData => {
+  const dto = plainToInstance(EnvDto, process.env);
+  const errors = validateSync(dto);
+  if (errors.length > 0) {
+    throw new Error(
+      `Invalid environment variables: ${errors.map((error) => `${error.property}=${error.value}`).join(', ')}`,
+    );
+  }
+
+  const includedWorkers = asSet(dto.IMMICH_WORKERS_INCLUDE, [ImmichWorker.API, ImmichWorker.MICROSERVICES]);
+  const excludedWorkers = asSet(dto.IMMICH_WORKERS_EXCLUDE, []);
+  const workers = [...setDifference(includedWorkers, excludedWorkers)];
+  for (const worker of workers) {
+    if (!WORKER_TYPES.has(worker)) {
+      throw new Error(`Invalid worker(s) found: ${workers.join(',')}`);
+    }
+  }
+
+  const environment = dto.IMMICH_ENV || ImmichEnvironment.PRODUCTION;
+  const isProd = environment === ImmichEnvironment.PRODUCTION;
+  const buildFolder = dto.IMMICH_BUILD_DATA || '/build';
+  const folders = {
+    // eslint-disable-next-line unicorn/prefer-module
+    dist: resolve(`${__dirname}/..`),
+    geodata: join(buildFolder, 'geodata'),
+    web: join(buildFolder, 'www'),
+  };
+
+  const databaseUrl = dto.DB_URL;
+
+  let redisConfig = {
+    host: dto.REDIS_HOSTNAME || 'redis',
+    port: dto.REDIS_PORT || 6379,
+    db: dto.REDIS_DBINDEX || 0,
+    username: dto.REDIS_USERNAME || undefined,
+    password: dto.REDIS_PASSWORD || undefined,
+    path: dto.REDIS_SOCKET || undefined,
+  };
+
+  const redisUrl = dto.REDIS_URL;
+  if (redisUrl && redisUrl.startsWith('ioredis://')) {
+    try {
+      redisConfig = JSON.parse(Buffer.from(redisUrl.slice(10), 'base64').toString());
+    } catch (error) {
+      throw new Error(`Failed to decode redis options: ${error}`);
+    }
+  }
+
+  const includedTelemetries =
+    dto.IMMICH_TELEMETRY_INCLUDE === 'all'
+      ? new Set(Object.values(ImmichTelemetry))
+      : asSet<ImmichTelemetry>(dto.IMMICH_TELEMETRY_INCLUDE, []);
+
+  const excludedTelemetries = asSet<ImmichTelemetry>(dto.IMMICH_TELEMETRY_EXCLUDE, []);
+  const telemetries = setDifference(includedTelemetries, excludedTelemetries);
+  for (const telemetry of telemetries) {
+    if (!TELEMETRY_TYPES.has(telemetry)) {
+      throw new Error(`Invalid telemetry found: ${telemetry}`);
+    }
+  }
+
+  return {
+    host: dto.IMMICH_HOST,
+    port: dto.IMMICH_PORT || 2283,
+    environment,
+    configFile: dto.IMMICH_CONFIG_FILE,
+    logLevel: dto.IMMICH_LOG_LEVEL,
+
+    buildMetadata: {
+      build: dto.IMMICH_BUILD,
+      buildUrl: dto.IMMICH_BUILD_URL,
+      buildImage: dto.IMMICH_BUILD_IMAGE,
+      buildImageUrl: dto.IMMICH_BUILD_IMAGE_URL,
+      repository: dto.IMMICH_REPOSITORY,
+      repositoryUrl: dto.IMMICH_REPOSITORY_URL,
+      sourceRef: dto.IMMICH_SOURCE_REF,
+      sourceCommit: dto.IMMICH_SOURCE_COMMIT,
+      sourceUrl: dto.IMMICH_SOURCE_URL,
+      thirdPartySourceUrl: dto.IMMICH_THIRD_PARTY_SOURCE_URL,
+      thirdPartyBugFeatureUrl: dto.IMMICH_THIRD_PARTY_BUG_FEATURE_URL,
+      thirdPartyDocumentationUrl: dto.IMMICH_THIRD_PARTY_DOCUMENTATION_URL,
+      thirdPartySupportUrl: dto.IMMICH_THIRD_PARTY_SUPPORT_URL,
+    },
+
+    bull: {
+      config: {
+        prefix: 'immich_bull',
+        connection: { ...redisConfig },
+        defaultJobOptions: {
+          attempts: 3,
+          removeOnComplete: true,
+          removeOnFail: false,
+        },
+      },
+      queues: Object.values(QueueName).map((name) => ({ name })),
+    },
+
+    cls: {
+      config: {
+        middleware: {
+          mount: true,
+          generateId: true,
+          setup: (cls, req: Request, res: Response) => {
+            const headerValues = req.headers[ImmichHeader.CID];
+            const headerValue = Array.isArray(headerValues) ? headerValues[0] : headerValues;
+            const cid = headerValue || cls.get(CLS_ID);
+            cls.set(CLS_ID, cid);
+            res.header(ImmichHeader.CID, cid);
+          },
+        },
+      },
+    },
+
+    database: {
+      config: {
+        type: 'postgres',
+        entities: [`${folders.dist}/entities` + '/*.entity.{js,ts}'],
+        migrations: [`${folders.dist}/migrations` + '/*.{js,ts}'],
+        subscribers: [`${folders.dist}/subscribers` + '/*.{js,ts}'],
+        migrationsRun: false,
+        synchronize: false,
+        connectTimeoutMS: 10_000, // 10 seconds
+        parseInt8: true,
+        ...(databaseUrl
+          ? { connectionType: 'url', url: databaseUrl }
+          : {
+              connectionType: 'parts',
+              host: dto.DB_HOSTNAME || 'database',
+              port: dto.DB_PORT || 5432,
+              username: dto.DB_USERNAME || 'postgres',
+              password: dto.DB_PASSWORD || 'postgres',
+              database: dto.DB_DATABASE_NAME || 'immich',
+            }),
+      },
+
+      skipMigrations: dto.DB_SKIP_MIGRATIONS ?? false,
+      vectorExtension: dto.DB_VECTOR_EXTENSION === 'pgvector' ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS,
+    },
+
+    licensePublicKey: isProd ? productionKeys : stagingKeys,
+
+    network: {
+      trustedProxies: dto.IMMICH_TRUSTED_PROXIES ?? ['linklocal', 'uniquelocal'],
+    },
+
+    otel: {
+      metrics: {
+        hostMetrics: telemetries.has(ImmichTelemetry.HOST),
+        apiMetrics: {
+          enable: telemetries.has(ImmichTelemetry.API),
+          ignoreRoutes: excludePaths,
+        },
+      },
+    },
+
+    redis: redisConfig,
+
+    resourcePaths: {
+      lockFile: join(buildFolder, 'build-lock.json'),
+      geodata: {
+        dateFile: join(folders.geodata, 'geodata-date.txt'),
+        admin1: join(folders.geodata, 'admin1CodesASCII.txt'),
+        admin2: join(folders.geodata, 'admin2Codes.txt'),
+        cities500: join(folders.geodata, citiesFile),
+        naturalEarthCountriesPath: join(folders.geodata, 'ne_10m_admin_0_countries.geojson'),
+      },
+      web: {
+        root: folders.web,
+        indexHtml: join(folders.web, 'index.html'),
+      },
+    },
+
+    storage: {
+      ignoreMountCheckErrors: !!dto.IMMICH_IGNORE_MOUNT_CHECK_ERRORS,
+    },
+
+    telemetry: {
+      apiPort: dto.IMMICH_API_METRICS_PORT || 8081,
+      microservicesPort: dto.IMMICH_MICROSERVICES_METRICS_PORT || 8082,
+      metrics: telemetries,
+    },
+
+    workers,
+
+    noColor: !!dto.NO_COLOR,
+  };
+};
+
+let cached: EnvData | undefined;
+
+@Injectable()
+@Telemetry({ enabled: false })
+export class ConfigRepository implements IConfigRepository {
+  constructor(@Inject(IWorker) @Optional() private worker?: ImmichWorker) {}
+
+  getEnv(): EnvData {
+    if (!cached) {
+      cached = getEnv();
+    }
+
+    return cached;
+  }
+
+  getWorker() {
+    return this.worker;
+  }
+}
+
+export const clearEnvCache = () => (cached = undefined);
diff --git a/server/src/repositories/cron.repository.ts b/server/src/repositories/cron.repository.ts
new file mode 100644
index 0000000000..fd7589a034
--- /dev/null
+++ b/server/src/repositories/cron.repository.ts
@@ -0,0 +1,52 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { SchedulerRegistry } from '@nestjs/schedule';
+import { CronJob, CronTime } from 'cron';
+import { CronCreate, CronUpdate, ICronRepository } from 'src/interfaces/cron.interface';
+import { ILoggerRepository } from 'src/interfaces/logger.interface';
+
+@Injectable()
+export class CronRepository implements ICronRepository {
+  constructor(
+    private schedulerRegistry: SchedulerRegistry,
+    @Inject(ILoggerRepository) private logger: ILoggerRepository,
+  ) {
+    this.logger.setContext(CronRepository.name);
+  }
+
+  create({ name, expression, onTick, start = true }: CronCreate): void {
+    const job = new CronJob<null, null>(
+      expression,
+      onTick,
+      // function to run onComplete
+      undefined,
+      // whether it should start directly
+      start,
+      // timezone
+      undefined,
+      // context
+      undefined,
+      // runOnInit
+      undefined,
+      // utcOffset
+      undefined,
+      // prevents memory leaking by automatically stopping when the node process finishes
+      true,
+    );
+
+    this.schedulerRegistry.addCronJob(name, job);
+  }
+
+  update({ name, expression, start }: CronUpdate): void {
+    const job = this.schedulerRegistry.getCronJob(name);
+    if (expression) {
+      job.setTime(new CronTime(expression));
+    }
+    if (start !== undefined) {
+      if (start) {
+        job.start();
+      } else {
+        job.stop();
+      }
+    }
+  }
+}
diff --git a/server/src/repositories/crypto.repository.ts b/server/src/repositories/crypto.repository.ts
index 72e75ef174..ee25609fec 100644
--- a/server/src/repositories/crypto.repository.ts
+++ b/server/src/repositories/crypto.repository.ts
@@ -3,9 +3,7 @@ import { compareSync, hash } from 'bcrypt';
 import { createHash, createPublicKey, createVerify, randomBytes, randomUUID } from 'node:crypto';
 import { createReadStream } from 'node:fs';
 import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 
-@Instrumentation()
 @Injectable()
 export class CryptoRepository implements ICryptoRepository {
   randomUUID() {
diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts
index 0453421a39..b5e2edfdea 100644
--- a/server/src/repositories/database.repository.ts
+++ b/server/src/repositories/database.repository.ts
@@ -3,7 +3,7 @@ import { InjectDataSource } from '@nestjs/typeorm';
 import AsyncLock from 'async-lock';
 import semver from 'semver';
 import { POSTGRES_VERSION_RANGE, VECTOR_VERSION_RANGE, VECTORS_VERSION_RANGE } from 'src/constants';
-import { getVectorExtension } from 'src/database.config';
+import { IConfigRepository } from 'src/interfaces/config.interface';
 import {
   DatabaseExtension,
   DatabaseLock,
@@ -15,19 +15,20 @@ import {
   VectorUpdateResult,
 } from 'src/interfaces/database.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 import { isValidInteger } from 'src/validation';
 import { DataSource, EntityManager, QueryRunner } from 'typeorm';
 
-@Instrumentation()
 @Injectable()
 export class DatabaseRepository implements IDatabaseRepository {
+  private vectorExtension: VectorExtension;
   readonly asyncLock = new AsyncLock();
 
   constructor(
     @InjectDataSource() private dataSource: DataSource,
     @Inject(ILoggerRepository) private logger: ILoggerRepository,
+    @Inject(IConfigRepository) configRepository: IConfigRepository,
   ) {
+    this.vectorExtension = configRepository.getEnv().database.vectorExtension;
     this.logger.setContext(DatabaseRepository.name);
   }
 
@@ -71,10 +72,6 @@ export class DatabaseRepository implements IDatabaseRepository {
     await this.dataSource.query(`CREATE EXTENSION IF NOT EXISTS ${extension}`);
   }
 
-  async updateExtension(extension: DatabaseExtension, version?: string): Promise<void> {
-    await this.dataSource.query(`ALTER EXTENSION ${extension} UPDATE${version ? ` TO '${version}'` : ''}`);
-  }
-
   async updateVectorExtension(extension: VectorExtension, targetVersion?: string): Promise<VectorUpdateResult> {
     const { availableVersion, installedVersion } = await this.getExtensionVersion(extension);
     if (!installedVersion) {
@@ -119,7 +116,7 @@ export class DatabaseRepository implements IDatabaseRepository {
     try {
       await this.dataSource.query(`REINDEX INDEX ${index}`);
     } catch (error) {
-      if (getVectorExtension() !== DatabaseExtension.VECTORS) {
+      if (this.vectorExtension !== DatabaseExtension.VECTORS) {
         throw error;
       }
       this.logger.warn(`Could not reindex index ${index}. Attempting to auto-fix.`);
@@ -141,7 +138,7 @@ export class DatabaseRepository implements IDatabaseRepository {
   }
 
   async shouldReindex(name: VectorIndex): Promise<boolean> {
-    if (getVectorExtension() !== DatabaseExtension.VECTORS) {
+    if (this.vectorExtension !== DatabaseExtension.VECTORS) {
       return false;
     }
 
diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts
index 9aa12e15dd..7de8defe6e 100644
--- a/server/src/repositories/event.repository.ts
+++ b/server/src/repositories/event.repository.ts
@@ -1,6 +1,5 @@
 import { Inject, Injectable } from '@nestjs/common';
-import { ModuleRef } from '@nestjs/core';
-import { EventEmitter2 } from '@nestjs/event-emitter';
+import { ModuleRef, Reflector } from '@nestjs/core';
 import {
   OnGatewayConnection,
   OnGatewayDisconnect,
@@ -8,23 +7,37 @@ import {
   WebSocketGateway,
   WebSocketServer,
 } from '@nestjs/websockets';
+import { ClassConstructor } from 'class-transformer';
+import _ from 'lodash';
 import { Server, Socket } from 'socket.io';
+import { EventConfig } from 'src/decorators';
+import { ImmichWorker, MetadataKey } from 'src/enum';
+import { IConfigRepository } from 'src/interfaces/config.interface';
 import {
   ArgsOf,
   ClientEventMap,
   EmitEvent,
   EmitHandler,
+  EventItem,
   IEventRepository,
-  ServerEvent,
-  ServerEventMap,
+  serverEvents,
+  ServerEvents,
 } from 'src/interfaces/event.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
+import { ConfigRepository } from 'src/repositories/config.repository';
 import { AuthService } from 'src/services/auth.service';
-import { Instrumentation } from 'src/utils/instrumentation';
+import { handlePromiseError } from 'src/utils/misc';
 
-type EmitHandlers = Partial<{ [T in EmitEvent]: EmitHandler<T>[] }>;
+type EmitHandlers = Partial<{ [T in EmitEvent]: Array<EventItem<T>> }>;
+
+type Item<T extends EmitEvent> = {
+  event: T;
+  handler: EmitHandler<T>;
+  priority: number;
+  server: boolean;
+  label: string;
+};
 
-@Instrumentation()
 @WebSocketGateway({
   cors: true,
   path: '/api/socket.io',
@@ -39,23 +52,70 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
 
   constructor(
     private moduleRef: ModuleRef,
-    private eventEmitter: EventEmitter2,
+    @Inject(IConfigRepository) private configRepository: ConfigRepository,
     @Inject(ILoggerRepository) private logger: ILoggerRepository,
   ) {
     this.logger.setContext(EventRepository.name);
   }
 
+  setup({ services }: { services: ClassConstructor<unknown>[] }) {
+    const reflector = this.moduleRef.get(Reflector, { strict: false });
+    const items: Item<EmitEvent>[] = [];
+    const worker = this.configRepository.getWorker();
+    if (!worker) {
+      throw new Error('Unable to determine worker type');
+    }
+
+    // discovery
+    for (const Service of services) {
+      const instance = this.moduleRef.get<any>(Service);
+      const ctx = Object.getPrototypeOf(instance);
+      for (const property of Object.getOwnPropertyNames(ctx)) {
+        const descriptor = Object.getOwnPropertyDescriptor(ctx, property);
+        if (!descriptor || descriptor.get || descriptor.set) {
+          continue;
+        }
+
+        const handler = instance[property];
+        if (typeof handler !== 'function') {
+          continue;
+        }
+
+        const event = reflector.get<EventConfig>(MetadataKey.EVENT_CONFIG, handler);
+        if (!event) {
+          continue;
+        }
+
+        const workers = event.workers ?? Object.values(ImmichWorker);
+        if (!workers.includes(worker)) {
+          continue;
+        }
+
+        items.push({
+          event: event.name,
+          priority: event.priority || 0,
+          server: event.server ?? false,
+          handler: handler.bind(instance),
+          label: `${Service.name}.${handler.name}`,
+        });
+      }
+    }
+
+    const handlers = _.orderBy(items, ['priority'], ['asc']);
+
+    // register by priority
+    for (const handler of handlers) {
+      this.addHandler(handler);
+    }
+  }
+
   afterInit(server: Server) {
     this.logger.log('Initialized websocket server');
 
-    for (const event of Object.values(ServerEvent)) {
-      if (event === ServerEvent.WEBSOCKET_CONNECT) {
-        continue;
-      }
-
-      server.on(event, (data: unknown) => {
+    for (const event of serverEvents) {
+      server.on(event, (...args: ArgsOf<any>) => {
         this.logger.debug(`Server event: ${event} (receive)`);
-        this.eventEmitter.emit(event, data);
+        handlePromiseError(this.onEvent({ name: event, args, server: true }), this.logger);
       });
     }
   }
@@ -72,7 +132,7 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
       if (auth.session) {
         await client.join(auth.session.id);
       }
-      this.serverSend(ServerEvent.WEBSOCKET_CONNECT, { userId: auth.user.id });
+      await this.onEvent({ name: 'websocket.connect', args: [{ userId: auth.user.id }], server: false });
     } catch (error: Error | any) {
       this.logger.error(`Websocket connection error: ${error}`, error?.stack);
       client.emit('error', 'unauthorized');
@@ -85,32 +145,42 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
     await client.leave(client.nsp.name);
   }
 
-  on<T extends EmitEvent>(event: T, handler: EmitHandler<T>): void {
+  private addHandler<T extends EmitEvent>(item: Item<T>): void {
+    const event = item.event;
+
     if (!this.emitHandlers[event]) {
       this.emitHandlers[event] = [];
     }
 
-    this.emitHandlers[event].push(handler);
+    this.emitHandlers[event].push(item);
   }
 
-  async emit<T extends EmitEvent>(event: T, ...args: ArgsOf<T>): Promise<void> {
-    const handlers = this.emitHandlers[event] || [];
-    for (const handler of handlers) {
-      await handler(...args);
+  emit<T extends EmitEvent>(event: T, ...args: ArgsOf<T>): Promise<void> {
+    return this.onEvent({ name: event, args, server: false });
+  }
+
+  private async onEvent<T extends EmitEvent>(event: { name: T; args: ArgsOf<T>; server: boolean }): Promise<void> {
+    const handlers = this.emitHandlers[event.name] || [];
+    for (const { handler, server } of handlers) {
+      // exclude handlers that ignore server events
+      if (!server && event.server) {
+        continue;
+      }
+
+      await handler(...event.args);
     }
   }
 
-  clientSend<E extends keyof ClientEventMap>(event: E, room: string, data: ClientEventMap[E]) {
-    this.server?.to(room).emit(event, data);
+  clientSend<T extends keyof ClientEventMap>(event: T, room: string, ...data: ClientEventMap[T]) {
+    this.server?.to(room).emit(event, ...data);
   }
 
-  clientBroadcast<E extends keyof ClientEventMap>(event: E, data: ClientEventMap[E]) {
-    this.server?.emit(event, data);
+  clientBroadcast<T extends keyof ClientEventMap>(event: T, ...data: ClientEventMap[T]) {
+    this.server?.emit(event, ...data);
   }
 
-  serverSend<E extends keyof ServerEventMap>(event: E, data: ServerEventMap[E]) {
+  serverSend<T extends ServerEvents>(event: T, ...args: ArgsOf<T>): void {
     this.logger.debug(`Server event: ${event} (send)`);
-    this.server?.serverSideEmit(event, data);
-    return this.eventEmitter.emit(event, data);
+    this.server?.serverSideEmit(event, ...args);
   }
 }
diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts
index 3be6b375a0..eb6a5d6f71 100644
--- a/server/src/repositories/index.ts
+++ b/server/src/repositories/index.ts
@@ -5,6 +5,8 @@ import { IAlbumRepository } from 'src/interfaces/album.interface';
 import { IKeyRepository } from 'src/interfaces/api-key.interface';
 import { IAssetRepository } from 'src/interfaces/asset.interface';
 import { IAuditRepository } from 'src/interfaces/audit.interface';
+import { IConfigRepository } from 'src/interfaces/config.interface';
+import { ICronRepository } from 'src/interfaces/cron.interface';
 import { ICryptoRepository } from 'src/interfaces/crypto.interface';
 import { IDatabaseRepository } from 'src/interfaces/database.interface';
 import { IEventRepository } from 'src/interfaces/event.interface';
@@ -16,11 +18,12 @@ import { IMapRepository } from 'src/interfaces/map.interface';
 import { IMediaRepository } from 'src/interfaces/media.interface';
 import { IMemoryRepository } from 'src/interfaces/memory.interface';
 import { IMetadataRepository } from 'src/interfaces/metadata.interface';
-import { IMetricRepository } from 'src/interfaces/metric.interface';
 import { IMoveRepository } from 'src/interfaces/move.interface';
 import { INotificationRepository } from 'src/interfaces/notification.interface';
+import { IOAuthRepository } from 'src/interfaces/oauth.interface';
 import { IPartnerRepository } from 'src/interfaces/partner.interface';
 import { IPersonRepository } from 'src/interfaces/person.interface';
+import { IProcessRepository } from 'src/interfaces/process.interface';
 import { ISearchRepository } from 'src/interfaces/search.interface';
 import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
 import { ISessionRepository } from 'src/interfaces/session.interface';
@@ -29,7 +32,11 @@ import { IStackRepository } from 'src/interfaces/stack.interface';
 import { IStorageRepository } from 'src/interfaces/storage.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { ITagRepository } from 'src/interfaces/tag.interface';
+import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
+import { ITrashRepository } from 'src/interfaces/trash.interface';
 import { IUserRepository } from 'src/interfaces/user.interface';
+import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
+import { IViewRepository } from 'src/interfaces/view.interface';
 import { AccessRepository } from 'src/repositories/access.repository';
 import { ActivityRepository } from 'src/repositories/activity.repository';
 import { AlbumUserRepository } from 'src/repositories/album-user.repository';
@@ -37,6 +44,8 @@ import { AlbumRepository } from 'src/repositories/album.repository';
 import { ApiKeyRepository } from 'src/repositories/api-key.repository';
 import { AssetRepository } from 'src/repositories/asset.repository';
 import { AuditRepository } from 'src/repositories/audit.repository';
+import { ConfigRepository } from 'src/repositories/config.repository';
+import { CronRepository } from 'src/repositories/cron.repository';
 import { CryptoRepository } from 'src/repositories/crypto.repository';
 import { DatabaseRepository } from 'src/repositories/database.repository';
 import { EventRepository } from 'src/repositories/event.repository';
@@ -48,11 +57,12 @@ import { MapRepository } from 'src/repositories/map.repository';
 import { MediaRepository } from 'src/repositories/media.repository';
 import { MemoryRepository } from 'src/repositories/memory.repository';
 import { MetadataRepository } from 'src/repositories/metadata.repository';
-import { MetricRepository } from 'src/repositories/metric.repository';
 import { MoveRepository } from 'src/repositories/move.repository';
 import { NotificationRepository } from 'src/repositories/notification.repository';
+import { OAuthRepository } from 'src/repositories/oauth.repository';
 import { PartnerRepository } from 'src/repositories/partner.repository';
 import { PersonRepository } from 'src/repositories/person.repository';
+import { ProcessRepository } from 'src/repositories/process.repository';
 import { SearchRepository } from 'src/repositories/search.repository';
 import { ServerInfoRepository } from 'src/repositories/server-info.repository';
 import { SessionRepository } from 'src/repositories/session.repository';
@@ -61,7 +71,11 @@ import { StackRepository } from 'src/repositories/stack.repository';
 import { StorageRepository } from 'src/repositories/storage.repository';
 import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
 import { TagRepository } from 'src/repositories/tag.repository';
+import { TelemetryRepository } from 'src/repositories/telemetry.repository';
+import { TrashRepository } from 'src/repositories/trash.repository';
 import { UserRepository } from 'src/repositories/user.repository';
+import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
+import { ViewRepository } from 'src/repositories/view-repository';
 
 export const repositories = [
   { provide: IAccessRepository, useClass: AccessRepository },
@@ -70,6 +84,8 @@ export const repositories = [
   { provide: IAlbumUserRepository, useClass: AlbumUserRepository },
   { provide: IAssetRepository, useClass: AssetRepository },
   { provide: IAuditRepository, useClass: AuditRepository },
+  { provide: IConfigRepository, useClass: ConfigRepository },
+  { provide: ICronRepository, useClass: CronRepository },
   { provide: ICryptoRepository, useClass: CryptoRepository },
   { provide: IDatabaseRepository, useClass: DatabaseRepository },
   { provide: IEventRepository, useClass: EventRepository },
@@ -82,11 +98,12 @@ export const repositories = [
   { provide: IMediaRepository, useClass: MediaRepository },
   { provide: IMemoryRepository, useClass: MemoryRepository },
   { provide: IMetadataRepository, useClass: MetadataRepository },
-  { provide: IMetricRepository, useClass: MetricRepository },
   { provide: IMoveRepository, useClass: MoveRepository },
   { provide: INotificationRepository, useClass: NotificationRepository },
+  { provide: IOAuthRepository, useClass: OAuthRepository },
   { provide: IPartnerRepository, useClass: PartnerRepository },
   { provide: IPersonRepository, useClass: PersonRepository },
+  { provide: IProcessRepository, useClass: ProcessRepository },
   { provide: ISearchRepository, useClass: SearchRepository },
   { provide: IServerInfoRepository, useClass: ServerInfoRepository },
   { provide: ISessionRepository, useClass: SessionRepository },
@@ -95,5 +112,9 @@ export const repositories = [
   { provide: IStorageRepository, useClass: StorageRepository },
   { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
   { provide: ITagRepository, useClass: TagRepository },
+  { provide: ITelemetryRepository, useClass: TelemetryRepository },
+  { provide: ITrashRepository, useClass: TrashRepository },
   { provide: IUserRepository, useClass: UserRepository },
+  { provide: IVersionHistoryRepository, useClass: VersionHistoryRepository },
+  { provide: IViewRepository, useClass: ViewRepository },
 ];
diff --git a/server/src/repositories/job.repository.ts b/server/src/repositories/job.repository.ts
index f64e5175e5..c6c2947617 100644
--- a/server/src/repositories/job.repository.ts
+++ b/server/src/repositories/job.repository.ts
@@ -1,157 +1,121 @@
 import { getQueueToken } from '@nestjs/bullmq';
 import { Inject, Injectable } from '@nestjs/common';
-import { ModuleRef } from '@nestjs/core';
+import { ModuleRef, Reflector } from '@nestjs/core';
 import { SchedulerRegistry } from '@nestjs/schedule';
-import { Job, JobsOptions, Processor, Queue, Worker, WorkerOptions } from 'bullmq';
-import { CronJob, CronTime } from 'cron';
+import { JobsOptions, Queue, Worker } from 'bullmq';
+import { ClassConstructor } from 'class-transformer';
 import { setTimeout } from 'node:timers/promises';
-import { bullConfig } from 'src/config';
+import { JobConfig } from 'src/decorators';
+import { MetadataKey } from 'src/enum';
+import { IConfigRepository } from 'src/interfaces/config.interface';
+import { IEventRepository } from 'src/interfaces/event.interface';
 import {
+  IEntityJob,
   IJobRepository,
   JobCounts,
   JobItem,
   JobName,
+  JobOf,
+  JobStatus,
   QueueCleanType,
   QueueName,
   QueueStatus,
 } from 'src/interfaces/job.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
+import { getKeyByValue, getMethodNames, ImmichStartupError } from 'src/utils/misc';
 
-export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
-  // misc
-  [JobName.ASSET_DELETION]: QueueName.BACKGROUND_TASK,
-  [JobName.ASSET_DELETION_CHECK]: QueueName.BACKGROUND_TASK,
-  [JobName.USER_DELETE_CHECK]: QueueName.BACKGROUND_TASK,
-  [JobName.USER_DELETION]: QueueName.BACKGROUND_TASK,
-  [JobName.DELETE_FILES]: QueueName.BACKGROUND_TASK,
-  [JobName.CLEAN_OLD_AUDIT_LOGS]: QueueName.BACKGROUND_TASK,
-  [JobName.CLEAN_OLD_SESSION_TOKENS]: QueueName.BACKGROUND_TASK,
-  [JobName.PERSON_CLEANUP]: QueueName.BACKGROUND_TASK,
-  [JobName.USER_SYNC_USAGE]: QueueName.BACKGROUND_TASK,
-
-  // conversion
-  [JobName.QUEUE_VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION,
-  [JobName.VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION,
-
-  // thumbnails
-  [JobName.QUEUE_GENERATE_THUMBNAILS]: QueueName.THUMBNAIL_GENERATION,
-  [JobName.GENERATE_PREVIEW]: QueueName.THUMBNAIL_GENERATION,
-  [JobName.GENERATE_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
-  [JobName.GENERATE_THUMBHASH]: QueueName.THUMBNAIL_GENERATION,
-  [JobName.GENERATE_PERSON_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
-
-  // metadata
-  [JobName.QUEUE_METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION,
-  [JobName.METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION,
-  [JobName.LINK_LIVE_PHOTOS]: QueueName.METADATA_EXTRACTION,
-
-  // storage template
-  [JobName.STORAGE_TEMPLATE_MIGRATION]: QueueName.STORAGE_TEMPLATE_MIGRATION,
-  [JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE]: QueueName.STORAGE_TEMPLATE_MIGRATION,
-
-  // migration
-  [JobName.QUEUE_MIGRATION]: QueueName.MIGRATION,
-  [JobName.MIGRATE_ASSET]: QueueName.MIGRATION,
-  [JobName.MIGRATE_PERSON]: QueueName.MIGRATION,
-
-  // facial recognition
-  [JobName.QUEUE_FACE_DETECTION]: QueueName.FACE_DETECTION,
-  [JobName.FACE_DETECTION]: QueueName.FACE_DETECTION,
-  [JobName.QUEUE_FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION,
-  [JobName.FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION,
-
-  // smart search
-  [JobName.QUEUE_SMART_SEARCH]: QueueName.SMART_SEARCH,
-  [JobName.SMART_SEARCH]: QueueName.SMART_SEARCH,
-
-  // duplicate detection
-  [JobName.QUEUE_DUPLICATE_DETECTION]: QueueName.DUPLICATE_DETECTION,
-  [JobName.DUPLICATE_DETECTION]: QueueName.DUPLICATE_DETECTION,
-
-  // XMP sidecars
-  [JobName.QUEUE_SIDECAR]: QueueName.SIDECAR,
-  [JobName.SIDECAR_DISCOVERY]: QueueName.SIDECAR,
-  [JobName.SIDECAR_SYNC]: QueueName.SIDECAR,
-  [JobName.SIDECAR_WRITE]: QueueName.SIDECAR,
-
-  // Library management
-  [JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY,
-  [JobName.LIBRARY_SCAN]: QueueName.LIBRARY,
-  [JobName.LIBRARY_DELETE]: QueueName.LIBRARY,
-  [JobName.LIBRARY_CHECK_OFFLINE]: QueueName.LIBRARY,
-  [JobName.LIBRARY_REMOVE_OFFLINE]: QueueName.LIBRARY,
-  [JobName.LIBRARY_QUEUE_SCAN_ALL]: QueueName.LIBRARY,
-  [JobName.LIBRARY_QUEUE_CLEANUP]: QueueName.LIBRARY,
-
-  // Notification
-  [JobName.SEND_EMAIL]: QueueName.NOTIFICATION,
-  [JobName.NOTIFY_ALBUM_INVITE]: QueueName.NOTIFICATION,
-  [JobName.NOTIFY_ALBUM_UPDATE]: QueueName.NOTIFICATION,
-  [JobName.NOTIFY_SIGNUP]: QueueName.NOTIFICATION,
-
-  // Version check
-  [JobName.VERSION_CHECK]: QueueName.BACKGROUND_TASK,
+type JobMapItem = {
+  jobName: JobName;
+  queueName: QueueName;
+  handler: (job: JobOf<any>) => Promise<JobStatus>;
+  label: string;
 };
 
-@Instrumentation()
 @Injectable()
 export class JobRepository implements IJobRepository {
   private workers: Partial<Record<QueueName, Worker>> = {};
+  private handlers: Partial<Record<JobName, JobMapItem>> = {};
 
   constructor(
-    private moduleReference: ModuleRef,
-    private schedulerReqistry: SchedulerRegistry,
+    private moduleRef: ModuleRef,
+    private schedulerRegistry: SchedulerRegistry,
+    @Inject(IConfigRepository) private configRepository: IConfigRepository,
+    @Inject(IEventRepository) private eventRepository: IEventRepository,
     @Inject(ILoggerRepository) private logger: ILoggerRepository,
   ) {
     this.logger.setContext(JobRepository.name);
   }
 
-  addHandler(queueName: QueueName, concurrency: number, handler: (item: JobItem) => Promise<void>) {
-    const workerHandler: Processor = async (job: Job) => handler(job as JobItem);
-    const workerOptions: WorkerOptions = { ...bullConfig, concurrency };
-    this.workers[queueName] = new Worker(queueName, workerHandler, workerOptions);
-  }
+  setup({ services }: { services: ClassConstructor<unknown>[] }) {
+    const reflector = this.moduleRef.get(Reflector, { strict: false });
 
-  addCronJob(name: string, expression: string, onTick: () => void, start = true): void {
-    const job = new CronJob<null, null>(
-      expression,
-      onTick,
-      // function to run onComplete
-      undefined,
-      // whether it should start directly
-      start,
-      // timezone
-      undefined,
-      // context
-      undefined,
-      // runOnInit
-      undefined,
-      // utcOffset
-      undefined,
-      // prevents memory leaking by automatically stopping when the node process finishes
-      true,
-    );
+    // discovery
+    for (const Service of services) {
+      const instance = this.moduleRef.get<any>(Service);
+      for (const methodName of getMethodNames(instance)) {
+        const handler = instance[methodName];
+        const config = reflector.get<JobConfig>(MetadataKey.JOB_CONFIG, handler);
+        if (!config) {
+          continue;
+        }
 
-    this.schedulerReqistry.addCronJob(name, job);
-  }
+        const { name: jobName, queue: queueName } = config;
+        const label = `${Service.name}.${handler.name}`;
 
-  updateCronJob(name: string, expression?: string, start?: boolean): void {
-    const job = this.schedulerReqistry.getCronJob(name);
-    if (expression) {
-      job.setTime(new CronTime(expression));
+        // one handler per job
+        if (this.handlers[jobName]) {
+          const jobKey = getKeyByValue(JobName, jobName);
+          const errorMessage = `Failed to add job handler for ${label}`;
+          this.logger.error(
+            `${errorMessage}. JobName.${jobKey} is already handled by ${this.handlers[jobName].label}.`,
+          );
+          throw new ImmichStartupError(errorMessage);
+        }
+
+        this.handlers[jobName] = {
+          label,
+          jobName,
+          queueName,
+          handler: handler.bind(instance),
+        };
+
+        this.logger.verbose(`Added job handler: ${jobName} => ${label}`);
+      }
     }
-    if (start !== undefined) {
-      if (start) {
-        job.start();
-      } else {
-        job.stop();
+
+    // no missing handlers
+    for (const [jobKey, jobName] of Object.entries(JobName)) {
+      const item = this.handlers[jobName];
+      if (!item) {
+        const errorMessage = `Failed to find job handler for Job.${jobKey} ("${jobName}")`;
+        this.logger.error(
+          `${errorMessage}. Make sure to add the @OnJob({ name: JobName.${jobKey}, queue: QueueName.XYZ }) decorator for the new job.`,
+        );
+        throw new ImmichStartupError(errorMessage);
       }
     }
   }
 
-  deleteCronJob(name: string): void {
-    this.schedulerReqistry.deleteCronJob(name);
+  startWorkers() {
+    const { bull } = this.configRepository.getEnv();
+    for (const queueName of Object.values(QueueName)) {
+      this.logger.debug(`Starting worker for queue: ${queueName}`);
+      this.workers[queueName] = new Worker(
+        queueName,
+        (job) => this.eventRepository.emit('job.start', queueName, job as JobItem),
+        { ...bull.config, concurrency: 1 },
+      );
+    }
+  }
+
+  async run({ name, data }: JobItem) {
+    const item = this.handlers[name as JobName];
+    if (!item) {
+      this.logger.warn(`Skipping unknown job: "${name}"`);
+      return JobStatus.SKIPPED;
+    }
+
+    return item.handler(data);
   }
 
   setConcurrency(queueName: QueueName, concurrency: number) {
@@ -200,6 +164,10 @@ export class JobRepository implements IJobRepository {
     ) as unknown as Promise<JobCounts>;
   }
 
+  private getQueueName(name: JobName) {
+    return (this.handlers[name] as JobMapItem).queueName;
+  }
+
   async queueAll(items: JobItem[]): Promise<void> {
     if (items.length === 0) {
       return;
@@ -208,7 +176,7 @@ export class JobRepository implements IJobRepository {
     const promises = [];
     const itemsByQueue = {} as Record<string, (JobItem & { data: any; options: JobsOptions | undefined })[]>;
     for (const item of items) {
-      const queueName = JOBS_TO_QUEUE[item.name];
+      const queueName = this.getQueueName(item.name);
       const job = {
         name: item.name,
         data: item.data || {},
@@ -250,6 +218,9 @@ export class JobRepository implements IJobRepository {
 
   private getJobOptions(item: JobItem): JobsOptions | null {
     switch (item.name) {
+      case JobName.NOTIFY_ALBUM_UPDATE: {
+        return { jobId: item.data.id, delay: item.data?.delay };
+      }
       case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: {
         return { jobId: item.data.id };
       }
@@ -259,7 +230,6 @@ export class JobRepository implements IJobRepository {
       case JobName.QUEUE_FACIAL_RECOGNITION: {
         return { jobId: JobName.QUEUE_FACIAL_RECOGNITION };
       }
-
       default: {
         return null;
       }
@@ -267,6 +237,22 @@ export class JobRepository implements IJobRepository {
   }
 
   private getQueue(queue: QueueName): Queue {
-    return this.moduleReference.get<Queue>(getQueueToken(queue), { strict: false });
+    return this.moduleRef.get<Queue>(getQueueToken(queue), { strict: false });
+  }
+
+  public async removeJob(jobId: string, name: JobName): Promise<IEntityJob | undefined> {
+    const existingJob = await this.getQueue(this.getQueueName(name)).getJob(jobId);
+    if (!existingJob) {
+      return;
+    }
+    try {
+      await existingJob.remove();
+    } catch (error: any) {
+      if (error.message?.includes('Missing key for job')) {
+        return;
+      }
+      throw error;
+    }
+    return existingJob.data;
   }
 }
diff --git a/server/src/repositories/library.repository.ts b/server/src/repositories/library.repository.ts
index 36fb4b9217..1446395854 100644
--- a/server/src/repositories/library.repository.ts
+++ b/server/src/repositories/library.repository.ts
@@ -4,11 +4,9 @@ import { DummyValue, GenerateSql } from 'src/decorators';
 import { LibraryStatsResponseDto } from 'src/dtos/library.dto';
 import { LibraryEntity } from 'src/entities/library.entity';
 import { ILibraryRepository } from 'src/interfaces/library.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 import { IsNull, Not } from 'typeorm';
 import { Repository } from 'typeorm/repository/Repository.js';
 
-@Instrumentation()
 @Injectable()
 export class LibraryRepository implements ILibraryRepository {
   constructor(@InjectRepository(LibraryEntity) private repository: Repository<LibraryEntity>) {}
diff --git a/server/src/repositories/logger.repository.spec.ts b/server/src/repositories/logger.repository.spec.ts
new file mode 100644
index 0000000000..dcb54ada7c
--- /dev/null
+++ b/server/src/repositories/logger.repository.spec.ts
@@ -0,0 +1,40 @@
+import { ClsService } from 'nestjs-cls';
+import { ImmichWorker } from 'src/enum';
+import { IConfigRepository } from 'src/interfaces/config.interface';
+import { LoggerRepository } from 'src/repositories/logger.repository';
+import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
+import { Mocked } from 'vitest';
+
+describe(LoggerRepository.name, () => {
+  let sut: LoggerRepository;
+
+  let configMock: Mocked<IConfigRepository>;
+  let clsMock: Mocked<ClsService>;
+
+  beforeEach(() => {
+    configMock = newConfigRepositoryMock();
+    clsMock = {
+      getId: vitest.fn(),
+    } as unknown as Mocked<ClsService>;
+  });
+
+  describe('formatContext', () => {
+    it('should use colors', () => {
+      configMock.getEnv.mockReturnValue(mockEnvData({ noColor: false }));
+
+      sut = new LoggerRepository(clsMock, configMock);
+      sut.setAppName(ImmichWorker.API);
+
+      expect(sut['formatContext']('context')).toBe('\u001B[33m[Api:context]\u001B[39m ');
+    });
+
+    it('should not use colors when noColor is true', () => {
+      configMock.getEnv.mockReturnValue(mockEnvData({ noColor: true }));
+
+      sut = new LoggerRepository(clsMock, configMock);
+      sut.setAppName(ImmichWorker.API);
+
+      expect(sut['formatContext']('context')).toBe('[Api:context] ');
+    });
+  });
+});
diff --git a/server/src/repositories/logger.repository.ts b/server/src/repositories/logger.repository.ts
index 1e0c7b74d9..4f1d3cac22 100644
--- a/server/src/repositories/logger.repository.ts
+++ b/server/src/repositories/logger.repository.ts
@@ -1,32 +1,50 @@
-import { ConsoleLogger, Injectable, Scope } from '@nestjs/common';
+import { ConsoleLogger, Inject, Injectable, Scope } from '@nestjs/common';
 import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util';
 import { ClsService } from 'nestjs-cls';
-import { LogLevel } from 'src/config';
+import { Telemetry } from 'src/decorators';
+import { LogLevel } from 'src/enum';
+import { IConfigRepository } from 'src/interfaces/config.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { LogColor } from 'src/utils/logger';
 
 const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL];
 
+enum LogColor {
+  RED = 31,
+  GREEN = 32,
+  YELLOW = 33,
+  BLUE = 34,
+  MAGENTA_BRIGHT = 95,
+  CYAN_BRIGHT = 96,
+}
+
 @Injectable({ scope: Scope.TRANSIENT })
+@Telemetry({ enabled: false })
 export class LoggerRepository extends ConsoleLogger implements ILoggerRepository {
   private static logLevels: LogLevel[] = [LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL];
+  private noColor: boolean;
 
-  constructor(private cls: ClsService) {
+  constructor(
+    private cls: ClsService,
+    @Inject(IConfigRepository) configRepository: IConfigRepository,
+  ) {
     super(LoggerRepository.name);
+
+    const { noColor } = configRepository.getEnv();
+    this.noColor = noColor;
   }
 
   private static appName?: string = undefined;
 
   setAppName(name: string): void {
-    LoggerRepository.appName = name;
+    LoggerRepository.appName = name.charAt(0).toUpperCase() + name.slice(1);
   }
 
   isLevelEnabled(level: LogLevel) {
     return isLogLevelEnabled(level, LoggerRepository.logLevels);
   }
 
-  setLogLevel(level: LogLevel): void {
-    LoggerRepository.logLevels = LOG_LEVELS.slice(LOG_LEVELS.indexOf(level));
+  setLogLevel(level: LogLevel | false): void {
+    LoggerRepository.logLevels = level ? LOG_LEVELS.slice(LOG_LEVELS.indexOf(level)) : [];
   }
 
   protected formatContext(context: string): string {
@@ -44,6 +62,19 @@ export class LoggerRepository extends ConsoleLogger implements ILoggerRepository
       return '';
     }
 
-    return LogColor.yellow(`[${prefix}]`) + ' ';
+    return this.colors.yellow(`[${prefix}]`) + ' ';
+  }
+
+  private colors = {
+    red: (text: string) => this.withColor(text, LogColor.RED),
+    green: (text: string) => this.withColor(text, LogColor.GREEN),
+    yellow: (text: string) => this.withColor(text, LogColor.YELLOW),
+    blue: (text: string) => this.withColor(text, LogColor.BLUE),
+    magentaBright: (text: string) => this.withColor(text, LogColor.MAGENTA_BRIGHT),
+    cyanBright: (text: string) => this.withColor(text, LogColor.CYAN_BRIGHT),
+  };
+
+  private withColor(text: string, color: LogColor) {
+    return this.noColor ? text : `\u001B[${color}m${text}\u001B[39m`;
   }
 }
diff --git a/server/src/repositories/machine-learning.repository.ts b/server/src/repositories/machine-learning.repository.ts
index b9404022ef..56cdf30a1e 100644
--- a/server/src/repositories/machine-learning.repository.ts
+++ b/server/src/repositories/machine-learning.repository.ts
@@ -1,6 +1,7 @@
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
 import { readFile } from 'node:fs/promises';
 import { CLIPConfig } from 'src/dtos/model-config.dto';
+import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import {
   ClipTextualResponse,
   ClipVisualResponse,
@@ -12,36 +13,43 @@ import {
   ModelTask,
   ModelType,
 } from 'src/interfaces/machine-learning.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 
-const errorPrefix = 'Machine learning request';
-
-@Instrumentation()
 @Injectable()
 export class MachineLearningRepository implements IMachineLearningRepository {
-  private async predict<T>(url: string, payload: ModelPayload, config: MachineLearningRequest): Promise<T> {
-    const formData = await this.getFormData(payload, config);
-
-    const res = await fetch(new URL('/predict', url), { method: 'POST', body: formData }).catch(
-      (error: Error | any) => {
-        throw new Error(`${errorPrefix} to "${url}" failed with ${error?.cause || error}`);
-      },
-    );
-
-    if (res.status >= 400) {
-      throw new Error(`${errorPrefix} '${JSON.stringify(config)}' failed with status ${res.status}: ${res.statusText}`);
-    }
-    return res.json();
+  constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) {
+    this.logger.setContext(MachineLearningRepository.name);
   }
 
-  async detectFaces(url: string, imagePath: string, { modelName, minScore }: FaceDetectionOptions) {
+  private async predict<T>(urls: string[], payload: ModelPayload, config: MachineLearningRequest): Promise<T> {
+    const formData = await this.getFormData(payload, config);
+    for (const url of urls) {
+      try {
+        const response = await fetch(new URL('/predict', url), { method: 'POST', body: formData });
+        if (response.ok) {
+          return response.json();
+        }
+
+        this.logger.warn(
+          `Machine learning request to "${url}" failed with status ${response.status}: ${response.statusText}`,
+        );
+      } catch (error: Error | unknown) {
+        this.logger.warn(
+          `Machine learning request to "${url}" failed: ${error instanceof Error ? error.message : error}`,
+        );
+      }
+    }
+
+    throw new Error(`Machine learning request '${JSON.stringify(config)}' failed for all URLs`);
+  }
+
+  async detectFaces(urls: string[], imagePath: string, { modelName, minScore }: FaceDetectionOptions) {
     const request = {
       [ModelTask.FACIAL_RECOGNITION]: {
         [ModelType.DETECTION]: { modelName, options: { minScore } },
         [ModelType.RECOGNITION]: { modelName },
       },
     };
-    const response = await this.predict<FacialRecognitionResponse>(url, { imagePath }, request);
+    const response = await this.predict<FacialRecognitionResponse>(urls, { imagePath }, request);
     return {
       imageHeight: response.imageHeight,
       imageWidth: response.imageWidth,
@@ -49,15 +57,15 @@ export class MachineLearningRepository implements IMachineLearningRepository {
     };
   }
 
-  async encodeImage(url: string, imagePath: string, { modelName }: CLIPConfig) {
+  async encodeImage(urls: string[], imagePath: string, { modelName }: CLIPConfig) {
     const request = { [ModelTask.SEARCH]: { [ModelType.VISUAL]: { modelName } } };
-    const response = await this.predict<ClipVisualResponse>(url, { imagePath }, request);
+    const response = await this.predict<ClipVisualResponse>(urls, { imagePath }, request);
     return response[ModelTask.SEARCH];
   }
 
-  async encodeText(url: string, text: string, { modelName }: CLIPConfig) {
+  async encodeText(urls: string[], text: string, { modelName }: CLIPConfig) {
     const request = { [ModelTask.SEARCH]: { [ModelType.TEXTUAL]: { modelName } } };
-    const response = await this.predict<ClipTextualResponse>(url, { text }, request);
+    const response = await this.predict<ClipTextualResponse>(urls, { text }, request);
     return response[ModelTask.SEARCH];
   }
 
diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts
index 3508de720b..348736a33d 100644
--- a/server/src/repositories/map.repository.ts
+++ b/server/src/repositories/map.repository.ts
@@ -1,14 +1,19 @@
 import { Inject, Injectable } from '@nestjs/common';
 import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
 import { getName } from 'i18n-iso-countries';
+import { randomUUID } from 'node:crypto';
 import { createReadStream, existsSync } from 'node:fs';
 import { readFile } from 'node:fs/promises';
 import readLine from 'node:readline';
-import { citiesFile, resourcePaths } from 'src/constants';
+import { citiesFile } from 'src/constants';
 import { AssetEntity } from 'src/entities/asset.entity';
-import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
-import { NaturalEarthCountriesEntity } from 'src/entities/natural-earth-countries.entity';
-import { SystemMetadataKey } from 'src/enum';
+import { GeodataPlacesEntity, GeodataPlacesTempEntity } from 'src/entities/geodata-places.entity';
+import {
+  NaturalEarthCountriesEntity,
+  NaturalEarthCountriesTempEntity,
+} from 'src/entities/natural-earth-countries.entity';
+import { LogLevel, SystemMetadataKey } from 'src/enum';
+import { IConfigRepository } from 'src/interfaces/config.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import {
   GeoPoint,
@@ -19,11 +24,9 @@ import {
 } from 'src/interfaces/map.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { OptionalBetween } from 'src/utils/database';
-import { Instrumentation } from 'src/utils/instrumentation';
-import { DataSource, In, IsNull, Not, QueryRunner, Repository } from 'typeorm';
+import { DataSource, In, IsNull, Not, Repository } from 'typeorm';
 import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
 
-@Instrumentation()
 @Injectable()
 export class MapRepository implements IMapRepository {
   constructor(
@@ -32,6 +35,7 @@ export class MapRepository implements IMapRepository {
     @InjectRepository(NaturalEarthCountriesEntity)
     private naturalEarthCountriesRepository: Repository<NaturalEarthCountriesEntity>,
     @InjectDataSource() private dataSource: DataSource,
+    @Inject(IConfigRepository) private configRepository: IConfigRepository,
     @Inject(ISystemMetadataRepository) private metadataRepository: ISystemMetadataRepository,
     @Inject(ILoggerRepository) private logger: ILoggerRepository,
   ) {
@@ -40,6 +44,7 @@ export class MapRepository implements IMapRepository {
 
   async init(): Promise<void> {
     this.logger.log('Initializing metadata repository');
+    const { resourcePaths } = this.configRepository.getEnv();
     const geodataDate = await readFile(resourcePaths.geodata.dateFile, 'utf8');
 
     // TODO move to service init
@@ -48,8 +53,7 @@ export class MapRepository implements IMapRepository {
       return;
     }
 
-    await this.importGeodata();
-    await this.importNaturalEarthCountries();
+    await Promise.all([this.importGeodata(), this.importNaturalEarthCountries()]);
 
     await this.metadataRepository.set(SystemMetadataKey.REVERSE_GEOCODING_STATE, {
       lastUpdate: geodataDate,
@@ -110,32 +114,23 @@ export class MapRepository implements IMapRepository {
     }));
   }
 
-  async fetchStyle(url: string) {
-    try {
-      const response = await fetch(url);
-
-      if (!response.ok) {
-        throw new Error(`Failed to fetch data from ${url} with status ${response.status}: ${await response.text()}`);
-      }
-
-      return response.json();
-    } catch (error) {
-      throw new Error(`Failed to fetch data from ${url}: ${error}`);
-    }
-  }
-
   async reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult> {
     this.logger.debug(`Request: ${point.latitude},${point.longitude}`);
 
     const response = await this.geodataPlacesRepository
       .createQueryBuilder('geoplaces')
-      .where('earth_box(ll_to_earth(:latitude, :longitude), 25000) @> "earthCoord"', point)
-      .orderBy('earth_distance(ll_to_earth(:latitude, :longitude), "earthCoord")')
+      .where(
+        'earth_box(ll_to_earth_public(:latitude, :longitude), 25000) @> ll_to_earth_public(latitude, longitude)',
+        point,
+      )
+      .orderBy('earth_distance(ll_to_earth_public(:latitude, :longitude), ll_to_earth_public(latitude, longitude))')
       .limit(1)
       .getOne();
 
     if (response) {
-      this.logger.verbose(`Raw: ${JSON.stringify(response, null, 2)}`);
+      if (this.logger.isLevelEnabled(LogLevel.VERBOSE)) {
+        this.logger.verbose(`Raw: ${JSON.stringify(response, null, 2)}`);
+      }
 
       const { countryCode, name: city, admin1Name } = response;
       const country = getName(countryCode, 'en') ?? null;
@@ -162,8 +157,9 @@ export class MapRepository implements IMapRepository {
       return { country: null, state: null, city: null };
     }
 
-    this.logger.verbose(`Raw: ${JSON.stringify(ne_response, ['id', 'admin', 'admin_a3', 'type'], 2)}`);
-
+    if (this.logger.isLevelEnabled(LogLevel.VERBOSE)) {
+      this.logger.verbose(`Raw: ${JSON.stringify(ne_response, ['id', 'admin', 'admin_a3', 'type'], 2)}`);
+    }
     const { admin_a3 } = ne_response;
     const country = getName(admin_a3, 'en') ?? null;
     const state = null;
@@ -172,142 +168,119 @@ export class MapRepository implements IMapRepository {
     return { country, state, city };
   }
 
-  private transformCoordinatesToPolygon(coordinates: number[][][]): string {
-    const pointsString = coordinates.map((point) => `(${point[0]},${point[1]})`).join(', ');
-    return `(${pointsString})`;
-  }
-
   private async importNaturalEarthCountries() {
-    const queryRunner = this.dataSource.createQueryRunner();
-    await queryRunner.connect();
+    const { resourcePaths } = this.configRepository.getEnv();
+    const geoJSONData = JSON.parse(await readFile(resourcePaths.geodata.naturalEarthCountriesPath, 'utf8'));
+    if (geoJSONData.type !== 'FeatureCollection' || !Array.isArray(geoJSONData.features)) {
+      this.logger.fatal('Invalid GeoJSON FeatureCollection');
+      return;
+    }
 
-    try {
-      await queryRunner.startTransaction();
-      await queryRunner.manager.clear(NaturalEarthCountriesEntity);
-
-      const fileContent = await readFile(resourcePaths.geodata.naturalEarthCountriesPath, 'utf8');
-      const geoJSONData = JSON.parse(fileContent);
-
-      if (geoJSONData.type !== 'FeatureCollection' || !Array.isArray(geoJSONData.features)) {
-        this.logger.fatal('Invalid GeoJSON FeatureCollection');
-        return;
-      }
-
-      for await (const feature of geoJSONData.features) {
-        for (const polygon of feature.geometry.coordinates) {
-          const featureRecord = new NaturalEarthCountriesEntity();
-          featureRecord.admin = feature.properties.ADMIN;
-          featureRecord.admin_a3 = feature.properties.ADM0_A3;
-          featureRecord.type = feature.properties.TYPE;
-
-          if (feature.geometry.type === 'MultiPolygon') {
-            featureRecord.coordinates = this.transformCoordinatesToPolygon(polygon[0]);
-            await queryRunner.manager.save(featureRecord);
-          } else if (feature.geometry.type === 'Polygon') {
-            featureRecord.coordinates = this.transformCoordinatesToPolygon(polygon);
-            await queryRunner.manager.save(featureRecord);
-            break;
-          }
+    await this.dataSource.query('DROP TABLE IF EXISTS naturalearth_countries_tmp');
+    await this.dataSource.query(
+      'CREATE UNLOGGED TABLE naturalearth_countries_tmp (LIKE naturalearth_countries INCLUDING ALL EXCLUDING INDEXES)',
+    );
+    const entities: Omit<NaturalEarthCountriesTempEntity, 'id'>[] = [];
+    for (const feature of geoJSONData.features) {
+      for (const entry of feature.geometry.coordinates) {
+        const coordinates: number[][][] = feature.geometry.type === 'MultiPolygon' ? entry[0] : entry;
+        const featureRecord: Omit<NaturalEarthCountriesTempEntity, 'id'> = {
+          admin: feature.properties.ADMIN,
+          admin_a3: feature.properties.ADM0_A3,
+          type: feature.properties.TYPE,
+          coordinates: `(${coordinates.map((point) => `(${point[0]},${point[1]})`).join(', ')})`,
+        };
+        entities.push(featureRecord);
+        if (feature.geometry.type === 'Polygon') {
+          break;
         }
       }
-
-      await queryRunner.commitTransaction();
-    } catch (error) {
-      this.logger.fatal('Error importing natural earth country data', error);
-      await queryRunner.rollbackTransaction();
-      throw error;
-    } finally {
-      await queryRunner.release();
     }
+    await this.dataSource.manager.insert(NaturalEarthCountriesTempEntity, entities);
+
+    await this.dataSource.query(`ALTER TABLE naturalearth_countries_tmp ADD PRIMARY KEY (id) WITH (FILLFACTOR = 100)`);
+
+    await this.dataSource.transaction(async (manager) => {
+      await manager.query('ALTER TABLE naturalearth_countries RENAME TO naturalearth_countries_old');
+      await manager.query('ALTER TABLE naturalearth_countries_tmp RENAME TO naturalearth_countries');
+      await manager.query('DROP TABLE naturalearth_countries_old');
+    });
   }
 
   private async importGeodata() {
-    const queryRunner = this.dataSource.createQueryRunner();
-    await queryRunner.connect();
+    const { resourcePaths } = this.configRepository.getEnv();
+    const [admin1, admin2] = await Promise.all([
+      this.loadAdmin(resourcePaths.geodata.admin1),
+      this.loadAdmin(resourcePaths.geodata.admin2),
+    ]);
 
-    const admin1 = await this.loadAdmin(resourcePaths.geodata.admin1);
-    const admin2 = await this.loadAdmin(resourcePaths.geodata.admin2);
+    await this.dataSource.query('DROP TABLE IF EXISTS geodata_places_tmp');
+    await this.dataSource.query(
+      'CREATE UNLOGGED TABLE geodata_places_tmp (LIKE geodata_places INCLUDING ALL EXCLUDING INDEXES)',
+    );
+    await this.loadCities500(admin1, admin2);
+    await this.createGeodataIndices();
 
-    try {
-      await queryRunner.startTransaction();
-
-      await queryRunner.manager.clear(GeodataPlacesEntity);
-      await this.loadCities500(queryRunner, admin1, admin2);
-
-      await queryRunner.commitTransaction();
-    } catch (error) {
-      this.logger.fatal('Error importing geodata', error);
-      await queryRunner.rollbackTransaction();
-      throw error;
-    } finally {
-      await queryRunner.release();
-    }
+    await this.dataSource.transaction(async (manager) => {
+      await manager.query('ALTER TABLE geodata_places RENAME TO geodata_places_old');
+      await manager.query('ALTER TABLE geodata_places_tmp RENAME TO geodata_places');
+      await manager.query('DROP TABLE geodata_places_old');
+    });
   }
 
-  private async loadGeodataToTableFromFile(
-    queryRunner: QueryRunner,
-    lineToEntityMapper: (lineSplit: string[]) => GeodataPlacesEntity,
-    filePath: string,
-    options?: { entityFilter?: (linesplit: string[]) => boolean },
-  ) {
-    const _entityFilter = options?.entityFilter ?? (() => true);
-    if (!existsSync(filePath)) {
-      this.logger.error(`Geodata file ${filePath} not found`);
-      throw new Error(`Geodata file ${filePath} not found`);
+  private async loadCities500(admin1Map: Map<string, string>, admin2Map: Map<string, string>) {
+    const { resourcePaths } = this.configRepository.getEnv();
+    const cities500 = resourcePaths.geodata.cities500;
+    if (!existsSync(cities500)) {
+      throw new Error(`Geodata file ${cities500} not found`);
     }
 
-    const input = createReadStream(filePath);
-    let bufferGeodata: QueryDeepPartialEntity<GeodataPlacesEntity>[] = [];
+    const input = createReadStream(cities500, { highWaterMark: 512 * 1024 * 1024 });
+    let bufferGeodata: QueryDeepPartialEntity<GeodataPlacesTempEntity>[] = [];
     const lineReader = readLine.createInterface({ input });
+    let count = 0;
 
+    let futures = [];
     for await (const line of lineReader) {
       const lineSplit = line.split('\t');
-      if (!_entityFilter(lineSplit)) {
+      if (lineSplit[7] === 'PPLX' && lineSplit[8] !== 'AU') {
         continue;
       }
-      const geoData = lineToEntityMapper(lineSplit);
+
+      const geoData = {
+        id: Number.parseInt(lineSplit[0]),
+        name: lineSplit[1],
+        alternateNames: lineSplit[3],
+        latitude: Number.parseFloat(lineSplit[4]),
+        longitude: Number.parseFloat(lineSplit[5]),
+        countryCode: lineSplit[8],
+        admin1Code: lineSplit[10],
+        admin2Code: lineSplit[11],
+        modificationDate: lineSplit[18],
+        admin1Name: admin1Map.get(`${lineSplit[8]}.${lineSplit[10]}`),
+        admin2Name: admin2Map.get(`${lineSplit[8]}.${lineSplit[10]}.${lineSplit[11]}`),
+      };
       bufferGeodata.push(geoData);
-      if (bufferGeodata.length > 1000) {
-        await queryRunner.manager.upsert(GeodataPlacesEntity, bufferGeodata, ['id']);
+      if (bufferGeodata.length >= 5000) {
+        const curLength = bufferGeodata.length;
+        futures.push(
+          this.dataSource.manager.insert(GeodataPlacesTempEntity, bufferGeodata).then(() => {
+            count += curLength;
+            if (count % 10_000 === 0) {
+              this.logger.log(`${count} geodata records imported`);
+            }
+          }),
+        );
         bufferGeodata = [];
+        // leave spare connection for other queries
+        if (futures.length >= 9) {
+          await Promise.all(futures);
+          futures = [];
+        }
       }
     }
-    await queryRunner.manager.upsert(GeodataPlacesEntity, bufferGeodata, ['id']);
-  }
 
-  private async loadCities500(
-    queryRunner: QueryRunner,
-    admin1Map: Map<string, string>,
-    admin2Map: Map<string, string>,
-  ) {
-    await this.loadGeodataToTableFromFile(
-      queryRunner,
-      (lineSplit: string[]) =>
-        this.geodataPlacesRepository.create({
-          id: Number.parseInt(lineSplit[0]),
-          name: lineSplit[1],
-          alternateNames: lineSplit[3],
-          latitude: Number.parseFloat(lineSplit[4]),
-          longitude: Number.parseFloat(lineSplit[5]),
-          countryCode: lineSplit[8],
-          admin1Code: lineSplit[10],
-          admin2Code: lineSplit[11],
-          modificationDate: lineSplit[18],
-          admin1Name: admin1Map.get(`${lineSplit[8]}.${lineSplit[10]}`),
-          admin2Name: admin2Map.get(`${lineSplit[8]}.${lineSplit[10]}.${lineSplit[11]}`),
-        }),
-      resourcePaths.geodata.cities500,
-      {
-        entityFilter: (lineSplit) => {
-          if (lineSplit[7] === 'PPLX') {
-            // Exclude populated subsections of cities that are not in Australia.
-            // Australia has a lot of PPLX areas, so we include them.
-            return lineSplit[8] === 'AU';
-          }
-          return true;
-        },
-      },
-    );
+    await this.dataSource.manager.insert(GeodataPlacesTempEntity, bufferGeodata);
   }
 
   private async loadAdmin(filePath: string) {
@@ -316,7 +289,7 @@ export class MapRepository implements IMapRepository {
       throw new Error(`Geodata file ${filePath} not found`);
     }
 
-    const input = createReadStream(filePath);
+    const input = createReadStream(filePath, { highWaterMark: 512 * 1024 * 1024 });
     const lineReader = readLine.createInterface({ input });
 
     const adminMap = new Map<string, string>();
@@ -327,4 +300,27 @@ export class MapRepository implements IMapRepository {
 
     return adminMap;
   }
+
+  private createGeodataIndices() {
+    return Promise.all([
+      this.dataSource.query(`ALTER TABLE geodata_places_tmp ADD PRIMARY KEY (id) WITH (FILLFACTOR = 100)`),
+      this.dataSource.query(`
+        CREATE INDEX IDX_geodata_gist_earthcoord_${randomUUID().replaceAll('-', '_')}
+          ON geodata_places_tmp
+          USING gist (ll_to_earth_public(latitude, longitude))
+          WITH (fillfactor = 100)`),
+      this.dataSource.query(`
+        CREATE INDEX idx_geodata_places_name_${randomUUID().replaceAll('-', '_')}
+          ON geodata_places_tmp
+          USING gin (f_unaccent(name) gin_trgm_ops)`),
+      this.dataSource.query(`
+        CREATE INDEX idx_geodata_places_admin1_name_${randomUUID().replaceAll('-', '_')}
+          ON geodata_places_tmp
+          USING gin (f_unaccent("admin1Name") gin_trgm_ops)`),
+      this.dataSource.query(`
+        CREATE INDEX idx_geodata_places_admin2_name_${randomUUID().replaceAll('-', '_')}
+          ON geodata_places_tmp
+          USING gin (f_unaccent("admin2Name") gin_trgm_ops)`),
+    ]);
+  }
 }
diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts
index a84ef6f596..8dcbf208c6 100644
--- a/server/src/repositories/media.repository.ts
+++ b/server/src/repositories/media.repository.ts
@@ -1,27 +1,41 @@
 import { Inject, Injectable } from '@nestjs/common';
 import { exiftool } from 'exiftool-vendored';
 import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
+import { Duration } from 'luxon';
 import fs from 'node:fs/promises';
 import { Writable } from 'node:stream';
-import { promisify } from 'node:util';
 import sharp from 'sharp';
-import { Colorspace } from 'src/config';
+import { ORIENTATION_TO_SHARP_ROTATION } from 'src/constants';
+import { Colorspace, LogLevel } from 'src/enum';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import {
+  DecodeToBufferOptions,
+  GenerateThumbhashOptions,
+  GenerateThumbnailOptions,
   IMediaRepository,
   ImageDimensions,
-  ThumbnailOptions,
+  ProbeOptions,
   TranscodeCommand,
   VideoInfo,
 } from 'src/interfaces/media.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 import { handlePromiseError } from 'src/utils/misc';
 
-const probe = promisify<string, FfprobeData>(ffmpeg.ffprobe);
+const probe = (input: string, options: string[]): Promise<FfprobeData> =>
+  new Promise((resolve, reject) =>
+    ffmpeg.ffprobe(input, options, (error, data) => (error ? reject(error) : resolve(data))),
+  );
 sharp.concurrency(0);
 sharp.cache({ files: 0 });
 
-@Instrumentation()
+type ProgressEvent = {
+  frames: number;
+  currentFps: number;
+  currentKbps: number;
+  targetSize: number;
+  timemark: string;
+  percent?: number;
+};
+
 @Injectable()
 export class MediaRepository implements IMediaRepository {
   constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) {
@@ -44,19 +58,12 @@ export class MediaRepository implements IMediaRepository {
     return true;
   }
 
-  async generateThumbnail(input: string | Buffer, output: string, options: ThumbnailOptions): Promise<void> {
-    // some invalid images can still be processed by sharp, but we want to fail on them by default to avoid crashes
-    const pipeline = sharp(input, { failOn: options.processInvalidImages ? 'none' : 'error', limitInputPixels: false })
-      .pipelineColorspace(options.colorspace === Colorspace.SRGB ? 'srgb' : 'rgb16')
-      .rotate();
+  decodeImage(input: string, options: DecodeToBufferOptions) {
+    return this.getImageDecodingPipeline(input, options).raw().toBuffer({ resolveWithObject: true });
+  }
 
-    if (options.crop) {
-      pipeline.extract(options.crop);
-    }
-
-    await pipeline
-      .resize(options.size, options.size, { fit: 'outside', withoutEnlargement: true })
-      .withIccProfile(options.colorspace)
+  async generateThumbnail(input: string | Buffer, options: GenerateThumbnailOptions, output: string): Promise<void> {
+    await this.getImageDecodingPipeline(input, options)
       .toFormat(options.format, {
         quality: options.quality,
         // this is default in libvips (except the threshold is 90), but we need to set it manually in sharp
@@ -65,28 +72,70 @@ export class MediaRepository implements IMediaRepository {
       .toFile(output);
   }
 
-  async probe(input: string): Promise<VideoInfo> {
-    const results = await probe(input);
+  private getImageDecodingPipeline(input: string | Buffer, options: DecodeToBufferOptions) {
+    let pipeline = sharp(input, {
+      // some invalid images can still be processed by sharp, but we want to fail on them by default to avoid crashes
+      failOn: options.processInvalidImages ? 'none' : 'error',
+      limitInputPixels: false,
+      raw: options.raw,
+    })
+      .pipelineColorspace(options.colorspace === Colorspace.SRGB ? 'srgb' : 'rgb16')
+      .withIccProfile(options.colorspace);
+
+    if (!options.raw) {
+      const { angle, flip, flop } = options.orientation ? ORIENTATION_TO_SHARP_ROTATION[options.orientation] : {};
+      pipeline = pipeline.rotate(angle);
+      if (flip) {
+        pipeline = pipeline.flip();
+      }
+
+      if (flop) {
+        pipeline = pipeline.flop();
+      }
+    }
+
+    if (options.crop) {
+      pipeline = pipeline.extract(options.crop);
+    }
+
+    return pipeline.resize(options.size, options.size, { fit: 'outside', withoutEnlargement: true });
+  }
+
+  async generateThumbhash(input: string | Buffer, options: GenerateThumbhashOptions): Promise<Buffer> {
+    const [{ rgbaToThumbHash }, { data, info }] = await Promise.all([
+      import('thumbhash'),
+      sharp(input, options)
+        .resize(100, 100, { fit: 'inside', withoutEnlargement: true })
+        .raw()
+        .ensureAlpha()
+        .toBuffer({ resolveWithObject: true }),
+    ]);
+    return Buffer.from(rgbaToThumbHash(info.width, info.height, data));
+  }
+
+  async probe(input: string, options?: ProbeOptions): Promise<VideoInfo> {
+    const results = await probe(input, options?.countFrames ? ['-count_packets'] : []); // gets frame count quickly: https://stackoverflow.com/a/28376817
     return {
       format: {
         formatName: results.format.format_name,
         formatLongName: results.format.format_long_name,
-        duration: results.format.duration || 0,
-        bitrate: results.format.bit_rate ?? 0,
+        duration: this.parseFloat(results.format.duration),
+        bitrate: this.parseInt(results.format.bit_rate),
       },
       videoStreams: results.streams
         .filter((stream) => stream.codec_type === 'video')
         .filter((stream) => !stream.disposition?.attached_pic)
         .map((stream) => ({
           index: stream.index,
-          height: stream.height || 0,
-          width: stream.width || 0,
+          height: this.parseInt(stream.height),
+          width: this.parseInt(stream.width),
           codecName: stream.codec_name === 'h265' ? 'hevc' : stream.codec_name,
           codecType: stream.codec_type,
-          frameCount: Number.parseInt(stream.nb_frames ?? '0'),
-          rotation: Number.parseInt(`${stream.rotation ?? 0}`),
+          frameCount: this.parseInt(options?.countFrames ? stream.nb_read_packets : stream.nb_frames),
+          rotation: this.parseInt(stream.rotation),
           isHDR: stream.color_transfer === 'smpte2084' || stream.color_transfer === 'arib-std-b67',
-          bitrate: Number.parseInt(stream.bit_rate ?? '0'),
+          bitrate: this.parseInt(stream.bit_rate),
+          pixelFormat: stream.pix_fmt || 'yuv420p',
         })),
       audioStreams: results.streams
         .filter((stream) => stream.codec_type === 'audio')
@@ -94,7 +143,7 @@ export class MediaRepository implements IMediaRepository {
           index: stream.index,
           codecType: stream.codec_type,
           codecName: stream.codec_name,
-          frameCount: Number.parseInt(stream.nb_frames ?? '0'),
+          frameCount: this.parseInt(options?.countFrames ? stream.nb_read_packets : stream.nb_frames),
         })),
     };
   }
@@ -137,29 +186,47 @@ export class MediaRepository implements IMediaRepository {
     });
   }
 
-  async generateThumbhash(imagePath: string): Promise<Buffer> {
-    const maxSize = 100;
-
-    const { data, info } = await sharp(imagePath)
-      .resize(maxSize, maxSize, { fit: 'inside', withoutEnlargement: true })
-      .raw()
-      .ensureAlpha()
-      .toBuffer({ resolveWithObject: true });
-
-    const thumbhash = await import('thumbhash');
-    return Buffer.from(thumbhash.rgbaToThumbHash(info.width, info.height, data));
-  }
-
   async getImageDimensions(input: string): Promise<ImageDimensions> {
     const { width = 0, height = 0 } = await sharp(input).metadata();
     return { width, height };
   }
 
   private configureFfmpegCall(input: string, output: string | Writable, options: TranscodeCommand) {
-    return ffmpeg(input, { niceness: 10 })
+    const ffmpegCall = ffmpeg(input, { niceness: 10 })
       .inputOptions(options.inputOptions)
       .outputOptions(options.outputOptions)
       .output(output)
-      .on('error', (error, stdout, stderr) => this.logger.error(stderr || error));
+      .on('start', (command: string) => this.logger.debug(command))
+      .on('error', (error, _, stderr) => this.logger.error(stderr || error));
+
+    const { frameCount, percentInterval } = options.progress;
+    const frameInterval = Math.ceil(frameCount / (100 / percentInterval));
+    if (this.logger.isLevelEnabled(LogLevel.DEBUG) && frameCount && frameInterval) {
+      let lastProgressFrame: number = 0;
+      ffmpegCall.on('progress', (progress: ProgressEvent) => {
+        if (progress.frames - lastProgressFrame < frameInterval) {
+          return;
+        }
+
+        lastProgressFrame = progress.frames;
+        const percent = ((progress.frames / frameCount) * 100).toFixed(2);
+        const ms = progress.currentFps ? Math.floor((frameCount - progress.frames) / progress.currentFps) * 1000 : 0;
+        const duration = ms ? Duration.fromMillis(ms).rescale().toHuman({ unitDisplay: 'narrow' }) : '';
+        const outputText = output instanceof Writable ? 'stream' : output.split('/').pop();
+        this.logger.debug(
+          `Transcoding ${percent}% done${duration ? `, estimated ${duration} remaining` : ''} for output ${outputText}`,
+        );
+      });
+    }
+
+    return ffmpegCall;
+  }
+
+  private parseInt(value: string | number | undefined): number {
+    return Number.parseInt(value as string) || 0;
+  }
+
+  private parseFloat(value: string | number | undefined): number {
+    return Number.parseFloat(value as string) || 0;
   }
 }
diff --git a/server/src/repositories/memory.repository.ts b/server/src/repositories/memory.repository.ts
index e9b4532fe9..3c2a1ae191 100644
--- a/server/src/repositories/memory.repository.ts
+++ b/server/src/repositories/memory.repository.ts
@@ -3,10 +3,8 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
 import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
 import { MemoryEntity } from 'src/entities/memory.entity';
 import { IMemoryRepository } from 'src/interfaces/memory.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 import { DataSource, In, Repository } from 'typeorm';
 
-@Instrumentation()
 @Injectable()
 export class MemoryRepository implements IMemoryRepository {
   constructor(
diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts
index f5933915ce..81c1b35e15 100644
--- a/server/src/repositories/metadata.repository.ts
+++ b/server/src/repositories/metadata.repository.ts
@@ -1,21 +1,16 @@
 import { Inject, Injectable } from '@nestjs/common';
-import { InjectRepository } from '@nestjs/typeorm';
 import { DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored';
 import geotz from 'geo-tz';
-import { DummyValue, GenerateSql } from 'src/decorators';
-import { ExifEntity } from 'src/entities/exif.entity';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
-import { Repository } from 'typeorm';
 
-@Instrumentation()
 @Injectable()
 export class MetadataRepository implements IMetadataRepository {
   private exiftool = new ExifTool({
     defaultVideosToUTC: true,
     backfillTimezones: true,
     inferTimezoneFromDatestamps: true,
+    inferTimezoneFromTimeStamp: true,
     useMWG: true,
     numericTags: [...DefaultReadTaskOptions.numericTags, 'FocalLength'],
     /* eslint unicorn/no-array-callback-reference: off, unicorn/no-array-method-this-argument: off */
@@ -25,10 +20,7 @@ export class MetadataRepository implements IMetadataRepository {
     writeArgs: ['-api', 'largefilesupport=1', '-overwrite_original'],
   });
 
-  constructor(
-    @InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-  ) {
+  constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) {
     this.logger.setContext(MetadataRepository.name);
   }
 
@@ -54,91 +46,4 @@ export class MetadataRepository implements IMetadataRepository {
       this.logger.warn(`Error writing exif data (${path}): ${error}`);
     }
   }
-
-  @GenerateSql({ params: [[DummyValue.UUID]] })
-  async getCountries(userIds: string[]): Promise<string[]> {
-    const results = await this.exifRepository
-      .createQueryBuilder('exif')
-      .leftJoin('exif.asset', 'asset')
-      .where('asset.ownerId IN (:...userIds )', { userIds })
-      .select('exif.country', 'country')
-      .distinctOn(['exif.country'])
-      .getRawMany<{ country: string }>();
-
-    return results.map(({ country }) => country).filter((item) => item !== '');
-  }
-
-  @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] })
-  async getStates(userIds: string[], country: string | undefined): Promise<string[]> {
-    const query = this.exifRepository
-      .createQueryBuilder('exif')
-      .leftJoin('exif.asset', 'asset')
-      .where('asset.ownerId IN (:...userIds )', { userIds })
-      .select('exif.state', 'state')
-      .distinctOn(['exif.state']);
-
-    if (country) {
-      query.andWhere('exif.country = :country', { country });
-    }
-
-    const result = await query.getRawMany<{ state: string }>();
-
-    return result.map(({ state }) => state).filter((item) => item !== '');
-  }
-
-  @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING, DummyValue.STRING] })
-  async getCities(userIds: string[], country: string | undefined, state: string | undefined): Promise<string[]> {
-    const query = this.exifRepository
-      .createQueryBuilder('exif')
-      .leftJoin('exif.asset', 'asset')
-      .where('asset.ownerId IN (:...userIds )', { userIds })
-      .select('exif.city', 'city')
-      .distinctOn(['exif.city']);
-
-    if (country) {
-      query.andWhere('exif.country = :country', { country });
-    }
-
-    if (state) {
-      query.andWhere('exif.state = :state', { state });
-    }
-
-    const results = await query.getRawMany<{ city: string }>();
-
-    return results.map(({ city }) => city).filter((item) => item !== '');
-  }
-
-  @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] })
-  async getCameraMakes(userIds: string[], model: string | undefined): Promise<string[]> {
-    const query = this.exifRepository
-      .createQueryBuilder('exif')
-      .leftJoin('exif.asset', 'asset')
-      .where('asset.ownerId IN (:...userIds )', { userIds })
-      .select('exif.make', 'make')
-      .distinctOn(['exif.make']);
-
-    if (model) {
-      query.andWhere('exif.model = :model', { model });
-    }
-
-    const results = await query.getRawMany<{ make: string }>();
-    return results.map(({ make }) => make).filter((item) => item !== '');
-  }
-
-  @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] })
-  async getCameraModels(userIds: string[], make: string | undefined): Promise<string[]> {
-    const query = this.exifRepository
-      .createQueryBuilder('exif')
-      .leftJoin('exif.asset', 'asset')
-      .where('asset.ownerId IN (:...userIds )', { userIds })
-      .select('exif.model', 'model')
-      .distinctOn(['exif.model']);
-
-    if (make) {
-      query.andWhere('exif.make = :make', { make });
-    }
-
-    const results = await query.getRawMany<{ model: string }>();
-    return results.map(({ model }) => model).filter((item) => item !== '');
-  }
 }
diff --git a/server/src/repositories/metric.repository.ts b/server/src/repositories/metric.repository.ts
deleted file mode 100644
index 5948e92fa6..0000000000
--- a/server/src/repositories/metric.repository.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { Injectable } from '@nestjs/common';
-import { MetricOptions } from '@opentelemetry/api';
-import { MetricService } from 'nestjs-otel';
-import { IMetricGroupRepository, IMetricRepository, MetricGroupOptions } from 'src/interfaces/metric.interface';
-import { apiMetrics, hostMetrics, jobMetrics, repoMetrics } from 'src/utils/instrumentation';
-
-class MetricGroupRepository implements IMetricGroupRepository {
-  private enabled = false;
-  constructor(private metricService: MetricService) {}
-
-  addToCounter(name: string, value: number, options?: MetricOptions): void {
-    if (this.enabled) {
-      this.metricService.getCounter(name, options).add(value);
-    }
-  }
-
-  addToGauge(name: string, value: number, options?: MetricOptions): void {
-    if (this.enabled) {
-      this.metricService.getUpDownCounter(name, options).add(value);
-    }
-  }
-
-  addToHistogram(name: string, value: number, options?: MetricOptions): void {
-    if (this.enabled) {
-      this.metricService.getHistogram(name, options).record(value);
-    }
-  }
-
-  configure(options: MetricGroupOptions): this {
-    this.enabled = options.enabled;
-    return this;
-  }
-}
-
-@Injectable()
-export class MetricRepository implements IMetricRepository {
-  api: MetricGroupRepository;
-  host: MetricGroupRepository;
-  jobs: MetricGroupRepository;
-  repo: MetricGroupRepository;
-
-  constructor(metricService: MetricService) {
-    this.api = new MetricGroupRepository(metricService).configure({ enabled: apiMetrics });
-    this.host = new MetricGroupRepository(metricService).configure({ enabled: hostMetrics });
-    this.jobs = new MetricGroupRepository(metricService).configure({ enabled: jobMetrics });
-    this.repo = new MetricGroupRepository(metricService).configure({ enabled: repoMetrics });
-  }
-}
diff --git a/server/src/repositories/move.repository.ts b/server/src/repositories/move.repository.ts
index a8416ff0ac..16d9004014 100644
--- a/server/src/repositories/move.repository.ts
+++ b/server/src/repositories/move.repository.ts
@@ -1,12 +1,11 @@
 import { Injectable } from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
 import { DummyValue, GenerateSql } from 'src/decorators';
-import { MoveEntity, PathType } from 'src/entities/move.entity';
+import { MoveEntity } from 'src/entities/move.entity';
+import { PathType } from 'src/enum';
 import { IMoveRepository, MoveCreate } from 'src/interfaces/move.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 import { Repository } from 'typeorm';
 
-@Instrumentation()
 @Injectable()
 export class MoveRepository implements IMoveRepository {
   constructor(@InjectRepository(MoveEntity) private repository: Repository<MoveEntity>) {}
diff --git a/server/src/repositories/notification.repository.spec.ts b/server/src/repositories/notification.repository.spec.ts
new file mode 100644
index 0000000000..368ba3f0ce
--- /dev/null
+++ b/server/src/repositories/notification.repository.spec.ts
@@ -0,0 +1,78 @@
+import { ILoggerRepository } from 'src/interfaces/logger.interface';
+import { EmailRenderRequest, EmailTemplate } from 'src/interfaces/notification.interface';
+import { NotificationRepository } from 'src/repositories/notification.repository';
+import { Mocked } from 'vitest';
+
+describe(NotificationRepository.name, () => {
+  let sut: NotificationRepository;
+  let loggerMock: Mocked<ILoggerRepository>;
+
+  beforeEach(() => {
+    loggerMock = {
+      setContext: vitest.fn(),
+      debug: vitest.fn(),
+    } as unknown as Mocked<ILoggerRepository>;
+
+    sut = new NotificationRepository(loggerMock);
+  });
+
+  describe('renderEmail', () => {
+    it('should render the email correctly for TEST_EMAIL template', async () => {
+      const request: EmailRenderRequest = {
+        template: EmailTemplate.TEST_EMAIL,
+        data: { displayName: 'Alen Turing', baseUrl: 'http://localhost' },
+        customTemplate: '',
+      };
+
+      const result = await sut.renderEmail(request);
+
+      expect(result.html).toContain('<!DOCTYPE html PUBLIC');
+      expect(result.text).toContain('test email');
+    });
+
+    it('should render the email correctly for WELCOME template', async () => {
+      const request: EmailRenderRequest = {
+        template: EmailTemplate.WELCOME,
+        data: { displayName: 'Alen Turing', username: 'turing', baseUrl: 'http://localhost' },
+        customTemplate: '',
+      };
+
+      const result = await sut.renderEmail(request);
+
+      expect(result.html).toContain('<!DOCTYPE html PUBLIC');
+      expect(result.text).toContain('A new account has been created for you');
+    });
+
+    it('should render the email correctly for ALBUM_INVITE template', async () => {
+      const request: EmailRenderRequest = {
+        template: EmailTemplate.ALBUM_INVITE,
+        data: {
+          albumName: 'Vacation',
+          albumId: '123',
+          senderName: 'John',
+          recipientName: 'Jane',
+          baseUrl: 'http://localhost',
+        },
+        customTemplate: '',
+      };
+
+      const result = await sut.renderEmail(request);
+
+      expect(result.html).toContain('<!DOCTYPE html PUBLIC');
+      expect(result.text).toContain('Vacation');
+    });
+
+    it('should render the email correctly for ALBUM_UPDATE template', async () => {
+      const request: EmailRenderRequest = {
+        template: EmailTemplate.ALBUM_UPDATE,
+        data: { albumName: 'Holiday', albumId: '123', recipientName: 'Jane', baseUrl: 'http://localhost' },
+        customTemplate: '',
+      };
+
+      const result = await sut.renderEmail(request);
+
+      expect(result.html).toContain('<!DOCTYPE html PUBLIC');
+      expect(result.text).toContain('Holiday');
+    });
+  });
+});
diff --git a/server/src/repositories/notification.repository.ts b/server/src/repositories/notification.repository.ts
index 9814a7bd5e..b2444301e5 100644
--- a/server/src/repositories/notification.repository.ts
+++ b/server/src/repositories/notification.repository.ts
@@ -15,9 +15,7 @@ import {
   SendEmailResponse,
   SmtpOptions,
 } from 'src/interfaces/notification.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 
-@Instrumentation()
 @Injectable()
 export class NotificationRepository implements INotificationRepository {
   constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) {
@@ -57,22 +55,22 @@ export class NotificationRepository implements INotificationRepository {
     }
   }
 
-  private render({ template, data }: EmailRenderRequest): React.FunctionComponentElement<any> {
+  private render({ template, data, customTemplate }: EmailRenderRequest): React.FunctionComponentElement<any> {
     switch (template) {
       case EmailTemplate.TEST_EMAIL: {
-        return React.createElement(TestEmail, data);
+        return React.createElement(TestEmail, { ...data, customTemplate });
       }
 
       case EmailTemplate.WELCOME: {
-        return React.createElement(WelcomeEmail, data);
+        return React.createElement(WelcomeEmail, { ...data, customTemplate });
       }
 
       case EmailTemplate.ALBUM_INVITE: {
-        return React.createElement(AlbumInviteEmail, data);
+        return React.createElement(AlbumInviteEmail, { ...data, customTemplate });
       }
 
       case EmailTemplate.ALBUM_UPDATE: {
-        return React.createElement(AlbumUpdateEmail, data);
+        return React.createElement(AlbumUpdateEmail, { ...data, customTemplate });
       }
     }
   }
diff --git a/server/src/repositories/oauth.repository.ts b/server/src/repositories/oauth.repository.ts
new file mode 100644
index 0000000000..ed038f0137
--- /dev/null
+++ b/server/src/repositories/oauth.repository.ts
@@ -0,0 +1,71 @@
+import { Inject, Injectable, InternalServerErrorException } from '@nestjs/common';
+import { custom, generators, Issuer } from 'openid-client';
+import { ILoggerRepository } from 'src/interfaces/logger.interface';
+import { IOAuthRepository, OAuthConfig, OAuthProfile } from 'src/interfaces/oauth.interface';
+
+@Injectable()
+export class OAuthRepository implements IOAuthRepository {
+  constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) {
+    this.logger.setContext(OAuthRepository.name);
+  }
+
+  init() {
+    custom.setHttpOptionsDefaults({ timeout: 30_000 });
+  }
+
+  async authorize(config: OAuthConfig, redirectUrl: string) {
+    const client = await this.getClient(config);
+    return client.authorizationUrl({
+      redirect_uri: redirectUrl,
+      scope: config.scope,
+      state: generators.state(),
+    });
+  }
+
+  async getLogoutEndpoint(config: OAuthConfig) {
+    const client = await this.getClient(config);
+    return client.issuer.metadata.end_session_endpoint;
+  }
+
+  async getProfile(config: OAuthConfig, url: string, redirectUrl: string): Promise<OAuthProfile> {
+    const client = await this.getClient(config);
+    const params = client.callbackParams(url);
+    try {
+      const tokens = await client.callback(redirectUrl, params, { state: params.state });
+      return await client.userinfo<OAuthProfile>(tokens.access_token || '');
+    } catch (error: Error | any) {
+      if (error.message.includes('unexpected JWT alg received')) {
+        this.logger.warn(
+          [
+            'Algorithm mismatch. Make sure the signing algorithm is set correctly in the OAuth settings.',
+            'Or, that you have specified a signing key in your OAuth provider.',
+          ].join(' '),
+        );
+      }
+
+      throw error;
+    }
+  }
+
+  private async getClient({
+    issuerUrl,
+    clientId,
+    clientSecret,
+    profileSigningAlgorithm,
+    signingAlgorithm,
+  }: OAuthConfig) {
+    try {
+      const issuer = await Issuer.discover(issuerUrl);
+      return new issuer.Client({
+        client_id: clientId,
+        client_secret: clientSecret,
+        response_types: ['code'],
+        userinfo_signed_response_alg: profileSigningAlgorithm === 'none' ? undefined : profileSigningAlgorithm,
+        id_token_signed_response_alg: signingAlgorithm,
+      });
+    } catch (error: any | AggregateError) {
+      this.logger.error(`Error in OAuth discovery: ${error}`, error?.stack, error?.errors);
+      throw new InternalServerErrorException(`Error in OAuth discovery: ${error}`, { cause: error });
+    }
+  }
+}
diff --git a/server/src/repositories/partner.repository.ts b/server/src/repositories/partner.repository.ts
index e0c8998dbf..6b11a4e31e 100644
--- a/server/src/repositories/partner.repository.ts
+++ b/server/src/repositories/partner.repository.ts
@@ -2,10 +2,8 @@ import { Injectable } from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
 import { PartnerEntity } from 'src/entities/partner.entity';
 import { IPartnerRepository, PartnerIds } from 'src/interfaces/partner.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 import { DeepPartial, Repository } from 'typeorm';
 
-@Instrumentation()
 @Injectable()
 export class PartnerRepository implements IPartnerRepository {
   constructor(@InjectRepository(PartnerEntity) private repository: Repository<PartnerEntity>) {}
diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts
index 2247195cc3..4229286706 100644
--- a/server/src/repositories/person.repository.ts
+++ b/server/src/repositories/person.repository.ts
@@ -5,24 +5,24 @@ import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
 import { AssetFaceEntity } from 'src/entities/asset-face.entity';
 import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
 import { AssetEntity } from 'src/entities/asset.entity';
+import { FaceSearchEntity } from 'src/entities/face-search.entity';
 import { PersonEntity } from 'src/entities/person.entity';
-import { SourceType } from 'src/enum';
+import { PaginationMode, SourceType } from 'src/enum';
 import {
   AssetFaceId,
-  DeleteAllFacesOptions,
+  DeleteFacesOptions,
   IPersonRepository,
   PeopleStatistics,
   PersonNameResponse,
   PersonNameSearchOptions,
   PersonSearchOptions,
   PersonStatistics,
+  UnassignFacesOptions,
   UpdateFacesData,
 } from 'src/interfaces/person.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
-import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
+import { Paginated, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
 import { DataSource, FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repository } from 'typeorm';
 
-@Instrumentation()
 @Injectable()
 export class PersonRepository implements IPersonRepository {
   constructor(
@@ -30,6 +30,7 @@ export class PersonRepository implements IPersonRepository {
     @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
     @InjectRepository(PersonEntity) private personRepository: Repository<PersonEntity>,
     @InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository<AssetFaceEntity>,
+    @InjectRepository(FaceSearchEntity) private faceSearchRepository: Repository<FaceSearchEntity>,
     @InjectRepository(AssetJobStatusEntity) private jobStatusRepository: Repository<AssetJobStatusEntity>,
   ) {}
 
@@ -39,35 +40,35 @@ export class PersonRepository implements IPersonRepository {
       .createQueryBuilder()
       .update()
       .set({ personId: newPersonId })
-      .where(_.omitBy({ personId: oldPersonId ?? undefined, id: faceIds ? In(faceIds) : undefined }, _.isUndefined))
+      .where(_.omitBy({ personId: oldPersonId, id: faceIds ? In(faceIds) : undefined }, _.isUndefined))
       .execute();
 
     return result.affected ?? 0;
   }
 
+  async unassignFaces({ sourceType }: UnassignFacesOptions): Promise<void> {
+    await this.assetFaceRepository
+      .createQueryBuilder()
+      .update()
+      .set({ personId: null })
+      .where({ sourceType })
+      .execute();
+
+    await this.vacuum({ reindexVectors: false });
+  }
+
   async delete(entities: PersonEntity[]): Promise<void> {
     await this.personRepository.remove(entities);
   }
 
-  async deleteAll(): Promise<void> {
-    await this.personRepository.clear();
-  }
-
-  async deleteAllFaces({ sourceType }: DeleteAllFacesOptions): Promise<void> {
-    if (!sourceType) {
-      return this.assetFaceRepository.query('TRUNCATE TABLE asset_faces CASCADE');
-    }
-
+  async deleteFaces({ sourceType }: DeleteFacesOptions): Promise<void> {
     await this.assetFaceRepository
       .createQueryBuilder('asset_faces')
       .delete()
       .andWhere('sourceType = :sourceType', { sourceType })
       .execute();
 
-    await this.assetFaceRepository.query('VACUUM ANALYZE asset_faces, face_search');
-    if (sourceType === SourceType.MACHINE_LEARNING) {
-      await this.assetFaceRepository.query('REINDEX INDEX face_index');
-    }
+    await this.vacuum({ reindexVectors: sourceType === SourceType.MACHINE_LEARNING });
   }
 
   getAllFaces(
@@ -82,10 +83,14 @@ export class PersonRepository implements IPersonRepository {
   }
 
   @GenerateSql({ params: [{ take: 10, skip: 10 }, DummyValue.UUID] })
-  getAllForUser(pagination: PaginationOptions, userId: string, options?: PersonSearchOptions): Paginated<PersonEntity> {
+  async getAllForUser(
+    pagination: PaginationOptions,
+    userId: string,
+    options?: PersonSearchOptions,
+  ): Paginated<PersonEntity> {
     const queryBuilder = this.personRepository
       .createQueryBuilder('person')
-      .leftJoin('person.faces', 'face')
+      .innerJoin('person.faces', 'face')
       .where('person.ownerId = :userId', { userId })
       .innerJoin('face.asset', 'asset')
       .andWhere('asset.isArchived = false')
@@ -94,13 +99,24 @@ export class PersonRepository implements IPersonRepository {
       .addOrderBy('COUNT(face.assetId)', 'DESC')
       .addOrderBy("NULLIF(person.name, '')", 'ASC', 'NULLS LAST')
       .addOrderBy('person.createdAt')
-      .andWhere("person.thumbnailPath != ''")
       .having("person.name != '' OR COUNT(face.assetId) >= :faces", { faces: options?.minimumFaceCount || 1 })
       .groupBy('person.id');
+    if (options?.closestFaceAssetId) {
+      const innerQueryBuilder = this.faceSearchRepository
+        .createQueryBuilder('face_search')
+        .select('embedding', 'embedding')
+        .where('"face_search"."faceId" = "person"."faceAssetId"');
+      const faceSelectQueryBuilder = this.faceSearchRepository
+        .createQueryBuilder('face_search')
+        .select('embedding', 'embedding')
+        .where('"face_search"."faceId" = :faceId', { faceId: options.closestFaceAssetId });
+      queryBuilder
+        .orderBy('(' + innerQueryBuilder.getQuery() + ') <=> (' + faceSelectQueryBuilder.getQuery() + ')')
+        .setParameters(faceSelectQueryBuilder.getParameters());
+    }
     if (!options?.withHidden) {
       queryBuilder.andWhere('person.isHidden = false');
     }
-
     return paginatedBuilder(queryBuilder, {
       mode: PaginationMode.LIMIT_OFFSET,
       ...pagination,
@@ -227,42 +243,16 @@ export class PersonRepository implements IPersonRepository {
     };
   }
 
-  @GenerateSql({ params: [DummyValue.UUID] })
-  getAssets(personId: string): Promise<AssetEntity[]> {
-    return this.assetRepository.find({
-      where: {
-        faces: {
-          personId,
-        },
-        isVisible: true,
-        isArchived: false,
-      },
-      relations: {
-        faces: {
-          person: true,
-        },
-        exifInfo: true,
-      },
-      order: {
-        fileCreatedAt: 'desc',
-      },
-      // TODO: remove after either (1) pagination or (2) time bucket is implemented for this query
-      take: 1000,
-    });
-  }
-
   @GenerateSql({ params: [DummyValue.UUID] })
   async getNumberOfPeople(userId: string): Promise<PeopleStatistics> {
     const items = await this.personRepository
       .createQueryBuilder('person')
-      .leftJoin('person.faces', 'face')
+      .innerJoin('person.faces', 'face')
       .where('person.ownerId = :userId', { userId })
       .innerJoin('face.asset', 'asset')
       .andWhere('asset.isArchived = false')
-      .andWhere("person.thumbnailPath != ''")
       .select('COUNT(DISTINCT(person.id))', 'total')
       .addSelect('COUNT(DISTINCT(person.id)) FILTER (WHERE person.isHidden = true)', 'hidden')
-      .having('COUNT(face.assetId) != 0')
       .getRawOne();
 
     if (items == undefined) {
@@ -286,17 +276,32 @@ export class PersonRepository implements IPersonRepository {
     return results.map((person) => person.id);
   }
 
-  async createFaces(entities: AssetFaceEntity[]): Promise<string[]> {
-    const res = await this.assetFaceRepository.save(entities);
-    return res.map((row) => row.id);
-  }
+  async refreshFaces(
+    facesToAdd: Partial<AssetFaceEntity>[],
+    faceIdsToRemove: string[],
+    embeddingsToAdd?: FaceSearchEntity[],
+  ): Promise<void> {
+    const query = this.faceSearchRepository.createQueryBuilder().select('1').fromDummy();
+    if (facesToAdd.length > 0) {
+      const insertCte = this.assetFaceRepository.createQueryBuilder().insert().values(facesToAdd);
+      query.addCommonTableExpression(insertCte, 'added');
+    }
 
-  async replaceFaces(assetId: string, entities: AssetFaceEntity[], sourceType: string): Promise<string[]> {
-    return this.dataSource.transaction(async (manager) => {
-      await manager.delete(AssetFaceEntity, { assetId, sourceType });
-      const assetFaces = await manager.save(AssetFaceEntity, entities);
-      return assetFaces.map(({ id }) => id);
-    });
+    if (faceIdsToRemove.length > 0) {
+      const deleteCte = this.assetFaceRepository
+        .createQueryBuilder()
+        .delete()
+        .where('id = any(:faceIdsToRemove)', { faceIdsToRemove });
+      query.addCommonTableExpression(deleteCte, 'deleted');
+    }
+
+    if (embeddingsToAdd?.length) {
+      const embeddingCte = this.faceSearchRepository.createQueryBuilder().insert().values(embeddingsToAdd).orIgnore();
+      query.addCommonTableExpression(embeddingCte, 'embeddings');
+      query.getQuery(); // typeorm mixes up parameters without this
+    }
+
+    await query.execute();
   }
 
   async update(person: Partial<PersonEntity>): Promise<PersonEntity> {
@@ -331,4 +336,13 @@ export class PersonRepository implements IPersonRepository {
     const { id } = await this.personRepository.save(person);
     return this.personRepository.findOneByOrFail({ id });
   }
+
+  private async vacuum({ reindexVectors }: { reindexVectors: boolean }): Promise<void> {
+    await this.assetFaceRepository.query('VACUUM ANALYZE asset_faces, face_search, person');
+    await this.assetFaceRepository.query('REINDEX TABLE asset_faces');
+    await this.assetFaceRepository.query('REINDEX TABLE person');
+    if (reindexVectors) {
+      await this.assetFaceRepository.query('REINDEX TABLE face_search');
+    }
+  }
 }
diff --git a/server/src/repositories/process.repository.ts b/server/src/repositories/process.repository.ts
new file mode 100644
index 0000000000..99ec51037c
--- /dev/null
+++ b/server/src/repositories/process.repository.ts
@@ -0,0 +1,16 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { ChildProcessWithoutNullStreams, spawn, SpawnOptionsWithoutStdio } from 'node:child_process';
+import { ILoggerRepository } from 'src/interfaces/logger.interface';
+import { IProcessRepository } from 'src/interfaces/process.interface';
+import { StorageRepository } from 'src/repositories/storage.repository';
+
+@Injectable()
+export class ProcessRepository implements IProcessRepository {
+  constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) {
+    this.logger.setContext(StorageRepository.name);
+  }
+
+  spawn(command: string, args: readonly string[], options?: SpawnOptionsWithoutStdio): ChildProcessWithoutNullStreams {
+    return spawn(command, args, options);
+  }
+}
diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts
index 40f87ddf24..0a529f2f6e 100644
--- a/server/src/repositories/search.repository.ts
+++ b/server/src/repositories/search.repository.ts
@@ -1,14 +1,15 @@
 import { Inject, Injectable } from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
-import { getVectorExtension } from 'src/database.config';
+import { randomUUID } from 'node:crypto';
 import { DummyValue, GenerateSql } from 'src/decorators';
 import { AssetFaceEntity } from 'src/entities/asset-face.entity';
 import { AssetEntity } from 'src/entities/asset.entity';
+import { ExifEntity } from 'src/entities/exif.entity';
 import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
-import { SmartInfoEntity } from 'src/entities/smart-info.entity';
 import { SmartSearchEntity } from 'src/entities/smart-search.entity';
-import { AssetType } from 'src/enum';
-import { DatabaseExtension } from 'src/interfaces/database.interface';
+import { AssetType, PaginationMode } from 'src/enum';
+import { IConfigRepository } from 'src/interfaces/config.interface';
+import { DatabaseExtension, VectorExtension } from 'src/interfaces/database.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import {
   AssetDuplicateResult,
@@ -16,30 +17,35 @@ import {
   AssetSearchOptions,
   FaceEmbeddingSearch,
   FaceSearchResult,
+  GetCameraMakesOptions,
+  GetCameraModelsOptions,
+  GetCitiesOptions,
+  GetStatesOptions,
   ISearchRepository,
   SearchPaginationOptions,
   SmartSearchOptions,
 } from 'src/interfaces/search.interface';
 import { asVector, searchAssetBuilder } from 'src/utils/database';
-import { Instrumentation } from 'src/utils/instrumentation';
-import { Paginated, PaginationMode, PaginationResult, paginatedBuilder } from 'src/utils/pagination';
+import { Paginated, PaginationResult, paginatedBuilder } from 'src/utils/pagination';
 import { isValidInteger } from 'src/validation';
-import { Repository, SelectQueryBuilder } from 'typeorm';
+import { Repository } from 'typeorm';
 
-@Instrumentation()
 @Injectable()
 export class SearchRepository implements ISearchRepository {
+  private vectorExtension: VectorExtension;
   private faceColumns: string[];
   private assetsByCityQuery: string;
 
   constructor(
-    @InjectRepository(SmartInfoEntity) private repository: Repository<SmartInfoEntity>,
     @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
+    @InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
     @InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository<AssetFaceEntity>,
     @InjectRepository(SmartSearchEntity) private smartSearchRepository: Repository<SmartSearchEntity>,
     @InjectRepository(GeodataPlacesEntity) private geodataPlacesRepository: Repository<GeodataPlacesEntity>,
     @Inject(ILoggerRepository) private logger: ILoggerRepository,
+    @Inject(IConfigRepository) configRepository: IConfigRepository,
   ) {
+    this.vectorExtension = configRepository.getEnv().database.vectorExtension;
     this.logger.setContext(SearchRepository.name);
     this.faceColumns = this.assetFaceRepository.manager.connection
       .getMetadata(AssetFaceEntity)
@@ -61,18 +67,16 @@ export class SearchRepository implements ISearchRepository {
       {
         takenAfter: DummyValue.DATE,
         lensModel: DummyValue.STRING,
-        ownerId: DummyValue.UUID,
         withStacked: true,
         isFavorite: true,
-        ownerIds: [DummyValue.UUID],
+        userIds: [DummyValue.UUID],
       },
     ],
   })
   async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated<AssetEntity> {
     let builder = this.assetRepository.createQueryBuilder('asset');
-    builder = searchAssetBuilder(builder, options);
+    builder = searchAssetBuilder(builder, options).orderBy('asset.fileCreatedAt', options.orderDirection ?? 'DESC');
 
-    builder.orderBy('asset.fileCreatedAt', options.orderDirection ?? 'DESC');
     return paginatedBuilder<AssetEntity>(builder, {
       mode: PaginationMode.SKIP_TAKE,
       skip: (pagination.page - 1) * pagination.size,
@@ -80,17 +84,38 @@ export class SearchRepository implements ISearchRepository {
     });
   }
 
-  private createPersonFilter(builder: SelectQueryBuilder<AssetFaceEntity>, personIds: string[]) {
-    return builder
-      .select(`${builder.alias}."assetId"`)
-      .where(`${builder.alias}."personId" IN (:...personIds)`, { personIds })
-      .groupBy(`${builder.alias}."assetId"`)
-      .having(`COUNT(DISTINCT ${builder.alias}."personId") = :personCount`, { personCount: personIds.length });
+  @GenerateSql({
+    params: [
+      100,
+      {
+        takenAfter: DummyValue.DATE,
+        lensModel: DummyValue.STRING,
+        withStacked: true,
+        isFavorite: true,
+        userIds: [DummyValue.UUID],
+      },
+    ],
+  })
+  async searchRandom(size: number, options: AssetSearchOptions): Promise<AssetEntity[]> {
+    const builder1 = searchAssetBuilder(this.assetRepository.createQueryBuilder('asset'), options);
+    const builder2 = builder1.clone();
+
+    const uuid = randomUUID();
+    builder1.andWhere('asset.id > :uuid', { uuid }).orderBy('asset.id').take(size);
+    builder2.andWhere('asset.id < :uuid', { uuid }).orderBy('asset.id').take(size);
+
+    const [assets1, assets2] = await Promise.all([builder1.getMany(), builder2.getMany()]);
+    const missingCount = size - assets1.length;
+    for (let i = 0; i < missingCount && i < assets2.length; i++) {
+      assets1.push(assets2[i]);
+    }
+
+    return assets1;
   }
 
   @GenerateSql({
     params: [
-      { page: 1, size: 100 },
+      { page: 1, size: 200 },
       {
         takenAfter: DummyValue.DATE,
         embedding: Array.from({ length: 512 }, Math.random),
@@ -103,21 +128,12 @@ export class SearchRepository implements ISearchRepository {
   })
   async searchSmart(
     pagination: SearchPaginationOptions,
-    { embedding, userIds, personIds, ...options }: SmartSearchOptions,
+    { embedding, userIds, ...options }: SmartSearchOptions,
   ): Paginated<AssetEntity> {
     let results: PaginationResult<AssetEntity> = { items: [], hasNextPage: false };
 
     await this.assetRepository.manager.transaction(async (manager) => {
       let builder = manager.createQueryBuilder(AssetEntity, 'asset');
-
-      if (personIds?.length) {
-        const assetFaceBuilder = manager.createQueryBuilder(AssetFaceEntity, 'asset_face');
-        const cte = this.createPersonFilter(assetFaceBuilder, personIds);
-        builder
-          .addCommonTableExpression(cte, 'asset_face_ids')
-          .innerJoin('asset_face_ids', 'a', 'a."assetId" = asset.id');
-      }
-
       builder = searchAssetBuilder(builder, options);
       builder
         .innerJoin('asset.smartSearch', 'search')
@@ -125,7 +141,10 @@ export class SearchRepository implements ISearchRepository {
         .orderBy('search.embedding <=> :embedding')
         .setParameters({ userIds, embedding: asVector(embedding) });
 
-      await manager.query(this.getRuntimeConfig(pagination.size));
+      const runtimeConfig = this.getRuntimeConfig(pagination.size);
+      if (runtimeConfig) {
+        await manager.query(runtimeConfig);
+      }
       results = await paginatedBuilder<AssetEntity>(builder, {
         mode: PaginationMode.LIMIT_OFFSET,
         skip: (pagination.page - 1) * pagination.size,
@@ -184,7 +203,7 @@ export class SearchRepository implements ISearchRepository {
       {
         userIds: [DummyValue.UUID],
         embedding: Array.from({ length: 512 }, Math.random),
-        numResults: 100,
+        numResults: 10,
         maxDistance: 0.6,
       },
     ],
@@ -224,7 +243,10 @@ export class SearchRepository implements ISearchRepository {
         cte.addSelect(`faces.${col}`, col);
       }
 
-      await manager.query(this.getRuntimeConfig(numResults));
+      const runtimeConfig = this.getRuntimeConfig(numResults);
+      if (runtimeConfig) {
+        await manager.query(runtimeConfig);
+      }
       results = await manager
         .createQueryBuilder()
         .select('res.*')
@@ -264,7 +286,7 @@ export class SearchRepository implements ISearchRepository {
   @GenerateSql({ params: [[DummyValue.UUID]] })
   async getAssetsByCity(userIds: string[]): Promise<AssetEntity[]> {
     const parameters = [userIds, true, false, AssetType.IMAGE];
-    const rawRes = await this.repository.query(this.assetsByCityQuery, parameters);
+    const rawRes = await this.assetRepository.query(this.assetsByCityQuery, parameters);
 
     const items: AssetEntity[] = [];
     for (const res of rawRes) {
@@ -322,17 +344,109 @@ export class SearchRepository implements ISearchRepository {
     return this.smartSearchRepository.clear();
   }
 
-  private getRuntimeConfig(numResults?: number): string {
-    if (getVectorExtension() === DatabaseExtension.VECTOR) {
+  @GenerateSql({ params: [[DummyValue.UUID]] })
+  async getCountries(userIds: string[]): Promise<string[]> {
+    const query = this.exifRepository
+      .createQueryBuilder('exif')
+      .innerJoin('exif.asset', 'asset')
+      .where('asset.ownerId IN (:...userIds )', { userIds })
+      .andWhere(`exif.country != ''`)
+      .andWhere('exif.country IS NOT NULL')
+      .select('exif.country', 'country')
+      .distinctOn(['exif.country']);
+
+    const results = await query.getRawMany<{ country: string }>();
+    return results.map(({ country }) => country);
+  }
+
+  @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] })
+  async getStates(userIds: string[], { country }: GetStatesOptions): Promise<string[]> {
+    const query = this.exifRepository
+      .createQueryBuilder('exif')
+      .innerJoin('exif.asset', 'asset')
+      .where('asset.ownerId IN (:...userIds )', { userIds })
+      .andWhere(`exif.state != ''`)
+      .andWhere('exif.state IS NOT NULL')
+      .select('exif.state', 'state')
+      .distinctOn(['exif.state']);
+
+    if (country) {
+      query.andWhere('exif.country = :country', { country });
+    }
+
+    const result = await query.getRawMany<{ state: string }>();
+    return result.map(({ state }) => state);
+  }
+
+  @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING, DummyValue.STRING] })
+  async getCities(userIds: string[], { country, state }: GetCitiesOptions): Promise<string[]> {
+    const query = this.exifRepository
+      .createQueryBuilder('exif')
+      .innerJoin('exif.asset', 'asset')
+      .where('asset.ownerId IN (:...userIds )', { userIds })
+      .andWhere(`exif.city != ''`)
+      .andWhere('exif.city IS NOT NULL')
+      .select('exif.city', 'city')
+      .distinctOn(['exif.city']);
+
+    if (country) {
+      query.andWhere('exif.country = :country', { country });
+    }
+
+    if (state) {
+      query.andWhere('exif.state = :state', { state });
+    }
+
+    const results = await query.getRawMany<{ city: string }>();
+    return results.map(({ city }) => city);
+  }
+
+  @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] })
+  async getCameraMakes(userIds: string[], { model }: GetCameraMakesOptions): Promise<string[]> {
+    const query = this.exifRepository
+      .createQueryBuilder('exif')
+      .innerJoin('exif.asset', 'asset')
+      .where('asset.ownerId IN (:...userIds )', { userIds })
+      .andWhere(`exif.make != ''`)
+      .andWhere('exif.make IS NOT NULL')
+      .select('exif.make', 'make')
+      .distinctOn(['exif.make']);
+
+    if (model) {
+      query.andWhere('exif.model = :model', { model });
+    }
+
+    const results = await query.getRawMany<{ make: string }>();
+    return results.map(({ make }) => make);
+  }
+
+  @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] })
+  async getCameraModels(userIds: string[], { make }: GetCameraModelsOptions): Promise<string[]> {
+    const query = this.exifRepository
+      .createQueryBuilder('exif')
+      .innerJoin('exif.asset', 'asset')
+      .where('asset.ownerId IN (:...userIds )', { userIds })
+      .andWhere(`exif.model != ''`)
+      .andWhere('exif.model IS NOT NULL')
+      .select('exif.model', 'model')
+      .distinctOn(['exif.model']);
+
+    if (make) {
+      query.andWhere('exif.make = :make', { make });
+    }
+
+    const results = await query.getRawMany<{ model: string }>();
+    return results.map(({ model }) => model);
+  }
+
+  private getRuntimeConfig(numResults?: number): string | undefined {
+    if (this.vectorExtension === DatabaseExtension.VECTOR) {
       return 'SET LOCAL hnsw.ef_search = 1000;'; // mitigate post-filter recall
     }
 
-    let runtimeConfig = 'SET LOCAL vectors.enable_prefilter=on; SET LOCAL vectors.search_mode=vbase;';
-    if (numResults) {
-      runtimeConfig += ` SET LOCAL vectors.hnsw_ef_search = ${numResults};`;
+    if (numResults && numResults !== 100) {
+      return `SET LOCAL vectors.hnsw_ef_search = ${Math.max(numResults, 100)};`;
     }
-
-    return runtimeConfig;
   }
 }
 
diff --git a/server/src/repositories/server-info.repository.ts b/server/src/repositories/server-info.repository.ts
index f74eb7dd0d..b4a4652871 100644
--- a/server/src/repositories/server-info.repository.ts
+++ b/server/src/repositories/server-info.repository.ts
@@ -4,10 +4,9 @@ import { exec as execCallback } from 'node:child_process';
 import { readFile } from 'node:fs/promises';
 import { promisify } from 'node:util';
 import sharp from 'sharp';
-import { resourcePaths } from 'src/constants';
+import { IConfigRepository } from 'src/interfaces/config.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { GitHubRelease, IServerInfoRepository, ServerBuildVersions } from 'src/interfaces/server-info.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 
 const exec = promisify(execCallback);
 const maybeFirstLine = async (command: string): Promise<string> => {
@@ -34,10 +33,12 @@ const getLockfileVersion = (name: string, lockfile?: BuildLockfile) => {
   return item?.version;
 };
 
-@Instrumentation()
 @Injectable()
 export class ServerInfoRepository implements IServerInfoRepository {
-  constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) {
+  constructor(
+    @Inject(IConfigRepository) private configRepository: IConfigRepository,
+    @Inject(ILoggerRepository) private logger: ILoggerRepository,
+  ) {
     this.logger.setContext(ServerInfoRepository.name);
   }
 
@@ -56,6 +57,8 @@ export class ServerInfoRepository implements IServerInfoRepository {
   }
 
   async getBuildVersions(): Promise<ServerBuildVersions> {
+    const { nodeVersion, resourcePaths } = this.configRepository.getEnv();
+
     const [nodejsOutput, ffmpegOutput, magickOutput] = await Promise.all([
       maybeFirstLine('node --version'),
       maybeFirstLine('ffmpeg -version'),
@@ -67,7 +70,7 @@ export class ServerInfoRepository implements IServerInfoRepository {
       .catch(() => this.logger.warn(`Failed to read ${resourcePaths.lockFile}`));
 
     return {
-      nodejs: nodejsOutput || process.env.NODE_VERSION || '',
+      nodejs: nodejsOutput || nodeVersion || '',
       exiftool: await exiftool.version(),
       ffmpeg: getLockfileVersion('ffmpeg', lockfile) || ffmpegOutput.replaceAll('ffmpeg version', '') || '',
       libvips: getLockfileVersion('libvips', lockfile) || sharp.versions.vips,
diff --git a/server/src/repositories/session.repository.ts b/server/src/repositories/session.repository.ts
index a4b55a19d7..3a0af1ef69 100644
--- a/server/src/repositories/session.repository.ts
+++ b/server/src/repositories/session.repository.ts
@@ -3,10 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
 import { DummyValue, GenerateSql } from 'src/decorators';
 import { SessionEntity } from 'src/entities/session.entity';
 import { ISessionRepository, SessionSearchOptions } from 'src/interfaces/session.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 import { LessThanOrEqual, Repository } from 'typeorm';
 
-@Instrumentation()
 @Injectable()
 export class SessionRepository implements ISessionRepository {
   constructor(@InjectRepository(SessionEntity) private repository: Repository<SessionEntity>) {}
diff --git a/server/src/repositories/shared-link.repository.ts b/server/src/repositories/shared-link.repository.ts
index 48dbb3ab90..1dfde99a75 100644
--- a/server/src/repositories/shared-link.repository.ts
+++ b/server/src/repositories/shared-link.repository.ts
@@ -3,10 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
 import { DummyValue, GenerateSql } from 'src/decorators';
 import { SharedLinkEntity } from 'src/entities/shared-link.entity';
 import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 import { Repository } from 'typeorm';
 
-@Instrumentation()
 @Injectable()
 export class SharedLinkRepository implements ISharedLinkRepository {
   constructor(@InjectRepository(SharedLinkEntity) private repository: Repository<SharedLinkEntity>) {}
diff --git a/server/src/repositories/stack.repository.ts b/server/src/repositories/stack.repository.ts
index f23a1c9a9c..9e9f09c442 100644
--- a/server/src/repositories/stack.repository.ts
+++ b/server/src/repositories/stack.repository.ts
@@ -3,10 +3,8 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { StackEntity } from 'src/entities/stack.entity';
 import { IStackRepository, StackSearch } from 'src/interfaces/stack.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 import { DataSource, In, Repository } from 'typeorm';
 
-@Instrumentation()
 @Injectable()
 export class StackRepository implements IStackRepository {
   constructor(
@@ -129,6 +127,7 @@ export class StackRepository implements IStackRepository {
       relations: {
         assets: {
           exifInfo: true,
+          tags: true,
         },
       },
       order: {
diff --git a/server/src/repositories/storage.repository.ts b/server/src/repositories/storage.repository.ts
index c699047ce1..a8d3db15d8 100644
--- a/server/src/repositories/storage.repository.ts
+++ b/server/src/repositories/storage.repository.ts
@@ -2,9 +2,10 @@ import { Inject, Injectable } from '@nestjs/common';
 import archiver from 'archiver';
 import chokidar, { WatchOptions } from 'chokidar';
 import { escapePath, glob, globStream } from 'fast-glob';
-import { constants, createReadStream, existsSync, mkdirSync } from 'node:fs';
+import { constants, createReadStream, createWriteStream, existsSync, mkdirSync } from 'node:fs';
 import fs from 'node:fs/promises';
 import path from 'node:path';
+import { Writable } from 'node:stream';
 import { CrawlOptionsDto, WalkOptionsDto } from 'src/dtos/library.dto';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import {
@@ -14,10 +15,8 @@ import {
   ImmichZipStream,
   WatchEvents,
 } from 'src/interfaces/storage.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 import { mimeTypes } from 'src/utils/mime-types';
 
-@Instrumentation()
 @Injectable()
 export class StorageRepository implements IStorageRepository {
   constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) {
@@ -40,8 +39,20 @@ export class StorageRepository implements IStorageRepository {
     return fs.stat(filepath);
   }
 
-  writeFile(filepath: string, buffer: Buffer) {
-    return fs.writeFile(filepath, buffer);
+  createFile(filepath: string, buffer: Buffer) {
+    return fs.writeFile(filepath, buffer, { flag: 'wx' });
+  }
+
+  createWriteStream(filepath: string): Writable {
+    return createWriteStream(filepath, { flags: 'w' });
+  }
+
+  createOrOverwriteFile(filepath: string, buffer: Buffer) {
+    return fs.writeFile(filepath, buffer, { flag: 'w' });
+  }
+
+  overwriteFile(filepath: string, buffer: Buffer) {
+    return fs.writeFile(filepath, buffer, { flag: 'r+' });
   }
 
   rename(source: string, target: string) {
@@ -148,7 +159,9 @@ export class StorageRepository implements IStorageRepository {
       return Promise.resolve([]);
     }
 
-    return glob(this.asGlob(pathsToCrawl), {
+    const globbedPaths = pathsToCrawl.map((path) => this.asGlob(path));
+
+    return glob(globbedPaths, {
       absolute: true,
       caseSensitiveMatch: false,
       onlyFiles: true,
@@ -164,7 +177,9 @@ export class StorageRepository implements IStorageRepository {
       return emptyGenerator();
     }
 
-    const stream = globStream(this.asGlob(pathsToCrawl), {
+    const globbedPaths = pathsToCrawl.map((path) => this.asGlob(path));
+
+    const stream = globStream(globbedPaths, {
       absolute: true,
       caseSensitiveMatch: false,
       onlyFiles: true,
@@ -198,10 +213,9 @@ export class StorageRepository implements IStorageRepository {
     return () => watcher.close();
   }
 
-  private asGlob(pathsToCrawl: string[]): string {
-    const escapedPaths = pathsToCrawl.map((path) => escapePath(path));
-    const base = escapedPaths.length === 1 ? escapedPaths[0] : `{${escapedPaths.join(',')}}`;
+  private asGlob(pathToCrawl: string): string {
+    const escapedPath = escapePath(pathToCrawl).replaceAll('"', '["]').replaceAll("'", "[']").replaceAll('`', '[`]');
     const extensions = `*{${mimeTypes.getSupportedFileExtensions().join(',')}}`;
-    return `${base}/**/${extensions}`;
+    return `${escapedPath}/**/${extensions}`;
   }
 }
diff --git a/server/src/repositories/system-metadata.repository.ts b/server/src/repositories/system-metadata.repository.ts
index d4e58bf74a..1c6aaf0517 100644
--- a/server/src/repositories/system-metadata.repository.ts
+++ b/server/src/repositories/system-metadata.repository.ts
@@ -3,10 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
 import { readFile } from 'node:fs/promises';
 import { SystemMetadata, SystemMetadataEntity } from 'src/entities/system-metadata.entity';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 import { Repository } from 'typeorm';
 
-@Instrumentation()
 @Injectable()
 export class SystemMetadataRepository implements ISystemMetadataRepository {
   constructor(
diff --git a/server/src/repositories/tag.repository.ts b/server/src/repositories/tag.repository.ts
index 9389aeb13b..df5f7e6e42 100644
--- a/server/src/repositories/tag.repository.ts
+++ b/server/src/repositories/tag.repository.ts
@@ -1,18 +1,21 @@
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
 import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
 import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
 import { TagEntity } from 'src/entities/tag.entity';
+import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { AssetTagItem, ITagRepository } from 'src/interfaces/tag.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
-import { DataSource, In, Repository } from 'typeorm';
+import { DataSource, In, Repository, TreeRepository } from 'typeorm';
 
-@Instrumentation()
 @Injectable()
 export class TagRepository implements ITagRepository {
   constructor(
     @InjectDataSource() private dataSource: DataSource,
     @InjectRepository(TagEntity) private repository: Repository<TagEntity>,
-  ) {}
+    @InjectRepository(TagEntity) private tree: TreeRepository<TagEntity>,
+    @Inject(ILoggerRepository) private logger: ILoggerRepository,
+  ) {
+    this.logger.setContext(TagRepository.name);
+  }
 
   get(id: string): Promise<TagEntity | null> {
     return this.repository.findOne({ where: { id } });
@@ -174,6 +177,34 @@ export class TagRepository implements ITagRepository {
     });
   }
 
+  async deleteEmptyTags() {
+    await this.dataSource.transaction(async (manager) => {
+      const ids = new Set<string>();
+      const tags = await manager.find(TagEntity);
+      for (const tag of tags) {
+        const count = await manager
+          .createQueryBuilder('assets', 'asset')
+          .innerJoin(
+            'asset.tags',
+            'asset_tags',
+            'asset_tags.id IN (SELECT id_descendant FROM tags_closure WHERE id_ancestor = :tagId)',
+            { tagId: tag.id },
+          )
+          .getCount();
+
+        if (count === 0) {
+          this.logger.debug(`Found empty tag: ${tag.id} - ${tag.value}`);
+          ids.add(tag.id);
+        }
+      }
+
+      if (ids.size > 0) {
+        await manager.delete(TagEntity, { id: In([...ids]) });
+        this.logger.log(`Deleted ${ids.size} empty tags`);
+      }
+    });
+  }
+
   private async save(partial: Partial<TagEntity>): Promise<TagEntity> {
     const { id } = await this.repository.save(partial);
     return this.repository.findOneOrFail({ where: { id } });
diff --git a/server/src/repositories/telemetry.repository.ts b/server/src/repositories/telemetry.repository.ts
new file mode 100644
index 0000000000..2510460967
--- /dev/null
+++ b/server/src/repositories/telemetry.repository.ts
@@ -0,0 +1,167 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Reflector } from '@nestjs/core';
+import { MetricOptions } from '@opentelemetry/api';
+import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
+import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
+import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
+import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis';
+import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core';
+import { PgInstrumentation } from '@opentelemetry/instrumentation-pg';
+import { NodeSDK, contextBase, metrics, resources } from '@opentelemetry/sdk-node';
+import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
+import { ClassConstructor } from 'class-transformer';
+import { snakeCase, startCase } from 'lodash';
+import { MetricService } from 'nestjs-otel';
+import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils';
+import { serverVersion } from 'src/constants';
+import { ImmichTelemetry, MetadataKey } from 'src/enum';
+import { IConfigRepository } from 'src/interfaces/config.interface';
+import { ILoggerRepository } from 'src/interfaces/logger.interface';
+import { IMetricGroupRepository, ITelemetryRepository, MetricGroupOptions } from 'src/interfaces/telemetry.interface';
+
+class MetricGroupRepository implements IMetricGroupRepository {
+  private enabled = false;
+
+  constructor(private metricService: MetricService) {}
+
+  addToCounter(name: string, value: number, options?: MetricOptions): void {
+    if (this.enabled) {
+      this.metricService.getCounter(name, options).add(value);
+    }
+  }
+
+  addToGauge(name: string, value: number, options?: MetricOptions): void {
+    if (this.enabled) {
+      this.metricService.getUpDownCounter(name, options).add(value);
+    }
+  }
+
+  addToHistogram(name: string, value: number, options?: MetricOptions): void {
+    if (this.enabled) {
+      this.metricService.getHistogram(name, options).record(value);
+    }
+  }
+
+  configure(options: MetricGroupOptions): this {
+    this.enabled = options.enabled;
+    return this;
+  }
+}
+
+const aggregation = new metrics.ExplicitBucketHistogramAggregation(
+  [0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10_000],
+  true,
+);
+
+let instance: NodeSDK | undefined;
+
+export const bootstrapTelemetry = (port: number) => {
+  if (instance) {
+    throw new Error('OpenTelemetry SDK already started');
+  }
+  instance = new NodeSDK({
+    resource: new resources.Resource({
+      [SemanticResourceAttributes.SERVICE_NAME]: `immich`,
+      [SemanticResourceAttributes.SERVICE_VERSION]: serverVersion.toString(),
+    }),
+    metricReader: new PrometheusExporter({ port }),
+    contextManager: new AsyncLocalStorageContextManager(),
+    instrumentations: [
+      new HttpInstrumentation(),
+      new IORedisInstrumentation(),
+      new NestInstrumentation(),
+      new PgInstrumentation(),
+    ],
+    views: [new metrics.View({ aggregation, instrumentName: '*', instrumentUnit: 'ms' })],
+  });
+
+  instance.start();
+};
+
+export const teardownTelemetry = async () => {
+  if (instance) {
+    await instance.shutdown();
+    instance = undefined;
+  }
+};
+
+@Injectable()
+export class TelemetryRepository implements ITelemetryRepository {
+  api: MetricGroupRepository;
+  host: MetricGroupRepository;
+  jobs: MetricGroupRepository;
+  repo: MetricGroupRepository;
+
+  constructor(
+    private metricService: MetricService,
+    private reflect: Reflector,
+    @Inject(IConfigRepository) private configRepository: IConfigRepository,
+    @Inject(ILoggerRepository) private logger: ILoggerRepository,
+  ) {
+    const { telemetry } = this.configRepository.getEnv();
+    const { metrics } = telemetry;
+
+    this.api = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.API) });
+    this.host = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.HOST) });
+    this.jobs = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.JOB) });
+    this.repo = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.REPO) });
+  }
+
+  setup({ repositories }: { repositories: ClassConstructor<unknown>[] }) {
+    const { telemetry } = this.configRepository.getEnv();
+    const { metrics } = telemetry;
+    if (!metrics.has(ImmichTelemetry.REPO)) {
+      return;
+    }
+
+    for (const Repository of repositories) {
+      const isEnabled = this.reflect.get(MetadataKey.TELEMETRY_ENABLED, Repository) ?? true;
+      if (!isEnabled) {
+        this.logger.debug(`Telemetry disabled for ${Repository.name}`);
+        continue;
+      }
+
+      this.wrap(Repository);
+    }
+  }
+
+  private wrap(Repository: ClassConstructor<unknown>) {
+    const className = Repository.name;
+    const descriptors = Object.getOwnPropertyDescriptors(Repository.prototype);
+    const unit = 'ms';
+
+    for (const [propName, descriptor] of Object.entries(descriptors)) {
+      const isMethod = typeof descriptor.value == 'function' && propName !== 'constructor';
+      if (!isMethod) {
+        continue;
+      }
+
+      const method = descriptor.value;
+      const propertyName = snakeCase(String(propName));
+      const metricName = `${snakeCase(className).replaceAll(/_(?=(repository)|(controller)|(provider)|(service)|(module))/g, '.')}.${propertyName}.duration`;
+
+      const histogram = this.metricService.getHistogram(metricName, {
+        prefix: 'immich',
+        description: `The elapsed time in ${unit} for the ${startCase(className)} to ${propertyName.toLowerCase()}`,
+        unit,
+        valueType: contextBase.ValueType.DOUBLE,
+      });
+
+      descriptor.value = function (...args: any[]) {
+        const start = performance.now();
+        const result = method.apply(this, args);
+
+        void Promise.resolve(result)
+          .then(() => histogram.record(performance.now() - start, {}))
+          .catch(() => {
+            // noop
+          });
+
+        return result;
+      };
+
+      copyMetadataFromFunctionToFunction(method, descriptor.value);
+      Object.defineProperty(Repository.prototype, propName, descriptor);
+    }
+  }
+}
diff --git a/server/src/repositories/trash.repository.ts b/server/src/repositories/trash.repository.ts
new file mode 100644
index 0000000000..d24f4f709a
--- /dev/null
+++ b/server/src/repositories/trash.repository.ts
@@ -0,0 +1,52 @@
+import { InjectRepository } from '@nestjs/typeorm';
+import { AssetEntity } from 'src/entities/asset.entity';
+import { AssetStatus } from 'src/enum';
+import { ITrashRepository } from 'src/interfaces/trash.interface';
+import { Paginated, paginatedBuilder, PaginationOptions } from 'src/utils/pagination';
+import { In, Repository } from 'typeorm';
+
+export class TrashRepository implements ITrashRepository {
+  constructor(@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>) {}
+
+  async getDeletedIds(pagination: PaginationOptions): Paginated<string> {
+    const { hasNextPage, items } = await paginatedBuilder(
+      this.assetRepository
+        .createQueryBuilder('asset')
+        .select('asset.id')
+        .where({ status: AssetStatus.DELETED })
+        .withDeleted(),
+      pagination,
+    );
+
+    return {
+      hasNextPage,
+      items: items.map((asset) => asset.id),
+    };
+  }
+
+  async restore(userId: string): Promise<number> {
+    const result = await this.assetRepository.update(
+      { ownerId: userId, status: AssetStatus.TRASHED },
+      { status: AssetStatus.ACTIVE, deletedAt: null },
+    );
+
+    return result.affected || 0;
+  }
+
+  async empty(userId: string): Promise<number> {
+    const result = await this.assetRepository.update(
+      { ownerId: userId, status: AssetStatus.TRASHED },
+      { status: AssetStatus.DELETED },
+    );
+
+    return result.affected || 0;
+  }
+
+  async restoreAll(ids: string[]): Promise<number> {
+    const result = await this.assetRepository.update(
+      { id: In(ids), status: AssetStatus.TRASHED },
+      { status: AssetStatus.ACTIVE, deletedAt: null },
+    );
+    return result.affected ?? 0;
+  }
+}
diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts
index c64d5a3655..a2e4375701 100644
--- a/server/src/repositories/user.repository.ts
+++ b/server/src/repositories/user.repository.ts
@@ -10,10 +10,8 @@ import {
   UserListFilter,
   UserStatsQueryResponse,
 } from 'src/interfaces/user.interface';
-import { Instrumentation } from 'src/utils/instrumentation';
 import { IsNull, Not, Repository } from 'typeorm';
 
-@Instrumentation()
 @Injectable()
 export class UserRepository implements IUserRepository {
   constructor(
@@ -110,6 +108,14 @@ export class UserRepository implements IUserRepository {
       .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'IMAGE' AND assets.isVisible)`, 'photos')
       .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'VIDEO' AND assets.isVisible)`, 'videos')
       .addSelect('COALESCE(SUM(exif.fileSizeInByte) FILTER (WHERE assets.libraryId IS NULL), 0)', 'usage')
+      .addSelect(
+        `COALESCE(SUM(exif.fileSizeInByte) FILTER (WHERE assets.libraryId IS NULL AND assets.type = 'IMAGE'), 0)`,
+        'usagePhotos',
+      )
+      .addSelect(
+        `COALESCE(SUM(exif.fileSizeInByte) FILTER (WHERE assets.libraryId IS NULL AND assets.type = 'VIDEO'), 0)`,
+        'usageVideos',
+      )
       .addSelect('users.quotaSizeInBytes', 'quotaSizeInBytes')
       .leftJoin('users.assets', 'assets')
       .leftJoin('assets.exifInfo', 'exif')
@@ -121,6 +127,8 @@ export class UserRepository implements IUserRepository {
       stat.photos = Number(stat.photos);
       stat.videos = Number(stat.videos);
       stat.usage = Number(stat.usage);
+      stat.usagePhotos = Number(stat.usagePhotos);
+      stat.usageVideos = Number(stat.usageVideos);
       stat.quotaSizeInBytes = stat.quotaSizeInBytes;
     }
 
diff --git a/server/src/repositories/version-history.repository.ts b/server/src/repositories/version-history.repository.ts
new file mode 100644
index 0000000000..e32ceaf4e9
--- /dev/null
+++ b/server/src/repositories/version-history.repository.ts
@@ -0,0 +1,23 @@
+import { Injectable } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { VersionHistoryEntity } from 'src/entities/version-history.entity';
+import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
+import { Repository } from 'typeorm';
+
+@Injectable()
+export class VersionHistoryRepository implements IVersionHistoryRepository {
+  constructor(@InjectRepository(VersionHistoryEntity) private repository: Repository<VersionHistoryEntity>) {}
+
+  async getAll(): Promise<VersionHistoryEntity[]> {
+    return this.repository.find({ order: { createdAt: 'DESC' } });
+  }
+
+  async getLatest(): Promise<VersionHistoryEntity | null> {
+    const results = await this.repository.find({ order: { createdAt: 'DESC' }, take: 1 });
+    return results[0] || null;
+  }
+
+  create(version: Omit<VersionHistoryEntity, 'id' | 'createdAt'>): Promise<VersionHistoryEntity> {
+    return this.repository.save(version);
+  }
+}
diff --git a/server/src/repositories/view-repository.ts b/server/src/repositories/view-repository.ts
new file mode 100644
index 0000000000..3645e3638a
--- /dev/null
+++ b/server/src/repositories/view-repository.ts
@@ -0,0 +1,48 @@
+import { InjectRepository } from '@nestjs/typeorm';
+import { DummyValue, GenerateSql } from 'src/decorators';
+import { AssetEntity } from 'src/entities/asset.entity';
+import { IViewRepository } from 'src/interfaces/view.interface';
+import { Brackets, Repository } from 'typeorm';
+
+export class ViewRepository implements IViewRepository {
+  constructor(@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>) {}
+
+  async getUniqueOriginalPaths(userId: string): Promise<string[]> {
+    const results = await this.assetRepository
+      .createQueryBuilder('asset')
+      .where({
+        isVisible: true,
+        isArchived: false,
+        ownerId: userId,
+      })
+      .select("DISTINCT substring(asset.originalPath FROM '^(.*/)[^/]*$')", 'directoryPath')
+      .getRawMany();
+
+    return results.map((row: { directoryPath: string }) => row.directoryPath.replaceAll(/^\/|\/$/g, ''));
+  }
+
+  @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
+  async getAssetsByOriginalPath(userId: string, partialPath: string): Promise<AssetEntity[]> {
+    const normalizedPath = partialPath.replaceAll(/^\/|\/$/g, '');
+    const assets = await this.assetRepository
+      .createQueryBuilder('asset')
+      .where({
+        isVisible: true,
+        isArchived: false,
+        ownerId: userId,
+      })
+      .leftJoinAndSelect('asset.exifInfo', 'exifInfo')
+      .andWhere(
+        new Brackets((qb) => {
+          qb.where('asset.originalPath LIKE :likePath', { likePath: `%${normalizedPath}/%` }).andWhere(
+            'asset.originalPath NOT LIKE :notLikePath',
+            { notLikePath: `%${normalizedPath}/%/%` },
+          );
+        }),
+      )
+      .orderBy(String.raw`regexp_replace(asset.originalPath, '.*/(.+)', '\1')`, 'ASC')
+      .getMany();
+
+    return assets;
+  }
+}
diff --git a/server/src/services/activity.service.spec.ts b/server/src/services/activity.service.spec.ts
index 30720b6c1f..f9a8e6ce47 100644
--- a/server/src/services/activity.service.spec.ts
+++ b/server/src/services/activity.service.spec.ts
@@ -4,20 +4,18 @@ import { IActivityRepository } from 'src/interfaces/activity.interface';
 import { ActivityService } from 'src/services/activity.service';
 import { activityStub } from 'test/fixtures/activity.stub';
 import { authStub } from 'test/fixtures/auth.stub';
-import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
-import { newActivityRepositoryMock } from 'test/repositories/activity.repository.mock';
+import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(ActivityService.name, () => {
   let sut: ActivityService;
+
   let accessMock: IAccessRepositoryMock;
   let activityMock: Mocked<IActivityRepository>;
 
   beforeEach(() => {
-    accessMock = newAccessRepositoryMock();
-    activityMock = newActivityRepositoryMock();
-
-    sut = new ActivityService(accessMock, activityMock);
+    ({ sut, accessMock, activityMock } = newTestService(ActivityService));
   });
 
   it('should work', () => {
diff --git a/server/src/services/activity.service.ts b/server/src/services/activity.service.ts
index 1e4034de93..fce104ecbd 100644
--- a/server/src/services/activity.service.ts
+++ b/server/src/services/activity.service.ts
@@ -1,4 +1,4 @@
-import { Inject, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
 import {
   ActivityCreateDto,
   ActivityDto,
@@ -13,20 +13,13 @@ import {
 import { AuthDto } from 'src/dtos/auth.dto';
 import { ActivityEntity } from 'src/entities/activity.entity';
 import { Permission } from 'src/enum';
-import { IAccessRepository } from 'src/interfaces/access.interface';
-import { IActivityRepository } from 'src/interfaces/activity.interface';
-import { requireAccess } from 'src/utils/access';
+import { BaseService } from 'src/services/base.service';
 
 @Injectable()
-export class ActivityService {
-  constructor(
-    @Inject(IAccessRepository) private access: IAccessRepository,
-    @Inject(IActivityRepository) private repository: IActivityRepository,
-  ) {}
-
+export class ActivityService extends BaseService {
   async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise<ActivityResponseDto[]> {
-    await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
-    const activities = await this.repository.search({
+    await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
+    const activities = await this.activityRepository.search({
       userId: dto.userId,
       albumId: dto.albumId,
       assetId: dto.level === ReactionLevel.ALBUM ? null : dto.assetId,
@@ -37,12 +30,12 @@ export class ActivityService {
   }
 
   async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
-    await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
-    return { comments: await this.repository.getStatistics(dto.assetId, dto.albumId) };
+    await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
+    return { comments: await this.activityRepository.getStatistics(dto.assetId, dto.albumId) };
   }
 
   async create(auth: AuthDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> {
-    await requireAccess(this.access, { auth, permission: Permission.ACTIVITY_CREATE, ids: [dto.albumId] });
+    await this.requireAccess({ auth, permission: Permission.ACTIVITY_CREATE, ids: [dto.albumId] });
 
     const common = {
       userId: auth.user.id,
@@ -55,7 +48,7 @@ export class ActivityService {
 
     if (dto.type === ReactionType.LIKE) {
       delete dto.comment;
-      [activity] = await this.repository.search({
+      [activity] = await this.activityRepository.search({
         ...common,
         // `null` will search for an album like
         assetId: dto.assetId ?? null,
@@ -65,7 +58,7 @@ export class ActivityService {
     }
 
     if (!activity) {
-      activity = await this.repository.create({
+      activity = await this.activityRepository.create({
         ...common,
         isLiked: dto.type === ReactionType.LIKE,
         comment: dto.comment,
@@ -76,7 +69,7 @@ export class ActivityService {
   }
 
   async delete(auth: AuthDto, id: string): Promise<void> {
-    await requireAccess(this.access, { auth, permission: Permission.ACTIVITY_DELETE, ids: [id] });
-    await this.repository.delete(id);
+    await this.requireAccess({ auth, permission: Permission.ACTIVITY_DELETE, ids: [id] });
+    await this.activityRepository.delete(id);
   }
 }
diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts
index 164e823336..12c93ee127 100644
--- a/server/src/services/album.service.spec.ts
+++ b/server/src/services/album.service.spec.ts
@@ -4,39 +4,27 @@ import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
 import { AlbumUserRole } from 'src/enum';
 import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
 import { IAlbumRepository } from 'src/interfaces/album.interface';
-import { IAssetRepository } from 'src/interfaces/asset.interface';
 import { IEventRepository } from 'src/interfaces/event.interface';
 import { IUserRepository } from 'src/interfaces/user.interface';
 import { AlbumService } from 'src/services/album.service';
 import { albumStub } from 'test/fixtures/album.stub';
 import { authStub } from 'test/fixtures/auth.stub';
 import { userStub } from 'test/fixtures/user.stub';
-import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
-import { newAlbumUserRepositoryMock } from 'test/repositories/album-user.repository.mock';
-import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
-import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
-import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
-import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
+import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(AlbumService.name, () => {
   let sut: AlbumService;
+
   let accessMock: IAccessRepositoryMock;
   let albumMock: Mocked<IAlbumRepository>;
-  let assetMock: Mocked<IAssetRepository>;
+  let albumUserMock: Mocked<IAlbumUserRepository>;
   let eventMock: Mocked<IEventRepository>;
   let userMock: Mocked<IUserRepository>;
-  let albumUserMock: Mocked<IAlbumUserRepository>;
 
   beforeEach(() => {
-    accessMock = newAccessRepositoryMock();
-    albumMock = newAlbumRepositoryMock();
-    assetMock = newAssetRepositoryMock();
-    eventMock = newEventRepositoryMock();
-    userMock = newUserRepositoryMock();
-    albumUserMock = newAlbumUserRepositoryMock();
-
-    sut = new AlbumService(accessMock, albumMock, assetMock, eventMock, userMock, albumUserMock);
+    ({ sut, accessMock, albumMock, albumUserMock, eventMock, userMock } = newTestService(AlbumService));
   });
 
   it('should work', () => {
@@ -67,7 +55,6 @@ describe(AlbumService.name, () => {
         { albumId: albumStub.empty.id, assetCount: 0, startDate: undefined, endDate: undefined },
         { albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: undefined, endDate: undefined },
       ]);
-      albumMock.getInvalidThumbnail.mockResolvedValue([]);
 
       const result = await sut.getAll(authStub.admin, {});
       expect(result).toHaveLength(2);
@@ -85,7 +72,6 @@ describe(AlbumService.name, () => {
           endDate: new Date('1970-01-01'),
         },
       ]);
-      albumMock.getInvalidThumbnail.mockResolvedValue([]);
 
       const result = await sut.getAll(authStub.admin, { assetId: albumStub.oneAsset.id });
       expect(result).toHaveLength(1);
@@ -98,7 +84,6 @@ describe(AlbumService.name, () => {
       albumMock.getMetadataForIds.mockResolvedValue([
         { albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: undefined, endDate: undefined },
       ]);
-      albumMock.getInvalidThumbnail.mockResolvedValue([]);
 
       const result = await sut.getAll(authStub.admin, { shared: true });
       expect(result).toHaveLength(1);
@@ -111,7 +96,6 @@ describe(AlbumService.name, () => {
       albumMock.getMetadataForIds.mockResolvedValue([
         { albumId: albumStub.empty.id, assetCount: 0, startDate: undefined, endDate: undefined },
       ]);
-      albumMock.getInvalidThumbnail.mockResolvedValue([]);
 
       const result = await sut.getAll(authStub.admin, { shared: false });
       expect(result).toHaveLength(1);
@@ -130,7 +114,6 @@ describe(AlbumService.name, () => {
         endDate: new Date('1970-01-01'),
       },
     ]);
-    albumMock.getInvalidThumbnail.mockResolvedValue([]);
 
     const result = await sut.getAll(authStub.admin, {});
 
@@ -139,48 +122,6 @@ describe(AlbumService.name, () => {
     expect(albumMock.getOwned).toHaveBeenCalledTimes(1);
   });
 
-  it('updates the album thumbnail by listing all albums', async () => {
-    albumMock.getOwned.mockResolvedValue([albumStub.oneAssetInvalidThumbnail]);
-    albumMock.getMetadataForIds.mockResolvedValue([
-      {
-        albumId: albumStub.oneAssetInvalidThumbnail.id,
-        assetCount: 1,
-        startDate: new Date('1970-01-01'),
-        endDate: new Date('1970-01-01'),
-      },
-    ]);
-    albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.oneAssetInvalidThumbnail.id]);
-    albumMock.update.mockResolvedValue(albumStub.oneAssetValidThumbnail);
-    assetMock.getFirstAssetForAlbumId.mockResolvedValue(albumStub.oneAssetInvalidThumbnail.assets[0]);
-
-    const result = await sut.getAll(authStub.admin, {});
-
-    expect(result).toHaveLength(1);
-    expect(albumMock.getInvalidThumbnail).toHaveBeenCalledTimes(1);
-    expect(albumMock.update).toHaveBeenCalledTimes(1);
-  });
-
-  it('removes the thumbnail for an empty album', async () => {
-    albumMock.getOwned.mockResolvedValue([albumStub.emptyWithInvalidThumbnail]);
-    albumMock.getMetadataForIds.mockResolvedValue([
-      {
-        albumId: albumStub.emptyWithInvalidThumbnail.id,
-        assetCount: 1,
-        startDate: new Date('1970-01-01'),
-        endDate: new Date('1970-01-01'),
-      },
-    ]);
-    albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.emptyWithInvalidThumbnail.id]);
-    albumMock.update.mockResolvedValue(albumStub.emptyWithValidThumbnail);
-    assetMock.getFirstAssetForAlbumId.mockResolvedValue(null);
-
-    const result = await sut.getAll(authStub.admin, {});
-
-    expect(result).toHaveLength(1);
-    expect(albumMock.getInvalidThumbnail).toHaveBeenCalledTimes(1);
-    expect(albumMock.update).toHaveBeenCalledTimes(1);
-  });
-
   describe('create', () => {
     it('creates album', async () => {
       albumMock.create.mockResolvedValue(albumStub.empty);
@@ -365,6 +306,17 @@ describe(AlbumService.name, () => {
       expect(albumMock.update).not.toHaveBeenCalled();
     });
 
+    it('should throw an error if the userId is the ownerId', async () => {
+      accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
+      albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin);
+      await expect(
+        sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, {
+          albumUsers: [{ userId: userStub.user1.id }],
+        }),
+      ).rejects.toBeInstanceOf(BadRequestException);
+      expect(albumMock.update).not.toHaveBeenCalled();
+    });
+
     it('should add valid shared users', async () => {
       accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
       albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithAdmin));
@@ -474,6 +426,19 @@ describe(AlbumService.name, () => {
     });
   });
 
+  describe('updateUser', () => {
+    it('should update user role', async () => {
+      accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
+      await sut.updateUser(authStub.user1, albumStub.sharedWithAdmin.id, userStub.admin.id, {
+        role: AlbumUserRole.EDITOR,
+      });
+      expect(albumUserMock.update).toHaveBeenCalledWith(
+        { albumId: albumStub.sharedWithAdmin.id, userId: userStub.admin.id },
+        { role: AlbumUserRole.EDITOR },
+      );
+    });
+  });
+
   describe('getAlbumInfo', () => {
     it('should get a shared album', async () => {
       albumMock.getById.mockResolvedValue(albumStub.oneAsset);
@@ -572,10 +537,6 @@ describe(AlbumService.name, () => {
         albumThumbnailAssetId: 'asset-1',
       });
       expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
-      expect(eventMock.emit).toHaveBeenCalledWith('album.update', {
-        id: 'album-123',
-        updatedBy: authStub.admin.user.id,
-      });
     });
 
     it('should not set the thumbnail if the album has one already', async () => {
@@ -618,7 +579,7 @@ describe(AlbumService.name, () => {
       expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
       expect(eventMock.emit).toHaveBeenCalledWith('album.update', {
         id: 'album-123',
-        updatedBy: authStub.user1.user.id,
+        recipientIds: ['admin_id'],
       });
     });
 
diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts
index b59364af9f..e57e6b168c 100644
--- a/server/src/services/album.service.ts
+++ b/server/src/services/album.service.ts
@@ -1,4 +1,4 @@
-import { BadRequestException, Inject, Injectable } from '@nestjs/common';
+import { BadRequestException, Injectable } from '@nestjs/common';
 import {
   AddUsersDto,
   AlbumInfoDto,
@@ -17,26 +17,12 @@ import { AlbumUserEntity } from 'src/entities/album-user.entity';
 import { AlbumEntity } from 'src/entities/album.entity';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { Permission } from 'src/enum';
-import { IAccessRepository } from 'src/interfaces/access.interface';
-import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
-import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface';
-import { IAssetRepository } from 'src/interfaces/asset.interface';
-import { IEventRepository } from 'src/interfaces/event.interface';
-import { IUserRepository } from 'src/interfaces/user.interface';
-import { checkAccess, requireAccess } from 'src/utils/access';
+import { AlbumAssetCount, AlbumInfoOptions } from 'src/interfaces/album.interface';
+import { BaseService } from 'src/services/base.service';
 import { addAssets, removeAssets } from 'src/utils/asset.util';
 
 @Injectable()
-export class AlbumService {
-  constructor(
-    @Inject(IAccessRepository) private access: IAccessRepository,
-    @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
-    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
-    @Inject(IEventRepository) private eventRepository: IEventRepository,
-    @Inject(IUserRepository) private userRepository: IUserRepository,
-    @Inject(IAlbumUserRepository) private albumUserRepository: IAlbumUserRepository,
-  ) {}
-
+export class AlbumService extends BaseService {
   async getStatistics(auth: AuthDto): Promise<AlbumStatisticsResponseDto> {
     const [owned, shared, notShared] = await Promise.all([
       this.albumRepository.getOwned(auth.user.id),
@@ -52,11 +38,7 @@ export class AlbumService {
   }
 
   async getAll({ user: { id: ownerId } }: AuthDto, { assetId, shared }: GetAlbumsDto): Promise<AlbumResponseDto[]> {
-    const invalidAlbumIds = await this.albumRepository.getInvalidThumbnail();
-    for (const albumId of invalidAlbumIds) {
-      const newThumbnail = await this.assetRepository.getFirstAssetForAlbumId(albumId);
-      await this.albumRepository.update({ id: albumId, albumThumbnailAsset: newThumbnail });
-    }
+    await this.albumRepository.updateThumbnails();
 
     let albums: AlbumEntity[];
     if (assetId) {
@@ -92,24 +74,26 @@ export class AlbumService {
           startDate: albumMetadata[album.id].startDate,
           endDate: albumMetadata[album.id].endDate,
           assetCount: albumMetadata[album.id].assetCount,
-          lastModifiedAssetTimestamp: lastModifiedAsset?.fileModifiedAt,
+          lastModifiedAssetTimestamp: lastModifiedAsset?.updatedAt,
         };
       }),
     );
   }
 
   async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> {
-    await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [id] });
     await this.albumRepository.updateThumbnails();
     const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets;
     const album = await this.findOrFail(id, { withAssets });
     const [albumMetadataForIds] = await this.albumRepository.getMetadataForIds([album.id]);
+    const lastModifiedAsset = await this.assetRepository.getLastUpdatedAssetForAlbumId(album.id);
 
     return {
       ...mapAlbum(album, withAssets, auth),
       startDate: albumMetadataForIds.startDate,
       endDate: albumMetadataForIds.endDate,
       assetCount: albumMetadataForIds.assetCount,
+      lastModifiedAssetTimestamp: lastModifiedAsset?.updatedAt,
     };
   }
 
@@ -123,7 +107,7 @@ export class AlbumService {
       }
     }
 
-    const allowedAssetIdsSet = await checkAccess(this.access, {
+    const allowedAssetIdsSet = await this.checkAccess({
       auth,
       permission: Permission.ASSET_SHARE,
       ids: dto.assetIds || [],
@@ -147,7 +131,7 @@ export class AlbumService {
   }
 
   async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> {
-    await requireAccess(this.access, { auth, permission: Permission.ALBUM_UPDATE, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.ALBUM_UPDATE, ids: [id] });
 
     const album = await this.findOrFail(id, { withAssets: true });
 
@@ -170,17 +154,17 @@ export class AlbumService {
   }
 
   async delete(auth: AuthDto, id: string): Promise<void> {
-    await requireAccess(this.access, { auth, permission: Permission.ALBUM_DELETE, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.ALBUM_DELETE, ids: [id] });
     await this.albumRepository.delete(id);
   }
 
   async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
     const album = await this.findOrFail(id, { withAssets: false });
-    await requireAccess(this.access, { auth, permission: Permission.ALBUM_ADD_ASSET, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.ALBUM_ADD_ASSET, ids: [id] });
 
     const results = await addAssets(
       auth,
-      { access: this.access, bulk: this.albumRepository },
+      { access: this.accessRepository, bulk: this.albumRepository },
       { parentId: id, assetIds: dto.ids },
     );
 
@@ -192,19 +176,25 @@ export class AlbumService {
         albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId,
       });
 
-      await this.eventRepository.emit('album.update', { id, updatedBy: auth.user.id });
+      const allUsersExceptUs = [...album.albumUsers.map(({ user }) => user.id), album.owner.id].filter(
+        (userId) => userId !== auth.user.id,
+      );
+
+      if (allUsersExceptUs.length > 0) {
+        await this.eventRepository.emit('album.update', { id, recipientIds: allUsersExceptUs });
+      }
     }
 
     return results;
   }
 
   async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
-    await requireAccess(this.access, { auth, permission: Permission.ALBUM_REMOVE_ASSET, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.ALBUM_REMOVE_ASSET, ids: [id] });
 
     const album = await this.findOrFail(id, { withAssets: false });
     const results = await removeAssets(
       auth,
-      { access: this.access, bulk: this.albumRepository },
+      { access: this.accessRepository, bulk: this.albumRepository },
       { parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.ALBUM_DELETE },
     );
 
@@ -220,7 +210,7 @@ export class AlbumService {
   }
 
   async addUsers(auth: AuthDto, id: string, { albumUsers }: AddUsersDto): Promise<AlbumResponseDto> {
-    await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [id] });
 
     const album = await this.findOrFail(id, { withAssets: false });
 
@@ -264,14 +254,14 @@ export class AlbumService {
 
     // non-admin can remove themselves
     if (auth.user.id !== userId) {
-      await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] });
+      await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [id] });
     }
 
     await this.albumUserRepository.delete({ albumId: id, userId });
   }
 
   async updateUser(auth: AuthDto, id: string, userId: string, dto: Partial<AlbumUserEntity>): Promise<void> {
-    await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [id] });
     await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role });
   }
 
diff --git a/server/src/services/api-key.service.spec.ts b/server/src/services/api-key.service.spec.ts
index 4d13eead57..3841ba1be9 100644
--- a/server/src/services/api-key.service.spec.ts
+++ b/server/src/services/api-key.service.spec.ts
@@ -5,19 +5,17 @@ import { ICryptoRepository } from 'src/interfaces/crypto.interface';
 import { APIKeyService } from 'src/services/api-key.service';
 import { keyStub } from 'test/fixtures/api-key.stub';
 import { authStub } from 'test/fixtures/auth.stub';
-import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock';
-import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(APIKeyService.name, () => {
   let sut: APIKeyService;
-  let keyMock: Mocked<IKeyRepository>;
+
   let cryptoMock: Mocked<ICryptoRepository>;
+  let keyMock: Mocked<IKeyRepository>;
 
   beforeEach(() => {
-    cryptoMock = newCryptoRepositoryMock();
-    keyMock = newKeyRepositoryMock();
-    sut = new APIKeyService(cryptoMock, keyMock);
+    ({ sut, cryptoMock, keyMock } = newTestService(APIKeyService));
   });
 
   describe('create', () => {
@@ -48,6 +46,15 @@ describe(APIKeyService.name, () => {
       expect(cryptoMock.newPassword).toHaveBeenCalled();
       expect(cryptoMock.hashSha256).toHaveBeenCalled();
     });
+
+    it('should throw an error if the api key does not have sufficient permissions', async () => {
+      await expect(
+        sut.create(
+          { ...authStub.admin, apiKey: { ...keyStub.admin, permissions: [] } },
+          { permissions: [Permission.ASSET_READ] },
+        ),
+      ).rejects.toBeInstanceOf(BadRequestException);
+    });
   });
 
   describe('update', () => {
diff --git a/server/src/services/api-key.service.ts b/server/src/services/api-key.service.ts
index 7dd1ed5c26..303ca05537 100644
--- a/server/src/services/api-key.service.ts
+++ b/server/src/services/api-key.service.ts
@@ -1,27 +1,21 @@
-import { BadRequestException, Inject, Injectable } from '@nestjs/common';
+import { BadRequestException, Injectable } from '@nestjs/common';
 import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
 import { APIKeyEntity } from 'src/entities/api-key.entity';
-import { IKeyRepository } from 'src/interfaces/api-key.interface';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { BaseService } from 'src/services/base.service';
 import { isGranted } from 'src/utils/access';
 
 @Injectable()
-export class APIKeyService {
-  constructor(
-    @Inject(ICryptoRepository) private crypto: ICryptoRepository,
-    @Inject(IKeyRepository) private repository: IKeyRepository,
-  ) {}
-
+export class APIKeyService extends BaseService {
   async create(auth: AuthDto, dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
-    const secret = this.crypto.newPassword(32);
+    const secret = this.cryptoRepository.newPassword(32);
 
     if (auth.apiKey && !isGranted({ requested: dto.permissions, current: auth.apiKey.permissions })) {
       throw new BadRequestException('Cannot grant permissions you do not have');
     }
 
-    const entity = await this.repository.create({
-      key: this.crypto.hashSha256(secret),
+    const entity = await this.keyRepository.create({
+      key: this.cryptoRepository.hashSha256(secret),
       name: dto.name || 'API Key',
       userId: auth.user.id,
       permissions: dto.permissions,
@@ -31,27 +25,27 @@ export class APIKeyService {
   }
 
   async update(auth: AuthDto, id: string, dto: APIKeyUpdateDto): Promise<APIKeyResponseDto> {
-    const exists = await this.repository.getById(auth.user.id, id);
+    const exists = await this.keyRepository.getById(auth.user.id, id);
     if (!exists) {
       throw new BadRequestException('API Key not found');
     }
 
-    const key = await this.repository.update(auth.user.id, id, { name: dto.name });
+    const key = await this.keyRepository.update(auth.user.id, id, { name: dto.name });
 
     return this.map(key);
   }
 
   async delete(auth: AuthDto, id: string): Promise<void> {
-    const exists = await this.repository.getById(auth.user.id, id);
+    const exists = await this.keyRepository.getById(auth.user.id, id);
     if (!exists) {
       throw new BadRequestException('API Key not found');
     }
 
-    await this.repository.delete(auth.user.id, id);
+    await this.keyRepository.delete(auth.user.id, id);
   }
 
   async getById(auth: AuthDto, id: string): Promise<APIKeyResponseDto> {
-    const key = await this.repository.getById(auth.user.id, id);
+    const key = await this.keyRepository.getById(auth.user.id, id);
     if (!key) {
       throw new BadRequestException('API Key not found');
     }
@@ -59,7 +53,7 @@ export class APIKeyService {
   }
 
   async getAll(auth: AuthDto): Promise<APIKeyResponseDto[]> {
-    const keys = await this.repository.getByUserId(auth.user.id);
+    const keys = await this.keyRepository.getByUserId(auth.user.id);
     return keys.map((key) => this.map(key));
   }
 
diff --git a/server/src/services/api.service.ts b/server/src/services/api.service.ts
index 039dcb9aae..66f8061d3c 100644
--- a/server/src/services/api.service.ts
+++ b/server/src/services/api.service.ts
@@ -2,7 +2,8 @@ import { Inject, Injectable } from '@nestjs/common';
 import { Cron, CronExpression, Interval } from '@nestjs/schedule';
 import { NextFunction, Request, Response } from 'express';
 import { readFileSync } from 'node:fs';
-import { ONE_HOUR, resourcePaths } from 'src/constants';
+import { ONE_HOUR } from 'src/constants';
+import { IConfigRepository } from 'src/interfaces/config.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { AuthService } from 'src/services/auth.service';
 import { JobService } from 'src/services/job.service';
@@ -37,6 +38,7 @@ export class ApiService {
     private jobService: JobService,
     private sharedLinkService: SharedLinkService,
     private versionService: VersionService,
+    @Inject(IConfigRepository) private configRepository: IConfigRepository,
     @Inject(ILoggerRepository) private logger: ILoggerRepository,
   ) {
     this.logger.setContext(ApiService.name);
@@ -53,6 +55,8 @@ export class ApiService {
   }
 
   ssr(excludePaths: string[]) {
+    const { resourcePaths } = this.configRepository.getEnv();
+
     let index = '';
     try {
       index = readFileSync(resourcePaths.web.indexHtml).toString();
diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts
index 9d6f0ff9cf..1daeb99d0b 100644
--- a/server/src/services/asset-media.service.spec.ts
+++ b/server/src/services/asset-media.service.spec.ts
@@ -1,28 +1,28 @@
-import { BadRequestException, NotFoundException, UnauthorizedException } from '@nestjs/common';
+import {
+  BadRequestException,
+  InternalServerErrorException,
+  NotFoundException,
+  UnauthorizedException,
+} from '@nestjs/common';
 import { Stats } from 'node:fs';
 import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto';
-import { AssetMediaCreateDto, AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto';
+import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto';
 import { AssetFileEntity } from 'src/entities/asset-files.entity';
 import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
-import { AssetType } from 'src/enum';
+import { AssetFileType, AssetStatus, AssetType, CacheControl } from 'src/enum';
 import { IAssetRepository } from 'src/interfaces/asset.interface';
-import { IEventRepository } from 'src/interfaces/event.interface';
 import { IJobRepository, JobName } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { IStorageRepository } from 'src/interfaces/storage.interface';
 import { IUserRepository } from 'src/interfaces/user.interface';
+import { AuthRequest } from 'src/middleware/auth.guard';
 import { AssetMediaService } from 'src/services/asset-media.service';
-import { CacheControl, ImmichFileResponse } from 'src/utils/file';
+import { ImmichFileResponse } from 'src/utils/file';
 import { assetStub } from 'test/fixtures/asset.stub';
 import { authStub } from 'test/fixtures/auth.stub';
 import { fileStub } from 'test/fixtures/file.stub';
-import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
-import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
-import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
-import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
-import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
+import { userStub } from 'test/fixtures/user.stub';
+import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
+import { newTestService } from 'test/utils';
 import { QueryFailedError } from 'typeorm';
 import { Mocked } from 'vitest';
 
@@ -98,7 +98,7 @@ const validImages = [
   '.x3f',
 ];
 
-const validVideos = ['.3gp', '.avi', '.flv', '.m2ts', '.mkv', '.mov', '.mp4', '.mpg', '.mts', '.webm', '.wmv'];
+const validVideos = ['.3gp', '.avi', '.flv', '.m2ts', '.mkv', '.mov', '.mp4', '.mpg', '.mts', '.vob', '.webm', '.wmv'];
 
 const uploadTests = [
   {
@@ -189,27 +189,22 @@ const copiedAsset = Object.freeze({
 
 describe(AssetMediaService.name, () => {
   let sut: AssetMediaService;
+
   let accessMock: IAccessRepositoryMock;
   let assetMock: Mocked<IAssetRepository>;
   let jobMock: Mocked<IJobRepository>;
-  let loggerMock: Mocked<ILoggerRepository>;
   let storageMock: Mocked<IStorageRepository>;
   let userMock: Mocked<IUserRepository>;
-  let eventMock: Mocked<IEventRepository>;
 
   beforeEach(() => {
-    accessMock = newAccessRepositoryMock();
-    assetMock = newAssetRepositoryMock();
-    jobMock = newJobRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-    storageMock = newStorageRepositoryMock();
-    userMock = newUserRepositoryMock();
-    eventMock = newEventRepositoryMock();
-
-    sut = new AssetMediaService(accessMock, assetMock, jobMock, storageMock, userMock, eventMock, loggerMock);
+    ({ sut, accessMock, assetMock, jobMock, storageMock, userMock } = newTestService(AssetMediaService));
   });
 
   describe('getUploadAssetIdByChecksum', () => {
+    it('should return if checksum is undefined', async () => {
+      await expect(sut.getUploadAssetIdByChecksum(authStub.admin)).resolves.toBe(undefined);
+    });
+
     it('should handle a non-existent asset', async () => {
       await expect(sut.getUploadAssetIdByChecksum(authStub.admin, file1.toString('hex'))).resolves.toBeUndefined();
       expect(assetMock.getUploadAssetIdByChecksum).toHaveBeenCalledWith(authStub.admin.user.id, file1);
@@ -311,6 +306,35 @@ describe(AssetMediaService.name, () => {
   });
 
   describe('uploadAsset', () => {
+    it('should throw an error if the quota is exceeded', async () => {
+      const file = {
+        uuid: 'random-uuid',
+        originalPath: 'fake_path/asset_1.jpeg',
+        mimeType: 'image/jpeg',
+        checksum: Buffer.from('file hash', 'utf8'),
+        originalName: 'asset_1.jpeg',
+        size: 42,
+      };
+
+      assetMock.create.mockResolvedValue(assetEntity);
+
+      await expect(
+        sut.uploadAsset(
+          { ...authStub.admin, user: { ...authStub.admin.user, quotaSizeInBytes: 42, quotaUsageInBytes: 1 } },
+          createDto,
+          file,
+        ),
+      ).rejects.toBeInstanceOf(BadRequestException);
+
+      expect(assetMock.create).not.toHaveBeenCalled();
+      expect(userMock.updateUsage).not.toHaveBeenCalledWith(authStub.user1.user.id, file.size);
+      expect(storageMock.utimes).not.toHaveBeenCalledWith(
+        file.originalPath,
+        expect.any(Date),
+        new Date(createDto.fileModifiedAt),
+      );
+    });
+
     it('should handle a file upload', async () => {
       const file = {
         uuid: 'random-uuid',
@@ -364,6 +388,31 @@ describe(AssetMediaService.name, () => {
       expect(userMock.updateUsage).not.toHaveBeenCalled();
     });
 
+    it('should throw an error if the duplicate could not be found by checksum', async () => {
+      const file = {
+        uuid: 'random-uuid',
+        originalPath: 'fake_path/asset_1.jpeg',
+        mimeType: 'image/jpeg',
+        checksum: Buffer.from('file hash', 'utf8'),
+        originalName: 'asset_1.jpeg',
+        size: 0,
+      };
+      const error = new QueryFailedError('', [], new Error('unique key violation'));
+      (error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
+
+      assetMock.create.mockRejectedValue(error);
+
+      await expect(sut.uploadAsset(authStub.user1, createDto, file)).rejects.toBeInstanceOf(
+        InternalServerErrorException,
+      );
+
+      expect(jobMock.queue).toHaveBeenCalledWith({
+        name: JobName.DELETE_FILES,
+        data: { files: ['fake_path/asset_1.jpeg', undefined] },
+      });
+      expect(userMock.updateUsage).not.toHaveBeenCalled();
+    });
+
     it('should handle a live photo', async () => {
       assetMock.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
       assetMock.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
@@ -401,6 +450,23 @@ describe(AssetMediaService.name, () => {
       expect(assetMock.getById).toHaveBeenCalledWith('live-photo-motion-asset');
       expect(assetMock.update).toHaveBeenCalledWith({ id: 'live-photo-motion-asset', isVisible: false });
     });
+
+    it('should handle a sidecar file', async () => {
+      assetMock.getById.mockResolvedValueOnce(assetStub.image);
+      assetMock.create.mockResolvedValueOnce(assetStub.image);
+
+      await expect(sut.uploadAsset(authStub.user1, createDto, fileStub.photo, fileStub.photoSidecar)).resolves.toEqual({
+        status: AssetMediaStatus.CREATED,
+        id: assetStub.image.id,
+      });
+
+      expect(storageMock.utimes).toHaveBeenCalledWith(
+        fileStub.photoSidecar.originalPath,
+        expect.any(Date),
+        new Date(createDto.fileModifiedAt),
+      );
+      expect(assetMock.update).not.toHaveBeenCalled();
+    });
   });
 
   describe('downloadOriginal', () => {
@@ -435,6 +501,173 @@ describe(AssetMediaService.name, () => {
     });
   });
 
+  describe('viewThumbnail', () => {
+    it('should require asset.view permissions', async () => {
+      await expect(sut.viewThumbnail(authStub.admin, 'id', {})).rejects.toBeInstanceOf(BadRequestException);
+
+      expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
+      expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
+      expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
+    });
+
+    it('should throw an error if the asset does not exist', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
+      assetMock.getById.mockResolvedValue(null);
+
+      await expect(
+        sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }),
+      ).rejects.toBeInstanceOf(NotFoundException);
+    });
+
+    it('should throw an error if the requested thumbnail file does not exist', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
+      assetMock.getById.mockResolvedValue({ ...assetStub.image, files: [] });
+
+      await expect(
+        sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
+      ).rejects.toBeInstanceOf(NotFoundException);
+    });
+
+    it('should throw an error if the requested preview file does not exist', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
+      assetMock.getById.mockResolvedValue({
+        ...assetStub.image,
+        files: [
+          {
+            assetId: assetStub.image.id,
+            createdAt: assetStub.image.fileCreatedAt,
+            id: '42',
+            path: '/path/to/preview',
+            type: AssetFileType.THUMBNAIL,
+            updatedAt: new Date(),
+          },
+        ],
+      });
+      await expect(
+        sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }),
+      ).rejects.toBeInstanceOf(NotFoundException);
+    });
+
+    it('should fall back to preview if the requested thumbnail file does not exist', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
+      assetMock.getById.mockResolvedValue({
+        ...assetStub.image,
+        files: [
+          {
+            assetId: assetStub.image.id,
+            createdAt: assetStub.image.fileCreatedAt,
+            id: '42',
+            path: '/path/to/preview.jpg',
+            type: AssetFileType.PREVIEW,
+            updatedAt: new Date(),
+          },
+        ],
+      });
+
+      await expect(
+        sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
+      ).resolves.toEqual(
+        new ImmichFileResponse({
+          path: '/path/to/preview.jpg',
+          cacheControl: CacheControl.PRIVATE_WITH_CACHE,
+          contentType: 'image/jpeg',
+          fileName: 'asset-id_thumbnail.jpg',
+        }),
+      );
+    });
+
+    it('should get preview file', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
+      assetMock.getById.mockResolvedValue({ ...assetStub.image });
+      await expect(
+        sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }),
+      ).resolves.toEqual(
+        new ImmichFileResponse({
+          path: assetStub.image.files[0].path,
+          cacheControl: CacheControl.PRIVATE_WITH_CACHE,
+          contentType: 'image/jpeg',
+          fileName: 'asset-id_preview.jpg',
+        }),
+      );
+    });
+
+    it('should get thumbnail file', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
+      assetMock.getById.mockResolvedValue({ ...assetStub.image });
+      await expect(
+        sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
+      ).resolves.toEqual(
+        new ImmichFileResponse({
+          path: assetStub.image.files[1].path,
+          cacheControl: CacheControl.PRIVATE_WITH_CACHE,
+          contentType: 'application/octet-stream',
+          fileName: 'asset-id_thumbnail.ext',
+        }),
+      );
+    });
+  });
+
+  describe('playbackVideo', () => {
+    it('should require asset.view permissions', async () => {
+      await expect(sut.playbackVideo(authStub.admin, 'id')).rejects.toBeInstanceOf(BadRequestException);
+
+      expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
+      expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
+      expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
+    });
+
+    it('should throw an error if the asset does not exist', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
+      assetMock.getById.mockResolvedValue(null);
+
+      await expect(sut.playbackVideo(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(NotFoundException);
+    });
+
+    it('should throw an error if the asset is not a video', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
+      assetMock.getById.mockResolvedValue(assetStub.image);
+
+      await expect(sut.playbackVideo(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException);
+    });
+
+    it('should return the encoded video path if available', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.hasEncodedVideo.id]));
+      assetMock.getById.mockResolvedValue(assetStub.hasEncodedVideo);
+
+      await expect(sut.playbackVideo(authStub.admin, assetStub.hasEncodedVideo.id)).resolves.toEqual(
+        new ImmichFileResponse({
+          path: assetStub.hasEncodedVideo.encodedVideoPath!,
+          cacheControl: CacheControl.PRIVATE_WITH_CACHE,
+          contentType: 'video/mp4',
+        }),
+      );
+    });
+
+    it('should fall back to the original path', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.video.id]));
+      assetMock.getById.mockResolvedValue(assetStub.video);
+
+      await expect(sut.playbackVideo(authStub.admin, assetStub.video.id)).resolves.toEqual(
+        new ImmichFileResponse({
+          path: assetStub.video.originalPath,
+          cacheControl: CacheControl.PRIVATE_WITH_CACHE,
+          contentType: 'application/octet-stream',
+        }),
+      );
+    });
+  });
+
+  describe('checkExistingAssets', () => {
+    it('should get existing asset ids', async () => {
+      assetMock.getByDeviceIds.mockResolvedValue(['42']);
+      await expect(
+        sut.checkExistingAssets(authStub.admin, { deviceId: '420', deviceAssetIds: ['69'] }),
+      ).resolves.toEqual({ existingIds: ['42'] });
+
+      expect(assetMock.getByDeviceIds).toHaveBeenCalledWith(userStub.admin.id, '420', ['69']);
+    });
+  });
+
   describe('replaceAsset', () => {
     it('should error when update photo does not exist', async () => {
       assetMock.getById.mockResolvedValueOnce(null);
@@ -478,7 +711,10 @@ describe(AssetMediaService.name, () => {
         }),
       );
 
-      expect(assetMock.softDeleteAll).toHaveBeenCalledWith([copiedAsset.id]);
+      expect(assetMock.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
+        deletedAt: expect.any(Date),
+        status: AssetStatus.TRASHED,
+      });
       expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
       expect(storageMock.utimes).toHaveBeenCalledWith(
         updatedFile.originalPath,
@@ -506,7 +742,10 @@ describe(AssetMediaService.name, () => {
         id: 'copied-asset',
       });
 
-      expect(assetMock.softDeleteAll).toHaveBeenCalledWith(['copied-asset']);
+      expect(assetMock.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
+        deletedAt: expect.any(Date),
+        status: AssetStatus.TRASHED,
+      });
       expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
       expect(storageMock.utimes).toHaveBeenCalledWith(
         updatedFile.originalPath,
@@ -532,7 +771,10 @@ describe(AssetMediaService.name, () => {
         id: 'copied-asset',
       });
 
-      expect(assetMock.softDeleteAll).toHaveBeenCalledWith(['copied-asset']);
+      expect(assetMock.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
+        deletedAt: expect.any(Date),
+        status: AssetStatus.TRASHED,
+      });
       expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
       expect(storageMock.utimes).toHaveBeenCalledWith(
         updatedFile.originalPath,
@@ -561,7 +803,7 @@ describe(AssetMediaService.name, () => {
       });
 
       expect(assetMock.create).not.toHaveBeenCalled();
-      expect(assetMock.softDeleteAll).not.toHaveBeenCalled();
+      expect(assetMock.updateAll).not.toHaveBeenCalled();
       expect(jobMock.queue).toHaveBeenCalledWith({
         name: JobName.DELETE_FILES,
         data: { files: [updatedFile.originalPath, undefined] },
@@ -608,5 +850,61 @@ describe(AssetMediaService.name, () => {
 
       expect(assetMock.getByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]);
     });
+
+    it('should return non-duplicates as well', async () => {
+      const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
+      const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex');
+
+      assetMock.getByChecksums.mockResolvedValue([{ id: 'asset-1', checksum: file1 } as AssetEntity]);
+
+      await expect(
+        sut.bulkUploadCheck(authStub.admin, {
+          assets: [
+            { id: '1', checksum: file1.toString('hex') },
+            { id: '2', checksum: file2.toString('base64') },
+          ],
+        }),
+      ).resolves.toEqual({
+        results: [
+          {
+            id: '1',
+            assetId: 'asset-1',
+            action: AssetUploadAction.REJECT,
+            reason: AssetRejectReason.DUPLICATE,
+            isTrashed: false,
+          },
+          {
+            id: '2',
+            action: AssetUploadAction.ACCEPT,
+          },
+        ],
+      });
+
+      expect(assetMock.getByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]);
+    });
+  });
+
+  describe('onUploadError', () => {
+    it('should queue a job to delete the uploaded file', async () => {
+      const request = { user: authStub.user1 } as AuthRequest;
+
+      const file = {
+        fieldname: UploadFieldName.ASSET_DATA,
+        originalname: 'image.jpg',
+        mimetype: 'image/jpeg',
+        buffer: Buffer.from(''),
+        size: 1000,
+        uuid: 'random-uuid',
+        checksum: Buffer.from('checksum', 'utf8'),
+        originalPath: 'upload/upload/user-id/ra/nd/random-uuid.jpg',
+      } as unknown as Express.Multer.File;
+
+      await sut.onUploadError(request, file);
+
+      expect(jobMock.queue).toHaveBeenCalledWith({
+        name: JobName.DELETE_FILES,
+        data: { files: ['upload/upload/user-id/ra/nd/random-uuid.jpg'] },
+      });
+    });
   });
 });
diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts
index df3b183442..e96d1fd0a6 100644
--- a/server/src/services/asset-media.service.ts
+++ b/server/src/services/asset-media.service.ts
@@ -1,13 +1,7 @@
-import {
-  BadRequestException,
-  Inject,
-  Injectable,
-  InternalServerErrorException,
-  NotFoundException,
-} from '@nestjs/common';
+import { BadRequestException, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
 import { extname } from 'node:path';
 import sanitize from 'sanitize-filename';
-import { StorageCore, StorageFolder } from 'src/cores/storage.core';
+import { StorageCore } from 'src/cores/storage.core';
 import {
   AssetBulkUploadCheckResponseDto,
   AssetMediaResponseDto,
@@ -27,17 +21,13 @@ import {
 } from 'src/dtos/asset-media.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
 import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
-import { AssetType, Permission } from 'src/enum';
-import { IAccessRepository } from 'src/interfaces/access.interface';
-import { IAssetRepository } from 'src/interfaces/asset.interface';
-import { IEventRepository } from 'src/interfaces/event.interface';
-import { IJobRepository, JobName } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { IStorageRepository } from 'src/interfaces/storage.interface';
-import { IUserRepository } from 'src/interfaces/user.interface';
-import { requireAccess, requireUploadAccess } from 'src/utils/access';
-import { getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
-import { CacheControl, ImmichFileResponse } from 'src/utils/file';
+import { AssetStatus, AssetType, CacheControl, Permission, StorageFolder } from 'src/enum';
+import { JobName } from 'src/interfaces/job.interface';
+import { AuthRequest } from 'src/middleware/auth.guard';
+import { BaseService } from 'src/services/base.service';
+import { requireUploadAccess } from 'src/utils/access';
+import { asRequest, getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
+import { getFilenameExtension, getFileNameWithoutExtension, ImmichFileResponse } from 'src/utils/file';
 import { mimeTypes } from 'src/utils/mime-types';
 import { fromChecksum } from 'src/utils/request';
 import { QueryFailedError } from 'typeorm';
@@ -56,19 +46,7 @@ export interface UploadFile {
 }
 
 @Injectable()
-export class AssetMediaService {
-  constructor(
-    @Inject(IAccessRepository) private access: IAccessRepository,
-    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
-    @Inject(IJobRepository) private jobRepository: IJobRepository,
-    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
-    @Inject(IUserRepository) private userRepository: IUserRepository,
-    @Inject(IEventRepository) private eventRepository: IEventRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-  ) {
-    this.logger.setContext(AssetMediaService.name);
-  }
-
+export class AssetMediaService extends BaseService {
   async getUploadAssetIdByChecksum(auth: AuthDto, checksum?: string): Promise<AssetMediaResponseDto | undefined> {
     if (!checksum) {
       return;
@@ -141,6 +119,14 @@ export class AssetMediaService {
     return folder;
   }
 
+  async onUploadError(request: AuthRequest, file: Express.Multer.File) {
+    const uploadFilename = this.getUploadFilename(asRequest(request, file));
+    const uploadFolder = this.getUploadFolder(asRequest(request, file));
+    const uploadPath = `${uploadFolder}/${uploadFilename}`;
+
+    await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [uploadPath] } });
+  }
+
   async uploadAsset(
     auth: AuthDto,
     dto: AssetMediaCreateDto,
@@ -148,7 +134,7 @@ export class AssetMediaService {
     sidecarFile?: UploadFile,
   ): Promise<AssetMediaResponseDto> {
     try {
-      await requireAccess(this.access, {
+      await this.requireAccess({
         auth,
         permission: Permission.ASSET_UPLOAD,
         // do not need an id here, but the interface requires it
@@ -182,7 +168,7 @@ export class AssetMediaService {
     sidecarFile?: UploadFile,
   ): Promise<AssetMediaResponseDto> {
     try {
-      await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: [id] });
+      await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: [id] });
       const asset = (await this.assetRepository.getById(id)) as AssetEntity;
 
       this.requireQuota(auth, file.size);
@@ -193,7 +179,7 @@ export class AssetMediaService {
       // but the local variable holds the original file data paths.
       const copiedPhoto = await this.createCopy(asset);
       // and immediate trash it
-      await this.assetRepository.softDeleteAll([copiedPhoto.id]);
+      await this.assetRepository.updateAll([copiedPhoto.id], { deletedAt: new Date(), status: AssetStatus.TRASHED });
       await this.eventRepository.emit('asset.trash', { assetId: copiedPhoto.id, userId: auth.user.id });
 
       await this.userRepository.updateUsage(auth.user.id, file.size);
@@ -205,12 +191,9 @@ export class AssetMediaService {
   }
 
   async downloadOriginal(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
-    await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.ASSET_DOWNLOAD, ids: [id] });
 
     const asset = await this.findOrFail(id);
-    if (!asset) {
-      throw new NotFoundException('Asset does not exist');
-    }
 
     return new ImmichFileResponse({
       path: asset.originalPath,
@@ -220,7 +203,7 @@ export class AssetMediaService {
   }
 
   async viewThumbnail(auth: AuthDto, id: string, dto: AssetMediaOptionsDto): Promise<ImmichFileResponse> {
-    await requireAccess(this.access, { auth, permission: Permission.ASSET_VIEW, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.ASSET_VIEW, ids: [id] });
 
     const asset = await this.findOrFail(id);
     const size = dto.size ?? AssetMediaSize.THUMBNAIL;
@@ -234,8 +217,12 @@ export class AssetMediaService {
     if (!filepath) {
       throw new NotFoundException('Asset media not found');
     }
+    let fileName = getFileNameWithoutExtension(asset.originalFileName);
+    fileName += `_${size}`;
+    fileName += getFilenameExtension(filepath);
 
     return new ImmichFileResponse({
+      fileName,
       path: filepath,
       contentType: mimeTypes.lookup(filepath),
       cacheControl: CacheControl.PRIVATE_WITH_CACHE,
@@ -243,12 +230,9 @@ export class AssetMediaService {
   }
 
   async playbackVideo(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
-    await requireAccess(this.access, { auth, permission: Permission.ASSET_VIEW, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.ASSET_VIEW, ids: [id] });
 
     const asset = await this.findOrFail(id);
-    if (!asset) {
-      throw new NotFoundException('Asset does not exist');
-    }
 
     if (asset.type !== AssetType.VIDEO) {
       throw new BadRequestException('Asset is not a video');
@@ -427,7 +411,6 @@ export class AssetMediaService {
       livePhotoVideoId: dto.livePhotoVideoId,
       originalFileName: file.originalName,
       sidecarPath: sidecarFile?.originalPath,
-      isOffline: dto.isOffline ?? false,
     });
 
     if (sidecarFile) {
diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts
index 3ac7aa1c71..5aab5032af 100755
--- a/server/src/services/asset.service.spec.ts
+++ b/server/src/services/asset.service.spec.ts
@@ -1,12 +1,12 @@
 import { BadRequestException } from '@nestjs/common';
+import { DateTime } from 'luxon';
 import { mapAsset } from 'src/dtos/asset-response.dto';
 import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto';
 import { AssetEntity } from 'src/entities/asset.entity';
-import { AssetType } from 'src/enum';
+import { AssetStatus, AssetType } from 'src/enum';
 import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface';
 import { IEventRepository } from 'src/interfaces/event.interface';
-import { IJobRepository, JobName } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
+import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
 import { IPartnerRepository } from 'src/interfaces/partner.interface';
 import { IStackRepository } from 'src/interfaces/stack.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
@@ -17,15 +17,8 @@ import { authStub } from 'test/fixtures/auth.stub';
 import { faceStub } from 'test/fixtures/face.stub';
 import { partnerStub } from 'test/fixtures/partner.stub';
 import { userStub } from 'test/fixtures/user.stub';
-import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
-import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
-import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
-import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
-import { newStackRepositoryMock } from 'test/repositories/stack.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
-import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
+import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked, vitest } from 'vitest';
 
 const stats: AssetStats = {
@@ -43,15 +36,15 @@ const statResponse: AssetStatsResponseDto = {
 
 describe(AssetService.name, () => {
   let sut: AssetService;
+
   let accessMock: IAccessRepositoryMock;
   let assetMock: Mocked<IAssetRepository>;
-  let jobMock: Mocked<IJobRepository>;
-  let userMock: Mocked<IUserRepository>;
   let eventMock: Mocked<IEventRepository>;
+  let jobMock: Mocked<IJobRepository>;
+  let partnerMock: Mocked<IPartnerRepository>;
   let stackMock: Mocked<IStackRepository>;
   let systemMock: Mocked<ISystemMetadataRepository>;
-  let partnerMock: Mocked<IPartnerRepository>;
-  let loggerMock: Mocked<ILoggerRepository>;
+  let userMock: Mocked<IUserRepository>;
 
   it('should work', () => {
     expect(sut).toBeDefined();
@@ -64,27 +57,8 @@ describe(AssetService.name, () => {
   };
 
   beforeEach(() => {
-    accessMock = newAccessRepositoryMock();
-    assetMock = newAssetRepositoryMock();
-    eventMock = newEventRepositoryMock();
-    jobMock = newJobRepositoryMock();
-    userMock = newUserRepositoryMock();
-    systemMock = newSystemMetadataRepositoryMock();
-    partnerMock = newPartnerRepositoryMock();
-    stackMock = newStackRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-
-    sut = new AssetService(
-      accessMock,
-      assetMock,
-      jobMock,
-      systemMock,
-      userMock,
-      eventMock,
-      partnerMock,
-      stackMock,
-      loggerMock,
-    );
+    ({ sut, accessMock, assetMock, eventMock, jobMock, partnerMock, stackMock, systemMock, userMock } =
+      newTestService(AssetService));
 
     mockGetById([assetStub.livePhotoStillAsset, assetStub.livePhotoMotionAsset]);
   });
@@ -106,7 +80,20 @@ describe(AssetService.name, () => {
       const image4 = { ...assetStub.image, localDateTime: new Date(2009, 1, 15) };
 
       partnerMock.getAll.mockResolvedValue([]);
-      assetMock.getByDayOfYear.mockResolvedValue([image1, image2, image3, image4]);
+      assetMock.getByDayOfYear.mockResolvedValue([
+        {
+          yearsAgo: 1,
+          assets: [image1, image2],
+        },
+        {
+          yearsAgo: 9,
+          assets: [image3],
+        },
+        {
+          yearsAgo: 15,
+          assets: [image4],
+        },
+      ]);
 
       await expect(sut.getMemoryLane(authStub.admin, { day: 15, month: 1 })).resolves.toEqual([
         { yearsAgo: 1, title: '1 year ago', assets: [mapAsset(image1), mapAsset(image2)] },
@@ -155,6 +142,28 @@ describe(AssetService.name, () => {
     });
   });
 
+  describe('getRandom', () => {
+    it('should get own random assets', async () => {
+      assetMock.getRandom.mockResolvedValue([assetStub.image]);
+      await sut.getRandom(authStub.admin, 1);
+      expect(assetMock.getRandom).toHaveBeenCalledWith([authStub.admin.user.id], 1);
+    });
+
+    it('should not include partner assets if not in timeline', async () => {
+      assetMock.getRandom.mockResolvedValue([assetStub.image]);
+      partnerMock.getAll.mockResolvedValue([{ ...partnerStub.user1ToAdmin1, inTimeline: false }]);
+      await sut.getRandom(authStub.admin, 1);
+      expect(assetMock.getRandom).toHaveBeenCalledWith([authStub.admin.user.id], 1);
+    });
+
+    it('should include partner assets if in timeline', async () => {
+      assetMock.getRandom.mockResolvedValue([assetStub.image]);
+      partnerMock.getAll.mockResolvedValue([partnerStub.user1ToAdmin1]);
+      await sut.getRandom(authStub.admin, 1);
+      expect(assetMock.getRandom).toHaveBeenCalledWith([userStub.admin.id, userStub.user1.id], 1);
+    });
+  });
+
   describe('get', () => {
     it('should allow owner access', async () => {
       accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
@@ -176,6 +185,23 @@ describe(AssetService.name, () => {
       );
     });
 
+    it('should strip metadata for shared link if exif is disabled', async () => {
+      accessMock.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id]));
+      assetMock.getById.mockResolvedValue(assetStub.image);
+
+      const result = await sut.get(
+        { ...authStub.adminSharedLink, sharedLink: { ...authStub.adminSharedLink.sharedLink!, showExif: false } },
+        assetStub.image.id,
+      );
+
+      expect(result).toEqual(expect.objectContaining({ hasMetadata: false }));
+      expect(result).not.toHaveProperty('exifInfo');
+      expect(accessMock.asset.checkSharedLinkAccess).toHaveBeenCalledWith(
+        authStub.adminSharedLink.sharedLink?.id,
+        new Set([assetStub.image.id]),
+      );
+    });
+
     it('should allow partner sharing access', async () => {
       accessMock.asset.checkPartnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
       assetMock.getById.mockResolvedValue(assetStub.image);
@@ -206,6 +232,11 @@ describe(AssetService.name, () => {
       expect(accessMock.asset.checkOwnerAccess).not.toHaveBeenCalled();
       expect(assetMock.getById).not.toHaveBeenCalled();
     });
+
+    it('should throw an error if the asset could not be found', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
+      await expect(sut.get(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException);
+    });
   });
 
   describe('update', () => {
@@ -236,6 +267,132 @@ describe(AssetService.name, () => {
       await sut.update(authStub.admin, 'asset-1', { rating: 3 });
       expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', rating: 3 });
     });
+
+    it('should fail linking a live video if the motion part could not be found', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
+      assetMock.getById.mockResolvedValue(null);
+
+      await expect(
+        sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
+          livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
+        }),
+      ).rejects.toBeInstanceOf(BadRequestException);
+
+      expect(assetMock.update).not.toHaveBeenCalledWith({
+        id: assetStub.livePhotoStillAsset.id,
+        livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
+      });
+      expect(assetMock.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
+      expect(eventMock.emit).not.toHaveBeenCalledWith('asset.show', {
+        assetId: assetStub.livePhotoMotionAsset.id,
+        userId: userStub.admin.id,
+      });
+    });
+
+    it('should fail linking a live video if the motion part is not a video', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
+      assetMock.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
+
+      await expect(
+        sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
+          livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
+        }),
+      ).rejects.toBeInstanceOf(BadRequestException);
+
+      expect(assetMock.update).not.toHaveBeenCalledWith({
+        id: assetStub.livePhotoStillAsset.id,
+        livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
+      });
+      expect(assetMock.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
+      expect(eventMock.emit).not.toHaveBeenCalledWith('asset.show', {
+        assetId: assetStub.livePhotoMotionAsset.id,
+        userId: userStub.admin.id,
+      });
+    });
+
+    it('should fail linking a live video if the motion part has a different owner', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
+      assetMock.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
+
+      await expect(
+        sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
+          livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
+        }),
+      ).rejects.toBeInstanceOf(BadRequestException);
+
+      expect(assetMock.update).not.toHaveBeenCalledWith({
+        id: assetStub.livePhotoStillAsset.id,
+        livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
+      });
+      expect(assetMock.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
+      expect(eventMock.emit).not.toHaveBeenCalledWith('asset.show', {
+        assetId: assetStub.livePhotoMotionAsset.id,
+        userId: userStub.admin.id,
+      });
+    });
+
+    it('should link a live video', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
+      assetMock.getById.mockResolvedValueOnce({
+        ...assetStub.livePhotoMotionAsset,
+        ownerId: authStub.admin.user.id,
+        isVisible: true,
+      });
+      assetMock.getById.mockResolvedValueOnce(assetStub.image);
+
+      await sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
+        livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
+      });
+
+      expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false });
+      expect(eventMock.emit).toHaveBeenCalledWith('asset.hide', {
+        assetId: assetStub.livePhotoMotionAsset.id,
+        userId: userStub.admin.id,
+      });
+      expect(assetMock.update).toHaveBeenCalledWith({
+        id: assetStub.livePhotoStillAsset.id,
+        livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
+      });
+    });
+
+    it('should throw an error if asset could not be found after update', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
+      await expect(sut.update(authStub.admin, 'asset-1', { isFavorite: true })).rejects.toBeInstanceOf(
+        BadRequestException,
+      );
+    });
+
+    it('should unlink a live video', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
+      assetMock.getById.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
+      assetMock.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
+      assetMock.getById.mockResolvedValueOnce(assetStub.image);
+
+      await sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null });
+
+      expect(assetMock.update).toHaveBeenCalledWith({
+        id: assetStub.livePhotoStillAsset.id,
+        livePhotoVideoId: null,
+      });
+      expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
+      expect(eventMock.emit).toHaveBeenCalledWith('asset.show', {
+        assetId: assetStub.livePhotoMotionAsset.id,
+        userId: userStub.admin.id,
+      });
+    });
+
+    it('should fail unlinking a live video if the asset could not be found', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
+      assetMock.getById.mockResolvedValue(null);
+
+      await expect(
+        sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null }),
+      ).rejects.toBeInstanceOf(BadRequestException);
+
+      expect(assetMock.update).not.toHaveBeenCalled();
+      expect(assetMock.update).not.toHaveBeenCalledWith();
+      expect(eventMock.emit).not.toHaveBeenCalledWith();
+    });
   });
 
   describe('updateAll', () => {
@@ -269,10 +426,10 @@ describe(AssetService.name, () => {
 
       await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: true });
 
-      expect(jobMock.queueAll).toHaveBeenCalledWith([
-        { name: JobName.ASSET_DELETION, data: { id: 'asset1', deleteOnDisk: true } },
-        { name: JobName.ASSET_DELETION, data: { id: 'asset2', deleteOnDisk: true } },
-      ]);
+      expect(eventMock.emit).toHaveBeenCalledWith('assets.delete', {
+        assetIds: ['asset1', 'asset2'],
+        userId: 'user-id',
+      });
     });
 
     it('should soft delete a batch of assets', async () => {
@@ -280,11 +437,50 @@ describe(AssetService.name, () => {
 
       await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: false });
 
-      expect(assetMock.softDeleteAll).toHaveBeenCalledWith(['asset1', 'asset2']);
+      expect(assetMock.updateAll).toHaveBeenCalledWith(['asset1', 'asset2'], {
+        deletedAt: expect.any(Date),
+        status: AssetStatus.TRASHED,
+      });
       expect(jobMock.queue.mock.calls).toEqual([]);
     });
   });
 
+  describe('handleAssetDeletionCheck', () => {
+    beforeAll(() => {
+      vi.useFakeTimers();
+    });
+
+    afterAll(() => {
+      vi.useRealTimers();
+    });
+
+    it('should immediately queue assets for deletion if trash is disabled', async () => {
+      assetMock.getAll.mockResolvedValue({ hasNextPage: false, items: [assetStub.image] });
+      systemMock.get.mockResolvedValue({ trash: { enabled: false } });
+
+      await expect(sut.handleAssetDeletionCheck()).resolves.toBe(JobStatus.SUCCESS);
+
+      expect(assetMock.getAll).toHaveBeenCalledWith(expect.anything(), { trashedBefore: new Date() });
+      expect(jobMock.queueAll).toHaveBeenCalledWith([
+        { name: JobName.ASSET_DELETION, data: { id: assetStub.image.id, deleteOnDisk: true } },
+      ]);
+    });
+
+    it('should queue assets for deletion after trash duration', async () => {
+      assetMock.getAll.mockResolvedValue({ hasNextPage: false, items: [assetStub.image] });
+      systemMock.get.mockResolvedValue({ trash: { enabled: true, days: 7 } });
+
+      await expect(sut.handleAssetDeletionCheck()).resolves.toBe(JobStatus.SUCCESS);
+
+      expect(assetMock.getAll).toHaveBeenCalledWith(expect.anything(), {
+        trashedBefore: DateTime.now().minus({ days: 7 }).toJSDate(),
+      });
+      expect(jobMock.queueAll).toHaveBeenCalledWith([
+        { name: JobName.ASSET_DELETION, data: { id: assetStub.image.id, deleteOnDisk: true } },
+      ]);
+    });
+  });
+
   describe('handleAssetDeletion', () => {
     it('should remove faces', async () => {
       const assetWithFace = { ...assetStub.image, faces: [faceStub.face1, faceStub.mergeFace1] };
@@ -324,6 +520,17 @@ describe(AssetService.name, () => {
       });
     });
 
+    it('should delete the entire stack if deleted asset was the primary asset and the stack would only contain one asset afterwards', async () => {
+      assetMock.getById.mockResolvedValue({
+        ...assetStub.primaryImage,
+        stack: { ...assetStub.primaryImage.stack, assets: assetStub.primaryImage.stack!.assets.slice(0, 2) },
+      } as AssetEntity);
+
+      await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true });
+
+      expect(stackMock.delete).toHaveBeenCalledWith('stack-1');
+    });
+
     it('should delete a live photo', async () => {
       assetMock.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
       assetMock.getLivePhotoCount.mockResolvedValue(0);
@@ -380,9 +587,21 @@ describe(AssetService.name, () => {
       await sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true });
       expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000);
     });
+
+    it('should fail if asset could not be found', async () => {
+      await expect(sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true })).resolves.toBe(
+        JobStatus.FAILED,
+      );
+    });
   });
 
   describe('run', () => {
+    it('should run the refresh faces job', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
+      await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REFRESH_FACES });
+      expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.FACE_DETECTION, data: { id: 'asset-1' } }]);
+    });
+
     it('should run the refresh metadata job', async () => {
       accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
       await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REFRESH_METADATA });
@@ -392,7 +611,7 @@ describe(AssetService.name, () => {
     it('should run the refresh thumbnails job', async () => {
       accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
       await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REGENERATE_THUMBNAIL });
-      expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.GENERATE_PREVIEW, data: { id: 'asset-1' } }]);
+      expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1' } }]);
     });
 
     it('should run the transcode video', async () => {
diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts
index 98d3dd1459..8751037119 100644
--- a/server/src/services/asset.service.ts
+++ b/server/src/services/asset.service.ts
@@ -1,7 +1,7 @@
-import { BadRequestException, Inject } from '@nestjs/common';
+import { BadRequestException } from '@nestjs/common';
 import _ from 'lodash';
 import { DateTime, Duration } from 'luxon';
-import { SystemConfigCore } from 'src/cores/system-config.core';
+import { OnJob } from 'src/decorators';
 import {
   AssetResponseDto,
   MemoryLaneResponseDto,
@@ -20,46 +20,21 @@ import {
 import { AuthDto } from 'src/dtos/auth.dto';
 import { MemoryLaneDto } from 'src/dtos/search.dto';
 import { AssetEntity } from 'src/entities/asset.entity';
-import { Permission } from 'src/enum';
-import { IAccessRepository } from 'src/interfaces/access.interface';
-import { IAssetRepository } from 'src/interfaces/asset.interface';
-import { IEventRepository } from 'src/interfaces/event.interface';
+import { AssetStatus, Permission } from 'src/enum';
 import {
-  IAssetDeleteJob,
-  IJobRepository,
   ISidecarWriteJob,
   JOBS_ASSET_PAGINATION_SIZE,
   JobItem,
   JobName,
+  JobOf,
   JobStatus,
+  QueueName,
 } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { IPartnerRepository } from 'src/interfaces/partner.interface';
-import { IStackRepository } from 'src/interfaces/stack.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
-import { IUserRepository } from 'src/interfaces/user.interface';
-import { requireAccess } from 'src/utils/access';
+import { BaseService } from 'src/services/base.service';
 import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util';
 import { usePagination } from 'src/utils/pagination';
 
-export class AssetService {
-  private configCore: SystemConfigCore;
-
-  constructor(
-    @Inject(IAccessRepository) private access: IAccessRepository,
-    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
-    @Inject(IJobRepository) private jobRepository: IJobRepository,
-    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
-    @Inject(IUserRepository) private userRepository: IUserRepository,
-    @Inject(IEventRepository) private eventRepository: IEventRepository,
-    @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
-    @Inject(IStackRepository) private stackRepository: IStackRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-  ) {
-    this.logger.setContext(AssetService.name);
-    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
-  }
-
+export class AssetService extends BaseService {
   async getMemoryLane(auth: AuthDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
     const partnerIds = await getMyPartnerIds({
       userId: auth.user.id,
@@ -68,28 +43,13 @@ export class AssetService {
     });
     const userIds = [auth.user.id, ...partnerIds];
 
-    const assets = await this.assetRepository.getByDayOfYear(userIds, dto);
-    const assetsWithThumbnails = assets.filter(({ files }) => !!getAssetFiles(files).thumbnailFile);
-    const groups: Record<number, AssetEntity[]> = {};
-    const currentYear = new Date().getFullYear();
-    for (const asset of assetsWithThumbnails) {
-      const yearsAgo = currentYear - asset.localDateTime.getFullYear();
-      if (!groups[yearsAgo]) {
-        groups[yearsAgo] = [];
-      }
-      groups[yearsAgo].push(asset);
-    }
-
-    return Object.keys(groups)
-      .map(Number)
-      .sort((a, b) => a - b)
-      .filter((yearsAgo) => yearsAgo > 0)
-      .map((yearsAgo) => ({
-        yearsAgo,
-        // TODO move this to clients
-        title: `${yearsAgo} year${yearsAgo > 1 ? 's' : ''} ago`,
-        assets: groups[yearsAgo].map((asset) => mapAsset(asset, { auth })),
-      }));
+    const groups = await this.assetRepository.getByDayOfYear(userIds, dto);
+    return groups.map(({ yearsAgo, assets }) => ({
+      yearsAgo,
+      // TODO move this to clients
+      title: `${yearsAgo} year${yearsAgo > 1 ? 's' : ''} ago`,
+      assets: assets.map((asset) => mapAsset(asset, { auth })),
+    }));
   }
 
   async getStatistics(auth: AuthDto, dto: AssetStatsDto) {
@@ -112,15 +72,14 @@ export class AssetService {
   }
 
   async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
-    await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.ASSET_READ, ids: [id] });
 
     const asset = await this.assetRepository.getById(
       id,
       {
         exifInfo: true,
-        tags: true,
         sharedLinks: true,
-        smartInfo: true,
+        tags: true,
         owner: true,
         faces: {
           person: true,
@@ -161,7 +120,7 @@ export class AssetService {
   }
 
   async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
-    await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: [id] });
 
     const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
     const repos = { asset: this.assetRepository, event: this.eventRepository };
@@ -187,7 +146,6 @@ export class AssetService {
     const asset = await this.assetRepository.getById(id, {
       exifInfo: true,
       owner: true,
-      smartInfo: true,
       tags: true,
       faces: {
         person: true,
@@ -204,7 +162,7 @@ export class AssetService {
 
   async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
     const { ids, dateTimeOriginal, latitude, longitude, ...options } = dto;
-    await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids });
+    await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids });
 
     for (const id of ids) {
       await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude });
@@ -213,8 +171,9 @@ export class AssetService {
     await this.assetRepository.updateAll(ids, options);
   }
 
+  @OnJob({ name: JobName.ASSET_DELETION_CHECK, queue: QueueName.BACKGROUND_TASK })
   async handleAssetDeletionCheck(): Promise<JobStatus> {
-    const config = await this.configCore.getConfig({ withCache: false });
+    const config = await this.getConfig({ withCache: false });
     const trashedDays = config.trash.enabled ? config.trash.days : 0;
     const trashedBefore = DateTime.now()
       .minus(Duration.fromObject({ days: trashedDays }))
@@ -238,7 +197,8 @@ export class AssetService {
     return JobStatus.SUCCESS;
   }
 
-  async handleAssetDeletion(job: IAssetDeleteJob): Promise<JobStatus> {
+  @OnJob({ name: JobName.ASSET_DELETION, queue: QueueName.BACKGROUND_TASK })
+  async handleAssetDeletion(job: JobOf<JobName.ASSET_DELETION>): Promise<JobStatus> {
     const { id, deleteOnDisk } = job;
 
     const asset = await this.assetRepository.getById(id, {
@@ -301,35 +261,33 @@ export class AssetService {
   async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> {
     const { ids, force } = dto;
 
-    await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids });
-
-    if (force) {
-      await this.jobRepository.queueAll(
-        ids.map((id) => ({
-          name: JobName.ASSET_DELETION,
-          data: { id, deleteOnDisk: true },
-        })),
-      );
-    } else {
-      await this.assetRepository.softDeleteAll(ids);
-      await this.eventRepository.emit('assets.trash', { assetIds: ids, userId: auth.user.id });
-    }
+    await this.requireAccess({ auth, permission: Permission.ASSET_DELETE, ids });
+    await this.assetRepository.updateAll(ids, {
+      deletedAt: new Date(),
+      status: force ? AssetStatus.DELETED : AssetStatus.TRASHED,
+    });
+    await this.eventRepository.emit(force ? 'assets.delete' : 'assets.trash', { assetIds: ids, userId: auth.user.id });
   }
 
   async run(auth: AuthDto, dto: AssetJobsDto) {
-    await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds });
+    await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds });
 
     const jobs: JobItem[] = [];
 
     for (const id of dto.assetIds) {
       switch (dto.name) {
+        case AssetJobName.REFRESH_FACES: {
+          jobs.push({ name: JobName.FACE_DETECTION, data: { id } });
+          break;
+        }
+
         case AssetJobName.REFRESH_METADATA: {
           jobs.push({ name: JobName.METADATA_EXTRACTION, data: { id } });
           break;
         }
 
         case AssetJobName.REGENERATE_THUMBNAIL: {
-          jobs.push({ name: JobName.GENERATE_PREVIEW, data: { id } });
+          jobs.push({ name: JobName.GENERATE_THUMBNAILS, data: { id } });
           break;
         }
 
diff --git a/server/src/services/audit.service.spec.ts b/server/src/services/audit.service.spec.ts
index ef685f4a87..c7a51565af 100644
--- a/server/src/services/audit.service.spec.ts
+++ b/server/src/services/audit.service.spec.ts
@@ -1,46 +1,28 @@
-import { DatabaseAction, EntityType } from 'src/enum';
+import { BadRequestException } from '@nestjs/common';
+import { FileReportItemDto } from 'src/dtos/audit.dto';
+import { AssetFileType, AssetPathType, DatabaseAction, EntityType, PersonPathType, UserPathType } from 'src/enum';
 import { IAssetRepository } from 'src/interfaces/asset.interface';
 import { IAuditRepository } from 'src/interfaces/audit.interface';
 import { ICryptoRepository } from 'src/interfaces/crypto.interface';
 import { JobStatus } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { IPersonRepository } from 'src/interfaces/person.interface';
-import { IStorageRepository } from 'src/interfaces/storage.interface';
 import { IUserRepository } from 'src/interfaces/user.interface';
 import { AuditService } from 'src/services/audit.service';
 import { auditStub } from 'test/fixtures/audit.stub';
 import { authStub } from 'test/fixtures/auth.stub';
-import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
-import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
-import { newAuditRepositoryMock } from 'test/repositories/audit.repository.mock';
-import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
-import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
-import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(AuditService.name, () => {
   let sut: AuditService;
-  let accessMock: IAccessRepositoryMock;
-  let assetMock: Mocked<IAssetRepository>;
   let auditMock: Mocked<IAuditRepository>;
+  let assetMock: Mocked<IAssetRepository>;
   let cryptoMock: Mocked<ICryptoRepository>;
   let personMock: Mocked<IPersonRepository>;
-  let storageMock: Mocked<IStorageRepository>;
   let userMock: Mocked<IUserRepository>;
-  let loggerMock: Mocked<ILoggerRepository>;
 
   beforeEach(() => {
-    accessMock = newAccessRepositoryMock();
-    assetMock = newAssetRepositoryMock();
-    cryptoMock = newCryptoRepositoryMock();
-    auditMock = newAuditRepositoryMock();
-    personMock = newPersonRepositoryMock();
-    storageMock = newStorageRepositoryMock();
-    userMock = newUserRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-    sut = new AuditService(accessMock, assetMock, cryptoMock, personMock, auditMock, storageMock, userMock, loggerMock);
+    ({ sut, auditMock, assetMock, cryptoMock, personMock, userMock } = newTestService(AuditService));
   });
 
   it('should work', () => {
@@ -87,4 +69,148 @@ describe(AuditService.name, () => {
       });
     });
   });
+
+  describe('getChecksums', () => {
+    it('should fail if the file is not in the immich path', async () => {
+      await expect(sut.getChecksums({ filenames: ['foo/bar'] })).rejects.toBeInstanceOf(BadRequestException);
+
+      expect(cryptoMock.hashFile).not.toHaveBeenCalled();
+    });
+
+    it('should get checksum for valid file', async () => {
+      await expect(sut.getChecksums({ filenames: ['./upload/my-file.jpg'] })).resolves.toEqual([
+        { filename: './upload/my-file.jpg', checksum: expect.any(String) },
+      ]);
+
+      expect(cryptoMock.hashFile).toHaveBeenCalledWith('./upload/my-file.jpg');
+    });
+  });
+
+  describe('fixItems', () => {
+    it('should fail if the file is not in the immich path', async () => {
+      await expect(
+        sut.fixItems([
+          { entityId: 'my-id', pathType: AssetPathType.ORIGINAL, pathValue: 'foo/bar' } as FileReportItemDto,
+        ]),
+      ).rejects.toBeInstanceOf(BadRequestException);
+
+      expect(assetMock.update).not.toHaveBeenCalled();
+      expect(assetMock.upsertFile).not.toHaveBeenCalled();
+      expect(personMock.update).not.toHaveBeenCalled();
+      expect(userMock.update).not.toHaveBeenCalled();
+    });
+
+    it('should update encoded video path', async () => {
+      await sut.fixItems([
+        {
+          entityId: 'my-id',
+          pathType: AssetPathType.ENCODED_VIDEO,
+          pathValue: './upload/my-video.mp4',
+        } as FileReportItemDto,
+      ]);
+
+      expect(assetMock.update).toHaveBeenCalledWith({ id: 'my-id', encodedVideoPath: './upload/my-video.mp4' });
+      expect(assetMock.upsertFile).not.toHaveBeenCalled();
+      expect(personMock.update).not.toHaveBeenCalled();
+      expect(userMock.update).not.toHaveBeenCalled();
+    });
+
+    it('should update preview path', async () => {
+      await sut.fixItems([
+        {
+          entityId: 'my-id',
+          pathType: AssetPathType.PREVIEW,
+          pathValue: './upload/my-preview.png',
+        } as FileReportItemDto,
+      ]);
+
+      expect(assetMock.upsertFile).toHaveBeenCalledWith({
+        assetId: 'my-id',
+        type: AssetFileType.PREVIEW,
+        path: './upload/my-preview.png',
+      });
+      expect(assetMock.update).not.toHaveBeenCalled();
+      expect(personMock.update).not.toHaveBeenCalled();
+      expect(userMock.update).not.toHaveBeenCalled();
+    });
+
+    it('should update thumbnail path', async () => {
+      await sut.fixItems([
+        {
+          entityId: 'my-id',
+          pathType: AssetPathType.THUMBNAIL,
+          pathValue: './upload/my-thumbnail.webp',
+        } as FileReportItemDto,
+      ]);
+
+      expect(assetMock.upsertFile).toHaveBeenCalledWith({
+        assetId: 'my-id',
+        type: AssetFileType.THUMBNAIL,
+        path: './upload/my-thumbnail.webp',
+      });
+      expect(assetMock.update).not.toHaveBeenCalled();
+      expect(personMock.update).not.toHaveBeenCalled();
+      expect(userMock.update).not.toHaveBeenCalled();
+    });
+
+    it('should update original path', async () => {
+      await sut.fixItems([
+        {
+          entityId: 'my-id',
+          pathType: AssetPathType.ORIGINAL,
+          pathValue: './upload/my-original.png',
+        } as FileReportItemDto,
+      ]);
+
+      expect(assetMock.update).toHaveBeenCalledWith({ id: 'my-id', originalPath: './upload/my-original.png' });
+      expect(assetMock.upsertFile).not.toHaveBeenCalled();
+      expect(personMock.update).not.toHaveBeenCalled();
+      expect(userMock.update).not.toHaveBeenCalled();
+    });
+
+    it('should update sidecar path', async () => {
+      await sut.fixItems([
+        {
+          entityId: 'my-id',
+          pathType: AssetPathType.SIDECAR,
+          pathValue: './upload/my-sidecar.xmp',
+        } as FileReportItemDto,
+      ]);
+
+      expect(assetMock.update).toHaveBeenCalledWith({ id: 'my-id', sidecarPath: './upload/my-sidecar.xmp' });
+      expect(assetMock.upsertFile).not.toHaveBeenCalled();
+      expect(personMock.update).not.toHaveBeenCalled();
+      expect(userMock.update).not.toHaveBeenCalled();
+    });
+
+    it('should update face path', async () => {
+      await sut.fixItems([
+        {
+          entityId: 'my-id',
+          pathType: PersonPathType.FACE,
+          pathValue: './upload/my-face.jpg',
+        } as FileReportItemDto,
+      ]);
+
+      expect(personMock.update).toHaveBeenCalledWith({ id: 'my-id', thumbnailPath: './upload/my-face.jpg' });
+      expect(assetMock.update).not.toHaveBeenCalled();
+      expect(assetMock.upsertFile).not.toHaveBeenCalled();
+      expect(userMock.update).not.toHaveBeenCalled();
+    });
+
+    it('should update profile path', async () => {
+      await sut.fixItems([
+        {
+          entityId: 'my-id',
+          pathType: UserPathType.PROFILE,
+          pathValue: './upload/my-profile-pic.jpg',
+        } as FileReportItemDto,
+      ]);
+
+      expect(userMock.update).toHaveBeenCalledWith('my-id', { profileImagePath: './upload/my-profile-pic.jpg' });
+      expect(assetMock.update).not.toHaveBeenCalled();
+      expect(assetMock.upsertFile).not.toHaveBeenCalled();
+      expect(personMock.update).not.toHaveBeenCalled();
+    });
+  });
 });
diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts
index 72db2b6eb5..3fc838e5e9 100644
--- a/server/src/services/audit.service.ts
+++ b/server/src/services/audit.service.ts
@@ -1,8 +1,9 @@
-import { BadRequestException, Inject, Injectable } from '@nestjs/common';
+import { BadRequestException, Injectable } from '@nestjs/common';
 import { DateTime } from 'luxon';
 import { resolve } from 'node:path';
 import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
-import { StorageCore, StorageFolder } from 'src/cores/storage.core';
+import { StorageCore } from 'src/cores/storage.core';
+import { OnJob } from 'src/decorators';
 import {
   AuditDeletesDto,
   AuditDeletesResponseDto,
@@ -12,46 +13,33 @@ import {
   PathEntityType,
 } from 'src/dtos/audit.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
-import { AssetPathType, PersonPathType, UserPathType } from 'src/entities/move.entity';
-import { AssetFileType, DatabaseAction, Permission } from 'src/enum';
-import { IAccessRepository } from 'src/interfaces/access.interface';
-import { IAssetRepository } from 'src/interfaces/asset.interface';
-import { IAuditRepository } from 'src/interfaces/audit.interface';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import { JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { IPersonRepository } from 'src/interfaces/person.interface';
-import { IStorageRepository } from 'src/interfaces/storage.interface';
-import { IUserRepository } from 'src/interfaces/user.interface';
-import { requireAccess } from 'src/utils/access';
+import {
+  AssetFileType,
+  AssetPathType,
+  DatabaseAction,
+  Permission,
+  PersonPathType,
+  StorageFolder,
+  UserPathType,
+} from 'src/enum';
+import { JobName, JOBS_ASSET_PAGINATION_SIZE, JobStatus, QueueName } from 'src/interfaces/job.interface';
+import { BaseService } from 'src/services/base.service';
 import { getAssetFiles } from 'src/utils/asset.util';
 import { usePagination } from 'src/utils/pagination';
 
 @Injectable()
-export class AuditService {
-  constructor(
-    @Inject(IAccessRepository) private access: IAccessRepository,
-    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
-    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
-    @Inject(IPersonRepository) private personRepository: IPersonRepository,
-    @Inject(IAuditRepository) private repository: IAuditRepository,
-    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
-    @Inject(IUserRepository) private userRepository: IUserRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-  ) {
-    this.logger.setContext(AuditService.name);
-  }
-
+export class AuditService extends BaseService {
+  @OnJob({ name: JobName.CLEAN_OLD_AUDIT_LOGS, queue: QueueName.BACKGROUND_TASK })
   async handleCleanup(): Promise<JobStatus> {
-    await this.repository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate());
+    await this.auditRepository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate());
     return JobStatus.SUCCESS;
   }
 
   async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
     const userId = dto.userId || auth.user.id;
-    await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [userId] });
+    await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: [userId] });
 
-    const audits = await this.repository.getAfter(dto.after, {
+    const audits = await this.auditRepository.getAfter(dto.after, {
       userIds: [userId],
       entityType: dto.entityType,
       action: DatabaseAction.DELETE,
diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts
index acc2d3459c..d34e2673f5 100644
--- a/server/src/services/auth.service.spec.ts
+++ b/server/src/services/auth.service.spec.ts
@@ -1,33 +1,35 @@
 import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
-import { Issuer, generators } from 'openid-client';
-import { AuthType } from 'src/constants';
 import { AuthDto, SignUpDto } from 'src/dtos/auth.dto';
 import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
 import { UserEntity } from 'src/entities/user.entity';
+import { AuthType, Permission } from 'src/enum';
 import { IKeyRepository } from 'src/interfaces/api-key.interface';
 import { ICryptoRepository } from 'src/interfaces/crypto.interface';
 import { IEventRepository } from 'src/interfaces/event.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
+import { IOAuthRepository } from 'src/interfaces/oauth.interface';
 import { ISessionRepository } from 'src/interfaces/session.interface';
 import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { IUserRepository } from 'src/interfaces/user.interface';
 import { AuthService } from 'src/services/auth.service';
 import { keyStub } from 'test/fixtures/api-key.stub';
-import { authStub, loginResponseStub } from 'test/fixtures/auth.stub';
+import { authStub } from 'test/fixtures/auth.stub';
 import { sessionStub } from 'test/fixtures/session.stub';
 import { sharedLinkStub } from 'test/fixtures/shared-link.stub';
 import { systemConfigStub } from 'test/fixtures/system-config.stub';
 import { userStub } from 'test/fixtures/user.stub';
-import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock';
-import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
-import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock';
-import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
-import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
-import { Mock, Mocked, vitest } from 'vitest';
+import { newTestService } from 'test/utils';
+import { Mocked } from 'vitest';
+
+const oauthResponse = {
+  accessToken: 'cmFuZG9tLWJ5dGVz',
+  userId: 'user-id',
+  userEmail: 'immich@test.com',
+  name: 'immich_name',
+  profileImagePath: '',
+  isAdmin: false,
+  shouldChangePassword: false,
+};
 
 // const token = Buffer.from('my-api-key', 'utf8').toString('base64');
 
@@ -51,60 +53,42 @@ const oauthUserWithDefaultQuota = {
   email,
   name: ' ',
   oauthId: sub,
-  quotaSizeInBytes: 1_073_741_824,
+  quotaSizeInBytes: '1073741824',
   storageLabel: null,
 };
 
 describe('AuthService', () => {
   let sut: AuthService;
+
   let cryptoMock: Mocked<ICryptoRepository>;
   let eventMock: Mocked<IEventRepository>;
-  let userMock: Mocked<IUserRepository>;
-  let loggerMock: Mocked<ILoggerRepository>;
-  let systemMock: Mocked<ISystemMetadataRepository>;
-  let sessionMock: Mocked<ISessionRepository>;
-  let shareMock: Mocked<ISharedLinkRepository>;
   let keyMock: Mocked<IKeyRepository>;
-
-  let callbackMock: Mock;
-  let userinfoMock: Mock;
+  let oauthMock: Mocked<IOAuthRepository>;
+  let sessionMock: Mocked<ISessionRepository>;
+  let sharedLinkMock: Mocked<ISharedLinkRepository>;
+  let systemMock: Mocked<ISystemMetadataRepository>;
+  let userMock: Mocked<IUserRepository>;
 
   beforeEach(() => {
-    callbackMock = vitest.fn().mockReturnValue({ access_token: 'access-token' });
-    userinfoMock = vitest.fn().mockResolvedValue({ sub, email });
+    ({ sut, cryptoMock, eventMock, keyMock, oauthMock, sessionMock, sharedLinkMock, systemMock, userMock } =
+      newTestService(AuthService));
 
-    vitest.spyOn(generators, 'state').mockReturnValue('state');
-    vitest.spyOn(Issuer, 'discover').mockResolvedValue({
-      id_token_signing_alg_values_supported: ['RS256'],
-      Client: vitest.fn().mockResolvedValue({
-        issuer: {
-          metadata: {
-            end_session_endpoint: 'http://end-session-endpoint',
-          },
-        },
-        authorizationUrl: vitest.fn().mockReturnValue('http://authorization-url'),
-        callbackParams: vitest.fn().mockReturnValue({ state: 'state' }),
-        callback: callbackMock,
-        userinfo: userinfoMock,
-      }),
-    } as any);
-
-    cryptoMock = newCryptoRepositoryMock();
-    eventMock = newEventRepositoryMock();
-    userMock = newUserRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-    systemMock = newSystemMetadataRepositoryMock();
-    sessionMock = newSessionRepositoryMock();
-    shareMock = newSharedLinkRepositoryMock();
-    keyMock = newKeyRepositoryMock();
-
-    sut = new AuthService(cryptoMock, eventMock, systemMock, loggerMock, userMock, sessionMock, shareMock, keyMock);
+    oauthMock.authorize.mockResolvedValue('access-token');
+    oauthMock.getProfile.mockResolvedValue({ sub, email });
+    oauthMock.getLogoutEndpoint.mockResolvedValue('http://end-session-endpoint');
   });
 
   it('should be defined', () => {
     expect(sut).toBeDefined();
   });
 
+  describe('onBootstrap', () => {
+    it('should init the repo', () => {
+      sut.onBootstrap();
+      expect(oauthMock.init).toHaveBeenCalled();
+    });
+  });
+
   describe('login', () => {
     it('should throw an error if password login is disabled', async () => {
       systemMock.get.mockResolvedValue(systemConfigStub.disabled);
@@ -126,7 +110,15 @@ describe('AuthService', () => {
     it('should successfully log the user in', async () => {
       userMock.getByEmail.mockResolvedValue(userStub.user1);
       sessionMock.create.mockResolvedValue(sessionStub.valid);
-      await expect(sut.login(fixtures.login, loginDetails)).resolves.toEqual(loginResponseStub.user1password);
+      await expect(sut.login(fixtures.login, loginDetails)).resolves.toEqual({
+        accessToken: 'cmFuZG9tLWJ5dGVz',
+        userId: 'user-id',
+        userEmail: 'immich@test.com',
+        name: 'immich_name',
+        profileImagePath: '',
+        isAdmin: false,
+        shouldChangePassword: false,
+      });
       expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
     });
   });
@@ -283,7 +275,7 @@ describe('AuthService', () => {
 
   describe('validate - shared key', () => {
     it('should not accept a non-existent key', async () => {
-      shareMock.getByKey.mockResolvedValue(null);
+      sharedLinkMock.getByKey.mockResolvedValue(null);
       await expect(
         sut.authenticate({
           headers: { 'x-immich-share-key': 'key' },
@@ -294,7 +286,7 @@ describe('AuthService', () => {
     });
 
     it('should not accept an expired key', async () => {
-      shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired);
+      sharedLinkMock.getByKey.mockResolvedValue(sharedLinkStub.expired);
       await expect(
         sut.authenticate({
           headers: { 'x-immich-share-key': 'key' },
@@ -304,8 +296,19 @@ describe('AuthService', () => {
       ).rejects.toBeInstanceOf(UnauthorizedException);
     });
 
+    it('should not accept a key on a non-shared route', async () => {
+      sharedLinkMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
+      await expect(
+        sut.authenticate({
+          headers: { 'x-immich-share-key': 'key' },
+          queryParams: {},
+          metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' },
+        }),
+      ).rejects.toBeInstanceOf(ForbiddenException);
+    });
+
     it('should not accept a key without a user', async () => {
-      shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired);
+      sharedLinkMock.getByKey.mockResolvedValue(sharedLinkStub.expired);
       userMock.get.mockResolvedValue(null);
       await expect(
         sut.authenticate({
@@ -317,7 +320,7 @@ describe('AuthService', () => {
     });
 
     it('should accept a base64url key', async () => {
-      shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
+      sharedLinkMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
       userMock.get.mockResolvedValue(userStub.admin);
       await expect(
         sut.authenticate({
@@ -329,11 +332,11 @@ describe('AuthService', () => {
         user: userStub.admin,
         sharedLink: sharedLinkStub.valid,
       });
-      expect(shareMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key);
+      expect(sharedLinkMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key);
     });
 
     it('should accept a hex key', async () => {
-      shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
+      sharedLinkMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
       userMock.get.mockResolvedValue(userStub.admin);
       await expect(
         sut.authenticate({
@@ -345,7 +348,7 @@ describe('AuthService', () => {
         user: userStub.admin,
         sharedLink: sharedLinkStub.valid,
       });
-      expect(shareMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key);
+      expect(sharedLinkMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key);
     });
   });
 
@@ -413,6 +416,17 @@ describe('AuthService', () => {
       expect(keyMock.getKey).toHaveBeenCalledWith('auth_token (hashed)');
     });
 
+    it('should throw an error if api key has insufficient permissions', async () => {
+      keyMock.getKey.mockResolvedValue({ ...keyStub.admin, permissions: [] });
+      await expect(
+        sut.authenticate({
+          headers: { 'x-api-key': 'auth_token' },
+          queryParams: {},
+          metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test', permission: Permission.ASSET_READ },
+        }),
+      ).rejects.toBeInstanceOf(ForbiddenException);
+    });
+
     it('should return an auth dto', async () => {
       keyMock.getKey.mockResolvedValue(keyStub.admin);
       await expect(
@@ -438,6 +452,20 @@ describe('AuthService', () => {
     });
   });
 
+  describe('authorize', () => {
+    it('should fail if oauth is disabled', async () => {
+      systemMock.get.mockResolvedValue({ oauth: { enabled: false } });
+      await expect(sut.authorize({ redirectUri: 'https://demo.immich.app' })).rejects.toBeInstanceOf(
+        BadRequestException,
+      );
+    });
+
+    it('should authorize the user', async () => {
+      systemMock.get.mockResolvedValue(systemConfigStub.oauthWithMobileOverride);
+      await sut.authorize({ redirectUri: 'https://demo.immich.app' });
+    });
+  });
+
   describe('callback', () => {
     it('should throw an error if OAuth is not enabled', async () => {
       await expect(sut.callback({ url: '' }, loginDetails)).rejects.toBeInstanceOf(BadRequestException);
@@ -459,7 +487,7 @@ describe('AuthService', () => {
       sessionMock.create.mockResolvedValue(sessionStub.valid);
 
       await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
-        loginResponseStub.user1oauth,
+        oauthResponse,
       );
 
       expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
@@ -488,13 +516,29 @@ describe('AuthService', () => {
       sessionMock.create.mockResolvedValue(sessionStub.valid);
 
       await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
-        loginResponseStub.user1oauth,
+        oauthResponse,
       );
 
       expect(userMock.getByEmail).toHaveBeenCalledTimes(2); // second call is for domain check before create
       expect(userMock.create).toHaveBeenCalledTimes(1);
     });
 
+    it('should throw an error if user should be auto registered but the email claim does not exist', async () => {
+      systemMock.get.mockResolvedValue(systemConfigStub.enabled);
+      userMock.getByEmail.mockResolvedValue(null);
+      userMock.getAdmin.mockResolvedValue(userStub.user1);
+      userMock.create.mockResolvedValue(userStub.user1);
+      sessionMock.create.mockResolvedValue(sessionStub.valid);
+      oauthMock.getProfile.mockResolvedValue({ sub, email: undefined });
+
+      await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).rejects.toBeInstanceOf(
+        BadRequestException,
+      );
+
+      expect(userMock.getByEmail).not.toHaveBeenCalled();
+      expect(userMock.create).not.toHaveBeenCalled();
+    });
+
     for (const url of [
       'app.immich:/',
       'app.immich://',
@@ -509,7 +553,7 @@ describe('AuthService', () => {
         sessionMock.create.mockResolvedValue(sessionStub.valid);
 
         await sut.callback({ url }, loginDetails);
-        expect(callbackMock).toHaveBeenCalledWith('http://mobile-redirect', { state: 'state' }, { state: 'state' });
+        expect(oauthMock.getProfile).toHaveBeenCalledWith(expect.objectContaining({}), url, 'http://mobile-redirect');
       });
     }
 
@@ -520,10 +564,10 @@ describe('AuthService', () => {
       userMock.create.mockResolvedValue(userStub.user1);
 
       await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
-        loginResponseStub.user1oauth,
+        oauthResponse,
       );
 
-      expect(userMock.create).toHaveBeenCalledWith(oauthUserWithDefaultQuota);
+      expect(userMock.create).toHaveBeenCalledWith({ ...oauthUserWithDefaultQuota, quotaSizeInBytes: 1_073_741_824 });
     });
 
     it('should ignore an invalid storage quota', async () => {
@@ -531,13 +575,13 @@ describe('AuthService', () => {
       userMock.getByEmail.mockResolvedValue(null);
       userMock.getAdmin.mockResolvedValue(userStub.user1);
       userMock.create.mockResolvedValue(userStub.user1);
-      userinfoMock.mockResolvedValue({ sub, email, immich_quota: 'abc' });
+      oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: 'abc' });
 
       await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
-        loginResponseStub.user1oauth,
+        oauthResponse,
       );
 
-      expect(userMock.create).toHaveBeenCalledWith(oauthUserWithDefaultQuota);
+      expect(userMock.create).toHaveBeenCalledWith({ ...oauthUserWithDefaultQuota, quotaSizeInBytes: 1_073_741_824 });
     });
 
     it('should ignore a negative quota', async () => {
@@ -545,13 +589,13 @@ describe('AuthService', () => {
       userMock.getByEmail.mockResolvedValue(null);
       userMock.getAdmin.mockResolvedValue(userStub.user1);
       userMock.create.mockResolvedValue(userStub.user1);
-      userinfoMock.mockResolvedValue({ sub, email, immich_quota: -5 });
+      oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: -5 });
 
       await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
-        loginResponseStub.user1oauth,
+        oauthResponse,
       );
 
-      expect(userMock.create).toHaveBeenCalledWith(oauthUserWithDefaultQuota);
+      expect(userMock.create).toHaveBeenCalledWith({ ...oauthUserWithDefaultQuota, quotaSizeInBytes: 1_073_741_824 });
     });
 
     it('should not set quota for 0 quota', async () => {
@@ -559,10 +603,10 @@ describe('AuthService', () => {
       userMock.getByEmail.mockResolvedValue(null);
       userMock.getAdmin.mockResolvedValue(userStub.user1);
       userMock.create.mockResolvedValue(userStub.user1);
-      userinfoMock.mockResolvedValue({ sub, email, immich_quota: 0 });
+      oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: 0 });
 
       await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
-        loginResponseStub.user1oauth,
+        oauthResponse,
       );
 
       expect(userMock.create).toHaveBeenCalledWith({
@@ -579,10 +623,10 @@ describe('AuthService', () => {
       userMock.getByEmail.mockResolvedValue(null);
       userMock.getAdmin.mockResolvedValue(userStub.user1);
       userMock.create.mockResolvedValue(userStub.user1);
-      userinfoMock.mockResolvedValue({ sub, email, immich_quota: 5 });
+      oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: 5 });
 
       await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
-        loginResponseStub.user1oauth,
+        oauthResponse,
       );
 
       expect(userMock.create).toHaveBeenCalledWith({
diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts
index 6eaf755d0e..0d44fa0562 100644
--- a/server/src/services/auth.service.ts
+++ b/server/src/services/auth.service.ts
@@ -1,26 +1,13 @@
-import {
-  BadRequestException,
-  ForbiddenException,
-  Inject,
-  Injectable,
-  InternalServerErrorException,
-  UnauthorizedException,
-} from '@nestjs/common';
-import { isNumber, isString } from 'class-validator';
+import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
+import { isString } from 'class-validator';
 import cookieParser from 'cookie';
 import { DateTime } from 'luxon';
 import { IncomingHttpHeaders } from 'node:http';
-import { Issuer, UserinfoResponse, custom, generators } from 'openid-client';
-import { SystemConfig } from 'src/config';
-import { AuthType, LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
-import { SystemConfigCore } from 'src/cores/system-config.core';
-import { UserCore } from 'src/cores/user.core';
+import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
+import { OnEvent } from 'src/decorators';
 import {
   AuthDto,
   ChangePasswordDto,
-  ImmichCookie,
-  ImmichHeader,
-  ImmichQuery,
   LoginCredentialDto,
   LogoutResponseDto,
   OAuthAuthorizeResponseDto,
@@ -31,15 +18,9 @@ import {
 } from 'src/dtos/auth.dto';
 import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
 import { UserEntity } from 'src/entities/user.entity';
-import { Permission } from 'src/enum';
-import { IKeyRepository } from 'src/interfaces/api-key.interface';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import { IEventRepository } from 'src/interfaces/event.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { ISessionRepository } from 'src/interfaces/session.interface';
-import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
-import { IUserRepository } from 'src/interfaces/user.interface';
+import { AuthType, ImmichCookie, ImmichHeader, ImmichQuery, Permission } from 'src/enum';
+import { OAuthProfile } from 'src/interfaces/oauth.interface';
+import { BaseService } from 'src/services/base.service';
 import { isGranted } from 'src/utils/access';
 import { HumanReadableSize } from 'src/utils/bytes';
 
@@ -50,8 +31,6 @@ export interface LoginDetails {
   deviceOS: string;
 }
 
-type OAuthProfile = UserinfoResponse;
-
 interface ClaimOptions<T> {
   key: string;
   default: T;
@@ -70,29 +49,14 @@ export type ValidateRequest = {
 };
 
 @Injectable()
-export class AuthService {
-  private configCore: SystemConfigCore;
-  private userCore: UserCore;
-
-  constructor(
-    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
-    @Inject(IEventRepository) private eventRepository: IEventRepository,
-    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-    @Inject(IUserRepository) private userRepository: IUserRepository,
-    @Inject(ISessionRepository) private sessionRepository: ISessionRepository,
-    @Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
-    @Inject(IKeyRepository) private keyRepository: IKeyRepository,
-  ) {
-    this.logger.setContext(AuthService.name);
-    this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
-    this.userCore = UserCore.create(cryptoRepository, userRepository);
-
-    custom.setHttpOptionsDefaults({ timeout: 30_000 });
+export class AuthService extends BaseService {
+  @OnEvent({ name: 'app.bootstrap' })
+  onBootstrap() {
+    this.oauthRepository.init();
   }
 
   async login(dto: LoginCredentialDto, details: LoginDetails) {
-    const config = await this.configCore.getConfig({ withCache: false });
+    const config = await this.getConfig({ withCache: false });
     if (!config.passwordLogin.enabled) {
       throw new UnauthorizedException('Password login has been disabled');
     }
@@ -150,7 +114,7 @@ export class AuthService {
       throw new BadRequestException('The server already has an admin');
     }
 
-    const admin = await this.userCore.createUser({
+    const admin = await this.createUser({
       isAdmin: true,
       email: dto.email,
       name: dto.name,
@@ -211,25 +175,20 @@ export class AuthService {
   }
 
   async authorize(dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
-    const config = await this.configCore.getConfig({ withCache: false });
-    if (!config.oauth.enabled) {
+    const { oauth } = await this.getConfig({ withCache: false });
+
+    if (!oauth.enabled) {
       throw new BadRequestException('OAuth is not enabled');
     }
 
-    const client = await this.getOAuthClient(config);
-    const url = client.authorizationUrl({
-      redirect_uri: this.normalize(config, dto.redirectUri),
-      scope: config.oauth.scope,
-      state: generators.state(),
-    });
-
+    const url = await this.oauthRepository.authorize(oauth, this.resolveRedirectUri(oauth, dto.redirectUri));
     return { url };
   }
 
   async callback(dto: OAuthCallbackDto, loginDetails: LoginDetails) {
-    const config = await this.configCore.getConfig({ withCache: false });
-    const profile = await this.getOAuthProfile(config, dto.url);
-    const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim } = config.oauth;
+    const { oauth } = await this.getConfig({ withCache: false });
+    const profile = await this.oauthRepository.getProfile(oauth, dto.url, this.resolveRedirectUri(oauth, dto.url));
+    const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim } = oauth;
     this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
     let user = await this.userRepository.getByOAuthId(profile.sub);
 
@@ -267,11 +226,11 @@ export class AuthService {
       const storageQuota = this.getClaim(profile, {
         key: storageQuotaClaim,
         default: defaultStorageQuota,
-        isValid: (value: unknown) => isNumber(value) && value >= 0,
+        isValid: (value: unknown) => Number(value) >= 0,
       });
 
       const userName = profile.name ?? `${profile.given_name || ''} ${profile.family_name || ''}`;
-      user = await this.userCore.createUser({
+      user = await this.createUser({
         name: userName,
         email: profile.email,
         oauthId: profile.sub,
@@ -284,8 +243,12 @@ export class AuthService {
   }
 
   async link(auth: AuthDto, dto: OAuthCallbackDto): Promise<UserAdminResponseDto> {
-    const config = await this.configCore.getConfig({ withCache: false });
-    const { sub: oauthId } = await this.getOAuthProfile(config, dto.url);
+    const { oauth } = await this.getConfig({ withCache: false });
+    const { sub: oauthId } = await this.oauthRepository.getProfile(
+      oauth,
+      dto.url,
+      this.resolveRedirectUri(oauth, dto.url),
+    );
     const duplicate = await this.userRepository.getByOAuthId(oauthId);
     if (duplicate && duplicate.id !== auth.user.id) {
       this.logger.warn(`OAuth link account failed: sub is already linked to another user (${duplicate.email}).`);
@@ -306,65 +269,12 @@ export class AuthService {
       return LOGIN_URL;
     }
 
-    const config = await this.configCore.getConfig({ withCache: false });
+    const config = await this.getConfig({ withCache: false });
     if (!config.oauth.enabled) {
       return LOGIN_URL;
     }
 
-    const client = await this.getOAuthClient(config);
-    return client.issuer.metadata.end_session_endpoint || LOGIN_URL;
-  }
-
-  private async getOAuthProfile(config: SystemConfig, url: string): Promise<OAuthProfile> {
-    const redirectUri = this.normalize(config, url.split('?')[0]);
-    const client = await this.getOAuthClient(config);
-    const params = client.callbackParams(url);
-    try {
-      const tokens = await client.callback(redirectUri, params, { state: params.state });
-      return client.userinfo<OAuthProfile>(tokens.access_token || '');
-    } catch (error: Error | any) {
-      if (error.message.includes('unexpected JWT alg received')) {
-        this.logger.warn(
-          [
-            'Algorithm mismatch. Make sure the signing algorithm is set correctly in the OAuth settings.',
-            'Or, that you have specified a signing key in your OAuth provider.',
-          ].join(' '),
-        );
-      }
-
-      throw error;
-    }
-  }
-
-  private async getOAuthClient(config: SystemConfig) {
-    const { enabled, clientId, clientSecret, issuerUrl, signingAlgorithm, profileSigningAlgorithm } = config.oauth;
-
-    if (!enabled) {
-      throw new BadRequestException('OAuth2 is not enabled');
-    }
-
-    try {
-      const issuer = await Issuer.discover(issuerUrl);
-      return new issuer.Client({
-        client_id: clientId,
-        client_secret: clientSecret,
-        response_types: ['code'],
-        userinfo_signed_response_alg: profileSigningAlgorithm === 'none' ? undefined : profileSigningAlgorithm,
-        id_token_signed_response_alg: signingAlgorithm,
-      });
-    } catch (error: any | AggregateError) {
-      this.logger.error(`Error in OAuth discovery: ${error}`, error?.stack, error?.errors);
-      throw new InternalServerErrorException(`Error in OAuth discovery: ${error}`, { cause: error });
-    }
-  }
-
-  private normalize(config: SystemConfig, redirectUri: string) {
-    const isMobile = redirectUri.startsWith('app.immich:/');
-    const { mobileRedirectUri, mobileOverrideEnabled } = config.oauth;
-    if (isMobile && mobileOverrideEnabled && mobileRedirectUri) {
-      return mobileRedirectUri;
-    }
-    return redirectUri;
+    return (await this.oauthRepository.getLogoutEndpoint(config.oauth)) || LOGIN_URL;
   }
 
   private getBearerToken(headers: IncomingHttpHeaders): string | null {
@@ -448,4 +358,16 @@ export class AuthService {
     const value = profile[options.key as keyof OAuthProfile];
     return options.isValid(value) ? (value as T) : options.default;
   }
+
+  private resolveRedirectUri(
+    { mobileRedirectUri, mobileOverrideEnabled }: { mobileRedirectUri: string; mobileOverrideEnabled: boolean },
+    url: string,
+  ) {
+    const redirectUri = url.split('?')[0];
+    const isMobile = redirectUri.startsWith('app.immich:/');
+    if (isMobile && mobileOverrideEnabled && mobileRedirectUri) {
+      return mobileRedirectUri;
+    }
+    return redirectUri;
+  }
 }
diff --git a/server/src/services/backup.service.spec.ts b/server/src/services/backup.service.spec.ts
new file mode 100644
index 0000000000..41ba7c2153
--- /dev/null
+++ b/server/src/services/backup.service.spec.ts
@@ -0,0 +1,230 @@
+import { PassThrough } from 'node:stream';
+import { defaults, SystemConfig } from 'src/config';
+import { StorageCore } from 'src/cores/storage.core';
+import { ImmichWorker, StorageFolder } from 'src/enum';
+import { IConfigRepository } from 'src/interfaces/config.interface';
+import { ICronRepository } from 'src/interfaces/cron.interface';
+import { IDatabaseRepository } from 'src/interfaces/database.interface';
+import { JobStatus } from 'src/interfaces/job.interface';
+import { IProcessRepository } from 'src/interfaces/process.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { BackupService } from 'src/services/backup.service';
+import { systemConfigStub } from 'test/fixtures/system-config.stub';
+import { mockSpawn, newTestService } from 'test/utils';
+import { describe, Mocked } from 'vitest';
+
+describe(BackupService.name, () => {
+  let sut: BackupService;
+
+  let databaseMock: Mocked<IDatabaseRepository>;
+  let configMock: Mocked<IConfigRepository>;
+  let cronMock: Mocked<ICronRepository>;
+  let processMock: Mocked<IProcessRepository>;
+  let storageMock: Mocked<IStorageRepository>;
+  let systemMock: Mocked<ISystemMetadataRepository>;
+
+  beforeEach(() => {
+    ({ sut, cronMock, configMock, databaseMock, processMock, storageMock, systemMock } = newTestService(BackupService));
+  });
+
+  it('should work', () => {
+    expect(sut).toBeDefined();
+  });
+
+  describe('onBootstrapEvent', () => {
+    it('should init cron job and handle config changes', async () => {
+      databaseMock.tryLock.mockResolvedValue(true);
+
+      await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig });
+
+      expect(cronMock.create).toHaveBeenCalled();
+    });
+
+    it('should not initialize backup database cron job when lock is taken', async () => {
+      databaseMock.tryLock.mockResolvedValue(false);
+
+      await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig });
+
+      expect(cronMock.create).not.toHaveBeenCalled();
+    });
+
+    it('should not initialise backup database job when running on microservices', async () => {
+      configMock.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES);
+      await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig });
+
+      expect(cronMock.create).not.toHaveBeenCalled();
+    });
+  });
+
+  describe('onConfigUpdateEvent', () => {
+    beforeEach(async () => {
+      databaseMock.tryLock.mockResolvedValue(true);
+      await sut.onConfigInit({ newConfig: defaults });
+    });
+
+    it('should update cron job if backup is enabled', () => {
+      sut.onConfigUpdate({
+        oldConfig: defaults,
+        newConfig: {
+          backup: {
+            database: {
+              enabled: true,
+              cronExpression: '0 1 * * *',
+            },
+          },
+        } as SystemConfig,
+      });
+
+      expect(cronMock.update).toHaveBeenCalledWith({ name: 'backupDatabase', expression: '0 1 * * *', start: true });
+      expect(cronMock.update).toHaveBeenCalled();
+    });
+
+    it('should do nothing if instance does not have the backup database lock', async () => {
+      databaseMock.tryLock.mockResolvedValue(false);
+      await sut.onConfigInit({ newConfig: defaults });
+      sut.onConfigUpdate({ newConfig: systemConfigStub.backupEnabled as SystemConfig, oldConfig: defaults });
+      expect(cronMock.update).not.toHaveBeenCalled();
+    });
+  });
+
+  describe('cleanupDatabaseBackups', () => {
+    it('should do nothing if not reached keepLastAmount', async () => {
+      systemMock.get.mockResolvedValue(systemConfigStub.backupEnabled);
+      storageMock.readdir.mockResolvedValue(['immich-db-backup-1.sql.gz']);
+      await sut.cleanupDatabaseBackups();
+      expect(storageMock.unlink).not.toHaveBeenCalled();
+    });
+
+    it('should remove failed backup files', async () => {
+      systemMock.get.mockResolvedValue(systemConfigStub.backupEnabled);
+      storageMock.readdir.mockResolvedValue([
+        'immich-db-backup-123.sql.gz.tmp',
+        'immich-db-backup-234.sql.gz',
+        'immich-db-backup-345.sql.gz.tmp',
+      ]);
+      await sut.cleanupDatabaseBackups();
+      expect(storageMock.unlink).toHaveBeenCalledTimes(2);
+      expect(storageMock.unlink).toHaveBeenCalledWith(
+        `${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-123.sql.gz.tmp`,
+      );
+      expect(storageMock.unlink).toHaveBeenCalledWith(
+        `${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-345.sql.gz.tmp`,
+      );
+    });
+
+    it('should remove old backup files over keepLastAmount', async () => {
+      systemMock.get.mockResolvedValue(systemConfigStub.backupEnabled);
+      storageMock.readdir.mockResolvedValue(['immich-db-backup-1.sql.gz', 'immich-db-backup-2.sql.gz']);
+      await sut.cleanupDatabaseBackups();
+      expect(storageMock.unlink).toHaveBeenCalledTimes(1);
+      expect(storageMock.unlink).toHaveBeenCalledWith(
+        `${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-1.sql.gz`,
+      );
+    });
+
+    it('should remove old backup files over keepLastAmount and failed backups', async () => {
+      systemMock.get.mockResolvedValue(systemConfigStub.backupEnabled);
+      storageMock.readdir.mockResolvedValue([
+        'immich-db-backup-1.sql.gz.tmp',
+        'immich-db-backup-2.sql.gz',
+        'immich-db-backup-3.sql.gz',
+      ]);
+      await sut.cleanupDatabaseBackups();
+      expect(storageMock.unlink).toHaveBeenCalledTimes(2);
+      expect(storageMock.unlink).toHaveBeenCalledWith(
+        `${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-1.sql.gz.tmp`,
+      );
+      expect(storageMock.unlink).toHaveBeenCalledWith(
+        `${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-2.sql.gz`,
+      );
+    });
+  });
+
+  describe('handleBackupDatabase', () => {
+    beforeEach(() => {
+      storageMock.readdir.mockResolvedValue([]);
+      processMock.spawn.mockReturnValue(mockSpawn(0, 'data', ''));
+      storageMock.rename.mockResolvedValue();
+      storageMock.unlink.mockResolvedValue();
+      systemMock.get.mockResolvedValue(systemConfigStub.backupEnabled);
+      storageMock.createWriteStream.mockReturnValue(new PassThrough());
+    });
+    it('should run a database backup successfully', async () => {
+      const result = await sut.handleBackupDatabase();
+      expect(result).toBe(JobStatus.SUCCESS);
+      expect(storageMock.createWriteStream).toHaveBeenCalled();
+    });
+    it('should rename file on success', async () => {
+      const result = await sut.handleBackupDatabase();
+      expect(result).toBe(JobStatus.SUCCESS);
+      expect(storageMock.rename).toHaveBeenCalled();
+    });
+    it('should fail if pg_dumpall fails', async () => {
+      processMock.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
+      const result = await sut.handleBackupDatabase();
+      expect(result).toBe(JobStatus.FAILED);
+    });
+    it('should not rename file if pgdump fails and gzip succeeds', async () => {
+      processMock.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
+      const result = await sut.handleBackupDatabase();
+      expect(result).toBe(JobStatus.FAILED);
+      expect(storageMock.rename).not.toHaveBeenCalled();
+    });
+    it('should fail if gzip fails', async () => {
+      processMock.spawn.mockReturnValueOnce(mockSpawn(0, 'data', ''));
+      processMock.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
+      const result = await sut.handleBackupDatabase();
+      expect(result).toBe(JobStatus.FAILED);
+    });
+    it('should fail if write stream fails', async () => {
+      storageMock.createWriteStream.mockImplementation(() => {
+        throw new Error('error');
+      });
+      const result = await sut.handleBackupDatabase();
+      expect(result).toBe(JobStatus.FAILED);
+    });
+    it('should fail if rename fails', async () => {
+      storageMock.rename.mockRejectedValue(new Error('error'));
+      const result = await sut.handleBackupDatabase();
+      expect(result).toBe(JobStatus.FAILED);
+    });
+    it('should ignore unlink failing and still return failed job status', async () => {
+      processMock.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
+      storageMock.unlink.mockRejectedValue(new Error('error'));
+      const result = await sut.handleBackupDatabase();
+      expect(storageMock.unlink).toHaveBeenCalled();
+      expect(result).toBe(JobStatus.FAILED);
+    });
+    it.each`
+      postgresVersion                       | expectedVersion
+      ${'14.10'}                            | ${14}
+      ${'14.10.3'}                          | ${14}
+      ${'14.10 (Debian 14.10-1.pgdg120+1)'} | ${14}
+      ${'15.3.3'}                           | ${15}
+      ${'16.4.2'}                           | ${16}
+      ${'17.15.1'}                          | ${17}
+    `(
+      `should use pg_dumpall $expectedVersion with postgres version $postgresVersion`,
+      async ({ postgresVersion, expectedVersion }) => {
+        databaseMock.getPostgresVersion.mockResolvedValue(postgresVersion);
+        await sut.handleBackupDatabase();
+        expect(processMock.spawn).toHaveBeenCalledWith(
+          `/usr/lib/postgresql/${expectedVersion}/bin/pg_dumpall`,
+          expect.any(Array),
+          expect.any(Object),
+        );
+      },
+    );
+    it.each`
+      postgresVersion
+      ${'13.99.99'}
+      ${'18.0.0'}
+    `(`should fail if postgres version $postgresVersion is not supported`, async ({ postgresVersion }) => {
+      databaseMock.getPostgresVersion.mockResolvedValue(postgresVersion);
+      const result = await sut.handleBackupDatabase();
+      expect(processMock.spawn).not.toHaveBeenCalled();
+      expect(result).toBe(JobStatus.FAILED);
+    });
+  });
+});
diff --git a/server/src/services/backup.service.ts b/server/src/services/backup.service.ts
new file mode 100644
index 0000000000..b3bc1dd8d1
--- /dev/null
+++ b/server/src/services/backup.service.ts
@@ -0,0 +1,188 @@
+import { Injectable } from '@nestjs/common';
+import { default as path } from 'node:path';
+import semver from 'semver';
+import { StorageCore } from 'src/cores/storage.core';
+import { OnEvent, OnJob } from 'src/decorators';
+import { ImmichWorker, StorageFolder } from 'src/enum';
+import { DatabaseLock } from 'src/interfaces/database.interface';
+import { ArgOf } from 'src/interfaces/event.interface';
+import { JobName, JobStatus, QueueName } from 'src/interfaces/job.interface';
+import { BaseService } from 'src/services/base.service';
+import { handlePromiseError } from 'src/utils/misc';
+
+@Injectable()
+export class BackupService extends BaseService {
+  private backupLock = false;
+
+  @OnEvent({ name: 'config.init', workers: [ImmichWorker.MICROSERVICES] })
+  async onConfigInit({
+    newConfig: {
+      backup: { database },
+    },
+  }: ArgOf<'config.init'>) {
+    this.backupLock = await this.databaseRepository.tryLock(DatabaseLock.BackupDatabase);
+
+    if (this.backupLock) {
+      this.cronRepository.create({
+        name: 'backupDatabase',
+        expression: database.cronExpression,
+        onTick: () => handlePromiseError(this.jobRepository.queue({ name: JobName.BACKUP_DATABASE }), this.logger),
+        start: database.enabled,
+      });
+    }
+  }
+
+  @OnEvent({ name: 'config.update', server: true })
+  onConfigUpdate({ newConfig: { backup } }: ArgOf<'config.update'>) {
+    if (!this.backupLock) {
+      return;
+    }
+
+    this.cronRepository.update({
+      name: 'backupDatabase',
+      expression: backup.database.cronExpression,
+      start: backup.database.enabled,
+    });
+  }
+
+  async cleanupDatabaseBackups() {
+    this.logger.debug(`Database Backup Cleanup Started`);
+    const {
+      backup: { database: config },
+    } = await this.getConfig({ withCache: false });
+
+    const backupsFolder = StorageCore.getBaseFolder(StorageFolder.BACKUPS);
+    const files = await this.storageRepository.readdir(backupsFolder);
+    const failedBackups = files.filter((file) => file.match(/immich-db-backup-\d+\.sql\.gz\.tmp$/));
+    const backups = files
+      .filter((file) => file.match(/immich-db-backup-\d+\.sql\.gz$/))
+      .sort()
+      .reverse();
+
+    const toDelete = backups.slice(config.keepLastAmount);
+    toDelete.push(...failedBackups);
+
+    for (const file of toDelete) {
+      await this.storageRepository.unlink(path.join(backupsFolder, file));
+    }
+    this.logger.debug(`Database Backup Cleanup Finished, deleted ${toDelete.length} backups`);
+  }
+
+  @OnJob({ name: JobName.BACKUP_DATABASE, queue: QueueName.BACKUP_DATABASE })
+  async handleBackupDatabase(): Promise<JobStatus> {
+    this.logger.debug(`Database Backup Started`);
+
+    const {
+      database: { config },
+    } = this.configRepository.getEnv();
+
+    const isUrlConnection = config.connectionType === 'url';
+
+    const databaseParams = isUrlConnection
+      ? ['--dbname', config.url]
+      : [
+          '--username',
+          config.username,
+          '--host',
+          config.host,
+          '--port',
+          `${config.port}`,
+          '--database',
+          config.database,
+        ];
+
+    databaseParams.push('--clean', '--if-exists');
+
+    const backupFilePath = path.join(
+      StorageCore.getBaseFolder(StorageFolder.BACKUPS),
+      `immich-db-backup-${Date.now()}.sql.gz.tmp`,
+    );
+
+    const databaseVersion = await this.databaseRepository.getPostgresVersion();
+    const databaseSemver = semver.coerce(databaseVersion);
+    const databaseMajorVersion = databaseSemver?.major;
+
+    if (!databaseMajorVersion || !databaseSemver || !semver.satisfies(databaseSemver, '>=14.0.0 <18.0.0')) {
+      this.logger.error(`Database Backup Failure: Unsupported PostgreSQL version: ${databaseVersion}`);
+      return JobStatus.FAILED;
+    }
+
+    this.logger.log(`Database Backup Starting. Database Version: ${databaseMajorVersion}`);
+
+    try {
+      await new Promise<void>((resolve, reject) => {
+        const pgdump = this.processRepository.spawn(
+          `/usr/lib/postgresql/${databaseMajorVersion}/bin/pg_dumpall`,
+          databaseParams,
+          {
+            env: {
+              PATH: process.env.PATH,
+              PGPASSWORD: isUrlConnection ? undefined : config.password,
+            },
+          },
+        );
+
+        // NOTE: `--rsyncable` is only supported in GNU gzip
+        const gzip = this.processRepository.spawn(`gzip`, ['--rsyncable']);
+        pgdump.stdout.pipe(gzip.stdin);
+
+        const fileStream = this.storageRepository.createWriteStream(backupFilePath);
+
+        gzip.stdout.pipe(fileStream);
+
+        pgdump.on('error', (err) => {
+          this.logger.error('Backup failed with error', err);
+          reject(err);
+        });
+
+        gzip.on('error', (err) => {
+          this.logger.error('Gzip failed with error', err);
+          reject(err);
+        });
+
+        let pgdumpLogs = '';
+        let gzipLogs = '';
+
+        pgdump.stderr.on('data', (data) => (pgdumpLogs += data));
+        gzip.stderr.on('data', (data) => (gzipLogs += data));
+
+        pgdump.on('exit', (code) => {
+          if (code !== 0) {
+            this.logger.error(`Backup failed with code ${code}`);
+            reject(`Backup failed with code ${code}`);
+            this.logger.error(pgdumpLogs);
+            return;
+          }
+          if (pgdumpLogs) {
+            this.logger.debug(`pgdump_all logs\n${pgdumpLogs}`);
+          }
+        });
+
+        gzip.on('exit', (code) => {
+          if (code !== 0) {
+            this.logger.error(`Gzip failed with code ${code}`);
+            reject(`Gzip failed with code ${code}`);
+            this.logger.error(gzipLogs);
+            return;
+          }
+          if (pgdump.exitCode !== 0) {
+            this.logger.error(`Gzip exited with code 0 but pgdump exited with ${pgdump.exitCode}`);
+            return;
+          }
+          resolve();
+        });
+      });
+      await this.storageRepository.rename(backupFilePath, backupFilePath.replace('.tmp', ''));
+    } catch (error) {
+      this.logger.error('Database Backup Failure', error);
+      await this.storageRepository
+        .unlink(backupFilePath)
+        .catch((error) => this.logger.error('Failed to delete failed backup file', error));
+      return JobStatus.FAILED;
+    }
+
+    this.logger.log(`Database Backup Success`);
+    await this.cleanupDatabaseBackups();
+    return JobStatus.SUCCESS;
+  }
+}
diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts
new file mode 100644
index 0000000000..3630d69c18
--- /dev/null
+++ b/server/src/services/base.service.ts
@@ -0,0 +1,157 @@
+import { BadRequestException, Inject } from '@nestjs/common';
+import sanitize from 'sanitize-filename';
+import { SystemConfig } from 'src/config';
+import { SALT_ROUNDS } from 'src/constants';
+import { StorageCore } from 'src/cores/storage.core';
+import { UserEntity } from 'src/entities/user.entity';
+import { IAccessRepository } from 'src/interfaces/access.interface';
+import { IActivityRepository } from 'src/interfaces/activity.interface';
+import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
+import { IAlbumRepository } from 'src/interfaces/album.interface';
+import { IKeyRepository } from 'src/interfaces/api-key.interface';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { IAuditRepository } from 'src/interfaces/audit.interface';
+import { IConfigRepository } from 'src/interfaces/config.interface';
+import { ICronRepository } from 'src/interfaces/cron.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { IDatabaseRepository } from 'src/interfaces/database.interface';
+import { IEventRepository } from 'src/interfaces/event.interface';
+import { IJobRepository } from 'src/interfaces/job.interface';
+import { ILibraryRepository } from 'src/interfaces/library.interface';
+import { ILoggerRepository } from 'src/interfaces/logger.interface';
+import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
+import { IMapRepository } from 'src/interfaces/map.interface';
+import { IMediaRepository } from 'src/interfaces/media.interface';
+import { IMemoryRepository } from 'src/interfaces/memory.interface';
+import { IMetadataRepository } from 'src/interfaces/metadata.interface';
+import { IMoveRepository } from 'src/interfaces/move.interface';
+import { INotificationRepository } from 'src/interfaces/notification.interface';
+import { IOAuthRepository } from 'src/interfaces/oauth.interface';
+import { IPartnerRepository } from 'src/interfaces/partner.interface';
+import { IPersonRepository } from 'src/interfaces/person.interface';
+import { IProcessRepository } from 'src/interfaces/process.interface';
+import { ISearchRepository } from 'src/interfaces/search.interface';
+import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
+import { ISessionRepository } from 'src/interfaces/session.interface';
+import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
+import { IStackRepository } from 'src/interfaces/stack.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { ITagRepository } from 'src/interfaces/tag.interface';
+import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
+import { ITrashRepository } from 'src/interfaces/trash.interface';
+import { IUserRepository } from 'src/interfaces/user.interface';
+import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
+import { IViewRepository } from 'src/interfaces/view.interface';
+import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access';
+import { getConfig, updateConfig } from 'src/utils/config';
+
+export class BaseService {
+  protected storageCore: StorageCore;
+
+  constructor(
+    @Inject(ILoggerRepository) protected logger: ILoggerRepository,
+    @Inject(IAccessRepository) protected accessRepository: IAccessRepository,
+    @Inject(IActivityRepository) protected activityRepository: IActivityRepository,
+    @Inject(IAuditRepository) protected auditRepository: IAuditRepository,
+    @Inject(IAlbumRepository) protected albumRepository: IAlbumRepository,
+    @Inject(IAlbumUserRepository) protected albumUserRepository: IAlbumUserRepository,
+    @Inject(IAssetRepository) protected assetRepository: IAssetRepository,
+    @Inject(IConfigRepository) protected configRepository: IConfigRepository,
+    @Inject(ICronRepository) protected cronRepository: ICronRepository,
+    @Inject(ICryptoRepository) protected cryptoRepository: ICryptoRepository,
+    @Inject(IDatabaseRepository) protected databaseRepository: IDatabaseRepository,
+    @Inject(IEventRepository) protected eventRepository: IEventRepository,
+    @Inject(IJobRepository) protected jobRepository: IJobRepository,
+    @Inject(IKeyRepository) protected keyRepository: IKeyRepository,
+    @Inject(ILibraryRepository) protected libraryRepository: ILibraryRepository,
+    @Inject(IMachineLearningRepository) protected machineLearningRepository: IMachineLearningRepository,
+    @Inject(IMapRepository) protected mapRepository: IMapRepository,
+    @Inject(IMediaRepository) protected mediaRepository: IMediaRepository,
+    @Inject(IMemoryRepository) protected memoryRepository: IMemoryRepository,
+    @Inject(IMetadataRepository) protected metadataRepository: IMetadataRepository,
+    @Inject(IMoveRepository) protected moveRepository: IMoveRepository,
+    @Inject(INotificationRepository) protected notificationRepository: INotificationRepository,
+    @Inject(IOAuthRepository) protected oauthRepository: IOAuthRepository,
+    @Inject(IPartnerRepository) protected partnerRepository: IPartnerRepository,
+    @Inject(IPersonRepository) protected personRepository: IPersonRepository,
+    @Inject(IProcessRepository) protected processRepository: IProcessRepository,
+    @Inject(ISearchRepository) protected searchRepository: ISearchRepository,
+    @Inject(IServerInfoRepository) protected serverInfoRepository: IServerInfoRepository,
+    @Inject(ISessionRepository) protected sessionRepository: ISessionRepository,
+    @Inject(ISharedLinkRepository) protected sharedLinkRepository: ISharedLinkRepository,
+    @Inject(IStackRepository) protected stackRepository: IStackRepository,
+    @Inject(IStorageRepository) protected storageRepository: IStorageRepository,
+    @Inject(ISystemMetadataRepository) protected systemMetadataRepository: ISystemMetadataRepository,
+    @Inject(ITagRepository) protected tagRepository: ITagRepository,
+    @Inject(ITelemetryRepository) protected telemetryRepository: ITelemetryRepository,
+    @Inject(ITrashRepository) protected trashRepository: ITrashRepository,
+    @Inject(IUserRepository) protected userRepository: IUserRepository,
+    @Inject(IVersionHistoryRepository) protected versionRepository: IVersionHistoryRepository,
+    @Inject(IViewRepository) protected viewRepository: IViewRepository,
+  ) {
+    this.logger.setContext(this.constructor.name);
+    this.storageCore = StorageCore.create(
+      assetRepository,
+      configRepository,
+      cryptoRepository,
+      moveRepository,
+      personRepository,
+      storageRepository,
+      systemMetadataRepository,
+      this.logger,
+    );
+  }
+
+  get worker() {
+    return this.configRepository.getWorker();
+  }
+
+  private get configRepos() {
+    return {
+      configRepo: this.configRepository,
+      metadataRepo: this.systemMetadataRepository,
+      logger: this.logger,
+    };
+  }
+
+  getConfig(options: { withCache: boolean }) {
+    return getConfig(this.configRepos, options);
+  }
+
+  updateConfig(newConfig: SystemConfig) {
+    return updateConfig(this.configRepos, newConfig);
+  }
+
+  requireAccess(request: AccessRequest) {
+    return requireAccess(this.accessRepository, request);
+  }
+
+  checkAccess(request: AccessRequest) {
+    return checkAccess(this.accessRepository, request);
+  }
+
+  async createUser(dto: Partial<UserEntity> & { email: string }): Promise<UserEntity> {
+    const user = await this.userRepository.getByEmail(dto.email);
+    if (user) {
+      throw new BadRequestException('User exists');
+    }
+
+    if (!dto.isAdmin) {
+      const localAdmin = await this.userRepository.getAdmin();
+      if (!localAdmin) {
+        throw new BadRequestException('The first registered account must the administrator.');
+      }
+    }
+
+    const payload: Partial<UserEntity> = { ...dto };
+    if (payload.password) {
+      payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS);
+    }
+    if (payload.storageLabel) {
+      payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', ''));
+    }
+
+    return this.userRepository.create(payload);
+  }
+}
diff --git a/server/src/services/cli.service.spec.ts b/server/src/services/cli.service.spec.ts
index e52c648664..ef520070ea 100644
--- a/server/src/services/cli.service.spec.ts
+++ b/server/src/services/cli.service.spec.ts
@@ -1,30 +1,26 @@
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { IUserRepository } from 'src/interfaces/user.interface';
 import { CliService } from 'src/services/cli.service';
 import { userStub } from 'test/fixtures/user.stub';
-import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
-import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked, describe, it } from 'vitest';
 
 describe(CliService.name, () => {
   let sut: CliService;
 
   let userMock: Mocked<IUserRepository>;
-  let cryptoMock: Mocked<ICryptoRepository>;
   let systemMock: Mocked<ISystemMetadataRepository>;
-  let loggerMock: Mocked<ILoggerRepository>;
 
   beforeEach(() => {
-    cryptoMock = newCryptoRepositoryMock();
-    systemMock = newSystemMetadataRepositoryMock();
-    userMock = newUserRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
+    ({ sut, userMock, systemMock } = newTestService(CliService));
+  });
 
-    sut = new CliService(cryptoMock, systemMock, userMock, loggerMock);
+  describe('listUsers', () => {
+    it('should list users', async () => {
+      userMock.getList.mockResolvedValue([userStub.admin]);
+      await expect(sut.listUsers()).resolves.toEqual([expect.objectContaining({ isAdmin: true })]);
+      expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: true });
+    });
   });
 
   describe('resetAdminPassword', () => {
@@ -65,4 +61,32 @@ describe(CliService.name, () => {
       expect(update.password).toBeDefined();
     });
   });
+
+  describe('disablePasswordLogin', () => {
+    it('should disable password login', async () => {
+      await sut.disablePasswordLogin();
+      expect(systemMock.set).toHaveBeenCalledWith('system-config', { passwordLogin: { enabled: false } });
+    });
+  });
+
+  describe('enablePasswordLogin', () => {
+    it('should enable password login', async () => {
+      await sut.enablePasswordLogin();
+      expect(systemMock.set).toHaveBeenCalledWith('system-config', {});
+    });
+  });
+
+  describe('disableOAuthLogin', () => {
+    it('should disable oauth login', async () => {
+      await sut.disableOAuthLogin();
+      expect(systemMock.set).toHaveBeenCalledWith('system-config', {});
+    });
+  });
+
+  describe('enableOAuthLogin', () => {
+    it('should enable oauth login', async () => {
+      await sut.enableOAuthLogin();
+      expect(systemMock.set).toHaveBeenCalledWith('system-config', { oauth: { enabled: true } });
+    });
+  });
 });
diff --git a/server/src/services/cli.service.ts b/server/src/services/cli.service.ts
index 1c25c306b6..18a79108c4 100644
--- a/server/src/services/cli.service.ts
+++ b/server/src/services/cli.service.ts
@@ -1,26 +1,10 @@
-import { Inject, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
 import { SALT_ROUNDS } from 'src/constants';
-import { SystemConfigCore } from 'src/cores/system-config.core';
 import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
-import { IUserRepository } from 'src/interfaces/user.interface';
+import { BaseService } from 'src/services/base.service';
 
 @Injectable()
-export class CliService {
-  private configCore: SystemConfigCore;
-
-  constructor(
-    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
-    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
-    @Inject(IUserRepository) private userRepository: IUserRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-  ) {
-    this.logger.setContext(CliService.name);
-    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
-  }
-
+export class CliService extends BaseService {
   async listUsers(): Promise<UserAdminResponseDto[]> {
     const users = await this.userRepository.getList({ withDeleted: true });
     return users.map((user) => mapUserAdmin(user));
@@ -42,26 +26,26 @@ export class CliService {
   }
 
   async disablePasswordLogin(): Promise<void> {
-    const config = await this.configCore.getConfig({ withCache: false });
+    const config = await this.getConfig({ withCache: false });
     config.passwordLogin.enabled = false;
-    await this.configCore.updateConfig(config);
+    await this.updateConfig(config);
   }
 
   async enablePasswordLogin(): Promise<void> {
-    const config = await this.configCore.getConfig({ withCache: false });
+    const config = await this.getConfig({ withCache: false });
     config.passwordLogin.enabled = true;
-    await this.configCore.updateConfig(config);
+    await this.updateConfig(config);
   }
 
   async disableOAuthLogin(): Promise<void> {
-    const config = await this.configCore.getConfig({ withCache: false });
+    const config = await this.getConfig({ withCache: false });
     config.oauth.enabled = false;
-    await this.configCore.updateConfig(config);
+    await this.updateConfig(config);
   }
 
   async enableOAuthLogin(): Promise<void> {
-    const config = await this.configCore.getConfig({ withCache: false });
+    const config = await this.getConfig({ withCache: false });
     config.oauth.enabled = true;
-    await this.configCore.updateConfig(config);
+    await this.updateConfig(config);
   }
 }
diff --git a/server/src/services/database.service.spec.ts b/server/src/services/database.service.spec.ts
index c63428560e..958fb158a0 100644
--- a/server/src/services/database.service.spec.ts
+++ b/server/src/services/database.service.spec.ts
@@ -1,12 +1,20 @@
-import { DatabaseExtension, EXTENSION_NAMES, IDatabaseRepository } from 'src/interfaces/database.interface';
+import { IConfigRepository } from 'src/interfaces/config.interface';
+import {
+  DatabaseExtension,
+  EXTENSION_NAMES,
+  IDatabaseRepository,
+  VectorExtension,
+} from 'src/interfaces/database.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { DatabaseService } from 'src/services/database.service';
-import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
+import { mockEnvData } from 'test/repositories/config.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(DatabaseService.name, () => {
   let sut: DatabaseService;
+
+  let configMock: Mocked<IConfigRepository>;
   let databaseMock: Mocked<IDatabaseRepository>;
   let loggerMock: Mocked<ILoggerRepository>;
   let extensionRange: string;
@@ -16,9 +24,7 @@ describe(DatabaseService.name, () => {
   let versionAboveRange: string;
 
   beforeEach(() => {
-    databaseMock = newDatabaseRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-    sut = new DatabaseService(databaseMock, loggerMock);
+    ({ sut, configMock, databaseMock, loggerMock } = newTestService(DatabaseService));
 
     extensionRange = '0.2.x';
     databaseMock.getExtensionVersionRange.mockReturnValue(extensionRange);
@@ -33,255 +39,376 @@ describe(DatabaseService.name, () => {
     });
   });
 
-  afterEach(() => {
-    delete process.env.DB_SKIP_MIGRATIONS;
-    delete process.env.DB_VECTOR_EXTENSION;
-  });
-
   it('should work', () => {
     expect(sut).toBeDefined();
   });
 
-  it('should throw an error if PostgreSQL version is below minimum supported version', async () => {
-    databaseMock.getPostgresVersion.mockResolvedValueOnce('13.10.0');
+  describe('onBootstrap', () => {
+    it('should throw an error if PostgreSQL version is below minimum supported version', async () => {
+      databaseMock.getPostgresVersion.mockResolvedValueOnce('13.10.0');
 
-    await expect(sut.onBootstrap()).rejects.toThrow('Invalid PostgreSQL version. Found 13.10.0');
+      await expect(sut.onBootstrap()).rejects.toThrow('Invalid PostgreSQL version. Found 13.10.0');
 
-    expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1);
-  });
-
-  describe.each([
-    { extension: DatabaseExtension.VECTOR, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTOR] },
-    { extension: DatabaseExtension.VECTORS, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORS] },
-  ])('should work with $extensionName', ({ extension, extensionName }) => {
-    beforeEach(() => {
-      process.env.DB_VECTOR_EXTENSION = extensionName;
+      expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1);
     });
 
-    it(`should start up successfully with ${extension}`, async () => {
-      databaseMock.getPostgresVersion.mockResolvedValue('14.0.0');
-      databaseMock.getExtensionVersion.mockResolvedValue({
-        installedVersion: null,
-        availableVersion: minVersionInRange,
+    describe.each(<Array<{ extension: VectorExtension; extensionName: string }>>[
+      { extension: DatabaseExtension.VECTOR, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTOR] },
+      { extension: DatabaseExtension.VECTORS, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORS] },
+    ])('should work with $extensionName', ({ extension, extensionName }) => {
+      beforeEach(() => {
+        configMock.getEnv.mockReturnValue(
+          mockEnvData({
+            database: {
+              config: {
+                connectionType: 'parts',
+                type: 'postgres',
+                host: 'database',
+                port: 5432,
+                username: 'postgres',
+                password: 'postgres',
+                database: 'immich',
+              },
+              skipMigrations: false,
+              vectorExtension: extension,
+            },
+          }),
+        );
       });
 
-      await expect(sut.onBootstrap()).resolves.toBeUndefined();
+      it(`should start up successfully with ${extension}`, async () => {
+        databaseMock.getPostgresVersion.mockResolvedValue('14.0.0');
+        databaseMock.getExtensionVersion.mockResolvedValue({
+          installedVersion: null,
+          availableVersion: minVersionInRange,
+        });
 
-      expect(databaseMock.getPostgresVersion).toHaveBeenCalled();
-      expect(databaseMock.createExtension).toHaveBeenCalledWith(extension);
-      expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
-      expect(databaseMock.getExtensionVersion).toHaveBeenCalled();
-      expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
-      expect(loggerMock.fatal).not.toHaveBeenCalled();
-    });
+        await expect(sut.onBootstrap()).resolves.toBeUndefined();
 
-    it(`should throw an error if the ${extension} extension is not installed`, async () => {
-      databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: null, availableVersion: null });
-      const message = `The ${extensionName} extension is not available in this Postgres instance.
+        expect(databaseMock.getPostgresVersion).toHaveBeenCalled();
+        expect(databaseMock.createExtension).toHaveBeenCalledWith(extension);
+        expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
+        expect(databaseMock.getExtensionVersion).toHaveBeenCalled();
+        expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
+        expect(loggerMock.fatal).not.toHaveBeenCalled();
+      });
+
+      it(`should throw an error if the ${extension} extension is not installed`, async () => {
+        databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: null, availableVersion: null });
+        const message = `The ${extensionName} extension is not available in this Postgres instance.
     If using a container image, ensure the image has the extension installed.`;
-      await expect(sut.onBootstrap()).rejects.toThrow(message);
+        await expect(sut.onBootstrap()).rejects.toThrow(message);
 
-      expect(databaseMock.createExtension).not.toHaveBeenCalled();
-      expect(databaseMock.runMigrations).not.toHaveBeenCalled();
-    });
-
-    it(`should throw an error if the ${extension} extension version is below minimum supported version`, async () => {
-      databaseMock.getExtensionVersion.mockResolvedValue({
-        installedVersion: versionBelowRange,
-        availableVersion: versionBelowRange,
+        expect(databaseMock.createExtension).not.toHaveBeenCalled();
+        expect(databaseMock.runMigrations).not.toHaveBeenCalled();
       });
 
-      await expect(sut.onBootstrap()).rejects.toThrow(
-        `The ${extensionName} extension version is ${versionBelowRange}, but Immich only supports ${extensionRange}`,
-      );
+      it(`should throw an error if the ${extension} extension version is below minimum supported version`, async () => {
+        databaseMock.getExtensionVersion.mockResolvedValue({
+          installedVersion: versionBelowRange,
+          availableVersion: versionBelowRange,
+        });
 
-      expect(databaseMock.runMigrations).not.toHaveBeenCalled();
-    });
+        await expect(sut.onBootstrap()).rejects.toThrow(
+          `The ${extensionName} extension version is ${versionBelowRange}, but Immich only supports ${extensionRange}`,
+        );
 
-    it(`should throw an error if ${extension} extension version is a nightly`, async () => {
-      databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: '0.0.0', availableVersion: '0.0.0' });
-
-      await expect(sut.onBootstrap()).rejects.toThrow(
-        `The ${extensionName} extension version is 0.0.0, which means it is a nightly release.`,
-      );
-
-      expect(databaseMock.createExtension).not.toHaveBeenCalled();
-      expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
-      expect(databaseMock.runMigrations).not.toHaveBeenCalled();
-    });
-
-    it(`should do in-range update for ${extension} extension`, async () => {
-      databaseMock.getExtensionVersion.mockResolvedValue({
-        availableVersion: updateInRange,
-        installedVersion: minVersionInRange,
-      });
-      databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
-
-      await expect(sut.onBootstrap()).resolves.toBeUndefined();
-
-      expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
-      expect(databaseMock.updateVectorExtension).toHaveBeenCalledTimes(1);
-      expect(databaseMock.getExtensionVersion).toHaveBeenCalled();
-      expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
-      expect(loggerMock.fatal).not.toHaveBeenCalled();
-    });
-
-    it(`should not upgrade ${extension} if same version`, async () => {
-      databaseMock.getExtensionVersion.mockResolvedValue({
-        availableVersion: minVersionInRange,
-        installedVersion: minVersionInRange,
+        expect(databaseMock.runMigrations).not.toHaveBeenCalled();
       });
 
-      await expect(sut.onBootstrap()).resolves.toBeUndefined();
+      it(`should throw an error if ${extension} extension version is a nightly`, async () => {
+        databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: '0.0.0', availableVersion: '0.0.0' });
 
-      expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
-      expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
-      expect(loggerMock.fatal).not.toHaveBeenCalled();
-    });
+        await expect(sut.onBootstrap()).rejects.toThrow(
+          `The ${extensionName} extension version is 0.0.0, which means it is a nightly release.`,
+        );
 
-    it(`should throw error if ${extension} available version is below range`, async () => {
-      databaseMock.getExtensionVersion.mockResolvedValue({
-        availableVersion: versionBelowRange,
-        installedVersion: null,
+        expect(databaseMock.createExtension).not.toHaveBeenCalled();
+        expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
+        expect(databaseMock.runMigrations).not.toHaveBeenCalled();
       });
 
-      await expect(sut.onBootstrap()).rejects.toThrow();
+      it(`should do in-range update for ${extension} extension`, async () => {
+        databaseMock.getExtensionVersion.mockResolvedValue({
+          availableVersion: updateInRange,
+          installedVersion: minVersionInRange,
+        });
+        databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
 
-      expect(databaseMock.createExtension).not.toHaveBeenCalled();
-      expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
-      expect(databaseMock.runMigrations).not.toHaveBeenCalled();
-      expect(loggerMock.fatal).not.toHaveBeenCalled();
-    });
+        await expect(sut.onBootstrap()).resolves.toBeUndefined();
 
-    it(`should throw error if ${extension} available version is above range`, async () => {
-      databaseMock.getExtensionVersion.mockResolvedValue({
-        availableVersion: versionAboveRange,
-        installedVersion: minVersionInRange,
+        expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
+        expect(databaseMock.updateVectorExtension).toHaveBeenCalledTimes(1);
+        expect(databaseMock.getExtensionVersion).toHaveBeenCalled();
+        expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
+        expect(loggerMock.fatal).not.toHaveBeenCalled();
       });
 
-      await expect(sut.onBootstrap()).rejects.toThrow();
+      it(`should not upgrade ${extension} if same version`, async () => {
+        databaseMock.getExtensionVersion.mockResolvedValue({
+          availableVersion: minVersionInRange,
+          installedVersion: minVersionInRange,
+        });
 
-      expect(databaseMock.createExtension).not.toHaveBeenCalled();
-      expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
-      expect(databaseMock.runMigrations).not.toHaveBeenCalled();
-      expect(loggerMock.fatal).not.toHaveBeenCalled();
-    });
+        await expect(sut.onBootstrap()).resolves.toBeUndefined();
 
-    it('should throw error if available version is below installed version', async () => {
-      databaseMock.getExtensionVersion.mockResolvedValue({
-        availableVersion: minVersionInRange,
-        installedVersion: updateInRange,
+        expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
+        expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
+        expect(loggerMock.fatal).not.toHaveBeenCalled();
       });
 
-      await expect(sut.onBootstrap()).rejects.toThrow(
-        `The database currently has ${extensionName} ${updateInRange} activated, but the Postgres instance only has ${minVersionInRange} available.`,
-      );
+      it(`should throw error if ${extension} available version is below range`, async () => {
+        databaseMock.getExtensionVersion.mockResolvedValue({
+          availableVersion: versionBelowRange,
+          installedVersion: null,
+        });
 
-      expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
-      expect(databaseMock.runMigrations).not.toHaveBeenCalled();
-      expect(loggerMock.fatal).not.toHaveBeenCalled();
-    });
+        await expect(sut.onBootstrap()).rejects.toThrow();
 
-    it(`should raise error if ${extension} extension upgrade failed`, async () => {
-      databaseMock.getExtensionVersion.mockResolvedValue({
-        availableVersion: updateInRange,
-        installedVersion: minVersionInRange,
+        expect(databaseMock.createExtension).not.toHaveBeenCalled();
+        expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
+        expect(databaseMock.runMigrations).not.toHaveBeenCalled();
+        expect(loggerMock.fatal).not.toHaveBeenCalled();
       });
-      databaseMock.updateVectorExtension.mockRejectedValue(new Error('Failed to update extension'));
 
-      await expect(sut.onBootstrap()).rejects.toThrow('Failed to update extension');
+      it(`should throw error if ${extension} available version is above range`, async () => {
+        databaseMock.getExtensionVersion.mockResolvedValue({
+          availableVersion: versionAboveRange,
+          installedVersion: minVersionInRange,
+        });
 
-      expect(loggerMock.warn.mock.calls[0][0]).toContain(
-        `The ${extensionName} extension can be updated to ${updateInRange}.`,
-      );
-      expect(loggerMock.fatal).not.toHaveBeenCalled();
-      expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
-      expect(databaseMock.runMigrations).not.toHaveBeenCalled();
-    });
+        await expect(sut.onBootstrap()).rejects.toThrow();
 
-    it(`should warn if ${extension} extension update requires restart`, async () => {
-      databaseMock.getExtensionVersion.mockResolvedValue({
-        availableVersion: updateInRange,
-        installedVersion: minVersionInRange,
+        expect(databaseMock.createExtension).not.toHaveBeenCalled();
+        expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
+        expect(databaseMock.runMigrations).not.toHaveBeenCalled();
+        expect(loggerMock.fatal).not.toHaveBeenCalled();
       });
-      databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: true });
 
-      await expect(sut.onBootstrap()).resolves.toBeUndefined();
+      it('should throw error if available version is below installed version', async () => {
+        databaseMock.getExtensionVersion.mockResolvedValue({
+          availableVersion: minVersionInRange,
+          installedVersion: updateInRange,
+        });
 
-      expect(loggerMock.warn).toHaveBeenCalledTimes(1);
-      expect(loggerMock.warn.mock.calls[0][0]).toContain(extensionName);
-      expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
-      expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
-      expect(loggerMock.fatal).not.toHaveBeenCalled();
-    });
+        await expect(sut.onBootstrap()).rejects.toThrow(
+          `The database currently has ${extensionName} ${updateInRange} activated, but the Postgres instance only has ${minVersionInRange} available.`,
+        );
 
-    it(`should reindex ${extension} indices if needed`, async () => {
-      databaseMock.shouldReindex.mockResolvedValue(true);
+        expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
+        expect(databaseMock.runMigrations).not.toHaveBeenCalled();
+        expect(loggerMock.fatal).not.toHaveBeenCalled();
+      });
 
-      await expect(sut.onBootstrap()).resolves.toBeUndefined();
+      it('should throw error if installed version is not in version range', async () => {
+        databaseMock.getExtensionVersion.mockResolvedValue({
+          availableVersion: minVersionInRange,
+          installedVersion: versionAboveRange,
+        });
 
-      expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2);
-      expect(databaseMock.reindex).toHaveBeenCalledTimes(2);
-      expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
-      expect(loggerMock.fatal).not.toHaveBeenCalled();
-    });
+        await expect(sut.onBootstrap()).rejects.toThrow(
+          `The ${extensionName} extension version is ${versionAboveRange}, but Immich only supports`,
+        );
 
-    it(`should not reindex ${extension} indices if not needed`, async () => {
-      databaseMock.shouldReindex.mockResolvedValue(false);
+        expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
+        expect(databaseMock.runMigrations).not.toHaveBeenCalled();
+        expect(loggerMock.fatal).not.toHaveBeenCalled();
+      });
 
-      await expect(sut.onBootstrap()).resolves.toBeUndefined();
+      it(`should raise error if ${extension} extension upgrade failed`, async () => {
+        databaseMock.getExtensionVersion.mockResolvedValue({
+          availableVersion: updateInRange,
+          installedVersion: minVersionInRange,
+        });
+        databaseMock.updateVectorExtension.mockRejectedValue(new Error('Failed to update extension'));
 
-      expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2);
-      expect(databaseMock.reindex).toHaveBeenCalledTimes(0);
-      expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
-      expect(loggerMock.fatal).not.toHaveBeenCalled();
+        await expect(sut.onBootstrap()).rejects.toThrow('Failed to update extension');
+
+        expect(loggerMock.warn.mock.calls[0][0]).toContain(
+          `The ${extensionName} extension can be updated to ${updateInRange}.`,
+        );
+        expect(loggerMock.fatal).not.toHaveBeenCalled();
+        expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
+        expect(databaseMock.runMigrations).not.toHaveBeenCalled();
+      });
+
+      it(`should warn if ${extension} extension update requires restart`, async () => {
+        databaseMock.getExtensionVersion.mockResolvedValue({
+          availableVersion: updateInRange,
+          installedVersion: minVersionInRange,
+        });
+        databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: true });
+
+        await expect(sut.onBootstrap()).resolves.toBeUndefined();
+
+        expect(loggerMock.warn).toHaveBeenCalledTimes(1);
+        expect(loggerMock.warn.mock.calls[0][0]).toContain(extensionName);
+        expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
+        expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
+        expect(loggerMock.fatal).not.toHaveBeenCalled();
+      });
+
+      it(`should reindex ${extension} indices if needed`, async () => {
+        databaseMock.shouldReindex.mockResolvedValue(true);
+
+        await expect(sut.onBootstrap()).resolves.toBeUndefined();
+
+        expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2);
+        expect(databaseMock.reindex).toHaveBeenCalledTimes(2);
+        expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
+        expect(loggerMock.fatal).not.toHaveBeenCalled();
+      });
+
+      it(`should throw an error if reindexing fails`, async () => {
+        databaseMock.shouldReindex.mockResolvedValue(true);
+        databaseMock.reindex.mockRejectedValue(new Error('Error reindexing'));
+
+        await expect(sut.onBootstrap()).rejects.toBeDefined();
+
+        expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(1);
+        expect(databaseMock.reindex).toHaveBeenCalledTimes(1);
+        expect(databaseMock.runMigrations).not.toHaveBeenCalled();
+        expect(loggerMock.fatal).not.toHaveBeenCalled();
+        expect(loggerMock.warn).toHaveBeenCalledWith(
+          expect.stringContaining('Could not run vector reindexing checks.'),
+        );
+      });
+
+      it(`should not reindex ${extension} indices if not needed`, async () => {
+        databaseMock.shouldReindex.mockResolvedValue(false);
+
+        await expect(sut.onBootstrap()).resolves.toBeUndefined();
+
+        expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2);
+        expect(databaseMock.reindex).toHaveBeenCalledTimes(0);
+        expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
+        expect(loggerMock.fatal).not.toHaveBeenCalled();
+      });
     });
 
     it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => {
-      process.env.DB_SKIP_MIGRATIONS = 'true';
+      configMock.getEnv.mockReturnValue(
+        mockEnvData({
+          database: {
+            config: {
+              connectionType: 'parts',
+              type: 'postgres',
+              host: 'database',
+              port: 5432,
+              username: 'postgres',
+              password: 'postgres',
+              database: 'immich',
+            },
+            skipMigrations: true,
+            vectorExtension: DatabaseExtension.VECTORS,
+          },
+        }),
+      );
 
       await expect(sut.onBootstrap()).resolves.toBeUndefined();
 
       expect(databaseMock.runMigrations).not.toHaveBeenCalled();
     });
+
+    it(`should throw error if pgvector extension could not be created`, async () => {
+      configMock.getEnv.mockReturnValue(
+        mockEnvData({
+          database: {
+            config: {
+              connectionType: 'parts',
+              type: 'postgres',
+              host: 'database',
+              port: 5432,
+              username: 'postgres',
+              password: 'postgres',
+              database: 'immich',
+            },
+            skipMigrations: true,
+            vectorExtension: DatabaseExtension.VECTOR,
+          },
+        }),
+      );
+      databaseMock.getExtensionVersion.mockResolvedValue({
+        installedVersion: null,
+        availableVersion: minVersionInRange,
+      });
+      databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
+      databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension'));
+
+      await expect(sut.onBootstrap()).rejects.toThrow('Failed to create extension');
+
+      expect(loggerMock.fatal).toHaveBeenCalledTimes(1);
+      expect(loggerMock.fatal.mock.calls[0][0]).toContain(
+        `Alternatively, if your Postgres instance has pgvecto.rs, you may use this instead`,
+      );
+      expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
+      expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
+      expect(databaseMock.runMigrations).not.toHaveBeenCalled();
+    });
+
+    it(`should throw error if pgvecto.rs extension could not be created`, async () => {
+      databaseMock.getExtensionVersion.mockResolvedValue({
+        installedVersion: null,
+        availableVersion: minVersionInRange,
+      });
+      databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
+      databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension'));
+
+      await expect(sut.onBootstrap()).rejects.toThrow('Failed to create extension');
+
+      expect(loggerMock.fatal).toHaveBeenCalledTimes(1);
+      expect(loggerMock.fatal.mock.calls[0][0]).toContain(
+        `Alternatively, if your Postgres instance has pgvector, you may use this instead`,
+      );
+      expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
+      expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
+      expect(databaseMock.runMigrations).not.toHaveBeenCalled();
+    });
   });
 
-  it(`should throw error if pgvector extension could not be created`, async () => {
-    process.env.DB_VECTOR_EXTENSION = 'pgvector';
-    databaseMock.getExtensionVersion.mockResolvedValue({
-      installedVersion: null,
-      availableVersion: minVersionInRange,
+  describe('handleConnectionError', () => {
+    beforeAll(() => {
+      vi.useFakeTimers();
     });
-    databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
-    databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension'));
 
-    await expect(sut.onBootstrap()).rejects.toThrow('Failed to create extension');
-
-    expect(loggerMock.fatal).toHaveBeenCalledTimes(1);
-    expect(loggerMock.fatal.mock.calls[0][0]).toContain(
-      `Alternatively, if your Postgres instance has pgvecto.rs, you may use this instead`,
-    );
-    expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
-    expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
-    expect(databaseMock.runMigrations).not.toHaveBeenCalled();
-  });
-
-  it(`should throw error if pgvecto.rs extension could not be created`, async () => {
-    databaseMock.getExtensionVersion.mockResolvedValue({
-      installedVersion: null,
-      availableVersion: minVersionInRange,
+    afterAll(() => {
+      vi.useRealTimers();
     });
-    databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
-    databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension'));
 
-    await expect(sut.onBootstrap()).rejects.toThrow('Failed to create extension');
+    it('should not override interval', () => {
+      sut.handleConnectionError(new Error('Error'));
+      expect(loggerMock.error).toHaveBeenCalled();
 
-    expect(loggerMock.fatal).toHaveBeenCalledTimes(1);
-    expect(loggerMock.fatal.mock.calls[0][0]).toContain(
-      `Alternatively, if your Postgres instance has pgvector, you may use this instead`,
-    );
-    expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
-    expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
-    expect(databaseMock.runMigrations).not.toHaveBeenCalled();
+      sut.handleConnectionError(new Error('foo'));
+      expect(loggerMock.error).toHaveBeenCalledTimes(1);
+    });
+
+    it('should reconnect when interval elapses', async () => {
+      databaseMock.reconnect.mockResolvedValue(true);
+
+      sut.handleConnectionError(new Error('error'));
+      await vi.advanceTimersByTimeAsync(5000);
+
+      expect(databaseMock.reconnect).toHaveBeenCalledTimes(1);
+      expect(loggerMock.log).toHaveBeenCalledWith('Database reconnected');
+
+      await vi.advanceTimersByTimeAsync(5000);
+      expect(databaseMock.reconnect).toHaveBeenCalledTimes(1);
+    });
+
+    it('should try again when reconnection fails', async () => {
+      databaseMock.reconnect.mockResolvedValueOnce(false);
+
+      sut.handleConnectionError(new Error('error'));
+      await vi.advanceTimersByTimeAsync(5000);
+
+      expect(databaseMock.reconnect).toHaveBeenCalledTimes(1);
+      expect(loggerMock.warn).toHaveBeenCalledWith(expect.stringContaining('Database connection failed'));
+
+      databaseMock.reconnect.mockResolvedValueOnce(true);
+      await vi.advanceTimersByTimeAsync(5000);
+      expect(databaseMock.reconnect).toHaveBeenCalledTimes(2);
+      expect(loggerMock.log).toHaveBeenCalledWith('Database reconnected');
+    });
   });
 });
diff --git a/server/src/services/database.service.ts b/server/src/services/database.service.ts
index a5280ff28b..b1a270abd8 100644
--- a/server/src/services/database.service.ts
+++ b/server/src/services/database.service.ts
@@ -1,17 +1,16 @@
-import { Inject, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
 import { Duration } from 'luxon';
 import semver from 'semver';
-import { getVectorExtension } from 'src/database.config';
-import { OnEmit } from 'src/decorators';
+import { OnEvent } from 'src/decorators';
 import {
   DatabaseExtension,
   DatabaseLock,
   EXTENSION_NAMES,
-  IDatabaseRepository,
   VectorExtension,
   VectorIndex,
 } from 'src/interfaces/database.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
+import { BootstrapEventPriority } from 'src/interfaces/event.interface';
+import { BaseService } from 'src/services/base.service';
 
 type CreateFailedArgs = { name: string; extension: string; otherName: string };
 type UpdateFailedArgs = { name: string; extension: string; availableVersion: string };
@@ -63,17 +62,10 @@ const messages = {
 const RETRY_DURATION = Duration.fromObject({ seconds: 5 });
 
 @Injectable()
-export class DatabaseService {
+export class DatabaseService extends BaseService {
   private reconnection?: NodeJS.Timeout;
 
-  constructor(
-    @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-  ) {
-    this.logger.setContext(DatabaseService.name);
-  }
-
-  @OnEmit({ event: 'app.bootstrap', priority: -200 })
+  @OnEvent({ name: 'app.bootstrap', priority: BootstrapEventPriority.DatabaseService })
   async onBootstrap() {
     const version = await this.databaseRepository.getPostgresVersion();
     const current = semver.coerce(version);
@@ -85,7 +77,8 @@ export class DatabaseService {
     }
 
     await this.databaseRepository.withLock(DatabaseLock.Migrations, async () => {
-      const extension = getVectorExtension();
+      const envData = this.configRepository.getEnv();
+      const extension = envData.database.vectorExtension;
       const name = EXTENSION_NAMES[extension];
       const extensionRange = this.databaseRepository.getExtensionVersionRange(extension);
 
@@ -116,7 +109,8 @@ export class DatabaseService {
 
       await this.checkReindexing();
 
-      if (process.env.DB_SKIP_MIGRATIONS !== 'true') {
+      const { database } = this.configRepository.getEnv();
+      if (!database.skipMigrations) {
         await this.databaseRepository.runMigrations();
       }
     });
diff --git a/server/src/services/download.service.spec.ts b/server/src/services/download.service.spec.ts
index 14fa7bab48..632d157384 100644
--- a/server/src/services/download.service.spec.ts
+++ b/server/src/services/download.service.spec.ts
@@ -7,10 +7,8 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
 import { DownloadService } from 'src/services/download.service';
 import { assetStub } from 'test/fixtures/asset.stub';
 import { authStub } from 'test/fixtures/auth.stub';
-import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
-import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
+import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
+import { newTestService } from 'test/utils';
 import { Readable } from 'typeorm/platform/PlatformTools.js';
 import { Mocked, vitest } from 'vitest';
 
@@ -36,15 +34,54 @@ describe(DownloadService.name, () => {
   });
 
   beforeEach(() => {
-    accessMock = newAccessRepositoryMock();
-    assetMock = newAssetRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-    storageMock = newStorageRepositoryMock();
-
-    sut = new DownloadService(accessMock, assetMock, loggerMock, storageMock);
+    ({ sut, accessMock, assetMock, loggerMock, storageMock } = newTestService(DownloadService));
   });
 
   describe('downloadArchive', () => {
+    it('should skip asset ids that could not be found', async () => {
+      const archiveMock = {
+        addFile: vitest.fn(),
+        finalize: vitest.fn(),
+        stream: new Readable(),
+      };
+
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
+      assetMock.getByIds.mockResolvedValue([{ ...assetStub.noResizePath, id: 'asset-1' }]);
+      storageMock.createZipStream.mockReturnValue(archiveMock);
+
+      await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({
+        stream: archiveMock.stream,
+      });
+
+      expect(archiveMock.addFile).toHaveBeenCalledTimes(1);
+      expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, 'upload/library/IMG_123.jpg', 'IMG_123.jpg');
+    });
+
+    it('should log a warning if the original path could not be resolved', async () => {
+      const archiveMock = {
+        addFile: vitest.fn(),
+        finalize: vitest.fn(),
+        stream: new Readable(),
+      };
+
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
+      storageMock.realpath.mockRejectedValue(new Error('Could not read file'));
+      assetMock.getByIds.mockResolvedValue([
+        { ...assetStub.noResizePath, id: 'asset-1' },
+        { ...assetStub.noWebpPath, id: 'asset-2' },
+      ]);
+      storageMock.createZipStream.mockReturnValue(archiveMock);
+
+      await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({
+        stream: archiveMock.stream,
+      });
+
+      expect(loggerMock.warn).toHaveBeenCalledTimes(2);
+      expect(archiveMock.addFile).toHaveBeenCalledTimes(2);
+      expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, 'upload/library/IMG_123.jpg', 'IMG_123.jpg');
+      expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, 'upload/library/IMG_456.jpg', 'IMG_456.jpg');
+    });
+
     it('should download an archive', async () => {
       const archiveMock = {
         addFile: vitest.fn(),
diff --git a/server/src/services/download.service.ts b/server/src/services/download.service.ts
index 988b859ff8..3d66f009cf 100644
--- a/server/src/services/download.service.ts
+++ b/server/src/services/download.service.ts
@@ -1,4 +1,4 @@
-import { BadRequestException, Inject, Injectable } from '@nestjs/common';
+import { BadRequestException, Injectable } from '@nestjs/common';
 import { parse } from 'node:path';
 import { StorageCore } from 'src/cores/storage.core';
 import { AssetIdsDto } from 'src/dtos/asset.dto';
@@ -6,26 +6,14 @@ import { AuthDto } from 'src/dtos/auth.dto';
 import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { Permission } from 'src/enum';
-import { IAccessRepository } from 'src/interfaces/access.interface';
-import { IAssetRepository } from 'src/interfaces/asset.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { ImmichReadStream, IStorageRepository } from 'src/interfaces/storage.interface';
-import { requireAccess } from 'src/utils/access';
+import { ImmichReadStream } from 'src/interfaces/storage.interface';
+import { BaseService } from 'src/services/base.service';
 import { HumanReadableSize } from 'src/utils/bytes';
 import { usePagination } from 'src/utils/pagination';
 import { getPreferences } from 'src/utils/preferences';
 
 @Injectable()
-export class DownloadService {
-  constructor(
-    @Inject(IAccessRepository) private access: IAccessRepository,
-    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
-  ) {
-    this.logger.setContext(DownloadService.name);
-  }
-
+export class DownloadService extends BaseService {
   async getDownloadInfo(auth: AuthDto, dto: DownloadInfoDto): Promise<DownloadResponseDto> {
     const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4;
     const archives: DownloadArchiveInfo[] = [];
@@ -73,7 +61,7 @@ export class DownloadService {
   }
 
   async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise<ImmichReadStream> {
-    await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: dto.assetIds });
+    await this.requireAccess({ auth, permission: Permission.ASSET_DOWNLOAD, ids: dto.assetIds });
 
     const zip = this.storageRepository.createZipStream();
     const assets = await this.assetRepository.getByIds(dto.assetIds);
@@ -116,20 +104,20 @@ export class DownloadService {
 
     if (dto.assetIds) {
       const assetIds = dto.assetIds;
-      await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: assetIds });
+      await this.requireAccess({ auth, permission: Permission.ASSET_DOWNLOAD, ids: assetIds });
       const assets = await this.assetRepository.getByIds(assetIds, { exifInfo: true });
       return usePagination(PAGINATION_SIZE, () => ({ hasNextPage: false, items: assets }));
     }
 
     if (dto.albumId) {
       const albumId = dto.albumId;
-      await requireAccess(this.access, { auth, permission: Permission.ALBUM_DOWNLOAD, ids: [albumId] });
+      await this.requireAccess({ auth, permission: Permission.ALBUM_DOWNLOAD, ids: [albumId] });
       return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId));
     }
 
     if (dto.userId) {
       const userId = dto.userId;
-      await requireAccess(this.access, { auth, permission: Permission.TIMELINE_DOWNLOAD, ids: [userId] });
+      await this.requireAccess({ auth, permission: Permission.TIMELINE_DOWNLOAD, ids: [userId] });
       return usePagination(PAGINATION_SIZE, (pagination) =>
         this.assetRepository.getByUserId(pagination, userId, { isVisible: true }),
       );
diff --git a/server/src/services/duplicate.service.spec.ts b/server/src/services/duplicate.service.spec.ts
index eada2fffcf..75af1ef6f1 100644
--- a/server/src/services/duplicate.service.spec.ts
+++ b/server/src/services/duplicate.service.spec.ts
@@ -1,5 +1,4 @@
 import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
 import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { ISearchRepository } from 'src/interfaces/search.interface';
@@ -7,40 +6,50 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf
 import { DuplicateService } from 'src/services/duplicate.service';
 import { SearchService } from 'src/services/search.service';
 import { assetStub } from 'test/fixtures/asset.stub';
-import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
-import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
-import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
+import { authStub } from 'test/fixtures/auth.stub';
+import { newTestService } from 'test/utils';
 import { Mocked, beforeEach, vitest } from 'vitest';
 
 vitest.useFakeTimers();
 
 describe(SearchService.name, () => {
   let sut: DuplicateService;
+
   let assetMock: Mocked<IAssetRepository>;
-  let systemMock: Mocked<ISystemMetadataRepository>;
-  let searchMock: Mocked<ISearchRepository>;
-  let loggerMock: Mocked<ILoggerRepository>;
-  let cryptoMock: Mocked<ICryptoRepository>;
   let jobMock: Mocked<IJobRepository>;
+  let loggerMock: Mocked<ILoggerRepository>;
+  let searchMock: Mocked<ISearchRepository>;
+  let systemMock: Mocked<ISystemMetadataRepository>;
 
   beforeEach(() => {
-    assetMock = newAssetRepositoryMock();
-    systemMock = newSystemMetadataRepositoryMock();
-    searchMock = newSearchRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-    cryptoMock = newCryptoRepositoryMock();
-    jobMock = newJobRepositoryMock();
-
-    sut = new DuplicateService(systemMock, searchMock, assetMock, loggerMock, cryptoMock, jobMock);
+    ({ sut, assetMock, jobMock, loggerMock, searchMock, systemMock } = newTestService(DuplicateService));
   });
 
   it('should work', () => {
     expect(sut).toBeDefined();
   });
 
+  describe('getDuplicates', () => {
+    it('should get duplicates', async () => {
+      assetMock.getDuplicates.mockResolvedValue([assetStub.hasDupe, assetStub.hasDupe]);
+      await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([
+        {
+          duplicateId: assetStub.hasDupe.duplicateId,
+          assets: [
+            expect.objectContaining({ id: assetStub.hasDupe.id }),
+            expect.objectContaining({ id: assetStub.hasDupe.id }),
+          ],
+        },
+      ]);
+    });
+
+    it('should update assets with duplicateId', async () => {
+      assetMock.getDuplicates.mockResolvedValue([assetStub.hasDupe]);
+      await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([]);
+      expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.hasDupe.id], { duplicateId: null });
+    });
+  });
+
   describe('handleQueueSearchDuplicates', () => {
     beforeEach(() => {
       systemMock.get.mockResolvedValue({
diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts
index 35a1a7325b..0d91df5790 100644
--- a/server/src/services/duplicate.service.ts
+++ b/server/src/services/duplicate.service.ts
@@ -1,50 +1,44 @@
-import { Inject, Injectable } from '@nestjs/common';
-import { SystemConfigCore } from 'src/cores/system-config.core';
+import { Injectable } from '@nestjs/common';
+import { OnJob } from 'src/decorators';
 import { mapAsset } from 'src/dtos/asset-response.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
 import { DuplicateResponseDto, mapDuplicateResponse } from 'src/dtos/duplicate.dto';
 import { AssetEntity } from 'src/entities/asset.entity';
-import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import {
-  IBaseJob,
-  IEntityJob,
-  IJobRepository,
-  JOBS_ASSET_PAGINATION_SIZE,
-  JobName,
-  JobStatus,
-} from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { AssetDuplicateResult, ISearchRepository } from 'src/interfaces/search.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { WithoutProperty } from 'src/interfaces/asset.interface';
+import { JOBS_ASSET_PAGINATION_SIZE, JobName, JobOf, JobStatus, QueueName } from 'src/interfaces/job.interface';
+import { AssetDuplicateResult } from 'src/interfaces/search.interface';
+import { BaseService } from 'src/services/base.service';
 import { getAssetFiles } from 'src/utils/asset.util';
 import { isDuplicateDetectionEnabled } from 'src/utils/misc';
 import { usePagination } from 'src/utils/pagination';
 
 @Injectable()
-export class DuplicateService {
-  private configCore: SystemConfigCore;
-
-  constructor(
-    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
-    @Inject(ISearchRepository) private searchRepository: ISearchRepository,
-    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
-    @Inject(IJobRepository) private jobRepository: IJobRepository,
-  ) {
-    this.logger.setContext(DuplicateService.name);
-    this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
-  }
-
+export class DuplicateService extends BaseService {
   async getDuplicates(auth: AuthDto): Promise<DuplicateResponseDto[]> {
     const res = await this.assetRepository.getDuplicates({ userIds: [auth.user.id] });
-
-    return mapDuplicateResponse(res.map((a) => mapAsset(a, { auth, withStack: true })));
+    const uniqueAssetIds: string[] = [];
+    const duplicates = mapDuplicateResponse(res.map((a) => mapAsset(a, { auth, withStack: true }))).filter(
+      (duplicate) => {
+        if (duplicate.assets.length === 1) {
+          uniqueAssetIds.push(duplicate.assets[0].id);
+          return false;
+        }
+        return true;
+      },
+    );
+    if (uniqueAssetIds.length > 0) {
+      try {
+        await this.assetRepository.updateAll(uniqueAssetIds, { duplicateId: null });
+      } catch (error: any) {
+        this.logger.error(`Failed to remove duplicateId from assets: ${error.message}`);
+      }
+    }
+    return duplicates;
   }
 
-  async handleQueueSearchDuplicates({ force }: IBaseJob): Promise<JobStatus> {
-    const { machineLearning } = await this.configCore.getConfig({ withCache: false });
+  @OnJob({ name: JobName.QUEUE_DUPLICATE_DETECTION, queue: QueueName.DUPLICATE_DETECTION })
+  async handleQueueSearchDuplicates({ force }: JobOf<JobName.QUEUE_DUPLICATE_DETECTION>): Promise<JobStatus> {
+    const { machineLearning } = await this.getConfig({ withCache: false });
     if (!isDuplicateDetectionEnabled(machineLearning)) {
       return JobStatus.SKIPPED;
     }
@@ -64,8 +58,9 @@ export class DuplicateService {
     return JobStatus.SUCCESS;
   }
 
-  async handleSearchDuplicates({ id }: IEntityJob): Promise<JobStatus> {
-    const { machineLearning } = await this.configCore.getConfig({ withCache: true });
+  @OnJob({ name: JobName.DUPLICATE_DETECTION, queue: QueueName.DUPLICATE_DETECTION })
+  async handleSearchDuplicates({ id }: JobOf<JobName.DUPLICATE_DETECTION>): Promise<JobStatus> {
+    const { machineLearning } = await this.getConfig({ withCache: true });
     if (!isDuplicateDetectionEnabled(machineLearning)) {
       return JobStatus.SKIPPED;
     }
diff --git a/server/src/services/index.ts b/server/src/services/index.ts
index 2cfbdb40c2..0dd8bdae66 100644
--- a/server/src/services/index.ts
+++ b/server/src/services/index.ts
@@ -6,6 +6,7 @@ import { AssetMediaService } from 'src/services/asset-media.service';
 import { AssetService } from 'src/services/asset.service';
 import { AuditService } from 'src/services/audit.service';
 import { AuthService } from 'src/services/auth.service';
+import { BackupService } from 'src/services/backup.service';
 import { CliService } from 'src/services/cli.service';
 import { DatabaseService } from 'src/services/database.service';
 import { DownloadService } from 'src/services/download.service';
@@ -16,7 +17,6 @@ import { MapService } from 'src/services/map.service';
 import { MediaService } from 'src/services/media.service';
 import { MemoryService } from 'src/services/memory.service';
 import { MetadataService } from 'src/services/metadata.service';
-import { MicroservicesService } from 'src/services/microservices.service';
 import { NotificationService } from 'src/services/notification.service';
 import { PartnerService } from 'src/services/partner.service';
 import { PersonService } from 'src/services/person.service';
@@ -48,6 +48,7 @@ export const services = [
   AssetService,
   AuditService,
   AuthService,
+  BackupService,
   CliService,
   DatabaseService,
   DownloadService,
@@ -58,7 +59,6 @@ export const services = [
   MediaService,
   MemoryService,
   MetadataService,
-  MicroservicesService,
   NotificationService,
   PartnerService,
   PersonService,
diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts
index 1c810facb4..a23b05073c 100644
--- a/server/src/services/job.service.spec.ts
+++ b/server/src/services/job.service.spec.ts
@@ -1,65 +1,46 @@
 import { BadRequestException } from '@nestjs/common';
-import { SystemConfig } from 'src/config';
-import { SystemConfigCore } from 'src/cores/system-config.core';
+import { defaults, SystemConfig } from 'src/config';
+import { ImmichWorker } from 'src/enum';
 import { IAssetRepository } from 'src/interfaces/asset.interface';
-import { IEventRepository } from 'src/interfaces/event.interface';
-import {
-  IJobRepository,
-  JobCommand,
-  JobHandler,
-  JobItem,
-  JobName,
-  JobStatus,
-  QueueName,
-} from 'src/interfaces/job.interface';
+import { IConfigRepository } from 'src/interfaces/config.interface';
+import { IJobRepository, JobCommand, JobItem, JobName, JobStatus, QueueName } from 'src/interfaces/job.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { IMetricRepository } from 'src/interfaces/metric.interface';
-import { IPersonRepository } from 'src/interfaces/person.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
 import { JobService } from 'src/services/job.service';
 import { assetStub } from 'test/fixtures/asset.stub';
-import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
-import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
-import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newMetricRepositoryMock } from 'test/repositories/metric.repository.mock';
-import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
-import { Mocked, vitest } from 'vitest';
-
-const makeMockHandlers = (status: JobStatus) => {
-  const mock = vitest.fn().mockResolvedValue(status);
-  return Object.fromEntries(Object.values(JobName).map((jobName) => [jobName, mock])) as unknown as Record<
-    JobName,
-    JobHandler
-  >;
-};
+import { newTestService } from 'test/utils';
+import { Mocked } from 'vitest';
 
 describe(JobService.name, () => {
   let sut: JobService;
   let assetMock: Mocked<IAssetRepository>;
-  let eventMock: Mocked<IEventRepository>;
+  let configMock: Mocked<IConfigRepository>;
   let jobMock: Mocked<IJobRepository>;
-  let personMock: Mocked<IPersonRepository>;
-  let metricMock: Mocked<IMetricRepository>;
-  let systemMock: Mocked<ISystemMetadataRepository>;
   let loggerMock: Mocked<ILoggerRepository>;
+  let telemetryMock: Mocked<ITelemetryRepository>;
 
   beforeEach(() => {
-    assetMock = newAssetRepositoryMock();
-    systemMock = newSystemMetadataRepositoryMock();
-    eventMock = newEventRepositoryMock();
-    jobMock = newJobRepositoryMock();
-    personMock = newPersonRepositoryMock();
-    metricMock = newMetricRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-    sut = new JobService(assetMock, eventMock, jobMock, systemMock, personMock, metricMock, loggerMock);
+    ({ sut, assetMock, configMock, jobMock, loggerMock, telemetryMock } = newTestService(JobService, {}));
+
+    configMock.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES);
   });
 
   it('should work', () => {
     expect(sut).toBeDefined();
   });
 
+  describe('onConfigUpdate', () => {
+    it('should update concurrency', () => {
+      sut.onConfigUpdate({ newConfig: defaults, oldConfig: {} as SystemConfig });
+
+      expect(jobMock.setConcurrency).toHaveBeenCalledTimes(15);
+      expect(jobMock.setConcurrency).toHaveBeenNthCalledWith(5, QueueName.FACIAL_RECOGNITION, 1);
+      expect(jobMock.setConcurrency).toHaveBeenNthCalledWith(7, QueueName.DUPLICATE_DETECTION, 1);
+      expect(jobMock.setConcurrency).toHaveBeenNthCalledWith(8, QueueName.BACKGROUND_TASK, 5);
+      expect(jobMock.setConcurrency).toHaveBeenNthCalledWith(9, QueueName.STORAGE_TEMPLATE_MIGRATION, 1);
+    });
+  });
+
   describe('handleNightlyJobs', () => {
     it('should run the scheduled jobs', async () => {
       await sut.handleNightlyJobs();
@@ -122,6 +103,7 @@ describe(JobService.name, () => {
         [QueueName.SIDECAR]: expectedJobStatus,
         [QueueName.LIBRARY]: expectedJobStatus,
         [QueueName.NOTIFICATION]: expectedJobStatus,
+        [QueueName.BACKUP_DATABASE]: expectedJobStatus,
       });
     });
   });
@@ -232,41 +214,19 @@ describe(JobService.name, () => {
     });
   });
 
-  describe('init', () => {
-    it('should register a handler for each queue', async () => {
-      await sut.init(makeMockHandlers(JobStatus.SUCCESS));
-      expect(systemMock.get).toHaveBeenCalled();
-      expect(jobMock.addHandler).toHaveBeenCalledTimes(Object.keys(QueueName).length);
-    });
+  describe('onJobStart', () => {
+    it('should process a successful job', async () => {
+      jobMock.run.mockResolvedValue(JobStatus.SUCCESS);
 
-    it('should subscribe to config changes', async () => {
-      await sut.init(makeMockHandlers(JobStatus.FAILED));
+      await sut.onJobStart(QueueName.BACKGROUND_TASK, {
+        name: JobName.DELETE_FILES,
+        data: { files: ['path/to/file'] },
+      });
 
-      SystemConfigCore.create(newSystemMetadataRepositoryMock(false), newLoggerRepositoryMock()).config$.next({
-        job: {
-          [QueueName.BACKGROUND_TASK]: { concurrency: 10 },
-          [QueueName.SMART_SEARCH]: { concurrency: 10 },
-          [QueueName.METADATA_EXTRACTION]: { concurrency: 10 },
-          [QueueName.FACE_DETECTION]: { concurrency: 10 },
-          [QueueName.SEARCH]: { concurrency: 10 },
-          [QueueName.SIDECAR]: { concurrency: 10 },
-          [QueueName.LIBRARY]: { concurrency: 10 },
-          [QueueName.MIGRATION]: { concurrency: 10 },
-          [QueueName.THUMBNAIL_GENERATION]: { concurrency: 10 },
-          [QueueName.VIDEO_CONVERSION]: { concurrency: 10 },
-          [QueueName.NOTIFICATION]: { concurrency: 5 },
-        },
-      } as SystemConfig);
-
-      expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.BACKGROUND_TASK, 10);
-      expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.SMART_SEARCH, 10);
-      expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION, 10);
-      expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.FACE_DETECTION, 10);
-      expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.SIDECAR, 10);
-      expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.LIBRARY, 10);
-      expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.MIGRATION, 10);
-      expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.THUMBNAIL_GENERATION, 10);
-      expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.VIDEO_CONVERSION, 10);
+      expect(telemetryMock.jobs.addToGauge).toHaveBeenCalledWith('immich.queues.background_task.active', 1);
+      expect(telemetryMock.jobs.addToGauge).toHaveBeenCalledWith('immich.queues.background_task.active', -1);
+      expect(telemetryMock.jobs.addToCounter).toHaveBeenCalledWith('immich.jobs.delete_files.success', 1);
+      expect(loggerMock.error).not.toHaveBeenCalled();
     });
 
     const tests: Array<{ item: JobItem; jobs: JobName[] }> = [
@@ -288,7 +248,7 @@ describe(JobService.name, () => {
       },
       {
         item: { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: { id: 'asset-1', source: 'upload' } },
-        jobs: [JobName.GENERATE_PREVIEW],
+        jobs: [JobName.GENERATE_THUMBNAILS],
       },
       {
         item: { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: { id: 'asset-1' } },
@@ -299,28 +259,16 @@ describe(JobService.name, () => {
         jobs: [],
       },
       {
-        item: { name: JobName.GENERATE_PREVIEW, data: { id: 'asset-1' } },
-        jobs: [JobName.GENERATE_THUMBNAIL, JobName.GENERATE_THUMBHASH],
+        item: { name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1' } },
+        jobs: [],
       },
       {
-        item: { name: JobName.GENERATE_PREVIEW, data: { id: 'asset-1', source: 'upload' } },
-        jobs: [
-          JobName.GENERATE_THUMBNAIL,
-          JobName.GENERATE_THUMBHASH,
-          JobName.SMART_SEARCH,
-          JobName.FACE_DETECTION,
-          JobName.VIDEO_CONVERSION,
-        ],
+        item: { name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1', source: 'upload' } },
+        jobs: [JobName.SMART_SEARCH, JobName.FACE_DETECTION, JobName.VIDEO_CONVERSION],
       },
       {
-        item: { name: JobName.GENERATE_PREVIEW, data: { id: 'asset-live-image', source: 'upload' } },
-        jobs: [
-          JobName.GENERATE_THUMBNAIL,
-          JobName.GENERATE_THUMBHASH,
-          JobName.SMART_SEARCH,
-          JobName.FACE_DETECTION,
-          JobName.VIDEO_CONVERSION,
-        ],
+        item: { name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-live-image', source: 'upload' } },
+        jobs: [JobName.SMART_SEARCH, JobName.FACE_DETECTION, JobName.VIDEO_CONVERSION],
       },
       {
         item: { name: JobName.SMART_SEARCH, data: { id: 'asset-1' } },
@@ -338,16 +286,17 @@ describe(JobService.name, () => {
 
     for (const { item, jobs } of tests) {
       it(`should queue ${jobs.length} jobs when a ${item.name} job finishes successfully`, async () => {
-        if (item.name === JobName.GENERATE_PREVIEW && item.data.source === 'upload') {
+        if (item.name === JobName.GENERATE_THUMBNAILS && item.data.source === 'upload') {
           if (item.data.id === 'asset-live-image') {
-            assetMock.getByIds.mockResolvedValue([assetStub.livePhotoStillAsset]);
+            assetMock.getByIdsWithAllRelations.mockResolvedValue([assetStub.livePhotoStillAsset]);
           } else {
-            assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
+            assetMock.getByIdsWithAllRelations.mockResolvedValue([assetStub.livePhotoMotionAsset]);
           }
         }
 
-        await sut.init(makeMockHandlers(JobStatus.SUCCESS));
-        await jobMock.addHandler.mock.calls[0][2](item);
+        jobMock.run.mockResolvedValue(JobStatus.SUCCESS);
+
+        await sut.onJobStart(QueueName.BACKGROUND_TASK, item);
 
         if (jobs.length > 1) {
           expect(jobMock.queueAll).toHaveBeenCalledWith(
@@ -361,9 +310,10 @@ describe(JobService.name, () => {
         }
       });
 
-      it(`should not queue any jobs when ${item.name} finishes with 'false'`, async () => {
-        await sut.init(makeMockHandlers(JobStatus.FAILED));
-        await jobMock.addHandler.mock.calls[0][2](item);
+      it(`should not queue any jobs when ${item.name} fails`, async () => {
+        jobMock.run.mockResolvedValue(JobStatus.FAILED);
+
+        await sut.onJobStart(QueueName.BACKGROUND_TASK, item);
 
         expect(jobMock.queueAll).not.toHaveBeenCalled();
       });
diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts
index aa61ccf3cb..2faed0a516 100644
--- a/server/src/services/job.service.ts
+++ b/server/src/services/job.service.ts
@@ -1,42 +1,63 @@
-import { BadRequestException, Inject, Injectable } from '@nestjs/common';
+import { BadRequestException, Injectable } from '@nestjs/common';
 import { snakeCase } from 'lodash';
-import { SystemConfigCore } from 'src/cores/system-config.core';
+import { OnEvent } from 'src/decorators';
 import { mapAsset } from 'src/dtos/asset-response.dto';
-import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from 'src/dtos/job.dto';
-import { AssetType } from 'src/enum';
-import { IAssetRepository } from 'src/interfaces/asset.interface';
-import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
+import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
+import { AssetType, ImmichWorker, ManualJobName } from 'src/enum';
+import { ArgOf, ArgsOf } from 'src/interfaces/event.interface';
 import {
   ConcurrentQueueName,
-  IJobRepository,
   JobCommand,
-  JobHandler,
   JobItem,
   JobName,
   JobStatus,
   QueueCleanType,
   QueueName,
 } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { IMetricRepository } from 'src/interfaces/metric.interface';
-import { IPersonRepository } from 'src/interfaces/person.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { BaseService } from 'src/services/base.service';
+
+const asJobItem = (dto: JobCreateDto): JobItem => {
+  switch (dto.name) {
+    case ManualJobName.TAG_CLEANUP: {
+      return { name: JobName.TAG_CLEANUP };
+    }
+
+    case ManualJobName.PERSON_CLEANUP: {
+      return { name: JobName.PERSON_CLEANUP };
+    }
+
+    case ManualJobName.USER_CLEANUP: {
+      return { name: JobName.USER_DELETE_CHECK };
+    }
+
+    default: {
+      throw new BadRequestException('Invalid job name');
+    }
+  }
+};
 
 @Injectable()
-export class JobService {
-  private configCore: SystemConfigCore;
+export class JobService extends BaseService {
+  @OnEvent({ name: 'config.init', workers: [ImmichWorker.MICROSERVICES] })
+  onConfigInit({ newConfig: config }: ArgOf<'config.init'>) {
+    this.logger.debug(`Updating queue concurrency settings`);
+    for (const queueName of Object.values(QueueName)) {
+      let concurrency = 1;
+      if (this.isConcurrentQueue(queueName)) {
+        concurrency = config.job[queueName].concurrency;
+      }
+      this.logger.debug(`Setting ${queueName} concurrency to ${concurrency}`);
+      this.jobRepository.setConcurrency(queueName, concurrency);
+    }
+  }
 
-  constructor(
-    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
-    @Inject(IEventRepository) private eventRepository: IEventRepository,
-    @Inject(IJobRepository) private jobRepository: IJobRepository,
-    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
-    @Inject(IPersonRepository) private personRepository: IPersonRepository,
-    @Inject(IMetricRepository) private metricRepository: IMetricRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-  ) {
-    this.logger.setContext(JobService.name);
-    this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
+  @OnEvent({ name: 'config.update', server: true })
+  onConfigUpdate({ newConfig: config }: ArgOf<'config.update'>) {
+    this.onConfigInit({ newConfig: config });
+  }
+
+  async create(dto: JobCreateDto): Promise<void> {
+    await this.jobRepository.queue(asJobItem(dto));
   }
 
   async handleCommand(queueName: QueueName, dto: JobCommandDto): Promise<JobStatusDto> {
@@ -96,7 +117,7 @@ export class JobService {
       throw new BadRequestException(`Job is already running`);
     }
 
-    this.metricRepository.jobs.addToCounter(`immich.queues.${snakeCase(name)}.started`, 1);
+    this.telemetryRepository.jobs.addToCounter(`immich.queues.${snakeCase(name)}.started`, 1);
 
     switch (name) {
       case QueueName.VIDEO_CONVERSION: {
@@ -140,7 +161,11 @@ export class JobService {
       }
 
       case QueueName.LIBRARY: {
-        return this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: { force } });
+        return this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ALL, data: { force } });
+      }
+
+      case QueueName.BACKUP_DATABASE: {
+        return this.jobRepository.queue({ name: JobName.BACKUP_DATABASE, data: { force } });
       }
 
       default: {
@@ -149,49 +174,22 @@ export class JobService {
     }
   }
 
-  async init(jobHandlers: Record<JobName, JobHandler>) {
-    const config = await this.configCore.getConfig({ withCache: false });
-    for (const queueName of Object.values(QueueName)) {
-      let concurrency = 1;
-
-      if (this.isConcurrentQueue(queueName)) {
-        concurrency = config.job[queueName].concurrency;
+  @OnEvent({ name: 'job.start' })
+  async onJobStart(...[queueName, job]: ArgsOf<'job.start'>) {
+    const queueMetric = `immich.queues.${snakeCase(queueName)}.active`;
+    this.telemetryRepository.jobs.addToGauge(queueMetric, 1);
+    try {
+      const status = await this.jobRepository.run(job);
+      const jobMetric = `immich.jobs.${job.name.replaceAll('-', '_')}.${status}`;
+      this.telemetryRepository.jobs.addToCounter(jobMetric, 1);
+      if (status === JobStatus.SUCCESS || status == JobStatus.SKIPPED) {
+        await this.onDone(job);
       }
-
-      this.logger.debug(`Registering ${queueName} with a concurrency of ${concurrency}`);
-      this.jobRepository.addHandler(queueName, concurrency, async (item: JobItem): Promise<void> => {
-        const { name, data } = item;
-
-        const queueMetric = `immich.queues.${snakeCase(queueName)}.active`;
-        this.metricRepository.jobs.addToGauge(queueMetric, 1);
-
-        try {
-          const handler = jobHandlers[name];
-          const status = await handler(data);
-          const jobMetric = `immich.jobs.${name.replaceAll('-', '_')}.${status}`;
-          this.metricRepository.jobs.addToCounter(jobMetric, 1);
-          if (status === JobStatus.SUCCESS || status == JobStatus.SKIPPED) {
-            await this.onDone(item);
-          }
-        } catch (error: Error | any) {
-          this.logger.error(`Unable to run job handler (${queueName}/${name}): ${error}`, error?.stack, data);
-        } finally {
-          this.metricRepository.jobs.addToGauge(queueMetric, -1);
-        }
-      });
+    } catch (error: Error | any) {
+      this.logger.error(`Unable to run job handler (${queueName}/${job.name}): ${error}`, error?.stack, job.data);
+    } finally {
+      this.telemetryRepository.jobs.addToGauge(queueMetric, -1);
     }
-
-    this.configCore.config$.subscribe((config) => {
-      this.logger.debug(`Updating queue concurrency settings`);
-      for (const queueName of Object.values(QueueName)) {
-        let concurrency = 1;
-        if (this.isConcurrentQueue(queueName)) {
-          concurrency = config.job[queueName].concurrency;
-        }
-        this.logger.debug(`Setting ${queueName} concurrency to ${concurrency}`);
-        this.jobRepository.setConcurrency(queueName, concurrency);
-      }
-    });
   }
 
   private isConcurrentQueue(name: QueueName): name is ConcurrentQueueName {
@@ -199,6 +197,7 @@ export class JobService {
       QueueName.FACIAL_RECOGNITION,
       QueueName.STORAGE_TEMPLATE_MIGRATION,
       QueueName.DUPLICATE_DETECTION,
+      QueueName.BACKUP_DATABASE,
     ].includes(name);
   }
 
@@ -238,7 +237,7 @@ export class JobService {
         if (item.data.source === 'sidecar-write') {
           const [asset] = await this.assetRepository.getByIdsWithAllRelations([item.data.id]);
           if (asset) {
-            this.eventRepository.clientSend(ClientEvent.ASSET_UPDATE, asset.ownerId, mapAsset(asset));
+            this.eventRepository.clientSend('on_asset_update', asset.ownerId, mapAsset(asset));
           }
         }
         await this.jobRepository.queue({ name: JobName.LINK_LIVE_PHOTOS, data: item.data });
@@ -252,7 +251,7 @@ export class JobService {
 
       case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: {
         if (item.data.source === 'upload' || item.data.source === 'copy') {
-          await this.jobRepository.queue({ name: JobName.GENERATE_PREVIEW, data: item.data });
+          await this.jobRepository.queue({ name: JobName.GENERATE_THUMBNAILS, data: item.data });
         }
         break;
       }
@@ -261,45 +260,38 @@ export class JobService {
         const { id } = item.data;
         const person = await this.personRepository.getById(id);
         if (person) {
-          this.eventRepository.clientSend(ClientEvent.PERSON_THUMBNAIL, person.ownerId, person.id);
+          this.eventRepository.clientSend('on_person_thumbnail', person.ownerId, person.id);
         }
         break;
       }
 
-      case JobName.GENERATE_PREVIEW: {
-        const jobs: JobItem[] = [
-          { name: JobName.GENERATE_THUMBNAIL, data: item.data },
-          { name: JobName.GENERATE_THUMBHASH, data: item.data },
-        ];
-
-        if (item.data.source === 'upload') {
-          jobs.push({ name: JobName.SMART_SEARCH, data: item.data }, { name: JobName.FACE_DETECTION, data: item.data });
-
-          const [asset] = await this.assetRepository.getByIds([item.data.id]);
-          if (asset) {
-            if (asset.type === AssetType.VIDEO) {
-              jobs.push({ name: JobName.VIDEO_CONVERSION, data: item.data });
-            } else if (asset.livePhotoVideoId) {
-              jobs.push({ name: JobName.VIDEO_CONVERSION, data: { id: asset.livePhotoVideoId } });
-            }
-          }
-        }
-
-        await this.jobRepository.queueAll(jobs);
-        break;
-      }
-
-      case JobName.GENERATE_THUMBNAIL: {
-        if (!(item.data.notify || item.data.source === 'upload')) {
+      case JobName.GENERATE_THUMBNAILS: {
+        if (!item.data.notify && item.data.source !== 'upload') {
           break;
         }
 
         const [asset] = await this.assetRepository.getByIdsWithAllRelations([item.data.id]);
-
-        // Only live-photo motion part will be marked as not visible immediately on upload. Skip notifying clients
-        if (asset && asset.isVisible) {
-          this.eventRepository.clientSend(ClientEvent.UPLOAD_SUCCESS, asset.ownerId, mapAsset(asset));
+        if (!asset) {
+          this.logger.warn(`Could not find asset ${item.data.id} after generating thumbnails`);
+          break;
         }
+
+        const jobs: JobItem[] = [
+          { name: JobName.SMART_SEARCH, data: item.data },
+          { name: JobName.FACE_DETECTION, data: item.data },
+        ];
+
+        if (asset.type === AssetType.VIDEO) {
+          jobs.push({ name: JobName.VIDEO_CONVERSION, data: item.data });
+        } else if (asset.livePhotoVideoId) {
+          jobs.push({ name: JobName.VIDEO_CONVERSION, data: { id: asset.livePhotoVideoId } });
+        }
+
+        await this.jobRepository.queueAll(jobs);
+        if (asset.isVisible) {
+          this.eventRepository.clientSend('on_upload_success', asset.ownerId, mapAsset(asset));
+        }
+
         break;
       }
 
@@ -311,7 +303,7 @@ export class JobService {
       }
 
       case JobName.USER_DELETION: {
-        this.eventRepository.clientBroadcast(ClientEvent.USER_DELETE, item.data.id);
+        this.eventRepository.clientBroadcast('on_user_delete', item.data.id);
         break;
       }
     }
diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts
index 36bdfd05dc..9b944045ab 100644
--- a/server/src/services/library.service.spec.ts
+++ b/server/src/services/library.service.spec.ts
@@ -1,101 +1,80 @@
 import { BadRequestException } from '@nestjs/common';
 import { Stats } from 'node:fs';
-import { SystemConfig } from 'src/config';
-import { SystemConfigCore } from 'src/cores/system-config.core';
+import { defaults, SystemConfig } from 'src/config';
 import { mapLibrary } from 'src/dtos/library.dto';
 import { UserEntity } from 'src/entities/user.entity';
-import { AssetType } from 'src/enum';
+import { AssetType, ImmichWorker } from 'src/enum';
 import { IAssetRepository } from 'src/interfaces/asset.interface';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { IConfigRepository } from 'src/interfaces/config.interface';
+import { ICronRepository } from 'src/interfaces/cron.interface';
 import { IDatabaseRepository } from 'src/interfaces/database.interface';
 import {
   IJobRepository,
+  ILibraryAssetJob,
   ILibraryFileJob,
-  ILibraryOfflineJob,
-  ILibraryRefreshJob,
   JobName,
   JOBS_LIBRARY_PAGINATION_SIZE,
   JobStatus,
 } from 'src/interfaces/job.interface';
 import { ILibraryRepository } from 'src/interfaces/library.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { IStorageRepository } from 'src/interfaces/storage.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { LibraryService } from 'src/services/library.service';
 import { assetStub } from 'test/fixtures/asset.stub';
 import { authStub } from 'test/fixtures/auth.stub';
 import { libraryStub } from 'test/fixtures/library.stub';
 import { systemConfigStub } from 'test/fixtures/system-config.stub';
 import { userStub } from 'test/fixtures/user.stub';
-import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
-import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
-import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
-import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
-import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { makeMockWatcher, newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
+import { makeMockWatcher } from 'test/repositories/storage.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked, vitest } from 'vitest';
 
+async function* mockWalk() {
+  yield await Promise.resolve(['/data/user1/photo.jpg']);
+}
+
 describe(LibraryService.name, () => {
   let sut: LibraryService;
 
   let assetMock: Mocked<IAssetRepository>;
-  let systemMock: Mocked<ISystemMetadataRepository>;
-  let cryptoMock: Mocked<ICryptoRepository>;
+  let configMock: Mocked<IConfigRepository>;
+  let cronMock: Mocked<ICronRepository>;
+  let databaseMock: Mocked<IDatabaseRepository>;
   let jobMock: Mocked<IJobRepository>;
   let libraryMock: Mocked<ILibraryRepository>;
   let storageMock: Mocked<IStorageRepository>;
-  let databaseMock: Mocked<IDatabaseRepository>;
-  let loggerMock: Mocked<ILoggerRepository>;
 
   beforeEach(() => {
-    systemMock = newSystemMetadataRepositoryMock();
-    libraryMock = newLibraryRepositoryMock();
-    assetMock = newAssetRepositoryMock();
-    jobMock = newJobRepositoryMock();
-    cryptoMock = newCryptoRepositoryMock();
-    storageMock = newStorageRepositoryMock();
-    databaseMock = newDatabaseRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-
-    sut = new LibraryService(
-      assetMock,
-      systemMock,
-      cryptoMock,
-      jobMock,
-      libraryMock,
-      storageMock,
-      databaseMock,
-      loggerMock,
-    );
+    ({ sut, assetMock, configMock, cronMock, databaseMock, jobMock, libraryMock, storageMock } =
+      newTestService(LibraryService));
 
     databaseMock.tryLock.mockResolvedValue(true);
+    configMock.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES);
   });
 
   it('should work', () => {
     expect(sut).toBeDefined();
   });
 
-  describe('onBootstrapEvent', () => {
-    it('should init cron job and subscribe to config changes', async () => {
-      systemMock.get.mockResolvedValue(systemConfigStub.libraryScan);
+  describe('onConfigInit', () => {
+    it('should init cron job and handle config changes', async () => {
+      await sut.onConfigInit({ newConfig: defaults });
 
-      await sut.onBootstrap();
-      expect(systemMock.get).toHaveBeenCalled();
-      expect(jobMock.addCronJob).toHaveBeenCalled();
+      expect(cronMock.create).toHaveBeenCalled();
 
-      SystemConfigCore.create(newSystemMetadataRepositoryMock(false), newLoggerRepositoryMock()).config$.next({
-        library: {
-          scan: {
-            enabled: true,
-            cronExpression: '0 1 * * *',
+      await sut.onConfigUpdate({
+        oldConfig: defaults,
+        newConfig: {
+          library: {
+            scan: {
+              enabled: true,
+              cronExpression: '0 1 * * *',
+            },
+            watch: { enabled: false },
           },
-          watch: { enabled: false },
-        },
-      } as SystemConfig);
+        } as SystemConfig,
+      });
 
-      expect(jobMock.updateCronJob).toHaveBeenCalledWith('libraryScan', '0 1 * * *', true);
+      expect(cronMock.update).toHaveBeenCalledWith({ name: 'libraryScan', expression: '0 1 * * *', start: true });
     });
 
     it('should initialize watcher for all external libraries', async () => {
@@ -104,7 +83,6 @@ describe(LibraryService.name, () => {
         libraryStub.externalLibraryWithImportPaths2,
       ]);
 
-      systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
       libraryMock.get.mockImplementation((id) =>
         Promise.resolve(
           [libraryStub.externalLibraryWithImportPaths1, libraryStub.externalLibraryWithImportPaths2].find(
@@ -113,7 +91,7 @@ describe(LibraryService.name, () => {
         ),
       );
 
-      await sut.onBootstrap();
+      await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
 
       expect(storageMock.watch.mock.calls).toEqual(
         expect.arrayContaining([
@@ -124,141 +102,97 @@ describe(LibraryService.name, () => {
     });
 
     it('should not initialize watcher when watching is disabled', async () => {
-      systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
-
-      await sut.onBootstrap();
+      await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchDisabled as SystemConfig });
 
       expect(storageMock.watch).not.toHaveBeenCalled();
     });
 
     it('should not initialize watcher when lock is taken', async () => {
-      systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
       databaseMock.tryLock.mockResolvedValue(false);
 
-      await sut.onBootstrap();
+      await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
 
       expect(storageMock.watch).not.toHaveBeenCalled();
     });
-  });
 
-  describe('onConfigValidateEvent', () => {
-    it('should allow a valid cron expression', () => {
-      expect(() =>
-        sut.onConfigValidate({
-          newConfig: { library: { scan: { cronExpression: '0 0 * * *' } } } as SystemConfig,
-          oldConfig: {} as SystemConfig,
-        }),
-      ).not.toThrow(expect.stringContaining('Invalid cron expression'));
-    });
+    it('should not initialize library scan cron job when lock is taken', async () => {
+      databaseMock.tryLock.mockResolvedValue(false);
 
-    it('should fail for an invalid cron expression', () => {
-      expect(() =>
-        sut.onConfigValidate({
-          newConfig: { library: { scan: { cronExpression: 'foo' } } } as SystemConfig,
-          oldConfig: {} as SystemConfig,
-        }),
-      ).toThrow(/Invalid cron expression.*/);
+      await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
+
+      expect(cronMock.create).not.toHaveBeenCalled();
     });
   });
 
-  describe('handleQueueAssetRefresh', () => {
-    it('should queue refresh of a new asset', async () => {
-      const mockLibraryJob: ILibraryRefreshJob = {
-        id: libraryStub.externalLibrary1.id,
-        refreshModifiedFiles: false,
-        refreshAllFiles: false,
-      };
+  describe('onConfigUpdateEvent', () => {
+    beforeEach(async () => {
+      databaseMock.tryLock.mockResolvedValue(true);
+      await sut.onConfigInit({ newConfig: defaults });
+    });
 
-      assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false });
+    it('should do nothing if instance does not have the watch lock', async () => {
+      databaseMock.tryLock.mockResolvedValue(false);
+      await sut.onConfigInit({ newConfig: defaults });
+      await sut.onConfigUpdate({ newConfig: systemConfigStub.libraryScan as SystemConfig, oldConfig: defaults });
+      expect(cronMock.update).not.toHaveBeenCalled();
+    });
 
-      libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
-      // eslint-disable-next-line @typescript-eslint/require-await
-      storageMock.walk.mockImplementation(async function* generator() {
-        yield ['/data/user1/photo.jpg'];
+    it('should update cron job and enable watching', async () => {
+      libraryMock.getAll.mockResolvedValue([]);
+      await sut.onConfigUpdate({
+        newConfig: systemConfigStub.libraryScanAndWatch as SystemConfig,
+        oldConfig: defaults,
       });
-      assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false });
 
-      await sut.handleQueueAssetRefresh(mockLibraryJob);
+      expect(cronMock.update).toHaveBeenCalledWith({
+        name: 'libraryScan',
+        expression: systemConfigStub.libraryScan.library.scan.cronExpression,
+        start: systemConfigStub.libraryScan.library.scan.enabled,
+      });
+    });
+
+    it('should update cron job and disable watching', async () => {
+      libraryMock.getAll.mockResolvedValue([]);
+      await sut.onConfigUpdate({
+        newConfig: systemConfigStub.libraryScanAndWatch as SystemConfig,
+        oldConfig: defaults,
+      });
+      await sut.onConfigUpdate({
+        newConfig: systemConfigStub.libraryScan as SystemConfig,
+        oldConfig: defaults,
+      });
+
+      expect(cronMock.update).toHaveBeenCalledWith({
+        name: 'libraryScan',
+        expression: systemConfigStub.libraryScan.library.scan.cronExpression,
+        start: systemConfigStub.libraryScan.library.scan.enabled,
+      });
+    });
+  });
+
+  describe('handleQueueSyncFiles', () => {
+    it('should queue refresh of a new asset', async () => {
+      libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
+      storageMock.walk.mockImplementation(mockWalk);
+
+      await sut.handleQueueSyncFiles({ id: libraryStub.externalLibrary1.id });
 
       expect(jobMock.queueAll).toHaveBeenCalledWith([
         {
-          name: JobName.LIBRARY_SCAN_ASSET,
+          name: JobName.LIBRARY_SYNC_FILE,
           data: {
             id: libraryStub.externalLibrary1.id,
             ownerId: libraryStub.externalLibrary1.owner.id,
             assetPath: '/data/user1/photo.jpg',
-            force: false,
-          },
-        },
-      ]);
-    });
-
-    it('should queue offline check of existing online assets', async () => {
-      const mockLibraryJob: ILibraryRefreshJob = {
-        id: libraryStub.externalLibrary1.id,
-        refreshModifiedFiles: false,
-        refreshAllFiles: false,
-      };
-
-      assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false });
-      libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
-      storageMock.walk.mockImplementation(async function* generator() {});
-      assetMock.getWith.mockResolvedValue({ items: [assetStub.external], hasNextPage: false });
-
-      await sut.handleQueueAssetRefresh(mockLibraryJob);
-
-      expect(jobMock.queueAll).toHaveBeenCalledWith([
-        {
-          name: JobName.LIBRARY_CHECK_OFFLINE,
-          data: {
-            id: assetStub.external.id,
-            importPaths: libraryStub.externalLibrary1.importPaths,
-            exclusionPatterns: [],
           },
         },
       ]);
     });
 
     it("should fail when library can't be found", async () => {
-      const mockLibraryJob: ILibraryRefreshJob = {
-        id: libraryStub.externalLibrary1.id,
-        refreshModifiedFiles: false,
-        refreshAllFiles: false,
-      };
-
       libraryMock.get.mockResolvedValue(null);
 
-      await expect(sut.handleQueueAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
-    });
-
-    it('should force queue new assets', async () => {
-      const mockLibraryJob: ILibraryRefreshJob = {
-        id: libraryStub.externalLibrary1.id,
-        refreshModifiedFiles: false,
-        refreshAllFiles: true,
-      };
-
-      assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false });
-      libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
-      // eslint-disable-next-line @typescript-eslint/require-await
-      storageMock.walk.mockImplementation(async function* generator() {
-        yield ['/data/user1/photo.jpg'];
-      });
-      assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false });
-
-      await sut.handleQueueAssetRefresh(mockLibraryJob);
-
-      expect(jobMock.queueAll).toHaveBeenCalledWith([
-        {
-          name: JobName.LIBRARY_SCAN_ASSET,
-          data: {
-            id: libraryStub.externalLibrary1.id,
-            ownerId: libraryStub.externalLibrary1.owner.id,
-            assetPath: '/data/user1/photo.jpg',
-            force: true,
-          },
-        },
-      ]);
+      await expect(sut.handleQueueSyncFiles({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SKIPPED);
     });
 
     it('should ignore import paths that do not exist', async () => {
@@ -274,18 +208,9 @@ describe(LibraryService.name, () => {
 
       storageMock.checkFileExists.mockResolvedValue(true);
 
-      assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false });
-
-      const mockLibraryJob: ILibraryRefreshJob = {
-        id: libraryStub.externalLibraryWithImportPaths1.id,
-        refreshModifiedFiles: false,
-        refreshAllFiles: false,
-      };
-
       libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
-      assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false });
 
-      await sut.handleQueueAssetRefresh(mockLibraryJob);
+      await sut.handleQueueSyncFiles({ id: libraryStub.externalLibraryWithImportPaths1.id });
 
       expect(storageMock.walk).toHaveBeenCalledWith({
         pathsToCrawl: [libraryStub.externalLibraryWithImportPaths1.importPaths[1]],
@@ -296,9 +221,36 @@ describe(LibraryService.name, () => {
     });
   });
 
-  describe('handleOfflineCheck', () => {
+  describe('handleQueueRemoveDeleted', () => {
+    it('should queue online check of existing assets', async () => {
+      libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
+      storageMock.walk.mockImplementation(async function* generator() {});
+      assetMock.getAll.mockResolvedValue({ items: [assetStub.external], hasNextPage: false });
+
+      await sut.handleQueueSyncAssets({ id: libraryStub.externalLibrary1.id });
+
+      expect(jobMock.queueAll).toHaveBeenCalledWith([
+        {
+          name: JobName.LIBRARY_SYNC_ASSET,
+          data: {
+            id: assetStub.external.id,
+            importPaths: libraryStub.externalLibrary1.importPaths,
+            exclusionPatterns: [],
+          },
+        },
+      ]);
+    });
+
+    it("should fail when library can't be found", async () => {
+      libraryMock.get.mockResolvedValue(null);
+
+      await expect(sut.handleQueueSyncAssets({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SKIPPED);
+    });
+  });
+
+  describe('handleSyncAsset', () => {
     it('should skip missing assets', async () => {
-      const mockAssetJob: ILibraryOfflineJob = {
+      const mockAssetJob: ILibraryAssetJob = {
         id: assetStub.external.id,
         importPaths: ['/'],
         exclusionPatterns: [],
@@ -306,41 +258,31 @@ describe(LibraryService.name, () => {
 
       assetMock.getById.mockResolvedValue(null);
 
-      await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SKIPPED);
+      await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SKIPPED);
 
-      expect(assetMock.update).not.toHaveBeenCalled();
-    });
-
-    it('should do nothing with already-offline assets', async () => {
-      const mockAssetJob: ILibraryOfflineJob = {
-        id: assetStub.external.id,
-        importPaths: ['/'],
-        exclusionPatterns: [],
-      };
-
-      assetMock.getById.mockResolvedValue(assetStub.offline);
-
-      await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
-
-      expect(assetMock.update).not.toHaveBeenCalled();
+      expect(assetMock.remove).not.toHaveBeenCalled();
     });
 
     it('should offline assets no longer on disk', async () => {
-      const mockAssetJob: ILibraryOfflineJob = {
+      const mockAssetJob: ILibraryAssetJob = {
         id: assetStub.external.id,
         importPaths: ['/'],
         exclusionPatterns: [],
       };
 
       assetMock.getById.mockResolvedValue(assetStub.external);
+      storageMock.stat.mockRejectedValue(new Error('ENOENT, no such file or directory'));
 
-      await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
+      await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
 
-      expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true });
+      expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.external.id], {
+        isOffline: true,
+        deletedAt: expect.any(Date),
+      });
     });
 
     it('should offline assets matching an exclusion pattern', async () => {
-      const mockAssetJob: ILibraryOfflineJob = {
+      const mockAssetJob: ILibraryAssetJob = {
         id: assetStub.external.id,
         importPaths: ['/'],
         exclusionPatterns: ['**/user1/**'],
@@ -348,13 +290,15 @@ describe(LibraryService.name, () => {
 
       assetMock.getById.mockResolvedValue(assetStub.external);
 
-      await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
-
-      expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true });
+      await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
+      expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.external.id], {
+        isOffline: true,
+        deletedAt: expect.any(Date),
+      });
     });
 
     it('should set assets outside of import paths as offline', async () => {
-      const mockAssetJob: ILibraryOfflineJob = {
+      const mockAssetJob: ILibraryAssetJob = {
         id: assetStub.external.id,
         importPaths: ['/data/user2'],
         exclusionPatterns: [],
@@ -363,28 +307,74 @@ describe(LibraryService.name, () => {
       assetMock.getById.mockResolvedValue(assetStub.external);
       storageMock.checkFileExists.mockResolvedValue(true);
 
-      await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
+      await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
 
-      expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true });
+      expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.external.id], {
+        isOffline: true,
+        deletedAt: expect.any(Date),
+      });
     });
 
     it('should do nothing with online assets', async () => {
-      const mockAssetJob: ILibraryOfflineJob = {
+      const mockAssetJob: ILibraryAssetJob = {
         id: assetStub.external.id,
         importPaths: ['/'],
         exclusionPatterns: [],
       };
 
       assetMock.getById.mockResolvedValue(assetStub.external);
-      storageMock.checkFileExists.mockResolvedValue(true);
+      storageMock.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats);
 
-      await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
+      await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
 
-      expect(assetMock.update).not.toHaveBeenCalled();
+      expect(assetMock.updateAll).not.toHaveBeenCalled();
+    });
+
+    it('should un-trash an asset previously marked as offline', async () => {
+      const mockAssetJob: ILibraryAssetJob = {
+        id: assetStub.external.id,
+        importPaths: ['/'],
+        exclusionPatterns: [],
+      };
+
+      assetMock.getById.mockResolvedValue(assetStub.trashedOffline);
+      storageMock.stat.mockResolvedValue({ mtime: assetStub.trashedOffline.fileModifiedAt } as Stats);
+
+      await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
+
+      expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.trashedOffline.id], {
+        deletedAt: null,
+        fileCreatedAt: assetStub.trashedOffline.fileModifiedAt,
+        fileModifiedAt: assetStub.trashedOffline.fileModifiedAt,
+        isOffline: false,
+        originalFileName: 'path.jpg',
+      });
     });
   });
 
-  describe('handleAssetRefresh', () => {
+  it('should update file when mtime has changed', async () => {
+    const mockAssetJob: ILibraryAssetJob = {
+      id: assetStub.external.id,
+      importPaths: ['/'],
+      exclusionPatterns: [],
+    };
+
+    const newMTime = new Date();
+    assetMock.getById.mockResolvedValue(assetStub.external);
+    storageMock.stat.mockResolvedValue({ mtime: newMTime } as Stats);
+
+    await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
+
+    expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.external.id], {
+      fileModifiedAt: newMTime,
+      fileCreatedAt: newMTime,
+      isOffline: false,
+      originalFileName: 'photo.jpg',
+      deletedAt: null,
+    });
+  });
+
+  describe('handleSyncFile', () => {
     let mockUser: UserEntity;
 
     beforeEach(() => {
@@ -397,42 +387,18 @@ describe(LibraryService.name, () => {
       } as Stats);
     });
 
-    it('should reject an unknown file extension', async () => {
-      const mockLibraryJob: ILibraryFileJob = {
-        id: libraryStub.externalLibrary1.id,
-        ownerId: mockUser.id,
-        assetPath: '/data/user1/file.xyz',
-        force: false,
-      };
-
-      assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
-
-      await expect(sut.handleAssetRefresh(mockLibraryJob)).rejects.toBeInstanceOf(BadRequestException);
-    });
-
-    it('should reject an unknown file type', async () => {
-      const mockLibraryJob: ILibraryFileJob = {
-        id: libraryStub.externalLibrary1.id,
-        ownerId: mockUser.id,
-        assetPath: '/data/user1/file.xyz',
-        force: false,
-      };
-
-      await expect(sut.handleAssetRefresh(mockLibraryJob)).rejects.toBeInstanceOf(BadRequestException);
-    });
-
-    it('should add a new image', async () => {
+    it('should import a new asset', async () => {
       const mockLibraryJob: ILibraryFileJob = {
         id: libraryStub.externalLibrary1.id,
         ownerId: mockUser.id,
         assetPath: '/data/user1/photo.jpg',
-        force: false,
       };
 
       assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
       assetMock.create.mockResolvedValue(assetStub.image);
+      libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
 
-      await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
+      await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
 
       expect(assetMock.create.mock.calls).toEqual([
         [
@@ -448,7 +414,6 @@ describe(LibraryService.name, () => {
             localDateTime: expect.any(Date),
             type: AssetType.IMAGE,
             originalFileName: 'photo.jpg',
-            sidecarPath: null,
             isExternal: true,
           },
         ],
@@ -457,75 +422,27 @@ describe(LibraryService.name, () => {
       expect(jobMock.queue.mock.calls).toEqual([
         [
           {
-            name: JobName.METADATA_EXTRACTION,
+            name: JobName.SIDECAR_DISCOVERY,
             data: {
               id: assetStub.image.id,
-              source: 'upload',
             },
           },
         ],
       ]);
     });
 
-    it('should add a new image with sidecar', async () => {
-      const mockLibraryJob: ILibraryFileJob = {
-        id: libraryStub.externalLibrary1.id,
-        ownerId: mockUser.id,
-        assetPath: '/data/user1/photo.jpg',
-        force: false,
-      };
-
-      assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
-      assetMock.create.mockResolvedValue(assetStub.image);
-      storageMock.checkFileExists.mockResolvedValue(true);
-
-      await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
-
-      expect(assetMock.create.mock.calls).toEqual([
-        [
-          {
-            ownerId: mockUser.id,
-            libraryId: libraryStub.externalLibrary1.id,
-            checksum: expect.any(Buffer),
-            originalPath: '/data/user1/photo.jpg',
-            deviceAssetId: expect.any(String),
-            deviceId: 'Library Import',
-            fileCreatedAt: expect.any(Date),
-            fileModifiedAt: expect.any(Date),
-            localDateTime: expect.any(Date),
-            type: AssetType.IMAGE,
-            originalFileName: 'photo.jpg',
-            sidecarPath: '/data/user1/photo.jpg.xmp',
-            isExternal: true,
-          },
-        ],
-      ]);
-
-      expect(jobMock.queue.mock.calls).toEqual([
-        [
-          {
-            name: JobName.METADATA_EXTRACTION,
-            data: {
-              id: assetStub.image.id,
-              source: 'upload',
-            },
-          },
-        ],
-      ]);
-    });
-
-    it('should add a new video', async () => {
+    it('should import a new video', async () => {
       const mockLibraryJob: ILibraryFileJob = {
         id: libraryStub.externalLibrary1.id,
         ownerId: mockUser.id,
         assetPath: '/data/user1/video.mp4',
-        force: false,
       };
 
       assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
       assetMock.create.mockResolvedValue(assetStub.video);
+      libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
 
-      await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
+      await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
 
       expect(assetMock.create.mock.calls).toEqual([
         [
@@ -541,7 +458,6 @@ describe(LibraryService.name, () => {
             localDateTime: expect.any(Date),
             type: AssetType.VIDEO,
             originalFileName: 'video.mp4',
-            sidecarPath: null,
             isExternal: true,
           },
         ],
@@ -550,47 +466,36 @@ describe(LibraryService.name, () => {
       expect(jobMock.queue.mock.calls).toEqual([
         [
           {
-            name: JobName.METADATA_EXTRACTION,
+            name: JobName.SIDECAR_DISCOVERY,
             data: {
               id: assetStub.image.id,
-              source: 'upload',
-            },
-          },
-        ],
-        [
-          {
-            name: JobName.VIDEO_CONVERSION,
-            data: {
-              id: assetStub.video.id,
             },
           },
         ],
       ]);
     });
 
-    it('should not add an image to a soft deleted library', async () => {
+    it('should not import an asset to a soft deleted library', async () => {
       const mockLibraryJob: ILibraryFileJob = {
         id: libraryStub.externalLibrary1.id,
         ownerId: mockUser.id,
         assetPath: '/data/user1/photo.jpg',
-        force: false,
       };
 
       assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
       assetMock.create.mockResolvedValue(assetStub.image);
       libraryMock.get.mockResolvedValue({ ...libraryStub.externalLibrary1, deletedAt: new Date() });
 
-      await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.FAILED);
+      await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.FAILED);
 
       expect(assetMock.create.mock.calls).toEqual([]);
     });
 
-    it('should not import an asset when mtime matches db asset', async () => {
+    it('should not refresh a file whose mtime matches existing asset', async () => {
       const mockLibraryJob: ILibraryFileJob = {
         id: libraryStub.externalLibrary1.id,
         ownerId: mockUser.id,
         assetPath: assetStub.hasFileExtension.originalPath,
-        force: false,
       };
 
       storageMock.stat.mockResolvedValue({
@@ -601,190 +506,73 @@ describe(LibraryService.name, () => {
 
       assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.hasFileExtension);
 
-      await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
+      await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
 
       expect(jobMock.queue).not.toHaveBeenCalled();
       expect(jobMock.queueAll).not.toHaveBeenCalled();
     });
 
-    it('should import an asset when mtime differs from db asset', async () => {
+    it('should skip existing asset', async () => {
       const mockLibraryJob: ILibraryFileJob = {
         id: libraryStub.externalLibrary1.id,
         ownerId: mockUser.id,
         assetPath: '/data/user1/photo.jpg',
-        force: false,
       };
 
       assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
-      assetMock.create.mockResolvedValue(assetStub.image);
 
-      await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
-
-      expect(jobMock.queue).toHaveBeenCalledWith({
-        name: JobName.METADATA_EXTRACTION,
-        data: {
-          id: assetStub.image.id,
-          source: 'upload',
-        },
-      });
-
-      expect(jobMock.queue).not.toHaveBeenCalledWith({
-        name: JobName.VIDEO_CONVERSION,
-        data: {
-          id: assetStub.image.id,
-        },
-      });
+      await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
     });
 
-    it('should import an asset that is missing a file extension', async () => {
-      // This tests for the case where the file extension is missing from the asset path.
-      // This happened in previous versions of Immich
+    it('should not refresh an asset trashed by user', async () => {
       const mockLibraryJob: ILibraryFileJob = {
         id: libraryStub.externalLibrary1.id,
         ownerId: mockUser.id,
-        assetPath: assetStub.missingFileExtension.originalPath,
-        force: false,
-      };
-
-      assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.missingFileExtension);
-
-      await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
-
-      expect(assetMock.updateAll).toHaveBeenCalledWith(
-        [assetStub.missingFileExtension.id],
-        expect.objectContaining({ originalFileName: 'photo.jpg' }),
-      );
-    });
-
-    it('should set a missing asset to offline', async () => {
-      storageMock.stat.mockRejectedValue(new Error('Path not found'));
-
-      const mockLibraryJob: ILibraryFileJob = {
-        id: assetStub.image.id,
-        ownerId: mockUser.id,
-        assetPath: '/data/user1/photo.jpg',
-        force: false,
-      };
-
-      assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
-      assetMock.create.mockResolvedValue(assetStub.image);
-
-      await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
-
-      expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, isOffline: true });
-      expect(jobMock.queue).not.toHaveBeenCalled();
-      expect(jobMock.queueAll).not.toHaveBeenCalled();
-    });
-
-    it('should online a previously-offline asset', async () => {
-      const mockLibraryJob: ILibraryFileJob = {
-        id: assetStub.offline.id,
-        ownerId: mockUser.id,
-        assetPath: '/data/user1/photo.jpg',
-        force: false,
-      };
-
-      assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.offline);
-      assetMock.create.mockResolvedValue(assetStub.offline);
-
-      await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
-
-      expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.offline.id, isOffline: false });
-
-      expect(jobMock.queue).toHaveBeenCalledWith({
-        name: JobName.METADATA_EXTRACTION,
-        data: {
-          id: assetStub.offline.id,
-          source: 'upload',
-        },
-      });
-
-      expect(jobMock.queue).not.toHaveBeenCalledWith({
-        name: JobName.VIDEO_CONVERSION,
-        data: {
-          id: assetStub.offline.id,
-        },
-      });
-    });
-
-    it('should do nothing when mtime matches existing asset', async () => {
-      const mockLibraryJob: ILibraryFileJob = {
-        id: assetStub.image.id,
-        ownerId: assetStub.image.ownerId,
-        assetPath: '/data/user1/photo.jpg',
-        force: false,
-      };
-
-      assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
-      assetMock.create.mockResolvedValue(assetStub.image);
-
-      expect(assetMock.update).not.toHaveBeenCalled();
-
-      await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
-    });
-
-    it('should refresh an existing asset if forced', async () => {
-      const mockLibraryJob: ILibraryFileJob = {
-        id: assetStub.image.id,
-        ownerId: assetStub.hasFileExtension.ownerId,
         assetPath: assetStub.hasFileExtension.originalPath,
-        force: true,
       };
 
-      assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.hasFileExtension);
-      assetMock.create.mockResolvedValue(assetStub.hasFileExtension);
+      assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.trashed);
 
-      await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
+      await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
 
-      expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.hasFileExtension.id], {
-        fileCreatedAt: new Date('2023-01-01'),
-        fileModifiedAt: new Date('2023-01-01'),
-        originalFileName: assetStub.hasFileExtension.originalFileName,
-      });
+      expect(jobMock.queue).not.toHaveBeenCalled();
+      expect(jobMock.queueAll).not.toHaveBeenCalled();
     });
 
-    it('should refresh an existing asset with modified mtime', async () => {
-      const filemtime = new Date();
-      filemtime.setSeconds(assetStub.image.fileModifiedAt.getSeconds() + 10);
+    it('should fail when the file could not be read', async () => {
+      storageMock.stat.mockRejectedValue(new Error('Could not read file'));
 
       const mockLibraryJob: ILibraryFileJob = {
         id: libraryStub.externalLibrary1.id,
         ownerId: userStub.admin.id,
         assetPath: '/data/user1/photo.jpg',
-        force: false,
       };
 
-      storageMock.stat.mockResolvedValue({
-        size: 100,
-        mtime: filemtime,
-        ctime: new Date('2023-01-01'),
-      } as Stats);
-
       assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
       assetMock.create.mockResolvedValue(assetStub.image);
 
-      await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
-
-      expect(assetMock.create).toHaveBeenCalled();
-      const createdAsset = assetMock.create.mock.calls[0][0];
-
-      expect(createdAsset.fileModifiedAt).toEqual(filemtime);
+      await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.FAILED);
+      expect(libraryMock.get).not.toHaveBeenCalled();
+      expect(assetMock.create).not.toHaveBeenCalled();
     });
 
-    it('should throw error when asset does not exist', async () => {
-      storageMock.stat.mockRejectedValue(new Error("ENOENT, no such file or directory '/data/user1/photo.jpg'"));
+    it('should skip if the file could not be found', async () => {
+      const error = new Error('File not found') as any;
+      error.code = 'ENOENT';
+      storageMock.stat.mockRejectedValue(error);
 
       const mockLibraryJob: ILibraryFileJob = {
         id: libraryStub.externalLibrary1.id,
         ownerId: userStub.admin.id,
         assetPath: '/data/user1/photo.jpg',
-        force: false,
       };
 
       assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
       assetMock.create.mockResolvedValue(assetStub.image);
 
-      await expect(sut.handleAssetRefresh(mockLibraryJob)).rejects.toBeInstanceOf(BadRequestException);
+      await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
+      expect(libraryMock.get).not.toHaveBeenCalled();
+      expect(assetMock.create).not.toHaveBeenCalled();
     });
   });
 
@@ -822,12 +610,10 @@ describe(LibraryService.name, () => {
       libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
       libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
 
-      systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
-
       const mockClose = vitest.fn();
       storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
 
-      await sut.onBootstrap();
+      await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
       await sut.delete(libraryStub.externalLibraryWithImportPaths1.id);
 
       expect(mockClose).toHaveBeenCalled();
@@ -857,7 +643,6 @@ describe(LibraryService.name, () => {
 
   describe('getStatistics', () => {
     it('should return library statistics', async () => {
-      libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
       libraryMock.getStatistics.mockResolvedValue({ photos: 10, videos: 0, total: 10, usage: 1337 });
       await expect(sut.getStatistics(libraryStub.externalLibrary1.id)).resolves.toEqual({
         photos: 10,
@@ -868,6 +653,10 @@ describe(LibraryService.name, () => {
 
       expect(libraryMock.getStatistics).toHaveBeenCalledWith(libraryStub.externalLibrary1.id);
     });
+
+    it('should throw an error if the library could not be found', async () => {
+      await expect(sut.getStatistics('foo')).rejects.toBeInstanceOf(BadRequestException);
+    });
   });
 
   describe('create', () => {
@@ -953,12 +742,11 @@ describe(LibraryService.name, () => {
       });
 
       it('should create watched with import paths', async () => {
-        systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
         libraryMock.create.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
         libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
         libraryMock.getAll.mockResolvedValue([]);
 
-        await sut.onBootstrap();
+        await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
         await sut.create({
           ownerId: authStub.admin.user.id,
           importPaths: libraryStub.externalLibraryWithImportPaths1.importPaths,
@@ -997,6 +785,13 @@ describe(LibraryService.name, () => {
     });
   });
 
+  describe('getAll', () => {
+    it('should get all libraries', async () => {
+      libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]);
+      await expect(sut.getAll()).resolves.toEqual([expect.objectContaining({ id: libraryStub.externalLibrary1.id })]);
+    });
+  });
+
   describe('handleQueueCleanup', () => {
     it('should queue cleanup jobs', async () => {
       libraryMock.getAllDeleted.mockResolvedValue([libraryStub.externalLibrary1, libraryStub.externalLibrary2]);
@@ -1011,26 +806,48 @@ describe(LibraryService.name, () => {
 
   describe('update', () => {
     beforeEach(async () => {
-      systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
       libraryMock.getAll.mockResolvedValue([]);
 
-      await sut.onBootstrap();
+      await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
+    });
+
+    it('should throw an error if an import path is invalid', async () => {
+      libraryMock.update.mockResolvedValue(libraryStub.externalLibrary1);
+      libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
+
+      await expect(sut.update('library-id', { importPaths: ['foo/bar'] })).rejects.toBeInstanceOf(BadRequestException);
+      expect(libraryMock.update).not.toHaveBeenCalled();
     });
 
     it('should update library', async () => {
       libraryMock.update.mockResolvedValue(libraryStub.externalLibrary1);
       libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
-      await expect(sut.update('library-id', {})).resolves.toEqual(mapLibrary(libraryStub.externalLibrary1));
+      storageMock.stat.mockResolvedValue({ isDirectory: () => true } as Stats);
+      storageMock.checkFileExists.mockResolvedValue(true);
+
+      const cwd = process.cwd();
+
+      await expect(sut.update('library-id', { importPaths: [`${cwd}/foo/bar`] })).resolves.toEqual(
+        mapLibrary(libraryStub.externalLibrary1),
+      );
       expect(libraryMock.update).toHaveBeenCalledWith(expect.objectContaining({ id: 'library-id' }));
     });
   });
 
+  describe('onShutdown', () => {
+    it('should do nothing if instance does not have the watch lock', async () => {
+      await sut.onShutdown();
+    });
+  });
+
   describe('watchAll', () => {
+    it('should return false if instance does not have the watch lock', async () => {
+      await expect(sut.watchAll()).resolves.toBe(false);
+    });
+
     describe('watching disabled', () => {
       beforeEach(async () => {
-        systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
-
-        await sut.onBootstrap();
+        await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchDisabled as SystemConfig });
       });
 
       it('should not watch library', async () => {
@@ -1044,9 +861,8 @@ describe(LibraryService.name, () => {
 
     describe('watching enabled', () => {
       beforeEach(async () => {
-        systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
         libraryMock.getAll.mockResolvedValue([]);
-        await sut.onBootstrap();
+        await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
       });
 
       it('should watch library', async () => {
@@ -1086,26 +902,30 @@ describe(LibraryService.name, () => {
       it('should handle a new file event', async () => {
         libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
         libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
+        assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
         storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/foo/photo.jpg' }] }));
 
         await sut.watchAll();
 
         expect(jobMock.queueAll).toHaveBeenCalledWith([
           {
-            name: JobName.LIBRARY_SCAN_ASSET,
+            name: JobName.LIBRARY_SYNC_FILE,
             data: {
               id: libraryStub.externalLibraryWithImportPaths1.id,
               assetPath: '/foo/photo.jpg',
               ownerId: libraryStub.externalLibraryWithImportPaths1.owner.id,
-              force: false,
             },
           },
         ]);
+        expect(jobMock.queueAll).toHaveBeenCalledWith([
+          { name: JobName.LIBRARY_SYNC_ASSET, data: expect.objectContaining({ id: assetStub.image.id }) },
+        ]);
       });
 
       it('should handle a file change event', async () => {
         libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
         libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
+        assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
         storageMock.watch.mockImplementation(
           makeMockWatcher({ items: [{ event: 'change', value: '/foo/photo.jpg' }] }),
         );
@@ -1114,28 +934,32 @@ describe(LibraryService.name, () => {
 
         expect(jobMock.queueAll).toHaveBeenCalledWith([
           {
-            name: JobName.LIBRARY_SCAN_ASSET,
+            name: JobName.LIBRARY_SYNC_FILE,
             data: {
               id: libraryStub.externalLibraryWithImportPaths1.id,
               assetPath: '/foo/photo.jpg',
               ownerId: libraryStub.externalLibraryWithImportPaths1.owner.id,
-              force: false,
             },
           },
         ]);
+        expect(jobMock.queueAll).toHaveBeenCalledWith([
+          { name: JobName.LIBRARY_SYNC_ASSET, data: expect.objectContaining({ id: assetStub.image.id }) },
+        ]);
       });
 
       it('should handle a file unlink event', async () => {
         libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
         libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
-        assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external);
+        assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
         storageMock.watch.mockImplementation(
           makeMockWatcher({ items: [{ event: 'unlink', value: '/foo/photo.jpg' }] }),
         );
 
         await sut.watchAll();
 
-        expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true });
+        expect(jobMock.queueAll).toHaveBeenCalledWith([
+          { name: JobName.LIBRARY_SYNC_ASSET, data: expect.objectContaining({ id: assetStub.image.id }) },
+        ]);
       });
 
       it('should handle an error event', async () => {
@@ -1190,7 +1014,6 @@ describe(LibraryService.name, () => {
         libraryStub.externalLibraryWithImportPaths2,
       ]);
 
-      systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
       libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
 
       libraryMock.get.mockImplementation((id) =>
@@ -1204,7 +1027,7 @@ describe(LibraryService.name, () => {
       const mockClose = vitest.fn();
       storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
 
-      await sut.onBootstrap();
+      await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
       await sut.onShutdown();
 
       expect(mockClose).toHaveBeenCalledTimes(2);
@@ -1215,15 +1038,14 @@ describe(LibraryService.name, () => {
     it('should delete an empty library', async () => {
       libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
       assetMock.getAll.mockResolvedValue({ items: [], hasNextPage: false });
-      libraryMock.delete.mockImplementation(async () => {});
 
       await expect(sut.handleDeleteLibrary({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
+      expect(libraryMock.delete).toHaveBeenCalled();
     });
 
-    it('should delete a library with assets', async () => {
+    it('should delete all assets in a library', async () => {
       libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
       assetMock.getAll.mockResolvedValue({ items: [assetStub.image1], hasNextPage: false });
-      libraryMock.delete.mockImplementation(async () => {});
 
       assetMock.getById.mockResolvedValue(assetStub.image1);
 
@@ -1232,72 +1054,23 @@ describe(LibraryService.name, () => {
   });
 
   describe('queueScan', () => {
-    it('should queue a library scan of external library', async () => {
+    it('should queue a library scan', async () => {
       libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
 
-      await sut.queueScan(libraryStub.externalLibrary1.id, {});
+      await sut.queueScan(libraryStub.externalLibrary1.id);
 
       expect(jobMock.queue.mock.calls).toEqual([
         [
           {
-            name: JobName.LIBRARY_SCAN,
+            name: JobName.LIBRARY_QUEUE_SYNC_FILES,
             data: {
               id: libraryStub.externalLibrary1.id,
-              refreshModifiedFiles: false,
-              refreshAllFiles: false,
             },
           },
         ],
-      ]);
-    });
-
-    it('should queue a library scan of all modified assets', async () => {
-      libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
-
-      await sut.queueScan(libraryStub.externalLibrary1.id, { refreshModifiedFiles: true });
-
-      expect(jobMock.queue.mock.calls).toEqual([
         [
           {
-            name: JobName.LIBRARY_SCAN,
-            data: {
-              id: libraryStub.externalLibrary1.id,
-              refreshModifiedFiles: true,
-              refreshAllFiles: false,
-            },
-          },
-        ],
-      ]);
-    });
-
-    it('should queue a forced library scan', async () => {
-      libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
-
-      await sut.queueScan(libraryStub.externalLibrary1.id, { refreshAllFiles: true });
-
-      expect(jobMock.queue.mock.calls).toEqual([
-        [
-          {
-            name: JobName.LIBRARY_SCAN,
-            data: {
-              id: libraryStub.externalLibrary1.id,
-              refreshModifiedFiles: false,
-              refreshAllFiles: true,
-            },
-          },
-        ],
-      ]);
-    });
-  });
-
-  describe('queueEmptyTrash', () => {
-    it('should queue the trash job', async () => {
-      await sut.queueRemoveOffline(libraryStub.externalLibrary1.id);
-
-      expect(jobMock.queue.mock.calls).toEqual([
-        [
-          {
-            name: JobName.LIBRARY_REMOVE_OFFLINE,
+            name: JobName.LIBRARY_QUEUE_SYNC_ASSETS,
             data: {
               id: libraryStub.externalLibrary1.id,
             },
@@ -1311,7 +1084,7 @@ describe(LibraryService.name, () => {
     it('should queue the refresh job', async () => {
       libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]);
 
-      await expect(sut.handleQueueAllScan({})).resolves.toBe(JobStatus.SUCCESS);
+      await expect(sut.handleQueueSyncAll()).resolves.toBe(JobStatus.SUCCESS);
 
       expect(jobMock.queue.mock.calls).toEqual([
         [
@@ -1323,53 +1096,41 @@ describe(LibraryService.name, () => {
       ]);
       expect(jobMock.queueAll).toHaveBeenCalledWith([
         {
-          name: JobName.LIBRARY_SCAN,
+          name: JobName.LIBRARY_QUEUE_SYNC_FILES,
           data: {
             id: libraryStub.externalLibrary1.id,
-            refreshModifiedFiles: true,
-            refreshAllFiles: false,
-          },
-        },
-      ]);
-    });
-
-    it('should queue the force refresh job', async () => {
-      libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]);
-
-      await expect(sut.handleQueueAllScan({ force: true })).resolves.toBe(JobStatus.SUCCESS);
-
-      expect(jobMock.queue).toHaveBeenCalledWith({
-        name: JobName.LIBRARY_QUEUE_CLEANUP,
-        data: {},
-      });
-
-      expect(jobMock.queueAll).toHaveBeenCalledWith([
-        {
-          name: JobName.LIBRARY_SCAN,
-          data: {
-            id: libraryStub.externalLibrary1.id,
-            refreshModifiedFiles: false,
-            refreshAllFiles: true,
           },
         },
       ]);
     });
   });
 
-  describe('handleRemoveOfflineFiles', () => {
-    it('should queue trash deletion jobs', async () => {
-      assetMock.getWith.mockResolvedValue({ items: [assetStub.image1], hasNextPage: false });
+  describe('handleQueueAssetOfflineCheck', () => {
+    it('should queue removal jobs', async () => {
+      libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
+      assetMock.getAll.mockResolvedValue({ items: [assetStub.image1], hasNextPage: false });
       assetMock.getById.mockResolvedValue(assetStub.image1);
 
-      await expect(sut.handleRemoveOffline({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
+      await expect(sut.handleQueueSyncAssets({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
 
       expect(jobMock.queueAll).toHaveBeenCalledWith([
-        { name: JobName.ASSET_DELETION, data: { id: assetStub.image1.id, deleteOnDisk: false } },
+        {
+          name: JobName.LIBRARY_SYNC_ASSET,
+          data: {
+            id: assetStub.image1.id,
+            importPaths: libraryStub.externalLibrary1.importPaths,
+            exclusionPatterns: libraryStub.externalLibrary1.exclusionPatterns,
+          },
+        },
       ]);
     });
   });
 
   describe('validate', () => {
+    it('should not require import paths', async () => {
+      await expect(sut.validate('library-id', {})).resolves.toEqual({ importPaths: [] });
+    });
+
     it('should validate directory', async () => {
       storageMock.stat.mockResolvedValue({
         isDirectory: () => true,
@@ -1455,14 +1216,31 @@ describe(LibraryService.name, () => {
       });
     });
 
+    it('should detect when import path is not absolute', async () => {
+      const cwd = process.cwd();
+
+      await expect(sut.validate('library-id', { importPaths: ['relative/path'] })).resolves.toEqual({
+        importPaths: [
+          {
+            importPath: 'relative/path',
+            isValid: false,
+            message: `Import path must be absolute, try ${cwd}/relative/path`,
+          },
+        ],
+      });
+    });
+
     it('should detect when import path is in immich media folder', async () => {
       storageMock.stat.mockResolvedValue({ isDirectory: () => true } as Stats);
-      const validImport = libraryStub.hasImmichPaths.importPaths[1];
+      const cwd = process.cwd();
+
+      const validImport = `${cwd}/${libraryStub.hasImmichPaths.importPaths[1]}`;
       storageMock.checkFileExists.mockImplementation((importPath) => Promise.resolve(importPath === validImport));
 
-      await expect(
-        sut.validate('library-id', { importPaths: libraryStub.hasImmichPaths.importPaths }),
-      ).resolves.toEqual({
+      const pathStubs = libraryStub.hasImmichPaths.importPaths;
+      const importPaths = [pathStubs[0], validImport, pathStubs[2]];
+
+      await expect(sut.validate('library-id', { importPaths })).resolves.toEqual({
         importPaths: [
           {
             importPath: libraryStub.hasImmichPaths.importPaths[0],
diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts
index 3dd81dd613..0deddc8941 100644
--- a/server/src/services/library.service.ts
+++ b/server/src/services/library.service.ts
@@ -1,111 +1,78 @@
-import { BadRequestException, Inject, Injectable } from '@nestjs/common';
+import { BadRequestException, Injectable } from '@nestjs/common';
 import { R_OK } from 'node:constants';
-import { Stats } from 'node:fs';
-import path, { basename, parse } from 'node:path';
+import path, { basename, isAbsolute, parse } from 'node:path';
 import picomatch from 'picomatch';
 import { StorageCore } from 'src/cores/storage.core';
-import { SystemConfigCore } from 'src/cores/system-config.core';
-import { OnEmit } from 'src/decorators';
+import { OnEvent, OnJob } from 'src/decorators';
 import {
   CreateLibraryDto,
   LibraryResponseDto,
   LibraryStatsResponseDto,
-  ScanLibraryDto,
+  mapLibrary,
   UpdateLibraryDto,
   ValidateLibraryDto,
   ValidateLibraryImportPathResponseDto,
   ValidateLibraryResponseDto,
-  mapLibrary,
 } from 'src/dtos/library.dto';
-import { AssetType } from 'src/enum';
-import { IAssetRepository, WithProperty } from 'src/interfaces/asset.interface';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
+import { AssetEntity } from 'src/entities/asset.entity';
+import { LibraryEntity } from 'src/entities/library.entity';
+import { AssetType, ImmichWorker } from 'src/enum';
+import { DatabaseLock } from 'src/interfaces/database.interface';
 import { ArgOf } from 'src/interfaces/event.interface';
-import {
-  IBaseJob,
-  IEntityJob,
-  IJobRepository,
-  ILibraryFileJob,
-  ILibraryOfflineJob,
-  ILibraryRefreshJob,
-  JOBS_LIBRARY_PAGINATION_SIZE,
-  JobName,
-  JobStatus,
-} from 'src/interfaces/job.interface';
-import { ILibraryRepository } from 'src/interfaces/library.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { IStorageRepository } from 'src/interfaces/storage.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { JobName, JobOf, JOBS_LIBRARY_PAGINATION_SIZE, JobStatus, QueueName } from 'src/interfaces/job.interface';
+import { BaseService } from 'src/services/base.service';
 import { mimeTypes } from 'src/utils/mime-types';
 import { handlePromiseError } from 'src/utils/misc';
 import { usePagination } from 'src/utils/pagination';
-import { validateCronExpression } from 'src/validation';
 
 @Injectable()
-export class LibraryService {
-  private configCore: SystemConfigCore;
+export class LibraryService extends BaseService {
   private watchLibraries = false;
-  private watchLock = false;
+  private lock = false;
   private watchers: Record<string, () => Promise<void>> = {};
 
-  constructor(
-    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
-    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
-    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
-    @Inject(IJobRepository) private jobRepository: IJobRepository,
-    @Inject(ILibraryRepository) private repository: ILibraryRepository,
-    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
-    @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-  ) {
-    this.logger.setContext(LibraryService.name);
-    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
-  }
-
-  @OnEmit({ event: 'app.bootstrap' })
-  async onBootstrap() {
-    const config = await this.configCore.getConfig({ withCache: false });
-
-    const { watch, scan } = config.library;
-
+  @OnEvent({ name: 'config.init', workers: [ImmichWorker.MICROSERVICES] })
+  async onConfigInit({
+    newConfig: {
+      library: { watch, scan },
+    },
+  }: ArgOf<'config.init'>) {
     // This ensures that library watching only occurs in one microservice
-    // TODO: we could make the lock be per-library instead of global
-    this.watchLock = await this.databaseRepository.tryLock(DatabaseLock.LibraryWatch);
+    this.lock = await this.databaseRepository.tryLock(DatabaseLock.Library);
 
-    this.watchLibraries = this.watchLock && watch.enabled;
+    this.watchLibraries = this.lock && watch.enabled;
 
-    this.jobRepository.addCronJob(
-      'libraryScan',
-      scan.cronExpression,
-      () =>
-        handlePromiseError(
-          this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: { force: false } }),
-          this.logger,
-        ),
-      scan.enabled,
-    );
+    if (this.lock) {
+      this.cronRepository.create({
+        name: 'libraryScan',
+        expression: scan.cronExpression,
+        onTick: () =>
+          handlePromiseError(this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ALL }), this.logger),
+        start: scan.enabled,
+      });
+    }
 
     if (this.watchLibraries) {
       await this.watchAll();
     }
-
-    this.configCore.config$.subscribe(({ library }) => {
-      this.jobRepository.updateCronJob('libraryScan', library.scan.cronExpression, library.scan.enabled);
-
-      if (library.watch.enabled !== this.watchLibraries) {
-        // Watch configuration changed, update accordingly
-        this.watchLibraries = library.watch.enabled;
-        handlePromiseError(this.watchLibraries ? this.watchAll() : this.unwatchAll(), this.logger);
-      }
-    });
   }
 
-  @OnEmit({ event: 'config.validate' })
-  onConfigValidate({ newConfig }: ArgOf<'config.validate'>) {
-    const { scan } = newConfig.library;
-    if (!validateCronExpression(scan.cronExpression)) {
-      throw new Error(`Invalid cron expression ${scan.cronExpression}`);
+  @OnEvent({ name: 'config.update', server: true })
+  async onConfigUpdate({ newConfig: { library } }: ArgOf<'config.update'>) {
+    if (!this.lock) {
+      return;
+    }
+
+    this.cronRepository.update({
+      name: 'libraryScan',
+      expression: library.scan.cronExpression,
+      start: library.scan.enabled,
+    });
+
+    if (library.watch.enabled !== this.watchLibraries) {
+      // Watch configuration changed, update accordingly
+      this.watchLibraries = library.watch.enabled;
+      await (this.watchLibraries ? this.watchAll() : this.unwatchAll());
     }
   }
 
@@ -143,7 +110,13 @@ export class LibraryService {
           const handler = async () => {
             this.logger.debug(`File add event received for ${path} in library ${library.id}}`);
             if (matcher(path)) {
-              await this.scanAssets(library.id, [path], library.ownerId, false);
+              const asset = await this.assetRepository.getByLibraryIdAndOriginalPath(library.id, path);
+              if (asset) {
+                await this.syncAssets(library, [asset.id]);
+              }
+              if (matcher(path)) {
+                await this.syncFiles(library, [path]);
+              }
             }
           };
           return handlePromiseError(handler(), this.logger);
@@ -151,9 +124,13 @@ export class LibraryService {
         onChange: (path) => {
           const handler = async () => {
             this.logger.debug(`Detected file change for ${path} in library ${library.id}`);
+            const asset = await this.assetRepository.getByLibraryIdAndOriginalPath(library.id, path);
+            if (asset) {
+              await this.syncAssets(library, [asset.id]);
+            }
             if (matcher(path)) {
               // Note: if the changed file was not previously imported, it will be imported now.
-              await this.scanAssets(library.id, [path], library.ownerId, false);
+              await this.syncFiles(library, [path]);
             }
           };
           return handlePromiseError(handler(), this.logger);
@@ -162,8 +139,8 @@ export class LibraryService {
           const handler = async () => {
             this.logger.debug(`Detected deleted file at ${path} in library ${library.id}`);
             const asset = await this.assetRepository.getByLibraryIdAndOriginalPath(library.id, path);
-            if (asset && matcher(path)) {
-              await this.assetRepository.update({ id: asset.id, isOffline: true });
+            if (asset) {
+              await this.syncAssets(library, [asset.id]);
             }
           };
           return handlePromiseError(handler(), this.logger);
@@ -187,13 +164,13 @@ export class LibraryService {
     }
   }
 
-  @OnEmit({ event: 'app.shutdown' })
+  @OnEvent({ name: 'app.shutdown' })
   async onShutdown() {
     await this.unwatchAll();
   }
 
   private async unwatchAll() {
-    if (!this.watchLock) {
+    if (!this.lock) {
       return false;
     }
 
@@ -203,20 +180,20 @@ export class LibraryService {
   }
 
   async watchAll() {
-    if (!this.watchLock) {
+    if (!this.lock) {
       return false;
     }
 
-    const libraries = await this.repository.getAll(false);
+    const libraries = await this.libraryRepository.getAll(false);
     for (const library of libraries) {
       await this.watch(library.id);
     }
   }
 
   async getStatistics(id: string): Promise<LibraryStatsResponseDto> {
-    const statistics = await this.repository.getStatistics(id);
+    const statistics = await this.libraryRepository.getStatistics(id);
     if (!statistics) {
-      throw new BadRequestException('Library not found');
+      throw new BadRequestException(`Library ${id} not found`);
     }
     return statistics;
   }
@@ -227,13 +204,14 @@ export class LibraryService {
   }
 
   async getAll(): Promise<LibraryResponseDto[]> {
-    const libraries = await this.repository.getAll(false);
+    const libraries = await this.libraryRepository.getAll(false);
     return libraries.map((library) => mapLibrary(library));
   }
 
+  @OnJob({ name: JobName.LIBRARY_QUEUE_CLEANUP, queue: QueueName.LIBRARY })
   async handleQueueCleanup(): Promise<JobStatus> {
     this.logger.debug('Cleaning up any pending library deletions');
-    const pendingDeletion = await this.repository.getAllDeleted();
+    const pendingDeletion = await this.libraryRepository.getAllDeleted();
     await this.jobRepository.queueAll(
       pendingDeletion.map((libraryToDelete) => ({ name: JobName.LIBRARY_DELETE, data: { id: libraryToDelete.id } })),
     );
@@ -241,7 +219,7 @@ export class LibraryService {
   }
 
   async create(dto: CreateLibraryDto): Promise<LibraryResponseDto> {
-    const library = await this.repository.create({
+    const library = await this.libraryRepository.create({
       ownerId: dto.ownerId,
       name: dto.name ?? 'New External Library',
       importPaths: dto.importPaths ?? [],
@@ -250,20 +228,28 @@ export class LibraryService {
     return mapLibrary(library);
   }
 
-  private async scanAssets(libraryId: string, assetPaths: string[], ownerId: string, force = false) {
+  private async syncFiles({ id, ownerId }: LibraryEntity, assetPaths: string[]) {
     await this.jobRepository.queueAll(
       assetPaths.map((assetPath) => ({
-        name: JobName.LIBRARY_SCAN_ASSET,
+        name: JobName.LIBRARY_SYNC_FILE,
         data: {
-          id: libraryId,
+          id,
           assetPath,
           ownerId,
-          force,
         },
       })),
     );
   }
 
+  private async syncAssets({ importPaths, exclusionPatterns }: LibraryEntity, assetIds: string[]) {
+    await this.jobRepository.queueAll(
+      assetIds.map((assetId) => ({
+        name: JobName.LIBRARY_SYNC_ASSET,
+        data: { id: assetId, importPaths, exclusionPatterns },
+      })),
+    );
+  }
+
   private async validateImportPath(importPath: string): Promise<ValidateLibraryImportPathResponseDto> {
     const validation = new ValidateLibraryImportPathResponseDto();
     validation.importPath = importPath;
@@ -273,6 +259,11 @@ export class LibraryService {
       return validation;
     }
 
+    if (!isAbsolute(importPath)) {
+      validation.message = `Import path must be absolute, try ${path.resolve(importPath)}`;
+      return validation;
+    }
+
     try {
       const stat = await this.storageRepository.stat(importPath);
       if (!stat.isDirectory()) {
@@ -308,7 +299,6 @@ export class LibraryService {
 
   async update(id: string, dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
     await this.findOrFail(id);
-    const library = await this.repository.update({ id, ...dto });
 
     if (dto.importPaths) {
       const validation = await this.validate(id, { importPaths: dto.importPaths });
@@ -321,6 +311,7 @@ export class LibraryService {
       }
     }
 
+    const library = await this.libraryRepository.update({ id, ...dto });
     return mapLibrary(library);
   }
 
@@ -331,11 +322,12 @@ export class LibraryService {
       await this.unwatch(id);
     }
 
-    await this.repository.softDelete(id);
+    await this.libraryRepository.softDelete(id);
     await this.jobRepository.queue({ name: JobName.LIBRARY_DELETE, data: { id } });
   }
 
-  async handleDeleteLibrary(job: IEntityJob): Promise<JobStatus> {
+  @OnJob({ name: JobName.LIBRARY_DELETE, queue: QueueName.LIBRARY })
+  async handleDeleteLibrary(job: JobOf<JobName.LIBRARY_DELETE>): Promise<JobStatus> {
     const libraryId = job.id;
 
     const assetPagination = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) =>
@@ -346,7 +338,10 @@ export class LibraryService {
 
     this.logger.debug(`Will delete all assets in library ${libraryId}`);
     for await (const assets of assetPagination) {
-      assetsFound = true;
+      if (assets.length > 0) {
+        assetsFound = true;
+      }
+
       this.logger.debug(`Queueing deletion of ${assets.length} asset(s) in library ${libraryId}`);
       await this.jobRepository.queueAll(
         assets.map((asset) => ({
@@ -361,263 +356,183 @@ export class LibraryService {
 
     if (!assetsFound) {
       this.logger.log(`Deleting library ${libraryId}`);
-      await this.repository.delete(libraryId);
+      await this.libraryRepository.delete(libraryId);
     }
     return JobStatus.SUCCESS;
   }
 
-  async handleAssetRefresh(job: ILibraryFileJob): Promise<JobStatus> {
+  @OnJob({ name: JobName.LIBRARY_SYNC_FILE, queue: QueueName.LIBRARY })
+  async handleSyncFile(job: JobOf<JobName.LIBRARY_SYNC_FILE>): Promise<JobStatus> {
+    // Only needs to handle new assets
     const assetPath = path.normalize(job.assetPath);
 
-    const existingAssetEntity = await this.assetRepository.getByLibraryIdAndOriginalPath(job.id, assetPath);
-
-    let stats: Stats;
-    try {
-      stats = await this.storageRepository.stat(assetPath);
-    } catch (error: Error | any) {
-      // Can't access file, probably offline
-      if (existingAssetEntity) {
-        // Mark asset as offline
-        this.logger.debug(`Marking asset as offline: ${assetPath}`);
-
-        await this.assetRepository.update({ id: existingAssetEntity.id, isOffline: true });
-        return JobStatus.SUCCESS;
-      } else {
-        // File can't be accessed and does not already exist in db
-        throw new BadRequestException('Cannot access file', { cause: error });
-      }
-    }
-
-    let doImport = false;
-    let doRefresh = false;
-
-    if (job.force) {
-      doRefresh = true;
-    }
-
-    const originalFileName = parse(assetPath).base;
-
-    if (!existingAssetEntity) {
-      // This asset is new to us, read it from disk
-      this.logger.debug(`Importing new asset: ${assetPath}`);
-      doImport = true;
-    } else if (stats.mtime.toISOString() !== existingAssetEntity.fileModifiedAt.toISOString()) {
-      // File modification time has changed since last time we checked, re-read from disk
-      this.logger.debug(
-        `File modification time has changed, re-importing asset: ${assetPath}. Old mtime: ${existingAssetEntity.fileModifiedAt}. New mtime: ${stats.mtime}`,
-      );
-      doRefresh = true;
-    } else if (existingAssetEntity.originalFileName !== originalFileName) {
-      // TODO: We can likely remove this check in the second half of 2024 when all assets have likely been re-imported by all users
-      this.logger.debug(
-        `Asset is missing file extension, re-importing: ${assetPath}. Current incorrect filename: ${existingAssetEntity.originalFileName}.`,
-      );
-      doRefresh = true;
-    } else if (!job.force && stats && !existingAssetEntity.isOffline) {
-      // Asset exists on disk and in db and mtime has not changed. Also, we are not forcing refresn. Therefore, do nothing
-      this.logger.debug(`Asset already exists in database and on disk, will not import: ${assetPath}`);
-    }
-
-    if (stats && existingAssetEntity?.isOffline) {
-      // File was previously offline but is now online
-      this.logger.debug(`Marking previously-offline asset as online: ${assetPath}`);
-      await this.assetRepository.update({ id: existingAssetEntity.id, isOffline: false });
-      doRefresh = true;
-    }
-
-    if (!doImport && !doRefresh) {
-      // If we don't import, exit here
+    let asset = await this.assetRepository.getByLibraryIdAndOriginalPath(job.id, assetPath);
+    if (asset) {
       return JobStatus.SKIPPED;
     }
 
-    let assetType: AssetType;
-
-    if (mimeTypes.isImage(assetPath)) {
-      assetType = AssetType.IMAGE;
-    } else if (mimeTypes.isVideo(assetPath)) {
-      assetType = AssetType.VIDEO;
-    } else {
-      throw new BadRequestException(`Unsupported file type ${assetPath}`);
+    let stat;
+    try {
+      stat = await this.storageRepository.stat(assetPath);
+    } catch (error: any) {
+      if (error.code === 'ENOENT') {
+        this.logger.error(`File not found: ${assetPath}`);
+        return JobStatus.SKIPPED;
+      }
+      this.logger.error(`Error reading file: ${assetPath}. Error: ${error}`);
+      return JobStatus.FAILED;
     }
 
-    // TODO: doesn't xmp replace the file extension? Will need investigation
-    let sidecarPath: string | null = null;
-    if (await this.storageRepository.checkFileExists(`${assetPath}.xmp`, R_OK)) {
-      sidecarPath = `${assetPath}.xmp`;
+    this.logger.log(`Importing new library asset: ${assetPath}`);
+
+    const library = await this.libraryRepository.get(job.id, true);
+    if (!library || library.deletedAt) {
+      this.logger.error('Cannot import asset into deleted library');
+      return JobStatus.FAILED;
     }
 
     // TODO: device asset id is deprecated, remove it
     const deviceAssetId = `${basename(assetPath)}`.replaceAll(/\s+/g, '');
 
-    let assetId;
-    if (doImport) {
-      const library = await this.repository.get(job.id, true);
-      if (library?.deletedAt) {
-        this.logger.error('Cannot import asset into deleted library');
-        return JobStatus.FAILED;
-      }
+    const pathHash = this.cryptoRepository.hashSha1(`path:${assetPath}`);
 
-      const pathHash = this.cryptoRepository.hashSha1(`path:${assetPath}`);
+    const assetType = mimeTypes.isVideo(assetPath) ? AssetType.VIDEO : AssetType.IMAGE;
 
-      // TODO: In wait of refactoring the domain asset service, this function is just manually written like this
-      const addedAsset = await this.assetRepository.create({
-        ownerId: job.ownerId,
-        libraryId: job.id,
-        checksum: pathHash,
-        originalPath: assetPath,
-        deviceAssetId,
-        deviceId: 'Library Import',
-        fileCreatedAt: stats.mtime,
-        fileModifiedAt: stats.mtime,
-        localDateTime: stats.mtime,
-        type: assetType,
-        originalFileName,
-        sidecarPath,
-        isExternal: true,
-      });
-      assetId = addedAsset.id;
-    } else if (doRefresh && existingAssetEntity) {
-      assetId = existingAssetEntity.id;
-      await this.assetRepository.updateAll([existingAssetEntity.id], {
-        fileCreatedAt: stats.mtime,
-        fileModifiedAt: stats.mtime,
-        originalFileName,
-      });
-    } else {
-      // Not importing and not refreshing, do nothing
-      return JobStatus.SKIPPED;
-    }
+    const mtime = stat.mtime;
 
-    this.logger.debug(`Queueing metadata extraction for: ${assetPath}`);
+    asset = await this.assetRepository.create({
+      ownerId: job.ownerId,
+      libraryId: job.id,
+      checksum: pathHash,
+      originalPath: assetPath,
+      deviceAssetId,
+      deviceId: 'Library Import',
+      fileCreatedAt: mtime,
+      fileModifiedAt: mtime,
+      localDateTime: mtime,
+      type: assetType,
+      originalFileName: parse(assetPath).base,
+      isExternal: true,
+    });
 
-    await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: assetId, source: 'upload' } });
-
-    if (assetType === AssetType.VIDEO) {
-      await this.jobRepository.queue({ name: JobName.VIDEO_CONVERSION, data: { id: assetId } });
-    }
+    await this.queuePostSyncJobs(asset);
 
     return JobStatus.SUCCESS;
   }
 
-  async queueScan(id: string, dto: ScanLibraryDto) {
-    await this.findOrFail(id);
+  async queuePostSyncJobs(asset: AssetEntity) {
+    this.logger.debug(`Queueing metadata extraction for: ${asset.originalPath}`);
 
+    // We queue a sidecar discovery which, in turn, queues metadata extraction
     await this.jobRepository.queue({
-      name: JobName.LIBRARY_SCAN,
-      data: {
-        id,
-        refreshModifiedFiles: dto.refreshModifiedFiles ?? false,
-        refreshAllFiles: dto.refreshAllFiles ?? false,
-      },
+      name: JobName.SIDECAR_DISCOVERY,
+      data: { id: asset.id },
     });
   }
 
-  async queueRemoveOffline(id: string) {
-    this.logger.verbose(`Queueing offline file removal from library ${id}`);
-    await this.jobRepository.queue({ name: JobName.LIBRARY_REMOVE_OFFLINE, data: { id } });
+  async queueScan(id: string) {
+    await this.findOrFail(id);
+
+    await this.jobRepository.queue({
+      name: JobName.LIBRARY_QUEUE_SYNC_FILES,
+      data: {
+        id,
+      },
+    });
+    await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, data: { id } });
   }
 
-  async handleQueueAllScan(job: IBaseJob): Promise<JobStatus> {
-    this.logger.debug(`Refreshing all external libraries: force=${job.force}`);
+  @OnJob({ name: JobName.LIBRARY_QUEUE_SYNC_ALL, queue: QueueName.LIBRARY })
+  async handleQueueSyncAll(): Promise<JobStatus> {
+    this.logger.debug(`Refreshing all external libraries`);
 
     await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_CLEANUP, data: {} });
 
-    const libraries = await this.repository.getAll(true);
+    const libraries = await this.libraryRepository.getAll(true);
     await this.jobRepository.queueAll(
       libraries.map((library) => ({
-        name: JobName.LIBRARY_SCAN,
+        name: JobName.LIBRARY_QUEUE_SYNC_FILES,
+        data: {
+          id: library.id,
+        },
+      })),
+    );
+    await this.jobRepository.queueAll(
+      libraries.map((library) => ({
+        name: JobName.LIBRARY_QUEUE_SYNC_ASSETS,
         data: {
           id: library.id,
-          refreshModifiedFiles: !job.force,
-          refreshAllFiles: job.force ?? false,
         },
       })),
     );
     return JobStatus.SUCCESS;
   }
 
-  async handleOfflineCheck(job: ILibraryOfflineJob): Promise<JobStatus> {
+  @OnJob({ name: JobName.LIBRARY_SYNC_ASSET, queue: QueueName.LIBRARY })
+  async handleSyncAsset(job: JobOf<JobName.LIBRARY_SYNC_ASSET>): Promise<JobStatus> {
     const asset = await this.assetRepository.getById(job.id);
-
     if (!asset) {
-      // Asset is no longer in the database, skip
       return JobStatus.SKIPPED;
     }
 
-    if (asset.isOffline) {
-      this.logger.verbose(`Asset is already offline: ${asset.originalPath}`);
-      return JobStatus.SUCCESS;
-    }
+    const markOffline = async (explanation: string) => {
+      if (!asset.isOffline) {
+        this.logger.debug(`${explanation}, removing: ${asset.originalPath}`);
+        await this.assetRepository.updateAll([asset.id], { isOffline: true, deletedAt: new Date() });
+      }
+    };
 
     const isInPath = job.importPaths.find((path) => asset.originalPath.startsWith(path));
     if (!isInPath) {
-      this.logger.debug(`Asset is no longer in an import path, marking offline: ${asset.originalPath}`);
-      await this.assetRepository.update({ id: asset.id, isOffline: true });
+      await markOffline('Asset is no longer in an import path');
       return JobStatus.SUCCESS;
     }
 
     const isExcluded = job.exclusionPatterns.some((pattern) => picomatch.isMatch(asset.originalPath, pattern));
     if (isExcluded) {
-      this.logger.debug(`Asset is covered by an exclusion pattern, marking offline: ${asset.originalPath}`);
-      await this.assetRepository.update({ id: asset.id, isOffline: true });
+      await markOffline('Asset is covered by an exclusion pattern');
       return JobStatus.SUCCESS;
     }
 
-    const fileExists = await this.storageRepository.checkFileExists(asset.originalPath, R_OK);
-    if (!fileExists) {
-      this.logger.debug(`Asset is no longer found on disk, marking offline: ${asset.originalPath}`);
-      await this.assetRepository.update({ id: asset.id, isOffline: true });
+    let stat;
+    try {
+      stat = await this.storageRepository.stat(asset.originalPath);
+    } catch {
+      await markOffline('Asset is no longer on disk or is inaccessible because of permissions');
       return JobStatus.SUCCESS;
     }
 
-    this.logger.verbose(
-      `Asset is found on disk, not covered by an exclusion pattern, and is in an import path, keeping online: ${asset.originalPath}`,
-    );
+    const mtime = stat.mtime;
+    const isAssetModified = mtime.toISOString() !== asset.fileModifiedAt.toISOString();
 
+    if (asset.isOffline || isAssetModified) {
+      this.logger.debug(`Asset was offline or modified, updating asset record ${asset.originalPath}`);
+      //TODO: When we have asset status, we need to leave deletedAt as is when status is trashed
+      await this.assetRepository.updateAll([asset.id], {
+        isOffline: false,
+        deletedAt: null,
+        fileCreatedAt: mtime,
+        fileModifiedAt: mtime,
+        originalFileName: parse(asset.originalPath).base,
+      });
+    }
+
+    if (isAssetModified) {
+      this.logger.debug(`Asset was modified, queuing metadata extraction for: ${asset.originalPath}`);
+      await this.queuePostSyncJobs(asset);
+    }
     return JobStatus.SUCCESS;
   }
 
-  async handleRemoveOffline(job: IEntityJob): Promise<JobStatus> {
-    this.logger.debug(`Removing offline assets for library ${job.id}`);
-
-    const assetPagination = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) =>
-      this.assetRepository.getWith(pagination, WithProperty.IS_OFFLINE, job.id, true),
-    );
-
-    let offlineAssets = 0;
-    for await (const assets of assetPagination) {
-      offlineAssets += assets.length;
-      if (assets.length > 0) {
-        this.logger.debug(`Discovered ${offlineAssets} offline assets in library ${job.id}`);
-        await this.jobRepository.queueAll(
-          assets.map((asset) => ({
-            name: JobName.ASSET_DELETION,
-            data: {
-              id: asset.id,
-              deleteOnDisk: false,
-            },
-          })),
-        );
-        this.logger.verbose(`Queued deletion of ${assets.length} offline assets in library ${job.id}`);
-      }
-    }
-
-    if (offlineAssets) {
-      this.logger.debug(`Finished queueing deletion of ${offlineAssets} offline assets for library ${job.id}`);
-    } else {
-      this.logger.debug(`Found no offline assets to delete from library ${job.id}`);
-    }
-
-    return JobStatus.SUCCESS;
-  }
-
-  async handleQueueAssetRefresh(job: ILibraryRefreshJob): Promise<JobStatus> {
-    const library = await this.repository.get(job.id);
+  @OnJob({ name: JobName.LIBRARY_QUEUE_SYNC_FILES, queue: QueueName.LIBRARY })
+  async handleQueueSyncFiles(job: JobOf<JobName.LIBRARY_QUEUE_SYNC_FILES>): Promise<JobStatus> {
+    const library = await this.libraryRepository.get(job.id);
     if (!library) {
+      this.logger.debug(`Library ${job.id} not found, skipping refresh`);
       return JobStatus.SKIPPED;
     }
 
-    this.logger.log(`Refreshing library ${library.id}`);
+    this.logger.log(`Refreshing library ${library.id} for new assets`);
 
     const validImportPaths: string[] = [];
 
@@ -641,49 +556,61 @@ export class LibraryService {
       take: JOBS_LIBRARY_PAGINATION_SIZE,
     });
 
-    let crawledAssets = 0;
+    let count = 0;
 
     for await (const assetBatch of assetsOnDisk) {
-      crawledAssets += assetBatch.length;
-      this.logger.debug(`Discovered ${crawledAssets} asset(s) on disk for library ${library.id}...`);
-      await this.scanAssets(job.id, assetBatch, library.ownerId, job.refreshAllFiles ?? false);
+      count += assetBatch.length;
+      this.logger.debug(`Discovered ${count} asset(s) on disk for library ${library.id}...`);
+      await this.syncFiles(library, assetBatch);
       this.logger.verbose(`Queued scan of ${assetBatch.length} crawled asset(s) in library ${library.id}...`);
     }
 
-    if (crawledAssets) {
-      this.logger.debug(`Finished queueing scan of ${crawledAssets} assets on disk for library ${library.id}`);
-    } else {
+    if (count > 0) {
+      this.logger.debug(`Finished queueing scan of ${count} assets on disk for library ${library.id}`);
+    } else if (validImportPaths.length > 0) {
       this.logger.debug(`No non-excluded assets found in any import path for library ${library.id}`);
     }
 
+    await this.libraryRepository.update({ id: job.id, refreshedAt: new Date() });
+
+    return JobStatus.SUCCESS;
+  }
+
+  @OnJob({ name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, queue: QueueName.LIBRARY })
+  async handleQueueSyncAssets(job: JobOf<JobName.LIBRARY_QUEUE_SYNC_ASSETS>): Promise<JobStatus> {
+    const library = await this.libraryRepository.get(job.id);
+    if (!library) {
+      return JobStatus.SKIPPED;
+    }
+
+    this.logger.log(`Scanning library ${library.id} for removed assets`);
+
     const onlineAssets = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) =>
-      this.assetRepository.getWith(pagination, WithProperty.IS_ONLINE, job.id),
+      this.assetRepository.getAll(pagination, { libraryId: job.id, withDeleted: true }),
     );
 
-    let onlineAssetCount = 0;
+    let assetCount = 0;
     for await (const assets of onlineAssets) {
-      onlineAssetCount += assets.length;
-      this.logger.debug(`Discovered ${onlineAssetCount} asset(s) in library ${library.id}...`);
+      assetCount += assets.length;
+      this.logger.debug(`Discovered ${assetCount} asset(s) in library ${library.id}...`);
       await this.jobRepository.queueAll(
         assets.map((asset) => ({
-          name: JobName.LIBRARY_CHECK_OFFLINE,
-          data: { id: asset.id, importPaths: validImportPaths, exclusionPatterns: library.exclusionPatterns },
+          name: JobName.LIBRARY_SYNC_ASSET,
+          data: { id: asset.id, importPaths: library.importPaths, exclusionPatterns: library.exclusionPatterns },
         })),
       );
-      this.logger.debug(`Queued online check of ${assets.length} asset(s) in library ${library.id}...`);
+      this.logger.debug(`Queued check of ${assets.length} asset(s) in library ${library.id}...`);
     }
 
-    if (onlineAssetCount) {
-      this.logger.log(`Finished queueing online check of ${onlineAssetCount} assets for library ${library.id}`);
+    if (assetCount) {
+      this.logger.log(`Finished queueing check of ${assetCount} assets for library ${library.id}`);
     }
 
-    await this.repository.update({ id: job.id, refreshedAt: new Date() });
-
     return JobStatus.SUCCESS;
   }
 
   private async findOrFail(id: string) {
-    const library = await this.repository.get(id);
+    const library = await this.libraryRepository.get(id);
     if (!library) {
       throw new BadRequestException('Library not found');
     }
diff --git a/server/src/services/map.service.spec.ts b/server/src/services/map.service.spec.ts
index f8b73260af..fde2ba7e0f 100644
--- a/server/src/services/map.service.spec.ts
+++ b/server/src/services/map.service.spec.ts
@@ -1,34 +1,23 @@
 import { IAlbumRepository } from 'src/interfaces/album.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { IMapRepository } from 'src/interfaces/map.interface';
 import { IPartnerRepository } from 'src/interfaces/partner.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { MapService } from 'src/services/map.service';
+import { albumStub } from 'test/fixtures/album.stub';
 import { assetStub } from 'test/fixtures/asset.stub';
 import { authStub } from 'test/fixtures/auth.stub';
-import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newMapRepositoryMock } from 'test/repositories/map.repository.mock';
-import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
+import { partnerStub } from 'test/fixtures/partner.stub';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(MapService.name, () => {
   let sut: MapService;
+
   let albumMock: Mocked<IAlbumRepository>;
-  let loggerMock: Mocked<ILoggerRepository>;
-  let partnerMock: Mocked<IPartnerRepository>;
   let mapMock: Mocked<IMapRepository>;
-  let systemMetadataMock: Mocked<ISystemMetadataRepository>;
+  let partnerMock: Mocked<IPartnerRepository>;
 
   beforeEach(() => {
-    albumMock = newAlbumRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-    partnerMock = newPartnerRepositoryMock();
-    mapMock = newMapRepositoryMock();
-    systemMetadataMock = newSystemMetadataRepositoryMock();
-
-    sut = new MapService(albumMock, loggerMock, partnerMock, mapMock, systemMetadataMock);
+    ({ sut, albumMock, mapMock, partnerMock } = newTestService(MapService));
   });
 
   describe('getMapMarkers', () => {
@@ -50,5 +39,62 @@ describe(MapService.name, () => {
       expect(markers).toHaveLength(1);
       expect(markers[0]).toEqual(marker);
     });
+
+    it('should include partner assets', async () => {
+      const asset = assetStub.withLocation;
+      const marker = {
+        id: asset.id,
+        lat: asset.exifInfo!.latitude!,
+        lon: asset.exifInfo!.longitude!,
+        city: asset.exifInfo!.city,
+        state: asset.exifInfo!.state,
+        country: asset.exifInfo!.country,
+      };
+      partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1]);
+      mapMock.getMapMarkers.mockResolvedValue([marker]);
+
+      const markers = await sut.getMapMarkers(authStub.user1, { withPartners: true });
+
+      expect(mapMock.getMapMarkers).toHaveBeenCalledWith(
+        [authStub.user1.user.id, partnerStub.adminToUser1.sharedById],
+        expect.arrayContaining([]),
+        { withPartners: true },
+      );
+      expect(markers).toHaveLength(1);
+      expect(markers[0]).toEqual(marker);
+    });
+
+    it('should include assets from shared albums', async () => {
+      const asset = assetStub.withLocation;
+      const marker = {
+        id: asset.id,
+        lat: asset.exifInfo!.latitude!,
+        lon: asset.exifInfo!.longitude!,
+        city: asset.exifInfo!.city,
+        state: asset.exifInfo!.state,
+        country: asset.exifInfo!.country,
+      };
+      partnerMock.getAll.mockResolvedValue([]);
+      mapMock.getMapMarkers.mockResolvedValue([marker]);
+      albumMock.getOwned.mockResolvedValue([albumStub.empty]);
+      albumMock.getShared.mockResolvedValue([albumStub.sharedWithUser]);
+
+      const markers = await sut.getMapMarkers(authStub.user1, { withSharedAlbums: true });
+
+      expect(markers).toHaveLength(1);
+      expect(markers[0]).toEqual(marker);
+    });
+  });
+
+  describe('reverseGeocode', () => {
+    it('should reverse geocode a location', async () => {
+      mapMock.reverseGeocode.mockResolvedValue({ city: 'foo', state: 'bar', country: 'baz' });
+
+      await expect(sut.reverseGeocode({ lat: 42, lon: 69 })).resolves.toEqual([
+        { city: 'foo', state: 'bar', country: 'baz' },
+      ]);
+
+      expect(mapMock.reverseGeocode).toHaveBeenCalledWith({ latitude: 42, longitude: 69 });
+    });
   });
 });
diff --git a/server/src/services/map.service.ts b/server/src/services/map.service.ts
index ffd84a3e02..860a782e79 100644
--- a/server/src/services/map.service.ts
+++ b/server/src/services/map.service.ts
@@ -1,28 +1,9 @@
-import { Inject } from '@nestjs/common';
-import { SystemConfigCore } from 'src/cores/system-config.core';
 import { AuthDto } from 'src/dtos/auth.dto';
 import { MapMarkerDto, MapMarkerResponseDto, MapReverseGeocodeDto } from 'src/dtos/map.dto';
-import { IAlbumRepository } from 'src/interfaces/album.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { IMapRepository } from 'src/interfaces/map.interface';
-import { IPartnerRepository } from 'src/interfaces/partner.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { BaseService } from 'src/services/base.service';
 import { getMyPartnerIds } from 'src/utils/asset.util';
 
-export class MapService {
-  private configCore: SystemConfigCore;
-
-  constructor(
-    @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-    @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
-    @Inject(IMapRepository) private mapRepository: IMapRepository,
-    @Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository,
-  ) {
-    this.logger.setContext(MapService.name);
-    this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
-  }
-
+export class MapService extends BaseService {
   async getMapMarkers(auth: AuthDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
     const userIds = [auth.user.id];
     if (options.withPartners) {
@@ -43,17 +24,6 @@ export class MapService {
     return this.mapRepository.getMapMarkers(userIds, albumIds, options);
   }
 
-  async getMapStyle(theme: 'light' | 'dark') {
-    const { map } = await this.configCore.getConfig({ withCache: false });
-    const styleUrl = theme === 'dark' ? map.darkStyle : map.lightStyle;
-
-    if (styleUrl) {
-      return this.mapRepository.fetchStyle(styleUrl);
-    }
-
-    return JSON.parse(await this.systemMetadataRepository.readFile(`./resources/style-${theme}.json`));
-  }
-
   async reverseGeocode(dto: MapReverseGeocodeDto) {
     const { lat: latitude, lon: longitude } = dto;
     // eventually this should probably return an array of results
diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts
index bf493de0f3..36a9045677 100644
--- a/server/src/services/media.service.spec.ts
+++ b/server/src/services/media.service.spec.ts
@@ -1,20 +1,21 @@
-import { Stats } from 'node:fs';
+import { SystemConfig } from 'src/config';
+import { AssetEntity } from 'src/entities/asset.entity';
+import { ExifEntity } from 'src/entities/exif.entity';
 import {
+  AssetFileType,
+  AssetPathType,
+  AssetType,
   AudioCodec,
   Colorspace,
   ImageFormat,
-  ToneMapping,
   TranscodeHWAccel,
   TranscodePolicy,
   VideoCodec,
-} from 'src/config';
-import { ExifEntity } from 'src/entities/exif.entity';
-import { AssetFileType, AssetType } from 'src/enum';
+} from 'src/enum';
 import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
+import { IJobRepository, JobCounts, JobName, JobStatus } from 'src/interfaces/job.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { IMediaRepository } from 'src/interfaces/media.interface';
+import { IMediaRepository, RawImageInfo } from 'src/interfaces/media.interface';
 import { IMoveRepository } from 'src/interfaces/move.interface';
 import { IPersonRepository } from 'src/interfaces/person.interface';
 import { IStorageRepository } from 'src/interfaces/storage.interface';
@@ -24,51 +25,24 @@ import { assetStub } from 'test/fixtures/asset.stub';
 import { faceStub } from 'test/fixtures/face.stub';
 import { probeStub } from 'test/fixtures/media.stub';
 import { personStub } from 'test/fixtures/person.stub';
-import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
-import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
-import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock';
-import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock';
-import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
-import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(MediaService.name, () => {
   let sut: MediaService;
+
   let assetMock: Mocked<IAssetRepository>;
   let jobMock: Mocked<IJobRepository>;
+  let loggerMock: Mocked<ILoggerRepository>;
   let mediaMock: Mocked<IMediaRepository>;
   let moveMock: Mocked<IMoveRepository>;
   let personMock: Mocked<IPersonRepository>;
   let storageMock: Mocked<IStorageRepository>;
   let systemMock: Mocked<ISystemMetadataRepository>;
-  let cryptoMock: Mocked<ICryptoRepository>;
-  let loggerMock: Mocked<ILoggerRepository>;
 
   beforeEach(() => {
-    assetMock = newAssetRepositoryMock();
-    systemMock = newSystemMetadataRepositoryMock();
-    jobMock = newJobRepositoryMock();
-    mediaMock = newMediaRepositoryMock();
-    moveMock = newMoveRepositoryMock();
-    personMock = newPersonRepositoryMock();
-    storageMock = newStorageRepositoryMock();
-    cryptoMock = newCryptoRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-
-    sut = new MediaService(
-      assetMock,
-      personMock,
-      jobMock,
-      mediaMock,
-      storageMock,
-      systemMock,
-      moveMock,
-      cryptoMock,
-      loggerMock,
-    );
+    ({ sut, assetMock, jobMock, loggerMock, mediaMock, moveMock, personMock, storageMock, systemMock } =
+      newTestService(MediaService));
   });
 
   it('should be defined', () => {
@@ -93,7 +67,7 @@ describe(MediaService.name, () => {
       expect(assetMock.getWithout).not.toHaveBeenCalled();
       expect(jobMock.queueAll).toHaveBeenCalledWith([
         {
-          name: JobName.GENERATE_PREVIEW,
+          name: JobName.GENERATE_THUMBNAILS,
           data: { id: assetStub.image.id },
         },
       ]);
@@ -126,7 +100,7 @@ describe(MediaService.name, () => {
       expect(assetMock.getWithout).not.toHaveBeenCalled();
       expect(jobMock.queueAll).toHaveBeenCalledWith([
         {
-          name: JobName.GENERATE_PREVIEW,
+          name: JobName.GENERATE_THUMBNAILS,
           data: { id: assetStub.trashed.id },
         },
       ]);
@@ -151,7 +125,7 @@ describe(MediaService.name, () => {
       expect(assetMock.getWithout).not.toHaveBeenCalled();
       expect(jobMock.queueAll).toHaveBeenCalledWith([
         {
-          name: JobName.GENERATE_PREVIEW,
+          name: JobName.GENERATE_THUMBNAILS,
           data: { id: assetStub.archived.id },
         },
       ]);
@@ -163,10 +137,10 @@ describe(MediaService.name, () => {
         hasNextPage: false,
       });
       personMock.getAll.mockResolvedValue({
-        items: [personStub.noThumbnail],
+        items: [personStub.noThumbnail, personStub.noThumbnail],
         hasNextPage: false,
       });
-      personMock.getRandomFace.mockResolvedValue(faceStub.face1);
+      personMock.getRandomFace.mockResolvedValueOnce(faceStub.face1);
 
       await sut.handleQueueGenerateThumbnails({ force: false });
 
@@ -175,6 +149,7 @@ describe(MediaService.name, () => {
 
       expect(personMock.getAll).toHaveBeenCalledWith({ skip: 0, take: 1000 }, { where: { thumbnailPath: '' } });
       expect(personMock.getRandomFace).toHaveBeenCalled();
+      expect(personMock.update).toHaveBeenCalledTimes(1);
       expect(jobMock.queueAll).toHaveBeenCalledWith([
         {
           name: JobName.GENERATE_PERSON_THUMBNAIL,
@@ -201,7 +176,7 @@ describe(MediaService.name, () => {
       expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL);
       expect(jobMock.queueAll).toHaveBeenCalledWith([
         {
-          name: JobName.GENERATE_PREVIEW,
+          name: JobName.GENERATE_THUMBNAILS,
           data: { id: assetStub.image.id },
         },
       ]);
@@ -225,7 +200,7 @@ describe(MediaService.name, () => {
       expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL);
       expect(jobMock.queueAll).toHaveBeenCalledWith([
         {
-          name: JobName.GENERATE_THUMBNAIL,
+          name: JobName.GENERATE_THUMBNAILS,
           data: { id: assetStub.image.id },
         },
       ]);
@@ -249,7 +224,7 @@ describe(MediaService.name, () => {
       expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL);
       expect(jobMock.queueAll).toHaveBeenCalledWith([
         {
-          name: JobName.GENERATE_THUMBHASH,
+          name: JobName.GENERATE_THUMBNAILS,
           data: { id: assetStub.image.id },
         },
       ]);
@@ -258,141 +233,233 @@ describe(MediaService.name, () => {
     });
   });
 
-  describe('handleGeneratePreview', () => {
+  describe('handleQueueMigration', () => {
+    it('should remove empty directories and queue jobs', async () => {
+      assetMock.getAll.mockResolvedValue({ hasNextPage: false, items: [assetStub.image] });
+      jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0 } as JobCounts);
+      personMock.getAll.mockResolvedValue({ hasNextPage: false, items: [personStub.withName] });
+
+      await expect(sut.handleQueueMigration()).resolves.toBe(JobStatus.SUCCESS);
+
+      expect(storageMock.removeEmptyDirs).toHaveBeenCalledTimes(2);
+      expect(jobMock.queueAll).toHaveBeenCalledWith([
+        { name: JobName.MIGRATE_ASSET, data: { id: assetStub.image.id } },
+      ]);
+      expect(jobMock.queueAll).toHaveBeenCalledWith([
+        { name: JobName.MIGRATE_PERSON, data: { id: personStub.withName.id } },
+      ]);
+    });
+  });
+
+  describe('handleAssetMigration', () => {
+    it('should fail if asset does not exist', async () => {
+      await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
+
+      expect(moveMock.getByEntity).not.toHaveBeenCalled();
+    });
+
+    it('should move asset files', async () => {
+      assetMock.getByIds.mockResolvedValue([assetStub.image]);
+      moveMock.create.mockResolvedValue({
+        entityId: assetStub.image.id,
+        id: 'move-id',
+        newPath: '/new/path',
+        oldPath: '/old/path',
+        pathType: AssetPathType.ORIGINAL,
+      });
+
+      await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS);
+      expect(moveMock.create).toHaveBeenCalledTimes(2);
+    });
+  });
+
+  describe('handleGenerateThumbnails', () => {
+    let rawBuffer: Buffer;
+    let rawInfo: RawImageInfo;
+
+    beforeEach(() => {
+      rawBuffer = Buffer.from('image data');
+      rawInfo = { width: 100, height: 100, channels: 3 };
+      mediaMock.decodeImage.mockResolvedValue({ data: rawBuffer, info: rawInfo });
+    });
+
     it('should skip thumbnail generation if asset not found', async () => {
-      assetMock.getByIds.mockResolvedValue([]);
-      await sut.handleGeneratePreview({ id: assetStub.image.id });
+      await sut.handleGenerateThumbnails({ id: assetStub.image.id });
+
+      expect(mediaMock.generateThumbnail).not.toHaveBeenCalled();
+      expect(assetMock.update).not.toHaveBeenCalledWith();
+    });
+
+    it('should skip thumbnail generation if asset type is unknown', async () => {
+      assetMock.getById.mockResolvedValue({ ...assetStub.image, type: 'foo' } as never as AssetEntity);
+
+      await expect(sut.handleGenerateThumbnails({ id: assetStub.image.id })).resolves.toBe(JobStatus.SKIPPED);
+      expect(mediaMock.probe).not.toHaveBeenCalled();
       expect(mediaMock.generateThumbnail).not.toHaveBeenCalled();
       expect(assetMock.update).not.toHaveBeenCalledWith();
     });
 
     it('should skip video thumbnail generation if no video stream', async () => {
       mediaMock.probe.mockResolvedValue(probeStub.noVideoStreams);
-      assetMock.getByIds.mockResolvedValue([assetStub.video]);
-      await sut.handleGeneratePreview({ id: assetStub.image.id });
+      assetMock.getById.mockResolvedValue(assetStub.video);
+      await expect(sut.handleGenerateThumbnails({ id: assetStub.video.id })).rejects.toThrowError();
       expect(mediaMock.generateThumbnail).not.toHaveBeenCalled();
       expect(assetMock.update).not.toHaveBeenCalledWith();
     });
 
     it('should skip invisible assets', async () => {
-      assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
+      assetMock.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
 
-      expect(await sut.handleGeneratePreview({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED);
+      expect(await sut.handleGenerateThumbnails({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED);
 
       expect(mediaMock.generateThumbnail).not.toHaveBeenCalled();
       expect(assetMock.update).not.toHaveBeenCalledWith();
     });
 
-    it.each(Object.values(ImageFormat))('should generate a %s preview for an image when specified', async (format) => {
-      systemMock.get.mockResolvedValue({ image: { previewFormat: format } });
-      assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      const previewPath = `upload/thumbs/user-id/as/se/asset-id-preview.${format}`;
-
-      await sut.handleGeneratePreview({ id: assetStub.image.id });
-
-      expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se');
-      expect(mediaMock.generateThumbnail).toHaveBeenCalledWith('/original/path.jpg', previewPath, {
-        size: 1440,
-        format,
-        quality: 80,
-        colorspace: Colorspace.SRGB,
-        processInvalidImages: false,
-      });
-      expect(assetMock.upsertFile).toHaveBeenCalledWith({
-        assetId: 'asset-id',
-        type: AssetFileType.PREVIEW,
-        path: previewPath,
-      });
-    });
-
     it('should delete previous preview if different path', async () => {
-      systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } });
-      assetMock.getByIds.mockResolvedValue([assetStub.image]);
+      systemMock.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.WEBP } } });
+      assetMock.getById.mockResolvedValue(assetStub.image);
 
-      await sut.handleGeneratePreview({ id: assetStub.image.id });
+      await sut.handleGenerateThumbnails({ id: assetStub.image.id });
 
       expect(storageMock.unlink).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg');
     });
 
-    it('should generate a P3 thumbnail for a wide gamut image', async () => {
-      assetMock.getByIds.mockResolvedValue([
-        { ...assetStub.image, exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as ExifEntity },
-      ]);
-      await sut.handleGeneratePreview({ id: assetStub.image.id });
+    it('should generate P3 thumbnails for a wide gamut image', async () => {
+      assetMock.getById.mockResolvedValue({
+        ...assetStub.image,
+        exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as ExifEntity,
+      });
+      const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
+      mediaMock.generateThumbhash.mockResolvedValue(thumbhashBuffer);
+
+      await sut.handleGenerateThumbnails({ id: assetStub.image.id });
 
       expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se');
-      expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
-        '/original/path.jpg',
-        'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
-        {
-          size: 1440,
-          format: ImageFormat.JPEG,
-          quality: 80,
-          colorspace: Colorspace.P3,
-          processInvalidImages: false,
-        },
-      );
-      expect(assetMock.upsertFile).toHaveBeenCalledWith({
-        assetId: 'asset-id',
-        type: AssetFileType.PREVIEW,
-        path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
+
+      expect(mediaMock.decodeImage).toHaveBeenCalledOnce();
+      expect(mediaMock.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, {
+        colorspace: Colorspace.P3,
+        processInvalidImages: false,
+        size: 1440,
       });
+
+      expect(mediaMock.generateThumbnail).toHaveBeenCalledTimes(2);
+      expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
+        rawBuffer,
+        {
+          colorspace: Colorspace.P3,
+          format: ImageFormat.JPEG,
+          size: 1440,
+          quality: 80,
+          processInvalidImages: false,
+          raw: rawInfo,
+        },
+        'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
+      );
+      expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
+        rawBuffer,
+        {
+          colorspace: Colorspace.P3,
+          format: ImageFormat.WEBP,
+          size: 250,
+          quality: 80,
+          processInvalidImages: false,
+          raw: rawInfo,
+        },
+        'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
+      );
+
+      expect(mediaMock.generateThumbhash).toHaveBeenCalledOnce();
+      expect(mediaMock.generateThumbhash).toHaveBeenCalledWith(rawBuffer, {
+        colorspace: Colorspace.P3,
+        processInvalidImages: false,
+        raw: rawInfo,
+      });
+
+      expect(assetMock.upsertFiles).toHaveBeenCalledWith([
+        {
+          assetId: 'asset-id',
+          type: AssetFileType.PREVIEW,
+          path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
+        },
+        {
+          assetId: 'asset-id',
+          type: AssetFileType.THUMBNAIL,
+          path: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
+        },
+      ]);
+      expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer });
     });
 
     it('should generate a thumbnail for a video', async () => {
       mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
-      assetMock.getByIds.mockResolvedValue([assetStub.video]);
-      await sut.handleGeneratePreview({ id: assetStub.video.id });
+      assetMock.getById.mockResolvedValue(assetStub.video);
+      await sut.handleGenerateThumbnails({ id: assetStub.video.id });
 
       expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se');
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
-        {
+        expect.objectContaining({
           inputOptions: ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int'],
           outputOptions: [
             '-fps_mode vfr',
             '-frames:v 1',
             '-update 1',
             '-v verbose',
-            String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,scale=-2:1440:flags=lanczos+accurate_rnd+full_chroma_int:out_color_matrix=601:out_range=pc,format=yuv420p`,
+            String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,scale=-2:1440:flags=lanczos+accurate_rnd+full_chroma_int:out_range=pc`,
           ],
           twoPass: false,
-        },
+        }),
       );
-      expect(assetMock.upsertFile).toHaveBeenCalledWith({
-        assetId: 'asset-id',
-        type: AssetFileType.PREVIEW,
-        path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
-      });
+      expect(assetMock.upsertFiles).toHaveBeenCalledWith([
+        {
+          assetId: 'asset-id',
+          type: AssetFileType.PREVIEW,
+          path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
+        },
+        {
+          assetId: 'asset-id',
+          type: AssetFileType.THUMBNAIL,
+          path: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
+        },
+      ]);
     });
 
     it('should tonemap thumbnail for hdr video', async () => {
       mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
-      assetMock.getByIds.mockResolvedValue([assetStub.video]);
-      await sut.handleGeneratePreview({ id: assetStub.video.id });
+      assetMock.getById.mockResolvedValue(assetStub.video);
+      await sut.handleGenerateThumbnails({ id: assetStub.video.id });
 
       expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se');
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
-        {
+        expect.objectContaining({
           inputOptions: ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int'],
           outputOptions: [
             '-fps_mode vfr',
             '-frames:v 1',
             '-update 1',
             '-v verbose',
-            String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=601:m=bt470bg:range=pc,format=yuv420p`,
+            String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p`,
           ],
           twoPass: false,
-        },
+        }),
       );
-      expect(assetMock.upsertFile).toHaveBeenCalledWith({
-        assetId: 'asset-id',
-        type: AssetFileType.PREVIEW,
-        path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
-      });
+      expect(assetMock.upsertFiles).toHaveBeenCalledWith([
+        {
+          assetId: 'asset-id',
+          type: AssetFileType.PREVIEW,
+          path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
+        },
+        {
+          assetId: 'asset-id',
+          type: AssetFileType.THUMBNAIL,
+          path: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
+        },
+      ]);
     });
 
     it('should always generate video thumbnail in one pass', async () => {
@@ -400,270 +467,260 @@ describe(MediaService.name, () => {
       systemMock.get.mockResolvedValue({
         ffmpeg: { twoPass: true, maxBitrate: '5000k' },
       });
-      assetMock.getByIds.mockResolvedValue([assetStub.video]);
-      await sut.handleGeneratePreview({ id: assetStub.video.id });
+      assetMock.getById.mockResolvedValue(assetStub.video);
+      await sut.handleGenerateThumbnails({ id: assetStub.video.id });
 
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
-        {
+        expect.objectContaining({
           inputOptions: ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int'],
           outputOptions: [
             '-fps_mode vfr',
             '-frames:v 1',
             '-update 1',
             '-v verbose',
-            String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=601:m=bt470bg:range=pc,format=yuv420p`,
+            String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p`,
           ],
           twoPass: false,
-        },
+        }),
+      );
+    });
+    it('should not skip intra frames for MTS file', async () => {
+      mediaMock.probe.mockResolvedValue(probeStub.videoStreamMTS);
+      assetMock.getById.mockResolvedValue(assetStub.video);
+      await sut.handleGenerateThumbnails({ id: assetStub.video.id });
+
+      expect(mediaMock.transcode).toHaveBeenCalledWith(
+        '/original/path.ext',
+        'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
+        expect.objectContaining({
+          inputOptions: ['-sws_flags accurate_rnd+full_chroma_int'],
+          outputOptions: expect.any(Array),
+          progress: expect.any(Object),
+          twoPass: false,
+        }),
       );
     });
 
     it('should use scaling divisible by 2 even when using quick sync', async () => {
       mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
       systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV } });
-      assetMock.getByIds.mockResolvedValue([assetStub.video]);
-      await sut.handleGeneratePreview({ id: assetStub.video.id });
+      assetMock.getById.mockResolvedValue(assetStub.video);
+      await sut.handleGenerateThumbnails({ id: assetStub.video.id });
 
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining([expect.stringContaining('scale=-2:1440')]),
           twoPass: false,
-        },
+        }),
       );
     });
 
-    it('should run successfully', async () => {
-      assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      await sut.handleGeneratePreview({ id: assetStub.image.id });
-    });
-  });
+    it.each(Object.values(ImageFormat))('should generate an image preview in %s format', async (format) => {
+      systemMock.get.mockResolvedValue({ image: { preview: { format } } });
+      assetMock.getById.mockResolvedValue(assetStub.image);
+      const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
+      mediaMock.generateThumbhash.mockResolvedValue(thumbhashBuffer);
+      const previewPath = `upload/thumbs/user-id/as/se/asset-id-preview.${format}`;
+      const thumbnailPath = `upload/thumbs/user-id/as/se/asset-id-thumbnail.webp`;
 
-  describe('handleGenerateThumbnail', () => {
-    it('should skip thumbnail generation if asset not found', async () => {
-      assetMock.getByIds.mockResolvedValue([]);
-      await sut.handleGenerateThumbnail({ id: assetStub.image.id });
-      expect(mediaMock.generateThumbnail).not.toHaveBeenCalled();
-      expect(assetMock.update).not.toHaveBeenCalledWith();
-    });
+      await sut.handleGenerateThumbnails({ id: assetStub.image.id });
 
-    it('should skip invisible assets', async () => {
-      assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
+      expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se');
+      expect(mediaMock.decodeImage).toHaveBeenCalledOnce();
+      expect(mediaMock.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, {
+        colorspace: Colorspace.SRGB,
+        processInvalidImages: false,
+        size: 1440,
+      });
 
-      expect(await sut.handleGenerateThumbnail({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED);
-
-      expect(mediaMock.generateThumbnail).not.toHaveBeenCalled();
-      expect(assetMock.update).not.toHaveBeenCalledWith();
-    });
-
-    it.each(Object.values(ImageFormat))(
-      'should generate a %s thumbnail for an image when specified',
-      async (format) => {
-        systemMock.get.mockResolvedValue({ image: { thumbnailFormat: format } });
-        assetMock.getByIds.mockResolvedValue([assetStub.image]);
-        const thumbnailPath = `upload/thumbs/user-id/as/se/asset-id-thumbnail.${format}`;
-
-        await sut.handleGenerateThumbnail({ id: assetStub.image.id });
-
-        expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se');
-        expect(mediaMock.generateThumbnail).toHaveBeenCalledWith('/original/path.jpg', thumbnailPath, {
-          size: 250,
-          format,
-          quality: 80,
+      expect(mediaMock.generateThumbnail).toHaveBeenCalledTimes(2);
+      expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
+        rawBuffer,
+        {
           colorspace: Colorspace.SRGB,
+          format,
+          size: 1440,
+          quality: 80,
           processInvalidImages: false,
-        });
-        expect(assetMock.upsertFile).toHaveBeenCalledWith({
-          assetId: 'asset-id',
-          type: AssetFileType.THUMBNAIL,
-          path: thumbnailPath,
-        });
-      },
-    );
+          raw: rawInfo,
+        },
+        previewPath,
+      );
+      expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
+        rawBuffer,
+        {
+          colorspace: Colorspace.SRGB,
+          format: ImageFormat.WEBP,
+          size: 250,
+          quality: 80,
+          processInvalidImages: false,
+          raw: rawInfo,
+        },
+        thumbnailPath,
+      );
+    });
+
+    it.each(Object.values(ImageFormat))('should generate an image thumbnail in %s format', async (format) => {
+      systemMock.get.mockResolvedValue({ image: { thumbnail: { format } } });
+      assetMock.getById.mockResolvedValue(assetStub.image);
+      const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
+      mediaMock.generateThumbhash.mockResolvedValue(thumbhashBuffer);
+      const previewPath = `upload/thumbs/user-id/as/se/asset-id-preview.jpeg`;
+      const thumbnailPath = `upload/thumbs/user-id/as/se/asset-id-thumbnail.${format}`;
+
+      await sut.handleGenerateThumbnails({ id: assetStub.image.id });
+
+      expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se');
+      expect(mediaMock.decodeImage).toHaveBeenCalledOnce();
+      expect(mediaMock.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, {
+        colorspace: Colorspace.SRGB,
+        processInvalidImages: false,
+        size: 1440,
+      });
+
+      expect(mediaMock.generateThumbnail).toHaveBeenCalledTimes(2);
+      expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
+        rawBuffer,
+        {
+          colorspace: Colorspace.SRGB,
+          format: ImageFormat.JPEG,
+          size: 1440,
+          quality: 80,
+          processInvalidImages: false,
+          raw: rawInfo,
+        },
+        previewPath,
+      );
+      expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
+        rawBuffer,
+        {
+          colorspace: Colorspace.SRGB,
+          format,
+          size: 250,
+          quality: 80,
+          processInvalidImages: false,
+          raw: rawInfo,
+        },
+        thumbnailPath,
+      );
+    });
 
     it('should delete previous thumbnail if different path', async () => {
-      systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } });
-      assetMock.getByIds.mockResolvedValue([assetStub.image]);
+      systemMock.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.WEBP } } });
+      assetMock.getById.mockResolvedValue(assetStub.image);
 
-      await sut.handleGenerateThumbnail({ id: assetStub.image.id });
+      await sut.handleGenerateThumbnails({ id: assetStub.image.id });
 
       expect(storageMock.unlink).toHaveBeenCalledWith('/uploads/user-id/webp/path.ext');
     });
-  });
 
-  it('should generate a P3 thumbnail for a wide gamut image', async () => {
-    assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
-    await sut.handleGenerateThumbnail({ id: assetStub.image.id });
+    it('should extract embedded image if enabled and available', async () => {
+      mediaMock.extract.mockResolvedValue(true);
+      mediaMock.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
+      systemMock.get.mockResolvedValue({ image: { extractEmbedded: true } });
+      assetMock.getById.mockResolvedValue(assetStub.imageDng);
 
-    expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se');
-    expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
-      assetStub.imageDng.originalPath,
-      'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
-      {
-        format: ImageFormat.WEBP,
-        size: 250,
-        quality: 80,
+      await sut.handleGenerateThumbnails({ id: assetStub.image.id });
+
+      const extractedPath = mediaMock.extract.mock.calls.at(-1)?.[1].toString();
+      expect(mediaMock.decodeImage).toHaveBeenCalledOnce();
+      expect(mediaMock.decodeImage).toHaveBeenCalledWith(extractedPath, {
         colorspace: Colorspace.P3,
         processInvalidImages: false,
-      },
-    );
-    expect(assetMock.upsertFile).toHaveBeenCalledWith({
-      assetId: 'asset-id',
-      type: AssetFileType.THUMBNAIL,
-      path: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
+        size: 1440,
+      });
+      expect(extractedPath?.endsWith('.tmp')).toBe(true);
+      expect(storageMock.unlink).toHaveBeenCalledWith(extractedPath);
     });
-  });
 
-  it('should extract embedded image if enabled and available', async () => {
-    mediaMock.extract.mockResolvedValue(true);
-    mediaMock.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
-    systemMock.get.mockResolvedValue({ image: { extractEmbedded: true } });
-    assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
+    it('should resize original image if embedded image is too small', async () => {
+      mediaMock.extract.mockResolvedValue(true);
+      mediaMock.getImageDimensions.mockResolvedValue({ width: 1000, height: 1000 });
+      systemMock.get.mockResolvedValue({ image: { extractEmbedded: true } });
+      assetMock.getById.mockResolvedValue(assetStub.imageDng);
 
-    await sut.handleGenerateThumbnail({ id: assetStub.image.id });
+      await sut.handleGenerateThumbnails({ id: assetStub.image.id });
 
-    const extractedPath = mediaMock.extract.mock.calls.at(-1)?.[1].toString();
-    expect(mediaMock.generateThumbnail.mock.calls).toEqual([
-      [
-        extractedPath,
-        'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
-        {
-          format: ImageFormat.WEBP,
-          size: 250,
-          quality: 80,
-          colorspace: Colorspace.P3,
-          processInvalidImages: false,
-        },
-      ],
-    ]);
-    expect(extractedPath?.endsWith('.tmp')).toBe(true);
-    expect(storageMock.unlink).toHaveBeenCalledWith(extractedPath);
-  });
+      expect(mediaMock.decodeImage).toHaveBeenCalledOnce();
+      expect(mediaMock.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, {
+        colorspace: Colorspace.P3,
+        processInvalidImages: false,
+        size: 1440,
+      });
+      const extractedPath = mediaMock.extract.mock.calls.at(-1)?.[1].toString();
+      expect(extractedPath?.endsWith('.tmp')).toBe(true);
+      expect(storageMock.unlink).toHaveBeenCalledWith(extractedPath);
+    });
 
-  it('should resize original image if embedded image is too small', async () => {
-    mediaMock.extract.mockResolvedValue(true);
-    mediaMock.getImageDimensions.mockResolvedValue({ width: 1000, height: 1000 });
-    systemMock.get.mockResolvedValue({ image: { extractEmbedded: true } });
-    assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
+    it('should resize original image if embedded image not found', async () => {
+      systemMock.get.mockResolvedValue({ image: { extractEmbedded: true } });
+      assetMock.getById.mockResolvedValue(assetStub.imageDng);
 
-    await sut.handleGenerateThumbnail({ id: assetStub.image.id });
+      await sut.handleGenerateThumbnails({ id: assetStub.image.id });
 
-    expect(mediaMock.generateThumbnail.mock.calls).toEqual([
-      [
+      expect(mediaMock.decodeImage).toHaveBeenCalledOnce();
+      expect(mediaMock.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, {
+        colorspace: Colorspace.P3,
+        processInvalidImages: false,
+        size: 1440,
+      });
+      expect(mediaMock.getImageDimensions).not.toHaveBeenCalled();
+    });
+
+    it('should resize original image if embedded image extraction is not enabled', async () => {
+      systemMock.get.mockResolvedValue({ image: { extractEmbedded: false } });
+      assetMock.getById.mockResolvedValue(assetStub.imageDng);
+
+      await sut.handleGenerateThumbnails({ id: assetStub.image.id });
+
+      expect(mediaMock.extract).not.toHaveBeenCalled();
+      expect(mediaMock.decodeImage).toHaveBeenCalledOnce();
+      expect(mediaMock.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, {
+        colorspace: Colorspace.P3,
+        processInvalidImages: false,
+        size: 1440,
+      });
+      expect(mediaMock.getImageDimensions).not.toHaveBeenCalled();
+    });
+
+    it('should process invalid images if enabled', async () => {
+      vi.stubEnv('IMMICH_PROCESS_INVALID_IMAGES', 'true');
+
+      assetMock.getById.mockResolvedValue(assetStub.imageDng);
+
+      await sut.handleGenerateThumbnails({ id: assetStub.image.id });
+
+      expect(mediaMock.decodeImage).toHaveBeenCalledOnce();
+      expect(mediaMock.decodeImage).toHaveBeenCalledWith(
         assetStub.imageDng.originalPath,
+        expect.objectContaining({ processInvalidImages: true }),
+      );
+
+      expect(mediaMock.generateThumbnail).toHaveBeenCalledTimes(2);
+      expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
+        rawBuffer,
+        expect.objectContaining({ processInvalidImages: true }),
+        'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
+      );
+      expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
+        rawBuffer,
+        expect.objectContaining({ processInvalidImages: true }),
         'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
-        {
-          format: ImageFormat.WEBP,
-          size: 250,
-          quality: 80,
-          colorspace: Colorspace.P3,
-          processInvalidImages: false,
-        },
-      ],
-    ]);
-    const extractedPath = mediaMock.extract.mock.calls.at(-1)?.[1].toString();
-    expect(extractedPath?.endsWith('.tmp')).toBe(true);
-    expect(storageMock.unlink).toHaveBeenCalledWith(extractedPath);
-  });
+      );
 
-  it('should resize original image if embedded image not found', async () => {
-    systemMock.get.mockResolvedValue({ image: { extractEmbedded: true } });
-    assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
+      expect(mediaMock.generateThumbhash).toHaveBeenCalledOnce();
+      expect(mediaMock.generateThumbhash).toHaveBeenCalledWith(
+        rawBuffer,
+        expect.objectContaining({ processInvalidImages: true }),
+      );
 
-    await sut.handleGenerateThumbnail({ id: assetStub.image.id });
-
-    expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
-      assetStub.imageDng.originalPath,
-      'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
-      {
-        format: ImageFormat.WEBP,
-        size: 250,
-        quality: 80,
-        colorspace: Colorspace.P3,
-        processInvalidImages: false,
-      },
-    );
-    expect(mediaMock.getImageDimensions).not.toHaveBeenCalled();
-  });
-
-  it('should resize original image if embedded image extraction is not enabled', async () => {
-    systemMock.get.mockResolvedValue({ image: { extractEmbedded: false } });
-    assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
-
-    await sut.handleGenerateThumbnail({ id: assetStub.image.id });
-
-    expect(mediaMock.extract).not.toHaveBeenCalled();
-    expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
-      assetStub.imageDng.originalPath,
-      'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
-      {
-        format: ImageFormat.WEBP,
-        size: 250,
-        quality: 80,
-        colorspace: Colorspace.P3,
-        processInvalidImages: false,
-      },
-    );
-    expect(mediaMock.getImageDimensions).not.toHaveBeenCalled();
-  });
-
-  it('should process invalid images if enabled', async () => {
-    vi.stubEnv('IMMICH_PROCESS_INVALID_IMAGES', 'true');
-
-    assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
-
-    await sut.handleGenerateThumbnail({ id: assetStub.image.id });
-
-    expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
-      assetStub.imageDng.originalPath,
-      'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
-      {
-        format: ImageFormat.WEBP,
-        size: 250,
-        quality: 80,
-        colorspace: Colorspace.P3,
-        processInvalidImages: true,
-      },
-    );
-    expect(mediaMock.getImageDimensions).not.toHaveBeenCalled();
-    vi.unstubAllEnvs();
-  });
-
-  describe('handleGenerateThumbhash', () => {
-    it('should skip thumbhash generation if asset not found', async () => {
-      assetMock.getByIds.mockResolvedValue([]);
-      await sut.handleGenerateThumbhash({ id: assetStub.image.id });
-      expect(mediaMock.generateThumbhash).not.toHaveBeenCalled();
-    });
-
-    it('should skip thumbhash generation if resize path is missing', async () => {
-      assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]);
-      await sut.handleGenerateThumbhash({ id: assetStub.noResizePath.id });
-      expect(mediaMock.generateThumbhash).not.toHaveBeenCalled();
-    });
-
-    it('should skip invisible assets', async () => {
-      assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
-
-      expect(await sut.handleGenerateThumbhash({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED);
-
-      expect(mediaMock.generateThumbhash).not.toHaveBeenCalled();
-      expect(assetMock.update).not.toHaveBeenCalledWith();
-    });
-
-    it('should generate a thumbhash', async () => {
-      const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
-      assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      mediaMock.generateThumbhash.mockResolvedValue(thumbhashBuffer);
-
-      await sut.handleGenerateThumbhash({ id: assetStub.image.id });
-
-      expect(mediaMock.generateThumbhash).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg');
-      expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer });
+      expect(mediaMock.getImageDimensions).not.toHaveBeenCalled();
+      vi.unstubAllEnvs();
     });
   });
 
@@ -712,6 +769,7 @@ describe(MediaService.name, () => {
   describe('handleVideoConversion', () => {
     beforeEach(() => {
       assetMock.getByIds.mockResolvedValue([assetStub.video]);
+      sut.videoInterfaces = { dri: ['renderD128'], mali: true };
     });
 
     it('should skip transcoding if asset not found', async () => {
@@ -730,21 +788,22 @@ describe(MediaService.name, () => {
 
     it('should transcode the longest stream', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.video]);
+      loggerMock.isLevelEnabled.mockReturnValue(false);
       mediaMock.probe.mockResolvedValue(probeStub.multipleVideoStreams);
 
       await sut.handleVideoConversion({ id: assetStub.video.id });
 
-      expect(mediaMock.probe).toHaveBeenCalledWith('/original/path.ext');
+      expect(mediaMock.probe).toHaveBeenCalledWith('/original/path.ext', { countFrames: false });
       expect(systemMock.get).toHaveBeenCalled();
       expect(storageMock.mkdirSync).toHaveBeenCalled();
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining(['-map 0:0', '-map 0:1']),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -762,6 +821,27 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).not.toHaveBeenCalled();
     });
 
+    it('should throw an error if an unknown transcode policy is configured', async () => {
+      mediaMock.probe.mockResolvedValue(probeStub.noAudioStreams);
+      systemMock.get.mockResolvedValue({ ffmpeg: { transcode: 'foo' } } as never as SystemConfig);
+      assetMock.getByIds.mockResolvedValue([assetStub.video]);
+
+      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError();
+      expect(mediaMock.transcode).not.toHaveBeenCalled();
+    });
+
+    it('should throw an error if transcoding fails and hw acceleration is disabled', async () => {
+      mediaMock.probe.mockResolvedValue(probeStub.multipleVideoStreams);
+      systemMock.get.mockResolvedValue({
+        ffmpeg: { transcode: TranscodePolicy.ALL, accel: TranscodeHWAccel.DISABLED },
+      });
+      assetMock.getByIds.mockResolvedValue([assetStub.video]);
+      mediaMock.transcode.mockRejectedValue(new Error('Error transcoding video'));
+
+      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
+      expect(mediaMock.transcode).toHaveBeenCalledTimes(1);
+    });
+
     it('should transcode when set to all', async () => {
       mediaMock.probe.mockResolvedValue(probeStub.multipleVideoStreams);
       systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.ALL } });
@@ -770,11 +850,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.any(Array),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -785,26 +865,41 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.any(Array),
           twoPass: false,
-        },
+        }),
       );
     });
 
-    it('should transcode when policy Bitrate and bitrate higher than max bitrate', async () => {
+    it('should transcode when policy bitrate and bitrate higher than max bitrate', async () => {
       mediaMock.probe.mockResolvedValue(probeStub.videoStream40Mbps);
       systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.BITRATE, maxBitrate: '30M' } });
       await sut.handleVideoConversion({ id: assetStub.video.id });
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.any(Array),
           twoPass: false,
-        },
+        }),
+      );
+    });
+
+    it('should transcode when max bitrate is not a number', async () => {
+      mediaMock.probe.mockResolvedValue(probeStub.videoStream40Mbps);
+      systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.BITRATE, maxBitrate: 'foo' } });
+      await sut.handleVideoConversion({ id: assetStub.video.id });
+      expect(mediaMock.transcode).toHaveBeenCalledWith(
+        '/original/path.ext',
+        'upload/encoded-video/user-id/as/se/asset-id.mp4',
+        expect.objectContaining({
+          inputOptions: expect.any(Array),
+          outputOptions: expect.any(Array),
+          twoPass: false,
+        }),
       );
     });
 
@@ -815,11 +910,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.not.arrayContaining([expect.stringContaining('scale')]),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -831,11 +926,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining([expect.stringMatching(/scale(_.+)?=-2:720/)]),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -847,11 +942,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining([expect.stringMatching(/scale(_.+)?=720:-2/)]),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -863,11 +958,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining([expect.stringMatching(/scale(_.+)?=-2:354/)]),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -879,11 +974,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining([expect.stringMatching(/scale(_.+)?=354:-2/)]),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -897,11 +992,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining(['-c:v copy', '-c:a aac']),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -919,11 +1014,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.not.arrayContaining(['-tag:v hvc1']),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -941,11 +1036,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining(['-c:v copy', '-tag:v hvc1']),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -957,11 +1052,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining(['-c:v h264', '-c:a copy']),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -972,11 +1067,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining(['-c:v copy', '-c:a copy']),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -984,7 +1079,7 @@ describe(MediaService.name, () => {
       mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
       systemMock.get.mockResolvedValue({ ffmpeg: { transcode: 'invalid' as any } });
 
-      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrow();
+      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError();
       expect(mediaMock.transcode).not.toHaveBeenCalled();
     });
 
@@ -1035,11 +1130,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining(['-c:v h264', '-maxrate 4500k', '-bufsize 9000k']),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1051,11 +1146,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining(['-c:v h264', '-b:v 3104k', '-minrate 1552k', '-maxrate 4500k']),
           twoPass: true,
-        },
+        }),
       );
     });
 
@@ -1067,11 +1162,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining(['-c:v h264', '-c:a copy']),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1089,11 +1184,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining(['-b:v 3104k', '-minrate 1552k', '-maxrate 4500k']),
           twoPass: true,
-        },
+        }),
       );
     });
 
@@ -1111,11 +1206,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.not.arrayContaining([expect.stringContaining('-maxrate')]),
           twoPass: true,
-        },
+        }),
       );
     });
 
@@ -1127,11 +1222,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining(['-cpu-used 2']),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1143,11 +1238,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.not.arrayContaining([expect.stringContaining('-cpu-used')]),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1159,11 +1254,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining(['-threads 2']),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1175,11 +1270,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining(['-threads 1', '-x264-params frame-threads=1:pools=none']),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1191,11 +1286,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.not.arrayContaining([expect.stringContaining('-threads')]),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1207,11 +1302,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining(['-c:v hevc', '-threads 1', '-x265-params frame-threads=1:pools=none']),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1223,11 +1318,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.not.arrayContaining([expect.stringContaining('-threads')]),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1239,22 +1334,21 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining([
-            '-c:v av1',
+            '-c:v libsvtav1',
             '-movflags faststart',
             '-fps_mode passthrough',
             '-map 0:0',
             '-map 0:1',
-            '-strict unofficial',
             '-v verbose',
-            '-vf scale=-2:720,format=yuv420p',
+            '-vf scale=-2:720',
             '-preset 12',
             '-crf 23',
           ]),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1266,11 +1360,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining(['-preset 4']),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1282,11 +1376,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining(['-svtav1-params mbr=2M']),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1298,11 +1392,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining(['-svtav1-params lp=4']),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1314,11 +1408,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining(['-svtav1-params lp=4:mbr=2M']),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1340,7 +1434,7 @@ describe(MediaService.name, () => {
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
       systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC, targetVideoCodec: VideoCodec.VP9 } });
       assetMock.getByIds.mockResolvedValue([assetStub.video]);
-      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
+      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError();
       expect(mediaMock.transcode).not.toHaveBeenCalled();
     });
 
@@ -1348,7 +1442,7 @@ describe(MediaService.name, () => {
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
       systemMock.get.mockResolvedValue({ ffmpeg: { accel: 'invalid' as any } });
       assetMock.getByIds.mockResolvedValue([assetStub.video]);
-      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
+      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError();
       expect(mediaMock.transcode).not.toHaveBeenCalled();
     });
 
@@ -1360,7 +1454,7 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']),
           outputOptions: expect.arrayContaining([
             '-tune hq',
@@ -1373,15 +1467,14 @@ describe(MediaService.name, () => {
             '-fps_mode passthrough',
             '-map 0:0',
             '-map 0:1',
-            '-strict unofficial',
             '-g 256',
             '-v verbose',
-            '-vf format=nv12,hwupload_cuda,scale_cuda=-2:720',
+            '-vf hwupload_cuda,scale_cuda=-2:720:format=nv12',
             '-preset p1',
             '-cq:v 23',
           ]),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1399,11 +1492,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']),
           outputOptions: expect.arrayContaining([expect.stringContaining('-multipass')]),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1415,11 +1508,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']),
           outputOptions: expect.arrayContaining(['-cq:v 23', '-maxrate 10000k', '-bufsize 6897k']),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1431,11 +1524,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']),
           outputOptions: expect.not.stringContaining('-maxrate'),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1447,11 +1540,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']),
           outputOptions: expect.not.arrayContaining([expect.stringContaining('-preset')]),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1463,11 +1556,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']),
           outputOptions: expect.not.arrayContaining([expect.stringContaining('-multipass')]),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1481,7 +1574,7 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining([
             '-hwaccel cuda',
             '-hwaccel_output_format cuda',
@@ -1490,7 +1583,7 @@ describe(MediaService.name, () => {
           ]),
           outputOptions: expect.arrayContaining([expect.stringContaining('scale_cuda=-2:720:format=nv12')]),
           twoPass: false,
-        },
+        }),
       );
     });
 
@@ -1504,20 +1597,37 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining(['-hwaccel cuda', '-hwaccel_output_format cuda']),
           outputOptions: expect.arrayContaining([
             expect.stringContaining(
-              'tonemap_cuda=desat=0:matrix=bt709:primaries=bt709:range=pc:tonemap=hable:transfer=bt709:format=nv12',
+              'tonemap_cuda=desat=0:matrix=bt709:primaries=bt709:range=pc:tonemap=hable:tonemap_mode=lum:transfer=bt709:peak=100:format=nv12',
             ),
           ]),
           twoPass: false,
-        },
+        }),
+      );
+    });
+
+    it('should set format to nv12 for nvenc if input is not yuv420p', async () => {
+      mediaMock.probe.mockResolvedValue(probeStub.videoStream10Bit);
+      systemMock.get.mockResolvedValue({
+        ffmpeg: { accel: TranscodeHWAccel.NVENC, accelDecode: true },
+      });
+      assetMock.getByIds.mockResolvedValue([assetStub.video]);
+      await sut.handleVideoConversion({ id: assetStub.video.id });
+      expect(mediaMock.transcode).toHaveBeenCalledWith(
+        '/original/path.ext',
+        'upload/encoded-video/user-id/as/se/asset-id.mp4',
+        expect.objectContaining({
+          inputOptions: expect.arrayContaining(['-hwaccel cuda', '-hwaccel_output_format cuda']),
+          outputOptions: expect.arrayContaining([expect.stringContaining('format=nv12')]),
+          twoPass: false,
+        }),
       );
     });
 
     it('should set options for qsv', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD128']);
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
       systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, maxBitrate: '10000k' } });
       assetMock.getByIds.mockResolvedValue([assetStub.video]);
@@ -1525,8 +1635,11 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
-          inputOptions: expect.arrayContaining(['-init_hw_device qsv=hw', '-filter_hw_device hw']),
+        expect.objectContaining({
+          inputOptions: expect.arrayContaining([
+            '-init_hw_device qsv=hw,child_device=/dev/dri/renderD128',
+            '-filter_hw_device hw',
+          ]),
           outputOptions: expect.arrayContaining([
             `-c:v h264_qsv`,
             '-c:a copy',
@@ -1534,24 +1647,22 @@ describe(MediaService.name, () => {
             '-fps_mode passthrough',
             '-map 0:0',
             '-map 0:1',
-            '-strict unofficial',
             '-bf 7',
             '-refs 5',
             '-g 256',
             '-v verbose',
-            '-vf format=nv12,hwupload=extra_hw_frames=64,scale_qsv=-1:720',
+            '-vf hwupload=extra_hw_frames=64,scale_qsv=-1:720:mode=hq:format=nv12',
             '-preset 7',
             '-global_quality:v 23',
             '-maxrate 10000k',
             '-bufsize 20000k',
           ]),
           twoPass: false,
-        },
+        }),
       );
     });
 
     it('should set options for qsv with custom dri node', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD128']);
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
       systemMock.get.mockResolvedValue({
         ffmpeg: {
@@ -1565,19 +1676,18 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining([
             '-init_hw_device qsv=hw,child_device=/dev/dri/renderD128',
             '-filter_hw_device hw',
           ]),
           outputOptions: expect.any(Array),
           twoPass: false,
-        },
+        }),
       );
     });
 
     it('should omit preset for qsv if invalid', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD128']);
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
       systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, preset: 'invalid' } });
       assetMock.getByIds.mockResolvedValue([assetStub.video]);
@@ -1585,16 +1695,18 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
-          inputOptions: expect.arrayContaining(['-init_hw_device qsv=hw', '-filter_hw_device hw']),
+        expect.objectContaining({
+          inputOptions: expect.arrayContaining([
+            '-init_hw_device qsv=hw,child_device=/dev/dri/renderD128',
+            '-filter_hw_device hw',
+          ]),
           outputOptions: expect.not.arrayContaining([expect.stringContaining('-preset')]),
           twoPass: false,
-        },
+        }),
       );
     });
 
     it('should set low power mode for qsv if target video codec is vp9', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD128']);
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
       systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, targetVideoCodec: VideoCodec.VP9 } });
       assetMock.getByIds.mockResolvedValue([assetStub.video]);
@@ -1602,25 +1714,49 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
-          inputOptions: expect.arrayContaining(['-init_hw_device qsv=hw', '-filter_hw_device hw']),
+        expect.objectContaining({
+          inputOptions: expect.arrayContaining([
+            '-init_hw_device qsv=hw,child_device=/dev/dri/renderD128',
+            '-filter_hw_device hw',
+          ]),
           outputOptions: expect.arrayContaining(['-low_power 1']),
           twoPass: false,
-        },
+        }),
       );
     });
 
     it('should fail for qsv if no hw devices', async () => {
-      storageMock.readdir.mockResolvedValue([]);
+      sut.videoInterfaces = { dri: [], mali: false };
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
       systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV } });
       assetMock.getByIds.mockResolvedValue([assetStub.video]);
-      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
+
+      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError();
+
       expect(mediaMock.transcode).not.toHaveBeenCalled();
     });
 
+    it('should prefer higher index renderD* device for qsv', async () => {
+      sut.videoInterfaces = { dri: ['card1', 'renderD129', 'card0', 'renderD128'], mali: false };
+      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
+      systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV } });
+      assetMock.getByIds.mockResolvedValue([assetStub.video]);
+      await sut.handleVideoConversion({ id: assetStub.video.id });
+      expect(mediaMock.transcode).toHaveBeenCalledWith(
+        '/original/path.ext',
+        'upload/encoded-video/user-id/as/se/asset-id.mp4',
+        expect.objectContaining({
+          inputOptions: expect.arrayContaining([
+            '-init_hw_device qsv=hw,child_device=/dev/dri/renderD129',
+            '-filter_hw_device hw',
+          ]),
+          outputOptions: expect.arrayContaining([`-c:v h264_qsv`]),
+          twoPass: false,
+        }),
+      );
+    });
+
     it('should use hardware decoding for qsv if enabled', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD128']);
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
       systemMock.get.mockResolvedValue({
         ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true },
@@ -1632,24 +1768,24 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining([
             '-hwaccel qsv',
             '-hwaccel_output_format qsv',
             '-async_depth 4',
             '-noautorotate',
             '-threads 1',
+            '-qsv_device /dev/dri/renderD128',
           ]),
           outputOptions: expect.arrayContaining([
             expect.stringContaining('scale_qsv=-1:720:async_depth=4:mode=hq:format=nv12'),
           ]),
           twoPass: false,
-        },
+        }),
       );
     });
 
     it('should use hardware tone-mapping for qsv if hardware decoding is enabled and should tone map', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD128']);
       mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
       systemMock.get.mockResolvedValue({
         ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true },
@@ -1661,7 +1797,7 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining([
             '-hwaccel qsv',
             '-hwaccel_output_format qsv',
@@ -1670,16 +1806,16 @@ describe(MediaService.name, () => {
           ]),
           outputOptions: expect.arrayContaining([
             expect.stringContaining(
-              'hwmap=derive_device=opencl,tonemap_opencl=desat=0:format=nv12:matrix=bt709:primaries=bt709:range=pc:tonemap=hable:transfer=bt709,hwmap=derive_device=qsv:reverse=1,format=qsv',
+              'hwmap=derive_device=opencl,tonemap_opencl=desat=0:format=nv12:matrix=bt709:primaries=bt709:transfer=bt709:range=pc:tonemap=hable:tonemap_mode=lum:peak=100,hwmap=derive_device=qsv:reverse=1,format=qsv',
             ),
           ]),
           twoPass: false,
-        },
+        }),
       );
     });
 
     it('should use preferred device for qsv when hardware decoding', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD128', 'renderD129', 'renderD130']);
+      sut.videoInterfaces = { dri: ['renderD128', 'renderD129', 'renderD130'], mali: false };
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
       systemMock.get.mockResolvedValue({
         ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true, preferredHwDevice: 'renderD129' },
@@ -1690,16 +1826,40 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining(['-hwaccel qsv', '-qsv_device /dev/dri/renderD129']),
           outputOptions: expect.any(Array),
           twoPass: false,
-        },
+        }),
+      );
+    });
+
+    it('should set format to nv12 for qsv if input is not yuv420p', async () => {
+      mediaMock.probe.mockResolvedValue(probeStub.videoStream10Bit);
+      systemMock.get.mockResolvedValue({
+        ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true },
+      });
+      assetMock.getByIds.mockResolvedValue([assetStub.video]);
+
+      await sut.handleVideoConversion({ id: assetStub.video.id });
+
+      expect(mediaMock.transcode).toHaveBeenCalledWith(
+        '/original/path.ext',
+        'upload/encoded-video/user-id/as/se/asset-id.mp4',
+        expect.objectContaining({
+          inputOptions: expect.arrayContaining([
+            '-hwaccel qsv',
+            '-hwaccel_output_format qsv',
+            '-async_depth 4',
+            '-threads 1',
+          ]),
+          outputOptions: expect.arrayContaining([expect.stringContaining('format=nv12')]),
+          twoPass: false,
+        }),
       );
     });
 
     it('should set options for vaapi', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD128']);
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
       systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
       assetMock.getByIds.mockResolvedValue([assetStub.video]);
@@ -1707,7 +1867,7 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining([
             '-init_hw_device vaapi=accel:/dev/dri/renderD128',
             '-filter_hw_device accel',
@@ -1719,20 +1879,18 @@ describe(MediaService.name, () => {
             '-fps_mode passthrough',
             '-map 0:0',
             '-map 0:1',
-            '-strict unofficial',
             '-g 256',
             '-v verbose',
-            '-vf format=nv12,hwupload,scale_vaapi=-2:720',
+            '-vf hwupload=extra_hw_frames=64,scale_vaapi=-2:720:mode=hq:out_range=pc:format=nv12',
             '-compression_level 7',
             '-rc_mode 1',
           ]),
           twoPass: false,
-        },
+        }),
       );
     });
 
     it('should set vbr options for vaapi when max bitrate is enabled', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD128']);
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
       systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, maxBitrate: '10000k' } });
       assetMock.getByIds.mockResolvedValue([assetStub.video]);
@@ -1740,7 +1898,7 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining([
             '-init_hw_device vaapi=accel:/dev/dri/renderD128',
             '-filter_hw_device accel',
@@ -1753,12 +1911,11 @@ describe(MediaService.name, () => {
             '-rc_mode 3',
           ]),
           twoPass: false,
-        },
+        }),
       );
     });
 
     it('should set cq options for vaapi when max bitrate is disabled', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD128']);
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
       systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
       assetMock.getByIds.mockResolvedValue([assetStub.video]);
@@ -1766,7 +1923,7 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining([
             '-init_hw_device vaapi=accel:/dev/dri/renderD128',
             '-filter_hw_device accel',
@@ -1779,12 +1936,11 @@ describe(MediaService.name, () => {
             '-rc_mode 1',
           ]),
           twoPass: false,
-        },
+        }),
       );
     });
 
     it('should omit preset for vaapi if invalid', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD128']);
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
       systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, preset: 'invalid' } });
       assetMock.getByIds.mockResolvedValue([assetStub.video]);
@@ -1792,19 +1948,19 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining([
             '-init_hw_device vaapi=accel:/dev/dri/renderD128',
             '-filter_hw_device accel',
           ]),
           outputOptions: expect.not.arrayContaining([expect.stringContaining('-compression_level')]),
           twoPass: false,
-        },
+        }),
       );
     });
 
-    it('should prefer gpu for vaapi if available', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD129', 'card1', 'card0', 'renderD128']);
+    it('should prefer higher index renderD* device for vaapi', async () => {
+      sut.videoInterfaces = { dri: ['card1', 'renderD129', 'card0', 'renderD128'], mali: false };
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
       systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
       assetMock.getByIds.mockResolvedValue([assetStub.video]);
@@ -1812,39 +1968,19 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining([
-            '-init_hw_device vaapi=accel:/dev/dri/card1',
+            '-init_hw_device vaapi=accel:/dev/dri/renderD129',
             '-filter_hw_device accel',
           ]),
           outputOptions: expect.arrayContaining([`-c:v h264_vaapi`]),
           twoPass: false,
-        },
-      );
-    });
-
-    it('should prefer higher index gpu node', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD129', 'renderD130', 'renderD128']);
-      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
-      systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
-      assetMock.getByIds.mockResolvedValue([assetStub.video]);
-      await sut.handleVideoConversion({ id: assetStub.video.id });
-      expect(mediaMock.transcode).toHaveBeenCalledWith(
-        '/original/path.ext',
-        'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
-          inputOptions: expect.arrayContaining([
-            '-init_hw_device vaapi=accel:/dev/dri/renderD130',
-            '-filter_hw_device accel',
-          ]),
-          outputOptions: expect.arrayContaining([`-c:v h264_vaapi`]),
-          twoPass: false,
-        },
+        }),
       );
     });
 
     it('should select specific gpu node if selected', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD129', 'card1', 'card0', 'renderD128']);
+      sut.videoInterfaces = { dri: ['renderD129', 'card1', 'card0', 'renderD128'], mali: false };
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
       systemMock.get.mockResolvedValue({
         ffmpeg: { accel: TranscodeHWAccel.VAAPI, preferredHwDevice: '/dev/dri/renderD128' },
@@ -1854,19 +1990,150 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining([
             '-init_hw_device vaapi=accel:/dev/dri/renderD128',
             '-filter_hw_device accel',
           ]),
           outputOptions: expect.arrayContaining([`-c:v h264_vaapi`]),
           twoPass: false,
-        },
+        }),
       );
     });
 
-    it('should fallback to sw transcoding if hw transcoding fails', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD128']);
+    it('should use hardware decoding for vaapi if enabled', async () => {
+      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
+      systemMock.get.mockResolvedValue({
+        ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true },
+      });
+      assetMock.getByIds.mockResolvedValue([assetStub.video]);
+
+      await sut.handleVideoConversion({ id: assetStub.video.id });
+
+      expect(mediaMock.transcode).toHaveBeenCalledWith(
+        '/original/path.ext',
+        'upload/encoded-video/user-id/as/se/asset-id.mp4',
+        expect.objectContaining({
+          inputOptions: expect.arrayContaining([
+            '-hwaccel vaapi',
+            '-hwaccel_output_format vaapi',
+            '-noautorotate',
+            '-threads 1',
+            '-hwaccel_device /dev/dri/renderD128',
+          ]),
+          outputOptions: expect.arrayContaining([
+            expect.stringContaining('scale_vaapi=-2:720:mode=hq:out_range=pc:format=nv12'),
+          ]),
+          twoPass: false,
+        }),
+      );
+    });
+
+    it('should use hardware tone-mapping for vaapi if hardware decoding is enabled and should tone map', async () => {
+      mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
+      systemMock.get.mockResolvedValue({
+        ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true },
+      });
+      assetMock.getByIds.mockResolvedValue([assetStub.video]);
+
+      await sut.handleVideoConversion({ id: assetStub.video.id });
+
+      expect(mediaMock.transcode).toHaveBeenCalledWith(
+        '/original/path.ext',
+        'upload/encoded-video/user-id/as/se/asset-id.mp4',
+        expect.objectContaining({
+          inputOptions: expect.arrayContaining(['-hwaccel vaapi', '-hwaccel_output_format vaapi', '-threads 1']),
+          outputOptions: expect.arrayContaining([
+            expect.stringContaining(
+              'hwmap=derive_device=opencl,tonemap_opencl=desat=0:format=nv12:matrix=bt709:primaries=bt709:transfer=bt709:range=pc:tonemap=hable:tonemap_mode=lum:peak=100,hwmap=derive_device=vaapi:reverse=1,format=vaapi',
+            ),
+          ]),
+          twoPass: false,
+        }),
+      );
+    });
+
+    it('should set format to nv12 for vaapi if input is not yuv420p', async () => {
+      mediaMock.probe.mockResolvedValue(probeStub.videoStream10Bit);
+      systemMock.get.mockResolvedValue({
+        ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true },
+      });
+      assetMock.getByIds.mockResolvedValue([assetStub.video]);
+
+      await sut.handleVideoConversion({ id: assetStub.video.id });
+
+      expect(mediaMock.transcode).toHaveBeenCalledWith(
+        '/original/path.ext',
+        'upload/encoded-video/user-id/as/se/asset-id.mp4',
+        expect.objectContaining({
+          inputOptions: expect.arrayContaining(['-hwaccel vaapi', '-hwaccel_output_format vaapi', '-threads 1']),
+          outputOptions: expect.arrayContaining([expect.stringContaining('format=nv12')]),
+          twoPass: false,
+        }),
+      );
+    });
+
+    it('should use preferred device for vaapi when hardware decoding', async () => {
+      sut.videoInterfaces = { dri: ['renderD128', 'renderD129', 'renderD130'], mali: false };
+      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
+      systemMock.get.mockResolvedValue({
+        ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true, preferredHwDevice: 'renderD129' },
+      });
+      assetMock.getByIds.mockResolvedValue([assetStub.video]);
+
+      await sut.handleVideoConversion({ id: assetStub.video.id });
+      expect(mediaMock.transcode).toHaveBeenCalledWith(
+        '/original/path.ext',
+        'upload/encoded-video/user-id/as/se/asset-id.mp4',
+        expect.objectContaining({
+          inputOptions: expect.arrayContaining(['-hwaccel vaapi', '-hwaccel_device /dev/dri/renderD129']),
+          outputOptions: expect.any(Array),
+          twoPass: false,
+        }),
+      );
+    });
+
+    it('should fallback to hw encoding and sw decoding if hw transcoding fails and hw decoding is enabled', async () => {
+      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
+      systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true } });
+      assetMock.getByIds.mockResolvedValue([assetStub.video]);
+      mediaMock.transcode.mockRejectedValueOnce(new Error('error'));
+      await sut.handleVideoConversion({ id: assetStub.video.id });
+      expect(mediaMock.transcode).toHaveBeenCalledTimes(2);
+      expect(mediaMock.transcode).toHaveBeenLastCalledWith(
+        '/original/path.ext',
+        'upload/encoded-video/user-id/as/se/asset-id.mp4',
+        expect.objectContaining({
+          inputOptions: expect.arrayContaining([
+            '-init_hw_device vaapi=accel:/dev/dri/renderD128',
+            '-filter_hw_device accel',
+          ]),
+          outputOptions: expect.arrayContaining([`-c:v h264_vaapi`]),
+          twoPass: false,
+        }),
+      );
+    });
+
+    it('should fallback to sw decoding if fallback to sw decoding + hw encoding fails', async () => {
+      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
+      systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true } });
+      assetMock.getByIds.mockResolvedValue([assetStub.video]);
+      mediaMock.transcode.mockRejectedValueOnce(new Error('error'));
+      mediaMock.transcode.mockRejectedValueOnce(new Error('error'));
+      await sut.handleVideoConversion({ id: assetStub.video.id });
+      expect(mediaMock.transcode).toHaveBeenCalledTimes(3);
+      expect(mediaMock.transcode).toHaveBeenLastCalledWith(
+        '/original/path.ext',
+        'upload/encoded-video/user-id/as/se/asset-id.mp4',
+        expect.objectContaining({
+          inputOptions: expect.any(Array),
+          outputOptions: expect.arrayContaining(['-c:v h264']),
+          twoPass: false,
+        }),
+      );
+    });
+
+    it('should fallback to sw transcoding if hw transcoding fails and hw decoding is disabled', async () => {
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
       systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
       assetMock.getByIds.mockResolvedValue([assetStub.video]);
@@ -1876,26 +2143,24 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenLastCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining(['-c:v h264']),
           twoPass: false,
-        },
+        }),
       );
     });
 
     it('should fail for vaapi if no hw devices', async () => {
-      storageMock.readdir.mockResolvedValue([]);
+      sut.videoInterfaces = { dri: [], mali: true };
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
       systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
       assetMock.getByIds.mockResolvedValue([assetStub.video]);
-      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
+      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError();
       expect(mediaMock.transcode).not.toHaveBeenCalled();
     });
 
     it('should set options for rkmpp', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD128']);
-      storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
       systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true } });
       assetMock.getByIds.mockResolvedValue([assetStub.video]);
@@ -1903,7 +2168,7 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining([
             '-hwaccel rkmpp',
             '-hwaccel_output_format drm_prime',
@@ -1917,22 +2182,19 @@ describe(MediaService.name, () => {
             '-fps_mode passthrough',
             '-map 0:0',
             '-map 0:1',
-            '-strict unofficial',
             '-g 256',
             '-v verbose',
-            '-vf scale_rkrga=-2:720:format=nv12:afbc=1',
+            '-vf scale_rkrga=-2:720:format=nv12:afbc=1:async_depth=4',
             '-level 51',
             '-rc_mode CQP',
             '-qp_init 23',
           ]),
           twoPass: false,
-        },
+        }),
       );
     });
 
     it('should set vbr options for rkmpp when max bitrate is enabled', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD128']);
-      storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
       mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
       systemMock.get.mockResolvedValue({
         ffmpeg: {
@@ -1947,17 +2209,15 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']),
           outputOptions: expect.arrayContaining([`-c:v hevc_rkmpp`, '-level 153', '-rc_mode AVBR', '-b:v 10000k']),
           twoPass: false,
-        },
+        }),
       );
     });
 
     it('should set cqp options for rkmpp when max bitrate is disabled', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD128']);
-      storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
       systemMock.get.mockResolvedValue({
         ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' },
@@ -1967,17 +2227,15 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']),
           outputOptions: expect.arrayContaining([`-c:v h264_rkmpp`, '-level 51', '-rc_mode CQP', '-qp_init 30']),
           twoPass: false,
-        },
+        }),
       );
     });
 
     it('should set OpenCL tonemapping options for rkmpp when OpenCL is available', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD128']);
-      storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
       mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
       systemMock.get.mockResolvedValue({
         ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' },
@@ -1987,21 +2245,40 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']),
           outputOptions: expect.arrayContaining([
             expect.stringContaining(
-              'scale_rkrga=-2:720:format=p010:afbc=1,hwmap=derive_device=opencl:mode=read,tonemap_opencl=format=nv12:r=pc:p=bt709:t=bt709:m=bt709:tonemap=hable:desat=0,hwmap=derive_device=rkmpp:mode=write:reverse=1,format=drm_prime',
+              'scale_rkrga=-2:720:format=p010:afbc=1:async_depth=4,hwmap=derive_device=opencl:mode=read,tonemap_opencl=format=nv12:r=pc:p=bt709:t=bt709:m=bt709:tonemap=hable:desat=0:tonemap_mode=lum:peak=100,hwmap=derive_device=rkmpp:mode=write:reverse=1,format=drm_prime',
             ),
           ]),
           twoPass: false,
-        },
+        }),
+      );
+    });
+
+    it('should set hardware decoding options for rkmpp when hardware decoding is enabled with no OpenCL on non-HDR file', async () => {
+      sut.videoInterfaces = { dri: ['renderD128'], mali: false };
+      mediaMock.probe.mockResolvedValue(probeStub.noAudioStreams);
+      systemMock.get.mockResolvedValue({
+        ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' },
+      });
+      assetMock.getByIds.mockResolvedValue([assetStub.video]);
+      await sut.handleVideoConversion({ id: assetStub.video.id });
+      expect(mediaMock.transcode).toHaveBeenCalledWith(
+        '/original/path.ext',
+        'upload/encoded-video/user-id/as/se/asset-id.mp4',
+        expect.objectContaining({
+          inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']),
+          outputOptions: expect.arrayContaining([
+            expect.stringContaining('scale_rkrga=-2:720:format=nv12:afbc=1:async_depth=4'),
+          ]),
+          twoPass: false,
+        }),
       );
     });
 
     it('should use software decoding and tone-mapping if hardware decoding is disabled', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD128']);
-      storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
       mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
       systemMock.get.mockResolvedValue({
         ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: false, crf: 30, maxBitrate: '0' },
@@ -2011,21 +2288,20 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
+        expect.objectContaining({
           inputOptions: [],
           outputOptions: expect.arrayContaining([
             expect.stringContaining(
-              'zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p',
+              'tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p',
             ),
           ]),
           twoPass: false,
-        },
+        }),
       );
     });
 
-    it('should use software decoding and tone-mapping if opencl is not available', async () => {
-      storageMock.readdir.mockResolvedValue(['renderD128']);
-      storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => false, isCharacterDevice: () => false });
+    it('should use software tone-mapping if opencl is not available', async () => {
+      sut.videoInterfaces = { dri: ['renderD128'], mali: false };
       mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
       systemMock.get.mockResolvedValue({
         ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' },
@@ -2035,77 +2311,121 @@ describe(MediaService.name, () => {
       expect(mediaMock.transcode).toHaveBeenCalledWith(
         '/original/path.ext',
         'upload/encoded-video/user-id/as/se/asset-id.mp4',
-        {
-          inputOptions: [],
+        expect.objectContaining({
+          inputOptions: expect.any(Array),
           outputOptions: expect.arrayContaining([
             expect.stringContaining(
-              'zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p',
+              'tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p',
             ),
           ]),
           twoPass: false,
+        }),
+      );
+    });
+
+    it('should tonemap when policy is required and video is hdr', async () => {
+      mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
+      systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } });
+      assetMock.getByIds.mockResolvedValue([assetStub.video]);
+      await sut.handleVideoConversion({ id: assetStub.video.id });
+      expect(mediaMock.transcode).toHaveBeenCalledWith(
+        '/original/path.ext',
+        'upload/encoded-video/user-id/as/se/asset-id.mp4',
+        expect.objectContaining({
+          inputOptions: expect.any(Array),
+          outputOptions: expect.arrayContaining([
+            '-c:v h264',
+            '-c:a copy',
+            '-vf tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p',
+          ]),
+          twoPass: false,
+        }),
+      );
+    });
+
+    it('should tonemap when policy is optimal and video is hdr', async () => {
+      mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
+      systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } });
+      assetMock.getByIds.mockResolvedValue([assetStub.video]);
+      await sut.handleVideoConversion({ id: assetStub.video.id });
+      expect(mediaMock.transcode).toHaveBeenCalledWith(
+        '/original/path.ext',
+        'upload/encoded-video/user-id/as/se/asset-id.mp4',
+        expect.objectContaining({
+          inputOptions: expect.any(Array),
+          outputOptions: expect.arrayContaining([
+            '-c:v h264',
+            '-c:a copy',
+            '-vf tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p',
+          ]),
+          twoPass: false,
+        }),
+      );
+    });
+
+    it('should transcode when policy is required and video is not yuv420p', async () => {
+      mediaMock.probe.mockResolvedValue(probeStub.videoStream10Bit);
+      systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } });
+      assetMock.getByIds.mockResolvedValue([assetStub.video]);
+      await sut.handleVideoConversion({ id: assetStub.video.id });
+      expect(mediaMock.transcode).toHaveBeenCalledWith(
+        '/original/path.ext',
+        'upload/encoded-video/user-id/as/se/asset-id.mp4',
+        expect.objectContaining({
+          inputOptions: expect.any(Array),
+          outputOptions: expect.arrayContaining(['-c:v h264', '-c:a copy', '-vf format=yuv420p']),
+          twoPass: false,
+        }),
+      );
+    });
+
+    it('should count frames for progress when log level is debug', async () => {
+      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
+      loggerMock.isLevelEnabled.mockReturnValue(true);
+      assetMock.getByIds.mockResolvedValue([assetStub.video]);
+
+      await sut.handleVideoConversion({ id: assetStub.video.id });
+
+      expect(mediaMock.probe).toHaveBeenCalledWith(assetStub.video.originalPath, { countFrames: true });
+      expect(mediaMock.transcode).toHaveBeenCalledWith(
+        assetStub.video.originalPath,
+        'upload/encoded-video/user-id/as/se/asset-id.mp4',
+        {
+          inputOptions: expect.any(Array),
+          outputOptions: expect.any(Array),
+          twoPass: false,
+          progress: {
+            frameCount: probeStub.videoStream2160p.videoStreams[0].frameCount,
+            percentInterval: expect.any(Number),
+          },
         },
       );
     });
-  });
 
-  it('should tonemap when policy is required and video is hdr', async () => {
-    mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
-    systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } });
-    assetMock.getByIds.mockResolvedValue([assetStub.video]);
-    await sut.handleVideoConversion({ id: assetStub.video.id });
-    expect(mediaMock.transcode).toHaveBeenCalledWith(
-      '/original/path.ext',
-      'upload/encoded-video/user-id/as/se/asset-id.mp4',
-      {
-        inputOptions: expect.any(Array),
-        outputOptions: expect.arrayContaining([
-          '-c:v h264',
-          '-c:a copy',
-          '-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p',
-        ]),
-        twoPass: false,
-      },
-    );
-  });
+    it('should not count frames for progress when log level is not debug', async () => {
+      mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
+      loggerMock.isLevelEnabled.mockReturnValue(false);
+      assetMock.getByIds.mockResolvedValue([assetStub.video]);
+      await sut.handleVideoConversion({ id: assetStub.video.id });
 
-  it('should tonemap when policy is optimal and video is hdr', async () => {
-    mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
-    systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } });
-    assetMock.getByIds.mockResolvedValue([assetStub.video]);
-    await sut.handleVideoConversion({ id: assetStub.video.id });
-    expect(mediaMock.transcode).toHaveBeenCalledWith(
-      '/original/path.ext',
-      'upload/encoded-video/user-id/as/se/asset-id.mp4',
-      {
-        inputOptions: expect.any(Array),
-        outputOptions: expect.arrayContaining([
-          '-c:v h264',
-          '-c:a copy',
-          '-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p',
-        ]),
-        twoPass: false,
-      },
-    );
-  });
+      expect(mediaMock.probe).toHaveBeenCalledWith(assetStub.video.originalPath, { countFrames: false });
+    });
 
-  it('should set npl to 250 for reinhard and mobius tone-mapping algorithms', async () => {
-    mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
-    systemMock.get.mockResolvedValue({ ffmpeg: { tonemap: ToneMapping.MOBIUS } });
-    assetMock.getByIds.mockResolvedValue([assetStub.video]);
-    await sut.handleVideoConversion({ id: assetStub.video.id });
-    expect(mediaMock.transcode).toHaveBeenCalledWith(
-      '/original/path.ext',
-      'upload/encoded-video/user-id/as/se/asset-id.mp4',
-      {
-        inputOptions: expect.any(Array),
-        outputOptions: expect.arrayContaining([
-          '-c:v h264',
-          '-c:a copy',
-          '-vf zscale=t=linear:npl=250,tonemap=mobius:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p',
-        ]),
-        twoPass: false,
-      },
-    );
+    it('should process unknown audio stream', async () => {
+      mediaMock.probe.mockResolvedValue(probeStub.audioStreamUnknown);
+      assetMock.getByIds.mockResolvedValue([assetStub.video]);
+      await sut.handleVideoConversion({ id: assetStub.video.id });
+
+      expect(mediaMock.transcode).toHaveBeenCalledWith(
+        '/original/path.ext',
+        'upload/encoded-video/user-id/as/se/asset-id.mp4',
+        expect.objectContaining({
+          inputOptions: expect.any(Array),
+          outputOptions: expect.arrayContaining(['-c:a copy']),
+          twoPass: false,
+        }),
+      );
+    });
   });
 
   describe('isSRGB', () => {
diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts
index e74335bdc3..7036bd32e8 100644
--- a/server/src/services/media.service.ts
+++ b/server/src/services/media.service.ts
@@ -1,76 +1,51 @@
-import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
 import { dirname } from 'node:path';
+import { StorageCore } from 'src/cores/storage.core';
+import { OnEvent, OnJob } from 'src/decorators';
+import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
+import { AssetEntity } from 'src/entities/asset.entity';
 import {
+  AssetFileType,
+  AssetPathType,
+  AssetType,
   AudioCodec,
   Colorspace,
-  ImageFormat,
+  LogLevel,
+  StorageFolder,
   TranscodeHWAccel,
   TranscodePolicy,
   TranscodeTarget,
   VideoCodec,
   VideoContainer,
-} from 'src/config';
-import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core';
-import { SystemConfigCore } from 'src/cores/system-config.core';
-import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
-import { AssetEntity } from 'src/entities/asset.entity';
-import { AssetPathType } from 'src/entities/move.entity';
-import { AssetFileType, AssetType } from 'src/enum';
-import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+} from 'src/enum';
+import { UpsertFileOptions, WithoutProperty } from 'src/interfaces/asset.interface';
 import {
-  IBaseJob,
-  IEntityJob,
-  IJobRepository,
   JOBS_ASSET_PAGINATION_SIZE,
   JobItem,
   JobName,
+  JobOf,
   JobStatus,
   QueueName,
 } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { AudioStreamInfo, IMediaRepository, VideoFormat, VideoStreamInfo } from 'src/interfaces/media.interface';
-import { IMoveRepository } from 'src/interfaces/move.interface';
-import { IPersonRepository } from 'src/interfaces/person.interface';
-import { IStorageRepository } from 'src/interfaces/storage.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { AudioStreamInfo, VideoFormat, VideoInterfaces, VideoStreamInfo } from 'src/interfaces/media.interface';
+import { BaseService } from 'src/services/base.service';
 import { getAssetFiles } from 'src/utils/asset.util';
 import { BaseConfig, ThumbnailConfig } from 'src/utils/media';
 import { mimeTypes } from 'src/utils/mime-types';
 import { usePagination } from 'src/utils/pagination';
 
 @Injectable()
-export class MediaService {
-  private configCore: SystemConfigCore;
-  private storageCore: StorageCore;
-  private maliOpenCL?: boolean;
-  private devices?: string[];
+export class MediaService extends BaseService {
+  videoInterfaces: VideoInterfaces = { dri: [], mali: false };
 
-  constructor(
-    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
-    @Inject(IPersonRepository) private personRepository: IPersonRepository,
-    @Inject(IJobRepository) private jobRepository: IJobRepository,
-    @Inject(IMediaRepository) private mediaRepository: IMediaRepository,
-    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
-    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
-    @Inject(IMoveRepository) moveRepository: IMoveRepository,
-    @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-  ) {
-    this.logger.setContext(MediaService.name);
-    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
-    this.storageCore = StorageCore.create(
-      assetRepository,
-      cryptoRepository,
-      moveRepository,
-      personRepository,
-      storageRepository,
-      systemMetadataRepository,
-      this.logger,
-    );
+  @OnEvent({ name: 'app.bootstrap' })
+  async onBootstrap() {
+    const [dri, mali] = await Promise.all([this.getDevices(), this.hasMaliOpenCL()]);
+    this.videoInterfaces = { dri, mali };
   }
 
-  async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise<JobStatus> {
+  @OnJob({ name: JobName.QUEUE_GENERATE_THUMBNAILS, queue: QueueName.THUMBNAIL_GENERATION })
+  async handleQueueGenerateThumbnails({ force }: JobOf<JobName.QUEUE_GENERATE_THUMBNAILS>): Promise<JobStatus> {
     const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
       return force
         ? this.assetRepository.getAll(pagination, {
@@ -87,18 +62,10 @@ export class MediaService {
       for (const asset of assets) {
         const { previewFile, thumbnailFile } = getAssetFiles(asset.files);
 
-        if (!previewFile || force) {
-          jobs.push({ name: JobName.GENERATE_PREVIEW, data: { id: asset.id } });
+        if (!previewFile || !thumbnailFile || !asset.thumbhash || force) {
+          jobs.push({ name: JobName.GENERATE_THUMBNAILS, data: { id: asset.id } });
           continue;
         }
-
-        if (!thumbnailFile) {
-          jobs.push({ name: JobName.GENERATE_THUMBNAIL, data: { id: asset.id } });
-        }
-
-        if (!asset.thumbhash) {
-          jobs.push({ name: JobName.GENERATE_THUMBHASH, data: { id: asset.id } });
-        }
       }
 
       await this.jobRepository.queueAll(jobs);
@@ -129,6 +96,7 @@ export class MediaService {
     return JobStatus.SUCCESS;
   }
 
+  @OnJob({ name: JobName.QUEUE_MIGRATION, queue: QueueName.MIGRATION })
   async handleQueueMigration(): Promise<JobStatus> {
     const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
       this.assetRepository.getAll(pagination),
@@ -159,164 +127,153 @@ export class MediaService {
     return JobStatus.SUCCESS;
   }
 
-  async handleAssetMigration({ id }: IEntityJob): Promise<JobStatus> {
-    const { image } = await this.configCore.getConfig({ withCache: true });
+  @OnJob({ name: JobName.MIGRATE_ASSET, queue: QueueName.MIGRATION })
+  async handleAssetMigration({ id }: JobOf<JobName.MIGRATE_ASSET>): Promise<JobStatus> {
+    const { image } = await this.getConfig({ withCache: true });
     const [asset] = await this.assetRepository.getByIds([id], { files: true });
     if (!asset) {
       return JobStatus.FAILED;
     }
 
-    await this.storageCore.moveAssetImage(asset, AssetPathType.PREVIEW, image.previewFormat);
-    await this.storageCore.moveAssetImage(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat);
+    await this.storageCore.moveAssetImage(asset, AssetPathType.PREVIEW, image.preview.format);
+    await this.storageCore.moveAssetImage(asset, AssetPathType.THUMBNAIL, image.thumbnail.format);
     await this.storageCore.moveAssetVideo(asset);
 
     return JobStatus.SUCCESS;
   }
 
-  async handleGeneratePreview({ id }: IEntityJob): Promise<JobStatus> {
-    const [{ image }, [asset]] = await Promise.all([
-      this.configCore.getConfig({ withCache: true }),
-      this.assetRepository.getByIds([id], { exifInfo: true, files: true }),
-    ]);
+  @OnJob({ name: JobName.GENERATE_THUMBNAILS, queue: QueueName.THUMBNAIL_GENERATION })
+  async handleGenerateThumbnails({ id }: JobOf<JobName.GENERATE_THUMBNAILS>): Promise<JobStatus> {
+    const asset = await this.assetRepository.getById(id, { exifInfo: true, files: true });
     if (!asset) {
+      this.logger.warn(`Thumbnail generation failed for asset ${id}: not found`);
       return JobStatus.FAILED;
     }
 
     if (!asset.isVisible) {
+      this.logger.verbose(`Thumbnail generation skipped for asset ${id}: not visible`);
       return JobStatus.SKIPPED;
     }
 
-    const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, image.previewFormat);
-    if (!previewPath) {
+    let generated: { previewPath: string; thumbnailPath: string; thumbhash: Buffer };
+    if (asset.type === AssetType.VIDEO || asset.originalFileName.toLowerCase().endsWith('.gif')) {
+      generated = await this.generateVideoThumbnails(asset);
+    } else if (asset.type === AssetType.IMAGE) {
+      generated = await this.generateImageThumbnails(asset);
+    } else {
+      this.logger.warn(`Skipping thumbnail generation for asset ${id}: ${asset.type} is not an image or video`);
       return JobStatus.SKIPPED;
     }
 
-    const { previewFile } = getAssetFiles(asset.files);
-    if (previewFile && previewFile.path !== previewPath) {
+    const { previewFile, thumbnailFile } = getAssetFiles(asset.files);
+    const toUpsert: UpsertFileOptions[] = [];
+    if (previewFile?.path !== generated.previewPath) {
+      toUpsert.push({ assetId: asset.id, path: generated.previewPath, type: AssetFileType.PREVIEW });
+    }
+
+    if (thumbnailFile?.path !== generated.thumbnailPath) {
+      toUpsert.push({ assetId: asset.id, path: generated.thumbnailPath, type: AssetFileType.THUMBNAIL });
+    }
+
+    if (toUpsert.length > 0) {
+      await this.assetRepository.upsertFiles(toUpsert);
+    }
+
+    const pathsToDelete = [];
+    if (previewFile && previewFile.path !== generated.previewPath) {
       this.logger.debug(`Deleting old preview for asset ${asset.id}`);
-      await this.storageRepository.unlink(previewFile.path);
+      pathsToDelete.push(previewFile.path);
     }
 
-    await this.assetRepository.upsertFile({ assetId: asset.id, type: AssetFileType.PREVIEW, path: previewPath });
-    await this.assetRepository.update({ id: asset.id, updatedAt: new Date() });
-    await this.assetRepository.upsertJobStatus({ assetId: asset.id, previewAt: new Date() });
+    if (thumbnailFile && thumbnailFile.path !== generated.thumbnailPath) {
+      this.logger.debug(`Deleting old thumbnail for asset ${asset.id}`);
+      pathsToDelete.push(thumbnailFile.path);
+    }
+
+    if (pathsToDelete.length > 0) {
+      await Promise.all(pathsToDelete.map((path) => this.storageRepository.unlink(path)));
+    }
+
+    if (asset.thumbhash != generated.thumbhash) {
+      await this.assetRepository.update({ id: asset.id, thumbhash: generated.thumbhash });
+    }
+
+    await this.assetRepository.upsertJobStatus({ assetId: asset.id, previewAt: new Date(), thumbnailAt: new Date() });
 
     return JobStatus.SUCCESS;
   }
 
-  private async generateThumbnail(asset: AssetEntity, type: GeneratedImageType, format: ImageFormat) {
-    const { image, ffmpeg } = await this.configCore.getConfig({ withCache: true });
-    const size = type === AssetPathType.PREVIEW ? image.previewSize : image.thumbnailSize;
-    const path = StorageCore.getImagePath(asset, type, format);
-    this.storageCore.ensureFolders(path);
+  private async generateImageThumbnails(asset: AssetEntity) {
+    const { image } = await this.getConfig({ withCache: true });
+    const previewPath = StorageCore.getImagePath(asset, AssetPathType.PREVIEW, image.preview.format);
+    const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format);
+    this.storageCore.ensureFolders(previewPath);
 
-    switch (asset.type) {
-      case AssetType.IMAGE: {
-        const shouldExtract = image.extractEmbedded && mimeTypes.isRaw(asset.originalPath);
-        const extractedPath = StorageCore.getTempPathInDir(dirname(path));
-        const didExtract = shouldExtract && (await this.mediaRepository.extract(asset.originalPath, extractedPath));
+    const shouldExtract = image.extractEmbedded && mimeTypes.isRaw(asset.originalPath);
+    const extractedPath = StorageCore.getTempPathInDir(dirname(previewPath));
+    const didExtract = shouldExtract && (await this.mediaRepository.extract(asset.originalPath, extractedPath));
 
-        try {
-          const useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.previewSize));
-          const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace;
-          const imageOptions = {
-            format,
-            size,
-            colorspace,
-            quality: image.quality,
-            processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true',
-          };
+    try {
+      const useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.preview.size));
+      const inputPath = useExtracted ? extractedPath : asset.originalPath;
+      const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace;
+      const processInvalidImages = process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true';
 
-          const outputPath = useExtracted ? extractedPath : asset.originalPath;
-          await this.mediaRepository.generateThumbnail(outputPath, path, imageOptions);
-        } finally {
-          if (didExtract) {
-            await this.storageRepository.unlink(extractedPath);
-          }
-        }
-        break;
-      }
+      const orientation = useExtracted && asset.exifInfo?.orientation ? Number(asset.exifInfo.orientation) : undefined;
+      const decodeOptions = { colorspace, processInvalidImages, size: image.preview.size, orientation };
+      const { data, info } = await this.mediaRepository.decodeImage(inputPath, decodeOptions);
 
-      case AssetType.VIDEO: {
-        const { audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath);
-        const mainVideoStream = this.getMainStream(videoStreams);
-        if (!mainVideoStream) {
-          this.logger.warn(`Skipped thumbnail generation for asset ${asset.id}: no video streams found`);
-          return;
-        }
-        const mainAudioStream = this.getMainStream(audioStreams);
-        const config = ThumbnailConfig.create({ ...ffmpeg, targetResolution: size.toString() });
-        const options = config.getCommand(TranscodeTarget.VIDEO, mainVideoStream, mainAudioStream);
-        await this.mediaRepository.transcode(asset.originalPath, path, options);
-        break;
-      }
+      const options = { colorspace, processInvalidImages, raw: info };
+      const outputs = await Promise.all([
+        this.mediaRepository.generateThumbnail(data, { ...image.thumbnail, ...options }, thumbnailPath),
+        this.mediaRepository.generateThumbnail(data, { ...image.preview, ...options }, previewPath),
+        this.mediaRepository.generateThumbhash(data, options),
+      ]);
 
-      default: {
-        throw new UnsupportedMediaTypeException(`Unsupported asset type for thumbnail generation: ${asset.type}`);
+      return { previewPath, thumbnailPath, thumbhash: outputs[2] };
+    } finally {
+      if (didExtract) {
+        await this.storageRepository.unlink(extractedPath);
       }
     }
+  }
 
-    const assetLabel = asset.isExternal ? asset.originalPath : asset.id;
-    this.logger.log(
-      `Successfully generated ${format.toUpperCase()} ${asset.type.toLowerCase()} ${type} for asset ${assetLabel}`,
+  private async generateVideoThumbnails(asset: AssetEntity) {
+    const { image, ffmpeg } = await this.getConfig({ withCache: true });
+    const previewPath = StorageCore.getImagePath(asset, AssetPathType.PREVIEW, image.preview.format);
+    const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format);
+    this.storageCore.ensureFolders(previewPath);
+
+    const { format, audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath);
+    const mainVideoStream = this.getMainStream(videoStreams);
+    if (!mainVideoStream) {
+      throw new Error(`No video streams found for asset ${asset.id}`);
+    }
+    const mainAudioStream = this.getMainStream(audioStreams);
+
+    const previewConfig = ThumbnailConfig.create({ ...ffmpeg, targetResolution: image.preview.size.toString() });
+    const thumbnailConfig = ThumbnailConfig.create({ ...ffmpeg, targetResolution: image.thumbnail.size.toString() });
+    const previewOptions = previewConfig.getCommand(TranscodeTarget.VIDEO, mainVideoStream, mainAudioStream, format);
+    const thumbnailOptions = thumbnailConfig.getCommand(
+      TranscodeTarget.VIDEO,
+      mainVideoStream,
+      mainAudioStream,
+      format,
     );
 
-    return path;
+    await this.mediaRepository.transcode(asset.originalPath, previewPath, previewOptions);
+    await this.mediaRepository.transcode(asset.originalPath, thumbnailPath, thumbnailOptions);
+
+    const thumbhash = await this.mediaRepository.generateThumbhash(previewPath, {
+      colorspace: image.colorspace,
+      processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true',
+    });
+
+    return { previewPath, thumbnailPath, thumbhash };
   }
 
-  async handleGenerateThumbnail({ id }: IEntityJob): Promise<JobStatus> {
-    const [{ image }, [asset]] = await Promise.all([
-      this.configCore.getConfig({ withCache: true }),
-      this.assetRepository.getByIds([id], { exifInfo: true, files: true }),
-    ]);
-    if (!asset) {
-      return JobStatus.FAILED;
-    }
-
-    if (!asset.isVisible) {
-      return JobStatus.SKIPPED;
-    }
-
-    const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat);
-    if (!thumbnailPath) {
-      return JobStatus.SKIPPED;
-    }
-
-    const { thumbnailFile } = getAssetFiles(asset.files);
-    if (thumbnailFile && thumbnailFile.path !== thumbnailPath) {
-      this.logger.debug(`Deleting old thumbnail for asset ${asset.id}`);
-      await this.storageRepository.unlink(thumbnailFile.path);
-    }
-
-    await this.assetRepository.upsertFile({ assetId: asset.id, type: AssetFileType.THUMBNAIL, path: thumbnailPath });
-    await this.assetRepository.update({ id: asset.id, updatedAt: new Date() });
-    await this.assetRepository.upsertJobStatus({ assetId: asset.id, thumbnailAt: new Date() });
-
-    return JobStatus.SUCCESS;
-  }
-
-  async handleGenerateThumbhash({ id }: IEntityJob): Promise<JobStatus> {
-    const [asset] = await this.assetRepository.getByIds([id], { files: true });
-    if (!asset) {
-      return JobStatus.FAILED;
-    }
-
-    if (!asset.isVisible) {
-      return JobStatus.SKIPPED;
-    }
-
-    const { previewFile } = getAssetFiles(asset.files);
-    if (!previewFile) {
-      return JobStatus.FAILED;
-    }
-
-    const thumbhash = await this.mediaRepository.generateThumbhash(previewFile.path);
-    await this.assetRepository.update({ id: asset.id, thumbhash });
-
-    return JobStatus.SUCCESS;
-  }
-
-  async handleQueueVideoConversion(job: IBaseJob): Promise<JobStatus> {
+  @OnJob({ name: JobName.QUEUE_VIDEO_CONVERSION, queue: QueueName.VIDEO_CONVERSION })
+  async handleQueueVideoConversion(job: JobOf<JobName.QUEUE_VIDEO_CONVERSION>): Promise<JobStatus> {
     const { force } = job;
 
     const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
@@ -334,7 +291,8 @@ export class MediaService {
     return JobStatus.SUCCESS;
   }
 
-  async handleVideoConversion({ id }: IEntityJob): Promise<JobStatus> {
+  @OnJob({ name: JobName.VIDEO_CONVERSION, queue: QueueName.VIDEO_CONVERSION })
+  async handleVideoConversion({ id }: JobOf<JobName.VIDEO_CONVERSION>): Promise<JobStatus> {
     const [asset] = await this.assetRepository.getByIds([id]);
     if (!asset || asset.type !== AssetType.VIDEO) {
       return JobStatus.FAILED;
@@ -344,52 +302,70 @@ export class MediaService {
     const output = StorageCore.getEncodedVideoPath(asset);
     this.storageCore.ensureFolders(output);
 
-    const { videoStreams, audioStreams, format } = await this.mediaRepository.probe(input);
-    const mainVideoStream = this.getMainStream(videoStreams);
-    const mainAudioStream = this.getMainStream(audioStreams);
-    if (!mainVideoStream || !format.formatName) {
+    const { videoStreams, audioStreams, format } = await this.mediaRepository.probe(input, {
+      countFrames: this.logger.isLevelEnabled(LogLevel.DEBUG), // makes frame count more reliable for progress logs
+    });
+    const videoStream = this.getMainStream(videoStreams);
+    const audioStream = this.getMainStream(audioStreams);
+    if (!videoStream || !format.formatName) {
       return JobStatus.FAILED;
     }
 
-    if (!mainVideoStream.height || !mainVideoStream.width) {
+    if (!videoStream.height || !videoStream.width) {
       this.logger.warn(`Skipped transcoding for asset ${asset.id}: no video streams found`);
       return JobStatus.FAILED;
     }
 
-    const { ffmpeg } = await this.configCore.getConfig({ withCache: true });
-    const target = this.getTranscodeTarget(ffmpeg, mainVideoStream, mainAudioStream);
+    let { ffmpeg } = await this.getConfig({ withCache: true });
+    const target = this.getTranscodeTarget(ffmpeg, videoStream, audioStream);
     if (target === TranscodeTarget.NONE && !this.isRemuxRequired(ffmpeg, format)) {
       if (asset.encodedVideoPath) {
         this.logger.log(`Transcoded video exists for asset ${asset.id}, but is no longer required. Deleting...`);
         await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [asset.encodedVideoPath] } });
         await this.assetRepository.update({ id: asset.id, encodedVideoPath: null });
+      } else {
+        this.logger.verbose(`Asset ${asset.id} does not require transcoding based on current policy, skipping`);
       }
 
       return JobStatus.SKIPPED;
     }
 
-    let command;
-    try {
-      const config = BaseConfig.create(ffmpeg, await this.getDevices(), await this.hasMaliOpenCL());
-      command = config.getCommand(target, mainVideoStream, mainAudioStream);
-    } catch (error) {
-      this.logger.error(`An error occurred while configuring transcoding options: ${error}`);
-      return JobStatus.FAILED;
+    const command = BaseConfig.create(ffmpeg, this.videoInterfaces).getCommand(target, videoStream, audioStream);
+    if (ffmpeg.accel === TranscodeHWAccel.DISABLED) {
+      this.logger.log(`Transcoding video ${asset.id} without hardware acceleration`);
+    } else {
+      this.logger.log(
+        `Transcoding video ${asset.id} with ${ffmpeg.accel.toUpperCase()}-accelerated encoding and${ffmpeg.accelDecode ? '' : ' software'} decoding`,
+      );
     }
 
-    this.logger.log(`Started encoding video ${asset.id} ${JSON.stringify(command)}`);
     try {
       await this.mediaRepository.transcode(input, output, command);
-    } catch (error) {
-      this.logger.error(error);
-      if (ffmpeg.accel !== TranscodeHWAccel.DISABLED) {
-        this.logger.error(
-          `Error occurred during transcoding. Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled.`,
-        );
+    } catch (error: any) {
+      this.logger.error(`Error occurred during transcoding: ${error.message}`);
+      if (ffmpeg.accel === TranscodeHWAccel.DISABLED) {
+        return JobStatus.FAILED;
+      }
+
+      let partialFallbackSuccess = false;
+      if (ffmpeg.accelDecode) {
+        try {
+          this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()}-accelerated encoding and software decoding`);
+          ffmpeg = { ...ffmpeg, accelDecode: false };
+          const command = BaseConfig.create(ffmpeg, this.videoInterfaces).getCommand(target, videoStream, audioStream);
+          await this.mediaRepository.transcode(input, output, command);
+          partialFallbackSuccess = true;
+        } catch (error: any) {
+          this.logger.error(`Error occurred during transcoding: ${error.message}`);
+        }
+      }
+
+      if (!partialFallbackSuccess) {
+        this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled`);
+        ffmpeg = { ...ffmpeg, accel: TranscodeHWAccel.DISABLED };
+        const command = BaseConfig.create(ffmpeg, this.videoInterfaces).getCommand(target, videoStream, audioStream);
+        await this.mediaRepository.transcode(input, output, command);
       }
-      const config = BaseConfig.create({ ...ffmpeg, accel: TranscodeHWAccel.DISABLED });
-      command = config.getCommand(target, mainVideoStream, mainAudioStream);
-      await this.mediaRepository.transcode(input, output, command);
     }
 
     this.logger.log(`Successfully encoded ${asset.id}`);
@@ -400,18 +376,16 @@ export class MediaService {
   }
 
   private getMainStream<T extends VideoStreamInfo | AudioStreamInfo>(streams: T[]): T {
-    return streams.sort((stream1, stream2) => stream2.frameCount - stream1.frameCount)[0];
+    return streams
+      .filter((stream) => stream.codecName !== 'unknown')
+      .sort((stream1, stream2) => stream2.frameCount - stream1.frameCount)[0];
   }
 
   private getTranscodeTarget(
     config: SystemConfigFFmpegDto,
-    videoStream?: VideoStreamInfo,
+    videoStream: VideoStreamInfo,
     audioStream?: AudioStreamInfo,
   ): TranscodeTarget {
-    if (!videoStream && !audioStream) {
-      return TranscodeTarget.NONE;
-    }
-
     const isAudioTranscodeRequired = this.isAudioTranscodeRequired(config, audioStream);
     const isVideoTranscodeRequired = this.isVideoTranscodeRequired(config, videoStream);
 
@@ -453,18 +427,14 @@ export class MediaService {
     }
   }
 
-  private isVideoTranscodeRequired(ffmpegConfig: SystemConfigFFmpegDto, stream?: VideoStreamInfo): boolean {
-    if (!stream) {
-      return false;
-    }
-
+  private isVideoTranscodeRequired(ffmpegConfig: SystemConfigFFmpegDto, stream: VideoStreamInfo): boolean {
     const scalingEnabled = ffmpegConfig.targetResolution !== 'original';
     const targetRes = Number.parseInt(ffmpegConfig.targetResolution);
     const isLargerThanTargetRes = scalingEnabled && Math.min(stream.height, stream.width) > targetRes;
     const isLargerThanTargetBitrate = stream.bitrate > this.parseBitrateToBps(ffmpegConfig.maxBitrate);
 
     const isTargetVideoCodec = ffmpegConfig.acceptedVideoCodecs.includes(stream.codecName as VideoCodec);
-    const isRequired = !isTargetVideoCodec || stream.isHDR;
+    const isRequired = !isTargetVideoCodec || !stream.pixelFormat.endsWith('420p');
 
     switch (ffmpegConfig.transcode) {
       case TranscodePolicy.DISABLED: {
@@ -534,30 +504,24 @@ export class MediaService {
   }
 
   private async getDevices() {
-    if (!this.devices) {
-      try {
-        this.devices = await this.storageRepository.readdir('/dev/dri');
-      } catch {
-        this.logger.debug('No devices found in /dev/dri.');
-        this.devices = [];
-      }
+    try {
+      return await this.storageRepository.readdir('/dev/dri');
+    } catch {
+      this.logger.debug('No devices found in /dev/dri.');
+      return [];
     }
-
-    return this.devices;
   }
 
   private async hasMaliOpenCL() {
-    if (this.maliOpenCL === undefined) {
-      try {
-        const maliIcdStat = await this.storageRepository.stat('/etc/OpenCL/vendors/mali.icd');
-        const maliDeviceStat = await this.storageRepository.stat('/dev/mali0');
-        this.maliOpenCL = maliIcdStat.isFile() && maliDeviceStat.isCharacterDevice();
-      } catch {
-        this.logger.debug('OpenCL not available for transcoding, using CPU decoding instead.');
-        this.maliOpenCL = false;
-      }
+    try {
+      const [maliIcdStat, maliDeviceStat] = await Promise.all([
+        this.storageRepository.stat('/etc/OpenCL/vendors/mali.icd'),
+        this.storageRepository.stat('/dev/mali0'),
+      ]);
+      return maliIcdStat.isFile() && maliDeviceStat.isCharacterDevice();
+    } catch {
+      this.logger.debug('OpenCL not available for transcoding, so RKMPP acceleration will use CPU tonemapping');
+      return false;
     }
-
-    return this.maliOpenCL;
   }
 }
diff --git a/server/src/services/memory.service.spec.ts b/server/src/services/memory.service.spec.ts
index ba184daa80..b5dd4c2553 100644
--- a/server/src/services/memory.service.spec.ts
+++ b/server/src/services/memory.service.spec.ts
@@ -5,20 +5,18 @@ import { MemoryService } from 'src/services/memory.service';
 import { authStub } from 'test/fixtures/auth.stub';
 import { memoryStub } from 'test/fixtures/memory.stub';
 import { userStub } from 'test/fixtures/user.stub';
-import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
-import { newMemoryRepositoryMock } from 'test/repositories/memory.repository.mock';
+import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(MemoryService.name, () => {
-  let accessMock: IAccessRepositoryMock;
-  let memoryMock: Mocked<IMemoryRepository>;
   let sut: MemoryService;
 
-  beforeEach(() => {
-    accessMock = newAccessRepositoryMock();
-    memoryMock = newMemoryRepositoryMock();
+  let accessMock: IAccessRepositoryMock;
+  let memoryMock: Mocked<IMemoryRepository>;
 
-    sut = new MemoryService(accessMock, memoryMock);
+  beforeEach(() => {
+    ({ sut, accessMock, memoryMock } = newTestService(MemoryService));
   });
 
   it('should be defined', () => {
diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts
index fb1ff49f0b..816b0fddeb 100644
--- a/server/src/services/memory.service.ts
+++ b/server/src/services/memory.service.ts
@@ -1,28 +1,21 @@
-import { BadRequestException, Inject, Injectable } from '@nestjs/common';
+import { BadRequestException, Injectable } from '@nestjs/common';
 import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
 import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { Permission } from 'src/enum';
-import { IAccessRepository } from 'src/interfaces/access.interface';
-import { IMemoryRepository } from 'src/interfaces/memory.interface';
-import { checkAccess, requireAccess } from 'src/utils/access';
+import { BaseService } from 'src/services/base.service';
 import { addAssets, removeAssets } from 'src/utils/asset.util';
 
 @Injectable()
-export class MemoryService {
-  constructor(
-    @Inject(IAccessRepository) private access: IAccessRepository,
-    @Inject(IMemoryRepository) private repository: IMemoryRepository,
-  ) {}
-
+export class MemoryService extends BaseService {
   async search(auth: AuthDto) {
-    const memories = await this.repository.search(auth.user.id);
+    const memories = await this.memoryRepository.search(auth.user.id);
     return memories.map((memory) => mapMemory(memory));
   }
 
   async get(auth: AuthDto, id: string): Promise<MemoryResponseDto> {
-    await requireAccess(this.access, { auth, permission: Permission.MEMORY_READ, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.MEMORY_READ, ids: [id] });
     const memory = await this.findOrFail(id);
     return mapMemory(memory);
   }
@@ -31,12 +24,12 @@ export class MemoryService {
     // TODO validate type/data combination
 
     const assetIds = dto.assetIds || [];
-    const allowedAssetIds = await checkAccess(this.access, {
+    const allowedAssetIds = await this.checkAccess({
       auth,
       permission: Permission.ASSET_SHARE,
       ids: assetIds,
     });
-    const memory = await this.repository.create({
+    const memory = await this.memoryRepository.create({
       ownerId: auth.user.id,
       type: dto.type,
       data: dto.data,
@@ -50,9 +43,9 @@ export class MemoryService {
   }
 
   async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise<MemoryResponseDto> {
-    await requireAccess(this.access, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.MEMORY_UPDATE, ids: [id] });
 
-    const memory = await this.repository.update({
+    const memory = await this.memoryRepository.update({
       id,
       isSaved: dto.isSaved,
       memoryAt: dto.memoryAt,
@@ -63,28 +56,28 @@ export class MemoryService {
   }
 
   async remove(auth: AuthDto, id: string): Promise<void> {
-    await requireAccess(this.access, { auth, permission: Permission.MEMORY_DELETE, ids: [id] });
-    await this.repository.delete(id);
+    await this.requireAccess({ auth, permission: Permission.MEMORY_DELETE, ids: [id] });
+    await this.memoryRepository.delete(id);
   }
 
   async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
-    await requireAccess(this.access, { auth, permission: Permission.MEMORY_READ, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.MEMORY_READ, ids: [id] });
 
-    const repos = { access: this.access, bulk: this.repository };
+    const repos = { access: this.accessRepository, bulk: this.memoryRepository };
     const results = await addAssets(auth, repos, { parentId: id, assetIds: dto.ids });
 
     const hasSuccess = results.find(({ success }) => success);
     if (hasSuccess) {
-      await this.repository.update({ id, updatedAt: new Date() });
+      await this.memoryRepository.update({ id, updatedAt: new Date() });
     }
 
     return results;
   }
 
   async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
-    await requireAccess(this.access, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.MEMORY_UPDATE, ids: [id] });
 
-    const repos = { access: this.access, bulk: this.repository };
+    const repos = { access: this.accessRepository, bulk: this.memoryRepository };
     const results = await removeAssets(auth, repos, {
       parentId: id,
       assetIds: dto.ids,
@@ -93,14 +86,14 @@ export class MemoryService {
 
     const hasSuccess = results.find(({ success }) => success);
     if (hasSuccess) {
-      await this.repository.update({ id, updatedAt: new Date() });
+      await this.memoryRepository.update({ id, updatedAt: new Date() });
     }
 
     return results;
   }
 
   private async findOrFail(id: string) {
-    const memory = await this.repository.get(id);
+    const memory = await this.memoryRepository.get(id);
     if (!memory) {
       throw new BadRequestException('Memory not found');
     }
diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts
index 19aaa2ea1a..390f18b777 100644
--- a/server/src/services/metadata.service.spec.ts
+++ b/server/src/services/metadata.service.spec.ts
@@ -3,103 +3,79 @@ import { randomBytes } from 'node:crypto';
 import { Stats } from 'node:fs';
 import { constants } from 'node:fs/promises';
 import { ExifEntity } from 'src/entities/exif.entity';
-import { AssetType, SourceType } from 'src/enum';
+import { AssetType, ExifOrientation, ImmichWorker, SourceType } from 'src/enum';
 import { IAlbumRepository } from 'src/interfaces/album.interface';
 import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
+import { IConfigRepository } from 'src/interfaces/config.interface';
 import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import { IDatabaseRepository } from 'src/interfaces/database.interface';
 import { IEventRepository } from 'src/interfaces/event.interface';
 import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { IMapRepository } from 'src/interfaces/map.interface';
 import { IMediaRepository } from 'src/interfaces/media.interface';
 import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
-import { IMoveRepository } from 'src/interfaces/move.interface';
 import { IPersonRepository } from 'src/interfaces/person.interface';
 import { IStorageRepository } from 'src/interfaces/storage.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { ITagRepository } from 'src/interfaces/tag.interface';
 import { IUserRepository } from 'src/interfaces/user.interface';
-import { MetadataService, Orientation } from 'src/services/metadata.service';
+import { MetadataService } from 'src/services/metadata.service';
 import { assetStub } from 'test/fixtures/asset.stub';
 import { fileStub } from 'test/fixtures/file.stub';
 import { probeStub } from 'test/fixtures/media.stub';
 import { metadataStub } from 'test/fixtures/metadata.stub';
 import { personStub } from 'test/fixtures/person.stub';
 import { tagStub } from 'test/fixtures/tag.stub';
-import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
-import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
-import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
-import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
-import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
-import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newMapRepositoryMock } from 'test/repositories/map.repository.mock';
-import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock';
-import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock';
-import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock';
-import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
-import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
-import { newTagRepositoryMock } from 'test/repositories/tag.repository.mock';
-import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(MetadataService.name, () => {
-  let albumMock: Mocked<IAlbumRepository>;
-  let assetMock: Mocked<IAssetRepository>;
-  let systemMock: Mocked<ISystemMetadataRepository>;
-  let cryptoRepository: Mocked<ICryptoRepository>;
-  let jobMock: Mocked<IJobRepository>;
-  let mapMock: Mocked<IMapRepository>;
-  let metadataMock: Mocked<IMetadataRepository>;
-  let moveMock: Mocked<IMoveRepository>;
-  let mediaMock: Mocked<IMediaRepository>;
-  let personMock: Mocked<IPersonRepository>;
-  let storageMock: Mocked<IStorageRepository>;
-  let eventMock: Mocked<IEventRepository>;
-  let databaseMock: Mocked<IDatabaseRepository>;
-  let userMock: Mocked<IUserRepository>;
-  let loggerMock: Mocked<ILoggerRepository>;
-  let tagMock: Mocked<ITagRepository>;
   let sut: MetadataService;
 
-  beforeEach(() => {
-    albumMock = newAlbumRepositoryMock();
-    assetMock = newAssetRepositoryMock();
-    cryptoRepository = newCryptoRepositoryMock();
-    jobMock = newJobRepositoryMock();
-    mapMock = newMapRepositoryMock();
-    metadataMock = newMetadataRepositoryMock();
-    moveMock = newMoveRepositoryMock();
-    personMock = newPersonRepositoryMock();
-    eventMock = newEventRepositoryMock();
-    storageMock = newStorageRepositoryMock();
-    systemMock = newSystemMetadataRepositoryMock();
-    mediaMock = newMediaRepositoryMock();
-    databaseMock = newDatabaseRepositoryMock();
-    userMock = newUserRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-    tagMock = newTagRepositoryMock();
+  let albumMock: Mocked<IAlbumRepository>;
+  let assetMock: Mocked<IAssetRepository>;
+  let configMock: Mocked<IConfigRepository>;
+  let cryptoMock: Mocked<ICryptoRepository>;
+  let eventMock: Mocked<IEventRepository>;
+  let jobMock: Mocked<IJobRepository>;
+  let mapMock: Mocked<IMapRepository>;
+  let mediaMock: Mocked<IMediaRepository>;
+  let metadataMock: Mocked<IMetadataRepository>;
+  let personMock: Mocked<IPersonRepository>;
+  let storageMock: Mocked<IStorageRepository>;
+  let systemMock: Mocked<ISystemMetadataRepository>;
+  let tagMock: Mocked<ITagRepository>;
+  let userMock: Mocked<IUserRepository>;
 
-    sut = new MetadataService(
+  const mockReadTags = (exifData?: Partial<ImmichTags>, sidecarData?: Partial<ImmichTags>) => {
+    metadataMock.readTags.mockReset();
+    metadataMock.readTags.mockResolvedValueOnce(exifData ?? {});
+    metadataMock.readTags.mockResolvedValueOnce(sidecarData ?? {});
+  };
+
+  beforeEach(() => {
+    ({
+      sut,
       albumMock,
       assetMock,
+      configMock,
+      cryptoMock,
       eventMock,
-      cryptoRepository,
-      databaseMock,
       jobMock,
       mapMock,
       mediaMock,
       metadataMock,
-      moveMock,
       personMock,
       storageMock,
       systemMock,
       tagMock,
       userMock,
-      loggerMock,
-    );
+    } = newTestService(MetadataService));
+
+    mockReadTags();
+
+    configMock.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES);
+
+    delete process.env.TZ;
   });
 
   afterEach(async () => {
@@ -112,22 +88,12 @@ describe(MetadataService.name, () => {
 
   describe('onBootstrapEvent', () => {
     it('should pause and resume queue during init', async () => {
-      await sut.onBootstrap('microservices');
+      await sut.onBootstrap();
 
       expect(jobMock.pause).toHaveBeenCalledTimes(1);
       expect(mapMock.init).toHaveBeenCalledTimes(1);
       expect(jobMock.resume).toHaveBeenCalledTimes(1);
     });
-
-    it('should return if reverse geocoding is disabled', async () => {
-      systemMock.get.mockResolvedValue({ reverseGeocoding: { enabled: false } });
-
-      await sut.onBootstrap('microservices');
-
-      expect(jobMock.pause).not.toHaveBeenCalled();
-      expect(mapMock.init).not.toHaveBeenCalled();
-      expect(jobMock.resume).not.toHaveBeenCalled();
-    });
   });
 
   describe('handleLivePhotoLinking', () => {
@@ -286,7 +252,7 @@ describe(MetadataService.name, () => {
     it('should handle an asset that could not be found', async () => {
       await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
 
-      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
+      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
       expect(assetMock.upsertExif).not.toHaveBeenCalled();
       expect(assetMock.update).not.toHaveBeenCalled();
     });
@@ -295,16 +261,10 @@ describe(MetadataService.name, () => {
       const originalDate = new Date('2023-11-21T16:13:17.517Z');
       const sidecarDate = new Date('2022-01-01T00:00:00.000Z');
       assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
-      metadataMock.readTags.mockImplementation((path) => {
-        const map = {
-          [assetStub.sidecar.originalPath]: originalDate.toISOString(),
-          [assetStub.sidecar.sidecarPath as string]: sidecarDate.toISOString(),
-        };
-        return Promise.resolve({ CreationDate: map[path] ?? new Date().toISOString() });
-      });
+      mockReadTags({ CreationDate: originalDate.toISOString() }, { CreationDate: sidecarDate.toISOString() });
 
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
-      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.sidecar.id]);
+      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.sidecar.id], { faces: { person: false } });
       expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: sidecarDate }));
       expect(assetMock.update).toHaveBeenCalledWith({
         id: assetStub.image.id,
@@ -314,12 +274,31 @@ describe(MetadataService.name, () => {
       });
     });
 
-    it('should handle lists of numbers', async () => {
-      assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      metadataMock.readTags.mockResolvedValue({ ISO: [160] as any });
+    it('should account for the server being in a non-UTC timezone', async () => {
+      process.env.TZ = 'America/Los_Angeles';
+      assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
+      mockReadTags({ DateTimeOriginal: '2022:01:01 00:00:00' });
 
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
-      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
+      expect(assetMock.upsertExif).toHaveBeenCalledWith(
+        expect.objectContaining({
+          dateTimeOriginal: new Date('2022-01-01T08:00:00.000Z'),
+        }),
+      );
+
+      expect(assetMock.update).toHaveBeenCalledWith(
+        expect.objectContaining({
+          localDateTime: new Date('2022-01-01T00:00:00.000Z'),
+        }),
+      );
+    });
+
+    it('should handle lists of numbers', async () => {
+      assetMock.getByIds.mockResolvedValue([assetStub.image]);
+      mockReadTags({ ISO: [160] });
+
+      await sut.handleMetadataExtraction({ id: assetStub.image.id });
+      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
       expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ iso: 160 }));
       expect(assetMock.update).toHaveBeenCalledWith({
         id: assetStub.image.id,
@@ -333,13 +312,13 @@ describe(MetadataService.name, () => {
       assetMock.getByIds.mockResolvedValue([assetStub.withLocation]);
       systemMock.get.mockResolvedValue({ reverseGeocoding: { enabled: true } });
       mapMock.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
-      metadataMock.readTags.mockResolvedValue({
+      mockReadTags({
         GPSLatitude: assetStub.withLocation.exifInfo!.latitude!,
         GPSLongitude: assetStub.withLocation.exifInfo!.longitude!,
       });
 
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
-      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
+      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
       expect(assetMock.upsertExif).toHaveBeenCalledWith(
         expect.objectContaining({ city: 'City', state: 'State', country: 'Country' }),
       );
@@ -353,19 +332,19 @@ describe(MetadataService.name, () => {
 
     it('should discard latitude and longitude on null island', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.withLocation]);
-      metadataMock.readTags.mockResolvedValue({
+      mockReadTags({
         GPSLatitude: 0,
         GPSLongitude: 0,
       });
 
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
-      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
+      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
       expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ latitude: null, longitude: null }));
     });
 
     it('should extract tags from TagsList', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      metadataMock.readTags.mockResolvedValue({ TagsList: ['Parent'] });
+      mockReadTags({ TagsList: ['Parent'] });
       tagMock.upsertValue.mockResolvedValue(tagStub.parent);
 
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
@@ -375,7 +354,7 @@ describe(MetadataService.name, () => {
 
     it('should extract hierarchy from TagsList', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      metadataMock.readTags.mockResolvedValue({ TagsList: ['Parent/Child'] });
+      mockReadTags({ TagsList: ['Parent/Child'] });
       tagMock.upsertValue.mockResolvedValueOnce(tagStub.parent);
       tagMock.upsertValue.mockResolvedValueOnce(tagStub.child);
 
@@ -391,7 +370,7 @@ describe(MetadataService.name, () => {
 
     it('should extract tags from Keywords as a string', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      metadataMock.readTags.mockResolvedValue({ Keywords: 'Parent' });
+      mockReadTags({ Keywords: 'Parent' });
       tagMock.upsertValue.mockResolvedValue(tagStub.parent);
 
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
@@ -401,7 +380,7 @@ describe(MetadataService.name, () => {
 
     it('should extract tags from Keywords as a list', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      metadataMock.readTags.mockResolvedValue({ Keywords: ['Parent'] });
+      mockReadTags({ Keywords: ['Parent'] });
       tagMock.upsertValue.mockResolvedValue(tagStub.parent);
 
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
@@ -411,7 +390,7 @@ describe(MetadataService.name, () => {
 
     it('should extract tags from Keywords as a list with a number', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      metadataMock.readTags.mockResolvedValue({ Keywords: ['Parent', 2024] as any[] });
+      mockReadTags({ Keywords: ['Parent', 2024] });
       tagMock.upsertValue.mockResolvedValue(tagStub.parent);
 
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
@@ -422,7 +401,7 @@ describe(MetadataService.name, () => {
 
     it('should extract hierarchal tags from Keywords', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      metadataMock.readTags.mockResolvedValue({ Keywords: 'Parent/Child' });
+      mockReadTags({ Keywords: 'Parent/Child' });
       tagMock.upsertValue.mockResolvedValue(tagStub.parent);
 
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
@@ -437,7 +416,7 @@ describe(MetadataService.name, () => {
 
     it('should ignore Keywords when TagsList is present', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      metadataMock.readTags.mockResolvedValue({ Keywords: 'Child', TagsList: ['Parent/Child'] });
+      mockReadTags({ Keywords: 'Child', TagsList: ['Parent/Child'] });
       tagMock.upsertValue.mockResolvedValue(tagStub.parent);
 
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
@@ -452,7 +431,7 @@ describe(MetadataService.name, () => {
 
     it('should extract hierarchy from HierarchicalSubject', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Parent|Child', 'TagA'] });
+      mockReadTags({ HierarchicalSubject: ['Parent|Child', 'TagA'] });
       tagMock.upsertValue.mockResolvedValueOnce(tagStub.parent);
       tagMock.upsertValue.mockResolvedValueOnce(tagStub.child);
 
@@ -467,9 +446,20 @@ describe(MetadataService.name, () => {
       expect(tagMock.upsertValue).toHaveBeenNthCalledWith(3, { userId: 'user-id', value: 'TagA', parent: undefined });
     });
 
+    it('should extract tags from HierarchicalSubject as a list with a number', async () => {
+      assetMock.getByIds.mockResolvedValue([assetStub.image]);
+      mockReadTags({ HierarchicalSubject: ['Parent', 2024] });
+      tagMock.upsertValue.mockResolvedValue(tagStub.parent);
+
+      await sut.handleMetadataExtraction({ id: assetStub.image.id });
+
+      expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined });
+      expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: '2024', parent: undefined });
+    });
+
     it('should extract ignore / characters in a HierarchicalSubject tag', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Mom/Dad'] });
+      mockReadTags({ HierarchicalSubject: ['Mom/Dad'] });
       tagMock.upsertValue.mockResolvedValueOnce(tagStub.parent);
 
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
@@ -483,7 +473,7 @@ describe(MetadataService.name, () => {
 
     it('should ignore HierarchicalSubject when TagsList is present', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Parent2|Child2'], TagsList: ['Parent/Child'] });
+      mockReadTags({ HierarchicalSubject: ['Parent2|Child2'], TagsList: ['Parent/Child'] });
       tagMock.upsertValue.mockResolvedValue(tagStub.parent);
 
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
@@ -498,7 +488,7 @@ describe(MetadataService.name, () => {
 
     it('should remove existing tags', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      metadataMock.readTags.mockResolvedValue({});
+      mockReadTags({});
 
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
 
@@ -510,8 +500,10 @@ describe(MetadataService.name, () => {
       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 
       await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id });
-      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id]);
-      expect(storageMock.writeFile).not.toHaveBeenCalled();
+      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], {
+        faces: { person: false },
+      });
+      expect(storageMock.createOrOverwriteFile).not.toHaveBeenCalled();
       expect(jobMock.queue).not.toHaveBeenCalled();
       expect(jobMock.queueAll).not.toHaveBeenCalled();
       expect(assetMock.update).not.toHaveBeenCalledWith(
@@ -521,7 +513,7 @@ describe(MetadataService.name, () => {
 
     it('should handle an invalid Directory Item', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      metadataMock.readTags.mockResolvedValue({
+      mockReadTags({
         MotionPhoto: 1,
         ContainerDirectory: [{ Foo: 100 }],
       });
@@ -532,19 +524,19 @@ describe(MetadataService.name, () => {
     it('should extract the correct video orientation', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.video]);
       mediaMock.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
-      metadataMock.readTags.mockResolvedValue({});
+      mockReadTags({});
 
       await sut.handleMetadataExtraction({ id: assetStub.video.id });
 
-      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]);
+      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id], { faces: { person: false } });
       expect(assetMock.upsertExif).toHaveBeenCalledWith(
-        expect.objectContaining({ orientation: Orientation.Rotate270CW.toString() }),
+        expect.objectContaining({ orientation: ExifOrientation.Rotate270CW.toString() }),
       );
     });
 
     it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => {
       assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]);
-      metadataMock.readTags.mockResolvedValue({
+      mockReadTags({
         Directory: 'foo/bar/',
         MotionPhotoVideo: new BinaryField(0, ''),
         // The below two are included to ensure that the MotionPhotoVideo tag is extracted
@@ -552,10 +544,10 @@ describe(MetadataService.name, () => {
         EmbeddedVideoFile: new BinaryField(0, ''),
         EmbeddedVideoType: 'MotionPhoto_Data',
       });
-      cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
+      cryptoMock.hashSha1.mockReturnValue(randomBytes(512));
       assetMock.getByChecksum.mockResolvedValue(null);
       assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
-      cryptoRepository.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid);
+      cryptoMock.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid);
       const video = randomBytes(512);
       metadataMock.extractBinaryTag.mockResolvedValue(video);
 
@@ -564,7 +556,9 @@ describe(MetadataService.name, () => {
         assetStub.livePhotoWithOriginalFileName.originalPath,
         'MotionPhotoVideo',
       );
-      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id]);
+      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id], {
+        faces: { person: false },
+      });
       expect(assetMock.create).toHaveBeenCalledWith({
         checksum: expect.any(Buffer),
         deviceAssetId: 'NONE',
@@ -581,7 +575,7 @@ describe(MetadataService.name, () => {
         type: AssetType.VIDEO,
       });
       expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512);
-      expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
+      expect(storageMock.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
       expect(assetMock.update).toHaveBeenNthCalledWith(1, {
         id: assetStub.livePhotoWithOriginalFileName.id,
         livePhotoVideoId: fileStub.livePhotoMotion.uuid,
@@ -590,15 +584,15 @@ describe(MetadataService.name, () => {
 
     it('should extract the EmbeddedVideo tag from Samsung JPEG motion photos', async () => {
       assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]);
-      metadataMock.readTags.mockResolvedValue({
+      mockReadTags({
         Directory: 'foo/bar/',
         EmbeddedVideoFile: new BinaryField(0, ''),
         EmbeddedVideoType: 'MotionPhoto_Data',
       });
-      cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
+      cryptoMock.hashSha1.mockReturnValue(randomBytes(512));
       assetMock.getByChecksum.mockResolvedValue(null);
       assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
-      cryptoRepository.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid);
+      cryptoMock.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid);
       const video = randomBytes(512);
       metadataMock.extractBinaryTag.mockResolvedValue(video);
 
@@ -607,7 +601,9 @@ describe(MetadataService.name, () => {
         assetStub.livePhotoWithOriginalFileName.originalPath,
         'EmbeddedVideoFile',
       );
-      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id]);
+      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id], {
+        faces: { person: false },
+      });
       expect(assetMock.create).toHaveBeenCalledWith({
         checksum: expect.any(Buffer),
         deviceAssetId: 'NONE',
@@ -624,7 +620,7 @@ describe(MetadataService.name, () => {
         type: AssetType.VIDEO,
       });
       expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512);
-      expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
+      expect(storageMock.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
       expect(assetMock.update).toHaveBeenNthCalledWith(1, {
         id: assetStub.livePhotoWithOriginalFileName.id,
         livePhotoVideoId: fileStub.livePhotoMotion.uuid,
@@ -633,21 +629,23 @@ describe(MetadataService.name, () => {
 
     it('should extract the motion photo video from the XMP directory entry ', async () => {
       assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]);
-      metadataMock.readTags.mockResolvedValue({
+      mockReadTags({
         Directory: 'foo/bar/',
         MotionPhoto: 1,
         MicroVideo: 1,
         MicroVideoOffset: 1,
       });
-      cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
+      cryptoMock.hashSha1.mockReturnValue(randomBytes(512));
       assetMock.getByChecksum.mockResolvedValue(null);
       assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
-      cryptoRepository.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid);
+      cryptoMock.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid);
       const video = randomBytes(512);
       storageMock.readFile.mockResolvedValue(video);
 
       await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id });
-      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id]);
+      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id], {
+        faces: { person: false },
+      });
       expect(storageMock.readFile).toHaveBeenCalledWith(
         assetStub.livePhotoWithOriginalFileName.originalPath,
         expect.any(Object),
@@ -668,7 +666,7 @@ describe(MetadataService.name, () => {
         type: AssetType.VIDEO,
       });
       expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512);
-      expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
+      expect(storageMock.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
       expect(assetMock.update).toHaveBeenNthCalledWith(1, {
         id: assetStub.livePhotoWithOriginalFileName.id,
         livePhotoVideoId: fileStub.livePhotoMotion.uuid,
@@ -677,13 +675,13 @@ describe(MetadataService.name, () => {
 
     it('should delete old motion photo video assets if they do not match what is extracted', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.livePhotoWithOriginalFileName]);
-      metadataMock.readTags.mockResolvedValue({
+      mockReadTags({
         Directory: 'foo/bar/',
         MotionPhoto: 1,
         MicroVideo: 1,
         MicroVideoOffset: 1,
       });
-      cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
+      cryptoMock.hashSha1.mockReturnValue(randomBytes(512));
       assetMock.getByChecksum.mockResolvedValue(null);
       assetMock.create.mockImplementation((asset) => Promise.resolve({ ...assetStub.livePhotoMotionAsset, ...asset }));
       const video = randomBytes(512);
@@ -702,13 +700,13 @@ describe(MetadataService.name, () => {
 
     it('should not create a new motion photo video asset if the hash of the extracted video matches an existing asset', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.livePhotoStillAsset]);
-      metadataMock.readTags.mockResolvedValue({
+      mockReadTags({
         Directory: 'foo/bar/',
         MotionPhoto: 1,
         MicroVideo: 1,
         MicroVideoOffset: 1,
       });
-      cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
+      cryptoMock.hashSha1.mockReturnValue(randomBytes(512));
       assetMock.getByChecksum.mockResolvedValue(assetStub.livePhotoMotionAsset);
       const video = randomBytes(512);
       storageMock.readFile.mockResolvedValue(video);
@@ -716,7 +714,7 @@ describe(MetadataService.name, () => {
 
       await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
       expect(assetMock.create).toHaveBeenCalledTimes(0);
-      expect(storageMock.writeFile).toHaveBeenCalledTimes(0);
+      expect(storageMock.createOrOverwriteFile).toHaveBeenCalledTimes(0);
       // The still asset gets saved by handleMetadataExtraction, but not the video
       expect(assetMock.update).toHaveBeenCalledTimes(1);
       expect(jobMock.queue).toHaveBeenCalledTimes(0);
@@ -724,13 +722,13 @@ describe(MetadataService.name, () => {
 
     it('should link and hide motion video asset to still asset if the hash of the extracted video matches an existing asset', async () => {
       assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]);
-      metadataMock.readTags.mockResolvedValue({
+      mockReadTags({
         Directory: 'foo/bar/',
         MotionPhoto: 1,
         MicroVideo: 1,
         MicroVideoOffset: 1,
       });
-      cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
+      cryptoMock.hashSha1.mockReturnValue(randomBytes(512));
       assetMock.getByChecksum.mockResolvedValue({ ...assetStub.livePhotoMotionAsset, isVisible: true });
       const video = randomBytes(512);
       storageMock.readFile.mockResolvedValue(video);
@@ -750,13 +748,13 @@ describe(MetadataService.name, () => {
       assetMock.getByIds.mockResolvedValue([
         { ...assetStub.livePhotoStillAsset, livePhotoVideoId: null, isExternal: true },
       ]);
-      metadataMock.readTags.mockResolvedValue({
+      mockReadTags({
         Directory: 'foo/bar/',
         MotionPhoto: 1,
         MicroVideo: 1,
         MicroVideoOffset: 1,
       });
-      cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
+      cryptoMock.hashSha1.mockReturnValue(randomBytes(512));
       assetMock.getByChecksum.mockResolvedValue(null);
       assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
       const video = randomBytes(512);
@@ -793,10 +791,10 @@ describe(MetadataService.name, () => {
         Rating: 3,
       };
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      metadataMock.readTags.mockResolvedValue(tags);
+      mockReadTags(tags);
 
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
-      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
+      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
       expect(assetMock.upsertExif).toHaveBeenCalledWith({
         assetId: assetStub.image.id,
         bitsPerSample: expect.any(Number),
@@ -851,10 +849,10 @@ describe(MetadataService.name, () => {
         tz: undefined,
       };
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      metadataMock.readTags.mockResolvedValue(tags);
+      mockReadTags(tags);
 
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
-      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
+      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
       expect(assetMock.upsertExif).toHaveBeenCalledWith(
         expect.objectContaining({
           timeZone: 'UTC+0',
@@ -874,7 +872,7 @@ describe(MetadataService.name, () => {
 
       await sut.handleMetadataExtraction({ id: assetStub.video.id });
 
-      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]);
+      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id], { faces: { person: false } });
       expect(assetMock.upsertExif).toHaveBeenCalled();
       expect(assetMock.update).toHaveBeenCalledWith(
         expect.objectContaining({
@@ -884,7 +882,7 @@ describe(MetadataService.name, () => {
       );
     });
 
-    it('only extracts duration for videos', async () => {
+    it('should only extract duration for videos', async () => {
       assetMock.getByIds.mockResolvedValue([{ ...assetStub.image }]);
       mediaMock.probe.mockResolvedValue({
         ...probeStub.videoStreamH264,
@@ -895,7 +893,7 @@ describe(MetadataService.name, () => {
       });
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
 
-      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
+      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
       expect(assetMock.upsertExif).toHaveBeenCalled();
       expect(assetMock.update).toHaveBeenCalledWith(
         expect.objectContaining({
@@ -905,7 +903,7 @@ describe(MetadataService.name, () => {
       );
     });
 
-    it('omits duration of zero', async () => {
+    it('should omit duration of zero', async () => {
       assetMock.getByIds.mockResolvedValue([{ ...assetStub.video }]);
       mediaMock.probe.mockResolvedValue({
         ...probeStub.videoStreamH264,
@@ -917,7 +915,7 @@ describe(MetadataService.name, () => {
 
       await sut.handleMetadataExtraction({ id: assetStub.video.id });
 
-      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]);
+      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id], { faces: { person: false } });
       expect(assetMock.upsertExif).toHaveBeenCalled();
       expect(assetMock.update).toHaveBeenCalledWith(
         expect.objectContaining({
@@ -927,7 +925,7 @@ describe(MetadataService.name, () => {
       );
     });
 
-    it('handles duration of 1 week', async () => {
+    it('should a handle duration of 1 week', async () => {
       assetMock.getByIds.mockResolvedValue([{ ...assetStub.video }]);
       mediaMock.probe.mockResolvedValue({
         ...probeStub.videoStreamH264,
@@ -939,7 +937,7 @@ describe(MetadataService.name, () => {
 
       await sut.handleMetadataExtraction({ id: assetStub.video.id });
 
-      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]);
+      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id], { faces: { person: false } });
       expect(assetMock.upsertExif).toHaveBeenCalled();
       expect(assetMock.update).toHaveBeenCalledWith(
         expect.objectContaining({
@@ -949,9 +947,17 @@ describe(MetadataService.name, () => {
       );
     });
 
-    it('trims whitespace from description', async () => {
+    it('should ignore duration from exif data', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      metadataMock.readTags.mockResolvedValue({ Description: '\t \v \f \n \r' });
+      mockReadTags({}, { Duration: { Value: 123 } });
+
+      await sut.handleMetadataExtraction({ id: assetStub.image.id });
+      expect(assetMock.update).toHaveBeenCalledWith(expect.objectContaining({ duration: null }));
+    });
+
+    it('should trim whitespace from description', async () => {
+      assetMock.getByIds.mockResolvedValue([assetStub.image]);
+      mockReadTags({ Description: '\t \v \f \n \r' });
 
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
       expect(assetMock.upsertExif).toHaveBeenCalledWith(
@@ -960,7 +966,7 @@ describe(MetadataService.name, () => {
         }),
       );
 
-      metadataMock.readTags.mockResolvedValue({ ImageDescription: ' my\n description' });
+      mockReadTags({ ImageDescription: ' my\n description' });
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
       expect(assetMock.upsertExif).toHaveBeenCalledWith(
         expect.objectContaining({
@@ -969,9 +975,9 @@ describe(MetadataService.name, () => {
       );
     });
 
-    it('handles a numeric description', async () => {
+    it('should handle a numeric description', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      metadataMock.readTags.mockResolvedValue({ Description: 1000 });
+      mockReadTags({ Description: 1000 });
 
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
       expect(assetMock.upsertExif).toHaveBeenCalledWith(
@@ -984,7 +990,7 @@ describe(MetadataService.name, () => {
     it('should skip importing metadata when the feature is disabled', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]);
       systemMock.get.mockResolvedValue({ metadata: { faces: { import: false } } });
-      metadataMock.readTags.mockResolvedValue(metadataStub.withFace);
+      mockReadTags(metadataStub.withFace);
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
       expect(personMock.getDistinctNames).not.toHaveBeenCalled();
     });
@@ -992,7 +998,7 @@ describe(MetadataService.name, () => {
     it('should skip importing metadata face for assets without tags.RegionInfo', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]);
       systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } });
-      metadataMock.readTags.mockResolvedValue(metadataStub.empty);
+      mockReadTags(metadataStub.empty);
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
       expect(personMock.getDistinctNames).not.toHaveBeenCalled();
     });
@@ -1000,43 +1006,39 @@ describe(MetadataService.name, () => {
     it('should skip importing faces without name', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]);
       systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } });
-      metadataMock.readTags.mockResolvedValue(metadataStub.withFaceNoName);
+      mockReadTags(metadataStub.withFaceNoName);
       personMock.getDistinctNames.mockResolvedValue([]);
       personMock.createAll.mockResolvedValue([]);
-      personMock.replaceFaces.mockResolvedValue([]);
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
-      expect(personMock.createAll).toHaveBeenCalledWith([]);
-      expect(personMock.replaceFaces).toHaveBeenCalledWith(assetStub.primaryImage.id, [], SourceType.EXIF);
-      expect(personMock.updateAll).toHaveBeenCalledWith([]);
+      expect(personMock.createAll).not.toHaveBeenCalled();
+      expect(personMock.refreshFaces).not.toHaveBeenCalled();
+      expect(personMock.updateAll).not.toHaveBeenCalled();
     });
 
     it('should skip importing faces with empty name', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]);
       systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } });
-      metadataMock.readTags.mockResolvedValue(metadataStub.withFaceEmptyName);
+      mockReadTags(metadataStub.withFaceEmptyName);
       personMock.getDistinctNames.mockResolvedValue([]);
       personMock.createAll.mockResolvedValue([]);
-      personMock.replaceFaces.mockResolvedValue([]);
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
-      expect(personMock.createAll).toHaveBeenCalledWith([]);
-      expect(personMock.replaceFaces).toHaveBeenCalledWith(assetStub.primaryImage.id, [], SourceType.EXIF);
-      expect(personMock.updateAll).toHaveBeenCalledWith([]);
+      expect(personMock.createAll).not.toHaveBeenCalled();
+      expect(personMock.refreshFaces).not.toHaveBeenCalled();
+      expect(personMock.updateAll).not.toHaveBeenCalled();
     });
 
     it('should apply metadata face tags creating new persons', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]);
       systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } });
-      metadataMock.readTags.mockResolvedValue(metadataStub.withFace);
+      mockReadTags(metadataStub.withFace);
       personMock.getDistinctNames.mockResolvedValue([]);
       personMock.createAll.mockResolvedValue([personStub.withName.id]);
-      personMock.replaceFaces.mockResolvedValue(['face-asset-uuid']);
       personMock.update.mockResolvedValue(personStub.withName);
       await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id });
-      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id]);
+      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id], { faces: { person: false } });
       expect(personMock.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true });
       expect(personMock.createAll).toHaveBeenCalledWith([expect.objectContaining({ name: personStub.withName.name })]);
-      expect(personMock.replaceFaces).toHaveBeenCalledWith(
-        assetStub.primaryImage.id,
+      expect(personMock.refreshFaces).toHaveBeenCalledWith(
         [
           {
             id: 'random-uuid',
@@ -1051,7 +1053,7 @@ describe(MetadataService.name, () => {
             sourceType: SourceType.EXIF,
           },
         ],
-        SourceType.EXIF,
+        [],
       );
       expect(personMock.updateAll).toHaveBeenCalledWith([{ id: 'random-uuid', faceAssetId: 'random-uuid' }]);
       expect(jobMock.queueAll).toHaveBeenCalledWith([
@@ -1065,17 +1067,15 @@ describe(MetadataService.name, () => {
     it('should assign metadata face tags to existing persons', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]);
       systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } });
-      metadataMock.readTags.mockResolvedValue(metadataStub.withFace);
+      mockReadTags(metadataStub.withFace);
       personMock.getDistinctNames.mockResolvedValue([{ id: personStub.withName.id, name: personStub.withName.name }]);
       personMock.createAll.mockResolvedValue([]);
-      personMock.replaceFaces.mockResolvedValue(['face-asset-uuid']);
       personMock.update.mockResolvedValue(personStub.withName);
       await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id });
-      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id]);
+      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id], { faces: { person: false } });
       expect(personMock.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true });
-      expect(personMock.createAll).toHaveBeenCalledWith([]);
-      expect(personMock.replaceFaces).toHaveBeenCalledWith(
-        assetStub.primaryImage.id,
+      expect(personMock.createAll).not.toHaveBeenCalled();
+      expect(personMock.refreshFaces).toHaveBeenCalledWith(
         [
           {
             id: 'random-uuid',
@@ -1090,15 +1090,15 @@ describe(MetadataService.name, () => {
             sourceType: SourceType.EXIF,
           },
         ],
-        SourceType.EXIF,
+        [],
       );
-      expect(personMock.updateAll).toHaveBeenCalledWith([]);
-      expect(jobMock.queueAll).toHaveBeenCalledWith([]);
+      expect(personMock.updateAll).not.toHaveBeenCalled();
+      expect(jobMock.queueAll).not.toHaveBeenCalledWith();
     });
 
     it('should handle invalid modify date', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      metadataMock.readTags.mockResolvedValue({ ModifyDate: '00:00:00.000' });
+      mockReadTags({ ModifyDate: '00:00:00.000' });
 
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
       expect(assetMock.upsertExif).toHaveBeenCalledWith(
@@ -1107,6 +1107,30 @@ describe(MetadataService.name, () => {
         }),
       );
     });
+
+    it('should handle invalid rating value', async () => {
+      assetMock.getByIds.mockResolvedValue([assetStub.image]);
+      mockReadTags({ Rating: 6 });
+
+      await sut.handleMetadataExtraction({ id: assetStub.image.id });
+      expect(assetMock.upsertExif).toHaveBeenCalledWith(
+        expect.objectContaining({
+          rating: null,
+        }),
+      );
+    });
+
+    it('should handle valid rating value', async () => {
+      assetMock.getByIds.mockResolvedValue([assetStub.image]);
+      mockReadTags({ Rating: 5 });
+
+      await sut.handleMetadataExtraction({ id: assetStub.image.id });
+      expect(assetMock.upsertExif).toHaveBeenCalledWith(
+        expect.objectContaining({
+          rating: 5,
+        }),
+      );
+    });
   });
 
   describe('handleQueueSidecar', () => {
diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts
index eaa491c3ee..e0566c84b7 100644
--- a/server/src/services/metadata.service.ts
+++ b/server/src/services/metadata.service.ts
@@ -1,4 +1,4 @@
-import { Inject, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
 import { ContainerDirectoryItem, ExifDateTime, Maybe, Tags } from 'exiftool-vendored';
 import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime';
 import _ from 'lodash';
@@ -7,37 +7,19 @@ import { constants } from 'node:fs/promises';
 import path from 'node:path';
 import { SystemConfig } from 'src/config';
 import { StorageCore } from 'src/cores/storage.core';
-import { SystemConfigCore } from 'src/cores/system-config.core';
-import { OnEmit } from 'src/decorators';
+import { OnEvent, OnJob } from 'src/decorators';
 import { AssetFaceEntity } from 'src/entities/asset-face.entity';
 import { AssetEntity } from 'src/entities/asset.entity';
+import { ExifEntity } from 'src/entities/exif.entity';
 import { PersonEntity } from 'src/entities/person.entity';
-import { AssetType, SourceType } from 'src/enum';
-import { IAlbumRepository } from 'src/interfaces/album.interface';
-import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
-import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
-import {
-  IBaseJob,
-  IEntityJob,
-  IJobRepository,
-  ISidecarWriteJob,
-  JobName,
-  JOBS_ASSET_PAGINATION_SIZE,
-  JobStatus,
-  QueueName,
-} from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { IMapRepository, ReverseGeocodeResult } from 'src/interfaces/map.interface';
-import { IMediaRepository } from 'src/interfaces/media.interface';
-import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
-import { IMoveRepository } from 'src/interfaces/move.interface';
-import { IPersonRepository } from 'src/interfaces/person.interface';
-import { IStorageRepository } from 'src/interfaces/storage.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
-import { ITagRepository } from 'src/interfaces/tag.interface';
-import { IUserRepository } from 'src/interfaces/user.interface';
+import { AssetType, ExifOrientation, ImmichWorker, SourceType } from 'src/enum';
+import { WithoutProperty } from 'src/interfaces/asset.interface';
+import { DatabaseLock } from 'src/interfaces/database.interface';
+import { ArgOf } from 'src/interfaces/event.interface';
+import { JobName, JobOf, JOBS_ASSET_PAGINATION_SIZE, JobStatus, QueueName } from 'src/interfaces/job.interface';
+import { ReverseGeocodeResult } from 'src/interfaces/map.interface';
+import { ImmichTags } from 'src/interfaces/metadata.interface';
+import { BaseService } from 'src/services/base.service';
 import { isFaceImportEnabled } from 'src/utils/misc';
 import { usePagination } from 'src/utils/pagination';
 import { upsertTags } from 'src/utils/tag';
@@ -54,17 +36,6 @@ const EXIF_DATE_TAGS: Array<keyof Tags> = [
   'DateTimeCreated',
 ];
 
-export enum Orientation {
-  Horizontal = 1,
-  MirrorHorizontal = 2,
-  Rotate180 = 3,
-  MirrorVertical = 4,
-  MirrorHorizontalRotate270CW = 5,
-  Rotate90CW = 6,
-  MirrorHorizontalRotate90CW = 7,
-  Rotate270CW = 8,
-}
-
 const validate = <T>(value: T): NonNullable<T> | null => {
   // handle lists of numbers
   if (Array.isArray(value)) {
@@ -83,62 +54,33 @@ const validate = <T>(value: T): NonNullable<T> | null => {
   return value ?? null;
 };
 
+const validateRange = (value: number | undefined, min: number, max: number): NonNullable<number> | null => {
+  // reutilizes the validate function
+  const val = validate(value);
+
+  // check if the value is within the range
+  if (val == null || val < min || val > max) {
+    return null;
+  }
+
+  return val;
+};
+
 @Injectable()
-export class MetadataService {
-  private storageCore: StorageCore;
-  private configCore: SystemConfigCore;
-
-  constructor(
-    @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
-    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
-    @Inject(IEventRepository) private eventRepository: IEventRepository,
-    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
-    @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
-    @Inject(IJobRepository) private jobRepository: IJobRepository,
-    @Inject(IMapRepository) private mapRepository: IMapRepository,
-    @Inject(IMediaRepository) private mediaRepository: IMediaRepository,
-    @Inject(IMetadataRepository) private repository: IMetadataRepository,
-    @Inject(IMoveRepository) moveRepository: IMoveRepository,
-    @Inject(IPersonRepository) private personRepository: IPersonRepository,
-    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
-    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
-    @Inject(ITagRepository) private tagRepository: ITagRepository,
-    @Inject(IUserRepository) private userRepository: IUserRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-  ) {
-    this.logger.setContext(MetadataService.name);
-    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
-    this.storageCore = StorageCore.create(
-      assetRepository,
-      cryptoRepository,
-      moveRepository,
-      personRepository,
-      storageRepository,
-      systemMetadataRepository,
-      this.logger,
-    );
+export class MetadataService extends BaseService {
+  @OnEvent({ name: 'app.bootstrap', workers: [ImmichWorker.MICROSERVICES] })
+  async onBootstrap() {
+    this.logger.log('Bootstrapping metadata service');
+    await this.init();
   }
 
-  @OnEmit({ event: 'app.bootstrap' })
-  async onBootstrap(app: ArgOf<'app.bootstrap'>) {
-    if (app !== 'microservices') {
-      return;
-    }
-    const config = await this.configCore.getConfig({ withCache: false });
-    await this.init(config);
+  @OnEvent({ name: 'app.shutdown' })
+  async onShutdown() {
+    await this.metadataRepository.teardown();
   }
 
-  @OnEmit({ event: 'config.update' })
-  async onConfigUpdate({ newConfig }: ArgOf<'config.update'>) {
-    await this.init(newConfig);
-  }
-
-  private async init({ reverseGeocoding }: SystemConfig) {
-    const { enabled } = reverseGeocoding;
-
-    if (!enabled) {
-      return;
-    }
+  private async init() {
+    this.logger.log('Initializing metadata service');
 
     try {
       await this.jobRepository.pause(QueueName.METADATA_EXTRACTION);
@@ -151,12 +93,8 @@ export class MetadataService {
     }
   }
 
-  @OnEmit({ event: 'app.shutdown' })
-  async onShutdown() {
-    await this.repository.teardown();
-  }
-
-  async handleLivePhotoLinking(job: IEntityJob): Promise<JobStatus> {
+  @OnJob({ name: JobName.LINK_LIVE_PHOTOS, queue: QueueName.METADATA_EXTRACTION })
+  async handleLivePhotoLinking(job: JobOf<JobName.LINK_LIVE_PHOTOS>): Promise<JobStatus> {
     const { id } = job;
     const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
     if (!asset?.exifInfo) {
@@ -191,7 +129,8 @@ export class MetadataService {
     return JobStatus.SUCCESS;
   }
 
-  async handleQueueMetadataExtraction(job: IBaseJob): Promise<JobStatus> {
+  @OnJob({ name: JobName.QUEUE_METADATA_EXTRACTION, queue: QueueName.METADATA_EXTRACTION })
+  async handleQueueMetadataExtraction(job: JobOf<JobName.QUEUE_METADATA_EXTRACTION>): Promise<JobStatus> {
     const { force } = job;
     const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
       return force
@@ -208,9 +147,10 @@ export class MetadataService {
     return JobStatus.SUCCESS;
   }
 
-  async handleMetadataExtraction({ id }: IEntityJob): Promise<JobStatus> {
-    const { metadata, reverseGeocoding } = await this.configCore.getConfig({ withCache: true });
-    const [asset] = await this.assetRepository.getByIds([id]);
+  @OnJob({ name: JobName.METADATA_EXTRACTION, queue: QueueName.METADATA_EXTRACTION })
+  async handleMetadataExtraction({ id }: JobOf<JobName.METADATA_EXTRACTION>): Promise<JobStatus> {
+    const { metadata, reverseGeocoding } = await this.getConfig({ withCache: true });
+    const [asset] = await this.assetRepository.getByIds([id], { faces: { person: false } });
     if (!asset) {
       return JobStatus.FAILED;
     }
@@ -224,7 +164,9 @@ export class MetadataService {
     const { dateTimeOriginal, localDateTime, timeZone, modifyDate } = this.getDates(asset, exifTags);
     const { latitude, longitude, country, state, city } = await this.getGeo(exifTags, reverseGeocoding);
 
-    const exifData = {
+    const { width, height } = this.getImageDimensions(exifTags);
+
+    const exifData: Partial<ExifEntity> = {
       assetId: asset.id,
 
       // dates
@@ -241,8 +183,8 @@ export class MetadataService {
 
       // image/file
       fileSizeInByte: stats.size,
-      exifImageHeight: validate(exifTags.ImageHeight),
-      exifImageWidth: validate(exifTags.ImageWidth),
+      exifImageHeight: validate(height),
+      exifImageWidth: validate(width),
       orientation: validate(exifTags.Orientation)?.toString() ?? null,
       projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null,
       bitsPerSample: this.getBitsPerSample(exifTags),
@@ -252,7 +194,7 @@ export class MetadataService {
       make: exifTags.Make ?? null,
       model: exifTags.Model ?? null,
       fps: validate(Number.parseFloat(exifTags.VideoFrameRate!)),
-      iso: validate(exifTags.ISO),
+      iso: validate(exifTags.ISO) as number,
       exposureTime: exifTags.ExposureTime ?? null,
       lensModel: exifTags.LensModel ?? null,
       fNumber: validate(exifTags.FNumber),
@@ -261,7 +203,7 @@ export class MetadataService {
       // comments
       description: String(exifTags.ImageDescription || exifTags.Description || '').trim(),
       profileDescription: exifTags.ProfileDescription || null,
-      rating: exifTags.Rating ?? null,
+      rating: validateRange(exifTags.Rating, 0, 5),
 
       // grouping
       livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null,
@@ -292,7 +234,8 @@ export class MetadataService {
     return JobStatus.SUCCESS;
   }
 
-  async handleQueueSidecar(job: IBaseJob): Promise<JobStatus> {
+  @OnJob({ name: JobName.QUEUE_SIDECAR, queue: QueueName.SIDECAR })
+  async handleQueueSidecar(job: JobOf<JobName.QUEUE_SIDECAR>): Promise<JobStatus> {
     const { force } = job;
     const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
       return force
@@ -312,25 +255,28 @@ export class MetadataService {
     return JobStatus.SUCCESS;
   }
 
-  handleSidecarSync({ id }: IEntityJob): Promise<JobStatus> {
+  @OnJob({ name: JobName.SIDECAR_SYNC, queue: QueueName.SIDECAR })
+  handleSidecarSync({ id }: JobOf<JobName.SIDECAR_SYNC>): Promise<JobStatus> {
     return this.processSidecar(id, true);
   }
 
-  handleSidecarDiscovery({ id }: IEntityJob): Promise<JobStatus> {
+  @OnJob({ name: JobName.SIDECAR_DISCOVERY, queue: QueueName.SIDECAR })
+  handleSidecarDiscovery({ id }: JobOf<JobName.SIDECAR_DISCOVERY>): Promise<JobStatus> {
     return this.processSidecar(id, false);
   }
 
-  @OnEmit({ event: 'asset.tag' })
+  @OnEvent({ name: 'asset.tag' })
   async handleTagAsset({ assetId }: ArgOf<'asset.tag'>) {
     await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id: assetId, tags: true } });
   }
 
-  @OnEmit({ event: 'asset.untag' })
+  @OnEvent({ name: 'asset.untag' })
   async handleUntagAsset({ assetId }: ArgOf<'asset.untag'>) {
     await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id: assetId, tags: true } });
   }
 
-  async handleSidecarWrite(job: ISidecarWriteJob): Promise<JobStatus> {
+  @OnJob({ name: JobName.SIDECAR_WRITE, queue: QueueName.SIDECAR })
+  async handleSidecarWrite(job: JobOf<JobName.SIDECAR_WRITE>): Promise<JobStatus> {
     const { id, description, dateTimeOriginal, latitude, longitude, rating, tags } = job;
     const [asset] = await this.assetRepository.getByIds([id], { tags: true });
     if (!asset) {
@@ -357,7 +303,7 @@ export class MetadataService {
       return JobStatus.SKIPPED;
     }
 
-    await this.repository.writeTags(sidecarPath, exif);
+    await this.metadataRepository.writeTags(sidecarPath, exif);
 
     if (!asset.sidecarPath) {
       await this.assetRepository.update({ id, sidecarPath });
@@ -366,12 +312,25 @@ export class MetadataService {
     return JobStatus.SUCCESS;
   }
 
+  private getImageDimensions(exifTags: ImmichTags): { width?: number; height?: number } {
+    /*
+     * The "true" values for width and height are a bit hidden, depending on the camera model and file format.
+     * For RAW images in the CR2 or RAF format, the "ImageSize" value seems to be correct,
+     * but ImageWidth and ImageHeight are not correct (they contain the dimensions of the preview image).
+     */
+    let [width, height] = exifTags.ImageSize?.split('x').map((dim) => Number.parseInt(dim) || undefined) || [];
+    if (!width || !height) {
+      [width, height] = [exifTags.ImageWidth, exifTags.ImageHeight];
+    }
+    return { width, height };
+  }
+
   private async getExifTags(asset: AssetEntity): Promise<ImmichTags> {
-    const mediaTags = await this.repository.readTags(asset.originalPath);
-    const sidecarTags = asset.sidecarPath ? await this.repository.readTags(asset.sidecarPath) : {};
+    const mediaTags = await this.metadataRepository.readTags(asset.originalPath);
+    const sidecarTags = asset.sidecarPath ? await this.metadataRepository.readTags(asset.sidecarPath) : {};
     const videoTags = asset.type === AssetType.VIDEO ? await this.getVideoTags(asset.originalPath) : {};
 
-    // make sure dates comes from sidecar
+    // prefer dates from sidecar tags
     const sidecarDate = firstDateTime(sidecarTags as Tags, EXIF_DATE_TAGS);
     if (sidecarDate) {
       for (const tag of EXIF_DATE_TAGS) {
@@ -379,17 +338,21 @@ export class MetadataService {
       }
     }
 
+    // prefer duration from video tags
+    delete mediaTags.Duration;
+    delete sidecarTags.Duration;
+
     return { ...mediaTags, ...videoTags, ...sidecarTags };
   }
 
   private async applyTagList(asset: AssetEntity, exifTags: ImmichTags) {
-    const tags: Array<string | number> = [];
+    const tags: string[] = [];
     if (exifTags.TagsList) {
-      tags.push(...exifTags.TagsList);
+      tags.push(...exifTags.TagsList.map(String));
     } else if (exifTags.HierarchicalSubject) {
       tags.push(
         ...exifTags.HierarchicalSubject.map((tag) =>
-          tag
+          String(tag)
             // convert | to /
             .replaceAll('/', '<PLACEHOLDER>')
             .replaceAll('|', '/')
@@ -401,10 +364,10 @@ export class MetadataService {
       if (!Array.isArray(keywords)) {
         keywords = [keywords];
       }
-      tags.push(...keywords);
+      tags.push(...keywords.map(String));
     }
 
-    const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags: tags.map(String) });
+    const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags });
     await this.tagRepository.upsertAssetTags({ assetId: asset.id, tagIds: results.map((tag) => tag.id) });
   }
 
@@ -452,11 +415,11 @@ export class MetadataService {
       // Samsung MotionPhoto video extraction
       //     HEIC-encoded
       if (hasMotionPhotoVideo) {
-        video = await this.repository.extractBinaryTag(asset.originalPath, 'MotionPhotoVideo');
+        video = await this.metadataRepository.extractBinaryTag(asset.originalPath, 'MotionPhotoVideo');
       }
       //     JPEG-encoded; HEIC also contains these tags, so this conditional must come second
       else if (hasEmbeddedVideoFile) {
-        video = await this.repository.extractBinaryTag(asset.originalPath, 'EmbeddedVideoFile');
+        video = await this.metadataRepository.extractBinaryTag(asset.originalPath, 'EmbeddedVideoFile');
       }
       // Default video extraction
       else {
@@ -529,7 +492,7 @@ export class MetadataService {
       const existsOnDisk = await this.storageRepository.checkFileExists(motionAsset.originalPath);
       if (!existsOnDisk) {
         this.storageCore.ensureFolders(motionAsset.originalPath);
-        await this.storageRepository.writeFile(motionAsset.originalPath, video);
+        await this.storageRepository.createFile(motionAsset.originalPath, video);
         this.logger.log(`Wrote motion photo video to ${motionAsset.originalPath}`);
         await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: motionAsset.id } });
       }
@@ -545,7 +508,7 @@ export class MetadataService {
       return;
     }
 
-    const discoveredFaces: Partial<AssetFaceEntity>[] = [];
+    const facesToAdd: Partial<AssetFaceEntity>[] = [];
     const existingNames = await this.personRepository.getDistinctNames(asset.ownerId, { withHidden: true });
     const existingNameMap = new Map(existingNames.map(({ id, name }) => [name.toLowerCase(), id]));
     const missing: Partial<PersonEntity>[] = [];
@@ -573,7 +536,7 @@ export class MetadataService {
         sourceType: SourceType.EXIF,
       };
 
-      discoveredFaces.push(face);
+      facesToAdd.push(face);
       if (!existingNameMap.has(loweredName)) {
         missing.push({ id: personId, ownerId: asset.ownerId, name: region.Name });
         missingWithFaceAsset.push({ id: personId, faceAssetId: face.id });
@@ -582,30 +545,32 @@ export class MetadataService {
 
     if (missing.length > 0) {
       this.logger.debug(`Creating missing persons: ${missing.map((p) => `${p.name}/${p.id}`)}`);
+      const newPersonIds = await this.personRepository.createAll(missing);
+      const jobs = newPersonIds.map((id) => ({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } }) as const);
+      await this.jobRepository.queueAll(jobs);
     }
 
-    const newPersonIds = await this.personRepository.createAll(missing);
+    const facesToRemove = asset.faces.filter((face) => face.sourceType === SourceType.EXIF).map((face) => face.id);
+    if (facesToRemove.length > 0) {
+      this.logger.debug(`Removing ${facesToRemove.length} faces for asset ${asset.id}`);
+    }
 
-    const faceIds = await this.personRepository.replaceFaces(asset.id, discoveredFaces, SourceType.EXIF);
-    this.logger.debug(`Created ${faceIds.length} faces for asset ${asset.id}`);
+    if (facesToAdd.length > 0) {
+      this.logger.debug(`Creating ${facesToAdd} faces from metadata for asset ${asset.id}`);
+    }
 
-    await this.personRepository.updateAll(missingWithFaceAsset);
+    if (facesToRemove.length > 0 || facesToAdd.length > 0) {
+      await this.personRepository.refreshFaces(facesToAdd, facesToRemove);
+    }
 
-    await this.jobRepository.queueAll(
-      newPersonIds.map((id) => ({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } })),
-    );
+    if (missingWithFaceAsset.length > 0) {
+      await this.personRepository.updateAll(missingWithFaceAsset);
+    }
   }
 
   private getDates(asset: AssetEntity, exifTags: ImmichTags) {
     const dateTime = firstDateTime(exifTags as Maybe<Tags>, EXIF_DATE_TAGS);
-    this.logger.debug(`Asset ${asset.id} date time is ${dateTime}`);
-
-    // created
-    let dateTimeOriginal = dateTime?.toDate();
-    if (!dateTimeOriginal) {
-      this.logger.warn(`Asset ${asset.id} has no valid date (${dateTime}), falling back to asset.fileCreatedAt`);
-      dateTimeOriginal = asset.fileCreatedAt;
-    }
+    this.logger.verbose(`Asset ${asset.id} date time is ${dateTime}`);
 
     // timezone
     let timeZone = exifTags.tz ?? null;
@@ -616,19 +581,21 @@ export class MetadataService {
     }
 
     if (timeZone) {
-      this.logger.debug(`Asset ${asset.id} timezone is ${timeZone} (via ${exifTags.tzSource})`);
+      this.logger.verbose(`Asset ${asset.id} timezone is ${timeZone} (via ${exifTags.tzSource})`);
     } else {
       this.logger.warn(`Asset ${asset.id} has no time zone information`);
     }
 
-    // offset minutes
-    const offsetMinutes = dateTime?.tzoffsetMinutes || 0;
-    let localDateTime = dateTimeOriginal;
-    if (offsetMinutes) {
-      localDateTime = new Date(dateTimeOriginal.getTime() + offsetMinutes * 60_000);
-      this.logger.debug(`Asset ${asset.id} local time is offset by ${offsetMinutes} minutes`);
+    let dateTimeOriginal = dateTime?.toDate();
+    let localDateTime = dateTime?.toDateTime().setZone('UTC', { keepLocalTime: true }).toJSDate();
+    if (!localDateTime || !dateTimeOriginal) {
+      this.logger.warn(`Asset ${asset.id} has no valid date, falling back to asset.fileCreatedAt`);
+      dateTimeOriginal = asset.fileCreatedAt;
+      localDateTime = asset.fileCreatedAt;
     }
 
+    this.logger.verbose(`Asset ${asset.id} has a local time of ${localDateTime.toISOString()}`);
+
     let modifyDate = asset.fileModifiedAt;
     try {
       modifyDate = (exifTags.ModifyDate as ExifDateTime)?.toDate() ?? modifyDate;
@@ -695,19 +662,19 @@ export class MetadataService {
     if (videoStreams[0]) {
       switch (videoStreams[0].rotation) {
         case -90: {
-          tags.Orientation = Orientation.Rotate90CW;
+          tags.Orientation = ExifOrientation.Rotate90CW;
           break;
         }
         case 0: {
-          tags.Orientation = Orientation.Horizontal;
+          tags.Orientation = ExifOrientation.Horizontal;
           break;
         }
         case 90: {
-          tags.Orientation = Orientation.Rotate270CW;
+          tags.Orientation = ExifOrientation.Rotate270CW;
           break;
         }
         case 180: {
-          tags.Orientation = Orientation.Rotate180;
+          tags.Orientation = ExifOrientation.Rotate180;
           break;
         }
       }
@@ -731,7 +698,7 @@ export class MetadataService {
       return JobStatus.FAILED;
     }
 
-    if (!isSync && (!asset.isVisible || asset.sidecarPath)) {
+    if (!isSync && (!asset.isVisible || asset.sidecarPath) && !asset.isExternal) {
       return JobStatus.FAILED;
     }
 
@@ -753,6 +720,13 @@ export class MetadataService {
       sidecarPath = sidecarPathWithoutExt;
     }
 
+    if (asset.isExternal) {
+      if (sidecarPath !== asset.sidecarPath) {
+        await this.assetRepository.update({ id: asset.id, sidecarPath });
+      }
+      return JobStatus.SUCCESS;
+    }
+
     if (sidecarPath) {
       await this.assetRepository.update({ id: asset.id, sidecarPath });
       return JobStatus.SUCCESS;
diff --git a/server/src/services/microservices.service.ts b/server/src/services/microservices.service.ts
deleted file mode 100644
index 025400cc9b..0000000000
--- a/server/src/services/microservices.service.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import { Injectable } from '@nestjs/common';
-import { OnEmit } from 'src/decorators';
-import { ArgOf } from 'src/interfaces/event.interface';
-import { IDeleteFilesJob, JobName } from 'src/interfaces/job.interface';
-import { AssetService } from 'src/services/asset.service';
-import { AuditService } from 'src/services/audit.service';
-import { DuplicateService } from 'src/services/duplicate.service';
-import { JobService } from 'src/services/job.service';
-import { LibraryService } from 'src/services/library.service';
-import { MediaService } from 'src/services/media.service';
-import { MetadataService } from 'src/services/metadata.service';
-import { NotificationService } from 'src/services/notification.service';
-import { PersonService } from 'src/services/person.service';
-import { SessionService } from 'src/services/session.service';
-import { SmartInfoService } from 'src/services/smart-info.service';
-import { StorageTemplateService } from 'src/services/storage-template.service';
-import { StorageService } from 'src/services/storage.service';
-import { UserService } from 'src/services/user.service';
-import { VersionService } from 'src/services/version.service';
-import { otelShutdown } from 'src/utils/instrumentation';
-
-@Injectable()
-export class MicroservicesService {
-  constructor(
-    private auditService: AuditService,
-    private assetService: AssetService,
-    private jobService: JobService,
-    private libraryService: LibraryService,
-    private mediaService: MediaService,
-    private metadataService: MetadataService,
-    private notificationService: NotificationService,
-    private personService: PersonService,
-    private smartInfoService: SmartInfoService,
-    private sessionService: SessionService,
-    private storageTemplateService: StorageTemplateService,
-    private storageService: StorageService,
-    private userService: UserService,
-    private duplicateService: DuplicateService,
-    private versionService: VersionService,
-  ) {}
-
-  @OnEmit({ event: 'app.bootstrap' })
-  async onBootstrap(app: ArgOf<'app.bootstrap'>) {
-    if (app !== 'microservices') {
-      return;
-    }
-
-    await this.jobService.init({
-      [JobName.ASSET_DELETION]: (data) => this.assetService.handleAssetDeletion(data),
-      [JobName.ASSET_DELETION_CHECK]: () => this.assetService.handleAssetDeletionCheck(),
-      [JobName.DELETE_FILES]: (data: IDeleteFilesJob) => this.storageService.handleDeleteFiles(data),
-      [JobName.CLEAN_OLD_AUDIT_LOGS]: () => this.auditService.handleCleanup(),
-      [JobName.CLEAN_OLD_SESSION_TOKENS]: () => this.sessionService.handleCleanup(),
-      [JobName.USER_DELETE_CHECK]: () => this.userService.handleUserDeleteCheck(),
-      [JobName.USER_DELETION]: (data) => this.userService.handleUserDelete(data),
-      [JobName.USER_SYNC_USAGE]: () => this.userService.handleUserSyncUsage(),
-      [JobName.QUEUE_SMART_SEARCH]: (data) => this.smartInfoService.handleQueueEncodeClip(data),
-      [JobName.SMART_SEARCH]: (data) => this.smartInfoService.handleEncodeClip(data),
-      [JobName.QUEUE_DUPLICATE_DETECTION]: (data) => this.duplicateService.handleQueueSearchDuplicates(data),
-      [JobName.DUPLICATE_DETECTION]: (data) => this.duplicateService.handleSearchDuplicates(data),
-      [JobName.STORAGE_TEMPLATE_MIGRATION]: () => this.storageTemplateService.handleMigration(),
-      [JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE]: (data) => this.storageTemplateService.handleMigrationSingle(data),
-      [JobName.QUEUE_MIGRATION]: () => this.mediaService.handleQueueMigration(),
-      [JobName.MIGRATE_ASSET]: (data) => this.mediaService.handleAssetMigration(data),
-      [JobName.MIGRATE_PERSON]: (data) => this.personService.handlePersonMigration(data),
-      [JobName.QUEUE_GENERATE_THUMBNAILS]: (data) => this.mediaService.handleQueueGenerateThumbnails(data),
-      [JobName.GENERATE_PREVIEW]: (data) => this.mediaService.handleGeneratePreview(data),
-      [JobName.GENERATE_THUMBNAIL]: (data) => this.mediaService.handleGenerateThumbnail(data),
-      [JobName.GENERATE_THUMBHASH]: (data) => this.mediaService.handleGenerateThumbhash(data),
-      [JobName.QUEUE_VIDEO_CONVERSION]: (data) => this.mediaService.handleQueueVideoConversion(data),
-      [JobName.VIDEO_CONVERSION]: (data) => this.mediaService.handleVideoConversion(data),
-      [JobName.QUEUE_METADATA_EXTRACTION]: (data) => this.metadataService.handleQueueMetadataExtraction(data),
-      [JobName.METADATA_EXTRACTION]: (data) => this.metadataService.handleMetadataExtraction(data),
-      [JobName.LINK_LIVE_PHOTOS]: (data) => this.metadataService.handleLivePhotoLinking(data),
-      [JobName.QUEUE_FACE_DETECTION]: (data) => this.personService.handleQueueDetectFaces(data),
-      [JobName.FACE_DETECTION]: (data) => this.personService.handleDetectFaces(data),
-      [JobName.QUEUE_FACIAL_RECOGNITION]: (data) => this.personService.handleQueueRecognizeFaces(data),
-      [JobName.FACIAL_RECOGNITION]: (data) => this.personService.handleRecognizeFaces(data),
-      [JobName.GENERATE_PERSON_THUMBNAIL]: (data) => this.personService.handleGeneratePersonThumbnail(data),
-      [JobName.PERSON_CLEANUP]: () => this.personService.handlePersonCleanup(),
-      [JobName.QUEUE_SIDECAR]: (data) => this.metadataService.handleQueueSidecar(data),
-      [JobName.SIDECAR_DISCOVERY]: (data) => this.metadataService.handleSidecarDiscovery(data),
-      [JobName.SIDECAR_SYNC]: (data) => this.metadataService.handleSidecarSync(data),
-      [JobName.SIDECAR_WRITE]: (data) => this.metadataService.handleSidecarWrite(data),
-      [JobName.LIBRARY_SCAN_ASSET]: (data) => this.libraryService.handleAssetRefresh(data),
-      [JobName.LIBRARY_SCAN]: (data) => this.libraryService.handleQueueAssetRefresh(data),
-      [JobName.LIBRARY_DELETE]: (data) => this.libraryService.handleDeleteLibrary(data),
-      [JobName.LIBRARY_CHECK_OFFLINE]: (data) => this.libraryService.handleOfflineCheck(data),
-      [JobName.LIBRARY_REMOVE_OFFLINE]: (data) => this.libraryService.handleRemoveOffline(data),
-      [JobName.LIBRARY_QUEUE_SCAN_ALL]: (data) => this.libraryService.handleQueueAllScan(data),
-      [JobName.LIBRARY_QUEUE_CLEANUP]: () => this.libraryService.handleQueueCleanup(),
-      [JobName.SEND_EMAIL]: (data) => this.notificationService.handleSendEmail(data),
-      [JobName.NOTIFY_ALBUM_INVITE]: (data) => this.notificationService.handleAlbumInvite(data),
-      [JobName.NOTIFY_ALBUM_UPDATE]: (data) => this.notificationService.handleAlbumUpdate(data),
-      [JobName.NOTIFY_SIGNUP]: (data) => this.notificationService.handleUserSignup(data),
-      [JobName.VERSION_CHECK]: () => this.versionService.handleVersionCheck(),
-    });
-  }
-
-  async onShutdown() {
-    await otelShutdown();
-  }
-}
diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts
index 9ef1310bfb..76da12bbd6 100644
--- a/server/src/services/notification.service.spec.ts
+++ b/server/src/services/notification.service.spec.ts
@@ -7,8 +7,7 @@ import { AssetFileType, UserMetadataKey } from 'src/enum';
 import { IAlbumRepository } from 'src/interfaces/album.interface';
 import { IAssetRepository } from 'src/interfaces/asset.interface';
 import { IEventRepository } from 'src/interfaces/event.interface';
-import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
+import { IJobRepository, INotifyAlbumUpdateJob, JobName, JobStatus } from 'src/interfaces/job.interface';
 import { EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { IUserRepository } from 'src/interfaces/user.interface';
@@ -16,14 +15,7 @@ import { NotificationService } from 'src/services/notification.service';
 import { albumStub } from 'test/fixtures/album.stub';
 import { assetStub } from 'test/fixtures/asset.stub';
 import { userStub } from 'test/fixtures/user.stub';
-import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
-import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
-import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
-import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newNotificationRepositoryMock } from 'test/repositories/notification.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
-import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 const configs = {
@@ -64,42 +56,34 @@ const configs = {
 };
 
 describe(NotificationService.name, () => {
+  let sut: NotificationService;
+
   let albumMock: Mocked<IAlbumRepository>;
   let assetMock: Mocked<IAssetRepository>;
   let eventMock: Mocked<IEventRepository>;
   let jobMock: Mocked<IJobRepository>;
-  let loggerMock: Mocked<ILoggerRepository>;
   let notificationMock: Mocked<INotificationRepository>;
-  let sut: NotificationService;
   let systemMock: Mocked<ISystemMetadataRepository>;
   let userMock: Mocked<IUserRepository>;
 
   beforeEach(() => {
-    albumMock = newAlbumRepositoryMock();
-    assetMock = newAssetRepositoryMock();
-    eventMock = newEventRepositoryMock();
-    jobMock = newJobRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-    notificationMock = newNotificationRepositoryMock();
-    systemMock = newSystemMetadataRepositoryMock();
-    userMock = newUserRepositoryMock();
-
-    sut = new NotificationService(
-      eventMock,
-      systemMock,
-      notificationMock,
-      userMock,
-      jobMock,
-      loggerMock,
-      assetMock,
-      albumMock,
-    );
+    ({ sut, albumMock, assetMock, eventMock, jobMock, notificationMock, systemMock, userMock } =
+      newTestService(NotificationService));
   });
 
   it('should work', () => {
     expect(sut).toBeDefined();
   });
 
+  describe('onConfigUpdate', () => {
+    it('should emit client and server events', () => {
+      const update = { oldConfig: defaults, newConfig: defaults };
+      expect(sut.onConfigUpdate(update)).toBeUndefined();
+      expect(eventMock.clientBroadcast).toHaveBeenCalledWith('on_config_update');
+      expect(eventMock.serverSend).toHaveBeenCalledWith('config.update', update);
+    });
+  });
+
   describe('onConfigValidateEvent', () => {
     it('validates smtp config when enabling smtp', async () => {
       const oldConfig = configs.smtpDisabled;
@@ -142,6 +126,14 @@ describe(NotificationService.name, () => {
       await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow();
       expect(notificationMock.verifySmtp).not.toHaveBeenCalled();
     });
+
+    it('should fail if smtp configuration is invalid', async () => {
+      const oldConfig = configs.smtpDisabled;
+      const newConfig = configs.smtpEnabled;
+
+      notificationMock.verifySmtp.mockRejectedValue(new Error('Failed validating smtp'));
+      await expect(sut.onConfigValidate({ oldConfig, newConfig })).rejects.toBeInstanceOf(Error);
+    });
   });
 
   describe('onAssetHide', () => {
@@ -155,7 +147,7 @@ describe(NotificationService.name, () => {
     it('should queue the generate thumbnail job', async () => {
       await sut.onAssetShow({ assetId: 'asset-id', userId: 'user-id' });
       expect(jobMock.queue).toHaveBeenCalledWith({
-        name: JobName.GENERATE_THUMBNAIL,
+        name: JobName.GENERATE_THUMBNAILS,
         data: { id: 'asset-id', notify: true },
       });
     });
@@ -178,10 +170,10 @@ describe(NotificationService.name, () => {
 
   describe('onAlbumUpdateEvent', () => {
     it('should queue notify album update event', async () => {
-      await sut.onAlbumUpdate({ id: '', updatedBy: '42' });
+      await sut.onAlbumUpdate({ id: 'album', recipientIds: ['42'] });
       expect(jobMock.queue).toHaveBeenCalledWith({
         name: JobName.NOTIFY_ALBUM_UPDATE,
-        data: { id: '', senderId: '42' },
+        data: { id: 'album', recipientIds: ['42'], delay: 300_000 },
       });
     });
   });
@@ -196,6 +188,18 @@ describe(NotificationService.name, () => {
     });
   });
 
+  describe('onSessionDeleteEvent', () => {
+    it('should send a on_session_delete client event', () => {
+      vi.useFakeTimers();
+      sut.onSessionDelete({ sessionId: 'id' });
+      expect(eventMock.clientSend).not.toHaveBeenCalled();
+
+      vi.advanceTimersByTime(500);
+
+      expect(eventMock.clientSend).toHaveBeenCalledWith('on_session_delete', 'id', 'id');
+    });
+  });
+
   describe('onAssetTrash', () => {
     it('should send connected clients an event', () => {
       sut.onAssetTrash({ assetId: 'asset-id', userId: 'user-id' });
@@ -227,28 +231,28 @@ describe(NotificationService.name, () => {
   describe('onStackCreate', () => {
     it('should send connected clients an event', () => {
       sut.onStackCreate({ stackId: 'stack-id', userId: 'user-id' });
-      expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []);
+      expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id');
     });
   });
 
   describe('onStackUpdate', () => {
     it('should send connected clients an event', () => {
       sut.onStackUpdate({ stackId: 'stack-id', userId: 'user-id' });
-      expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []);
+      expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id');
     });
   });
 
   describe('onStackDelete', () => {
     it('should send connected clients an event', () => {
       sut.onStackDelete({ stackId: 'stack-id', userId: 'user-id' });
-      expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []);
+      expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id');
     });
   });
 
   describe('onStacksDelete', () => {
     it('should send connected clients an event', () => {
       sut.onStacksDelete({ stackIds: ['stack-id'], userId: 'user-id' });
-      expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []);
+      expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id');
     });
   });
 
@@ -508,34 +512,17 @@ describe(NotificationService.name, () => {
 
   describe('handleAlbumUpdate', () => {
     it('should skip if album could not be found', async () => {
-      await expect(sut.handleAlbumUpdate({ id: '', senderId: '' })).resolves.toBe(JobStatus.SKIPPED);
+      await expect(sut.handleAlbumUpdate({ id: '', recipientIds: ['1'] })).resolves.toBe(JobStatus.SKIPPED);
       expect(userMock.get).not.toHaveBeenCalled();
     });
 
     it('should skip if owner could not be found', async () => {
       albumMock.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail);
 
-      await expect(sut.handleAlbumUpdate({ id: '', senderId: '' })).resolves.toBe(JobStatus.SKIPPED);
+      await expect(sut.handleAlbumUpdate({ id: '', recipientIds: ['1'] })).resolves.toBe(JobStatus.SKIPPED);
       expect(systemMock.get).not.toHaveBeenCalled();
     });
 
-    it('should filter out the sender', async () => {
-      albumMock.getById.mockResolvedValue({
-        ...albumStub.emptyWithValidThumbnail,
-        albumUsers: [
-          { user: { id: userStub.user1.id } } as AlbumUserEntity,
-          { user: { id: userStub.user2.id } } as AlbumUserEntity,
-        ],
-      });
-      userMock.get.mockResolvedValue(userStub.user1);
-      notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
-
-      await sut.handleAlbumUpdate({ id: '', senderId: userStub.user1.id });
-      expect(userMock.get).not.toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false });
-      expect(userMock.get).toHaveBeenCalledWith(userStub.user2.id, { withDeleted: false });
-      expect(notificationMock.renderEmail).toHaveBeenCalledOnce();
-    });
-
     it('should skip recipient that could not be looked up', async () => {
       albumMock.getById.mockResolvedValue({
         ...albumStub.emptyWithValidThumbnail,
@@ -544,7 +531,7 @@ describe(NotificationService.name, () => {
       userMock.get.mockResolvedValueOnce(userStub.user1);
       notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
 
-      await sut.handleAlbumUpdate({ id: '', senderId: '' });
+      await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] });
       expect(userMock.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false });
       expect(notificationMock.renderEmail).not.toHaveBeenCalled();
     });
@@ -567,7 +554,7 @@ describe(NotificationService.name, () => {
       });
       notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
 
-      await sut.handleAlbumUpdate({ id: '', senderId: '' });
+      await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] });
       expect(userMock.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false });
       expect(notificationMock.renderEmail).not.toHaveBeenCalled();
     });
@@ -590,7 +577,7 @@ describe(NotificationService.name, () => {
       });
       notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
 
-      await sut.handleAlbumUpdate({ id: '', senderId: '' });
+      await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] });
       expect(userMock.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false });
       expect(notificationMock.renderEmail).not.toHaveBeenCalled();
     });
@@ -603,11 +590,24 @@ describe(NotificationService.name, () => {
       userMock.get.mockResolvedValue(userStub.user1);
       notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
 
-      await sut.handleAlbumUpdate({ id: '', senderId: '' });
+      await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] });
       expect(userMock.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false });
       expect(notificationMock.renderEmail).toHaveBeenCalled();
       expect(jobMock.queue).toHaveBeenCalled();
     });
+
+    it('should add new recipients for new images if job is already queued', async () => {
+      jobMock.removeJob.mockResolvedValue({ id: '1', recipientIds: ['2', '3', '4'] } as INotifyAlbumUpdateJob);
+      await sut.onAlbumUpdate({ id: '1', recipientIds: ['1', '2', '3'] } as INotifyAlbumUpdateJob);
+      expect(jobMock.queue).toHaveBeenCalledWith({
+        name: JobName.NOTIFY_ALBUM_UPDATE,
+        data: {
+          id: '1',
+          delay: 300_000,
+          recipientIds: ['1', '2', '3', '4'],
+        },
+      });
+    });
   });
 
   describe('handleSendEmail', () => {
@@ -616,11 +616,6 @@ describe(NotificationService.name, () => {
       await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.SKIPPED);
     });
 
-    it('should fail if email could not be sent', async () => {
-      systemMock.get.mockResolvedValue({ notifications: { smtp: { enabled: true } } });
-      await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.FAILED);
-    });
-
     it('should send mail successfully', async () => {
       systemMock.get.mockResolvedValue({ notifications: { smtp: { enabled: true, from: 'test@immich.app' } } });
       notificationMock.sendEmail.mockResolvedValue({ messageId: '', response: '' });
diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts
index 4eef49c631..37b265c6ae 100644
--- a/server/src/services/notification.service.ts
+++ b/server/src/services/notification.service.ts
@@ -1,49 +1,36 @@
-import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
-import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
-import { SystemConfigCore } from 'src/cores/system-config.core';
-import { OnEmit } from 'src/decorators';
+import { BadRequestException, Injectable } from '@nestjs/common';
+import { OnEvent, OnJob } from 'src/decorators';
 import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
 import { AlbumEntity } from 'src/entities/album.entity';
-import { IAlbumRepository } from 'src/interfaces/album.interface';
-import { IAssetRepository } from 'src/interfaces/asset.interface';
-import { ArgOf, ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
+import { ArgOf } from 'src/interfaces/event.interface';
 import {
-  IEmailJob,
-  IJobRepository,
-  INotifyAlbumInviteJob,
+  IEntityJob,
   INotifyAlbumUpdateJob,
-  INotifySignupJob,
+  JobItem,
   JobName,
+  JobOf,
   JobStatus,
+  QueueName,
 } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { EmailImageAttachment, EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
-import { IUserRepository } from 'src/interfaces/user.interface';
+import { EmailImageAttachment, EmailTemplate } from 'src/interfaces/notification.interface';
+import { BaseService } from 'src/services/base.service';
 import { getAssetFiles } from 'src/utils/asset.util';
 import { getFilenameExtension } from 'src/utils/file';
+import { getExternalDomain } from 'src/utils/misc';
 import { isEqualObject } from 'src/utils/object';
 import { getPreferences } from 'src/utils/preferences';
 
 @Injectable()
-export class NotificationService {
-  private configCore: SystemConfigCore;
+export class NotificationService extends BaseService {
+  private static albumUpdateEmailDelayMs = 300_000;
 
-  constructor(
-    @Inject(IEventRepository) private eventRepository: IEventRepository,
-    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
-    @Inject(INotificationRepository) private notificationRepository: INotificationRepository,
-    @Inject(IUserRepository) private userRepository: IUserRepository,
-    @Inject(IJobRepository) private jobRepository: IJobRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
-    @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
-  ) {
-    this.logger.setContext(NotificationService.name);
-    this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
+  @OnEvent({ name: 'config.update' })
+  onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) {
+    this.eventRepository.clientBroadcast('on_config_update');
+    this.eventRepository.serverSend('config.update', { oldConfig, newConfig });
   }
 
-  @OnEmit({ event: 'config.validate', priority: -100 })
+  @OnEvent({ name: 'config.validate', priority: -100 })
   async onConfigValidate({ oldConfig, newConfig }: ArgOf<'config.validate'>) {
     try {
       if (
@@ -58,80 +45,102 @@ export class NotificationService {
     }
   }
 
-  @OnEmit({ event: 'asset.hide' })
+  @OnEvent({ name: 'asset.hide' })
   onAssetHide({ assetId, userId }: ArgOf<'asset.hide'>) {
-    this.eventRepository.clientSend(ClientEvent.ASSET_HIDDEN, userId, assetId);
+    this.eventRepository.clientSend('on_asset_hidden', userId, assetId);
   }
 
-  @OnEmit({ event: 'asset.show' })
+  @OnEvent({ name: 'asset.show' })
   async onAssetShow({ assetId }: ArgOf<'asset.show'>) {
-    await this.jobRepository.queue({ name: JobName.GENERATE_THUMBNAIL, data: { id: assetId, notify: true } });
+    await this.jobRepository.queue({ name: JobName.GENERATE_THUMBNAILS, data: { id: assetId, notify: true } });
   }
 
-  @OnEmit({ event: 'asset.trash' })
+  @OnEvent({ name: 'asset.trash' })
   onAssetTrash({ assetId, userId }: ArgOf<'asset.trash'>) {
-    this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, userId, [assetId]);
+    this.eventRepository.clientSend('on_asset_trash', userId, [assetId]);
   }
 
-  @OnEmit({ event: 'asset.delete' })
+  @OnEvent({ name: 'asset.delete' })
   onAssetDelete({ assetId, userId }: ArgOf<'asset.delete'>) {
-    this.eventRepository.clientSend(ClientEvent.ASSET_DELETE, userId, assetId);
+    this.eventRepository.clientSend('on_asset_delete', userId, assetId);
   }
 
-  @OnEmit({ event: 'assets.trash' })
+  @OnEvent({ name: 'assets.trash' })
   onAssetsTrash({ assetIds, userId }: ArgOf<'assets.trash'>) {
-    this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, userId, assetIds);
+    this.eventRepository.clientSend('on_asset_trash', userId, assetIds);
   }
 
-  @OnEmit({ event: 'assets.restore' })
+  @OnEvent({ name: 'assets.restore' })
   onAssetsRestore({ assetIds, userId }: ArgOf<'assets.restore'>) {
-    this.eventRepository.clientSend(ClientEvent.ASSET_RESTORE, userId, assetIds);
+    this.eventRepository.clientSend('on_asset_restore', userId, assetIds);
   }
 
-  @OnEmit({ event: 'stack.create' })
+  @OnEvent({ name: 'stack.create' })
   onStackCreate({ userId }: ArgOf<'stack.create'>) {
-    this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []);
+    this.eventRepository.clientSend('on_asset_stack_update', userId);
   }
 
-  @OnEmit({ event: 'stack.update' })
+  @OnEvent({ name: 'stack.update' })
   onStackUpdate({ userId }: ArgOf<'stack.update'>) {
-    this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []);
+    this.eventRepository.clientSend('on_asset_stack_update', userId);
   }
 
-  @OnEmit({ event: 'stack.delete' })
+  @OnEvent({ name: 'stack.delete' })
   onStackDelete({ userId }: ArgOf<'stack.delete'>) {
-    this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []);
+    this.eventRepository.clientSend('on_asset_stack_update', userId);
   }
 
-  @OnEmit({ event: 'stacks.delete' })
+  @OnEvent({ name: 'stacks.delete' })
   onStacksDelete({ userId }: ArgOf<'stacks.delete'>) {
-    this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []);
+    this.eventRepository.clientSend('on_asset_stack_update', userId);
   }
 
-  @OnEmit({ event: 'user.signup' })
+  @OnEvent({ name: 'user.signup' })
   async onUserSignup({ notify, id, tempPassword }: ArgOf<'user.signup'>) {
     if (notify) {
       await this.jobRepository.queue({ name: JobName.NOTIFY_SIGNUP, data: { id, tempPassword } });
     }
   }
 
-  @OnEmit({ event: 'album.update' })
-  async onAlbumUpdate({ id, updatedBy }: ArgOf<'album.update'>) {
-    await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_UPDATE, data: { id, senderId: updatedBy } });
+  @OnEvent({ name: 'album.update' })
+  async onAlbumUpdate({ id, recipientIds }: ArgOf<'album.update'>) {
+    // if recipientIds is empty, album likely only has one user part of it, don't queue notification if so
+    if (recipientIds.length === 0) {
+      return;
+    }
+
+    const job: JobItem = {
+      name: JobName.NOTIFY_ALBUM_UPDATE,
+      data: { id, recipientIds, delay: NotificationService.albumUpdateEmailDelayMs },
+    };
+
+    const previousJobData = await this.jobRepository.removeJob(id, JobName.NOTIFY_ALBUM_UPDATE);
+    if (previousJobData && this.isAlbumUpdateJob(previousJobData)) {
+      for (const id of previousJobData.recipientIds) {
+        if (!recipientIds.includes(id)) {
+          recipientIds.push(id);
+        }
+      }
+    }
+    await this.jobRepository.queue(job);
   }
 
-  @OnEmit({ event: 'album.invite' })
+  private isAlbumUpdateJob(job: IEntityJob): job is INotifyAlbumUpdateJob {
+    return 'recipientIds' in job;
+  }
+
+  @OnEvent({ name: 'album.invite' })
   async onAlbumInvite({ id, userId }: ArgOf<'album.invite'>) {
     await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_INVITE, data: { id, recipientId: userId } });
   }
 
-  @OnEmit({ event: 'session.delete' })
+  @OnEvent({ name: 'session.delete' })
   onSessionDelete({ sessionId }: ArgOf<'session.delete'>) {
     // after the response is sent
-    setTimeout(() => this.eventRepository.clientSend(ClientEvent.SESSION_DELETE, sessionId, sessionId), 500);
+    setTimeout(() => this.eventRepository.clientSend('on_session_delete', sessionId, sessionId), 500);
   }
 
-  async sendTestEmail(id: string, dto: SystemConfigSmtpDto) {
+  async sendTestEmail(id: string, dto: SystemConfigSmtpDto, tempTemplate?: string) {
     const user = await this.userRepository.get(id, { withDeleted: false });
     if (!user) {
       throw new Error('User not found');
@@ -140,19 +149,20 @@ export class NotificationService {
     try {
       await this.notificationRepository.verifySmtp(dto.transport);
     } catch (error) {
-      throw new HttpException('Failed to verify SMTP configuration', HttpStatus.BAD_REQUEST, { cause: error });
+      throw new BadRequestException('Failed to verify SMTP configuration', { cause: error });
     }
 
-    const { server } = await this.configCore.getConfig({ withCache: false });
+    const { server } = await this.getConfig({ withCache: false });
+    const { port } = this.configRepository.getEnv();
     const { html, text } = await this.notificationRepository.renderEmail({
       template: EmailTemplate.TEST_EMAIL,
       data: {
-        baseUrl: server.externalDomain || DEFAULT_EXTERNAL_DOMAIN,
+        baseUrl: getExternalDomain(server, port),
         displayName: user.name,
       },
+      customTemplate: tempTemplate!,
     });
-
-    await this.notificationRepository.sendEmail({
+    const { messageId } = await this.notificationRepository.sendEmail({
       to: user.email,
       subject: 'Test email from Immich',
       html,
@@ -161,23 +171,91 @@ export class NotificationService {
       replyTo: dto.replyTo || dto.from,
       smtp: dto.transport,
     });
+
+    return { messageId };
   }
 
-  async handleUserSignup({ id, tempPassword }: INotifySignupJob) {
+  async getTemplate(name: EmailTemplate, customTemplate: string) {
+    const { server, templates } = await this.getConfig({ withCache: false });
+    const { port } = this.configRepository.getEnv();
+
+    let templateResponse = '';
+
+    switch (name) {
+      case EmailTemplate.WELCOME: {
+        const { html: _welcomeHtml } = await this.notificationRepository.renderEmail({
+          template: EmailTemplate.WELCOME,
+          data: {
+            baseUrl: getExternalDomain(server, port),
+            displayName: 'John Doe',
+            username: 'john@doe.com',
+            password: 'thisIsAPassword123',
+          },
+          customTemplate: customTemplate || templates.email.welcomeTemplate,
+        });
+
+        templateResponse = _welcomeHtml;
+        break;
+      }
+      case EmailTemplate.ALBUM_UPDATE: {
+        const { html: _updateAlbumHtml } = await this.notificationRepository.renderEmail({
+          template: EmailTemplate.ALBUM_UPDATE,
+          data: {
+            baseUrl: getExternalDomain(server, port),
+            albumId: '1',
+            albumName: 'Favorite Photos',
+            recipientName: 'Jane Doe',
+            cid: undefined,
+          },
+          customTemplate: customTemplate || templates.email.albumInviteTemplate,
+        });
+        templateResponse = _updateAlbumHtml;
+        break;
+      }
+
+      case EmailTemplate.ALBUM_INVITE: {
+        const { html } = await this.notificationRepository.renderEmail({
+          template: EmailTemplate.ALBUM_INVITE,
+          data: {
+            baseUrl: getExternalDomain(server, port),
+            albumId: '1',
+            albumName: "John Doe's Favorites",
+            senderName: 'John Doe',
+            recipientName: 'Jane Doe',
+            cid: undefined,
+          },
+          customTemplate: customTemplate || templates.email.albumInviteTemplate,
+        });
+        templateResponse = html;
+        break;
+      }
+      default: {
+        templateResponse = '';
+        break;
+      }
+    }
+
+    return { name, html: templateResponse };
+  }
+
+  @OnJob({ name: JobName.NOTIFY_SIGNUP, queue: QueueName.NOTIFICATION })
+  async handleUserSignup({ id, tempPassword }: JobOf<JobName.NOTIFY_SIGNUP>) {
     const user = await this.userRepository.get(id, { withDeleted: false });
     if (!user) {
       return JobStatus.SKIPPED;
     }
 
-    const { server } = await this.configCore.getConfig({ withCache: true });
+    const { server, templates } = await this.getConfig({ withCache: true });
+    const { port } = this.configRepository.getEnv();
     const { html, text } = await this.notificationRepository.renderEmail({
       template: EmailTemplate.WELCOME,
       data: {
-        baseUrl: server.externalDomain || DEFAULT_EXTERNAL_DOMAIN,
+        baseUrl: getExternalDomain(server, port),
         displayName: user.name,
         username: user.email,
         password: tempPassword,
       },
+      customTemplate: templates.email.welcomeTemplate,
     });
 
     await this.jobRepository.queue({
@@ -193,7 +271,8 @@ export class NotificationService {
     return JobStatus.SUCCESS;
   }
 
-  async handleAlbumInvite({ id, recipientId }: INotifyAlbumInviteJob) {
+  @OnJob({ name: JobName.NOTIFY_ALBUM_INVITE, queue: QueueName.NOTIFICATION })
+  async handleAlbumInvite({ id, recipientId }: JobOf<JobName.NOTIFY_ALBUM_INVITE>) {
     const album = await this.albumRepository.getById(id, { withAssets: false });
     if (!album) {
       return JobStatus.SKIPPED;
@@ -212,17 +291,19 @@ export class NotificationService {
 
     const attachment = await this.getAlbumThumbnailAttachment(album);
 
-    const { server } = await this.configCore.getConfig({ withCache: false });
+    const { server, templates } = await this.getConfig({ withCache: false });
+    const { port } = this.configRepository.getEnv();
     const { html, text } = await this.notificationRepository.renderEmail({
       template: EmailTemplate.ALBUM_INVITE,
       data: {
-        baseUrl: server.externalDomain || DEFAULT_EXTERNAL_DOMAIN,
+        baseUrl: getExternalDomain(server, port),
         albumId: album.id,
         albumName: album.albumName,
         senderName: album.owner.name,
         recipientName: recipient.name,
         cid: attachment ? attachment.cid : undefined,
       },
+      customTemplate: templates.email.albumInviteTemplate,
     });
 
     await this.jobRepository.queue({
@@ -239,7 +320,8 @@ export class NotificationService {
     return JobStatus.SUCCESS;
   }
 
-  async handleAlbumUpdate({ id, senderId }: INotifyAlbumUpdateJob) {
+  @OnJob({ name: JobName.NOTIFY_ALBUM_UPDATE, queue: QueueName.NOTIFICATION })
+  async handleAlbumUpdate({ id, recipientIds }: JobOf<JobName.NOTIFY_ALBUM_UPDATE>) {
     const album = await this.albumRepository.getById(id, { withAssets: false });
 
     if (!album) {
@@ -251,10 +333,13 @@ export class NotificationService {
       return JobStatus.SKIPPED;
     }
 
-    const recipients = [...album.albumUsers.map((user) => user.user), owner].filter((user) => user.id !== senderId);
+    const recipients = [...album.albumUsers.map((user) => user.user), owner].filter((user) =>
+      recipientIds.includes(user.id),
+    );
     const attachment = await this.getAlbumThumbnailAttachment(album);
 
-    const { server } = await this.configCore.getConfig({ withCache: false });
+    const { server, templates } = await this.getConfig({ withCache: false });
+    const { port } = this.configRepository.getEnv();
 
     for (const recipient of recipients) {
       const user = await this.userRepository.get(recipient.id, { withDeleted: false });
@@ -271,12 +356,13 @@ export class NotificationService {
       const { html, text } = await this.notificationRepository.renderEmail({
         template: EmailTemplate.ALBUM_UPDATE,
         data: {
-          baseUrl: server.externalDomain || DEFAULT_EXTERNAL_DOMAIN,
+          baseUrl: getExternalDomain(server, port),
           albumId: album.id,
           albumName: album.albumName,
           recipientName: recipient.name,
           cid: attachment ? attachment.cid : undefined,
         },
+        customTemplate: templates.email.albumUpdateTemplate,
       });
 
       await this.jobRepository.queue({
@@ -294,8 +380,9 @@ export class NotificationService {
     return JobStatus.SUCCESS;
   }
 
-  async handleSendEmail(data: IEmailJob): Promise<JobStatus> {
-    const { notifications } = await this.configCore.getConfig({ withCache: false });
+  @OnJob({ name: JobName.SEND_EMAIL, queue: QueueName.NOTIFICATION })
+  async handleSendEmail(data: JobOf<JobName.SEND_EMAIL>): Promise<JobStatus> {
+    const { notifications } = await this.getConfig({ withCache: false });
     if (!notifications.smtp.enabled) {
       return JobStatus.SKIPPED;
     }
@@ -312,10 +399,6 @@ export class NotificationService {
       imageAttachments: data.imageAttachments,
     });
 
-    if (!response) {
-      return JobStatus.FAILED;
-    }
-
     this.logger.log(`Sent mail with id: ${response.messageId} status: ${response.response}`);
 
     return JobStatus.SUCCESS;
diff --git a/server/src/services/partner.service.spec.ts b/server/src/services/partner.service.spec.ts
index b2b3401251..2e11c4f9ad 100644
--- a/server/src/services/partner.service.spec.ts
+++ b/server/src/services/partner.service.spec.ts
@@ -1,20 +1,20 @@
 import { BadRequestException } from '@nestjs/common';
-import { IAccessRepository } from 'src/interfaces/access.interface';
 import { IPartnerRepository, PartnerDirection } from 'src/interfaces/partner.interface';
 import { PartnerService } from 'src/services/partner.service';
 import { authStub } from 'test/fixtures/auth.stub';
 import { partnerStub } from 'test/fixtures/partner.stub';
-import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
+import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(PartnerService.name, () => {
   let sut: PartnerService;
+
+  let accessMock: IAccessRepositoryMock;
   let partnerMock: Mocked<IPartnerRepository>;
-  let accessMock: Mocked<IAccessRepository>;
 
   beforeEach(() => {
-    partnerMock = newPartnerRepositoryMock();
-    sut = new PartnerService(partnerMock, accessMock);
+    ({ sut, accessMock, partnerMock } = newTestService(PartnerService));
   });
 
   it('should work', () => {
@@ -74,4 +74,24 @@ describe(PartnerService.name, () => {
       expect(partnerMock.remove).not.toHaveBeenCalled();
     });
   });
+
+  describe('update', () => {
+    it('should require access', async () => {
+      await expect(sut.update(authStub.admin, 'shared-by-id', { inTimeline: false })).rejects.toBeInstanceOf(
+        BadRequestException,
+      );
+    });
+
+    it('should update partner', async () => {
+      accessMock.partner.checkUpdateAccess.mockResolvedValue(new Set(['shared-by-id']));
+      partnerMock.update.mockResolvedValue(partnerStub.adminToUser1);
+
+      await expect(sut.update(authStub.admin, 'shared-by-id', { inTimeline: true })).resolves.toBeDefined();
+      expect(partnerMock.update).toHaveBeenCalledWith({
+        sharedById: 'shared-by-id',
+        sharedWithId: authStub.admin.user.id,
+        inTimeline: true,
+      });
+    });
+  });
 });
diff --git a/server/src/services/partner.service.ts b/server/src/services/partner.service.ts
index 4b7cd4c516..ee36f1ce45 100644
--- a/server/src/services/partner.service.ts
+++ b/server/src/services/partner.service.ts
@@ -1,43 +1,37 @@
-import { BadRequestException, Inject, Injectable } from '@nestjs/common';
+import { BadRequestException, Injectable } from '@nestjs/common';
 import { AuthDto } from 'src/dtos/auth.dto';
 import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
 import { mapUser } from 'src/dtos/user.dto';
 import { PartnerEntity } from 'src/entities/partner.entity';
 import { Permission } from 'src/enum';
-import { IAccessRepository } from 'src/interfaces/access.interface';
-import { IPartnerRepository, PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface';
-import { requireAccess } from 'src/utils/access';
+import { PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface';
+import { BaseService } from 'src/services/base.service';
 
 @Injectable()
-export class PartnerService {
-  constructor(
-    @Inject(IPartnerRepository) private repository: IPartnerRepository,
-    @Inject(IAccessRepository) private access: IAccessRepository,
-  ) {}
-
+export class PartnerService extends BaseService {
   async create(auth: AuthDto, sharedWithId: string): Promise<PartnerResponseDto> {
     const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId };
-    const exists = await this.repository.get(partnerId);
+    const exists = await this.partnerRepository.get(partnerId);
     if (exists) {
       throw new BadRequestException(`Partner already exists`);
     }
 
-    const partner = await this.repository.create(partnerId);
+    const partner = await this.partnerRepository.create(partnerId);
     return this.mapPartner(partner, PartnerDirection.SharedBy);
   }
 
   async remove(auth: AuthDto, sharedWithId: string): Promise<void> {
     const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId };
-    const partner = await this.repository.get(partnerId);
+    const partner = await this.partnerRepository.get(partnerId);
     if (!partner) {
       throw new BadRequestException('Partner not found');
     }
 
-    await this.repository.remove(partner);
+    await this.partnerRepository.remove(partner);
   }
 
   async search(auth: AuthDto, { direction }: PartnerSearchDto): Promise<PartnerResponseDto[]> {
-    const partners = await this.repository.getAll(auth.user.id);
+    const partners = await this.partnerRepository.getAll(auth.user.id);
     const key = direction === PartnerDirection.SharedBy ? 'sharedById' : 'sharedWithId';
     return partners
       .filter((partner) => partner.sharedBy && partner.sharedWith) // Filter out soft deleted users
@@ -46,10 +40,10 @@ export class PartnerService {
   }
 
   async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise<PartnerResponseDto> {
-    await requireAccess(this.access, { auth, permission: Permission.PARTNER_UPDATE, ids: [sharedById] });
+    await this.requireAccess({ auth, permission: Permission.PARTNER_UPDATE, ids: [sharedById] });
     const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id };
 
-    const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline });
+    const entity = await this.partnerRepository.update({ ...partnerId, inTimeline: dto.inTimeline });
     return this.mapPartner(entity, PartnerDirection.SharedWith);
   }
 
diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts
index 2b111706f1..3b749c0ab6 100644
--- a/server/src/services/person.service.spec.ts
+++ b/server/src/services/person.service.spec.ts
@@ -1,39 +1,26 @@
 import { BadRequestException, NotFoundException } from '@nestjs/common';
-import { Colorspace } from 'src/config';
 import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
 import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto';
 import { AssetFaceEntity } from 'src/entities/asset-face.entity';
-import { SourceType, SystemMetadataKey } from 'src/enum';
+import { CacheControl, Colorspace, ImageFormat, SourceType, SystemMetadataKey } from 'src/enum';
 import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
 import { ICryptoRepository } from 'src/interfaces/crypto.interface';
 import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { DetectedFaces, IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
 import { IMediaRepository } from 'src/interfaces/media.interface';
-import { IMoveRepository } from 'src/interfaces/move.interface';
 import { IPersonRepository } from 'src/interfaces/person.interface';
 import { FaceSearchResult, ISearchRepository } from 'src/interfaces/search.interface';
 import { IStorageRepository } from 'src/interfaces/storage.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { PersonService } from 'src/services/person.service';
-import { CacheControl, ImmichFileResponse } from 'src/utils/file';
+import { ImmichFileResponse } from 'src/utils/file';
 import { assetStub } from 'test/fixtures/asset.stub';
 import { authStub } from 'test/fixtures/auth.stub';
 import { faceStub } from 'test/fixtures/face.stub';
 import { personStub } from 'test/fixtures/person.stub';
 import { systemConfigStub } from 'test/fixtures/system-config.stub';
-import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
-import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
-import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
-import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock';
-import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock';
-import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock';
-import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
-import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock';
-import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
+import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
+import { newTestService } from 'test/utils';
 import { IsNull } from 'typeorm';
 import { Mocked } from 'vitest';
 
@@ -48,65 +35,63 @@ const responseDto: PersonResponseDto = {
 
 const statistics = { assets: 3 };
 
+const faceId = 'face-id';
+const face = {
+  id: faceId,
+  assetId: 'asset-id',
+  boundingBoxX1: 100,
+  boundingBoxY1: 100,
+  boundingBoxX2: 200,
+  boundingBoxY2: 200,
+  imageHeight: 500,
+  imageWidth: 400,
+};
+const faceSearch = { faceId, embedding: [1, 2, 3, 4] };
 const detectFaceMock: DetectedFaces = {
   faces: [
     {
       boundingBox: {
-        x1: 100,
-        y1: 100,
-        x2: 200,
-        y2: 200,
+        x1: face.boundingBoxX1,
+        y1: face.boundingBoxY1,
+        x2: face.boundingBoxX2,
+        y2: face.boundingBoxY2,
       },
-      embedding: [1, 2, 3, 4],
+      embedding: faceSearch.embedding,
       score: 0.2,
     },
   ],
-  imageHeight: 500,
-  imageWidth: 400,
+  imageHeight: face.imageHeight,
+  imageWidth: face.imageWidth,
 };
 
 describe(PersonService.name, () => {
+  let sut: PersonService;
+
   let accessMock: IAccessRepositoryMock;
   let assetMock: Mocked<IAssetRepository>;
-  let systemMock: Mocked<ISystemMetadataRepository>;
+  let cryptoMock: Mocked<ICryptoRepository>;
   let jobMock: Mocked<IJobRepository>;
   let machineLearningMock: Mocked<IMachineLearningRepository>;
   let mediaMock: Mocked<IMediaRepository>;
-  let moveMock: Mocked<IMoveRepository>;
   let personMock: Mocked<IPersonRepository>;
-  let storageMock: Mocked<IStorageRepository>;
   let searchMock: Mocked<ISearchRepository>;
-  let cryptoMock: Mocked<ICryptoRepository>;
-  let loggerMock: Mocked<ILoggerRepository>;
-  let sut: PersonService;
+  let storageMock: Mocked<IStorageRepository>;
+  let systemMock: Mocked<ISystemMetadataRepository>;
 
   beforeEach(() => {
-    accessMock = newAccessRepositoryMock();
-    assetMock = newAssetRepositoryMock();
-    systemMock = newSystemMetadataRepositoryMock();
-    jobMock = newJobRepositoryMock();
-    machineLearningMock = newMachineLearningRepositoryMock();
-    moveMock = newMoveRepositoryMock();
-    mediaMock = newMediaRepositoryMock();
-    personMock = newPersonRepositoryMock();
-    storageMock = newStorageRepositoryMock();
-    searchMock = newSearchRepositoryMock();
-    cryptoMock = newCryptoRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-    sut = new PersonService(
+    ({
+      sut,
       accessMock,
       assetMock,
+      cryptoMock,
+      jobMock,
       machineLearningMock,
-      moveMock,
       mediaMock,
       personMock,
-      systemMock,
-      storageMock,
-      jobMock,
       searchMock,
-      cryptoMock,
-      loggerMock,
-    );
+      storageMock,
+      systemMock,
+    } = newTestService(PersonService));
   });
 
   it('should be defined', () => {
@@ -204,23 +189,6 @@ describe(PersonService.name, () => {
     });
   });
 
-  describe('getAssets', () => {
-    it('should require person.read permission', async () => {
-      personMock.getAssets.mockResolvedValue([assetStub.image, assetStub.video]);
-      await expect(sut.getAssets(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
-      expect(personMock.getAssets).not.toHaveBeenCalled();
-      expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
-    });
-
-    it("should return a person's assets", async () => {
-      personMock.getAssets.mockResolvedValue([assetStub.image, assetStub.video]);
-      accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
-      await sut.getAssets(authStub.admin, 'person-1');
-      expect(personMock.getAssets).toHaveBeenCalledWith('person-1');
-      expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
-    });
-  });
-
   describe('update', () => {
     it('should require person.write permission', async () => {
       personMock.getById.mockResolvedValue(personStub.noName);
@@ -242,7 +210,6 @@ describe(PersonService.name, () => {
 
     it("should update a person's name", async () => {
       personMock.update.mockResolvedValue(personStub.withName);
-      personMock.getAssets.mockResolvedValue([assetStub.image]);
       accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
 
       await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).resolves.toEqual(responseDto);
@@ -253,7 +220,6 @@ describe(PersonService.name, () => {
 
     it("should update a person's date of birth", async () => {
       personMock.update.mockResolvedValue(personStub.withBirthDate);
-      personMock.getAssets.mockResolvedValue([assetStub.image]);
       accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
 
       await expect(sut.update(authStub.admin, 'person-1', { birthDate: '1976-06-30' })).resolves.toEqual({
@@ -272,7 +238,6 @@ describe(PersonService.name, () => {
 
     it('should update a person visibility', async () => {
       personMock.update.mockResolvedValue(personStub.withName);
-      personMock.getAssets.mockResolvedValue([assetStub.image]);
       accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
 
       await expect(sut.update(authStub.admin, 'person-1', { isHidden: false })).resolves.toEqual(responseDto);
@@ -476,7 +441,7 @@ describe(PersonService.name, () => {
         hasNextPage: false,
       });
 
-      await sut.handleQueueDetectFaces({});
+      await sut.handleQueueDetectFaces({ force: false });
 
       expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.FACES);
       expect(jobMock.queueAll).toHaveBeenCalledWith([
@@ -492,14 +457,13 @@ describe(PersonService.name, () => {
         items: [assetStub.image],
         hasNextPage: false,
       });
-      personMock.getAll.mockResolvedValue({
-        items: [personStub.withName],
-        hasNextPage: false,
-      });
-      personMock.getAllWithoutFaces.mockResolvedValue([]);
+      personMock.getAllWithoutFaces.mockResolvedValue([personStub.withName]);
 
       await sut.handleQueueDetectFaces({ force: true });
 
+      expect(personMock.deleteFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING });
+      expect(personMock.delete).toHaveBeenCalledWith([personStub.withName]);
+      expect(storageMock.unlink).toHaveBeenCalledWith(personStub.withName.thumbnailPath);
       expect(assetMock.getAll).toHaveBeenCalled();
       expect(jobMock.queueAll).toHaveBeenCalledWith([
         {
@@ -509,6 +473,27 @@ describe(PersonService.name, () => {
       ]);
     });
 
+    it('should refresh all assets', async () => {
+      assetMock.getAll.mockResolvedValue({
+        items: [assetStub.image],
+        hasNextPage: false,
+      });
+
+      await sut.handleQueueDetectFaces({ force: undefined });
+
+      expect(personMock.delete).not.toHaveBeenCalled();
+      expect(personMock.deleteFaces).not.toHaveBeenCalled();
+      expect(storageMock.unlink).not.toHaveBeenCalled();
+      expect(assetMock.getAll).toHaveBeenCalled();
+      expect(jobMock.queueAll).toHaveBeenCalledWith([
+        {
+          name: JobName.FACE_DETECTION,
+          data: { id: assetStub.image.id },
+        },
+      ]);
+      expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.PERSON_CLEANUP });
+    });
+
     it('should delete existing people and faces if forced', async () => {
       personMock.getAll.mockResolvedValue({
         items: [faceStub.face1.person, personStub.randomPerson],
@@ -569,7 +554,7 @@ describe(PersonService.name, () => {
 
       expect(personMock.getAllFaces).toHaveBeenCalledWith(
         { skip: 0, take: 1000 },
-        { where: { personId: IsNull(), sourceType: IsNull() } },
+        { where: { personId: IsNull(), sourceType: SourceType.MACHINE_LEARNING } },
       );
       expect(jobMock.queueAll).toHaveBeenCalledWith([
         {
@@ -661,7 +646,7 @@ describe(PersonService.name, () => {
       expect(systemMock.set).not.toHaveBeenCalled();
     });
 
-    it('should delete existing people and faces if forced', async () => {
+    it('should delete existing people if forced', async () => {
       jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 });
       personMock.getAll.mockResolvedValue({
         items: [faceStub.face1.person, personStub.randomPerson],
@@ -676,7 +661,8 @@ describe(PersonService.name, () => {
 
       await sut.handleQueueRecognizeFaces({ force: true });
 
-      expect(personMock.deleteAllFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING });
+      expect(personMock.deleteFaces).not.toHaveBeenCalled();
+      expect(personMock.unassignFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING });
       expect(jobMock.queueAll).toHaveBeenCalledWith([
         {
           name: JobName.FACIAL_RECOGNITION,
@@ -689,6 +675,10 @@ describe(PersonService.name, () => {
   });
 
   describe('handleDetectFaces', () => {
+    beforeEach(() => {
+      cryptoMock.randomUUID.mockReturnValue(faceId);
+    });
+
     it('should skip if machine learning is disabled', async () => {
       systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
 
@@ -727,11 +717,10 @@ describe(PersonService.name, () => {
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
       await sut.handleDetectFaces({ id: assetStub.image.id });
       expect(machineLearningMock.detectFaces).toHaveBeenCalledWith(
-        'http://immich-machine-learning:3003',
+        ['http://immich-machine-learning:3003'],
         '/uploads/user-id/thumbs/path.jpg',
         expect.objectContaining({ minScore: 0.7, modelName: 'buffalo_l' }),
       );
-      expect(personMock.createFaces).not.toHaveBeenCalled();
       expect(jobMock.queue).not.toHaveBeenCalled();
       expect(jobMock.queueAll).not.toHaveBeenCalled();
 
@@ -743,29 +732,73 @@ describe(PersonService.name, () => {
     });
 
     it('should create a face with no person and queue recognition job', async () => {
-      personMock.createFaces.mockResolvedValue([faceStub.face1.id]);
       machineLearningMock.detectFaces.mockResolvedValue(detectFaceMock);
-      searchMock.searchFaces.mockResolvedValue([{ face: faceStub.face1, distance: 0.7 }]);
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      const faceId = 'face-id';
-      cryptoMock.randomUUID.mockReturnValue(faceId);
-      const face = {
-        id: faceId,
-        assetId: 'asset-id',
-        boundingBoxX1: 100,
-        boundingBoxY1: 100,
-        boundingBoxX2: 200,
-        boundingBoxY2: 200,
-        imageHeight: 500,
-        imageWidth: 400,
-        faceSearch: { faceId, embedding: [1, 2, 3, 4] },
-      };
 
       await sut.handleDetectFaces({ id: assetStub.image.id });
 
-      expect(personMock.createFaces).toHaveBeenCalledWith([face]);
+      expect(personMock.refreshFaces).toHaveBeenCalledWith([face], [], [faceSearch]);
       expect(jobMock.queueAll).toHaveBeenCalledWith([
-        { name: JobName.FACIAL_RECOGNITION, data: { id: faceStub.face1.id } },
+        { name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } },
+        { name: JobName.FACIAL_RECOGNITION, data: { id: faceId } },
+      ]);
+      expect(personMock.reassignFace).not.toHaveBeenCalled();
+      expect(personMock.reassignFaces).not.toHaveBeenCalled();
+    });
+
+    it('should delete an existing face not among the new detected faces', async () => {
+      machineLearningMock.detectFaces.mockResolvedValue({ faces: [], imageHeight: 500, imageWidth: 400 });
+      assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, faces: [faceStub.primaryFace1] }]);
+
+      await sut.handleDetectFaces({ id: assetStub.image.id });
+
+      expect(personMock.refreshFaces).toHaveBeenCalledWith([], [faceStub.primaryFace1.id], []);
+      expect(jobMock.queueAll).not.toHaveBeenCalled();
+      expect(personMock.reassignFace).not.toHaveBeenCalled();
+      expect(personMock.reassignFaces).not.toHaveBeenCalled();
+    });
+
+    it('should add new face and delete an existing face not among the new detected faces', async () => {
+      machineLearningMock.detectFaces.mockResolvedValue(detectFaceMock);
+      assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, faces: [faceStub.primaryFace1] }]);
+
+      await sut.handleDetectFaces({ id: assetStub.image.id });
+
+      expect(personMock.refreshFaces).toHaveBeenCalledWith([face], [faceStub.primaryFace1.id], [faceSearch]);
+      expect(jobMock.queueAll).toHaveBeenCalledWith([
+        { name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } },
+        { name: JobName.FACIAL_RECOGNITION, data: { id: faceId } },
+      ]);
+      expect(personMock.reassignFace).not.toHaveBeenCalled();
+      expect(personMock.reassignFaces).not.toHaveBeenCalled();
+    });
+
+    it('should add embedding to matching metadata face', async () => {
+      machineLearningMock.detectFaces.mockResolvedValue(detectFaceMock);
+      assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, faces: [faceStub.fromExif1] }]);
+
+      await sut.handleDetectFaces({ id: assetStub.image.id });
+
+      expect(personMock.refreshFaces).toHaveBeenCalledWith(
+        [],
+        [],
+        [{ faceId: faceStub.fromExif1.id, embedding: faceSearch.embedding }],
+      );
+      expect(jobMock.queueAll).not.toHaveBeenCalled();
+      expect(personMock.reassignFace).not.toHaveBeenCalled();
+      expect(personMock.reassignFaces).not.toHaveBeenCalled();
+    });
+
+    it('should not add embedding to non-matching metadata face', async () => {
+      machineLearningMock.detectFaces.mockResolvedValue(detectFaceMock);
+      assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, faces: [faceStub.fromExif2] }]);
+
+      await sut.handleDetectFaces({ id: assetStub.image.id });
+
+      expect(personMock.refreshFaces).toHaveBeenCalledWith([face], [], [faceSearch]);
+      expect(jobMock.queueAll).toHaveBeenCalledWith([
+        { name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } },
+        { name: JobName.FACIAL_RECOGNITION, data: { id: faceId } },
       ]);
       expect(personMock.reassignFace).not.toHaveBeenCalled();
       expect(personMock.reassignFaces).not.toHaveBeenCalled();
@@ -780,7 +813,6 @@ describe(PersonService.name, () => {
 
       expect(personMock.reassignFaces).not.toHaveBeenCalled();
       expect(personMock.create).not.toHaveBeenCalled();
-      expect(personMock.createFaces).not.toHaveBeenCalled();
     });
 
     it('should fail if face does not have asset', async () => {
@@ -791,7 +823,6 @@ describe(PersonService.name, () => {
 
       expect(personMock.reassignFaces).not.toHaveBeenCalled();
       expect(personMock.create).not.toHaveBeenCalled();
-      expect(personMock.createFaces).not.toHaveBeenCalled();
     });
 
     it('should skip if face already has an assigned person', async () => {
@@ -801,7 +832,6 @@ describe(PersonService.name, () => {
 
       expect(personMock.reassignFaces).not.toHaveBeenCalled();
       expect(personMock.create).not.toHaveBeenCalled();
-      expect(personMock.createFaces).not.toHaveBeenCalled();
     });
 
     it('should match existing person', async () => {
@@ -962,12 +992,11 @@ describe(PersonService.name, () => {
       expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/admin_id/pe/rs');
       expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
         assetStub.primaryImage.originalPath,
-        'upload/thumbs/admin_id/pe/rs/person-1.jpeg',
         {
-          format: 'jpeg',
+          colorspace: Colorspace.P3,
+          format: ImageFormat.JPEG,
           size: 250,
           quality: 80,
-          colorspace: Colorspace.P3,
           crop: {
             left: 238,
             top: 163,
@@ -976,6 +1005,7 @@ describe(PersonService.name, () => {
           },
           processInvalidImages: false,
         },
+        'upload/thumbs/admin_id/pe/rs/person-1.jpeg',
       );
       expect(personMock.update).toHaveBeenCalledWith({
         id: 'person-1',
@@ -991,13 +1021,12 @@ describe(PersonService.name, () => {
       await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id });
 
       expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
-        assetStub.image.originalPath,
-        'upload/thumbs/admin_id/pe/rs/person-1.jpeg',
+        assetStub.primaryImage.originalPath,
         {
-          format: 'jpeg',
+          colorspace: Colorspace.P3,
+          format: ImageFormat.JPEG,
           size: 250,
           quality: 80,
-          colorspace: Colorspace.P3,
           crop: {
             left: 0,
             top: 85,
@@ -1006,6 +1035,7 @@ describe(PersonService.name, () => {
           },
           processInvalidImages: false,
         },
+        'upload/thumbs/admin_id/pe/rs/person-1.jpeg',
       );
     });
 
@@ -1018,12 +1048,11 @@ describe(PersonService.name, () => {
 
       expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
         assetStub.primaryImage.originalPath,
-        'upload/thumbs/admin_id/pe/rs/person-1.jpeg',
         {
-          format: 'jpeg',
+          colorspace: Colorspace.P3,
+          format: ImageFormat.JPEG,
           size: 250,
           quality: 80,
-          colorspace: Colorspace.P3,
           crop: {
             left: 591,
             top: 591,
@@ -1032,33 +1061,7 @@ describe(PersonService.name, () => {
           },
           processInvalidImages: false,
         },
-      );
-    });
-
-    it('should use preview path for videos', async () => {
-      personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.end.assetId });
-      personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.end);
-      assetMock.getById.mockResolvedValue(assetStub.video);
-      mediaMock.getImageDimensions.mockResolvedValue({ width: 2560, height: 1440 });
-
-      await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id });
-
-      expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
-        '/uploads/user-id/thumbs/path.jpg',
         'upload/thumbs/admin_id/pe/rs/person-1.jpeg',
-        {
-          format: 'jpeg',
-          size: 250,
-          quality: 80,
-          colorspace: Colorspace.P3,
-          crop: {
-            left: 1741,
-            top: 851,
-            width: 588,
-            height: 588,
-          },
-          processInvalidImages: false,
-        },
       );
     });
   });
diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts
index dd4a4cecf2..bdec6f88e8 100644
--- a/server/src/services/person.service.ts
+++ b/server/src/services/person.service.ts
@@ -1,10 +1,8 @@
-import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
-import { ImageFormat } from 'src/config';
+import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
 import { FACE_THUMBNAIL_SIZE } from 'src/constants';
 import { StorageCore } from 'src/cores/storage.core';
-import { SystemConfigCore } from 'src/cores/system-config.core';
+import { OnJob } from 'src/decorators';
 import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
-import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
 import {
   AssetFaceResponseDto,
@@ -23,85 +21,61 @@ import {
 } from 'src/dtos/person.dto';
 import { AssetFaceEntity } from 'src/entities/asset-face.entity';
 import { AssetEntity } from 'src/entities/asset.entity';
-import { PersonPathType } from 'src/entities/move.entity';
+import { FaceSearchEntity } from 'src/entities/face-search.entity';
 import { PersonEntity } from 'src/entities/person.entity';
-import { AssetType, Permission, SourceType, SystemMetadataKey } from 'src/enum';
-import { IAccessRepository } from 'src/interfaces/access.interface';
-import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
 import {
-  IBaseJob,
-  IDeferrableJob,
-  IEntityJob,
-  IJobRepository,
-  INightlyJob,
+  AssetType,
+  CacheControl,
+  ImageFormat,
+  Permission,
+  PersonPathType,
+  SourceType,
+  SystemMetadataKey,
+} from 'src/enum';
+import { WithoutProperty } from 'src/interfaces/asset.interface';
+import {
   JOBS_ASSET_PAGINATION_SIZE,
   JobItem,
   JobName,
+  JobOf,
   JobStatus,
   QueueName,
 } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { BoundingBox, IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
-import { CropOptions, IMediaRepository, ImageDimensions, InputDimensions } from 'src/interfaces/media.interface';
-import { IMoveRepository } from 'src/interfaces/move.interface';
-import { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.interface';
-import { ISearchRepository } from 'src/interfaces/search.interface';
-import { IStorageRepository } from 'src/interfaces/storage.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
-import { checkAccess, requireAccess } from 'src/utils/access';
+import { BoundingBox } from 'src/interfaces/machine-learning.interface';
+import { CropOptions, ImageDimensions, InputDimensions } from 'src/interfaces/media.interface';
+import { UpdateFacesData } from 'src/interfaces/person.interface';
+import { BaseService } from 'src/services/base.service';
 import { getAssetFiles } from 'src/utils/asset.util';
-import { CacheControl, ImmichFileResponse } from 'src/utils/file';
+import { ImmichFileResponse } from 'src/utils/file';
 import { mimeTypes } from 'src/utils/mime-types';
 import { isFaceImportEnabled, isFacialRecognitionEnabled } from 'src/utils/misc';
 import { usePagination } from 'src/utils/pagination';
 import { IsNull } from 'typeorm';
 
 @Injectable()
-export class PersonService {
-  private configCore: SystemConfigCore;
-  private storageCore: StorageCore;
-
-  constructor(
-    @Inject(IAccessRepository) private access: IAccessRepository,
-    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
-    @Inject(IMachineLearningRepository) private machineLearningRepository: IMachineLearningRepository,
-    @Inject(IMoveRepository) moveRepository: IMoveRepository,
-    @Inject(IMediaRepository) private mediaRepository: IMediaRepository,
-    @Inject(IPersonRepository) private repository: IPersonRepository,
-    @Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository,
-    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
-    @Inject(IJobRepository) private jobRepository: IJobRepository,
-    @Inject(ISearchRepository) private smartInfoRepository: ISearchRepository,
-    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-  ) {
-    this.logger.setContext(PersonService.name);
-    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
-    this.storageCore = StorageCore.create(
-      assetRepository,
-      cryptoRepository,
-      moveRepository,
-      repository,
-      storageRepository,
-      systemMetadataRepository,
-      this.logger,
-    );
-  }
-
+export class PersonService extends BaseService {
   async getAll(auth: AuthDto, dto: PersonSearchDto): Promise<PeopleResponseDto> {
-    const { withHidden = false, page, size } = dto;
+    const { withHidden = false, closestAssetId, closestPersonId, page, size } = dto;
+    let closestFaceAssetId = closestAssetId;
     const pagination = {
       take: size,
       skip: (page - 1) * size,
     };
 
-    const { machineLearning } = await this.configCore.getConfig({ withCache: false });
-    const { items, hasNextPage } = await this.repository.getAllForUser(pagination, auth.user.id, {
+    if (closestPersonId) {
+      const person = await this.personRepository.getById(closestPersonId);
+      if (!person?.faceAssetId) {
+        throw new NotFoundException('Person not found');
+      }
+      closestFaceAssetId = person.faceAssetId;
+    }
+    const { machineLearning } = await this.getConfig({ withCache: false });
+    const { items, hasNextPage } = await this.personRepository.getAllForUser(pagination, auth.user.id, {
       minimumFaceCount: machineLearning.facialRecognition.minFaces,
       withHidden,
+      closestFaceAssetId,
     });
-    const { total, hidden } = await this.repository.getNumberOfPeople(auth.user.id);
+    const { total, hidden } = await this.personRepository.getNumberOfPeople(auth.user.id);
 
     return {
       people: items.map((person) => mapPerson(person)),
@@ -112,15 +86,15 @@ export class PersonService {
   }
 
   async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> {
-    await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] });
+    await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [personId] });
     const person = await this.findOrFail(personId);
     const result: PersonResponseDto[] = [];
     const changeFeaturePhoto: string[] = [];
     for (const data of dto.data) {
-      const faces = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]);
+      const faces = await this.personRepository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]);
 
       for (const face of faces) {
-        await requireAccess(this.access, { auth, permission: Permission.PERSON_CREATE, ids: [face.id] });
+        await this.requireAccess({ auth, permission: Permission.PERSON_CREATE, ids: [face.id] });
         if (person.faceAssetId === null) {
           changeFeaturePhoto.push(person.id);
         }
@@ -128,7 +102,7 @@ export class PersonService {
           changeFeaturePhoto.push(face.person.id);
         }
 
-        await this.repository.reassignFace(face.id, personId);
+        await this.personRepository.reassignFace(face.id, personId);
       }
 
       result.push(person);
@@ -141,12 +115,12 @@ export class PersonService {
   }
 
   async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> {
-    await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] });
-    await requireAccess(this.access, { auth, permission: Permission.PERSON_CREATE, ids: [dto.id] });
-    const face = await this.repository.getFaceById(dto.id);
+    await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [personId] });
+    await this.requireAccess({ auth, permission: Permission.PERSON_CREATE, ids: [dto.id] });
+    const face = await this.personRepository.getFaceById(dto.id);
     const person = await this.findOrFail(personId);
 
-    await this.repository.reassignFace(face.id, personId);
+    await this.personRepository.reassignFace(face.id, personId);
     if (person.faceAssetId === null) {
       await this.createNewFeaturePhoto([person.id]);
     }
@@ -158,8 +132,8 @@ export class PersonService {
   }
 
   async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
-    await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [dto.id] });
-    const faces = await this.repository.getFaces(dto.id);
+    await this.requireAccess({ auth, permission: Permission.ASSET_READ, ids: [dto.id] });
+    const faces = await this.personRepository.getFaces(dto.id);
     return faces.map((asset) => mapFaces(asset, auth));
   }
 
@@ -170,10 +144,10 @@ export class PersonService {
 
     const jobs: JobItem[] = [];
     for (const personId of changeFeaturePhoto) {
-      const assetFace = await this.repository.getRandomFace(personId);
+      const assetFace = await this.personRepository.getRandomFace(personId);
 
       if (assetFace !== null) {
-        await this.repository.update({ id: personId, faceAssetId: assetFace.id });
+        await this.personRepository.update({ id: personId, faceAssetId: assetFace.id });
         jobs.push({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: personId } });
       }
     }
@@ -182,18 +156,18 @@ export class PersonService {
   }
 
   async getById(auth: AuthDto, id: string): Promise<PersonResponseDto> {
-    await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.PERSON_READ, ids: [id] });
     return this.findOrFail(id).then(mapPerson);
   }
 
   async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> {
-    await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] });
-    return this.repository.getStatistics(id);
+    await this.requireAccess({ auth, permission: Permission.PERSON_READ, ids: [id] });
+    return this.personRepository.getStatistics(id);
   }
 
   async getThumbnail(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
-    await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] });
-    const person = await this.repository.getById(id);
+    await this.requireAccess({ auth, permission: Permission.PERSON_READ, ids: [id] });
+    const person = await this.personRepository.getById(id);
     if (!person || !person.thumbnailPath) {
       throw new NotFoundException();
     }
@@ -205,14 +179,8 @@ export class PersonService {
     });
   }
 
-  async getAssets(auth: AuthDto, id: string): Promise<AssetResponseDto[]> {
-    await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] });
-    const assets = await this.repository.getAssets(id);
-    return assets.map((asset) => mapAsset(asset));
-  }
-
   create(auth: AuthDto, dto: PersonCreateDto): Promise<PersonResponseDto> {
-    return this.repository.create({
+    return this.personRepository.create({
       ownerId: auth.user.id,
       name: dto.name,
       birthDate: dto.birthDate,
@@ -221,14 +189,14 @@ export class PersonService {
   }
 
   async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
-    await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [id] });
 
     const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto;
     // TODO: set by faceId directly
     let faceId: string | undefined = undefined;
     if (assetId) {
-      await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [assetId] });
-      const [face] = await this.repository.getFacesByIds([{ personId: id, assetId }]);
+      await this.requireAccess({ auth, permission: Permission.ASSET_READ, ids: [assetId] });
+      const [face] = await this.personRepository.getFacesByIds([{ personId: id, assetId }]);
       if (!face) {
         throw new BadRequestException('Invalid assetId for feature face');
       }
@@ -236,7 +204,7 @@ export class PersonService {
       faceId = face.id;
     }
 
-    const person = await this.repository.update({ id, faceAssetId: faceId, name, birthDate, isHidden });
+    const person = await this.personRepository.update({ id, faceAssetId: faceId, name, birthDate, isHidden });
 
     if (assetId) {
       await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } });
@@ -266,46 +234,38 @@ export class PersonService {
 
   private async delete(people: PersonEntity[]) {
     await Promise.all(people.map((person) => this.storageRepository.unlink(person.thumbnailPath)));
-    await this.repository.delete(people);
+    await this.personRepository.delete(people);
     this.logger.debug(`Deleted ${people.length} people`);
   }
 
-  private async deleteAllPeople() {
-    const personPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
-      this.repository.getAll({ ...pagination, skip: 0 }),
-    );
-
-    for await (const people of personPagination) {
-      await this.delete(people); // deletes thumbnails too
-    }
-  }
-
+  @OnJob({ name: JobName.PERSON_CLEANUP, queue: QueueName.BACKGROUND_TASK })
   async handlePersonCleanup(): Promise<JobStatus> {
-    const people = await this.repository.getAllWithoutFaces();
+    const people = await this.personRepository.getAllWithoutFaces();
     await this.delete(people);
     return JobStatus.SUCCESS;
   }
 
-  async handleQueueDetectFaces({ force }: IBaseJob): Promise<JobStatus> {
-    const { machineLearning } = await this.configCore.getConfig({ withCache: false });
+  @OnJob({ name: JobName.QUEUE_FACE_DETECTION, queue: QueueName.FACE_DETECTION })
+  async handleQueueDetectFaces({ force }: JobOf<JobName.QUEUE_FACE_DETECTION>): Promise<JobStatus> {
+    const { machineLearning } = await this.getConfig({ withCache: false });
     if (!isFacialRecognitionEnabled(machineLearning)) {
       return JobStatus.SKIPPED;
     }
 
     if (force) {
-      await this.repository.deleteAllFaces({ sourceType: SourceType.MACHINE_LEARNING });
+      await this.personRepository.deleteFaces({ sourceType: SourceType.MACHINE_LEARNING });
       await this.handlePersonCleanup();
     }
 
     const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
-      return force
-        ? this.assetRepository.getAll(pagination, {
+      return force === false
+        ? this.assetRepository.getWithout(pagination, WithoutProperty.FACES)
+        : this.assetRepository.getAll(pagination, {
             orderDirection: 'DESC',
             withFaces: true,
             withArchived: true,
             isVisible: true,
-          })
-        : this.assetRepository.getWithout(pagination, WithoutProperty.FACES);
+          });
     });
 
     for await (const assets of assetPagination) {
@@ -314,11 +274,16 @@ export class PersonService {
       );
     }
 
+    if (force === undefined) {
+      await this.jobRepository.queue({ name: JobName.PERSON_CLEANUP });
+    }
+
     return JobStatus.SUCCESS;
   }
 
-  async handleDetectFaces({ id }: IEntityJob): Promise<JobStatus> {
-    const { machineLearning } = await this.configCore.getConfig({ withCache: true });
+  @OnJob({ name: JobName.FACE_DETECTION, queue: QueueName.FACE_DETECTION })
+  async handleDetectFaces({ id }: JobOf<JobName.FACE_DETECTION>): Promise<JobStatus> {
+    const { machineLearning } = await this.getConfig({ withCache: true });
     if (!isFacialRecognitionEnabled(machineLearning)) {
       return JobStatus.SKIPPED;
     }
@@ -332,54 +297,98 @@ export class PersonService {
     };
     const [asset] = await this.assetRepository.getByIds([id], relations);
     const { previewFile } = getAssetFiles(asset.files);
-    if (!asset || !previewFile || asset.faces?.length > 0) {
+    if (!asset || !previewFile) {
       return JobStatus.FAILED;
     }
 
-    if (!asset.isVisible || asset.faces.length > 0) {
+    if (!asset.isVisible) {
       return JobStatus.SKIPPED;
     }
 
     const { imageHeight, imageWidth, faces } = await this.machineLearningRepository.detectFaces(
-      machineLearning.url,
+      machineLearning.urls,
       previewFile.path,
       machineLearning.facialRecognition,
     );
-
     this.logger.debug(`${faces.length} faces detected in ${previewFile.path}`);
 
-    if (faces.length > 0) {
-      await this.jobRepository.queue({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } });
-      const mappedFaces: Partial<AssetFaceEntity>[] = [];
-      for (const face of faces) {
+    const facesToAdd: (Partial<AssetFaceEntity> & { id: string })[] = [];
+    const embeddings: FaceSearchEntity[] = [];
+    const mlFaceIds = new Set<string>();
+    for (const face of asset.faces) {
+      if (face.sourceType === SourceType.MACHINE_LEARNING) {
+        mlFaceIds.add(face.id);
+      }
+    }
+
+    const heightScale = imageHeight / (asset.faces[0]?.imageHeight || 1);
+    const widthScale = imageWidth / (asset.faces[0]?.imageWidth || 1);
+    for (const { boundingBox, embedding } of faces) {
+      const scaledBox = {
+        x1: boundingBox.x1 * widthScale,
+        y1: boundingBox.y1 * heightScale,
+        x2: boundingBox.x2 * widthScale,
+        y2: boundingBox.y2 * heightScale,
+      };
+      const match = asset.faces.find((face) => this.iou(face, scaledBox) > 0.5);
+
+      if (match && !mlFaceIds.delete(match.id)) {
+        embeddings.push({ faceId: match.id, embedding });
+      } else if (!match) {
         const faceId = this.cryptoRepository.randomUUID();
-        mappedFaces.push({
+        facesToAdd.push({
           id: faceId,
           assetId: asset.id,
           imageHeight,
           imageWidth,
-          boundingBoxX1: face.boundingBox.x1,
-          boundingBoxY1: face.boundingBox.y1,
-          boundingBoxX2: face.boundingBox.x2,
-          boundingBoxY2: face.boundingBox.y2,
-          faceSearch: { faceId, embedding: face.embedding },
+          boundingBoxX1: boundingBox.x1,
+          boundingBoxY1: boundingBox.y1,
+          boundingBoxX2: boundingBox.x2,
+          boundingBoxY2: boundingBox.y2,
         });
+        embeddings.push({ faceId, embedding });
       }
+    }
+    const faceIdsToRemove = [...mlFaceIds];
 
-      const faceIds = await this.repository.createFaces(mappedFaces);
-      await this.jobRepository.queueAll(faceIds.map((id) => ({ name: JobName.FACIAL_RECOGNITION, data: { id } })));
+    if (facesToAdd.length > 0 || faceIdsToRemove.length > 0 || embeddings.length > 0) {
+      await this.personRepository.refreshFaces(facesToAdd, faceIdsToRemove, embeddings);
     }
 
-    await this.assetRepository.upsertJobStatus({
-      assetId: asset.id,
-      facesRecognizedAt: new Date(),
-    });
+    if (faceIdsToRemove.length > 0) {
+      this.logger.log(`Removed ${faceIdsToRemove.length} faces below detection threshold in asset ${id}`);
+    }
+
+    if (facesToAdd.length > 0) {
+      this.logger.log(`Detected ${facesToAdd.length} new faces in asset ${id}`);
+      const jobs = facesToAdd.map((face) => ({ name: JobName.FACIAL_RECOGNITION, data: { id: face.id } }) as const);
+      await this.jobRepository.queueAll([{ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } }, ...jobs]);
+    } else if (embeddings.length > 0) {
+      this.logger.log(`Added ${embeddings.length} face embeddings for asset ${id}`);
+    }
+
+    await this.assetRepository.upsertJobStatus({ assetId: asset.id, facesRecognizedAt: new Date() });
 
     return JobStatus.SUCCESS;
   }
 
-  async handleQueueRecognizeFaces({ force, nightly }: INightlyJob): Promise<JobStatus> {
-    const { machineLearning } = await this.configCore.getConfig({ withCache: false });
+  private iou(face: AssetFaceEntity, newBox: BoundingBox): number {
+    const x1 = Math.max(face.boundingBoxX1, newBox.x1);
+    const y1 = Math.max(face.boundingBoxY1, newBox.y1);
+    const x2 = Math.min(face.boundingBoxX2, newBox.x2);
+    const y2 = Math.min(face.boundingBoxY2, newBox.y2);
+
+    const intersection = Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
+    const area1 = (face.boundingBoxX2 - face.boundingBoxX1) * (face.boundingBoxY2 - face.boundingBoxY1);
+    const area2 = (newBox.x2 - newBox.x1) * (newBox.y2 - newBox.y1);
+    const union = area1 + area2 - intersection;
+
+    return intersection / union;
+  }
+
+  @OnJob({ name: JobName.QUEUE_FACIAL_RECOGNITION, queue: QueueName.FACIAL_RECOGNITION })
+  async handleQueueRecognizeFaces({ force, nightly }: JobOf<JobName.QUEUE_FACIAL_RECOGNITION>): Promise<JobStatus> {
+    const { machineLearning } = await this.getConfig({ withCache: false });
     if (!isFacialRecognitionEnabled(machineLearning)) {
       return JobStatus.SKIPPED;
     }
@@ -389,7 +398,7 @@ export class PersonService {
     if (nightly) {
       const [state, latestFaceDate] = await Promise.all([
         this.systemMetadataRepository.get(SystemMetadataKey.FACIAL_RECOGNITION_STATE),
-        this.repository.getLatestFaceDate(),
+        this.personRepository.getLatestFaceDate(),
       ]);
 
       if (state?.lastRun && latestFaceDate && state.lastRun > latestFaceDate) {
@@ -401,7 +410,7 @@ export class PersonService {
     const { waiting } = await this.jobRepository.getJobCounts(QueueName.FACIAL_RECOGNITION);
 
     if (force) {
-      await this.repository.deleteAllFaces({ sourceType: SourceType.MACHINE_LEARNING });
+      await this.personRepository.unassignFaces({ sourceType: SourceType.MACHINE_LEARNING });
       await this.handlePersonCleanup();
     } else if (waiting) {
       this.logger.debug(
@@ -412,8 +421,8 @@ export class PersonService {
 
     const lastRun = new Date().toISOString();
     const facePagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
-      this.repository.getAllFaces(pagination, {
-        where: force ? undefined : { personId: IsNull(), sourceType: IsNull() },
+      this.personRepository.getAllFaces(pagination, {
+        where: force ? undefined : { personId: IsNull(), sourceType: SourceType.MACHINE_LEARNING },
       }),
     );
 
@@ -428,13 +437,14 @@ export class PersonService {
     return JobStatus.SUCCESS;
   }
 
-  async handleRecognizeFaces({ id, deferred }: IDeferrableJob): Promise<JobStatus> {
-    const { machineLearning } = await this.configCore.getConfig({ withCache: true });
+  @OnJob({ name: JobName.FACIAL_RECOGNITION, queue: QueueName.FACIAL_RECOGNITION })
+  async handleRecognizeFaces({ id, deferred }: JobOf<JobName.FACIAL_RECOGNITION>): Promise<JobStatus> {
+    const { machineLearning } = await this.getConfig({ withCache: true });
     if (!isFacialRecognitionEnabled(machineLearning)) {
       return JobStatus.SKIPPED;
     }
 
-    const face = await this.repository.getFaceByIdWithAssets(
+    const face = await this.personRepository.getFaceByIdWithAssets(
       id,
       { person: true, asset: true, faceSearch: true },
       { id: true, personId: true, sourceType: true, faceSearch: { embedding: true } },
@@ -459,7 +469,7 @@ export class PersonService {
       return JobStatus.SKIPPED;
     }
 
-    const matches = await this.smartInfoRepository.searchFaces({
+    const matches = await this.searchRepository.searchFaces({
       userIds: [face.asset.ownerId],
       embedding: face.faceSearch.embedding,
       maxDistance: machineLearning.facialRecognition.maxDistance,
@@ -483,7 +493,7 @@ export class PersonService {
 
     let personId = matches.find((match) => match.face.personId)?.face.personId;
     if (!personId) {
-      const matchWithPerson = await this.smartInfoRepository.searchFaces({
+      const matchWithPerson = await this.searchRepository.searchFaces({
         userIds: [face.asset.ownerId],
         embedding: face.faceSearch.embedding,
         maxDistance: machineLearning.facialRecognition.maxDistance,
@@ -498,21 +508,22 @@ export class PersonService {
 
     if (isCore && !personId) {
       this.logger.log(`Creating new person for face ${id}`);
-      const newPerson = await this.repository.create({ ownerId: face.asset.ownerId, faceAssetId: face.id });
+      const newPerson = await this.personRepository.create({ ownerId: face.asset.ownerId, faceAssetId: face.id });
       await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: newPerson.id } });
       personId = newPerson.id;
     }
 
     if (personId) {
       this.logger.debug(`Assigning face ${id} to person ${personId}`);
-      await this.repository.reassignFaces({ faceIds: [id], newPersonId: personId });
+      await this.personRepository.reassignFaces({ faceIds: [id], newPersonId: personId });
     }
 
     return JobStatus.SUCCESS;
   }
 
-  async handlePersonMigration({ id }: IEntityJob): Promise<JobStatus> {
-    const person = await this.repository.getById(id);
+  @OnJob({ name: JobName.MIGRATE_PERSON, queue: QueueName.MIGRATION })
+  async handlePersonMigration({ id }: JobOf<JobName.MIGRATE_PERSON>): Promise<JobStatus> {
+    const person = await this.personRepository.getById(id);
     if (!person) {
       return JobStatus.FAILED;
     }
@@ -522,19 +533,20 @@ export class PersonService {
     return JobStatus.SUCCESS;
   }
 
-  async handleGeneratePersonThumbnail(data: IEntityJob): Promise<JobStatus> {
-    const { machineLearning, metadata, image } = await this.configCore.getConfig({ withCache: true });
+  @OnJob({ name: JobName.GENERATE_PERSON_THUMBNAIL, queue: QueueName.THUMBNAIL_GENERATION })
+  async handleGeneratePersonThumbnail(data: JobOf<JobName.GENERATE_PERSON_THUMBNAIL>): Promise<JobStatus> {
+    const { machineLearning, metadata, image } = await this.getConfig({ withCache: true });
     if (!isFacialRecognitionEnabled(machineLearning) && !isFaceImportEnabled(metadata)) {
       return JobStatus.SKIPPED;
     }
 
-    const person = await this.repository.getById(data.id);
+    const person = await this.personRepository.getById(data.id);
     if (!person?.faceAssetId) {
       this.logger.error(`Could not generate person thumbnail: person ${person?.id} has no face asset`);
       return JobStatus.FAILED;
     }
 
-    const face = await this.repository.getFaceByIdWithAssets(person.faceAssetId);
+    const face = await this.personRepository.getFaceByIdWithAssets(person.faceAssetId);
     if (face === null) {
       this.logger.error(`Could not generate person thumbnail: face ${person.faceAssetId} not found`);
       return JobStatus.FAILED;
@@ -565,16 +577,16 @@ export class PersonService {
     this.storageCore.ensureFolders(thumbnailPath);
 
     const thumbnailOptions = {
+      colorspace: image.colorspace,
       format: ImageFormat.JPEG,
       size: FACE_THUMBNAIL_SIZE,
-      colorspace: image.colorspace,
-      quality: image.quality,
+      quality: image.thumbnail.quality,
       crop: this.getCrop({ old: { width: oldWidth, height: oldHeight }, new: { width, height } }, { x1, y1, x2, y2 }),
       processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true',
-    } as const;
+    };
 
-    await this.mediaRepository.generateThumbnail(inputPath, thumbnailPath, thumbnailOptions);
-    await this.repository.update({ id: person.id, thumbnailPath });
+    await this.mediaRepository.generateThumbnail(inputPath, thumbnailOptions, thumbnailPath);
+    await this.personRepository.update({ id: person.id, thumbnailPath });
 
     return JobStatus.SUCCESS;
   }
@@ -585,13 +597,13 @@ export class PersonService {
       throw new BadRequestException('Cannot merge a person into themselves');
     }
 
-    await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [id] });
     let primaryPerson = await this.findOrFail(id);
     const primaryName = primaryPerson.name || primaryPerson.id;
 
     const results: BulkIdResponseDto[] = [];
 
-    const allowedIds = await checkAccess(this.access, {
+    const allowedIds = await this.checkAccess({
       auth,
       permission: Permission.PERSON_MERGE,
       ids: mergeIds,
@@ -605,7 +617,7 @@ export class PersonService {
       }
 
       try {
-        const mergePerson = await this.repository.getById(mergeId);
+        const mergePerson = await this.personRepository.getById(mergeId);
         if (!mergePerson) {
           results.push({ id: mergeId, success: false, error: BulkIdErrorReason.NOT_FOUND });
           continue;
@@ -621,14 +633,14 @@ export class PersonService {
         }
 
         if (Object.keys(update).length > 0) {
-          primaryPerson = await this.repository.update({ id: primaryPerson.id, ...update });
+          primaryPerson = await this.personRepository.update({ id: primaryPerson.id, ...update });
         }
 
         const mergeName = mergePerson.name || mergePerson.id;
         const mergeData: UpdateFacesData = { oldPersonId: mergeId, newPersonId: id };
         this.logger.log(`Merging ${mergeName} into ${primaryName}`);
 
-        await this.repository.reassignFaces(mergeData);
+        await this.personRepository.reassignFaces(mergeData);
         await this.delete([mergePerson]);
 
         this.logger.log(`Merged ${mergeName} into ${primaryName}`);
@@ -642,7 +654,7 @@ export class PersonService {
   }
 
   private async findOrFail(id: string) {
-    const person = await this.repository.getById(id);
+    const person = await this.personRepository.getById(id);
     if (!person) {
       throw new BadRequestException('Person not found');
     }
diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts
index ded087b8b5..3933526167 100644
--- a/server/src/services/search.service.spec.ts
+++ b/server/src/services/search.service.spec.ts
@@ -1,60 +1,26 @@
 import { mapAsset } from 'src/dtos/asset-response.dto';
 import { SearchSuggestionType } from 'src/dtos/search.dto';
 import { IAssetRepository } from 'src/interfaces/asset.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
-import { IMetadataRepository } from 'src/interfaces/metadata.interface';
-import { IPartnerRepository } from 'src/interfaces/partner.interface';
 import { IPersonRepository } from 'src/interfaces/person.interface';
 import { ISearchRepository } from 'src/interfaces/search.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { SearchService } from 'src/services/search.service';
 import { assetStub } from 'test/fixtures/asset.stub';
 import { authStub } from 'test/fixtures/auth.stub';
 import { personStub } from 'test/fixtures/person.stub';
-import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock';
-import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock';
-import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
-import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
-import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked, beforeEach, vitest } from 'vitest';
 
 vitest.useFakeTimers();
 
 describe(SearchService.name, () => {
   let sut: SearchService;
+
   let assetMock: Mocked<IAssetRepository>;
-  let systemMock: Mocked<ISystemMetadataRepository>;
-  let machineMock: Mocked<IMachineLearningRepository>;
   let personMock: Mocked<IPersonRepository>;
   let searchMock: Mocked<ISearchRepository>;
-  let partnerMock: Mocked<IPartnerRepository>;
-  let metadataMock: Mocked<IMetadataRepository>;
-  let loggerMock: Mocked<ILoggerRepository>;
 
   beforeEach(() => {
-    assetMock = newAssetRepositoryMock();
-    systemMock = newSystemMetadataRepositoryMock();
-    machineMock = newMachineLearningRepositoryMock();
-    personMock = newPersonRepositoryMock();
-    searchMock = newSearchRepositoryMock();
-    partnerMock = newPartnerRepositoryMock();
-    metadataMock = newMetadataRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-
-    sut = new SearchService(
-      systemMock,
-      machineMock,
-      personMock,
-      searchMock,
-      assetMock,
-      partnerMock,
-      metadataMock,
-      loggerMock,
-    );
+    ({ sut, assetMock, personMock, searchMock } = newTestService(SearchService));
   });
 
   it('should work', () => {
@@ -81,14 +47,9 @@ describe(SearchService.name, () => {
         fieldName: 'exifInfo.city',
         items: [{ value: 'Paris', data: assetStub.image.id }],
       });
-      assetMock.getAssetIdByTag.mockResolvedValue({
-        fieldName: 'smartInfo.tags',
-        items: [{ value: 'train', data: assetStub.imageFrom2015.id }],
-      });
       assetMock.getByIdsWithAllRelations.mockResolvedValue([assetStub.image, assetStub.imageFrom2015]);
       const expectedResponse = [
         { fieldName: 'exifInfo.city', items: [{ value: 'Paris', data: mapAsset(assetStub.image) }] },
-        { fieldName: 'smartInfo.tags', items: [{ value: 'train', data: mapAsset(assetStub.imageFrom2015) }] },
       ];
 
       const result = await sut.getExploreData(authStub.user1);
@@ -98,20 +59,84 @@ describe(SearchService.name, () => {
   });
 
   describe('getSearchSuggestions', () => {
-    it('should return search suggestions (including null)', async () => {
-      metadataMock.getCountries.mockResolvedValue(['USA', null]);
-      await expect(
-        sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.COUNTRY }),
-      ).resolves.toEqual(['USA', null]);
-      expect(metadataMock.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]);
-    });
-
-    it('should return search suggestions (without null)', async () => {
-      metadataMock.getCountries.mockResolvedValue(['USA', null]);
+    it('should return search suggestions for country', async () => {
+      searchMock.getCountries.mockResolvedValue(['USA']);
       await expect(
         sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.COUNTRY }),
       ).resolves.toEqual(['USA']);
-      expect(metadataMock.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]);
+      expect(searchMock.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]);
+    });
+
+    it('should return search suggestions for country (including null)', async () => {
+      searchMock.getCountries.mockResolvedValue(['USA']);
+      await expect(
+        sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.COUNTRY }),
+      ).resolves.toEqual(['USA', null]);
+      expect(searchMock.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]);
+    });
+
+    it('should return search suggestions for state', async () => {
+      searchMock.getStates.mockResolvedValue(['California']);
+      await expect(
+        sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.STATE }),
+      ).resolves.toEqual(['California']);
+      expect(searchMock.getStates).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
+    });
+
+    it('should return search suggestions for state (including null)', async () => {
+      searchMock.getStates.mockResolvedValue(['California']);
+      await expect(
+        sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.STATE }),
+      ).resolves.toEqual(['California', null]);
+      expect(searchMock.getStates).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
+    });
+
+    it('should return search suggestions for city', async () => {
+      searchMock.getCities.mockResolvedValue(['Denver']);
+      await expect(
+        sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.CITY }),
+      ).resolves.toEqual(['Denver']);
+      expect(searchMock.getCities).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
+    });
+
+    it('should return search suggestions for city (including null)', async () => {
+      searchMock.getCities.mockResolvedValue(['Denver']);
+      await expect(
+        sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.CITY }),
+      ).resolves.toEqual(['Denver', null]);
+      expect(searchMock.getCities).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
+    });
+
+    it('should return search suggestions for camera make', async () => {
+      searchMock.getCameraMakes.mockResolvedValue(['Nikon']);
+      await expect(
+        sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.CAMERA_MAKE }),
+      ).resolves.toEqual(['Nikon']);
+      expect(searchMock.getCameraMakes).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
+    });
+
+    it('should return search suggestions for camera make (including null)', async () => {
+      searchMock.getCameraMakes.mockResolvedValue(['Nikon']);
+      await expect(
+        sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.CAMERA_MAKE }),
+      ).resolves.toEqual(['Nikon', null]);
+      expect(searchMock.getCameraMakes).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
+    });
+
+    it('should return search suggestions for camera model', async () => {
+      searchMock.getCameraModels.mockResolvedValue(['Fujifilm X100VI']);
+      await expect(
+        sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.CAMERA_MODEL }),
+      ).resolves.toEqual(['Fujifilm X100VI']);
+      expect(searchMock.getCameraModels).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
+    });
+
+    it('should return search suggestions for camera model (including null)', async () => {
+      searchMock.getCameraModels.mockResolvedValue(['Fujifilm X100VI']);
+      await expect(
+        sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.CAMERA_MODEL }),
+      ).resolves.toEqual(['Fujifilm X100VI', null]);
+      expect(searchMock.getCameraModels).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
     });
   });
 });
diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts
index 4c86d4ad75..7fc947a8b5 100644
--- a/server/src/services/search.service.ts
+++ b/server/src/services/search.service.ts
@@ -1,11 +1,11 @@
-import { BadRequestException, Inject, Injectable } from '@nestjs/common';
-import { SystemConfigCore } from 'src/cores/system-config.core';
+import { BadRequestException, Injectable } from '@nestjs/common';
 import { AssetMapOptions, AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
 import { PersonResponseDto } from 'src/dtos/person.dto';
 import {
   MetadataSearchDto,
   PlacesResponseDto,
+  RandomSearchDto,
   SearchPeopleDto,
   SearchPlacesDto,
   SearchResponseDto,
@@ -16,35 +16,13 @@ import {
 } from 'src/dtos/search.dto';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { AssetOrder } from 'src/enum';
-import { IAssetRepository } from 'src/interfaces/asset.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
-import { IMetadataRepository } from 'src/interfaces/metadata.interface';
-import { IPartnerRepository } from 'src/interfaces/partner.interface';
-import { IPersonRepository } from 'src/interfaces/person.interface';
-import { ISearchRepository, SearchExploreItem } from 'src/interfaces/search.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { SearchExploreItem } from 'src/interfaces/search.interface';
+import { BaseService } from 'src/services/base.service';
 import { getMyPartnerIds } from 'src/utils/asset.util';
 import { isSmartSearchEnabled } from 'src/utils/misc';
 
 @Injectable()
-export class SearchService {
-  private configCore: SystemConfigCore;
-
-  constructor(
-    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
-    @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
-    @Inject(IPersonRepository) private personRepository: IPersonRepository,
-    @Inject(ISearchRepository) private searchRepository: ISearchRepository,
-    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
-    @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
-    @Inject(IMetadataRepository) private metadataRepository: IMetadataRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-  ) {
-    this.logger.setContext(SearchService.name);
-    this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
-  }
-
+export class SearchService extends BaseService {
   async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
     return this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden });
   }
@@ -56,10 +34,8 @@ export class SearchService {
 
   async getExploreData(auth: AuthDto): Promise<SearchExploreItem<AssetResponseDto>[]> {
     const options = { maxFields: 12, minAssetsPerField: 5 };
-    const results = await Promise.all([
-      this.assetRepository.getAssetIdByCity(auth.user.id, options),
-      this.assetRepository.getAssetIdByTag(auth.user.id, options),
-    ]);
+    const result = await this.assetRepository.getAssetIdByCity(auth.user.id, options);
+    const results = [result];
     const assetIds = new Set<string>(results.flatMap((field) => field.items.map((item) => item.data)));
     const assets = await this.assetRepository.getByIdsWithAllRelations([...assetIds]);
     const assetMap = new Map<string, AssetResponseDto>(assets.map((asset) => [asset.id, mapAsset(asset)]));
@@ -95,15 +71,25 @@ export class SearchService {
     return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth });
   }
 
+  async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise<AssetResponseDto[]> {
+    const userIds = await this.getUserIdsToSearch(auth);
+    const items = await this.searchRepository.searchRandom(dto.size || 250, { ...dto, userIds });
+    return items.map((item) => mapAsset(item, { auth }));
+  }
+
   async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
-    const { machineLearning } = await this.configCore.getConfig({ withCache: false });
+    const { machineLearning } = await this.getConfig({ withCache: false });
     if (!isSmartSearchEnabled(machineLearning)) {
       throw new BadRequestException('Smart search is not enabled');
     }
 
     const userIds = await this.getUserIdsToSearch(auth);
 
-    const embedding = await this.machineLearning.encodeText(machineLearning.url, dto.query, machineLearning.clip);
+    const embedding = await this.machineLearningRepository.encodeText(
+      machineLearning.urls,
+      dto.query,
+      machineLearning.clip,
+    );
     const page = dto.page ?? 1;
     const size = dto.size || 100;
     const { hasNextPage, items } = await this.searchRepository.searchSmart(
@@ -122,29 +108,32 @@ export class SearchService {
 
   async getSearchSuggestions(auth: AuthDto, dto: SearchSuggestionRequestDto) {
     const userIds = await this.getUserIdsToSearch(auth);
-    const results = await this.getSuggestions(userIds, dto);
-    return results.filter((result) => (dto.includeNull ? true : result !== null));
+    const suggestions = await this.getSuggestions(userIds, dto);
+    if (dto.includeNull) {
+      suggestions.push(null);
+    }
+    return suggestions;
   }
 
   private getSuggestions(userIds: string[], dto: SearchSuggestionRequestDto) {
     switch (dto.type) {
       case SearchSuggestionType.COUNTRY: {
-        return this.metadataRepository.getCountries(userIds);
+        return this.searchRepository.getCountries(userIds);
       }
       case SearchSuggestionType.STATE: {
-        return this.metadataRepository.getStates(userIds, dto.country);
+        return this.searchRepository.getStates(userIds, dto);
       }
       case SearchSuggestionType.CITY: {
-        return this.metadataRepository.getCities(userIds, dto.country, dto.state);
+        return this.searchRepository.getCities(userIds, dto);
       }
       case SearchSuggestionType.CAMERA_MAKE: {
-        return this.metadataRepository.getCameraMakes(userIds, dto.model);
+        return this.searchRepository.getCameraMakes(userIds, dto);
       }
       case SearchSuggestionType.CAMERA_MODEL: {
-        return this.metadataRepository.getCameraModels(userIds, dto.make);
+        return this.searchRepository.getCameraModels(userIds, dto);
       }
       default: {
-        return [];
+        return [] as (string | null)[];
       }
     }
   }
diff --git a/server/src/services/server.service.spec.ts b/server/src/services/server.service.spec.ts
index ac899f7b13..3f7fafcebf 100644
--- a/server/src/services/server.service.spec.ts
+++ b/server/src/services/server.service.spec.ts
@@ -1,37 +1,20 @@
 import { SystemMetadataKey } from 'src/enum';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
 import { IStorageRepository } from 'src/interfaces/storage.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { IUserRepository } from 'src/interfaces/user.interface';
 import { ServerService } from 'src/services/server.service';
-import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock';
-import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
-import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(ServerService.name, () => {
   let sut: ServerService;
+
   let storageMock: Mocked<IStorageRepository>;
-  let userMock: Mocked<IUserRepository>;
-  let serverInfoMock: Mocked<IServerInfoRepository>;
   let systemMock: Mocked<ISystemMetadataRepository>;
-  let loggerMock: Mocked<ILoggerRepository>;
-  let cryptoMock: Mocked<ICryptoRepository>;
+  let userMock: Mocked<IUserRepository>;
 
   beforeEach(() => {
-    storageMock = newStorageRepositoryMock();
-    userMock = newUserRepositoryMock();
-    serverInfoMock = newServerInfoRepositoryMock();
-    systemMock = newSystemMetadataRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-    cryptoMock = newCryptoRepositoryMock();
-
-    sut = new ServerService(userMock, storageMock, systemMock, serverInfoMock, loggerMock, cryptoMock);
+    ({ sut, storageMock, systemMock, userMock } = newTestService(ServerService));
   });
 
   it('should work', () => {
@@ -176,9 +159,9 @@ describe(ServerService.name, () => {
     });
   });
 
-  describe('getConfig', () => {
+  describe('getSystemConfig', () => {
     it('should respond the server configuration', async () => {
-      await expect(sut.getConfig()).resolves.toEqual({
+      await expect(sut.getSystemConfig()).resolves.toEqual({
         loginPageMessage: '',
         oauthButtonText: 'Login with OAuth',
         trashDays: 30,
@@ -186,6 +169,9 @@ describe(ServerService.name, () => {
         isInitialized: undefined,
         isOnboarded: false,
         externalDomain: '',
+        publicUsers: true,
+        mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
+        mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
       });
       expect(systemMock.get).toHaveBeenCalled();
     });
@@ -200,6 +186,8 @@ describe(ServerService.name, () => {
           photos: 10,
           videos: 11,
           usage: 12_345,
+          usagePhotos: 1,
+          usageVideos: 11_345,
           quotaSizeInBytes: 0,
         },
         {
@@ -208,6 +196,8 @@ describe(ServerService.name, () => {
           photos: 10,
           videos: 20,
           usage: 123_456,
+          usagePhotos: 100,
+          usageVideos: 23_456,
           quotaSizeInBytes: 0,
         },
         {
@@ -216,6 +206,8 @@ describe(ServerService.name, () => {
           photos: 100,
           videos: 0,
           usage: 987_654,
+          usagePhotos: 900,
+          usageVideos: 87_654,
           quotaSizeInBytes: 0,
         },
       ]);
@@ -224,11 +216,15 @@ describe(ServerService.name, () => {
         photos: 120,
         videos: 31,
         usage: 1_123_455,
+        usagePhotos: 1001,
+        usageVideos: 122_455,
         usageByUser: [
           {
             photos: 10,
             quotaSizeInBytes: 0,
             usage: 12_345,
+            usagePhotos: 1,
+            usageVideos: 11_345,
             userName: '1 User',
             userId: 'user1',
             videos: 11,
@@ -237,6 +233,8 @@ describe(ServerService.name, () => {
             photos: 10,
             quotaSizeInBytes: 0,
             usage: 123_456,
+            usagePhotos: 100,
+            usageVideos: 23_456,
             userName: '2 User',
             userId: 'user2',
             videos: 20,
@@ -245,6 +243,8 @@ describe(ServerService.name, () => {
             photos: 100,
             quotaSizeInBytes: 0,
             usage: 987_654,
+            usagePhotos: 900,
+            usageVideos: 87_654,
             userName: '3 User',
             userId: 'user3',
             videos: 0,
diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts
index e57a206765..e9dd908a7c 100644
--- a/server/src/services/server.service.ts
+++ b/server/src/services/server.service.ts
@@ -1,9 +1,7 @@
-import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
-import { getBuildMetadata, getServerLicensePublicKey } from 'src/config';
+import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
 import { serverVersion } from 'src/constants';
-import { StorageCore, StorageFolder } from 'src/cores/storage.core';
-import { SystemConfigCore } from 'src/cores/system-config.core';
-import { OnEmit } from 'src/decorators';
+import { StorageCore } from 'src/cores/storage.core';
+import { OnEvent } from 'src/decorators';
 import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
 import {
   ServerAboutResponseDto,
@@ -15,34 +13,16 @@ import {
   ServerStorageResponseDto,
   UsageByUserDto,
 } from 'src/dtos/server.dto';
-import { SystemMetadataKey } from 'src/enum';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
-import { IStorageRepository } from 'src/interfaces/storage.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
-import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface';
+import { StorageFolder, SystemMetadataKey } from 'src/enum';
+import { UserStatsQueryResponse } from 'src/interfaces/user.interface';
+import { BaseService } from 'src/services/base.service';
 import { asHumanReadable } from 'src/utils/bytes';
 import { mimeTypes } from 'src/utils/mime-types';
 import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isSmartSearchEnabled } from 'src/utils/misc';
 
 @Injectable()
-export class ServerService {
-  private configCore: SystemConfigCore;
-
-  constructor(
-    @Inject(IUserRepository) private userRepository: IUserRepository,
-    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
-    @Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository,
-    @Inject(IServerInfoRepository) private serverInfoRepository: IServerInfoRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
-  ) {
-    this.logger.setContext(ServerService.name);
-    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
-  }
-
-  @OnEmit({ event: 'app.bootstrap' })
+export class ServerService extends BaseService {
+  @OnEvent({ name: 'app.bootstrap' })
   async onBootstrap(): Promise<void> {
     const featureFlags = await this.getFeatures();
     if (featureFlags.configFile) {
@@ -55,7 +35,7 @@ export class ServerService {
 
   async getAboutInfo(): Promise<ServerAboutResponseDto> {
     const version = `v${serverVersion.toString()}`;
-    const buildMetadata = getBuildMetadata();
+    const { buildMetadata } = this.configRepository.getEnv();
     const buildVersions = await this.serverInfoRepository.getBuildVersions();
     const licensed = await this.systemMetadataRepository.get(SystemMetadataKey.LICENSE);
 
@@ -91,7 +71,8 @@ export class ServerService {
 
   async getFeatures(): Promise<ServerFeaturesDto> {
     const { reverseGeocoding, metadata, map, machineLearning, trash, oauth, passwordLogin, notifications } =
-      await this.configCore.getConfig({ withCache: false });
+      await this.getConfig({ withCache: false });
+    const { configFile } = this.configRepository.getEnv();
 
     return {
       smartSearch: isSmartSearchEnabled(machineLearning),
@@ -106,18 +87,18 @@ export class ServerService {
       oauth: oauth.enabled,
       oauthAutoLaunch: oauth.autoLaunch,
       passwordLogin: passwordLogin.enabled,
-      configFile: this.configCore.isUsingConfigFile(),
+      configFile: !!configFile,
       email: notifications.smtp.enabled,
     };
   }
 
   async getTheme() {
-    const { theme } = await this.configCore.getConfig({ withCache: false });
+    const { theme } = await this.getConfig({ withCache: false });
     return theme;
   }
 
-  async getConfig(): Promise<ServerConfigDto> {
-    const config = await this.configCore.getConfig({ withCache: false });
+  async getSystemConfig(): Promise<ServerConfigDto> {
+    const config = await this.getConfig({ withCache: false });
     const isInitialized = await this.userRepository.hasAdmin();
     const onboarding = await this.systemMetadataRepository.get(SystemMetadataKey.ADMIN_ONBOARDING);
 
@@ -129,6 +110,9 @@ export class ServerService {
       isInitialized,
       isOnboarded: onboarding?.isOnboarded || false,
       externalDomain: config.server.externalDomain,
+      publicUsers: config.server.publicUsers,
+      mapDarkStyleUrl: config.map.darkStyle,
+      mapLightStyleUrl: config.map.lightStyle,
     };
   }
 
@@ -143,11 +127,16 @@ export class ServerService {
       usage.photos = user.photos;
       usage.videos = user.videos;
       usage.usage = user.usage;
+      usage.usagePhotos = user.usagePhotos;
+      usage.usageVideos = user.usageVideos;
       usage.quotaSizeInBytes = user.quotaSizeInBytes;
 
       serverStats.photos += usage.photos;
       serverStats.videos += usage.videos;
       serverStats.usage += usage.usage;
+      serverStats.usagePhotos += usage.usagePhotos;
+      serverStats.usageVideos += usage.usageVideos;
+
       serverStats.usageByUser.push(usage);
     }
 
@@ -178,20 +167,13 @@ export class ServerService {
     if (!dto.licenseKey.startsWith('IMSV-')) {
       throw new BadRequestException('Invalid license key');
     }
-    const licenseValid = this.cryptoRepository.verifySha256(
-      dto.licenseKey,
-      dto.activationKey,
-      getServerLicensePublicKey(),
-    );
-
+    const { licensePublicKey } = this.configRepository.getEnv();
+    const licenseValid = this.cryptoRepository.verifySha256(dto.licenseKey, dto.activationKey, licensePublicKey.server);
     if (!licenseValid) {
       throw new BadRequestException('Invalid license key');
     }
 
-    const licenseData = {
-      ...dto,
-      activatedAt: new Date(),
-    };
+    const licenseData = { ...dto, activatedAt: new Date() };
 
     await this.systemMetadataRepository.set(SystemMetadataKey.LICENSE, licenseData);
 
diff --git a/server/src/services/session.service.spec.ts b/server/src/services/session.service.spec.ts
index ca3d2fd858..49d1227712 100644
--- a/server/src/services/session.service.spec.ts
+++ b/server/src/services/session.service.spec.ts
@@ -1,27 +1,21 @@
 import { UserEntity } from 'src/entities/user.entity';
 import { JobStatus } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { ISessionRepository } from 'src/interfaces/session.interface';
 import { SessionService } from 'src/services/session.service';
 import { authStub } from 'test/fixtures/auth.stub';
 import { sessionStub } from 'test/fixtures/session.stub';
-import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock';
+import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe('SessionService', () => {
   let sut: SessionService;
+
   let accessMock: Mocked<IAccessRepositoryMock>;
-  let loggerMock: Mocked<ILoggerRepository>;
   let sessionMock: Mocked<ISessionRepository>;
 
   beforeEach(() => {
-    accessMock = newAccessRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-    sessionMock = newSessionRepositoryMock();
-
-    sut = new SessionService(accessMock, loggerMock, sessionMock);
+    ({ sut, accessMock, sessionMock } = newTestService(SessionService));
   });
 
   it('should be defined', () => {
diff --git a/server/src/services/session.service.ts b/server/src/services/session.service.ts
index 47abf3c380..68df7828ad 100644
--- a/server/src/services/session.service.ts
+++ b/server/src/services/session.service.ts
@@ -1,25 +1,16 @@
-import { Inject, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
 import { DateTime } from 'luxon';
+import { OnJob } from 'src/decorators';
 import { AuthDto } from 'src/dtos/auth.dto';
 import { SessionResponseDto, mapSession } from 'src/dtos/session.dto';
 import { Permission } from 'src/enum';
-import { IAccessRepository } from 'src/interfaces/access.interface';
-import { JobStatus } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { ISessionRepository } from 'src/interfaces/session.interface';
-import { requireAccess } from 'src/utils/access';
+import { JobName, JobStatus, QueueName } from 'src/interfaces/job.interface';
+import { BaseService } from 'src/services/base.service';
 
 @Injectable()
-export class SessionService {
-  constructor(
-    @Inject(IAccessRepository) private access: IAccessRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-    @Inject(ISessionRepository) private sessionRepository: ISessionRepository,
-  ) {
-    this.logger.setContext(SessionService.name);
-  }
-
-  async handleCleanup() {
+export class SessionService extends BaseService {
+  @OnJob({ name: JobName.CLEAN_OLD_SESSION_TOKENS, queue: QueueName.BACKGROUND_TASK })
+  async handleCleanup(): Promise<JobStatus> {
     const sessions = await this.sessionRepository.search({
       updatedBefore: DateTime.now().minus({ days: 90 }).toJSDate(),
     });
@@ -44,7 +35,7 @@ export class SessionService {
   }
 
   async delete(auth: AuthDto, id: string): Promise<void> {
-    await requireAccess(this.access, { auth, permission: Permission.AUTH_DEVICE_DELETE, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.AUTH_DEVICE_DELETE, ids: [id] });
     await this.sessionRepository.delete(id);
   }
 
diff --git a/server/src/services/shared-link.service.spec.ts b/server/src/services/shared-link.service.spec.ts
index 0fd47b612e..6554421418 100644
--- a/server/src/services/shared-link.service.spec.ts
+++ b/server/src/services/shared-link.service.spec.ts
@@ -1,40 +1,25 @@
 import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
 import _ from 'lodash';
-import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
 import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto';
 import { SharedLinkType } from 'src/enum';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { SharedLinkService } from 'src/services/shared-link.service';
 import { albumStub } from 'test/fixtures/album.stub';
 import { assetStub } from 'test/fixtures/asset.stub';
 import { authStub } from 'test/fixtures/auth.stub';
 import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub';
-import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
-import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
+import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(SharedLinkService.name, () => {
   let sut: SharedLinkService;
+
   let accessMock: IAccessRepositoryMock;
-  let cryptoMock: Mocked<ICryptoRepository>;
-  let shareMock: Mocked<ISharedLinkRepository>;
-  let systemMock: Mocked<ISystemMetadataRepository>;
-  let logMock: Mocked<ILoggerRepository>;
+  let sharedLinkMock: Mocked<ISharedLinkRepository>;
 
   beforeEach(() => {
-    accessMock = newAccessRepositoryMock();
-    cryptoMock = newCryptoRepositoryMock();
-    shareMock = newSharedLinkRepositoryMock();
-    systemMock = newSystemMetadataRepositoryMock();
-    logMock = newLoggerRepositoryMock();
-
-    sut = new SharedLinkService(accessMock, cryptoMock, logMock, shareMock, systemMock);
+    ({ sut, accessMock, sharedLinkMock } = newTestService(SharedLinkService));
   });
 
   it('should work', () => {
@@ -43,55 +28,64 @@ describe(SharedLinkService.name, () => {
 
   describe('getAll', () => {
     it('should return all shared links for a user', async () => {
-      shareMock.getAll.mockResolvedValue([sharedLinkStub.expired, sharedLinkStub.valid]);
+      sharedLinkMock.getAll.mockResolvedValue([sharedLinkStub.expired, sharedLinkStub.valid]);
       await expect(sut.getAll(authStub.user1)).resolves.toEqual([
         sharedLinkResponseStub.expired,
         sharedLinkResponseStub.valid,
       ]);
-      expect(shareMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
+      expect(sharedLinkMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
     });
   });
 
   describe('getMine', () => {
     it('should only work for a public user', async () => {
       await expect(sut.getMine(authStub.admin, {})).rejects.toBeInstanceOf(ForbiddenException);
-      expect(shareMock.get).not.toHaveBeenCalled();
+      expect(sharedLinkMock.get).not.toHaveBeenCalled();
     });
 
     it('should return the shared link for the public user', async () => {
       const authDto = authStub.adminSharedLink;
-      shareMock.get.mockResolvedValue(sharedLinkStub.valid);
+      sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid);
       await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.valid);
-      expect(shareMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
+      expect(sharedLinkMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
     });
 
     it('should not return metadata', async () => {
       const authDto = authStub.adminSharedLinkNoExif;
-      shareMock.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
+      sharedLinkMock.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
       await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.readonlyNoMetadata);
-      expect(shareMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
+      expect(sharedLinkMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
     });
 
-    it('should throw an error for an password protected shared link', async () => {
+    it('should throw an error for an invalid password protected shared link', async () => {
       const authDto = authStub.adminSharedLink;
-      shareMock.get.mockResolvedValue(sharedLinkStub.passwordRequired);
+      sharedLinkMock.get.mockResolvedValue(sharedLinkStub.passwordRequired);
       await expect(sut.getMine(authDto, {})).rejects.toBeInstanceOf(UnauthorizedException);
-      expect(shareMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
+      expect(sharedLinkMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
+    });
+
+    it('should allow a correct password on a password protected shared link', async () => {
+      sharedLinkMock.get.mockResolvedValue({ ...sharedLinkStub.individual, password: '123' });
+      await expect(sut.getMine(authStub.adminSharedLink, { password: '123' })).resolves.toBeDefined();
+      expect(sharedLinkMock.get).toHaveBeenCalledWith(
+        authStub.adminSharedLink.user.id,
+        authStub.adminSharedLink.sharedLink?.id,
+      );
     });
   });
 
   describe('get', () => {
     it('should throw an error for an invalid shared link', async () => {
-      shareMock.get.mockResolvedValue(null);
+      sharedLinkMock.get.mockResolvedValue(null);
       await expect(sut.get(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException);
-      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
-      expect(shareMock.update).not.toHaveBeenCalled();
+      expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
+      expect(sharedLinkMock.update).not.toHaveBeenCalled();
     });
 
     it('should get a shared link by id', async () => {
-      shareMock.get.mockResolvedValue(sharedLinkStub.valid);
+      sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid);
       await expect(sut.get(authStub.user1, sharedLinkStub.valid.id)).resolves.toEqual(sharedLinkResponseStub.valid);
-      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
+      expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
     });
   });
 
@@ -122,7 +116,7 @@ describe(SharedLinkService.name, () => {
 
     it('should create an album shared link', async () => {
       accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.oneAsset.id]));
-      shareMock.create.mockResolvedValue(sharedLinkStub.valid);
+      sharedLinkMock.create.mockResolvedValue(sharedLinkStub.valid);
 
       await sut.create(authStub.admin, { type: SharedLinkType.ALBUM, albumId: albumStub.oneAsset.id });
 
@@ -130,7 +124,7 @@ describe(SharedLinkService.name, () => {
         authStub.admin.user.id,
         new Set([albumStub.oneAsset.id]),
       );
-      expect(shareMock.create).toHaveBeenCalledWith({
+      expect(sharedLinkMock.create).toHaveBeenCalledWith({
         type: SharedLinkType.ALBUM,
         userId: authStub.admin.user.id,
         albumId: albumStub.oneAsset.id,
@@ -146,7 +140,7 @@ describe(SharedLinkService.name, () => {
 
     it('should create an individual shared link', async () => {
       accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
-      shareMock.create.mockResolvedValue(sharedLinkStub.individual);
+      sharedLinkMock.create.mockResolvedValue(sharedLinkStub.individual);
 
       await sut.create(authStub.admin, {
         type: SharedLinkType.INDIVIDUAL,
@@ -160,7 +154,7 @@ describe(SharedLinkService.name, () => {
         authStub.admin.user.id,
         new Set([assetStub.image.id]),
       );
-      expect(shareMock.create).toHaveBeenCalledWith({
+      expect(sharedLinkMock.create).toHaveBeenCalledWith({
         type: SharedLinkType.INDIVIDUAL,
         userId: authStub.admin.user.id,
         albumId: null,
@@ -176,7 +170,7 @@ describe(SharedLinkService.name, () => {
 
     it('should create a shared link with allowDownload set to false when showMetadata is false', async () => {
       accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
-      shareMock.create.mockResolvedValue(sharedLinkStub.individual);
+      sharedLinkMock.create.mockResolvedValue(sharedLinkStub.individual);
 
       await sut.create(authStub.admin, {
         type: SharedLinkType.INDIVIDUAL,
@@ -190,7 +184,7 @@ describe(SharedLinkService.name, () => {
         authStub.admin.user.id,
         new Set([assetStub.image.id]),
       );
-      expect(shareMock.create).toHaveBeenCalledWith({
+      expect(sharedLinkMock.create).toHaveBeenCalledWith({
         type: SharedLinkType.INDIVIDUAL,
         userId: authStub.admin.user.id,
         albumId: null,
@@ -207,18 +201,18 @@ describe(SharedLinkService.name, () => {
 
   describe('update', () => {
     it('should throw an error for an invalid shared link', async () => {
-      shareMock.get.mockResolvedValue(null);
+      sharedLinkMock.get.mockResolvedValue(null);
       await expect(sut.update(authStub.user1, 'missing-id', {})).rejects.toBeInstanceOf(BadRequestException);
-      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
-      expect(shareMock.update).not.toHaveBeenCalled();
+      expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
+      expect(sharedLinkMock.update).not.toHaveBeenCalled();
     });
 
     it('should update a shared link', async () => {
-      shareMock.get.mockResolvedValue(sharedLinkStub.valid);
-      shareMock.update.mockResolvedValue(sharedLinkStub.valid);
+      sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid);
+      sharedLinkMock.update.mockResolvedValue(sharedLinkStub.valid);
       await sut.update(authStub.user1, sharedLinkStub.valid.id, { allowDownload: false });
-      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
-      expect(shareMock.update).toHaveBeenCalledWith({
+      expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
+      expect(sharedLinkMock.update).toHaveBeenCalledWith({
         id: sharedLinkStub.valid.id,
         userId: authStub.user1.user.id,
         allowDownload: false,
@@ -228,31 +222,31 @@ describe(SharedLinkService.name, () => {
 
   describe('remove', () => {
     it('should throw an error for an invalid shared link', async () => {
-      shareMock.get.mockResolvedValue(null);
+      sharedLinkMock.get.mockResolvedValue(null);
       await expect(sut.remove(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException);
-      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
-      expect(shareMock.update).not.toHaveBeenCalled();
+      expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
+      expect(sharedLinkMock.update).not.toHaveBeenCalled();
     });
 
     it('should remove a key', async () => {
-      shareMock.get.mockResolvedValue(sharedLinkStub.valid);
+      sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid);
       await sut.remove(authStub.user1, sharedLinkStub.valid.id);
-      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
-      expect(shareMock.remove).toHaveBeenCalledWith(sharedLinkStub.valid);
+      expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
+      expect(sharedLinkMock.remove).toHaveBeenCalledWith(sharedLinkStub.valid);
     });
   });
 
   describe('addAssets', () => {
     it('should not work on album shared links', async () => {
-      shareMock.get.mockResolvedValue(sharedLinkStub.valid);
+      sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid);
       await expect(sut.addAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
         BadRequestException,
       );
     });
 
     it('should add assets to a shared link', async () => {
-      shareMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual));
-      shareMock.create.mockResolvedValue(sharedLinkStub.individual);
+      sharedLinkMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual));
+      sharedLinkMock.create.mockResolvedValue(sharedLinkStub.individual);
       accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-3']));
 
       await expect(
@@ -264,7 +258,7 @@ describe(SharedLinkService.name, () => {
       ]);
 
       expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledTimes(1);
-      expect(shareMock.update).toHaveBeenCalledWith({
+      expect(sharedLinkMock.update).toHaveBeenCalledWith({
         ...sharedLinkStub.individual,
         assets: [assetStub.image, { id: 'asset-3' }],
       });
@@ -273,15 +267,15 @@ describe(SharedLinkService.name, () => {
 
   describe('removeAssets', () => {
     it('should not work on album shared links', async () => {
-      shareMock.get.mockResolvedValue(sharedLinkStub.valid);
+      sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid);
       await expect(sut.removeAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
         BadRequestException,
       );
     });
 
     it('should remove assets from a shared link', async () => {
-      shareMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual));
-      shareMock.create.mockResolvedValue(sharedLinkStub.individual);
+      sharedLinkMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual));
+      sharedLinkMock.create.mockResolvedValue(sharedLinkStub.individual);
 
       await expect(
         sut.removeAssets(authStub.admin, 'link-1', { assetIds: [assetStub.image.id, 'asset-2'] }),
@@ -290,29 +284,39 @@ describe(SharedLinkService.name, () => {
         { assetId: 'asset-2', success: false, error: AssetIdErrorReason.NOT_FOUND },
       ]);
 
-      expect(shareMock.update).toHaveBeenCalledWith({ ...sharedLinkStub.individual, assets: [] });
+      expect(sharedLinkMock.update).toHaveBeenCalledWith({ ...sharedLinkStub.individual, assets: [] });
     });
   });
 
   describe('getMetadataTags', () => {
     it('should return null when auth is not a shared link', async () => {
       await expect(sut.getMetadataTags(authStub.admin)).resolves.toBe(null);
-      expect(shareMock.get).not.toHaveBeenCalled();
+      expect(sharedLinkMock.get).not.toHaveBeenCalled();
     });
 
     it('should return null when shared link has a password', async () => {
       await expect(sut.getMetadataTags(authStub.passwordSharedLink)).resolves.toBe(null);
-      expect(shareMock.get).not.toHaveBeenCalled();
+      expect(sharedLinkMock.get).not.toHaveBeenCalled();
     });
 
     it('should return metadata tags', async () => {
-      shareMock.get.mockResolvedValue(sharedLinkStub.individual);
+      sharedLinkMock.get.mockResolvedValue(sharedLinkStub.individual);
       await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
         description: '1 shared photos & videos',
-        imageUrl: `${DEFAULT_EXTERNAL_DOMAIN}/api/assets/asset-id/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`,
+        imageUrl: `http://localhost:2283/api/assets/asset-id/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`,
         title: 'Public Share',
       });
-      expect(shareMock.get).toHaveBeenCalled();
+      expect(sharedLinkMock.get).toHaveBeenCalled();
+    });
+
+    it('should return metadata tags with a default image path if the asset id is not set', async () => {
+      sharedLinkMock.get.mockResolvedValue({ ...sharedLinkStub.individual, album: undefined, assets: [] });
+      await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
+        description: '0 shared photos & videos',
+        imageUrl: `http://localhost:2283/feature-panel.png`,
+        title: 'Public Share',
+      });
+      expect(sharedLinkMock.get).toHaveBeenCalled();
     });
   });
 });
diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts
index 54c7fdf25b..5ef140d26d 100644
--- a/server/src/services/shared-link.service.ts
+++ b/server/src/services/shared-link.service.ts
@@ -1,45 +1,25 @@
-import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
-import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
-import { SystemConfigCore } from 'src/cores/system-config.core';
+import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
 import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
 import { AssetIdsDto } from 'src/dtos/asset.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
 import {
+  mapSharedLink,
+  mapSharedLinkWithoutMetadata,
   SharedLinkCreateDto,
   SharedLinkEditDto,
   SharedLinkPasswordDto,
   SharedLinkResponseDto,
-  mapSharedLink,
-  mapSharedLinkWithoutMetadata,
 } from 'src/dtos/shared-link.dto';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { SharedLinkEntity } from 'src/entities/shared-link.entity';
 import { Permission, SharedLinkType } from 'src/enum';
-import { IAccessRepository } from 'src/interfaces/access.interface';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
-import { checkAccess, requireAccess } from 'src/utils/access';
-import { OpenGraphTags } from 'src/utils/misc';
+import { BaseService } from 'src/services/base.service';
+import { getExternalDomain, OpenGraphTags } from 'src/utils/misc';
 
 @Injectable()
-export class SharedLinkService {
-  private configCore: SystemConfigCore;
-
-  constructor(
-    @Inject(IAccessRepository) private access: IAccessRepository,
-    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-    @Inject(ISharedLinkRepository) private repository: ISharedLinkRepository,
-    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
-  ) {
-    this.logger.setContext(SharedLinkService.name);
-    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
-  }
-
-  getAll(auth: AuthDto): Promise<SharedLinkResponseDto[]> {
-    return this.repository.getAll(auth.user.id).then((links) => links.map((link) => mapSharedLink(link)));
+export class SharedLinkService extends BaseService {
+  async getAll(auth: AuthDto): Promise<SharedLinkResponseDto[]> {
+    return this.sharedLinkRepository.getAll(auth.user.id).then((links) => links.map((link) => mapSharedLink(link)));
   }
 
   async getMine(auth: AuthDto, dto: SharedLinkPasswordDto): Promise<SharedLinkResponseDto> {
@@ -67,7 +47,7 @@ export class SharedLinkService {
         if (!dto.albumId) {
           throw new BadRequestException('Invalid albumId');
         }
-        await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [dto.albumId] });
+        await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [dto.albumId] });
         break;
       }
 
@@ -76,13 +56,13 @@ export class SharedLinkService {
           throw new BadRequestException('Invalid assetIds');
         }
 
-        await requireAccess(this.access, { auth, permission: Permission.ASSET_SHARE, ids: dto.assetIds });
+        await this.requireAccess({ auth, permission: Permission.ASSET_SHARE, ids: dto.assetIds });
 
         break;
       }
     }
 
-    const sharedLink = await this.repository.create({
+    const sharedLink = await this.sharedLinkRepository.create({
       key: this.cryptoRepository.randomBytes(50),
       userId: auth.user.id,
       type: dto.type,
@@ -101,7 +81,7 @@ export class SharedLinkService {
 
   async update(auth: AuthDto, id: string, dto: SharedLinkEditDto) {
     await this.findOrFail(auth.user.id, id);
-    const sharedLink = await this.repository.update({
+    const sharedLink = await this.sharedLinkRepository.update({
       id,
       userId: auth.user.id,
       description: dto.description,
@@ -116,12 +96,12 @@ export class SharedLinkService {
 
   async remove(auth: AuthDto, id: string): Promise<void> {
     const sharedLink = await this.findOrFail(auth.user.id, id);
-    await this.repository.remove(sharedLink);
+    await this.sharedLinkRepository.remove(sharedLink);
   }
 
   // TODO: replace `userId` with permissions and access control checks
   private async findOrFail(userId: string, id: string) {
-    const sharedLink = await this.repository.get(userId, id);
+    const sharedLink = await this.sharedLinkRepository.get(userId, id);
     if (!sharedLink) {
       throw new BadRequestException('Shared link not found');
     }
@@ -137,7 +117,7 @@ export class SharedLinkService {
 
     const existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id));
     const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId));
-    const allowedAssetIds = await checkAccess(this.access, {
+    const allowedAssetIds = await this.checkAccess({
       auth,
       permission: Permission.ASSET_SHARE,
       ids: notPresentAssetIds,
@@ -161,7 +141,7 @@ export class SharedLinkService {
       sharedLink.assets.push({ id: assetId } as AssetEntity);
     }
 
-    await this.repository.update(sharedLink);
+    await this.sharedLinkRepository.update(sharedLink);
 
     return results;
   }
@@ -185,7 +165,7 @@ export class SharedLinkService {
       sharedLink.assets = sharedLink.assets.filter((asset) => asset.id !== assetId);
     }
 
-    await this.repository.update(sharedLink);
+    await this.sharedLinkRepository.update(sharedLink);
 
     return results;
   }
@@ -195,7 +175,8 @@ export class SharedLinkService {
       return null;
     }
 
-    const config = await this.configCore.getConfig({ withCache: true });
+    const config = await this.getConfig({ withCache: true });
+    const { port } = this.configRepository.getEnv();
     const sharedLink = await this.findOrFail(auth.sharedLink.userId, auth.sharedLink.id);
     const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
     const assetCount = sharedLink.assets.length > 0 ? sharedLink.assets.length : sharedLink.album?.assets.length || 0;
@@ -206,7 +187,7 @@ export class SharedLinkService {
     return {
       title: sharedLink.album ? sharedLink.album.albumName : 'Public Share',
       description: sharedLink.description || `${assetCount} shared photos & videos`,
-      imageUrl: new URL(imagePath, config.server.externalDomain || DEFAULT_EXTERNAL_DOMAIN).href,
+      imageUrl: new URL(imagePath, getExternalDomain(config.server, port)).href,
     };
   }
 
diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts
index 97d22da9b8..0b0ee6b20f 100644
--- a/server/src/services/smart-info.service.spec.ts
+++ b/server/src/services/smart-info.service.spec.ts
@@ -1,8 +1,9 @@
 import { SystemConfig } from 'src/config';
+import { ImmichWorker } from 'src/enum';
 import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
+import { IConfigRepository } from 'src/interfaces/config.interface';
 import { IDatabaseRepository } from 'src/interfaces/database.interface';
 import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
 import { ISearchRepository } from 'src/interfaces/search.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
@@ -10,36 +11,26 @@ import { SmartInfoService } from 'src/services/smart-info.service';
 import { getCLIPModelInfo } from 'src/utils/misc';
 import { assetStub } from 'test/fixtures/asset.stub';
 import { systemConfigStub } from 'test/fixtures/system-config.stub';
-import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
-import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
-import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock';
-import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(SmartInfoService.name, () => {
   let sut: SmartInfoService;
+
   let assetMock: Mocked<IAssetRepository>;
-  let systemMock: Mocked<ISystemMetadataRepository>;
-  let jobMock: Mocked<IJobRepository>;
-  let searchMock: Mocked<ISearchRepository>;
-  let machineMock: Mocked<IMachineLearningRepository>;
   let databaseMock: Mocked<IDatabaseRepository>;
-  let loggerMock: Mocked<ILoggerRepository>;
+  let jobMock: Mocked<IJobRepository>;
+  let machineLearningMock: Mocked<IMachineLearningRepository>;
+  let searchMock: Mocked<ISearchRepository>;
+  let systemMock: Mocked<ISystemMetadataRepository>;
+  let configMock: Mocked<IConfigRepository>;
 
   beforeEach(() => {
-    assetMock = newAssetRepositoryMock();
-    systemMock = newSystemMetadataRepositoryMock();
-    searchMock = newSearchRepositoryMock();
-    jobMock = newJobRepositoryMock();
-    machineMock = newMachineLearningRepositoryMock();
-    databaseMock = newDatabaseRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-    sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, searchMock, systemMock, loggerMock);
+    ({ sut, assetMock, databaseMock, jobMock, machineLearningMock, searchMock, systemMock, configMock } =
+      newTestService(SmartInfoService));
 
     assetMock.getByIds.mockResolvedValue([assetStub.image]);
+    configMock.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES);
   });
 
   it('should work', () => {
@@ -75,26 +66,10 @@ describe(SmartInfoService.name, () => {
     });
   });
 
-  describe('onBootstrapEvent', () => {
-    it('should return if not microservices', async () => {
-      await sut.onBootstrap('api');
-
-      expect(systemMock.get).not.toHaveBeenCalled();
-      expect(searchMock.getDimensionSize).not.toHaveBeenCalled();
-      expect(searchMock.setDimensionSize).not.toHaveBeenCalled();
-      expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
-      expect(jobMock.getQueueStatus).not.toHaveBeenCalled();
-      expect(jobMock.pause).not.toHaveBeenCalled();
-      expect(jobMock.waitForQueueCompletion).not.toHaveBeenCalled();
-      expect(jobMock.resume).not.toHaveBeenCalled();
-    });
-
+  describe('onConfigInit', () => {
     it('should return if machine learning is disabled', async () => {
-      systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
+      await sut.onConfigInit({ newConfig: systemConfigStub.machineLearningDisabled as SystemConfig });
 
-      await sut.onBootstrap('microservices');
-
-      expect(systemMock.get).toHaveBeenCalledTimes(1);
       expect(searchMock.getDimensionSize).not.toHaveBeenCalled();
       expect(searchMock.setDimensionSize).not.toHaveBeenCalled();
       expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
@@ -107,9 +82,8 @@ describe(SmartInfoService.name, () => {
     it('should return if model and DB dimension size are equal', async () => {
       searchMock.getDimensionSize.mockResolvedValue(512);
 
-      await sut.onBootstrap('microservices');
+      await sut.onConfigInit({ newConfig: systemConfigStub.machineLearningEnabled as SystemConfig });
 
-      expect(systemMock.get).toHaveBeenCalledTimes(1);
       expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
       expect(searchMock.setDimensionSize).not.toHaveBeenCalled();
       expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
@@ -123,9 +97,8 @@ describe(SmartInfoService.name, () => {
       searchMock.getDimensionSize.mockResolvedValue(768);
       jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
 
-      await sut.onBootstrap('microservices');
+      await sut.onConfigInit({ newConfig: systemConfigStub.machineLearningEnabled as SystemConfig });
 
-      expect(systemMock.get).toHaveBeenCalledTimes(1);
       expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
       expect(searchMock.setDimensionSize).toHaveBeenCalledWith(512);
       expect(jobMock.getQueueStatus).toHaveBeenCalledTimes(1);
@@ -138,9 +111,8 @@ describe(SmartInfoService.name, () => {
       searchMock.getDimensionSize.mockResolvedValue(768);
       jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: true });
 
-      await sut.onBootstrap('microservices');
+      await sut.onConfigInit({ newConfig: systemConfigStub.machineLearningEnabled as SystemConfig });
 
-      expect(systemMock.get).toHaveBeenCalledTimes(1);
       expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
       expect(searchMock.setDimensionSize).toHaveBeenCalledWith(512);
       expect(jobMock.getQueueStatus).toHaveBeenCalledTimes(1);
@@ -299,7 +271,7 @@ describe(SmartInfoService.name, () => {
       expect(await sut.handleEncodeClip({ id: '123' })).toEqual(JobStatus.SKIPPED);
 
       expect(assetMock.getByIds).not.toHaveBeenCalled();
-      expect(machineMock.encodeImage).not.toHaveBeenCalled();
+      expect(machineLearningMock.encodeImage).not.toHaveBeenCalled();
     });
 
     it('should skip assets without a resize path', async () => {
@@ -308,16 +280,16 @@ describe(SmartInfoService.name, () => {
       expect(await sut.handleEncodeClip({ id: assetStub.noResizePath.id })).toEqual(JobStatus.FAILED);
 
       expect(searchMock.upsert).not.toHaveBeenCalled();
-      expect(machineMock.encodeImage).not.toHaveBeenCalled();
+      expect(machineLearningMock.encodeImage).not.toHaveBeenCalled();
     });
 
     it('should save the returned objects', async () => {
-      machineMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]);
+      machineLearningMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]);
 
       expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS);
 
-      expect(machineMock.encodeImage).toHaveBeenCalledWith(
-        'http://immich-machine-learning:3003',
+      expect(machineLearningMock.encodeImage).toHaveBeenCalledWith(
+        ['http://immich-machine-learning:3003'],
         '/uploads/user-id/thumbs/path.jpg',
         expect.objectContaining({ modelName: 'ViT-B-32__openai' }),
       );
@@ -329,9 +301,33 @@ describe(SmartInfoService.name, () => {
 
       expect(await sut.handleEncodeClip({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED);
 
-      expect(machineMock.encodeImage).not.toHaveBeenCalled();
+      expect(machineLearningMock.encodeImage).not.toHaveBeenCalled();
       expect(searchMock.upsert).not.toHaveBeenCalled();
     });
+
+    it('should fail if asset could not be found', async () => {
+      assetMock.getByIds.mockResolvedValue([]);
+
+      expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.FAILED);
+
+      expect(machineLearningMock.encodeImage).not.toHaveBeenCalled();
+      expect(searchMock.upsert).not.toHaveBeenCalled();
+    });
+
+    it('should wait for database', async () => {
+      machineLearningMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]);
+      databaseMock.isBusy.mockReturnValue(true);
+
+      expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS);
+
+      expect(databaseMock.wait).toHaveBeenCalledWith(512);
+      expect(machineLearningMock.encodeImage).toHaveBeenCalledWith(
+        ['http://immich-machine-learning:3003'],
+        '/uploads/user-id/thumbs/path.jpg',
+        expect.objectContaining({ modelName: 'ViT-B-32__openai' }),
+      );
+      expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, [0.01, 0.02, 0.03]);
+    });
   });
 
   describe('getCLIPModelInfo', () => {
diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts
index a75594100f..8fef961fe1 100644
--- a/server/src/services/smart-info.service.ts
+++ b/server/src/services/smart-info.service.ts
@@ -1,55 +1,29 @@
-import { Inject, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
 import { SystemConfig } from 'src/config';
-import { SystemConfigCore } from 'src/cores/system-config.core';
-import { OnEmit } from 'src/decorators';
-import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
-import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
+import { OnEvent, OnJob } from 'src/decorators';
+import { ImmichWorker } from 'src/enum';
+import { WithoutProperty } from 'src/interfaces/asset.interface';
+import { DatabaseLock } from 'src/interfaces/database.interface';
 import { ArgOf } from 'src/interfaces/event.interface';
-import {
-  IBaseJob,
-  IEntityJob,
-  IJobRepository,
-  JOBS_ASSET_PAGINATION_SIZE,
-  JobName,
-  JobStatus,
-  QueueName,
-} from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
-import { ISearchRepository } from 'src/interfaces/search.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { JOBS_ASSET_PAGINATION_SIZE, JobName, JobOf, JobStatus, QueueName } from 'src/interfaces/job.interface';
+import { BaseService } from 'src/services/base.service';
 import { getAssetFiles } from 'src/utils/asset.util';
 import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc';
 import { usePagination } from 'src/utils/pagination';
 
 @Injectable()
-export class SmartInfoService {
-  private configCore: SystemConfigCore;
-
-  constructor(
-    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
-    @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
-    @Inject(IJobRepository) private jobRepository: IJobRepository,
-    @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
-    @Inject(ISearchRepository) private repository: ISearchRepository,
-    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-  ) {
-    this.logger.setContext(SmartInfoService.name);
-    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
+export class SmartInfoService extends BaseService {
+  @OnEvent({ name: 'config.init', workers: [ImmichWorker.MICROSERVICES] })
+  async onConfigInit({ newConfig }: ArgOf<'config.init'>) {
+    await this.init(newConfig);
   }
 
-  @OnEmit({ event: 'app.bootstrap' })
-  async onBootstrap(app: ArgOf<'app.bootstrap'>) {
-    if (app !== 'microservices') {
-      return;
-    }
-
-    const config = await this.configCore.getConfig({ withCache: false });
-    await this.init(config);
+  @OnEvent({ name: 'config.update', workers: [ImmichWorker.MICROSERVICES], server: true })
+  async onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) {
+    await this.init(newConfig, oldConfig);
   }
 
-  @OnEmit({ event: 'config.validate' })
+  @OnEvent({ name: 'config.validate' })
   onConfigValidate({ newConfig }: ArgOf<'config.validate'>) {
     try {
       getCLIPModelInfo(newConfig.machineLearning.clip.modelName);
@@ -60,11 +34,6 @@ export class SmartInfoService {
     }
   }
 
-  @OnEmit({ event: 'config.update' })
-  async onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) {
-    await this.init(newConfig, oldConfig);
-  }
-
   private async init(newConfig: SystemConfig, oldConfig?: SystemConfig) {
     if (!isSmartSearchEnabled(newConfig.machineLearning)) {
       return;
@@ -72,7 +41,7 @@ export class SmartInfoService {
 
     await this.databaseRepository.withLock(DatabaseLock.CLIPDimSize, async () => {
       const { dimSize } = getCLIPModelInfo(newConfig.machineLearning.clip.modelName);
-      const dbDimSize = await this.repository.getDimensionSize();
+      const dbDimSize = await this.searchRepository.getDimensionSize();
       this.logger.verbose(`Current database CLIP dimension size is ${dbDimSize}`);
 
       const modelChange =
@@ -93,10 +62,10 @@ export class SmartInfoService {
           `Dimension size of model ${newConfig.machineLearning.clip.modelName} is ${dimSize}, but database expects ${dbDimSize}.`,
         );
         this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`);
-        await this.repository.setDimensionSize(dimSize);
+        await this.searchRepository.setDimensionSize(dimSize);
         this.logger.log(`Successfully updated database CLIP dimension size from ${dbDimSize} to ${dimSize}.`);
       } else {
-        await this.repository.deleteAllSearchEmbeddings();
+        await this.searchRepository.deleteAllSearchEmbeddings();
       }
 
       if (!isPaused) {
@@ -105,14 +74,15 @@ export class SmartInfoService {
     });
   }
 
-  async handleQueueEncodeClip({ force }: IBaseJob): Promise<JobStatus> {
-    const { machineLearning } = await this.configCore.getConfig({ withCache: false });
+  @OnJob({ name: JobName.QUEUE_SMART_SEARCH, queue: QueueName.SMART_SEARCH })
+  async handleQueueEncodeClip({ force }: JobOf<JobName.QUEUE_SMART_SEARCH>): Promise<JobStatus> {
+    const { machineLearning } = await this.getConfig({ withCache: false });
     if (!isSmartSearchEnabled(machineLearning)) {
       return JobStatus.SKIPPED;
     }
 
     if (force) {
-      await this.repository.deleteAllSearchEmbeddings();
+      await this.searchRepository.deleteAllSearchEmbeddings();
     }
 
     const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
@@ -130,8 +100,9 @@ export class SmartInfoService {
     return JobStatus.SUCCESS;
   }
 
-  async handleEncodeClip({ id }: IEntityJob): Promise<JobStatus> {
-    const { machineLearning } = await this.configCore.getConfig({ withCache: true });
+  @OnJob({ name: JobName.SMART_SEARCH, queue: QueueName.SMART_SEARCH })
+  async handleEncodeClip({ id }: JobOf<JobName.SMART_SEARCH>): Promise<JobStatus> {
+    const { machineLearning } = await this.getConfig({ withCache: true });
     if (!isSmartSearchEnabled(machineLearning)) {
       return JobStatus.SKIPPED;
     }
@@ -150,8 +121,8 @@ export class SmartInfoService {
       return JobStatus.FAILED;
     }
 
-    const embedding = await this.machineLearning.encodeImage(
-      machineLearning.url,
+    const embedding = await this.machineLearningRepository.encodeImage(
+      machineLearning.urls,
       previewFile.path,
       machineLearning.clip,
     );
@@ -161,7 +132,7 @@ export class SmartInfoService {
       await this.databaseRepository.wait(DatabaseLock.CLIPDimSize);
     }
 
-    await this.repository.upsert(asset.id, embedding);
+    await this.searchRepository.upsert(asset.id, embedding);
 
     return JobStatus.SUCCESS;
   }
diff --git a/server/src/services/stack.service.spec.ts b/server/src/services/stack.service.spec.ts
new file mode 100644
index 0000000000..4e8813145c
--- /dev/null
+++ b/server/src/services/stack.service.spec.ts
@@ -0,0 +1,193 @@
+import { BadRequestException } from '@nestjs/common';
+import { IEventRepository } from 'src/interfaces/event.interface';
+import { IStackRepository } from 'src/interfaces/stack.interface';
+import { StackService } from 'src/services/stack.service';
+import { assetStub, stackStub } from 'test/fixtures/asset.stub';
+import { authStub } from 'test/fixtures/auth.stub';
+import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
+import { newTestService } from 'test/utils';
+import { Mocked } from 'vitest';
+
+describe(StackService.name, () => {
+  let sut: StackService;
+
+  let accessMock: IAccessRepositoryMock;
+  let eventMock: Mocked<IEventRepository>;
+  let stackMock: Mocked<IStackRepository>;
+
+  beforeEach(() => {
+    ({ sut, accessMock, eventMock, stackMock } = newTestService(StackService));
+  });
+
+  it('should be defined', () => {
+    expect(sut).toBeDefined();
+  });
+
+  describe('search', () => {
+    it('should search stacks', async () => {
+      stackMock.search.mockResolvedValue([stackStub('stack-id', [assetStub.image])]);
+
+      await sut.search(authStub.admin, { primaryAssetId: assetStub.image.id });
+      expect(stackMock.search).toHaveBeenCalledWith({
+        ownerId: authStub.admin.user.id,
+        primaryAssetId: assetStub.image.id,
+      });
+    });
+  });
+
+  describe('create', () => {
+    it('should require asset.update permissions', async () => {
+      await expect(
+        sut.create(authStub.admin, { assetIds: [assetStub.image.id, assetStub.image1.id] }),
+      ).rejects.toBeInstanceOf(BadRequestException);
+
+      expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalled();
+      expect(stackMock.create).not.toHaveBeenCalled();
+    });
+
+    it('should create a stack', async () => {
+      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id, assetStub.image1.id]));
+      stackMock.create.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1]));
+      await expect(
+        sut.create(authStub.admin, { assetIds: [assetStub.image.id, assetStub.image1.id] }),
+      ).resolves.toEqual({
+        id: 'stack-id',
+        primaryAssetId: assetStub.image.id,
+        assets: [
+          expect.objectContaining({ id: assetStub.image.id }),
+          expect.objectContaining({ id: assetStub.image1.id }),
+        ],
+      });
+
+      expect(eventMock.emit).toHaveBeenCalledWith('stack.create', {
+        stackId: 'stack-id',
+        userId: authStub.admin.user.id,
+      });
+      expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalled();
+    });
+  });
+
+  describe('get', () => {
+    it('should require stack.read permissions', async () => {
+      await expect(sut.get(authStub.admin, 'stack-id')).rejects.toBeInstanceOf(BadRequestException);
+
+      expect(accessMock.stack.checkOwnerAccess).toHaveBeenCalled();
+      expect(stackMock.getById).not.toHaveBeenCalled();
+    });
+
+    it('should fail if stack could not be found', async () => {
+      accessMock.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
+
+      await expect(sut.get(authStub.admin, 'stack-id')).rejects.toBeInstanceOf(Error);
+
+      expect(accessMock.stack.checkOwnerAccess).toHaveBeenCalled();
+      expect(stackMock.getById).toHaveBeenCalledWith('stack-id');
+    });
+
+    it('should get stack', async () => {
+      accessMock.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
+      stackMock.getById.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1]));
+
+      await expect(sut.get(authStub.admin, 'stack-id')).resolves.toEqual({
+        id: 'stack-id',
+        primaryAssetId: assetStub.image.id,
+        assets: [
+          expect.objectContaining({ id: assetStub.image.id }),
+          expect.objectContaining({ id: assetStub.image1.id }),
+        ],
+      });
+      expect(accessMock.stack.checkOwnerAccess).toHaveBeenCalled();
+      expect(stackMock.getById).toHaveBeenCalledWith('stack-id');
+    });
+  });
+
+  describe('update', () => {
+    it('should require stack.update permissions', async () => {
+      await expect(sut.update(authStub.admin, 'stack-id', {})).rejects.toBeInstanceOf(BadRequestException);
+
+      expect(stackMock.getById).not.toHaveBeenCalled();
+      expect(stackMock.update).not.toHaveBeenCalled();
+      expect(eventMock.emit).not.toHaveBeenCalled();
+    });
+
+    it('should fail if stack could not be found', async () => {
+      accessMock.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
+
+      await expect(sut.update(authStub.admin, 'stack-id', {})).rejects.toBeInstanceOf(Error);
+
+      expect(stackMock.getById).toHaveBeenCalledWith('stack-id');
+      expect(stackMock.update).not.toHaveBeenCalled();
+      expect(eventMock.emit).not.toHaveBeenCalled();
+    });
+
+    it('should fail if the provided primary asset id is not in the stack', async () => {
+      accessMock.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
+      stackMock.getById.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1]));
+
+      await expect(sut.update(authStub.admin, 'stack-id', { primaryAssetId: 'unknown-asset' })).rejects.toBeInstanceOf(
+        BadRequestException,
+      );
+
+      expect(stackMock.getById).toHaveBeenCalledWith('stack-id');
+      expect(stackMock.update).not.toHaveBeenCalled();
+      expect(eventMock.emit).not.toHaveBeenCalled();
+    });
+
+    it('should update stack', async () => {
+      accessMock.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
+      stackMock.getById.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1]));
+      stackMock.update.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1]));
+
+      await sut.update(authStub.admin, 'stack-id', { primaryAssetId: assetStub.image1.id });
+
+      expect(stackMock.getById).toHaveBeenCalledWith('stack-id');
+      expect(stackMock.update).toHaveBeenCalledWith({ id: 'stack-id', primaryAssetId: assetStub.image1.id });
+      expect(eventMock.emit).toHaveBeenCalledWith('stack.update', {
+        stackId: 'stack-id',
+        userId: authStub.admin.user.id,
+      });
+    });
+  });
+
+  describe('delete', () => {
+    it('should require stack.delete permissions', async () => {
+      await expect(sut.delete(authStub.admin, 'stack-id')).rejects.toBeInstanceOf(BadRequestException);
+
+      expect(stackMock.delete).not.toHaveBeenCalled();
+      expect(eventMock.emit).not.toHaveBeenCalled();
+    });
+
+    it('should delete stack', async () => {
+      accessMock.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
+
+      await sut.delete(authStub.admin, 'stack-id');
+
+      expect(stackMock.delete).toHaveBeenCalledWith('stack-id');
+      expect(eventMock.emit).toHaveBeenCalledWith('stack.delete', {
+        stackId: 'stack-id',
+        userId: authStub.admin.user.id,
+      });
+    });
+  });
+
+  describe('deleteAll', () => {
+    it('should require stack.delete permissions', async () => {
+      await expect(sut.deleteAll(authStub.admin, { ids: ['stack-id'] })).rejects.toBeInstanceOf(BadRequestException);
+
+      expect(stackMock.deleteAll).not.toHaveBeenCalled();
+      expect(eventMock.emit).not.toHaveBeenCalled();
+    });
+
+    it('should delete all stacks', async () => {
+      accessMock.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
+
+      await sut.deleteAll(authStub.admin, { ids: ['stack-id'] });
+
+      expect(stackMock.deleteAll).toHaveBeenCalledWith(['stack-id']);
+      expect(eventMock.emit).toHaveBeenCalledWith('stacks.delete', {
+        stackIds: ['stack-id'],
+        userId: authStub.admin.user.id,
+      });
+    });
+  });
+});
diff --git a/server/src/services/stack.service.ts b/server/src/services/stack.service.ts
index 29a598d4b4..58fccc8be2 100644
--- a/server/src/services/stack.service.ts
+++ b/server/src/services/stack.service.ts
@@ -1,21 +1,12 @@
-import { BadRequestException, Inject, Injectable } from '@nestjs/common';
+import { BadRequestException, Injectable } from '@nestjs/common';
 import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
 import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto';
 import { Permission } from 'src/enum';
-import { IAccessRepository } from 'src/interfaces/access.interface';
-import { IEventRepository } from 'src/interfaces/event.interface';
-import { IStackRepository } from 'src/interfaces/stack.interface';
-import { requireAccess } from 'src/utils/access';
+import { BaseService } from 'src/services/base.service';
 
 @Injectable()
-export class StackService {
-  constructor(
-    @Inject(IAccessRepository) private access: IAccessRepository,
-    @Inject(IEventRepository) private eventRepository: IEventRepository,
-    @Inject(IStackRepository) private stackRepository: IStackRepository,
-  ) {}
-
+export class StackService extends BaseService {
   async search(auth: AuthDto, dto: StackSearchDto): Promise<StackResponseDto[]> {
     const stacks = await this.stackRepository.search({
       ownerId: auth.user.id,
@@ -26,7 +17,7 @@ export class StackService {
   }
 
   async create(auth: AuthDto, dto: StackCreateDto): Promise<StackResponseDto> {
-    await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds });
+    await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds });
 
     const stack = await this.stackRepository.create({ ownerId: auth.user.id, assetIds: dto.assetIds });
 
@@ -36,13 +27,13 @@ export class StackService {
   }
 
   async get(auth: AuthDto, id: string): Promise<StackResponseDto> {
-    await requireAccess(this.access, { auth, permission: Permission.STACK_READ, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.STACK_READ, ids: [id] });
     const stack = await this.findOrFail(id);
     return mapStack(stack, { auth });
   }
 
   async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise<StackResponseDto> {
-    await requireAccess(this.access, { auth, permission: Permission.STACK_UPDATE, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.STACK_UPDATE, ids: [id] });
     const stack = await this.findOrFail(id);
     if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) {
       throw new BadRequestException('Primary asset must be in the stack');
@@ -56,13 +47,13 @@ export class StackService {
   }
 
   async delete(auth: AuthDto, id: string): Promise<void> {
-    await requireAccess(this.access, { auth, permission: Permission.STACK_DELETE, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.STACK_DELETE, ids: [id] });
     await this.stackRepository.delete(id);
     await this.eventRepository.emit('stack.delete', { stackId: id, userId: auth.user.id });
   }
 
   async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise<void> {
-    await requireAccess(this.access, { auth, permission: Permission.STACK_DELETE, ids: dto.ids });
+    await this.requireAccess({ auth, permission: Permission.STACK_DELETE, ids: dto.ids });
     await this.stackRepository.deleteAll(dto.ids);
     await this.eventRepository.emit('stacks.delete', { stackIds: dto.ids, userId: auth.user.id });
   }
diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts
index 093cc5b2ff..728e891d05 100644
--- a/server/src/services/storage-template.service.spec.ts
+++ b/server/src/services/storage-template.service.spec.ts
@@ -1,16 +1,12 @@
 import { Stats } from 'node:fs';
 import { SystemConfig, defaults } from 'src/config';
-import { SystemConfigCore } from 'src/cores/system-config.core';
 import { AssetEntity } from 'src/entities/asset.entity';
-import { AssetPathType } from 'src/entities/move.entity';
+import { AssetPathType } from 'src/enum';
 import { IAlbumRepository } from 'src/interfaces/album.interface';
 import { IAssetRepository } from 'src/interfaces/asset.interface';
 import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import { IDatabaseRepository } from 'src/interfaces/database.interface';
 import { JobStatus } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { IMoveRepository } from 'src/interfaces/move.interface';
-import { IPersonRepository } from 'src/interfaces/person.interface';
 import { IStorageRepository } from 'src/interfaces/storage.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { IUserRepository } from 'src/interfaces/user.interface';
@@ -18,63 +14,31 @@ import { StorageTemplateService } from 'src/services/storage-template.service';
 import { albumStub } from 'test/fixtures/album.stub';
 import { assetStub } from 'test/fixtures/asset.stub';
 import { userStub } from 'test/fixtures/user.stub';
-import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
-import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
-import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
-import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock';
-import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
-import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
-import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(StorageTemplateService.name, () => {
   let sut: StorageTemplateService;
+
   let albumMock: Mocked<IAlbumRepository>;
   let assetMock: Mocked<IAssetRepository>;
   let cryptoMock: Mocked<ICryptoRepository>;
-  let databaseMock: Mocked<IDatabaseRepository>;
   let moveMock: Mocked<IMoveRepository>;
-  let personMock: Mocked<IPersonRepository>;
   let storageMock: Mocked<IStorageRepository>;
   let systemMock: Mocked<ISystemMetadataRepository>;
   let userMock: Mocked<IUserRepository>;
-  let loggerMock: Mocked<ILoggerRepository>;
 
   it('should work', () => {
     expect(sut).toBeDefined();
   });
 
   beforeEach(() => {
-    assetMock = newAssetRepositoryMock();
-    albumMock = newAlbumRepositoryMock();
-    cryptoMock = newCryptoRepositoryMock();
-    databaseMock = newDatabaseRepositoryMock();
-    moveMock = newMoveRepositoryMock();
-    personMock = newPersonRepositoryMock();
-    storageMock = newStorageRepositoryMock();
-    systemMock = newSystemMetadataRepositoryMock();
-    userMock = newUserRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
+    ({ sut, albumMock, assetMock, cryptoMock, moveMock, storageMock, systemMock, userMock } =
+      newTestService(StorageTemplateService));
 
     systemMock.get.mockResolvedValue({ storageTemplate: { enabled: true } });
 
-    sut = new StorageTemplateService(
-      albumMock,
-      assetMock,
-      systemMock,
-      moveMock,
-      personMock,
-      storageMock,
-      userMock,
-      cryptoMock,
-      databaseMock,
-      loggerMock,
-    );
-
-    SystemConfigCore.create(systemMock, loggerMock).config$.next(defaults);
+    sut.onConfigInit({ newConfig: defaults });
   });
 
   describe('onConfigValidate', () => {
@@ -106,6 +70,41 @@ describe(StorageTemplateService.name, () => {
     });
   });
 
+  describe('getStorageTemplateOptions', () => {
+    it('should send back the datetime variables', () => {
+      expect(sut.getStorageTemplateOptions()).toEqual({
+        dayOptions: ['d', 'dd'],
+        hourOptions: ['h', 'hh', 'H', 'HH'],
+        minuteOptions: ['m', 'mm'],
+        monthOptions: ['M', 'MM', 'MMM', 'MMMM'],
+        presetOptions: [
+          '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
+          '{{y}}/{{MM}}-{{dd}}/{{filename}}',
+          '{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
+          '{{y}}/{{MM}}/{{filename}}',
+          '{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}',
+          '{{y}}/{{MMM}}/{{filename}}',
+          '{{y}}/{{MMMM}}/{{filename}}',
+          '{{y}}/{{MM}}/{{dd}}/{{filename}}',
+          '{{y}}/{{MMMM}}/{{dd}}/{{filename}}',
+          '{{y}}/{{y}}-{{MM}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
+          '{{y}}-{{MM}}-{{dd}}/{{filename}}',
+          '{{y}}-{{MMM}}-{{dd}}/{{filename}}',
+          '{{y}}-{{MMMM}}-{{dd}}/{{filename}}',
+          '{{y}}/{{y}}-{{MM}}/{{filename}}',
+          '{{y}}/{{y}}-{{WW}}/{{filename}}',
+          '{{y}}/{{y}}-{{MM}}-{{dd}}/{{assetId}}',
+          '{{y}}/{{y}}-{{MM}}/{{assetId}}',
+          '{{y}}/{{y}}-{{WW}}/{{assetId}}',
+          '{{album}}/{{filename}}',
+        ],
+        secondOptions: ['s', 'ss', 'SSS'],
+        weekOptions: ['W', 'WW'],
+        yearOptions: ['y', 'yy'],
+      });
+    });
+  });
+
   describe('handleMigrationSingle', () => {
     it('should skip when storage template is disabled', async () => {
       systemMock.get.mockResolvedValue({ storageTemplate: { enabled: false } });
@@ -164,13 +163,15 @@ describe(StorageTemplateService.name, () => {
         originalPath: newMotionPicturePath,
       });
     });
-    it('Should use handlebar if condition for album', async () => {
+
+    it('should use handlebar if condition for album', async () => {
       const asset = assetStub.image;
       const user = userStub.user1;
       const album = albumStub.oneAsset;
       const config = structuredClone(defaults);
       config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}';
-      SystemConfigCore.create(systemMock, loggerMock).config$.next(config);
+
+      sut.onConfigInit({ newConfig: config });
 
       userMock.get.mockResolvedValue(user);
       assetMock.getByIds.mockResolvedValueOnce([asset]);
@@ -185,12 +186,13 @@ describe(StorageTemplateService.name, () => {
         pathType: AssetPathType.ORIGINAL,
       });
     });
-    it('Should use handlebar else condition for album', async () => {
+
+    it('should use handlebar else condition for album', async () => {
       const asset = assetStub.image;
       const user = userStub.user1;
       const config = structuredClone(defaults);
       config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other//{{MM}}{{/if}}/{{filename}}';
-      SystemConfigCore.create(systemMock, loggerMock).config$.next(config);
+      sut.onConfigInit({ newConfig: config });
 
       userMock.get.mockResolvedValue(user);
       assetMock.getByIds.mockResolvedValueOnce([asset]);
@@ -205,6 +207,7 @@ describe(StorageTemplateService.name, () => {
         pathType: AssetPathType.ORIGINAL,
       });
     });
+
     it('should migrate previously failed move from original path when it still exists', async () => {
       userMock.get.mockResolvedValue(userStub.user1);
       const previousFailedNewPath = `upload/library/${userStub.user1.id}/2023/Feb/${assetStub.image.id}.jpg`;
@@ -242,6 +245,7 @@ describe(StorageTemplateService.name, () => {
         originalPath: newPath,
       });
     });
+
     it('should migrate previously failed move from previous new path when old path no longer exists, should validate file size still matches before moving', async () => {
       userMock.get.mockResolvedValue(userStub.user1);
       const previousFailedNewPath = `upload/library/${userStub.user1.id}/2023/Feb/${assetStub.image.id}.jpg`;
diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts
index 9836ad40ac..e8e4bd12a5 100644
--- a/server/src/services/storage-template.service.ts
+++ b/server/src/services/storage-template.service.ts
@@ -1,39 +1,52 @@
-import { Inject, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
 import handlebar from 'handlebars';
 import { DateTime } from 'luxon';
 import path from 'node:path';
 import sanitize from 'sanitize-filename';
-import { SystemConfig } from 'src/config';
-import {
-  supportedDayTokens,
-  supportedHourTokens,
-  supportedMinuteTokens,
-  supportedMonthTokens,
-  supportedSecondTokens,
-  supportedWeekTokens,
-  supportedYearTokens,
-} from 'src/constants';
-import { StorageCore, StorageFolder } from 'src/cores/storage.core';
-import { SystemConfigCore } from 'src/cores/system-config.core';
-import { OnEmit } from 'src/decorators';
+import { StorageCore } from 'src/cores/storage.core';
+import { OnEvent, OnJob } from 'src/decorators';
+import { SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto';
 import { AssetEntity } from 'src/entities/asset.entity';
-import { AssetPathType } from 'src/entities/move.entity';
-import { AssetType } from 'src/enum';
-import { IAlbumRepository } from 'src/interfaces/album.interface';
-import { IAssetRepository } from 'src/interfaces/asset.interface';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
+import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
+import { DatabaseLock } from 'src/interfaces/database.interface';
 import { ArgOf } from 'src/interfaces/event.interface';
-import { IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { IMoveRepository } from 'src/interfaces/move.interface';
-import { IPersonRepository } from 'src/interfaces/person.interface';
-import { IStorageRepository } from 'src/interfaces/storage.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
-import { IUserRepository } from 'src/interfaces/user.interface';
+import { JobName, JobOf, JOBS_ASSET_PAGINATION_SIZE, JobStatus, QueueName } from 'src/interfaces/job.interface';
+import { BaseService } from 'src/services/base.service';
 import { getLivePhotoMotionFilename } from 'src/utils/file';
 import { usePagination } from 'src/utils/pagination';
 
+const storageTokens = {
+  secondOptions: ['s', 'ss', 'SSS'],
+  minuteOptions: ['m', 'mm'],
+  dayOptions: ['d', 'dd'],
+  weekOptions: ['W', 'WW'],
+  hourOptions: ['h', 'hh', 'H', 'HH'],
+  yearOptions: ['y', 'yy'],
+  monthOptions: ['M', 'MM', 'MMM', 'MMMM'],
+};
+
+const storagePresets = [
+  '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
+  '{{y}}/{{MM}}-{{dd}}/{{filename}}',
+  '{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
+  '{{y}}/{{MM}}/{{filename}}',
+  '{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}',
+  '{{y}}/{{MMM}}/{{filename}}',
+  '{{y}}/{{MMMM}}/{{filename}}',
+  '{{y}}/{{MM}}/{{dd}}/{{filename}}',
+  '{{y}}/{{MMMM}}/{{dd}}/{{filename}}',
+  '{{y}}/{{y}}-{{MM}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
+  '{{y}}-{{MM}}-{{dd}}/{{filename}}',
+  '{{y}}-{{MMM}}-{{dd}}/{{filename}}',
+  '{{y}}-{{MMMM}}-{{dd}}/{{filename}}',
+  '{{y}}/{{y}}-{{MM}}/{{filename}}',
+  '{{y}}/{{y}}-{{WW}}/{{filename}}',
+  '{{y}}/{{y}}-{{MM}}-{{dd}}/{{assetId}}',
+  '{{y}}/{{y}}-{{MM}}/{{assetId}}',
+  '{{y}}/{{y}}-{{WW}}/{{assetId}}',
+  '{{album}}/{{filename}}',
+];
+
 export interface MoveAssetMetadata {
   storageLabel: string | null;
   filename: string;
@@ -47,9 +60,7 @@ interface RenderMetadata {
 }
 
 @Injectable()
-export class StorageTemplateService {
-  private configCore: SystemConfigCore;
-  private storageCore: StorageCore;
+export class StorageTemplateService extends BaseService {
   private _template: {
     compiled: HandlebarsTemplateDelegate<any>;
     raw: string;
@@ -63,33 +74,21 @@ export class StorageTemplateService {
     return this._template;
   }
 
-  constructor(
-    @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
-    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
-    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
-    @Inject(IMoveRepository) moveRepository: IMoveRepository,
-    @Inject(IPersonRepository) personRepository: IPersonRepository,
-    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
-    @Inject(IUserRepository) private userRepository: IUserRepository,
-    @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
-    @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-  ) {
-    this.logger.setContext(StorageTemplateService.name);
-    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
-    this.configCore.config$.subscribe((config) => this.onConfig(config));
-    this.storageCore = StorageCore.create(
-      assetRepository,
-      cryptoRepository,
-      moveRepository,
-      personRepository,
-      storageRepository,
-      systemMetadataRepository,
-      this.logger,
-    );
+  @OnEvent({ name: 'config.init' })
+  onConfigInit({ newConfig }: ArgOf<'config.init'>) {
+    const template = newConfig.storageTemplate.template;
+    if (!this._template || template !== this.template.raw) {
+      this.logger.debug(`Compiling new storage template: ${template}`);
+      this._template = this.compile(template);
+    }
   }
 
-  @OnEmit({ event: 'config.validate' })
+  @OnEvent({ name: 'config.update', server: true })
+  onConfigUpdate({ newConfig }: ArgOf<'config.update'>) {
+    this.onConfigInit({ newConfig });
+  }
+
+  @OnEvent({ name: 'config.validate' })
   onConfigValidate({ newConfig }: ArgOf<'config.validate'>) {
     try {
       const { compiled } = this.compile(newConfig.storageTemplate.template);
@@ -110,8 +109,13 @@ export class StorageTemplateService {
     }
   }
 
-  async handleMigrationSingle({ id }: IEntityJob): Promise<JobStatus> {
-    const config = await this.configCore.getConfig({ withCache: true });
+  getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
+    return { ...storageTokens, presetOptions: storagePresets };
+  }
+
+  @OnJob({ name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, queue: QueueName.STORAGE_TEMPLATE_MIGRATION })
+  async handleMigrationSingle({ id }: JobOf<JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE>): Promise<JobStatus> {
+    const config = await this.getConfig({ withCache: true });
     const storageTemplateEnabled = config.storageTemplate.enabled;
     if (!storageTemplateEnabled) {
       return JobStatus.SKIPPED;
@@ -139,9 +143,10 @@ export class StorageTemplateService {
     return JobStatus.SUCCESS;
   }
 
+  @OnJob({ name: JobName.STORAGE_TEMPLATE_MIGRATION, queue: QueueName.STORAGE_TEMPLATE_MIGRATION })
   async handleMigration(): Promise<JobStatus> {
     this.logger.log('Starting storage template migration');
-    const { storageTemplate } = await this.configCore.getConfig({ withCache: true });
+    const { storageTemplate } = await this.getConfig({ withCache: true });
     const { enabled } = storageTemplate;
     if (!enabled) {
       this.logger.log('Storage template migration disabled, skipping');
@@ -283,14 +288,6 @@ export class StorageTemplateService {
     }
   }
 
-  private onConfig(config: SystemConfig) {
-    const template = config.storageTemplate.template;
-    if (!this._template || template !== this.template.raw) {
-      this.logger.debug(`Compiling new storage template: ${template}`);
-      this._template = this.compile(template);
-    }
-  }
-
   private compile(template: string) {
     return {
       raw: template,
@@ -315,17 +312,7 @@ export class StorageTemplateService {
     const zone = asset.exifInfo?.timeZone || systemTimeZone;
     const dt = DateTime.fromJSDate(asset.fileCreatedAt, { zone });
 
-    const dateTokens = [
-      ...supportedYearTokens,
-      ...supportedMonthTokens,
-      ...supportedWeekTokens,
-      ...supportedDayTokens,
-      ...supportedHourTokens,
-      ...supportedMinuteTokens,
-      ...supportedSecondTokens,
-    ];
-
-    for (const token of dateTokens) {
+    for (const token of Object.values(storageTokens).flat()) {
       substitutions[token] = dt.toFormat(token);
     }
 
diff --git a/server/src/services/storage.service.spec.ts b/server/src/services/storage.service.spec.ts
index b0f38554cb..dd97a063ae 100644
--- a/server/src/services/storage.service.spec.ts
+++ b/server/src/services/storage.service.spec.ts
@@ -1,29 +1,24 @@
 import { SystemMetadataKey } from 'src/enum';
-import { IDatabaseRepository } from 'src/interfaces/database.interface';
+import { IConfigRepository } from 'src/interfaces/config.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { IStorageRepository } from 'src/interfaces/storage.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { StorageService } from 'src/services/storage.service';
-import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
+import { ImmichStartupError } from 'src/utils/misc';
+import { mockEnvData } from 'test/repositories/config.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(StorageService.name, () => {
   let sut: StorageService;
-  let databaseMock: Mocked<IDatabaseRepository>;
-  let storageMock: Mocked<IStorageRepository>;
+
+  let configMock: Mocked<IConfigRepository>;
   let loggerMock: Mocked<ILoggerRepository>;
+  let storageMock: Mocked<IStorageRepository>;
   let systemMock: Mocked<ISystemMetadataRepository>;
 
   beforeEach(() => {
-    databaseMock = newDatabaseRepositoryMock();
-    storageMock = newStorageRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-    systemMock = newSystemMetadataRepositoryMock();
-
-    sut = new StorageService(databaseMock, storageMock, loggerMock, systemMock);
+    ({ sut, configMock, loggerMock, storageMock, systemMock } = newTestService(StorageService));
   });
 
   it('should work', () => {
@@ -36,28 +31,110 @@ describe(StorageService.name, () => {
 
       await expect(sut.onBootstrap()).resolves.toBeUndefined();
 
-      expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.SYSTEM_FLAGS, { mountFiles: true });
+      expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.SYSTEM_FLAGS, {
+        mountChecks: {
+          backups: true,
+          'encoded-video': true,
+          library: true,
+          profile: true,
+          thumbs: true,
+          upload: true,
+        },
+      });
       expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/encoded-video');
       expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/library');
       expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/profile');
       expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs');
+      expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/upload');
+      expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/backups');
+      expect(storageMock.createFile).toHaveBeenCalledWith('upload/encoded-video/.immich', expect.any(Buffer));
+      expect(storageMock.createFile).toHaveBeenCalledWith('upload/library/.immich', expect.any(Buffer));
+      expect(storageMock.createFile).toHaveBeenCalledWith('upload/profile/.immich', expect.any(Buffer));
+      expect(storageMock.createFile).toHaveBeenCalledWith('upload/thumbs/.immich', expect.any(Buffer));
+      expect(storageMock.createFile).toHaveBeenCalledWith('upload/upload/.immich', expect.any(Buffer));
+      expect(storageMock.createFile).toHaveBeenCalledWith('upload/backups/.immich', expect.any(Buffer));
+    });
+
+    it('should enable mount folder checking for a new folder type', async () => {
+      systemMock.get.mockResolvedValue({
+        mountChecks: {
+          backups: false,
+          'encoded-video': true,
+          library: false,
+          profile: true,
+          thumbs: true,
+          upload: true,
+        },
+      });
+
+      await expect(sut.onBootstrap()).resolves.toBeUndefined();
+
+      expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.SYSTEM_FLAGS, {
+        mountChecks: {
+          backups: true,
+          'encoded-video': true,
+          library: true,
+          profile: true,
+          thumbs: true,
+          upload: true,
+        },
+      });
+      expect(storageMock.mkdirSync).toHaveBeenCalledTimes(2);
+      expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/library');
+      expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/backups');
+      expect(storageMock.createFile).toHaveBeenCalledTimes(2);
+      expect(storageMock.createFile).toHaveBeenCalledWith('upload/library/.immich', expect.any(Buffer));
+      expect(storageMock.createFile).toHaveBeenCalledWith('upload/backups/.immich', expect.any(Buffer));
     });
 
     it('should throw an error if .immich is missing', async () => {
-      systemMock.get.mockResolvedValue({ mountFiles: true });
+      systemMock.get.mockResolvedValue({ mountChecks: { upload: true } });
       storageMock.readFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'"));
 
-      await expect(sut.onBootstrap()).rejects.toThrow('Failed to validate folder mount');
+      await expect(sut.onBootstrap()).rejects.toThrow('Failed to read');
 
-      expect(storageMock.writeFile).not.toHaveBeenCalled();
+      expect(storageMock.createOrOverwriteFile).not.toHaveBeenCalled();
       expect(systemMock.set).not.toHaveBeenCalled();
     });
 
     it('should throw an error if .immich is present but read-only', async () => {
-      systemMock.get.mockResolvedValue({ mountFiles: true });
-      storageMock.writeFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'"));
+      systemMock.get.mockResolvedValue({ mountChecks: { upload: true } });
+      storageMock.overwriteFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'"));
 
-      await expect(sut.onBootstrap()).rejects.toThrow('Failed to validate folder mount');
+      await expect(sut.onBootstrap()).rejects.toThrow('Failed to write');
+
+      expect(systemMock.set).not.toHaveBeenCalled();
+    });
+
+    it('should skip mount file creation if file already exists', async () => {
+      const error = new Error('Error creating file') as any;
+      error.code = 'EEXIST';
+      systemMock.get.mockResolvedValue({ mountChecks: {} });
+      storageMock.createFile.mockRejectedValue(error);
+
+      await expect(sut.onBootstrap()).resolves.toBeUndefined();
+
+      expect(loggerMock.warn).toHaveBeenCalledWith('Found existing mount file, skipping creation');
+    });
+
+    it('should throw an error if mount file could not be created', async () => {
+      systemMock.get.mockResolvedValue({ mountChecks: {} });
+      storageMock.createFile.mockRejectedValue(new Error('Error creating file'));
+
+      await expect(sut.onBootstrap()).rejects.toBeInstanceOf(ImmichStartupError);
+      expect(systemMock.set).not.toHaveBeenCalled();
+    });
+
+    it('should startup if checks are disabled', async () => {
+      systemMock.get.mockResolvedValue({ mountChecks: { upload: true } });
+      configMock.getEnv.mockReturnValue(
+        mockEnvData({
+          storage: { ignoreMountCheckErrors: true },
+        }),
+      );
+      storageMock.overwriteFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'"));
+
+      await expect(sut.onBootstrap()).resolves.toBeUndefined();
 
       expect(systemMock.set).not.toHaveBeenCalled();
     });
diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts
index a8f6a76e74..ce26df4869 100644
--- a/server/src/services/storage.service.ts
+++ b/server/src/services/storage.service.ts
@@ -1,55 +1,71 @@
-import { Inject, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
 import { join } from 'node:path';
-import { StorageCore, StorageFolder } from 'src/cores/storage.core';
-import { OnEmit } from 'src/decorators';
-import { SystemMetadataKey } from 'src/enum';
-import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
-import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { IStorageRepository } from 'src/interfaces/storage.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
-import { ImmichStartupError } from 'src/utils/events';
+import { StorageCore } from 'src/cores/storage.core';
+import { OnEvent, OnJob } from 'src/decorators';
+import { SystemFlags } from 'src/entities/system-metadata.entity';
+import { StorageFolder, SystemMetadataKey } from 'src/enum';
+import { DatabaseLock } from 'src/interfaces/database.interface';
+import { JobName, JobOf, JobStatus, QueueName } from 'src/interfaces/job.interface';
+import { BaseService } from 'src/services/base.service';
+import { ImmichStartupError } from 'src/utils/misc';
+
+const docsMessage = `Please see https://immich.app/docs/administration/system-integrity#folder-checks for more information.`;
 
 @Injectable()
-export class StorageService {
-  constructor(
-    @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
-    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-    @Inject(ISystemMetadataRepository) private systemMetadata: ISystemMetadataRepository,
-  ) {
-    this.logger.setContext(StorageService.name);
-  }
-
-  @OnEmit({ event: 'app.bootstrap' })
+export class StorageService extends BaseService {
+  @OnEvent({ name: 'app.bootstrap' })
   async onBootstrap() {
+    const envData = this.configRepository.getEnv();
+
     await this.databaseRepository.withLock(DatabaseLock.SystemFileMounts, async () => {
-      const flags = (await this.systemMetadata.get(SystemMetadataKey.SYSTEM_FLAGS)) || { mountFiles: false };
+      const flags =
+        (await this.systemMetadataRepository.get(SystemMetadataKey.SYSTEM_FLAGS)) ||
+        ({ mountChecks: {} } as SystemFlags);
 
-      this.logger.log('Verifying system mount folder checks');
+      if (!flags.mountChecks) {
+        flags.mountChecks = {};
+      }
 
-      // check each folder exists and is writable
-      for (const folder of Object.values(StorageFolder)) {
-        if (!flags.mountFiles) {
-          this.logger.log(`Writing initial mount file for the ${folder} folder`);
+      let updated = false;
+
+      this.logger.log(`Verifying system mount folder checks, current state: ${JSON.stringify(flags)}`);
+
+      try {
+        // check each folder exists and is writable
+        for (const folder of Object.values(StorageFolder)) {
+          if (!flags.mountChecks[folder]) {
+            this.logger.log(`Writing initial mount file for the ${folder} folder`);
+            await this.createMountFile(folder);
+          }
+
+          await this.verifyReadAccess(folder);
           await this.verifyWriteAccess(folder);
+
+          if (!flags.mountChecks[folder]) {
+            flags.mountChecks[folder] = true;
+            updated = true;
+          }
         }
 
-        await this.verifyReadAccess(folder);
-        await this.verifyWriteAccess(folder);
-      }
+        if (updated) {
+          await this.systemMetadataRepository.set(SystemMetadataKey.SYSTEM_FLAGS, flags);
+          this.logger.log('Successfully enabled system mount folders checks');
+        }
 
-      if (!flags.mountFiles) {
-        flags.mountFiles = true;
-        await this.systemMetadata.set(SystemMetadataKey.SYSTEM_FLAGS, flags);
-        this.logger.log('Successfully enabled system mount folders checks');
+        this.logger.log('Successfully verified system mount folder checks');
+      } catch (error) {
+        if (envData.storage.ignoreMountCheckErrors) {
+          this.logger.error(error);
+          this.logger.warn('Ignoring mount folder errors');
+        } else {
+          throw error;
+        }
       }
-
-      this.logger.log('Successfully verified system mount folder checks');
     });
   }
 
-  async handleDeleteFiles(job: IDeleteFilesJob) {
+  @OnJob({ name: JobName.DELETE_FILES, queue: QueueName.BACKGROUND_TASK })
+  async handleDeleteFiles(job: JobOf<JobName.DELETE_FILES>): Promise<JobStatus> {
     const { files } = job;
 
     // TODO: one job per file
@@ -69,36 +85,45 @@ export class StorageService {
   }
 
   private async verifyReadAccess(folder: StorageFolder) {
-    const { filePath } = this.getMountFilePaths(folder);
+    const { internalPath, externalPath } = this.getMountFilePaths(folder);
     try {
-      await this.storageRepository.readFile(filePath);
+      await this.storageRepository.readFile(internalPath);
     } catch (error) {
-      this.logger.error(`Failed to read ${filePath}: ${error}`);
-      this.logger.error(
-        `The "${folder}" folder appears to be offline/missing, please make sure the volume is mounted with the correct permissions`,
-      );
-      throw new ImmichStartupError(`Failed to validate folder mount (read from "<MEDIA_LOCATION>/${folder}")`);
+      this.logger.error(`Failed to read ${internalPath}: ${error}`);
+      throw new ImmichStartupError(`Failed to read "${externalPath} - ${docsMessage}"`);
+    }
+  }
+
+  private async createMountFile(folder: StorageFolder) {
+    const { folderPath, internalPath, externalPath } = this.getMountFilePaths(folder);
+    try {
+      this.storageRepository.mkdirSync(folderPath);
+      await this.storageRepository.createFile(internalPath, Buffer.from(`${Date.now()}`));
+    } catch (error) {
+      if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
+        this.logger.warn('Found existing mount file, skipping creation');
+        return;
+      }
+      this.logger.error(`Failed to create ${internalPath}: ${error}`);
+      throw new ImmichStartupError(`Failed to create "${externalPath} - ${docsMessage}"`);
     }
   }
 
   private async verifyWriteAccess(folder: StorageFolder) {
-    const { folderPath, filePath } = this.getMountFilePaths(folder);
+    const { internalPath, externalPath } = this.getMountFilePaths(folder);
     try {
-      this.storageRepository.mkdirSync(folderPath);
-      await this.storageRepository.writeFile(filePath, Buffer.from(`${Date.now()}`));
+      await this.storageRepository.overwriteFile(internalPath, Buffer.from(`${Date.now()}`));
     } catch (error) {
-      this.logger.error(`Failed to write ${filePath}: ${error}`);
-      this.logger.error(
-        `The "${folder}" folder cannot be written to, please make sure the volume is mounted with the correct permissions`,
-      );
-      throw new ImmichStartupError(`Failed to validate folder mount (write to "<MEDIA_LOCATION>/${folder}")`);
+      this.logger.error(`Failed to write ${internalPath}: ${error}`);
+      throw new ImmichStartupError(`Failed to write "${externalPath} - ${docsMessage}"`);
     }
   }
 
   private getMountFilePaths(folder: StorageFolder) {
     const folderPath = StorageCore.getBaseFolder(folder);
-    const filePath = join(folderPath, '.immich');
+    const internalPath = join(folderPath, '.immich');
+    const externalPath = `<UPLOAD_LOCATION>/${folder}/.immich`;
 
-    return { folderPath, filePath };
+    return { folderPath, internalPath, externalPath };
   }
 }
diff --git a/server/src/services/sync.service.spec.ts b/server/src/services/sync.service.spec.ts
index a0ded6dba3..8dc270d020 100644
--- a/server/src/services/sync.service.spec.ts
+++ b/server/src/services/sync.service.spec.ts
@@ -1,6 +1,5 @@
 import { mapAsset } from 'src/dtos/asset-response.dto';
 import { AssetEntity } from 'src/entities/asset.entity';
-import { IAccessRepository } from 'src/interfaces/access.interface';
 import { IAssetRepository } from 'src/interfaces/asset.interface';
 import { IAuditRepository } from 'src/interfaces/audit.interface';
 import { IPartnerRepository } from 'src/interfaces/partner.interface';
@@ -8,10 +7,7 @@ import { SyncService } from 'src/services/sync.service';
 import { assetStub } from 'test/fixtures/asset.stub';
 import { authStub } from 'test/fixtures/auth.stub';
 import { partnerStub } from 'test/fixtures/partner.stub';
-import { newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
-import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
-import { newAuditRepositoryMock } from 'test/repositories/audit.repository.mock';
-import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 const untilDate = new Date(2024);
@@ -19,17 +15,13 @@ const mapAssetOpts = { auth: authStub.user1, stripMetadata: false, withStack: tr
 
 describe(SyncService.name, () => {
   let sut: SyncService;
-  let accessMock: Mocked<IAccessRepository>;
+
   let assetMock: Mocked<IAssetRepository>;
-  let partnerMock: Mocked<IPartnerRepository>;
   let auditMock: Mocked<IAuditRepository>;
+  let partnerMock: Mocked<IPartnerRepository>;
 
   beforeEach(() => {
-    partnerMock = newPartnerRepositoryMock();
-    assetMock = newAssetRepositoryMock();
-    accessMock = newAccessRepositoryMock();
-    auditMock = newAuditRepositoryMock();
-    sut = new SyncService(accessMock, assetMock, partnerMock, auditMock);
+    ({ sut, assetMock, auditMock, partnerMock } = newTestService(SyncService));
   });
 
   it('should exist', () => {
diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts
index 7da3fbd9be..f85200db48 100644
--- a/server/src/services/sync.service.ts
+++ b/server/src/services/sync.service.ts
@@ -1,32 +1,20 @@
-import { Inject } from '@nestjs/common';
 import { DateTime } from 'luxon';
 import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
 import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
 import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto';
 import { DatabaseAction, EntityType, Permission } from 'src/enum';
-import { IAccessRepository } from 'src/interfaces/access.interface';
-import { IAssetRepository } from 'src/interfaces/asset.interface';
-import { IAuditRepository } from 'src/interfaces/audit.interface';
-import { IPartnerRepository } from 'src/interfaces/partner.interface';
-import { requireAccess } from 'src/utils/access';
+import { BaseService } from 'src/services/base.service';
 import { getMyPartnerIds } from 'src/utils/asset.util';
 import { setIsEqual } from 'src/utils/set';
 
 const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] };
 
-export class SyncService {
-  constructor(
-    @Inject(IAccessRepository) private access: IAccessRepository,
-    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
-    @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
-    @Inject(IAuditRepository) private auditRepository: IAuditRepository,
-  ) {}
-
+export class SyncService extends BaseService {
   async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise<AssetResponseDto[]> {
     // mobile implementation is faster if this is a single id
     const userId = dto.userId || auth.user.id;
-    await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [userId] });
+    await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: [userId] });
     const assets = await this.assetRepository.getAllForUserFullSync({
       ownerId: userId,
       updatedUntil: dto.updatedUntil,
@@ -50,7 +38,7 @@ export class SyncService {
       return FULL_SYNC;
     }
 
-    await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: dto.userIds });
+    await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: dto.userIds });
 
     const limit = 10_000;
     const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds });
diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts
index 409cd6a52f..2a20f32933 100644
--- a/server/src/services/system-config.service.spec.ts
+++ b/server/src/services/system-config.service.spec.ts
@@ -1,27 +1,25 @@
 import { BadRequestException } from '@nestjs/common';
+import { defaults, SystemConfig } from 'src/config';
 import {
   AudioCodec,
-  CQMode,
   Colorspace,
+  CQMode,
   ImageFormat,
   LogLevel,
-  SystemConfig,
   ToneMapping,
   TranscodeHWAccel,
   TranscodePolicy,
   VideoCodec,
   VideoContainer,
-  defaults,
-} from 'src/config';
-import { SystemMetadataKey } from 'src/enum';
-import { IEventRepository, ServerEvent } from 'src/interfaces/event.interface';
+} from 'src/enum';
+import { IConfigRepository } from 'src/interfaces/config.interface';
+import { IEventRepository } from 'src/interfaces/event.interface';
 import { QueueName } from 'src/interfaces/job.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { SystemConfigService } from 'src/services/system-config.service';
-import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
+import { mockEnvData } from 'test/repositories/config.repository.mock';
+import { newTestService } from 'test/utils';
 import { DeepPartial } from 'typeorm';
 import { Mocked } from 'vitest';
 
@@ -46,12 +44,19 @@ const updatedConfig = Object.freeze<SystemConfig>({
     [QueueName.VIDEO_CONVERSION]: { concurrency: 1 },
     [QueueName.NOTIFICATION]: { concurrency: 5 },
   },
+  backup: {
+    database: {
+      enabled: true,
+      cronExpression: '0 02 * * *',
+      keepLastAmount: 14,
+    },
+  },
   ffmpeg: {
     crf: 30,
     threads: 0,
     preset: 'ultrafast',
     targetAudioCodec: AudioCodec.AAC,
-    acceptedAudioCodecs: [AudioCodec.AAC, AudioCodec.MP3, AudioCodec.LIBOPUS],
+    acceptedAudioCodecs: [AudioCodec.AAC, AudioCodec.MP3, AudioCodec.LIBOPUS, AudioCodec.PCMS16LE],
     targetResolution: '720',
     targetVideoCodec: VideoCodec.H264,
     acceptedVideoCodecs: [VideoCodec.H264],
@@ -60,7 +65,6 @@ const updatedConfig = Object.freeze<SystemConfig>({
     bframes: -1,
     refs: 0,
     gopSize: 0,
-    npl: 0,
     temporalAQ: false,
     cqMode: CQMode.AUTO,
     twoPass: false,
@@ -81,7 +85,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
   },
   machineLearning: {
     enabled: true,
-    url: 'http://immich-machine-learning:3003',
+    urls: ['http://immich-machine-learning:3003'],
     clip: {
       enabled: true,
       modelName: 'ViT-B-32__openai',
@@ -100,8 +104,8 @@ const updatedConfig = Object.freeze<SystemConfig>({
   },
   map: {
     enabled: true,
-    lightStyle: '',
-    darkStyle: '',
+    lightStyle: 'https://tiles.immich.cloud/v1/style/light.json',
+    darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json',
   },
   reverseGeocoding: {
     enabled: true,
@@ -129,6 +133,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
   server: {
     externalDomain: '',
     loginPageMessage: '',
+    publicUsers: true,
   },
   storageTemplate: {
     enabled: false,
@@ -136,11 +141,16 @@ const updatedConfig = Object.freeze<SystemConfig>({
     template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
   },
   image: {
-    thumbnailFormat: ImageFormat.WEBP,
-    thumbnailSize: 250,
-    previewFormat: ImageFormat.JPEG,
-    previewSize: 1440,
-    quality: 80,
+    thumbnail: {
+      size: 250,
+      format: ImageFormat.WEBP,
+      quality: 80,
+    },
+    preview: {
+      size: 1440,
+      format: ImageFormat.JPEG,
+      quality: 80,
+    },
     colorspace: Colorspace.P3,
     extractEmbedded: false,
   },
@@ -180,20 +190,25 @@ const updatedConfig = Object.freeze<SystemConfig>({
       },
     },
   },
+  templates: {
+    email: {
+      albumInviteTemplate: '',
+      welcomeTemplate: '',
+      albumUpdateTemplate: '',
+    },
+  },
 });
 
 describe(SystemConfigService.name, () => {
   let sut: SystemConfigService;
-  let systemMock: Mocked<ISystemMetadataRepository>;
+
+  let configMock: Mocked<IConfigRepository>;
   let eventMock: Mocked<IEventRepository>;
   let loggerMock: Mocked<ILoggerRepository>;
+  let systemMock: Mocked<ISystemMetadataRepository>;
 
   beforeEach(() => {
-    delete process.env.IMMICH_CONFIG_FILE;
-    systemMock = newSystemMetadataRepositoryMock();
-    eventMock = newEventRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-    sut = new SystemConfigService(systemMock, eventMock, loggerMock);
+    ({ sut, configMock, eventMock, loggerMock, systemMock } = newTestService(SystemConfigService));
   });
 
   it('should work', () => {
@@ -213,7 +228,7 @@ describe(SystemConfigService.name, () => {
     it('should return the default config', async () => {
       systemMock.get.mockResolvedValue({});
 
-      await expect(sut.getConfig()).resolves.toEqual(defaults);
+      await expect(sut.getSystemConfig()).resolves.toEqual(defaults);
     });
 
     it('should merge the overrides', async () => {
@@ -224,25 +239,65 @@ describe(SystemConfigService.name, () => {
         user: { deleteDelay: 15 },
       });
 
-      await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
+      await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig);
     });
 
     it('should load the config from a json file', async () => {
-      process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
-
+      configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
       systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
 
-      await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
+      await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig);
 
       expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
     });
 
+    it('should transform booleans', async () => {
+      configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
+      systemMock.readFile.mockResolvedValue(JSON.stringify({ ffmpeg: { twoPass: 'false' } }));
+
+      await expect(sut.getSystemConfig()).resolves.toMatchObject({
+        ffmpeg: expect.objectContaining({ twoPass: false }),
+      });
+    });
+
+    it('should transform numbers', async () => {
+      configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
+      systemMock.readFile.mockResolvedValue(JSON.stringify({ ffmpeg: { threads: '42' } }));
+
+      await expect(sut.getSystemConfig()).resolves.toMatchObject({
+        ffmpeg: expect.objectContaining({ threads: 42 }),
+      });
+    });
+
+    it('should accept valid cron expressions', async () => {
+      configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
+      systemMock.readFile.mockResolvedValue(JSON.stringify({ library: { scan: { cronExpression: '0 0 * * *' } } }));
+
+      await expect(sut.getSystemConfig()).resolves.toMatchObject({
+        library: {
+          scan: {
+            enabled: true,
+            cronExpression: '0 0 * * *',
+          },
+        },
+      });
+    });
+
+    it('should reject invalid cron expressions', async () => {
+      configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
+      systemMock.readFile.mockResolvedValue(JSON.stringify({ library: { scan: { cronExpression: 'foo' } } }));
+
+      await expect(sut.getSystemConfig()).rejects.toThrow(
+        'library.scan.cronExpression has failed the following constraints: cronValidator',
+      );
+    });
+
     it('should log errors with the config file', async () => {
-      process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
+      configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
 
       systemMock.readFile.mockResolvedValue(`{ "ffmpeg2": true, "ffmpeg2": true }`);
 
-      await expect(sut.getConfig()).rejects.toBeInstanceOf(Error);
+      await expect(sut.getSystemConfig()).rejects.toBeInstanceOf(Error);
 
       expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
       expect(loggerMock.error).toHaveBeenCalledTimes(2);
@@ -253,7 +308,7 @@ describe(SystemConfigService.name, () => {
     });
 
     it('should load the config from a yaml file', async () => {
-      process.env.IMMICH_CONFIG_FILE = 'immich-config.yaml';
+      configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.yaml' }));
       const partialConfig = `
         ffmpeg:
           crf: 30
@@ -266,37 +321,54 @@ describe(SystemConfigService.name, () => {
       `;
       systemMock.readFile.mockResolvedValue(partialConfig);
 
-      await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
+      await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig);
 
       expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.yaml');
     });
 
     it('should accept an empty configuration file', async () => {
-      process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
+      configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
       systemMock.readFile.mockResolvedValue(JSON.stringify({}));
 
-      await expect(sut.getConfig()).resolves.toEqual(defaults);
+      await expect(sut.getSystemConfig()).resolves.toEqual(defaults);
 
       expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
     });
 
     it('should allow underscores in the machine learning url', async () => {
-      process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
-      const partialConfig = { machineLearning: { url: 'immich_machine_learning' } };
+      configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
+      const partialConfig = { machineLearning: { urls: ['immich_machine_learning'] } };
       systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
 
-      const config = await sut.getConfig();
-      expect(config.machineLearning.url).toEqual('immich_machine_learning');
+      const config = await sut.getSystemConfig();
+      expect(config.machineLearning.urls).toEqual(['immich_machine_learning']);
     });
 
+    const externalDomainTests = [
+      { should: 'with a trailing slash', externalDomain: 'https://demo.immich.app/' },
+      { should: 'without a trailing slash', externalDomain: 'https://demo.immich.app' },
+      { should: 'with a port', externalDomain: 'https://demo.immich.app:42', result: 'https://demo.immich.app:42' },
+    ];
+
+    for (const { should, externalDomain, result } of externalDomainTests) {
+      it(`should normalize an external domain ${should}`, async () => {
+        configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
+        const partialConfig = { server: { externalDomain } };
+        systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
+
+        const config = await sut.getSystemConfig();
+        expect(config.server.externalDomain).toEqual(result ?? 'https://demo.immich.app');
+      });
+    }
+
     it('should warn for unknown options in yaml', async () => {
-      process.env.IMMICH_CONFIG_FILE = 'immich-config.yaml';
+      configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.yaml' }));
       const partialConfig = `
         unknownOption: true
       `;
       systemMock.readFile.mockResolvedValue(partialConfig);
 
-      await sut.getConfig();
+      await sut.getSystemConfig();
       expect(loggerMock.warn).toHaveBeenCalled();
     });
 
@@ -311,69 +383,33 @@ describe(SystemConfigService.name, () => {
 
     for (const test of tests) {
       it(`should ${test.should}`, async () => {
-        process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
+        configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
         systemMock.readFile.mockResolvedValue(JSON.stringify(test.config));
 
         if (test.warn) {
-          await sut.getConfig();
+          await sut.getSystemConfig();
           expect(loggerMock.warn).toHaveBeenCalled();
         } else {
-          await expect(sut.getConfig()).rejects.toBeInstanceOf(Error);
+          await expect(sut.getSystemConfig()).rejects.toBeInstanceOf(Error);
         }
       });
     }
   });
 
-  describe('getStorageTemplateOptions', () => {
-    it('should send back the datetime variables', () => {
-      expect(sut.getStorageTemplateOptions()).toEqual({
-        dayOptions: ['d', 'dd'],
-        hourOptions: ['h', 'hh', 'H', 'HH'],
-        minuteOptions: ['m', 'mm'],
-        monthOptions: ['M', 'MM', 'MMM', 'MMMM'],
-        presetOptions: [
-          '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
-          '{{y}}/{{MM}}-{{dd}}/{{filename}}',
-          '{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
-          '{{y}}/{{MM}}/{{filename}}',
-          '{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}',
-          '{{y}}/{{MMM}}/{{filename}}',
-          '{{y}}/{{MMMM}}/{{filename}}',
-          '{{y}}/{{MM}}/{{dd}}/{{filename}}',
-          '{{y}}/{{MMMM}}/{{dd}}/{{filename}}',
-          '{{y}}/{{y}}-{{MM}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
-          '{{y}}-{{MM}}-{{dd}}/{{filename}}',
-          '{{y}}-{{MMM}}-{{dd}}/{{filename}}',
-          '{{y}}-{{MMMM}}-{{dd}}/{{filename}}',
-          '{{y}}/{{y}}-{{MM}}/{{filename}}',
-          '{{y}}/{{y}}-{{WW}}/{{filename}}',
-          '{{y}}/{{y}}-{{MM}}-{{dd}}/{{assetId}}',
-          '{{y}}/{{y}}-{{MM}}/{{assetId}}',
-          '{{y}}/{{y}}-{{WW}}/{{assetId}}',
-          '{{album}}/{{filename}}',
-        ],
-        secondOptions: ['s', 'ss', 'SSS'],
-        weekOptions: ['W', 'WW'],
-        yearOptions: ['y', 'yy'],
-      });
-    });
-  });
-
   describe('updateConfig', () => {
-    it('should update the config and emit client and server events', async () => {
+    it('should update the config and emit an event', async () => {
       systemMock.get.mockResolvedValue(partialConfig);
-
-      await expect(sut.updateConfig(updatedConfig)).resolves.toEqual(updatedConfig);
-
-      expect(eventMock.clientBroadcast).toHaveBeenCalled();
-      expect(eventMock.serverSend).toHaveBeenCalledWith(ServerEvent.CONFIG_UPDATE, null);
-      expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.SYSTEM_CONFIG, partialConfig);
+      await expect(sut.updateSystemConfig(updatedConfig)).resolves.toEqual(updatedConfig);
+      expect(eventMock.emit).toHaveBeenCalledWith(
+        'config.update',
+        expect.objectContaining({ oldConfig: expect.any(Object), newConfig: updatedConfig }),
+      );
     });
 
     it('should throw an error if a config file is in use', async () => {
-      process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
+      configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
       systemMock.readFile.mockResolvedValue(JSON.stringify({}));
-      await expect(sut.updateConfig(defaults)).rejects.toBeInstanceOf(BadRequestException);
+      await expect(sut.updateSystemConfig(defaults)).rejects.toBeInstanceOf(BadRequestException);
       expect(systemMock.set).not.toHaveBeenCalled();
     });
   });
diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts
index 5ec9ab7a5d..b5ae42e098 100644
--- a/server/src/services/system-config.service.ts
+++ b/server/src/services/system-config.service.ts
@@ -1,47 +1,24 @@
-import { BadRequestException, Inject, Injectable } from '@nestjs/common';
+import { BadRequestException, Injectable } from '@nestjs/common';
 import { instanceToPlain } from 'class-transformer';
 import _ from 'lodash';
-import { LogLevel, SystemConfig, defaults } from 'src/config';
-import {
-  supportedDayTokens,
-  supportedHourTokens,
-  supportedMinuteTokens,
-  supportedMonthTokens,
-  supportedPresetTokens,
-  supportedSecondTokens,
-  supportedWeekTokens,
-  supportedYearTokens,
-} from 'src/constants';
-import { SystemConfigCore } from 'src/cores/system-config.core';
-import { OnEmit, OnServerEvent } from 'src/decorators';
-import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto';
-import { ArgOf, ClientEvent, IEventRepository, ServerEvent } from 'src/interfaces/event.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { defaults } from 'src/config';
+import { OnEvent } from 'src/decorators';
+import { SystemConfigDto, mapConfig } from 'src/dtos/system-config.dto';
+import { ArgOf, BootstrapEventPriority } from 'src/interfaces/event.interface';
+import { BaseService } from 'src/services/base.service';
+import { clearConfigCache } from 'src/utils/config';
 import { toPlainObject } from 'src/utils/object';
 
 @Injectable()
-export class SystemConfigService {
-  private core: SystemConfigCore;
-
-  constructor(
-    @Inject(ISystemMetadataRepository) repository: ISystemMetadataRepository,
-    @Inject(IEventRepository) private eventRepository: IEventRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-  ) {
-    this.logger.setContext(SystemConfigService.name);
-    this.core = SystemConfigCore.create(repository, this.logger);
-    this.core.config$.subscribe((config) => this.setLogLevel(config));
-  }
-
-  @OnEmit({ event: 'app.bootstrap', priority: -100 })
+export class SystemConfigService extends BaseService {
+  @OnEvent({ name: 'app.bootstrap', priority: BootstrapEventPriority.SystemConfig })
   async onBootstrap() {
-    const config = await this.core.getConfig({ withCache: false });
-    this.core.config$.next(config);
+    const config = await this.getConfig({ withCache: false });
+    await this.eventRepository.emit('config.init', { newConfig: config });
   }
 
-  async getConfig(): Promise<SystemConfigDto> {
-    const config = await this.core.getConfig({ withCache: false });
+  async getSystemConfig(): Promise<SystemConfigDto> {
+    const config = await this.getConfig({ withCache: false });
     return mapConfig(config);
   }
 
@@ -49,19 +26,36 @@ export class SystemConfigService {
     return mapConfig(defaults);
   }
 
-  @OnEmit({ event: 'config.validate' })
+  @OnEvent({ name: 'config.init' })
+  onConfigInit({ newConfig: { logging } }: ArgOf<'config.init'>) {
+    const { logLevel: envLevel } = this.configRepository.getEnv();
+    const configLevel = logging.enabled ? logging.level : false;
+    const level = envLevel ?? configLevel;
+    this.logger.setLogLevel(level);
+    this.logger.log(`LogLevel=${level} ${envLevel ? '(set via IMMICH_LOG_LEVEL)' : '(set via system config)'}`);
+  }
+
+  @OnEvent({ name: 'config.update', server: true })
+  onConfigUpdate({ newConfig }: ArgOf<'config.update'>) {
+    this.onConfigInit({ newConfig });
+    clearConfigCache();
+  }
+
+  @OnEvent({ name: 'config.validate' })
   onConfigValidate({ newConfig, oldConfig }: ArgOf<'config.validate'>) {
-    if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && this.getEnvLogLevel()) {
+    const { logLevel } = this.configRepository.getEnv();
+    if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && logLevel) {
       throw new Error('Logging cannot be changed while the environment variable IMMICH_LOG_LEVEL is set.');
     }
   }
 
-  async updateConfig(dto: SystemConfigDto): Promise<SystemConfigDto> {
-    if (this.core.isUsingConfigFile()) {
+  async updateSystemConfig(dto: SystemConfigDto): Promise<SystemConfigDto> {
+    const { configFile } = this.configRepository.getEnv();
+    if (configFile) {
       throw new BadRequestException('Cannot update configuration while IMMICH_CONFIG_FILE is in use');
     }
 
-    const oldConfig = await this.core.getConfig({ withCache: false });
+    const oldConfig = await this.getConfig({ withCache: false });
 
     try {
       await this.eventRepository.emit('config.validate', { newConfig: toPlainObject(dto), oldConfig });
@@ -70,50 +64,15 @@ export class SystemConfigService {
       throw new BadRequestException(error instanceof Error ? error.message : error);
     }
 
-    const newConfig = await this.core.updateConfig(dto);
+    const newConfig = await this.updateConfig(dto);
 
-    // TODO probably move web socket emits to a separate service
-    this.eventRepository.clientBroadcast(ClientEvent.CONFIG_UPDATE, {});
-    this.eventRepository.serverSend(ServerEvent.CONFIG_UPDATE, null);
     await this.eventRepository.emit('config.update', { newConfig, oldConfig });
 
     return mapConfig(newConfig);
   }
 
-  getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
-    const options = new SystemConfigTemplateStorageOptionDto();
-
-    options.dayOptions = supportedDayTokens;
-    options.weekOptions = supportedWeekTokens;
-    options.monthOptions = supportedMonthTokens;
-    options.yearOptions = supportedYearTokens;
-    options.hourOptions = supportedHourTokens;
-    options.secondOptions = supportedSecondTokens;
-    options.minuteOptions = supportedMinuteTokens;
-    options.presetOptions = supportedPresetTokens;
-
-    return options;
-  }
-
   async getCustomCss(): Promise<string> {
-    const { theme } = await this.core.getConfig({ withCache: false });
+    const { theme } = await this.getConfig({ withCache: false });
     return theme.customCss;
   }
-
-  @OnServerEvent(ServerEvent.CONFIG_UPDATE)
-  async onConfigUpdateEvent() {
-    await this.core.refreshConfig();
-  }
-
-  private setLogLevel({ logging }: SystemConfig) {
-    const envLevel = this.getEnvLogLevel();
-    const configLevel = logging.enabled ? logging.level : false;
-    const level = envLevel ?? configLevel;
-    this.logger.setLogLevel(level);
-    this.logger.log(`LogLevel=${level} ${envLevel ? '(set via IMMICH_LOG_LEVEL)' : '(set via system config)'}`);
-  }
-
-  private getEnvLogLevel() {
-    return process.env.IMMICH_LOG_LEVEL as LogLevel;
-  }
 }
diff --git a/server/src/services/system-metadata.service.spec.ts b/server/src/services/system-metadata.service.spec.ts
index 5799ee859d..3dc2f0a6bb 100644
--- a/server/src/services/system-metadata.service.spec.ts
+++ b/server/src/services/system-metadata.service.spec.ts
@@ -1,31 +1,60 @@
 import { SystemMetadataKey } from 'src/enum';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { SystemMetadataService } from 'src/services/system-metadata.service';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(SystemMetadataService.name, () => {
   let sut: SystemMetadataService;
-  let metadataMock: Mocked<ISystemMetadataRepository>;
+  let systemMock: Mocked<ISystemMetadataRepository>;
 
   beforeEach(() => {
-    metadataMock = newSystemMetadataRepositoryMock();
-    sut = new SystemMetadataService(metadataMock);
+    ({ sut, systemMock } = newTestService(SystemMetadataService));
   });
 
   it('should work', () => {
     expect(sut).toBeDefined();
   });
 
+  describe('getAdminOnboarding', () => {
+    it('should get isOnboarded state', async () => {
+      systemMock.get.mockResolvedValue({ isOnboarded: true });
+      await expect(sut.getAdminOnboarding()).resolves.toEqual({ isOnboarded: true });
+      expect(systemMock.get).toHaveBeenCalledWith('admin-onboarding');
+    });
+
+    it('should default isOnboarded to false', async () => {
+      await expect(sut.getAdminOnboarding()).resolves.toEqual({ isOnboarded: false });
+      expect(systemMock.get).toHaveBeenCalledWith('admin-onboarding');
+    });
+  });
+
   describe('updateAdminOnboarding', () => {
     it('should update isOnboarded to true', async () => {
       await expect(sut.updateAdminOnboarding({ isOnboarded: true })).resolves.toBeUndefined();
-      expect(metadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true });
+      expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true });
     });
 
     it('should update isOnboarded to false', async () => {
       await expect(sut.updateAdminOnboarding({ isOnboarded: false })).resolves.toBeUndefined();
-      expect(metadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: false });
+      expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: false });
+    });
+  });
+
+  describe('getReverseGeocodingState', () => {
+    it('should get reverse geocoding state', async () => {
+      systemMock.get.mockResolvedValue({ lastUpdate: '2024-01-01', lastImportFileName: 'foo.bar' });
+      await expect(sut.getReverseGeocodingState()).resolves.toEqual({
+        lastUpdate: '2024-01-01',
+        lastImportFileName: 'foo.bar',
+      });
+    });
+
+    it('should default reverse geocoding state to null', async () => {
+      await expect(sut.getReverseGeocodingState()).resolves.toEqual({
+        lastUpdate: null,
+        lastImportFileName: null,
+      });
     });
   });
 });
diff --git a/server/src/services/system-metadata.service.ts b/server/src/services/system-metadata.service.ts
index c2c9a4fdfc..93449c7a7b 100644
--- a/server/src/services/system-metadata.service.ts
+++ b/server/src/services/system-metadata.service.ts
@@ -1,29 +1,27 @@
-import { Inject, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
 import {
   AdminOnboardingResponseDto,
   AdminOnboardingUpdateDto,
   ReverseGeocodingStateResponseDto,
 } from 'src/dtos/system-metadata.dto';
 import { SystemMetadataKey } from 'src/enum';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { BaseService } from 'src/services/base.service';
 
 @Injectable()
-export class SystemMetadataService {
-  constructor(@Inject(ISystemMetadataRepository) private repository: ISystemMetadataRepository) {}
-
+export class SystemMetadataService extends BaseService {
   async getAdminOnboarding(): Promise<AdminOnboardingResponseDto> {
-    const value = await this.repository.get(SystemMetadataKey.ADMIN_ONBOARDING);
+    const value = await this.systemMetadataRepository.get(SystemMetadataKey.ADMIN_ONBOARDING);
     return { isOnboarded: false, ...value };
   }
 
   async updateAdminOnboarding(dto: AdminOnboardingUpdateDto): Promise<void> {
-    await this.repository.set(SystemMetadataKey.ADMIN_ONBOARDING, {
+    await this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, {
       isOnboarded: dto.isOnboarded,
     });
   }
 
   async getReverseGeocodingState(): Promise<ReverseGeocodingStateResponseDto> {
-    const value = await this.repository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE);
+    const value = await this.systemMetadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE);
     return { lastUpdate: null, lastImportFileName: null, ...value };
   }
 }
diff --git a/server/src/services/tag.service.spec.ts b/server/src/services/tag.service.spec.ts
index de270777b0..54cef40d04 100644
--- a/server/src/services/tag.service.spec.ts
+++ b/server/src/services/tag.service.spec.ts
@@ -1,26 +1,22 @@
 import { BadRequestException } from '@nestjs/common';
 import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
-import { IEventRepository } from 'src/interfaces/event.interface';
+import { JobStatus } from 'src/interfaces/job.interface';
 import { ITagRepository } from 'src/interfaces/tag.interface';
 import { TagService } from 'src/services/tag.service';
 import { authStub } from 'test/fixtures/auth.stub';
 import { tagResponseStub, tagStub } from 'test/fixtures/tag.stub';
-import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
-import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
-import { newTagRepositoryMock } from 'test/repositories/tag.repository.mock';
+import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(TagService.name, () => {
   let sut: TagService;
+
   let accessMock: IAccessRepositoryMock;
-  let eventMock: Mocked<IEventRepository>;
   let tagMock: Mocked<ITagRepository>;
 
   beforeEach(() => {
-    accessMock = newAccessRepositoryMock();
-    eventMock = newEventRepositoryMock();
-    tagMock = newTagRepositoryMock();
-    sut = new TagService(accessMock, eventMock, tagMock);
+    ({ sut, accessMock, tagMock } = newTestService(TagService));
 
     accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1']));
   });
@@ -140,6 +136,23 @@ describe(TagService.name, () => {
         parent: expect.objectContaining({ id: 'tag-parent' }),
       });
     });
+
+    it('should upsert a tag and ignore leading and trailing slashes', async () => {
+      tagMock.getByValue.mockResolvedValueOnce(null);
+      tagMock.upsertValue.mockResolvedValueOnce(tagStub.parent);
+      tagMock.upsertValue.mockResolvedValueOnce(tagStub.child);
+      await expect(sut.upsert(authStub.admin, { tags: ['/Parent/Child/'] })).resolves.toBeDefined();
+      expect(tagMock.upsertValue).toHaveBeenNthCalledWith(1, {
+        value: 'Parent',
+        userId: 'admin_id',
+        parent: undefined,
+      });
+      expect(tagMock.upsertValue).toHaveBeenNthCalledWith(2, {
+        value: 'Parent/Child',
+        userId: 'admin_id',
+        parent: expect.objectContaining({ id: 'tag-parent' }),
+      });
+    });
   });
 
   describe('remove', () => {
@@ -249,4 +262,11 @@ describe(TagService.name, () => {
       expect(tagMock.removeAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1']);
     });
   });
+
+  describe('handleTagCleanup', () => {
+    it('should delete empty tags', async () => {
+      await expect(sut.handleTagCleanup()).resolves.toBe(JobStatus.SUCCESS);
+      expect(tagMock.deleteEmptyTags).toHaveBeenCalled();
+    });
+  });
 });
diff --git a/server/src/services/tag.service.ts b/server/src/services/tag.service.ts
index 97b0ef1be6..2aca400cc7 100644
--- a/server/src/services/tag.service.ts
+++ b/server/src/services/tag.service.ts
@@ -1,4 +1,5 @@
-import { BadRequestException, Inject, Injectable } from '@nestjs/common';
+import { BadRequestException, Injectable } from '@nestjs/common';
+import { OnJob } from 'src/decorators';
 import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
 import {
@@ -12,28 +13,21 @@ import {
 } from 'src/dtos/tag.dto';
 import { TagEntity } from 'src/entities/tag.entity';
 import { Permission } from 'src/enum';
-import { IAccessRepository } from 'src/interfaces/access.interface';
-import { IEventRepository } from 'src/interfaces/event.interface';
-import { AssetTagItem, ITagRepository } from 'src/interfaces/tag.interface';
-import { checkAccess, requireAccess } from 'src/utils/access';
+import { JobName, JobStatus, QueueName } from 'src/interfaces/job.interface';
+import { AssetTagItem } from 'src/interfaces/tag.interface';
+import { BaseService } from 'src/services/base.service';
 import { addAssets, removeAssets } from 'src/utils/asset.util';
 import { upsertTags } from 'src/utils/tag';
 
 @Injectable()
-export class TagService {
-  constructor(
-    @Inject(IAccessRepository) private access: IAccessRepository,
-    @Inject(IEventRepository) private eventRepository: IEventRepository,
-    @Inject(ITagRepository) private repository: ITagRepository,
-  ) {}
-
+export class TagService extends BaseService {
   async getAll(auth: AuthDto) {
-    const tags = await this.repository.getAll(auth.user.id);
+    const tags = await this.tagRepository.getAll(auth.user.id);
     return tags.map((tag) => mapTag(tag));
   }
 
   async get(auth: AuthDto, id: string): Promise<TagResponseDto> {
-    await requireAccess(this.access, { auth, permission: Permission.TAG_READ, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.TAG_READ, ids: [id] });
     const tag = await this.findOrFail(id);
     return mapTag(tag);
   }
@@ -41,8 +35,8 @@ export class TagService {
   async create(auth: AuthDto, dto: TagCreateDto) {
     let parent: TagEntity | undefined;
     if (dto.parentId) {
-      await requireAccess(this.access, { auth, permission: Permission.TAG_READ, ids: [dto.parentId] });
-      parent = (await this.repository.get(dto.parentId)) || undefined;
+      await this.requireAccess({ auth, permission: Permission.TAG_READ, ids: [dto.parentId] });
+      parent = (await this.tagRepository.get(dto.parentId)) || undefined;
       if (!parent) {
         throw new BadRequestException('Tag not found');
       }
@@ -50,41 +44,41 @@ export class TagService {
 
     const userId = auth.user.id;
     const value = parent ? `${parent.value}/${dto.name}` : dto.name;
-    const duplicate = await this.repository.getByValue(userId, value);
+    const duplicate = await this.tagRepository.getByValue(userId, value);
     if (duplicate) {
       throw new BadRequestException(`A tag with that name already exists`);
     }
 
-    const tag = await this.repository.create({ userId, value, parent });
+    const tag = await this.tagRepository.create({ userId, value, parent });
 
     return mapTag(tag);
   }
 
   async update(auth: AuthDto, id: string, dto: TagUpdateDto): Promise<TagResponseDto> {
-    await requireAccess(this.access, { auth, permission: Permission.TAG_UPDATE, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.TAG_UPDATE, ids: [id] });
 
     const { color } = dto;
-    const tag = await this.repository.update({ id, color });
+    const tag = await this.tagRepository.update({ id, color });
     return mapTag(tag);
   }
 
   async upsert(auth: AuthDto, dto: TagUpsertDto) {
-    const tags = await upsertTags(this.repository, { userId: auth.user.id, tags: dto.tags });
+    const tags = await upsertTags(this.tagRepository, { userId: auth.user.id, tags: dto.tags });
     return tags.map((tag) => mapTag(tag));
   }
 
   async remove(auth: AuthDto, id: string): Promise<void> {
-    await requireAccess(this.access, { auth, permission: Permission.TAG_DELETE, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.TAG_DELETE, ids: [id] });
 
     // TODO sync tag changes for affected assets
 
-    await this.repository.delete(id);
+    await this.tagRepository.delete(id);
   }
 
   async bulkTagAssets(auth: AuthDto, dto: TagBulkAssetsDto): Promise<TagBulkAssetsResponseDto> {
     const [tagIds, assetIds] = await Promise.all([
-      checkAccess(this.access, { auth, permission: Permission.TAG_ASSET, ids: dto.tagIds }),
-      checkAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }),
+      this.checkAccess({ auth, permission: Permission.TAG_ASSET, ids: dto.tagIds }),
+      this.checkAccess({ auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }),
     ]);
 
     const items: AssetTagItem[] = [];
@@ -94,7 +88,7 @@ export class TagService {
       }
     }
 
-    const results = await this.repository.upsertAssetIds(items);
+    const results = await this.tagRepository.upsertAssetIds(items);
     for (const assetId of new Set(results.map((item) => item.assetId))) {
       await this.eventRepository.emit('asset.tag', { assetId });
     }
@@ -103,11 +97,11 @@ export class TagService {
   }
 
   async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
-    await requireAccess(this.access, { auth, permission: Permission.TAG_ASSET, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.TAG_ASSET, ids: [id] });
 
     const results = await addAssets(
       auth,
-      { access: this.access, bulk: this.repository },
+      { access: this.accessRepository, bulk: this.tagRepository },
       { parentId: id, assetIds: dto.ids },
     );
 
@@ -121,11 +115,11 @@ export class TagService {
   }
 
   async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
-    await requireAccess(this.access, { auth, permission: Permission.TAG_ASSET, ids: [id] });
+    await this.requireAccess({ auth, permission: Permission.TAG_ASSET, ids: [id] });
 
     const results = await removeAssets(
       auth,
-      { access: this.access, bulk: this.repository },
+      { access: this.accessRepository, bulk: this.tagRepository },
       { parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.TAG_DELETE },
     );
 
@@ -138,8 +132,14 @@ export class TagService {
     return results;
   }
 
+  @OnJob({ name: JobName.TAG_CLEANUP, queue: QueueName.BACKGROUND_TASK })
+  async handleTagCleanup() {
+    await this.tagRepository.deleteEmptyTags();
+    return JobStatus.SUCCESS;
+  }
+
   private async findOrFail(id: string) {
-    const tag = await this.repository.get(id);
+    const tag = await this.tagRepository.get(id);
     if (!tag) {
       throw new BadRequestException('Tag not found');
     }
diff --git a/server/src/services/timeline.service.spec.ts b/server/src/services/timeline.service.spec.ts
index 981fc11c3f..db6890c27b 100644
--- a/server/src/services/timeline.service.spec.ts
+++ b/server/src/services/timeline.service.spec.ts
@@ -1,25 +1,20 @@
 import { BadRequestException } from '@nestjs/common';
 import { IAssetRepository, TimeBucketSize } from 'src/interfaces/asset.interface';
-import { IPartnerRepository } from 'src/interfaces/partner.interface';
 import { TimelineService } from 'src/services/timeline.service';
 import { assetStub } from 'test/fixtures/asset.stub';
 import { authStub } from 'test/fixtures/auth.stub';
-import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
-import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
-import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
+import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(TimelineService.name, () => {
   let sut: TimelineService;
+
   let accessMock: IAccessRepositoryMock;
   let assetMock: Mocked<IAssetRepository>;
-  let partnerMock: Mocked<IPartnerRepository>;
-  beforeEach(() => {
-    accessMock = newAccessRepositoryMock();
-    assetMock = newAssetRepositoryMock();
-    partnerMock = newPartnerRepositoryMock();
 
-    sut = new TimelineService(accessMock, assetMock, partnerMock);
+  beforeEach(() => {
+    ({ sut, accessMock, assetMock } = newTestService(TimelineService));
   });
 
   describe('getTimeBuckets', () => {
@@ -74,6 +69,70 @@ describe(TimelineService.name, () => {
       });
     });
 
+    it('should include partner shared assets', async () => {
+      assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
+
+      await expect(
+        sut.getTimeBucket(authStub.admin, {
+          size: TimeBucketSize.DAY,
+          timeBucket: 'bucket',
+          isArchived: false,
+          userId: authStub.admin.user.id,
+          withPartners: true,
+        }),
+      ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
+      expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', {
+        size: TimeBucketSize.DAY,
+        timeBucket: 'bucket',
+        isArchived: false,
+        withPartners: true,
+        userIds: [authStub.admin.user.id],
+      });
+    });
+
+    it('should check permissions to read tag', async () => {
+      assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
+      accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-123']));
+
+      await expect(
+        sut.getTimeBucket(authStub.admin, {
+          size: TimeBucketSize.DAY,
+          timeBucket: 'bucket',
+          userId: authStub.admin.user.id,
+          tagId: 'tag-123',
+        }),
+      ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
+      expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', {
+        size: TimeBucketSize.DAY,
+        tagId: 'tag-123',
+        timeBucket: 'bucket',
+        userIds: [authStub.admin.user.id],
+      });
+    });
+
+    it('should strip metadata if showExif is disabled', async () => {
+      accessMock.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-id']));
+      assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
+
+      const buckets = await sut.getTimeBucket(
+        { ...authStub.admin, sharedLink: { ...authStub.adminSharedLink.sharedLink!, showExif: false } },
+        {
+          size: TimeBucketSize.DAY,
+          timeBucket: 'bucket',
+          isArchived: true,
+          albumId: 'album-id',
+        },
+      );
+      expect(buckets).toEqual([expect.objectContaining({ id: 'asset-id' })]);
+      expect(buckets[0]).not.toHaveProperty('exif');
+      expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', {
+        size: TimeBucketSize.DAY,
+        timeBucket: 'bucket',
+        isArchived: true,
+        albumId: 'album-id',
+      });
+    });
+
     it('should return the assets for a library time bucket if user has library.read', async () => {
       assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
 
diff --git a/server/src/services/timeline.service.ts b/server/src/services/timeline.service.ts
index bc08505b94..04fd206fe7 100644
--- a/server/src/services/timeline.service.ts
+++ b/server/src/services/timeline.service.ts
@@ -1,26 +1,17 @@
-import { BadRequestException, Inject } from '@nestjs/common';
+import { BadRequestException } from '@nestjs/common';
 import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
 import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
 import { Permission } from 'src/enum';
-import { IAccessRepository } from 'src/interfaces/access.interface';
-import { IAssetRepository, TimeBucketOptions } from 'src/interfaces/asset.interface';
-import { IPartnerRepository } from 'src/interfaces/partner.interface';
-import { requireAccess } from 'src/utils/access';
+import { TimeBucketOptions } from 'src/interfaces/asset.interface';
+import { BaseService } from 'src/services/base.service';
 import { getMyPartnerIds } from 'src/utils/asset.util';
 
-export class TimelineService {
-  constructor(
-    @Inject(IAccessRepository) private access: IAccessRepository,
-    @Inject(IAssetRepository) private repository: IAssetRepository,
-    @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
-  ) {}
-
+export class TimelineService extends BaseService {
   async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
     await this.timeBucketChecks(auth, dto);
     const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
-
-    return this.repository.getTimeBuckets(timeBucketOptions);
+    return this.assetRepository.getTimeBuckets(timeBucketOptions);
   }
 
   async getTimeBucket(
@@ -29,7 +20,7 @@ export class TimelineService {
   ): Promise<AssetResponseDto[] | SanitizedAssetResponseDto[]> {
     await this.timeBucketChecks(auth, dto);
     const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
-    const assets = await this.repository.getTimeBucket(dto.timeBucket, timeBucketOptions);
+    const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions);
     return !auth.sharedLink || auth.sharedLink?.showExif
       ? assets.map((asset) => mapAsset(asset, { withStack: true, auth }))
       : assets.map((asset) => mapAsset(asset, { stripMetadata: true, auth }));
@@ -56,20 +47,20 @@ export class TimelineService {
 
   private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
     if (dto.albumId) {
-      await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
+      await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
     } else {
       dto.userId = dto.userId || auth.user.id;
     }
 
     if (dto.userId) {
-      await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [dto.userId] });
+      await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: [dto.userId] });
       if (dto.isArchived !== false) {
-        await requireAccess(this.access, { auth, permission: Permission.ARCHIVE_READ, ids: [dto.userId] });
+        await this.requireAccess({ auth, permission: Permission.ARCHIVE_READ, ids: [dto.userId] });
       }
     }
 
     if (dto.tagId) {
-      await requireAccess(this.access, { auth, permission: Permission.TAG_READ, ids: [dto.tagId] });
+      await this.requireAccess({ auth, permission: Permission.TAG_READ, ids: [dto.tagId] });
     }
 
     if (dto.withPartners) {
diff --git a/server/src/services/trash.service.spec.ts b/server/src/services/trash.service.spec.ts
index 5c0609956a..748faa14ab 100644
--- a/server/src/services/trash.service.spec.ts
+++ b/server/src/services/trash.service.spec.ts
@@ -1,34 +1,25 @@
 import { BadRequestException } from '@nestjs/common';
-import { IAssetRepository } from 'src/interfaces/asset.interface';
-import { IEventRepository } from 'src/interfaces/event.interface';
-import { IJobRepository, JobName } from 'src/interfaces/job.interface';
+import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
+import { ITrashRepository } from 'src/interfaces/trash.interface';
 import { TrashService } from 'src/services/trash.service';
-import { assetStub } from 'test/fixtures/asset.stub';
 import { authStub } from 'test/fixtures/auth.stub';
-import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
-import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
-import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
-import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
+import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(TrashService.name, () => {
   let sut: TrashService;
+
   let accessMock: IAccessRepositoryMock;
-  let assetMock: Mocked<IAssetRepository>;
   let jobMock: Mocked<IJobRepository>;
-  let eventMock: Mocked<IEventRepository>;
+  let trashMock: Mocked<ITrashRepository>;
 
   it('should work', () => {
     expect(sut).toBeDefined();
   });
 
   beforeEach(() => {
-    accessMock = newAccessRepositoryMock();
-    assetMock = newAssetRepositoryMock();
-    eventMock = newEventRepositoryMock();
-    jobMock = newJobRepositoryMock();
-
-    sut = new TrashService(accessMock, assetMock, jobMock, eventMock);
+    ({ sut, accessMock, jobMock, trashMock } = newTestService(TrashService));
   });
 
   describe('restoreAssets', () => {
@@ -40,44 +31,70 @@ describe(TrashService.name, () => {
       ).rejects.toBeInstanceOf(BadRequestException);
     });
 
+    it('should handle an empty list', async () => {
+      await expect(sut.restoreAssets(authStub.user1, { ids: [] })).resolves.toEqual({ count: 0 });
+      expect(accessMock.asset.checkOwnerAccess).not.toHaveBeenCalled();
+    });
+
     it('should restore a batch of assets', async () => {
       accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2']));
 
       await sut.restoreAssets(authStub.user1, { ids: ['asset1', 'asset2'] });
 
-      expect(assetMock.restoreAll).toHaveBeenCalledWith(['asset1', 'asset2']);
+      expect(trashMock.restoreAll).toHaveBeenCalledWith(['asset1', 'asset2']);
       expect(jobMock.queue.mock.calls).toEqual([]);
     });
   });
 
   describe('restore', () => {
     it('should handle an empty trash', async () => {
-      assetMock.getByUserId.mockResolvedValue({ items: [], hasNextPage: false });
-      await expect(sut.restore(authStub.user1)).resolves.toBeUndefined();
-      expect(assetMock.restoreAll).not.toHaveBeenCalled();
-      expect(eventMock.clientSend).not.toHaveBeenCalled();
+      trashMock.getDeletedIds.mockResolvedValue({ items: [], hasNextPage: false });
+      trashMock.restore.mockResolvedValue(0);
+      await expect(sut.restore(authStub.user1)).resolves.toEqual({ count: 0 });
+      expect(trashMock.restore).toHaveBeenCalledWith('user-id');
     });
 
-    it('should restore and notify', async () => {
-      assetMock.getByUserId.mockResolvedValue({ items: [assetStub.image], hasNextPage: false });
-      await expect(sut.restore(authStub.user1)).resolves.toBeUndefined();
-      expect(assetMock.restoreAll).toHaveBeenCalledWith([assetStub.image.id]);
-      expect(eventMock.emit).toHaveBeenCalledWith('assets.restore', { assetIds: ['asset-id'], userId: 'user-id' });
+    it('should restore', async () => {
+      trashMock.getDeletedIds.mockResolvedValue({ items: ['asset-1'], hasNextPage: false });
+      trashMock.restore.mockResolvedValue(1);
+      await expect(sut.restore(authStub.user1)).resolves.toEqual({ count: 1 });
+      expect(trashMock.restore).toHaveBeenCalledWith('user-id');
     });
   });
 
   describe('empty', () => {
     it('should handle an empty trash', async () => {
-      assetMock.getByUserId.mockResolvedValue({ items: [], hasNextPage: false });
-      await expect(sut.empty(authStub.user1)).resolves.toBeUndefined();
-      expect(jobMock.queueAll).toHaveBeenCalledWith([]);
+      trashMock.getDeletedIds.mockResolvedValue({ items: [], hasNextPage: false });
+      trashMock.empty.mockResolvedValue(0);
+      await expect(sut.empty(authStub.user1)).resolves.toEqual({ count: 0 });
+      expect(jobMock.queue).not.toHaveBeenCalled();
     });
 
     it('should empty the trash', async () => {
-      assetMock.getByUserId.mockResolvedValue({ items: [assetStub.image], hasNextPage: false });
-      await expect(sut.empty(authStub.user1)).resolves.toBeUndefined();
+      trashMock.getDeletedIds.mockResolvedValue({ items: ['asset-1'], hasNextPage: false });
+      trashMock.empty.mockResolvedValue(1);
+      await expect(sut.empty(authStub.user1)).resolves.toEqual({ count: 1 });
+      expect(trashMock.empty).toHaveBeenCalledWith('user-id');
+      expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_TRASH_EMPTY, data: {} });
+    });
+  });
+
+  describe('onAssetsDelete', () => {
+    it('should queue the empty trash job', async () => {
+      await expect(sut.onAssetsDelete()).resolves.toBeUndefined();
+      expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_TRASH_EMPTY, data: {} });
+    });
+  });
+
+  describe('handleQueueEmptyTrash', () => {
+    it('should queue asset delete jobs', async () => {
+      trashMock.getDeletedIds.mockResolvedValue({ items: ['asset-1'], hasNextPage: false });
+      await expect(sut.handleQueueEmptyTrash()).resolves.toEqual(JobStatus.SUCCESS);
       expect(jobMock.queueAll).toHaveBeenCalledWith([
-        { name: JobName.ASSET_DELETION, data: { id: assetStub.image.id, deleteOnDisk: true } },
+        {
+          name: JobName.ASSET_DELETION,
+          data: { id: 'asset-1', deleteOnDisk: true },
+        },
       ]);
     });
   });
diff --git a/server/src/services/trash.service.ts b/server/src/services/trash.service.ts
index 712b9e50f2..621dee0f81 100644
--- a/server/src/services/trash.service.ts
+++ b/server/src/services/trash.service.ts
@@ -1,69 +1,72 @@
-import { Inject } from '@nestjs/common';
-import { DateTime } from 'luxon';
+import { OnEvent, OnJob } from 'src/decorators';
 import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
+import { TrashResponseDto } from 'src/dtos/trash.dto';
 import { Permission } from 'src/enum';
-import { IAccessRepository } from 'src/interfaces/access.interface';
-import { IAssetRepository } from 'src/interfaces/asset.interface';
-import { IEventRepository } from 'src/interfaces/event.interface';
-import { IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/interfaces/job.interface';
-import { requireAccess } from 'src/utils/access';
+import { JOBS_ASSET_PAGINATION_SIZE, JobName, JobStatus, QueueName } from 'src/interfaces/job.interface';
+import { BaseService } from 'src/services/base.service';
 import { usePagination } from 'src/utils/pagination';
 
-export class TrashService {
-  constructor(
-    @Inject(IAccessRepository) private access: IAccessRepository,
-    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
-    @Inject(IJobRepository) private jobRepository: IJobRepository,
-    @Inject(IEventRepository) private eventRepository: IEventRepository,
-  ) {}
-
-  async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise<void> {
+export class TrashService extends BaseService {
+  async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise<TrashResponseDto> {
     const { ids } = dto;
-    await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids });
-    await this.restoreAndSend(auth, ids);
-  }
-
-  async restore(auth: AuthDto): Promise<void> {
-    const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
-      this.assetRepository.getByUserId(pagination, auth.user.id, {
-        trashedBefore: DateTime.now().toJSDate(),
-      }),
-    );
-
-    for await (const assets of assetPagination) {
-      const ids = assets.map((a) => a.id);
-      await this.restoreAndSend(auth, ids);
+    if (ids.length === 0) {
+      return { count: 0 };
     }
+
+    await this.requireAccess({ auth, permission: Permission.ASSET_DELETE, ids });
+    await this.trashRepository.restoreAll(ids);
+    await this.eventRepository.emit('assets.restore', { assetIds: ids, userId: auth.user.id });
+
+    this.logger.log(`Restored ${ids.length} assets from trash`);
+
+    return { count: ids.length };
   }
 
-  async empty(auth: AuthDto): Promise<void> {
+  async restore(auth: AuthDto): Promise<TrashResponseDto> {
+    const count = await this.trashRepository.restore(auth.user.id);
+    if (count > 0) {
+      this.logger.log(`Restored ${count} assets from trash`);
+    }
+    return { count };
+  }
+
+  async empty(auth: AuthDto): Promise<TrashResponseDto> {
+    const count = await this.trashRepository.empty(auth.user.id);
+    if (count > 0) {
+      await this.jobRepository.queue({ name: JobName.QUEUE_TRASH_EMPTY, data: {} });
+    }
+    return { count };
+  }
+
+  @OnEvent({ name: 'assets.delete' })
+  async onAssetsDelete() {
+    await this.jobRepository.queue({ name: JobName.QUEUE_TRASH_EMPTY, data: {} });
+  }
+
+  @OnJob({ name: JobName.QUEUE_TRASH_EMPTY, queue: QueueName.BACKGROUND_TASK })
+  async handleQueueEmptyTrash() {
+    let count = 0;
     const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
-      this.assetRepository.getByUserId(pagination, auth.user.id, {
-        trashedBefore: DateTime.now().toJSDate(),
-        withArchived: true,
-      }),
+      this.trashRepository.getDeletedIds(pagination),
     );
 
-    for await (const assets of assetPagination) {
+    for await (const assetIds of assetPagination) {
+      this.logger.debug(`Queueing ${assetIds.length} assets for deletion from the trash`);
+      count += assetIds.length;
       await this.jobRepository.queueAll(
-        assets.map((asset) => ({
+        assetIds.map((assetId) => ({
           name: JobName.ASSET_DELETION,
           data: {
-            id: asset.id,
+            id: assetId,
             deleteOnDisk: true,
           },
         })),
       );
     }
-  }
 
-  private async restoreAndSend(auth: AuthDto, ids: string[]) {
-    if (ids.length === 0) {
-      return;
-    }
+    this.logger.log(`Queued ${count} assets for deletion from the trash`);
 
-    await this.assetRepository.restoreAll(ids);
-    await this.eventRepository.emit('assets.restore', { assetIds: ids, userId: auth.user.id });
+    return JobStatus.SUCCESS;
   }
 }
diff --git a/server/src/services/user-admin.service.spec.ts b/server/src/services/user-admin.service.spec.ts
index 8e80aa4dc1..70999332dc 100644
--- a/server/src/services/user-admin.service.spec.ts
+++ b/server/src/services/user-admin.service.spec.ts
@@ -1,41 +1,22 @@
 import { BadRequestException, ForbiddenException } from '@nestjs/common';
 import { mapUserAdmin } from 'src/dtos/user.dto';
 import { UserStatus } from 'src/enum';
-import { IAlbumRepository } from 'src/interfaces/album.interface';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import { IEventRepository } from 'src/interfaces/event.interface';
 import { IJobRepository, JobName } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { IUserRepository } from 'src/interfaces/user.interface';
 import { UserAdminService } from 'src/services/user-admin.service';
 import { authStub } from 'test/fixtures/auth.stub';
 import { userStub } from 'test/fixtures/user.stub';
-import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
-import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
-import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
-import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked, describe } from 'vitest';
 
 describe(UserAdminService.name, () => {
   let sut: UserAdminService;
-  let albumMock: Mocked<IAlbumRepository>;
-  let cryptoMock: Mocked<ICryptoRepository>;
-  let eventMock: Mocked<IEventRepository>;
+
   let jobMock: Mocked<IJobRepository>;
-  let loggerMock: Mocked<ILoggerRepository>;
   let userMock: Mocked<IUserRepository>;
 
   beforeEach(() => {
-    albumMock = newAlbumRepositoryMock();
-    cryptoMock = newCryptoRepositoryMock();
-    eventMock = newEventRepositoryMock();
-    jobMock = newJobRepositoryMock();
-    userMock = newUserRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-
-    sut = new UserAdminService(albumMock, cryptoMock, eventMock, jobMock, userMock, loggerMock);
+    ({ sut, jobMock, userMock } = newTestService(UserAdminService));
 
     userMock.get.mockImplementation((userId) =>
       Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? null),
diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts
index 6a5b6ea06e..a4be671c22 100644
--- a/server/src/services/user-admin.service.ts
+++ b/server/src/services/user-admin.service.ts
@@ -1,6 +1,5 @@
-import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common';
+import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';
 import { SALT_ROUNDS } from 'src/constants';
-import { UserCore } from 'src/cores/user.core';
 import { AuthDto } from 'src/dtos/auth.dto';
 import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
 import {
@@ -12,43 +11,31 @@ import {
   mapUserAdmin,
 } from 'src/dtos/user.dto';
 import { UserMetadataKey, UserStatus } from 'src/enum';
-import { IAlbumRepository } from 'src/interfaces/album.interface';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import { IEventRepository } from 'src/interfaces/event.interface';
-import { IJobRepository, JobName } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
+import { JobName } from 'src/interfaces/job.interface';
+import { UserFindOptions } from 'src/interfaces/user.interface';
+import { BaseService } from 'src/services/base.service';
 import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
 
 @Injectable()
-export class UserAdminService {
-  private userCore: UserCore;
-
-  constructor(
-    @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
-    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
-    @Inject(IEventRepository) private eventRepository: IEventRepository,
-    @Inject(IJobRepository) private jobRepository: IJobRepository,
-    @Inject(IUserRepository) private userRepository: IUserRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-  ) {
-    this.userCore = UserCore.create(cryptoRepository, userRepository);
-    this.logger.setContext(UserAdminService.name);
-  }
-
+export class UserAdminService extends BaseService {
   async search(auth: AuthDto, dto: UserAdminSearchDto): Promise<UserAdminResponseDto[]> {
     const users = await this.userRepository.getList({ withDeleted: dto.withDeleted });
     return users.map((user) => mapUserAdmin(user));
   }
 
   async create(dto: UserAdminCreateDto): Promise<UserAdminResponseDto> {
-    const { notify, ...rest } = dto;
-    const user = await this.userCore.createUser(rest);
+    const { notify, ...userDto } = dto;
+    const config = await this.getConfig({ withCache: false });
+    if (!config.oauth.enabled && !userDto.password) {
+      throw new BadRequestException('password is required');
+    }
+
+    const user = await this.createUser(userDto);
 
     await this.eventRepository.emit('user.signup', {
       notify: !!notify,
       id: user.id,
-      tempPassword: user.shouldChangePassword ? rest.password : undefined,
+      tempPassword: user.shouldChangePassword ? userDto.password : undefined,
     });
 
     return mapUserAdmin(user);
diff --git a/server/src/services/user.service.spec.ts b/server/src/services/user.service.spec.ts
index 0ac0ea6dbc..08b663046b 100644
--- a/server/src/services/user.service.spec.ts
+++ b/server/src/services/user.service.spec.ts
@@ -1,25 +1,17 @@
 import { BadRequestException, InternalServerErrorException, NotFoundException } from '@nestjs/common';
 import { UserEntity } from 'src/entities/user.entity';
-import { UserMetadataKey } from 'src/enum';
+import { CacheControl, UserMetadataKey } from 'src/enum';
 import { IAlbumRepository } from 'src/interfaces/album.interface';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
 import { IJobRepository, JobName } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { IStorageRepository } from 'src/interfaces/storage.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { IUserRepository } from 'src/interfaces/user.interface';
 import { UserService } from 'src/services/user.service';
-import { CacheControl, ImmichFileResponse } from 'src/utils/file';
+import { ImmichFileResponse } from 'src/utils/file';
 import { authStub } from 'test/fixtures/auth.stub';
 import { systemConfigStub } from 'test/fixtures/system-config.stub';
 import { userStub } from 'test/fixtures/user.stub';
-import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
-import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
-import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
-import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 const makeDeletedAt = (daysAgo: number) => {
@@ -30,25 +22,15 @@ const makeDeletedAt = (daysAgo: number) => {
 
 describe(UserService.name, () => {
   let sut: UserService;
-  let userMock: Mocked<IUserRepository>;
-  let cryptoRepositoryMock: Mocked<ICryptoRepository>;
 
   let albumMock: Mocked<IAlbumRepository>;
   let jobMock: Mocked<IJobRepository>;
   let storageMock: Mocked<IStorageRepository>;
   let systemMock: Mocked<ISystemMetadataRepository>;
-  let loggerMock: Mocked<ILoggerRepository>;
+  let userMock: Mocked<IUserRepository>;
 
   beforeEach(() => {
-    albumMock = newAlbumRepositoryMock();
-    systemMock = newSystemMetadataRepositoryMock();
-    cryptoRepositoryMock = newCryptoRepositoryMock();
-    jobMock = newJobRepositoryMock();
-    storageMock = newStorageRepositoryMock();
-    userMock = newUserRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-
-    sut = new UserService(albumMock, cryptoRepositoryMock, jobMock, storageMock, systemMock, userMock, loggerMock);
+    ({ sut, albumMock, jobMock, storageMock, systemMock, userMock } = newTestService(UserService));
 
     userMock.get.mockImplementation((userId) =>
       Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? null),
@@ -56,9 +38,9 @@ describe(UserService.name, () => {
   });
 
   describe('getAll', () => {
-    it('should get all users', async () => {
+    it('admin should get all users', async () => {
       userMock.getList.mockResolvedValue([userStub.admin]);
-      await expect(sut.search()).resolves.toEqual([
+      await expect(sut.search(authStub.admin)).resolves.toEqual([
         expect.objectContaining({
           id: authStub.admin.user.id,
           email: authStub.admin.user.email,
@@ -66,6 +48,29 @@ describe(UserService.name, () => {
       ]);
       expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: false });
     });
+
+    it('non-admin should get all users when publicUsers enabled', async () => {
+      userMock.getList.mockResolvedValue([userStub.user1]);
+      await expect(sut.search(authStub.user1)).resolves.toEqual([
+        expect.objectContaining({
+          id: authStub.user1.user.id,
+          email: authStub.user1.user.email,
+        }),
+      ]);
+      expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: false });
+    });
+
+    it('non-admin user should only receive itself when publicUsers is disabled', async () => {
+      userMock.getList.mockResolvedValue([userStub.user1]);
+      systemMock.get.mockResolvedValue(systemConfigStub.publicUsersDisabled);
+      await expect(sut.search(authStub.user1)).resolves.toEqual([
+        expect.objectContaining({
+          id: authStub.user1.user.id,
+          email: authStub.user1.user.email,
+        }),
+      ]);
+      expect(userMock.getList).not.toHaveBeenCalledWith({ withDeleted: false });
+    });
   });
 
   describe('get', () => {
diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts
index 92404a6958..f4ae42b5ed 100644
--- a/server/src/services/user.service.ts
+++ b/server/src/services/user.service.ts
@@ -1,46 +1,32 @@
-import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
+import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
 import { DateTime } from 'luxon';
-import { getClientLicensePublicKey, getServerLicensePublicKey } from 'src/config';
 import { SALT_ROUNDS } from 'src/constants';
-import { StorageCore, StorageFolder } from 'src/cores/storage.core';
-import { SystemConfigCore } from 'src/cores/system-config.core';
+import { StorageCore } from 'src/cores/storage.core';
+import { OnJob } from 'src/decorators';
 import { AuthDto } from 'src/dtos/auth.dto';
 import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
 import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
-import { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto';
+import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
 import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
 import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
 import { UserEntity } from 'src/entities/user.entity';
-import { UserMetadataKey } from 'src/enum';
-import { IAlbumRepository } from 'src/interfaces/album.interface';
-import { ICryptoRepository } from 'src/interfaces/crypto.interface';
-import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { IStorageRepository } from 'src/interfaces/storage.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
-import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
-import { CacheControl, ImmichFileResponse } from 'src/utils/file';
+import { CacheControl, StorageFolder, UserMetadataKey } from 'src/enum';
+import { JobName, JobOf, JobStatus, QueueName } from 'src/interfaces/job.interface';
+import { UserFindOptions } from 'src/interfaces/user.interface';
+import { BaseService } from 'src/services/base.service';
+import { ImmichFileResponse } from 'src/utils/file';
 import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
 
 @Injectable()
-export class UserService {
-  private configCore: SystemConfigCore;
+export class UserService extends BaseService {
+  async search(auth: AuthDto): Promise<UserResponseDto[]> {
+    const config = await this.getConfig({ withCache: false });
 
-  constructor(
-    @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
-    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
-    @Inject(IJobRepository) private jobRepository: IJobRepository,
-    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
-    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
-    @Inject(IUserRepository) private userRepository: IUserRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-  ) {
-    this.logger.setContext(UserService.name);
-    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
-  }
+    let users: UserEntity[] = [auth.user];
+    if (auth.user.isAdmin || config.server.publicUsers) {
+      users = await this.userRepository.getList({ withDeleted: false });
+    }
 
-  async search(): Promise<UserResponseDto[]> {
-    const users = await this.userRepository.getList({ withDeleted: false });
     return users.map((user) => mapUser(user));
   }
 
@@ -93,13 +79,23 @@ export class UserService {
     return mapUser(user);
   }
 
-  async createProfileImage(auth: AuthDto, fileInfo: Express.Multer.File): Promise<CreateProfileImageResponseDto> {
+  async createProfileImage(auth: AuthDto, file: Express.Multer.File): Promise<CreateProfileImageResponseDto> {
     const { profileImagePath: oldpath } = await this.findOrFail(auth.user.id, { withDeleted: false });
-    const updatedUser = await this.userRepository.update(auth.user.id, { profileImagePath: fileInfo.path });
+
+    const user = await this.userRepository.update(auth.user.id, {
+      profileImagePath: file.path,
+      profileChangedAt: new Date(),
+    });
+
     if (oldpath !== '') {
       await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [oldpath] } });
     }
-    return mapCreateProfileImageResponse(updatedUser.id, updatedUser.profileImagePath);
+
+    return {
+      userId: user.id,
+      profileImagePath: user.profileImagePath,
+      profileChangedAt: user.profileChangedAt,
+    };
   }
 
   async deleteProfileImage(auth: AuthDto): Promise<void> {
@@ -107,7 +103,7 @@ export class UserService {
     if (user.profileImagePath === '') {
       throw new BadRequestException("Can't delete a missing profile Image");
     }
-    await this.userRepository.update(auth.user.id, { profileImagePath: '' });
+    await this.userRepository.update(auth.user.id, { profileImagePath: '', profileChangedAt: new Date() });
     await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [user.profileImagePath] } });
   }
 
@@ -143,16 +139,18 @@ export class UserService {
       throw new BadRequestException('Invalid license key');
     }
 
+    const { licensePublicKey } = this.configRepository.getEnv();
+
     const clientLicenseValid = this.cryptoRepository.verifySha256(
       license.licenseKey,
       license.activationKey,
-      getClientLicensePublicKey(),
+      licensePublicKey.client,
     );
 
     const serverLicenseValid = this.cryptoRepository.verifySha256(
       license.licenseKey,
       license.activationKey,
-      getServerLicensePublicKey(),
+      licensePublicKey.server,
     );
 
     if (!clientLicenseValid && !serverLicenseValid) {
@@ -172,14 +170,16 @@ export class UserService {
     return licenseData;
   }
 
+  @OnJob({ name: JobName.USER_SYNC_USAGE, queue: QueueName.BACKGROUND_TASK })
   async handleUserSyncUsage(): Promise<JobStatus> {
     await this.userRepository.syncUsage();
     return JobStatus.SUCCESS;
   }
 
+  @OnJob({ name: JobName.USER_DELETE_CHECK, queue: QueueName.BACKGROUND_TASK })
   async handleUserDeleteCheck(): Promise<JobStatus> {
     const users = await this.userRepository.getDeletedUsers();
-    const config = await this.configCore.getConfig({ withCache: false });
+    const config = await this.getConfig({ withCache: false });
     await this.jobRepository.queueAll(
       users.flatMap((user) =>
         this.isReadyForDeletion(user, config.user.deleteDelay)
@@ -190,8 +190,9 @@ export class UserService {
     return JobStatus.SUCCESS;
   }
 
-  async handleUserDelete({ id, force }: IEntityJob): Promise<JobStatus> {
-    const config = await this.configCore.getConfig({ withCache: false });
+  @OnJob({ name: JobName.USER_DELETION, queue: QueueName.BACKGROUND_TASK })
+  async handleUserDelete({ id, force }: JobOf<JobName.USER_DELETION>): Promise<JobStatus> {
+    const config = await this.getConfig({ withCache: false });
     const user = await this.userRepository.get(id, { withDeleted: true });
     if (!user) {
       return JobStatus.FAILED;
diff --git a/server/src/services/version.service.spec.ts b/server/src/services/version.service.spec.ts
index 02dfe7588f..46f8f620c4 100644
--- a/server/src/services/version.service.spec.ts
+++ b/server/src/services/version.service.spec.ts
@@ -1,17 +1,17 @@
 import { DateTime } from 'luxon';
+import { SemVer } from 'semver';
 import { serverVersion } from 'src/constants';
-import { SystemMetadataKey } from 'src/enum';
+import { ImmichEnvironment, SystemMetadataKey } from 'src/enum';
+import { IConfigRepository } from 'src/interfaces/config.interface';
 import { IEventRepository } from 'src/interfaces/event.interface';
 import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
 import { VersionService } from 'src/services/version.service';
-import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
-import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
-import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
-import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock';
-import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
+import { mockEnvData } from 'test/repositories/config.repository.mock';
+import { newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 const mockRelease = (version: string) => ({
@@ -26,26 +26,41 @@ const mockRelease = (version: string) => ({
 
 describe(VersionService.name, () => {
   let sut: VersionService;
+
+  let configMock: Mocked<IConfigRepository>;
   let eventMock: Mocked<IEventRepository>;
   let jobMock: Mocked<IJobRepository>;
-  let serverMock: Mocked<IServerInfoRepository>;
-  let systemMock: Mocked<ISystemMetadataRepository>;
   let loggerMock: Mocked<ILoggerRepository>;
+  let serverInfoMock: Mocked<IServerInfoRepository>;
+  let systemMock: Mocked<ISystemMetadataRepository>;
+  let versionHistoryMock: Mocked<IVersionHistoryRepository>;
 
   beforeEach(() => {
-    eventMock = newEventRepositoryMock();
-    jobMock = newJobRepositoryMock();
-    serverMock = newServerInfoRepositoryMock();
-    systemMock = newSystemMetadataRepositoryMock();
-    loggerMock = newLoggerRepositoryMock();
-
-    sut = new VersionService(eventMock, jobMock, serverMock, systemMock, loggerMock);
+    ({ sut, configMock, eventMock, jobMock, loggerMock, serverInfoMock, systemMock, versionHistoryMock } =
+      newTestService(VersionService));
   });
 
   it('should work', () => {
     expect(sut).toBeDefined();
   });
 
+  describe('onBootstrap', () => {
+    it('should record a new version', async () => {
+      await expect(sut.onBootstrap()).resolves.toBeUndefined();
+      expect(versionHistoryMock.create).toHaveBeenCalledWith({ version: expect.any(String) });
+    });
+
+    it('should skip a duplicate version', async () => {
+      versionHistoryMock.getLatest.mockResolvedValue({
+        id: 'version-1',
+        createdAt: new Date(),
+        version: serverVersion.toString(),
+      });
+      await expect(sut.onBootstrap()).resolves.toBeUndefined();
+      expect(versionHistoryMock.create).not.toHaveBeenCalled();
+    });
+  });
+
   describe('getVersion', () => {
     it('should respond the server version', () => {
       expect(sut.getVersion()).toEqual({
@@ -56,6 +71,14 @@ describe(VersionService.name, () => {
     });
   });
 
+  describe('getVersionHistory', () => {
+    it('should respond the server version history', async () => {
+      const upgrade = { id: 'upgrade-1', createdAt: new Date(), version: '1.0.0' };
+      versionHistoryMock.getAll.mockResolvedValue([upgrade]);
+      await expect(sut.getVersionHistory()).resolves.toEqual([upgrade]);
+    });
+  });
+
   describe('handQueueVersionCheck', () => {
     it('should queue a version check job', async () => {
       await expect(sut.handleQueueVersionCheck()).resolves.toBeUndefined();
@@ -65,11 +88,11 @@ describe(VersionService.name, () => {
 
   describe('handVersionCheck', () => {
     beforeEach(() => {
-      process.env.IMMICH_ENV = 'production';
+      configMock.getEnv.mockReturnValue(mockEnvData({ environment: ImmichEnvironment.PRODUCTION }));
     });
 
     it('should not run in dev mode', async () => {
-      process.env.IMMICH_ENV = 'development';
+      configMock.getEnv.mockReturnValue(mockEnvData({ environment: ImmichEnvironment.DEVELOPMENT }));
       await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.SKIPPED);
     });
 
@@ -81,8 +104,13 @@ describe(VersionService.name, () => {
       await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.SKIPPED);
     });
 
+    it('should not run if version check is disabled', async () => {
+      systemMock.get.mockResolvedValue({ newVersionCheck: { enabled: false } });
+      await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.SKIPPED);
+    });
+
     it('should run if it has been > 60 minutes', async () => {
-      serverMock.getGitHubRelease.mockResolvedValue(mockRelease('v100.0.0'));
+      serverInfoMock.getGitHubRelease.mockResolvedValue(mockRelease('v100.0.0'));
       systemMock.get.mockResolvedValue({
         checkedAt: DateTime.utc().minus({ minutes: 65 }).toISO(),
         releaseVersion: '1.0.0',
@@ -94,7 +122,7 @@ describe(VersionService.name, () => {
     });
 
     it('should not notify if the version is equal', async () => {
-      serverMock.getGitHubRelease.mockResolvedValue(mockRelease(serverVersion.toString()));
+      serverInfoMock.getGitHubRelease.mockResolvedValue(mockRelease(serverVersion.toString()));
       await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.SUCCESS);
       expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.VERSION_CHECK_STATE, {
         checkedAt: expect.any(String),
@@ -104,11 +132,26 @@ describe(VersionService.name, () => {
     });
 
     it('should handle a github error', async () => {
-      serverMock.getGitHubRelease.mockRejectedValue(new Error('GitHub is down'));
+      serverInfoMock.getGitHubRelease.mockRejectedValue(new Error('GitHub is down'));
       await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.FAILED);
       expect(systemMock.set).not.toHaveBeenCalled();
       expect(eventMock.clientBroadcast).not.toHaveBeenCalled();
       expect(loggerMock.warn).toHaveBeenCalled();
     });
   });
+
+  describe('onWebsocketConnectionEvent', () => {
+    it('should send on_server_version client event', async () => {
+      await sut.onWebsocketConnection({ userId: '42' });
+      expect(eventMock.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
+      expect(eventMock.clientSend).toHaveBeenCalledTimes(1);
+    });
+
+    it('should also send a new release notification', async () => {
+      systemMock.get.mockResolvedValue({ checkedAt: '2024-01-01', releaseVersion: 'v1.42.0' });
+      await sut.onWebsocketConnection({ userId: '42' });
+      expect(eventMock.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
+      expect(eventMock.clientSend).toHaveBeenCalledWith('on_new_release', '42', expect.any(Object));
+    });
+  });
 });
diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts
index 468e8c9bdd..ff4fa3c6bf 100644
--- a/server/src/services/version.service.ts
+++ b/server/src/services/version.service.ts
@@ -1,17 +1,15 @@
-import { Inject, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
 import { DateTime } from 'luxon';
 import semver, { SemVer } from 'semver';
-import { isDev, serverVersion } from 'src/constants';
-import { SystemConfigCore } from 'src/cores/system-config.core';
-import { OnEmit, OnServerEvent } from 'src/decorators';
+import { serverVersion } from 'src/constants';
+import { OnEvent, OnJob } from 'src/decorators';
 import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
 import { VersionCheckMetadata } from 'src/entities/system-metadata.entity';
-import { SystemMetadataKey } from 'src/enum';
-import { ClientEvent, IEventRepository, ServerEvent, ServerEventMap } from 'src/interfaces/event.interface';
-import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
-import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { ImmichEnvironment, SystemMetadataKey } from 'src/enum';
+import { DatabaseLock } from 'src/interfaces/database.interface';
+import { ArgOf } from 'src/interfaces/event.interface';
+import { JobName, JobStatus, QueueName } from 'src/interfaces/job.interface';
+import { BaseService } from 'src/services/base.service';
 
 const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => {
   return {
@@ -23,42 +21,44 @@ const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): Re
 };
 
 @Injectable()
-export class VersionService {
-  private configCore: SystemConfigCore;
-
-  constructor(
-    @Inject(IEventRepository) private eventRepository: IEventRepository,
-    @Inject(IJobRepository) private jobRepository: IJobRepository,
-    @Inject(IServerInfoRepository) private repository: IServerInfoRepository,
-    @Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository,
-    @Inject(ILoggerRepository) private logger: ILoggerRepository,
-  ) {
-    this.logger.setContext(VersionService.name);
-    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
-  }
-
-  @OnEmit({ event: 'app.bootstrap' })
+export class VersionService extends BaseService {
+  @OnEvent({ name: 'app.bootstrap' })
   async onBootstrap(): Promise<void> {
     await this.handleVersionCheck();
+
+    await this.databaseRepository.withLock(DatabaseLock.VersionHistory, async () => {
+      const latest = await this.versionRepository.getLatest();
+      const current = serverVersion.toString();
+      if (!latest || latest.version !== current) {
+        this.logger.log(`Version has changed, adding ${current} to history`);
+        await this.versionRepository.create({ version: current });
+      }
+    });
   }
 
   getVersion() {
     return ServerVersionResponseDto.fromSemVer(serverVersion);
   }
 
+  getVersionHistory() {
+    return this.versionRepository.getAll();
+  }
+
   async handleQueueVersionCheck() {
     await this.jobRepository.queue({ name: JobName.VERSION_CHECK, data: {} });
   }
 
+  @OnJob({ name: JobName.VERSION_CHECK, queue: QueueName.BACKGROUND_TASK })
   async handleVersionCheck(): Promise<JobStatus> {
     try {
       this.logger.debug('Running version check');
 
-      if (isDev()) {
+      const { environment } = this.configRepository.getEnv();
+      if (environment === ImmichEnvironment.DEVELOPMENT) {
         return JobStatus.SKIPPED;
       }
 
-      const { newVersionCheck } = await this.configCore.getConfig({ withCache: true });
+      const { newVersionCheck } = await this.getConfig({ withCache: true });
       if (!newVersionCheck.enabled) {
         return JobStatus.SKIPPED;
       }
@@ -73,14 +73,15 @@ export class VersionService {
         }
       }
 
-      const { tag_name: releaseVersion, published_at: publishedAt } = await this.repository.getGitHubRelease();
+      const { tag_name: releaseVersion, published_at: publishedAt } =
+        await this.serverInfoRepository.getGitHubRelease();
       const metadata: VersionCheckMetadata = { checkedAt: DateTime.utc().toISO(), releaseVersion };
 
       await this.systemMetadataRepository.set(SystemMetadataKey.VERSION_CHECK_STATE, metadata);
 
       if (semver.gt(releaseVersion, serverVersion)) {
         this.logger.log(`Found ${releaseVersion}, released at ${new Date(publishedAt).toLocaleString()}`);
-        this.eventRepository.clientBroadcast(ClientEvent.NEW_RELEASE, asNotification(metadata));
+        this.eventRepository.clientBroadcast('on_new_release', asNotification(metadata));
       }
     } catch (error: Error | any) {
       this.logger.warn(`Unable to run version check: ${error}`, error?.stack);
@@ -90,12 +91,12 @@ export class VersionService {
     return JobStatus.SUCCESS;
   }
 
-  @OnServerEvent(ServerEvent.WEBSOCKET_CONNECT)
-  async onWebsocketConnection({ userId }: ServerEventMap[ServerEvent.WEBSOCKET_CONNECT]) {
-    this.eventRepository.clientSend(ClientEvent.SERVER_VERSION, userId, serverVersion);
+  @OnEvent({ name: 'websocket.connect' })
+  async onWebsocketConnection({ userId }: ArgOf<'websocket.connect'>) {
+    this.eventRepository.clientSend('on_server_version', userId, serverVersion);
     const metadata = await this.systemMetadataRepository.get(SystemMetadataKey.VERSION_CHECK_STATE);
     if (metadata) {
-      this.eventRepository.clientSend(ClientEvent.NEW_RELEASE, userId, asNotification(metadata));
+      this.eventRepository.clientSend('on_new_release', userId, asNotification(metadata));
     }
   }
 }
diff --git a/server/src/services/view.service.spec.ts b/server/src/services/view.service.spec.ts
index 3f9aa9f2f5..e9373ce66f 100644
--- a/server/src/services/view.service.spec.ts
+++ b/server/src/services/view.service.spec.ts
@@ -1,21 +1,18 @@
 import { mapAsset } from 'src/dtos/asset-response.dto';
-import { IAssetRepository } from 'src/interfaces/asset.interface';
-
+import { IViewRepository } from 'src/interfaces/view.interface';
 import { ViewService } from 'src/services/view.service';
 import { assetStub } from 'test/fixtures/asset.stub';
 import { authStub } from 'test/fixtures/auth.stub';
-import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
+import { newTestService } from 'test/utils';
 
 import { Mocked } from 'vitest';
 
 describe(ViewService.name, () => {
   let sut: ViewService;
-  let assetMock: Mocked<IAssetRepository>;
+  let viewMock: Mocked<IViewRepository>;
 
   beforeEach(() => {
-    assetMock = newAssetRepositoryMock();
-
-    sut = new ViewService(assetMock);
+    ({ sut, viewMock } = newTestService(ViewService));
   });
 
   it('should work', () => {
@@ -25,12 +22,12 @@ describe(ViewService.name, () => {
   describe('getUniqueOriginalPaths', () => {
     it('should return unique original paths', async () => {
       const mockPaths = ['path1', 'path2', 'path3'];
-      assetMock.getUniqueOriginalPaths.mockResolvedValue(mockPaths);
+      viewMock.getUniqueOriginalPaths.mockResolvedValue(mockPaths);
 
       const result = await sut.getUniqueOriginalPaths(authStub.admin);
 
       expect(result).toEqual(mockPaths);
-      expect(assetMock.getUniqueOriginalPaths).toHaveBeenCalledWith(authStub.admin.user.id);
+      expect(viewMock.getUniqueOriginalPaths).toHaveBeenCalledWith(authStub.admin.user.id);
     });
   });
 
@@ -45,11 +42,11 @@ describe(ViewService.name, () => {
 
       const mockAssetReponseDto = mockAssets.map((a) => mapAsset(a, { auth: authStub.admin }));
 
-      assetMock.getAssetsByOriginalPath.mockResolvedValue(mockAssets);
+      viewMock.getAssetsByOriginalPath.mockResolvedValue(mockAssets);
 
       const result = await sut.getAssetsByOriginalPath(authStub.admin, path);
       expect(result).toEqual(mockAssetReponseDto);
-      await expect(assetMock.getAssetsByOriginalPath(authStub.admin.user.id, path)).resolves.toEqual(mockAssets);
+      await expect(viewMock.getAssetsByOriginalPath(authStub.admin.user.id, path)).resolves.toEqual(mockAssets);
     });
   });
 });
diff --git a/server/src/services/view.service.ts b/server/src/services/view.service.ts
index 1bf9a3408c..cb80536870 100644
--- a/server/src/services/view.service.ts
+++ b/server/src/services/view.service.ts
@@ -1,18 +1,14 @@
-import { Inject } from '@nestjs/common';
 import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
-import { IAssetRepository } from 'src/interfaces/asset.interface';
-
-export class ViewService {
-  constructor(@Inject(IAssetRepository) private assetRepository: IAssetRepository) {}
+import { BaseService } from 'src/services/base.service';
 
+export class ViewService extends BaseService {
   getUniqueOriginalPaths(auth: AuthDto): Promise<string[]> {
-    return this.assetRepository.getUniqueOriginalPaths(auth.user.id);
+    return this.viewRepository.getUniqueOriginalPaths(auth.user.id);
   }
 
   async getAssetsByOriginalPath(auth: AuthDto, path: string): Promise<AssetResponseDto[]> {
-    const assets = await this.assetRepository.getAssetsByOriginalPath(auth.user.id, path);
-
+    const assets = await this.viewRepository.getAssetsByOriginalPath(auth.user.id, path);
     return assets.map((asset) => mapAsset(asset, { auth }));
   }
 }
diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts
index 44c291e139..f8bed5485f 100644
--- a/server/src/utils/asset.util.ts
+++ b/server/src/utils/asset.util.ts
@@ -1,6 +1,7 @@
 import { BadRequestException } from '@nestjs/common';
 import { StorageCore } from 'src/cores/storage.core';
 import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
+import { UploadFieldName } from 'src/dtos/asset-media.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
 import { AssetFileEntity } from 'src/entities/asset-files.entity';
 import { AssetFileType, AssetType, Permission } from 'src/enum';
@@ -8,6 +9,9 @@ import { IAccessRepository } from 'src/interfaces/access.interface';
 import { IAssetRepository } from 'src/interfaces/asset.interface';
 import { IEventRepository } from 'src/interfaces/event.interface';
 import { IPartnerRepository } from 'src/interfaces/partner.interface';
+import { AuthRequest } from 'src/middleware/auth.guard';
+import { ImmichFile } from 'src/middleware/file-upload.interceptor';
+import { UploadFile } from 'src/services/asset-media.service';
 import { checkAccess } from 'src/utils/access';
 
 export interface IBulkAsset {
@@ -181,3 +185,21 @@ export const onAfterUnlink = async (
   await assetRepository.update({ id: livePhotoVideoId, isVisible: true });
   await eventRepository.emit('asset.show', { assetId: livePhotoVideoId, userId });
 };
+
+export function mapToUploadFile(file: ImmichFile): UploadFile {
+  return {
+    uuid: file.uuid,
+    checksum: file.checksum,
+    originalPath: file.path,
+    originalName: Buffer.from(file.originalname, 'latin1').toString('utf8'),
+    size: file.size,
+  };
+}
+
+export const asRequest = (request: AuthRequest, file: Express.Multer.File) => {
+  return {
+    auth: request.user || null,
+    fieldName: file.fieldname as UploadFieldName,
+    file: mapToUploadFile(file as ImmichFile),
+  };
+};
diff --git a/server/src/utils/config.ts b/server/src/utils/config.ts
new file mode 100644
index 0000000000..ce8a2da839
--- /dev/null
+++ b/server/src/utils/config.ts
@@ -0,0 +1,132 @@
+import AsyncLock from 'async-lock';
+import { instanceToPlain, plainToInstance } from 'class-transformer';
+import { validate } from 'class-validator';
+import { load as loadYaml } from 'js-yaml';
+import * as _ from 'lodash';
+import { SystemConfig, defaults } from 'src/config';
+import { SystemConfigDto } from 'src/dtos/system-config.dto';
+import { SystemMetadataKey } from 'src/enum';
+import { IConfigRepository } from 'src/interfaces/config.interface';
+import { DatabaseLock } from 'src/interfaces/database.interface';
+import { ILoggerRepository } from 'src/interfaces/logger.interface';
+import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { getKeysDeep, unsetDeep } from 'src/utils/misc';
+import { DeepPartial } from 'typeorm';
+
+export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise<void>;
+
+type RepoDeps = {
+  configRepo: IConfigRepository;
+  metadataRepo: ISystemMetadataRepository;
+  logger: ILoggerRepository;
+};
+
+const asyncLock = new AsyncLock();
+let config: SystemConfig | null = null;
+let lastUpdated: number | null = null;
+
+export const clearConfigCache = () => {
+  config = null;
+  lastUpdated = null;
+};
+
+export const getConfig = async (repos: RepoDeps, { withCache }: { withCache: boolean }): Promise<SystemConfig> => {
+  if (!withCache || !config) {
+    const timestamp = lastUpdated;
+    await asyncLock.acquire(DatabaseLock[DatabaseLock.GetSystemConfig], async () => {
+      if (timestamp === lastUpdated) {
+        config = await buildConfig(repos);
+        lastUpdated = Date.now();
+      }
+    });
+  }
+
+  return config!;
+};
+
+export const updateConfig = async (repos: RepoDeps, newConfig: SystemConfig): Promise<SystemConfig> => {
+  const { metadataRepo } = repos;
+  // get the difference between the new config and the default config
+  const partialConfig: DeepPartial<SystemConfig> = {};
+  for (const property of getKeysDeep(defaults)) {
+    const newValue = _.get(newConfig, property);
+    const isEmpty = newValue === undefined || newValue === null || newValue === '';
+    const defaultValue = _.get(defaults, property);
+    const isEqual = newValue === defaultValue || _.isEqual(newValue, defaultValue);
+
+    if (isEmpty || isEqual) {
+      continue;
+    }
+
+    _.set(partialConfig, property, newValue);
+  }
+
+  await metadataRepo.set(SystemMetadataKey.SYSTEM_CONFIG, partialConfig);
+
+  return getConfig(repos, { withCache: false });
+};
+
+const loadFromFile = async ({ metadataRepo, logger }: RepoDeps, filepath: string) => {
+  try {
+    const file = await metadataRepo.readFile(filepath);
+    return loadYaml(file.toString()) as unknown;
+  } catch (error: Error | any) {
+    logger.error(`Unable to load configuration file: ${filepath}`);
+    logger.error(error);
+    throw error;
+  }
+};
+
+const buildConfig = async (repos: RepoDeps) => {
+  const { configRepo, metadataRepo, logger } = repos;
+  const { configFile } = configRepo.getEnv();
+
+  // load partial
+  const partial = configFile
+    ? await loadFromFile(repos, configFile)
+    : await metadataRepo.get(SystemMetadataKey.SYSTEM_CONFIG);
+
+  // merge with defaults
+  const rawConfig = _.cloneDeep(defaults);
+  for (const property of getKeysDeep(partial)) {
+    _.set(rawConfig, property, _.get(partial, property));
+  }
+
+  // check for extra properties
+  const unknownKeys = _.cloneDeep(rawConfig);
+  for (const property of getKeysDeep(defaults)) {
+    unsetDeep(unknownKeys, property);
+  }
+
+  if (!_.isEmpty(unknownKeys)) {
+    logger.warn(`Unknown keys found: ${JSON.stringify(unknownKeys, null, 2)}`);
+  }
+
+  // validate full config
+  const instance = plainToInstance(SystemConfigDto, rawConfig);
+  const errors = await validate(instance);
+  if (errors.length > 0) {
+    if (configFile) {
+      throw new Error(`Invalid value(s) in file: ${errors}`);
+    } else {
+      logger.error('Validation error', errors);
+    }
+  }
+
+  // return config with class-transform changes
+  const config = instanceToPlain(instance) as SystemConfig;
+
+  if (config.server.externalDomain.length > 0) {
+    config.server.externalDomain = new URL(config.server.externalDomain).origin;
+  }
+
+  if (!config.ffmpeg.acceptedVideoCodecs.includes(config.ffmpeg.targetVideoCodec)) {
+    config.ffmpeg.acceptedVideoCodecs.push(config.ffmpeg.targetVideoCodec);
+  }
+
+  if (!config.ffmpeg.acceptedAudioCodecs.includes(config.ffmpeg.targetAudioCodec)) {
+    config.ffmpeg.acceptedAudioCodecs.push(config.ffmpeg.targetAudioCodec);
+  }
+
+  return config;
+};
diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts
index f3232eb78b..ad2198b38c 100644
--- a/server/src/utils/database.ts
+++ b/server/src/utils/database.ts
@@ -1,4 +1,5 @@
 import _ from 'lodash';
+import { AssetFaceEntity } from 'src/entities/asset-face.entity';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { AssetSearchBuilderOptions } from 'src/interfaces/search.interface';
 import { Between, IsNull, LessThanOrEqual, MoreThanOrEqual, Not, SelectQueryBuilder } from 'typeorm';
@@ -80,7 +81,7 @@ export function searchAssetBuilder(
     });
   }
 
-  const status = _.pick(options, ['isFavorite', 'isOffline', 'isVisible', 'type']);
+  const status = _.pick(options, ['isFavorite', 'isVisible', 'type']);
   const {
     isArchived,
     isEncoded,
@@ -89,9 +90,7 @@ export function searchAssetBuilder(
     isNotInAlbum,
     withFaces,
     withPeople,
-    withSmartInfo,
     personIds,
-    withExif,
     withStacked,
     trashedAfter,
     trashedBefore,
@@ -120,23 +119,20 @@ export function searchAssetBuilder(
   }
 
   if (withPeople) {
-    builder.leftJoinAndSelect(`${builder.alias}.person`, 'person');
-  }
-
-  if (withSmartInfo) {
-    builder.leftJoinAndSelect(`${builder.alias}.smartInfo`, 'smartInfo');
+    builder.leftJoinAndSelect('faces.person', 'person');
   }
 
   if (personIds && personIds.length > 0) {
-    builder
-      .leftJoin(`${builder.alias}.faces`, 'faces')
-      .andWhere('faces.personId IN (:...personIds)', { personIds })
-      .addGroupBy(`${builder.alias}.id`)
-      .having('COUNT(DISTINCT faces.personId) = :personCount', { personCount: personIds.length });
+    const cte = builder
+      .createQueryBuilder()
+      .select('faces."assetId"')
+      .from(AssetFaceEntity, 'faces')
+      .where('faces."personId" IN (:...personIds)', { personIds })
+      .groupBy(`faces."assetId"`)
+      .having(`COUNT(DISTINCT faces."personId") = :personCount`, { personCount: personIds.length });
+    builder.addCommonTableExpression(cte, 'face_ids').innerJoin('face_ids', 'a', 'a."assetId" = asset.id');
 
-    if (withExif) {
-      builder.addGroupBy('exifInfo.assetId');
-    }
+    builder.getQuery(); // typeorm mixes up parameters without this  (੭ °ཀ°)੭
   }
 
   if (withStacked) {
diff --git a/server/src/utils/date-time.ts b/server/src/utils/date-time.ts
new file mode 100644
index 0000000000..e1578cbb19
--- /dev/null
+++ b/server/src/utils/date-time.ts
@@ -0,0 +1,5 @@
+import { AssetEntity } from 'src/entities/asset.entity';
+
+export const getAssetDateTime = (asset: AssetEntity | undefined) => {
+  return asset?.exifInfo?.dateTimeOriginal || asset?.fileCreatedAt;
+};
diff --git a/server/src/utils/events.ts b/server/src/utils/events.ts
deleted file mode 100644
index 064c9f7507..0000000000
--- a/server/src/utils/events.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { ModuleRef, Reflector } from '@nestjs/core';
-import _ from 'lodash';
-import { EmitConfig } from 'src/decorators';
-import { EmitEvent, EmitHandler, IEventRepository } from 'src/interfaces/event.interface';
-import { Metadata } from 'src/middleware/auth.guard';
-import { services } from 'src/services';
-
-type Item<T extends EmitEvent> = {
-  event: T;
-  handler: EmitHandler<T>;
-  priority: number;
-  label: string;
-};
-
-export class ImmichStartupError extends Error {}
-export const isStartUpError = (error: unknown): error is ImmichStartupError => error instanceof ImmichStartupError;
-
-export const setupEventHandlers = (moduleRef: ModuleRef) => {
-  const reflector = moduleRef.get(Reflector, { strict: false });
-  const repository = moduleRef.get<IEventRepository>(IEventRepository);
-  const items: Item<EmitEvent>[] = [];
-
-  // discovery
-  for (const Service of services) {
-    const instance = moduleRef.get<any>(Service);
-    const ctx = Object.getPrototypeOf(instance);
-    for (const property of Object.getOwnPropertyNames(ctx)) {
-      const descriptor = Object.getOwnPropertyDescriptor(ctx, property);
-      if (!descriptor || descriptor.get || descriptor.set) {
-        continue;
-      }
-
-      const handler = instance[property];
-      if (typeof handler !== 'function') {
-        continue;
-      }
-
-      const options = reflector.get<EmitConfig>(Metadata.ON_EMIT_CONFIG, handler);
-      if (!options) {
-        continue;
-      }
-
-      items.push({
-        event: options.event,
-        priority: options.priority || 0,
-        handler: handler.bind(instance),
-        label: `${Service.name}.${handler.name}`,
-      });
-    }
-  }
-
-  const handlers = _.orderBy(items, ['priority'], ['asc']);
-
-  // register by priority
-  for (const { event, handler } of handlers) {
-    repository.on(event as EmitEvent, handler);
-  }
-
-  return handlers;
-};
diff --git a/server/src/utils/file.ts b/server/src/utils/file.ts
index 53a4d571dc..869e4d7876 100644
--- a/server/src/utils/file.ts
+++ b/server/src/utils/file.ts
@@ -3,6 +3,7 @@ import { NextFunction, Response } from 'express';
 import { access, constants } from 'node:fs/promises';
 import { basename, extname, isAbsolute } from 'node:path';
 import { promisify } from 'node:util';
+import { CacheControl } from 'src/enum';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { ImmichReadStream } from 'src/interfaces/storage.interface';
 import { isConnectionAborted } from 'src/utils/misc';
@@ -19,16 +20,11 @@ export function getLivePhotoMotionFilename(stillName: string, motionName: string
   return getFileNameWithoutExtension(stillName) + extname(motionName);
 }
 
-export enum CacheControl {
-  PRIVATE_WITH_CACHE = 'private_with_cache',
-  PRIVATE_WITHOUT_CACHE = 'private_without_cache',
-  NONE = 'none',
-}
-
 export class ImmichFileResponse {
   public readonly path!: string;
   public readonly contentType!: string;
   public readonly cacheControl!: CacheControl;
+  public readonly fileName?: string;
 
   constructor(response: ImmichFileResponse) {
     Object.assign(this, response);
@@ -61,6 +57,9 @@ export const sendFile = async (
     }
 
     res.header('Content-Type', file.contentType);
+    if (file.fileName) {
+      res.header('Content-Disposition', `inline; filename*=UTF-8''${encodeURIComponent(file.fileName)}`);
+    }
 
     const options: SendFileOptions = { dotfiles: 'allow' };
     if (!isAbsolute(file.path)) {
diff --git a/server/src/utils/healthcheck.ts b/server/src/utils/healthcheck.ts
deleted file mode 100644
index 763fce81b4..0000000000
--- a/server/src/utils/healthcheck.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/usr/bin/env node
-const port = Number(process.env.IMMICH_PORT) || 3001;
-const controller = new AbortController();
-
-const main = async () => {
-  if (!process.env.IMMICH_WORKERS_INCLUDE?.includes('api')) {
-    process.exit();
-  }
-
-  const timeout = setTimeout(() => controller.abort(), 2000);
-  try {
-    const response = await fetch(`http://localhost:${port}/api/server-info/ping`, {
-      signal: controller.signal,
-    });
-
-    if (response.ok) {
-      const body = await response.json();
-      if (body.res === 'pong') {
-        process.exit();
-      }
-    }
-  } catch (error) {
-    if (error instanceof DOMException === false) {
-      console.error(error);
-    }
-  } finally {
-    clearTimeout(timeout);
-  }
-
-  process.exit(1);
-};
-
-void main();
diff --git a/server/src/utils/instrumentation.ts b/server/src/utils/instrumentation.ts
deleted file mode 100644
index 484ba5901c..0000000000
--- a/server/src/utils/instrumentation.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
-import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
-import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
-import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis';
-import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core';
-import { PgInstrumentation } from '@opentelemetry/instrumentation-pg';
-import { NodeSDK, contextBase, metrics, resources } from '@opentelemetry/sdk-node';
-import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
-import { snakeCase, startCase } from 'lodash';
-import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces';
-import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils';
-import { performance } from 'node:perf_hooks';
-import { excludePaths, serverVersion } from 'src/constants';
-import { DecorateAll } from 'src/decorators';
-
-let metricsEnabled = process.env.IMMICH_METRICS === 'true';
-export const hostMetrics =
-  process.env.IMMICH_HOST_METRICS == null ? metricsEnabled : process.env.IMMICH_HOST_METRICS === 'true';
-export const apiMetrics =
-  process.env.IMMICH_API_METRICS == null ? metricsEnabled : process.env.IMMICH_API_METRICS === 'true';
-export const repoMetrics =
-  process.env.IMMICH_IO_METRICS == null ? metricsEnabled : process.env.IMMICH_IO_METRICS === 'true';
-export const jobMetrics =
-  process.env.IMMICH_JOB_METRICS == null ? metricsEnabled : process.env.IMMICH_JOB_METRICS === 'true';
-
-metricsEnabled ||= hostMetrics || apiMetrics || repoMetrics || jobMetrics;
-if (!metricsEnabled && process.env.OTEL_SDK_DISABLED === undefined) {
-  process.env.OTEL_SDK_DISABLED = 'true';
-}
-
-const aggregation = new metrics.ExplicitBucketHistogramAggregation(
-  [0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10_000],
-  true,
-);
-
-let otelSingleton: NodeSDK | undefined;
-
-export const otelStart = (port: number) => {
-  if (otelSingleton) {
-    throw new Error('OpenTelemetry SDK already started');
-  }
-  otelSingleton = new NodeSDK({
-    resource: new resources.Resource({
-      [SemanticResourceAttributes.SERVICE_NAME]: `immich`,
-      [SemanticResourceAttributes.SERVICE_VERSION]: serverVersion.toString(),
-    }),
-    metricReader: new PrometheusExporter({ port }),
-    contextManager: new AsyncLocalStorageContextManager(),
-    instrumentations: [
-      new HttpInstrumentation(),
-      new IORedisInstrumentation(),
-      new NestInstrumentation(),
-      new PgInstrumentation(),
-    ],
-    views: [new metrics.View({ aggregation, instrumentName: '*', instrumentUnit: 'ms' })],
-  });
-  otelSingleton.start();
-};
-
-export const otelShutdown = async () => {
-  if (otelSingleton) {
-    await otelSingleton.shutdown();
-    otelSingleton = undefined;
-  }
-};
-
-export const otelConfig: OpenTelemetryModuleOptions = {
-  metrics: {
-    hostMetrics,
-    apiMetrics: {
-      enable: apiMetrics,
-      ignoreRoutes: excludePaths,
-    },
-  },
-};
-
-function ExecutionTimeHistogram({
-  description,
-  unit = 'ms',
-  valueType = contextBase.ValueType.DOUBLE,
-}: contextBase.MetricOptions = {}) {
-  return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
-    if (!repoMetrics || process.env.OTEL_SDK_DISABLED) {
-      return;
-    }
-
-    const method = descriptor.value;
-    const className = target.constructor.name as string;
-    const propertyName = String(propertyKey);
-    const metricName = `${snakeCase(className).replaceAll(/_(?=(repository)|(controller)|(provider)|(service)|(module))/g, '.')}.${snakeCase(propertyName)}.duration`;
-
-    const metricDescription =
-      description ??
-      `The elapsed time in ${unit} for the ${startCase(className)} to ${startCase(propertyName).toLowerCase()}`;
-
-    let histogram: contextBase.Histogram | undefined;
-
-    descriptor.value = function (...args: any[]) {
-      const start = performance.now();
-      const result = method.apply(this, args);
-
-      void Promise.resolve(result)
-        .then(() => {
-          const end = performance.now();
-          if (!histogram) {
-            histogram = contextBase.metrics
-              .getMeter('immich')
-              .createHistogram(metricName, { description: metricDescription, unit, valueType });
-          }
-          histogram.record(end - start, {});
-        })
-        .catch(() => {
-          // noop
-        });
-
-      return result;
-    };
-
-    copyMetadataFromFunctionToFunction(method, descriptor.value);
-  };
-}
-
-export const Instrumentation = () => DecorateAll(ExecutionTimeHistogram());
diff --git a/server/src/utils/logger.ts b/server/src/utils/logger.ts
index d4eb02ead2..cf66404d69 100644
--- a/server/src/utils/logger.ts
+++ b/server/src/utils/logger.ts
@@ -2,24 +2,6 @@ import { HttpException } from '@nestjs/common';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { TypeORMError } from 'typeorm';
 
-type ColorTextFn = (text: string) => string;
-
-const isColorAllowed = () => !process.env.NO_COLOR;
-const colorIfAllowed = (colorFn: ColorTextFn) => (text: string) => (isColorAllowed() ? colorFn(text) : text);
-
-export const LogColor = {
-  red: colorIfAllowed((text: string) => `\u001B[31m${text}\u001B[39m`),
-  green: colorIfAllowed((text: string) => `\u001B[32m${text}\u001B[39m`),
-  yellow: colorIfAllowed((text: string) => `\u001B[33m${text}\u001B[39m`),
-  blue: colorIfAllowed((text: string) => `\u001B[34m${text}\u001B[39m`),
-  magentaBright: colorIfAllowed((text: string) => `\u001B[95m${text}\u001B[39m`),
-  cyanBright: colorIfAllowed((text: string) => `\u001B[96m${text}\u001B[39m`),
-};
-
-export const LogStyle = {
-  bold: colorIfAllowed((text: string) => `\u001B[1m${text}\u001B[0m`),
-};
-
 export const logGlobalError = (logger: ILoggerRepository, error: Error) => {
   if (error instanceof HttpException) {
     const status = error.getStatus();
@@ -34,7 +16,7 @@ export const logGlobalError = (logger: ILoggerRepository, error: Error) => {
   }
 
   if (error instanceof Error) {
-    logger.error(`Unknown error: ${error}`);
+    logger.error(`Unknown error: ${error}`, error?.stack);
     return;
   }
 };
diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts
index 8068f4a5e6..678e8cb15a 100644
--- a/server/src/utils/media.ts
+++ b/server/src/utils/media.ts
@@ -1,11 +1,13 @@
-import { CQMode, ToneMapping, TranscodeHWAccel, TranscodeTarget, VideoCodec } from 'src/config';
 import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
+import { CQMode, ToneMapping, TranscodeHWAccel, TranscodeTarget, VideoCodec } from 'src/enum';
 import {
   AudioStreamInfo,
   BitrateDistribution,
   TranscodeCommand,
   VideoCodecHWConfig,
   VideoCodecSWConfig,
+  VideoFormat,
+  VideoInterfaces,
   VideoStreamInfo,
 } from 'src/interfaces/media.interface';
 
@@ -13,11 +15,11 @@ export class BaseConfig implements VideoCodecSWConfig {
   readonly presets = ['veryslow', 'slower', 'slow', 'medium', 'fast', 'faster', 'veryfast', 'superfast', 'ultrafast'];
   protected constructor(protected config: SystemConfigFFmpegDto) {}
 
-  static create(config: SystemConfigFFmpegDto, devices: string[] = [], hasMaliOpenCL = false): VideoCodecSWConfig {
+  static create(config: SystemConfigFFmpegDto, interfaces: VideoInterfaces): VideoCodecSWConfig {
     if (config.accel === TranscodeHWAccel.DISABLED) {
       return this.getSWCodecConfig(config);
     }
-    return this.getHWCodecConfig(config, devices, hasMaliOpenCL);
+    return this.getHWCodecConfig(config, interfaces);
   }
 
   private static getSWCodecConfig(config: SystemConfigFFmpegDto) {
@@ -40,26 +42,31 @@ export class BaseConfig implements VideoCodecSWConfig {
     }
   }
 
-  private static getHWCodecConfig(config: SystemConfigFFmpegDto, devices: string[] = [], hasMaliOpenCL = false) {
+  private static getHWCodecConfig(config: SystemConfigFFmpegDto, interfaces: VideoInterfaces) {
     let handler: VideoCodecHWConfig;
     switch (config.accel) {
       case TranscodeHWAccel.NVENC: {
-        handler = config.accelDecode ? new NvencHwDecodeConfig(config) : new NvencSwDecodeConfig(config);
+        handler = config.accelDecode
+          ? new NvencHwDecodeConfig(config, interfaces)
+          : new NvencSwDecodeConfig(config, interfaces);
         break;
       }
       case TranscodeHWAccel.QSV: {
-        handler = config.accelDecode ? new QsvHwDecodeConfig(config, devices) : new QsvSwDecodeConfig(config, devices);
+        handler = config.accelDecode
+          ? new QsvHwDecodeConfig(config, interfaces)
+          : new QsvSwDecodeConfig(config, interfaces);
         break;
       }
       case TranscodeHWAccel.VAAPI: {
-        handler = new VAAPIConfig(config, devices);
+        handler = config.accelDecode
+          ? new VaapiHwDecodeConfig(config, interfaces)
+          : new VaapiSwDecodeConfig(config, interfaces);
         break;
       }
       case TranscodeHWAccel.RKMPP: {
-        handler =
-          config.accelDecode && hasMaliOpenCL
-            ? new RkmppHwDecodeConfig(config, devices)
-            : new RkmppSwDecodeConfig(config, devices);
+        handler = config.accelDecode
+          ? new RkmppHwDecodeConfig(config, interfaces)
+          : new RkmppSwDecodeConfig(config, interfaces);
         break;
       }
       default: {
@@ -75,11 +82,17 @@ export class BaseConfig implements VideoCodecSWConfig {
     return handler;
   }
 
-  getCommand(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
+  getCommand(
+    target: TranscodeTarget,
+    videoStream: VideoStreamInfo,
+    audioStream?: AudioStreamInfo,
+    format?: VideoFormat,
+  ) {
     const options = {
-      inputOptions: this.getBaseInputOptions(videoStream),
+      inputOptions: this.getBaseInputOptions(videoStream, format),
       outputOptions: [...this.getBaseOutputOptions(target, videoStream, audioStream), '-v verbose'],
       twoPass: this.eligibleForTwoPass(),
+      progress: { frameCount: videoStream.frameCount, percentInterval: 5 },
     } as TranscodeCommand;
     if ([TranscodeTarget.ALL, TranscodeTarget.VIDEO].includes(target)) {
       const filters = this.getFilterOptions(videoStream);
@@ -98,7 +111,7 @@ export class BaseConfig implements VideoCodecSWConfig {
   }
 
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  getBaseInputOptions(videoStream: VideoStreamInfo): string[] {
+  getBaseInputOptions(videoStream: VideoStreamInfo, format?: VideoFormat): string[] {
     return this.getInputThreadOptions();
   }
 
@@ -115,7 +128,6 @@ export class BaseConfig implements VideoCodecSWConfig {
       '-fps_mode passthrough',
       // explicitly selects the video stream instead of leaving it up to FFmpeg
       `-map 0:${videoStream.index}`,
-      '-strict unofficial',
     ];
 
     if (audioStream) {
@@ -147,7 +159,11 @@ export class BaseConfig implements VideoCodecSWConfig {
       options.push(`scale=${this.getScaling(videoStream)}`);
     }
 
-    options.push(...this.getToneMapping(videoStream), 'format=yuv420p');
+    options.push(...this.getToneMapping(videoStream));
+    if (options.length === 0 && !videoStream.pixelFormat.endsWith('420p')) {
+      options.push(`format=yuv420p`);
+    }
+
     return options;
   }
 
@@ -275,27 +291,14 @@ export class BaseConfig implements VideoCodecSWConfig {
     };
   }
 
-  getNPL() {
-    if (this.config.npl <= 0) {
-      // since hable already outputs a darker image, we use a lower npl value for it
-      return this.config.tonemap === ToneMapping.HABLE ? 100 : 250;
-    } else {
-      return this.config.npl;
-    }
-  }
-
   getToneMapping(videoStream: VideoStreamInfo) {
     if (!this.shouldToneMap(videoStream)) {
       return [];
     }
 
-    const colors = this.getColors();
-
-    return [
-      `zscale=t=linear:npl=${this.getNPL()}`,
-      `tonemap=${this.config.tonemap}:desat=0`,
-      `zscale=p=${colors.primaries}:t=${colors.transfer}:m=${colors.matrix}:range=pc`,
-    ];
+    const { primaries, transfer, matrix } = this.getColors();
+    const options = `tonemapx=tonemap=${this.config.tonemap}:desat=0:p=${primaries}:t=${transfer}:m=${matrix}:r=pc:peak=100:format=yuv420p`;
+    return [options];
   }
 
   getAudioCodec(): string {
@@ -324,14 +327,16 @@ export class BaseConfig implements VideoCodecSWConfig {
 }
 
 export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig {
-  protected devices: string[];
+  protected device: string;
+  protected interfaces: VideoInterfaces;
 
   constructor(
     protected config: SystemConfigFFmpegDto,
-    devices: string[] = [],
+    interfaces: VideoInterfaces,
   ) {
     super(config);
-    this.devices = this.validateDevices(devices);
+    this.interfaces = interfaces;
+    this.device = this.getDevice(interfaces);
   }
 
   getSupportedCodecs() {
@@ -339,18 +344,29 @@ export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig {
   }
 
   validateDevices(devices: string[]) {
-    return devices
-      .filter((device) => device.startsWith('renderD') || device.startsWith('card'))
-      .sort((a, b) => {
-        // order GPU devices first
-        if (a.startsWith('card') && b.startsWith('renderD')) {
-          return -1;
-        }
-        if (a.startsWith('renderD') && b.startsWith('card')) {
-          return 1;
-        }
-        return -a.localeCompare(b);
-      });
+    if (devices.length === 0) {
+      throw new Error('No /dev/dri devices found. If using Docker, make sure at least one /dev/dri device is mounted');
+    }
+
+    return devices.filter(function (device) {
+      return device.startsWith('renderD') || device.startsWith('card');
+    });
+  }
+
+  getDevice({ dri }: VideoInterfaces) {
+    if (this.config.preferredHwDevice === 'auto') {
+      // eslint-disable-next-line unicorn/no-array-reduce
+      return `/dev/dri/${this.validateDevices(dri).reduce(function (a, b) {
+        return a.localeCompare(b) < 0 ? b : a;
+      })}`;
+    }
+
+    const deviceName = this.config.preferredHwDevice.replace('/dev/dri/', '');
+    if (!dri.includes(deviceName)) {
+      throw new Error(`Device '${deviceName}' does not exist. If using Docker, make sure this device is mounted`);
+    }
+
+    return `/dev/dri/${deviceName}`;
   }
 
   getVideoCodec(): string {
@@ -363,20 +379,6 @@ export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig {
     }
     return this.config.gopSize;
   }
-
-  getPreferredHardwareDevice(): string | undefined {
-    const device = this.config.preferredHwDevice;
-    if (device === 'auto') {
-      return;
-    }
-
-    const deviceName = device.replace('/dev/dri/', '');
-    if (!this.devices.includes(deviceName)) {
-      throw new Error(`Device '${device}' does not exist`);
-    }
-
-    return `/dev/dri/${deviceName}`;
-  }
 }
 
 export class ThumbnailConfig extends BaseConfig {
@@ -384,8 +386,11 @@ export class ThumbnailConfig extends BaseConfig {
     return new ThumbnailConfig(config);
   }
 
-  getBaseInputOptions(): string[] {
-    return ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int'];
+  getBaseInputOptions(videoStream: VideoStreamInfo, format?: VideoFormat): string[] {
+    // skip_frame nointra skips all frames for some MPEG-TS files. Look at ffmpeg tickets 7950 and 7895 for more details.
+    return format?.formatName === 'mpegts'
+      ? ['-sws_flags accurate_rnd+full_chroma_int']
+      : ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int'];
   }
 
   getBaseOutputOptions() {
@@ -393,19 +398,14 @@ export class ThumbnailConfig extends BaseConfig {
   }
 
   getFilterOptions(videoStream: VideoStreamInfo): string[] {
-    const options = [
+    return [
       'fps=12:eof_action=pass:round=down',
       'thumbnail=12',
       String.raw`select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20)`,
       'trim=end_frame=2',
       'reverse',
+      ...super.getFilterOptions(videoStream),
     ];
-    if (this.shouldScale(videoStream)) {
-      options.push(`scale=${this.getScaling(videoStream)}`);
-    }
-
-    options.push(...this.getToneMapping(videoStream), 'format=yuv420p');
-    return options;
   }
 
   getPresetOptions() {
@@ -421,19 +421,7 @@ export class ThumbnailConfig extends BaseConfig {
   }
 
   getScaling(videoStream: VideoStreamInfo) {
-    let options = super.getScaling(videoStream) + ':flags=lanczos+accurate_rnd+full_chroma_int';
-    if (!this.shouldToneMap(videoStream)) {
-      options += ':out_color_matrix=601:out_range=pc';
-    }
-    return options;
-  }
-
-  getColors() {
-    return {
-      primaries: 'bt709',
-      transfer: '601',
-      matrix: 'bt470bg',
-    };
+    return super.getScaling(videoStream) + ':flags=lanczos+accurate_rnd+full_chroma_int:out_range=pc';
   }
 }
 
@@ -491,6 +479,10 @@ export class VP9Config extends BaseConfig {
 }
 
 export class AV1Config extends BaseConfig {
+  getVideoCodec(): string {
+    return 'libsvtav1';
+  }
+
   getPresetOptions() {
     const speed = this.getPresetIndex() + 4; // Use 4 as slowest, giving us an effective range of 4-12 which is far more useful than 0-8
     if (speed >= 0) {
@@ -525,12 +517,16 @@ export class AV1Config extends BaseConfig {
 }
 
 export class NvencSwDecodeConfig extends BaseHWConfig {
+  getDevice() {
+    return '0';
+  }
+
   getSupportedCodecs() {
     return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.AV1];
   }
 
   getBaseInputOptions() {
-    return ['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda'];
+    return [`-init_hw_device cuda=cuda:${this.device}`, '-filter_hw_device cuda'];
   }
 
   getBaseOutputOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
@@ -553,9 +549,9 @@ export class NvencSwDecodeConfig extends BaseHWConfig {
 
   getFilterOptions(videoStream: VideoStreamInfo) {
     const options = this.getToneMapping(videoStream);
-    options.push('format=nv12', 'hwupload_cuda');
+    options.push('hwupload_cuda');
     if (this.shouldScale(videoStream)) {
-      options.push(`scale_cuda=${this.getScaling(videoStream)}`);
+      options.push(`scale_cuda=${this.getScaling(videoStream)}:format=nv12`);
     }
 
     return options;
@@ -616,6 +612,8 @@ export class NvencHwDecodeConfig extends NvencSwDecodeConfig {
     options.push(...this.getToneMapping(videoStream));
     if (options.length > 0) {
       options[options.length - 1] += ':format=nv12';
+    } else if (!videoStream.pixelFormat.endsWith('420p')) {
+      options.push('format=nv12');
     }
     return options;
   }
@@ -625,14 +623,16 @@ export class NvencHwDecodeConfig extends NvencSwDecodeConfig {
       return [];
     }
 
-    const colors = this.getColors();
+    const { matrix, primaries, transfer } = this.getColors();
     const tonemapOptions = [
       'desat=0',
-      `matrix=${colors.matrix}`,
-      `primaries=${colors.primaries}`,
+      `matrix=${matrix}`,
+      `primaries=${primaries}`,
       'range=pc',
       `tonemap=${this.config.tonemap}`,
-      `transfer=${colors.transfer}`,
+      'tonemap_mode=lum',
+      `transfer=${transfer}`,
+      'peak=100',
     ];
 
     return [`tonemap_cuda=${tonemapOptions.join(':')}`];
@@ -649,17 +649,7 @@ export class NvencHwDecodeConfig extends NvencSwDecodeConfig {
 
 export class QsvSwDecodeConfig extends BaseHWConfig {
   getBaseInputOptions() {
-    if (this.devices.length === 0) {
-      throw new Error('No QSV device found');
-    }
-
-    let qsvString = '';
-    const hwDevice = this.getPreferredHardwareDevice();
-    if (hwDevice) {
-      qsvString = `,child_device=${hwDevice}`;
-    }
-
-    return [`-init_hw_device qsv=hw${qsvString}`, '-filter_hw_device hw'];
+    return [`-init_hw_device qsv=hw,child_device=${this.device}`, '-filter_hw_device hw'];
   }
 
   getBaseOutputOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
@@ -673,9 +663,9 @@ export class QsvSwDecodeConfig extends BaseHWConfig {
 
   getFilterOptions(videoStream: VideoStreamInfo) {
     const options = this.getToneMapping(videoStream);
-    options.push('format=nv12', 'hwupload=extra_hw_frames=64');
+    options.push('hwupload=extra_hw_frames=64');
     if (this.shouldScale(videoStream)) {
-      options.push(`scale_qsv=${this.getScaling(videoStream)}`);
+      options.push(`scale_qsv=${this.getScaling(videoStream)}:mode=hq:format=nv12`);
     }
     return options;
   }
@@ -729,36 +719,30 @@ export class QsvSwDecodeConfig extends BaseHWConfig {
 
 export class QsvHwDecodeConfig extends QsvSwDecodeConfig {
   getBaseInputOptions() {
-    if (this.devices.length === 0) {
-      throw new Error('No QSV device found');
-    }
-
-    const options = [
+    return [
       '-hwaccel qsv',
       '-hwaccel_output_format qsv',
       '-async_depth 4',
       '-noautorotate',
+      `-qsv_device ${this.device}`,
       ...this.getInputThreadOptions(),
     ];
-    const hwDevice = this.getPreferredHardwareDevice();
-    if (hwDevice) {
-      options.push(`-qsv_device ${hwDevice}`);
-    }
-
-    return options;
   }
 
   getFilterOptions(videoStream: VideoStreamInfo) {
     const options = [];
-    if (this.shouldScale(videoStream) || !this.shouldToneMap(videoStream)) {
+    const tonemapOptions = this.getToneMapping(videoStream);
+    if (this.shouldScale(videoStream) || tonemapOptions.length === 0) {
       let scaling = `scale_qsv=${this.getScaling(videoStream)}:async_depth=4:mode=hq`;
-      if (!this.shouldToneMap(videoStream)) {
+      if (tonemapOptions.length === 0) {
         scaling += ':format=nv12';
       }
       options.push(scaling);
     }
-
-    options.push(...this.getToneMapping(videoStream));
+    options.push(...tonemapOptions);
+    if (options.length === 0 && !videoStream.pixelFormat.endsWith('420p')) {
+      options.push('format=nv12');
+    }
     return options;
   }
 
@@ -767,15 +751,17 @@ export class QsvHwDecodeConfig extends QsvSwDecodeConfig {
       return [];
     }
 
-    const colors = this.getColors();
+    const { matrix, primaries, transfer } = this.getColors();
     const tonemapOptions = [
       'desat=0',
       'format=nv12',
-      `matrix=${colors.matrix}`,
-      `primaries=${colors.primaries}`,
+      `matrix=${matrix}`,
+      `primaries=${primaries}`,
+      `transfer=${transfer}`,
       'range=pc',
       `tonemap=${this.config.tonemap}`,
-      `transfer=${colors.transfer}`,
+      'tonemap_mode=lum',
+      'peak=100',
     ];
 
     return [
@@ -790,25 +776,16 @@ export class QsvHwDecodeConfig extends QsvSwDecodeConfig {
   }
 }
 
-export class VAAPIConfig extends BaseHWConfig {
+export class VaapiSwDecodeConfig extends BaseHWConfig {
   getBaseInputOptions() {
-    if (this.devices.length === 0) {
-      throw new Error('No VAAPI device found');
-    }
-
-    let hwDevice = this.getPreferredHardwareDevice();
-    if (!hwDevice) {
-      hwDevice = `/dev/dri/${this.devices[0]}`;
-    }
-
-    return [`-init_hw_device vaapi=accel:${hwDevice}`, '-filter_hw_device accel'];
+    return [`-init_hw_device vaapi=accel:${this.device}`, '-filter_hw_device accel'];
   }
 
   getFilterOptions(videoStream: VideoStreamInfo) {
     const options = this.getToneMapping(videoStream);
-    options.push('format=nv12', 'hwupload');
+    options.push('hwupload=extra_hw_frames=64');
     if (this.shouldScale(videoStream)) {
-      options.push(`scale_vaapi=${this.getScaling(videoStream)}`);
+      options.push(`scale_vaapi=${this.getScaling(videoStream)}:mode=hq:out_range=pc:format=nv12`);
     }
 
     return options;
@@ -857,22 +834,70 @@ export class VAAPIConfig extends BaseHWConfig {
   }
 }
 
-export class RkmppSwDecodeConfig extends BaseHWConfig {
-  constructor(
-    protected config: SystemConfigFFmpegDto,
-    devices: string[] = [],
-  ) {
-    super(config, devices);
+export class VaapiHwDecodeConfig extends VaapiSwDecodeConfig {
+  getBaseInputOptions() {
+    return [
+      '-hwaccel vaapi',
+      '-hwaccel_output_format vaapi',
+      '-noautorotate',
+      `-hwaccel_device ${this.device}`,
+      ...this.getInputThreadOptions(),
+    ];
   }
 
+  getFilterOptions(videoStream: VideoStreamInfo) {
+    const options = [];
+    const tonemapOptions = this.getToneMapping(videoStream);
+    if (this.shouldScale(videoStream) || tonemapOptions.length === 0) {
+      let scaling = `scale_vaapi=${this.getScaling(videoStream)}:mode=hq:out_range=pc`;
+      if (tonemapOptions.length === 0) {
+        scaling += ':format=nv12';
+      }
+      options.push(scaling);
+    }
+    options.push(...tonemapOptions);
+    if (options.length === 0 && !videoStream.pixelFormat.endsWith('420p')) {
+      options.push('format=nv12');
+    }
+    return options;
+  }
+
+  getToneMapping(videoStream: VideoStreamInfo): string[] {
+    if (!this.shouldToneMap(videoStream)) {
+      return [];
+    }
+
+    const { matrix, primaries, transfer } = this.getColors();
+    const tonemapOptions = [
+      'desat=0',
+      'format=nv12',
+      `matrix=${matrix}`,
+      `primaries=${primaries}`,
+      `transfer=${transfer}`,
+      'range=pc',
+      `tonemap=${this.config.tonemap}`,
+      'tonemap_mode=lum',
+      'peak=100',
+    ];
+
+    return [
+      'hwmap=derive_device=opencl',
+      `tonemap_opencl=${tonemapOptions.join(':')}`,
+      'hwmap=derive_device=vaapi:reverse=1,format=vaapi',
+    ];
+  }
+
+  getInputThreadOptions() {
+    return [`-threads 1`];
+  }
+}
+
+export class RkmppSwDecodeConfig extends BaseHWConfig {
   eligibleForTwoPass(): boolean {
     return false;
   }
 
   getBaseInputOptions(): string[] {
-    if (this.devices.length === 0) {
-      throw new Error('No RKMPP device found');
-    }
     return [];
   }
 
@@ -913,25 +938,32 @@ export class RkmppSwDecodeConfig extends BaseHWConfig {
 
 export class RkmppHwDecodeConfig extends RkmppSwDecodeConfig {
   getBaseInputOptions() {
-    if (this.devices.length === 0) {
-      throw new Error('No RKMPP device found');
-    }
-
     return ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga', '-noautorotate'];
   }
 
   getFilterOptions(videoStream: VideoStreamInfo) {
     if (this.shouldToneMap(videoStream)) {
-      const colors = this.getColors();
+      const { primaries, transfer, matrix } = this.getColors();
+      if (this.interfaces.mali) {
+        return [
+          // use RKMPP for scaling, OpenCL for tone mapping
+          `scale_rkrga=${this.getScaling(videoStream)}:format=p010:afbc=1:async_depth=4`,
+          'hwmap=derive_device=opencl:mode=read',
+          `tonemap_opencl=format=nv12:r=pc:p=${primaries}:t=${transfer}:m=${matrix}:tonemap=${this.config.tonemap}:desat=0:tonemap_mode=lum:peak=100`,
+          'hwmap=derive_device=rkmpp:mode=write:reverse=1',
+          'format=drm_prime',
+        ];
+      }
       return [
-        `scale_rkrga=${this.getScaling(videoStream)}:format=p010:afbc=1`,
-        'hwmap=derive_device=opencl:mode=read',
-        `tonemap_opencl=format=nv12:r=pc:p=${colors.primaries}:t=${colors.transfer}:m=${colors.matrix}:tonemap=${this.config.tonemap}:desat=0`,
-        'hwmap=derive_device=rkmpp:mode=write:reverse=1',
-        'format=drm_prime',
+        // use RKMPP for scaling, CPU for tone mapping (only works on RK3588, which supports 10-bit output)
+        `scale_rkrga=${this.getScaling(videoStream)}:format=p010:afbc=1:async_depth=4`,
+        'hwdownload',
+        'format=p010',
+        `tonemapx=tonemap=${this.config.tonemap}:desat=0:p=${primaries}:t=${transfer}:m=${matrix}:r=pc:peak=100:format=yuv420p`,
+        'hwupload',
       ];
     } else if (this.shouldScale(videoStream)) {
-      return [`scale_rkrga=${this.getScaling(videoStream)}:format=nv12:afbc=1`];
+      return [`scale_rkrga=${this.getScaling(videoStream)}:format=nv12:afbc=1:async_depth=4`];
     }
     return [];
   }
diff --git a/server/src/utils/mime-types.spec.ts b/server/src/utils/mime-types.spec.ts
index 50fe760a04..05cd8566c8 100644
--- a/server/src/utils/mime-types.spec.ts
+++ b/server/src/utils/mime-types.spec.ts
@@ -92,6 +92,7 @@ describe('mimeTypes', () => {
     { mimetype: 'video/x-matroska', extension: '.mkv' },
     { mimetype: 'video/x-ms-wmv', extension: '.wmv' },
     { mimetype: 'video/x-msvideo', extension: '.avi' },
+    { mimetype: 'video/mpeg', extension: '.vob' },
   ]) {
     it(`should map ${extension} to ${mimetype}`, () => {
       expect({ ...mimeTypes.image, ...mimeTypes.video }[extension]).toContain(mimetype);
diff --git a/server/src/utils/mime-types.ts b/server/src/utils/mime-types.ts
index cbf6e5b489..165eb44a4f 100644
--- a/server/src/utils/mime-types.ts
+++ b/server/src/utils/mime-types.ts
@@ -74,6 +74,7 @@ const video: Record<string, string[]> = {
   '.mpeg': ['video/mpeg'],
   '.mpg': ['video/mpeg'],
   '.mts': ['video/mp2t'],
+  '.vob': ['video/mpeg'],
   '.webm': ['video/webm'],
   '.wmv': ['video/x-ms-wmv'],
 };
diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts
index 47f3f552c4..6a64923a3b 100644
--- a/server/src/utils/misc.ts
+++ b/server/src/utils/misc.ts
@@ -11,10 +11,38 @@ import _ from 'lodash';
 import { writeFileSync } from 'node:fs';
 import path from 'node:path';
 import { SystemConfig } from 'src/config';
-import { CLIP_MODEL_INFO, isDev, serverVersion } from 'src/constants';
-import { ImmichCookie, ImmichHeader } from 'src/dtos/auth.dto';
+import { CLIP_MODEL_INFO, serverVersion } from 'src/constants';
+import { ImmichCookie, ImmichHeader, MetadataKey } from 'src/enum';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { Metadata } from 'src/middleware/auth.guard';
+
+export class ImmichStartupError extends Error {}
+export const isStartUpError = (error: unknown): error is ImmichStartupError => error instanceof ImmichStartupError;
+
+export const getKeyByValue = (object: Record<string, unknown>, value: unknown) =>
+  Object.keys(object).find((key) => object[key] === value);
+
+export const getMethodNames = (instance: any) => {
+  const ctx = Object.getPrototypeOf(instance);
+  const methods: string[] = [];
+  for (const property of Object.getOwnPropertyNames(ctx)) {
+    const descriptor = Object.getOwnPropertyDescriptor(ctx, property);
+    if (!descriptor || descriptor.get || descriptor.set) {
+      continue;
+    }
+
+    const handler = instance[property];
+    if (typeof handler !== 'function') {
+      continue;
+    }
+
+    methods.push(property);
+  }
+
+  return methods;
+};
+
+export const getExternalDomain = (server: SystemConfig['server'], port: number) =>
+  server.externalDomain || `http://localhost:${port}`;
 
 /**
  * @returns a list of strings representing the keys of the object in dot notation
@@ -193,7 +221,7 @@ const patchOpenAPI = (document: OpenAPIObject) => {
   return document;
 };
 
-export const useSwagger = (app: INestApplication, force = false) => {
+export const useSwagger = (app: INestApplication, { write }: { write: boolean }) => {
   const config = new DocumentBuilder()
     .setTitle('Immich')
     .setDescription('Immich API')
@@ -210,7 +238,7 @@ export const useSwagger = (app: INestApplication, force = false) => {
         in: 'header',
         name: ImmichHeader.API_KEY,
       },
-      Metadata.API_KEY_SECURITY,
+      MetadataKey.API_KEY_SECURITY,
     )
     .addServer('/api')
     .build();
@@ -230,7 +258,7 @@ export const useSwagger = (app: INestApplication, force = false) => {
 
   SwaggerModule.setup('doc', app, specification, customOptions);
 
-  if (isDev() || force) {
+  if (write) {
     // Generate API Documentation only in development mode
     const outputPath = path.resolve(process.cwd(), '../open-api/immich-openapi-specs.json');
     writeFileSync(outputPath, JSON.stringify(patchOpenAPI(specification), null, 2), { encoding: 'utf8' });
diff --git a/server/src/utils/pagination.ts b/server/src/utils/pagination.ts
index dec1a9de0c..4009f219c1 100644
--- a/server/src/utils/pagination.ts
+++ b/server/src/utils/pagination.ts
@@ -1,4 +1,5 @@
 import _ from 'lodash';
+import { PaginationMode } from 'src/enum';
 import { FindManyOptions, ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm';
 
 export interface PaginationOptions {
@@ -6,11 +7,6 @@ export interface PaginationOptions {
   skip?: number;
 }
 
-export enum PaginationMode {
-  LIMIT_OFFSET = 'limit-offset',
-  SKIP_TAKE = 'skip-take',
-}
-
 export interface PaginatedBuilderOptions {
   take: number;
   skip?: number;
diff --git a/server/src/utils/replace-template-tags.ts b/server/src/utils/replace-template-tags.ts
new file mode 100644
index 0000000000..70333d7dff
--- /dev/null
+++ b/server/src/utils/replace-template-tags.ts
@@ -0,0 +1,5 @@
+export const replaceTemplateTags = (template: string, variables: Record<string, string | undefined>) => {
+  return template.replaceAll(/{(.*?)}/g, (_, key) => {
+    return variables[key] || `{${key}}`;
+  });
+};
diff --git a/server/src/utils/response.ts b/server/src/utils/response.ts
index f318ca3300..679d947afb 100644
--- a/server/src/utils/response.ts
+++ b/server/src/utils/response.ts
@@ -1,6 +1,7 @@
 import { CookieOptions, Response } from 'express';
 import { Duration } from 'luxon';
-import { CookieResponse, ImmichCookie } from 'src/dtos/auth.dto';
+import { CookieResponse } from 'src/dtos/auth.dto';
+import { ImmichCookie } from 'src/enum';
 
 export const respondWithCookie = <T>(res: Response, body: T, { isSecure, values }: CookieResponse) => {
   const defaults: CookieOptions = {
diff --git a/server/src/utils/tag.ts b/server/src/utils/tag.ts
index 6d6c70f1d7..027afcf040 100644
--- a/server/src/utils/tag.ts
+++ b/server/src/utils/tag.ts
@@ -8,7 +8,7 @@ export const upsertTags = async (repository: ITagRepository, { userId, tags }: U
   const results: TagEntity[] = [];
 
   for (const tag of tags) {
-    const parts = tag.split('/');
+    const parts = tag.split('/').filter(Boolean);
     let parent: TagEntity | undefined;
 
     for (const part of parts) {
diff --git a/server/src/utils/workers.spec.ts b/server/src/utils/workers.spec.ts
deleted file mode 100644
index 1e4ff5e2d3..0000000000
--- a/server/src/utils/workers.spec.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { getWorkers } from 'src/utils/workers';
-
-describe('getWorkers', () => {
-  beforeEach(() => {
-    process.env.IMMICH_WORKERS_INCLUDE = '';
-    process.env.IMMICH_WORKERS_EXCLUDE = '';
-  });
-
-  it('should return default workers', () => {
-    expect(getWorkers()).toEqual(['api', 'microservices']);
-  });
-
-  it('should return included workers', () => {
-    process.env.IMMICH_WORKERS_INCLUDE = 'api';
-    expect(getWorkers()).toEqual(['api']);
-  });
-
-  it('should excluded workers from defaults', () => {
-    process.env.IMMICH_WORKERS_EXCLUDE = 'api';
-    expect(getWorkers()).toEqual(['microservices']);
-  });
-
-  it('should exclude workers from include list', () => {
-    process.env.IMMICH_WORKERS_INCLUDE = 'api,microservices,randomservice';
-    process.env.IMMICH_WORKERS_EXCLUDE = 'randomservice,microservices';
-    expect(getWorkers()).toEqual(['api']);
-  });
-
-  it('should remove whitespace from included workers before parsing', () => {
-    process.env.IMMICH_WORKERS_INCLUDE = 'api, microservices';
-    expect(getWorkers()).toEqual(['api', 'microservices']);
-  });
-
-  it('should remove whitespace from excluded workers before parsing', () => {
-    process.env.IMMICH_WORKERS_EXCLUDE = 'api, microservices';
-    expect(getWorkers()).toEqual([]);
-  });
-
-  it('should remove whitespace from included and excluded workers before parsing', () => {
-    process.env.IMMICH_WORKERS_INCLUDE = 'api, microservices, randomservice,randomservice2';
-    process.env.IMMICH_WORKERS_EXCLUDE = 'randomservice,microservices, randomservice2';
-    expect(getWorkers()).toEqual(['api']);
-  });
-
-  it('should throw error for invalid workers', () => {
-    process.env.IMMICH_WORKERS_INCLUDE = 'api,microservices,randomservice';
-    expect(getWorkers).toThrowError('Invalid worker(s) found: api,microservices,randomservice');
-  });
-});
diff --git a/server/src/utils/workers.ts b/server/src/utils/workers.ts
deleted file mode 100644
index 14daa2620f..0000000000
--- a/server/src/utils/workers.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-const WORKER_TYPES = new Set(['api', 'microservices']);
-
-export const getWorkers = () => {
-  let workers = ['api', 'microservices'];
-  const includedWorkers = process.env.IMMICH_WORKERS_INCLUDE?.replaceAll(/\s/g, '');
-  const excludedWorkers = process.env.IMMICH_WORKERS_EXCLUDE?.replaceAll(/\s/g, '');
-
-  if (includedWorkers) {
-    workers = includedWorkers.split(',');
-  }
-
-  if (excludedWorkers) {
-    workers = workers.filter((worker) => !excludedWorkers.split(',').includes(worker));
-  }
-
-  if (workers.some((worker) => !WORKER_TYPES.has(worker))) {
-    throw new Error(`Invalid worker(s) found: ${workers}`);
-  }
-
-  return workers;
-};
diff --git a/server/src/validation.ts b/server/src/validation.ts
index 81b309d663..177e439919 100644
--- a/server/src/validation.ts
+++ b/server/src/validation.ts
@@ -16,15 +16,19 @@ import {
   IsOptional,
   IsString,
   IsUUID,
+  Validate,
   ValidateBy,
   ValidateIf,
   ValidationOptions,
+  ValidatorConstraint,
+  ValidatorConstraintInterface,
   buildMessage,
   isDateString,
 } from 'class-validator';
 import { CronJob } from 'cron';
 import { DateTime } from 'luxon';
 import sanitize from 'sanitize-filename';
+import { isIP, isIPRange } from 'validator';
 
 @Injectable()
 export class ParseMeUUIDPipe extends ParseUUIDPipe {
@@ -155,16 +159,20 @@ export const ValidateBoolean = (options?: BooleanOptions) => {
   return applyDecorators(...decorators);
 };
 
-export function validateCronExpression(expression: string) {
-  try {
-    new CronJob(expression, () => {});
-  } catch {
-    return false;
+@ValidatorConstraint({ name: 'cronValidator' })
+class CronValidator implements ValidatorConstraintInterface {
+  validate(expression: string): boolean {
+    try {
+      new CronJob(expression, () => {});
+      return true;
+    } catch {
+      return false;
+    }
   }
-
-  return true;
 }
 
+export const IsCronExpression = () => Validate(CronValidator, { message: 'Invalid cron expression' });
+
 type IValue = { value: unknown };
 
 export const toEmail = ({ value }: IValue) => (typeof value === 'string' ? value.toLowerCase() : value);
@@ -228,3 +236,32 @@ export function MaxDateString(
     validationOptions,
   );
 }
+
+type IsIPRangeOptions = { requireCIDR?: boolean };
+export function IsIPRange(options: IsIPRangeOptions, validationOptions?: ValidationOptions): PropertyDecorator {
+  const { requireCIDR } = { requireCIDR: true, ...options };
+
+  return ValidateBy(
+    {
+      name: 'isIPRange',
+      validator: {
+        validate: (value): boolean => {
+          if (isIPRange(value)) {
+            return true;
+          }
+
+          if (!requireCIDR && isIP(value)) {
+            return true;
+          }
+
+          return false;
+        },
+        defaultMessage: buildMessage(
+          (eachPrefix) => eachPrefix + '$property must be an ip address, or ip address range',
+          validationOptions,
+        ),
+      },
+    },
+    validationOptions,
+  );
+}
diff --git a/server/src/workers/api.ts b/server/src/workers/api.ts
index 629c50c653..efc705deaf 100644
--- a/server/src/workers/api.ts
+++ b/server/src/workers/api.ts
@@ -5,47 +5,42 @@ import cookieParser from 'cookie-parser';
 import { existsSync } from 'node:fs';
 import sirv from 'sirv';
 import { ApiModule } from 'src/app.module';
-import { envName, excludePaths, isDev, resourcePaths, serverVersion } from 'src/constants';
+import { excludePaths, serverVersion } from 'src/constants';
+import { ImmichEnvironment } from 'src/enum';
+import { IConfigRepository } from 'src/interfaces/config.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
+import { ConfigRepository } from 'src/repositories/config.repository';
+import { bootstrapTelemetry } from 'src/repositories/telemetry.repository';
 import { ApiService } from 'src/services/api.service';
-import { isStartUpError } from 'src/utils/events';
-import { otelStart } from 'src/utils/instrumentation';
-import { useSwagger } from 'src/utils/misc';
-
-const host = process.env.HOST;
-
-function parseTrustedProxy(input?: string) {
-  if (!input) {
-    return [];
-  }
-  // Split on ',' char to allow multiple IPs
-  return input.split(',');
-}
+import { isStartUpError, useSwagger } from 'src/utils/misc';
 
 async function bootstrap() {
   process.title = 'immich-api';
-  const otelPort = Number.parseInt(process.env.IMMICH_API_METRICS_PORT ?? '8081');
-  const trustedProxies = parseTrustedProxy(process.env.IMMICH_TRUSTED_PROXIES ?? '');
 
-  otelStart(otelPort);
+  const { telemetry, network } = new ConfigRepository().getEnv();
+  if (telemetry.metrics.size > 0) {
+    bootstrapTelemetry(telemetry.apiPort);
+  }
 
-  const port = Number(process.env.IMMICH_PORT) || 3001;
   const app = await NestFactory.create<NestExpressApplication>(ApiModule, { bufferLogs: true });
   const logger = await app.resolve<ILoggerRepository>(ILoggerRepository);
+  const configRepository = app.get<IConfigRepository>(IConfigRepository);
+
+  const { environment, host, port, resourcePaths } = configRepository.getEnv();
+  const isDev = environment === ImmichEnvironment.DEVELOPMENT;
 
-  logger.setAppName('Api');
   logger.setContext('Bootstrap');
   app.useLogger(logger);
-  app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal', ...trustedProxies]);
+  app.set('trust proxy', ['loopback', ...network.trustedProxies]);
   app.set('etag', 'strong');
   app.use(cookieParser());
   app.use(json({ limit: '10mb' }));
-  if (isDev()) {
+  if (isDev) {
     app.enableCors();
   }
   app.useWebSocketAdapter(new WebSocketAdapter(app));
-  useSwagger(app);
+  useSwagger(app, { write: isDev });
 
   app.setGlobalPrefix('api', { exclude: excludePaths });
   if (existsSync(resourcePaths.web.root)) {
@@ -70,7 +65,7 @@ async function bootstrap() {
   const server = await (host ? app.listen(port, host) : app.listen(port));
   server.requestTimeout = 30 * 60 * 1000;
 
-  logger.log(`Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `);
+  logger.log(`Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${environment}] `);
 }
 
 bootstrap().catch((error) => {
diff --git a/server/src/workers/microservices.ts b/server/src/workers/microservices.ts
index 789b6f5287..0fa056d5d4 100644
--- a/server/src/workers/microservices.ts
+++ b/server/src/workers/microservices.ts
@@ -1,27 +1,31 @@
 import { NestFactory } from '@nestjs/core';
 import { isMainThread } from 'node:worker_threads';
 import { MicroservicesModule } from 'src/app.module';
-import { envName, serverVersion } from 'src/constants';
+import { serverVersion } from 'src/constants';
+import { IConfigRepository } from 'src/interfaces/config.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
-import { isStartUpError } from 'src/utils/events';
-import { otelStart } from 'src/utils/instrumentation';
+import { ConfigRepository } from 'src/repositories/config.repository';
+import { bootstrapTelemetry } from 'src/repositories/telemetry.repository';
+import { isStartUpError } from 'src/utils/misc';
 
 export async function bootstrap() {
-  const otelPort = Number.parseInt(process.env.IMMICH_MICROSERVICES_METRICS_PORT ?? '8082');
-
-  otelStart(otelPort);
+  const { telemetry } = new ConfigRepository().getEnv();
+  if (telemetry.metrics.size > 0) {
+    bootstrapTelemetry(telemetry.microservicesPort);
+  }
 
   const app = await NestFactory.create(MicroservicesModule, { bufferLogs: true });
   const logger = await app.resolve(ILoggerRepository);
-  logger.setAppName('Microservices');
   logger.setContext('Bootstrap');
   app.useLogger(logger);
   app.useWebSocketAdapter(new WebSocketAdapter(app));
 
   await app.listen(0);
 
-  logger.log(`Immich Microservices is running [v${serverVersion}] [${envName}] `);
+  const configRepository = app.get<IConfigRepository>(IConfigRepository);
+  const { environment } = configRepository.getEnv();
+  logger.log(`Immich Microservices is running [v${serverVersion}] [${environment}] `);
 }
 
 if (!isMainThread) {
diff --git a/server/start.sh b/server/start.sh
index 5aa7ee01b2..1a08d01a75 100755
--- a/server/start.sh
+++ b/server/start.sh
@@ -1,7 +1,10 @@
 #!/usr/bin/env bash
 
+echo "Initializing Immich $IMMICH_SOURCE_REF"
+
 lib_path="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.2"
 export LD_PRELOAD="$lib_path"
+export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/lib/jellyfin-ffmpeg/lib"
 
 read_file_and_export() {
 	if [ -n "${!1}" ]; then
diff --git a/server/test/fixtures/album.stub.ts b/server/test/fixtures/album.stub.ts
index c2c59a8007..3d2899d3c6 100644
--- a/server/test/fixtures/album.stub.ts
+++ b/server/test/fixtures/album.stub.ts
@@ -155,55 +155,4 @@ export const albumStub = {
     isActivityEnabled: true,
     order: AssetOrder.DESC,
   }),
-  emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({
-    id: 'album-5',
-    albumName: 'Empty album with invalid thumbnail',
-    description: '',
-    ownerId: authStub.admin.user.id,
-    owner: userStub.admin,
-    assets: [],
-    albumThumbnailAsset: null,
-    albumThumbnailAssetId: null,
-    createdAt: new Date(),
-    updatedAt: new Date(),
-    deletedAt: null,
-    sharedLinks: [],
-    albumUsers: [],
-    isActivityEnabled: true,
-    order: AssetOrder.DESC,
-  }),
-  oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({
-    id: 'album-6',
-    albumName: 'Album with one asset and invalid thumbnail',
-    description: '',
-    ownerId: authStub.admin.user.id,
-    owner: userStub.admin,
-    assets: [assetStub.image],
-    albumThumbnailAsset: assetStub.livePhotoMotionAsset,
-    albumThumbnailAssetId: assetStub.livePhotoMotionAsset.id,
-    createdAt: new Date(),
-    updatedAt: new Date(),
-    deletedAt: null,
-    sharedLinks: [],
-    albumUsers: [],
-    isActivityEnabled: true,
-    order: AssetOrder.DESC,
-  }),
-  oneAssetValidThumbnail: Object.freeze<AlbumEntity>({
-    id: 'album-6',
-    albumName: 'Album with one asset and invalid thumbnail',
-    description: '',
-    ownerId: authStub.admin.user.id,
-    owner: userStub.admin,
-    assets: [assetStub.image],
-    albumThumbnailAsset: assetStub.image,
-    albumThumbnailAssetId: assetStub.image.id,
-    createdAt: new Date(),
-    updatedAt: new Date(),
-    deletedAt: null,
-    sharedLinks: [],
-    albumUsers: [],
-    isActivityEnabled: true,
-    order: AssetOrder.DESC,
-  }),
 };
diff --git a/server/test/fixtures/api-key.stub.ts b/server/test/fixtures/api-key.stub.ts
index 954c8f35a0..f8b1832c84 100644
--- a/server/test/fixtures/api-key.stub.ts
+++ b/server/test/fixtures/api-key.stub.ts
@@ -11,7 +11,3 @@ export const keyStub = {
     user: userStub.admin,
   } as APIKeyEntity),
 };
-
-export const apiKeyCreateStub = {
-  name: 'API Key',
-};
diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts
index 5ee42224ba..45390cf92e 100644
--- a/server/test/fixtures/asset.stub.ts
+++ b/server/test/fixtures/asset.stub.ts
@@ -2,7 +2,7 @@ import { AssetFileEntity } from 'src/entities/asset-files.entity';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { ExifEntity } from 'src/entities/exif.entity';
 import { StackEntity } from 'src/entities/stack.entity';
-import { AssetFileType, AssetType } from 'src/enum';
+import { AssetFileType, AssetStatus, AssetType } from 'src/enum';
 import { authStub } from 'test/fixtures/auth.stub';
 import { fileStub } from 'test/fixtures/file.stub';
 import { libraryStub } from 'test/fixtures/library.stub';
@@ -42,6 +42,7 @@ export const stackStub = (stackId: string, assets: AssetEntity[]): StackEntity =
 export const assetStub = {
   noResizePath: Object.freeze<AssetEntity>({
     id: 'asset-id',
+    status: AssetStatus.ACTIVE,
     originalFileName: 'IMG_123.jpg',
     deviceAssetId: 'device-asset-id',
     fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -69,13 +70,14 @@ export const assetStub = {
     faces: [],
     sidecarPath: null,
     deletedAt: null,
-    isOffline: false,
     isExternal: false,
     duplicateId: null,
+    isOffline: false,
   }),
 
   noWebpPath: Object.freeze<AssetEntity>({
     id: 'asset-id',
+    status: AssetStatus.ACTIVE,
     deviceAssetId: 'device-asset-id',
     fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
     fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -83,7 +85,6 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: 'upload/library/IMG_456.jpg',
-
     files: [previewFile],
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.IMAGE,
@@ -103,17 +104,18 @@ export const assetStub = {
     originalFileName: 'IMG_456.jpg',
     faces: [],
     sidecarPath: null,
-    isOffline: false,
     isExternal: false,
     exifInfo: {
       fileSizeInByte: 123_000,
     } as ExifEntity,
     deletedAt: null,
     duplicateId: null,
+    isOffline: false,
   }),
 
   noThumbhash: Object.freeze<AssetEntity>({
     id: 'asset-id',
+    status: AssetStatus.ACTIVE,
     deviceAssetId: 'device-asset-id',
     fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
     fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -131,7 +133,6 @@ export const assetStub = {
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
     isFavorite: true,
     isArchived: false,
-    isOffline: false,
     duration: null,
     isVisible: true,
     isExternal: false,
@@ -144,10 +145,12 @@ export const assetStub = {
     sidecarPath: null,
     deletedAt: null,
     duplicateId: null,
+    isOffline: false,
   }),
 
   primaryImage: Object.freeze<AssetEntity>({
     id: 'primary-asset-id',
+    status: AssetStatus.ACTIVE,
     deviceAssetId: 'device-asset-id',
     fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
     fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -170,7 +173,6 @@ export const assetStub = {
     isExternal: false,
     livePhotoVideo: null,
     livePhotoVideoId: null,
-    isOffline: false,
     tags: [],
     sharedLinks: [],
     originalFileName: 'asset-id.jpg',
@@ -188,10 +190,12 @@ export const assetStub = {
       { id: 'stack-child-asset-2' } as AssetEntity,
     ]),
     duplicateId: null,
+    isOffline: false,
   }),
 
   image: Object.freeze<AssetEntity>({
     id: 'asset-id',
+    status: AssetStatus.ACTIVE,
     deviceAssetId: 'device-asset-id',
     fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
     fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -214,7 +218,6 @@ export const assetStub = {
     isExternal: false,
     livePhotoVideo: null,
     livePhotoVideoId: null,
-    isOffline: false,
     tags: [],
     sharedLinks: [],
     originalFileName: 'asset-id.jpg',
@@ -227,6 +230,7 @@ export const assetStub = {
       exifImageWidth: 2160,
     } as ExifEntity,
     duplicateId: null,
+    isOffline: false,
   }),
 
   trashed: Object.freeze<AssetEntity>({
@@ -254,7 +258,6 @@ export const assetStub = {
     isExternal: false,
     livePhotoVideo: null,
     livePhotoVideoId: null,
-    isOffline: false,
     tags: [],
     sharedLinks: [],
     originalFileName: 'asset-id.jpg',
@@ -266,10 +269,52 @@ export const assetStub = {
       exifImageWidth: 2160,
     } as ExifEntity,
     duplicateId: null,
+    isOffline: false,
+    status: AssetStatus.TRASHED,
   }),
 
+  trashedOffline: Object.freeze<AssetEntity>({
+    id: 'asset-id',
+    status: AssetStatus.ACTIVE,
+    deviceAssetId: 'device-asset-id',
+    fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
+    fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
+    owner: userStub.user1,
+    ownerId: 'user-id',
+    deviceId: 'device-id',
+    originalPath: '/original/path.jpg',
+    checksum: Buffer.from('file hash', 'utf8'),
+    type: AssetType.IMAGE,
+    files,
+    thumbhash: Buffer.from('blablabla', 'base64'),
+    encodedVideoPath: null,
+    createdAt: new Date('2023-02-23T05:06:29.716Z'),
+    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
+    deletedAt: new Date('2023-02-24T05:06:29.716Z'),
+    localDateTime: new Date('2023-02-23T05:06:29.716Z'),
+    isFavorite: false,
+    isArchived: false,
+    duration: null,
+    isVisible: true,
+    isExternal: false,
+    livePhotoVideo: null,
+    livePhotoVideoId: null,
+    tags: [],
+    sharedLinks: [],
+    originalFileName: 'asset-id.jpg',
+    faces: [],
+    sidecarPath: null,
+    exifInfo: {
+      fileSizeInByte: 5000,
+      exifImageHeight: 3840,
+      exifImageWidth: 2160,
+    } as ExifEntity,
+    duplicateId: null,
+    isOffline: true,
+  }),
   archived: Object.freeze<AssetEntity>({
     id: 'asset-id',
+    status: AssetStatus.ACTIVE,
     deviceAssetId: 'device-asset-id',
     fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
     fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -292,7 +337,6 @@ export const assetStub = {
     isExternal: false,
     livePhotoVideo: null,
     livePhotoVideoId: null,
-    isOffline: false,
     tags: [],
     sharedLinks: [],
     originalFileName: 'asset-id.jpg',
@@ -305,10 +349,12 @@ export const assetStub = {
       exifImageWidth: 2160,
     } as ExifEntity,
     duplicateId: null,
+    isOffline: false,
   }),
 
   external: Object.freeze<AssetEntity>({
     id: 'asset-id',
+    status: AssetStatus.ACTIVE,
     deviceAssetId: 'device-asset-id',
     fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
     fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -331,99 +377,24 @@ export const assetStub = {
     isVisible: true,
     livePhotoVideo: null,
     livePhotoVideoId: null,
+    libraryId: 'library-id',
+    library: libraryStub.externalLibrary1,
+    tags: [],
+    sharedLinks: [],
+    originalFileName: 'asset-id.jpg',
+    faces: [],
+    deletedAt: null,
+    sidecarPath: null,
+    exifInfo: {
+      fileSizeInByte: 5000,
+    } as ExifEntity,
+    duplicateId: null,
     isOffline: false,
-    libraryId: 'library-id',
-    library: libraryStub.externalLibrary1,
-    tags: [],
-    sharedLinks: [],
-    originalFileName: 'asset-id.jpg',
-    faces: [],
-    deletedAt: null,
-    sidecarPath: null,
-    exifInfo: {
-      fileSizeInByte: 5000,
-    } as ExifEntity,
-    duplicateId: null,
-  }),
-
-  offline: Object.freeze<AssetEntity>({
-    id: 'asset-id',
-    deviceAssetId: 'device-asset-id',
-    fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
-    fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
-    owner: userStub.user1,
-    ownerId: 'user-id',
-    deviceId: 'device-id',
-    originalPath: '/original/path.jpg',
-    checksum: Buffer.from('file hash', 'utf8'),
-    type: AssetType.IMAGE,
-    files,
-    thumbhash: Buffer.from('blablabla', 'base64'),
-    encodedVideoPath: null,
-    createdAt: new Date('2023-02-23T05:06:29.716Z'),
-    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
-    localDateTime: new Date('2023-02-23T05:06:29.716Z'),
-    isFavorite: true,
-    isArchived: false,
-    isExternal: false,
-    duration: null,
-    isVisible: true,
-    livePhotoVideo: null,
-    livePhotoVideoId: null,
-    isOffline: true,
-    tags: [],
-    sharedLinks: [],
-    originalFileName: 'asset-id.jpg',
-    faces: [],
-    sidecarPath: null,
-    exifInfo: {
-      fileSizeInByte: 5000,
-    } as ExifEntity,
-    deletedAt: null,
-    duplicateId: null,
-  }),
-
-  externalOffline: Object.freeze<AssetEntity>({
-    id: 'asset-id',
-    deviceAssetId: 'device-asset-id',
-    fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
-    fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
-    owner: userStub.user1,
-    ownerId: 'user-id',
-    deviceId: 'device-id',
-    originalPath: '/data/user1/photo.jpg',
-    checksum: Buffer.from('path hash', 'utf8'),
-    type: AssetType.IMAGE,
-    files,
-    thumbhash: Buffer.from('blablabla', 'base64'),
-    encodedVideoPath: null,
-    createdAt: new Date('2023-02-23T05:06:29.716Z'),
-    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
-    localDateTime: new Date('2023-02-23T05:06:29.716Z'),
-    isFavorite: true,
-    isArchived: false,
-    isExternal: true,
-    duration: null,
-    isVisible: true,
-    livePhotoVideo: null,
-    livePhotoVideoId: null,
-    isOffline: true,
-    libraryId: 'library-id',
-    library: libraryStub.externalLibrary1,
-    tags: [],
-    sharedLinks: [],
-    originalFileName: 'asset-id.jpg',
-    faces: [],
-    sidecarPath: null,
-    exifInfo: {
-      fileSizeInByte: 5000,
-    } as ExifEntity,
-    deletedAt: null,
-    duplicateId: null,
   }),
 
   image1: Object.freeze<AssetEntity>({
     id: 'asset-id-1',
+    status: AssetStatus.ACTIVE,
     deviceAssetId: 'device-asset-id',
     fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
     fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -447,7 +418,6 @@ export const assetStub = {
     livePhotoVideo: null,
     livePhotoVideoId: null,
     isExternal: false,
-    isOffline: false,
     tags: [],
     sharedLinks: [],
     originalFileName: 'asset-id.ext',
@@ -457,10 +427,12 @@ export const assetStub = {
       fileSizeInByte: 5000,
     } as ExifEntity,
     duplicateId: null,
+    isOffline: false,
   }),
 
   imageFrom2015: Object.freeze<AssetEntity>({
     id: 'asset-id-1',
+    status: AssetStatus.ACTIVE,
     deviceAssetId: 'device-asset-id',
     fileModifiedAt: new Date('2015-02-23T05:06:29.716Z'),
     fileCreatedAt: new Date('2015-02-23T05:06:29.716Z'),
@@ -479,7 +451,6 @@ export const assetStub = {
     isFavorite: true,
     isArchived: false,
     isExternal: false,
-    isOffline: false,
     duration: null,
     isVisible: true,
     livePhotoVideo: null,
@@ -494,10 +465,12 @@ export const assetStub = {
     } as ExifEntity,
     deletedAt: null,
     duplicateId: null,
+    isOffline: false,
   }),
 
   video: Object.freeze<AssetEntity>({
     id: 'asset-id',
+    status: AssetStatus.ACTIVE,
     originalFileName: 'asset-id.ext',
     deviceAssetId: 'device-asset-id',
     fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -517,7 +490,6 @@ export const assetStub = {
     isFavorite: true,
     isArchived: false,
     isExternal: false,
-    isOffline: false,
     duration: null,
     isVisible: true,
     livePhotoVideo: null,
@@ -533,9 +505,11 @@ export const assetStub = {
     } as ExifEntity,
     deletedAt: null,
     duplicateId: null,
+    isOffline: false,
   }),
 
   livePhotoMotionAsset: Object.freeze({
+    status: AssetStatus.ACTIVE,
     id: fileStub.livePhotoMotion.uuid,
     originalPath: fileStub.livePhotoMotion.originalPath,
     ownerId: authStub.user1.user.id,
@@ -549,53 +523,9 @@ export const assetStub = {
     },
   } as AssetEntity),
 
-  liveMotionWithThumb: Object.freeze({
-    id: fileStub.livePhotoMotion.uuid,
-    originalPath: fileStub.livePhotoMotion.originalPath,
-    ownerId: authStub.user1.user.id,
-    type: AssetType.VIDEO,
-    isVisible: false,
-    fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
-    fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
-    files: [
-      {
-        assetId: 'asset-id',
-        type: AssetFileType.PREVIEW,
-        path: '/uploads/user-id/thumbs/path.ext',
-        createdAt: new Date('2023-02-23T05:06:29.716Z'),
-        updatedAt: new Date('2023-02-23T05:06:29.716Z'),
-      },
-      {
-        assetId: 'asset-id',
-        type: AssetFileType.THUMBNAIL,
-        path: '/uploads/user-id/webp/path.ext',
-        createdAt: new Date('2023-02-23T05:06:29.716Z'),
-        updatedAt: new Date('2023-02-23T05:06:29.716Z'),
-      },
-    ],
-    exifInfo: {
-      fileSizeInByte: 100_000,
-      timeZone: `America/New_York`,
-    },
-  } as AssetEntity),
-
   livePhotoStillAsset: Object.freeze({
     id: 'live-photo-still-asset',
-    originalPath: fileStub.livePhotoStill.originalPath,
-    ownerId: authStub.user1.user.id,
-    type: AssetType.IMAGE,
-    livePhotoVideoId: 'live-photo-motion-asset',
-    isVisible: true,
-    fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
-    fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
-    exifInfo: {
-      fileSizeInByte: 25_000,
-      timeZone: `America/New_York`,
-    },
-  } as AssetEntity),
-
-  livePhotoStillAssetWithTheSameLivePhotoMotionAsset: Object.freeze({
-    id: 'live-photo-still-asset-1',
+    status: AssetStatus.ACTIVE,
     originalPath: fileStub.livePhotoStill.originalPath,
     ownerId: authStub.user1.user.id,
     type: AssetType.IMAGE,
@@ -611,6 +541,7 @@ export const assetStub = {
 
   livePhotoWithOriginalFileName: Object.freeze({
     id: 'live-photo-still-asset',
+    status: AssetStatus.ACTIVE,
     originalPath: fileStub.livePhotoStill.originalPath,
     originalFileName: fileStub.livePhotoStill.originalName,
     ownerId: authStub.user1.user.id,
@@ -627,6 +558,7 @@ export const assetStub = {
 
   withLocation: Object.freeze<AssetEntity>({
     id: 'asset-with-favorite-id',
+    status: AssetStatus.ACTIVE,
     deviceAssetId: 'device-asset-id',
     fileModifiedAt: new Date('2023-02-22T05:06:29.716Z'),
     fileCreatedAt: new Date('2023-02-22T05:06:29.716Z'),
@@ -646,7 +578,6 @@ export const assetStub = {
     isFavorite: false,
     isArchived: false,
     isExternal: false,
-    isOffline: false,
     duration: null,
     isVisible: true,
     livePhotoVideo: null,
@@ -665,9 +596,12 @@ export const assetStub = {
     } as ExifEntity,
     deletedAt: null,
     duplicateId: null,
+    isOffline: false,
   }),
+
   sidecar: Object.freeze<AssetEntity>({
     id: 'asset-id',
+    status: AssetStatus.ACTIVE,
     deviceAssetId: 'device-asset-id',
     fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
     fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -686,7 +620,6 @@ export const assetStub = {
     isFavorite: true,
     isArchived: false,
     isExternal: false,
-    isOffline: false,
     duration: null,
     isVisible: true,
     livePhotoVideo: null,
@@ -698,9 +631,12 @@ export const assetStub = {
     sidecarPath: '/original/path.ext.xmp',
     deletedAt: null,
     duplicateId: null,
+    isOffline: false,
   }),
+
   sidecarWithoutExt: Object.freeze<AssetEntity>({
     id: 'asset-id',
+    status: AssetStatus.ACTIVE,
     deviceAssetId: 'device-asset-id',
     fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
     fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -719,7 +655,6 @@ export const assetStub = {
     isFavorite: true,
     isArchived: false,
     isExternal: false,
-    isOffline: false,
     duration: null,
     isVisible: true,
     livePhotoVideo: null,
@@ -731,44 +666,12 @@ export const assetStub = {
     sidecarPath: '/original/path.xmp',
     deletedAt: null,
     duplicateId: null,
-  }),
-
-  readOnly: Object.freeze<AssetEntity>({
-    id: 'read-only-asset',
-    deviceAssetId: 'device-asset-id',
-    fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
-    fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
-    owner: userStub.user1,
-    ownerId: 'user-id',
-    deviceId: 'device-id',
-    originalPath: '/original/path.ext',
-    thumbhash: null,
-    checksum: Buffer.from('file hash', 'utf8'),
-    type: AssetType.IMAGE,
-    files: [previewFile],
-    encodedVideoPath: null,
-    createdAt: new Date('2023-02-23T05:06:29.716Z'),
-    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
-    localDateTime: new Date('2023-02-23T05:06:29.716Z'),
-    isFavorite: true,
-    isArchived: false,
-    isExternal: false,
     isOffline: false,
-    duration: null,
-    isVisible: true,
-    livePhotoVideo: null,
-    livePhotoVideoId: null,
-    tags: [],
-    sharedLinks: [],
-    originalFileName: 'asset-id.ext',
-    faces: [],
-    sidecarPath: '/original/path.ext.xmp',
-    deletedAt: null,
-    duplicateId: null,
   }),
 
   hasEncodedVideo: Object.freeze<AssetEntity>({
     id: 'asset-id',
+    status: AssetStatus.ACTIVE,
     originalFileName: 'asset-id.ext',
     deviceAssetId: 'device-asset-id',
     fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -788,7 +691,6 @@ export const assetStub = {
     isFavorite: true,
     isArchived: false,
     isExternal: false,
-    isOffline: false,
     duration: null,
     isVisible: true,
     livePhotoVideo: null,
@@ -802,47 +704,12 @@ export const assetStub = {
     } as ExifEntity,
     deletedAt: null,
     duplicateId: null,
-  }),
-  missingFileExtension: Object.freeze<AssetEntity>({
-    id: 'asset-id',
-    deviceAssetId: 'device-asset-id',
-    fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
-    fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
-    owner: userStub.user1,
-    ownerId: 'user-id',
-    deviceId: 'device-id',
-    originalPath: '/data/user1/photo.jpg',
-    checksum: Buffer.from('file hash', 'utf8'),
-    type: AssetType.IMAGE,
-    files,
-    thumbhash: Buffer.from('blablabla', 'base64'),
-    encodedVideoPath: null,
-    createdAt: new Date('2023-02-23T05:06:29.716Z'),
-    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
-    localDateTime: new Date('2023-02-23T05:06:29.716Z'),
-    isFavorite: true,
-    isArchived: false,
-    isExternal: true,
-    duration: null,
-    isVisible: true,
-    livePhotoVideo: null,
-    livePhotoVideoId: null,
     isOffline: false,
-    libraryId: 'library-id',
-    library: libraryStub.externalLibrary1,
-    tags: [],
-    sharedLinks: [],
-    originalFileName: 'photo',
-    faces: [],
-    deletedAt: null,
-    sidecarPath: null,
-    exifInfo: {
-      fileSizeInByte: 5000,
-    } as ExifEntity,
-    duplicateId: null,
   }),
+
   hasFileExtension: Object.freeze<AssetEntity>({
     id: 'asset-id',
+    status: AssetStatus.ACTIVE,
     deviceAssetId: 'device-asset-id',
     fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
     fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -865,7 +732,6 @@ export const assetStub = {
     isVisible: true,
     livePhotoVideo: null,
     livePhotoVideoId: null,
-    isOffline: false,
     libraryId: 'library-id',
     library: libraryStub.externalLibrary1,
     tags: [],
@@ -878,9 +744,12 @@ export const assetStub = {
       fileSizeInByte: 5000,
     } as ExifEntity,
     duplicateId: null,
+    isOffline: false,
   }),
+
   imageDng: Object.freeze<AssetEntity>({
     id: 'asset-id',
+    status: AssetStatus.ACTIVE,
     deviceAssetId: 'device-asset-id',
     fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
     fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -903,7 +772,6 @@ export const assetStub = {
     isExternal: false,
     livePhotoVideo: null,
     livePhotoVideoId: null,
-    isOffline: false,
     tags: [],
     sharedLinks: [],
     originalFileName: 'asset-id.jpg',
@@ -916,9 +784,12 @@ export const assetStub = {
       bitsPerSample: 14,
     } as ExifEntity,
     duplicateId: null,
+    isOffline: false,
   }),
+
   hasEmbedding: Object.freeze<AssetEntity>({
     id: 'asset-id-embedding',
+    status: AssetStatus.ACTIVE,
     deviceAssetId: 'device-asset-id',
     fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
     fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -941,7 +812,6 @@ export const assetStub = {
     isExternal: false,
     livePhotoVideo: null,
     livePhotoVideoId: null,
-    isOffline: false,
     tags: [],
     sharedLinks: [],
     originalFileName: 'asset-id.jpg',
@@ -956,9 +826,12 @@ export const assetStub = {
       assetId: 'asset-id',
       embedding: Array.from({ length: 512 }, Math.random),
     },
+    isOffline: false,
   }),
+
   hasDupe: Object.freeze<AssetEntity>({
     id: 'asset-id-dupe',
+    status: AssetStatus.ACTIVE,
     deviceAssetId: 'device-asset-id',
     fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
     fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -981,7 +854,6 @@ export const assetStub = {
     isExternal: false,
     livePhotoVideo: null,
     livePhotoVideoId: null,
-    isOffline: false,
     tags: [],
     sharedLinks: [],
     originalFileName: 'asset-id.jpg',
@@ -996,5 +868,6 @@ export const assetStub = {
       assetId: 'asset-id',
       embedding: Array.from({ length: 512 }, Math.random),
     },
+    isOffline: false,
   }),
 };
diff --git a/server/test/fixtures/audit.stub.ts b/server/test/fixtures/audit.stub.ts
index 3e79a60819..24f78a17ce 100644
--- a/server/test/fixtures/audit.stub.ts
+++ b/server/test/fixtures/audit.stub.ts
@@ -3,22 +3,6 @@ import { DatabaseAction, EntityType } from 'src/enum';
 import { authStub } from 'test/fixtures/auth.stub';
 
 export const auditStub = {
-  create: Object.freeze<AuditEntity>({
-    id: 1,
-    entityId: 'asset-created',
-    action: DatabaseAction.CREATE,
-    entityType: EntityType.ASSET,
-    ownerId: authStub.admin.user.id,
-    createdAt: new Date(),
-  }),
-  update: Object.freeze<AuditEntity>({
-    id: 2,
-    entityId: 'asset-updated',
-    action: DatabaseAction.UPDATE,
-    entityType: EntityType.ASSET,
-    ownerId: authStub.admin.user.id,
-    createdAt: new Date(),
-  }),
   delete: Object.freeze<AuditEntity>({
     id: 3,
     entityId: 'asset-deleted',
diff --git a/server/test/fixtures/auth.stub.ts b/server/test/fixtures/auth.stub.ts
index bbb53d4db6..2989c0cce1 100644
--- a/server/test/fixtures/auth.stub.ts
+++ b/server/test/fixtures/auth.stub.ts
@@ -35,17 +35,6 @@ export const authStub = {
       id: 'token-id',
     } as SessionEntity,
   }),
-  external1: Object.freeze<AuthDto>({
-    user: {
-      id: 'user-id',
-      email: 'immich@test.com',
-      isAdmin: false,
-      metadata: [] as UserMetadataEntity[],
-    } as UserEntity,
-    session: {
-      id: 'token-id',
-    } as SessionEntity,
-  }),
   adminSharedLink: Object.freeze<AuthDto>({
     user: {
       id: 'admin_id',
@@ -76,20 +65,6 @@ export const authStub = {
       key: Buffer.from('shared-link-key'),
     } as SharedLinkEntity,
   }),
-  readonlySharedLink: Object.freeze<AuthDto>({
-    user: {
-      id: 'admin_id',
-      email: 'admin@test.com',
-      isAdmin: true,
-      metadata: [] as UserMetadataEntity[],
-    } as UserEntity,
-    sharedLink: {
-      id: '123',
-      allowUpload: false,
-      allowDownload: false,
-      showExif: true,
-    } as SharedLinkEntity,
-  }),
   passwordSharedLink: Object.freeze<AuthDto>({
     user: {
       id: 'admin_id',
@@ -106,35 +81,3 @@ export const authStub = {
     } as SharedLinkEntity,
   }),
 };
-
-export const loginResponseStub = {
-  admin: {
-    response: {
-      accessToken: expect.any(String),
-      name: 'Immich Admin',
-      isAdmin: true,
-      profileImagePath: '',
-      shouldChangePassword: true,
-      userEmail: 'admin@immich.app',
-      userId: expect.any(String),
-    },
-  },
-  user1oauth: {
-    accessToken: 'cmFuZG9tLWJ5dGVz',
-    userId: 'user-id',
-    userEmail: 'immich@test.com',
-    name: 'immich_name',
-    profileImagePath: '',
-    isAdmin: false,
-    shouldChangePassword: false,
-  },
-  user1password: {
-    accessToken: 'cmFuZG9tLWJ5dGVz',
-    userId: 'user-id',
-    userEmail: 'immich@test.com',
-    name: 'immich_name',
-    profileImagePath: '',
-    isAdmin: false,
-    shouldChangePassword: false,
-  },
-};
diff --git a/server/test/fixtures/face.stub.ts b/server/test/fixtures/face.stub.ts
index 27ca2a4356..b8c68d5bf4 100644
--- a/server/test/fixtures/face.stub.ts
+++ b/server/test/fixtures/face.stub.ts
@@ -51,21 +51,6 @@ export const faceStub = {
     sourceType: SourceType.MACHINE_LEARNING,
     faceSearch: { faceId: 'assetFaceId3', embedding: [1, 2, 3, 4] },
   }),
-  mergeFace2: Object.freeze<NonNullableProperty<AssetFaceEntity>>({
-    id: 'assetFaceId4',
-    assetId: assetStub.image1.id,
-    asset: assetStub.image1,
-    personId: personStub.mergePerson.id,
-    person: personStub.mergePerson,
-    boundingBoxX1: 0,
-    boundingBoxY1: 0,
-    boundingBoxX2: 1,
-    boundingBoxY2: 1,
-    imageHeight: 1024,
-    imageWidth: 1024,
-    sourceType: SourceType.MACHINE_LEARNING,
-    faceSearch: { faceId: 'assetFaceId4', embedding: [1, 2, 3, 4] },
-  }),
   start: Object.freeze<NonNullableProperty<AssetFaceEntity>>({
     id: 'assetFaceId5',
     assetId: assetStub.image.id,
@@ -141,4 +126,32 @@ export const faceStub = {
     sourceType: SourceType.MACHINE_LEARNING,
     faceSearch: { faceId: 'assetFaceId9', embedding: [1, 2, 3, 4] },
   }),
+  fromExif1: Object.freeze<AssetFaceEntity>({
+    id: 'assetFaceId9',
+    assetId: assetStub.image.id,
+    asset: assetStub.image,
+    personId: personStub.randomPerson.id,
+    person: personStub.randomPerson,
+    boundingBoxX1: 100,
+    boundingBoxY1: 100,
+    boundingBoxX2: 200,
+    boundingBoxY2: 200,
+    imageHeight: 500,
+    imageWidth: 400,
+    sourceType: SourceType.EXIF,
+  }),
+  fromExif2: Object.freeze<AssetFaceEntity>({
+    id: 'assetFaceId9',
+    assetId: assetStub.image.id,
+    asset: assetStub.image,
+    personId: personStub.randomPerson.id,
+    person: personStub.randomPerson,
+    boundingBoxX1: 0,
+    boundingBoxY1: 0,
+    boundingBoxX2: 1,
+    boundingBoxY2: 1,
+    imageHeight: 1024,
+    imageWidth: 1024,
+    sourceType: SourceType.EXIF,
+  }),
 };
diff --git a/server/test/fixtures/library.stub.ts b/server/test/fixtures/library.stub.ts
index 1a83ffe5d7..bb40035dcc 100644
--- a/server/test/fixtures/library.stub.ts
+++ b/server/test/fixtures/library.stub.ts
@@ -1,6 +1,3 @@
-import { join } from 'node:path';
-import { APP_MEDIA_LOCATION } from 'src/constants';
-import { THUMBNAIL_DIR } from 'src/cores/storage.core';
 import { LibraryEntity } from 'src/entities/library.entity';
 import { userStub } from 'test/fixtures/user.stub';
 
@@ -53,18 +50,6 @@ export const libraryStub = {
     refreshedAt: null,
     exclusionPatterns: [],
   }),
-  externalLibraryWithExclusionPattern: Object.freeze<LibraryEntity>({
-    id: 'library-id',
-    name: 'test_library',
-    assets: [],
-    owner: userStub.admin,
-    ownerId: 'user-id',
-    importPaths: [],
-    createdAt: new Date('2023-01-01'),
-    updatedAt: new Date('2023-01-01'),
-    refreshedAt: null,
-    exclusionPatterns: ['**/dir1/**'],
-  }),
   patternPath: Object.freeze<LibraryEntity>({
     id: 'library-id1337',
     name: 'importpath-exclusion-library1',
@@ -83,7 +68,7 @@ export const libraryStub = {
     assets: [],
     owner: userStub.admin,
     ownerId: 'user-id',
-    importPaths: [join(THUMBNAIL_DIR, 'library'), '/xyz', join(APP_MEDIA_LOCATION, 'library')],
+    importPaths: ['upload/thumbs', 'xyz', 'upload/library'],
     createdAt: new Date('2023-01-01'),
     updatedAt: new Date('2023-01-01'),
     refreshedAt: null,
diff --git a/server/test/fixtures/media.stub.ts b/server/test/fixtures/media.stub.ts
index 9b4e15a95d..de11c23f0a 100644
--- a/server/test/fixtures/media.stub.ts
+++ b/server/test/fixtures/media.stub.ts
@@ -17,6 +17,7 @@ const probeStubDefaultVideoStream: VideoStreamInfo[] = [
     rotation: 0,
     isHDR: false,
     bitrate: 0,
+    pixelFormat: 'yuv420p',
   },
 ];
 
@@ -43,6 +44,7 @@ export const probeStub = {
         rotation: 0,
         isHDR: false,
         bitrate: 0,
+        pixelFormat: 'yuv420p',
       },
       {
         index: 1,
@@ -53,6 +55,7 @@ export const probeStub = {
         rotation: 0,
         isHDR: false,
         bitrate: 0,
+        pixelFormat: 'yuv420p',
       },
     ],
   }),
@@ -68,6 +71,7 @@ export const probeStub = {
         rotation: 0,
         isHDR: false,
         bitrate: 0,
+        pixelFormat: 'yuv420p',
       },
     ],
   }),
@@ -83,6 +87,7 @@ export const probeStub = {
         rotation: 0,
         isHDR: false,
         bitrate: 0,
+        pixelFormat: 'yuv420p',
       },
     ],
   }),
@@ -90,6 +95,13 @@ export const probeStub = {
     ...probeStubDefault,
     videoStreams: [{ ...probeStubDefaultVideoStream[0], bitrate: 40_000_000 }],
   }),
+  videoStreamMTS: Object.freeze<VideoInfo>({
+    ...probeStubDefault,
+    format: {
+      ...probeStubDefaultFormat,
+      formatName: 'mpegts',
+    },
+  }),
   videoStreamHDR: Object.freeze<VideoInfo>({
     ...probeStubDefault,
     videoStreams: [
@@ -102,6 +114,23 @@ export const probeStub = {
         rotation: 0,
         isHDR: true,
         bitrate: 0,
+        pixelFormat: 'yuv420p10le',
+      },
+    ],
+  }),
+  videoStream10Bit: Object.freeze<VideoInfo>({
+    ...probeStubDefault,
+    videoStreams: [
+      {
+        index: 0,
+        height: 480,
+        width: 480,
+        codecName: 'h264',
+        frameCount: 100,
+        rotation: 0,
+        isHDR: false,
+        bitrate: 0,
+        pixelFormat: 'yuv420p10le',
       },
     ],
   }),
@@ -117,6 +146,7 @@ export const probeStub = {
         rotation: 90,
         isHDR: false,
         bitrate: 0,
+        pixelFormat: 'yuv420p',
       },
     ],
   }),
@@ -132,6 +162,7 @@ export const probeStub = {
         rotation: 0,
         isHDR: false,
         bitrate: 0,
+        pixelFormat: 'yuv420p',
       },
     ],
   }),
@@ -147,6 +178,7 @@ export const probeStub = {
         rotation: 0,
         isHDR: false,
         bitrate: 0,
+        pixelFormat: 'yuv420p',
       },
     ],
   }),
@@ -154,6 +186,13 @@ export const probeStub = {
     ...probeStubDefault,
     audioStreams: [{ index: 1, codecName: 'aac', frameCount: 100 }],
   }),
+  audioStreamUnknown: Object.freeze<VideoInfo>({
+    ...probeStubDefault,
+    audioStreams: [
+      { index: 0, codecName: 'aac', frameCount: 100 },
+      { index: 1, codecName: 'unknown', frameCount: 200 },
+    ],
+  }),
   matroskaContainer: Object.freeze<VideoInfo>({
     ...probeStubDefault,
     format: {
diff --git a/server/test/fixtures/person.stub.ts b/server/test/fixtures/person.stub.ts
index 3584d0486e..544894b31e 100644
--- a/server/test/fixtures/person.stub.ts
+++ b/server/test/fixtures/person.stub.ts
@@ -44,20 +44,6 @@ export const personStub = {
     faceAsset: null,
     isHidden: false,
   }),
-  noBirthDate: Object.freeze<PersonEntity>({
-    id: 'person-1',
-    createdAt: new Date('2021-01-01'),
-    updatedAt: new Date('2021-01-01'),
-    ownerId: userStub.admin.id,
-    owner: userStub.admin,
-    name: 'Person 1',
-    birthDate: null,
-    thumbnailPath: '/path/to/thumbnail.jpg',
-    faces: [],
-    faceAssetId: null,
-    faceAsset: null,
-    isHidden: false,
-  }),
   withBirthDate: Object.freeze<PersonEntity>({
     id: 'person-1',
     createdAt: new Date('2021-01-01'),
diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts
index 54898d8693..a8b8e02d74 100644
--- a/server/test/fixtures/shared-link.stub.ts
+++ b/server/test/fixtures/shared-link.stub.ts
@@ -5,7 +5,7 @@ import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto';
 import { mapUser } from 'src/dtos/user.dto';
 import { SharedLinkEntity } from 'src/entities/shared-link.entity';
 import { UserEntity } from 'src/entities/user.entity';
-import { AssetOrder, AssetType, SharedLinkType } from 'src/enum';
+import { AssetOrder, AssetStatus, AssetType, SharedLinkType } from 'src/enum';
 import { assetStub } from 'test/fixtures/asset.stub';
 import { authStub } from 'test/fixtures/auth.stub';
 import { userStub } from 'test/fixtures/user.stub';
@@ -62,10 +62,6 @@ const assetResponse: AssetResponseDto = {
   updatedAt: today,
   isFavorite: false,
   isArchived: false,
-  smartInfo: {
-    tags: [],
-    objects: ['a', 'b', 'c'],
-  },
   duration: '0:00:00.00000',
   exifInfo: assetInfo,
   livePhotoVideoId: null,
@@ -188,6 +184,7 @@ export const sharedLinkStub = {
       assets: [
         {
           id: 'id_1',
+          status: AssetStatus.ACTIVE,
           owner: undefined as unknown as UserEntity,
           ownerId: 'user_id_1',
           deviceAssetId: 'device_asset_id_1',
@@ -204,12 +201,6 @@ export const sharedLinkStub = {
           isArchived: false,
           isExternal: false,
           isOffline: false,
-          smartInfo: {
-            assetId: 'id_1',
-            tags: [],
-            objects: ['a', 'b', 'c'],
-            asset: null as any,
-          },
           files: [],
           thumbhash: null,
           encodedVideoPath: '',
@@ -308,21 +299,6 @@ export const sharedLinkResponseStub = {
     type: SharedLinkType.ALBUM,
     userId: 'admin_id',
   }),
-  readonly: Object.freeze<SharedLinkResponseDto>({
-    id: '123',
-    userId: 'admin_id',
-    key: sharedLinkBytes.toString('base64url'),
-    type: SharedLinkType.ALBUM,
-    createdAt: today,
-    expiresAt: tomorrow,
-    description: null,
-    password: null,
-    allowUpload: false,
-    allowDownload: false,
-    showMetadata: true,
-    album: albumResponse,
-    assets: [assetResponse],
-  }),
   readonlyNoMetadata: Object.freeze<SharedLinkResponseDto>({
     id: '123',
     userId: 'admin_id',
diff --git a/server/test/fixtures/system-config.stub.ts b/server/test/fixtures/system-config.stub.ts
index be21fc4060..ed8cc8694a 100644
--- a/server/test/fixtures/system-config.stub.ts
+++ b/server/test/fixtures/system-config.stub.ts
@@ -54,6 +54,9 @@ export const systemConfigStub = {
   },
   libraryWatchEnabled: {
     library: {
+      scan: {
+        enabled: false,
+      },
       watch: {
         enabled: true,
       },
@@ -61,6 +64,9 @@ export const systemConfigStub = {
   },
   libraryWatchDisabled: {
     library: {
+      scan: {
+        enabled: false,
+      },
       watch: {
         enabled: false,
       },
@@ -72,6 +78,29 @@ export const systemConfigStub = {
         enabled: true,
         cronExpression: '0 0 * * *',
       },
+      watch: {
+        enabled: false,
+      },
+    },
+  },
+  libraryScanAndWatch: {
+    library: {
+      scan: {
+        enabled: true,
+        cronExpression: '0 0 * * *',
+      },
+      watch: {
+        enabled: true,
+      },
+    },
+  },
+  backupEnabled: {
+    backup: {
+      database: {
+        enabled: true,
+        cronExpression: '0 0 * * *',
+        keepLastAmount: 1,
+      },
     },
   },
   machineLearningDisabled: {
@@ -79,4 +108,18 @@ export const systemConfigStub = {
       enabled: false,
     },
   },
+  machineLearningEnabled: {
+    machineLearning: {
+      enabled: true,
+      clip: {
+        modelName: 'ViT-B-16__openai',
+        enabled: true,
+      },
+    },
+  },
+  publicUsersDisabled: {
+    server: {
+      publicUsers: false,
+    },
+  },
 } satisfies Record<string, DeepPartial<SystemConfig>>;
diff --git a/server/test/fixtures/user.stub.ts b/server/test/fixtures/user.stub.ts
index 6f3a819eef..9553b5344a 100644
--- a/server/test/fixtures/user.stub.ts
+++ b/server/test/fixtures/user.stub.ts
@@ -2,35 +2,12 @@ import { UserEntity } from 'src/entities/user.entity';
 import { UserAvatarColor, UserMetadataKey } from 'src/enum';
 import { authStub } from 'test/fixtures/auth.stub';
 
-export const userDto = {
-  user1: {
-    email: 'user1@immich.app',
-    password: 'Password123',
-    name: 'User 1',
-  },
-  user2: {
-    email: 'user2@immich.app',
-    password: 'Password123',
-    name: 'User 2',
-  },
-  user3: {
-    email: 'user3@immich.app',
-    password: 'Password123',
-    name: 'User 3',
-  },
-  userWithQuota: {
-    email: 'quota-user@immich.app',
-    password: 'Password123',
-    name: 'User with quota',
-    quotaSizeInBytes: 42,
-  },
-};
-
 export const userStub = {
   admin: Object.freeze<UserEntity>({
     ...authStub.admin.user,
     password: 'admin_password',
     name: 'admin_name',
+    id: 'admin_id',
     storageLabel: 'admin',
     oauthId: '',
     shouldChangePassword: false,
@@ -100,22 +77,6 @@ export const userStub = {
     quotaSizeInBytes: null,
     quotaUsageInBytes: 0,
   }),
-  externalPathRoot: Object.freeze<UserEntity>({
-    ...authStub.user1.user,
-    password: 'immich_password',
-    name: 'immich_name',
-    storageLabel: 'label-1',
-    oauthId: '',
-    shouldChangePassword: false,
-    profileImagePath: '',
-    createdAt: new Date('2021-01-01'),
-    deletedAt: null,
-    updatedAt: new Date('2021-01-01'),
-    tags: [],
-    assets: [],
-    quotaSizeInBytes: null,
-    quotaUsageInBytes: 0,
-  }),
   profilePath: Object.freeze<UserEntity>({
     ...authStub.user1.user,
     password: 'immich_password',
diff --git a/server/test/medium/metadata.service.spec.ts b/server/test/medium/metadata.service.spec.ts
new file mode 100644
index 0000000000..3ccce0f16e
--- /dev/null
+++ b/server/test/medium/metadata.service.spec.ts
@@ -0,0 +1,137 @@
+import { Stats } from 'node:fs';
+import { writeFile } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { AssetEntity } from 'src/entities/asset.entity';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { MetadataRepository } from 'src/repositories/metadata.repository';
+import { MetadataService } from 'src/services/metadata.service';
+import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
+import { newRandomImage, newTestService } from 'test/utils';
+import { Mocked } from 'vitest';
+
+const metadataRepository = new MetadataRepository(newLoggerRepositoryMock());
+
+const createTestFile = async (exifData: Record<string, any>) => {
+  const data = newRandomImage();
+  const filePath = join(tmpdir(), 'test.png');
+  await writeFile(filePath, data);
+  await metadataRepository.writeTags(filePath, exifData);
+  return { filePath };
+};
+
+type TimeZoneTest = {
+  description: string;
+  serverTimeZone?: string;
+  exifData: Record<string, any>;
+  expected: {
+    localDateTime: string;
+    dateTimeOriginal: string;
+    timeZone: string | null;
+  };
+};
+
+describe(MetadataService.name, () => {
+  let sut: MetadataService;
+
+  let assetMock: Mocked<IAssetRepository>;
+  let storageMock: Mocked<IStorageRepository>;
+
+  beforeEach(() => {
+    ({ sut, assetMock, storageMock } = newTestService(MetadataService, { metadataRepository }));
+
+    storageMock.stat.mockResolvedValue({ size: 123_456 } as Stats);
+
+    delete process.env.TZ;
+  });
+
+  it('should be defined', () => {
+    expect(sut).toBeDefined();
+  });
+
+  describe('handleMetadataExtraction', () => {
+    const timeZoneTests: TimeZoneTest[] = [
+      {
+        description: 'should handle no time zone information',
+        exifData: {
+          DateTimeOriginal: '2022:01:01 00:00:00',
+        },
+        expected: {
+          localDateTime: '2022-01-01T00:00:00.000Z',
+          dateTimeOriginal: '2022-01-01T00:00:00.000Z',
+          timeZone: null,
+        },
+      },
+      {
+        description: 'should handle no time zone information and server behind UTC',
+        serverTimeZone: 'America/Los_Angeles',
+        exifData: {
+          DateTimeOriginal: '2022:01:01 00:00:00',
+        },
+        expected: {
+          localDateTime: '2022-01-01T00:00:00.000Z',
+          dateTimeOriginal: '2022-01-01T08:00:00.000Z',
+          timeZone: null,
+        },
+      },
+      {
+        description: 'should handle no time zone information and server ahead of UTC',
+        serverTimeZone: 'Europe/Brussels',
+        exifData: {
+          DateTimeOriginal: '2022:01:01 00:00:00',
+        },
+        expected: {
+          localDateTime: '2022-01-01T00:00:00.000Z',
+          dateTimeOriginal: '2021-12-31T23:00:00.000Z',
+          timeZone: null,
+        },
+      },
+      {
+        description: 'should handle no time zone information and server ahead of UTC in the summer',
+        serverTimeZone: 'Europe/Brussels',
+        exifData: {
+          DateTimeOriginal: '2022:06:01 00:00:00',
+        },
+        expected: {
+          localDateTime: '2022-06-01T00:00:00.000Z',
+          dateTimeOriginal: '2022-05-31T22:00:00.000Z',
+          timeZone: null,
+        },
+      },
+      {
+        description: 'should handle a +13:00 time zone',
+        exifData: {
+          DateTimeOriginal: '2022:01:01 00:00:00+13:00',
+        },
+        expected: {
+          localDateTime: '2022-01-01T00:00:00.000Z',
+          dateTimeOriginal: '2021-12-31T11:00:00.000Z',
+          timeZone: 'UTC+13',
+        },
+      },
+    ];
+
+    it.each(timeZoneTests)('$description', async ({ exifData, serverTimeZone, expected }) => {
+      process.env.TZ = serverTimeZone ?? undefined;
+
+      const { filePath } = await createTestFile(exifData);
+      assetMock.getByIds.mockResolvedValue([{ id: 'asset-1', originalPath: filePath } as AssetEntity]);
+
+      await sut.handleMetadataExtraction({ id: 'asset-1' });
+
+      expect(assetMock.upsertExif).toHaveBeenCalledWith(
+        expect.objectContaining({
+          dateTimeOriginal: new Date(expected.dateTimeOriginal),
+          timeZone: expected.timeZone,
+        }),
+      );
+
+      expect(assetMock.update).toHaveBeenCalledWith(
+        expect.objectContaining({
+          localDateTime: new Date(expected.localDateTime),
+        }),
+      );
+    });
+  });
+});
diff --git a/server/test/repositories/album.repository.mock.ts b/server/test/repositories/album.repository.mock.ts
index af267dd49c..dd5c3af6a8 100644
--- a/server/test/repositories/album.repository.mock.ts
+++ b/server/test/repositories/album.repository.mock.ts
@@ -4,17 +4,14 @@ import { Mocked, vitest } from 'vitest';
 export const newAlbumRepositoryMock = (): Mocked<IAlbumRepository> => {
   return {
     getById: vitest.fn(),
-    getByIds: vitest.fn(),
     getByAssetId: vitest.fn(),
     getMetadataForIds: vitest.fn(),
-    getInvalidThumbnail: vitest.fn(),
     getOwned: vitest.fn(),
     getShared: vitest.fn(),
     getNotShared: vitest.fn(),
     restoreAll: vitest.fn(),
     softDeleteAll: vitest.fn(),
     deleteAll: vitest.fn(),
-    getAll: vitest.fn(),
     addAssetIds: vitest.fn(),
     removeAsset: vitest.fn(),
     removeAssetIds: vitest.fn(),
diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts
index 69f07bf105..928a7956c5 100644
--- a/server/test/repositories/asset.repository.mock.ts
+++ b/server/test/repositories/asset.repository.mock.ts
@@ -17,16 +17,13 @@ export const newAssetRepositoryMock = (): Mocked<IAssetRepository> => {
     getByChecksum: vitest.fn(),
     getByChecksums: vitest.fn(),
     getUploadAssetIdByChecksum: vitest.fn(),
-    getWith: vitest.fn(),
     getRandom: vitest.fn(),
-    getFirstAssetForAlbumId: vitest.fn(),
     getLastUpdatedAssetForAlbumId: vitest.fn(),
     getAll: vitest.fn().mockResolvedValue({ items: [], hasNextPage: false }),
     getAllByDeviceId: vitest.fn(),
     getLivePhotoCount: vitest.fn(),
     updateAll: vitest.fn(),
     updateDuplicates: vitest.fn(),
-    getExternalLibraryAssetPaths: vitest.fn(),
     getByLibraryIdAndOriginalPath: vitest.fn(),
     deleteAll: vitest.fn(),
     update: vitest.fn(),
@@ -35,15 +32,11 @@ export const newAssetRepositoryMock = (): Mocked<IAssetRepository> => {
     getStatistics: vitest.fn(),
     getTimeBucket: vitest.fn(),
     getTimeBuckets: vitest.fn(),
-    restoreAll: vitest.fn(),
-    softDeleteAll: vitest.fn(),
     getAssetIdByCity: vitest.fn(),
-    getAssetIdByTag: vitest.fn(),
     getAllForUserFullSync: vitest.fn(),
     getChangedDeltaSync: vitest.fn(),
     getDuplicates: vitest.fn(),
     upsertFile: vitest.fn(),
-    getAssetsByOriginalPath: vitest.fn(),
-    getUniqueOriginalPaths: vitest.fn(),
+    upsertFiles: vitest.fn(),
   };
 };
diff --git a/server/test/repositories/config.repository.mock.ts b/server/test/repositories/config.repository.mock.ts
new file mode 100644
index 0000000000..df26f7f725
--- /dev/null
+++ b/server/test/repositories/config.repository.mock.ts
@@ -0,0 +1,101 @@
+import { ImmichEnvironment, ImmichWorker } from 'src/enum';
+import { EnvData, IConfigRepository } from 'src/interfaces/config.interface';
+import { DatabaseExtension } from 'src/interfaces/database.interface';
+import { Mocked, vitest } from 'vitest';
+
+const envData: EnvData = {
+  port: 2283,
+  environment: ImmichEnvironment.PRODUCTION,
+
+  buildMetadata: {},
+  bull: {
+    config: {
+      prefix: 'immich_bull',
+    },
+    queues: [{ name: 'queue-1' }],
+  },
+
+  cls: {
+    config: {},
+  },
+
+  database: {
+    config: {
+      connectionType: 'parts',
+      database: 'immich',
+      type: 'postgres',
+      host: 'database',
+      port: 5432,
+      username: 'postgres',
+      password: 'postgres',
+      name: 'immich',
+      synchronize: false,
+      migrationsRun: true,
+    },
+
+    skipMigrations: false,
+    vectorExtension: DatabaseExtension.VECTORS,
+  },
+
+  licensePublicKey: {
+    client: 'client-public-key',
+    server: 'server-public-key',
+  },
+
+  network: {
+    trustedProxies: [],
+  },
+
+  otel: {
+    metrics: {
+      hostMetrics: false,
+      apiMetrics: {
+        enable: false,
+        ignoreRoutes: [],
+      },
+    },
+  },
+
+  redis: {
+    host: 'redis',
+    port: 6379,
+    db: 0,
+  },
+
+  resourcePaths: {
+    lockFile: 'build-lock.json',
+    geodata: {
+      dateFile: '/build/geodata/geodata-date.txt',
+      admin1: '/build/geodata/admin1CodesASCII.txt',
+      admin2: '/build/geodata/admin2Codes.txt',
+      cities500: '/build/geodata/cities500.txt',
+      naturalEarthCountriesPath: 'build/ne_10m_admin_0_countries.geojson',
+    },
+    web: {
+      root: '/build/www',
+      indexHtml: '/build/www/index.html',
+    },
+  },
+
+  storage: {
+    ignoreMountCheckErrors: false,
+  },
+
+  telemetry: {
+    apiPort: 8081,
+    microservicesPort: 8082,
+    metrics: new Set(),
+  },
+
+  workers: [ImmichWorker.API, ImmichWorker.MICROSERVICES],
+
+  noColor: false,
+};
+
+export const mockEnvData = (config: Partial<EnvData>) => ({ ...envData, ...config });
+export const newConfigRepositoryMock = (): Mocked<IConfigRepository> => {
+  return {
+    getEnv: vitest.fn().mockReturnValue(mockEnvData({})),
+    getWorker: vitest.fn().mockReturnValue(ImmichWorker.API),
+  };
+};
diff --git a/server/test/repositories/cron.repository.mock.ts b/server/test/repositories/cron.repository.mock.ts
new file mode 100644
index 0000000000..2b0784e8ac
--- /dev/null
+++ b/server/test/repositories/cron.repository.mock.ts
@@ -0,0 +1,9 @@
+import { ICronRepository } from 'src/interfaces/cron.interface';
+import { Mocked, vitest } from 'vitest';
+
+export const newCronRepositoryMock = (): Mocked<ICronRepository> => {
+  return {
+    create: vitest.fn(),
+    update: vitest.fn(),
+  };
+};
diff --git a/server/test/repositories/database.repository.mock.ts b/server/test/repositories/database.repository.mock.ts
index 0e1d4ab3e7..da6417a38c 100644
--- a/server/test/repositories/database.repository.mock.ts
+++ b/server/test/repositories/database.repository.mock.ts
@@ -9,7 +9,6 @@ export const newDatabaseRepositoryMock = (): Mocked<IDatabaseRepository> => {
     getPostgresVersion: vitest.fn().mockResolvedValue('14.10 (Debian 14.10-1.pgdg120+1)'),
     getPostgresVersionRange: vitest.fn().mockReturnValue('>=14.0.0'),
     createExtension: vitest.fn().mockResolvedValue(void 0),
-    updateExtension: vitest.fn(),
     updateVectorExtension: vitest.fn(),
     reindex: vitest.fn(),
     shouldReindex: vitest.fn(),
diff --git a/server/test/repositories/event.repository.mock.ts b/server/test/repositories/event.repository.mock.ts
index a9af627599..a425ddef3a 100644
--- a/server/test/repositories/event.repository.mock.ts
+++ b/server/test/repositories/event.repository.mock.ts
@@ -3,10 +3,10 @@ import { Mocked, vitest } from 'vitest';
 
 export const newEventRepositoryMock = (): Mocked<IEventRepository> => {
   return {
-    on: vitest.fn(),
+    setup: vitest.fn(),
     emit: vitest.fn() as any,
-    clientSend: vitest.fn(),
-    clientBroadcast: vitest.fn(),
+    clientSend: vitest.fn() as any,
+    clientBroadcast: vitest.fn() as any,
     serverSend: vitest.fn(),
   };
 };
diff --git a/server/test/repositories/job.repository.mock.ts b/server/test/repositories/job.repository.mock.ts
index 6bffe184fd..e9557af59b 100644
--- a/server/test/repositories/job.repository.mock.ts
+++ b/server/test/repositories/job.repository.mock.ts
@@ -3,10 +3,9 @@ import { Mocked, vitest } from 'vitest';
 
 export const newJobRepositoryMock = (): Mocked<IJobRepository> => {
   return {
-    addHandler: vitest.fn(),
-    addCronJob: vitest.fn(),
-    deleteCronJob: vitest.fn(),
-    updateCronJob: vitest.fn(),
+    setup: vitest.fn(),
+    startWorkers: vitest.fn(),
+    run: vitest.fn(),
     setConcurrency: vitest.fn(),
     empty: vitest.fn(),
     pause: vitest.fn(),
@@ -17,5 +16,6 @@ export const newJobRepositoryMock = (): Mocked<IJobRepository> => {
     getJobCounts: vitest.fn(),
     clear: vitest.fn(),
     waitForQueueCompletion: vitest.fn(),
+    removeJob: vitest.fn(),
   };
 };
diff --git a/server/test/repositories/logger.repository.mock.ts b/server/test/repositories/logger.repository.mock.ts
index 5f7262c7e5..6342e9e73c 100644
--- a/server/test/repositories/logger.repository.mock.ts
+++ b/server/test/repositories/logger.repository.mock.ts
@@ -6,7 +6,7 @@ export const newLoggerRepositoryMock = (): Mocked<ILoggerRepository> => {
     setLogLevel: vitest.fn(),
     setContext: vitest.fn(),
     setAppName: vitest.fn(),
-
+    isLevelEnabled: vitest.fn(),
     verbose: vitest.fn(),
     debug: vitest.fn(),
     log: vitest.fn(),
diff --git a/server/test/repositories/map.repository.mock.ts b/server/test/repositories/map.repository.mock.ts
index 95965522e3..703e8696f1 100644
--- a/server/test/repositories/map.repository.mock.ts
+++ b/server/test/repositories/map.repository.mock.ts
@@ -6,6 +6,5 @@ export const newMapRepositoryMock = (): Mocked<IMapRepository> => {
     init: vitest.fn(),
     reverseGeocode: vitest.fn(),
     getMapMarkers: vitest.fn(),
-    fetchStyle: vitest.fn(),
   };
 };
diff --git a/server/test/repositories/media.repository.mock.ts b/server/test/repositories/media.repository.mock.ts
index 4c344a9866..a809b08162 100644
--- a/server/test/repositories/media.repository.mock.ts
+++ b/server/test/repositories/media.repository.mock.ts
@@ -3,8 +3,9 @@ import { Mocked, vitest } from 'vitest';
 
 export const newMediaRepositoryMock = (): Mocked<IMediaRepository> => {
   return {
-    generateThumbnail: vitest.fn(),
-    generateThumbhash: vitest.fn(),
+    generateThumbnail: vitest.fn().mockImplementation(() => Promise.resolve()),
+    generateThumbhash: vitest.fn().mockImplementation(() => Promise.resolve()),
+    decodeImage: vitest.fn().mockResolvedValue({ data: Buffer.from(''), info: {} }),
     extract: vitest.fn().mockResolvedValue(false),
     probe: vitest.fn(),
     transcode: vitest.fn(),
diff --git a/server/test/repositories/metadata.repository.mock.ts b/server/test/repositories/metadata.repository.mock.ts
index 5dbfb3d453..60c5644b36 100644
--- a/server/test/repositories/metadata.repository.mock.ts
+++ b/server/test/repositories/metadata.repository.mock.ts
@@ -7,10 +7,5 @@ export const newMetadataRepositoryMock = (): Mocked<IMetadataRepository> => {
     readTags: vitest.fn(),
     writeTags: vitest.fn(),
     extractBinaryTag: vitest.fn(),
-    getCameraMakes: vitest.fn(),
-    getCameraModels: vitest.fn(),
-    getCities: vitest.fn(),
-    getCountries: vitest.fn(),
-    getStates: vitest.fn(),
   };
 };
diff --git a/server/test/repositories/metric.repository.mock.ts b/server/test/repositories/metric.repository.mock.ts
deleted file mode 100644
index e2c3e2aac1..0000000000
--- a/server/test/repositories/metric.repository.mock.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { IMetricRepository } from 'src/interfaces/metric.interface';
-import { Mocked, vitest } from 'vitest';
-
-export const newMetricRepositoryMock = (): Mocked<IMetricRepository> => {
-  return {
-    api: {
-      addToCounter: vitest.fn(),
-      addToGauge: vitest.fn(),
-      addToHistogram: vitest.fn(),
-      configure: vitest.fn(),
-    },
-    host: {
-      addToCounter: vitest.fn(),
-      addToGauge: vitest.fn(),
-      addToHistogram: vitest.fn(),
-      configure: vitest.fn(),
-    },
-    jobs: {
-      addToCounter: vitest.fn(),
-      addToGauge: vitest.fn(),
-      addToHistogram: vitest.fn(),
-      configure: vitest.fn(),
-    },
-    repo: {
-      addToCounter: vitest.fn(),
-      addToGauge: vitest.fn(),
-      addToHistogram: vitest.fn(),
-      configure: vitest.fn(),
-    },
-  };
-};
diff --git a/server/test/repositories/notification.repository.mock.ts b/server/test/repositories/notification.repository.mock.ts
index 71975b429c..16862dc3d7 100644
--- a/server/test/repositories/notification.repository.mock.ts
+++ b/server/test/repositories/notification.repository.mock.ts
@@ -4,7 +4,7 @@ import { Mocked } from 'vitest';
 export const newNotificationRepositoryMock = (): Mocked<INotificationRepository> => {
   return {
     renderEmail: vitest.fn(),
-    sendEmail: vitest.fn(),
+    sendEmail: vitest.fn().mockResolvedValue({ messageId: 'message-1' }),
     verifySmtp: vitest.fn(),
   };
 };
diff --git a/server/test/repositories/oauth.repository.mock.ts b/server/test/repositories/oauth.repository.mock.ts
new file mode 100644
index 0000000000..f87b3781e9
--- /dev/null
+++ b/server/test/repositories/oauth.repository.mock.ts
@@ -0,0 +1,11 @@
+import { IOAuthRepository } from 'src/interfaces/oauth.interface';
+import { Mocked } from 'vitest';
+
+export const newOAuthRepositoryMock = (): Mocked<IOAuthRepository> => {
+  return {
+    init: vitest.fn(),
+    authorize: vitest.fn(),
+    getLogoutEndpoint: vitest.fn(),
+    getProfile: vitest.fn(),
+  };
+};
diff --git a/server/test/repositories/person.repository.mock.ts b/server/test/repositories/person.repository.mock.ts
index 77e8ccf010..d7b92d3eab 100644
--- a/server/test/repositories/person.repository.mock.ts
+++ b/server/test/repositories/person.repository.mock.ts
@@ -6,7 +6,6 @@ export const newPersonRepositoryMock = (): Mocked<IPersonRepository> => {
     getById: vitest.fn(),
     getAll: vitest.fn(),
     getAllForUser: vitest.fn(),
-    getAssets: vitest.fn(),
     getAllWithoutFaces: vitest.fn(),
 
     getByName: vitest.fn(),
@@ -17,8 +16,7 @@ export const newPersonRepositoryMock = (): Mocked<IPersonRepository> => {
     update: vitest.fn(),
     updateAll: vitest.fn(),
     delete: vitest.fn(),
-    deleteAll: vitest.fn(),
-    deleteAllFaces: vitest.fn(),
+    deleteFaces: vitest.fn(),
 
     getStatistics: vitest.fn(),
     getAllFaces: vitest.fn(),
@@ -26,8 +24,8 @@ export const newPersonRepositoryMock = (): Mocked<IPersonRepository> => {
     getRandomFace: vitest.fn(),
 
     reassignFaces: vitest.fn(),
-    createFaces: vitest.fn(),
-    replaceFaces: vitest.fn(),
+    unassignFaces: vitest.fn(),
+    refreshFaces: vitest.fn(),
     getFaces: vitest.fn(),
     reassignFace: vitest.fn(),
     getFaceById: vitest.fn(),
diff --git a/server/test/repositories/process.repository.mock.ts b/server/test/repositories/process.repository.mock.ts
new file mode 100644
index 0000000000..9a3c5a30b6
--- /dev/null
+++ b/server/test/repositories/process.repository.mock.ts
@@ -0,0 +1,8 @@
+import { IProcessRepository } from 'src/interfaces/process.interface';
+import { Mocked, vitest } from 'vitest';
+
+export const newProcessRepositoryMock = (): Mocked<IProcessRepository> => {
+  return {
+    spawn: vitest.fn(),
+  };
+};
diff --git a/server/test/repositories/search.repository.mock.ts b/server/test/repositories/search.repository.mock.ts
index fd244c6f5c..be0e753e30 100644
--- a/server/test/repositories/search.repository.mock.ts
+++ b/server/test/repositories/search.repository.mock.ts
@@ -7,11 +7,17 @@ export const newSearchRepositoryMock = (): Mocked<ISearchRepository> => {
     searchSmart: vitest.fn(),
     searchDuplicates: vitest.fn(),
     searchFaces: vitest.fn(),
+    searchRandom: vitest.fn(),
     upsert: vitest.fn(),
     searchPlaces: vitest.fn(),
     getAssetsByCity: vitest.fn(),
     deleteAllSearchEmbeddings: vitest.fn(),
     getDimensionSize: vitest.fn(),
     setDimensionSize: vitest.fn(),
+    getCameraMakes: vitest.fn(),
+    getCameraModels: vitest.fn(),
+    getCities: vitest.fn(),
+    getCountries: vitest.fn(),
+    getStates: vitest.fn(),
   };
 };
diff --git a/server/test/repositories/storage.repository.mock.ts b/server/test/repositories/storage.repository.mock.ts
index 5c2951e097..0af16a8d17 100644
--- a/server/test/repositories/storage.repository.mock.ts
+++ b/server/test/repositories/storage.repository.mock.ts
@@ -48,7 +48,10 @@ export const newStorageRepositoryMock = (reset = true): Mocked<IStorageRepositor
     createZipStream: vitest.fn(),
     createReadStream: vitest.fn(),
     readFile: vitest.fn(),
-    writeFile: vitest.fn(),
+    createFile: vitest.fn(),
+    createWriteStream: vitest.fn(),
+    createOrOverwriteFile: vitest.fn(),
+    overwriteFile: vitest.fn(),
     unlink: vitest.fn(),
     unlinkDir: vitest.fn().mockResolvedValue(true),
     removeEmptyDirs: vitest.fn(),
diff --git a/server/test/repositories/system-metadata.repository.mock.ts b/server/test/repositories/system-metadata.repository.mock.ts
index e44301fb21..793dd4c1c0 100644
--- a/server/test/repositories/system-metadata.repository.mock.ts
+++ b/server/test/repositories/system-metadata.repository.mock.ts
@@ -1,12 +1,9 @@
-import { SystemConfigCore } from 'src/cores/system-config.core';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { clearConfigCache } from 'src/utils/config';
 import { Mocked, vitest } from 'vitest';
 
-export const newSystemMetadataRepositoryMock = (reset = true): Mocked<ISystemMetadataRepository> => {
-  if (reset) {
-    SystemConfigCore.reset();
-  }
-
+export const newSystemMetadataRepositoryMock = (): Mocked<ISystemMetadataRepository> => {
+  clearConfigCache();
   return {
     get: vitest.fn() as any,
     set: vitest.fn(),
diff --git a/server/test/repositories/tag.repository.mock.ts b/server/test/repositories/tag.repository.mock.ts
index a3fc0e77e0..acc2b59f6d 100644
--- a/server/test/repositories/tag.repository.mock.ts
+++ b/server/test/repositories/tag.repository.mock.ts
@@ -17,5 +17,6 @@ export const newTagRepositoryMock = (): Mocked<ITagRepository> => {
     addAssetIds: vitest.fn(),
     removeAssetIds: vitest.fn(),
     upsertAssetIds: vitest.fn(),
+    deleteEmptyTags: vitest.fn(),
   };
 };
diff --git a/server/test/repositories/telemetry.repository.mock.ts b/server/test/repositories/telemetry.repository.mock.ts
new file mode 100644
index 0000000000..2d537e888a
--- /dev/null
+++ b/server/test/repositories/telemetry.repository.mock.ts
@@ -0,0 +1,21 @@
+import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
+import { Mocked, vitest } from 'vitest';
+
+const newMetricGroupMock = () => {
+  return {
+    addToCounter: vitest.fn(),
+    addToGauge: vitest.fn(),
+    addToHistogram: vitest.fn(),
+    configure: vitest.fn(),
+  };
+};
+
+export const newTelemetryRepositoryMock = (): Mocked<ITelemetryRepository> => {
+  return {
+    setup: vitest.fn(),
+    api: newMetricGroupMock(),
+    host: newMetricGroupMock(),
+    jobs: newMetricGroupMock(),
+    repo: newMetricGroupMock(),
+  };
+};
diff --git a/server/test/repositories/trash.repository.mock.ts b/server/test/repositories/trash.repository.mock.ts
new file mode 100644
index 0000000000..472b315b01
--- /dev/null
+++ b/server/test/repositories/trash.repository.mock.ts
@@ -0,0 +1,11 @@
+import { ITrashRepository } from 'src/interfaces/trash.interface';
+import { Mocked, vitest } from 'vitest';
+
+export const newTrashRepositoryMock = (): Mocked<ITrashRepository> => {
+  return {
+    empty: vitest.fn(),
+    restore: vitest.fn(),
+    restoreAll: vitest.fn(),
+    getDeletedIds: vitest.fn(),
+  };
+};
diff --git a/server/test/repositories/user.repository.mock.ts b/server/test/repositories/user.repository.mock.ts
index 6071ae47fa..6362ab6a99 100644
--- a/server/test/repositories/user.repository.mock.ts
+++ b/server/test/repositories/user.repository.mock.ts
@@ -1,12 +1,7 @@
-import { UserCore } from 'src/cores/user.core';
 import { IUserRepository } from 'src/interfaces/user.interface';
 import { Mocked, vitest } from 'vitest';
 
-export const newUserRepositoryMock = (reset = true): Mocked<IUserRepository> => {
-  if (reset) {
-    UserCore.reset();
-  }
-
+export const newUserRepositoryMock = (): Mocked<IUserRepository> => {
   return {
     get: vitest.fn(),
     getAdmin: vitest.fn(),
diff --git a/server/test/repositories/version-history.repository.mock.ts b/server/test/repositories/version-history.repository.mock.ts
new file mode 100644
index 0000000000..7c35e316d3
--- /dev/null
+++ b/server/test/repositories/version-history.repository.mock.ts
@@ -0,0 +1,10 @@
+import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
+import { Mocked, vitest } from 'vitest';
+
+export const newVersionHistoryRepositoryMock = (): Mocked<IVersionHistoryRepository> => {
+  return {
+    getAll: vitest.fn().mockResolvedValue([]),
+    getLatest: vitest.fn(),
+    create: vitest.fn(),
+  };
+};
diff --git a/server/test/repositories/view.repository.mock.ts b/server/test/repositories/view.repository.mock.ts
new file mode 100644
index 0000000000..a002362ae7
--- /dev/null
+++ b/server/test/repositories/view.repository.mock.ts
@@ -0,0 +1,9 @@
+import { IViewRepository } from 'src/interfaces/view.interface';
+import { Mocked, vitest } from 'vitest';
+
+export const newViewRepositoryMock = (): Mocked<IViewRepository> => {
+  return {
+    getAssetsByOriginalPath: vitest.fn(),
+    getUniqueOriginalPaths: vitest.fn(),
+  };
+};
diff --git a/server/test/utils.ts b/server/test/utils.ts
new file mode 100644
index 0000000000..7f5b75020c
--- /dev/null
+++ b/server/test/utils.ts
@@ -0,0 +1,252 @@
+import { ChildProcessWithoutNullStreams } from 'node:child_process';
+import { Writable } from 'node:stream';
+import { PNG } from 'pngjs';
+import { ImmichWorker } from 'src/enum';
+import { IMetadataRepository } from 'src/interfaces/metadata.interface';
+import { BaseService } from 'src/services/base.service';
+import { newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
+import { newActivityRepositoryMock } from 'test/repositories/activity.repository.mock';
+import { newAlbumUserRepositoryMock } from 'test/repositories/album-user.repository.mock';
+import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
+import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock';
+import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
+import { newAuditRepositoryMock } from 'test/repositories/audit.repository.mock';
+import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
+import { newCronRepositoryMock } from 'test/repositories/cron.repository.mock';
+import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
+import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
+import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
+import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
+import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock';
+import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
+import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock';
+import { newMapRepositoryMock } from 'test/repositories/map.repository.mock';
+import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock';
+import { newMemoryRepositoryMock } from 'test/repositories/memory.repository.mock';
+import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock';
+import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock';
+import { newNotificationRepositoryMock } from 'test/repositories/notification.repository.mock';
+import { newOAuthRepositoryMock } from 'test/repositories/oauth.repository.mock';
+import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
+import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
+import { newProcessRepositoryMock } from 'test/repositories/process.repository.mock';
+import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock';
+import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock';
+import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock';
+import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock';
+import { newStackRepositoryMock } from 'test/repositories/stack.repository.mock';
+import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
+import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
+import { newTagRepositoryMock } from 'test/repositories/tag.repository.mock';
+import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
+import { newTrashRepositoryMock } from 'test/repositories/trash.repository.mock';
+import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
+import { newVersionHistoryRepositoryMock } from 'test/repositories/version-history.repository.mock';
+import { newViewRepositoryMock } from 'test/repositories/view.repository.mock';
+import { Readable } from 'typeorm/platform/PlatformTools';
+import { Mocked, vitest } from 'vitest';
+
+type Overrides = {
+  worker?: ImmichWorker;
+  metadataRepository?: IMetadataRepository;
+};
+type BaseServiceArgs = ConstructorParameters<typeof BaseService>;
+type Constructor<Type, Args extends Array<any>> = {
+  new (...deps: Args): Type;
+};
+
+export const newTestService = <T extends BaseService>(
+  Service: Constructor<T, BaseServiceArgs>,
+  overrides?: Overrides,
+) => {
+  const { metadataRepository } = overrides || {};
+
+  const accessMock = newAccessRepositoryMock();
+  const loggerMock = newLoggerRepositoryMock();
+  const cronMock = newCronRepositoryMock();
+  const cryptoMock = newCryptoRepositoryMock();
+  const activityMock = newActivityRepositoryMock();
+  const auditMock = newAuditRepositoryMock();
+  const albumMock = newAlbumRepositoryMock();
+  const albumUserMock = newAlbumUserRepositoryMock();
+  const assetMock = newAssetRepositoryMock();
+  const configMock = newConfigRepositoryMock();
+  const databaseMock = newDatabaseRepositoryMock();
+  const eventMock = newEventRepositoryMock();
+  const jobMock = newJobRepositoryMock();
+  const keyMock = newKeyRepositoryMock();
+  const libraryMock = newLibraryRepositoryMock();
+  const machineLearningMock = newMachineLearningRepositoryMock();
+  const mapMock = newMapRepositoryMock();
+  const mediaMock = newMediaRepositoryMock();
+  const memoryMock = newMemoryRepositoryMock();
+  const metadataMock = (metadataRepository || newMetadataRepositoryMock()) as Mocked<IMetadataRepository>;
+  const moveMock = newMoveRepositoryMock();
+  const notificationMock = newNotificationRepositoryMock();
+  const oauthMock = newOAuthRepositoryMock();
+  const partnerMock = newPartnerRepositoryMock();
+  const personMock = newPersonRepositoryMock();
+  const processMock = newProcessRepositoryMock();
+  const searchMock = newSearchRepositoryMock();
+  const serverInfoMock = newServerInfoRepositoryMock();
+  const sessionMock = newSessionRepositoryMock();
+  const sharedLinkMock = newSharedLinkRepositoryMock();
+  const stackMock = newStackRepositoryMock();
+  const storageMock = newStorageRepositoryMock();
+  const systemMock = newSystemMetadataRepositoryMock();
+  const tagMock = newTagRepositoryMock();
+  const telemetryMock = newTelemetryRepositoryMock();
+  const trashMock = newTrashRepositoryMock();
+  const userMock = newUserRepositoryMock();
+  const versionHistoryMock = newVersionHistoryRepositoryMock();
+  const viewMock = newViewRepositoryMock();
+
+  const sut = new Service(
+    loggerMock,
+    accessMock,
+    activityMock,
+    auditMock,
+    albumMock,
+    albumUserMock,
+    assetMock,
+    configMock,
+    cronMock,
+    cryptoMock,
+    databaseMock,
+    eventMock,
+    jobMock,
+    keyMock,
+    libraryMock,
+    machineLearningMock,
+    mapMock,
+    mediaMock,
+    memoryMock,
+    metadataMock,
+    moveMock,
+    notificationMock,
+    oauthMock,
+    partnerMock,
+    personMock,
+    processMock,
+    searchMock,
+    serverInfoMock,
+    sessionMock,
+    sharedLinkMock,
+    stackMock,
+    storageMock,
+    systemMock,
+    tagMock,
+    telemetryMock,
+    trashMock,
+    userMock,
+    versionHistoryMock,
+    viewMock,
+  );
+
+  return {
+    sut,
+    accessMock,
+    loggerMock,
+    cronMock,
+    cryptoMock,
+    activityMock,
+    auditMock,
+    albumMock,
+    albumUserMock,
+    assetMock,
+    configMock,
+    databaseMock,
+    eventMock,
+    jobMock,
+    keyMock,
+    libraryMock,
+    machineLearningMock,
+    mapMock,
+    mediaMock,
+    memoryMock,
+    metadataMock,
+    moveMock,
+    notificationMock,
+    oauthMock,
+    partnerMock,
+    personMock,
+    processMock,
+    searchMock,
+    serverInfoMock,
+    sessionMock,
+    sharedLinkMock,
+    stackMock,
+    storageMock,
+    systemMock,
+    tagMock,
+    telemetryMock,
+    trashMock,
+    userMock,
+    versionHistoryMock,
+    viewMock,
+  };
+};
+
+const createPNG = (r: number, g: number, b: number) => {
+  const image = new PNG({ width: 1, height: 1 });
+  image.data[0] = r;
+  image.data[1] = g;
+  image.data[2] = b;
+  image.data[3] = 255;
+  return PNG.sync.write(image);
+};
+
+function* newPngFactory() {
+  for (let r = 0; r < 255; r++) {
+    for (let g = 0; g < 255; g++) {
+      for (let b = 0; b < 255; b++) {
+        yield createPNG(r, g, b);
+      }
+    }
+  }
+}
+
+const pngFactory = newPngFactory();
+
+export const newRandomImage = () => {
+  const { value } = pngFactory.next();
+  if (!value) {
+    throw new Error('Ran out of random asset data');
+  }
+
+  return value;
+};
+
+export const mockSpawn = vitest.fn((exitCode: number, stdout: string, stderr: string, error?: unknown) => {
+  return {
+    stdout: new Readable({
+      read() {
+        this.push(stdout); // write mock data to stdout
+        this.push(null); // end stream
+      },
+    }),
+    stderr: new Readable({
+      read() {
+        this.push(stderr); // write mock data to stderr
+        this.push(null); // end stream
+      },
+    }),
+    stdin: new Writable({
+      write(chunk, encoding, callback) {
+        callback();
+      },
+    }),
+    exitCode,
+    on: vitest.fn((event, callback: any) => {
+      if (event === 'close') {
+        callback(0);
+      }
+      if (event === 'error' && error) {
+        callback(error);
+      }
+      if (event === 'exit') {
+        callback(exitCode);
+      }
+    }),
+  } as unknown as ChildProcessWithoutNullStreams;
+});
diff --git a/server/vitest.config.medium.mjs b/server/vitest.config.medium.mjs
new file mode 100644
index 0000000000..40dad8d6a5
--- /dev/null
+++ b/server/vitest.config.medium.mjs
@@ -0,0 +1,17 @@
+import swc from 'unplugin-swc';
+import tsconfigPaths from 'vite-tsconfig-paths';
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  test: {
+    root: './',
+    globals: true,
+    include: ['test/medium/**/*.spec.ts'],
+    server: {
+      deps: {
+        fallbackCJS: true,
+      },
+    },
+  },
+  plugins: [swc.vite(), tsconfigPaths()],
+});
diff --git a/server/vitest.config.mjs b/server/vitest.config.mjs
index 3c0ea00c84..92fc027d40 100644
--- a/server/vitest.config.mjs
+++ b/server/vitest.config.mjs
@@ -6,13 +6,20 @@ export default defineConfig({
   test: {
     root: './',
     globals: true,
+    include: ['src/**/*.spec.ts'],
     coverage: {
       provider: 'v8',
       include: ['src/cores/**', 'src/interfaces/**', 'src/services/**', 'src/utils/**'],
+      exclude: [
+        'src/services/*.spec.ts',
+        'src/services/api.service.ts',
+        'src/services/microservices.service.ts',
+        'src/services/index.ts',
+      ],
       thresholds: {
-        lines: 80,
-        statements: 80,
-        branches: 85,
+        lines: 85,
+        statements: 85,
+        branches: 90,
         functions: 85,
       },
     },
diff --git a/web/.nvmrc b/web/.nvmrc
index 3516580bbb..1d9b7831ba 100644
--- a/web/.nvmrc
+++ b/web/.nvmrc
@@ -1 +1 @@
-20.17.0
+22.12.0
diff --git a/web/Dockerfile b/web/Dockerfile
index 19d8d890ab..bf6aa5af5c 100644
--- a/web/Dockerfile
+++ b/web/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:20.17.0-alpine3.20@sha256:2d07db07a2df6830718ae2a47db6fedce6745f5bcd174c398f2acdda90a11c03
+FROM node:22.12.0-alpine3.20@sha256:96cc8323e25c8cc6ddcb8b965e135cfd57846e8003ec0d7bcec16c5fd5f6d39f
 
 RUN apk add --no-cache tini
 USER node
diff --git a/web/README.md b/web/README.md
index e9693ceb01..603c7ad64e 100644
--- a/web/README.md
+++ b/web/README.md
@@ -2,4 +2,4 @@
 
 This project uses the [SvelteKit](https://kit.svelte.dev/) web framework. Please refer to [the SvelteKit docs](https://kit.svelte.dev/docs) for information on getting started as a contributor to this project. In particular, it will help you navigate the project's code if you understand the basics of [SvelteKit routing](https://kit.svelte.dev/docs/routing).
 
-When developing locally, you will run a SvelteKit Node.js server. When this project is deployed to production, it is built as a SPA and deployed as part of [../server](the server project).
+When developing locally, you will run a SvelteKit Node.js server. When this project is deployed to production, it is built as a SPA and deployed as part of [the server project](../server).
diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs
index f1ba46355f..f3cf9d7f10 100644
--- a/web/eslint.config.mjs
+++ b/web/eslint.config.mjs
@@ -33,6 +33,7 @@ export default [
       'eslint.config.mjs',
       'postcss.config.cjs',
       'tailwind.config.js',
+      'coverage',
     ],
   },
   ...compat.extends(
diff --git a/web/package-lock.json b/web/package-lock.json
index 09e1f49f12..799808fc67 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -1,31 +1,31 @@
 {
   "name": "immich-web",
-  "version": "1.115.0",
+  "version": "1.123.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "immich-web",
-      "version": "1.115.0",
+      "version": "1.123.0",
       "license": "GNU Affero General Public License version 3",
       "dependencies": {
         "@formatjs/icu-messageformat-parser": "^2.7.8",
         "@immich/sdk": "file:../open-api/typescript-sdk",
-        "@mapbox/mapbox-gl-rtl-text": "^0.2.3",
+        "@mapbox/mapbox-gl-rtl-text": "0.2.3",
         "@mdi/js": "^7.4.47",
         "@photo-sphere-viewer/core": "^5.7.1",
         "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
         "@photo-sphere-viewer/video-plugin": "^5.7.2",
-        "@zoom-image/svelte": "^0.2.6",
+        "@zoom-image/svelte": "^0.3.0",
         "dom-to-image": "^2.6.0",
         "handlebars": "^4.7.8",
         "intl-messageformat": "^10.5.14",
         "justified-layout": "^4.1.0",
         "lodash-es": "^4.17.21",
         "luxon": "^3.4.4",
-        "socket.io-client": "^4.7.4",
+        "socket.io-client": "~4.7.5",
         "svelte-gestures": "^5.0.4",
-        "svelte-i18n": "^4.0.0",
+        "svelte-i18n": "^4.0.1",
         "svelte-local-storage-store": "^0.6.4",
         "svelte-maplibre": "^0.9.13",
         "thumbhash": "^0.1.1"
@@ -35,26 +35,26 @@
         "@eslint/js": "^9.8.0",
         "@faker-js/faker": "^9.0.0",
         "@socket.io/component-emitter": "^3.1.0",
-        "@sveltejs/adapter-static": "^3.0.1",
-        "@sveltejs/enhanced-img": "^0.3.0",
-        "@sveltejs/kit": "^2.5.18",
-        "@sveltejs/vite-plugin-svelte": "^3.1.2",
+        "@sveltejs/adapter-static": "^3.0.5",
+        "@sveltejs/enhanced-img": "^0.4.0",
+        "@sveltejs/kit": "^2.12.0",
+        "@sveltejs/vite-plugin-svelte": "^4.0.0",
         "@testing-library/jest-dom": "^6.4.2",
-        "@testing-library/svelte": "^5.2.0",
+        "@testing-library/svelte": "^5.2.4",
         "@testing-library/user-event": "^14.5.2",
         "@types/dom-to-image": "^2.6.7",
         "@types/justified-layout": "^4.1.4",
         "@types/lodash-es": "^4.17.12",
         "@types/luxon": "^3.4.2",
-        "@typescript-eslint/eslint-plugin": "^8.0.0",
-        "@typescript-eslint/parser": "^8.0.0",
+        "@typescript-eslint/eslint-plugin": "^8.15.0",
+        "@typescript-eslint/parser": "^8.15.0",
         "@vitest/coverage-v8": "^2.0.5",
         "autoprefixer": "^10.4.17",
         "dotenv": "^16.4.5",
-        "eslint": "^9.0.0",
+        "eslint": "^9.14.0",
         "eslint-config-prettier": "^9.1.0",
-        "eslint-plugin-svelte": "^2.43.0",
-        "eslint-plugin-unicorn": "^55.0.0",
+        "eslint-plugin-svelte": "^2.45.1",
+        "eslint-plugin-unicorn": "^56.0.1",
         "factory.ts": "^1.4.1",
         "globals": "^15.9.0",
         "postcss": "^8.4.35",
@@ -63,24 +63,24 @@
         "prettier-plugin-sort-json": "^4.0.0",
         "prettier-plugin-svelte": "^3.2.6",
         "rollup-plugin-visualizer": "^5.12.0",
-        "svelte": "^4.2.19",
-        "svelte-check": "^4.0.0",
+        "svelte": "^5.1.5",
+        "svelte-check": "^4.0.9",
         "tailwindcss": "^3.4.1",
         "tslib": "^2.6.2",
         "typescript": "^5.5.0",
-        "vite": "^5.1.4",
+        "vite": "^5.4.4",
         "vitest": "^2.0.5"
       }
     },
     "../open-api/typescript-sdk": {
       "name": "@immich/sdk",
-      "version": "1.115.0",
+      "version": "1.123.0",
       "license": "GNU Affero General Public License version 3",
       "dependencies": {
         "@oazapfts/runtime": "^1.0.2"
       },
       "devDependencies": {
-        "@types/node": "^20.16.5",
+        "@types/node": "^22.10.2",
         "typescript": "^5.3.3"
       }
     },
@@ -138,19 +138,21 @@
       }
     },
     "node_modules/@babel/helper-string-parser": {
-      "version": "7.24.8",
-      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
-      "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
+      "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-validator-identifier": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
-      "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+      "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
       }
@@ -170,12 +172,13 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.25.3",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
-      "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
+      "version": "7.26.2",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz",
+      "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/types": "^7.25.2"
+        "@babel/types": "^7.26.0"
       },
       "bin": {
         "parser": "bin/babel-parser.js"
@@ -197,14 +200,14 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.25.2",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
-      "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
+      "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/helper-string-parser": "^7.24.8",
-        "@babel/helper-validator-identifier": "^7.24.7",
-        "to-fast-properties": "^2.0.0"
+        "@babel/helper-string-parser": "^7.25.9",
+        "@babel/helper-validator-identifier": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -633,9 +636,9 @@
       }
     },
     "node_modules/@eslint-community/regexpp": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz",
-      "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==",
+      "version": "4.12.1",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+      "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -657,10 +660,20 @@
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       }
     },
+    "node_modules/@eslint/core": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz",
+      "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
     "node_modules/@eslint/eslintrc": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
-      "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
+      "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -682,9 +695,9 @@
       }
     },
     "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
-      "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
       "dev": true,
       "license": "Apache-2.0",
       "engines": {
@@ -695,15 +708,15 @@
       }
     },
     "node_modules/@eslint/eslintrc/node_modules/espree": {
-      "version": "10.1.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz",
-      "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==",
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
+      "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
-        "acorn": "^8.12.0",
+        "acorn": "^8.14.0",
         "acorn-jsx": "^5.3.2",
-        "eslint-visitor-keys": "^4.0.0"
+        "eslint-visitor-keys": "^4.2.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -726,9 +739,9 @@
       }
     },
     "node_modules/@eslint/js": {
-      "version": "9.9.1",
-      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz",
-      "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==",
+      "version": "9.15.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz",
+      "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -745,10 +758,23 @@
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       }
     },
+    "node_modules/@eslint/plugin-kit": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz",
+      "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "levn": "^0.4.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
     "node_modules/@faker-js/faker": {
-      "version": "9.0.0",
-      "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.0.tgz",
-      "integrity": "sha512-dTDHJSmz6c1OJ6HO7jiUiIb4sB20Dlkb3pxYsKm0qTXm2Bmj97rlXIhlvaFsW2rvCi+OLlwKLVSS6ZxFUVZvjQ==",
+      "version": "9.2.0",
+      "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.2.0.tgz",
+      "integrity": "sha512-ulqQu4KMr1/sTFIYvqSdegHT8NIkt66tFAkugGnHA+1WAfEn6hMzNR+svjXGFRVLnapxvej67Z/LwchFrnLBUg==",
       "dev": true,
       "funding": [
         {
@@ -763,47 +789,77 @@
       }
     },
     "node_modules/@formatjs/ecma402-abstract": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz",
-      "integrity": "sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g==",
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.3.tgz",
+      "integrity": "sha512-aElGmleuReGnk2wtYOzYFmNWYoiWWmf1pPPCYg0oiIQSJj0mjc4eUfzUXaSOJ4S8WzI/cLqnCTWjqz904FT2OQ==",
+      "license": "MIT",
       "dependencies": {
-        "@formatjs/intl-localematcher": "0.5.4",
-        "tslib": "^2.4.0"
+        "@formatjs/fast-memoize": "2.2.3",
+        "@formatjs/intl-localematcher": "0.5.7",
+        "tslib": "2"
       }
     },
     "node_modules/@formatjs/fast-memoize": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz",
-      "integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==",
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.3.tgz",
+      "integrity": "sha512-3jeJ+HyOfu8osl3GNSL4vVHUuWFXR03Iz9jjgI7RwjG6ysu/Ymdr0JRCPHfF5yGbTE6JCrd63EpvX1/WybYRbA==",
+      "license": "MIT",
       "dependencies": {
-        "tslib": "^2.4.0"
+        "tslib": "2"
       }
     },
     "node_modules/@formatjs/icu-messageformat-parser": {
-      "version": "2.7.8",
-      "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.8.tgz",
-      "integrity": "sha512-nBZJYmhpcSX0WeJ5SDYUkZ42AgR3xiyhNCsQweFx3cz/ULJjym8bHAzWKvG5e2+1XO98dBYC0fWeeAECAVSwLA==",
+      "version": "2.9.3",
+      "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.3.tgz",
+      "integrity": "sha512-9L99QsH14XjOCIp4TmbT8wxuffJxGK8uLNO1zNhLtcZaVXvv626N0s4A2qgRCKG3dfYWx9psvGlFmvyVBa6u/w==",
+      "license": "MIT",
       "dependencies": {
-        "@formatjs/ecma402-abstract": "2.0.0",
-        "@formatjs/icu-skeleton-parser": "1.8.2",
-        "tslib": "^2.4.0"
+        "@formatjs/ecma402-abstract": "2.2.3",
+        "@formatjs/icu-skeleton-parser": "1.8.7",
+        "tslib": "2"
       }
     },
     "node_modules/@formatjs/icu-skeleton-parser": {
-      "version": "1.8.2",
-      "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.2.tgz",
-      "integrity": "sha512-k4ERKgw7aKGWJZgTarIcNEmvyTVD9FYh0mTrrBMHZ1b8hUu6iOJ4SzsZlo3UNAvHYa+PnvntIwRPt1/vy4nA9Q==",
+      "version": "1.8.7",
+      "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.7.tgz",
+      "integrity": "sha512-fI+6SmS2g7h3srfAKSWa5dwreU5zNEfon2uFo99OToiLF6yxGE+WikvFSbsvMAYkscucvVmTYNlWlaDPp0n5HA==",
+      "license": "MIT",
       "dependencies": {
-        "@formatjs/ecma402-abstract": "2.0.0",
-        "tslib": "^2.4.0"
+        "@formatjs/ecma402-abstract": "2.2.3",
+        "tslib": "2"
       }
     },
     "node_modules/@formatjs/intl-localematcher": {
-      "version": "0.5.4",
-      "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz",
-      "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==",
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.7.tgz",
+      "integrity": "sha512-GGFtfHGQVFe/niOZp24Kal5b2i36eE2bNL0xi9Sg/yd0TR8aLjcteApZdHmismP5QQax1cMnZM9yWySUUjJteA==",
+      "license": "MIT",
       "dependencies": {
-        "tslib": "^2.4.0"
+        "tslib": "2"
+      }
+    },
+    "node_modules/@humanfs/core": {
+      "version": "0.19.1",
+      "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+      "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanfs/node": {
+      "version": "0.16.6",
+      "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
+      "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@humanfs/core": "^0.19.1",
+        "@humanwhocodes/retry": "^0.3.0"
+      },
+      "engines": {
+        "node": ">=18.18.0"
       }
     },
     "node_modules/@humanwhocodes/module-importer": {
@@ -820,9 +876,9 @@
       }
     },
     "node_modules/@humanwhocodes/retry": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz",
-      "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==",
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+      "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
       "dev": true,
       "license": "Apache-2.0",
       "engines": {
@@ -1422,9 +1478,10 @@
       }
     },
     "node_modules/@jridgewell/sourcemap-codec": {
-      "version": "1.4.15",
-      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
-      "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+      "license": "MIT"
     },
     "node_modules/@jridgewell/trace-mapping": {
       "version": "0.3.25",
@@ -1539,6 +1596,7 @@
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/@namnode/store/-/store-0.1.0.tgz",
       "integrity": "sha512-4NGTldxKcmY0UuZ7OEkvCjs8ZEoeYB6M2UwMu74pdLiFMKxXbj9HdNk1Qn213bxX1O7bY5h+PLh5DZsTURZkYA==",
+      "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/willnguyen1312"
@@ -1580,30 +1638,30 @@
       }
     },
     "node_modules/@photo-sphere-viewer/core": {
-      "version": "5.9.0",
-      "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.9.0.tgz",
-      "integrity": "sha512-Th8S2SbKpKEE5l150Mh0Na+3RirceJL9ioRl+33kE59s0Dx675snGWI7gy/xFKEWsdYOhj9f6xNWZ8MSqs8RhQ==",
+      "version": "5.11.1",
+      "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.11.1.tgz",
+      "integrity": "sha512-bxWnoQGYjXfmHGee4OSkoYLZmdgqvJWMn7wmpK0V0Vf46Fqu+TJ4Yt8+dY2PgpM89HoKzNr15Dzt6jqOfjkFxQ==",
       "license": "MIT",
       "dependencies": {
-        "three": "^0.167.0"
+        "three": "^0.169.0"
       }
     },
     "node_modules/@photo-sphere-viewer/equirectangular-video-adapter": {
-      "version": "5.9.0",
-      "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.9.0.tgz",
-      "integrity": "sha512-mQPnuKQPQvtNKMtjY8M3b6ANupA7soSDDLL/R8igtlP9vGMPgbVzPmGbrkyq6Ed2bQr+u8j2LkT38ztZ70Ingg==",
+      "version": "5.11.1",
+      "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.11.1.tgz",
+      "integrity": "sha512-fkWuVeArtZSWd0z282/J82YSc+oernQaE/cpo0soVaStaNbS1V35iSnPlaBKw40qX6tucJWYw15QwM8xgPC2IQ==",
       "license": "MIT",
       "peerDependencies": {
-        "@photo-sphere-viewer/core": "5.9.0"
+        "@photo-sphere-viewer/core": "5.11.1"
       }
     },
     "node_modules/@photo-sphere-viewer/video-plugin": {
-      "version": "5.9.0",
-      "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.9.0.tgz",
-      "integrity": "sha512-u1li4KEO7iRMhlLWZsn55Jprb8LdSyFbisvHvk75wcSLGZIZj24vabogPrDtdiXuELaC1DTD6En9IpVD/H+mGQ==",
+      "version": "5.11.1",
+      "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.11.1.tgz",
+      "integrity": "sha512-02spWwv9bjyI6inNdZsczX/qdMICVV9B8lWX/J4iNBaiUCHqPKmk8CeZbRyC/Uh3OHSusSJHyW0FDEOf6qjjww==",
       "license": "MIT",
       "peerDependencies": {
-        "@photo-sphere-viewer/core": "5.9.0"
+        "@photo-sphere-viewer/core": "5.11.1"
       }
     },
     "node_modules/@pkgjs/parseargs": {
@@ -1651,9 +1709,9 @@
       "dev": true
     },
     "node_modules/@rollup/rollup-android-arm-eabi": {
-      "version": "4.21.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.1.tgz",
-      "integrity": "sha512-2thheikVEuU7ZxFXubPDOtspKn1x0yqaYQwvALVtEcvFhMifPADBrgRPyHV0TF3b+9BgvgjgagVyvA/UqPZHmg==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz",
+      "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==",
       "cpu": [
         "arm"
       ],
@@ -1665,9 +1723,9 @@
       ]
     },
     "node_modules/@rollup/rollup-android-arm64": {
-      "version": "4.21.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.1.tgz",
-      "integrity": "sha512-t1lLYn4V9WgnIFHXy1d2Di/7gyzBWS8G5pQSXdZqfrdCGTwi1VasRMSS81DTYb+avDs/Zz4A6dzERki5oRYz1g==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz",
+      "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==",
       "cpu": [
         "arm64"
       ],
@@ -1679,9 +1737,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-arm64": {
-      "version": "4.21.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.1.tgz",
-      "integrity": "sha512-AH/wNWSEEHvs6t4iJ3RANxW5ZCK3fUnmf0gyMxWCesY1AlUj8jY7GC+rQE4wd3gwmZ9XDOpL0kcFnCjtN7FXlA==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz",
+      "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==",
       "cpu": [
         "arm64"
       ],
@@ -1693,9 +1751,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-x64": {
-      "version": "4.21.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.1.tgz",
-      "integrity": "sha512-dO0BIz/+5ZdkLZrVgQrDdW7m2RkrLwYTh2YMFG9IpBtlC1x1NPNSXkfczhZieOlOLEqgXOFH3wYHB7PmBtf+Bg==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz",
+      "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==",
       "cpu": [
         "x64"
       ],
@@ -1706,10 +1764,38 @@
         "darwin"
       ]
     },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz",
+      "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz",
+      "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
     "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-      "version": "4.21.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.1.tgz",
-      "integrity": "sha512-sWWgdQ1fq+XKrlda8PsMCfut8caFwZBmhYeoehJ05FdI0YZXk6ZyUjWLrIgbR/VgiGycrFKMMgp7eJ69HOF2pQ==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz",
+      "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==",
       "cpu": [
         "arm"
       ],
@@ -1721,9 +1807,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-musleabihf": {
-      "version": "4.21.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.1.tgz",
-      "integrity": "sha512-9OIiSuj5EsYQlmwhmFRA0LRO0dRRjdCVZA3hnmZe1rEwRk11Jy3ECGGq3a7RrVEZ0/pCsYWx8jG3IvcrJ6RCew==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz",
+      "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==",
       "cpu": [
         "arm"
       ],
@@ -1735,9 +1821,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-gnu": {
-      "version": "4.21.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.1.tgz",
-      "integrity": "sha512-0kuAkRK4MeIUbzQYu63NrJmfoUVicajoRAL1bpwdYIYRcs57iyIV9NLcuyDyDXE2GiZCL4uhKSYAnyWpjZkWow==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz",
+      "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==",
       "cpu": [
         "arm64"
       ],
@@ -1749,9 +1835,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-musl": {
-      "version": "4.21.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.1.tgz",
-      "integrity": "sha512-/6dYC9fZtfEY0vozpc5bx1RP4VrtEOhNQGb0HwvYNwXD1BBbwQ5cKIbUVVU7G2d5WRE90NfB922elN8ASXAJEA==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz",
+      "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==",
       "cpu": [
         "arm64"
       ],
@@ -1762,10 +1848,24 @@
         "linux"
       ]
     },
+    "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz",
+      "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
     "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
-      "version": "4.21.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.1.tgz",
-      "integrity": "sha512-ltUWy+sHeAh3YZ91NUsV4Xg3uBXAlscQe8ZOXRCVAKLsivGuJsrkawYPUEyCV3DYa9urgJugMLn8Z3Z/6CeyRQ==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz",
+      "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==",
       "cpu": [
         "ppc64"
       ],
@@ -1777,9 +1877,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-riscv64-gnu": {
-      "version": "4.21.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.1.tgz",
-      "integrity": "sha512-BggMndzI7Tlv4/abrgLwa/dxNEMn2gC61DCLrTzw8LkpSKel4o+O+gtjbnkevZ18SKkeN3ihRGPuBxjaetWzWg==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz",
+      "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==",
       "cpu": [
         "riscv64"
       ],
@@ -1791,9 +1891,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-s390x-gnu": {
-      "version": "4.21.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.1.tgz",
-      "integrity": "sha512-z/9rtlGd/OMv+gb1mNSjElasMf9yXusAxnRDrBaYB+eS1shFm6/4/xDH1SAISO5729fFKUkJ88TkGPRUh8WSAA==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz",
+      "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==",
       "cpu": [
         "s390x"
       ],
@@ -1805,9 +1905,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-gnu": {
-      "version": "4.21.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.1.tgz",
-      "integrity": "sha512-kXQVcWqDcDKw0S2E0TmhlTLlUgAmMVqPrJZR+KpH/1ZaZhLSl23GZpQVmawBQGVhyP5WXIsIQ/zqbDBBYmxm5w==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz",
+      "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==",
       "cpu": [
         "x64"
       ],
@@ -1819,9 +1919,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-musl": {
-      "version": "4.21.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.1.tgz",
-      "integrity": "sha512-CbFv/WMQsSdl+bpX6rVbzR4kAjSSBuDgCqb1l4J68UYsQNalz5wOqLGYj4ZI0thGpyX5kc+LLZ9CL+kpqDovZA==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz",
+      "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==",
       "cpu": [
         "x64"
       ],
@@ -1833,9 +1933,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-arm64-msvc": {
-      "version": "4.21.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.1.tgz",
-      "integrity": "sha512-3Q3brDgA86gHXWHklrwdREKIrIbxC0ZgU8lwpj0eEKGBQH+31uPqr0P2v11pn0tSIxHvcdOWxa4j+YvLNx1i6g==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz",
+      "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==",
       "cpu": [
         "arm64"
       ],
@@ -1847,9 +1947,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-ia32-msvc": {
-      "version": "4.21.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.1.tgz",
-      "integrity": "sha512-tNg+jJcKR3Uwe4L0/wY3Ro0H+u3nrb04+tcq1GSYzBEmKLeOQF2emk1whxlzNqb6MMrQ2JOcQEpuuiPLyRcSIw==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz",
+      "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==",
       "cpu": [
         "ia32"
       ],
@@ -1861,9 +1961,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-x64-msvc": {
-      "version": "4.21.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.1.tgz",
-      "integrity": "sha512-xGiIH95H1zU7naUyTKEyOA/I0aexNMUdO9qRv0bLKN3qu25bBdrxZHqA3PTJ24YNN/GdMzG4xkDcd/GvjuhfLg==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz",
+      "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==",
       "cpu": [
         "x64"
       ],
@@ -1880,9 +1980,9 @@
       "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
     },
     "node_modules/@sveltejs/adapter-static": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.4.tgz",
-      "integrity": "sha512-Qm4GAHCnRXwfWG9/AtnQ7mqjyjTs7i0Opyb8H2KH9rMR7fLxqiPx/tXeoE6HHo66+72CjyOb4nFH3lrejY4vzA==",
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.6.tgz",
+      "integrity": "sha512-MGJcesnJWj7FxDcB/GbrdYD3q24Uk0PIL4QIX149ku+hlJuj//nxUbb0HxUTpjkecWfHjVveSUnUaQWnPRXlpg==",
       "dev": true,
       "license": "MIT",
       "peerDependencies": {
@@ -1890,40 +1990,41 @@
       }
     },
     "node_modules/@sveltejs/enhanced-img": {
-      "version": "0.3.4",
-      "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.3.4.tgz",
-      "integrity": "sha512-eX+ob5uWr0bTLMKeG9nhhM84aR88hqiLiyEfWZPX7ijhk/wlmYSUX9nOiaVHh2ct1U+Ju9Hhb90Copw+ZNOB8w==",
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.4.1.tgz",
+      "integrity": "sha512-Z0xwQWM7tfdlNYuaFsAsbjEosEZb961yP7hlvZBLlh3+Rv4tI3BboD6bUkmInj+cC66p/5rybgvEtxX5LILSuw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "magic-string": "^0.30.5",
-        "svelte-parse-markup": "^0.1.2",
-        "vite-imagetools": "^7.0.1"
+        "svelte-parse-markup": "^0.1.5",
+        "vite-imagetools": "^7.0.1",
+        "zimmerframe": "^1.1.2"
       },
       "peerDependencies": {
-        "svelte": "^4.0.0 || ^5.0.0-next.0",
+        "svelte": "^5.0.0",
         "vite": ">= 5.0.0"
       }
     },
     "node_modules/@sveltejs/kit": {
-      "version": "2.5.25",
-      "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.25.tgz",
-      "integrity": "sha512-5hBSEN8XEjDZ5+2bHkFh8Z0QyOk0C187cyb12aANe1c8aeKbfu5ZD5XaC2vEH4h0alJFDXPdUkXQBmeeXeMr1A==",
+      "version": "2.13.0",
+      "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.13.0.tgz",
+      "integrity": "sha512-6t6ne00vZx/TjD6s0Jvwt8wRLKBwbSAN1nhlOzcLUSTYX1hTp4eCBaTPB5Yz/lu+tYcvz4YPEEuPv3yfsNp2gw==",
       "dev": true,
       "hasInstallScript": true,
       "license": "MIT",
       "dependencies": {
         "@types/cookie": "^0.6.0",
         "cookie": "^0.6.0",
-        "devalue": "^5.0.0",
-        "esm-env": "^1.0.0",
+        "devalue": "^5.1.0",
+        "esm-env": "^1.2.1",
         "import-meta-resolve": "^4.1.0",
         "kleur": "^4.1.5",
         "magic-string": "^0.30.5",
         "mrmime": "^2.0.0",
         "sade": "^1.8.1",
         "set-cookie-parser": "^2.6.0",
-        "sirv": "^2.0.4",
+        "sirv": "^3.0.0",
         "tiny-glob": "^0.2.9"
       },
       "bin": {
@@ -1933,49 +2034,48 @@
         "node": ">=18.13"
       },
       "peerDependencies": {
-        "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1",
+        "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0",
         "svelte": "^4.0.0 || ^5.0.0-next.0",
-        "vite": "^5.0.3"
+        "vite": "^5.0.3 || ^6.0.0"
       }
     },
     "node_modules/@sveltejs/vite-plugin-svelte": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.2.tgz",
-      "integrity": "sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==",
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.1.tgz",
+      "integrity": "sha512-prXoAE/GleD2C4pKgHa9vkdjpzdYwCSw/kmjw6adIyu0vk5YKCfqIztkLg10m+kOYnzZu3bb0NaPTxlWre2a9Q==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@sveltejs/vite-plugin-svelte-inspector": "^2.1.0",
-        "debug": "^4.3.4",
+        "@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0",
+        "debug": "^4.3.7",
         "deepmerge": "^4.3.1",
         "kleur": "^4.1.5",
-        "magic-string": "^0.30.10",
-        "svelte-hmr": "^0.16.0",
-        "vitefu": "^0.2.5"
+        "magic-string": "^0.30.12",
+        "vitefu": "^1.0.3"
       },
       "engines": {
-        "node": "^18.0.0 || >=20"
+        "node": "^18.0.0 || ^20.0.0 || >=22"
       },
       "peerDependencies": {
-        "svelte": "^4.0.0 || ^5.0.0-next.0",
+        "svelte": "^5.0.0-next.96 || ^5.0.0",
         "vite": "^5.0.0"
       }
     },
     "node_modules/@sveltejs/vite-plugin-svelte-inspector": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz",
-      "integrity": "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==",
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz",
+      "integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "debug": "^4.3.4"
+        "debug": "^4.3.7"
       },
       "engines": {
-        "node": "^18.0.0 || >=20"
+        "node": "^18.0.0 || ^20.0.0 || >=22"
       },
       "peerDependencies": {
-        "@sveltejs/vite-plugin-svelte": "^3.0.0",
-        "svelte": "^4.0.0 || ^5.0.0-next.0",
+        "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0",
+        "svelte": "^5.0.0-next.96 || ^5.0.0",
         "vite": "^5.0.0"
       }
     },
@@ -2070,9 +2170,9 @@
       }
     },
     "node_modules/@testing-library/jest-dom": {
-      "version": "6.5.0",
-      "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz",
-      "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==",
+      "version": "6.6.3",
+      "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz",
+      "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -2171,11 +2271,10 @@
       }
     },
     "node_modules/@testing-library/svelte": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-5.2.1.tgz",
-      "integrity": "sha512-yXSqBsYaQAeP2xt7gqKu135Q67+NTsBDcpL1akv5MVAQ/amb7AQ0zW5nzrquTIE2lvrc6q58KZhQA61Vc05ZOg==",
+      "version": "5.2.4",
+      "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-5.2.4.tgz",
+      "integrity": "sha512-EFdy73+lULQgMJ1WolAymrxWWrPv9DWyDuDFKKlUip2PA/EXuHptzfYOKWljccFWDKhhGOu3dqNmoc2f/h/Ecg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "@testing-library/dom": "^10.0.0"
       },
@@ -2228,9 +2327,10 @@
       "dev": true
     },
     "node_modules/@types/estree": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
-      "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+      "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+      "license": "MIT"
     },
     "node_modules/@types/geojson": {
       "version": "7946.0.14",
@@ -2245,6 +2345,13 @@
         "@types/geojson": "*"
       }
     },
+    "node_modules/@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/justified-layout": {
       "version": "4.1.4",
       "resolved": "https://registry.npmjs.org/@types/justified-layout/-/justified-layout-4.1.4.tgz",
@@ -2323,17 +2430,17 @@
       }
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz",
-      "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz",
+      "integrity": "sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/regexpp": "^4.10.0",
-        "@typescript-eslint/scope-manager": "8.3.0",
-        "@typescript-eslint/type-utils": "8.3.0",
-        "@typescript-eslint/utils": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0",
+        "@typescript-eslint/scope-manager": "8.15.0",
+        "@typescript-eslint/type-utils": "8.15.0",
+        "@typescript-eslint/utils": "8.15.0",
+        "@typescript-eslint/visitor-keys": "8.15.0",
         "graphemer": "^1.4.0",
         "ignore": "^5.3.1",
         "natural-compare": "^1.4.0",
@@ -2357,16 +2464,16 @@
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz",
-      "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz",
+      "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
-        "@typescript-eslint/scope-manager": "8.3.0",
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/typescript-estree": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0",
+        "@typescript-eslint/scope-manager": "8.15.0",
+        "@typescript-eslint/types": "8.15.0",
+        "@typescript-eslint/typescript-estree": "8.15.0",
+        "@typescript-eslint/visitor-keys": "8.15.0",
         "debug": "^4.3.4"
       },
       "engines": {
@@ -2386,14 +2493,14 @@
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz",
-      "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz",
+      "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0"
+        "@typescript-eslint/types": "8.15.0",
+        "@typescript-eslint/visitor-keys": "8.15.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2404,14 +2511,14 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz",
-      "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz",
+      "integrity": "sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "8.3.0",
-        "@typescript-eslint/utils": "8.3.0",
+        "@typescript-eslint/typescript-estree": "8.15.0",
+        "@typescript-eslint/utils": "8.15.0",
         "debug": "^4.3.4",
         "ts-api-utils": "^1.3.0"
       },
@@ -2422,6 +2529,9 @@
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
       },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0"
+      },
       "peerDependenciesMeta": {
         "typescript": {
           "optional": true
@@ -2429,9 +2539,9 @@
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz",
-      "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz",
+      "integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -2443,14 +2553,14 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz",
-      "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz",
+      "integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/visitor-keys": "8.3.0",
+        "@typescript-eslint/types": "8.15.0",
+        "@typescript-eslint/visitor-keys": "8.15.0",
         "debug": "^4.3.4",
         "fast-glob": "^3.3.2",
         "is-glob": "^4.0.3",
@@ -2497,30 +2607,17 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
-    "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
-      "version": "7.6.3",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
-      "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
-      "dev": true,
-      "license": "ISC",
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
     "node_modules/@typescript-eslint/utils": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz",
-      "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.15.0.tgz",
+      "integrity": "sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
-        "@typescript-eslint/scope-manager": "8.3.0",
-        "@typescript-eslint/types": "8.3.0",
-        "@typescript-eslint/typescript-estree": "8.3.0"
+        "@typescript-eslint/scope-manager": "8.15.0",
+        "@typescript-eslint/types": "8.15.0",
+        "@typescript-eslint/typescript-estree": "8.15.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2531,17 +2628,22 @@
       },
       "peerDependencies": {
         "eslint": "^8.57.0 || ^9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz",
-      "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz",
+      "integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.3.0",
-        "eslint-visitor-keys": "^3.4.3"
+        "@typescript-eslint/types": "8.15.0",
+        "eslint-visitor-keys": "^4.2.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2551,22 +2653,36 @@
         "url": "https://opencollective.com/typescript-eslint"
       }
     },
-    "node_modules/@vitest/coverage-v8": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz",
-      "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==",
+    "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
       "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@vitest/coverage-v8": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.5.tgz",
+      "integrity": "sha512-/RoopB7XGW7UEkUndRXF87A9CwkoZAJW01pj8/3pgmDVsjMH2IKy6H1A38po9tmUlwhSyYs0az82rbKd9Yaynw==",
+      "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@ampproject/remapping": "^2.3.0",
         "@bcoe/v8-coverage": "^0.2.3",
-        "debug": "^4.3.5",
+        "debug": "^4.3.7",
         "istanbul-lib-coverage": "^3.2.2",
         "istanbul-lib-report": "^3.0.1",
         "istanbul-lib-source-maps": "^5.0.6",
         "istanbul-reports": "^3.1.7",
-        "magic-string": "^0.30.10",
-        "magicast": "^0.3.4",
-        "std-env": "^3.7.0",
+        "magic-string": "^0.30.12",
+        "magicast": "^0.3.5",
+        "std-env": "^3.8.0",
         "test-exclude": "^7.0.1",
         "tinyrainbow": "^1.2.0"
       },
@@ -2574,29 +2690,64 @@
         "url": "https://opencollective.com/vitest"
       },
       "peerDependencies": {
-        "vitest": "2.0.5"
+        "@vitest/browser": "2.1.5",
+        "vitest": "2.1.5"
+      },
+      "peerDependenciesMeta": {
+        "@vitest/browser": {
+          "optional": true
+        }
       }
     },
     "node_modules/@vitest/expect": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz",
-      "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.5.tgz",
+      "integrity": "sha512-nZSBTW1XIdpZvEJyoP/Sy8fUg0b8od7ZpGDkTUcfJ7wz/VoZAFzFfLyxVxGFhUjJzhYqSbIpfMtl/+k/dpWa3Q==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@vitest/spy": "2.0.5",
-        "@vitest/utils": "2.0.5",
-        "chai": "^5.1.1",
+        "@vitest/spy": "2.1.5",
+        "@vitest/utils": "2.1.5",
+        "chai": "^5.1.2",
         "tinyrainbow": "^1.2.0"
       },
       "funding": {
         "url": "https://opencollective.com/vitest"
       }
     },
-    "node_modules/@vitest/pretty-format": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz",
-      "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==",
+    "node_modules/@vitest/mocker": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.5.tgz",
+      "integrity": "sha512-XYW6l3UuBmitWqSUXTNXcVBUCRytDogBsWuNXQijc00dtnU/9OqpXWp4OJroVrad/gLIomAq9aW8yWDBtMthhQ==",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/spy": "2.1.5",
+        "estree-walker": "^3.0.3",
+        "magic-string": "^0.30.12"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "msw": "^2.4.9",
+        "vite": "^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "msw": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vitest/pretty-format": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.5.tgz",
+      "integrity": "sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw==",
+      "dev": true,
+      "license": "MIT",
       "dependencies": {
         "tinyrainbow": "^1.2.0"
       },
@@ -2605,12 +2756,13 @@
       }
     },
     "node_modules/@vitest/runner": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz",
-      "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.5.tgz",
+      "integrity": "sha512-pKHKy3uaUdh7X6p1pxOkgkVAFW7r2I818vHDthYLvUyjRfkKOU6P45PztOch4DZarWQne+VOaIMwA/erSSpB9g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@vitest/utils": "2.0.5",
+        "@vitest/utils": "2.1.5",
         "pathe": "^1.1.2"
       },
       "funding": {
@@ -2618,13 +2770,14 @@
       }
     },
     "node_modules/@vitest/snapshot": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz",
-      "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.5.tgz",
+      "integrity": "sha512-zmYw47mhfdfnYbuhkQvkkzYroXUumrwWDGlMjpdUr4jBd3HZiV2w7CQHj+z7AAS4VOtWxI4Zt4bWt4/sKcoIjg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@vitest/pretty-format": "2.0.5",
-        "magic-string": "^0.30.10",
+        "@vitest/pretty-format": "2.1.5",
+        "magic-string": "^0.30.12",
         "pathe": "^1.1.2"
       },
       "funding": {
@@ -2632,26 +2785,27 @@
       }
     },
     "node_modules/@vitest/spy": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz",
-      "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.5.tgz",
+      "integrity": "sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "tinyspy": "^3.0.0"
+        "tinyspy": "^3.0.2"
       },
       "funding": {
         "url": "https://opencollective.com/vitest"
       }
     },
     "node_modules/@vitest/utils": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz",
-      "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.5.tgz",
+      "integrity": "sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@vitest/pretty-format": "2.0.5",
-        "estree-walker": "^3.0.3",
-        "loupe": "^3.1.1",
+        "@vitest/pretty-format": "2.1.5",
+        "loupe": "^3.1.2",
         "tinyrainbow": "^1.2.0"
       },
       "funding": {
@@ -2659,9 +2813,9 @@
       }
     },
     "node_modules/@zoom-image/core": {
-      "version": "0.36.2",
-      "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.36.2.tgz",
-      "integrity": "sha512-NtqIA82xJUtTS8RMey3VUGF/q1tjkFZZUAR6edGdtiy43xIV7a239uuxomuU94WBkBtFztXL/ieyxxL8iPiyFg==",
+      "version": "0.39.0",
+      "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.39.0.tgz",
+      "integrity": "sha512-JD6UghIOvfdRdI5FCFQRtvaJGht2gIpkzFp+5NrcwKXbHQwSfl00VQ9JQ0TYbaeHa6tc+dxgepYgJukCtrPVgg==",
       "license": "MIT",
       "dependencies": {
         "@namnode/store": "^0.1.0"
@@ -2672,25 +2826,25 @@
       }
     },
     "node_modules/@zoom-image/svelte": {
-      "version": "0.2.19",
-      "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.19.tgz",
-      "integrity": "sha512-mnQ8eEmUkGi/UolaReQJmEsQu7DmX+8Y+5cdcS6nHmIM/LZImClB53/AySjJym+y5ZbDLUOOc7phgijTkPYz9g==",
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.3.0.tgz",
+      "integrity": "sha512-0dfAPgpGRm+j6d3fn044swV7r821l2ZFJZmR0WqUATUUaPZ3GbDkDyrVuZGmP7s4QAk/Nvs1F3+cBhcMWt9Zfw==",
       "license": "MIT",
       "dependencies": {
-        "@zoom-image/core": "0.36.2"
+        "@zoom-image/core": "0.39.0"
       },
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/willnguyen1312"
       },
       "peerDependencies": {
-        "svelte": "^3.0.0 || ^4.0.0"
+        "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0"
       }
     },
     "node_modules/acorn": {
-      "version": "8.12.0",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz",
-      "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==",
+      "version": "8.14.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
+      "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
       "license": "MIT",
       "bin": {
         "acorn": "bin/acorn"
@@ -2708,6 +2862,15 @@
         "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
       }
     },
+    "node_modules/acorn-typescript": {
+      "version": "1.4.13",
+      "resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz",
+      "integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==",
+      "license": "MIT",
+      "peerDependencies": {
+        "acorn": ">=8.9.0"
+      }
+    },
     "node_modules/agent-base": {
       "version": "7.1.0",
       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
@@ -2794,6 +2957,7 @@
       "version": "5.3.0",
       "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
       "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+      "dev": true,
       "dependencies": {
         "dequal": "^2.0.3"
       }
@@ -2811,6 +2975,7 @@
       "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
       "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=12"
       }
@@ -2870,11 +3035,12 @@
       }
     },
     "node_modules/axobject-query": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz",
-      "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==",
-      "dependencies": {
-        "dequal": "^2.0.3"
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
+      "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">= 0.4"
       }
     },
     "node_modules/balanced-match": {
@@ -2903,21 +3069,22 @@
       }
     },
     "node_modules/braces": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "fill-range": "^7.0.1"
+        "fill-range": "^7.1.1"
       },
       "engines": {
         "node": ">=8"
       }
     },
     "node_modules/browserslist": {
-      "version": "4.23.3",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
-      "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
+      "version": "4.24.3",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
+      "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
       "dev": true,
       "funding": [
         {
@@ -2935,10 +3102,10 @@
       ],
       "license": "MIT",
       "dependencies": {
-        "caniuse-lite": "^1.0.30001646",
-        "electron-to-chromium": "^1.5.4",
-        "node-releases": "^2.0.18",
-        "update-browserslist-db": "^1.1.0"
+        "caniuse-lite": "^1.0.30001688",
+        "electron-to-chromium": "^1.5.73",
+        "node-releases": "^2.0.19",
+        "update-browserslist-db": "^1.1.1"
       },
       "bin": {
         "browserslist": "cli.js"
@@ -2987,6 +3154,7 @@
       "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
       "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
@@ -3010,9 +3178,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001651",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
-      "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
+      "version": "1.0.30001689",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz",
+      "integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==",
       "dev": true,
       "funding": [
         {
@@ -3031,10 +3199,11 @@
       "license": "CC-BY-4.0"
     },
     "node_modules/chai": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
-      "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz",
+      "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "assertion-error": "^2.0.1",
         "check-error": "^2.1.1",
@@ -3065,21 +3234,17 @@
       "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
       "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">= 16"
       }
     },
     "node_modules/chokidar": {
-      "version": "3.5.3",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
-      "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
       "dev": true,
-      "funding": [
-        {
-          "type": "individual",
-          "url": "https://paulmillr.com/funding/"
-        }
-      ],
+      "license": "MIT",
       "dependencies": {
         "anymatch": "~3.1.2",
         "braces": "~3.0.2",
@@ -3092,6 +3257,9 @@
       "engines": {
         "node": ">= 8.10.0"
       },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      },
       "optionalDependencies": {
         "fsevents": "~2.3.2"
       }
@@ -3178,18 +3346,6 @@
         "node": ">=6"
       }
     },
-    "node_modules/code-red": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz",
-      "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==",
-      "dependencies": {
-        "@jridgewell/sourcemap-codec": "^1.4.15",
-        "@types/estree": "^1.0.1",
-        "acorn": "^8.10.0",
-        "estree-walker": "^3.0.3",
-        "periscopic": "^3.1.0"
-      }
-    },
     "node_modules/color": {
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -3285,12 +3441,13 @@
       }
     },
     "node_modules/core-js-compat": {
-      "version": "3.37.1",
-      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
-      "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
+      "version": "3.39.0",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz",
+      "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "browserslist": "^4.23.0"
+        "browserslist": "^4.24.2"
       },
       "funding": {
         "type": "opencollective",
@@ -3298,10 +3455,11 @@
       }
     },
     "node_modules/cross-spawn": {
-      "version": "7.0.3",
-      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
-      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "path-key": "^3.1.0",
         "shebang-command": "^2.0.0",
@@ -3311,18 +3469,6 @@
         "node": ">= 8"
       }
     },
-    "node_modules/css-tree": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
-      "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
-      "dependencies": {
-        "mdn-data": "2.0.30",
-        "source-map-js": "^1.0.1"
-      },
-      "engines": {
-        "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
-      }
-    },
     "node_modules/css.escape": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
@@ -3412,11 +3558,12 @@
       }
     },
     "node_modules/debug": {
-      "version": "4.3.6",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
-      "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+      "license": "MIT",
       "dependencies": {
-        "ms": "2.1.2"
+        "ms": "^2.1.3"
       },
       "engines": {
         "node": ">=6.0"
@@ -3440,6 +3587,7 @@
       "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
       "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
@@ -3496,10 +3644,11 @@
       }
     },
     "node_modules/devalue": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.0.0.tgz",
-      "integrity": "sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==",
-      "dev": true
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
+      "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/didyoumean": {
       "version": "1.2.2",
@@ -3548,9 +3697,9 @@
       "dev": true
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.5.6",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz",
-      "integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==",
+      "version": "1.5.74",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz",
+      "integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==",
       "dev": true,
       "license": "ISC"
     },
@@ -3561,41 +3710,23 @@
       "dev": true
     },
     "node_modules/engine.io-client": {
-      "version": "6.5.2",
-      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz",
-      "integrity": "sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==",
+      "version": "6.5.4",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz",
+      "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==",
+      "license": "MIT",
       "dependencies": {
         "@socket.io/component-emitter": "~3.1.0",
         "debug": "~4.3.1",
         "engine.io-parser": "~5.2.1",
-        "ws": "~8.11.0",
+        "ws": "~8.17.1",
         "xmlhttprequest-ssl": "~2.0.0"
       }
     },
-    "node_modules/engine.io-client/node_modules/ws": {
-      "version": "8.11.0",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
-      "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
-      "engines": {
-        "node": ">=10.0.0"
-      },
-      "peerDependencies": {
-        "bufferutil": "^4.0.1",
-        "utf-8-validate": "^5.0.2"
-      },
-      "peerDependenciesMeta": {
-        "bufferutil": {
-          "optional": true
-        },
-        "utf-8-validate": {
-          "optional": true
-        }
-      }
-    },
     "node_modules/engine.io-parser": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
-      "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==",
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+      "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+      "license": "MIT",
       "engines": {
         "node": ">=10.0.0"
       }
@@ -3623,6 +3754,13 @@
         "is-arrayish": "^0.2.1"
       }
     },
+    "node_modules/es-module-lexer": {
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
+      "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/es5-ext": {
       "version": "0.10.64",
       "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
@@ -3711,9 +3849,9 @@
       }
     },
     "node_modules/escalade": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
-      "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -3730,28 +3868,32 @@
       }
     },
     "node_modules/eslint": {
-      "version": "9.9.1",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz",
-      "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==",
+      "version": "9.14.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz",
+      "integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
-        "@eslint-community/regexpp": "^4.11.0",
+        "@eslint-community/regexpp": "^4.12.1",
         "@eslint/config-array": "^0.18.0",
+        "@eslint/core": "^0.7.0",
         "@eslint/eslintrc": "^3.1.0",
-        "@eslint/js": "9.9.1",
+        "@eslint/js": "9.14.0",
+        "@eslint/plugin-kit": "^0.2.0",
+        "@humanfs/node": "^0.16.6",
         "@humanwhocodes/module-importer": "^1.0.1",
-        "@humanwhocodes/retry": "^0.3.0",
-        "@nodelib/fs.walk": "^1.2.8",
+        "@humanwhocodes/retry": "^0.4.0",
+        "@types/estree": "^1.0.6",
+        "@types/json-schema": "^7.0.15",
         "ajv": "^6.12.4",
         "chalk": "^4.0.0",
         "cross-spawn": "^7.0.2",
         "debug": "^4.3.2",
         "escape-string-regexp": "^4.0.0",
-        "eslint-scope": "^8.0.2",
-        "eslint-visitor-keys": "^4.0.0",
-        "espree": "^10.1.0",
+        "eslint-scope": "^8.2.0",
+        "eslint-visitor-keys": "^4.2.0",
+        "espree": "^10.3.0",
         "esquery": "^1.5.0",
         "esutils": "^2.0.2",
         "fast-deep-equal": "^3.1.3",
@@ -3761,14 +3903,11 @@
         "ignore": "^5.2.0",
         "imurmurhash": "^0.1.4",
         "is-glob": "^4.0.0",
-        "is-path-inside": "^3.0.3",
         "json-stable-stringify-without-jsonify": "^1.0.1",
-        "levn": "^0.4.1",
         "lodash.merge": "^4.6.2",
         "minimatch": "^3.1.2",
         "natural-compare": "^1.4.0",
         "optionator": "^0.9.3",
-        "strip-ansi": "^6.0.1",
         "text-table": "^0.2.0"
       },
       "bin": {
@@ -3805,39 +3944,6 @@
         "eslint": ">=6.0.0"
       }
     },
-    "node_modules/eslint-compat-utils/node_modules/lru-cache": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-      "dev": true,
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/eslint-compat-utils/node_modules/semver": {
-      "version": "7.6.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
-      "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
-      "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/eslint-compat-utils/node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-      "dev": true
-    },
     "node_modules/eslint-config-prettier": {
       "version": "9.1.0",
       "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
@@ -3851,9 +3957,9 @@
       }
     },
     "node_modules/eslint-plugin-svelte": {
-      "version": "2.43.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.43.0.tgz",
-      "integrity": "sha512-REkxQWvg2pp7QVLxQNa+dJ97xUqRe7Y2JJbSWkHSuszu0VcblZtXkPBPckkivk99y5CdLw4slqfPylL2d/X4jQ==",
+      "version": "2.46.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.0.tgz",
+      "integrity": "sha512-1A7iEMkzmCZ9/Iz+EAfOGYL8IoIG6zeKEq1SmpxGeM5SXmoQq+ZNnCpXFVJpsxPWYx8jIVGMerQMzX20cqUl0g==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -3861,13 +3967,13 @@
         "@jridgewell/sourcemap-codec": "^1.4.15",
         "eslint-compat-utils": "^0.5.1",
         "esutils": "^2.0.3",
-        "known-css-properties": "^0.34.0",
+        "known-css-properties": "^0.35.0",
         "postcss": "^8.4.38",
         "postcss-load-config": "^3.1.4",
         "postcss-safe-parser": "^6.0.0",
         "postcss-selector-parser": "^6.1.0",
         "semver": "^7.6.2",
-        "svelte-eslint-parser": "^0.41.0"
+        "svelte-eslint-parser": "^0.43.0"
       },
       "engines": {
         "node": "^14.17.0 || >=16.0.0"
@@ -3877,7 +3983,7 @@
       },
       "peerDependencies": {
         "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0",
-        "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0-next.191"
+        "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0"
       },
       "peerDependenciesMeta": {
         "svelte": {
@@ -3885,33 +3991,20 @@
         }
       }
     },
-    "node_modules/eslint-plugin-svelte/node_modules/semver": {
-      "version": "7.6.3",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
-      "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
-      "dev": true,
-      "license": "ISC",
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
     "node_modules/eslint-plugin-unicorn": {
-      "version": "55.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz",
-      "integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==",
+      "version": "56.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.1.tgz",
+      "integrity": "sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@babel/helper-validator-identifier": "^7.24.5",
+        "@babel/helper-validator-identifier": "^7.24.7",
         "@eslint-community/eslint-utils": "^4.4.0",
         "ci-info": "^4.0.0",
         "clean-regexp": "^1.0.0",
-        "core-js-compat": "^3.37.0",
-        "esquery": "^1.5.0",
-        "globals": "^15.7.0",
+        "core-js-compat": "^3.38.1",
+        "esquery": "^1.6.0",
+        "globals": "^15.9.0",
         "indent-string": "^4.0.0",
         "is-builtin-module": "^3.2.1",
         "jsesc": "^3.0.2",
@@ -3919,7 +4012,7 @@
         "read-pkg-up": "^7.0.1",
         "regexp-tree": "^0.1.27",
         "regjsparser": "^0.10.0",
-        "semver": "^7.6.1",
+        "semver": "^7.6.3",
         "strip-indent": "^3.0.0"
       },
       "engines": {
@@ -3945,19 +4038,6 @@
         "node": ">=6"
       }
     },
-    "node_modules/eslint-plugin-unicorn/node_modules/semver": {
-      "version": "7.6.3",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
-      "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
-      "dev": true,
-      "license": "ISC",
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
     "node_modules/eslint-scope": {
       "version": "7.2.2",
       "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -3986,6 +4066,30 @@
         "url": "https://opencollective.com/eslint"
       }
     },
+    "node_modules/eslint/node_modules/@eslint/js": {
+      "version": "9.14.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz",
+      "integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/eslint/node_modules/@humanwhocodes/retry": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz",
+      "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
     "node_modules/eslint/node_modules/ansi-styles": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -4053,9 +4157,9 @@
       }
     },
     "node_modules/eslint/node_modules/eslint-scope": {
-      "version": "8.0.2",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz",
-      "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==",
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
+      "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
@@ -4070,9 +4174,9 @@
       }
     },
     "node_modules/eslint/node_modules/eslint-visitor-keys": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
-      "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
       "dev": true,
       "license": "Apache-2.0",
       "engines": {
@@ -4083,15 +4187,15 @@
       }
     },
     "node_modules/eslint/node_modules/espree": {
-      "version": "10.1.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz",
-      "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==",
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
+      "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
-        "acorn": "^8.12.0",
+        "acorn": "^8.14.0",
         "acorn-jsx": "^5.3.2",
-        "eslint-visitor-keys": "^4.0.0"
+        "eslint-visitor-keys": "^4.2.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4124,10 +4228,10 @@
       }
     },
     "node_modules/esm-env": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz",
-      "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==",
-      "dev": true
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz",
+      "integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==",
+      "license": "MIT"
     },
     "node_modules/esniff": {
       "version": "2.0.1",
@@ -4161,10 +4265,11 @@
       }
     },
     "node_modules/esquery": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
-      "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+      "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "estraverse": "^5.1.0"
       },
@@ -4172,6 +4277,16 @@
         "node": ">=0.10"
       }
     },
+    "node_modules/esrap": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.2.2.tgz",
+      "integrity": "sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.4.15",
+        "@types/estree": "^1.0.1"
+      }
+    },
     "node_modules/esrecurse": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
@@ -4197,6 +4312,7 @@
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
       "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+      "dev": true,
       "dependencies": {
         "@types/estree": "^1.0.0"
       }
@@ -4219,39 +4335,14 @@
         "es5-ext": "~0.10.14"
       }
     },
-    "node_modules/execa": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
-      "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
+    "node_modules/expect-type": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz",
+      "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==",
       "dev": true,
-      "dependencies": {
-        "cross-spawn": "^7.0.3",
-        "get-stream": "^8.0.1",
-        "human-signals": "^5.0.0",
-        "is-stream": "^3.0.0",
-        "merge-stream": "^2.0.0",
-        "npm-run-path": "^5.1.0",
-        "onetime": "^6.0.0",
-        "signal-exit": "^4.1.0",
-        "strip-final-newline": "^3.0.0"
-      },
+      "license": "Apache-2.0",
       "engines": {
-        "node": ">=16.17"
-      },
-      "funding": {
-        "url": "https://github.com/sindresorhus/execa?sponsor=1"
-      }
-    },
-    "node_modules/execa/node_modules/get-stream": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
-      "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
-      "dev": true,
-      "engines": {
-        "node": ">=16"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": ">=12.0.0"
       }
     },
     "node_modules/ext": {
@@ -4274,10 +4365,11 @@
       }
     },
     "node_modules/factory.ts": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/factory.ts/-/factory.ts-1.4.1.tgz",
-      "integrity": "sha512-x5hrzGOZvQnw82ZK+fUo/p1nlbJGCi564FBx3jQWQix6xyEK8xvdCwjdgdmbaUiqfURWWfjgTJyBU5OSfs52tw==",
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/factory.ts/-/factory.ts-1.4.2.tgz",
+      "integrity": "sha512-8x2hqK1+EGkja4Ah8H3nkP7rDUJsBK1N3iFDqzqsaOV114o2IphSdVkFIw9nDHHr37gFFy2NXeN6n10ieqHzZg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "clone-deep": "^4.0.1",
         "source-map-support": "^0.5.21"
@@ -4361,10 +4453,11 @@
       }
     },
     "node_modules/fill-range": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "to-regex-range": "^5.0.1"
       },
@@ -4454,12 +4547,6 @@
         "url": "https://github.com/sponsors/rawify"
       }
     },
-    "node_modules/fs.realpath": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
-      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
-      "dev": true
-    },
     "node_modules/fsevents": {
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -4494,15 +4581,6 @@
         "node": "6.* || 8.* || >= 10.*"
       }
     },
-    "node_modules/get-func-name": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
-      "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
-      "dev": true,
-      "engines": {
-        "node": "*"
-      }
-    },
     "node_modules/get-stream": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -4527,6 +4605,27 @@
       "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
       "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="
     },
+    "node_modules/glob": {
+      "version": "10.4.5",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+      "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^1.11.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/glob-parent": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -4539,6 +4638,32 @@
         "node": ">=10.13.0"
       }
     },
+    "node_modules/glob/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/glob/node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/global-prefix": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz",
@@ -4564,9 +4689,9 @@
       }
     },
     "node_modules/globals": {
-      "version": "15.9.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz",
-      "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==",
+      "version": "15.12.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz",
+      "integrity": "sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -4696,15 +4821,6 @@
         "node": ">= 14"
       }
     },
-    "node_modules/human-signals": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
-      "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=16.17.0"
-      }
-    },
     "node_modules/iconv-lite": {
       "version": "0.6.3",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -4813,22 +4929,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/inflight": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
-      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
-      "dev": true,
-      "dependencies": {
-        "once": "^1.3.0",
-        "wrappy": "1"
-      }
-    },
-    "node_modules/inherits": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
-      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
-      "dev": true
-    },
     "node_modules/ini": {
       "version": "1.3.8",
       "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
@@ -4843,14 +4943,15 @@
       }
     },
     "node_modules/intl-messageformat": {
-      "version": "10.5.14",
-      "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.14.tgz",
-      "integrity": "sha512-IjC6sI0X7YRjjyVH9aUgdftcmZK7WXdHeil4KwbjDnRWjnVitKpAx3rr6t6di1joFp5188VqKcobOPA6mCLG/w==",
+      "version": "10.7.6",
+      "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.6.tgz",
+      "integrity": "sha512-IsMU/hqyy3FJwNJ0hxDfY2heJ7MteSuFvcnCebxRp67di4Fhx1gKKE+qS0bBwUF8yXkX9SsPUhLeX/B6h5SKUA==",
+      "license": "BSD-3-Clause",
       "dependencies": {
-        "@formatjs/ecma402-abstract": "2.0.0",
-        "@formatjs/fast-memoize": "2.2.0",
-        "@formatjs/icu-messageformat-parser": "2.7.8",
-        "tslib": "^2.4.0"
+        "@formatjs/ecma402-abstract": "2.2.3",
+        "@formatjs/fast-memoize": "2.2.3",
+        "@formatjs/icu-messageformat-parser": "2.9.3",
+        "tslib": "2"
       }
     },
     "node_modules/is-arrayish": {
@@ -4956,19 +5057,11 @@
       "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
       "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.12.0"
       }
     },
-    "node_modules/is-path-inside": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
-      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/is-plain-object": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@@ -4994,23 +5087,12 @@
       "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="
     },
     "node_modules/is-reference": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
-      "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
+      "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
+      "license": "MIT",
       "dependencies": {
-        "@types/estree": "*"
-      }
-    },
-    "node_modules/is-stream": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
-      "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
-      "dev": true,
-      "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "@types/estree": "^1.0.6"
       }
     },
     "node_modules/is-wsl": {
@@ -5125,10 +5207,11 @@
       }
     },
     "node_modules/jiti": {
-      "version": "1.21.0",
-      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
-      "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
+      "version": "1.21.6",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
+      "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
       "dev": true,
+      "license": "MIT",
       "bin": {
         "jiti": "bin/jiti.js"
       }
@@ -5271,9 +5354,9 @@
       }
     },
     "node_modules/known-css-properties": {
-      "version": "0.34.0",
-      "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.34.0.tgz",
-      "integrity": "sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==",
+      "version": "0.35.0",
+      "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz",
+      "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==",
       "dev": true,
       "license": "MIT"
     },
@@ -5343,13 +5426,11 @@
       "dev": true
     },
     "node_modules/loupe": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz",
-      "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz",
+      "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==",
       "dev": true,
-      "dependencies": {
-        "get-func-name": "^2.0.1"
-      }
+      "license": "MIT"
     },
     "node_modules/lru-queue": {
       "version": "0.1.0",
@@ -5378,22 +5459,23 @@
       }
     },
     "node_modules/magic-string": {
-      "version": "0.30.10",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz",
-      "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==",
+      "version": "0.30.12",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz",
+      "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==",
       "license": "MIT",
       "dependencies": {
-        "@jridgewell/sourcemap-codec": "^1.4.15"
+        "@jridgewell/sourcemap-codec": "^1.5.0"
       }
     },
     "node_modules/magicast": {
-      "version": "0.3.4",
-      "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz",
-      "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==",
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
+      "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/parser": "^7.24.4",
-        "@babel/types": "^7.24.0",
+        "@babel/parser": "^7.25.4",
+        "@babel/types": "^7.25.4",
         "source-map-js": "^1.2.0"
       }
     },
@@ -5412,18 +5494,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/make-dir/node_modules/semver": {
-      "version": "7.6.3",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
-      "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
-      "dev": true,
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
     "node_modules/mapbox-gl": {
       "version": "1.13.3",
       "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.3.tgz",
@@ -5536,11 +5606,6 @@
         "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
       }
     },
-    "node_modules/mdn-data": {
-      "version": "2.0.30",
-      "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
-      "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
-    },
     "node_modules/memoizee": {
       "version": "0.4.17",
       "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz",
@@ -5559,12 +5624,6 @@
         "node": ">=0.12"
       }
     },
-    "node_modules/merge-stream": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
-      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
-      "dev": true
-    },
     "node_modules/merge2": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -5575,12 +5634,13 @@
       }
     },
     "node_modules/micromatch": {
-      "version": "4.0.5",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
-      "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "braces": "^3.0.2",
+        "braces": "^3.0.3",
         "picomatch": "^2.3.1"
       },
       "engines": {
@@ -5612,18 +5672,6 @@
         "node": ">= 0.6"
       }
     },
-    "node_modules/mimic-fn": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
-      "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/min-indent": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@@ -5680,9 +5728,10 @@
       }
     },
     "node_modules/ms": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
     },
     "node_modules/murmurhash-js": {
       "version": "1.0.0",
@@ -5701,9 +5750,9 @@
       }
     },
     "node_modules/nanoid": {
-      "version": "3.3.7",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
-      "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+      "version": "3.3.8",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+      "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
       "dev": true,
       "funding": [
         {
@@ -5711,6 +5760,7 @@
           "url": "https://github.com/sponsors/ai"
         }
       ],
+      "license": "MIT",
       "bin": {
         "nanoid": "bin/nanoid.cjs"
       },
@@ -5735,9 +5785,9 @@
       "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
     },
     "node_modules/node-releases": {
-      "version": "2.0.18",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
-      "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
+      "version": "2.0.19",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+      "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
       "dev": true,
       "license": "MIT"
     },
@@ -5780,33 +5830,6 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/npm-run-path": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz",
-      "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==",
-      "dev": true,
-      "dependencies": {
-        "path-key": "^4.0.0"
-      },
-      "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/npm-run-path/node_modules/path-key": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
-      "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/nwsapi": {
       "version": "2.2.7",
       "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz",
@@ -5833,30 +5856,6 @@
         "node": ">= 6"
       }
     },
-    "node_modules/once": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
-      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
-      "dev": true,
-      "dependencies": {
-        "wrappy": "1"
-      }
-    },
-    "node_modules/onetime": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
-      "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
-      "dev": true,
-      "dependencies": {
-        "mimic-fn": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/open": {
       "version": "8.4.2",
       "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
@@ -5989,15 +5988,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/path-is-absolute": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
-      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/path-key": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -6039,13 +6029,15 @@
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
       "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/pathval": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
       "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">= 14.16"
       }
@@ -6062,20 +6054,10 @@
         "pbf": "bin/pbf"
       }
     },
-    "node_modules/periscopic": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
-      "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==",
-      "dependencies": {
-        "@types/estree": "^1.0.0",
-        "estree-walker": "^3.0.0",
-        "is-reference": "^3.0.0"
-      }
-    },
     "node_modules/picocolors": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
-      "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
       "dev": true,
       "license": "ISC"
     },
@@ -6128,9 +6110,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.4.41",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
-      "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
+      "version": "8.4.49",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+      "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
       "dev": true,
       "funding": [
         {
@@ -6149,8 +6131,8 @@
       "license": "MIT",
       "dependencies": {
         "nanoid": "^3.3.7",
-        "picocolors": "^1.0.1",
-        "source-map-js": "^1.2.0"
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
       },
       "engines": {
         "node": "^10 || ^12 || >=14"
@@ -6222,20 +6204,27 @@
       }
     },
     "node_modules/postcss-nested": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
-      "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+      "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
       "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
       "dependencies": {
-        "postcss-selector-parser": "^6.0.11"
+        "postcss-selector-parser": "^6.1.1"
       },
       "engines": {
         "node": ">=12.0"
       },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/postcss/"
-      },
       "peerDependencies": {
         "postcss": "^8.2.14"
       }
@@ -6284,9 +6273,9 @@
       }
     },
     "node_modules/postcss-selector-parser": {
-      "version": "6.1.0",
-      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz",
-      "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==",
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+      "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -6334,21 +6323,17 @@
       }
     },
     "node_modules/prettier-plugin-organize-imports": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
-      "integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz",
+      "integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==",
       "dev": true,
       "license": "MIT",
       "peerDependencies": {
-        "@vue/language-plugin-pug": "^2.0.24",
         "prettier": ">=2.0",
         "typescript": ">=2.9",
-        "vue-tsc": "^2.0.24"
+        "vue-tsc": "^2.1.0"
       },
       "peerDependenciesMeta": {
-        "@vue/language-plugin-pug": {
-          "optional": true
-        },
         "vue-tsc": {
           "optional": true
         }
@@ -6367,9 +6352,9 @@
       }
     },
     "node_modules/prettier-plugin-svelte": {
-      "version": "3.2.6",
-      "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.6.tgz",
-      "integrity": "sha512-Y1XWLw7vXUQQZmgv1JAEiLcErqUniAF2wO7QJsw8BVMvpLET2dI5WpEIEJx1r11iHVdSMzQxivyfrH9On9t2IQ==",
+      "version": "3.2.8",
+      "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.8.tgz",
+      "integrity": "sha512-PAHmmU5cGZdnhW4mWhmvxuG2PVbbHIxUuPOdUKvfE+d4Qt2d29iU5VWrPdsaW5YqVEE0nqhlvN4eoKmVMpIF3Q==",
       "dev": true,
       "license": "MIT",
       "peerDependencies": {
@@ -6654,10 +6639,11 @@
       "peer": true
     },
     "node_modules/resolve": {
-      "version": "1.22.6",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz",
-      "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==",
+      "version": "1.22.8",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
+      "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "is-core-module": "^2.13.0",
         "path-parse": "^1.0.7",
@@ -6689,13 +6675,13 @@
       }
     },
     "node_modules/rollup": {
-      "version": "4.21.1",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.1.tgz",
-      "integrity": "sha512-ZnYyKvscThhgd3M5+Qt3pmhO4jIRR5RGzaSovB6Q7rGNrK5cUncrtLmcTTJVSdcKXyZjW8X8MB0JMSuH9bcAJg==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz",
+      "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@types/estree": "1.0.5"
+        "@types/estree": "1.0.6"
       },
       "bin": {
         "rollup": "dist/bin/rollup"
@@ -6705,22 +6691,25 @@
         "npm": ">=8.0.0"
       },
       "optionalDependencies": {
-        "@rollup/rollup-android-arm-eabi": "4.21.1",
-        "@rollup/rollup-android-arm64": "4.21.1",
-        "@rollup/rollup-darwin-arm64": "4.21.1",
-        "@rollup/rollup-darwin-x64": "4.21.1",
-        "@rollup/rollup-linux-arm-gnueabihf": "4.21.1",
-        "@rollup/rollup-linux-arm-musleabihf": "4.21.1",
-        "@rollup/rollup-linux-arm64-gnu": "4.21.1",
-        "@rollup/rollup-linux-arm64-musl": "4.21.1",
-        "@rollup/rollup-linux-powerpc64le-gnu": "4.21.1",
-        "@rollup/rollup-linux-riscv64-gnu": "4.21.1",
-        "@rollup/rollup-linux-s390x-gnu": "4.21.1",
-        "@rollup/rollup-linux-x64-gnu": "4.21.1",
-        "@rollup/rollup-linux-x64-musl": "4.21.1",
-        "@rollup/rollup-win32-arm64-msvc": "4.21.1",
-        "@rollup/rollup-win32-ia32-msvc": "4.21.1",
-        "@rollup/rollup-win32-x64-msvc": "4.21.1",
+        "@rollup/rollup-android-arm-eabi": "4.28.1",
+        "@rollup/rollup-android-arm64": "4.28.1",
+        "@rollup/rollup-darwin-arm64": "4.28.1",
+        "@rollup/rollup-darwin-x64": "4.28.1",
+        "@rollup/rollup-freebsd-arm64": "4.28.1",
+        "@rollup/rollup-freebsd-x64": "4.28.1",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.28.1",
+        "@rollup/rollup-linux-arm-musleabihf": "4.28.1",
+        "@rollup/rollup-linux-arm64-gnu": "4.28.1",
+        "@rollup/rollup-linux-arm64-musl": "4.28.1",
+        "@rollup/rollup-linux-loongarch64-gnu": "4.28.1",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1",
+        "@rollup/rollup-linux-riscv64-gnu": "4.28.1",
+        "@rollup/rollup-linux-s390x-gnu": "4.28.1",
+        "@rollup/rollup-linux-x64-gnu": "4.28.1",
+        "@rollup/rollup-linux-x64-musl": "4.28.1",
+        "@rollup/rollup-win32-arm64-msvc": "4.28.1",
+        "@rollup/rollup-win32-ia32-msvc": "4.28.1",
+        "@rollup/rollup-win32-x64-msvc": "4.28.1",
         "fsevents": "~2.3.2"
       }
     },
@@ -6828,6 +6817,19 @@
         "node": ">=v12.22.7"
       }
     },
+    "node_modules/semver": {
+      "version": "7.6.3",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+      "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/set-cookie-parser": {
       "version": "2.6.0",
       "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
@@ -6900,39 +6902,6 @@
         "@img/sharp-win32-x64": "0.33.3"
       }
     },
-    "node_modules/sharp/node_modules/lru-cache": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-      "dev": true,
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/sharp/node_modules/semver": {
-      "version": "7.6.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
-      "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
-      "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/sharp/node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-      "dev": true
-    },
     "node_modules/shebang-command": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -6988,23 +6957,25 @@
       "dev": true
     },
     "node_modules/sirv": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
-      "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz",
+      "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@polka/url": "^1.0.0-next.24",
         "mrmime": "^2.0.0",
         "totalist": "^3.0.0"
       },
       "engines": {
-        "node": ">= 10"
+        "node": ">=18"
       }
     },
     "node_modules/socket.io-client": {
       "version": "4.7.5",
       "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz",
       "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==",
+      "license": "MIT",
       "dependencies": {
         "@socket.io/component-emitter": "~3.1.0",
         "debug": "~4.3.2",
@@ -7068,9 +7039,11 @@
       }
     },
     "node_modules/source-map-js": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
-      "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "dev": true,
+      "license": "BSD-3-Clause",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -7158,10 +7131,11 @@
       "dev": true
     },
     "node_modules/std-env": {
-      "version": "3.7.0",
-      "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
-      "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==",
-      "dev": true
+      "version": "3.8.0",
+      "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz",
+      "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/string-width": {
       "version": "4.2.3",
@@ -7217,18 +7191,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/strip-final-newline": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
-      "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/strip-indent": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
@@ -7254,14 +7216,15 @@
       }
     },
     "node_modules/sucrase": {
-      "version": "3.34.0",
-      "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz",
-      "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==",
+      "version": "3.35.0",
+      "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
+      "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@jridgewell/gen-mapping": "^0.3.2",
         "commander": "^4.0.0",
-        "glob": "7.1.6",
+        "glob": "^10.3.10",
         "lines-and-columns": "^1.1.6",
         "mz": "^2.7.0",
         "pirates": "^4.0.1",
@@ -7272,27 +7235,7 @@
         "sucrase-node": "bin/sucrase-node"
       },
       "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/sucrase/node_modules/glob": {
-      "version": "7.1.6",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
-      "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
-      "dev": true,
-      "dependencies": {
-        "fs.realpath": "^1.0.0",
-        "inflight": "^1.0.4",
-        "inherits": "2",
-        "minimatch": "^3.0.4",
-        "once": "^1.3.0",
-        "path-is-absolute": "^1.0.0"
-      },
-      "engines": {
-        "node": "*"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
+        "node": ">=16 || 14 >=14.17"
       }
     },
     "node_modules/supercluster": {
@@ -7328,39 +7271,38 @@
       }
     },
     "node_modules/svelte": {
-      "version": "4.2.19",
-      "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz",
-      "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==",
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.2.1.tgz",
+      "integrity": "sha512-WzyA7VUVlDTLPt+m71bLD5BXasavqvAo68DelxWaPo8dNEZ3tmeq3DSJPsWqnG37cG2hfn7HaD3x882qF+7UOw==",
       "license": "MIT",
       "dependencies": {
-        "@ampproject/remapping": "^2.2.1",
-        "@jridgewell/sourcemap-codec": "^1.4.15",
-        "@jridgewell/trace-mapping": "^0.3.18",
-        "@types/estree": "^1.0.1",
-        "acorn": "^8.9.0",
-        "aria-query": "^5.3.0",
-        "axobject-query": "^4.0.0",
-        "code-red": "^1.0.3",
-        "css-tree": "^2.3.1",
-        "estree-walker": "^3.0.3",
-        "is-reference": "^3.0.1",
+        "@ampproject/remapping": "^2.3.0",
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@types/estree": "^1.0.5",
+        "acorn": "^8.12.1",
+        "acorn-typescript": "^1.4.13",
+        "aria-query": "^5.3.1",
+        "axobject-query": "^4.1.0",
+        "esm-env": "^1.0.0",
+        "esrap": "^1.2.2",
+        "is-reference": "^3.0.3",
         "locate-character": "^3.0.0",
-        "magic-string": "^0.30.4",
-        "periscopic": "^3.1.0"
+        "magic-string": "^0.30.11",
+        "zimmerframe": "^1.1.2"
       },
       "engines": {
-        "node": ">=16"
+        "node": ">=18"
       }
     },
     "node_modules/svelte-check": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.0.0.tgz",
-      "integrity": "sha512-QgKO6OQbee9B2dyWZgrGruS3WHKrUZ718Ug53nK45vamsx93Al3on6tOrxyCMVX+OMOLLlrenn7b2VAomePwxQ==",
+      "version": "4.0.9",
+      "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.0.9.tgz",
+      "integrity": "sha512-SVNCz2L+9ZELGli7G0n3B3QE5kdf0u27RtKr2ZivWQhcWIXatZxwM4VrQ6AiA2k9zKp2mk5AxkEhdjbpjv7rEw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@jridgewell/trace-mapping": "^0.3.25",
-        "chokidar": "^3.4.1",
+        "chokidar": "^4.0.1",
         "fdir": "^6.2.0",
         "picocolors": "^1.0.0",
         "sade": "^1.7.4"
@@ -7376,10 +7318,26 @@
         "typescript": ">=5.0.0"
       }
     },
+    "node_modules/svelte-check/node_modules/chokidar": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
+      "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "readdirp": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 14.16.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
     "node_modules/svelte-check/node_modules/fdir": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.3.0.tgz",
-      "integrity": "sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==",
+      "version": "6.4.2",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz",
+      "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==",
       "dev": true,
       "license": "MIT",
       "peerDependencies": {
@@ -7406,10 +7364,24 @@
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
+    "node_modules/svelte-check/node_modules/readdirp": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
+      "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14.16.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
     "node_modules/svelte-eslint-parser": {
-      "version": "0.41.0",
-      "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.41.0.tgz",
-      "integrity": "sha512-L6f4hOL+AbgfBIB52Z310pg1d2QjRqm7wy3kI1W6hhdhX5bvu7+f0R6w4ykp5HoDdzq+vGhIJmsisaiJDGmVfA==",
+      "version": "0.43.0",
+      "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz",
+      "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -7426,7 +7398,7 @@
         "url": "https://github.com/sponsors/ota-meshi"
       },
       "peerDependencies": {
-        "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0-next.191"
+        "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0"
       },
       "peerDependenciesMeta": {
         "svelte": {
@@ -7435,27 +7407,16 @@
       }
     },
     "node_modules/svelte-gestures": {
-      "version": "5.0.4",
-      "resolved": "https://registry.npmjs.org/svelte-gestures/-/svelte-gestures-5.0.4.tgz",
-      "integrity": "sha512-a6cnR46AfFZ8zZyvA38A1wBLBFI7rYuAWQnmv3yYgSdbaJK/U7JG34rSkjMCePRvf4BETJSDfMNngLs5zEAfbw==",
+      "version": "5.0.6",
+      "resolved": "https://registry.npmjs.org/svelte-gestures/-/svelte-gestures-5.0.6.tgz",
+      "integrity": "sha512-kElJnoZrQtlkXE0O/RcKioz9NP0Sxx05j31ohyosNkydo6NOEsZB85mhoaCxOQNjxN+QPumYWfmIUsznYFjihA==",
       "license": "MIT"
     },
-    "node_modules/svelte-hmr": {
-      "version": "0.16.0",
-      "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz",
-      "integrity": "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==",
-      "dev": true,
-      "engines": {
-        "node": "^12.20 || ^14.13.1 || >= 16"
-      },
-      "peerDependencies": {
-        "svelte": "^3.19.0 || ^4.0.0"
-      }
-    },
     "node_modules/svelte-i18n": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/svelte-i18n/-/svelte-i18n-4.0.0.tgz",
-      "integrity": "sha512-4vivjKZADUMRIhTs38JuBNy3unbnh9AFRxWFLxq62P4NHic+/BaIZZlAsvqsCdnp7IdJf5EoSiH6TNdItcjA6g==",
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/svelte-i18n/-/svelte-i18n-4.0.1.tgz",
+      "integrity": "sha512-jaykGlGT5PUaaq04JWbJREvivlCnALtT+m87Kbm0fxyYHynkQaxQMnIKHLm2WeIuBRoljzwgyvz0Z6/CMwfdmQ==",
+      "license": "MIT",
       "dependencies": {
         "cli-color": "^2.0.3",
         "deepmerge": "^4.2.2",
@@ -7472,7 +7433,7 @@
         "node": ">= 16"
       },
       "peerDependencies": {
-        "svelte": "^3 || ^4"
+        "svelte": "^3 || ^4 || ^5"
       }
     },
     "node_modules/svelte-i18n/node_modules/@esbuild/aix-ppc64": {
@@ -7482,6 +7443,7 @@
       "cpu": [
         "ppc64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "aix"
@@ -7497,6 +7459,7 @@
       "cpu": [
         "arm"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "android"
@@ -7512,6 +7475,7 @@
       "cpu": [
         "arm64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "android"
@@ -7527,6 +7491,7 @@
       "cpu": [
         "x64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "android"
@@ -7542,6 +7507,7 @@
       "cpu": [
         "arm64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
@@ -7557,6 +7523,7 @@
       "cpu": [
         "x64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
@@ -7572,6 +7539,7 @@
       "cpu": [
         "arm64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "freebsd"
@@ -7587,6 +7555,7 @@
       "cpu": [
         "x64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "freebsd"
@@ -7602,6 +7571,7 @@
       "cpu": [
         "arm"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -7617,6 +7587,7 @@
       "cpu": [
         "arm64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -7632,6 +7603,7 @@
       "cpu": [
         "ia32"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -7647,6 +7619,7 @@
       "cpu": [
         "loong64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -7662,6 +7635,7 @@
       "cpu": [
         "mips64el"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -7677,6 +7651,7 @@
       "cpu": [
         "ppc64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -7692,6 +7667,7 @@
       "cpu": [
         "riscv64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -7707,6 +7683,7 @@
       "cpu": [
         "s390x"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -7722,6 +7699,7 @@
       "cpu": [
         "x64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -7737,6 +7715,7 @@
       "cpu": [
         "x64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "netbsd"
@@ -7752,6 +7731,7 @@
       "cpu": [
         "x64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "openbsd"
@@ -7767,6 +7747,7 @@
       "cpu": [
         "x64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "sunos"
@@ -7782,6 +7763,7 @@
       "cpu": [
         "arm64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
@@ -7797,6 +7779,7 @@
       "cpu": [
         "ia32"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
@@ -7812,6 +7795,7 @@
       "cpu": [
         "x64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
@@ -7825,6 +7809,7 @@
       "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
       "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
       "hasInstallScript": true,
+      "license": "MIT",
       "bin": {
         "esbuild": "bin/esbuild"
       },
@@ -7860,7 +7845,8 @@
     "node_modules/svelte-i18n/node_modules/estree-walker": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
-      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "license": "MIT"
     },
     "node_modules/svelte-local-storage-store": {
       "version": "0.6.4",
@@ -7874,9 +7860,9 @@
       }
     },
     "node_modules/svelte-maplibre": {
-      "version": "0.9.13",
-      "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.9.13.tgz",
-      "integrity": "sha512-XHQFKE86dKQ0PqjPGZ97jcHi83XdQRa4RW3hXDqmuxJ4yi2yvawdbO1Y0b2raAemCVERTcIU9HYgx0TAvqJgrA==",
+      "version": "0.9.14",
+      "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.9.14.tgz",
+      "integrity": "sha512-5HBvibzU/Uf3g8eEz4Hty5XAwoBhW9Tp7NQEvb80U/glR/M1IHyzUKss6XMq8Zbci2wtsASeoPc6dA5R4+0e0w==",
       "license": "MIT",
       "dependencies": {
         "d3-geo": "^3.1.0",
@@ -7905,15 +7891,25 @@
       }
     },
     "node_modules/svelte-parse-markup": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/svelte-parse-markup/-/svelte-parse-markup-0.1.2.tgz",
-      "integrity": "sha512-DycY7DJr7VqofiJ63ut1/NEG92HrWWL56VWITn/cJCu+LlZhMoBkBXT4opUitPEEwbq1nMQbv4vTKUfbOqIW1g==",
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/svelte-parse-markup/-/svelte-parse-markup-0.1.5.tgz",
+      "integrity": "sha512-T6mqZrySltPCDwfKXWQ6zehipVLk4GWfH1zCMGgRtLlOIFPuw58ZxVYxVvotMJgJaurKi1i14viB2GIRKXeJTQ==",
       "dev": true,
+      "license": "MIT",
       "funding": {
         "url": "https://bjornlu.com/sponsor"
       },
       "peerDependencies": {
-        "svelte": "^3.0.0 || ^4.0.0"
+        "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1"
+      }
+    },
+    "node_modules/svelte/node_modules/aria-query": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
+      "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">= 0.4"
       }
     },
     "node_modules/symbol-tree": {
@@ -7925,34 +7921,34 @@
       "peer": true
     },
     "node_modules/tailwindcss": {
-      "version": "3.4.10",
-      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
-      "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==",
+      "version": "3.4.15",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.15.tgz",
+      "integrity": "sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@alloc/quick-lru": "^5.2.0",
         "arg": "^5.0.2",
-        "chokidar": "^3.5.3",
+        "chokidar": "^3.6.0",
         "didyoumean": "^1.2.2",
         "dlv": "^1.1.3",
-        "fast-glob": "^3.3.0",
+        "fast-glob": "^3.3.2",
         "glob-parent": "^6.0.2",
         "is-glob": "^4.0.3",
-        "jiti": "^1.21.0",
+        "jiti": "^1.21.6",
         "lilconfig": "^2.1.0",
-        "micromatch": "^4.0.5",
+        "micromatch": "^4.0.8",
         "normalize-path": "^3.0.0",
         "object-hash": "^3.0.0",
-        "picocolors": "^1.0.0",
-        "postcss": "^8.4.23",
+        "picocolors": "^1.1.1",
+        "postcss": "^8.4.47",
         "postcss-import": "^15.1.0",
         "postcss-js": "^4.0.1",
-        "postcss-load-config": "^4.0.1",
-        "postcss-nested": "^6.0.1",
-        "postcss-selector-parser": "^6.0.11",
-        "resolve": "^1.22.2",
-        "sucrase": "^3.32.0"
+        "postcss-load-config": "^4.0.2",
+        "postcss-nested": "^6.2.0",
+        "postcss-selector-parser": "^6.1.2",
+        "resolve": "^1.22.8",
+        "sucrase": "^3.35.0"
       },
       "bin": {
         "tailwind": "lib/cli.js",
@@ -8012,9 +8008,9 @@
       }
     },
     "node_modules/tailwindcss/node_modules/yaml": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
-      "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz",
+      "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==",
       "dev": true,
       "license": "ISC",
       "bin": {
@@ -8047,26 +8043,6 @@
         "balanced-match": "^1.0.0"
       }
     },
-    "node_modules/test-exclude/node_modules/glob": {
-      "version": "10.4.5",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
-      "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
-      "dev": true,
-      "dependencies": {
-        "foreground-child": "^3.1.0",
-        "jackspeak": "^3.1.2",
-        "minimatch": "^9.0.4",
-        "minipass": "^7.1.2",
-        "package-json-from-dist": "^1.0.0",
-        "path-scurry": "^1.11.1"
-      },
-      "bin": {
-        "glob": "dist/esm/bin.mjs"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      }
-    },
     "node_modules/test-exclude/node_modules/minimatch": {
       "version": "9.0.5",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -8086,7 +8062,8 @@
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
       "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/thenify": {
       "version": "3.3.1",
@@ -8110,9 +8087,9 @@
       }
     },
     "node_modules/three": {
-      "version": "0.167.1",
-      "resolved": "https://registry.npmjs.org/three/-/three-0.167.1.tgz",
-      "integrity": "sha512-gYTLJA/UQip6J/tJvl91YYqlZF47+D/kxiWrbTon35ZHlXEN0VOo+Qke2walF1/x92v55H6enomymg4Dak52kw==",
+      "version": "0.169.0",
+      "resolved": "https://registry.npmjs.org/three/-/three-0.169.0.tgz",
+      "integrity": "sha512-Ed906MA3dR4TS5riErd4QBsRGPcx+HBDX2O5yYE5GqJeFQTPU+M56Va/f/Oph9X7uZo3W3o4l2ZhBZ6f6qUv0w==",
       "license": "MIT"
     },
     "node_modules/thumbhash": {
@@ -8139,16 +8116,25 @@
       }
     },
     "node_modules/tinybench": {
-      "version": "2.8.0",
-      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz",
-      "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==",
-      "dev": true
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+      "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tinyexec": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz",
+      "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/tinypool": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz",
-      "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz",
+      "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": "^18.0.0 || >=20.0.0"
       }
@@ -8168,28 +8154,21 @@
       }
     },
     "node_modules/tinyspy": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz",
-      "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==",
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
+      "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=14.0.0"
       }
     },
-    "node_modules/to-fast-properties": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
-      "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/to-regex-range": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
       "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "is-number": "^7.0.0"
       },
@@ -8256,9 +8235,9 @@
       "dev": true
     },
     "node_modules/tslib": {
-      "version": "2.7.0",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
-      "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
       "license": "0BSD"
     },
     "node_modules/type": {
@@ -8279,9 +8258,9 @@
       }
     },
     "node_modules/typescript": {
-      "version": "5.5.4",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
-      "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
+      "version": "5.6.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+      "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
       "dev": true,
       "license": "Apache-2.0",
       "bin": {
@@ -8343,9 +8322,9 @@
       }
     },
     "node_modules/update-browserslist-db": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
-      "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
+      "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
       "dev": true,
       "funding": [
         {
@@ -8363,8 +8342,8 @@
       ],
       "license": "MIT",
       "dependencies": {
-        "escalade": "^3.1.2",
-        "picocolors": "^1.0.1"
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.0"
       },
       "bin": {
         "update-browserslist-db": "cli.js"
@@ -8411,14 +8390,14 @@
       }
     },
     "node_modules/vite": {
-      "version": "5.4.2",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz",
-      "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==",
+      "version": "5.4.11",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
+      "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "esbuild": "^0.21.3",
-        "postcss": "^8.4.41",
+        "postcss": "^8.4.43",
         "rollup": "^4.20.0"
       },
       "bin": {
@@ -8484,15 +8463,16 @@
       }
     },
     "node_modules/vite-node": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz",
-      "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.5.tgz",
+      "integrity": "sha512-rd0QIgx74q4S1Rd56XIiL2cYEdyWn13cunYBIuqh9mpmQr7gGS0IxXoP8R6OaZtNQQLyXSWbd4rXKYUbhFpK5w==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "cac": "^6.7.14",
-        "debug": "^4.3.5",
+        "debug": "^4.3.7",
+        "es-module-lexer": "^1.5.4",
         "pathe": "^1.1.2",
-        "tinyrainbow": "^1.2.0",
         "vite": "^5.0.0"
       },
       "bin": {
@@ -8506,12 +8486,17 @@
       }
     },
     "node_modules/vitefu": {
-      "version": "0.2.5",
-      "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz",
-      "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.3.tgz",
+      "integrity": "sha512-iKKfOMBHob2WxEJbqbJjHAkmYgvFDPhuqrO82om83S8RLk+17FtyMBfcyeH8GqD0ihShtkMW/zzJgiA51hCNCQ==",
       "dev": true,
+      "license": "MIT",
+      "workspaces": [
+        "tests/deps/*",
+        "tests/projects/*"
+      ],
       "peerDependencies": {
-        "vite": "^3.0.0 || ^4.0.0 || ^5.0.0"
+        "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0-beta.0"
       },
       "peerDependenciesMeta": {
         "vite": {
@@ -8520,29 +8505,31 @@
       }
     },
     "node_modules/vitest": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz",
-      "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.5.tgz",
+      "integrity": "sha512-P4ljsdpuzRTPI/kbND2sDZ4VmieerR2c9szEZpjc+98Z9ebvnXmM5+0tHEKqYZumXqlvnmfWsjeFOjXVriDG7A==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@ampproject/remapping": "^2.3.0",
-        "@vitest/expect": "2.0.5",
-        "@vitest/pretty-format": "^2.0.5",
-        "@vitest/runner": "2.0.5",
-        "@vitest/snapshot": "2.0.5",
-        "@vitest/spy": "2.0.5",
-        "@vitest/utils": "2.0.5",
-        "chai": "^5.1.1",
-        "debug": "^4.3.5",
-        "execa": "^8.0.1",
-        "magic-string": "^0.30.10",
+        "@vitest/expect": "2.1.5",
+        "@vitest/mocker": "2.1.5",
+        "@vitest/pretty-format": "^2.1.5",
+        "@vitest/runner": "2.1.5",
+        "@vitest/snapshot": "2.1.5",
+        "@vitest/spy": "2.1.5",
+        "@vitest/utils": "2.1.5",
+        "chai": "^5.1.2",
+        "debug": "^4.3.7",
+        "expect-type": "^1.1.0",
+        "magic-string": "^0.30.12",
         "pathe": "^1.1.2",
-        "std-env": "^3.7.0",
-        "tinybench": "^2.8.0",
-        "tinypool": "^1.0.0",
+        "std-env": "^3.8.0",
+        "tinybench": "^2.9.0",
+        "tinyexec": "^0.3.1",
+        "tinypool": "^1.0.1",
         "tinyrainbow": "^1.2.0",
         "vite": "^5.0.0",
-        "vite-node": "2.0.5",
+        "vite-node": "2.1.5",
         "why-is-node-running": "^2.3.0"
       },
       "bin": {
@@ -8557,8 +8544,8 @@
       "peerDependencies": {
         "@edge-runtime/vm": "*",
         "@types/node": "^18.0.0 || >=20.0.0",
-        "@vitest/browser": "2.0.5",
-        "@vitest/ui": "2.0.5",
+        "@vitest/browser": "2.1.5",
+        "@vitest/ui": "2.1.5",
         "happy-dom": "*",
         "jsdom": "*"
       },
@@ -8795,19 +8782,11 @@
       "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
       "dev": true
     },
-    "node_modules/wrappy": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
-      "dev": true
-    },
     "node_modules/ws": {
-      "version": "8.14.2",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
-      "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
-      "dev": true,
-      "optional": true,
-      "peer": true,
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+      "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+      "license": "MIT",
       "engines": {
         "node": ">=10.0.0"
       },
@@ -8907,6 +8886,12 @@
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
+    },
+    "node_modules/zimmerframe": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
+      "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
+      "license": "MIT"
     }
   }
 }
diff --git a/web/package.json b/web/package.json
index 46b4af599b..2d42de1a8e 100644
--- a/web/package.json
+++ b/web/package.json
@@ -1,6 +1,6 @@
 {
   "name": "immich-web",
-  "version": "1.115.0",
+  "version": "1.123.0",
   "license": "GNU Affero General Public License version 3",
   "scripts": {
     "dev": "vite dev --host 0.0.0.0 --port 3000",
@@ -8,7 +8,7 @@
     "build:stats": "BUILD_STATS=true vite build",
     "package": "svelte-kit package",
     "preview": "vite preview",
-    "check:svelte": "svelte-check --no-tsconfig --fail-on-warnings",
+    "check:svelte": "svelte-check --no-tsconfig --fail-on-warnings --compiler-warnings 'reactive_declaration_non_reactive_property:ignore'",
     "check:typescript": "tsc --noEmit",
     "check:watch": "npm run check:svelte -- --watch",
     "check:code": "npm run format && npm run lint && npm run check:svelte && npm run check:typescript",
@@ -27,26 +27,26 @@
     "@eslint/js": "^9.8.0",
     "@faker-js/faker": "^9.0.0",
     "@socket.io/component-emitter": "^3.1.0",
-    "@sveltejs/adapter-static": "^3.0.1",
-    "@sveltejs/enhanced-img": "^0.3.0",
-    "@sveltejs/kit": "^2.5.18",
-    "@sveltejs/vite-plugin-svelte": "^3.1.2",
+    "@sveltejs/adapter-static": "^3.0.5",
+    "@sveltejs/enhanced-img": "^0.4.0",
+    "@sveltejs/kit": "^2.12.0",
+    "@sveltejs/vite-plugin-svelte": "^4.0.0",
     "@testing-library/jest-dom": "^6.4.2",
-    "@testing-library/svelte": "^5.2.0",
+    "@testing-library/svelte": "^5.2.4",
     "@testing-library/user-event": "^14.5.2",
     "@types/dom-to-image": "^2.6.7",
     "@types/justified-layout": "^4.1.4",
     "@types/lodash-es": "^4.17.12",
     "@types/luxon": "^3.4.2",
-    "@typescript-eslint/eslint-plugin": "^8.0.0",
-    "@typescript-eslint/parser": "^8.0.0",
+    "@typescript-eslint/eslint-plugin": "^8.15.0",
+    "@typescript-eslint/parser": "^8.15.0",
     "@vitest/coverage-v8": "^2.0.5",
     "autoprefixer": "^10.4.17",
     "dotenv": "^16.4.5",
-    "eslint": "^9.0.0",
+    "eslint": "^9.14.0",
     "eslint-config-prettier": "^9.1.0",
-    "eslint-plugin-svelte": "^2.43.0",
-    "eslint-plugin-unicorn": "^55.0.0",
+    "eslint-plugin-svelte": "^2.45.1",
+    "eslint-plugin-unicorn": "^56.0.1",
     "factory.ts": "^1.4.1",
     "globals": "^15.9.0",
     "postcss": "^8.4.35",
@@ -55,38 +55,38 @@
     "prettier-plugin-sort-json": "^4.0.0",
     "prettier-plugin-svelte": "^3.2.6",
     "rollup-plugin-visualizer": "^5.12.0",
-    "svelte": "^4.2.19",
-    "svelte-check": "^4.0.0",
+    "svelte": "^5.1.5",
+    "svelte-check": "^4.0.9",
     "tailwindcss": "^3.4.1",
     "tslib": "^2.6.2",
     "typescript": "^5.5.0",
-    "vite": "^5.1.4",
+    "vite": "^5.4.4",
     "vitest": "^2.0.5"
   },
   "type": "module",
   "dependencies": {
     "@formatjs/icu-messageformat-parser": "^2.7.8",
     "@immich/sdk": "file:../open-api/typescript-sdk",
-    "@mapbox/mapbox-gl-rtl-text": "^0.2.3",
+    "@mapbox/mapbox-gl-rtl-text": "0.2.3",
     "@mdi/js": "^7.4.47",
     "@photo-sphere-viewer/core": "^5.7.1",
     "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
     "@photo-sphere-viewer/video-plugin": "^5.7.2",
-    "@zoom-image/svelte": "^0.2.6",
+    "@zoom-image/svelte": "^0.3.0",
     "dom-to-image": "^2.6.0",
     "handlebars": "^4.7.8",
     "intl-messageformat": "^10.5.14",
     "justified-layout": "^4.1.0",
     "lodash-es": "^4.17.21",
     "luxon": "^3.4.4",
-    "socket.io-client": "^4.7.4",
+    "socket.io-client": "~4.7.5",
     "svelte-gestures": "^5.0.4",
-    "svelte-i18n": "^4.0.0",
+    "svelte-i18n": "^4.0.1",
     "svelte-local-storage-store": "^0.6.4",
     "svelte-maplibre": "^0.9.13",
     "thumbhash": "^0.1.1"
   },
   "volta": {
-    "node": "20.17.0"
+    "node": "22.12.0"
   }
 }
diff --git a/web/src/app.d.ts b/web/src/app.d.ts
index b13a0c97d5..d0d25443c9 100644
--- a/web/src/app.d.ts
+++ b/web/src/app.d.ts
@@ -28,7 +28,7 @@ interface Element {
   requestFullscreen?(options?: FullscreenOptions): Promise<void>;
 }
 
-import type en from '$lib/i18n/en.json';
+import type en from '$i18n/en.json';
 import 'svelte-i18n';
 
 type NestedKeys<T, K = keyof T> = K extends keyof T & string
diff --git a/web/src/app.html b/web/src/app.html
index 778375c1e1..6fd02dc9f8 100644
--- a/web/src/app.html
+++ b/web/src/app.html
@@ -13,6 +13,8 @@
     <link rel="icon" type="image/png" sizes="96x96" href="/favicon-96.png" />
     <link rel="icon" type="image/png" sizes="144x144" href="/favicon-144.png" />
     <link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180.png" />
+    <link rel="preload" as="font" type="font/ttf" href="%app.font%" crossorigin="anonymous" />
+    <link rel="preload" as="font" type="font/ttf" href="%app.monofont%" crossorigin="anonymous" />
     %sveltekit.head%
     <style>
       /* prevent FOUC */
@@ -74,7 +76,7 @@
       if (!theme) {
         theme = { value: 'light', system: true };
       } else if (theme === 'dark' || theme === 'light') {
-        theme = { value: item, system: false };
+        theme = { value: theme, system: false };
         localStorage.setItem(colorThemeKeyName, JSON.stringify(theme));
       } else {
         theme = JSON.parse(theme);
diff --git a/web/src/hooks.server.ts b/web/src/hooks.server.ts
new file mode 100644
index 0000000000..1606f92796
--- /dev/null
+++ b/web/src/hooks.server.ts
@@ -0,0 +1,12 @@
+import overpass from '$lib/assets/fonts/overpass/Overpass.ttf?url';
+import overpassMono from '$lib/assets/fonts/overpass/OverpassMono.ttf?url';
+import type { Handle } from '@sveltejs/kit';
+
+// only used during the build to replace the variables from app.html
+export const handle = (async ({ event, resolve }) => {
+  return resolve(event, {
+    transformPageChunk: ({ html }) => {
+      return html.replace('%app.font%', overpass).replace('%app.monofont%', overpassMono);
+    },
+  });
+}) satisfies Handle;
diff --git a/web/src/lib/__mocks__/animate.mock.ts b/web/src/lib/__mocks__/animate.mock.ts
new file mode 100644
index 0000000000..5f0d367d86
--- /dev/null
+++ b/web/src/lib/__mocks__/animate.mock.ts
@@ -0,0 +1,17 @@
+import { tick } from 'svelte';
+import { vi } from 'vitest';
+
+export const getAnimateMock = () =>
+  vi.fn().mockImplementation(() => {
+    let onfinish: (() => void) | null = null;
+    void tick().then(() => onfinish?.());
+
+    return {
+      set onfinish(fn: () => void) {
+        onfinish = fn;
+      },
+      cancel() {
+        onfinish = null;
+      },
+    };
+  });
diff --git a/web/src/lib/__mocks__/visual-viewport.mock.ts b/web/src/lib/__mocks__/visual-viewport.mock.ts
new file mode 100644
index 0000000000..23903d56cd
--- /dev/null
+++ b/web/src/lib/__mocks__/visual-viewport.mock.ts
@@ -0,0 +1,9 @@
+export const getVisualViewportMock = () => ({
+  height: window.innerHeight,
+  width: window.innerWidth,
+  scale: 1,
+  offsetLeft: 0,
+  offsetTop: 0,
+  addEventListener: vi.fn(),
+  removeEventListener: vi.fn(),
+});
diff --git a/web/src/lib/actions/__test__/focus-trap-test.svelte b/web/src/lib/actions/__test__/focus-trap-test.svelte
index 207c880cd9..e1cb6fa4fb 100644
--- a/web/src/lib/actions/__test__/focus-trap-test.svelte
+++ b/web/src/lib/actions/__test__/focus-trap-test.svelte
@@ -1,16 +1,20 @@
 <script lang="ts">
   import { focusTrap } from '$lib/actions/focus-trap';
 
-  export let show: boolean;
+  interface Props {
+    show: boolean;
+  }
+
+  let { show = $bindable() }: Props = $props();
 </script>
 
-<button type="button" on:click={() => (show = true)}>Open</button>
+<button type="button" onclick={() => (show = true)}>Open</button>
 
 {#if show}
   <div use:focusTrap>
     <div>
       <span>text</span>
-      <button data-testid="one" type="button" on:click={() => (show = false)}>Close</button>
+      <button data-testid="one" type="button" onclick={() => (show = false)}>Close</button>
     </div>
     <input data-testid="two" disabled />
     <input data-testid="three" />
diff --git a/web/src/lib/actions/autogrow.ts b/web/src/lib/actions/autogrow.ts
index ff80454ef3..0e6dec8e81 100644
--- a/web/src/lib/actions/autogrow.ts
+++ b/web/src/lib/actions/autogrow.ts
@@ -1,7 +1,19 @@
-export const autoGrowHeight = (textarea: HTMLTextAreaElement, height = 'auto') => {
-  if (!textarea) {
-    return;
-  }
-  textarea.style.height = height;
-  textarea.style.height = `${textarea.scrollHeight}px`;
+import { tick } from 'svelte';
+import type { Action } from 'svelte/action';
+
+type Parameters = {
+  height?: string;
+  value: string; // added to enable reactivity
+};
+
+export const autoGrowHeight: Action<HTMLTextAreaElement, Parameters> = (textarea, { height = 'auto' }) => {
+  const update = () => {
+    void tick().then(() => {
+      textarea.style.height = height;
+      textarea.style.height = `${textarea.scrollHeight}px`;
+    });
+  };
+
+  update();
+  return { update };
 };
diff --git a/web/src/lib/actions/click-outside.ts b/web/src/lib/actions/click-outside.ts
index bbcb0c405b..1a421f1f56 100644
--- a/web/src/lib/actions/click-outside.ts
+++ b/web/src/lib/actions/click-outside.ts
@@ -6,6 +6,12 @@ interface Options {
   onEscape?: () => void;
 }
 
+/**
+ * Calls a function when a click occurs outside of the element, or when the escape key is pressed.
+ * @param node
+ * @param options Object containing onOutclick and onEscape functions
+ * @returns
+ */
 export function clickOutside(node: HTMLElement, options: Options = {}): ActionReturn {
   const { onOutclick, onEscape } = options;
 
diff --git a/web/src/lib/actions/context-menu-navigation.ts b/web/src/lib/actions/context-menu-navigation.ts
index 3b45e7fe52..89b7b76d24 100644
--- a/web/src/lib/actions/context-menu-navigation.ts
+++ b/web/src/lib/actions/context-menu-navigation.ts
@@ -10,7 +10,7 @@ interface Options {
   /**
    * The container element that with direct children that should be navigated.
    */
-  container: HTMLElement;
+  container?: HTMLElement;
   /**
    * Indicates if the dropdown is open.
    */
@@ -52,7 +52,11 @@ export const contextMenuNavigation: Action<HTMLElement, Options> = (node, option
       await tick();
     }
 
-    const children = Array.from(container?.children).filter((child) => child.tagName !== 'HR') as HTMLElement[];
+    if (!container) {
+      return;
+    }
+
+    const children = Array.from(container.children).filter((child) => child.tagName !== 'HR') as HTMLElement[];
     if (children.length === 0) {
       return;
     }
diff --git a/web/src/lib/actions/focus-outside.ts b/web/src/lib/actions/focus-outside.ts
index 2266ea8f0f..c302e33d4c 100644
--- a/web/src/lib/actions/focus-outside.ts
+++ b/web/src/lib/actions/focus-outside.ts
@@ -2,6 +2,11 @@ interface Options {
   onFocusOut?: (event: FocusEvent) => void;
 }
 
+/**
+ * Calls a function when focus leaves the element.
+ * @param node
+ * @param options Object containing onFocusOut function
+ */
 export function focusOutside(node: HTMLElement, options: Options = {}) {
   const { onFocusOut } = options;
 
diff --git a/web/src/lib/actions/focus.ts b/web/src/lib/actions/focus.ts
index 81185625f7..3b6049f247 100644
--- a/web/src/lib/actions/focus.ts
+++ b/web/src/lib/actions/focus.ts
@@ -1,3 +1,4 @@
+/** Focus the given element when it is mounted. */
 export const initInput = (element: HTMLInputElement) => {
   element.focus();
 };
diff --git a/web/src/lib/actions/intersection-observer.ts b/web/src/lib/actions/intersection-observer.ts
index 700ae0c373..edbc07e5c1 100644
--- a/web/src/lib/actions/intersection-observer.ts
+++ b/web/src/lib/actions/intersection-observer.ts
@@ -13,7 +13,9 @@ type OnIntersectCallback = (entryOrElement: IntersectionObserverEntry | HTMLElem
 type OnSeparateCallback = (element: HTMLElement) => unknown;
 type IntersectionObserverActionProperties = {
   key?: string;
+  /** Function to execute when the element leaves the viewport */
   onSeparate?: OnSeparateCallback;
+  /** Function to execute when the element enters the viewport */
   onIntersect?: OnIntersectCallback;
 
   root?: Element | Document | null;
@@ -112,6 +114,12 @@ function _intersectionObserver(
   };
 }
 
+/**
+ * Monitors an element's visibility in the viewport and calls functions when it enters or leaves (based on a threshold).
+ * @param element
+ * @param properties One or multiple configurations for the IntersectionObserver(s)
+ * @returns
+ */
 export function intersectionObserver(
   element: HTMLElement,
   properties: IntersectionObserverActionProperties | IntersectionObserverActionProperties[],
diff --git a/web/src/lib/actions/list-navigation.ts b/web/src/lib/actions/list-navigation.ts
index b981f67521..cd4214f700 100644
--- a/web/src/lib/actions/list-navigation.ts
+++ b/web/src/lib/actions/list-navigation.ts
@@ -1,8 +1,20 @@
 import { shortcuts } from '$lib/actions/shortcut';
 import type { Action } from 'svelte/action';
 
-export const listNavigation: Action<HTMLElement, HTMLElement> = (node, container: HTMLElement) => {
+/**
+ * Enables keyboard navigation (up and down arrows) for a list of elements.
+ * @param node Element which listens for keyboard events
+ * @param container Element containing the list of elements
+ */
+export const listNavigation: Action<HTMLElement, HTMLElement | undefined> = (
+  node: HTMLElement,
+  container?: HTMLElement,
+) => {
   const moveFocus = (direction: 'up' | 'down') => {
+    if (!container) {
+      return;
+    }
+
     const children = Array.from(container?.children);
     if (children.length === 0) {
       return;
diff --git a/web/src/lib/actions/scroll-memory.ts b/web/src/lib/actions/scroll-memory.ts
new file mode 100644
index 0000000000..1c19fdd8ab
--- /dev/null
+++ b/web/src/lib/actions/scroll-memory.ts
@@ -0,0 +1,87 @@
+import { navigating } from '$app/stores';
+import { AppRoute, SessionStorageKey } from '$lib/constants';
+import { handlePromiseError } from '$lib/utils';
+
+interface Options {
+  /**
+   * {@link AppRoute} for subpages that scroll state should be kept while visiting.
+   *
+   * This must be kept the same in all subpages of this route for the scroll memory clearer to work.
+   */
+  routeStartsWith: AppRoute;
+  /**
+   * Function to clear additional data/state before scrolling (ex infinite scroll).
+   */
+  beforeClear?: () => void;
+}
+
+interface PageOptions extends Options {
+  /**
+   * Function to save additional data/state before scrolling (ex infinite scroll).
+   */
+  beforeSave?: () => void;
+  /**
+   * Function to load additional data/state before scrolling (ex infinite scroll).
+   */
+  beforeScroll?: () => Promise<void>;
+}
+
+/**
+ * @param node The scroll slot element, typically from {@link UserPageLayout}
+ */
+export function scrollMemory(
+  node: HTMLElement,
+  { routeStartsWith, beforeSave, beforeClear, beforeScroll }: PageOptions,
+) {
+  const unsubscribeNavigating = navigating.subscribe((navigation) => {
+    const existingScroll = sessionStorage.getItem(SessionStorageKey.SCROLL_POSITION);
+    if (navigation?.to && !existingScroll) {
+      // Save current scroll information when going into a subpage.
+      if (navigation.to.url.pathname.startsWith(routeStartsWith)) {
+        beforeSave?.();
+        sessionStorage.setItem(SessionStorageKey.SCROLL_POSITION, node.scrollTop.toString());
+      } else {
+        beforeClear?.();
+        sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION);
+      }
+    }
+  });
+
+  handlePromiseError(
+    (async () => {
+      await beforeScroll?.();
+
+      const newScroll = sessionStorage.getItem(SessionStorageKey.SCROLL_POSITION);
+      if (newScroll) {
+        node.scroll({
+          top: Number.parseFloat(newScroll),
+          behavior: 'instant',
+        });
+      }
+      beforeClear?.();
+      sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION);
+    })(),
+  );
+
+  return {
+    destroy() {
+      unsubscribeNavigating();
+    },
+  };
+}
+
+export function scrollMemoryClearer(_node: HTMLElement, { routeStartsWith, beforeClear }: Options) {
+  const unsubscribeNavigating = navigating.subscribe((navigation) => {
+    // Forget scroll position from main page if going somewhere else.
+    if (navigation?.to && !navigation?.to.url.pathname.startsWith(routeStartsWith)) {
+      beforeClear?.();
+      sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION);
+    }
+  });
+
+  return {
+    destroy() {
+      unsubscribeNavigating();
+    },
+  };
+}
diff --git a/web/src/lib/actions/shortcut.ts b/web/src/lib/actions/shortcut.ts
index df155ea821..6348257c40 100644
--- a/web/src/lib/actions/shortcut.ts
+++ b/web/src/lib/actions/shortcut.ts
@@ -10,11 +10,16 @@ export type Shortcut = {
 
 export type ShortcutOptions<T = HTMLElement> = {
   shortcut: Shortcut;
+  /** If true, the event handler will not execute if the event comes from an input field */
   ignoreInputFields?: boolean;
   onShortcut: (event: KeyboardEvent & { currentTarget: T }) => unknown;
   preventDefault?: boolean;
 };
 
+/** Determines whether an event should be ignored. The event will be ignored if:
+ *  - The element dispatching the event is not the same as the element which the event listener is attached to
+ *  - The element dispatching the event is an input field
+ */
 export const shouldIgnoreEvent = (event: KeyboardEvent | ClipboardEvent): boolean => {
   if (event.target === event.currentTarget) {
     return false;
@@ -33,6 +38,7 @@ export const matchesShortcut = (event: KeyboardEvent, shortcut: Shortcut) => {
   );
 };
 
+/** Bind a single keyboard shortcut to node. */
 export const shortcut = <T extends HTMLElement>(
   node: T,
   option: ShortcutOptions<T>,
@@ -47,6 +53,7 @@ export const shortcut = <T extends HTMLElement>(
   };
 };
 
+/** Binds multiple keyboard shortcuts to node */
 export const shortcuts = <T extends HTMLElement>(
   node: T,
   options: ShortcutOptions<T>[],
diff --git a/web/src/lib/actions/thumbhash.ts b/web/src/lib/actions/thumbhash.ts
index ab9d28ffc9..e49f04dbee 100644
--- a/web/src/lib/actions/thumbhash.ts
+++ b/web/src/lib/actions/thumbhash.ts
@@ -1,6 +1,11 @@
 import { decodeBase64 } from '$lib/utils';
 import { thumbHashToRGBA } from 'thumbhash';
 
+/**
+ * Renders a thumbnail onto a canvas from a base64 encoded hash.
+ * @param canvas
+ * @param param1 object containing the base64 encoded hash (base64Thumbhash: yourString)
+ */
 export function thumbhash(canvas: HTMLCanvasElement, { base64ThumbHash }: { base64ThumbHash: string }) {
   const ctx = canvas.getContext('2d');
   if (ctx) {
diff --git a/web/src/lib/actions/use-actions.ts b/web/src/lib/actions/use-actions.ts
new file mode 100644
index 0000000000..762cfdccf7
--- /dev/null
+++ b/web/src/lib/actions/use-actions.ts
@@ -0,0 +1,67 @@
+/**
+ * @license Apache-2.0
+ * https://github.com/hperrin/svelte-material-ui/blob/master/packages/common/src/internal/useActions.ts
+ */
+
+export type SvelteActionReturnType<P> = {
+  update?: (newParams?: P) => void;
+  destroy?: () => void;
+} | void;
+
+export type SvelteHTMLActionType<P> = (node: HTMLElement, params?: P) => SvelteActionReturnType<P>;
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export type HTMLActionEntry<P = any> = SvelteHTMLActionType<P> | [SvelteHTMLActionType<P>, P];
+
+export type HTMLActionArray = HTMLActionEntry[];
+
+export type SvelteSVGActionType<P> = (node: SVGElement, params?: P) => SvelteActionReturnType<P>;
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export type SVGActionEntry<P = any> = SvelteSVGActionType<P> | [SvelteSVGActionType<P>, P];
+
+export type SVGActionArray = SVGActionEntry[];
+
+export type ActionArray = HTMLActionArray | SVGActionArray;
+
+export function useActions(node: HTMLElement | SVGElement, actions: ActionArray) {
+  const actionReturns: SvelteActionReturnType<unknown>[] = [];
+
+  if (actions) {
+    for (const actionEntry of actions) {
+      const action = Array.isArray(actionEntry) ? actionEntry[0] : actionEntry;
+      if (Array.isArray(actionEntry) && actionEntry.length > 1) {
+        actionReturns.push(action(node as HTMLElement & SVGElement, actionEntry[1]));
+      } else {
+        actionReturns.push(action(node as HTMLElement & SVGElement));
+      }
+    }
+  }
+
+  return {
+    update(actions: ActionArray) {
+      if ((actions?.length || 0) != actionReturns.length) {
+        throw new Error('You must not change the length of an actions array.');
+      }
+
+      if (actions) {
+        for (const [i, returnEntry] of actionReturns.entries()) {
+          if (returnEntry && returnEntry.update) {
+            const actionEntry = actions[i];
+            if (Array.isArray(actionEntry) && actionEntry.length > 1) {
+              returnEntry.update(actionEntry[1]);
+            } else {
+              returnEntry.update();
+            }
+          }
+        }
+      }
+    },
+
+    destroy() {
+      for (const returnEntry of actionReturns) {
+        returnEntry?.destroy?.();
+      }
+    },
+  };
+}
diff --git a/web/src/lib/assets/svg-paths.ts b/web/src/lib/assets/svg-paths.ts
index cc8d0a1800..9c37849fcc 100644
--- a/web/src/lib/assets/svg-paths.ts
+++ b/web/src/lib/assets/svg-paths.ts
@@ -4,3 +4,6 @@ export const sunPath =
 
 export const moonViewBox = '0 0 20 20';
 export const sunViewBox = '0 0 20 20';
+
+export const discordPath =
+  'M 9.1367188 3.8691406 C 9.1217187 3.8691406 9.1067969 3.8700938 9.0917969 3.8710938 C 8.9647969 3.8810937 5.9534375 4.1403594 4.0234375 5.6933594 C 3.0154375 6.6253594 1 12.073203 1 16.783203 C 1 16.866203 1.0215 16.946531 1.0625 17.019531 C 2.4535 19.462531 6.2473281 20.102859 7.1113281 20.130859 L 7.1269531 20.130859 C 7.2799531 20.130859 7.4236719 20.057594 7.5136719 19.933594 L 8.3886719 18.732422 C 6.0296719 18.122422 4.8248594 17.086391 4.7558594 17.025391 C 4.5578594 16.850391 4.5378906 16.549563 4.7128906 16.351562 C 4.8068906 16.244563 4.9383125 16.189453 5.0703125 16.189453 C 5.1823125 16.189453 5.2957188 16.228594 5.3867188 16.308594 C 5.4157187 16.334594 7.6340469 18.216797 11.998047 18.216797 C 16.370047 18.216797 18.589328 16.325641 18.611328 16.306641 C 18.702328 16.227641 18.815734 16.189453 18.927734 16.189453 C 19.059734 16.189453 19.190156 16.243562 19.285156 16.351562 C 19.459156 16.549563 19.441141 16.851391 19.244141 17.025391 C 19.174141 17.087391 17.968375 18.120469 15.609375 18.730469 L 16.484375 19.933594 C 16.574375 20.057594 16.718094 20.130859 16.871094 20.130859 L 16.886719 20.130859 C 17.751719 20.103859 21.5465 19.463531 22.9375 17.019531 C 22.9785 16.947531 23 16.866203 23 16.783203 C 23 12.073203 20.984172 6.624875 19.951172 5.671875 C 18.047172 4.140875 15.036203 3.8820937 14.908203 3.8710938 C 14.895203 3.8700938 14.880188 3.8691406 14.867188 3.8691406 C 14.681188 3.8691406 14.510594 3.9793906 14.433594 4.1503906 C 14.427594 4.1623906 14.362062 4.3138281 14.289062 4.5488281 C 15.548063 4.7608281 17.094141 5.1895937 18.494141 6.0585938 C 18.718141 6.1975938 18.787437 6.4917969 18.648438 6.7167969 C 18.558438 6.8627969 18.402188 6.9433594 18.242188 6.9433594 C 18.156188 6.9433594 18.069234 6.9200937 17.990234 6.8710938 C 15.584234 5.3800938 12.578 5.3046875 12 5.3046875 C 11.422 5.3046875 8.4157187 5.3810469 6.0117188 6.8730469 C 5.9327188 6.9210469 5.8457656 6.9433594 5.7597656 6.9433594 C 5.5997656 6.9433594 5.4425625 6.86475 5.3515625 6.71875 C 5.2115625 6.49375 5.2818594 6.1985938 5.5058594 6.0585938 C 6.9058594 5.1905937 8.4528906 4.7627812 9.7128906 4.5507812 C 9.6388906 4.3147813 9.5714062 4.1643437 9.5664062 4.1523438 C 9.4894063 3.9813438 9.3217188 3.8691406 9.1367188 3.8691406 z M 12 7.3046875 C 12.296 7.3046875 14.950594 7.3403125 16.933594 8.5703125 C 17.326594 8.8143125 17.777234 8.9453125 18.240234 8.9453125 C 18.633234 8.9453125 19.010656 8.8555 19.347656 8.6875 C 19.964656 10.2405 20.690828 12.686219 20.923828 15.199219 C 20.883828 15.143219 20.840922 15.089109 20.794922 15.037109 C 20.324922 14.498109 19.644687 14.191406 18.929688 14.191406 C 18.332687 14.191406 17.754078 14.405437 17.330078 14.773438 C 17.257078 14.832437 15.505 16.21875 12 16.21875 C 8.496 16.21875 6.7450313 14.834687 6.7070312 14.804688 C 6.2540312 14.407687 5.6742656 14.189453 5.0722656 14.189453 C 4.3612656 14.189453 3.6838438 14.494391 3.2148438 15.025391 C 3.1658438 15.080391 3.1201719 15.138266 3.0761719 15.197266 C 3.3091719 12.686266 4.0344375 10.235594 4.6484375 8.6835938 C 4.9864375 8.8525938 5.3657656 8.9433594 5.7597656 8.9433594 C 6.2217656 8.9433594 6.6724531 8.8143125 7.0644531 8.5703125 C 9.0494531 7.3393125 11.704 7.3046875 12 7.3046875 z M 8.890625 10.044922 C 7.966625 10.044922 7.2167969 10.901031 7.2167969 11.957031 C 7.2167969 13.013031 7.965625 13.869141 8.890625 13.869141 C 9.815625 13.869141 10.564453 13.013031 10.564453 11.957031 C 10.564453 10.900031 9.815625 10.044922 8.890625 10.044922 z M 15.109375 10.044922 C 14.185375 10.044922 13.435547 10.901031 13.435547 11.957031 C 13.435547 13.013031 14.184375 13.869141 15.109375 13.869141 C 16.034375 13.869141 16.783203 13.013031 16.783203 11.957031 C 16.783203 10.900031 16.033375 10.044922 15.109375 10.044922 z';
diff --git a/web/src/lib/components/admin-page/delete-confirm-dialogue.svelte b/web/src/lib/components/admin-page/delete-confirm-dialogue.svelte
index e8490339a6..6eb603263e 100644
--- a/web/src/lib/components/admin-page/delete-confirm-dialogue.svelte
+++ b/web/src/lib/components/admin-page/delete-confirm-dialogue.svelte
@@ -1,25 +1,25 @@
 <script lang="ts">
+  import Checkbox from '$lib/components/elements/checkbox.svelte';
+  import FormatMessage from '$lib/components/i18n/format-message.svelte';
   import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
+  import { serverConfig } from '$lib/stores/server-config.store';
   import { handleError } from '$lib/utils/handle-error';
   import { deleteUserAdmin, type UserResponseDto } from '@immich/sdk';
-  import { serverConfig } from '$lib/stores/server-config.store';
-  import { createEventDispatcher } from 'svelte';
-  import Checkbox from '$lib/components/elements/checkbox.svelte';
   import { t } from 'svelte-i18n';
-  import FormatMessage from '$lib/components/i18n/format-message.svelte';
 
-  export let user: UserResponseDto;
+  interface Props {
+    user: UserResponseDto;
+    onSuccess: () => void;
+    onFail: () => void;
+    onCancel: () => void;
+  }
 
-  let forceDelete = false;
-  let deleteButtonDisabled = false;
+  let { user, onSuccess, onFail, onCancel }: Props = $props();
+
+  let forceDelete = $state(false);
+  let deleteButtonDisabled = $state(false);
   let userIdInput: string = '';
 
-  const dispatch = createEventDispatcher<{
-    success: void;
-    fail: void;
-    cancel: void;
-  }>();
-
   const handleDeleteUser = async () => {
     try {
       const { deletedAt } = await deleteUserAdmin({
@@ -28,13 +28,13 @@
       });
 
       if (deletedAt == undefined) {
-        dispatch('fail');
+        onFail();
       } else {
-        dispatch('success');
+        onSuccess();
       }
     } catch (error) {
       handleError(error, $t('errors.unable_to_delete_user'));
-      dispatch('fail');
+      onFail();
     }
   };
 
@@ -48,15 +48,17 @@
   title={$t('delete_user')}
   confirmText={forceDelete ? $t('permanently_delete') : $t('delete')}
   onConfirm={handleDeleteUser}
-  onCancel={() => dispatch('cancel')}
+  {onCancel}
   disabled={deleteButtonDisabled}
 >
-  <svelte:fragment slot="prompt">
+  {#snippet promptSnippet()}
     <div class="flex flex-col gap-4">
       {#if forceDelete}
         <p>
-          <FormatMessage key="admin.user_delete_immediately" values={{ user: user.name }} let:message>
-            <b>{message}</b>
+          <FormatMessage key="admin.user_delete_immediately" values={{ user: user.name }}>
+            {#snippet children({ message })}
+              <b>{message}</b>
+            {/snippet}
           </FormatMessage>
         </p>
       {:else}
@@ -64,9 +66,10 @@
           <FormatMessage
             key="admin.user_delete_delay"
             values={{ user: user.name, delay: $serverConfig.userDeleteDelay }}
-            let:message
           >
-            <b>{message}</b>
+            {#snippet children({ message })}
+              <b>{message}</b>
+            {/snippet}
           </FormatMessage>
         </p>
       {/if}
@@ -77,7 +80,7 @@
           label={$t('admin.user_delete_immediately_checkbox')}
           labelClass="text-sm dark:text-immich-dark-fg"
           bind:checked={forceDelete}
-          on:change={() => {
+          onchange={() => {
             deleteButtonDisabled = forceDelete;
           }}
         />
@@ -96,9 +99,9 @@
           aria-describedby="confirm-user-desc"
           name="confirm-user-id"
           type="text"
-          on:input={handleConfirm}
+          oninput={handleConfirm}
         />
       {/if}
     </div>
-  </svelte:fragment>
+  {/snippet}
 </ConfirmDialog>
diff --git a/web/src/lib/components/admin-page/jobs/job-tile-button.svelte b/web/src/lib/components/admin-page/jobs/job-tile-button.svelte
index 0aa90ed4d8..f71d8a3e44 100644
--- a/web/src/lib/components/admin-page/jobs/job-tile-button.svelte
+++ b/web/src/lib/components/admin-page/jobs/job-tile-button.svelte
@@ -1,14 +1,23 @@
-<script lang="ts" context="module">
-  export type Colors = 'light-gray' | 'gray';
+<script lang="ts" module>
+  export type Colors = 'light-gray' | 'gray' | 'dark-gray';
 </script>
 
 <script lang="ts">
-  export let color: Colors;
-  export let disabled = false;
+  import type { Snippet } from 'svelte';
+
+  interface Props {
+    color: Colors;
+    disabled?: boolean;
+    children?: Snippet;
+    onClick?: () => void;
+  }
+
+  let { color, disabled = false, onClick = () => {}, children }: Props = $props();
 
   const colorClasses: Record<Colors, string> = {
-    'light-gray': 'bg-gray-300/90 dark:bg-gray-600/90',
-    gray: 'bg-gray-300 dark:bg-gray-600',
+    'light-gray': 'bg-gray-300/80 dark:bg-gray-700',
+    gray: 'bg-gray-300/90 dark:bg-gray-700/90',
+    'dark-gray': 'bg-gray-300 dark:bg-gray-700/80',
   };
 
   const hoverClasses = disabled
@@ -22,7 +31,7 @@
   class="flex h-full w-full flex-col place-content-center place-items-center gap-2 px-8 py-2 text-xs text-gray-600 transition-colors dark:text-gray-200 {colorClasses[
     color
   ]} {hoverClasses}"
-  on:click
+  onclick={onClick}
 >
-  <slot />
+  {@render children?.()}
 </button>
diff --git a/web/src/lib/components/admin-page/jobs/job-tile-status.svelte b/web/src/lib/components/admin-page/jobs/job-tile-status.svelte
index ca36764797..5bffa45b89 100644
--- a/web/src/lib/components/admin-page/jobs/job-tile-status.svelte
+++ b/web/src/lib/components/admin-page/jobs/job-tile-status.svelte
@@ -1,9 +1,16 @@
-<script lang="ts" context="module">
+<script lang="ts" module>
   export type Color = 'success' | 'warning';
 </script>
 
 <script lang="ts">
-  export let color: Color;
+  import type { Snippet } from 'svelte';
+
+  interface Props {
+    color: Color;
+    children?: Snippet;
+  }
+
+  let { color, children }: Props = $props();
 
   const colorClasses: Record<Color, string> = {
     success: 'bg-green-500/70 text-gray-900 dark:bg-green-700/90 dark:text-gray-100',
@@ -12,5 +19,5 @@
 </script>
 
 <div class="w-full p-2 text-center text-sm {colorClasses[color]}">
-  <slot />
+  {@render children?.()}
 </div>
diff --git a/web/src/lib/components/admin-page/jobs/job-tile.svelte b/web/src/lib/components/admin-page/jobs/job-tile.svelte
index 5d26d1174f..0e39647c75 100644
--- a/web/src/lib/components/admin-page/jobs/job-tile.svelte
+++ b/web/src/lib/components/admin-page/jobs/job-tile.svelte
@@ -1,5 +1,6 @@
 <script lang="ts">
   import Badge from '$lib/components/elements/badge.svelte';
+  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import Icon from '$lib/components/elements/icon.svelte';
   import { locale } from '$lib/stores/preferences.store';
   import { JobCommand, type JobCommandDto, type JobCountsDto, type QueueStatusDto } from '@immich/sdk';
@@ -8,34 +9,49 @@
     mdiAllInclusive,
     mdiClose,
     mdiFastForward,
+    mdiImageRefreshOutline,
     mdiPause,
     mdiPlay,
     mdiSelectionSearch,
   } from '@mdi/js';
-  import { createEventDispatcher, type ComponentType } from 'svelte';
+  import { type Component } from 'svelte';
+  import { t } from 'svelte-i18n';
   import JobTileButton from './job-tile-button.svelte';
   import JobTileStatus from './job-tile-status.svelte';
-  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
-  import { t } from 'svelte-i18n';
 
-  export let title: string;
-  export let subtitle: string | undefined;
-  export let description: ComponentType | undefined;
-  export let jobCounts: JobCountsDto;
-  export let queueStatus: QueueStatusDto;
-  export let allowForceCommand = true;
-  export let icon: string;
-  export let disabled = false;
+  interface Props {
+    title: string;
+    subtitle: string | undefined;
+    description: Component | undefined;
+    jobCounts: JobCountsDto;
+    queueStatus: QueueStatusDto;
+    icon: string;
+    disabled?: boolean;
+    allText: string | undefined;
+    refreshText: string | undefined;
+    missingText: string;
+    onCommand: (command: JobCommandDto) => void;
+  }
 
-  export let allText: string;
-  export let missingText: string;
+  let {
+    title,
+    subtitle,
+    description,
+    jobCounts,
+    queueStatus,
+    icon,
+    disabled = false,
+    allText,
+    refreshText,
+    missingText,
+    onCommand,
+  }: Props = $props();
 
-  $: waitingCount = jobCounts.waiting + jobCounts.paused + jobCounts.delayed;
-  $: isIdle = !queueStatus.isActive && !queueStatus.isPaused;
+  let waitingCount = $derived(jobCounts.waiting + jobCounts.paused + jobCounts.delayed);
+  let isIdle = $derived(!queueStatus.isActive && !queueStatus.isPaused);
+  let multipleButtons = $derived(allText || refreshText);
 
   const commonClasses = 'flex place-items-center justify-between w-full py-2 sm:py-4 pr-4 pl-6';
-
-  const dispatch = createEventDispatcher<{ command: JobCommandDto }>();
 </script>
 
 <div
@@ -66,7 +82,7 @@
                   title={$t('clear_message')}
                   size="12"
                   padding="1"
-                  on:click={() => dispatch('command', { command: JobCommand.ClearFailed, force: false })}
+                  onclick={() => onCommand({ command: JobCommand.ClearFailed, force: false })}
                 />
               </div>
             </Badge>
@@ -86,8 +102,9 @@
       {/if}
 
       {#if description}
+        {@const SvelteComponent = description}
         <div class="text-sm dark:text-white">
-          <svelte:component this={description} />
+          <SvelteComponent />
         </div>
       {/if}
 
@@ -117,54 +134,56 @@
       <JobTileButton
         disabled={true}
         color="light-gray"
-        on:click={() => dispatch('command', { command: JobCommand.Start, force: false })}
+        onClick={() => onCommand({ command: JobCommand.Start, force: false })}
       >
         <Icon path={mdiAlertCircle} size="36" />
         {$t('disabled').toUpperCase()}
       </JobTileButton>
-    {:else if !isIdle}
+    {/if}
+
+    {#if !disabled && !isIdle}
       {#if waitingCount > 0}
-        <JobTileButton color="gray" on:click={() => dispatch('command', { command: JobCommand.Empty, force: false })}>
+        <JobTileButton color="gray" onClick={() => onCommand({ command: JobCommand.Empty, force: false })}>
           <Icon path={mdiClose} size="24" />
           {$t('clear').toUpperCase()}
         </JobTileButton>
       {/if}
       {#if queueStatus.isPaused}
         {@const size = waitingCount > 0 ? '24' : '48'}
-        <JobTileButton
-          color="light-gray"
-          on:click={() => dispatch('command', { command: JobCommand.Resume, force: false })}
-        >
+        <JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Resume, force: false })}>
           <!-- size property is not reactive, so have to use width and height -->
           <Icon path={mdiFastForward} {size} />
           {$t('resume').toUpperCase()}
         </JobTileButton>
       {:else}
-        <JobTileButton
-          color="light-gray"
-          on:click={() => dispatch('command', { command: JobCommand.Pause, force: false })}
-        >
+        <JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Pause, force: false })}>
           <Icon path={mdiPause} size="24" />
           {$t('pause').toUpperCase()}
         </JobTileButton>
       {/if}
-    {:else if allowForceCommand}
-      <JobTileButton color="gray" on:click={() => dispatch('command', { command: JobCommand.Start, force: true })}>
-        <Icon path={mdiAllInclusive} size="24" />
-        {allText}
-      </JobTileButton>
-      <JobTileButton
-        color="light-gray"
-        on:click={() => dispatch('command', { command: JobCommand.Start, force: false })}
-      >
+    {/if}
+
+    {#if !disabled && multipleButtons && isIdle}
+      {#if allText}
+        <JobTileButton color="dark-gray" onClick={() => onCommand({ command: JobCommand.Start, force: true })}>
+          <Icon path={mdiAllInclusive} size="24" />
+          {allText}
+        </JobTileButton>
+      {/if}
+      {#if refreshText}
+        <JobTileButton color="gray" onClick={() => onCommand({ command: JobCommand.Start, force: undefined })}>
+          <Icon path={mdiImageRefreshOutline} size="24" />
+          {refreshText}
+        </JobTileButton>
+      {/if}
+      <JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Start, force: false })}>
         <Icon path={mdiSelectionSearch} size="24" />
         {missingText}
       </JobTileButton>
-    {:else}
-      <JobTileButton
-        color="light-gray"
-        on:click={() => dispatch('command', { command: JobCommand.Start, force: false })}
-      >
+    {/if}
+
+    {#if !disabled && !multipleButtons && isIdle}
+      <JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Start, force: false })}>
         <Icon path={mdiPlay} size="48" />
         {$t('start').toUpperCase()}
       </JobTileButton>
diff --git a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte
index cd9855eea2..9b4f3ffdd6 100644
--- a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte
+++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte
@@ -19,23 +19,27 @@
     mdiTagFaces,
     mdiVideo,
   } from '@mdi/js';
-  import type { ComponentType } from 'svelte';
+  import type { Component } from 'svelte';
   import JobTile from './job-tile.svelte';
   import StorageMigrationDescription from './storage-migration-description.svelte';
   import { dialogController } from '$lib/components/shared-components/dialog/dialog';
   import { t } from 'svelte-i18n';
 
-  export let jobs: AllJobStatusResponseDto;
+  interface Props {
+    jobs: AllJobStatusResponseDto;
+  }
+
+  let { jobs = $bindable() }: Props = $props();
 
   interface JobDetails {
     title: string;
     subtitle?: string;
-    description?: ComponentType;
+    description?: Component;
     allText?: string;
-    missingText?: string;
+    refreshText?: string;
+    missingText: string;
     disabled?: boolean;
     icon: string;
-    allowForceCommand?: boolean;
     handleCommand?: (jobId: JobName, jobCommand: JobCommandDto) => Promise<void>;
   }
 
@@ -56,48 +60,59 @@
     await handleCommand(jobId, dto);
   };
 
-  $: jobDetails = <Partial<Record<JobName, JobDetails>>>{
+  let jobDetails: Partial<Record<JobName, JobDetails>> = {
     [JobName.ThumbnailGeneration]: {
       icon: mdiFileJpgBox,
       title: $getJobName(JobName.ThumbnailGeneration),
       subtitle: $t('admin.thumbnail_generation_job_description'),
+      allText: $t('all'),
+      missingText: $t('missing'),
     },
     [JobName.MetadataExtraction]: {
       icon: mdiTable,
       title: $getJobName(JobName.MetadataExtraction),
       subtitle: $t('admin.metadata_extraction_job_description'),
+      allText: $t('all'),
+      missingText: $t('missing'),
     },
     [JobName.Library]: {
       icon: mdiLibraryShelves,
       title: $getJobName(JobName.Library),
       subtitle: $t('admin.library_tasks_description'),
-      allText: $t('all').toUpperCase(),
-      missingText: $t('refresh').toUpperCase(),
+      allText: $t('all'),
+      missingText: $t('refresh'),
     },
     [JobName.Sidecar]: {
       title: $getJobName(JobName.Sidecar),
       icon: mdiFileXmlBox,
       subtitle: $t('admin.sidecar_job_description'),
-      allText: $t('sync').toUpperCase(),
-      missingText: $t('discover').toUpperCase(),
+      allText: $t('sync'),
+      missingText: $t('discover'),
       disabled: !$featureFlags.sidecar,
     },
     [JobName.SmartSearch]: {
       icon: mdiImageSearch,
       title: $getJobName(JobName.SmartSearch),
       subtitle: $t('admin.smart_search_job_description'),
+      allText: $t('all'),
+      missingText: $t('missing'),
       disabled: !$featureFlags.smartSearch,
     },
     [JobName.DuplicateDetection]: {
       icon: mdiContentDuplicate,
       title: $getJobName(JobName.DuplicateDetection),
       subtitle: $t('admin.duplicate_detection_job_description'),
+      allText: $t('all'),
+      missingText: $t('missing'),
       disabled: !$featureFlags.duplicateDetection,
     },
     [JobName.FaceDetection]: {
       icon: mdiFaceRecognition,
       title: $getJobName(JobName.FaceDetection),
       subtitle: $t('admin.face_detection_description'),
+      allText: $t('reset'),
+      refreshText: $t('refresh'),
+      missingText: $t('missing'),
       handleCommand: handleConfirmCommand,
       disabled: !$featureFlags.facialRecognition,
     },
@@ -105,6 +120,8 @@
       icon: mdiTagFaces,
       title: $getJobName(JobName.FacialRecognition),
       subtitle: $t('admin.facial_recognition_job_description'),
+      allText: $t('reset'),
+      missingText: $t('missing'),
       handleCommand: handleConfirmCommand,
       disabled: !$featureFlags.facialRecognition,
     },
@@ -112,21 +129,24 @@
       icon: mdiVideo,
       title: $getJobName(JobName.VideoConversion),
       subtitle: $t('admin.video_conversion_job_description'),
+      allText: $t('all'),
+      missingText: $t('missing'),
     },
     [JobName.StorageTemplateMigration]: {
       icon: mdiFolderMove,
       title: $getJobName(JobName.StorageTemplateMigration),
-      allowForceCommand: false,
+      missingText: $t('missing'),
       description: StorageMigrationDescription,
     },
     [JobName.Migration]: {
       icon: mdiFolderMove,
       title: $getJobName(JobName.Migration),
       subtitle: $t('admin.migration_job_description'),
-      allowForceCommand: false,
+      missingText: $t('missing'),
     },
   };
-  $: jobList = Object.entries(jobDetails) as [JobName, JobDetails][];
+
+  let jobList = Object.entries(jobDetails) as [JobName, JobDetails][];
 
   async function handleCommand(jobId: JobName, jobCommand: JobCommandDto) {
     const title = jobDetails[jobId]?.title;
@@ -150,7 +170,7 @@
 </script>
 
 <div class="flex flex-col gap-7">
-  {#each jobList as [jobName, { title, subtitle, description, disabled, allText, missingText, allowForceCommand, icon, handleCommand: handleCommandOverride }]}
+  {#each jobList as [jobName, { title, subtitle, description, disabled, allText, refreshText, missingText, icon, handleCommand: handleCommandOverride }]}
     {@const { jobCounts, queueStatus } = jobs[jobName]}
     <JobTile
       {icon}
@@ -158,12 +178,12 @@
       {disabled}
       {subtitle}
       {description}
-      allText={allText || $t('all').toUpperCase()}
-      missingText={missingText || $t('missing').toUpperCase()}
-      {allowForceCommand}
+      allText={allText?.toUpperCase()}
+      refreshText={refreshText?.toUpperCase()}
+      missingText={missingText.toUpperCase()}
       {jobCounts}
       {queueStatus}
-      on:command={({ detail }) => (handleCommandOverride || handleCommand)(jobName, detail)}
+      onCommand={(command) => (handleCommandOverride || handleCommand)(jobName, command)}
     />
   {/each}
 </div>
diff --git a/web/src/lib/components/admin-page/jobs/storage-migration-description.svelte b/web/src/lib/components/admin-page/jobs/storage-migration-description.svelte
index 8a74d2c5ad..b47df1daae 100644
--- a/web/src/lib/components/admin-page/jobs/storage-migration-description.svelte
+++ b/web/src/lib/components/admin-page/jobs/storage-migration-description.svelte
@@ -7,12 +7,13 @@
 <FormatMessage
   key="admin.storage_template_migration_description"
   values={{ template: $t('admin.storage_template_settings') }}
-  let:message
 >
-  <a
-    href="{AppRoute.ADMIN_SETTINGS}?{QueryParameter.IS_OPEN}={OpenSettingQueryParameterValue.STORAGE_TEMPLATE}"
-    class="text-immich-primary dark:text-immich-dark-primary"
-  >
-    {message}
-  </a>
+  {#snippet children({ message })}
+    <a
+      href="{AppRoute.ADMIN_SETTINGS}?{QueryParameter.IS_OPEN}={OpenSettingQueryParameterValue.STORAGE_TEMPLATE}"
+      class="text-immich-primary dark:text-immich-dark-primary"
+    >
+      {message}
+    </a>
+  {/snippet}
 </FormatMessage>
diff --git a/web/src/lib/components/admin-page/restore-dialogue.svelte b/web/src/lib/components/admin-page/restore-dialogue.svelte
index 9b274d2c2f..a72ada2ca5 100644
--- a/web/src/lib/components/admin-page/restore-dialogue.svelte
+++ b/web/src/lib/components/admin-page/restore-dialogue.svelte
@@ -3,28 +3,28 @@
   import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
   import { handleError } from '$lib/utils/handle-error';
   import { restoreUserAdmin, type UserResponseDto } from '@immich/sdk';
-  import { createEventDispatcher } from 'svelte';
   import { t } from 'svelte-i18n';
 
-  export let user: UserResponseDto;
+  interface Props {
+    user: UserResponseDto;
+    onSuccess: () => void;
+    onFail: () => void;
+    onCancel: () => void;
+  }
 
-  const dispatch = createEventDispatcher<{
-    success: void;
-    fail: void;
-    cancel: void;
-  }>();
+  let { user, onSuccess, onFail, onCancel }: Props = $props();
 
   const handleRestoreUser = async () => {
     try {
       const { deletedAt } = await restoreUserAdmin({ id: user.id });
       if (deletedAt == undefined) {
-        dispatch('success');
+        onSuccess();
       } else {
-        dispatch('fail');
+        onFail();
       }
     } catch (error) {
       handleError(error, $t('errors.unable_to_restore_user'));
-      dispatch('fail');
+      onFail();
     }
   };
 </script>
@@ -34,13 +34,15 @@
   confirmText={$t('continue')}
   confirmColor="green"
   onConfirm={handleRestoreUser}
-  onCancel={() => dispatch('cancel')}
+  {onCancel}
 >
-  <svelte:fragment slot="prompt">
+  {#snippet promptSnippet()}
     <p>
-      <FormatMessage key="admin.user_restore_description" values={{ user: user.name }} let:message>
-        <b>{message}</b>
+      <FormatMessage key="admin.user_restore_description" values={{ user: user.name }}>
+        {#snippet children({ message })}
+          <b>{message}</b>
+        {/snippet}
       </FormatMessage>
     </p>
-  </svelte:fragment>
+  {/snippet}
 </ConfirmDialog>
diff --git a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte
index 35afc0962d..bb288511ac 100644
--- a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte
+++ b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte
@@ -7,14 +7,22 @@
   import StatsCard from './stats-card.svelte';
   import { t } from 'svelte-i18n';
 
-  export let stats: ServerStatsResponseDto = {
-    photos: 0,
-    videos: 0,
-    usage: 0,
-    usageByUser: [],
-  };
+  interface Props {
+    stats?: ServerStatsResponseDto;
+  }
 
-  $: zeros = (value: number) => {
+  let {
+    stats = {
+      photos: 0,
+      videos: 0,
+      usage: 0,
+      usagePhotos: 0,
+      usageVideos: 0,
+      usageByUser: [],
+    },
+  }: Props = $props();
+
+  const zeros = (value: number) => {
     const maxLength = 13;
     const valueLength = value.toString().length;
     const zeroLength = maxLength - valueLength;
@@ -23,7 +31,7 @@
   };
 
   const TiB = 1024 ** 4;
-  $: [statsUsage, statsUsageUnit] = getBytesWithUnit(stats.usage, stats.usage > TiB ? 2 : 0);
+  let [statsUsage, statsUsageUnit] = $derived(getBytesWithUnit(stats.usage, stats.usage > TiB ? 2 : 0));
 </script>
 
 <div class="flex flex-col gap-5">
@@ -99,8 +107,12 @@
             class="flex h-[50px] w-full place-items-center text-center odd:bg-immich-gray even:bg-immich-bg odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50"
           >
             <td class="w-1/4 text-ellipsis px-2 text-sm">{user.userName}</td>
-            <td class="w-1/4 text-ellipsis px-2 text-sm">{user.photos.toLocaleString($locale)}</td>
-            <td class="w-1/4 text-ellipsis px-2 text-sm">{user.videos.toLocaleString($locale)}</td>
+            <td class="w-1/4 text-ellipsis px-2 text-sm"
+              >{user.photos.toLocaleString($locale)} ({getByteUnitString(user.usagePhotos, $locale, 0)})</td
+            >
+            <td class="w-1/4 text-ellipsis px-2 text-sm"
+              >{user.videos.toLocaleString($locale)} ({getByteUnitString(user.usageVideos, $locale, 0)})</td
+            >
             <td class="w-1/4 text-ellipsis px-2 text-sm">
               {getByteUnitString(user.usage, $locale, 0)}
               {#if user.quotaSizeInBytes}
diff --git a/web/src/lib/components/admin-page/server-stats/stats-card.svelte b/web/src/lib/components/admin-page/server-stats/stats-card.svelte
index 31baa0afdd..14d32c055f 100644
--- a/web/src/lib/components/admin-page/server-stats/stats-card.svelte
+++ b/web/src/lib/components/admin-page/server-stats/stats-card.svelte
@@ -2,18 +2,22 @@
   import Icon from '$lib/components/elements/icon.svelte';
   import { ByteUnit } from '$lib/utils/byte-units';
 
-  export let icon: string;
-  export let title: string;
-  export let value: number;
-  export let unit: ByteUnit | undefined = undefined;
+  interface Props {
+    icon: string;
+    title: string;
+    value: number;
+    unit?: ByteUnit | undefined;
+  }
 
-  $: zeros = () => {
+  let { icon, title, value, unit = undefined }: Props = $props();
+
+  const zeros = $derived(() => {
     const maxLength = 13;
     const valueLength = value.toString().length;
     const zeroLength = maxLength - valueLength;
 
     return '0'.repeat(zeroLength);
-  };
+  });
 </script>
 
 <div class="flex h-[140px] w-[250px] flex-col justify-between rounded-3xl bg-immich-gray p-5 dark:bg-immich-dark-gray">
diff --git a/web/src/lib/components/admin-page/settings/admin-settings.svelte b/web/src/lib/components/admin-page/settings/admin-settings.svelte
index 19a8580d6b..199db0b571 100644
--- a/web/src/lib/components/admin-page/settings/admin-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/admin-settings.svelte
@@ -1,5 +1,3 @@
-<svelte:options accessors />
-
 <script lang="ts">
   import {
     NotificationType,
@@ -13,12 +11,17 @@
   import type { SettingsResetOptions } from './admin-settings';
   import { t } from 'svelte-i18n';
 
-  export let config: SystemConfigDto;
+  interface Props {
+    config: SystemConfigDto;
+    children: import('svelte').Snippet<[{ savedConfig: SystemConfigDto; defaultConfig: SystemConfigDto }]>;
+  }
 
-  let savedConfig: SystemConfigDto;
-  let defaultConfig: SystemConfigDto;
+  let { config = $bindable(), children }: Props = $props();
 
-  const handleReset = async (options: SettingsResetOptions) => {
+  let savedConfig: SystemConfigDto | undefined = $state();
+  let defaultConfig: SystemConfigDto | undefined = $state();
+
+  export const handleReset = async (options: SettingsResetOptions) => {
     await (options.default ? resetToDefault(options.configKeys) : reset(options.configKeys));
   };
 
@@ -26,7 +29,8 @@
     let systemConfigDto = {
       ...savedConfig,
       ...update,
-    };
+    } as SystemConfigDto;
+
     if (isEqual(systemConfigDto, savedConfig)) {
       return;
     }
@@ -59,6 +63,10 @@
   };
 
   const resetToDefault = (configKeys: Array<keyof SystemConfigDto>) => {
+    if (!defaultConfig) {
+      return;
+    }
+
     for (const key of configKeys) {
       config = { ...config, [key]: defaultConfig[key] };
     }
@@ -75,5 +83,5 @@
 </script>
 
 {#if savedConfig && defaultConfig}
-  <slot {handleReset} {handleSave} {savedConfig} {defaultConfig} />
+  {@render children({ savedConfig, defaultConfig })}
 {/if}
diff --git a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte
index 37f875c604..5380a76286 100644
--- a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte
@@ -2,9 +2,7 @@
   import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
   import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
   import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
-  import SettingInputField, {
-    SettingInputFieldType,
-  } from '$lib/components/shared-components/settings/setting-input-field.svelte';
+  import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
   import { type SystemConfigDto } from '@immich/sdk';
   import { isEqual } from 'lodash-es';
@@ -12,21 +10,26 @@
   import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
   import { t } from 'svelte-i18n';
   import FormatMessage from '$lib/components/i18n/format-message.svelte';
+  import { SettingInputFieldType } from '$lib/constants';
 
-  export let savedConfig: SystemConfigDto;
-  export let defaultConfig: SystemConfigDto;
-  export let config: SystemConfigDto; // this is the config that is being edited
-  export let disabled = false;
-  export let onReset: SettingsResetEvent;
-  export let onSave: SettingsSaveEvent;
+  interface Props {
+    savedConfig: SystemConfigDto;
+    defaultConfig: SystemConfigDto;
+    config: SystemConfigDto;
+    disabled?: boolean;
+    onReset: SettingsResetEvent;
+    onSave: SettingsSaveEvent;
+  }
 
-  let isConfirmOpen = false;
+  let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
+
+  let isConfirmOpen = $state(false);
 
   const handleToggleOverride = () => {
     // click runs before bind
     const previouslyEnabled = config.oauth.mobileOverrideEnabled;
     if (!previouslyEnabled && !config.oauth.mobileRedirectUri) {
-      config.oauth.mobileRedirectUri = window.location.origin + '/api/oauth/mobile-redirect';
+      config.oauth.mobileRedirectUri = globalThis.location.origin + '/api/oauth/mobile-redirect';
     }
   };
 
@@ -48,30 +51,32 @@
     onCancel={() => (isConfirmOpen = false)}
     onConfirm={() => handleSave(true)}
   >
-    <svelte:fragment slot="prompt">
+    {#snippet promptSnippet()}
       <div class="flex flex-col gap-4">
         <p>{$t('admin.authentication_settings_disable_all')}</p>
         <p>
-          <FormatMessage key="admin.authentication_settings_reenable" let:message>
-            <a
-              href="https://immich.app/docs/administration/server-commands"
-              rel="noreferrer"
-              target="_blank"
-              class="underline"
-            >
-              {message}
-            </a>
+          <FormatMessage key="admin.authentication_settings_reenable">
+            {#snippet children({ message })}
+              <a
+                href="https://immich.app/docs/administration/server-commands"
+                rel="noreferrer"
+                target="_blank"
+                class="underline"
+              >
+                {message}
+              </a>
+            {/snippet}
           </FormatMessage>
         </p>
       </div>
-    </svelte:fragment>
+    {/snippet}
   </ConfirmDialog>
 {/if}
 
 <div>
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault>
-      <div class="ml-4 mt-4 flex flex-col gap-4">
+    <form autocomplete="off" onsubmit={(e) => e.preventDefault()}>
+      <div class="ml-4 mt-4 flex flex-col">
         <SettingAccordion
           key="oauth"
           title={$t('admin.oauth_settings')}
@@ -79,15 +84,17 @@
         >
           <div class="ml-4 mt-4 flex flex-col gap-4">
             <p class="text-sm dark:text-immich-dark-fg">
-              <FormatMessage key="admin.oauth_settings_more_details" let:message>
-                <a
-                  href="https://immich.app/docs/administration/oauth"
-                  class="underline"
-                  target="_blank"
-                  rel="noreferrer"
-                >
-                  {message}
-                </a>
+              <FormatMessage key="admin.oauth_settings_more_details">
+                {#snippet children({ message })}
+                  <a
+                    href="https://immich.app/docs/administration/oauth"
+                    class="underline"
+                    target="_blank"
+                    rel="noreferrer"
+                  >
+                    {message}
+                  </a>
+                {/snippet}
               </FormatMessage>
             </p>
 
@@ -147,7 +154,7 @@
               <SettingInputField
                 inputType={SettingInputFieldType.TEXT}
                 label={$t('admin.oauth_profile_signing_algorithm').toUpperCase()}
-                desc={$t('admin.oauth_profile_signing_algorithm_description')}
+                description={$t('admin.oauth_profile_signing_algorithm_description')}
                 bind:value={config.oauth.profileSigningAlgorithm}
                 required={true}
                 disabled={disabled || !config.oauth.enabled}
@@ -157,7 +164,7 @@
               <SettingInputField
                 inputType={SettingInputFieldType.TEXT}
                 label={$t('admin.oauth_storage_label_claim').toUpperCase()}
-                desc={$t('admin.oauth_storage_label_claim_description')}
+                description={$t('admin.oauth_storage_label_claim_description')}
                 bind:value={config.oauth.storageLabelClaim}
                 required={true}
                 disabled={disabled || !config.oauth.enabled}
@@ -167,7 +174,7 @@
               <SettingInputField
                 inputType={SettingInputFieldType.TEXT}
                 label={$t('admin.oauth_storage_quota_claim').toUpperCase()}
-                desc={$t('admin.oauth_storage_quota_claim_description')}
+                description={$t('admin.oauth_storage_quota_claim_description')}
                 bind:value={config.oauth.storageQuotaClaim}
                 required={true}
                 disabled={disabled || !config.oauth.enabled}
@@ -177,7 +184,7 @@
               <SettingInputField
                 inputType={SettingInputFieldType.NUMBER}
                 label={$t('admin.oauth_storage_quota_default').toUpperCase()}
-                desc={$t('admin.oauth_storage_quota_default_description')}
+                description={$t('admin.oauth_storage_quota_default_description')}
                 bind:value={config.oauth.defaultStorageQuota}
                 required={true}
                 disabled={disabled || !config.oauth.enabled}
@@ -213,7 +220,7 @@
                   values: { callback: 'app.immich:///oauth-callback' },
                 })}
                 disabled={disabled || !config.oauth.enabled}
-                on:click={() => handleToggleOverride()}
+                onToggle={() => handleToggleOverride()}
                 bind:checked={config.oauth.mobileOverrideEnabled}
               />
 
diff --git a/web/src/lib/components/admin-page/settings/backup-settings/backup-settings.svelte b/web/src/lib/components/admin-page/settings/backup-settings/backup-settings.svelte
new file mode 100644
index 0000000000..3ec477e29c
--- /dev/null
+++ b/web/src/lib/components/admin-page/settings/backup-settings/backup-settings.svelte
@@ -0,0 +1,100 @@
+<script lang="ts">
+  import type { SystemConfigDto } from '@immich/sdk';
+  import { isEqual } from 'lodash-es';
+  import { fade } from 'svelte/transition';
+  import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
+  import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
+  import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
+  import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
+  import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
+  import { t } from 'svelte-i18n';
+  import FormatMessage from '$lib/components/i18n/format-message.svelte';
+  import { SettingInputFieldType } from '$lib/constants';
+
+  interface Props {
+    savedConfig: SystemConfigDto;
+    defaultConfig: SystemConfigDto;
+    config: SystemConfigDto;
+    disabled?: boolean;
+    onReset: SettingsResetEvent;
+    onSave: SettingsSaveEvent;
+  }
+
+  let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
+
+  let cronExpressionOptions = $derived([
+    { text: $t('interval.night_at_midnight'), value: '0 0 * * *' },
+    { text: $t('interval.night_at_twoam'), value: '0 02 * * *' },
+    { text: $t('interval.day_at_onepm'), value: '0 13 * * *' },
+    { text: $t('interval.hours', { values: { hours: 6 } }), value: '0 */6 * * *' },
+  ]);
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+  };
+</script>
+
+<div>
+  <div in:fade={{ duration: 500 }}>
+    <form autocomplete="off" {onsubmit}>
+      <div class="ml-4 mt-4 flex flex-col gap-4">
+        <SettingSwitch
+          title={$t('admin.backup_database_enable_description')}
+          {disabled}
+          bind:checked={config.backup.database.enabled}
+        />
+
+        <SettingSelect
+          options={cronExpressionOptions}
+          disabled={disabled || !config.backup.database.enabled}
+          name="expression"
+          label={$t('admin.cron_expression_presets')}
+          bind:value={config.backup.database.cronExpression}
+        />
+
+        <SettingInputField
+          inputType={SettingInputFieldType.TEXT}
+          required={true}
+          disabled={disabled || !config.backup.database.enabled}
+          label={$t('admin.cron_expression')}
+          bind:value={config.backup.database.cronExpression}
+          isEdited={config.backup.database.cronExpression !== savedConfig.backup.database.cronExpression}
+        >
+          {#snippet descriptionSnippet()}
+            <p class="text-sm dark:text-immich-dark-fg">
+              <FormatMessage key="admin.cron_expression_description">
+                {#snippet children({ message })}
+                  <a
+                    href="https://crontab.guru/#{config.backup.database.cronExpression.replaceAll(' ', '_')}"
+                    class="underline"
+                    target="_blank"
+                    rel="noreferrer"
+                  >
+                    {message}
+                    <br />
+                  </a>
+                {/snippet}
+              </FormatMessage>
+            </p>
+          {/snippet}
+        </SettingInputField>
+
+        <SettingInputField
+          inputType={SettingInputFieldType.NUMBER}
+          required={true}
+          label={$t('admin.backup_keep_last_amount')}
+          disabled={disabled || !config.backup.database.enabled}
+          bind:value={config.backup.database.keepLastAmount}
+          isEdited={config.backup.database.keepLastAmount !== savedConfig.backup.database.keepLastAmount}
+        />
+
+        <SettingButtonsRow
+          onReset={(options) => onReset({ ...options, configKeys: ['backup'] })}
+          onSave={() => onSave({ backup: config.backup })}
+          showResetToDefault={!isEqual(savedConfig.backup, defaultConfig.backup)}
+          {disabled}
+        />
+      </div>
+    </form>
+  </div>
+</div>
diff --git a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte
index 7ddb71cbde..702ec1c171 100644
--- a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte
@@ -15,44 +15,53 @@
   import { fade } from 'svelte/transition';
   import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
   import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
-  import SettingInputField, {
-    SettingInputFieldType,
-  } from '$lib/components/shared-components/settings/setting-input-field.svelte';
+  import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
   import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
   import SettingCheckboxes from '$lib/components/shared-components/settings/setting-checkboxes.svelte';
   import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
   import { t } from 'svelte-i18n';
   import FormatMessage from '$lib/components/i18n/format-message.svelte';
+  import { SettingInputFieldType } from '$lib/constants';
 
-  export let savedConfig: SystemConfigDto;
-  export let defaultConfig: SystemConfigDto;
-  export let config: SystemConfigDto; // this is the config that is being edited
-  export let disabled = false;
-  export let onReset: SettingsResetEvent;
-  export let onSave: SettingsSaveEvent;
+  interface Props {
+    savedConfig: SystemConfigDto;
+    defaultConfig: SystemConfigDto;
+    config: SystemConfigDto;
+    disabled?: boolean;
+    onReset: SettingsResetEvent;
+    onSave: SettingsSaveEvent;
+  }
+
+  let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+  };
 </script>
 
 <div>
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault>
+    <form autocomplete="off" {onsubmit}>
       <div class="ml-4 mt-4 flex flex-col gap-4">
         <p class="text-sm dark:text-immich-dark-fg">
           <Icon path={mdiHelpCircleOutline} class="inline" size="15" />
-          <FormatMessage key="admin.transcoding_codecs_learn_more" let:tag let:message>
-            {#if tag === 'h264-link'}
-              <a href="https://trac.ffmpeg.org/wiki/Encode/H.264" class="underline" target="_blank" rel="noreferrer">
-                {message}
-              </a>
-            {:else if tag === 'hevc-link'}
-              <a href="https://trac.ffmpeg.org/wiki/Encode/H.265" class="underline" target="_blank" rel="noreferrer">
-                {message}
-              </a>
-            {:else if tag === 'vp9-link'}
-              <a href="https://trac.ffmpeg.org/wiki/Encode/VP9" class="underline" target="_blank" rel="noreferrer">
-                {message}
-              </a>
-            {/if}
+          <FormatMessage key="admin.transcoding_codecs_learn_more">
+            {#snippet children({ tag, message })}
+              {#if tag === 'h264-link'}
+                <a href="https://trac.ffmpeg.org/wiki/Encode/H.264" class="underline" target="_blank" rel="noreferrer">
+                  {message}
+                </a>
+              {:else if tag === 'hevc-link'}
+                <a href="https://trac.ffmpeg.org/wiki/Encode/H.265" class="underline" target="_blank" rel="noreferrer">
+                  {message}
+                </a>
+              {:else if tag === 'vp9-link'}
+                <a href="https://trac.ffmpeg.org/wiki/Encode/VP9" class="underline" target="_blank" rel="noreferrer">
+                  {message}
+                </a>
+              {/if}
+            {/snippet}
           </FormatMessage>
         </p>
 
@@ -60,7 +69,7 @@
           inputType={SettingInputFieldType.NUMBER}
           {disabled}
           label={$t('admin.transcoding_constant_rate_factor')}
-          desc={$t('admin.transcoding_constant_rate_factor_description')}
+          description={$t('admin.transcoding_constant_rate_factor_description')}
           bind:value={config.ffmpeg.crf}
           required={true}
           isEdited={config.ffmpeg.crf !== savedConfig.ffmpeg.crf}
@@ -99,9 +108,10 @@
           ]}
           name="vcodec"
           isEdited={config.ffmpeg.targetVideoCodec !== savedConfig.ffmpeg.targetVideoCodec}
-          on:select={() => (config.ffmpeg.acceptedVideoCodecs = [config.ffmpeg.targetVideoCodec])}
+          onSelect={() => (config.ffmpeg.acceptedVideoCodecs = [config.ffmpeg.targetVideoCodec])}
         />
 
+        <!-- PCM is excluded here since it's a bad choice for users storage-wise -->
         <SettingSelect
           label={$t('admin.transcoding_audio_codec')}
           {disabled}
@@ -114,7 +124,7 @@
           ]}
           name="acodec"
           isEdited={config.ffmpeg.targetAudioCodec !== savedConfig.ffmpeg.targetAudioCodec}
-          on:select={() =>
+          onSelect={() =>
             config.ffmpeg.acceptedAudioCodecs.includes(config.ffmpeg.targetAudioCodec)
               ? null
               : config.ffmpeg.acceptedAudioCodecs.push(config.ffmpeg.targetAudioCodec)}
@@ -145,6 +155,7 @@
             { value: AudioCodec.Aac, text: 'AAC' },
             { value: AudioCodec.Mp3, text: 'MP3' },
             { value: AudioCodec.Libopus, text: 'Opus' },
+            { value: AudioCodec.PcmS16Le, text: 'PCM (16 bit)' },
           ]}
           isEdited={!isEqual(sortBy(config.ffmpeg.acceptedAudioCodecs), sortBy(savedConfig.ffmpeg.acceptedAudioCodecs))}
         />
@@ -184,7 +195,7 @@
           inputType={SettingInputFieldType.TEXT}
           {disabled}
           label={$t('admin.transcoding_max_bitrate')}
-          desc={$t('admin.transcoding_max_bitrate_description')}
+          description={$t('admin.transcoding_max_bitrate_description')}
           bind:value={config.ffmpeg.maxBitrate}
           isEdited={config.ffmpeg.maxBitrate !== savedConfig.ffmpeg.maxBitrate}
         />
@@ -193,7 +204,7 @@
           inputType={SettingInputFieldType.NUMBER}
           {disabled}
           label={$t('admin.transcoding_threads')}
-          desc={$t('admin.transcoding_threads_description')}
+          description={$t('admin.transcoding_threads_description')}
           bind:value={config.ffmpeg.threads}
           isEdited={config.ffmpeg.threads !== savedConfig.ffmpeg.threads}
         />
@@ -327,7 +338,7 @@
             <SettingInputField
               inputType={SettingInputFieldType.TEXT}
               label={$t('admin.transcoding_preferred_hardware_device')}
-              desc={$t('admin.transcoding_preferred_hardware_device_description')}
+              description={$t('admin.transcoding_preferred_hardware_device_description')}
               bind:value={config.ffmpeg.preferredHwDevice}
               isEdited={config.ffmpeg.preferredHwDevice !== savedConfig.ffmpeg.preferredHwDevice}
               {disabled}
@@ -341,19 +352,10 @@
           subtitle={$t('admin.transcoding_advanced_options_description')}
         >
           <div class="ml-4 mt-4 flex flex-col gap-4">
-            <SettingInputField
-              inputType={SettingInputFieldType.NUMBER}
-              label={$t('admin.transcoding_tone_mapping_npl')}
-              desc={$t('admin.transcoding_tone_mapping_npl_description')}
-              bind:value={config.ffmpeg.npl}
-              isEdited={config.ffmpeg.npl !== savedConfig.ffmpeg.npl}
-              {disabled}
-            />
-
             <SettingInputField
               inputType={SettingInputFieldType.NUMBER}
               label={$t('admin.transcoding_max_b_frames')}
-              desc={$t('admin.transcoding_max_b_frames_description')}
+              description={$t('admin.transcoding_max_b_frames_description')}
               bind:value={config.ffmpeg.bframes}
               isEdited={config.ffmpeg.bframes !== savedConfig.ffmpeg.bframes}
               {disabled}
@@ -362,7 +364,7 @@
             <SettingInputField
               inputType={SettingInputFieldType.NUMBER}
               label={$t('admin.transcoding_reference_frames')}
-              desc={$t('admin.transcoding_reference_frames_description')}
+              description={$t('admin.transcoding_reference_frames_description')}
               bind:value={config.ffmpeg.refs}
               isEdited={config.ffmpeg.refs !== savedConfig.ffmpeg.refs}
               {disabled}
@@ -371,7 +373,7 @@
             <SettingInputField
               inputType={SettingInputFieldType.NUMBER}
               label={$t('admin.transcoding_max_keyframe_interval')}
-              desc={$t('admin.transcoding_max_keyframe_interval_description')}
+              description={$t('admin.transcoding_max_keyframe_interval_description')}
               bind:value={config.ffmpeg.gopSize}
               isEdited={config.ffmpeg.gopSize !== savedConfig.ffmpeg.gopSize}
               {disabled}
diff --git a/web/src/lib/components/admin-page/settings/image/image-settings.svelte b/web/src/lib/components/admin-page/settings/image/image-settings.svelte
index a7b47920fd..2f2bcbca64 100644
--- a/web/src/lib/components/admin-page/settings/image/image-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/image/image-settings.svelte
@@ -7,96 +7,136 @@
 
   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
   import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
-  import SettingInputField, {
-    SettingInputFieldType,
-  } from '$lib/components/shared-components/settings/setting-input-field.svelte';
+  import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
   import { t } from 'svelte-i18n';
+  import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
+  import { SettingInputFieldType } from '$lib/constants';
 
-  export let savedConfig: SystemConfigDto;
-  export let defaultConfig: SystemConfigDto;
-  export let config: SystemConfigDto; // this is the config that is being edited
-  export let disabled = false;
-  export let onReset: SettingsResetEvent;
-  export let onSave: SettingsSaveEvent;
+  interface Props {
+    savedConfig: SystemConfigDto;
+    defaultConfig: SystemConfigDto;
+    config: SystemConfigDto;
+    disabled?: boolean;
+    onReset: SettingsResetEvent;
+    onSave: SettingsSaveEvent;
+    openByDefault?: boolean;
+  }
+
+  let {
+    savedConfig,
+    defaultConfig,
+    config = $bindable(),
+    disabled = false,
+    onReset,
+    onSave,
+    openByDefault = false,
+  }: Props = $props();
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+  };
 </script>
 
 <div>
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault>
+    <form autocomplete="off" {onsubmit}>
       <div class="ml-4 mt-4 flex flex-col gap-4">
-        <SettingSelect
-          label={$t('admin.image_thumbnail_format')}
-          desc={$t('admin.image_format_description')}
-          bind:value={config.image.thumbnailFormat}
-          options={[
-            { value: ImageFormat.Jpeg, text: 'JPEG' },
-            { value: ImageFormat.Webp, text: 'WebP' },
-          ]}
-          name="format"
-          isEdited={config.image.thumbnailFormat !== savedConfig.image.thumbnailFormat}
-          {disabled}
-        />
+        <SettingAccordion
+          key="thumbnail-settings"
+          title={$t('admin.image_thumbnail_title')}
+          subtitle={$t('admin.image_thumbnail_description')}
+          isOpen={openByDefault}
+        >
+          <SettingSelect
+            label={$t('admin.image_format')}
+            desc={$t('admin.image_format_description')}
+            bind:value={config.image.thumbnail.format}
+            options={[
+              { value: ImageFormat.Jpeg, text: 'JPEG' },
+              { value: ImageFormat.Webp, text: 'WebP' },
+            ]}
+            name="format"
+            isEdited={config.image.thumbnail.format !== savedConfig.image.thumbnail.format}
+            {disabled}
+          />
 
-        <SettingSelect
-          label={$t('admin.image_thumbnail_resolution')}
-          desc={$t('admin.image_thumbnail_resolution_description')}
-          number
-          bind:value={config.image.thumbnailSize}
-          options={[
-            { value: 1080, text: '1080p' },
-            { value: 720, text: '720p' },
-            { value: 480, text: '480p' },
-            { value: 250, text: '250p' },
-            { value: 200, text: '200p' },
-          ]}
-          name="resolution"
-          isEdited={config.image.thumbnailSize !== savedConfig.image.thumbnailSize}
-          {disabled}
-        />
+          <SettingSelect
+            label={$t('admin.image_resolution')}
+            desc={$t('admin.image_resolution_description')}
+            number
+            bind:value={config.image.thumbnail.size}
+            options={[
+              { value: 1080, text: '1080p' },
+              { value: 720, text: '720p' },
+              { value: 480, text: '480p' },
+              { value: 250, text: '250p' },
+              { value: 200, text: '200p' },
+            ]}
+            name="resolution"
+            isEdited={config.image.thumbnail.size !== savedConfig.image.thumbnail.size}
+            {disabled}
+          />
 
-        <SettingSelect
-          label={$t('admin.image_preview_format')}
-          desc={$t('admin.image_format_description')}
-          bind:value={config.image.previewFormat}
-          options={[
-            { value: ImageFormat.Jpeg, text: 'JPEG' },
-            { value: ImageFormat.Webp, text: 'WebP' },
-          ]}
-          name="format"
-          isEdited={config.image.previewFormat !== savedConfig.image.previewFormat}
-          {disabled}
-        />
+          <SettingInputField
+            inputType={SettingInputFieldType.NUMBER}
+            label={$t('admin.image_quality')}
+            description={$t('admin.image_thumbnail_quality_description')}
+            bind:value={config.image.thumbnail.quality}
+            isEdited={config.image.thumbnail.quality !== savedConfig.image.thumbnail.quality}
+            {disabled}
+          />
+        </SettingAccordion>
 
-        <SettingSelect
-          label={$t('admin.image_preview_resolution')}
-          desc={$t('admin.image_preview_resolution_description')}
-          number
-          bind:value={config.image.previewSize}
-          options={[
-            { value: 2160, text: '4K' },
-            { value: 1440, text: '1440p' },
-            { value: 1080, text: '1080p' },
-            { value: 720, text: '720p' },
-          ]}
-          name="resolution"
-          isEdited={config.image.previewSize !== savedConfig.image.previewSize}
-          {disabled}
-        />
+        <SettingAccordion
+          key="preview-settings"
+          title={$t('admin.image_preview_title')}
+          subtitle={$t('admin.image_preview_description')}
+          isOpen={openByDefault}
+        >
+          <SettingSelect
+            label={$t('admin.image_format')}
+            desc={$t('admin.image_format_description')}
+            bind:value={config.image.preview.format}
+            options={[
+              { value: ImageFormat.Jpeg, text: 'JPEG' },
+              { value: ImageFormat.Webp, text: 'WebP' },
+            ]}
+            name="format"
+            isEdited={config.image.preview.format !== savedConfig.image.preview.format}
+            {disabled}
+          />
 
-        <SettingInputField
-          inputType={SettingInputFieldType.NUMBER}
-          label={$t('admin.image_quality')}
-          desc={$t('admin.image_quality_description')}
-          bind:value={config.image.quality}
-          isEdited={config.image.quality !== savedConfig.image.quality}
-          {disabled}
-        />
+          <SettingSelect
+            label={$t('admin.image_resolution')}
+            desc={$t('admin.image_resolution_description')}
+            number
+            bind:value={config.image.preview.size}
+            options={[
+              { value: 2160, text: '4K' },
+              { value: 1440, text: '1440p' },
+              { value: 1080, text: '1080p' },
+              { value: 720, text: '720p' },
+            ]}
+            name="resolution"
+            isEdited={config.image.preview.size !== savedConfig.image.preview.size}
+            {disabled}
+          />
+
+          <SettingInputField
+            inputType={SettingInputFieldType.NUMBER}
+            label={$t('admin.image_quality')}
+            description={$t('admin.image_preview_quality_description')}
+            bind:value={config.image.preview.quality}
+            isEdited={config.image.preview.quality !== savedConfig.image.preview.quality}
+            {disabled}
+          />
+        </SettingAccordion>
 
         <SettingSwitch
           title={$t('admin.image_prefer_wide_gamut')}
           subtitle={$t('admin.image_prefer_wide_gamut_setting_description')}
           checked={config.image.colorspace === Colorspace.P3}
-          on:toggle={(e) => (config.image.colorspace = e.detail ? Colorspace.P3 : Colorspace.Srgb)}
+          onToggle={(isChecked) => (config.image.colorspace = isChecked ? Colorspace.P3 : Colorspace.Srgb)}
           isEdited={config.image.colorspace !== savedConfig.image.colorspace}
           {disabled}
         />
@@ -105,7 +145,7 @@
           title={$t('admin.image_prefer_embedded_preview')}
           subtitle={$t('admin.image_prefer_embedded_preview_setting_description')}
           checked={config.image.extractEmbedded}
-          on:toggle={() => (config.image.extractEmbedded = !config.image.extractEmbedded)}
+          onToggle={() => (config.image.extractEmbedded = !config.image.extractEmbedded)}
           isEdited={config.image.extractEmbedded !== savedConfig.image.extractEmbedded}
           {disabled}
         />
diff --git a/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte b/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte
index e09fde8bae..356de6ae86 100644
--- a/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte
@@ -5,17 +5,20 @@
   import { fade } from 'svelte/transition';
   import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
   import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
-  import SettingInputField, {
-    SettingInputFieldType,
-  } from '$lib/components/shared-components/settings/setting-input-field.svelte';
+  import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
   import { t } from 'svelte-i18n';
+  import { SettingInputFieldType } from '$lib/constants';
 
-  export let savedConfig: SystemConfigDto;
-  export let defaultConfig: SystemConfigDto;
-  export let config: SystemConfigDto; // this is the config that is being edited
-  export let disabled = false;
-  export let onReset: SettingsResetEvent;
-  export let onSave: SettingsSaveEvent;
+  interface Props {
+    savedConfig: SystemConfigDto;
+    defaultConfig: SystemConfigDto;
+    config: SystemConfigDto;
+    disabled?: boolean;
+    onReset: SettingsResetEvent;
+    onSave: SettingsSaveEvent;
+  }
+
+  let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
 
   const jobNames = [
     JobName.ThumbnailGeneration,
@@ -34,11 +37,15 @@
   function isSystemConfigJobDto(jobName: any): jobName is keyof SystemConfigJobDto {
     return jobName in config.job;
   }
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+  };
 </script>
 
 <div>
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault>
+    <form autocomplete="off" {onsubmit}>
       {#each jobNames as jobName}
         <div class="ml-4 mt-4 flex flex-col gap-4">
           {#if isSystemConfigJobDto(jobName)}
@@ -46,7 +53,7 @@
               inputType={SettingInputFieldType.NUMBER}
               {disabled}
               label={$t('admin.job_concurrency', { values: { job: $getJobName(jobName) } })}
-              desc=""
+              description=""
               bind:value={config.job[jobName].concurrency}
               required={true}
               isEdited={!(config.job[jobName].concurrency == savedConfig.job[jobName].concurrency)}
@@ -55,7 +62,7 @@
             <SettingInputField
               inputType={SettingInputFieldType.NUMBER}
               label={$t('admin.job_concurrency', { values: { job: $getJobName(jobName) } })}
-              desc=""
+              description=""
               value="1"
               disabled={true}
               title={$t('admin.job_not_concurrency_safe')}
diff --git a/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte b/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte
index 68867a2a49..b1012c0287 100644
--- a/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte
@@ -4,38 +4,55 @@
   import { fade } from 'svelte/transition';
   import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
   import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
-  import SettingInputField, {
-    SettingInputFieldType,
-  } from '$lib/components/shared-components/settings/setting-input-field.svelte';
+  import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
   import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
   import { t } from 'svelte-i18n';
   import FormatMessage from '$lib/components/i18n/format-message.svelte';
+  import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
+  import { SettingInputFieldType } from '$lib/constants';
 
-  export let savedConfig: SystemConfigDto;
-  export let defaultConfig: SystemConfigDto;
-  export let config: SystemConfigDto; // this is the config that is being edited
-  export let disabled = false;
-  export let onReset: SettingsResetEvent;
-  export let onSave: SettingsSaveEvent;
+  interface Props {
+    savedConfig: SystemConfigDto;
+    defaultConfig: SystemConfigDto;
+    config: SystemConfigDto;
+    disabled?: boolean;
+    onReset: SettingsResetEvent;
+    onSave: SettingsSaveEvent;
+    openByDefault?: boolean;
+  }
 
-  $: cronExpressionOptions = [
-    { title: $t('interval.night_at_midnight'), expression: '0 0 * * *' },
-    { title: $t('interval.night_at_twoam'), expression: '0 2 * * *' },
-    { title: $t('interval.day_at_onepm'), expression: '0 13 * * *' },
-    { title: $t('interval.hours', { values: { hours: 6 } }), expression: '0 */6 * * *' },
-  ];
+  let {
+    savedConfig,
+    defaultConfig,
+    config = $bindable(),
+    disabled = false,
+    onReset,
+    onSave,
+    openByDefault = false,
+  }: Props = $props();
+
+  let cronExpressionOptions = $derived([
+    { text: $t('interval.night_at_midnight'), value: '0 0 * * *' },
+    { text: $t('interval.night_at_twoam'), value: '0 2 * * *' },
+    { text: $t('interval.day_at_onepm'), value: '0 13 * * *' },
+    { text: $t('interval.hours', { values: { hours: 6 } }), value: '0 */6 * * *' },
+  ]);
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+  };
 </script>
 
 <div>
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault>
+    <form autocomplete="off" {onsubmit}>
       <div class="ml-4 mt-4 flex flex-col gap-4">
         <SettingAccordion
           key="library-watching"
           title={$t('admin.library_watching_settings')}
           subtitle={$t('admin.library_watching_settings_description')}
-          isOpen
+          isOpen={openByDefault}
         >
           <div class="ml-4 mt-4 flex flex-col gap-4">
             <SettingSwitch
@@ -50,7 +67,7 @@
           key="library-scanning"
           title={$t('admin.library_scanning')}
           subtitle={$t('admin.library_scanning_description')}
-          isOpen
+          isOpen={openByDefault}
         >
           <div class="ml-4 mt-4 flex flex-col gap-4">
             <SettingSwitch
@@ -59,43 +76,38 @@
               bind:checked={config.library.scan.enabled}
             />
 
-            <div class="flex flex-col my-2 dark:text-immich-dark-fg">
-              <label
-                class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm"
-                for="expression-select"
-              >
-                {$t('admin.library_cron_expression_presets')}
-              </label>
-              <select
-                class="p-2 mt-2 text-sm rounded-lg bg-slate-200 hover:cursor-pointer dark:bg-gray-600"
-                disabled={disabled || !config.library.scan.enabled}
-                name="expression"
-                id="expression-select"
-                bind:value={config.library.scan.cronExpression}
-              >
-                {#each cronExpressionOptions as { title, expression }}
-                  <option value={expression}>{title}</option>
-                {/each}
-              </select>
-            </div>
+            <SettingSelect
+              options={cronExpressionOptions}
+              disabled={disabled || !config.library.scan.enabled}
+              name="expression"
+              label={$t('admin.cron_expression_presets')}
+              bind:value={config.library.scan.cronExpression}
+            />
 
             <SettingInputField
               inputType={SettingInputFieldType.TEXT}
               required={true}
               disabled={disabled || !config.library.scan.enabled}
-              label={$t('admin.library_cron_expression')}
+              label={$t('admin.cron_expression')}
               bind:value={config.library.scan.cronExpression}
               isEdited={config.library.scan.cronExpression !== savedConfig.library.scan.cronExpression}
             >
-              <svelte:fragment slot="desc">
+              {#snippet descriptionSnippet()}
                 <p class="text-sm dark:text-immich-dark-fg">
-                  <FormatMessage key="admin.library_cron_expression_description" let:message>
-                    <a href="https://crontab.guru" class="underline" target="_blank" rel="noreferrer">
-                      {message}
-                    </a>
+                  <FormatMessage key="admin.cron_expression_description">
+                    {#snippet children({ message })}
+                      <a
+                        href="https://crontab.guru/#{config.library.scan.cronExpression.replaceAll(' ', '_')}"
+                        class="underline"
+                        target="_blank"
+                        rel="noreferrer"
+                      >
+                        {message}
+                      </a>
+                    {/snippet}
                   </FormatMessage>
                 </p>
-              </svelte:fragment>
+              {/snippet}
             </SettingInputField>
           </div>
         </SettingAccordion>
diff --git a/web/src/lib/components/admin-page/settings/logging-settings/logging-settings.svelte b/web/src/lib/components/admin-page/settings/logging-settings/logging-settings.svelte
index 6e71ba926c..29a1c65162 100644
--- a/web/src/lib/components/admin-page/settings/logging-settings/logging-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/logging-settings/logging-settings.svelte
@@ -8,17 +8,25 @@
   import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
   import { t } from 'svelte-i18n';
 
-  export let savedConfig: SystemConfigDto;
-  export let defaultConfig: SystemConfigDto;
-  export let config: SystemConfigDto; // this is the config that is being edited
-  export let disabled = false;
-  export let onReset: SettingsResetEvent;
-  export let onSave: SettingsSaveEvent;
+  interface Props {
+    savedConfig: SystemConfigDto;
+    defaultConfig: SystemConfigDto;
+    config: SystemConfigDto;
+    disabled?: boolean;
+    onReset: SettingsResetEvent;
+    onSave: SettingsSaveEvent;
+  }
+
+  let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+  };
 </script>
 
 <div>
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault>
+    <form autocomplete="off" {onsubmit}>
       <div class="ml-4 mt-4 flex flex-col gap-4">
         <SettingSwitch
           title={$t('admin.logging_enable_description')}
diff --git a/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte
index 05a5224bd0..90131d7238 100644
--- a/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte
@@ -5,26 +5,36 @@
   import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
   import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
   import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
-  import SettingInputField, {
-    SettingInputFieldType,
-  } from '$lib/components/shared-components/settings/setting-input-field.svelte';
+  import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
   import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
   import { featureFlags } from '$lib/stores/server-config.store';
   import { t } from 'svelte-i18n';
   import FormatMessage from '$lib/components/i18n/format-message.svelte';
+  import { SettingInputFieldType } from '$lib/constants';
+  import Button from '$lib/components/elements/buttons/button.svelte';
+  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
+  import { mdiMinusCircle } from '@mdi/js';
 
-  export let savedConfig: SystemConfigDto;
-  export let defaultConfig: SystemConfigDto;
-  export let config: SystemConfigDto; // this is the config that is being edited
-  export let disabled = false;
-  export let onReset: SettingsResetEvent;
-  export let onSave: SettingsSaveEvent;
+  interface Props {
+    savedConfig: SystemConfigDto;
+    defaultConfig: SystemConfigDto;
+    config: SystemConfigDto;
+    disabled?: boolean;
+    onReset: SettingsResetEvent;
+    onSave: SettingsSaveEvent;
+  }
+
+  let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+  };
 </script>
 
 <div class="mt-2">
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault class="mx-4 mt-4">
+    <form autocomplete="off" {onsubmit} class="mx-4 mt-4">
       <div class="flex flex-col gap-4">
         <SettingSwitch
           title={$t('admin.machine_learning_enabled')}
@@ -35,15 +45,42 @@
 
         <hr />
 
-        <SettingInputField
-          inputType={SettingInputFieldType.TEXT}
-          label={$t('url')}
-          desc={$t('admin.machine_learning_url_description')}
-          bind:value={config.machineLearning.url}
-          required={true}
-          disabled={disabled || !config.machineLearning.enabled}
-          isEdited={config.machineLearning.url !== savedConfig.machineLearning.url}
-        />
+        <div>
+          {#each config.machineLearning.urls as _, i}
+            {#snippet removeButton()}
+              {#if config.machineLearning.urls.length > 1}
+                <CircleIconButton
+                  size="24"
+                  class="ml-2"
+                  padding="2"
+                  color="red"
+                  title=""
+                  onclick={() => config.machineLearning.urls.splice(i, 1)}
+                  icon={mdiMinusCircle}
+                />
+              {/if}
+            {/snippet}
+
+            <SettingInputField
+              inputType={SettingInputFieldType.TEXT}
+              label={i === 0 ? $t('url') : undefined}
+              description={i === 0 ? $t('admin.machine_learning_url_description') : undefined}
+              bind:value={config.machineLearning.urls[i]}
+              required={i === 0}
+              disabled={disabled || !config.machineLearning.enabled}
+              isEdited={i === 0 && !isEqual(config.machineLearning.urls, savedConfig.machineLearning.urls)}
+              trailingSnippet={removeButton}
+            />
+          {/each}
+        </div>
+
+        <Button
+          class="mb-2"
+          type="button"
+          size="sm"
+          onclick={() => config.machineLearning.urls.splice(0, 0, '')}
+          disabled={disabled || !config.machineLearning.enabled}>{$t('add_url')}</Button
+        >
       </div>
 
       <SettingAccordion
@@ -69,11 +106,15 @@
             disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.clip.enabled}
             isEdited={config.machineLearning.clip.modelName !== savedConfig.machineLearning.clip.modelName}
           >
-            <p slot="desc" class="immich-form-label pb-2 text-sm">
-              <FormatMessage key="admin.machine_learning_clip_model_description" let:message>
-                <a href="https://huggingface.co/immich-app"><u>{message}</u></a>
-              </FormatMessage>
-            </p>
+            {#snippet descriptionSnippet()}
+              <p class="immich-form-label pb-2 text-sm">
+                <FormatMessage key="admin.machine_learning_clip_model_description">
+                  {#snippet children({ message })}
+                    <a href="https://huggingface.co/immich-app"><u>{message}</u></a>
+                  {/snippet}
+                </FormatMessage>
+              </p>
+            {/snippet}
           </SettingInputField>
         </div>
       </SettingAccordion>
@@ -100,7 +141,7 @@
             step="0.0005"
             min={0.001}
             max={0.1}
-            desc={$t('admin.machine_learning_max_detection_distance_description')}
+            description={$t('admin.machine_learning_max_detection_distance_description')}
             disabled={disabled || !$featureFlags.duplicateDetection}
             isEdited={config.machineLearning.duplicateDetection.maxDistance !==
               savedConfig.machineLearning.duplicateDetection.maxDistance}
@@ -142,10 +183,10 @@
           <SettingInputField
             inputType={SettingInputFieldType.NUMBER}
             label={$t('admin.machine_learning_min_detection_score')}
-            desc={$t('admin.machine_learning_min_detection_score_description')}
+            description={$t('admin.machine_learning_min_detection_score_description')}
             bind:value={config.machineLearning.facialRecognition.minScore}
             step="0.1"
-            min={0}
+            min={0.1}
             max={1}
             disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled}
             isEdited={config.machineLearning.facialRecognition.minScore !==
@@ -155,10 +196,10 @@
           <SettingInputField
             inputType={SettingInputFieldType.NUMBER}
             label={$t('admin.machine_learning_max_recognition_distance')}
-            desc={$t('admin.machine_learning_max_recognition_distance_description')}
+            description={$t('admin.machine_learning_max_recognition_distance_description')}
             bind:value={config.machineLearning.facialRecognition.maxDistance}
             step="0.1"
-            min={0}
+            min={0.1}
             max={2}
             disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled}
             isEdited={config.machineLearning.facialRecognition.maxDistance !==
@@ -168,7 +209,7 @@
           <SettingInputField
             inputType={SettingInputFieldType.NUMBER}
             label={$t('admin.machine_learning_min_recognized_faces')}
-            desc={$t('admin.machine_learning_min_recognized_faces_description')}
+            description={$t('admin.machine_learning_min_recognized_faces_description')}
             bind:value={config.machineLearning.facialRecognition.minFaces}
             step="1"
             min={1}
diff --git a/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte b/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte
index 7c2c5c856a..4a4b23ded2 100644
--- a/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte
@@ -6,23 +6,30 @@
   import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
   import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
-  import SettingInputField, {
-    SettingInputFieldType,
-  } from '$lib/components/shared-components/settings/setting-input-field.svelte';
+  import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
   import { t } from 'svelte-i18n';
   import FormatMessage from '$lib/components/i18n/format-message.svelte';
+  import { SettingInputFieldType } from '$lib/constants';
 
-  export let savedConfig: SystemConfigDto;
-  export let defaultConfig: SystemConfigDto;
-  export let config: SystemConfigDto; // this is the config that is being edited
-  export let disabled = false;
-  export let onReset: SettingsResetEvent;
-  export let onSave: SettingsSaveEvent;
+  interface Props {
+    savedConfig: SystemConfigDto;
+    defaultConfig: SystemConfigDto;
+    config: SystemConfigDto;
+    disabled?: boolean;
+    onReset: SettingsResetEvent;
+    onSave: SettingsSaveEvent;
+  }
+
+  let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+  };
 </script>
 
 <div class="mt-2">
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault>
+    <form autocomplete="off" {onsubmit}>
       <div class="flex flex-col gap-4">
         <SettingAccordion key="map" title={$t('admin.map_settings')} subtitle={$t('admin.map_settings_description')}>
           <div class="ml-4 mt-4 flex flex-col gap-4">
@@ -38,7 +45,7 @@
             <SettingInputField
               inputType={SettingInputFieldType.TEXT}
               label={$t('admin.map_light_style')}
-              desc={$t('admin.map_style_description')}
+              description={$t('admin.map_style_description')}
               bind:value={config.map.lightStyle}
               disabled={disabled || !config.map.enabled}
               isEdited={config.map.lightStyle !== savedConfig.map.lightStyle}
@@ -46,7 +53,7 @@
             <SettingInputField
               inputType={SettingInputFieldType.TEXT}
               label={$t('admin.map_dark_style')}
-              desc={$t('admin.map_style_description')}
+              description={$t('admin.map_style_description')}
               bind:value={config.map.darkStyle}
               disabled={disabled || !config.map.enabled}
               isEdited={config.map.darkStyle !== savedConfig.map.darkStyle}
@@ -55,20 +62,22 @@
         >
 
         <SettingAccordion key="reverse-geocoding" title={$t('admin.map_reverse_geocoding_settings')}>
-          <svelte:fragment slot="subtitle">
+          {#snippet subtitleSnippet()}
             <p class="text-sm dark:text-immich-dark-fg">
-              <FormatMessage key="admin.map_manage_reverse_geocoding_settings" let:message>
-                <a
-                  href="https://immich.app/docs/features/reverse-geocoding"
-                  class="underline"
-                  target="_blank"
-                  rel="noreferrer"
-                >
-                  {message}
-                </a>
+              <FormatMessage key="admin.map_manage_reverse_geocoding_settings">
+                {#snippet children({ message })}
+                  <a
+                    href="https://immich.app/docs/features/reverse-geocoding"
+                    class="underline"
+                    target="_blank"
+                    rel="noreferrer"
+                  >
+                    {message}
+                  </a>
+                {/snippet}
               </FormatMessage>
             </p>
-          </svelte:fragment>
+          {/snippet}
           <div class="ml-4 mt-4 flex flex-col gap-4">
             <SettingSwitch
               title={$t('admin.map_reverse_geocoding_enable_description')}
diff --git a/web/src/lib/components/admin-page/settings/metadata-settings/metadata-settings.svelte b/web/src/lib/components/admin-page/settings/metadata-settings/metadata-settings.svelte
index c28050e022..1ba82b2eb9 100644
--- a/web/src/lib/components/admin-page/settings/metadata-settings/metadata-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/metadata-settings/metadata-settings.svelte
@@ -7,17 +7,25 @@
   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
   import { t } from 'svelte-i18n';
 
-  export let savedConfig: SystemConfigDto;
-  export let defaultConfig: SystemConfigDto;
-  export let config: SystemConfigDto; // this is the config that is being edited
-  export let disabled = false;
-  export let onReset: SettingsResetEvent;
-  export let onSave: SettingsSaveEvent;
+  interface Props {
+    savedConfig: SystemConfigDto;
+    defaultConfig: SystemConfigDto;
+    config: SystemConfigDto;
+    disabled?: boolean;
+    onReset: SettingsResetEvent;
+    onSave: SettingsSaveEvent;
+  }
+
+  let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+  };
 </script>
 
 <div class="mt-2">
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault class="mx-4 mt-4">
+    <form autocomplete="off" {onsubmit} class="mx-4 mt-4">
       <div class="ml-4 mt-4 flex flex-col gap-4">
         <SettingSwitch
           title={$t('admin.metadata_faces_import_setting')}
diff --git a/web/src/lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte b/web/src/lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte
index 76c238df82..1a6f0ab866 100644
--- a/web/src/lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte
@@ -7,17 +7,25 @@
   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
   import { t } from 'svelte-i18n';
 
-  export let savedConfig: SystemConfigDto;
-  export let defaultConfig: SystemConfigDto;
-  export let config: SystemConfigDto; // this is the config that is being edited
-  export let disabled = false;
-  export let onReset: SettingsResetEvent;
-  export let onSave: SettingsSaveEvent;
+  interface Props {
+    savedConfig: SystemConfigDto;
+    defaultConfig: SystemConfigDto;
+    config: SystemConfigDto;
+    disabled?: boolean;
+    onReset: SettingsResetEvent;
+    onSave: SettingsSaveEvent;
+  }
+
+  let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+  };
 </script>
 
 <div>
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault>
+    <form autocomplete="off" {onsubmit}>
       <div class="ml-4 mt-4">
         <SettingSwitch
           title={$t('admin.version_check_enabled_description')}
diff --git a/web/src/lib/components/admin-page/settings/notification-settings/notification-settings.svelte b/web/src/lib/components/admin-page/settings/notification-settings/notification-settings.svelte
index fcd26c684b..30a9fbad5c 100644
--- a/web/src/lib/components/admin-page/settings/notification-settings/notification-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/notification-settings/notification-settings.svelte
@@ -3,9 +3,7 @@
   import { isEqual } from 'lodash-es';
   import { fade } from 'svelte/transition';
   import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
-  import SettingInputField, {
-    SettingInputFieldType,
-  } from '$lib/components/shared-components/settings/setting-input-field.svelte';
+  import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
   import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
   import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
@@ -18,15 +16,21 @@
   import { user } from '$lib/stores/user.store';
   import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
   import { handleError } from '$lib/utils/handle-error';
+  import { SettingInputFieldType } from '$lib/constants';
+  import TemplateSettings from '$lib/components/admin-page/settings/template-settings/template-settings.svelte';
 
-  export let savedConfig: SystemConfigDto;
-  export let defaultConfig: SystemConfigDto;
-  export let config: SystemConfigDto; // this is the config that is being edited
-  export let disabled = false;
-  export let onReset: SettingsResetEvent;
-  export let onSave: SettingsSaveEvent;
+  interface Props {
+    savedConfig: SystemConfigDto;
+    defaultConfig: SystemConfigDto;
+    config: SystemConfigDto;
+    disabled?: boolean;
+    onReset: SettingsResetEvent;
+    onSave: SettingsSaveEvent;
+  }
 
-  let isSending = false;
+  let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
+
+  let isSending = $state(false);
 
   const handleSendTestEmail = async () => {
     if (isSending) {
@@ -65,11 +69,15 @@
       isSending = false;
     }
   };
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+  };
 </script>
 
 <div>
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault class="mt-4">
+    <form autocomplete="off" {onsubmit} class="mt-4">
       <div class="flex flex-col gap-4">
         <SettingAccordion key="email" title={$t('email')} subtitle={$t('admin.notification_email_setting_description')}>
           <div class="ml-4 mt-4 flex flex-col gap-4">
@@ -85,7 +93,7 @@
               inputType={SettingInputFieldType.TEXT}
               required
               label={$t('host')}
-              desc={$t('admin.notification_email_host_description')}
+              description={$t('admin.notification_email_host_description')}
               disabled={disabled || !config.notifications.smtp.enabled}
               bind:value={config.notifications.smtp.transport.host}
               isEdited={config.notifications.smtp.transport.host !== savedConfig.notifications.smtp.transport.host}
@@ -95,7 +103,7 @@
               inputType={SettingInputFieldType.NUMBER}
               required
               label={$t('port')}
-              desc={$t('admin.notification_email_port_description')}
+              description={$t('admin.notification_email_port_description')}
               disabled={disabled || !config.notifications.smtp.enabled}
               bind:value={config.notifications.smtp.transport.port}
               isEdited={config.notifications.smtp.transport.port !== savedConfig.notifications.smtp.transport.port}
@@ -104,7 +112,7 @@
             <SettingInputField
               inputType={SettingInputFieldType.TEXT}
               label={$t('username')}
-              desc={$t('admin.notification_email_username_description')}
+              description={$t('admin.notification_email_username_description')}
               disabled={disabled || !config.notifications.smtp.enabled}
               bind:value={config.notifications.smtp.transport.username}
               isEdited={config.notifications.smtp.transport.username !==
@@ -114,7 +122,7 @@
             <SettingInputField
               inputType={SettingInputFieldType.PASSWORD}
               label={$t('password')}
-              desc={$t('admin.notification_email_password_description')}
+              description={$t('admin.notification_email_password_description')}
               disabled={disabled || !config.notifications.smtp.enabled}
               bind:value={config.notifications.smtp.transport.password}
               isEdited={config.notifications.smtp.transport.password !==
@@ -134,14 +142,14 @@
               inputType={SettingInputFieldType.TEXT}
               required
               label={$t('admin.notification_email_from_address')}
-              desc={$t('admin.notification_email_from_address_description')}
+              description={$t('admin.notification_email_from_address_description')}
               disabled={disabled || !config.notifications.smtp.enabled}
               bind:value={config.notifications.smtp.from}
               isEdited={config.notifications.smtp.from !== savedConfig.notifications.smtp.from}
             />
 
             <div class="flex gap-2 place-items-center">
-              <Button size="sm" disabled={!config.notifications.smtp.enabled} on:click={handleSendTestEmail}>
+              <Button size="sm" disabled={!config.notifications.smtp.enabled} onclick={handleSendTestEmail}>
                 {#if disabled}
                   {$t('admin.notification_email_test_email')}
                 {:else}
@@ -155,13 +163,14 @@
           </div>
         </SettingAccordion>
       </div>
-
-      <SettingButtonsRow
-        onReset={(options) => onReset({ ...options, configKeys: ['notifications'] })}
-        onSave={() => onSave({ notifications: config.notifications })}
-        showResetToDefault={!isEqual(savedConfig, defaultConfig)}
-        {disabled}
-      />
     </form>
   </div>
+  <TemplateSettings {defaultConfig} {config} {savedConfig} {onReset} {onSave} />
+
+  <SettingButtonsRow
+    onReset={(options) => onReset({ ...options, configKeys: ['notifications', 'templates'] })}
+    onSave={() => onSave({ notifications: config.notifications, templates: config.templates })}
+    showResetToDefault={!isEqual(savedConfig, defaultConfig)}
+    {disabled}
+  />
 </div>
diff --git a/web/src/lib/components/admin-page/settings/server/server-settings.svelte b/web/src/lib/components/admin-page/settings/server/server-settings.svelte
index f021c99f24..b9134d1e50 100644
--- a/web/src/lib/components/admin-page/settings/server/server-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/server/server-settings.svelte
@@ -3,28 +3,36 @@
   import { isEqual } from 'lodash-es';
   import { fade } from 'svelte/transition';
   import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
-  import SettingInputField, {
-    SettingInputFieldType,
-  } from '$lib/components/shared-components/settings/setting-input-field.svelte';
+  import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
   import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
+  import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
   import { t } from 'svelte-i18n';
+  import { SettingInputFieldType } from '$lib/constants';
 
-  export let savedConfig: SystemConfigDto;
-  export let defaultConfig: SystemConfigDto;
-  export let config: SystemConfigDto; // this is the config that is being edited
-  export let disabled = false;
-  export let onReset: SettingsResetEvent;
-  export let onSave: SettingsSaveEvent;
+  interface Props {
+    savedConfig: SystemConfigDto;
+    defaultConfig: SystemConfigDto;
+    config: SystemConfigDto;
+    disabled?: boolean;
+    onReset: SettingsResetEvent;
+    onSave: SettingsSaveEvent;
+  }
+
+  let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+  };
 </script>
 
 <div>
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault>
+    <form autocomplete="off" {onsubmit}>
       <div class="mt-4 ml-4">
         <SettingInputField
           inputType={SettingInputFieldType.TEXT}
           label={$t('admin.server_external_domain_settings')}
-          desc={$t('admin.server_external_domain_settings_description')}
+          description={$t('admin.server_external_domain_settings_description')}
           bind:value={config.server.externalDomain}
           isEdited={config.server.externalDomain !== savedConfig.server.externalDomain}
         />
@@ -32,11 +40,18 @@
         <SettingInputField
           inputType={SettingInputFieldType.TEXT}
           label={$t('admin.server_welcome_message')}
-          desc={$t('admin.server_welcome_message_description')}
+          description={$t('admin.server_welcome_message_description')}
           bind:value={config.server.loginPageMessage}
           isEdited={config.server.loginPageMessage !== savedConfig.server.loginPageMessage}
         />
 
+        <SettingSwitch
+          title={$t('admin.server_public_users')}
+          subtitle={$t('admin.server_public_users_description')}
+          {disabled}
+          bind:checked={config.server.publicUsers}
+        />
+
         <div class="ml-4">
           <SettingButtonsRow
             onReset={(options) => onReset({ ...options, configKeys: ['server'] })}
diff --git a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte
index 4ebf4ed118..74d240a4a6 100644
--- a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte
@@ -1,6 +1,9 @@
 <script lang="ts">
+  import { createBubbler, preventDefault } from 'svelte/legacy';
+
+  const bubble = createBubbler();
   import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
-  import { AppRoute } from '$lib/constants';
+  import { AppRoute, SettingInputFieldType } from '$lib/constants';
   import { user } from '$lib/stores/user.store';
   import {
     getStorageTemplateOptions,
@@ -15,24 +18,38 @@
   import SupportedDatetimePanel from './supported-datetime-panel.svelte';
   import SupportedVariablesPanel from './supported-variables-panel.svelte';
   import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
-  import SettingInputField, {
-    SettingInputFieldType,
-  } from '$lib/components/shared-components/settings/setting-input-field.svelte';
+  import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
   import { t } from 'svelte-i18n';
   import FormatMessage from '$lib/components/i18n/format-message.svelte';
+  import type { Snippet } from 'svelte';
 
-  export let savedConfig: SystemConfigDto;
-  export let defaultConfig: SystemConfigDto;
-  export let config: SystemConfigDto; // this is the config that is being edited
-  export let disabled = false;
-  export let minified = false;
-  export let onReset: SettingsResetEvent;
-  export let onSave: SettingsSaveEvent;
-  export let duration: number = 500;
+  interface Props {
+    savedConfig: SystemConfigDto;
+    defaultConfig: SystemConfigDto;
+    config: SystemConfigDto;
+    disabled?: boolean;
+    minified?: boolean;
+    onReset: SettingsResetEvent;
+    onSave: SettingsSaveEvent;
+    duration?: number;
+    children?: Snippet;
+  }
 
-  let templateOptions: SystemConfigTemplateStorageOptionDto;
-  let selectedPreset = '';
+  let {
+    savedConfig,
+    defaultConfig,
+    config = $bindable(),
+    disabled = false,
+    minified = false,
+    onReset,
+    onSave,
+    duration = 500,
+    children,
+  }: Props = $props();
+
+  let templateOptions: SystemConfigTemplateStorageOptionDto | undefined = $state();
+  let selectedPreset = $state('');
 
   const getTemplateOptions = async () => {
     templateOptions = await getStorageTemplateOptions();
@@ -41,15 +58,11 @@
 
   const getSupportDateTimeFormat = () => getStorageTemplateOptions();
 
-  $: parsedTemplate = () => {
-    try {
-      return renderTemplate(config.storageTemplate.template);
-    } catch {
-      return 'error';
-    }
-  };
-
   const renderTemplate = (templateString: string) => {
+    if (!templateOptions) {
+      return '';
+    }
+
     const template = handlebar.compile(templateString, {
       knownHelpers: undefined,
     });
@@ -85,31 +98,40 @@
   const handlePresetSelection = () => {
     config.storageTemplate.template = selectedPreset;
   };
+  let parsedTemplate = $derived(() => {
+    try {
+      return renderTemplate(config.storageTemplate.template);
+    } catch {
+      return 'error';
+    }
+  });
 </script>
 
 <section class="dark:text-immich-dark-fg mt-2">
   <div in:fade={{ duration }} class="mx-4 flex flex-col gap-4 py-4">
     <p class="text-sm dark:text-immich-dark-fg">
-      <FormatMessage key="admin.storage_template_more_details" let:tag let:message>
-        {#if tag === 'template-link'}
-          <a
-            href="https://immich.app/docs/administration/storage-template"
-            class="underline"
-            target="_blank"
-            rel="noreferrer"
-          >
-            {message}
-          </a>
-        {:else if tag === 'implications-link'}
-          <a
-            href="https://immich.app/docs/administration/backup-and-restore#asset-types-and-storage-locations"
-            class="underline"
-            target="_blank"
-            rel="noreferrer"
-          >
-            {message}
-          </a>
-        {/if}
+      <FormatMessage key="admin.storage_template_more_details">
+        {#snippet children({ tag, message })}
+          {#if tag === 'template-link'}
+            <a
+              href="https://immich.app/docs/administration/storage-template"
+              class="underline"
+              target="_blank"
+              rel="noreferrer"
+            >
+              {message}
+            </a>
+          {:else if tag === 'implications-link'}
+            <a
+              href="https://immich.app/docs/administration/backup-and-restore#asset-types-and-storage-locations"
+              class="underline"
+              target="_blank"
+              rel="noreferrer"
+            >
+              {message}
+            </a>
+          {/if}
+        {/snippet}
       </FormatMessage>
     </p>
   </div>
@@ -164,19 +186,18 @@
             <FormatMessage
               key="admin.storage_template_path_length"
               values={{ length: parsedTemplate().length + $user.id.length + 'UPLOAD_LOCATION'.length, limit: 260 }}
-              let:message
             >
-              <span class="font-semibold text-immich-primary dark:text-immich-dark-primary">{message}</span>
+              {#snippet children({ message })}
+                <span class="font-semibold text-immich-primary dark:text-immich-dark-primary">{message}</span>
+              {/snippet}
             </FormatMessage>
           </p>
 
           <p class="text-sm">
-            <FormatMessage
-              key="admin.storage_template_user_label"
-              values={{ label: $user.storageLabel || $user.id }}
-              let:message
-            >
-              <code class="text-immich-primary dark:text-immich-dark-primary">{message}</code>
+            <FormatMessage key="admin.storage_template_user_label" values={{ label: $user.storageLabel || $user.id }}>
+              {#snippet children({ message })}
+                <code class="text-immich-primary dark:text-immich-dark-primary">{message}</code>
+              {/snippet}
             </FormatMessage>
           </p>
 
@@ -186,24 +207,30 @@
             >/{parsedTemplate()}.jpg
           </p>
 
-          <form autocomplete="off" class="flex flex-col" on:submit|preventDefault>
+          <form autocomplete="off" class="flex flex-col" onsubmit={preventDefault(bubble('submit'))}>
             <div class="flex flex-col my-2">
-              <label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="preset-select">
-                {$t('preset')}
-              </label>
-              <select
-                class="immich-form-input p-2 mt-2 text-sm rounded-lg bg-slate-200 hover:cursor-pointer dark:bg-gray-600"
-                disabled={disabled || !config.storageTemplate.enabled}
-                name="presets"
-                id="preset-select"
-                bind:value={selectedPreset}
-                on:change={handlePresetSelection}
-              >
-                {#each templateOptions.presetOptions as preset}
-                  <option value={preset}>{renderTemplate(preset)}</option>
-                {/each}
-              </select>
+              {#if templateOptions}
+                <label
+                  class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm"
+                  for="preset-select"
+                >
+                  {$t('preset')}
+                </label>
+                <select
+                  class="immich-form-input p-2 mt-2 text-sm rounded-lg bg-slate-200 hover:cursor-pointer dark:bg-gray-600"
+                  disabled={disabled || !config.storageTemplate.enabled}
+                  name="presets"
+                  id="preset-select"
+                  bind:value={selectedPreset}
+                  onchange={handlePresetSelection}
+                >
+                  {#each templateOptions.presetOptions as preset}
+                    <option value={preset}>{renderTemplate(preset)}</option>
+                  {/each}
+                </select>
+              {/if}
             </div>
+
             <div class="flex gap-2 align-bottom">
               <SettingInputField
                 label={$t('template')}
@@ -232,11 +259,12 @@
                     <FormatMessage
                       key="admin.storage_template_migration_info"
                       values={{ job: $t('admin.storage_template_migration_job') }}
-                      let:message
                     >
-                      <a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary">
-                        {message}
-                      </a>
+                      {#snippet children({ message })}
+                        <a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary">
+                          {message}
+                        </a>
+                      {/snippet}
                     </FormatMessage>
                   </p>
                 </section>
@@ -247,7 +275,7 @@
       {/if}
 
       {#if minified}
-        <slot />
+        {@render children?.()}
       {:else}
         <SettingButtonsRow
           onReset={(options) => onReset({ ...options, configKeys: ['storageTemplate'] })}
diff --git a/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte b/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte
index 10f22c1805..379e366df6 100644
--- a/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte
+++ b/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte
@@ -4,7 +4,11 @@
   import { DateTime } from 'luxon';
   import { t } from 'svelte-i18n';
 
-  export let options: SystemConfigTemplateStorageOptionDto;
+  interface Props {
+    options: SystemConfigTemplateStorageOptionDto;
+  }
+
+  let { options }: Props = $props();
 
   const getLuxonExample = (format: string) => {
     return DateTime.fromISO('2022-09-04T20:03:05.250Z', { locale: $locale }).toFormat(format);
diff --git a/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte b/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte
new file mode 100644
index 0000000000..c27df817c2
--- /dev/null
+++ b/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte
@@ -0,0 +1,131 @@
+<script lang="ts">
+  import { type SystemConfigDto, type SystemConfigTemplateEmailsDto, getNotificationTemplate } from '@immich/sdk';
+  import { fade } from 'svelte/transition';
+  import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
+  import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
+  import { t } from 'svelte-i18n';
+  import FormatMessage from '$lib/components/i18n/format-message.svelte';
+  import Button from '$lib/components/elements/buttons/button.svelte';
+  import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
+  import { mdiEyeOutline } from '@mdi/js';
+  import { handleError } from '$lib/utils/handle-error';
+  import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
+  import SettingTextarea from '$lib/components/shared-components/settings/setting-textarea.svelte';
+
+  interface Props {
+    savedConfig: SystemConfigDto;
+    defaultConfig: SystemConfigDto;
+    config: SystemConfigDto;
+    disabled?: boolean;
+    onReset: SettingsResetEvent;
+    onSave: SettingsSaveEvent;
+  }
+
+  let { savedConfig, config = $bindable() }: Props = $props();
+
+  let htmlPreview = $state('');
+  let loadingPreview = $state(false);
+
+  const getTemplate = async (name: string, template: string) => {
+    try {
+      loadingPreview = true;
+      const result = await getNotificationTemplate({ name, templateDto: { template } });
+      htmlPreview = result.html;
+    } catch (error) {
+      handleError(error, 'Could not load template.');
+    } finally {
+      loadingPreview = false;
+    }
+  };
+
+  const closePreviewModal = () => {
+    htmlPreview = '';
+  };
+
+  const templateConfigs = [
+    {
+      label: $t('admin.template_email_welcome'),
+      templateKey: 'welcomeTemplate' as const,
+      descriptionTags: '{username}, {password}, {displayName}, {baseUrl}',
+      templateName: 'welcome',
+    },
+    {
+      label: $t('admin.template_email_invite_album'),
+      templateKey: 'albumInviteTemplate' as const,
+      descriptionTags: '{senderName}, {recipientName}, {albumId}, {albumName}, {baseUrl}',
+      templateName: 'album-invite',
+    },
+    {
+      label: $t('admin.template_email_update_album'),
+      templateKey: 'albumUpdateTemplate' as const,
+      descriptionTags: '{recipientName}, {albumId}, {albumName}, {baseUrl}',
+      templateName: 'album-update',
+    },
+  ];
+
+  const isEdited = (templateKey: keyof SystemConfigTemplateEmailsDto) =>
+    config.templates.email[templateKey] !== savedConfig.templates.email[templateKey];
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+  };
+</script>
+
+<div>
+  <div in:fade={{ duration: 500 }}>
+    <form autocomplete="off" {onsubmit} class="mt-4">
+      <div class="flex flex-col gap-4">
+        <SettingAccordion
+          key="templates"
+          title={$t('admin.template_email_settings')}
+          subtitle={$t('admin.template_settings_description')}
+        >
+          <div class="ml-4 mt-4 flex flex-col gap-4">
+            <p class="text-sm dark:text-immich-dark-fg">
+              <FormatMessage key="admin.template_email_if_empty">
+                {$t('admin.template_email_if_empty')}
+              </FormatMessage>
+            </p>
+            <hr />
+            {#if loadingPreview}
+              <LoadingSpinner />
+            {/if}
+
+            {#each templateConfigs as { label, templateKey, descriptionTags, templateName }}
+              <SettingTextarea
+                {label}
+                description={$t('admin.template_email_available_tags', { values: { tags: descriptionTags } })}
+                bind:value={config.templates.email[templateKey]}
+                isEdited={isEdited(templateKey)}
+                disabled={!config.notifications.smtp.enabled}
+              />
+              <div class="flex justify-between">
+                <Button
+                  size="sm"
+                  onclick={() => getTemplate(templateName, config.templates.email[templateKey])}
+                  title={$t('admin.template_email_preview')}
+                >
+                  <Icon path={mdiEyeOutline} class="mr-1" />
+                  {$t('admin.template_email_preview')}
+                </Button>
+              </div>
+            {/each}
+          </div>
+        </SettingAccordion>
+      </div>
+
+      {#if htmlPreview}
+        <FullScreenModal title={$t('admin.template_email_preview')} onClose={closePreviewModal} width="wide">
+          <div style="position:relative; width:100%; height:90vh; overflow: hidden">
+            <iframe
+              title={$t('admin.template_email_preview')}
+              srcdoc={htmlPreview}
+              style="width: 100%; height: 100%; border: none; overflow:hidden; position: absolute; top: 0; left: 0;"
+            ></iframe>
+          </div>
+        </FullScreenModal>
+      {/if}
+    </form>
+  </div>
+</div>
diff --git a/web/src/lib/components/admin-page/settings/theme/theme-settings.svelte b/web/src/lib/components/admin-page/settings/theme/theme-settings.svelte
index 84a12e05c9..79b5f838e3 100644
--- a/web/src/lib/components/admin-page/settings/theme/theme-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/theme/theme-settings.svelte
@@ -7,24 +7,31 @@
   import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
   import { t } from 'svelte-i18n';
 
-  export let savedConfig: SystemConfigDto;
-  export let defaultConfig: SystemConfigDto;
-  export let config: SystemConfigDto; // this is the config that is being edited
-  export let disabled = false;
-  export let onReset: SettingsResetEvent;
-  export let onSave: SettingsSaveEvent;
+  interface Props {
+    savedConfig: SystemConfigDto;
+    defaultConfig: SystemConfigDto;
+    config: SystemConfigDto;
+    disabled?: boolean;
+    onReset: SettingsResetEvent;
+    onSave: SettingsSaveEvent;
+  }
+
+  let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+  };
 </script>
 
 <div>
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault>
+    <form autocomplete="off" {onsubmit}>
       <div class="ml-4 mt-4 flex flex-col gap-4">
         <SettingTextarea
           {disabled}
           label={$t('admin.theme_custom_css_settings')}
-          desc={$t('admin.theme_custom_css_settings_description')}
+          description={$t('admin.theme_custom_css_settings_description')}
           bind:value={config.theme.customCss}
-          required={true}
           isEdited={config.theme.customCss !== savedConfig.theme.customCss}
         />
 
diff --git a/web/src/lib/components/admin-page/settings/trash-settings/trash-settings.svelte b/web/src/lib/components/admin-page/settings/trash-settings/trash-settings.svelte
index 8f287d48e0..05979bf9f0 100644
--- a/web/src/lib/components/admin-page/settings/trash-settings/trash-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/trash-settings/trash-settings.svelte
@@ -4,23 +4,30 @@
   import { fade } from 'svelte/transition';
   import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
-  import SettingInputField, {
-    SettingInputFieldType,
-  } from '$lib/components/shared-components/settings/setting-input-field.svelte';
+  import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
   import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
   import { t } from 'svelte-i18n';
+  import { SettingInputFieldType } from '$lib/constants';
 
-  export let savedConfig: SystemConfigDto;
-  export let defaultConfig: SystemConfigDto;
-  export let config: SystemConfigDto; // this is the config that is being edited
-  export let disabled = false;
-  export let onReset: SettingsResetEvent;
-  export let onSave: SettingsSaveEvent;
+  interface Props {
+    savedConfig: SystemConfigDto;
+    defaultConfig: SystemConfigDto;
+    config: SystemConfigDto;
+    disabled?: boolean;
+    onReset: SettingsResetEvent;
+    onSave: SettingsSaveEvent;
+  }
+
+  let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+  };
 </script>
 
 <div>
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault>
+    <form autocomplete="off" {onsubmit}>
       <div class="ml-4 mt-4 flex flex-col gap-4">
         <SettingSwitch title={$t('admin.trash_enabled_description')} {disabled} bind:checked={config.trash.enabled} />
 
@@ -29,7 +36,7 @@
         <SettingInputField
           inputType={SettingInputFieldType.NUMBER}
           label={$t('admin.trash_number_of_days')}
-          desc={$t('admin.trash_number_of_days_description')}
+          description={$t('admin.trash_number_of_days_description')}
           bind:value={config.trash.days}
           required={true}
           disabled={disabled || !config.trash.enabled}
diff --git a/web/src/lib/components/admin-page/settings/user-settings/user-settings.svelte b/web/src/lib/components/admin-page/settings/user-settings/user-settings.svelte
index 21453cbc70..f96c3808a8 100644
--- a/web/src/lib/components/admin-page/settings/user-settings/user-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/user-settings/user-settings.svelte
@@ -5,28 +5,31 @@
   import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
 
   import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
-  import SettingInputField, {
-    SettingInputFieldType,
-  } from '$lib/components/shared-components/settings/setting-input-field.svelte';
+  import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
   import { t } from 'svelte-i18n';
+  import { SettingInputFieldType } from '$lib/constants';
 
-  export let savedConfig: SystemConfigDto;
-  export let defaultConfig: SystemConfigDto;
-  export let config: SystemConfigDto; // this is the config that is being edited
-  export let disabled = false;
-  export let onReset: SettingsResetEvent;
-  export let onSave: SettingsSaveEvent;
+  interface Props {
+    savedConfig: SystemConfigDto;
+    defaultConfig: SystemConfigDto;
+    config: SystemConfigDto;
+    disabled?: boolean;
+    onReset: SettingsResetEvent;
+    onSave: SettingsSaveEvent;
+  }
+
+  let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
 </script>
 
 <div>
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault>
+    <form autocomplete="off" onsubmit={(e) => e.preventDefault()}>
       <div class="ml-4 mt-4 flex flex-col gap-4">
         <SettingInputField
           inputType={SettingInputFieldType.NUMBER}
           min={1}
           label={$t('admin.user_delete_delay_settings')}
-          desc={$t('admin.user_delete_delay_settings_description')}
+          description={$t('admin.user_delete_delay_settings_description')}
           bind:value={config.user.deleteDelay}
           isEdited={config.user.deleteDelay !== savedConfig.user.deleteDelay}
         />
diff --git a/web/src/lib/components/album-page/__tests__/album-card.spec.ts b/web/src/lib/components/album-page/__tests__/album-card.spec.ts
index 79136bca02..9e396bec3e 100644
--- a/web/src/lib/components/album-page/__tests__/album-card.spec.ts
+++ b/web/src/lib/components/album-page/__tests__/album-card.spec.ts
@@ -1,18 +1,19 @@
 import { sdkMock } from '$lib/__mocks__/sdk.mock';
 import { albumFactory } from '@test-data/factories/album-factory';
 import '@testing-library/jest-dom';
-import { fireEvent, render, waitFor, type RenderResult } from '@testing-library/svelte';
+import { render, waitFor, type RenderResult } from '@testing-library/svelte';
+import userEvent from '@testing-library/user-event';
 import { init, register, waitLocale } from 'svelte-i18n';
 import AlbumCard from '../album-card.svelte';
 
 const onShowContextMenu = vi.fn();
 
 describe('AlbumCard component', () => {
-  let sut: RenderResult<AlbumCard>;
+  let sut: RenderResult<typeof AlbumCard>;
 
   beforeAll(async () => {
     await init({ fallbackLocale: 'en-US' });
-    register('en-US', () => import('$lib/i18n/en.json'));
+    register('en-US', () => import('$i18n/en.json'));
     await waitLocale('en-US');
   });
 
@@ -110,13 +111,9 @@ describe('AlbumCard component', () => {
         toJSON: () => ({}),
       });
 
-      await fireEvent(
-        contextMenuButton,
-        new MouseEvent('click', {
-          clientX: 123,
-          clientY: 456,
-        }),
-      );
+      const user = userEvent.setup();
+      await user.click(contextMenuButton);
+
       expect(onShowContextMenu).toHaveBeenCalledTimes(1);
       expect(onShowContextMenu).toHaveBeenCalledWith(expect.objectContaining({ x: 123, y: 456 }));
     });
diff --git a/web/src/lib/components/album-page/album-card-group.svelte b/web/src/lib/components/album-page/album-card-group.svelte
index f899cebd8c..ae2b27efac 100644
--- a/web/src/lib/components/album-page/album-card-group.svelte
+++ b/web/src/lib/components/album-page/album-card-group.svelte
@@ -11,28 +11,43 @@
   import Icon from '$lib/components/elements/icon.svelte';
   import { t } from 'svelte-i18n';
 
-  export let albums: AlbumResponseDto[];
-  export let group: AlbumGroup | undefined = undefined;
-  export let showOwner = false;
-  export let showDateRange = false;
-  export let showItemCount = false;
-  export let onShowContextMenu: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined =
-    undefined;
+  interface Props {
+    albums: AlbumResponseDto[];
+    group?: AlbumGroup | undefined;
+    showOwner?: boolean;
+    showDateRange?: boolean;
+    showItemCount?: boolean;
+    onShowContextMenu?: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined;
+  }
 
-  $: isCollapsed = !!group && isAlbumGroupCollapsed($albumViewSettings, group.id);
+  let {
+    albums,
+    group = undefined,
+    showOwner = false,
+    showDateRange = false,
+    showItemCount = false,
+    onShowContextMenu = undefined,
+  }: Props = $props();
+
+  let isCollapsed = $derived(!!group && isAlbumGroupCollapsed($albumViewSettings, group.id));
 
   const showContextMenu = (position: ContextMenuPosition, album: AlbumResponseDto) => {
     onShowContextMenu?.(position, album);
   };
 
-  $: iconRotation = isCollapsed ? 'rotate-0' : 'rotate-90';
+  let iconRotation = $derived(isCollapsed ? 'rotate-0' : 'rotate-90');
+
+  const oncontextmenu = (event: MouseEvent, album: AlbumResponseDto) => {
+    event.preventDefault();
+    showContextMenu({ x: event.x, y: event.y }, album);
+  };
 </script>
 
 {#if group}
   <div class="grid">
     <button
       type="button"
-      on:click={() => toggleAlbumGroupCollapsing(group.id)}
+      onclick={() => toggleAlbumGroupCollapsing(group.id)}
       class="w-fit mt-2 pt-2 pr-2 mb-2 dark:text-immich-dark-fg"
       aria-expanded={!isCollapsed}
     >
@@ -56,7 +71,7 @@
           data-sveltekit-preload-data="hover"
           href="{AppRoute.ALBUMS}/{album.id}"
           animate:flip={{ duration: 400 }}
-          on:contextmenu|preventDefault={(e) => showContextMenu({ x: e.x, y: e.y }, album)}
+          oncontextmenu={(event) => oncontextmenu(event, album)}
         >
           <AlbumCard
             {album}
diff --git a/web/src/lib/components/album-page/album-card.svelte b/web/src/lib/components/album-page/album-card.svelte
index f574c65f0b..cec4919e4e 100644
--- a/web/src/lib/components/album-page/album-card.svelte
+++ b/web/src/lib/components/album-page/album-card.svelte
@@ -8,12 +8,23 @@
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import { t } from 'svelte-i18n';
 
-  export let album: AlbumResponseDto;
-  export let showOwner = false;
-  export let showDateRange = false;
-  export let showItemCount = false;
-  export let preload = false;
-  export let onShowContextMenu: ((position: ContextMenuPosition) => unknown) | undefined = undefined;
+  interface Props {
+    album: AlbumResponseDto;
+    showOwner?: boolean;
+    showDateRange?: boolean;
+    showItemCount?: boolean;
+    preload?: boolean;
+    onShowContextMenu?: ((position: ContextMenuPosition) => unknown) | undefined;
+  }
+
+  let {
+    album,
+    showOwner = false,
+    showDateRange = false,
+    showItemCount = false,
+    preload = false,
+    onShowContextMenu = undefined,
+  }: Props = $props();
 
   const showAlbumContextMenu = (e: MouseEvent) => {
     e.stopPropagation();
@@ -39,7 +50,7 @@
         size="20"
         padding="2"
         class="icon-white-drop-shadow"
-        on:click={showAlbumContextMenu}
+        onclick={showAlbumContextMenu}
       />
     </div>
   {/if}
diff --git a/web/src/lib/components/album-page/album-cover.svelte b/web/src/lib/components/album-page/album-cover.svelte
index d0444f3599..3f71bbe632 100644
--- a/web/src/lib/components/album-page/album-cover.svelte
+++ b/web/src/lib/components/album-page/album-cover.svelte
@@ -5,13 +5,18 @@
   import AssetCover from '$lib/components/sharedlinks-page/covers/asset-cover.svelte';
   import { t } from 'svelte-i18n';
 
-  export let album: AlbumResponseDto;
-  export let preload = false;
-  let className = '';
-  export { className as class };
+  interface Props {
+    album: AlbumResponseDto;
+    preload?: boolean;
+    class?: string;
+  }
 
-  $: alt = album.albumName || $t('unnamed_album');
-  $: thumbnailUrl = album.albumThumbnailAssetId ? getAssetThumbnailUrl({ id: album.albumThumbnailAssetId }) : null;
+  let { album, preload = false, class: className = '' }: Props = $props();
+
+  let alt = $derived(album.albumName || $t('unnamed_album'));
+  let thumbnailUrl = $derived(
+    album.albumThumbnailAssetId ? getAssetThumbnailUrl({ id: album.albumThumbnailAssetId }) : null,
+  );
 </script>
 
 {#if thumbnailUrl}
diff --git a/web/src/lib/components/album-page/album-description.svelte b/web/src/lib/components/album-page/album-description.svelte
index b3ad688a30..46b424f93a 100644
--- a/web/src/lib/components/album-page/album-description.svelte
+++ b/web/src/lib/components/album-page/album-description.svelte
@@ -4,9 +4,13 @@
   import AutogrowTextarea from '$lib/components/shared-components/autogrow-textarea.svelte';
   import { t } from 'svelte-i18n';
 
-  export let id: string;
-  export let description: string;
-  export let isOwned: boolean;
+  interface Props {
+    id: string;
+    description: string;
+    isOwned: boolean;
+  }
+
+  let { id, description = $bindable(), isOwned }: Props = $props();
 
   const handleUpdateDescription = async (newDescription: string) => {
     try {
diff --git a/web/src/lib/components/album-page/album-options.svelte b/web/src/lib/components/album-page/album-options.svelte
index 84a2873788..884de8c2a2 100644
--- a/web/src/lib/components/album-page/album-options.svelte
+++ b/web/src/lib/components/album-page/album-options.svelte
@@ -1,8 +1,15 @@
 <script lang="ts">
   import Icon from '$lib/components/elements/icon.svelte';
-  import { updateAlbumInfo, type AlbumResponseDto, type UserResponseDto, AssetOrder } from '@immich/sdk';
-  import { mdiArrowDownThin, mdiArrowUpThin, mdiPlus } from '@mdi/js';
-  import { createEventDispatcher } from 'svelte';
+  import {
+    updateAlbumInfo,
+    removeUserFromAlbum,
+    type AlbumResponseDto,
+    type UserResponseDto,
+    AssetOrder,
+    AlbumUserRole,
+    updateAlbumUser,
+  } from '@immich/sdk';
+  import { mdiArrowDownThin, mdiArrowUpThin, mdiPlus, mdiDotsVertical } from '@mdi/js';
   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
   import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
@@ -11,30 +18,49 @@
   import { handleError } from '$lib/utils/handle-error';
   import { findKey } from 'lodash-es';
   import { t } from 'svelte-i18n';
+  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
+  import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
+  import { notificationController, NotificationType } from '../shared-components/notification/notification';
+  import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
 
-  export let album: AlbumResponseDto;
-  export let order: AssetOrder | undefined;
-  export let user: UserResponseDto;
-  export let onChangeOrder: (order: AssetOrder) => void;
+  interface Props {
+    album: AlbumResponseDto;
+    order: AssetOrder | undefined;
+    user: UserResponseDto;
+    onChangeOrder: (order: AssetOrder) => void;
+    onClose: () => void;
+    onToggleEnabledActivity: () => void;
+    onShowSelectSharedUser: () => void;
+    onRemove: (userId: string) => void;
+    onRefreshAlbum: () => void;
+  }
+
+  let {
+    album,
+    order,
+    user,
+    onChangeOrder,
+    onClose,
+    onToggleEnabledActivity,
+    onShowSelectSharedUser,
+    onRemove,
+    onRefreshAlbum,
+  }: Props = $props();
+
+  let selectedRemoveUser: UserResponseDto | null = $state(null);
 
   const options: Record<AssetOrder, RenderedOption> = {
     [AssetOrder.Asc]: { icon: mdiArrowUpThin, title: $t('oldest_first') },
     [AssetOrder.Desc]: { icon: mdiArrowDownThin, title: $t('newest_first') },
   };
 
-  $: selectedOption = order ? options[order] : options[AssetOrder.Desc];
+  let selectedOption = $derived(order ? options[order] : options[AssetOrder.Desc]);
 
-  const dispatch = createEventDispatcher<{
-    close: void;
-    toggleEnableActivity: void;
-    showSelectSharedUser: void;
-  }>();
-
-  const handleToggle = async (returnedOption: RenderedOption) => {
+  const handleToggle = async (returnedOption: RenderedOption): Promise<void> => {
     if (selectedOption === returnedOption) {
       return;
     }
-    let order = AssetOrder.Desc;
+    let order: AssetOrder = AssetOrder.Desc;
     order = findKey(options, (option) => option === returnedOption) as AssetOrder;
 
     try {
@@ -49,54 +75,127 @@
       handleError(error, $t('errors.unable_to_save_album'));
     }
   };
+
+  const handleMenuRemove = (user: UserResponseDto): void => {
+    selectedRemoveUser = user;
+  };
+
+  const handleRemoveUser = async (): Promise<void> => {
+    if (!selectedRemoveUser) {
+      return;
+    }
+    try {
+      await removeUserFromAlbum({ id: album.id, userId: selectedRemoveUser.id });
+      onRemove(selectedRemoveUser.id);
+      notificationController.show({
+        type: NotificationType.Info,
+        message: $t('album_user_removed', { values: { user: selectedRemoveUser.name } }),
+      });
+    } catch (error) {
+      handleError(error, $t('errors.unable_to_remove_album_users'));
+    } finally {
+      selectedRemoveUser = null;
+    }
+  };
+
+  const handleUpdateSharedUserRole = async (user: UserResponseDto, role: AlbumUserRole) => {
+    try {
+      await updateAlbumUser({ id: album.id, userId: user.id, updateAlbumUserDto: { role } });
+      const message = $t('user_role_set', {
+        values: { user: user.name, role: role == AlbumUserRole.Viewer ? $t('role_viewer') : $t('role_editor') },
+      });
+      onRefreshAlbum();
+      notificationController.show({ type: NotificationType.Info, message });
+    } catch (error) {
+      handleError(error, $t('errors.unable_to_change_album_user_role'));
+    } finally {
+      selectedRemoveUser = null;
+    }
+  };
 </script>
 
-<FullScreenModal title={$t('options')} onClose={() => dispatch('close')}>
-  <div class="items-center justify-center">
-    <div class="py-2">
-      <h2 class="text-gray text-sm mb-2">{$t('settings').toUpperCase()}</h2>
-      <div class="grid p-2 gap-y-2">
-        {#if order}
-          <SettingDropdown
-            title={$t('display_order')}
-            options={Object.values(options)}
-            selectedOption={options[order]}
-            onToggle={handleToggle}
+{#if !selectedRemoveUser}
+  <FullScreenModal title={$t('options')} {onClose}>
+    <div class="items-center justify-center">
+      <div class="py-2">
+        <h2 class="text-gray text-sm mb-2">{$t('settings').toUpperCase()}</h2>
+        <div class="grid p-2 gap-y-2">
+          {#if order}
+            <SettingDropdown
+              title={$t('display_order')}
+              options={Object.values(options)}
+              selectedOption={options[order]}
+              onToggle={handleToggle}
+            />
+          {/if}
+          <SettingSwitch
+            title={$t('comments_and_likes')}
+            subtitle={$t('let_others_respond')}
+            checked={album.isActivityEnabled}
+            onToggle={onToggleEnabledActivity}
           />
-        {/if}
-        <SettingSwitch
-          title={$t('comments_and_likes')}
-          subtitle={$t('let_others_respond')}
-          checked={album.isActivityEnabled}
-          on:toggle={() => dispatch('toggleEnableActivity')}
-        />
-      </div>
-    </div>
-    <div class="py-2">
-      <div class="text-gray text-sm mb-3">{$t('people').toUpperCase()}</div>
-      <div class="p-2">
-        <button type="button" class="flex items-center gap-2" on:click={() => dispatch('showSelectSharedUser')}>
-          <div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
-            <div><Icon path={mdiPlus} size="25" /></div>
-          </div>
-          <div>{$t('invite_people')}</div>
-        </button>
-        <div class="flex items-center gap-2 py-2 mt-2">
-          <div>
-            <UserAvatar {user} size="md" />
-          </div>
-          <div class="w-full">{user.name}</div>
-          <div>{$t('owner')}</div>
         </div>
-        {#each album.albumUsers as { user } (user.id)}
-          <div class="flex items-center gap-2 py-2">
+      </div>
+      <div class="py-2">
+        <div class="text-gray text-sm mb-3">{$t('people').toUpperCase()}</div>
+        <div class="p-2">
+          <button type="button" class="flex items-center gap-2" onclick={onShowSelectSharedUser}>
+            <div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
+              <div><Icon path={mdiPlus} size="25" /></div>
+            </div>
+            <div>{$t('invite_people')}</div>
+          </button>
+
+          <div class="flex items-center gap-2 py-2 mt-2">
             <div>
               <UserAvatar {user} size="md" />
             </div>
             <div class="w-full">{user.name}</div>
+            <div>{$t('owner')}</div>
           </div>
-        {/each}
+
+          {#each album.albumUsers as { user, role } (user.id)}
+            <div class="flex items-center gap-2 py-2">
+              <div>
+                <UserAvatar {user} size="md" />
+              </div>
+              <div class="w-full">{user.name}</div>
+              {#if role === AlbumUserRole.Viewer}
+                {$t('role_viewer')}
+              {:else}
+                {$t('role_editor')}
+              {/if}
+              {#if user.id !== album.ownerId}
+                <ButtonContextMenu icon={mdiDotsVertical} size="20" title={$t('options')}>
+                  {#if role === AlbumUserRole.Viewer}
+                    <MenuOption
+                      onClick={() => handleUpdateSharedUserRole(user, AlbumUserRole.Editor)}
+                      text={$t('allow_edits')}
+                    />
+                  {:else}
+                    <MenuOption
+                      onClick={() => handleUpdateSharedUserRole(user, AlbumUserRole.Viewer)}
+                      text={$t('disallow_edits')}
+                    />
+                  {/if}
+                  <!-- Allow deletion for non-owners -->
+                  <MenuOption onClick={() => handleMenuRemove(user)} text={$t('remove')} />
+                </ButtonContextMenu>
+              {/if}
+            </div>
+          {/each}
+        </div>
       </div>
     </div>
-  </div>
-</FullScreenModal>
+  </FullScreenModal>
+{/if}
+
+{#if selectedRemoveUser}
+  <ConfirmDialog
+    title={$t('album_remove_user')}
+    prompt={$t('album_remove_user_confirmation', { values: { user: selectedRemoveUser.name } })}
+    confirmText={$t('remove_user')}
+    onConfirm={handleRemoveUser}
+    onCancel={() => (selectedRemoveUser = null)}
+  />
+{/if}
diff --git a/web/src/lib/components/album-page/album-summary.svelte b/web/src/lib/components/album-page/album-summary.svelte
index 0277035d5c..f2cd23f616 100644
--- a/web/src/lib/components/album-page/album-summary.svelte
+++ b/web/src/lib/components/album-page/album-summary.svelte
@@ -4,10 +4,11 @@
   import type { AlbumResponseDto } from '@immich/sdk';
   import { t } from 'svelte-i18n';
 
-  export let album: AlbumResponseDto;
+  interface Props {
+    album: AlbumResponseDto;
+  }
 
-  $: startDate = formatDate(album.startDate);
-  $: endDate = formatDate(album.endDate);
+  let { album }: Props = $props();
 
   const formatDate = (date?: string) => {
     return date ? new Date(date).toLocaleDateString($locale, dateFormats.album) : undefined;
@@ -24,6 +25,8 @@
 
     return '';
   };
+  let startDate = $derived(formatDate(album.startDate));
+  let endDate = $derived(formatDate(album.endDate));
 </script>
 
 <span class="my-2 flex gap-2 text-sm font-medium text-gray-500" data-testid="album-details">
diff --git a/web/src/lib/components/album-page/album-title.svelte b/web/src/lib/components/album-page/album-title.svelte
index 22c26aa10c..74786c1ea4 100644
--- a/web/src/lib/components/album-page/album-title.svelte
+++ b/web/src/lib/components/album-page/album-title.svelte
@@ -4,11 +4,20 @@
   import { shortcut } from '$lib/actions/shortcut';
   import { t } from 'svelte-i18n';
 
-  export let id: string;
-  export let albumName: string;
-  export let isOwned: boolean;
+  interface Props {
+    id: string;
+    albumName: string;
+    isOwned: boolean;
+    onUpdate: (albumName: string) => void;
+  }
 
-  $: newAlbumName = albumName;
+  let { id, albumName = $bindable(), isOwned, onUpdate }: Props = $props();
+
+  let newAlbumName = $state(albumName);
+
+  $effect(() => {
+    newAlbumName = albumName;
+  });
 
   const handleUpdateName = async () => {
     if (newAlbumName === albumName) {
@@ -16,23 +25,23 @@
     }
 
     try {
-      await updateAlbumInfo({
+      ({ albumName } = await updateAlbumInfo({
         id,
         updateAlbumDto: {
           albumName: newAlbumName,
         },
-      });
+      }));
+      onUpdate(albumName);
     } catch (error) {
       handleError(error, $t('errors.unable_to_save_album'));
       return;
     }
-    albumName = newAlbumName;
   };
 </script>
 
 <input
   use:shortcut={{ shortcut: { key: 'Enter' }, onShortcut: (e) => e.currentTarget.blur() }}
-  on:blur={handleUpdateName}
+  onblur={handleUpdateName}
   class="w-[99%] mb-2 border-b-2 border-transparent text-2xl md:text-4xl lg:text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned
     ? 'hover:border-gray-400'
     : 'hover:border-transparent'} bg-immich-bg focus:border-b-2 focus:border-immich-primary focus:outline-none dark:bg-immich-dark-bg dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray"
diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte
index 2256c79eb0..02544e3e07 100644
--- a/web/src/lib/components/album-page/album-viewer.svelte
+++ b/web/src/lib/components/album-page/album-viewer.svelte
@@ -4,9 +4,8 @@
   import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
   import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
   import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk';
-  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
   import { AssetStore } from '$lib/stores/assets.store';
-  import { downloadAlbum } from '$lib/utils/asset-utils';
+  import { cancelMultiselect, downloadAlbum } from '$lib/utils/asset-utils';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
   import DownloadAction from '../photos-page/actions/download-action.svelte';
   import AssetGrid from '../photos-page/asset-grid.svelte';
@@ -20,18 +19,22 @@
   import AlbumSummary from './album-summary.svelte';
   import { t } from 'svelte-i18n';
   import { onDestroy } from 'svelte';
+  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 
-  export let sharedLink: SharedLinkResponseDto;
-  export let user: UserResponseDto | undefined = undefined;
+  interface Props {
+    sharedLink: SharedLinkResponseDto;
+    user?: UserResponseDto | undefined;
+  }
+
+  let { sharedLink, user = undefined }: Props = $props();
 
   const album = sharedLink.album as AlbumResponseDto;
-  let innerWidth: number;
+  let innerWidth: number = $state(0);
 
   let { isViewing: showAssetViewer } = assetViewingStore;
 
   const assetStore = new AssetStore({ albumId: album.id, order: album.order });
-  const assetInteractionStore = createAssetInteractionStore();
-  const { isMultiSelectState, selectedAssets } = assetInteractionStore;
+  const assetInteraction = new AssetInteraction();
 
   dragAndDropFilesStore.subscribe((value) => {
     if (value.isDragging && value.files.length > 0) {
@@ -48,8 +51,8 @@
   use:shortcut={{
     shortcut: { key: 'Escape' },
     onShortcut: () => {
-      if (!$showAssetViewer && $isMultiSelectState) {
-        assetInteractionStore.clearMultiselect();
+      if (!$showAssetViewer && assetInteraction.selectionActive) {
+        cancelMultiselect(assetInteraction);
       }
     },
   }}
@@ -57,28 +60,28 @@
 />
 
 <header>
-  {#if $isMultiSelectState}
+  {#if assetInteraction.selectionActive}
     <AssetSelectControlBar
       ownerId={user?.id}
-      assets={$selectedAssets}
-      clearSelect={() => assetInteractionStore.clearMultiselect()}
+      assets={assetInteraction.selectedAssets}
+      clearSelect={() => assetInteraction.clearMultiselect()}
     >
-      <SelectAllAssets {assetStore} {assetInteractionStore} />
+      <SelectAllAssets {assetStore} {assetInteraction} />
       {#if sharedLink.allowDownload}
         <DownloadAction filename="{album.albumName}.zip" />
       {/if}
     </AssetSelectControlBar>
   {:else}
     <ControlAppBar showBackButton={false}>
-      <svelte:fragment slot="leading">
+      {#snippet leading()}
         <ImmichLogoSmallLink width={innerWidth} />
-      </svelte:fragment>
+      {/snippet}
 
-      <svelte:fragment slot="trailing">
+      {#snippet trailing()}
         {#if sharedLink.allowUpload}
           <CircleIconButton
             title={$t('add_photos')}
-            on:click={() => openFileUploadDialog({ albumId: album.id })}
+            onclick={() => openFileUploadDialog({ albumId: album.id })}
             icon={mdiFileImagePlusOutline}
           />
         {/if}
@@ -86,19 +89,19 @@
         {#if album.assetCount > 0 && sharedLink.allowDownload}
           <CircleIconButton
             title={$t('download')}
-            on:click={() => downloadAlbum(album)}
+            onclick={() => downloadAlbum(album)}
             icon={mdiFolderDownloadOutline}
           />
         {/if}
 
         <ThemeButton />
-      </svelte:fragment>
+      {/snippet}
     </ControlAppBar>
   {/if}
 </header>
 
 <main class="relative h-screen overflow-hidden bg-immich-bg px-6 pt-[var(--navbar-height)] dark:bg-immich-dark-bg">
-  <AssetGrid enableRouting={true} {album} {assetStore} {assetInteractionStore}>
+  <AssetGrid enableRouting={true} {album} {assetStore} {assetInteraction}>
     <section class="pt-8 md:pt-24">
       <!-- ALBUM TITLE -->
       <h1
diff --git a/web/src/lib/components/album-page/albums-controls.svelte b/web/src/lib/components/album-page/albums-controls.svelte
index ae8178a805..85a7260f40 100644
--- a/web/src/lib/components/album-page/albums-controls.svelte
+++ b/web/src/lib/components/album-page/albums-controls.svelte
@@ -38,8 +38,12 @@
   import { fly } from 'svelte/transition';
   import { t } from 'svelte-i18n';
 
-  export let albumGroups: string[];
-  export let searchQuery: string;
+  interface Props {
+    albumGroups: string[];
+    searchQuery: string;
+  }
+
+  let { albumGroups, searchQuery = $bindable() }: Props = $props();
 
   const flipOrdering = (ordering: string) => {
     return ordering === SortOrder.Asc ? SortOrder.Desc : SortOrder.Asc;
@@ -73,57 +77,38 @@
       $albumViewSettings.view === AlbumViewMode.Cover ? AlbumViewMode.List : AlbumViewMode.Cover;
   };
 
-  let selectedGroupOption: AlbumGroupOptionMetadata;
-  let groupIcon: string;
-
-  $: selectedFilterOption = albumFilterNames[findFilterOption($albumViewSettings.filter)];
-
-  $: selectedSortOption = findSortOptionMetadata($albumViewSettings.sortBy);
-
-  $: {
-    selectedGroupOption = findGroupOptionMetadata($albumViewSettings.groupBy);
-    if (selectedGroupOption.isDisabled()) {
-      selectedGroupOption = findGroupOptionMetadata(AlbumGroupBy.None);
+  let groupIcon = $derived.by(() => {
+    if (selectedGroupOption?.id === AlbumGroupBy.None) {
+      return mdiFolderRemoveOutline;
     }
-  }
+    return $albumViewSettings.groupOrder === SortOrder.Desc ? mdiFolderArrowDownOutline : mdiFolderArrowUpOutline;
+  });
 
-  $: {
-    if (selectedGroupOption.id === AlbumGroupBy.None) {
-      groupIcon = mdiFolderRemoveOutline;
-    } else {
-      groupIcon =
-        $albumViewSettings.groupOrder === SortOrder.Desc ? mdiFolderArrowDownOutline : mdiFolderArrowUpOutline;
-    }
-  }
+  let albumFilterNames: Record<AlbumFilter, string> = $derived({
+    [AlbumFilter.All]: $t('all'),
+    [AlbumFilter.Owned]: $t('owned'),
+    [AlbumFilter.Shared]: $t('shared'),
+  });
 
-  $: sortIcon = $albumViewSettings.sortOrder === SortOrder.Desc ? mdiArrowDownThin : mdiArrowUpThin;
+  let selectedFilterOption = $derived(albumFilterNames[findFilterOption($albumViewSettings.filter)]);
+  let selectedSortOption = $derived(findSortOptionMetadata($albumViewSettings.sortBy));
+  let selectedGroupOption = $derived(findGroupOptionMetadata($albumViewSettings.groupBy));
+  let sortIcon = $derived($albumViewSettings.sortOrder === SortOrder.Desc ? mdiArrowDownThin : mdiArrowUpThin);
 
-  $: albumFilterNames = ((): Record<AlbumFilter, string> => {
-    return {
-      [AlbumFilter.All]: $t('all'),
-      [AlbumFilter.Owned]: $t('owned'),
-      [AlbumFilter.Shared]: $t('shared'),
-    };
-  })();
+  let albumSortByNames: Record<AlbumSortBy, string> = $derived({
+    [AlbumSortBy.Title]: $t('sort_title'),
+    [AlbumSortBy.ItemCount]: $t('sort_items'),
+    [AlbumSortBy.DateModified]: $t('sort_modified'),
+    [AlbumSortBy.DateCreated]: $t('sort_created'),
+    [AlbumSortBy.MostRecentPhoto]: $t('sort_recent'),
+    [AlbumSortBy.OldestPhoto]: $t('sort_oldest'),
+  });
 
-  $: albumSortByNames = ((): Record<AlbumSortBy, string> => {
-    return {
-      [AlbumSortBy.Title]: $t('sort_title'),
-      [AlbumSortBy.ItemCount]: $t('sort_items'),
-      [AlbumSortBy.DateModified]: $t('sort_modified'),
-      [AlbumSortBy.DateCreated]: $t('sort_created'),
-      [AlbumSortBy.MostRecentPhoto]: $t('sort_recent'),
-      [AlbumSortBy.OldestPhoto]: $t('sort_oldest'),
-    };
-  })();
-
-  $: albumGroupByNames = ((): Record<AlbumGroupBy, string> => {
-    return {
-      [AlbumGroupBy.None]: $t('group_no'),
-      [AlbumGroupBy.Owner]: $t('group_owner'),
-      [AlbumGroupBy.Year]: $t('group_year'),
-    };
-  })();
+  let albumGroupByNames: Record<AlbumGroupBy, string> = $derived({
+    [AlbumGroupBy.None]: $t('group_no'),
+    [AlbumGroupBy.Owner]: $t('group_owner'),
+    [AlbumGroupBy.Year]: $t('group_year'),
+  });
 </script>
 
 <!-- Filter Albums by Sharing Status (All, Owned, Shared) -->
@@ -142,7 +127,7 @@
 </div>
 
 <!-- Create Album -->
-<LinkButton on:click={() => createAlbumAndRedirect()}>
+<LinkButton onclick={() => createAlbumAndRedirect()}>
   <div class="flex place-items-center gap-2 text-sm">
     <Icon path={mdiPlusBoxOutline} size="18" />
     <p class="hidden md:block">{$t('create_album')}</p>
@@ -154,7 +139,7 @@
   title={$t('sort_albums_by')}
   options={Object.values(sortOptionsMetadata)}
   selectedOption={selectedSortOption}
-  on:select={({ detail }) => handleChangeSortBy(detail)}
+  onSelect={handleChangeSortBy}
   render={({ id }) => ({
     title: albumSortByNames[id],
     icon: sortIcon,
@@ -166,7 +151,7 @@
   title={$t('group_albums_by')}
   options={Object.values(groupOptionsMetadata)}
   selectedOption={selectedGroupOption}
-  on:select={({ detail }) => handleChangeGroupBy(detail)}
+  onSelect={handleChangeGroupBy}
   render={({ id, isDisabled }) => ({
     title: albumGroupByNames[id],
     icon: groupIcon,
@@ -179,7 +164,7 @@
     <!-- Expand Album Groups -->
     <div class="hidden xl:flex gap-0">
       <div class="block">
-        <LinkButton title={$t('expand_all')} on:click={() => expandAllAlbumGroups()}>
+        <LinkButton title={$t('expand_all')} onclick={() => expandAllAlbumGroups()}>
           <div class="flex place-items-center gap-2 text-sm">
             <Icon path={mdiUnfoldMoreHorizontal} size="18" />
           </div>
@@ -188,7 +173,7 @@
 
       <!-- Collapse Album Groups -->
       <div class="block">
-        <LinkButton title={$t('collapse_all')} on:click={() => collapseAllAlbumGroups(albumGroups)}>
+        <LinkButton title={$t('collapse_all')} onclick={() => collapseAllAlbumGroups(albumGroups)}>
           <div class="flex place-items-center gap-2 text-sm">
             <Icon path={mdiUnfoldLessHorizontal} size="18" />
           </div>
@@ -199,7 +184,7 @@
 {/if}
 
 <!-- Cover/List Display Toggle -->
-<LinkButton on:click={() => handleChangeListMode()}>
+<LinkButton onclick={() => handleChangeListMode()}>
   <div class="flex place-items-center gap-2 text-sm">
     {#if $albumViewSettings.view === AlbumViewMode.List}
       <Icon path={mdiViewGridOutline} size="18" />
diff --git a/web/src/lib/components/album-page/albums-list.svelte b/web/src/lib/components/album-page/albums-list.svelte
index 5e3499bd10..178190dc34 100644
--- a/web/src/lib/components/album-page/albums-list.svelte
+++ b/web/src/lib/components/album-page/albums-list.svelte
@@ -1,5 +1,5 @@
 <script lang="ts">
-  import { onMount } from 'svelte';
+  import { onMount, type Snippet } from 'svelte';
   import { groupBy } from 'lodash-es';
   import { addUsersToAlbum, deleteAlbum, type AlbumUserAddDto, type AlbumResponseDto, isHttpError } from '@immich/sdk';
   import { mdiDeleteOutline, mdiShareVariantOutline, mdiFolderDownloadOutline, mdiRenameOutline } from '@mdi/js';
@@ -38,14 +38,29 @@
   import { goto } from '$app/navigation';
   import { AppRoute } from '$lib/constants';
   import { t } from 'svelte-i18n';
+  import { run } from 'svelte/legacy';
 
-  export let ownedAlbums: AlbumResponseDto[] = [];
-  export let sharedAlbums: AlbumResponseDto[] = [];
-  export let searchQuery: string = '';
-  export let userSettings: AlbumViewSettings;
-  export let allowEdit = false;
-  export let showOwner = false;
-  export let albumGroupIds: string[] = [];
+  interface Props {
+    ownedAlbums?: AlbumResponseDto[];
+    sharedAlbums?: AlbumResponseDto[];
+    searchQuery?: string;
+    userSettings: AlbumViewSettings;
+    allowEdit?: boolean;
+    showOwner?: boolean;
+    albumGroupIds?: string[];
+    empty?: Snippet;
+  }
+
+  let {
+    ownedAlbums = $bindable([]),
+    sharedAlbums = $bindable([]),
+    searchQuery = '',
+    userSettings,
+    allowEdit = false,
+    showOwner = false,
+    albumGroupIds = $bindable([]),
+    empty,
+  }: Props = $props();
 
   interface AlbumGroupOption {
     [option: string]: (order: SortOrder, albums: AlbumResponseDto[]) => AlbumGroup[];
@@ -118,24 +133,24 @@
     },
   };
 
-  let albums: AlbumResponseDto[] = [];
-  let filteredAlbums: AlbumResponseDto[] = [];
-  let groupedAlbums: AlbumGroup[] = [];
+  let albums: AlbumResponseDto[] = $state([]);
+  let filteredAlbums: AlbumResponseDto[] = $state([]);
+  let groupedAlbums: AlbumGroup[] = $state([]);
 
-  let albumGroupOption: string = AlbumGroupBy.None;
+  let albumGroupOption: string = $state(AlbumGroupBy.None);
 
-  let showShareByURLModal = false;
+  let showShareByURLModal = $state(false);
 
-  let albumToEdit: AlbumResponseDto | null = null;
-  let albumToShare: AlbumResponseDto | null = null;
+  let albumToEdit: AlbumResponseDto | null = $state(null);
+  let albumToShare: AlbumResponseDto | null = $state(null);
   let albumToDelete: AlbumResponseDto | null = null;
 
-  let contextMenuPosition: ContextMenuPosition = { x: 0, y: 0 };
-  let contextMenuTargetAlbum: AlbumResponseDto | null = null;
-  let isOpen = false;
+  let contextMenuPosition: ContextMenuPosition = $state({ x: 0, y: 0 });
+  let contextMenuTargetAlbum: AlbumResponseDto | undefined = $state();
+  let isOpen = $state(false);
 
   // Step 1: Filter between Owned and Shared albums, or both.
-  $: {
+  run(() => {
     switch (userSettings.filter) {
       case AlbumFilter.Owned: {
         albums = ownedAlbums;
@@ -151,10 +166,10 @@
         albums = nonOwnedAlbums.length > 0 ? ownedAlbums.concat(nonOwnedAlbums) : ownedAlbums;
       }
     }
-  }
+  });
 
   // Step 2: Filter using the given search query.
-  $: {
+  run(() => {
     if (searchQuery) {
       const searchAlbumNormalized = normalizeSearchString(searchQuery);
 
@@ -164,17 +179,17 @@
     } else {
       filteredAlbums = albums;
     }
-  }
+  });
 
   // Step 3: Group albums.
-  $: {
+  run(() => {
     albumGroupOption = getSelectedAlbumGroupOption(userSettings);
     const groupFunc = groupOptions[albumGroupOption] ?? groupOptions[AlbumGroupBy.None];
     groupedAlbums = groupFunc(stringToSortOrder(userSettings.groupOrder), filteredAlbums);
-  }
+  });
 
   // Step 4: Sort albums amongst each group.
-  $: {
+  run(() => {
     groupedAlbums = groupedAlbums.map((group) => ({
       id: group.id,
       name: group.name,
@@ -182,9 +197,11 @@
     }));
 
     albumGroupIds = groupedAlbums.map(({ id }) => id);
-  }
+  });
 
-  $: showFullContextMenu = allowEdit && contextMenuTargetAlbum && contextMenuTargetAlbum.ownerId === $user.id;
+  let showFullContextMenu = $derived(
+    allowEdit && contextMenuTargetAlbum && contextMenuTargetAlbum.ownerId === $user.id,
+  );
 
   onMount(async () => {
     if (allowEdit) {
@@ -319,6 +336,10 @@
   };
 
   const openShareModal = () => {
+    if (!contextMenuTargetAlbum) {
+      return;
+    }
+
     albumToShare = contextMenuTargetAlbum;
     closeAlbumContextMenu();
   };
@@ -358,7 +379,7 @@
   {/if}
 {:else}
   <!-- Empty Message -->
-  <slot name="empty" />
+  {@render empty?.()}
 {/if}
 
 <!-- Context Menu -->
@@ -394,13 +415,13 @@
       <CreateSharedLinkModal
         albumId={albumToShare.id}
         onClose={() => closeShareModal()}
-        on:created={() => albumToShare && handleSharedLinkCreated(albumToShare)}
+        onCreated={() => albumToShare && handleSharedLinkCreated(albumToShare)}
       />
     {:else}
       <UserSelectionModal
         album={albumToShare}
-        on:select={({ detail: users }) => handleAddUsers(users)}
-        on:share={() => (showShareByURLModal = true)}
+        onSelect={handleAddUsers}
+        onShare={() => (showShareByURLModal = true)}
         onClose={() => closeShareModal()}
       />
     {/if}
diff --git a/web/src/lib/components/album-page/albums-table-header.svelte b/web/src/lib/components/album-page/albums-table-header.svelte
index 2c396bebed..4c018f7454 100644
--- a/web/src/lib/components/album-page/albums-table-header.svelte
+++ b/web/src/lib/components/album-page/albums-table-header.svelte
@@ -3,7 +3,11 @@
   import type { AlbumSortOptionMetadata } from '$lib/utils/album-utils';
   import { t } from 'svelte-i18n';
 
-  export let option: AlbumSortOptionMetadata;
+  interface Props {
+    option: AlbumSortOptionMetadata;
+  }
+
+  let { option }: Props = $props();
 
   const handleSort = () => {
     if ($albumViewSettings.sortBy === option.id) {
@@ -14,23 +18,21 @@
     }
   };
 
-  $: albumSortByNames = ((): Record<AlbumSortBy, string> => {
-    return {
-      [AlbumSortBy.Title]: $t('sort_title'),
-      [AlbumSortBy.ItemCount]: $t('sort_items'),
-      [AlbumSortBy.DateModified]: $t('sort_modified'),
-      [AlbumSortBy.DateCreated]: $t('sort_created'),
-      [AlbumSortBy.MostRecentPhoto]: $t('sort_recent'),
-      [AlbumSortBy.OldestPhoto]: $t('sort_oldest'),
-    };
-  })();
+  let albumSortByNames: Record<AlbumSortBy, string> = $derived({
+    [AlbumSortBy.Title]: $t('sort_title'),
+    [AlbumSortBy.ItemCount]: $t('sort_items'),
+    [AlbumSortBy.DateModified]: $t('sort_modified'),
+    [AlbumSortBy.DateCreated]: $t('sort_created'),
+    [AlbumSortBy.MostRecentPhoto]: $t('sort_recent'),
+    [AlbumSortBy.OldestPhoto]: $t('sort_oldest'),
+  });
 </script>
 
 <th class="text-sm font-medium {option.columnStyle}">
   <button
     type="button"
     class="rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50"
-    on:click={handleSort}
+    onclick={handleSort}
   >
     {#if $albumViewSettings.sortBy === option.id}
       {#if $albumViewSettings.sortOrder === SortOrder.Desc}
diff --git a/web/src/lib/components/album-page/albums-table-row.svelte b/web/src/lib/components/album-page/albums-table-row.svelte
index 3e9027de3d..c900930f8a 100644
--- a/web/src/lib/components/album-page/albums-table-row.svelte
+++ b/web/src/lib/components/album-page/albums-table-row.svelte
@@ -9,9 +9,12 @@
   import Icon from '$lib/components/elements/icon.svelte';
   import { t } from 'svelte-i18n';
 
-  export let album: AlbumResponseDto;
-  export let onShowContextMenu: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined =
-    undefined;
+  interface Props {
+    album: AlbumResponseDto;
+    onShowContextMenu?: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined;
+  }
+
+  let { album, onShowContextMenu = undefined }: Props = $props();
 
   const showContextMenu = (position: ContextMenuPosition) => {
     onShowContextMenu?.(position, album);
@@ -20,12 +23,17 @@
   const dateLocaleString = (dateString: string) => {
     return new Date(dateString).toLocaleDateString($locale, dateFormats.album);
   };
+
+  const oncontextmenu = (event: MouseEvent) => {
+    event.preventDefault();
+    showContextMenu({ x: event.x, y: event.y });
+  };
 </script>
 
 <tr
   class="flex h-[50px] w-full place-items-center border-[3px] border-transparent p-2 text-center odd:bg-immich-gray even:bg-immich-bg hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5"
-  on:click={() => goto(`${AppRoute.ALBUMS}/${album.id}`)}
-  on:contextmenu|preventDefault={(e) => showContextMenu({ x: e.x, y: e.y })}
+  onclick={() => goto(`${AppRoute.ALBUMS}/${album.id}`)}
+  {oncontextmenu}
 >
   <td class="text-md text-ellipsis text-left w-8/12 sm:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%] items-center">
     {album.albumName}
diff --git a/web/src/lib/components/album-page/albums-table.svelte b/web/src/lib/components/album-page/albums-table.svelte
index d9ffe8595b..bd7c7fd7f5 100644
--- a/web/src/lib/components/album-page/albums-table.svelte
+++ b/web/src/lib/components/album-page/albums-table.svelte
@@ -15,10 +15,13 @@
   } from '$lib/utils/album-utils';
   import { t } from 'svelte-i18n';
 
-  export let groupedAlbums: AlbumGroup[];
-  export let albumGroupOption: string = AlbumGroupBy.None;
-  export let onShowContextMenu: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined =
-    undefined;
+  interface Props {
+    groupedAlbums: AlbumGroup[];
+    albumGroupOption?: string;
+    onShowContextMenu?: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined;
+  }
+
+  let { groupedAlbums, albumGroupOption = AlbumGroupBy.None, onShowContextMenu }: Props = $props();
 </script>
 
 <table class="mt-2 w-full text-left">
@@ -46,7 +49,7 @@
       >
         <tr
           class="flex w-full place-items-center p-2 md:pl-5 md:pr-5 md:pt-3 md:pb-3"
-          on:click={() => toggleAlbumGroupCollapsing(albumGroup.id)}
+          onclick={() => toggleAlbumGroupCollapsing(albumGroup.id)}
           aria-expanded={!isCollapsed}
         >
           <td class="text-md text-left -mb-1">
diff --git a/web/src/lib/components/album-page/share-info-modal.svelte b/web/src/lib/components/album-page/share-info-modal.svelte
index c9ac224992..778943af3a 100644
--- a/web/src/lib/components/album-page/share-info-modal.svelte
+++ b/web/src/lib/components/album-page/share-info-modal.svelte
@@ -8,7 +8,7 @@
     AlbumUserRole,
   } from '@immich/sdk';
   import { mdiDotsVertical } from '@mdi/js';
-  import { createEventDispatcher, onMount } from 'svelte';
+  import { onMount } from 'svelte';
   import { handleError } from '../../utils/handle-error';
   import ConfirmDialog from '../shared-components/dialog/confirm-dialog.svelte';
   import MenuOption from '../shared-components/context-menu/menu-option.svelte';
@@ -18,18 +18,19 @@
   import { t } from 'svelte-i18n';
   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
 
-  export let album: AlbumResponseDto;
-  export let onClose: () => void;
+  interface Props {
+    album: AlbumResponseDto;
+    onClose: () => void;
+    onRemove: (userId: string) => void;
+    onRefreshAlbum: () => void;
+  }
 
-  const dispatch = createEventDispatcher<{
-    remove: string;
-    refreshAlbum: void;
-  }>();
+  let { album, onClose, onRemove, onRefreshAlbum }: Props = $props();
 
-  let currentUser: UserResponseDto;
-  let selectedRemoveUser: UserResponseDto | null = null;
+  let currentUser: UserResponseDto | undefined = $state();
+  let selectedRemoveUser: UserResponseDto | null = $state(null);
 
-  $: isOwned = currentUser?.id == album.ownerId;
+  let isOwned = $derived(currentUser?.id == album.ownerId);
 
   onMount(async () => {
     try {
@@ -52,7 +53,7 @@
 
     try {
       await removeUserFromAlbum({ id: album.id, userId });
-      dispatch('remove', userId);
+      onRemove(userId);
       const message =
         userId === 'me'
           ? $t('album_user_left', { values: { album: album.albumName } })
@@ -71,7 +72,7 @@
       const message = $t('user_role_set', {
         values: { user: user.name, role: role == AlbumUserRole.Viewer ? $t('role_viewer') : $t('role_editor') },
       });
-      dispatch('refreshAlbum');
+      onRefreshAlbum();
       notificationController.show({ type: NotificationType.Info, message });
     } catch (error) {
       handleError(error, $t('errors.unable_to_change_album_user_role'));
@@ -126,7 +127,7 @@
             {:else if user.id == currentUser?.id}
               <button
                 type="button"
-                on:click={() => (selectedRemoveUser = user)}
+                onclick={() => (selectedRemoveUser = user)}
                 class="text-sm font-medium text-immich-primary transition-colors hover:text-immich-primary/75 dark:text-immich-dark-primary"
                 >{$t('leave')}</button
               >
diff --git a/web/src/lib/components/album-page/user-selection-modal.svelte b/web/src/lib/components/album-page/user-selection-modal.svelte
index 5521d52173..85155866f9 100644
--- a/web/src/lib/components/album-page/user-selection-modal.svelte
+++ b/web/src/lib/components/album-page/user-selection-modal.svelte
@@ -13,15 +13,22 @@
     type UserResponseDto,
   } from '@immich/sdk';
   import { mdiCheck, mdiEye, mdiLink, mdiPencil, mdiShareCircle } from '@mdi/js';
-  import { createEventDispatcher, onMount } from 'svelte';
+  import { onMount } from 'svelte';
   import Button from '../elements/buttons/button.svelte';
   import UserAvatar from '../shared-components/user-avatar.svelte';
   import { t } from 'svelte-i18n';
 
-  export let album: AlbumResponseDto;
-  export let onClose: () => void;
-  let users: UserResponseDto[] = [];
-  let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = {};
+  interface Props {
+    album: AlbumResponseDto;
+    onClose: () => void;
+    onSelect: (selectedUsers: AlbumUserAddDto[]) => void;
+    onShare: () => void;
+  }
+
+  let { album, onClose, onSelect, onShare }: Props = $props();
+
+  let users: UserResponseDto[] = $state([]);
+  let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = $state({});
 
   const roleOptions: Array<{ title: string; value: AlbumUserRole | 'none'; icon?: string }> = [
     { title: $t('role_editor'), value: AlbumUserRole.Editor, icon: mdiPencil },
@@ -29,11 +36,7 @@
     { title: $t('remove_user'), value: 'none' },
   ];
 
-  const dispatch = createEventDispatcher<{
-    select: AlbumUserAddDto[];
-    share: void;
-  }>();
-  let sharedLinks: SharedLinkResponseDto[] = [];
+  let sharedLinks: SharedLinkResponseDto[] = $state([]);
   onMount(async () => {
     await getSharedLinks();
     const data = await searchUsers();
@@ -55,7 +58,6 @@
   const handleToggle = (user: UserResponseDto) => {
     if (Object.keys(selectedUsers).includes(user.id)) {
       delete selectedUsers[user.id];
-      selectedUsers = selectedUsers;
     } else {
       selectedUsers[user.id] = { user, role: AlbumUserRole.Editor };
     }
@@ -64,7 +66,6 @@
   const handleChangeRole = (user: UserResponseDto, role: AlbumUserRole | 'none') => {
     if (role === 'none') {
       delete selectedUsers[user.id];
-      selectedUsers = selectedUsers;
     } else {
       selectedUsers[user.id].role = role;
     }
@@ -99,7 +100,7 @@
                 title={$t('role')}
                 options={roleOptions}
                 render={({ title, icon }) => ({ title, icon })}
-                on:select={({ detail: { value } }) => handleChangeRole(user, value)}
+                onSelect={({ value }) => handleChangeRole(user, value)}
               />
             </div>
           {/key}
@@ -122,11 +123,7 @@
         {#each users as user}
           {#if !Object.keys(selectedUsers).includes(user.id)}
             <div class="flex place-items-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl">
-              <button
-                type="button"
-                on:click={() => handleToggle(user)}
-                class="flex w-full place-items-center gap-4 p-4"
-              >
+              <button type="button" onclick={() => handleToggle(user)} class="flex w-full place-items-center gap-4 p-4">
                 <UserAvatar {user} size="md" />
                 <div class="text-left flex-grow">
                   <p class="text-immich-fg dark:text-immich-dark-fg">
@@ -151,11 +148,9 @@
         fullwidth
         rounded="full"
         disabled={Object.keys(selectedUsers).length === 0}
-        on:click={() =>
-          dispatch(
-            'select',
-            Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })),
-          )}>{$t('add')}</Button
+        onclick={() =>
+          onSelect(Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })))}
+        >{$t('add')}</Button
       >
     </div>
   {/if}
@@ -166,7 +161,7 @@
     <button
       type="button"
       class="flex flex-col place-content-center place-items-center gap-2 hover:cursor-pointer"
-      on:click={() => dispatch('share')}
+      onclick={onShare}
     >
       <Icon path={mdiLink} size={24} />
       <p class="text-sm">{$t('create_link')}</p>
diff --git a/web/src/lib/components/asset-viewer/actions/action.ts b/web/src/lib/components/asset-viewer/actions/action.ts
index d6136f2d18..f8cfd447f0 100644
--- a/web/src/lib/components/asset-viewer/actions/action.ts
+++ b/web/src/lib/components/asset-viewer/actions/action.ts
@@ -12,6 +12,7 @@ type ActionMap = {
   [AssetAction.ADD]: { asset: AssetResponseDto };
   [AssetAction.ADD_TO_ALBUM]: { asset: AssetResponseDto; album: AlbumResponseDto };
   [AssetAction.UNSTACK]: { assets: AssetResponseDto[] };
+  [AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: AssetResponseDto };
 };
 
 export type Action = {
diff --git a/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte b/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte
index 15d3b6accc..ab0da059d0 100644
--- a/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte
+++ b/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte
@@ -9,11 +9,15 @@
   import { mdiImageAlbum, mdiShareVariantOutline } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  export let asset: AssetResponseDto;
-  export let onAction: OnAction;
-  export let shared = false;
+  interface Props {
+    asset: AssetResponseDto;
+    onAction: OnAction;
+    shared?: boolean;
+  }
 
-  let showSelectionModal = false;
+  let { asset, onAction, shared = false }: Props = $props();
+
+  let showSelectionModal = $state(false);
 
   const handleAddToNewAlbum = async (albumName: string) => {
     showSelectionModal = false;
@@ -40,8 +44,8 @@
   <Portal target="body">
     <AlbumSelectionModal
       {shared}
-      on:newAlbum={({ detail }) => handleAddToNewAlbum(detail)}
-      on:album={({ detail }) => handleAddToAlbum(detail)}
+      onNewAlbum={handleAddToNewAlbum}
+      onAlbumClick={handleAddToAlbum}
       onClose={() => (showSelectionModal = false)}
     />
   </Portal>
diff --git a/web/src/lib/components/asset-viewer/actions/archive-action.svelte b/web/src/lib/components/asset-viewer/actions/archive-action.svelte
index 3e2c453f39..6337b27892 100644
--- a/web/src/lib/components/asset-viewer/actions/archive-action.svelte
+++ b/web/src/lib/components/asset-viewer/actions/archive-action.svelte
@@ -8,8 +8,12 @@
   import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  export let asset: AssetResponseDto;
-  export let onAction: OnAction;
+  interface Props {
+    asset: AssetResponseDto;
+    onAction: OnAction;
+  }
+
+  let { asset, onAction }: Props = $props();
 
   const onArchive = async () => {
     const updatedAsset = await toggleArchive(asset);
diff --git a/web/src/lib/components/asset-viewer/actions/close-action.svelte b/web/src/lib/components/asset-viewer/actions/close-action.svelte
index 647ad61e4f..26cb81edd8 100644
--- a/web/src/lib/components/asset-viewer/actions/close-action.svelte
+++ b/web/src/lib/components/asset-viewer/actions/close-action.svelte
@@ -4,9 +4,13 @@
   import { mdiArrowLeft } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  export let onClose: () => void;
+  interface Props {
+    onClose: () => void;
+  }
+
+  let { onClose }: Props = $props();
 </script>
 
 <svelte:window use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onClose }} />
 
-<CircleIconButton color="opaque" icon={mdiArrowLeft} title={$t('go_back')} on:click={onClose} />
+<CircleIconButton color="opaque" icon={mdiArrowLeft} title={$t('go_back')} onclick={onClose} />
diff --git a/web/src/lib/components/asset-viewer/actions/delete-action.svelte b/web/src/lib/components/asset-viewer/actions/delete-action.svelte
index 1e3cfdd28d..c0f163634a 100644
--- a/web/src/lib/components/asset-viewer/actions/delete-action.svelte
+++ b/web/src/lib/components/asset-viewer/actions/delete-action.svelte
@@ -16,10 +16,14 @@
   import { t } from 'svelte-i18n';
   import type { OnAction } from './action';
 
-  export let asset: AssetResponseDto;
-  export let onAction: OnAction;
+  interface Props {
+    asset: AssetResponseDto;
+    onAction: OnAction;
+  }
 
-  let showConfirmModal = false;
+  let { asset, onAction }: Props = $props();
+
+  let showConfirmModal = $state(false);
 
   const trashOrDelete = async (force = false) => {
     if (force || !$featureFlags.trash) {
@@ -77,11 +81,11 @@
   color="opaque"
   icon={asset.isTrashed ? mdiDeleteForeverOutline : mdiDeleteOutline}
   title={asset.isTrashed ? $t('permanently_delete') : $t('delete')}
-  on:click={() => trashOrDelete(asset.isTrashed)}
+  onclick={() => trashOrDelete(asset.isTrashed)}
 />
 
 {#if showConfirmModal}
   <Portal target="body">
-    <DeleteAssetDialog size={1} on:cancel={() => (showConfirmModal = false)} on:confirm={() => deleteAsset()} />
+    <DeleteAssetDialog size={1} onCancel={() => (showConfirmModal = false)} onConfirm={deleteAsset} />
   </Portal>
 {/if}
diff --git a/web/src/lib/components/asset-viewer/actions/download-action.svelte b/web/src/lib/components/asset-viewer/actions/download-action.svelte
index 88c0eeadf2..d7f4f56352 100644
--- a/web/src/lib/components/asset-viewer/actions/download-action.svelte
+++ b/web/src/lib/components/asset-viewer/actions/download-action.svelte
@@ -7,8 +7,12 @@
   import { mdiFolderDownloadOutline } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  export let asset: AssetResponseDto;
-  export let menuItem = false;
+  interface Props {
+    asset: AssetResponseDto;
+    menuItem?: boolean;
+  }
+
+  let { asset, menuItem = false }: Props = $props();
 
   const onDownloadFile = () => downloadFile(asset);
 </script>
@@ -16,7 +20,7 @@
 <svelte:window use:shortcut={{ shortcut: { key: 'd', shift: true }, onShortcut: onDownloadFile }} />
 
 {#if !menuItem}
-  <CircleIconButton color="opaque" icon={mdiFolderDownloadOutline} title={$t('download')} on:click={onDownloadFile} />
+  <CircleIconButton color="opaque" icon={mdiFolderDownloadOutline} title={$t('download')} onclick={onDownloadFile} />
 {:else}
   <MenuOption icon={mdiFolderDownloadOutline} text={$t('download')} onClick={onDownloadFile} />
 {/if}
diff --git a/web/src/lib/components/asset-viewer/actions/favorite-action.svelte b/web/src/lib/components/asset-viewer/actions/favorite-action.svelte
index 488ed7ecb2..0cc3188d51 100644
--- a/web/src/lib/components/asset-viewer/actions/favorite-action.svelte
+++ b/web/src/lib/components/asset-viewer/actions/favorite-action.svelte
@@ -12,8 +12,12 @@
   import { t } from 'svelte-i18n';
   import type { OnAction } from './action';
 
-  export let asset: AssetResponseDto;
-  export let onAction: OnAction;
+  interface Props {
+    asset: AssetResponseDto;
+    onAction: OnAction;
+  }
+
+  let { asset, onAction }: Props = $props();
 
   const toggleFavorite = async () => {
     try {
@@ -24,7 +28,8 @@
         },
       });
 
-      asset.isFavorite = data.isFavorite;
+      asset = { ...asset, isFavorite: data.isFavorite };
+
       onAction({ type: asset.isFavorite ? AssetAction.FAVORITE : AssetAction.UNFAVORITE, asset });
 
       notificationController.show({
@@ -43,5 +48,5 @@
   color="opaque"
   icon={asset.isFavorite ? mdiHeart : mdiHeartOutline}
   title={asset.isFavorite ? $t('unfavorite') : $t('to_favorite')}
-  on:click={toggleFavorite}
+  onclick={toggleFavorite}
 />
diff --git a/web/src/lib/components/asset-viewer/actions/keep-this-delete-others.svelte b/web/src/lib/components/asset-viewer/actions/keep-this-delete-others.svelte
new file mode 100644
index 0000000000..3d52b5f28d
--- /dev/null
+++ b/web/src/lib/components/asset-viewer/actions/keep-this-delete-others.svelte
@@ -0,0 +1,33 @@
+<script lang="ts">
+  import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
+  import { AssetAction } from '$lib/constants';
+  import { keepThisDeleteOthers } from '$lib/utils/asset-utils';
+  import type { AssetResponseDto, StackResponseDto } from '@immich/sdk';
+  import { mdiPinOutline } from '@mdi/js';
+  import type { OnAction } from './action';
+  import { t } from 'svelte-i18n';
+  import { dialogController } from '$lib/components/shared-components/dialog/dialog';
+
+  export let stack: StackResponseDto;
+  export let asset: AssetResponseDto;
+  export let onAction: OnAction;
+
+  const handleKeepThisDeleteOthers = async () => {
+    const isConfirmed = await dialogController.show({
+      title: $t('keep_this_delete_others'),
+      prompt: $t('confirm_keep_this_delete_others'),
+      confirmText: $t('delete_others'),
+    });
+
+    if (!isConfirmed) {
+      return;
+    }
+
+    const keptAsset = await keepThisDeleteOthers(asset, stack);
+    if (keptAsset) {
+      onAction({ type: AssetAction.UNSTACK, assets: [keptAsset] });
+    }
+  };
+</script>
+
+<MenuOption icon={mdiPinOutline} onClick={handleKeepThisDeleteOthers} text={$t('keep_this_delete_others')} />
diff --git a/web/src/lib/components/asset-viewer/actions/motion-photo-action.svelte b/web/src/lib/components/asset-viewer/actions/motion-photo-action.svelte
index fd519a05d4..f629a42db7 100644
--- a/web/src/lib/components/asset-viewer/actions/motion-photo-action.svelte
+++ b/web/src/lib/components/asset-viewer/actions/motion-photo-action.svelte
@@ -3,13 +3,17 @@
   import { mdiMotionPauseOutline, mdiPlaySpeed } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  export let isPlaying: boolean;
-  export let onClick: (shouldPlay: boolean) => void;
+  interface Props {
+    isPlaying: boolean;
+    onClick: (shouldPlay: boolean) => void;
+  }
+
+  let { isPlaying, onClick }: Props = $props();
 </script>
 
 <CircleIconButton
   color="opaque"
   icon={isPlaying ? mdiMotionPauseOutline : mdiPlaySpeed}
   title={isPlaying ? $t('stop_motion_photo') : $t('play_motion_photo')}
-  on:click={() => onClick(!isPlaying)}
+  onclick={() => onClick(!isPlaying)}
 />
diff --git a/web/src/lib/components/asset-viewer/actions/next-asset-action.svelte b/web/src/lib/components/asset-viewer/actions/next-asset-action.svelte
index cc074f3b6c..355f816a6b 100644
--- a/web/src/lib/components/asset-viewer/actions/next-asset-action.svelte
+++ b/web/src/lib/components/asset-viewer/actions/next-asset-action.svelte
@@ -5,7 +5,11 @@
   import { t } from 'svelte-i18n';
   import NavigationArea from '../navigation-area.svelte';
 
-  export let onNextAsset: () => void;
+  interface Props {
+    onNextAsset: () => void;
+  }
+
+  let { onNextAsset }: Props = $props();
 </script>
 
 <svelte:window
diff --git a/web/src/lib/components/asset-viewer/actions/previous-asset-action.svelte b/web/src/lib/components/asset-viewer/actions/previous-asset-action.svelte
index 9f8c638e12..1770bc673a 100644
--- a/web/src/lib/components/asset-viewer/actions/previous-asset-action.svelte
+++ b/web/src/lib/components/asset-viewer/actions/previous-asset-action.svelte
@@ -5,7 +5,11 @@
   import { t } from 'svelte-i18n';
   import NavigationArea from '../navigation-area.svelte';
 
-  export let onPreviousAsset: () => void;
+  interface Props {
+    onPreviousAsset: () => void;
+  }
+
+  let { onPreviousAsset }: Props = $props();
 </script>
 
 <svelte:window
diff --git a/web/src/lib/components/asset-viewer/actions/restore-action.svelte b/web/src/lib/components/asset-viewer/actions/restore-action.svelte
index c000dad9a1..abcae5c4c9 100644
--- a/web/src/lib/components/asset-viewer/actions/restore-action.svelte
+++ b/web/src/lib/components/asset-viewer/actions/restore-action.svelte
@@ -11,8 +11,12 @@
   import { t } from 'svelte-i18n';
   import type { OnAction } from './action';
 
-  export let asset: AssetResponseDto;
-  export let onAction: OnAction;
+  interface Props {
+    asset: AssetResponseDto;
+    onAction: OnAction;
+  }
+
+  let { asset = $bindable(), onAction }: Props = $props();
 
   const handleRestoreAsset = async () => {
     try {
diff --git a/web/src/lib/components/asset-viewer/actions/set-album-cover-action.svelte b/web/src/lib/components/asset-viewer/actions/set-album-cover-action.svelte
index f20c4872bc..c015c224ff 100644
--- a/web/src/lib/components/asset-viewer/actions/set-album-cover-action.svelte
+++ b/web/src/lib/components/asset-viewer/actions/set-album-cover-action.svelte
@@ -9,8 +9,12 @@
   import { mdiImageOutline } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  export let asset: AssetResponseDto;
-  export let album: AlbumResponseDto;
+  interface Props {
+    asset: AssetResponseDto;
+    album: AlbumResponseDto;
+  }
+
+  let { asset, album }: Props = $props();
 
   const handleUpdateThumbnail = async () => {
     try {
diff --git a/web/src/lib/components/asset-viewer/actions/set-person-featured-action.svelte b/web/src/lib/components/asset-viewer/actions/set-person-featured-action.svelte
new file mode 100644
index 0000000000..70e1c4f1ba
--- /dev/null
+++ b/web/src/lib/components/asset-viewer/actions/set-person-featured-action.svelte
@@ -0,0 +1,29 @@
+<script lang="ts">
+  import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
+  import {
+    notificationController,
+    NotificationType,
+  } from '$lib/components/shared-components/notification/notification';
+  import { handleError } from '$lib/utils/handle-error';
+  import { updatePerson, type AssetResponseDto, type PersonResponseDto } from '@immich/sdk';
+  import { mdiFaceManProfile } from '@mdi/js';
+  import { t } from 'svelte-i18n';
+
+  interface Props {
+    asset: AssetResponseDto;
+    person: PersonResponseDto;
+  }
+
+  let { asset, person }: Props = $props();
+
+  const handleSelectFeaturePhoto = async () => {
+    try {
+      await updatePerson({ id: person.id, personUpdateDto: { featureFaceAssetId: asset.id } });
+      notificationController.show({ message: $t('feature_photo_updated'), type: NotificationType.Info });
+    } catch (error) {
+      handleError(error, $t('errors.unable_to_set_feature_photo'));
+    }
+  };
+</script>
+
+<MenuOption text={$t('set_as_featured_photo')} icon={mdiFaceManProfile} onClick={handleSelectFeaturePhoto} />
diff --git a/web/src/lib/components/asset-viewer/actions/set-profile-picture-action.svelte b/web/src/lib/components/asset-viewer/actions/set-profile-picture-action.svelte
index 23c147815c..a35ff11c48 100644
--- a/web/src/lib/components/asset-viewer/actions/set-profile-picture-action.svelte
+++ b/web/src/lib/components/asset-viewer/actions/set-profile-picture-action.svelte
@@ -6,9 +6,13 @@
   import { mdiAccountCircleOutline } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  export let asset: AssetResponseDto;
+  interface Props {
+    asset: AssetResponseDto;
+  }
 
-  let showProfileImageCrop = false;
+  let { asset }: Props = $props();
+
+  let showProfileImageCrop = $state(false);
 </script>
 
 <MenuOption
diff --git a/web/src/lib/components/asset-viewer/actions/share-action.svelte b/web/src/lib/components/asset-viewer/actions/share-action.svelte
index f0b2177128..6fd5aa456e 100644
--- a/web/src/lib/components/asset-viewer/actions/share-action.svelte
+++ b/web/src/lib/components/asset-viewer/actions/share-action.svelte
@@ -6,17 +6,16 @@
   import { mdiShareVariantOutline } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  export let asset: AssetResponseDto;
+  interface Props {
+    asset: AssetResponseDto;
+  }
 
-  let showModal = false;
+  let { asset }: Props = $props();
+
+  let showModal = $state(false);
 </script>
 
-<CircleIconButton
-  color="opaque"
-  icon={mdiShareVariantOutline}
-  on:click={() => (showModal = true)}
-  title={$t('share')}
-/>
+<CircleIconButton color="opaque" icon={mdiShareVariantOutline} onclick={() => (showModal = true)} title={$t('share')} />
 
 {#if showModal}
   <Portal target="body">
diff --git a/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte b/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte
index 66e5d0e10f..5613114cad 100644
--- a/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte
+++ b/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte
@@ -4,9 +4,13 @@
   import { mdiInformationOutline } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  export let onShowDetail: () => void;
+  interface Props {
+    onShowDetail: () => void;
+  }
+
+  let { onShowDetail }: Props = $props();
 </script>
 
 <svelte:window use:shortcut={{ shortcut: { key: 'i' }, onShortcut: onShowDetail }} />
 
-<CircleIconButton color="opaque" icon={mdiInformationOutline} on:click={onShowDetail} title={$t('info')} />
+<CircleIconButton color="opaque" icon={mdiInformationOutline} onclick={onShowDetail} title={$t('info')} />
diff --git a/web/src/lib/components/asset-viewer/actions/unstack-action.svelte b/web/src/lib/components/asset-viewer/actions/unstack-action.svelte
index bd18e0e8bf..f2a50cce13 100644
--- a/web/src/lib/components/asset-viewer/actions/unstack-action.svelte
+++ b/web/src/lib/components/asset-viewer/actions/unstack-action.svelte
@@ -7,8 +7,12 @@
   import { t } from 'svelte-i18n';
   import type { OnAction } from './action';
 
-  export let stack: StackResponseDto;
-  export let onAction: OnAction;
+  interface Props {
+    stack: StackResponseDto;
+    onAction: OnAction;
+  }
+
+  let { stack, onAction }: Props = $props();
 
   const handleUnstack = async () => {
     const unstackedAssets = await deleteStack([stack.id]);
diff --git a/web/src/lib/components/asset-viewer/activity-status.svelte b/web/src/lib/components/asset-viewer/activity-status.svelte
index 8646570fec..494c6fcbf7 100644
--- a/web/src/lib/components/asset-viewer/activity-status.svelte
+++ b/web/src/lib/components/asset-viewer/activity-status.svelte
@@ -2,26 +2,26 @@
   import { locale } from '$lib/stores/preferences.store';
   import type { ActivityResponseDto } from '@immich/sdk';
   import { mdiCommentOutline, mdiHeart, mdiHeartOutline } from '@mdi/js';
-  import { createEventDispatcher } from 'svelte';
   import Icon from '../elements/icon.svelte';
 
-  export let isLiked: ActivityResponseDto | null;
-  export let numberOfComments: number | undefined;
-  export let disabled: boolean;
+  interface Props {
+    isLiked: ActivityResponseDto | null;
+    numberOfComments: number | undefined;
+    disabled: boolean;
+    onOpenActivityTab: () => void;
+    onFavorite: () => void;
+  }
 
-  const dispatch = createEventDispatcher<{
-    openActivityTab: void;
-    favorite: void;
-  }>();
+  let { isLiked, numberOfComments, disabled, onOpenActivityTab, onFavorite }: Props = $props();
 </script>
 
 <div class="w-full flex p-4 text-white items-center justify-center rounded-full gap-5 bg-immich-dark-bg bg-opacity-60">
-  <button type="button" class={disabled ? 'cursor-not-allowed' : ''} on:click={() => dispatch('favorite')} {disabled}>
+  <button type="button" class={disabled ? 'cursor-not-allowed' : ''} onclick={onFavorite} {disabled}>
     <div class="items-center justify-center">
       <Icon path={isLiked ? mdiHeart : mdiHeartOutline} size={24} />
     </div>
   </button>
-  <button type="button" on:click={() => dispatch('openActivityTab')}>
+  <button type="button" onclick={onOpenActivityTab}>
     <div class="flex gap-2 items-center justify-center">
       <Icon path={mdiCommentOutline} class="scale-x-[-1]" size={24} />
       {#if numberOfComments}
diff --git a/web/src/lib/components/asset-viewer/activity-viewer.svelte b/web/src/lib/components/asset-viewer/activity-viewer.svelte
index 050802e1d0..caa1ced290 100644
--- a/web/src/lib/components/asset-viewer/activity-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/activity-viewer.svelte
@@ -17,7 +17,7 @@
   } from '@immich/sdk';
   import { mdiClose, mdiDotsVertical, mdiHeart, mdiSend, mdiDeleteOutline } from '@mdi/js';
   import * as luxon from 'luxon';
-  import { createEventDispatcher, onMount } from 'svelte';
+  import { onMount } from 'svelte';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
   import LoadingSpinner from '../shared-components/loading-spinner.svelte';
   import { NotificationType, notificationController } from '../shared-components/notification/notification';
@@ -47,43 +47,44 @@
     return relativeFormatter.format(Math.trunc(diff.as(unit)), unit);
   };
 
-  export let reactions: ActivityResponseDto[];
-  export let user: UserResponseDto;
-  export let assetId: string | undefined = undefined;
-  export let albumId: string;
-  export let assetType: AssetTypeEnum | undefined = undefined;
-  export let albumOwnerId: string;
-  export let disabled: boolean;
-  export let isLiked: ActivityResponseDto | null;
-
-  let textArea: HTMLTextAreaElement;
-  let innerHeight: number;
-  let activityHeight: number;
-  let chatHeight: number;
-  let divHeight: number;
-  let previousAssetId: string | undefined = assetId;
-  let message = '';
-  let isSendingMessage = false;
-
-  const dispatch = createEventDispatcher<{
-    deleteComment: void;
-    deleteLike: void;
-    addComment: void;
-    close: void;
-  }>();
-
-  $: {
-    if (innerHeight && activityHeight) {
-      divHeight = innerHeight - activityHeight;
-    }
+  interface Props {
+    reactions: ActivityResponseDto[];
+    user: UserResponseDto;
+    assetId?: string | undefined;
+    albumId: string;
+    assetType?: AssetTypeEnum | undefined;
+    albumOwnerId: string;
+    disabled: boolean;
+    isLiked: ActivityResponseDto | null;
+    onDeleteComment: () => void;
+    onDeleteLike: () => void;
+    onAddComment: () => void;
+    onClose: () => void;
   }
 
-  $: {
-    if (assetId && previousAssetId != assetId) {
-      handlePromiseError(getReactions());
-      previousAssetId = assetId;
-    }
-  }
+  let {
+    reactions = $bindable(),
+    user,
+    assetId = undefined,
+    albumId,
+    assetType = undefined,
+    albumOwnerId,
+    disabled,
+    isLiked,
+    onDeleteComment,
+    onDeleteLike,
+    onAddComment,
+    onClose,
+  }: Props = $props();
+
+  let innerHeight: number = $state(0);
+  let activityHeight: number = $state(0);
+  let chatHeight: number = $state(0);
+  let divHeight: number = $state(0);
+  let previousAssetId: string | undefined = $state(assetId);
+  let message = $state('');
+  let isSendingMessage = $state(false);
+
   onMount(async () => {
     await getReactions();
   });
@@ -109,11 +110,10 @@
     try {
       await deleteActivity({ id: reaction.id });
       reactions.splice(index, 1);
-      reactions = reactions;
       if (isLiked && reaction.type === ReactionType.Like && reaction.id == isLiked.id) {
-        dispatch('deleteLike');
+        onDeleteLike();
       } else {
-        dispatch('deleteComment');
+        onDeleteComment();
       }
 
       const deleteMessages: Record<ReactionType, string> = {
@@ -139,11 +139,9 @@
         activityCreateDto: { albumId, assetId, type: ReactionType.Comment, comment: message },
       });
       reactions.push(data);
-      textArea.style.height = '18px';
+
       message = '';
-      dispatch('addComment');
-      // Re-render the activity feed
-      reactions = reactions;
+      onAddComment();
     } catch (error) {
       handleError(error, $t('errors.unable_to_add_comment'));
     } finally {
@@ -151,6 +149,22 @@
     }
     isSendingMessage = false;
   };
+  $effect(() => {
+    if (innerHeight && activityHeight) {
+      divHeight = innerHeight - activityHeight;
+    }
+  });
+  $effect(() => {
+    if (assetId && previousAssetId != assetId) {
+      handlePromiseError(getReactions());
+      previousAssetId = assetId;
+    }
+  });
+
+  const onsubmit = async (event: Event) => {
+    event.preventDefault();
+    await handleSendComment();
+  };
 </script>
 
 <div class="overflow-y-hidden relative h-full" bind:offsetHeight={innerHeight}>
@@ -160,7 +174,7 @@
       bind:clientHeight={activityHeight}
     >
       <div class="flex place-items-center gap-2">
-        <CircleIconButton on:click={() => dispatch('close')} icon={mdiClose} title={$t('close')} />
+        <CircleIconButton onclick={onClose} icon={mdiClose} title={$t('close')} />
 
         <p class="text-lg text-immich-fg dark:text-immich-dark-fg">{$t('activity')}</p>
       </div>
@@ -280,15 +294,13 @@
         <div>
           <UserAvatar {user} size="md" showTitle={false} />
         </div>
-        <form class="flex w-full max-h-56 gap-1" on:submit|preventDefault={() => handleSendComment()}>
+        <form class="flex w-full max-h-56 gap-1" {onsubmit}>
           <div class="flex w-full items-center gap-4">
             <textarea
               {disabled}
-              bind:this={textArea}
               bind:value={message}
-              use:autoGrowHeight={'5px'}
+              use:autoGrowHeight={{ height: '5px', value: message }}
               placeholder={disabled ? $t('comments_are_disabled') : $t('say_something')}
-              on:input={() => autoGrowHeight(textArea, '5px')}
               use:shortcut={{
                 shortcut: { key: 'Enter' },
                 onShortcut: () => handleSendComment(),
@@ -296,7 +308,7 @@
               class="h-[18px] {disabled
                 ? 'cursor-not-allowed'
                 : ''} w-full max-h-56 pr-2 items-center overflow-y-auto leading-4 outline-none resize-none bg-gray-200"
-            />
+            ></textarea>
           </div>
           {#if isSendingMessage}
             <div class="flex items-end place-items-center pb-2 ml-0">
@@ -311,7 +323,7 @@
                 size="15"
                 icon={mdiSend}
                 class="dark:text-immich-dark-gray"
-                on:click={() => handleSendComment()}
+                onclick={() => handleSendComment()}
               />
             </div>
           {/if}
diff --git a/web/src/lib/components/asset-viewer/album-list-item-details.svelte b/web/src/lib/components/asset-viewer/album-list-item-details.svelte
index ecc38b7c24..08dd105ca1 100644
--- a/web/src/lib/components/asset-viewer/album-list-item-details.svelte
+++ b/web/src/lib/components/asset-viewer/album-list-item-details.svelte
@@ -2,7 +2,11 @@
   import type { AlbumResponseDto } from '@immich/sdk';
   import { t } from 'svelte-i18n';
 
-  export let album: AlbumResponseDto;
+  interface Props {
+    album: AlbumResponseDto;
+  }
+
+  let { album }: Props = $props();
 </script>
 
 <span>{$t('items_count', { values: { count: album.assetCount } })}</span>
diff --git a/web/src/lib/components/asset-viewer/album-list-item.svelte b/web/src/lib/components/asset-viewer/album-list-item.svelte
index ab049da652..43352a4904 100644
--- a/web/src/lib/components/asset-viewer/album-list-item.svelte
+++ b/web/src/lib/components/asset-viewer/album-list-item.svelte
@@ -1,21 +1,22 @@
 <script lang="ts">
   import { getAssetThumbnailUrl } from '$lib/utils';
   import { type AlbumResponseDto } from '@immich/sdk';
-  import { createEventDispatcher } from 'svelte';
   import { normalizeSearchString } from '$lib/utils/string-utils.js';
   import AlbumListItemDetails from './album-list-item-details.svelte';
 
-  const dispatch = createEventDispatcher<{
-    album: void;
-  }>();
+  interface Props {
+    album: AlbumResponseDto;
+    searchQuery?: string;
+    onAlbumClick: () => void;
+  }
 
-  export let album: AlbumResponseDto;
-  export let searchQuery = '';
-  let albumNameArray: string[] = ['', '', ''];
+  let { album, searchQuery = '', onAlbumClick }: Props = $props();
+
+  let albumNameArray: string[] = $state(['', '', '']);
 
   // This part of the code is responsible for splitting album name into 3 parts where part 2 is the search query
   // It is used to highlight the search query in the album name
-  $: {
+  $effect(() => {
     let { albumName } = album;
     let findIndex = normalizeSearchString(albumName).indexOf(normalizeSearchString(searchQuery));
     let findLength = searchQuery.length;
@@ -24,12 +25,12 @@
       albumName.slice(findIndex, findIndex + findLength),
       albumName.slice(findIndex + findLength),
     ];
-  }
+  });
 </script>
 
 <button
   type="button"
-  on:click={() => dispatch('album')}
+  onclick={onAlbumClick}
   class="flex w-full gap-4 px-6 py-2 text-left transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"
 >
   <span class="h-12 w-12 shrink-0 rounded-xl bg-slate-300">
diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts
index c82d9f9659..a25ea6bf90 100644
--- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts
+++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts
@@ -15,13 +15,31 @@ describe('AssetViewerNavBar component', () => {
     showShareButton: false,
     onZoomImage: () => {},
     onCopyImage: () => {},
+    onAction: () => {},
+    onRunJob: () => {},
+    onPlaySlideshow: () => {},
+    onShowDetail: () => {},
+    onClose: () => {},
   };
 
+  beforeAll(() => {
+    Element.prototype.animate = vi.fn().mockImplementation(() => ({
+      cancel: () => {},
+    }));
+    vi.stubGlobal(
+      'ResizeObserver',
+      vi.fn(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn() })),
+    );
+  });
+
   afterEach(() => {
-    vi.resetAllMocks();
     resetSavedUser();
   });
 
+  afterAll(() => {
+    vi.restoreAllMocks();
+  });
+
   it('shows back button', () => {
     const asset = assetFactory.build({ isTrashed: false });
     const { getByTitle } = render(AssetViewerNavBar, { asset, ...additionalProps });
diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte
index db216641d5..442302198b 100644
--- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte
+++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte
@@ -9,10 +9,12 @@
   import FavoriteAction from '$lib/components/asset-viewer/actions/favorite-action.svelte';
   import RestoreAction from '$lib/components/asset-viewer/actions/restore-action.svelte';
   import SetAlbumCoverAction from '$lib/components/asset-viewer/actions/set-album-cover-action.svelte';
+  import SetFeaturedPhotoAction from '$lib/components/asset-viewer/actions/set-person-featured-action.svelte';
   import SetProfilePictureAction from '$lib/components/asset-viewer/actions/set-profile-picture-action.svelte';
   import ShareAction from '$lib/components/asset-viewer/actions/share-action.svelte';
   import ShowDetailAction from '$lib/components/asset-viewer/actions/show-detail-action.svelte';
   import UnstackAction from '$lib/components/asset-viewer/actions/unstack-action.svelte';
+  import KeepThisDeleteOthersAction from '$lib/components/asset-viewer/actions/keep-this-delete-others.svelte';
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
   import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
@@ -26,6 +28,7 @@
     AssetTypeEnum,
     type AlbumResponseDto,
     type AssetResponseDto,
+    type PersonResponseDto,
     type StackResponseDto,
   } from '@immich/sdk';
   import {
@@ -34,6 +37,7 @@
     mdiContentCopy,
     mdiDatabaseRefreshOutline,
     mdiDotsVertical,
+    mdiHeadSyncOutline,
     mdiImageRefreshOutline,
     mdiImageSearch,
     mdiMagnifyMinusOutline,
@@ -43,25 +47,46 @@
   } from '@mdi/js';
   import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
   import { t } from 'svelte-i18n';
+  import type { Snippet } from 'svelte';
 
-  export let asset: AssetResponseDto;
-  export let album: AlbumResponseDto | null = null;
-  export let stack: StackResponseDto | null = null;
-  export let showDetailButton: boolean;
-  export let showSlideshow = false;
-  export let onZoomImage: () => void;
-  export let onCopyImage: () => void;
-  export let onAction: OnAction;
-  export let onRunJob: (name: AssetJobName) => void;
-  export let onPlaySlideshow: () => void;
-  export let onShowDetail: () => void;
-  // export let showEditorHandler: () => void;
-  export let onClose: () => void;
+  interface Props {
+    asset: AssetResponseDto;
+    album?: AlbumResponseDto | null;
+    person?: PersonResponseDto | null;
+    stack?: StackResponseDto | null;
+    showDetailButton: boolean;
+    showSlideshow?: boolean;
+    onZoomImage: () => void;
+    onCopyImage?: () => Promise<void>;
+    onAction: OnAction;
+    onRunJob: (name: AssetJobName) => void;
+    onPlaySlideshow: () => void;
+    onShowDetail: () => void;
+    // export let showEditorHandler: () => void;
+    onClose: () => void;
+    motionPhoto?: Snippet;
+  }
+
+  let {
+    asset,
+    album = null,
+    person = null,
+    stack = null,
+    showDetailButton,
+    showSlideshow = false,
+    onZoomImage,
+    onCopyImage,
+    onAction,
+    onRunJob,
+    onPlaySlideshow,
+    onShowDetail,
+    onClose,
+    motionPhoto,
+  }: Props = $props();
 
   const sharedLink = getSharedLink();
-
-  $: isOwner = $user && asset.ownerId === $user?.id;
-  $: showDownloadButton = sharedLink ? sharedLink.allowDownload : !asset.isOffline;
+  let isOwner = $derived($user && asset.ownerId === $user?.id);
+  let showDownloadButton = $derived(sharedLink ? sharedLink.allowDownload : !asset.isOffline);
   // $: showEditorButton =
   //   isOwner &&
   //   asset.type === AssetTypeEnum.Image &&
@@ -79,18 +104,15 @@
   <div class="text-white">
     <CloseAction {onClose} />
   </div>
-  <div
-    class="flex w-[calc(100%-3rem)] justify-end gap-2 overflow-hidden text-white"
-    data-testid="asset-viewer-navbar-actions"
-  >
+  <div class="flex gap-2 overflow-x-auto text-white" data-testid="asset-viewer-navbar-actions">
     {#if !asset.isTrashed && $user}
       <ShareAction {asset} />
     {/if}
     {#if asset.isOffline}
-      <CircleIconButton color="opaque" icon={mdiAlertOutline} on:click={onShowDetail} title={$t('asset_offline')} />
+      <CircleIconButton color="alert" icon={mdiAlertOutline} onclick={onShowDetail} title={$t('asset_offline')} />
     {/if}
     {#if asset.livePhotoVideoId}
-      <slot name="motion-photo" />
+      {@render motionPhoto?.()}
     {/if}
     {#if asset.type === AssetTypeEnum.Image}
       <CircleIconButton
@@ -98,11 +120,11 @@
         hideMobile={true}
         icon={$photoZoomState && $photoZoomState.currentZoom > 1 ? mdiMagnifyMinusOutline : mdiMagnifyPlusOutline}
         title={$t('zoom_image')}
-        on:click={onZoomImage}
+        onclick={onZoomImage}
       />
     {/if}
     {#if canCopyImageToClipboard() && asset.type === AssetTypeEnum.Image}
-      <CircleIconButton color="opaque" icon={mdiContentCopy} title={$t('copy_image')} on:click={onCopyImage} />
+      <CircleIconButton color="opaque" icon={mdiContentCopy} title={$t('copy_image')} onclick={() => onCopyImage?.()} />
     {/if}
 
     {#if !isOwner && showDownloadButton}
@@ -121,7 +143,7 @@
         color="opaque"
         hideMobile={true}
         icon={mdiImageEditOutline}
-        on:click={showEditorHandler}
+        onclick={showEditorHandler}
         title={$t('editor')}
       />
     {/if} -->
@@ -146,10 +168,14 @@
         {#if isOwner}
           {#if stack}
             <UnstackAction {stack} {onAction} />
+            <KeepThisDeleteOthersAction {stack} {asset} {onAction} />
           {/if}
           {#if album}
             <SetAlbumCoverAction {asset} {album} />
           {/if}
+          {#if person}
+            <SetFeaturedPhotoAction {asset} {person} />
+          {/if}
           {#if asset.type === AssetTypeEnum.Image}
             <SetProfilePictureAction {asset} />
           {/if}
@@ -167,6 +193,11 @@
             />
           {/if}
           <hr />
+          <MenuOption
+            icon={mdiHeadSyncOutline}
+            onClick={() => onRunJob(AssetJobName.RefreshFaces)}
+            text={$getAssetJobName(AssetJobName.RefreshFaces)}
+          />
           <MenuOption
             icon={mdiDatabaseRefreshOutline}
             onClick={() => onRunJob(AssetJobName.RefreshMetadata)}
diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte
index 69d35b9aa4..7a2f97bb65 100644
--- a/web/src/lib/components/asset-viewer/asset-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte
@@ -30,9 +30,10 @@
     type ActivityResponseDto,
     type AlbumResponseDto,
     type AssetResponseDto,
+    type PersonResponseDto,
     type StackResponseDto,
   } from '@immich/sdk';
-  import { createEventDispatcher, onDestroy, onMount } from 'svelte';
+  import { onDestroy, onMount, untrack } from 'svelte';
   import { t } from 'svelte-i18n';
   import { fly } from 'svelte/transition';
   import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
@@ -43,21 +44,44 @@
   import DetailPanel from './detail-panel.svelte';
   import CropArea from './editor/crop-tool/crop-area.svelte';
   import EditorPanel from './editor/editor-panel.svelte';
-  import PanoramaViewer from './panorama-viewer.svelte';
   import PhotoViewer from './photo-viewer.svelte';
   import SlideshowBar from './slideshow-bar.svelte';
   import VideoViewer from './video-wrapper-viewer.svelte';
+  import ImagePanoramaViewer from './image-panorama-viewer.svelte';
 
-  export let assetStore: AssetStore | null = null;
-  export let asset: AssetResponseDto;
-  export let preloadAssets: AssetResponseDto[] = [];
-  export let showNavigation = true;
-  export let withStacked = false;
-  export let isShared = false;
-  export let album: AlbumResponseDto | null = null;
-  export let onAction: OnAction | undefined = undefined;
+  interface Props {
+    assetStore?: AssetStore | null;
+    asset: AssetResponseDto;
+    preloadAssets?: AssetResponseDto[];
+    showNavigation?: boolean;
+    withStacked?: boolean;
+    isShared?: boolean;
+    album?: AlbumResponseDto | null;
+    person?: PersonResponseDto | null;
+    onAction?: OnAction | undefined;
+    reactions?: ActivityResponseDto[];
+    onClose: (dto: { asset: AssetResponseDto }) => void;
+    onNext: () => void;
+    onPrevious: () => void;
+    copyImage?: () => Promise<void>;
+  }
 
-  let reactions: ActivityResponseDto[] = [];
+  let {
+    assetStore = null,
+    asset = $bindable(),
+    preloadAssets = $bindable([]),
+    showNavigation = true,
+    withStacked = false,
+    isShared = false,
+    album = null,
+    person = null,
+    onAction = undefined,
+    reactions = $bindable([]),
+    onClose,
+    onNext,
+    onPrevious,
+    copyImage = $bindable(),
+  }: Props = $props();
 
   const { setAsset } = assetViewingStore;
   const {
@@ -65,34 +89,26 @@
     stopProgress: stopSlideshowProgress,
     slideshowNavigation,
     slideshowState,
+    slideshowTransition,
   } = slideshowStore;
 
-  const dispatch = createEventDispatcher<{
-    action: { type: AssetAction; asset: AssetResponseDto };
-    close: { asset: AssetResponseDto };
-    next: void;
-    previous: void;
-  }>();
-
-  let appearsInAlbums: AlbumResponseDto[] = [];
-  let shouldPlayMotionPhoto = false;
+  let appearsInAlbums: AlbumResponseDto[] = $state([]);
+  let shouldPlayMotionPhoto = $state(false);
   let sharedLink = getSharedLink();
   let enableDetailPanel = asset.hasMetadata;
   let slideshowStateUnsubscribe: () => void;
   let shuffleSlideshowUnsubscribe: () => void;
-  let previewStackedAsset: AssetResponseDto | undefined;
-  let isShowActivity = false;
-  let isShowEditor = false;
-  let isLiked: ActivityResponseDto | null = null;
-  let numberOfComments: number;
-  let fullscreenElement: Element;
+  let previewStackedAsset: AssetResponseDto | undefined = $state();
+  let isShowActivity = $state(false);
+  let isShowEditor = $state(false);
+  let isLiked: ActivityResponseDto | null = $state(null);
+  let numberOfComments = $state(0);
+  let fullscreenElement = $state<Element>();
   let unsubscribes: (() => void)[] = [];
-  let zoomToggle = () => void 0;
-  let copyImage: () => Promise<void>;
+  let selectedEditType: string = $state('');
+  let stack: StackResponseDto | null = $state(null);
 
-  $: isFullScreen = fullscreenElement !== null;
-
-  let stack: StackResponseDto | null = null;
+  let zoomToggle = $state(() => void 0);
 
   const refreshStack = async () => {
     if (isSharedLink()) {
@@ -107,21 +123,13 @@
       stack = null;
     }
 
-    if (stack && stack?.assets.length > 1) {
-      preloadAssets.push(stack.assets[1]);
-    }
+    untrack(() => {
+      if (stack && stack?.assets.length > 1) {
+        preloadAssets.push(stack.assets[1]);
+      }
+    });
   };
 
-  $: if (asset) {
-    handlePromiseError(refreshStack());
-  }
-
-  $: {
-    if (album && !album.isActivityEnabled && numberOfComments === 0) {
-      isShowActivity = false;
-    }
-  }
-
   const handleAddComment = () => {
     numberOfComments++;
     updateNumberOfComments(1);
@@ -187,13 +195,6 @@
     }
   };
 
-  $: {
-    if (isShared && asset.id) {
-      handlePromiseError(getFavorite());
-      handlePromiseError(getNumberOfComments());
-    }
-  }
-
   onMount(async () => {
     unsubscribes.push(
       websocketEvents.on('on_upload_success', onAssetUpdate),
@@ -236,12 +237,6 @@
     }
   });
 
-  $: {
-    if (asset.id && !sharedLink) {
-      handlePromiseError(handleGetAllAlbums());
-    }
-  }
-
   const handleGetAllAlbums = async () => {
     if (isSharedLink()) {
       return;
@@ -267,7 +262,7 @@
   };
 
   const closeViewer = () => {
-    dispatch('close', { asset });
+    onClose({ asset });
   };
 
   const closeEditor = () => {
@@ -316,7 +311,8 @@
     }
 
     e?.stopPropagation();
-    dispatch(order);
+    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
+    order === 'previous' ? onPrevious() : onNext();
   };
 
   // const showEditorHandler = () => {
@@ -339,7 +335,7 @@
    * Slide show mode
    */
 
-  let assetViewerHtmlElement: HTMLElement;
+  let assetViewerHtmlElement = $state<HTMLElement>();
 
   const slideshowHistory = new SlideshowHistory((asset) => {
     setAsset(asset);
@@ -354,7 +350,7 @@
 
   const handlePlaySlideshow = async () => {
     try {
-      await assetViewerHtmlElement.requestFullscreen?.();
+      await assetViewerHtmlElement?.requestFullscreen?.();
     } catch (error) {
       handleError(error, $t('errors.unable_to_enter_fullscreen'));
       $slideshowState = SlideshowState.StopSlideshow;
@@ -386,6 +382,7 @@
         break;
       }
 
+      case AssetAction.KEEP_THIS_DELETE_OTHERS:
       case AssetAction.UNSTACK: {
         closeViewer();
       }
@@ -394,11 +391,32 @@
     onAction?.(action);
   };
 
-  let selectedEditType: string = '';
-
-  function handleUpdateSelectedEditType(type: string) {
+  const handleUpdateSelectedEditType = (type: string) => {
     selectedEditType = type;
-  }
+  };
+  let isFullScreen = $derived(fullscreenElement !== null);
+  $effect(() => {
+    if (asset) {
+      previewStackedAsset = undefined;
+      handlePromiseError(refreshStack());
+    }
+  });
+  $effect(() => {
+    if (album && !album.isActivityEnabled && numberOfComments === 0) {
+      isShowActivity = false;
+    }
+  });
+  $effect(() => {
+    if (isShared && asset.id) {
+      handlePromiseError(getFavorite());
+      handlePromiseError(getNumberOfComments());
+    }
+  });
+  $effect(() => {
+    if (asset.id && !sharedLink) {
+      handlePromiseError(handleGetAllAlbums());
+    }
+  });
 </script>
 
 <svelte:document bind:fullscreenElement />
@@ -414,6 +432,7 @@
       <AssetViewerNavBar
         {asset}
         {album}
+        {person}
         {stack}
         showDetailButton={enableDetailPanel}
         showSlideshow={!!assetStore}
@@ -425,11 +444,12 @@
         onShowDetail={toggleDetailPanel}
         onClose={closeViewer}
       >
-        <MotionPhotoAction
-          slot="motion-photo"
-          isPlaying={shouldPlayMotionPhoto}
-          onClick={(shouldPlay) => (shouldPlayMotionPhoto = shouldPlay)}
-        />
+        {#snippet motionPhoto()}
+          <MotionPhotoAction
+            isPlaying={shouldPlayMotionPhoto}
+            onClick={(shouldPlay) => (shouldPlayMotionPhoto = shouldPlay)}
+          />
+        {/snippet}
       </AssetViewerNavBar>
     </div>
   {/if}
@@ -446,7 +466,7 @@
       <div class="z-[1000] absolute w-full flex">
         <SlideshowBar
           {isFullScreen}
-          onSetToFullScreen={() => assetViewerHtmlElement.requestFullscreen?.()}
+          onSetToFullScreen={() => assetViewerHtmlElement?.requestFullscreen?.()}
           onPrevious={() => navigateAsset('previous')}
           onNext={() => navigateAsset('next')}
           onClose={() => ($slideshowState = SlideshowState.StopSlideshow)}
@@ -464,7 +484,7 @@
             {preloadAssets}
             onPreviousAsset={() => navigateAsset('previous')}
             onNextAsset={() => navigateAsset('next')}
-            on:close={closeViewer}
+            onClose={closeViewer}
             haveFadeTransition={false}
             {sharedLink}
           />
@@ -476,9 +496,9 @@
             loopVideo={true}
             onPreviousAsset={() => navigateAsset('previous')}
             onNextAsset={() => navigateAsset('next')}
-            on:close={closeViewer}
-            on:onVideoEnded={() => navigateAsset()}
-            on:onVideoStarted={handleVideoStarted}
+            onClose={closeViewer}
+            onVideoEnded={() => navigateAsset()}
+            onVideoStarted={handleVideoStarted}
           />
         {/if}
       {/key}
@@ -493,13 +513,12 @@
               loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
               onPreviousAsset={() => navigateAsset('previous')}
               onNextAsset={() => navigateAsset('next')}
-              on:close={closeViewer}
-              on:onVideoEnded={() => (shouldPlayMotionPhoto = false)}
+              onVideoEnded={() => (shouldPlayMotionPhoto = false)}
             />
           {:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || (asset.originalPath && asset.originalPath
                 .toLowerCase()
                 .endsWith('.insp'))}
-            <PanoramaViewer {asset} />
+            <ImagePanoramaViewer {asset} />
           {:else if isShowEditor && selectedEditType === 'crop'}
             <CropArea {asset} />
           {:else}
@@ -510,8 +529,9 @@
               {preloadAssets}
               onPreviousAsset={() => navigateAsset('previous')}
               onNextAsset={() => navigateAsset('next')}
-              on:close={closeViewer}
+              onClose={closeViewer}
               {sharedLink}
+              haveFadeTransition={$slideshowState === SlideshowState.None || $slideshowTransition}
             />
           {/if}
         {:else}
@@ -522,9 +542,9 @@
             loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
             onPreviousAsset={() => navigateAsset('previous')}
             onNextAsset={() => navigateAsset('next')}
-            on:close={closeViewer}
-            on:onVideoEnded={() => navigateAsset()}
-            on:onVideoStarted={handleVideoStarted}
+            onClose={closeViewer}
+            onVideoEnded={() => navigateAsset()}
+            onVideoStarted={handleVideoStarted}
           />
         {/if}
         {#if $slideshowState === SlideshowState.None && isShared && ((album && album.isActivityEnabled) || numberOfComments > 0)}
@@ -533,8 +553,8 @@
               disabled={!album?.isActivityEnabled}
               {isLiked}
               {numberOfComments}
-              on:favorite={handleFavorite}
-              on:openActivityTab={handleOpenActivity}
+              onFavorite={handleFavorite}
+              onOpenActivityTab={handleOpenActivity}
             />
           </div>
         {/if}
@@ -555,7 +575,7 @@
       class="z-[1002] row-start-1 row-span-4 w-[360px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-l-immich-dark-gray dark:bg-immich-dark-bg"
       translate="yes"
     >
-      <DetailPanel {asset} currentAlbum={album} albums={appearsInAlbums} on:close={() => ($isShowDetail = false)} />
+      <DetailPanel {asset} currentAlbum={album} albums={appearsInAlbums} onClose={() => ($isShowDetail = false)} />
     </div>
   {/if}
 
@@ -577,7 +597,7 @@
       class="z-[1002] flex place-item-center place-content-center absolute bottom-0 w-full col-span-4 col-start-1 overflow-x-auto horizontal-scrollbar"
     >
       <div class="relative w-full whitespace-nowrap transition-all">
-        {#each stackedAssets as stackedAsset, index (stackedAsset.id)}
+        {#each stackedAssets as stackedAsset (stackedAsset.id)}
           <div
             class="{stackedAsset.id == asset.id
               ? '-translate-y-[1px]'
@@ -590,9 +610,9 @@
               asset={stackedAsset}
               onClick={(stackedAsset) => {
                 asset = stackedAsset;
-                preloadAssets = index + 1 >= stackedAssets.length ? [] : [stackedAssets[index + 1]];
               }}
               onMouseEvent={({ isMouseOver }) => handleStackedAssetMouseEvent(isMouseOver, stackedAsset)}
+              disableMouseOver
               readonly
               thumbnailSize={stackedAsset.id == asset.id ? 65 : 60}
               showStackedIcon={false}
@@ -600,7 +620,7 @@
 
             {#if stackedAsset.id == asset.id}
               <div class="w-full flex place-items-center place-content-center">
-                <div class="w-2 h-2 bg-white rounded-full flex mt-[2px]" />
+                <div class="w-2 h-2 bg-white rounded-full flex mt-[2px]"></div>
               </div>
             {/if}
           </div>
@@ -625,10 +645,10 @@
         assetId={asset.id}
         {isLiked}
         bind:reactions
-        on:addComment={handleAddComment}
-        on:deleteComment={handleRemoveComment}
-        on:deleteLike={() => (isLiked = null)}
-        on:close={() => (isShowActivity = false)}
+        onAddComment={handleAddComment}
+        onDeleteComment={handleRemoveComment}
+        onDeleteLike={() => (isLiked = null)}
+        onClose={() => (isShowActivity = false)}
       />
     </div>
   {/if}
diff --git a/web/src/lib/components/asset-viewer/detail-panel-description.svelte b/web/src/lib/components/asset-viewer/detail-panel-description.svelte
index b916733476..0eba78b0c0 100644
--- a/web/src/lib/components/asset-viewer/detail-panel-description.svelte
+++ b/web/src/lib/components/asset-viewer/detail-panel-description.svelte
@@ -8,14 +8,21 @@
   import AutogrowTextarea from '$lib/components/shared-components/autogrow-textarea.svelte';
   import { t } from 'svelte-i18n';
 
-  export let asset: AssetResponseDto;
-  export let isOwner: boolean;
+  interface Props {
+    asset: AssetResponseDto;
+    isOwner: boolean;
+  }
 
-  $: description = asset.exifInfo?.description || '';
+  let { asset, isOwner }: Props = $props();
+
+  let description = $derived(asset.exifInfo?.description || '');
 
   const handleFocusOut = async (newDescription: string) => {
     try {
       await updateAsset({ id: asset.id, updateAssetDto: { description: newDescription } });
+
+      asset.exifInfo = { ...asset.exifInfo, description: newDescription };
+
       notificationController.show({
         type: NotificationType.Info,
         message: $t('asset_description_updated'),
@@ -23,7 +30,6 @@
     } catch (error) {
       handleError(error, $t('cannot_update_the_description'));
     }
-    description = newDescription;
   };
 </script>
 
diff --git a/web/src/lib/components/asset-viewer/detail-panel-location.svelte b/web/src/lib/components/asset-viewer/detail-panel-location.svelte
index a93c90d0d4..9e59243aa1 100644
--- a/web/src/lib/components/asset-viewer/detail-panel-location.svelte
+++ b/web/src/lib/components/asset-viewer/detail-panel-location.svelte
@@ -7,10 +7,14 @@
   import { mdiMapMarkerOutline, mdiPencil } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  export let isOwner: boolean;
-  export let asset: AssetResponseDto;
+  interface Props {
+    isOwner: boolean;
+    asset: AssetResponseDto;
+  }
 
-  let isShowChangeLocation = false;
+  let { isOwner, asset = $bindable() }: Props = $props();
+
+  let isShowChangeLocation = $state(false);
 
   async function handleConfirmChangeLocation(gps: { lng: number; lat: number }) {
     isShowChangeLocation = false;
@@ -30,7 +34,7 @@
   <button
     type="button"
     class="flex w-full text-left justify-between place-items-start gap-4 py-4"
-    on:click={() => (isOwner ? (isShowChangeLocation = true) : null)}
+    onclick={() => (isOwner ? (isShowChangeLocation = true) : null)}
     title={isOwner ? $t('edit_location') : ''}
     class:hover:dark:text-immich-dark-primary={isOwner}
     class:hover:text-immich-primary={isOwner}
@@ -65,7 +69,7 @@
   <button
     type="button"
     class="flex w-full text-left justify-between place-items-start gap-4 py-4 rounded-lg hover:dark:text-immich-dark-primary hover:text-immich-primary"
-    on:click={() => (isShowChangeLocation = true)}
+    onclick={() => (isShowChangeLocation = true)}
     title={$t('add_location')}
   >
     <div class="flex gap-4">
@@ -81,10 +85,6 @@
 
 {#if isShowChangeLocation}
   <Portal>
-    <ChangeLocation
-      {asset}
-      on:confirm={({ detail: gps }) => handleConfirmChangeLocation(gps)}
-      on:cancel={() => (isShowChangeLocation = false)}
-    />
+    <ChangeLocation {asset} onConfirm={handleConfirmChangeLocation} onCancel={() => (isShowChangeLocation = false)} />
   </Portal>
 {/if}
diff --git a/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte b/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte
index b73fe71716..4c5bfd71a8 100644
--- a/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte
+++ b/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte
@@ -6,10 +6,14 @@
   import { handlePromiseError, isSharedLink } from '$lib/utils';
   import { preferences } from '$lib/stores/user.store';
 
-  export let asset: AssetResponseDto;
-  export let isOwner: boolean;
+  interface Props {
+    asset: AssetResponseDto;
+    isOwner: boolean;
+  }
 
-  $: rating = asset.exifInfo?.rating || 0;
+  let { asset, isOwner }: Props = $props();
+
+  let rating = $derived(asset.exifInfo?.rating || 0);
 
   const handleChangeRating = async (rating: number) => {
     try {
diff --git a/web/src/lib/components/asset-viewer/detail-panel-tags.svelte b/web/src/lib/components/asset-viewer/detail-panel-tags.svelte
index 434682f73e..39ca096efd 100644
--- a/web/src/lib/components/asset-viewer/detail-panel-tags.svelte
+++ b/web/src/lib/components/asset-viewer/detail-panel-tags.svelte
@@ -1,6 +1,7 @@
 <script lang="ts">
   import Icon from '$lib/components/elements/icon.svelte';
   import TagAssetForm from '$lib/components/forms/tag-asset-form.svelte';
+  import Portal from '$lib/components/shared-components/portal/portal.svelte';
   import { AppRoute } from '$lib/constants';
   import { isSharedLink } from '$lib/utils';
   import { removeTag, tagAssets } from '$lib/utils/asset-utils';
@@ -8,12 +9,16 @@
   import { mdiClose, mdiPlus } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  export let asset: AssetResponseDto;
-  export let isOwner: boolean;
+  interface Props {
+    asset: AssetResponseDto;
+    isOwner: boolean;
+  }
 
-  $: tags = asset.tags || [];
+  let { asset = $bindable(), isOwner }: Props = $props();
 
-  let isOpen = false;
+  let tags = $derived(asset.tags || []);
+
+  let isOpen = $state(false);
 
   const handleAdd = () => (isOpen = true);
 
@@ -41,7 +46,7 @@
     <div class="flex h-10 w-full items-center justify-between text-sm">
       <h2>{$t('tags').toUpperCase()}</h2>
     </div>
-    <section class="flex flex-wrap pt-2 gap-1">
+    <section class="flex flex-wrap pt-2 gap-1" data-testid="detail-panel-tags">
       {#each tags as tag (tag.id)}
         <div class="flex group transition-all">
           <a
@@ -57,7 +62,7 @@
             type="button"
             class="text-gray-100 dark:text-immich-dark-gray bg-immich-primary/95 dark:bg-immich-dark-primary/95 rounded-tr-full rounded-br-full place-items-center place-content-center pr-2 pl-1 py-1 hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
             title="Remove tag"
-            on:click={() => handleRemove(tag.id)}
+            onclick={() => handleRemove(tag.id)}
           >
             <Icon path={mdiClose} />
           </button>
@@ -67,7 +72,7 @@
         type="button"
         class="rounded-full bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-700 dark:hover:text-gray-200 flex place-items-center place-content-center gap-1 px-2 py-1"
         title="Add tag"
-        on:click={handleAdd}
+        onclick={handleAdd}
       >
         <span class="text-sm px-1 flex place-items-center place-content-center gap-1"><Icon path={mdiPlus} />Add</span>
       </button>
@@ -76,5 +81,7 @@
 {/if}
 
 {#if isOpen}
-  <TagAssetForm onTag={(tagsIds) => handleTag(tagsIds)} onCancel={handleCancel} />
+  <Portal>
+    <TagAssetForm onTag={(tagsIds) => handleTag(tagsIds)} onCancel={handleCancel} />
+  </Portal>
 {/if}
diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte
index 5dc4fc0812..9908630233 100644
--- a/web/src/lib/components/asset-viewer/detail-panel.svelte
+++ b/web/src/lib/components/asset-viewer/detail-panel.svelte
@@ -1,5 +1,9 @@
 <script lang="ts">
+  import { goto } from '$app/navigation';
+  import DetailPanelDescription from '$lib/components/asset-viewer/detail-panel-description.svelte';
   import DetailPanelLocation from '$lib/components/asset-viewer/detail-panel-location.svelte';
+  import DetailPanelRating from '$lib/components/asset-viewer/detail-panel-star-rating.svelte';
+  import DetailPanelTags from '$lib/components/asset-viewer/detail-panel-tags.svelte';
   import Icon from '$lib/components/elements/icon.svelte';
   import ChangeDate from '$lib/components/shared-components/change-date.svelte';
   import { AppRoute, QueryParameter, timeToLoadTheMap } from '$lib/constants';
@@ -9,6 +13,9 @@
   import { preferences, user } from '$lib/stores/user.store';
   import { getAssetThumbnailUrl, getPeopleThumbnailUrl, handlePromiseError, isSharedLink } from '$lib/utils';
   import { delay, isFlipped } from '$lib/utils/asset-utils';
+  import { getByteUnitString } from '$lib/utils/byte-units';
+  import { handleError } from '$lib/utils/handle-error';
+  import { fromDateTimeOriginal, fromLocalDateTime } from '$lib/utils/timeline-util';
   import {
     AssetMediaSize,
     getAssetInfo,
@@ -18,6 +25,7 @@
     type ExifResponseDto,
   } from '@immich/sdk';
   import {
+    mdiAccountOff,
     mdiCalendar,
     mdiCameraIris,
     mdiClose,
@@ -26,28 +34,26 @@
     mdiImageOutline,
     mdiInformationOutline,
     mdiPencil,
-    mdiAccountOff,
   } from '@mdi/js';
   import { DateTime } from 'luxon';
-  import { createEventDispatcher } from 'svelte';
+  import { t } from 'svelte-i18n';
   import { slide } from 'svelte/transition';
-  import { getByteUnitString } from '$lib/utils/byte-units';
-  import { handleError } from '$lib/utils/handle-error';
   import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
   import PersonSidePanel from '../faces-page/person-side-panel.svelte';
-  import UserAvatar from '../shared-components/user-avatar.svelte';
   import LoadingSpinner from '../shared-components/loading-spinner.svelte';
+  import UserAvatar from '../shared-components/user-avatar.svelte';
   import AlbumListItemDetails from './album-list-item-details.svelte';
-  import DetailPanelDescription from '$lib/components/asset-viewer/detail-panel-description.svelte';
-  import DetailPanelRating from '$lib/components/asset-viewer/detail-panel-star-rating.svelte';
-  import { t } from 'svelte-i18n';
-  import { goto } from '$app/navigation';
-  import DetailPanelTags from '$lib/components/asset-viewer/detail-panel-tags.svelte';
+  import Portal from '$lib/components/shared-components/portal/portal.svelte';
 
-  export let asset: AssetResponseDto;
-  export let albums: AlbumResponseDto[] = [];
-  export let currentAlbum: AlbumResponseDto | null = null;
+  interface Props {
+    asset: AssetResponseDto;
+    albums?: AlbumResponseDto[];
+    currentAlbum?: AlbumResponseDto | null;
+    onClose: () => void;
+  }
+
+  let { asset, albums = [], currentAlbum = null, onClose }: Props = $props();
 
   const getDimensions = (exifInfo: ExifResponseDto) => {
     const { exifImageWidth: width, exifImageHeight: height } = exifInfo;
@@ -58,11 +64,11 @@
     return { width, height };
   };
 
-  let showAssetPath = false;
-  let showEditFaces = false;
-  let previousId: string;
+  let showAssetPath = $state(false);
+  let showEditFaces = $state(false);
+  let previousId: string | undefined = $state();
 
-  $: {
+  $effect(() => {
     if (!previousId) {
       previousId = asset.id;
     }
@@ -70,9 +76,9 @@
       showEditFaces = false;
       previousId = asset.id;
     }
-  }
+  });
 
-  $: isOwner = $user?.id === asset.ownerId;
+  let isOwner = $derived($user?.id === asset.ownerId);
 
   const handleNewAsset = async (newAsset: AssetResponseDto) => {
     // TODO: check if reloading asset data is necessary
@@ -83,25 +89,30 @@
     }
   };
 
-  $: handlePromiseError(handleNewAsset(asset));
+  $effect(() => {
+    handlePromiseError(handleNewAsset(asset));
+  });
 
-  $: latlng = (() => {
-    const lat = asset.exifInfo?.latitude;
-    const lng = asset.exifInfo?.longitude;
+  let latlng = $derived(
+    (() => {
+      const lat = asset.exifInfo?.latitude;
+      const lng = asset.exifInfo?.longitude;
 
-    if (lat && lng) {
-      return { lat: Number(lat.toFixed(7)), lng: Number(lng.toFixed(7)) };
-    }
-  })();
+      if (lat && lng) {
+        return { lat: Number(lat.toFixed(7)), lng: Number(lng.toFixed(7)) };
+      }
+    })(),
+  );
 
-  $: people = asset.people || [];
-  $: showingHiddenPeople = false;
-
-  $: unassignedFaces = asset.unassignedFaces || [];
-
-  const dispatch = createEventDispatcher<{
-    close: void;
-  }>();
+  let people = $state(asset.people || []);
+  let unassignedFaces = $state(asset.unassignedFaces || []);
+  let showingHiddenPeople = $state(false);
+  let timeZone = $derived(asset.exifInfo?.timeZone);
+  let dateTime = $derived(
+    timeZone && asset.exifInfo?.dateTimeOriginal
+      ? fromDateTimeOriginal(asset.exifInfo.dateTimeOriginal, timeZone)
+      : fromLocalDateTime(asset.localDateTime),
+  );
 
   const getMegapixel = (width: number, height: number): number | undefined => {
     const megapixel = Math.round((height * width) / 1_000_000);
@@ -123,7 +134,7 @@
 
   const toggleAssetPath = () => (showAssetPath = !showAssetPath);
 
-  let isShowChangeDate = false;
+  let isShowChangeDate = $state(false);
 
   async function handleConfirmChangeDate(dateTimeOriginal: string) {
     isShowChangeDate = false;
@@ -137,19 +148,28 @@
 
 <section class="relative p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
   <div class="flex place-items-center gap-2">
-    <CircleIconButton icon={mdiClose} title={$t('close')} on:click={() => dispatch('close')} />
+    <CircleIconButton icon={mdiClose} title={$t('close')} onclick={onClose} />
     <p class="text-lg text-immich-fg dark:text-immich-dark-fg">{$t('info')}</p>
   </div>
 
   {#if asset.isOffline}
     <section class="px-4 py-4">
       <div role="alert">
-        <div class="rounded-t bg-red-500 px-4 py-2 font-bold text-white">{$t('asset_offline')}</div>
-        <div class="rounded-b border border-t-0 border-red-400 bg-red-100 px-4 py-3 text-red-700">
+        <div class="rounded-t bg-red-500 px-4 py-2 font-bold text-white">
+          {$t('asset_offline')}
+        </div>
+        <div class="border border-t-0 border-red-400 bg-red-100 px-4 py-3 text-red-700">
           <p>
-            {$t('asset_offline_description')}
+            {#if $user?.isAdmin}
+              {$t('admin.asset_offline_description')}
+            {:else}
+              {$t('asset_offline_description')}
+            {/if}
           </p>
         </div>
+        <div class="rounded-b bg-red-500 px-4 py-2 text-white text-sm">
+          <p>{asset.originalPath}</p>
+        </div>
       </div>
     </section>
   {/if}
@@ -177,7 +197,7 @@
               icon={showingHiddenPeople ? mdiEyeOff : mdiEye}
               padding="1"
               buttonSize="32"
-              on:click={() => (showingHiddenPeople = !showingHiddenPeople)}
+              onclick={() => (showingHiddenPeople = !showingHiddenPeople)}
             />
           {/if}
           <CircleIconButton
@@ -186,7 +206,7 @@
             padding="1"
             size="20"
             buttonSize="32"
-            on:click={() => (showEditFaces = true)}
+            onclick={() => (showEditFaces = true)}
           />
         </div>
       </div>
@@ -199,10 +219,10 @@
               href="{AppRoute.PEOPLE}/{person.id}?{QueryParameter.PREVIOUS_ROUTE}={currentAlbum?.id
                 ? `${AppRoute.ALBUMS}/${currentAlbum?.id}`
                 : AppRoute.PHOTOS}"
-              on:focus={() => ($boundingBoxesArray = people[index].faces)}
-              on:blur={() => ($boundingBoxesArray = [])}
-              on:mouseover={() => ($boundingBoxesArray = people[index].faces)}
-              on:mouseleave={() => ($boundingBoxesArray = [])}
+              onfocus={() => ($boundingBoxesArray = people[index].faces)}
+              onblur={() => ($boundingBoxesArray = [])}
+              onmouseover={() => ($boundingBoxesArray = people[index].faces)}
+              onmouseleave={() => ($boundingBoxesArray = [])}
             >
               <div class="relative">
                 <ImageThumbnail
@@ -261,14 +281,11 @@
       <p class="text-sm">{$t('no_exif_info_available').toUpperCase()}</p>
     {/if}
 
-    {#if asset.exifInfo?.dateTimeOriginal}
-      {@const assetDateTimeOriginal = DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
-        zone: asset.exifInfo.timeZone ?? undefined,
-      })}
+    {#if dateTime}
       <button
         type="button"
         class="flex w-full text-left justify-between place-items-start gap-4 py-4"
-        on:click={() => (isOwner ? (isShowChangeDate = true) : null)}
+        onclick={() => (isOwner ? (isShowChangeDate = true) : null)}
         title={isOwner ? $t('edit_date') : ''}
         class:hover:dark:text-immich-dark-primary={isOwner}
         class:hover:text-immich-primary={isOwner}
@@ -280,7 +297,7 @@
 
           <div>
             <p>
-              {assetDateTimeOriginal.toLocaleString(
+              {dateTime.toLocaleString(
                 {
                   month: 'short',
                   day: 'numeric',
@@ -291,12 +308,12 @@
             </p>
             <div class="flex gap-2 text-sm">
               <p>
-                {assetDateTimeOriginal.toLocaleString(
+                {dateTime.toLocaleString(
                   {
                     weekday: 'short',
                     hour: 'numeric',
                     minute: '2-digit',
-                    timeZoneName: 'longOffset',
+                    timeZoneName: timeZone ? 'longOffset' : undefined,
                   },
                   { locale: $locale },
                 )}
@@ -311,7 +328,7 @@
           </div>
         {/if}
       </button>
-    {:else if !asset.exifInfo?.dateTimeOriginal && isOwner}
+    {:else if !dateTime && isOwner}
       <div class="flex justify-between place-items-start gap-4 py-4">
         <div class="flex gap-4">
           <div>
@@ -325,58 +342,55 @@
     {/if}
 
     {#if isShowChangeDate}
-      {@const assetDateTimeOriginal = asset.exifInfo?.dateTimeOriginal
-        ? DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
-            zone: asset.exifInfo.timeZone ?? undefined,
-            locale: $locale,
-          })
-        : DateTime.now()}
-      {@const assetTimeZoneOriginal = asset.exifInfo?.timeZone ?? ''}
-      <ChangeDate
-        initialDate={assetDateTimeOriginal}
-        initialTimeZone={assetTimeZoneOriginal}
-        on:confirm={({ detail: date }) => handleConfirmChangeDate(date)}
-        on:cancel={() => (isShowChangeDate = false)}
-      />
+      <Portal>
+        <ChangeDate
+          initialDate={dateTime}
+          initialTimeZone={timeZone ?? ''}
+          onConfirm={handleConfirmChangeDate}
+          onCancel={() => (isShowChangeDate = false)}
+        />
+      </Portal>
     {/if}
 
-    {#if asset.exifInfo?.fileSizeInByte}
-      <div class="flex gap-4 py-4">
-        <div><Icon path={mdiImageOutline} size="24" /></div>
+    <div class="flex gap-4 py-4">
+      <div><Icon path={mdiImageOutline} size="24" /></div>
 
-        <div>
-          <p class="break-all flex place-items-center gap-2">
-            {asset.originalFileName}
-            {#if isOwner}
-              <CircleIconButton
-                icon={mdiInformationOutline}
-                title={$t('show_file_location')}
-                size="16"
-                padding="2"
-                on:click={toggleAssetPath}
-              />
-            {/if}
+      <div>
+        <p class="break-all flex place-items-center gap-2">
+          {asset.originalFileName}
+          {#if isOwner}
+            <CircleIconButton
+              icon={mdiInformationOutline}
+              title={$t('show_file_location')}
+              size="16"
+              padding="2"
+              onclick={toggleAssetPath}
+            />
+          {/if}
+        </p>
+        {#if showAssetPath}
+          <p class="text-xs opacity-50 break-all pb-2" transition:slide={{ duration: 250 }}>
+            {asset.originalPath}
           </p>
+        {/if}
+        {#if (asset.exifInfo?.exifImageHeight && asset.exifInfo?.exifImageWidth) || asset.exifInfo?.fileSizeInByte}
           <div class="flex gap-2 text-sm">
-            {#if asset.exifInfo.exifImageHeight && asset.exifInfo.exifImageWidth}
+            {#if asset.exifInfo?.exifImageHeight && asset.exifInfo?.exifImageWidth}
               {#if getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)}
                 <p>
                   {getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)} MP
                 </p>
+                {@const { width, height } = getDimensions(asset.exifInfo)}
+                <p>{width} x {height}</p>
               {/if}
-              {@const { width, height } = getDimensions(asset.exifInfo)}
-              <p>{width} x {height}</p>
             {/if}
-            <p>{getByteUnitString(asset.exifInfo.fileSizeInByte, $locale)}</p>
+            {#if asset.exifInfo?.fileSizeInByte}
+              <p>{getByteUnitString(asset.exifInfo.fileSizeInByte, $locale)}</p>
+            {/if}
           </div>
-          {#if showAssetPath}
-            <p class="text-xs opacity-50 break-all" transition:slide={{ duration: 250 }}>
-              {asset.originalPath}
-            </p>
-          {/if}
-        </div>
+        {/if}
       </div>
-    {/if}
+    </div>
 
     {#if asset.exifInfo?.make || asset.exifInfo?.model || asset.exifInfo?.fNumber}
       <div class="flex gap-4 py-4">
@@ -421,8 +435,7 @@
         </div>
       {/await}
     {:then component}
-      <svelte:component
-        this={component.default}
+      <component.default
         mapMarkers={[
           {
             lat: latlng.lat,
@@ -439,7 +452,7 @@
         useLocationPin
         onOpenInMapView={() => goto(`${AppRoute.MAP}#12.5/${latlng.lat}/${latlng.lng}`)}
       >
-        <svelte:fragment slot="popup" let:marker>
+        {#snippet popup({ marker })}
           {@const { lat, lon } = marker}
           <div class="flex flex-col items-center gap-1">
             <p class="font-bold">{lat.toPrecision(6)}, {lon.toPrecision(6)}</p>
@@ -451,8 +464,8 @@
               {$t('open_in_openstreetmap')}
             </a>
           </div>
-        </svelte:fragment>
-      </svelte:component>
+        {/snippet}
+      </component.default>
     {/await}
   </div>
 {/if}
@@ -514,9 +527,7 @@
   <PersonSidePanel
     assetId={asset.id}
     assetType={asset.type}
-    on:close={() => {
-      showEditFaces = false;
-    }}
-    on:refresh={handleRefreshPeople}
+    onClose={() => (showEditFaces = false)}
+    onRefresh={handleRefreshPeople}
   />
 {/if}
diff --git a/web/src/lib/components/asset-viewer/download-panel.svelte b/web/src/lib/components/asset-viewer/download-panel.svelte
index 333ee0c13b..17f5e7e6a8 100644
--- a/web/src/lib/components/asset-viewer/download-panel.svelte
+++ b/web/src/lib/components/asset-viewer/download-panel.svelte
@@ -32,7 +32,7 @@
             </div>
             <div class="flex place-items-center gap-2">
               <div class="h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
-                <div class="h-[7px] rounded-full bg-immich-primary" style={`width: ${download.percentage}%`} />
+                <div class="h-[7px] rounded-full bg-immich-primary" style={`width: ${download.percentage}%`}></div>
               </div>
               <p class="min-w-[4em] whitespace-nowrap text-right">
                 <span class="text-immich-primary">
@@ -44,7 +44,7 @@
           <div class="absolute right-2">
             <CircleIconButton
               title={$t('close')}
-              on:click={() => abort(downloadKey, download)}
+              onclick={() => abort(downloadKey, download)}
               size="20"
               icon={mdiClose}
               class="dark:text-immich-dark-gray"
diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte
index c35fd91519..028074bc02 100644
--- a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte
+++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte
@@ -1,5 +1,5 @@
 <script lang="ts">
-  import { onMount, afterUpdate, onDestroy, tick } from 'svelte';
+  import { onMount, onDestroy, tick } from 'svelte';
   import { t } from 'svelte-i18n';
   import { getAssetOriginalUrl } from '$lib/utils';
   import { handleError } from '$lib/utils/handle-error';
@@ -17,11 +17,23 @@
     resetGlobalCropStore,
     rotateDegrees,
   } from '$lib/stores/asset-editor.store';
+  import type { AssetResponseDto } from '@immich/sdk';
 
-  export let asset;
-  let img: HTMLImageElement;
+  interface Props {
+    asset: AssetResponseDto;
+  }
 
-  $: imgElement.set(img);
+  let { asset }: Props = $props();
+
+  let img = $state<HTMLImageElement>();
+
+  $effect(() => {
+    if (!img) {
+      return;
+    }
+
+    imgElement.set(img);
+  });
 
   cropAspectRatio.subscribe((value) => {
     if (!img || !$cropAreaEl) {
@@ -45,16 +57,16 @@
       handleError(error, $t('error_loading_image'));
     });
 
-    window.addEventListener('mousemove', handleMouseMove);
+    globalThis.addEventListener('mousemove', handleMouseMove);
   });
 
   onDestroy(() => {
-    window.removeEventListener('mousemove', handleMouseMove);
+    globalThis.removeEventListener('mousemove', handleMouseMove);
     resetCropStore();
     resetGlobalCropStore();
   });
 
-  afterUpdate(() => {
+  $effect(() => {
     resizeCanvas();
   });
 </script>
@@ -64,8 +76,8 @@
     class={`crop-area ${$changedOriention ? 'changedOriention' : ''}`}
     style={`rotate:${$rotateDegrees}deg`}
     bind:this={$cropAreaEl}
-    on:mousedown={handleMouseDown}
-    on:mouseup={handleMouseUp}
+    onmousedown={handleMouseDown}
+    onmouseup={handleMouseUp}
     aria-label="Crop area"
     type="button"
   >
diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-preset.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-preset.svelte
index 667191274f..eb788b2d16 100644
--- a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-preset.svelte
+++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-preset.svelte
@@ -3,37 +3,41 @@
   import Icon from '$lib/components/elements/icon.svelte';
   import type { CropAspectRatio } from '$lib/stores/asset-editor.store';
 
-  export let size: {
-    icon: string;
-    name: CropAspectRatio;
-    viewBox: string;
-    rotate?: boolean;
-  };
-  export let selectedSize: CropAspectRatio;
-  export let rotateHorizontal: boolean;
-  export let selectType: (size: CropAspectRatio) => void;
+  interface Props {
+    size: {
+      icon: string;
+      name: CropAspectRatio;
+      viewBox: string;
+      rotate?: boolean;
+    };
+    selectedSize: CropAspectRatio;
+    rotateHorizontal: boolean;
+    selectType: (size: CropAspectRatio) => void;
+  }
 
-  $: isSelected = selectedSize === size.name;
-  $: buttonColor = (isSelected ? 'primary' : 'transparent-gray') as Color;
+  let { size, selectedSize, rotateHorizontal, selectType }: Props = $props();
 
-  $: rotatedTitle = (title: string, toRotate: boolean) => {
+  let isSelected = $derived(selectedSize === size.name);
+  let buttonColor = $derived((isSelected ? 'primary' : 'transparent-gray') as Color);
+
+  let rotatedTitle = $derived((title: string, toRotate: boolean) => {
     let sides = title.split(':');
     if (toRotate) {
       sides.reverse();
     }
     return sides.join(':');
-  };
+  });
 
-  $: toRotate = (def: boolean | undefined) => {
+  let toRotate = $derived((def: boolean | undefined) => {
     if (def === false) {
       return false;
     }
     return (def && !rotateHorizontal) || (!def && rotateHorizontal);
-  };
+  });
 </script>
 
 <li>
-  <Button color={buttonColor} class="flex-col gap-1" size="sm" rounded="lg" on:click={() => selectType(size.name)}>
+  <Button color={buttonColor} class="flex-col gap-1" size="sm" rounded="lg" onclick={() => selectType(size.name)}>
     <Icon size="1.75em" path={size.icon} viewBox={size.viewBox} class={toRotate(size.rotate) ? 'rotate-90' : ''} />
     <span>{rotatedTitle(size.name, rotateHorizontal)}</span>
   </Button>
diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte
index dba3be5d67..363bec7c1f 100644
--- a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte
+++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte
@@ -16,7 +16,7 @@
   import { tick } from 'svelte';
   import CropPreset from './crop-preset.svelte';
 
-  $: rotateHorizontal = [90, 270].includes($normaizedRorateDegrees);
+  let rotateHorizontal = $derived([90, 270].includes($normaizedRorateDegrees));
   const icon_16_9 = `M200-280q-33 0-56.5-23.5T120-360v-240q0-33 23.5-56.5T200-680h560q33 0 56.5 23.5T840-600v240q0 33-23.5 56.5T760-280H200Zm0-80h560v-240H200v240Zm0 0v-240 240Z`;
   const icon_4_3 = `M19 5H5c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 12H5V7h14v10z`;
   const icon_3_2 = `M200-240q-33 0-56.5-23.5T120-320v-320q0-33 23.5-56.5T200-720h560q33 0 56.5 23.5T840-640v320q0 33-23.5 56.5T760-240H200Zm0-80h560v-320H200v320Zm0 0v-320 320Z`;
@@ -92,14 +92,17 @@
     },
   ];
 
-  let selectedSize: CropAspectRatio = 'free';
-  $cropAspectRatio = selectedSize;
+  let selectedSize: CropAspectRatio = $state('free');
 
-  $: sizesRows = [
+  $effect(() => {
+    $cropAspectRatio = selectedSize;
+  });
+
+  let sizesRows = $derived([
     sizes.filter((s) => s.rotate === false),
     sizes.filter((s) => s.rotate === undefined),
     sizes.filter((s) => s.rotate === true),
-  ];
+  ]);
 
   async function rotate(clock: boolean) {
     rotateDegrees.update((v) => {
@@ -145,7 +148,7 @@
     <h2>{$t('editor_crop_tool_h2_rotation').toUpperCase()}</h2>
   </div>
   <ul class="flex-wrap flex-row flex gap-x-6 gap-y-4 justify-center">
-    <li><CircleIconButton title={$t('anti_clockwise')} on:click={() => rotate(false)} icon={mdiRotateLeft} /></li>
-    <li><CircleIconButton title={$t('clockwise')} on:click={() => rotate(true)} icon={mdiRotateRight} /></li>
+    <li><CircleIconButton title={$t('anti_clockwise')} onclick={() => rotate(false)} icon={mdiRotateLeft} /></li>
+    <li><CircleIconButton title={$t('clockwise')} onclick={() => rotate(true)} icon={mdiRotateRight} /></li>
   </ul>
 </div>
diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts
index 656fd09294..b00f5331b2 100644
--- a/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts
+++ b/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts
@@ -58,7 +58,7 @@ export function handleMouseDown(e: MouseEvent) {
   }
 
   document.body.style.userSelect = 'none';
-  window.addEventListener('mouseup', handleMouseUp);
+  globalThis.addEventListener('mouseup', handleMouseUp);
 }
 
 export function handleMouseMove(e: MouseEvent) {
@@ -80,7 +80,7 @@ export function handleMouseMove(e: MouseEvent) {
 }
 
 export function handleMouseUp() {
-  window.removeEventListener('mouseup', handleMouseUp);
+  globalThis.removeEventListener('mouseup', handleMouseUp);
   document.body.style.userSelect = '';
   stopInteraction();
 }
diff --git a/web/src/lib/components/asset-viewer/editor/editor-panel.svelte b/web/src/lib/components/asset-viewer/editor/editor-panel.svelte
index 1adef32735..133d9c9021 100644
--- a/web/src/lib/components/asset-viewer/editor/editor-panel.svelte
+++ b/web/src/lib/components/asset-viewer/editor/editor-panel.svelte
@@ -9,8 +9,6 @@
   import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
   import { shortcut } from '$lib/actions/shortcut';
 
-  export let asset: AssetResponseDto;
-
   onMount(() => {
     return websocketEvents.on('on_asset_update', (assetUpdate) => {
       if (assetUpdate.id === asset.id) {
@@ -19,11 +17,16 @@
     });
   });
 
-  export let onUpdateSelectedType: (type: string) => void;
-  export let onClose: () => void;
+  interface Props {
+    asset: AssetResponseDto;
+    onUpdateSelectedType: (type: string) => void;
+    onClose: () => void;
+  }
 
-  let selectedType: string = editTypes[0].name;
-  $: selectedTypeObj = editTypes.find((t) => t.name === selectedType) || editTypes[0];
+  let { asset = $bindable(), onUpdateSelectedType, onClose }: Props = $props();
+
+  let selectedType: string = $state(editTypes[0].name);
+  let selectedTypeObj = $derived(editTypes.find((t) => t.name === selectedType) || editTypes[0]);
 
   setTimeout(() => {
     onUpdateSelectedType(selectedType);
@@ -38,7 +41,7 @@
 
 <section class="relative p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
   <div class="flex place-items-center gap-2">
-    <CircleIconButton icon={mdiClose} title={$t('close')} on:click={onClose} />
+    <CircleIconButton icon={mdiClose} title={$t('close')} onclick={onClose} />
     <p class="text-lg text-immich-fg dark:text-immich-dark-fg capitalize">{$t('editor')}</p>
   </div>
   <section class="px-4 py-4">
@@ -49,14 +52,14 @@
             color={etype.name === selectedType ? 'primary' : 'opaque'}
             icon={etype.icon}
             title={etype.name}
-            on:click={() => selectType(etype.name)}
+            onclick={() => selectType(etype.name)}
           />
         </li>
       {/each}
     </ul>
   </section>
   <section>
-    <svelte:component this={selectedTypeObj.component} />
+    <selectedTypeObj.component />
   </section>
 </section>
 
diff --git a/web/src/lib/components/asset-viewer/image-panorama-viewer.svelte b/web/src/lib/components/asset-viewer/image-panorama-viewer.svelte
new file mode 100644
index 0000000000..6da8cc33d3
--- /dev/null
+++ b/web/src/lib/components/asset-viewer/image-panorama-viewer.svelte
@@ -0,0 +1,32 @@
+<script lang="ts">
+  import { getAssetOriginalUrl, getKey } from '$lib/utils';
+  import { isWebCompatibleImage } from '$lib/utils/asset-utils';
+  import { AssetMediaSize, viewAsset, type AssetResponseDto } from '@immich/sdk';
+  import { fade } from 'svelte/transition';
+  import LoadingSpinner from '../shared-components/loading-spinner.svelte';
+  import { t } from 'svelte-i18n';
+
+  interface Props {
+    asset: AssetResponseDto;
+  }
+
+  const { asset }: Props = $props();
+
+  const loadAssetData = async (id: string) => {
+    const data = await viewAsset({ id, size: AssetMediaSize.Preview, key: getKey() });
+    return URL.createObjectURL(data);
+  };
+</script>
+
+<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
+  {#await Promise.all([loadAssetData(asset.id), import('./photo-sphere-viewer-adapter.svelte')])}
+    <LoadingSpinner />
+  {:then [data, { default: PhotoSphereViewer }]}
+    <PhotoSphereViewer
+      panorama={data}
+      originalImageUrl={isWebCompatibleImage(asset) ? getAssetOriginalUrl(asset.id) : undefined}
+    />
+  {:catch}
+    {$t('errors.failed_to_load_asset')}
+  {/await}
+</div>
diff --git a/web/src/lib/components/asset-viewer/navigation-area.svelte b/web/src/lib/components/asset-viewer/navigation-area.svelte
index e69d93b6b6..88f0baf0bc 100644
--- a/web/src/lib/components/asset-viewer/navigation-area.svelte
+++ b/web/src/lib/components/asset-viewer/navigation-area.svelte
@@ -1,13 +1,20 @@
 <script lang="ts">
-  export let onClick: (e: MouseEvent) => void;
-  export let label: string;
+  import type { Snippet } from 'svelte';
+
+  interface Props {
+    onClick: (e: MouseEvent) => void;
+    label: string;
+    children?: Snippet;
+  }
+
+  let { onClick, label, children }: Props = $props();
 </script>
 
 <button
   type="button"
   class="my-auto mx-4 rounded-full p-3 text-gray-500 transition hover:bg-gray-500 hover:text-white"
   aria-label={label}
-  on:click={onClick}
+  onclick={onClick}
 >
-  <slot />
+  {@render children?.()}
 </button>
diff --git a/web/src/lib/components/asset-viewer/panorama-viewer.svelte b/web/src/lib/components/asset-viewer/panorama-viewer.svelte
deleted file mode 100644
index 396685e351..0000000000
--- a/web/src/lib/components/asset-viewer/panorama-viewer.svelte
+++ /dev/null
@@ -1,57 +0,0 @@
-<script lang="ts">
-  import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
-  import { getAssetOriginalUrl, getKey } from '$lib/utils';
-  import { isWebCompatibleImage } from '$lib/utils/asset-utils';
-  import { AssetMediaSize, AssetTypeEnum, viewAsset, type AssetResponseDto } from '@immich/sdk';
-  import type { AdapterConstructor, PluginConstructor } from '@photo-sphere-viewer/core';
-  import { t } from 'svelte-i18n';
-  import { fade } from 'svelte/transition';
-  import LoadingSpinner from '../shared-components/loading-spinner.svelte';
-
-  export let asset: { id: string; type: AssetTypeEnum.Video } | AssetResponseDto;
-
-  const photoSphereConfigs =
-    asset.type === AssetTypeEnum.Video
-      ? ([
-          import('@photo-sphere-viewer/equirectangular-video-adapter').then(
-            ({ EquirectangularVideoAdapter }) => EquirectangularVideoAdapter,
-          ),
-          import('@photo-sphere-viewer/video-plugin').then(({ VideoPlugin }) => [VideoPlugin]),
-          true,
-          import('@photo-sphere-viewer/video-plugin/index.css'),
-        ] as [PromiseLike<AdapterConstructor>, Promise<PluginConstructor[]>, true, unknown])
-      : ([undefined, [], false] as [undefined, [], false]);
-
-  const originalImageUrl =
-    asset.type === AssetTypeEnum.Image && isWebCompatibleImage(asset) ? getAssetOriginalUrl(asset.id) : null;
-
-  const loadAssetData = async () => {
-    if (asset.type === AssetTypeEnum.Video) {
-      return { source: getAssetOriginalUrl(asset.id) };
-    }
-    if (originalImageUrl && $alwaysLoadOriginalFile) {
-      return getAssetOriginalUrl(asset.id);
-    }
-    const data = await viewAsset({ id: asset.id, size: AssetMediaSize.Preview, key: getKey() });
-    const url = URL.createObjectURL(data);
-    return url;
-  };
-</script>
-
-<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
-  <!-- the photo sphere viewer is quite large, so lazy load it in parallel with loading the data -->
-  {#await Promise.all([loadAssetData(), import('./photo-sphere-viewer-adapter.svelte'), ...photoSphereConfigs])}
-    <LoadingSpinner />
-  {:then [data, module, adapter, plugins, navbar]}
-    <svelte:component
-      this={module.default}
-      panorama={data}
-      plugins={plugins ?? undefined}
-      {navbar}
-      {adapter}
-      {originalImageUrl}
-    />
-  {:catch}
-    {$t('errors.failed_to_load_asset')}
-  {/await}
-</div>
diff --git a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte
index da8febc3d9..0c8f76a01e 100644
--- a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte
+++ b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte
@@ -10,26 +10,35 @@
   import '@photo-sphere-viewer/core/index.css';
   import { onDestroy, onMount } from 'svelte';
 
-  export let panorama: string | { source: string };
-  export let originalImageUrl: string | null;
-  export let adapter: AdapterConstructor | [AdapterConstructor, unknown] = EquirectangularAdapter;
-  export let plugins: (PluginConstructor | [PluginConstructor, unknown])[] = [];
-  export let navbar = false;
+  interface Props {
+    panorama: string | { source: string };
+    originalImageUrl?: string;
+    adapter?: AdapterConstructor | [AdapterConstructor, unknown];
+    plugins?: (PluginConstructor | [PluginConstructor, unknown])[];
+    navbar?: boolean;
+  }
 
-  let container: HTMLDivElement;
+  let { panorama, originalImageUrl, adapter = EquirectangularAdapter, plugins = [], navbar = false }: Props = $props();
+
+  let container: HTMLDivElement | undefined = $state();
   let viewer: Viewer;
 
   onMount(() => {
+    if (!container) {
+      return;
+    }
+
     viewer = new Viewer({
       adapter,
       plugins,
       container,
       panorama,
-      touchmoveTwoFingers: true,
+      touchmoveTwoFingers: false,
       mousewheelCtrlKey: false,
       navbar,
-      maxFov: 180,
-      fisheye: true,
+      minFov: 10,
+      maxFov: 120,
+      fisheye: false,
     });
 
     if (originalImageUrl && !$alwaysLoadOriginalFile) {
@@ -54,4 +63,4 @@
   });
 </script>
 
-<div class="h-full w-full mb-0" bind:this={container} />
+<div class="h-full w-full mb-0" bind:this={container}></div>
diff --git a/web/src/lib/components/asset-viewer/photo-viewer.spec.ts b/web/src/lib/components/asset-viewer/photo-viewer.spec.ts
index e64dc680a9..e1372e37da 100644
--- a/web/src/lib/components/asset-viewer/photo-viewer.spec.ts
+++ b/web/src/lib/components/asset-viewer/photo-viewer.spec.ts
@@ -1,3 +1,4 @@
+import { getAnimateMock } from '$lib/__mocks__/animate.mock';
 import PhotoViewer from '$lib/components/asset-viewer/photo-viewer.svelte';
 import * as utils from '$lib/utils';
 import { AssetMediaSize } from '@immich/sdk';
@@ -24,6 +25,10 @@ describe('PhotoViewer component', () => {
     getAssetThumbnailUrlSpy = vi.spyOn(utils, 'getAssetThumbnailUrl');
   });
 
+  beforeEach(() => {
+    Element.prototype.animate = getAnimateMock();
+  });
+
   afterEach(() => {
     vi.resetAllMocks();
   });
diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte
index 4157c558d2..c05f24daf4 100644
--- a/web/src/lib/components/asset-viewer/photo-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte
@@ -20,33 +20,38 @@
   import { NotificationType, notificationController } from '../shared-components/notification/notification';
   import { handleError } from '$lib/utils/handle-error';
 
-  export let asset: AssetResponseDto;
-  export let preloadAssets: AssetResponseDto[] | undefined = undefined;
-  export let element: HTMLDivElement | undefined = undefined;
-  export let haveFadeTransition = true;
-  export let sharedLink: SharedLinkResponseDto | undefined = undefined;
-  export let onPreviousAsset: (() => void) | null = null;
-  export let onNextAsset: (() => void) | null = null;
-  export let copyImage: (() => Promise<void>) | null = null;
-  export let zoomToggle: (() => void) | null = null;
+  interface Props {
+    asset: AssetResponseDto;
+    preloadAssets?: AssetResponseDto[] | undefined;
+    element?: HTMLDivElement | undefined;
+    haveFadeTransition?: boolean;
+    sharedLink?: SharedLinkResponseDto | undefined;
+    onPreviousAsset?: (() => void) | null;
+    onNextAsset?: (() => void) | null;
+    copyImage?: () => Promise<void>;
+    zoomToggle?: (() => void) | null;
+    onClose?: () => void;
+  }
+
+  let {
+    asset,
+    preloadAssets = undefined,
+    element = $bindable(),
+    haveFadeTransition = true,
+    sharedLink = undefined,
+    onPreviousAsset = null,
+    onNextAsset = null,
+    copyImage = $bindable(),
+    zoomToggle = $bindable(),
+  }: Props = $props();
 
   const { slideshowState, slideshowLook } = slideshowStore;
 
-  let assetFileUrl: string = '';
-  let imageLoaded: boolean = false;
-  let imageError: boolean = false;
-  let forceUseOriginal: boolean = false;
-  let loader: HTMLImageElement;
+  let assetFileUrl: string = $state('');
+  let imageLoaded: boolean = $state(false);
+  let imageError: boolean = $state(false);
 
-  $: isWebCompatible = isWebCompatibleImage(asset);
-  $: useOriginalByDefault = isWebCompatible && $alwaysLoadOriginalFile;
-  $: useOriginalImage = useOriginalByDefault || forceUseOriginal;
-  // when true, will force loading of the original image
-  $: forceUseOriginal =
-    forceUseOriginal || asset.originalMimeType === 'image/gif' || ($photoZoomState.currentZoom > 1 && isWebCompatible);
-
-  $: preload(useOriginalImage, preloadAssets);
-  $: imageLoaderUrl = getAssetUrl(asset.id, useOriginalImage, asset.checksum);
+  let loader = $state<HTMLImageElement>();
 
   photoZoomState.set({
     currentRotation: 0,
@@ -102,7 +107,7 @@
   };
 
   const onCopyShortcut = (event: KeyboardEvent) => {
-    if (window.getSelection()?.type === 'Range') {
+    if (globalThis.getSelection()?.type === 'Range') {
       return;
     }
     event.preventDefault();
@@ -129,16 +134,31 @@
     const onerror = () => {
       imageError = imageLoaded = true;
     };
-    if (loader.complete) {
+    if (loader?.complete) {
       onload();
     }
-    loader.addEventListener('load', onload);
-    loader.addEventListener('error', onerror);
+    loader?.addEventListener('load', onload);
+    loader?.addEventListener('error', onerror);
     return () => {
       loader?.removeEventListener('load', onload);
       loader?.removeEventListener('error', onerror);
     };
   });
+  let isWebCompatible = $derived(isWebCompatibleImage(asset));
+  let useOriginalByDefault = $derived(isWebCompatible && $alwaysLoadOriginalFile);
+  // when true, will force loading of the original image
+
+  let forceUseOriginal: boolean = $derived(
+    asset.originalMimeType === 'image/gif' || ($photoZoomState.currentZoom > 1 && isWebCompatible),
+  );
+
+  let useOriginalImage = $derived(useOriginalByDefault || forceUseOriginal);
+
+  $effect(() => {
+    preload(useOriginalImage, preloadAssets);
+  });
+
+  let imageLoaderUrl = $derived(getAssetUrl(asset.id, useOriginalImage, asset.checksum));
 </script>
 
 <svelte:window
@@ -150,15 +170,15 @@
 {#if imageError}
   <BrokenAsset class="text-xl" />
 {/if}
-<!-- svelte-ignore a11y-missing-attribute -->
+<!-- svelte-ignore a11y_missing_attribute -->
 <img bind:this={loader} style="display:none" src={imageLoaderUrl} aria-hidden="true" />
 <div bind:this={element} class="relative h-full select-none">
   <img
     style="display:none"
     src={imageLoaderUrl}
     alt={$getAltText(asset)}
-    on:load={() => ((imageLoaded = true), (assetFileUrl = imageLoaderUrl))}
-    on:error={() => (imageError = imageLoaded = true)}
+    onload={() => ((imageLoaded = true), (assetFileUrl = imageLoaderUrl))}
+    onerror={() => (imageError = imageLoaded = true)}
   />
   {#if !imageLoaded}
     <div id="spinner" class="flex h-full items-center justify-center">
@@ -168,7 +188,7 @@
     <div
       use:zoomImageAction
       use:swipe
-      on:swipe={onSwipe}
+      onswipe={onSwipe}
       class="h-full w-full"
       transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
     >
@@ -193,7 +213,7 @@
         <div
           class="absolute border-solid border-white border-[3px] rounded-lg"
           style="top: {boundingbox.top}px; left: {boundingbox.left}px; height: {boundingbox.height}px; width: {boundingbox.width}px;"
-        />
+        ></div>
       {/each}
     </div>
   {/if}
diff --git a/web/src/lib/components/asset-viewer/slideshow-bar.svelte b/web/src/lib/components/asset-viewer/slideshow-bar.svelte
index 63e501f6dd..3fc2af14da 100644
--- a/web/src/lib/components/asset-viewer/slideshow-bar.svelte
+++ b/web/src/lib/components/asset-viewer/slideshow-bar.svelte
@@ -1,28 +1,39 @@
 <script lang="ts">
   import { shortcuts } from '$lib/actions/shortcut';
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
-  import ProgressBar, { ProgressBarStatus } from '$lib/components/shared-components/progress-bar/progress-bar.svelte';
+  import ProgressBar from '$lib/components/shared-components/progress-bar/progress-bar.svelte';
   import SlideshowSettings from '$lib/components/slideshow-settings.svelte';
+  import { ProgressBarStatus } from '$lib/constants';
   import { SlideshowNavigation, slideshowStore } from '$lib/stores/slideshow.store';
   import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiCog, mdiFullscreen, mdiPause, mdiPlay } from '@mdi/js';
   import { onDestroy, onMount } from 'svelte';
   import { t } from 'svelte-i18n';
   import { fly } from 'svelte/transition';
 
-  export let isFullScreen: boolean;
-  export let onNext = () => {};
-  export let onPrevious = () => {};
-  export let onClose = () => {};
-  export let onSetToFullScreen = () => {};
+  interface Props {
+    isFullScreen: boolean;
+    onNext?: () => void;
+    onPrevious?: () => void;
+    onClose?: () => void;
+    onSetToFullScreen?: () => void;
+  }
+
+  let {
+    isFullScreen,
+    onNext = () => {},
+    onPrevious = () => {},
+    onClose = () => {},
+    onSetToFullScreen = () => {},
+  }: Props = $props();
 
   const { restartProgress, stopProgress, slideshowDelay, showProgressBar, slideshowNavigation } = slideshowStore;
 
-  let progressBarStatus: ProgressBarStatus;
-  let progressBar: ProgressBar;
-  let showSettings = false;
-  let showControls = true;
+  let progressBarStatus: ProgressBarStatus | undefined = $state();
+  let progressBar = $state<ReturnType<typeof ProgressBar>>();
+  let showSettings = $state(false);
+  let showControls = $state(true);
   let timer: NodeJS.Timeout;
-  let isOverControls = false;
+  let isOverControls = $state(false);
 
   let unsubscribeRestart: () => void;
   let unsubscribeStop: () => void;
@@ -55,13 +66,13 @@
     hideControlsAfterDelay();
     unsubscribeRestart = restartProgress.subscribe((value) => {
       if (value) {
-        progressBar.restart(value);
+        progressBar?.restart(value);
       }
     });
 
     unsubscribeStop = stopProgress.subscribe((value) => {
       if (value) {
-        progressBar.restart(false);
+        progressBar?.restart(false);
         stopControlsHideTimer();
       }
     });
@@ -77,7 +88,9 @@
     }
   });
 
-  const handleDone = () => {
+  const handleDone = async () => {
+    await progressBar?.reset();
+
     if ($slideshowNavigation === SlideshowNavigation.AscendingOrder) {
       onPrevious();
       return;
@@ -87,7 +100,7 @@
 </script>
 
 <svelte:window
-  on:mousemove={showControlBar}
+  onmousemove={showControlBar}
   use:shortcuts={[
     { shortcut: { key: 'Escape' }, onShortcut: onClose },
     { shortcut: { key: 'ArrowLeft' }, onShortcut: onPrevious },
@@ -98,32 +111,32 @@
 {#if showControls}
   <div
     class="m-4 flex gap-2"
-    on:mouseenter={() => (isOverControls = true)}
-    on:mouseleave={() => (isOverControls = false)}
+    onmouseenter={() => (isOverControls = true)}
+    onmouseleave={() => (isOverControls = false)}
     transition:fly={{ duration: 150 }}
     role="navigation"
   >
-    <CircleIconButton buttonSize="50" icon={mdiClose} on:click={onClose} title={$t('exit_slideshow')} />
+    <CircleIconButton buttonSize="50" icon={mdiClose} onclick={onClose} title={$t('exit_slideshow')} />
 
     <CircleIconButton
       buttonSize="50"
       icon={progressBarStatus === ProgressBarStatus.Paused ? mdiPlay : mdiPause}
-      on:click={() => (progressBarStatus === ProgressBarStatus.Paused ? progressBar.play() : progressBar.pause())}
+      onclick={() => (progressBarStatus === ProgressBarStatus.Paused ? progressBar?.play() : progressBar?.pause())}
       title={progressBarStatus === ProgressBarStatus.Paused ? $t('play') : $t('pause')}
     />
-    <CircleIconButton buttonSize="50" icon={mdiChevronLeft} on:click={onPrevious} title={$t('previous')} />
-    <CircleIconButton buttonSize="50" icon={mdiChevronRight} on:click={onNext} title={$t('next')} />
+    <CircleIconButton buttonSize="50" icon={mdiChevronLeft} onclick={onPrevious} title={$t('previous')} />
+    <CircleIconButton buttonSize="50" icon={mdiChevronRight} onclick={onNext} title={$t('next')} />
     <CircleIconButton
       buttonSize="50"
       icon={mdiCog}
-      on:click={() => (showSettings = !showSettings)}
+      onclick={() => (showSettings = !showSettings)}
       title={$t('slideshow_settings')}
     />
     {#if !isFullScreen}
       <CircleIconButton
         buttonSize="50"
         icon={mdiFullscreen}
-        on:click={onSetToFullScreen}
+        onclick={onSetToFullScreen}
         title={$t('set_slideshow_to_fullscreen')}
       />
     {/if}
@@ -139,5 +152,5 @@
   duration={$slideshowDelay}
   bind:this={progressBar}
   bind:status={progressBarStatus}
-  on:done={handleDone}
+  onDone={handleDone}
 />
diff --git a/web/src/lib/components/asset-viewer/video-native-viewer.svelte b/web/src/lib/components/asset-viewer/video-native-viewer.svelte
index e0adcdfc9d..d019ef273f 100644
--- a/web/src/lib/components/asset-viewer/video-native-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/video-native-viewer.svelte
@@ -4,50 +4,72 @@
   import { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
   import { handleError } from '$lib/utils/handle-error';
   import { AssetMediaSize } from '@immich/sdk';
-  import { createEventDispatcher, tick } from 'svelte';
+  import { onDestroy, onMount } from 'svelte';
   import { swipe } from 'svelte-gestures';
   import type { SwipeCustomEvent } from 'svelte-gestures';
   import { fade } from 'svelte/transition';
   import { t } from 'svelte-i18n';
 
-  export let assetId: string;
-  export let loopVideo: boolean;
-  export let checksum: string;
-  export let onPreviousAsset: () => void;
-  export let onNextAsset: () => void;
-
-  let element: HTMLVideoElement | undefined = undefined;
-  let isVideoLoading = true;
-  let assetFileUrl: string;
-  let forceMuted = false;
-
-  $: if (element) {
-    assetFileUrl = getAssetPlaybackUrl({ id: assetId, checksum });
-    forceMuted = false;
-    element.load();
+  interface Props {
+    assetId: string;
+    loopVideo: boolean;
+    checksum: string;
+    onPreviousAsset?: () => void;
+    onNextAsset?: () => void;
+    onVideoEnded?: () => void;
+    onVideoStarted?: () => void;
+    onClose?: () => void;
   }
 
-  const dispatch = createEventDispatcher<{ onVideoEnded: void; onVideoStarted: void }>();
+  let {
+    assetId,
+    loopVideo,
+    checksum,
+    onPreviousAsset = () => {},
+    onNextAsset = () => {},
+    onVideoEnded = () => {},
+    onVideoStarted = () => {},
+    onClose = () => {},
+  }: Props = $props();
+
+  let videoPlayer: HTMLVideoElement | undefined = $state();
+  let isLoading = $state(true);
+  let assetFileUrl = $state('');
+  let forceMuted = $state(false);
+
+  onMount(() => {
+    if (videoPlayer) {
+      assetFileUrl = getAssetPlaybackUrl({ id: assetId, checksum });
+      forceMuted = false;
+      videoPlayer.load();
+    }
+  });
+
+  onDestroy(() => {
+    if (videoPlayer) {
+      videoPlayer.src = '';
+    }
+  });
 
   const handleCanPlay = async (video: HTMLVideoElement) => {
     try {
       await video.play();
-      dispatch('onVideoStarted');
+      onVideoStarted();
     } catch (error) {
       if (error instanceof DOMException && error.name === 'NotAllowedError' && !forceMuted) {
         await tryForceMutedPlay(video);
         return;
       }
+
       handleError(error, $t('errors.unable_to_play_video'));
     } finally {
-      isVideoLoading = false;
+      isLoading = false;
     }
   };
 
   const tryForceMutedPlay = async (video: HTMLVideoElement) => {
     try {
-      forceMuted = true;
-      await tick();
+      video.muted = true;
       await handleCanPlay(video);
     } catch (error) {
       handleError(error, $t('errors.unable_to_play_video'));
@@ -66,30 +88,30 @@
 
 <div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
   <video
-    bind:this={element}
+    bind:this={videoPlayer}
     loop={$loopVideoPreference && loopVideo}
     autoplay
     playsinline
     controls
     class="h-full object-contain"
     use:swipe
-    on:swipe={onSwipe}
-    on:canplay={(e) => handleCanPlay(e.currentTarget)}
-    on:ended={() => dispatch('onVideoEnded')}
-    on:volumechange={(e) => {
+    onswipe={onSwipe}
+    oncanplay={(e) => handleCanPlay(e.currentTarget)}
+    onended={onVideoEnded}
+    onvolumechange={(e) => {
       if (!forceMuted) {
         $videoViewerMuted = e.currentTarget.muted;
       }
     }}
+    onclose={() => onClose()}
     muted={forceMuted || $videoViewerMuted}
     bind:volume={$videoViewerVolume}
     poster={getAssetThumbnailUrl({ id: assetId, size: AssetMediaSize.Preview, checksum })}
+    src={assetFileUrl}
   >
-    <source src={assetFileUrl} type="video/mp4" />
-    <track kind="captions" />
   </video>
 
-  {#if isVideoLoading}
+  {#if isLoading}
     <div class="absolute flex place-content-center place-items-center">
       <LoadingSpinner />
     </div>
diff --git a/web/src/lib/components/asset-viewer/video-panorama-viewer.svelte b/web/src/lib/components/asset-viewer/video-panorama-viewer.svelte
new file mode 100644
index 0000000000..73315d661e
--- /dev/null
+++ b/web/src/lib/components/asset-viewer/video-panorama-viewer.svelte
@@ -0,0 +1,29 @@
+<script lang="ts">
+  import { getAssetOriginalUrl } from '$lib/utils';
+  import { fade } from 'svelte/transition';
+  import LoadingSpinner from '../shared-components/loading-spinner.svelte';
+  import { t } from 'svelte-i18n';
+
+  interface Props {
+    assetId: string;
+  }
+
+  const { assetId }: Props = $props();
+
+  const modules = Promise.all([
+    import('./photo-sphere-viewer-adapter.svelte').then((module) => module.default),
+    import('@photo-sphere-viewer/equirectangular-video-adapter').then((module) => module.EquirectangularVideoAdapter),
+    import('@photo-sphere-viewer/video-plugin').then((module) => module.VideoPlugin),
+    import('@photo-sphere-viewer/video-plugin/index.css'),
+  ]);
+</script>
+
+<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
+  {#await modules}
+    <LoadingSpinner />
+  {:then [PhotoSphereViewer, adapter, videoPlugin]}
+    <PhotoSphereViewer panorama={{ source: getAssetOriginalUrl(assetId) }} plugins={[videoPlugin]} {adapter} navbar />
+  {:catch}
+    {$t('errors.failed_to_load_asset')}
+  {/await}
+</div>
diff --git a/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte b/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte
index 5f03784c42..7ee21e59a2 100644
--- a/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte
@@ -1,19 +1,35 @@
 <script lang="ts">
-  import { AssetTypeEnum } from '@immich/sdk';
   import { ProjectionType } from '$lib/constants';
   import VideoNativeViewer from '$lib/components/asset-viewer/video-native-viewer.svelte';
-  import PanoramaViewer from '$lib/components/asset-viewer/panorama-viewer.svelte';
+  import VideoPanoramaViewer from '$lib/components/asset-viewer/video-panorama-viewer.svelte';
 
-  export let assetId: string;
-  export let projectionType: string | null | undefined;
-  export let checksum: string;
-  export let loopVideo: boolean;
-  export let onPreviousAsset: () => void;
-  export let onNextAsset: () => void;
+  interface Props {
+    assetId: string;
+    projectionType: string | null | undefined;
+    checksum: string;
+    loopVideo: boolean;
+    onClose?: () => void;
+    onPreviousAsset?: () => void;
+    onNextAsset?: () => void;
+    onVideoEnded?: () => void;
+    onVideoStarted?: () => void;
+  }
+
+  let {
+    assetId,
+    projectionType,
+    checksum,
+    loopVideo,
+    onPreviousAsset,
+    onClose,
+    onNextAsset,
+    onVideoEnded,
+    onVideoStarted,
+  }: Props = $props();
 </script>
 
 {#if projectionType === ProjectionType.EQUIRECTANGULAR}
-  <PanoramaViewer asset={{ id: assetId, type: AssetTypeEnum.Video }} />
+  <VideoPanoramaViewer {assetId} />
 {:else}
   <VideoNativeViewer
     {loopVideo}
@@ -21,7 +37,8 @@
     {assetId}
     {onPreviousAsset}
     {onNextAsset}
-    on:onVideoEnded
-    on:onVideoStarted
+    {onVideoEnded}
+    {onVideoStarted}
+    {onClose}
   />
 {/if}
diff --git a/web/src/lib/components/assets/broken-asset.svelte b/web/src/lib/components/assets/broken-asset.svelte
index dd54afba01..31acb832e5 100644
--- a/web/src/lib/components/assets/broken-asset.svelte
+++ b/web/src/lib/components/assets/broken-asset.svelte
@@ -3,11 +3,14 @@
   import { mdiImageBrokenVariant } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  let className = '';
-  export { className as class };
-  export let hideMessage = false;
-  export let width: string | undefined = undefined;
-  export let height: string | undefined = undefined;
+  interface Props {
+    class?: string;
+    hideMessage?: boolean;
+    width?: string | undefined;
+    height?: string | undefined;
+  }
+
+  let { class: className = '', hideMessage = false, width = undefined, height = undefined }: Props = $props();
 </script>
 
 <div
diff --git a/web/src/lib/components/assets/thumbnail/__test__/image-thumbnail.spec.ts b/web/src/lib/components/assets/thumbnail/__test__/image-thumbnail.spec.ts
index 2525b86160..e14628a42f 100644
--- a/web/src/lib/components/assets/thumbnail/__test__/image-thumbnail.spec.ts
+++ b/web/src/lib/components/assets/thumbnail/__test__/image-thumbnail.spec.ts
@@ -3,9 +3,9 @@ import { render } from '@testing-library/svelte';
 
 describe('ImageThumbnail component', () => {
   beforeAll(() => {
-    Object.defineProperty(HTMLImageElement.prototype, 'complete', {
-      value: true,
-    });
+    Element.prototype.animate = vi.fn().mockImplementation(() => ({
+      cancel: () => {},
+    }));
   });
 
   it('shows thumbhash while image is loading', () => {
diff --git a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte
index 662209544a..9d69bdeeb2 100644
--- a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte
+++ b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte
@@ -7,29 +7,49 @@
   import { onMount } from 'svelte';
   import { fade } from 'svelte/transition';
 
-  export let url: string;
-  export let altText: string | undefined;
-  export let title: string | null = null;
-  export let heightStyle: string | undefined = undefined;
-  export let widthStyle: string;
-  export let base64ThumbHash: string | null = null;
-  export let curve = false;
-  export let shadow = false;
-  export let circle = false;
-  export let hidden = false;
-  export let border = false;
-  export let preload = true;
-  export let hiddenIconClass = 'text-white';
-  export let onComplete: (() => void) | undefined = undefined;
+  interface Props {
+    url: string;
+    altText: string | undefined;
+    title?: string | null;
+    heightStyle?: string | undefined;
+    widthStyle: string;
+    base64ThumbHash?: string | null;
+    curve?: boolean;
+    shadow?: boolean;
+    circle?: boolean;
+    hidden?: boolean;
+    border?: boolean;
+    preload?: boolean;
+    hiddenIconClass?: string;
+    onComplete?: (() => void) | undefined;
+    onClick?: (() => void) | undefined;
+  }
+
+  let {
+    url,
+    altText,
+    title = null,
+    heightStyle = undefined,
+    widthStyle,
+    base64ThumbHash = null,
+    curve = false,
+    shadow = false,
+    circle = false,
+    hidden = false,
+    border = false,
+    preload = true,
+    hiddenIconClass = 'text-white',
+    onComplete = undefined,
+  }: Props = $props();
 
   let {
     IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION },
   } = TUNABLES;
 
-  let loaded = false;
-  let errored = false;
+  let loaded = $state(false);
+  let errored = $state(false);
 
-  let img: HTMLImageElement;
+  let img = $state<HTMLImageElement>();
 
   const setLoaded = () => {
     loaded = true;
@@ -40,20 +60,22 @@
     onComplete?.();
   };
   onMount(() => {
-    if (img.complete) {
+    if (img?.complete) {
       setLoaded();
     }
   });
 
-  $: optionalClasses = [
-    curve && 'rounded-xl',
-    circle && 'rounded-full',
-    shadow && 'shadow-lg',
-    (circle || !heightStyle) && 'aspect-square',
-    border && 'border-[3px] border-immich-dark-primary/80 hover:border-immich-primary',
-  ]
-    .filter(Boolean)
-    .join(' ');
+  let optionalClasses = $derived(
+    [
+      curve && 'rounded-xl',
+      circle && 'rounded-full',
+      shadow && 'shadow-lg',
+      (circle || !heightStyle) && 'aspect-square',
+      border && 'border-[3px] border-immich-dark-primary/80 hover:border-immich-primary',
+    ]
+      .filter(Boolean)
+      .join(' '),
+  );
 </script>
 
 {#if errored}
@@ -61,8 +83,8 @@
 {:else}
   <img
     bind:this={img}
-    on:load={setLoaded}
-    on:error={setErrored}
+    onload={setLoaded}
+    onerror={setErrored}
     loading={preload ? 'eager' : 'lazy'}
     style:width={widthStyle}
     style:height={heightStyle}
@@ -96,5 +118,5 @@
     class:rounded-full={circle}
     draggable="false"
     out:fade={{ duration: THUMBHASH_FADE_DURATION }}
-  />
+  ></canvas>
 {/if}
diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte
index af22887185..536ea90163 100644
--- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte
+++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte
@@ -31,61 +31,89 @@
   import { TUNABLES } from '$lib/utils/tunables';
   import { thumbhash } from '$lib/actions/thumbhash';
 
-  export let asset: AssetResponseDto;
-  export let dateGroup: DateGroup | undefined = undefined;
-  export let assetStore: AssetStore | undefined = undefined;
-  export let groupIndex = 0;
-  export let thumbnailSize: number | undefined = undefined;
-  export let thumbnailWidth: number | undefined = undefined;
-  export let thumbnailHeight: number | undefined = undefined;
-  export let selected = false;
-  export let selectionCandidate = false;
-  export let disabled = false;
-  export let readonly = false;
-  export let showArchiveIcon = false;
-  export let showStackedIcon = true;
-  export let intersectionConfig: {
-    root?: HTMLElement;
-    bottom?: string;
-    top?: string;
-    left?: string;
-    priority?: number;
+  interface Props {
+    asset: AssetResponseDto;
+    dateGroup?: DateGroup | undefined;
+    assetStore?: AssetStore | undefined;
+    groupIndex?: number;
+    thumbnailSize?: number | undefined;
+    thumbnailWidth?: number | undefined;
+    thumbnailHeight?: number | undefined;
+    selected?: boolean;
+    selectionCandidate?: boolean;
     disabled?: boolean;
-  } = {};
+    readonly?: boolean;
+    showArchiveIcon?: boolean;
+    showStackedIcon?: boolean;
+    disableMouseOver?: boolean;
+    intersectionConfig?: {
+      root?: HTMLElement;
+      bottom?: string;
+      top?: string;
+      left?: string;
+      priority?: number;
+      disabled?: boolean;
+    };
+    retrieveElement?: boolean;
+    onIntersected?: (() => void) | undefined;
+    onClick?: ((asset: AssetResponseDto) => void) | undefined;
+    onRetrieveElement?: ((elment: HTMLElement) => void) | undefined;
+    onSelect?: ((asset: AssetResponseDto) => void) | undefined;
+    onMouseEvent?: ((event: { isMouseOver: boolean; selectedGroupIndex: number }) => void) | undefined;
+    class?: string;
+  }
 
-  export let retrieveElement: boolean = false;
-  export let onIntersected: (() => void) | undefined = undefined;
-  export let onClick: ((asset: AssetResponseDto) => void) | undefined = undefined;
-  export let onRetrieveElement: ((elment: HTMLElement) => void) | undefined = undefined;
-  export let onSelect: ((asset: AssetResponseDto) => void) | undefined = undefined;
-  export let onMouseEvent: ((event: { isMouseOver: boolean; selectedGroupIndex: number }) => void) | undefined =
-    undefined;
-
-  let className = '';
-  export { className as class };
+  let {
+    asset,
+    dateGroup = undefined,
+    assetStore = undefined,
+    groupIndex = 0,
+    thumbnailSize = undefined,
+    thumbnailWidth = undefined,
+    thumbnailHeight = undefined,
+    selected = false,
+    selectionCandidate = false,
+    disabled = false,
+    readonly = false,
+    showArchiveIcon = false,
+    showStackedIcon = true,
+    disableMouseOver = false,
+    intersectionConfig = {},
+    retrieveElement = false,
+    onIntersected = undefined,
+    onClick = undefined,
+    onRetrieveElement = undefined,
+    onSelect = undefined,
+    onMouseEvent = undefined,
+    class: className = '',
+  }: Props = $props();
 
   let {
     IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION },
   } = TUNABLES;
 
   const componentId = generateId();
-  let element: HTMLElement | undefined;
-  let mouseOver = false;
-  let intersecting = false;
-  let lastRetrievedElement: HTMLElement | undefined;
-  let loaded = false;
+  let element: HTMLElement | undefined = $state();
+  let mouseOver = $state(false);
+  let intersecting = $state(false);
+  let lastRetrievedElement: HTMLElement | undefined = $state();
+  let loaded = $state(false);
 
-  $: if (!retrieveElement) {
-    lastRetrievedElement = undefined;
-  }
-  $: if (retrieveElement && element && lastRetrievedElement !== element) {
-    lastRetrievedElement = element;
-    onRetrieveElement?.(element);
-  }
+  $effect(() => {
+    if (!retrieveElement) {
+      lastRetrievedElement = undefined;
+    }
+  });
+  $effect(() => {
+    if (retrieveElement && element && lastRetrievedElement !== element) {
+      lastRetrievedElement = element;
+      onRetrieveElement?.(element);
+    }
+  });
 
-  $: width = thumbnailSize || thumbnailWidth || 235;
-  $: height = thumbnailSize || thumbnailHeight || 235;
-  $: display = intersecting;
+  let width = $derived(thumbnailSize || thumbnailWidth || 235);
+  let height = $derived(thumbnailSize || thumbnailHeight || 235);
+  let display = $derived(intersecting);
 
   const onIconClickedHandler = (e?: MouseEvent) => {
     e?.stopPropagation();
@@ -196,18 +224,18 @@
       class="group"
       class:cursor-not-allowed={disabled}
       class:cursor-pointer={!disabled}
-      on:mouseenter={onMouseEnter}
-      on:mouseleave={onMouseLeave}
-      on:keypress={(evt) => {
+      onmouseenter={onMouseEnter}
+      onmouseleave={onMouseLeave}
+      onkeypress={(evt) => {
         if (evt.key === 'Enter') {
           callClickHandlers();
         }
       }}
       tabindex={0}
-      on:click={handleClick}
+      onclick={handleClick}
       role="link"
     >
-      {#if mouseOver}
+      {#if mouseOver && !disableMouseOver}
         <!-- lazy show the url on mouse over-->
         <a
           class="absolute z-30 {className} top-[41px]"
@@ -215,8 +243,9 @@
           style:width="{width}px"
           style:height="{height}px"
           href={currentUrlReplaceAssetId(asset.id)}
-          on:click={(evt) => evt.preventDefault()}
+          onclick={(evt) => evt.preventDefault()}
           tabindex={0}
+          aria-label="Thumbnail URL"
         >
         </a>
       {/if}
@@ -225,7 +254,7 @@
         {#if !readonly && (mouseOver || selected || selectionCandidate)}
           <button
             type="button"
-            on:click={onIconClickedHandler}
+            onclick={onIconClickedHandler}
             class="absolute p-2 focus:outline-none"
             class:cursor-not-allowed={disabled}
             role="checkbox"
@@ -254,12 +283,12 @@
         <div
           class="absolute z-10 h-full w-full bg-gradient-to-b from-black/25 via-[transparent_25%] opacity-0 transition-opacity group-hover:opacity-100"
           class:rounded-xl={selected}
-        />
+        ></div>
 
         <!-- Outline on focus -->
         <div
           class="absolute size-full group-focus-visible:outline outline-4 -outline-offset-4 outline-immich-primary"
-        />
+        ></div>
 
         <!-- Favorite asset star -->
         {#if !isSharedLink() && asset.isFavorite}
@@ -338,7 +367,7 @@
           class="absolute top-0 h-full w-full bg-immich-primary opacity-40"
           in:fade={{ duration: 100 }}
           out:fade={{ duration: 100 }}
-        />
+        ></div>
       {/if}
     </div>
   {/if}
diff --git a/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte
index 5cac0b1945..9188ab9a4f 100644
--- a/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte
+++ b/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte
@@ -7,31 +7,47 @@
   import { generateId } from '$lib/utils/generate-id';
   import { onDestroy } from 'svelte';
 
-  export let assetStore: AssetStore | undefined = undefined;
-  export let url: string;
-  export let durationInSeconds = 0;
-  export let enablePlayback = false;
-  export let playbackOnIconHover = false;
-  export let showTime = true;
-  export let curve = false;
-  export let playIcon = mdiPlayCircleOutline;
-  export let pauseIcon = mdiPauseCircleOutline;
+  interface Props {
+    assetStore?: AssetStore | undefined;
+    url: string;
+    durationInSeconds?: number;
+    enablePlayback?: boolean;
+    playbackOnIconHover?: boolean;
+    showTime?: boolean;
+    curve?: boolean;
+    playIcon?: string;
+    pauseIcon?: string;
+  }
+
+  let {
+    assetStore = undefined,
+    url,
+    durationInSeconds = 0,
+    enablePlayback = $bindable(false),
+    playbackOnIconHover = false,
+    showTime = true,
+    curve = false,
+    playIcon = mdiPlayCircleOutline,
+    pauseIcon = mdiPauseCircleOutline,
+  }: Props = $props();
 
   const componentId = generateId();
-  let remainingSeconds = durationInSeconds;
-  let loading = true;
-  let error = false;
-  let player: HTMLVideoElement;
+  let remainingSeconds = $state(durationInSeconds);
+  let loading = $state(true);
+  let error = $state(false);
+  let player: HTMLVideoElement | undefined = $state();
 
-  $: if (!enablePlayback) {
-    // Reset remaining time when playback is disabled.
-    remainingSeconds = durationInSeconds;
+  $effect(() => {
+    if (!enablePlayback) {
+      // Reset remaining time when playback is disabled.
+      remainingSeconds = durationInSeconds;
 
-    if (player) {
-      // Cancel video buffering.
-      player.src = '';
+      if (player) {
+        // Cancel video buffering.
+        player.src = '';
+      }
     }
-  }
+  });
   const onMouseEnter = () => {
     if (assetStore) {
       assetStore.taskManager.queueScrollSensitiveTask({
@@ -78,8 +94,8 @@
     </span>
   {/if}
 
-  <!-- svelte-ignore a11y-no-static-element-interactions -->
-  <span class="pr-2 pt-2" on:mouseenter={onMouseEnter} on:mouseleave={onMouseLeave}>
+  <!-- svelte-ignore a11y_no_static_element_interactions -->
+  <span class="pr-2 pt-2" onmouseenter={onMouseEnter} onmouseleave={onMouseLeave}>
     {#if enablePlayback}
       {#if loading}
         <LoadingSpinner />
@@ -103,17 +119,24 @@
     autoplay
     loop
     src={url}
-    on:play={() => {
+    onplay={() => {
       loading = false;
       error = false;
     }}
-    on:error={() => {
+    onerror={() => {
+      if (!player?.src) {
+        // Do not show error when the URL is empty.
+        return;
+      }
       error = true;
       loading = false;
     }}
-    on:timeupdate={({ currentTarget }) => {
+    ontimeupdate={({ currentTarget }) => {
       const remaining = currentTarget.duration - currentTarget.currentTime;
-      remainingSeconds = Math.min(Math.ceil(remaining), durationInSeconds);
+      remainingSeconds = Math.min(
+        Math.ceil(Number.isNaN(remaining) ? Number.POSITIVE_INFINITY : remaining),
+        durationInSeconds,
+      );
     }}
-  />
+  ></video>
 {/if}
diff --git a/web/src/lib/components/elements/badge.svelte b/web/src/lib/components/elements/badge.svelte
index da305e40f9..0db6e3fa40 100644
--- a/web/src/lib/components/elements/badge.svelte
+++ b/web/src/lib/components/elements/badge.svelte
@@ -1,11 +1,18 @@
-<script lang="ts" context="module">
+<script lang="ts" module>
   export type Color = 'primary' | 'secondary';
   export type Rounded = false | true | 'full';
 </script>
 
 <script lang="ts">
-  export let color: Color = 'primary';
-  export let rounded: Rounded = true;
+  import type { Snippet } from 'svelte';
+
+  interface Props {
+    color?: Color;
+    rounded?: Rounded;
+    children?: Snippet;
+  }
+
+  let { color = 'primary', rounded = true, children }: Props = $props();
 
   const colorClasses: Record<Color, string> = {
     primary: 'text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary',
@@ -20,5 +27,5 @@
   class:rounded-md={rounded === true}
   class:rounded-full={rounded === 'full'}
 >
-  <slot />
+  {@render children?.()}
 </span>
diff --git a/web/src/lib/components/elements/buttons/button.svelte b/web/src/lib/components/elements/buttons/button.svelte
index cdd7463445..7e8418e2f5 100644
--- a/web/src/lib/components/elements/buttons/button.svelte
+++ b/web/src/lib/components/elements/buttons/button.svelte
@@ -1,6 +1,4 @@
-<script lang="ts" context="module">
-  import type { HTMLButtonAttributes, HTMLLinkAttributes } from 'svelte/elements';
-
+<script lang="ts" module>
   export type Color =
     | 'primary'
     | 'primary-inversed'
@@ -17,44 +15,47 @@
   export type Size = 'tiny' | 'icon' | 'link' | 'sm' | 'base' | 'lg';
   export type Rounded = 'lg' | '3xl' | 'full' | 'none';
   export type Shadow = 'md' | false;
+</script>
 
-  type BaseProps = {
-    class?: string;
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+
+  interface Props {
+    type?: string;
+    href?: string;
     color?: Color;
     size?: Size;
     rounded?: Rounded;
     shadow?: Shadow;
     fullwidth?: boolean;
     border?: boolean;
-  };
+    class?: string;
+    children?: Snippet;
+    onclick?: (event: MouseEvent) => void;
+    onfocus?: () => void;
+    onblur?: () => void;
+    form?: string;
+    disabled?: boolean;
+    title?: string;
+    'aria-current'?: 'page' | 'step' | 'location' | 'date' | 'time' | undefined | null;
+  }
 
-  export type ButtonProps = HTMLButtonAttributes &
-    BaseProps & {
-      href?: never;
-    };
-
-  export type LinkProps = HTMLLinkAttributes &
-    BaseProps & {
-      type?: never;
-    };
-
-  export type Props = ButtonProps | LinkProps;
-</script>
-
-<script lang="ts">
-  type $$Props = Props;
-
-  export let type: $$Props['type'] = 'button';
-  export let href: $$Props['href'] = undefined;
-  export let color: Color = 'primary';
-  export let size: Size = 'base';
-  export let rounded: Rounded = '3xl';
-  export let shadow: Shadow = 'md';
-  export let fullwidth = false;
-  export let border = false;
-
-  let className = '';
-  export { className as class };
+  let {
+    type = 'button',
+    href = undefined,
+    color = 'primary',
+    size = 'base',
+    rounded = '3xl',
+    shadow = 'md',
+    fullwidth = false,
+    border = false,
+    class: className = '',
+    children,
+    onclick,
+    onfocus,
+    onblur,
+    ...rest
+  }: Props = $props();
 
   const colorClasses: Record<Color, string> = {
     primary:
@@ -93,29 +94,31 @@
     full: 'rounded-full',
   };
 
-  $: computedClass = [
-    className,
-    colorClasses[color],
-    sizeClasses[size],
-    roundedClasses[rounded],
-    shadow === 'md' && 'shadow-md',
-    fullwidth && 'w-full',
-    border && 'border',
-  ]
-    .filter(Boolean)
-    .join(' ');
+  let computedClass = $derived(
+    [
+      className,
+      colorClasses[color],
+      sizeClasses[size],
+      roundedClasses[rounded],
+      shadow === 'md' && 'shadow-md',
+      fullwidth && 'w-full',
+      border && 'border',
+    ]
+      .filter(Boolean)
+      .join(' '),
+  );
 </script>
 
-<!-- svelte-ignore a11y-no-static-element-interactions -->
+<!-- svelte-ignore a11y_no_static_element_interactions -->
 <svelte:element
   this={href ? 'a' : 'button'}
   type={href ? undefined : type}
   {href}
-  on:click
-  on:focus
-  on:blur
+  {onclick}
+  {onfocus}
+  {onblur}
   class="inline-flex items-center justify-center transition-colors disabled:cursor-not-allowed disabled:opacity-60 disabled:pointer-events-none {computedClass}"
-  {...$$restProps}
+  {...rest}
 >
-  <slot />
+  {@render children?.()}
 </svelte:element>
diff --git a/web/src/lib/components/elements/buttons/circle-icon-button.svelte b/web/src/lib/components/elements/buttons/circle-icon-button.svelte
index 76f962f107..071a645000 100644
--- a/web/src/lib/components/elements/buttons/circle-icon-button.svelte
+++ b/web/src/lib/components/elements/buttons/circle-icon-button.svelte
@@ -1,71 +1,75 @@
-<script lang="ts" context="module">
-  import type { HTMLButtonAttributes, HTMLLinkAttributes } from 'svelte/elements';
-
-  export type Color = 'transparent' | 'light' | 'dark' | 'gray' | 'primary' | 'opaque';
+<script lang="ts" module>
+  export type Color = 'transparent' | 'light' | 'dark' | 'red' | 'gray' | 'primary' | 'opaque' | 'alert' | 'neutral';
   export type Padding = '1' | '2' | '3';
-
-  type BaseProps = {
-    icon: string;
-    title: string;
-    class?: string;
-    color?: Color;
-    padding?: Padding;
-    size?: string;
-    hideMobile?: true;
-    buttonSize?: string;
-    viewBox?: string;
-  };
-
-  export type ButtonProps = HTMLButtonAttributes &
-    BaseProps & {
-      href?: never;
-    };
-
-  export type LinkProps = HTMLLinkAttributes &
-    BaseProps & {
-      type?: never;
-    };
-
-  export type Props = ButtonProps | LinkProps;
 </script>
 
 <script lang="ts">
   import Icon from '$lib/components/elements/icon.svelte';
 
-  type $$Props = Props;
-
-  export let type: $$Props['type'] = 'button';
-  export let href: $$Props['href'] = undefined;
-  export let icon: string;
-  export let color: Color = 'transparent';
-  export let title: string;
-  /**
-   * The padding of the button, used by the `p-{padding}` Tailwind CSS class.
-   */
-  export let padding: Padding = '3';
-  /**
-   * Size of the button, used for a CSS value.
-   */
-  export let size = '24';
-  export let hideMobile = false;
-  export let buttonSize: string | undefined = undefined;
-  /**
-   * viewBox attribute for the SVG icon.
-   */
-  export let viewBox: string | undefined = undefined;
-
   /**
    * Override the default styling of the button for specific use cases, such as the icon color.
    */
-  let className = '';
-  export { className as class };
+  interface Props {
+    id?: string;
+    type?: string;
+    href?: string;
+    icon: string;
+    color?: Color;
+    title: string;
+    /**
+     * The padding of the button, used by the `p-{padding}` Tailwind CSS class.
+     */
+    padding?: Padding;
+    /**
+     * Size of the button, used for a CSS value.
+     */
+    size?: string;
+    hideMobile?: boolean;
+    buttonSize?: string | undefined;
+    /**
+     * viewBox attribute for the SVG icon.
+     */
+    viewBox?: string | undefined;
+    class?: string;
+
+    'aria-hidden'?: boolean | undefined | null;
+    'aria-checked'?: 'true' | 'false' | undefined | null;
+    'aria-current'?: 'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false' | undefined | null;
+    'aria-controls'?: string | undefined | null;
+    'aria-expanded'?: boolean;
+    'aria-haspopup'?: boolean;
+    tabindex?: number | undefined | null;
+    role?: string | undefined | null;
+    onclick: (e: MouseEvent) => void;
+    disabled?: boolean;
+  }
+
+  let {
+    type = 'button',
+    href = undefined,
+    icon,
+    color = 'transparent',
+    title,
+    padding = '3',
+    size = '24',
+    hideMobile = false,
+    buttonSize = undefined,
+    viewBox = undefined,
+    class: className = '',
+    onclick,
+    ...rest
+  }: Props = $props();
 
   const colorClasses: Record<Color, string> = {
     transparent: 'bg-transparent hover:bg-[#d3d3d3] dark:text-immich-dark-fg',
     opaque: 'bg-transparent hover:bg-immich-bg/30 text-white hover:dark:text-white',
     light: 'bg-white hover:bg-[#d3d3d3]',
+    red: 'text-red-400 hover:bg-[#d3d3d3]',
     dark: 'bg-[#202123] hover:bg-[#d3d3d3]',
+    alert: 'text-[#ff0000] hover:text-white',
     gray: 'bg-[#d3d3d3] hover:bg-[#e2e7e9] text-immich-dark-gray hover:text-black',
+    neutral:
+      'dark:bg-immich-dark-gray dark:text-gray-300 hover:dark:bg-immich-dark-gray/50 hover:dark:text-gray-300 bg-gray-200 text-gray-700 hover:bg-gray-300',
     primary:
       'bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 hover:dark:bg-immich-dark-primary/80 text-white dark:text-immich-dark-gray',
   };
@@ -76,12 +80,12 @@
     '3': 'p-3',
   };
 
-  $: colorClass = colorClasses[color];
-  $: mobileClass = hideMobile ? 'hidden sm:flex' : '';
-  $: paddingClass = paddingClasses[padding];
+  let colorClass = $derived(colorClasses[color]);
+  let mobileClass = $derived(hideMobile ? 'hidden sm:flex' : '');
+  let paddingClass = $derived(paddingClasses[padding]);
 </script>
 
-<!-- svelte-ignore a11y-no-static-element-interactions -->
+<!-- svelte-ignore a11y_no_static_element_interactions -->
 <svelte:element
   this={href ? 'a' : 'button'}
   type={href ? undefined : type}
@@ -90,8 +94,8 @@
   style:width={buttonSize ? buttonSize + 'px' : ''}
   style:height={buttonSize ? buttonSize + 'px' : ''}
   class="flex place-content-center place-items-center rounded-full {colorClass} {paddingClass} transition-all disabled:cursor-default hover:dark:text-immich-dark-gray {className} {mobileClass}"
-  on:click
-  {...$$restProps}
+  {onclick}
+  {...rest}
 >
   <Icon path={icon} {size} ariaLabel={title} {viewBox} color="currentColor" />
 </svelte:element>
diff --git a/web/src/lib/components/elements/buttons/link-button.svelte b/web/src/lib/components/elements/buttons/link-button.svelte
index b8e81f4469..a39e2608cf 100644
--- a/web/src/lib/components/elements/buttons/link-button.svelte
+++ b/web/src/lib/components/elements/buttons/link-button.svelte
@@ -1,22 +1,25 @@
-<script lang="ts" context="module">
+<script lang="ts" module>
   export type Color = 'transparent-primary' | 'transparent-gray';
-
-  type BaseProps = {
-    color?: Color;
-  };
-
-  export type Props = (LinkProps & BaseProps) | (ButtonProps & BaseProps);
 </script>
 
 <script lang="ts">
-  import Button, { type ButtonProps, type LinkProps } from '$lib/components/elements/buttons/button.svelte';
+  import Button from '$lib/components/elements/buttons/button.svelte';
+  import type { Snippet } from 'svelte';
 
-  // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  type $$Props = Props;
+  interface Props {
+    href?: string;
+    color?: Color;
+    children?: Snippet;
+    onclick?: (e: MouseEvent) => void;
+    title?: string;
+    disabled?: boolean;
+    fullwidth?: boolean;
+    class?: string;
+  }
 
-  export let color: Color = 'transparent-gray';
+  let { color = 'transparent-gray', children, ...rest }: Props = $props();
 </script>
 
-<Button size="link" {color} shadow={false} rounded="lg" on:click {...$$restProps}>
-  <slot />
+<Button size="link" {color} shadow={false} rounded="lg" {...rest}>
+  {@render children?.()}
 </Button>
diff --git a/web/src/lib/components/elements/buttons/skip-link.svelte b/web/src/lib/components/elements/buttons/skip-link.svelte
index d1ad667379..858d296c30 100644
--- a/web/src/lib/components/elements/buttons/skip-link.svelte
+++ b/web/src/lib/components/elements/buttons/skip-link.svelte
@@ -2,13 +2,17 @@
   import { t } from 'svelte-i18n';
   import Button from './button.svelte';
 
-  /**
-   * Target for the skip link to move focus to.
-   */
-  export let target: string = 'main';
-  export let text: string = $t('skip_to_content');
+  interface Props {
+    /**
+     * Target for the skip link to move focus to.
+     */
+    target?: string;
+    text?: string;
+  }
 
-  let isFocused = false;
+  let { target = 'main', text = $t('skip_to_content') }: Props = $props();
+
+  let isFocused = $state(false);
 
   const moveFocus = () => {
     const targetEl = document.querySelector<HTMLElement>(target);
@@ -20,9 +24,9 @@
   <Button
     size={'sm'}
     rounded="none"
-    on:click={moveFocus}
-    on:focus={() => (isFocused = true)}
-    on:blur={() => (isFocused = false)}
+    onclick={moveFocus}
+    onfocus={() => (isFocused = true)}
+    onblur={() => (isFocused = false)}
   >
     {text}
   </Button>
diff --git a/web/src/lib/components/elements/checkbox.svelte b/web/src/lib/components/elements/checkbox.svelte
index 3407262551..4595c06bfb 100644
--- a/web/src/lib/components/elements/checkbox.svelte
+++ b/web/src/lib/components/elements/checkbox.svelte
@@ -1,11 +1,25 @@
 <script lang="ts">
-  export let id: string;
-  export let label: string;
-  export let checked: boolean | undefined = undefined;
-  export let disabled: boolean = false;
-  export let labelClass: string | undefined = undefined;
-  export let name: string | undefined = undefined;
-  export let value: string | undefined = undefined;
+  interface Props {
+    id: string;
+    label: string;
+    checked?: boolean | undefined;
+    disabled?: boolean;
+    labelClass?: string | undefined;
+    name?: string | undefined;
+    value?: string | undefined;
+    onchange?: () => void;
+  }
+
+  let {
+    id,
+    label,
+    checked = $bindable(),
+    disabled = false,
+    labelClass = undefined,
+    name = undefined,
+    value = undefined,
+    onchange = () => {},
+  }: Props = $props();
 </script>
 
 <div class="flex items-center space-x-2">
@@ -17,7 +31,7 @@
     {disabled}
     class="size-5 flex-shrink-0 focus-visible:ring"
     bind:checked
-    on:change
+    {onchange}
   />
   <label class={labelClass} for={id}>{label}</label>
 </div>
diff --git a/web/src/lib/components/elements/date-input.svelte b/web/src/lib/components/elements/date-input.svelte
index f42fff4359..687e9442e7 100644
--- a/web/src/lib/components/elements/date-input.svelte
+++ b/web/src/lib/components/elements/date-input.svelte
@@ -1,29 +1,35 @@
 <script lang="ts">
-  import type { HTMLInputAttributes } from 'svelte/elements';
-
-  interface $$Props extends HTMLInputAttributes {
+  interface Props {
     type: 'date' | 'datetime-local';
+    value?: string;
+    min?: string;
+    max?: string;
+    class?: string;
+    id?: string;
+    name?: string;
+    placeholder?: string;
   }
 
-  export let type: $$Props['type'];
-  export let value: $$Props['value'] = undefined;
-  export let max: $$Props['max'] = undefined;
+  let { type, value = $bindable(), max = undefined, ...rest }: Props = $props();
 
-  $: fallbackMax = type === 'date' ? '9999-12-31' : '9999-12-31T23:59';
+  let fallbackMax = $derived(type === 'date' ? '9999-12-31' : '9999-12-31T23:59');
 
   // Updating `value` directly causes the date input to reset itself or
   // interfere with user changes.
-  $: updatedValue = value;
+  let updatedValue = $state<string>();
+  $effect(() => {
+    updatedValue = value;
+  });
 </script>
 
 <input
-  {...$$restProps}
+  {...rest}
   {type}
   {value}
   max={max || fallbackMax}
-  on:input={(e) => (updatedValue = e.currentTarget.value)}
-  on:blur={() => (value = updatedValue)}
-  on:keydown={(e) => {
+  oninput={(e) => (updatedValue = e.currentTarget.value)}
+  onblur={() => (value = updatedValue)}
+  onkeydown={(e) => {
     if (e.key === 'Enter') {
       value = updatedValue;
     }
diff --git a/web/src/lib/components/elements/dropdown.svelte b/web/src/lib/components/elements/dropdown.svelte
index d98fb517af..b146f347dc 100644
--- a/web/src/lib/components/elements/dropdown.svelte
+++ b/web/src/lib/components/elements/dropdown.svelte
@@ -1,4 +1,4 @@
-<script lang="ts" context="module">
+<script lang="ts" module>
   // Necessary for eslint
   /* eslint-disable @typescript-eslint/no-explicit-any */
   type T = any;
@@ -19,35 +19,43 @@
   import LinkButton from './buttons/link-button.svelte';
   import { clickOutside } from '$lib/actions/click-outside';
   import { fly } from 'svelte/transition';
-  import { createEventDispatcher } from 'svelte';
 
-  let className = '';
-  export { className as class };
+  interface Props {
+    class?: string;
+    options: T[];
+    selectedOption?: any;
+    showMenu?: boolean;
+    controlable?: boolean;
+    hideTextOnSmallScreen?: boolean;
+    title?: string | undefined;
+    onSelect: (option: T) => void;
+    onClickOutside?: () => void;
+    render?: (item: T) => string | RenderedOption;
+  }
 
-  const dispatch = createEventDispatcher<{
-    select: T;
-    'click-outside': void;
-  }>();
-
-  export let options: T[];
-  export let selectedOption = options[0];
-  export let showMenu = false;
-  export let controlable = false;
-  export let hideTextOnSmallScreen = true;
-  export let title: string | undefined = undefined;
-
-  export let render: (item: T) => string | RenderedOption = String;
+  let {
+    class: className = '',
+    options,
+    selectedOption = $bindable(options[0]),
+    showMenu = $bindable(false),
+    controlable = false,
+    hideTextOnSmallScreen = true,
+    title = undefined,
+    onSelect,
+    onClickOutside = () => {},
+    render = String,
+  }: Props = $props();
 
   const handleClickOutside = () => {
     if (!controlable) {
       showMenu = false;
     }
 
-    dispatch('click-outside');
+    onClickOutside();
   };
 
   const handleSelectOption = (option: T) => {
-    dispatch('select', option);
+    onSelect(option);
     selectedOption = option;
 
     showMenu = false;
@@ -69,12 +77,12 @@
     }
   };
 
-  $: renderedSelectedOption = renderOption(selectedOption);
+  let renderedSelectedOption = $derived(renderOption(selectedOption));
 </script>
 
 <div use:clickOutside={{ onOutclick: handleClickOutside, onEscape: handleClickOutside }}>
   <!-- BUTTON TITLE -->
-  <LinkButton on:click={() => (showMenu = true)} fullwidth {title}>
+  <LinkButton onclick={() => (showMenu = true)} fullwidth {title}>
     <div class="flex place-items-center gap-2 text-sm">
       {#if renderedSelectedOption?.icon}
         <Icon path={renderedSelectedOption.icon} size="18" />
@@ -96,7 +104,7 @@
           type="button"
           class="grid grid-cols-[36px,1fr] place-items-center p-2 disabled:opacity-40 {buttonStyle}"
           disabled={renderedOption.disabled}
-          on:click={() => !renderedOption.disabled && handleSelectOption(option)}
+          onclick={() => !renderedOption.disabled && handleSelectOption(option)}
         >
           {#if isEqual(selectedOption, option)}
             <div class="text-immich-primary dark:text-immich-dark-primary">
@@ -106,7 +114,7 @@
               {renderedOption.title}
             </p>
           {:else}
-            <div />
+            <div></div>
             <p class="justify-self-start">
               {renderedOption.title}
             </p>
diff --git a/web/src/lib/components/elements/group-tab.svelte b/web/src/lib/components/elements/group-tab.svelte
index f5e2f79350..021d5ca96f 100644
--- a/web/src/lib/components/elements/group-tab.svelte
+++ b/web/src/lib/components/elements/group-tab.svelte
@@ -1,10 +1,14 @@
 <script lang="ts">
   import { generateId } from '$lib/utils/generate-id';
 
-  export let filters: string[];
-  export let selected: string;
-  export let label: string;
-  export let onSelect: (selected: string) => void;
+  interface Props {
+    filters: string[];
+    selected: string;
+    label: string;
+    onSelect: (selected: string) => void;
+  }
+
+  let { filters, selected, label, onSelect }: Props = $props();
 
   const id = `group-tab-${generateId()}`;
 </script>
@@ -22,7 +26,7 @@
         class="peer sr-only"
         value={filter}
         checked={filter === selected}
-        on:change={() => onSelect(filter)}
+        onchange={() => onSelect(filter)}
       />
       <label
         for="{id}-{index}"
diff --git a/web/src/lib/components/elements/icon.svelte b/web/src/lib/components/elements/icon.svelte
index 5965928718..4bc55b3247 100644
--- a/web/src/lib/components/elements/icon.svelte
+++ b/web/src/lib/components/elements/icon.svelte
@@ -1,22 +1,41 @@
 <script lang="ts">
   import type { AriaRole } from 'svelte/elements';
 
-  export let size: string | number = '1em';
-  export let color = 'currentColor';
-  export let path: string;
-  export let title: string | null = null;
-  export let desc = '';
-  export let flipped = false;
-  let className = '';
-  export { className as class };
-  export let viewBox = '0 0 24 24';
-  export let role: AriaRole = 'img';
-  export let ariaHidden: boolean | undefined = undefined;
-  export let ariaLabel: string | undefined = undefined;
-  export let ariaLabelledby: string | undefined = undefined;
-  export let strokeWidth: number = 0;
-  export let strokeColor: string = 'currentColor';
-  export let spin = false;
+  interface Props {
+    size?: string | number;
+    color?: string;
+    path: string;
+    title?: string | null;
+    desc?: string;
+    flipped?: boolean;
+    class?: string;
+    viewBox?: string;
+    role?: AriaRole;
+    ariaHidden?: boolean | undefined;
+    ariaLabel?: string | undefined;
+    ariaLabelledby?: string | undefined;
+    strokeWidth?: number;
+    strokeColor?: string;
+    spin?: boolean;
+  }
+
+  let {
+    size = '1em',
+    color = 'currentColor',
+    path,
+    title = null,
+    desc = '',
+    flipped = false,
+    class: className = '',
+    viewBox = '0 0 24 24',
+    role = 'img',
+    ariaHidden = undefined,
+    ariaLabel = undefined,
+    ariaLabelledby = undefined,
+    strokeWidth = 0,
+    strokeColor = 'currentColor',
+    spin = false,
+  }: Props = $props();
 </script>
 
 <svg
diff --git a/web/src/lib/components/elements/radio-button.svelte b/web/src/lib/components/elements/radio-button.svelte
index a3c47e5fbc..1d110ff644 100644
--- a/web/src/lib/components/elements/radio-button.svelte
+++ b/web/src/lib/components/elements/radio-button.svelte
@@ -1,9 +1,13 @@
 <script lang="ts">
-  export let id: string;
-  export let label: string;
-  export let name: string;
-  export let value: string;
-  export let group: string | undefined = undefined;
+  interface Props {
+    id: string;
+    label: string;
+    name: string;
+    value: string;
+    group?: string | undefined;
+  }
+
+  let { id, label, name, value, group = $bindable(undefined) }: Props = $props();
 </script>
 
 <div class="flex items-center space-x-2">
diff --git a/web/src/lib/components/elements/search-bar.svelte b/web/src/lib/components/elements/search-bar.svelte
index 686e5691ed..c852be3b68 100644
--- a/web/src/lib/components/elements/search-bar.svelte
+++ b/web/src/lib/components/elements/search-bar.svelte
@@ -1,29 +1,39 @@
 <script lang="ts">
   import { mdiClose, mdiMagnify } from '@mdi/js';
-  import { createEventDispatcher } from 'svelte';
   import type { SearchOptions } from '$lib/utils/dipatch';
   import LoadingSpinner from '../shared-components/loading-spinner.svelte';
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import { t } from 'svelte-i18n';
 
-  export let name: string;
-  export let roundedBottom = true;
-  export let showLoadingSpinner: boolean;
-  export let placeholder: string;
+  interface Props {
+    name: string;
+    roundedBottom?: boolean;
+    showLoadingSpinner: boolean;
+    placeholder: string;
+    onSearch?: (options: SearchOptions) => void;
+    onReset?: () => void;
+  }
 
-  let inputRef: HTMLElement;
+  let {
+    name = $bindable(),
+    roundedBottom = true,
+    showLoadingSpinner,
+    placeholder,
+    onSearch = () => {},
+    onReset = () => {},
+  }: Props = $props();
 
-  const dispatch = createEventDispatcher<{ search: SearchOptions; reset: void }>();
+  let inputRef = $state<HTMLElement>();
 
   const resetSearch = () => {
     name = '';
-    dispatch('reset');
+    onReset();
     inputRef?.focus();
   };
 
   const handleSearch = (event: KeyboardEvent) => {
     if (event.key === 'Enter') {
-      dispatch('search', { force: true });
+      onSearch({ force: true });
     }
   };
 </script>
@@ -38,7 +48,7 @@
     title={$t('search')}
     size="16"
     padding="2"
-    on:click={() => dispatch('search', { force: true })}
+    onclick={() => onSearch({ force: true })}
   />
   <input
     class="w-full gap-2 bg-gray-200 dark:bg-immich-dark-gray dark:text-white"
@@ -46,8 +56,8 @@
     {placeholder}
     bind:value={name}
     bind:this={inputRef}
-    on:keydown={handleSearch}
-    on:input={() => dispatch('search', { force: false })}
+    onkeydown={handleSearch}
+    oninput={() => onSearch({ force: false })}
   />
   {#if showLoadingSpinner}
     <div class="flex place-items-center">
@@ -55,6 +65,6 @@
     </div>
   {/if}
   {#if name}
-    <CircleIconButton icon={mdiClose} title={$t('clear_value')} size="16" padding="2" on:click={resetSearch} />
+    <CircleIconButton icon={mdiClose} title={$t('clear_value')} size="16" padding="2" onclick={resetSearch} />
   {/if}
 </div>
diff --git a/web/src/lib/components/elements/slider.svelte b/web/src/lib/components/elements/slider.svelte
index 68b085fb91..5c80eb2a9e 100644
--- a/web/src/lib/components/elements/slider.svelte
+++ b/web/src/lib/components/elements/slider.svelte
@@ -1,19 +1,27 @@
 <script lang="ts">
-  import { createEventDispatcher } from 'svelte';
+  interface Props {
+    /**
+     * Unique identifier for the checkbox element, used to associate labels with the input element.
+     */
+    id: string;
+    /**
+     * Optional aria-describedby attribute to associate the checkbox with a description.
+     */
+    ariaDescribedBy?: string | undefined;
+    checked?: boolean;
+    disabled?: boolean;
+    onToggle?: ((checked: boolean) => void) | undefined;
+  }
 
-  /**
-   * Unique identifier for the checkbox element, used to associate labels with the input element.
-   */
-  export let id: string;
-  /**
-   * Optional aria-describedby attribute to associate the checkbox with a description.
-   */
-  export let ariaDescribedBy: string | undefined = undefined;
-  export let checked = false;
-  export let disabled = false;
+  let {
+    id,
+    ariaDescribedBy = undefined,
+    checked = $bindable(false),
+    disabled = false,
+    onToggle = undefined,
+  }: Props = $props();
 
-  const dispatch = createEventDispatcher<{ toggle: boolean }>();
-  const onToggle = (event: Event) => dispatch('toggle', (event.target as HTMLInputElement).checked);
+  const handleToggle = (event: Event) => onToggle?.((event.target as HTMLInputElement).checked);
 </script>
 
 <label class="relative inline-block h-[10px] w-[36px] flex-none">
@@ -22,7 +30,7 @@
     class="disabled::cursor-not-allowed h-0 w-0 opacity-0 peer"
     type="checkbox"
     bind:checked
-    on:click={onToggle}
+    onclick={handleToggle}
     {disabled}
     aria-describedby={ariaDescribedBy}
   />
@@ -30,11 +38,11 @@
   {#if disabled}
     <span
       class="slider slider-disabled cursor-not-allowed border border-transparent before:border before:border-transparent"
-    />
+    ></span>
   {:else}
     <span
       class="slider slider-enabled cursor-pointer border-2 border-transparent before:border-2 before:border-transparent peer-focus-visible:outline before:peer-focus-visible:outline peer-focus-visible:dark:outline-gray-200 before:peer-focus-visible:dark:outline-gray-200 peer-focus-visible:outline-gray-600 before:peer-focus-visible:outline-gray-600 peer-focus-visible:dark:border-black before:peer-focus-visible:dark:border-black peer-focus-visible:border-white before:peer-focus-visible:border-white"
-    />
+    ></span>
   {/if}
 </label>
 
diff --git a/web/src/lib/components/error.svelte b/web/src/lib/components/error.svelte
index cbc8c26bd8..54466b5a55 100644
--- a/web/src/lib/components/error.svelte
+++ b/web/src/lib/components/error.svelte
@@ -6,7 +6,11 @@
   import { mdiCodeTags, mdiContentCopy, mdiMessage, mdiPartyPopper } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  export let error: { message: string; code?: string | number; stack?: string } | undefined | null = undefined;
+  interface Props {
+    error?: { message: string; code?: string | number; stack?: string } | undefined | null;
+  }
+
+  let { error = undefined }: Props = $props();
 
   const handleCopy = async () => {
     if (!error) {
@@ -41,7 +45,7 @@
                 color="primary"
                 icon={mdiContentCopy}
                 title={$t('copy_error')}
-                on:click={() => handleCopy()}
+                onclick={() => handleCopy()}
               />
             </div>
           </div>
diff --git a/web/src/lib/components/faces-page/assign-face-side-panel.svelte b/web/src/lib/components/faces-page/assign-face-side-panel.svelte
index eba26e6e61..fe6a454307 100644
--- a/web/src/lib/components/faces-page/assign-face-side-panel.svelte
+++ b/web/src/lib/components/faces-page/assign-face-side-panel.svelte
@@ -1,10 +1,9 @@
 <script lang="ts">
   import { timeBeforeShowLoadingSpinner } from '$lib/constants';
   import { getPersonNameWithHiddenValue } from '$lib/utils/person';
-  import { getPeopleThumbnailUrl } from '$lib/utils';
-  import { AssetTypeEnum, type AssetFaceResponseDto, type PersonResponseDto } from '@immich/sdk';
+  import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
+  import { AssetTypeEnum, type AssetFaceResponseDto, type PersonResponseDto, getAllPeople } from '@immich/sdk';
   import { mdiArrowLeftThin, mdiClose, mdiMagnify, mdiPlus } from '@mdi/js';
-  import { createEventDispatcher } from 'svelte';
   import { linear } from 'svelte/easing';
   import { fly } from 'svelte/transition';
   import { photoViewer } from '$lib/stores/assets.store';
@@ -14,42 +13,62 @@
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import { zoomImageToBase64 } from '$lib/utils/people-utils';
   import { t } from 'svelte-i18n';
+  import { handleError } from '$lib/utils/handle-error';
+  import { onMount } from 'svelte';
 
-  export let allPeople: PersonResponseDto[];
-  export let editedFace: AssetFaceResponseDto;
-  export let assetId: string;
-  export let assetType: AssetTypeEnum;
+  interface Props {
+    editedFace: AssetFaceResponseDto;
+    assetId: string;
+    assetType: AssetTypeEnum;
+    onClose: () => void;
+    onCreatePerson: (featurePhoto: string | null) => void;
+    onReassign: (person: PersonResponseDto) => void;
+  }
+
+  let { editedFace, assetId, assetType, onClose, onCreatePerson, onReassign }: Props = $props();
+
+  let allPeople: PersonResponseDto[] = $state([]);
+
+  let isShowLoadingPeople = $state(false);
+
+  async function loadPeople() {
+    const timeout = setTimeout(() => (isShowLoadingPeople = true), timeBeforeShowLoadingSpinner);
+    try {
+      const { people } = await getAllPeople({ withHidden: true, closestAssetId: editedFace.id });
+      allPeople = people;
+    } catch (error) {
+      handleError(error, $t('errors.cant_get_faces'));
+    } finally {
+      clearTimeout(timeout);
+    }
+    isShowLoadingPeople = false;
+  }
 
   // loading spinners
-  let isShowLoadingNewPerson = false;
-  let isShowLoadingSearch = false;
+  let isShowLoadingNewPerson = $state(false);
+  let isShowLoadingSearch = $state(false);
 
   // search people
-  let searchedPeople: PersonResponseDto[] = [];
-  let searchFaces = false;
-  let searchName = '';
+  let searchedPeople: PersonResponseDto[] = $state([]);
+  let searchFaces = $state(false);
+  let searchName = $state('');
 
-  $: showPeople = searchName ? searchedPeople : allPeople.filter((person) => !person.isHidden);
+  let showPeople = $derived(searchName ? searchedPeople : allPeople.filter((person) => !person.isHidden));
 
-  const dispatch = createEventDispatcher<{
-    close: void;
-    createPerson: string | null;
-    reassign: PersonResponseDto;
-  }>();
-  const handleBackButton = () => {
-    dispatch('close');
-  };
+  onMount(() => {
+    handlePromiseError(loadPeople());
+  });
 
   const handleCreatePerson = async () => {
     const timeout = setTimeout(() => (isShowLoadingNewPerson = true), timeBeforeShowLoadingSpinner);
 
     const newFeaturePhoto = await zoomImageToBase64(editedFace, assetId, assetType, $photoViewer);
 
-    dispatch('createPerson', newFeaturePhoto);
+    onCreatePerson(newFeaturePhoto);
 
     clearTimeout(timeout);
     isShowLoadingNewPerson = false;
-    dispatch('createPerson', newFeaturePhoto);
+    onCreatePerson(newFeaturePhoto);
   };
 </script>
 
@@ -60,19 +79,19 @@
   <div class="flex place-items-center justify-between gap-2">
     {#if !searchFaces}
       <div class="flex items-center gap-2">
-        <CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={handleBackButton} />
+        <CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} onclick={onClose} />
         <p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">{$t('select_face')}</p>
       </div>
       <div class="flex justify-end gap-2">
         <CircleIconButton
           icon={mdiMagnify}
           title={$t('search_for_existing_person')}
-          on:click={() => {
+          onclick={() => {
             searchFaces = true;
           }}
         />
         {#if !isShowLoadingNewPerson}
-          <CircleIconButton icon={mdiPlus} title={$t('create_new_person')} on:click={handleCreatePerson} />
+          <CircleIconButton icon={mdiPlus} title={$t('create_new_person')} onclick={handleCreatePerson} />
         {:else}
           <div class="flex place-content-center place-items-center">
             <LoadingSpinner />
@@ -80,7 +99,7 @@
         {/if}
       </div>
     {:else}
-      <CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={handleBackButton} />
+      <CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} onclick={onClose} />
       <div class="w-full flex">
         <SearchPeople
           type="input"
@@ -94,36 +113,45 @@
           </div>
         {/if}
       </div>
-      <CircleIconButton icon={mdiClose} title={$t('cancel_search')} on:click={() => (searchFaces = false)} />
+      <CircleIconButton icon={mdiClose} title={$t('cancel_search')} onclick={() => (searchFaces = false)} />
     {/if}
   </div>
   <div class="px-4 py-4 text-sm">
     <h2 class="mb-8 mt-4 uppercase">{$t('all_people')}</h2>
-    <div class="immich-scrollbar mt-4 flex flex-wrap gap-2 overflow-y-auto">
-      {#each showPeople as person (person.id)}
-        {#if !editedFace.person || person.id !== editedFace.person.id}
-          <div class="w-fit">
-            <button type="button" class="w-[90px]" on:click={() => dispatch('reassign', person)}>
-              <div class="relative">
-                <ImageThumbnail
-                  curve
-                  shadow
-                  url={getPeopleThumbnailUrl(person)}
-                  altText={$getPersonNameWithHiddenValue(person.name, person.isHidden)}
-                  title={$getPersonNameWithHiddenValue(person.name, person.isHidden)}
-                  widthStyle="90px"
-                  heightStyle="90px"
-                  hidden={person.isHidden}
-                />
-              </div>
+    {#if isShowLoadingPeople}
+      <div class="flex w-full justify-center">
+        <LoadingSpinner />
+      </div>
+    {:else}
+      <div class="immich-scrollbar mt-4 flex flex-wrap gap-2 overflow-y-auto">
+        {#each showPeople as person (person.id)}
+          {#if !editedFace.person || person.id !== editedFace.person.id}
+            <div class="w-fit">
+              <button type="button" class="w-[90px]" onclick={() => onReassign(person)}>
+                <div class="relative">
+                  <ImageThumbnail
+                    curve
+                    shadow
+                    url={getPeopleThumbnailUrl(person)}
+                    altText={$getPersonNameWithHiddenValue(person.name, person.isHidden)}
+                    title={$getPersonNameWithHiddenValue(person.name, person.isHidden)}
+                    widthStyle="90px"
+                    heightStyle="90px"
+                    hidden={person.isHidden}
+                  />
+                </div>
 
-              <p class="mt-1 truncate font-medium" title={$getPersonNameWithHiddenValue(person.name, person.isHidden)}>
-                {person.name}
-              </p>
-            </button>
-          </div>
-        {/if}
-      {/each}
-    </div>
+                <p
+                  class="mt-1 truncate font-medium"
+                  title={$getPersonNameWithHiddenValue(person.name, person.isHidden)}
+                >
+                  {person.name}
+                </p>
+              </button>
+            </div>
+          {/if}
+        {/each}
+      </div>
+    {/if}
   </div>
 </section>
diff --git a/web/src/lib/components/faces-page/edit-name-input.svelte b/web/src/lib/components/faces-page/edit-name-input.svelte
index f48a3de15a..ebb44c4008 100644
--- a/web/src/lib/components/faces-page/edit-name-input.svelte
+++ b/web/src/lib/components/faces-page/edit-name-input.svelte
@@ -1,33 +1,41 @@
 <script lang="ts">
   import { type PersonResponseDto } from '@immich/sdk';
-  import { createEventDispatcher } from 'svelte';
   import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
   import Button from '../elements/buttons/button.svelte';
   import SearchPeople from '$lib/components/faces-page/people-search.svelte';
   import { t } from 'svelte-i18n';
 
-  export let person: PersonResponseDto;
-  export let name: string;
-  export let suggestedPeople: PersonResponseDto[];
-  export let thumbnailData: string;
-  export let isSearchingPeople: boolean;
+  interface Props {
+    person: PersonResponseDto;
+    name: string;
+    suggestedPeople: PersonResponseDto[];
+    thumbnailData: string;
+    isSearchingPeople: boolean;
+    onChange: (name: string) => void;
+  }
 
-  const dispatch = createEventDispatcher<{
-    change: string;
-  }>();
+  let {
+    person,
+    name = $bindable(),
+    suggestedPeople = $bindable(),
+    thumbnailData,
+    isSearchingPeople = $bindable(),
+    onChange,
+  }: Props = $props();
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+    onChange(name);
+  };
 </script>
 
 <div
   class="flex w-full h-14 place-items-center {suggestedPeople.length > 0
     ? 'rounded-t-lg dark:border-immich-dark-gray'
-    : 'rounded-lg'}  bg-gray-100 p-2 dark:bg-gray-700"
+    : 'rounded-lg'}  bg-gray-100 p-2 dark:bg-gray-700 border border-gray-200 dark:border-immich-dark-gray"
 >
   <ImageThumbnail circle shadow url={thumbnailData} altText={person.name} widthStyle="2rem" heightStyle="2rem" />
-  <form
-    class="ml-4 flex w-full justify-between gap-16"
-    autocomplete="off"
-    on:submit|preventDefault={() => dispatch('change', name)}
-  >
+  <form class="ml-4 flex w-full justify-between gap-16" autocomplete="off" {onsubmit}>
     <SearchPeople
       bind:searchName={name}
       bind:searchedPeopleLocal={suggestedPeople}
diff --git a/web/src/lib/components/faces-page/face-thumbnail.svelte b/web/src/lib/components/faces-page/face-thumbnail.svelte
index 58e1e0e39b..cc3fffe5d7 100644
--- a/web/src/lib/components/faces-page/face-thumbnail.svelte
+++ b/web/src/lib/components/faces-page/face-thumbnail.svelte
@@ -1,29 +1,33 @@
 <script lang="ts">
   import { getPeopleThumbnailUrl } from '$lib/utils';
   import { type PersonResponseDto } from '@immich/sdk';
-  import { createEventDispatcher } from 'svelte';
   import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
 
-  export let person: PersonResponseDto;
-  export let selectable = false;
-  export let selected = false;
-  export let thumbnailSize: number | null = null;
-  export let circle = false;
-  export let border = false;
+  interface Props {
+    person: PersonResponseDto;
+    selectable?: boolean;
+    selected?: boolean;
+    thumbnailSize?: number | null;
+    circle?: boolean;
+    border?: boolean;
+    onClick?: (person: PersonResponseDto) => void;
+  }
 
-  let dispatch = createEventDispatcher<{
-    click: PersonResponseDto;
-  }>();
-
-  const handleOnClicked = () => {
-    dispatch('click', person);
-  };
+  let {
+    person,
+    selectable = false,
+    selected = false,
+    thumbnailSize = null,
+    circle = false,
+    border = false,
+    onClick = () => {},
+  }: Props = $props();
 </script>
 
 <button
   type="button"
   class="relative rounded-lg transition-all"
-  on:click={handleOnClicked}
+  onclick={() => onClick(person)}
   disabled={!selectable}
   style:width={thumbnailSize ? thumbnailSize + 'px' : '100%'}
   style:height={thumbnailSize ? thumbnailSize + 'px' : '100%'}
@@ -44,14 +48,14 @@
     class:hover:opacity-100={selectable}
     class:rounded-full={circle}
     class:rounded-lg={!circle}
-  />
+  ></div>
 
   {#if selected}
     <div
       class="absolute left-0 top-0 h-full w-full bg-blue-500/80"
       class:rounded-full={circle}
       class:rounded-lg={!circle}
-    />
+    ></div>
   {/if}
 
   {#if person.name}
diff --git a/web/src/lib/components/faces-page/manage-people-visibility.svelte b/web/src/lib/components/faces-page/manage-people-visibility.svelte
index 23a69e7759..196fe13900 100644
--- a/web/src/lib/components/faces-page/manage-people-visibility.svelte
+++ b/web/src/lib/components/faces-page/manage-people-visibility.svelte
@@ -1,11 +1,3 @@
-<script lang="ts" context="module">
-  const enum ToggleVisibility {
-    HIDE_ALL = 'hide-all',
-    HIDE_UNNANEMD = 'hide-unnamed',
-    SHOW_ALL = 'show-all',
-  }
-</script>
-
 <script lang="ts">
   import { shortcut } from '$lib/actions/shortcut';
   import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
@@ -23,18 +15,20 @@
   import { mdiClose, mdiEye, mdiEyeOff, mdiEyeSettings, mdiRestart } from '@mdi/js';
   import { t } from 'svelte-i18n';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
+  import { ToggleVisibility } from '$lib/constants';
 
-  export let people: PersonResponseDto[];
-  export let totalPeopleCount: number;
-  export let titleId: string | undefined = undefined;
-  export let onClose: () => void;
-  export let loadNextPage: () => void;
+  interface Props {
+    people: PersonResponseDto[];
+    totalPeopleCount: number;
+    titleId?: string | undefined;
+    onClose: () => void;
+    loadNextPage: () => void;
+  }
 
-  let toggleVisibility = ToggleVisibility.SHOW_ALL;
-  let showLoadingSpinner = false;
+  let { people = $bindable(), totalPeopleCount, titleId = undefined, onClose, loadNextPage }: Props = $props();
 
-  $: personIsHidden = getPersonIsHidden(people);
-  $: toggleButton = toggleButtonOptions[getNextVisibility(toggleVisibility)];
+  let toggleVisibility = $state(ToggleVisibility.SHOW_ALL);
+  let showLoadingSpinner = $state(false);
 
   const getPersonIsHidden = (people: PersonResponseDto[]) => {
     const personIsHidden: Record<string, boolean> = {};
@@ -44,14 +38,6 @@
     return personIsHidden;
   };
 
-  $: toggleButtonOptions = ((): Record<ToggleVisibility, { icon: string; label: string }> => {
-    return {
-      [ToggleVisibility.HIDE_ALL]: { icon: mdiEyeOff, label: $t('hide_all_people') },
-      [ToggleVisibility.HIDE_UNNANEMD]: { icon: mdiEyeSettings, label: $t('hide_unnamed_people') },
-      [ToggleVisibility.SHOW_ALL]: { icon: mdiEye, label: $t('show_all_people') },
-    };
-  })();
-
   const getNextVisibility = (toggleVisibility: ToggleVisibility) => {
     if (toggleVisibility === ToggleVisibility.SHOW_ALL) {
       return ToggleVisibility.HIDE_UNNANEMD;
@@ -104,7 +90,6 @@
       for (const person of people) {
         person.isHidden = personIsHidden[person.id];
       }
-      people = people;
 
       onClose();
     } catch (error) {
@@ -113,6 +98,15 @@
       showLoadingSpinner = false;
     }
   };
+
+  let personIsHidden = $state(getPersonIsHidden(people));
+
+  let toggleButtonOptions: Record<ToggleVisibility, { icon: string; label: string }> = $derived({
+    [ToggleVisibility.HIDE_ALL]: { icon: mdiEyeOff, label: $t('hide_all_people') },
+    [ToggleVisibility.HIDE_UNNANEMD]: { icon: mdiEyeSettings, label: $t('hide_unnamed_people') },
+    [ToggleVisibility.SHOW_ALL]: { icon: mdiEye, label: $t('show_all_people') },
+  });
+  let toggleButton = $derived(toggleButtonOptions[getNextVisibility(toggleVisibility)]);
 </script>
 
 <svelte:window use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onClose }} />
@@ -121,7 +115,7 @@
   class="fixed top-0 z-10 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8"
 >
   <div class="flex items-center">
-    <CircleIconButton title={$t('close')} icon={mdiClose} on:click={onClose} />
+    <CircleIconButton title={$t('close')} icon={mdiClose} onclick={onClose} />
     <div class="flex gap-2 items-center">
       <p id={titleId} class="ml-2">{$t('show_and_hide_people')}</p>
       <p class="text-sm text-gray-400 dark:text-gray-600">({totalPeopleCount.toLocaleString($locale)})</p>
@@ -129,11 +123,11 @@
   </div>
   <div class="flex items-center justify-end">
     <div class="flex items-center md:mr-4">
-      <CircleIconButton title={$t('reset_people_visibility')} icon={mdiRestart} on:click={handleResetVisibility} />
-      <CircleIconButton title={toggleButton.label} icon={toggleButton.icon} on:click={handleToggleVisibility} />
+      <CircleIconButton title={$t('reset_people_visibility')} icon={mdiRestart} onclick={handleResetVisibility} />
+      <CircleIconButton title={toggleButton.label} icon={toggleButton.icon} onclick={handleToggleVisibility} />
     </div>
     {#if !showLoadingSpinner}
-      <Button on:click={handleSaveVisibility} size="sm" rounded="lg">{$t('done')}</Button>
+      <Button onclick={handleSaveVisibility} size="sm" rounded="lg">{$t('done')}</Button>
     {:else}
       <LoadingSpinner />
     {/if}
@@ -141,29 +135,31 @@
 </div>
 
 <div class="flex flex-wrap gap-1 bg-immich-bg p-2 pb-8 dark:bg-immich-dark-bg md:px-8 mt-16">
-  <PeopleInfiniteScroll {people} hasNextPage={true} {loadNextPage} let:person let:index>
-    {@const hidden = personIsHidden[person.id]}
-    <button
-      type="button"
-      class="group relative"
-      on:click={() => (personIsHidden[person.id] = !hidden)}
-      aria-pressed={hidden}
-      aria-label={person.name ? $t('hide_named_person', { values: { name: person.name } }) : $t('hide_person')}
-    >
-      <ImageThumbnail
-        preload={index < 20}
-        {hidden}
-        shadow
-        url={getPeopleThumbnailUrl(person)}
-        altText={person.name}
-        widthStyle="100%"
-        hiddenIconClass="text-white group-hover:text-black transition-colors"
-      />
-      {#if person.name}
-        <span class="absolute bottom-2 left-0 w-full select-text px-1 text-center font-medium text-white">
-          {person.name}
-        </span>
-      {/if}
-    </button>
+  <PeopleInfiniteScroll {people} hasNextPage={true} {loadNextPage}>
+    {#snippet children({ person, index })}
+      {@const hidden = personIsHidden[person.id]}
+      <button
+        type="button"
+        class="group relative w-full h-full"
+        onclick={() => (personIsHidden[person.id] = !hidden)}
+        aria-pressed={hidden}
+        aria-label={person.name ? $t('hide_named_person', { values: { name: person.name } }) : $t('hide_person')}
+      >
+        <ImageThumbnail
+          preload={index < 20}
+          {hidden}
+          shadow
+          url={getPeopleThumbnailUrl(person)}
+          altText={person.name}
+          widthStyle="100%"
+          hiddenIconClass="text-white group-hover:text-black transition-colors"
+        />
+        {#if person.name}
+          <span class="absolute bottom-2 left-0 w-full select-text px-1 text-center font-medium text-white">
+            {person.name}
+          </span>
+        {/if}
+      </button>
+    {/snippet}
   </PeopleInfiniteScroll>
 </div>
diff --git a/web/src/lib/components/faces-page/merge-face-selector.svelte b/web/src/lib/components/faces-page/merge-face-selector.svelte
index 75f3420424..0e68ebcfcb 100644
--- a/web/src/lib/components/faces-page/merge-face-selector.svelte
+++ b/web/src/lib/components/faces-page/merge-face-selector.svelte
@@ -1,12 +1,12 @@
 <script lang="ts">
   import { goto } from '$app/navigation';
-  import { page } from '$app/stores';
+  import { page } from '$app/state';
   import Icon from '$lib/components/elements/icon.svelte';
   import { ActionQueryParameterValue, AppRoute, QueryParameter } from '$lib/constants';
   import { handleError } from '$lib/utils/handle-error';
   import { getAllPeople, getPerson, mergePerson, type PersonResponseDto } from '@immich/sdk';
   import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js';
-  import { createEventDispatcher, onMount } from 'svelte';
+  import { onMount } from 'svelte';
   import { flip } from 'svelte/animate';
   import { quintOut } from 'svelte/easing';
   import { fly } from 'svelte/transition';
@@ -19,32 +19,32 @@
   import { dialogController } from '$lib/components/shared-components/dialog/dialog';
   import { t } from 'svelte-i18n';
 
-  export let person: PersonResponseDto;
-  let people: PersonResponseDto[] = [];
-  let selectedPeople: PersonResponseDto[] = [];
-  let screenHeight: number;
+  interface Props {
+    person: PersonResponseDto;
+    onBack: () => void;
+    onMerge: (mergedPerson: PersonResponseDto) => void;
+  }
 
-  let dispatch = createEventDispatcher<{
-    back: void;
-    merge: PersonResponseDto;
-  }>();
+  let { person = $bindable(), onBack, onMerge }: Props = $props();
 
-  $: hasSelection = selectedPeople.length > 0;
-  $: peopleToNotShow = [...selectedPeople, person];
+  let people: PersonResponseDto[] = $state([]);
+  let selectedPeople: PersonResponseDto[] = $state([]);
+  let screenHeight: number = $state(0);
 
-  onMount(async () => {
-    const data = await getAllPeople({ withHidden: false });
+  let hasSelection = $derived(selectedPeople.length > 0);
+  let peopleToNotShow = $derived([...selectedPeople, person]);
+
+  const handleSearch = async (sortFaces: boolean = false) => {
+    const data = await getAllPeople({ withHidden: false, closestPersonId: sortFaces ? person.id : undefined });
     people = data.people;
-  });
-
-  const onClose = () => {
-    dispatch('back');
   };
 
+  onMount(handleSearch);
+
   const handleSwapPeople = async () => {
     [person, selectedPeople[0]] = [selectedPeople[0], person];
-    $page.url.searchParams.set(QueryParameter.ACTION, ActionQueryParameterValue.MERGE);
-    await goto(`${AppRoute.PEOPLE}/${person.id}?${$page.url.searchParams.toString()}`);
+    page.url.searchParams.set(QueryParameter.ACTION, ActionQueryParameterValue.MERGE);
+    await goto(`${AppRoute.PEOPLE}/${person.id}?${page.url.searchParams.toString()}`);
   };
 
   const onSelect = async (selected: PersonResponseDto) => {
@@ -88,7 +88,7 @@
         message: $t('merged_people_count', { values: { count } }),
         type: NotificationType.Info,
       });
-      dispatch('merge', mergedPerson);
+      onMerge(mergedPerson);
     } catch (error) {
       handleError(error, $t('cannot_merge_people'));
     }
@@ -101,21 +101,21 @@
   transition:fly={{ y: 500, duration: 100, easing: quintOut }}
   class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
 >
-  <ControlAppBar on:close={onClose}>
-    <svelte:fragment slot="leading">
+  <ControlAppBar onClose={onBack}>
+    {#snippet leading()}
       {#if hasSelection}
         {$t('selected_count', { values: { count: selectedPeople.length } })}
       {:else}
         {$t('merge_people')}
       {/if}
-      <div />
-    </svelte:fragment>
-    <svelte:fragment slot="trailing">
-      <Button size={'sm'} disabled={!hasSelection} on:click={handleMerge}>
+      <div></div>
+    {/snippet}
+    {#snippet trailing()}
+      <Button size={'sm'} disabled={!hasSelection} onclick={handleMerge}>
         <Icon path={mdiMerge} size={18} />
         <span class="ml-2">{$t('merge')}</span></Button
       >
-    </svelte:fragment>
+    {/snippet}
   </ControlAppBar>
   <section class="bg-immich-bg px-[70px] pt-[100px] dark:bg-immich-dark-bg">
     <section id="merge-face-selector relative">
@@ -125,7 +125,7 @@
         <div class="grid grid-flow-col-dense place-content-center place-items-center gap-4">
           {#each selectedPeople as person (person.id)}
             <div animate:flip={{ duration: 250, easing: quintOut }}>
-              <FaceThumbnail border circle {person} selectable thumbnailSize={120} on:click={() => onSelect(person)} />
+              <FaceThumbnail border circle {person} selectable thumbnailSize={120} onClick={() => onSelect(person)} />
             </div>
           {/each}
 
@@ -141,7 +141,7 @@
                       title={$t('swap_merge_direction')}
                       icon={mdiSwapHorizontal}
                       size="24"
-                      on:click={handleSwapPeople}
+                      onclick={handleSwapPeople}
                     />
                   </div>
                 {/if}
@@ -151,8 +151,7 @@
           <FaceThumbnail {person} border circle selectable={false} thumbnailSize={180} />
         </div>
       </div>
-
-      <PeopleList {people} {peopleToNotShow} {screenHeight} on:select={({ detail }) => onSelect(detail)} />
+      <PeopleList {people} {peopleToNotShow} {screenHeight} {onSelect} {handleSearch} />
     </section>
   </section>
 </section>
diff --git a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte
index d781e1cc56..a4ac76f198 100644
--- a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte
+++ b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte
@@ -4,34 +4,41 @@
   import { getPeopleThumbnailUrl } from '$lib/utils';
   import { type PersonResponseDto } from '@immich/sdk';
   import { mdiArrowLeft, mdiMerge } from '@mdi/js';
-  import { createEventDispatcher } from 'svelte';
   import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
   import Button from '../elements/buttons/button.svelte';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
   import { t } from 'svelte-i18n';
 
-  export let personMerge1: PersonResponseDto;
-  export let personMerge2: PersonResponseDto;
-  export let potentialMergePeople: PersonResponseDto[];
+  interface Props {
+    personMerge1: PersonResponseDto;
+    personMerge2: PersonResponseDto;
+    potentialMergePeople: PersonResponseDto[];
+    onReject: () => void;
+    onConfirm: ([personMerge1, personMerge2]: [PersonResponseDto, PersonResponseDto]) => void;
+    onClose: () => void;
+  }
 
-  let choosePersonToMerge = false;
+  let {
+    personMerge1 = $bindable(),
+    personMerge2 = $bindable(),
+    potentialMergePeople = $bindable(),
+    onReject,
+    onConfirm,
+    onClose,
+  }: Props = $props();
+
+  let choosePersonToMerge = $state(false);
 
   const title = personMerge2.name;
 
-  const dispatch = createEventDispatcher<{
-    reject: void;
-    confirm: [PersonResponseDto, PersonResponseDto];
-    close: void;
-  }>();
-
-  const changePersonToMerge = (newperson: PersonResponseDto) => {
-    const index = potentialMergePeople.indexOf(newperson);
+  const changePersonToMerge = (newPerson: PersonResponseDto) => {
+    const index = potentialMergePeople.indexOf(newPerson);
     [potentialMergePeople[index], personMerge2] = [personMerge2, potentialMergePeople[index]];
     choosePersonToMerge = false;
   };
 </script>
 
-<FullScreenModal title="{$t('merge_people')} - {title}" onClose={() => dispatch('close')}>
+<FullScreenModal title="{$t('merge_people')} - {title}" {onClose}>
   <div class="flex items-center justify-center py-4 md:h-36 md:py-4">
     {#if !choosePersonToMerge}
       <div class="flex h-20 w-20 items-center px-1 md:h-24 md:w-24 md:px-2">
@@ -47,7 +54,7 @@
         <CircleIconButton
           title={$t('swap_merge_direction')}
           icon={mdiMerge}
-          on:click={() => ([personMerge1, personMerge2] = [personMerge2, personMerge1])}
+          onclick={() => ([personMerge1, personMerge2] = [personMerge2, personMerge1])}
         />
       </div>
 
@@ -55,7 +62,7 @@
         type="button"
         disabled={potentialMergePeople.length === 0}
         class="flex h-28 w-28 items-center rounded-full border-2 border-immich-primary px-1 dark:border-immich-dark-primary md:h-32 md:w-32 md:px-2"
-        on:click={() => {
+        onclick={() => {
           if (potentialMergePeople.length > 0) {
             choosePersonToMerge = !choosePersonToMerge;
           }
@@ -73,13 +80,13 @@
     {:else}
       <div class="grid w-full grid-cols-1 gap-2">
         <div class="px-2">
-          <button type="button" on:click={() => (choosePersonToMerge = false)}> <Icon path={mdiArrowLeft} /></button>
+          <button type="button" onclick={() => (choosePersonToMerge = false)}> <Icon path={mdiArrowLeft} /></button>
         </div>
         <div class="flex items-center justify-center">
           <div class="flex flex-wrap justify-center md:grid md:grid-cols-{potentialMergePeople.length}">
             {#each potentialMergePeople as person (person.id)}
               <div class="h-24 w-24 md:h-28 md:w-28">
-                <button type="button" class="p-2 w-full" on:click={() => changePersonToMerge(person)}>
+                <button type="button" class="p-2 w-full" onclick={() => changePersonToMerge(person)}>
                   <ImageThumbnail
                     border={true}
                     circle
@@ -87,7 +94,7 @@
                     url={getPeopleThumbnailUrl(person)}
                     altText={person.name}
                     widthStyle="100%"
-                    on:click={() => changePersonToMerge(person)}
+                    onClick={() => changePersonToMerge(person)}
                   />
                 </button>
               </div>
@@ -104,8 +111,9 @@
   <div class="flex px-4 pt-2">
     <p class="text-sm text-gray-500 dark:text-gray-300">{$t('they_will_be_merged_together')}</p>
   </div>
-  <svelte:fragment slot="sticky-bottom">
-    <Button fullwidth color="gray" on:click={() => dispatch('reject')}>{$t('no')}</Button>
-    <Button fullwidth on:click={() => dispatch('confirm', [personMerge1, personMerge2])}>{$t('yes')}</Button>
-  </svelte:fragment>
+
+  {#snippet stickyBottom()}
+    <Button fullwidth color="gray" onclick={onReject}>{$t('no')}</Button>
+    <Button fullwidth onclick={() => onConfirm([personMerge1, personMerge2])}>{$t('yes')}</Button>
+  {/snippet}
 </FullScreenModal>
diff --git a/web/src/lib/components/faces-page/people-card.svelte b/web/src/lib/components/faces-page/people-card.svelte
index 21f48e42eb..a83d1180f9 100644
--- a/web/src/lib/components/faces-page/people-card.svelte
+++ b/web/src/lib/components/faces-page/people-card.svelte
@@ -9,42 +9,38 @@
     mdiDotsVertical,
     mdiEyeOffOutline,
   } from '@mdi/js';
-  import { createEventDispatcher } from 'svelte';
   import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
   import MenuOption from '../shared-components/context-menu/menu-option.svelte';
   import { t } from 'svelte-i18n';
   import { focusOutside } from '$lib/actions/focus-outside';
   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
 
-  export let person: PersonResponseDto;
-  export let preload = false;
+  interface Props {
+    person: PersonResponseDto;
+    preload?: boolean;
+    onChangeName: () => void;
+    onSetBirthDate: () => void;
+    onMergePeople: () => void;
+    onHidePerson: () => void;
+  }
 
-  type MenuItemEvent = 'change-name' | 'set-birth-date' | 'merge-people' | 'hide-person';
-  let dispatch = createEventDispatcher<{
-    'change-name': void;
-    'set-birth-date': void;
-    'merge-people': void;
-    'hide-person': void;
-  }>();
+  let { person, preload = false, onChangeName, onSetBirthDate, onMergePeople, onHidePerson }: Props = $props();
 
-  let showVerticalDots = false;
-  const onMenuClick = (event: MenuItemEvent) => {
-    dispatch(event);
-  };
+  let showVerticalDots = $state(false);
 </script>
 
 <div
   id="people-card"
   class="relative"
-  on:mouseenter={() => (showVerticalDots = true)}
-  on:mouseleave={() => (showVerticalDots = false)}
+  onmouseenter={() => (showVerticalDots = true)}
+  onmouseleave={() => (showVerticalDots = false)}
   role="group"
   use:focusOutside={{ onFocusOut: () => (showVerticalDots = false) }}
 >
   <a
     href="{AppRoute.PEOPLE}/{person.id}?{QueryParameter.PREVIOUS_ROUTE}={AppRoute.PEOPLE}"
     draggable="false"
-    on:focus={() => (showVerticalDots = true)}
+    onfocus={() => (showVerticalDots = true)}
   >
     <div class="w-full h-full rounded-xl brightness-95 filter">
       <ImageThumbnail
@@ -76,18 +72,10 @@
         icon={mdiDotsVertical}
         title={$t('show_person_options')}
       >
-        <MenuOption onClick={() => onMenuClick('hide-person')} icon={mdiEyeOffOutline} text={$t('hide_person')} />
-        <MenuOption onClick={() => onMenuClick('change-name')} icon={mdiAccountEditOutline} text={$t('change_name')} />
-        <MenuOption
-          onClick={() => onMenuClick('set-birth-date')}
-          icon={mdiCalendarEditOutline}
-          text={$t('set_date_of_birth')}
-        />
-        <MenuOption
-          onClick={() => onMenuClick('merge-people')}
-          icon={mdiAccountMultipleCheckOutline}
-          text={$t('merge_people')}
-        />
+        <MenuOption onClick={onHidePerson} icon={mdiEyeOffOutline} text={$t('hide_person')} />
+        <MenuOption onClick={onChangeName} icon={mdiAccountEditOutline} text={$t('change_name')} />
+        <MenuOption onClick={onSetBirthDate} icon={mdiCalendarEditOutline} text={$t('set_date_of_birth')} />
+        <MenuOption onClick={onMergePeople} icon={mdiAccountMultipleCheckOutline} text={$t('merge_people')} />
       </ButtonContextMenu>
     </div>
   {/if}
diff --git a/web/src/lib/components/faces-page/people-infinite-scroll.svelte b/web/src/lib/components/faces-page/people-infinite-scroll.svelte
index aefd6fe957..0de084c4b2 100644
--- a/web/src/lib/components/faces-page/people-infinite-scroll.svelte
+++ b/web/src/lib/components/faces-page/people-infinite-scroll.svelte
@@ -1,11 +1,16 @@
 <script lang="ts">
   import type { PersonResponseDto } from '@immich/sdk';
 
-  export let people: PersonResponseDto[];
-  export let hasNextPage: boolean | undefined = undefined;
-  export let loadNextPage: () => void;
+  interface Props {
+    people: PersonResponseDto[];
+    hasNextPage?: boolean | undefined;
+    loadNextPage: () => void;
+    children?: import('svelte').Snippet<[{ person: PersonResponseDto; index: number }]>;
+  }
 
-  let lastPersonContainer: HTMLElement | undefined;
+  let { people, hasNextPage = undefined, loadNextPage, children }: Props = $props();
+
+  let lastPersonContainer: HTMLElement | undefined = $state();
 
   const intersectionObserver = new IntersectionObserver((entries) => {
     const entry = entries.find((entry) => entry.target === lastPersonContainer);
@@ -14,20 +19,22 @@
     }
   });
 
-  $: if (lastPersonContainer) {
-    intersectionObserver.disconnect();
-    intersectionObserver.observe(lastPersonContainer);
-  }
+  $effect(() => {
+    if (lastPersonContainer) {
+      intersectionObserver.disconnect();
+      intersectionObserver.observe(lastPersonContainer);
+    }
+  });
 </script>
 
 <div class="w-full grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-9 gap-1">
   {#each people as person, index (person.id)}
     {#if hasNextPage && index === people.length - 1}
       <div bind:this={lastPersonContainer}>
-        <slot {person} {index} />
+        {@render children?.({ person, index })}
       </div>
     {:else}
-      <slot {person} {index} />
+      {@render children?.({ person, index })}
     {/if}
   {/each}
 </div>
diff --git a/web/src/lib/components/faces-page/people-list.svelte b/web/src/lib/components/faces-page/people-list.svelte
index 5130baf30b..1c1eee39ec 100644
--- a/web/src/lib/components/faces-page/people-list.svelte
+++ b/web/src/lib/components/faces-page/people-list.svelte
@@ -1,49 +1,56 @@
 <script lang="ts">
   import { type PersonResponseDto } from '@immich/sdk';
-  import { createEventDispatcher } from 'svelte';
   import FaceThumbnail from './face-thumbnail.svelte';
   import SearchPeople from '$lib/components/faces-page/people-search.svelte';
   import { t } from 'svelte-i18n';
+  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
+  import { mdiSwapVertical } from '@mdi/js';
 
-  export let screenHeight: number;
-  export let people: PersonResponseDto[];
-  export let peopleToNotShow: PersonResponseDto[];
-  let searchedPeopleLocal: PersonResponseDto[] = [];
-
-  let name = '';
-  let showPeople: PersonResponseDto[];
-
-  let dispatch = createEventDispatcher<{
-    select: PersonResponseDto;
-  }>();
-
-  $: {
-    showPeople = name ? searchedPeopleLocal : people;
-    showPeople = showPeople.filter(
-      (person) => !peopleToNotShow.some((unselectedPerson) => unselectedPerson.id === person.id),
-    );
+  interface Props {
+    screenHeight: number;
+    people: PersonResponseDto[];
+    peopleToNotShow: PersonResponseDto[];
+    onSelect: (person: PersonResponseDto) => void;
+    handleSearch?: (sortFaces: boolean) => void;
   }
+
+  let { screenHeight, people, peopleToNotShow, onSelect, handleSearch }: Props = $props();
+  let searchedPeopleLocal: PersonResponseDto[] = $state([]);
+  let sortBySimilarirty = $state(false);
+  let name = $state('');
+
+  const showPeople = $derived(
+    (name ? searchedPeopleLocal : people).filter(
+      (person) => !peopleToNotShow.some((unselectedPerson) => unselectedPerson.id === person.id),
+    ),
+  );
 </script>
 
-<div class=" w-40 sm:w-48 md:w-96 h-14 mb-8">
-  <SearchPeople type="searchBar" placeholder={$t('search_people')} bind:searchName={name} bind:searchedPeopleLocal />
+<div class="w-40 sm:w-48 md:w-full h-14 flex gap-4 place-items-center">
+  <div class="md:w-96">
+    <SearchPeople type="searchBar" placeholder={$t('search_people')} bind:searchName={name} bind:searchedPeopleLocal />
+  </div>
+
+  {#if handleSearch}
+    <CircleIconButton
+      icon={mdiSwapVertical}
+      onclick={() => {
+        sortBySimilarirty = !sortBySimilarirty;
+        handleSearch(sortBySimilarirty);
+      }}
+      color="neutral"
+      title={$t('sort_people_by_similarity')}
+    ></CircleIconButton>
+  {/if}
 </div>
 
 <div
-  class="immich-scrollbar overflow-y-auto rounded-3xl bg-gray-200 p-10 dark:bg-immich-dark-gray"
+  class="immich-scrollbar overflow-y-auto rounded-3xl bg-gray-200 p-10 dark:bg-immich-dark-gray mt-6"
   style:max-height={screenHeight - 400 + 'px'}
 >
   <div class="grid-col-2 grid gap-8 md:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10">
     {#each showPeople as person (person.id)}
-      <FaceThumbnail
-        {person}
-        on:click={() => {
-          dispatch('select', person);
-        }}
-        circle
-        border
-        selectable
-      />
+      <FaceThumbnail {person} onClick={() => onSelect(person)} circle border selectable />
     {/each}
   </div>
 </div>
diff --git a/web/src/lib/components/faces-page/people-search.svelte b/web/src/lib/components/faces-page/people-search.svelte
index cfd4c8f29a..835f4188c4 100644
--- a/web/src/lib/components/faces-page/people-search.svelte
+++ b/web/src/lib/components/faces-page/people-search.svelte
@@ -7,16 +7,6 @@
   import { searchPerson, type PersonResponseDto } from '@immich/sdk';
   import { t } from 'svelte-i18n';
 
-  export let searchName: string;
-  export let searchedPeopleLocal: PersonResponseDto[];
-  export let type: 'searchBar' | 'input';
-  export let numberPeopleToSearch: number = maximumLengthSearchPeople;
-  export let inputClass: string = 'w-full gap-2 bg-immich-bg dark:bg-immich-dark-bg';
-  export let showLoadingSpinner: boolean = false;
-  export let placeholder: string = $t('name_or_nickname');
-  export let onReset = () => {};
-  export let onSearch = () => {};
-
   let searchedPeople: PersonResponseDto[] = [];
   let searchWord: string;
   let abortController: AbortController | null = null;
@@ -43,7 +33,36 @@
     }
   };
 
-  export let handleSearch = async (force?: boolean, name?: string) => {
+  interface Props {
+    searchName: string;
+    searchedPeopleLocal: PersonResponseDto[];
+    type: 'searchBar' | 'input';
+    numberPeopleToSearch?: number;
+    inputClass?: string;
+    showLoadingSpinner?: boolean;
+    placeholder?: string;
+    onReset?: () => void;
+    onSearch?: () => void;
+  }
+
+  let {
+    searchName = $bindable(),
+    searchedPeopleLocal = $bindable(),
+    type,
+    numberPeopleToSearch = maximumLengthSearchPeople,
+    inputClass = 'w-full gap-2 bg-immich-bg dark:bg-immich-dark-bg',
+    showLoadingSpinner = $bindable(false),
+    placeholder = $t('name_or_nickname'),
+    onReset = () => {},
+    onSearch = () => {},
+  }: Props = $props();
+
+  const handleReset = () => {
+    reset();
+    onReset();
+  };
+
+  export async function searchPeople(force?: boolean, name?: string) {
     searchName = name ?? searchName;
     onSearch();
     if (searchName === '') {
@@ -70,12 +89,7 @@
       showLoadingSpinner = false;
       search();
     }
-  };
-
-  const handleReset = () => {
-    reset();
-    onReset();
-  };
+  }
 </script>
 
 {#if type === 'searchBar'}
@@ -83,8 +97,8 @@
     bind:name={searchName}
     {showLoadingSpinner}
     {placeholder}
-    on:reset={handleReset}
-    on:search={({ detail }) => handleSearch(detail.force ?? false)}
+    onReset={handleReset}
+    onSearch={({ force }) => searchPeople(force ?? false)}
   />
 {:else}
   <input
@@ -92,7 +106,7 @@
     type="text"
     {placeholder}
     bind:value={searchName}
-    on:input={() => handleSearch(false)}
+    oninput={() => searchPeople(false)}
     use:initInput
   />
 {/if}
diff --git a/web/src/lib/components/faces-page/person-side-panel.svelte b/web/src/lib/components/faces-page/person-side-panel.svelte
index fd4fbdf964..f2bab9996a 100644
--- a/web/src/lib/components/faces-page/person-side-panel.svelte
+++ b/web/src/lib/components/faces-page/person-side-panel.svelte
@@ -8,17 +8,15 @@
   import { getPersonNameWithHiddenValue } from '$lib/utils/person';
   import {
     createPerson,
-    getAllPeople,
     getFaces,
     reassignFacesById,
     AssetTypeEnum,
     type AssetFaceResponseDto,
     type PersonResponseDto,
   } from '@immich/sdk';
-  import { mdiAccountOff } from '@mdi/js';
   import Icon from '$lib/components/elements/icon.svelte';
-  import { mdiArrowLeftThin, mdiMinus, mdiRestart } from '@mdi/js';
-  import { createEventDispatcher, onMount } from 'svelte';
+  import { mdiAccountOff, mdiArrowLeftThin, mdiPencil, mdiRestart } from '@mdi/js';
+  import { onMount } from 'svelte';
   import { linear } from 'svelte/easing';
   import { fly } from 'svelte/transition';
   import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
@@ -29,26 +27,31 @@
   import { photoViewer } from '$lib/stores/assets.store';
   import { t } from 'svelte-i18n';
 
-  export let assetId: string;
-  export let assetType: AssetTypeEnum;
+  interface Props {
+    assetId: string;
+    assetType: AssetTypeEnum;
+    onClose: () => void;
+    onRefresh: () => void;
+  }
+
+  let { assetId, assetType, onClose, onRefresh }: Props = $props();
 
   // keep track of the changes
   let peopleToCreate: string[] = [];
   let assetFaceGenerated: string[] = [];
 
   // faces
-  let peopleWithFaces: AssetFaceResponseDto[] = [];
-  let selectedPersonToReassign: Record<string, PersonResponseDto> = {};
-  let selectedPersonToCreate: Record<string, string> = {};
-  let editedFace: AssetFaceResponseDto;
+  let peopleWithFaces: AssetFaceResponseDto[] = $state([]);
+  let selectedPersonToReassign: Record<string, PersonResponseDto> = $state({});
+  let selectedPersonToCreate: Record<string, string> = $state({});
+  let editedFace: AssetFaceResponseDto | undefined = $state();
 
   // loading spinners
-  let isShowLoadingDone = false;
-  let isShowLoadingPeople = false;
+  let isShowLoadingDone = $state(false);
+  let isShowLoadingPeople = $state(false);
 
   // search people
-  let showSelectedFaces = false;
-  let allPeople: PersonResponseDto[] = [];
+  let showSelectedFaces = $state(false);
 
   // timers
   let loaderLoadingDoneTimeout: ReturnType<typeof setTimeout>;
@@ -56,16 +59,9 @@
 
   const thumbnailWidth = '90px';
 
-  const dispatch = createEventDispatcher<{
-    close: void;
-    refresh: void;
-  }>();
-
   async function loadPeople() {
     const timeout = setTimeout(() => (isShowLoadingPeople = true), timeBeforeShowLoadingSpinner);
     try {
-      const { people } = await getAllPeople({ withHidden: true });
-      allPeople = people;
       peopleWithFaces = await getFaces({ id: assetId });
     } catch (error) {
       handleError(error, $t('errors.cant_get_faces'));
@@ -85,7 +81,7 @@
     ) {
       clearTimeout(loaderLoadingDoneTimeout);
       clearTimeout(automaticRefreshTimeout);
-      dispatch('refresh');
+      onRefresh();
     }
   };
 
@@ -98,22 +94,12 @@
     return b.every((valueB) => a.includes(valueB));
   };
 
-  const handleBackButton = () => {
-    dispatch('close');
-  };
-
   const handleReset = (id: string) => {
     if (selectedPersonToReassign[id]) {
       delete selectedPersonToReassign[id];
-
-      // trigger reactivity
-      selectedPersonToReassign = selectedPersonToReassign;
     }
     if (selectedPersonToCreate[id]) {
       delete selectedPersonToCreate[id];
-
-      // trigger reactivity
-      selectedPersonToCreate = selectedPersonToCreate;
     }
   };
 
@@ -153,21 +139,21 @@
     isShowLoadingDone = false;
     if (peopleToCreate.length === 0) {
       clearTimeout(loaderLoadingDoneTimeout);
-      dispatch('refresh');
+      onRefresh();
     } else {
-      automaticRefreshTimeout = setTimeout(() => dispatch('refresh'), 15_000);
+      automaticRefreshTimeout = setTimeout(onRefresh, 15_000);
     }
   };
 
   const handleCreatePerson = (newFeaturePhoto: string | null) => {
-    if (newFeaturePhoto) {
+    if (newFeaturePhoto && editedFace) {
       selectedPersonToCreate[editedFace.id] = newFeaturePhoto;
     }
     showSelectedFaces = false;
   };
 
   const handleReassignFace = (person: PersonResponseDto | null) => {
-    if (person) {
+    if (person && editedFace) {
       selectedPersonToReassign[editedFace.id] = person;
     }
     showSelectedFaces = false;
@@ -185,14 +171,14 @@
 >
   <div class="flex place-items-center justify-between gap-2">
     <div class="flex items-center gap-2">
-      <CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={handleBackButton} />
+      <CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} onclick={onClose} />
       <p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">{$t('edit_faces')}</p>
     </div>
     {#if !isShowLoadingDone}
       <button
         type="button"
         class="justify-self-end rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50"
-        on:click={() => handleEditFaces()}
+        onclick={() => handleEditFaces()}
       >
         {$t('done')}
       </button>
@@ -215,9 +201,9 @@
               role="button"
               tabindex={index}
               class="absolute left-0 top-0 h-[90px] w-[90px] cursor-default"
-              on:focus={() => ($boundingBoxesArray = [peopleWithFaces[index]])}
-              on:mouseover={() => ($boundingBoxesArray = [peopleWithFaces[index]])}
-              on:mouseleave={() => ($boundingBoxesArray = [])}
+              onfocus={() => ($boundingBoxesArray = [peopleWithFaces[index]])}
+              onmouseover={() => ($boundingBoxesArray = [peopleWithFaces[index]])}
+              onmouseleave={() => ($boundingBoxesArray = [])}
             >
               <div class="relative">
                 {#if selectedPersonToCreate[face.id]}
@@ -299,17 +285,17 @@
                     size="18"
                     padding="1"
                     class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
-                    on:click={() => handleReset(face.id)}
+                    onclick={() => handleReset(face.id)}
                   />
                 {:else}
                   <CircleIconButton
                     color="primary"
-                    icon={mdiMinus}
+                    icon={mdiPencil}
                     title={$t('select_new_face')}
                     size="18"
                     padding="1"
                     class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
-                    on:click={() => handleFacePicker(face)}
+                    onclick={() => handleFacePicker(face)}
                   />
                 {/if}
               </div>
@@ -330,14 +316,13 @@
   </div>
 </section>
 
-{#if showSelectedFaces}
+{#if showSelectedFaces && editedFace}
   <AssignFaceSidePanel
-    {allPeople}
     {editedFace}
     {assetId}
     {assetType}
-    on:close={() => (showSelectedFaces = false)}
-    on:createPerson={(event) => handleCreatePerson(event.detail)}
-    on:reassign={(event) => handleReassignFace(event.detail)}
+    onClose={() => (showSelectedFaces = false)}
+    onCreatePerson={handleCreatePerson}
+    onReassign={handleReassignFace}
   />
 {/if}
diff --git a/web/src/lib/components/faces-page/set-birth-date-modal.svelte b/web/src/lib/components/faces-page/set-birth-date-modal.svelte
index b670f34dfd..f5ecbfabf0 100644
--- a/web/src/lib/components/faces-page/set-birth-date-modal.svelte
+++ b/web/src/lib/components/faces-page/set-birth-date-modal.svelte
@@ -1,34 +1,34 @@
 <script lang="ts">
-  import { createEventDispatcher } from 'svelte';
   import Button from '../elements/buttons/button.svelte';
   import FullScreenModal from '../shared-components/full-screen-modal.svelte';
   import { mdiCake } from '@mdi/js';
   import DateInput from '../elements/date-input.svelte';
   import { t } from 'svelte-i18n';
 
-  export let birthDate: string;
+  interface Props {
+    birthDate: string;
+    onClose: () => void;
+    onUpdate: (birthDate: string) => void;
+  }
 
-  const dispatch = createEventDispatcher<{
-    close: void;
-    updated: string;
-  }>();
+  let { birthDate = $bindable(), onClose, onUpdate }: Props = $props();
 
   const todayFormatted = new Date().toISOString().split('T')[0];
 
-  const handleCancel = () => dispatch('close');
-  const handleSubmit = () => {
-    dispatch('updated', birthDate);
+  const onSubmit = (event: Event) => {
+    event.preventDefault();
+    onUpdate(birthDate);
   };
 </script>
 
-<FullScreenModal title={$t('set_date_of_birth')} icon={mdiCake} onClose={handleCancel}>
+<FullScreenModal title={$t('set_date_of_birth')} icon={mdiCake} {onClose}>
   <div class="text-immich-primary dark:text-immich-dark-primary">
     <p class="text-sm dark:text-immich-dark-fg">
       {$t('birthdate_set_description')}
     </p>
   </div>
 
-  <form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="set-birth-date-form">
+  <form onsubmit={(e) => onSubmit(e)} autocomplete="off" id="set-birth-date-form">
     <div class="my-4 flex flex-col gap-2">
       <DateInput
         class="immich-form-input"
@@ -40,8 +40,9 @@
       />
     </div>
   </form>
-  <svelte:fragment slot="sticky-bottom">
-    <Button color="gray" fullwidth on:click={() => handleCancel()}>{$t('cancel')}</Button>
+
+  {#snippet stickyBottom()}
+    <Button color="gray" fullwidth onclick={onClose}>{$t('cancel')}</Button>
     <Button type="submit" fullwidth form="set-birth-date-form">{$t('set')}</Button>
-  </svelte:fragment>
+  {/snippet}
 </FullScreenModal>
diff --git a/web/src/lib/components/faces-page/unmerge-face-selector.svelte b/web/src/lib/components/faces-page/unmerge-face-selector.svelte
index c89c8338d3..06c53f3618 100644
--- a/web/src/lib/components/faces-page/unmerge-face-selector.svelte
+++ b/web/src/lib/components/faces-page/unmerge-face-selector.svelte
@@ -10,7 +10,7 @@
     type PersonResponseDto,
   } from '@immich/sdk';
   import { mdiMerge, mdiPlus } from '@mdi/js';
-  import { createEventDispatcher, onMount } from 'svelte';
+  import { onMount, type Snippet } from 'svelte';
   import { quintOut } from 'svelte/easing';
   import { fly } from 'svelte/transition';
   import Button from '../elements/buttons/button.svelte';
@@ -21,23 +21,26 @@
   import PeopleList from './people-list.svelte';
   import { t } from 'svelte-i18n';
 
-  export let assetIds: string[];
-  export let personAssets: PersonResponseDto;
+  interface Props {
+    assetIds: string[];
+    personAssets: PersonResponseDto;
+    onConfirm: () => void;
+    onClose: () => void;
+    header?: Snippet;
+    merge?: Snippet;
+  }
 
-  let people: PersonResponseDto[] = [];
-  let selectedPerson: PersonResponseDto | null = null;
-  let disableButtons = false;
-  let showLoadingSpinnerCreate = false;
-  let showLoadingSpinnerReassign = false;
-  let hasSelection = false;
-  let screenHeight: number;
+  let { assetIds, personAssets, onConfirm, onClose, header, merge }: Props = $props();
 
-  $: peopleToNotShow = selectedPerson ? [personAssets, selectedPerson] : [personAssets];
+  let people: PersonResponseDto[] = $state([]);
+  let selectedPerson: PersonResponseDto | null = $state(null);
+  let disableButtons = $state(false);
+  let showLoadingSpinnerCreate = $state(false);
+  let showLoadingSpinnerReassign = $state(false);
+  let hasSelection = $state(false);
+  let screenHeight: number = $state(0);
 
-  let dispatch = createEventDispatcher<{
-    confirm: void;
-    close: void;
-  }>();
+  let peopleToNotShow = $derived(selectedPerson ? [personAssets, selectedPerson] : [personAssets]);
 
   const selectedPeople: AssetFaceUpdateItem[] = [];
 
@@ -50,10 +53,6 @@
     people = data.people;
   });
 
-  const onClose = () => {
-    dispatch('close');
-  };
-
   const handleSelectedPerson = (person: PersonResponseDto) => {
     if (selectedPerson && selectedPerson.id === person.id) {
       handleRemoveSelectedPerson();
@@ -87,7 +86,7 @@
     }
 
     showLoadingSpinnerCreate = false;
-    dispatch('confirm');
+    onConfirm();
   };
 
   const handleReassign = async () => {
@@ -113,7 +112,7 @@
     }
 
     showLoadingSpinnerReassign = false;
-    dispatch('confirm');
+    onConfirm();
   };
 </script>
 
@@ -123,18 +122,18 @@
   transition:fly={{ y: 500, duration: 100, easing: quintOut }}
   class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
 >
-  <ControlAppBar on:close={onClose}>
-    <svelte:fragment slot="leading">
-      <slot name="header" />
-      <div />
-    </svelte:fragment>
-    <svelte:fragment slot="trailing">
+  <ControlAppBar {onClose}>
+    {#snippet leading()}
+      {@render header?.()}
+      <div></div>
+    {/snippet}
+    {#snippet trailing()}
       <div class="flex gap-4">
         <Button
           title={$t('create_new_person_hint')}
           size={'sm'}
           disabled={disableButtons || hasSelection}
-          on:click={handleCreate}
+          onclick={handleCreate}
         >
           {#if !showLoadingSpinnerCreate}
             <Icon path={mdiPlus} size={18} />
@@ -147,7 +146,7 @@
           size={'sm'}
           title={$t('reassing_hint')}
           disabled={disableButtons || !hasSelection}
-          on:click={handleReassign}
+          onclick={handleReassign}
         >
           {#if !showLoadingSpinnerReassign}
             <div>
@@ -159,9 +158,9 @@
           <span class="ml-2"> {$t('reassign')}</span></Button
         >
       </div>
-    </svelte:fragment>
+    {/snippet}
   </ControlAppBar>
-  <slot name="merge" />
+  {@render merge?.()}
   <section class="bg-immich-bg px-[70px] pt-[100px] dark:bg-immich-dark-bg">
     <section id="merge-face-selector relative">
       {#if selectedPerson !== null}
@@ -175,12 +174,12 @@
               circle
               selectable
               thumbnailSize={180}
-              on:click={handleRemoveSelectedPerson}
+              onClick={handleRemoveSelectedPerson}
             />
           </div>
         </div>
       {/if}
-      <PeopleList {people} {peopleToNotShow} {screenHeight} on:select={({ detail }) => handleSelectedPerson(detail)} />
+      <PeopleList {people} {peopleToNotShow} {screenHeight} onSelect={handleSelectedPerson} />
     </section>
   </section>
 </section>
diff --git a/web/src/lib/components/forms/admin-registration-form.svelte b/web/src/lib/components/forms/admin-registration-form.svelte
index d49ab55439..b4ecd56283 100644
--- a/web/src/lib/components/forms/admin-registration-form.svelte
+++ b/web/src/lib/components/forms/admin-registration-form.svelte
@@ -8,15 +8,15 @@
   import { t } from 'svelte-i18n';
   import { retrieveServerConfig } from '$lib/stores/server-config.store';
 
-  let email = '';
-  let password = '';
-  let confirmPassword = '';
-  let name = '';
+  let email = $state('');
+  let password = $state('');
+  let confirmPassword = $state('');
+  let name = $state('');
 
-  let errorMessage: string;
-  let canRegister = false;
+  let errorMessage: string = $state('');
+  let canRegister = $state(false);
 
-  $: {
+  $effect(() => {
     if (password !== confirmPassword && confirmPassword.length > 0) {
       errorMessage = $t('password_does_not_match');
       canRegister = false;
@@ -24,7 +24,7 @@
       errorMessage = '';
       canRegister = true;
     }
-  }
+  });
 
   async function registerAdmin() {
     if (canRegister) {
@@ -40,9 +40,14 @@
       }
     }
   }
+
+  const onsubmit = async (event: Event) => {
+    event.preventDefault();
+    await registerAdmin();
+  };
 </script>
 
-<form on:submit|preventDefault={registerAdmin} method="post" class="mt-5 flex flex-col gap-5">
+<form {onsubmit} method="post" class="mt-5 flex flex-col gap-5">
   <div class="flex flex-col gap-2">
     <label class="immich-form-label" for="email">{$t('admin_email')}</label>
     <input class="immich-form-input" id="email" bind:value={email} type="email" autocomplete="email" required />
diff --git a/web/src/lib/components/forms/api-key-form.svelte b/web/src/lib/components/forms/api-key-form.svelte
index 5b1341db44..086d7708c3 100644
--- a/web/src/lib/components/forms/api-key-form.svelte
+++ b/web/src/lib/components/forms/api-key-form.svelte
@@ -5,13 +5,23 @@
   import FullScreenModal from '../shared-components/full-screen-modal.svelte';
   import { NotificationType, notificationController } from '../shared-components/notification/notification';
 
-  export let apiKey: { name: string };
-  export let title: string;
-  export let cancelText = $t('cancel');
-  export let submitText = $t('save');
+  interface Props {
+    apiKey: { name: string };
+    title: string;
+    cancelText?: string;
+    submitText?: string;
+    onSubmit: (apiKey: { name: string }) => void;
+    onCancel: () => void;
+  }
 
-  export let onSubmit: (apiKey: { name: string }) => void;
-  export let onCancel: () => void;
+  let {
+    apiKey = $bindable(),
+    title,
+    cancelText = $t('cancel'),
+    submitText = $t('save'),
+    onSubmit,
+    onCancel,
+  }: Props = $props();
 
   const handleSubmit = () => {
     if (apiKey.name) {
@@ -23,17 +33,23 @@
       });
     }
   };
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+    handleSubmit();
+  };
 </script>
 
 <FullScreenModal {title} icon={mdiKeyVariant} onClose={() => onCancel()}>
-  <form on:submit|preventDefault={handleSubmit} autocomplete="off" id="api-key-form">
+  <form {onsubmit} autocomplete="off" id="api-key-form">
     <div class="mb-4 flex flex-col gap-2">
       <label class="immich-form-label" for="name">{$t('name')}</label>
       <input class="immich-form-input" id="name" name="name" type="text" bind:value={apiKey.name} />
     </div>
   </form>
-  <svelte:fragment slot="sticky-bottom">
-    <Button color="gray" fullwidth on:click={() => onCancel()}>{cancelText}</Button>
+
+  {#snippet stickyBottom()}
+    <Button color="gray" fullwidth onclick={() => onCancel()}>{cancelText}</Button>
     <Button type="submit" fullwidth form="api-key-form">{submitText}</Button>
-  </svelte:fragment>
+  {/snippet}
 </FullScreenModal>
diff --git a/web/src/lib/components/forms/api-key-secret.svelte b/web/src/lib/components/forms/api-key-secret.svelte
index b7bf8e1836..fd0503e850 100644
--- a/web/src/lib/components/forms/api-key-secret.svelte
+++ b/web/src/lib/components/forms/api-key-secret.svelte
@@ -1,20 +1,19 @@
 <script lang="ts">
   import { copyToClipboard } from '$lib/utils';
   import { mdiKeyVariant } from '@mdi/js';
-  import { createEventDispatcher } from 'svelte';
   import Button from '../elements/buttons/button.svelte';
   import FullScreenModal from '../shared-components/full-screen-modal.svelte';
   import { t } from 'svelte-i18n';
 
-  export let secret = '';
+  interface Props {
+    secret?: string;
+    onDone: () => void;
+  }
 
-  const dispatch = createEventDispatcher<{
-    done: void;
-  }>();
-  const handleDone = () => dispatch('done');
+  let { secret = '', onDone }: Props = $props();
 </script>
 
-<FullScreenModal title={$t('api_key')} icon={mdiKeyVariant} onClose={() => handleDone()}>
+<FullScreenModal title={$t('api_key')} icon={mdiKeyVariant} onClose={onDone}>
   <div class="text-immich-primary dark:text-immich-dark-primary">
     <p class="text-sm dark:text-immich-dark-fg">
       {$t('api_key_description')}
@@ -23,11 +22,11 @@
 
   <div class="my-4 flex flex-col gap-2">
     <!-- <label class="immich-form-label" for="secret">{ $t("api_key") }</label> -->
-    <textarea class="immich-form-input" id="secret" name="secret" readonly={true} value={secret} />
+    <textarea class="immich-form-input" id="secret" name="secret" readonly={true} value={secret}></textarea>
   </div>
 
-  <svelte:fragment slot="sticky-bottom">
-    <Button on:click={() => copyToClipboard(secret)} fullwidth>{$t('copy_to_clipboard')}</Button>
-    <Button on:click={() => handleDone()} fullwidth>{$t('done')}</Button>
-  </svelte:fragment>
+  {#snippet stickyBottom()}
+    <Button onclick={() => copyToClipboard(secret)} fullwidth>{$t('copy_to_clipboard')}</Button>
+    <Button onclick={onDone} fullwidth>{$t('done')}</Button>
+  {/snippet}
 </FullScreenModal>
diff --git a/web/src/lib/components/forms/change-password-form.svelte b/web/src/lib/components/forms/change-password-form.svelte
index 799dde7ef3..6f16781d9a 100644
--- a/web/src/lib/components/forms/change-password-form.svelte
+++ b/web/src/lib/components/forms/change-password-form.svelte
@@ -1,19 +1,23 @@
 <script lang="ts">
-  import { createEventDispatcher } from 'svelte';
   import Button from '../elements/buttons/button.svelte';
   import PasswordField from '../shared-components/password-field.svelte';
   import { updateMyUser } from '@immich/sdk';
   import { t } from 'svelte-i18n';
 
-  let errorMessage: string;
-  let success: string;
+  interface Props {
+    onSuccess: () => void;
+  }
 
-  let password = '';
-  let passwordConfirm = '';
+  let { onSuccess }: Props = $props();
 
-  let valid = false;
+  let errorMessage: string = $state('');
 
-  $: {
+  let password = $state('');
+  let passwordConfirm = $state('');
+
+  let valid = $state(false);
+
+  $effect(() => {
     if (password !== passwordConfirm && passwordConfirm.length > 0) {
       errorMessage = $t('password_does_not_match');
       valid = false;
@@ -21,11 +25,7 @@
       errorMessage = '';
       valid = true;
     }
-  }
-
-  const dispatch = createEventDispatcher<{
-    success: void;
-  }>();
+  });
 
   async function changePassword() {
     if (valid) {
@@ -33,12 +33,17 @@
 
       await updateMyUser({ userUpdateMeDto: { password: String(password) } });
 
-      dispatch('success');
+      onSuccess();
     }
   }
+
+  const onsubmit = async (event: Event) => {
+    event.preventDefault();
+    await changePassword();
+  };
 </script>
 
-<form on:submit|preventDefault={changePassword} method="post" class="mt-5 flex flex-col gap-5">
+<form {onsubmit} method="post" class="mt-5 flex flex-col gap-5">
   <div class="flex flex-col gap-2">
     <label class="immich-form-label" for="password">{$t('new_password')}</label>
     <PasswordField id="password" bind:password autocomplete="new-password" />
@@ -53,9 +58,6 @@
     <p class="text-sm text-red-400">{errorMessage}</p>
   {/if}
 
-  {#if success}
-    <p class="text-sm text-immich-primary">{success}</p>
-  {/if}
   <div class="my-5 flex w-full">
     <Button type="submit" size="lg" fullwidth>{$t('to_change_password')}</Button>
   </div>
diff --git a/web/src/lib/components/forms/create-user-form.svelte b/web/src/lib/components/forms/create-user-form.svelte
index 3557241c59..7aa1c76ed3 100644
--- a/web/src/lib/components/forms/create-user-form.svelte
+++ b/web/src/lib/components/forms/create-user-form.svelte
@@ -1,36 +1,44 @@
 <script lang="ts">
-  import { serverInfo } from '$lib/stores/server-info.store';
-  import { handleError } from '$lib/utils/handle-error';
-  import { createUserAdmin } from '@immich/sdk';
-  import { createEventDispatcher } from 'svelte';
-  import Button from '../elements/buttons/button.svelte';
-  import PasswordField from '../shared-components/password-field.svelte';
-  import Slider from '../elements/slider.svelte';
   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
   import { featureFlags } from '$lib/stores/server-config.store';
-  import { t } from 'svelte-i18n';
+  import { userInteraction } from '$lib/stores/user.svelte';
   import { ByteUnit, convertToBytes } from '$lib/utils/byte-units';
+  import { handleError } from '$lib/utils/handle-error';
+  import { createUserAdmin } from '@immich/sdk';
+  import { t } from 'svelte-i18n';
+  import Button from '../elements/buttons/button.svelte';
+  import Slider from '../elements/slider.svelte';
+  import PasswordField from '../shared-components/password-field.svelte';
 
-  export let onClose: () => void;
+  interface Props {
+    onClose: () => void;
+    onSubmit: () => void;
+    onCancel: () => void;
+    oauthEnabled?: boolean;
+  }
 
-  let error: string;
-  let success: string;
+  let { onClose, onSubmit, onCancel, oauthEnabled = false }: Props = $props();
 
-  let email = '';
-  let password = '';
-  let confirmPassword = '';
-  let name = '';
-  let shouldChangePassword = true;
-  let notify = true;
+  let error = $state('');
+  let success = $state('');
 
-  let canCreateUser = false;
-  let quotaSize: number | undefined;
-  let isCreatingUser = false;
+  let email = $state('');
+  let password = $state('');
+  let confirmPassword = $state('');
+  let name = $state('');
+  let shouldChangePassword = $state(true);
+  let notify = $state(true);
 
-  $: quotaSizeInBytes = quotaSize ? convertToBytes(quotaSize, ByteUnit.GiB) : null;
-  $: quotaSizeWarning = quotaSizeInBytes && quotaSizeInBytes > $serverInfo.diskSizeRaw;
+  let canCreateUser = $state(false);
+  let quotaSize: number | undefined = $state();
+  let isCreatingUser = $state(false);
 
-  $: {
+  let quotaSizeInBytes = $derived(quotaSize ? convertToBytes(quotaSize, ByteUnit.GiB) : null);
+  let quotaSizeWarning = $derived(
+    quotaSizeInBytes && userInteraction.serverInfo && quotaSizeInBytes > userInteraction.serverInfo.diskSizeRaw,
+  );
+
+  $effect(() => {
     if (password !== confirmPassword && confirmPassword.length > 0) {
       error = $t('password_does_not_match');
       canCreateUser = false;
@@ -38,11 +46,7 @@
       error = '';
       canCreateUser = true;
     }
-  }
-  const dispatch = createEventDispatcher<{
-    submit: void;
-    cancel: void;
-  }>();
+  });
 
   async function registerUser() {
     if (canCreateUser && !isCreatingUser) {
@@ -63,7 +67,7 @@
 
         success = $t('new_user_created');
 
-        dispatch('submit');
+        onSubmit();
 
         return;
       } catch (error) {
@@ -73,10 +77,15 @@
       }
     }
   }
+
+  const onsubmit = async (event: Event) => {
+    event.preventDefault();
+    await registerUser();
+  };
 </script>
 
 <FullScreenModal title={$t('create_new_user')} showLogo {onClose}>
-  <form on:submit|preventDefault={registerUser} autocomplete="off" id="create-new-user-form">
+  <form {onsubmit} autocomplete="off" id="create-new-user-form">
     <div class="my-4 flex flex-col gap-2">
       <label class="immich-form-label" for="email">{$t('email')}</label>
       <input class="immich-form-input" id="email" bind:value={email} type="email" required />
@@ -93,12 +102,17 @@
 
     <div class="my-4 flex flex-col gap-2">
       <label class="immich-form-label" for="password">{$t('password')}</label>
-      <PasswordField id="password" bind:password autocomplete="new-password" />
+      <PasswordField id="password" bind:password autocomplete="new-password" required={!oauthEnabled} />
     </div>
 
     <div class="my-4 flex flex-col gap-2">
       <label class="immich-form-label" for="confirmPassword">{$t('confirm_password')}</label>
-      <PasswordField id="confirmPassword" bind:password={confirmPassword} autocomplete="new-password" />
+      <PasswordField
+        id="confirmPassword"
+        bind:password={confirmPassword}
+        autocomplete="new-password"
+        required={!oauthEnabled}
+      />
     </div>
 
     <div class="my-4 flex place-items-center justify-between gap-2">
@@ -131,8 +145,9 @@
       <p class="text-sm text-immich-primary">{success}</p>
     {/if}
   </form>
-  <svelte:fragment slot="sticky-bottom">
-    <Button color="gray" fullwidth on:click={() => dispatch('cancel')}>{$t('cancel')}</Button>
+
+  {#snippet stickyBottom()}
+    <Button color="gray" fullwidth onclick={onCancel}>{$t('cancel')}</Button>
     <Button type="submit" disabled={isCreatingUser} fullwidth form="create-new-user-form">{$t('create')}</Button>
-  </svelte:fragment>
+  {/snippet}
 </FullScreenModal>
diff --git a/web/src/lib/components/forms/edit-album-form.svelte b/web/src/lib/components/forms/edit-album-form.svelte
index bcb097eca6..bd61cd2068 100644
--- a/web/src/lib/components/forms/edit-album-form.svelte
+++ b/web/src/lib/components/forms/edit-album-form.svelte
@@ -6,15 +6,19 @@
   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
   import { t } from 'svelte-i18n';
 
-  export let album: AlbumResponseDto;
-  export let onEditSuccess: ((album: AlbumResponseDto) => unknown) | undefined = undefined;
-  export let onCancel: (() => unknown) | undefined = undefined;
-  export let onClose: () => void;
+  interface Props {
+    album: AlbumResponseDto;
+    onEditSuccess?: ((album: AlbumResponseDto) => unknown) | undefined;
+    onCancel?: (() => unknown) | undefined;
+    onClose: () => void;
+  }
 
-  let albumName = album.albumName;
-  let description = album.description;
+  let { album = $bindable(), onEditSuccess = undefined, onCancel = undefined, onClose }: Props = $props();
 
-  let isSubmitting = false;
+  let albumName = $state(album.albumName);
+  let description = $state(album.description);
+
+  let isSubmitting = $state(false);
 
   const handleUpdateAlbumInfo = async () => {
     isSubmitting = true;
@@ -35,10 +39,15 @@
       isSubmitting = false;
     }
   };
+
+  const onsubmit = async (event: Event) => {
+    event.preventDefault();
+    await handleUpdateAlbumInfo();
+  };
 </script>
 
 <FullScreenModal title={$t('edit_album')} width="wide" {onClose}>
-  <form on:submit|preventDefault={handleUpdateAlbumInfo} autocomplete="off" id="edit-album-form">
+  <form {onsubmit} autocomplete="off" id="edit-album-form">
     <div class="flex items-center">
       <div class="hidden sm:flex">
         <AlbumCover {album} class="h-[200px] w-[200px] m-4 shadow-lg" />
@@ -52,13 +61,14 @@
 
         <div class="m-4 flex flex-col gap-2">
           <label class="immich-form-label" for="description">{$t('description')}</label>
-          <textarea class="immich-form-input" id="description" bind:value={description} />
+          <textarea class="immich-form-input" id="description" bind:value={description}></textarea>
         </div>
       </div>
     </div>
   </form>
-  <svelte:fragment slot="sticky-bottom">
-    <Button color="gray" fullwidth on:click={() => onCancel?.()}>{$t('cancel')}</Button>
+
+  {#snippet stickyBottom()}
+    <Button color="gray" fullwidth onclick={() => onCancel?.()}>{$t('cancel')}</Button>
     <Button type="submit" fullwidth disabled={isSubmitting} form="edit-album-form">{$t('ok')}</Button>
-  </svelte:fragment>
+  {/snippet}
 </FullScreenModal>
diff --git a/web/src/lib/components/forms/edit-user-form.svelte b/web/src/lib/components/forms/edit-user-form.svelte
index b326565122..02b965fb22 100644
--- a/web/src/lib/components/forms/edit-user-form.svelte
+++ b/web/src/lib/components/forms/edit-user-form.svelte
@@ -1,37 +1,43 @@
 <script lang="ts">
   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
   import { AppRoute } from '$lib/constants';
-  import { serverInfo } from '$lib/stores/server-info.store';
+  import { userInteraction } from '$lib/stores/user.svelte';
   import { handleError } from '$lib/utils/handle-error';
   import { updateUserAdmin, type UserAdminResponseDto } from '@immich/sdk';
   import { mdiAccountEditOutline } from '@mdi/js';
-  import { createEventDispatcher } from 'svelte';
   import Button from '../elements/buttons/button.svelte';
   import { dialogController } from '$lib/components/shared-components/dialog/dialog';
   import { t } from 'svelte-i18n';
   import { ByteUnit, convertFromBytes, convertToBytes } from '$lib/utils/byte-units';
 
-  export let user: UserAdminResponseDto;
-  export let canResetPassword = true;
-  export let newPassword: string;
-  export let onClose: () => void;
+  interface Props {
+    user: UserAdminResponseDto;
+    canResetPassword?: boolean;
+    newPassword: string;
+    onClose: () => void;
+    onResetPasswordSuccess: () => void;
+    onEditSuccess: () => void;
+  }
 
-  let error: string;
-  let success: string;
-  let quotaSize = user.quotaSizeInBytes ? convertFromBytes(user.quotaSizeInBytes, ByteUnit.GiB) : null;
+  let {
+    user,
+    canResetPassword = true,
+    newPassword = $bindable(),
+    onClose,
+    onResetPasswordSuccess,
+    onEditSuccess,
+  }: Props = $props();
+
+  let quotaSize = $state(user.quotaSizeInBytes ? convertFromBytes(user.quotaSizeInBytes, ByteUnit.GiB) : null);
 
   const previousQutoa = user.quotaSizeInBytes;
 
-  $: quotaSizeWarning =
+  let quotaSizeWarning = $derived(
     previousQutoa !== convertToBytes(Number(quotaSize), ByteUnit.GiB) &&
-    !!quotaSize &&
-    convertToBytes(Number(quotaSize), ByteUnit.GiB) > $serverInfo.diskSizeRaw;
-
-  const dispatch = createEventDispatcher<{
-    close: void;
-    resetPasswordSuccess: void;
-    editSuccess: void;
-  }>();
+      !!quotaSize &&
+      userInteraction.serverInfo &&
+      convertToBytes(Number(quotaSize), ByteUnit.GiB) > userInteraction.serverInfo.diskSizeRaw,
+  );
 
   const editUser = async () => {
     try {
@@ -46,7 +52,7 @@
         },
       });
 
-      dispatch('editSuccess');
+      onEditSuccess();
     } catch (error) {
       handleError(error, $t('errors.unable_to_update_user'));
     }
@@ -72,7 +78,7 @@
         },
       });
 
-      dispatch('resetPasswordSuccess');
+      onResetPasswordSuccess();
     } catch (error) {
       handleError(error, $t('errors.unable_to_reset_password'));
     }
@@ -94,10 +100,15 @@
 
     return generatedPassword;
   }
+
+  const onSubmit = async (event: Event) => {
+    event.preventDefault();
+    await editUser();
+  };
 </script>
 
 <FullScreenModal title={$t('edit_user')} icon={mdiAccountEditOutline} {onClose}>
-  <form on:submit|preventDefault={editUser} autocomplete="off" id="edit-user-form">
+  <form onsubmit={onSubmit} autocomplete="off" id="edit-user-form">
     <div class="my-4 flex flex-col gap-2">
       <label class="immich-form-label" for="email">{$t('email')}</label>
       <input class="immich-form-input" id="email" name="email" type="email" bind:value={user.email} />
@@ -136,19 +147,12 @@
         </a>
       </p>
     </div>
-
-    {#if error}
-      <p class="ml-4 text-sm text-red-400">{error}</p>
-    {/if}
-
-    {#if success}
-      <p class="ml-4 text-sm text-immich-primary">{success}</p>
-    {/if}
   </form>
-  <svelte:fragment slot="sticky-bottom">
+
+  {#snippet stickyBottom()}
     {#if canResetPassword}
-      <Button color="light-red" fullwidth on:click={resetPassword}>{$t('reset_password')}</Button>
+      <Button color="light-red" fullwidth onclick={resetPassword}>{$t('reset_password')}</Button>
     {/if}
     <Button type="submit" fullwidth form="edit-user-form">{$t('confirm')}</Button>
-  </svelte:fragment>
+  {/snippet}
 </FullScreenModal>
diff --git a/web/src/lib/components/forms/library-exclusion-pattern-form.svelte b/web/src/lib/components/forms/library-exclusion-pattern-form.svelte
index c09f1fbaf6..e79b60d265 100644
--- a/web/src/lib/components/forms/library-exclusion-pattern-form.svelte
+++ b/web/src/lib/components/forms/library-exclusion-pattern-form.svelte
@@ -1,15 +1,29 @@
 <script lang="ts">
-  import { createEventDispatcher } from 'svelte';
   import Button from '../elements/buttons/button.svelte';
   import FullScreenModal from '../shared-components/full-screen-modal.svelte';
   import { mdiFolderRemove } from '@mdi/js';
   import { onMount } from 'svelte';
   import { t } from 'svelte-i18n';
 
-  export let exclusionPattern: string;
-  export let exclusionPatterns: string[] = [];
-  export let isEditing = false;
-  export let submitText = $t('submit');
+  interface Props {
+    exclusionPattern: string;
+    exclusionPatterns?: string[];
+    isEditing?: boolean;
+    submitText?: string;
+    onCancel: () => void;
+    onSubmit: (exclusionPattern: string) => void;
+    onDelete?: () => void;
+  }
+
+  let {
+    exclusionPattern = $bindable(),
+    exclusionPatterns = $bindable([]),
+    isEditing = false,
+    submitText = $t('submit'),
+    onCancel,
+    onSubmit,
+    onDelete,
+  }: Props = $props();
 
   onMount(() => {
     if (isEditing) {
@@ -17,20 +31,19 @@
     }
   });
 
-  $: isDuplicate = exclusionPattern !== null && exclusionPatterns.includes(exclusionPattern);
-  $: canSubmit = exclusionPattern && !exclusionPatterns.includes(exclusionPattern);
+  let isDuplicate = $derived(exclusionPattern !== null && exclusionPatterns.includes(exclusionPattern));
+  let canSubmit = $derived(exclusionPattern && !exclusionPatterns.includes(exclusionPattern));
 
-  const dispatch = createEventDispatcher<{
-    cancel: void;
-    submit: { excludePattern: string };
-    delete: void;
-  }>();
-  const handleCancel = () => dispatch('cancel');
-  const handleSubmit = () => dispatch('submit', { excludePattern: exclusionPattern });
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+    if (canSubmit) {
+      onSubmit(exclusionPattern);
+    }
+  };
 </script>
 
-<FullScreenModal title={$t('add_exclusion_pattern')} icon={mdiFolderRemove} onClose={handleCancel}>
-  <form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="add-exclusion-pattern-form">
+<FullScreenModal title={$t('add_exclusion_pattern')} icon={mdiFolderRemove} onClose={onCancel}>
+  <form {onsubmit} autocomplete="off" id="add-exclusion-pattern-form">
     <p class="py-5 text-sm">
       {$t('admin.exclusion_pattern_description')}
       <br /><br />
@@ -52,11 +65,12 @@
       {/if}
     </div>
   </form>
-  <svelte:fragment slot="sticky-bottom">
-    <Button color="gray" fullwidth on:click={() => handleCancel()}>{$t('cancel')}</Button>
+
+  {#snippet stickyBottom()}
+    <Button color="gray" fullwidth onclick={onCancel}>{$t('cancel')}</Button>
     {#if isEditing}
-      <Button color="red" fullwidth on:click={() => dispatch('delete')}>{$t('delete')}</Button>
+      <Button color="red" fullwidth onclick={onDelete}>{$t('delete')}</Button>
     {/if}
     <Button type="submit" disabled={!canSubmit} fullwidth form="add-exclusion-pattern-form">{submitText}</Button>
-  </svelte:fragment>
+  {/snippet}
 </FullScreenModal>
diff --git a/web/src/lib/components/forms/library-import-path-form.svelte b/web/src/lib/components/forms/library-import-path-form.svelte
index f82d573386..33e763f0f0 100644
--- a/web/src/lib/components/forms/library-import-path-form.svelte
+++ b/web/src/lib/components/forms/library-import-path-form.svelte
@@ -1,17 +1,33 @@
 <script lang="ts">
-  import { createEventDispatcher } from 'svelte';
   import Button from '../elements/buttons/button.svelte';
   import FullScreenModal from '../shared-components/full-screen-modal.svelte';
   import { mdiFolderSync } from '@mdi/js';
   import { onMount } from 'svelte';
   import { t } from 'svelte-i18n';
 
-  export let importPath: string | null;
-  export let importPaths: string[] = [];
-  export let title = $t('import_path');
-  export let cancelText = $t('cancel');
-  export let submitText = $t('save');
-  export let isEditing = false;
+  interface Props {
+    importPath: string | null;
+    importPaths?: string[];
+    title?: string;
+    cancelText?: string;
+    submitText?: string;
+    isEditing?: boolean;
+    onCancel: () => void;
+    onSubmit: (importPath: string | null) => void;
+    onDelete?: () => void;
+  }
+
+  let {
+    importPath = $bindable(),
+    importPaths = $bindable([]),
+    title = $t('import_path'),
+    cancelText = $t('cancel'),
+    submitText = $t('save'),
+    isEditing = false,
+    onCancel,
+    onSubmit,
+    onDelete,
+  }: Props = $props();
 
   onMount(() => {
     if (isEditing) {
@@ -19,20 +35,19 @@
     }
   });
 
-  $: isDuplicate = importPath !== null && importPaths.includes(importPath);
-  $: canSubmit = importPath !== '' && importPath !== null && !importPaths.includes(importPath);
+  let isDuplicate = $derived(importPath !== null && importPaths.includes(importPath));
+  let canSubmit = $derived(importPath !== '' && importPath !== null && !importPaths.includes(importPath));
 
-  const dispatch = createEventDispatcher<{
-    cancel: void;
-    submit: { importPath: string | null };
-    delete: void;
-  }>();
-  const handleCancel = () => dispatch('cancel');
-  const handleSubmit = () => dispatch('submit', { importPath });
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+    if (canSubmit) {
+      onSubmit(importPath);
+    }
+  };
 </script>
 
-<FullScreenModal {title} icon={mdiFolderSync} onClose={handleCancel}>
-  <form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="library-import-path-form">
+<FullScreenModal {title} icon={mdiFolderSync} onClose={onCancel}>
+  <form {onsubmit} autocomplete="off" id="library-import-path-form">
     <p class="py-5 text-sm">{$t('admin.library_import_path_description')}</p>
 
     <div class="my-4 flex flex-col gap-2">
@@ -46,11 +61,12 @@
       {/if}
     </div>
   </form>
-  <svelte:fragment slot="sticky-bottom">
-    <Button color="gray" fullwidth on:click={() => handleCancel()}>{cancelText}</Button>
+
+  {#snippet stickyBottom()}
+    <Button color="gray" fullwidth onclick={onCancel}>{cancelText}</Button>
     {#if isEditing}
-      <Button color="red" fullwidth on:click={() => dispatch('delete')}>{$t('delete')}</Button>
+      <Button color="red" fullwidth onclick={onDelete}>{$t('delete')}</Button>
     {/if}
     <Button type="submit" disabled={!canSubmit} fullwidth form="library-import-path-form">{submitText}</Button>
-  </svelte:fragment>
+  {/snippet}
 </FullScreenModal>
diff --git a/web/src/lib/components/forms/library-import-paths-form.svelte b/web/src/lib/components/forms/library-import-paths-form.svelte
index a2bb3a9686..3acd46520f 100644
--- a/web/src/lib/components/forms/library-import-paths-form.svelte
+++ b/web/src/lib/components/forms/library-import-paths-form.svelte
@@ -1,5 +1,5 @@
 <script lang="ts">
-  import { createEventDispatcher, onMount } from 'svelte';
+  import { onMount } from 'svelte';
   import { handleError } from '../../utils/handle-error';
   import Button from '../elements/buttons/button.svelte';
   import LibraryImportPathForm from './library-import-path-form.svelte';
@@ -11,17 +11,23 @@
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import { t } from 'svelte-i18n';
 
-  export let library: LibraryResponseDto;
+  interface Props {
+    library: LibraryResponseDto;
+    onCancel: () => void;
+    onSubmit: (library: LibraryResponseDto) => void;
+  }
 
-  let addImportPath = false;
-  let editImportPath: number | null = null;
+  let { library = $bindable(), onCancel, onSubmit }: Props = $props();
 
-  let importPathToAdd: string | null = null;
-  let editedImportPath: string;
+  let addImportPath = $state(false);
+  let editImportPath: number | null = $state(null);
 
-  let validatedPaths: ValidateLibraryImportPathResponseDto[] = [];
+  let importPathToAdd: string | null = $state(null);
+  let editedImportPath: string = $state('');
 
-  $: importPaths = validatedPaths.map((validatedPath) => validatedPath.importPath);
+  let validatedPaths: ValidateLibraryImportPathResponseDto[] = $state([]);
+
+  let importPaths = $derived(validatedPaths.map((validatedPath) => validatedPath.importPath));
 
   onMount(async () => {
     if (library.importPaths) {
@@ -65,19 +71,6 @@
     }
   };
 
-  const dispatch = createEventDispatcher<{
-    cancel: void;
-    submit: Partial<LibraryResponseDto>;
-  }>();
-
-  const handleCancel = () => {
-    dispatch('cancel');
-  };
-
-  const handleSubmit = () => {
-    dispatch('submit', { ...library });
-  };
-
   const handleAddImportPath = async () => {
     if (!addImportPath || !importPathToAdd) {
       return;
@@ -145,6 +138,11 @@
       editImportPath = null;
     }
   };
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+    onSubmit({ ...library });
+  };
 </script>
 
 {#if addImportPath}
@@ -153,8 +151,8 @@
     submitText={$t('add')}
     bind:importPath={importPathToAdd}
     {importPaths}
-    on:submit={handleAddImportPath}
-    on:cancel={() => {
+    onSubmit={handleAddImportPath}
+    onCancel={() => {
       addImportPath = false;
       importPathToAdd = null;
     }}
@@ -168,15 +166,13 @@
     isEditing={true}
     bind:importPath={editedImportPath}
     {importPaths}
-    on:submit={handleEditImportPath}
-    on:delete={handleDeleteImportPath}
-    on:cancel={() => {
-      editImportPath = null;
-    }}
+    onSubmit={handleEditImportPath}
+    onDelete={handleDeleteImportPath}
+    onCancel={() => (editImportPath = null)}
   />
 {/if}
 
-<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" class="m-4 flex flex-col gap-4">
+<form {onsubmit} autocomplete="off" class="m-4 flex flex-col gap-4">
   <table class="text-left">
     <tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
       {#each validatedPaths as validatedPath, listIndex}
@@ -212,7 +208,7 @@
               icon={mdiPencilOutline}
               title={$t('edit_import_path')}
               size="16"
-              on:click={() => {
+              onclick={() => {
                 editImportPath = listIndex;
                 editedImportPath = validatedPath.importPath;
               }}
@@ -236,7 +232,7 @@
           ><Button
             type="button"
             size="sm"
-            on:click={() => {
+            onclick={() => {
               addImportPath = true;
             }}>{$t('add_path')}</Button
           ></td
@@ -246,12 +242,12 @@
   </table>
   <div class="flex justify-between w-full">
     <div class="justify-end gap-2">
-      <Button size="sm" color="gray" on:click={() => revalidate()}
+      <Button size="sm" color="gray" onclick={() => revalidate()}
         ><Icon path={mdiRefresh} size={20} />{$t('validate')}</Button
       >
     </div>
     <div class="justify-end gap-2">
-      <Button size="sm" color="gray" on:click={() => handleCancel()}>{$t('cancel')}</Button>
+      <Button size="sm" color="gray" onclick={onCancel}>{$t('cancel')}</Button>
       <Button size="sm" type="submit">{$t('save')}</Button>
     </div>
   </div>
diff --git a/web/src/lib/components/forms/library-rename-form.svelte b/web/src/lib/components/forms/library-rename-form.svelte
index e09e0a4f2b..3f20709474 100644
--- a/web/src/lib/components/forms/library-rename-form.svelte
+++ b/web/src/lib/components/forms/library-rename-form.svelte
@@ -1,31 +1,29 @@
 <script lang="ts">
   import type { LibraryResponseDto } from '@immich/sdk';
-  import { createEventDispatcher } from 'svelte';
   import Button from '../elements/buttons/button.svelte';
   import { t } from 'svelte-i18n';
 
-  export let library: Partial<LibraryResponseDto>;
+  interface Props {
+    library: Partial<LibraryResponseDto>;
+    onCancel: () => void;
+    onSubmit: (library: Partial<LibraryResponseDto>) => void;
+  }
 
-  const dispatch = createEventDispatcher<{
-    cancel: void;
-    submit: Partial<LibraryResponseDto>;
-  }>();
-  const handleCancel = () => {
-    dispatch('cancel');
-  };
+  let { library = $bindable(), onCancel, onSubmit }: Props = $props();
 
-  const handleSubmit = () => {
-    dispatch('submit', { ...library });
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+    onSubmit({ ...library });
   };
 </script>
 
-<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" class="m-4 flex flex-col gap-2">
+<form {onsubmit} autocomplete="off" class="m-4 flex flex-col gap-2">
   <div class="flex flex-col gap-2">
     <label class="immich-form-label" for="path">{$t('name')}</label>
     <input class="immich-form-input" id="name" name="name" type="text" bind:value={library.name} />
   </div>
   <div class="flex w-full justify-end gap-2 pt-2">
-    <Button size="sm" color="gray" on:click={() => handleCancel()}>{$t('cancel')}</Button>
+    <Button size="sm" color="gray" onclick={onCancel}>{$t('cancel')}</Button>
     <Button size="sm" type="submit">{$t('save')}</Button>
   </div>
 </form>
diff --git a/web/src/lib/components/forms/library-scan-settings-form.svelte b/web/src/lib/components/forms/library-scan-settings-form.svelte
index 5e025a406a..68e99641e8 100644
--- a/web/src/lib/components/forms/library-scan-settings-form.svelte
+++ b/web/src/lib/components/forms/library-scan-settings-form.svelte
@@ -1,22 +1,28 @@
 <script lang="ts">
   import { type LibraryResponseDto } from '@immich/sdk';
   import { mdiPencilOutline } from '@mdi/js';
-  import { createEventDispatcher, onMount } from 'svelte';
+  import { onMount } from 'svelte';
   import { handleError } from '../../utils/handle-error';
   import Button from '../elements/buttons/button.svelte';
   import LibraryExclusionPatternForm from './library-exclusion-pattern-form.svelte';
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import { t } from 'svelte-i18n';
 
-  export let library: Partial<LibraryResponseDto>;
+  interface Props {
+    library: Partial<LibraryResponseDto>;
+    onCancel: () => void;
+    onSubmit: (library: Partial<LibraryResponseDto>) => void;
+  }
 
-  let addExclusionPattern = false;
-  let editExclusionPattern: number | null = null;
+  let { library = $bindable(), onCancel, onSubmit }: Props = $props();
 
-  let exclusionPatternToAdd: string;
-  let editedExclusionPattern: string;
+  let addExclusionPattern = $state(false);
+  let editExclusionPattern: number | null = $state(null);
 
-  let exclusionPatterns: string[] = [];
+  let exclusionPatternToAdd: string = $state('');
+  let editedExclusionPattern: string = $state('');
+
+  let exclusionPatterns: string[] = $state([]);
 
   onMount(() => {
     if (library.exclusionPatterns) {
@@ -26,18 +32,6 @@
     }
   });
 
-  const dispatch = createEventDispatcher<{
-    cancel: void;
-    submit: Partial<LibraryResponseDto>;
-  }>();
-  const handleCancel = () => {
-    dispatch('cancel');
-  };
-
-  const handleSubmit = () => {
-    dispatch('submit', library);
-  };
-
   const handleAddExclusionPattern = () => {
     if (!addExclusionPattern) {
       return;
@@ -99,6 +93,11 @@
       editExclusionPattern = null;
     }
   };
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+    onSubmit(library);
+  };
 </script>
 
 {#if addExclusionPattern}
@@ -106,10 +105,8 @@
     submitText={$t('add')}
     bind:exclusionPattern={exclusionPatternToAdd}
     {exclusionPatterns}
-    on:submit={handleAddExclusionPattern}
-    on:cancel={() => {
-      addExclusionPattern = false;
-    }}
+    onSubmit={handleAddExclusionPattern}
+    onCancel={() => (addExclusionPattern = false)}
   />
 {/if}
 
@@ -119,15 +116,13 @@
     isEditing={true}
     bind:exclusionPattern={editedExclusionPattern}
     {exclusionPatterns}
-    on:submit={handleEditExclusionPattern}
-    on:delete={handleDeleteExclusionPattern}
-    on:cancel={() => {
-      editExclusionPattern = null;
-    }}
+    onSubmit={handleEditExclusionPattern}
+    onDelete={handleDeleteExclusionPattern}
+    onCancel={() => (editExclusionPattern = null)}
   />
 {/if}
 
-<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" class="m-4 flex flex-col gap-4">
+<form {onsubmit} autocomplete="off" class="m-4 flex flex-col gap-4">
   <table class="w-full text-left">
     <tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
       {#each exclusionPatterns as exclusionPattern, listIndex}
@@ -145,7 +140,7 @@
               icon={mdiPencilOutline}
               title={$t('edit_exclusion_pattern')}
               size="16"
-              on:click={() => {
+              onclick={() => {
                 editExclusionPattern = listIndex;
                 editedExclusionPattern = exclusionPattern;
               }}
@@ -168,7 +163,7 @@
         <td class="w-1/4 text-ellipsis px-4 text-sm"
           ><Button
             size="sm"
-            on:click={() => {
+            onclick={() => {
               addExclusionPattern = true;
             }}>{$t('add_exclusion_pattern')}</Button
           ></td
@@ -178,7 +173,7 @@
   </table>
 
   <div class="flex w-full justify-end gap-4">
-    <Button size="sm" color="gray" on:click={() => handleCancel()}>{$t('cancel')}</Button>
+    <Button size="sm" color="gray" onclick={onCancel}>{$t('cancel')}</Button>
     <Button size="sm" type="submit">{$t('save')}</Button>
   </div>
 </form>
diff --git a/web/src/lib/components/forms/library-user-picker-form.svelte b/web/src/lib/components/forms/library-user-picker-form.svelte
index 4b4e7308dc..137a49921a 100644
--- a/web/src/lib/components/forms/library-user-picker-form.svelte
+++ b/web/src/lib/components/forms/library-user-picker-form.svelte
@@ -1,5 +1,4 @@
 <script lang="ts">
-  import { createEventDispatcher } from 'svelte';
   import Button from '../elements/buttons/button.svelte';
   import FullScreenModal from '../shared-components/full-screen-modal.svelte';
   import { mdiFolderSync } from '@mdi/js';
@@ -9,33 +8,37 @@
   import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
   import { t } from 'svelte-i18n';
 
-  let ownerId: string = $user.id;
+  interface Props {
+    onCancel: () => void;
+    onSubmit: (ownerId: string) => void;
+  }
 
-  let userOptions: { value: string; text: string }[] = [];
+  let { onCancel, onSubmit }: Props = $props();
+
+  let ownerId: string = $state($user.id);
+
+  let userOptions: { value: string; text: string }[] = $state([]);
 
   onMount(async () => {
     const users = await searchUsersAdmin({});
     userOptions = users.map((user) => ({ value: user.id, text: user.name }));
   });
 
-  const dispatch = createEventDispatcher<{
-    cancel: void;
-    submit: { ownerId: string };
-    delete: void;
-  }>();
-
-  const handleCancel = () => dispatch('cancel');
-  const handleSubmit = () => dispatch('submit', { ownerId });
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+    onSubmit(ownerId);
+  };
 </script>
 
-<FullScreenModal title={$t('select_library_owner')} icon={mdiFolderSync} onClose={handleCancel}>
-  <form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="select-library-owner-form">
+<FullScreenModal title={$t('select_library_owner')} icon={mdiFolderSync} onClose={onCancel}>
+  <form {onsubmit} autocomplete="off" id="select-library-owner-form">
     <p class="p-5 text-sm">{$t('admin.note_cannot_be_changed_later')}</p>
 
     <SettingSelect bind:value={ownerId} options={userOptions} name="user" />
   </form>
-  <svelte:fragment slot="sticky-bottom">
-    <Button color="gray" fullwidth on:click={() => handleCancel()}>{$t('cancel')}</Button>
+
+  {#snippet stickyBottom()}
+    <Button color="gray" fullwidth onclick={onCancel}>{$t('cancel')}</Button>
     <Button type="submit" fullwidth form="select-library-owner-form">{$t('create')}</Button>
-  </svelte:fragment>
+  {/snippet}
 </FullScreenModal>
diff --git a/web/src/lib/components/forms/login-form.svelte b/web/src/lib/components/forms/login-form.svelte
index b1af7a01f4..6c1dcecba3 100644
--- a/web/src/lib/components/forms/login-form.svelte
+++ b/web/src/lib/components/forms/login-form.svelte
@@ -12,16 +12,20 @@
   import PasswordField from '../shared-components/password-field.svelte';
   import { t } from 'svelte-i18n';
 
-  export let onSuccess: () => unknown | Promise<unknown>;
-  export let onFirstLogin: () => unknown | Promise<unknown>;
-  export let onOnboarding: () => unknown | Promise<unknown>;
+  interface Props {
+    onSuccess: () => unknown | Promise<unknown>;
+    onFirstLogin: () => unknown | Promise<unknown>;
+    onOnboarding: () => unknown | Promise<unknown>;
+  }
 
-  let errorMessage: string;
-  let email = '';
-  let password = '';
-  let oauthError = '';
-  let loading = false;
-  let oauthLoading = true;
+  let { onSuccess, onFirstLogin, onOnboarding }: Props = $props();
+
+  let errorMessage: string = $state('');
+  let email = $state('');
+  let password = $state('');
+  let oauthError = $state('');
+  let loading = $state(false);
+  let oauthLoading = $state(true);
 
   onMount(async () => {
     if (!$featureFlags.oauth) {
@@ -29,9 +33,9 @@
       return;
     }
 
-    if (oauth.isCallback(window.location)) {
+    if (oauth.isCallback(globalThis.location)) {
       try {
-        await oauth.login(window.location);
+        await oauth.login(globalThis.location);
         await onSuccess();
         return;
       } catch (error) {
@@ -42,9 +46,9 @@
     }
 
     try {
-      if ($featureFlags.oauthAutoLaunch && !oauth.isAutoLaunchDisabled(window.location)) {
+      if ($featureFlags.oauthAutoLaunch && !oauth.isAutoLaunchDisabled(globalThis.location)) {
         await goto(`${AppRoute.AUTH_LOGIN}?autoLaunch=0`, { replaceState: true });
-        await oauth.authorize(window.location);
+        await oauth.authorize(globalThis.location);
         return;
       }
     } catch (error) {
@@ -81,16 +85,21 @@
   const handleOAuthLogin = async () => {
     oauthLoading = true;
     oauthError = '';
-    const success = await oauth.authorize(window.location);
+    const success = await oauth.authorize(globalThis.location);
     if (!success) {
       oauthLoading = false;
       oauthError = $t('errors.unable_to_login_with_oauth');
     }
   };
+
+  const onsubmit = async (event: Event) => {
+    event.preventDefault();
+    await handleLogin();
+  };
 </script>
 
 {#if !oauthLoading && $featureFlags.passwordLogin}
-  <form on:submit|preventDefault={handleLogin} class="mt-5 flex flex-col gap-5">
+  <form {onsubmit} class="mt-5 flex flex-col gap-5">
     {#if errorMessage}
       <p class="text-red-400" transition:fade>
         {errorMessage}
@@ -150,7 +159,7 @@
       size="lg"
       fullwidth
       color={$featureFlags.passwordLogin ? 'secondary' : 'primary'}
-      on:click={handleOAuthLogin}
+      onclick={handleOAuthLogin}
     >
       {#if oauthLoading}
         <span class="h-6">
diff --git a/web/src/lib/components/forms/tag-asset-form.svelte b/web/src/lib/components/forms/tag-asset-form.svelte
index 7500a6faac..3419e62a18 100644
--- a/web/src/lib/components/forms/tag-asset-form.svelte
+++ b/web/src/lib/components/forms/tag-asset-form.svelte
@@ -5,18 +5,22 @@
   import Combobox, { type ComboBoxOption } from '../shared-components/combobox.svelte';
   import FullScreenModal from '../shared-components/full-screen-modal.svelte';
   import { onMount } from 'svelte';
-  import { getAllTags, type TagResponseDto } from '@immich/sdk';
+  import { getAllTags, upsertTags, type TagResponseDto } from '@immich/sdk';
   import Icon from '$lib/components/elements/icon.svelte';
-  import { AppRoute } from '$lib/constants';
-  import FormatMessage from '$lib/components/i18n/format-message.svelte';
+  import { SvelteSet } from 'svelte/reactivity';
 
-  export let onTag: (tagIds: string[]) => void;
-  export let onCancel: () => void;
+  interface Props {
+    onTag: (tagIds: string[]) => void;
+    onCancel: () => void;
+  }
 
-  let allTags: TagResponseDto[] = [];
-  $: tagMap = Object.fromEntries(allTags.map((tag) => [tag.id, tag]));
-  let selectedIds = new Set<string>();
-  $: disabled = selectedIds.size === 0;
+  let { onTag, onCancel }: Props = $props();
+
+  let allTags: TagResponseDto[] = $state([]);
+  let tagMap = $derived(Object.fromEntries(allTags.map((tag) => [tag.id, tag])));
+  let selectedIds = $state(new SvelteSet<string>());
+  let disabled = $derived(selectedIds.size === 0);
+  let allowCreate: boolean = $state(true);
 
   onMount(async () => {
     allTags = await getAllTags();
@@ -24,36 +28,38 @@
 
   const handleSubmit = () => onTag([...selectedIds]);
 
-  const handleSelect = (option?: ComboBoxOption) => {
+  const handleSelect = async (option?: ComboBoxOption) => {
     if (!option) {
       return;
     }
 
-    selectedIds.add(option.value);
-    selectedIds = selectedIds;
+    if (option.id) {
+      selectedIds.add(option.value);
+    } else {
+      const [newTag] = await upsertTags({ tagUpsertDto: { tags: [option.label] } });
+      allTags.push(newTag);
+      selectedIds.add(newTag.id);
+    }
   };
 
   const handleRemove = (tag: string) => {
     selectedIds.delete(tag);
-    selectedIds = selectedIds;
+  };
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+    handleSubmit();
   };
 </script>
 
 <FullScreenModal title={$t('tag_assets')} icon={mdiTag} onClose={onCancel}>
-  <div class="text-sm">
-    <p>
-      <FormatMessage key="tag_not_found_question" let:message>
-        <a href={AppRoute.TAGS} class="text-immich-primary dark:text-immich-dark-primary underline">
-          {message}
-        </a>
-      </FormatMessage>
-    </p>
-  </div>
-  <form on:submit|preventDefault={handleSubmit} autocomplete="off" id="create-tag-form">
+  <form {onsubmit} autocomplete="off" id="create-tag-form">
     <div class="my-4 flex flex-col gap-2">
       <Combobox
-        on:select={({ detail: option }) => handleSelect(option)}
+        onSelect={handleSelect}
         label={$t('tag')}
+        {allowCreate}
+        defaultFirstOption
         options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))}
         placeholder={$t('search_tags')}
       />
@@ -77,7 +83,7 @@
             type="button"
             class="text-gray-100 dark:text-immich-dark-gray bg-immich-primary/95 dark:bg-immich-dark-primary/95 rounded-tr-full rounded-br-full place-items-center place-content-center pr-2 pl-1 py-1 hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
             title="Remove tag"
-            on:click={() => handleRemove(tagId)}
+            onclick={() => handleRemove(tagId)}
           >
             <Icon path={mdiClose} />
           </button>
@@ -86,8 +92,8 @@
     {/each}
   </section>
 
-  <svelte:fragment slot="sticky-bottom">
-    <Button color="gray" fullwidth on:click={onCancel}>{$t('cancel')}</Button>
+  {#snippet stickyBottom()}
+    <Button color="gray" fullwidth onclick={onCancel}>{$t('cancel')}</Button>
     <Button type="submit" fullwidth form="create-tag-form" {disabled}>{$t('tag_assets')}</Button>
-  </svelte:fragment>
+  {/snippet}
 </FullScreenModal>
diff --git a/web/src/lib/components/i18n/__test__/format-message.spec.ts b/web/src/lib/components/i18n/__test__/format-message.spec.ts
index 52eb77c80b..a496237f90 100644
--- a/web/src/lib/components/i18n/__test__/format-message.spec.ts
+++ b/web/src/lib/components/i18n/__test__/format-message.spec.ts
@@ -5,6 +5,8 @@ import { render, screen } from '@testing-library/svelte';
 import { init, locale, register, waitLocale, type Translations } from 'svelte-i18n';
 import { describe } from 'vitest';
 
+const getSanitizedHTML = (container: HTMLElement) => container.innerHTML.replaceAll('<!---->', '');
+
 describe('FormatMessage component', () => {
   beforeAll(async () => {
     register('en', () =>
@@ -57,7 +59,7 @@ describe('FormatMessage component', () => {
       key: 'html' as Translations,
       values: { name: 'test' },
     });
-    expect(container.innerHTML).toBe('Hello <strong>test</strong>');
+    expect(getSanitizedHTML(container)).toBe('Hello <strong>test</strong>');
   });
 
   it('renders a message with html and plural', () => {
@@ -65,7 +67,7 @@ describe('FormatMessage component', () => {
       key: 'plural' as Translations,
       values: { count: 1 },
     });
-    expect(container.innerHTML).toBe('You have <strong>1 item</strong>');
+    expect(getSanitizedHTML(container)).toBe('You have <strong>1 item</strong>');
   });
 
   it('protects agains XSS injection', () => {
@@ -85,7 +87,7 @@ describe('FormatMessage component', () => {
       key: 'plural_with_html' as Translations,
       values: { count: 10 },
     });
-    expect(container.innerHTML).toBe('You have <strong>10</strong> items');
+    expect(getSanitizedHTML(container)).toBe('You have <strong>10</strong> items');
   });
 
   it('supports html tags inside select', () => {
@@ -93,7 +95,7 @@ describe('FormatMessage component', () => {
       key: 'select_with_html' as Translations,
       values: { status: true },
     });
-    expect(container.innerHTML).toBe('Item is <strong>disabled</strong>');
+    expect(getSanitizedHTML(container)).toBe('Item is <strong>disabled</strong>');
   });
 
   it('supports html tags inside selectordinal', () => {
@@ -101,6 +103,6 @@ describe('FormatMessage component', () => {
       key: 'ordinal_with_html' as Translations,
       values: { count: 4 },
     });
-    expect(container.innerHTML).toBe('<strong>4th</strong> item');
+    expect(getSanitizedHTML(container)).toBe('<strong>4th</strong> item');
   });
 });
diff --git a/web/src/lib/components/i18n/__test__/format-tag-b.svelte b/web/src/lib/components/i18n/__test__/format-tag-b.svelte
index 122358c6b7..6e8b2412e1 100644
--- a/web/src/lib/components/i18n/__test__/format-tag-b.svelte
+++ b/web/src/lib/components/i18n/__test__/format-tag-b.svelte
@@ -3,12 +3,18 @@
   import FormatMessage from '../format-message.svelte';
   import type { ComponentProps } from 'svelte';
 
-  export let key: Translations;
-  export let values: ComponentProps<FormatMessage>['values'];
+  interface Props {
+    key: Translations;
+    values: ComponentProps<typeof FormatMessage>['values'];
+  }
+
+  let { key, values }: Props = $props();
 </script>
 
-<FormatMessage {key} {values} let:tag let:message>
-  {#if tag === 'b'}
-    <strong>{message}</strong>
-  {/if}
+<FormatMessage {key} {values}>
+  {#snippet children({ tag, message })}
+    {#if tag === 'b'}
+      <strong>{message}</strong>
+    {/if}
+  {/snippet}
 </FormatMessage>
diff --git a/web/src/lib/components/i18n/format-bold-message.svelte b/web/src/lib/components/i18n/format-bold-message.svelte
index 052b220edc..ab497add33 100644
--- a/web/src/lib/components/i18n/format-bold-message.svelte
+++ b/web/src/lib/components/i18n/format-bold-message.svelte
@@ -1,14 +1,20 @@
 <script lang="ts">
+  import type { InterpolationValues } from '$lib/components/i18n/format-message';
   import FormatMessage from '$lib/components/i18n/format-message.svelte';
-  import type { InterpolationValues } from '$lib/components/i18n/format-message.svelte';
   import type { Translations } from 'svelte-i18n';
 
-  export let key: Translations;
-  export let values: InterpolationValues = {};
+  interface Props {
+    key: Translations;
+    values?: InterpolationValues;
+  }
+
+  let { key, values = {} }: Props = $props();
 </script>
 
-<FormatMessage {key} {values} let:message let:tag>
-  {#if tag === 'b'}
-    <b>{message}</b>
-  {/if}
+<FormatMessage {key} {values}>
+  {#snippet children({ message, tag })}
+    {#if tag === 'b'}
+      <b>{message}</b>
+    {/if}
+  {/snippet}
 </FormatMessage>
diff --git a/web/src/lib/components/i18n/format-message.svelte b/web/src/lib/components/i18n/format-message.svelte
index 48c59478c6..d65e1096fe 100644
--- a/web/src/lib/components/i18n/format-message.svelte
+++ b/web/src/lib/components/i18n/format-message.svelte
@@ -1,10 +1,5 @@
-<script lang="ts" context="module">
-  import type { FormatXMLElementFn, PrimitiveType } from 'intl-messageformat';
-  export type InterpolationValues = Record<string, PrimitiveType | FormatXMLElementFn<unknown>>;
-</script>
-
 <script lang="ts">
-  import { IntlMessageFormat } from 'intl-messageformat';
+  import { IntlMessageFormat, type FormatXMLElementFn } from 'intl-messageformat';
   import {
     TYPE,
     type MessageFormatElement,
@@ -12,14 +7,20 @@
     type SelectElement,
   } from '@formatjs/icu-messageformat-parser';
   import { locale as i18nLocale, json, type Translations } from 'svelte-i18n';
+  import type { InterpolationValues } from '$lib/components/i18n/format-message';
 
   type MessagePart = {
     message: string;
     tag?: string;
   };
 
-  export let key: Translations;
-  export let values: InterpolationValues = {};
+  interface Props {
+    key: Translations;
+    values?: InterpolationValues;
+    children?: import('svelte').Snippet<[{ tag?: string; message?: string }]>;
+  }
+
+  let { key, values = {}, children }: Props = $props();
 
   const getLocale = (locale?: string | null) => {
     if (locale == null) {
@@ -96,9 +97,9 @@
     }
   };
 
-  $: message = ($json(key) as string) || key;
-  $: locale = getLocale($i18nLocale);
-  $: parts = getParts(message, locale);
+  let message = $derived(($json(key) as string) || key);
+  let locale = $derived(getLocale($i18nLocale));
+  let parts = $derived(getParts(message, locale));
 </script>
 
 <!--
@@ -130,7 +131,7 @@ Result: Visit <a href="">docs</a> <strong>now</strong>
 -->
 {#each parts as { tag, message }}
   {#if tag}
-    <slot {tag} {message}>{message}</slot>
+    {#if children}{@render children({ tag, message })}{:else}{message}{/if}
   {:else}
     {message}
   {/if}
diff --git a/web/src/lib/components/i18n/format-message.ts b/web/src/lib/components/i18n/format-message.ts
new file mode 100644
index 0000000000..d93c5c9b1a
--- /dev/null
+++ b/web/src/lib/components/i18n/format-message.ts
@@ -0,0 +1,2 @@
+import type { FormatXMLElementFn, PrimitiveType } from 'intl-messageformat';
+export type InterpolationValues = Record<string, PrimitiveType | FormatXMLElementFn<unknown>>;
diff --git a/web/src/lib/components/layouts/user-page-layout.svelte b/web/src/lib/components/layouts/user-page-layout.svelte
index 5bca13b060..6822035b19 100644
--- a/web/src/lib/components/layouts/user-page-layout.svelte
+++ b/web/src/lib/components/layouts/user-page-layout.svelte
@@ -1,4 +1,4 @@
-<script lang="ts" context="module">
+<script lang="ts" module>
   export const headerId = 'user-page-header';
 </script>
 
@@ -7,39 +7,60 @@
   import NavigationBar from '../shared-components/navigation-bar/navigation-bar.svelte';
   import SideBar from '../shared-components/side-bar/side-bar.svelte';
   import AdminSideBar from '../shared-components/side-bar/admin-side-bar.svelte';
+  import { useActions, type ActionArray } from '$lib/actions/use-actions';
+  import type { Snippet } from 'svelte';
 
-  export let hideNavbar = false;
-  export let showUploadButton = false;
-  export let title: string | undefined = undefined;
-  export let description: string | undefined = undefined;
-  export let scrollbar = true;
-  export let admin = false;
+  interface Props {
+    hideNavbar?: boolean;
+    showUploadButton?: boolean;
+    title?: string | undefined;
+    description?: string | undefined;
+    scrollbar?: boolean;
+    admin?: boolean;
+    use?: ActionArray;
+    header?: Snippet;
+    sidebar?: Snippet;
+    buttons?: Snippet;
+    children?: Snippet;
+  }
 
-  $: scrollbarClass = scrollbar ? 'immich-scrollbar p-2 pb-8' : 'scrollbar-hidden';
-  $: hasTitleClass = title ? 'top-16 h-[calc(100%-theme(spacing.16))]' : 'top-0 h-full';
+  let {
+    hideNavbar = false,
+    showUploadButton = false,
+    title = undefined,
+    description = undefined,
+    scrollbar = true,
+    admin = false,
+    use = [],
+    header,
+    sidebar,
+    buttons,
+    children,
+  }: Props = $props();
+
+  let scrollbarClass = $derived(scrollbar ? 'immich-scrollbar p-2 pb-8' : 'scrollbar-hidden');
+  let hasTitleClass = $derived(title ? 'top-16 h-[calc(100%-theme(spacing.16))]' : 'top-0 h-full');
 </script>
 
 <header>
   {#if !hideNavbar}
-    <NavigationBar {showUploadButton} on:uploadClicked={() => openFileUploadDialog()} />
+    <NavigationBar {showUploadButton} onUploadClick={() => openFileUploadDialog()} />
   {/if}
 
-  <slot name="header" />
+  {@render header?.()}
 </header>
 <main
   tabindex="-1"
   class="relative grid h-screen grid-cols-[theme(spacing.18)_auto] overflow-hidden bg-immich-bg pt-[var(--navbar-height)] dark:bg-immich-dark-bg md:grid-cols-[theme(spacing.64)_auto]"
 >
-  <slot name="sidebar">
-    {#if admin}
-      <AdminSideBar />
-    {:else}
-      <SideBar />
-    {/if}
-  </slot>
+  {#if sidebar}{@render sidebar()}{:else if admin}
+    <AdminSideBar />
+  {:else}
+    <SideBar />
+  {/if}
 
   <section class="relative">
-    {#if title || $$slots.buttons}
+    {#if title || buttons}
       <div
         class="absolute flex h-16 w-full place-items-center justify-between border-b p-4 dark:border-immich-dark-gray dark:text-immich-dark-fg"
       >
@@ -51,12 +72,12 @@
             <p class="text-sm text-gray-400 dark:text-gray-600">{description}</p>
           {/if}
         </div>
-        <slot name="buttons" />
+        {@render buttons?.()}
       </div>
     {/if}
 
-    <div class="{scrollbarClass} scrollbar-stable absolute {hasTitleClass} w-full overflow-y-auto">
-      <slot />
+    <div class="{scrollbarClass} scrollbar-stable absolute {hasTitleClass} w-full overflow-y-auto" use:useActions={use}>
+      {@render children?.()}
     </div>
   </section>
 </main>
diff --git a/web/src/lib/components/map-page/map-settings-modal.svelte b/web/src/lib/components/map-page/map-settings-modal.svelte
index b442396c84..270978e120 100644
--- a/web/src/lib/components/map-page/map-settings-modal.svelte
+++ b/web/src/lib/components/map-page/map-settings-modal.svelte
@@ -4,30 +4,30 @@
   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
   import type { MapSettings } from '$lib/stores/preferences.store';
   import { Duration } from 'luxon';
-  import { createEventDispatcher } from 'svelte';
   import { t } from 'svelte-i18n';
   import { fly } from 'svelte/transition';
   import Button from '../elements/buttons/button.svelte';
   import LinkButton from '../elements/buttons/link-button.svelte';
   import DateInput from '../elements/date-input.svelte';
 
-  export let settings: MapSettings;
-  let customDateRange = !!settings.dateAfter || !!settings.dateBefore;
+  interface Props {
+    settings: MapSettings;
+    onClose: () => void;
+    onSave: (settings: MapSettings) => void;
+  }
 
-  const dispatch = createEventDispatcher<{
-    close: void;
-    save: MapSettings;
-  }>();
+  let { settings = $bindable(), onClose, onSave }: Props = $props();
 
-  const handleClose = () => dispatch('close');
+  let customDateRange = $state(!!settings.dateAfter || !!settings.dateBefore);
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+    onSave(settings);
+  };
 </script>
 
-<FullScreenModal title={$t('map_settings')} onClose={handleClose}>
-  <form
-    on:submit|preventDefault={() => dispatch('save', settings)}
-    class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary"
-    id="map-settings-form"
-  >
+<FullScreenModal title={$t('map_settings')} {onClose}>
+  <form {onsubmit} class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary" id="map-settings-form">
     <SettingSwitch title={$t('allow_dark_mode')} bind:checked={settings.allowDarkMode} />
     <SettingSwitch title={$t('only_favorites')} bind:checked={settings.onlyFavorites} />
     <SettingSwitch title={$t('include_archived')} bind:checked={settings.includeArchived} />
@@ -51,7 +51,7 @@
         </div>
         <div class="flex justify-center text-xs">
           <LinkButton
-            on:click={() => {
+            onclick={() => {
               customDateRange = false;
               settings.dateAfter = '';
               settings.dateBefore = '';
@@ -96,7 +96,7 @@
         />
         <div class="text-xs">
           <LinkButton
-            on:click={() => {
+            onclick={() => {
               customDateRange = true;
               settings.relativeDate = '';
             }}
@@ -107,8 +107,9 @@
       </div>
     {/if}
   </form>
-  <svelte:fragment slot="sticky-bottom">
-    <Button color="gray" size="sm" fullwidth on:click={handleClose}>{$t('cancel')}</Button>
+
+  {#snippet stickyBottom()}
+    <Button color="gray" size="sm" fullwidth onclick={onClose}>{$t('cancel')}</Button>
     <Button type="submit" size="sm" fullwidth form="map-settings-form">{$t('save')}</Button>
-  </svelte:fragment>
+  {/snippet}
 </FullScreenModal>
diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte
index ae6416873e..a45274bc1c 100644
--- a/web/src/lib/components/memory-page/memory-viewer.svelte
+++ b/web/src/lib/components/memory-page/memory-viewer.svelte
@@ -17,6 +17,7 @@
   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
   import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
   import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
+  import { cancelMultiselect } from '$lib/utils/asset-utils';
   import { AppRoute, QueryParameter } from '$lib/constants';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
   import { type Viewport } from '$lib/stores/assets.store';
@@ -42,8 +43,11 @@
   import { onMount } from 'svelte';
   import { t } from 'svelte-i18n';
   import { tweened } from 'svelte/motion';
-  import { derived } from 'svelte/store';
+  import { derived as storeDerived } from 'svelte/store';
   import { fade } from 'svelte/transition';
+  import { preferences } from '$lib/stores/user.store';
+  import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
+  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 
   type MemoryIndex = {
     memoryIndex: number;
@@ -59,19 +63,21 @@
     nextMemory?: MemoryLaneResponseDto;
   };
 
-  let memoryGallery: HTMLElement;
-  let memoryWrapper: HTMLElement;
-  let galleryInView = false;
-  let paused = false;
-  let selectedAssets: Set<AssetResponseDto> = new Set();
-  let current: MemoryAsset | undefined = undefined;
+  let memoryGallery: HTMLElement | undefined = $state();
+  let memoryWrapper: HTMLElement | undefined = $state();
+  let galleryInView = $state(false);
+  let paused = $state(false);
+  let current: MemoryAsset | undefined = $state(undefined);
   // let memories: MemoryAsset[] = [];
-  let resetPromise = Promise.resolve();
+  let resetPromise = $state(Promise.resolve());
 
   const { isViewing } = assetViewingStore;
-  const viewport: Viewport = { width: 0, height: 0 };
-  const progress = tweened<number>(0, { duration: (from: number, to: number) => (to ? 5000 * (to - from) : 0) });
-  const memories = derived(memoryStore, (memories) => {
+  const viewport: Viewport = $state({ width: 0, height: 0 });
+  const assetInteraction = new AssetInteraction();
+  const progressBarController = tweened<number>(0, {
+    duration: (from: number, to: number) => (to ? 5000 * (to - from) : 0),
+  });
+  const memories = storeDerived(memoryStore, (memories) => {
     memories = memories ?? [];
     const memoryAssets: MemoryAsset[] = [];
     let previous: MemoryAsset | undefined;
@@ -100,13 +106,6 @@
     return memoryAssets;
   });
 
-  $: isMultiSelectionMode = selectedAssets.size > 0;
-  $: isAllArchived = [...selectedAssets].every((asset) => asset.isArchived);
-  $: isAllFavorite = [...selectedAssets].every((asset) => asset.isFavorite);
-  $: selectedAssets = galleryInView ? selectedAssets : new Set();
-  $: handlePromiseError(handleProgress($progress));
-  $: handlePromiseError(handleAction(galleryInView ? 'pause' : 'play'));
-
   const loadFromParams = (memories: MemoryAsset[], page: typeof $page | NavigationTarget | null) => {
     const assetId = page?.params?.assetId ?? page?.url.searchParams.get(QueryParameter.ID) ?? undefined;
     handlePromiseError(handleAction($isViewing ? 'pause' : 'reset'));
@@ -130,24 +129,24 @@
   const handleNextMemory = () => handleNavigate(current?.nextMemory?.assets[0]);
   const handlePreviousMemory = () => handleNavigate(current?.previousMemory?.assets[0]);
   const handleEscape = async () => goto(AppRoute.PHOTOS);
-  const handleSelectAll = () => (selectedAssets = new Set(current?.memory.assets || []));
+  const handleSelectAll = () => assetInteraction.selectAssets(current?.memory.assets || []);
   const handleAction = async (action: 'reset' | 'pause' | 'play') => {
     switch (action) {
       case 'play': {
         paused = false;
-        await progress.set(1);
+        await progressBarController.set(1);
         break;
       }
 
       case 'pause': {
         paused = true;
-        await progress.set($progress);
+        await progressBarController.set($progressBarController);
         break;
       }
 
       case 'reset': {
         paused = false;
-        resetPromise = progress.set(0);
+        resetPromise = progressBarController.set(0);
         break;
       }
     }
@@ -159,6 +158,7 @@
     }
 
     if (progress === 1) {
+      await progressBarController.set(0);
       await (current?.next ? handleNextAsset() : handleAction('pause'));
     }
   };
@@ -210,6 +210,14 @@
 
     current = loadFromParams($memories, target);
   });
+
+  $effect(() => {
+    handlePromiseError(handleProgress($progressBarController));
+  });
+
+  $effect(() => {
+    handlePromiseError(handleAction(galleryInView ? 'pause' : 'play'));
+  });
 </script>
 
 <svelte:window
@@ -224,24 +232,30 @@
       ]}
 />
 
-{#if isMultiSelectionMode}
+{#if assetInteraction.selectionActive}
   <div class="sticky top-0 z-[90]">
-    <AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
+    <AssetSelectControlBar
+      assets={assetInteraction.selectedAssets}
+      clearSelect={() => cancelMultiselect(assetInteraction)}
+    >
       <CreateSharedLink />
-      <CircleIconButton title={$t('select_all')} icon={mdiSelectAll} on:click={handleSelectAll} />
+      <CircleIconButton title={$t('select_all')} icon={mdiSelectAll} onclick={handleSelectAll} />
 
       <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
         <AddToAlbum />
         <AddToAlbum shared />
       </ButtonContextMenu>
 
-      <FavoriteAction removeFavorite={isAllFavorite} onFavorite={handleUpdate} />
+      <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={handleUpdate} />
 
       <ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}>
         <DownloadAction menuItem />
         <ChangeDate menuItem />
         <ChangeLocation menuItem />
-        <ArchiveAction menuItem unarchive={isAllArchived} onArchive={handleRemove} />
+        <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} onArchive={handleRemove} />
+        {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
+          <TagAction menuItem />
+        {/if}
         <DeleteAssets menuItem onAssetDelete={handleRemove} />
       </ButtonContextMenu>
     </AssetSelectControlBar>
@@ -250,31 +264,34 @@
 
 <section id="memory-viewer" class="w-full bg-immich-dark-gray" bind:this={memoryWrapper}>
   {#if current && current.memory.assets.length > 0}
-    <ControlAppBar on:close={() => goto(AppRoute.PHOTOS)} forceDark>
-      <svelte:fragment slot="leading">
-        <p class="text-lg">
-          {$memoryLaneTitle(current.memory.yearsAgo)}
-        </p>
-      </svelte:fragment>
+    <ControlAppBar onClose={() => goto(AppRoute.PHOTOS)} forceDark>
+      {#snippet leading()}
+        {#if current}
+          <p class="text-lg">
+            {$memoryLaneTitle(current.memory.yearsAgo)}
+          </p>
+        {/if}
+      {/snippet}
 
       <div class="flex place-content-center place-items-center gap-2 overflow-hidden">
         <CircleIconButton
           title={paused ? $t('play_memories') : $t('pause_memories')}
           icon={paused ? mdiPlay : mdiPause}
-          on:click={() => handleAction(paused ? 'play' : 'pause')}
+          onclick={() => handleAction(paused ? 'play' : 'pause')}
           class="hover:text-black"
         />
 
         {#each current.memory.assets as asset, index}
           <a class="relative w-full py-2" href={asHref(asset)}>
-            <span class="absolute left-0 h-[2px] w-full bg-gray-500" />
+            <span class="absolute left-0 h-[2px] w-full bg-gray-500"></span>
             {#await resetPromise}
-              <span class="absolute left-0 h-[2px] bg-white" style:width={`${index < current.assetIndex ? 100 : 0}%`} />
+              <span class="absolute left-0 h-[2px] bg-white" style:width={`${index < current.assetIndex ? 100 : 0}%`}
+              ></span>
             {:then}
               <span
                 class="absolute left-0 h-[2px] bg-white"
-                style:width={`${index < current.assetIndex ? 100 : index > current.assetIndex ? 0 : $progress * 100}%`}
-              />
+                style:width={`${index < current.assetIndex ? 100 : index > current.assetIndex ? 0 : $progressBarController * 100}%`}
+              ></span>
             {/await}
           </a>
         {/each}
@@ -295,10 +312,10 @@
       >
         <button
           type="button"
-          on:click={() => memoryWrapper.scrollIntoView({ behavior: 'smooth' })}
+          onclick={() => memoryWrapper?.scrollIntoView({ behavior: 'smooth' })}
           disabled={!galleryInView}
         >
-          <CircleIconButton title={$t('hide_gallery')} icon={mdiChevronUp} color="light" />
+          <CircleIconButton title={$t('hide_gallery')} icon={mdiChevronUp} color="light" onclick={() => {}} />
         </button>
       </div>
     {/if}
@@ -313,7 +330,7 @@
             type="button"
             class="relative h-full w-full rounded-2xl"
             disabled={!current.previousMemory}
-            on:click={handlePreviousMemory}
+            onclick={handlePreviousMemory}
           >
             {#if current.previousMemory && current.previousMemory.assets.length > 0}
               <img
@@ -366,6 +383,7 @@
                 icon={mdiImageSearch}
                 title={$t('view_in_timeline')}
                 color="light"
+                onclick={() => {}}
               />
             </div>
             <!-- CONTROL BUTTONS -->
@@ -375,7 +393,7 @@
                   title={$t('previous_memory')}
                   icon={mdiChevronLeft}
                   color="dark"
-                  on:click={handlePreviousAsset}
+                  onclick={handlePreviousAsset}
                 />
               </div>
             {/if}
@@ -386,7 +404,7 @@
                   title={$t('next_memory')}
                   icon={mdiChevronRight}
                   color="dark"
-                  on:click={handleNextAsset}
+                  onclick={handleNextAsset}
                 />
               </div>
             {/if}
@@ -408,7 +426,7 @@
           <button
             type="button"
             class="relative h-full w-full rounded-2xl"
-            on:click={handleNextMemory}
+            onclick={handleNextMemory}
             disabled={!current.nextMemory}
           >
             {#if current.nextMemory && current.nextMemory.assets.length > 0}
@@ -450,7 +468,7 @@
           title={$t('show_gallery')}
           icon={mdiChevronDown}
           color="light"
-          on:click={() => memoryGallery.scrollIntoView({ behavior: 'smooth' })}
+          onclick={() => memoryGallery?.scrollIntoView({ behavior: 'smooth' })}
         />
       </div>
 
@@ -469,7 +487,7 @@
           onPrevious={handlePreviousAsset}
           assets={current.memory.assets}
           {viewport}
-          bind:selectedAssets
+          {assetInteraction}
         />
       </div>
     </section>
diff --git a/web/src/lib/components/onboarding-page/onboarding-card.svelte b/web/src/lib/components/onboarding-page/onboarding-card.svelte
index 9b2378ccd8..54951dfa09 100644
--- a/web/src/lib/components/onboarding-page/onboarding-card.svelte
+++ b/web/src/lib/components/onboarding-page/onboarding-card.svelte
@@ -1,9 +1,15 @@
 <script lang="ts">
   import Icon from '$lib/components/elements/icon.svelte';
+  import type { Snippet } from 'svelte';
   import { fade } from 'svelte/transition';
 
-  export let title: string | undefined = undefined;
-  export let icon: string | undefined = undefined;
+  interface Props {
+    title?: string | undefined;
+    icon?: string | undefined;
+    children?: Snippet;
+  }
+
+  let { title = undefined, icon = undefined, children }: Props = $props();
 </script>
 
 <div
@@ -23,5 +29,5 @@
       {/if}
     </div>
   {/if}
-  <slot />
+  {@render children?.()}
 </div>
diff --git a/web/src/lib/components/onboarding-page/onboarding-hello.svelte b/web/src/lib/components/onboarding-page/onboarding-hello.svelte
index 466e1d29f7..102465f019 100644
--- a/web/src/lib/components/onboarding-page/onboarding-hello.svelte
+++ b/web/src/lib/components/onboarding-page/onboarding-hello.svelte
@@ -7,7 +7,11 @@
   import { user } from '$lib/stores/user.store';
   import { t } from 'svelte-i18n';
 
-  export let onDone: () => void;
+  interface Props {
+    onDone: () => void;
+  }
+
+  let { onDone }: Props = $props();
 </script>
 
 <OnboardingCard>
@@ -18,7 +22,7 @@
   <p class="text-3xl pb-6 font-light">{$t('onboarding_welcome_description')}</p>
 
   <div class="w-full flex place-content-end">
-    <Button class="flex gap-2 place-content-center" on:click={() => onDone()}>
+    <Button class="flex gap-2 place-content-center" onclick={() => onDone()}>
       <p>{$t('theme')}</p>
       <Icon path={mdiArrowRight} size="18" />
     </Button>
diff --git a/web/src/lib/components/onboarding-page/onboarding-privacy.svelte b/web/src/lib/components/onboarding-page/onboarding-privacy.svelte
index da36f741f1..8ff8a9200d 100644
--- a/web/src/lib/components/onboarding-page/onboarding-privacy.svelte
+++ b/web/src/lib/components/onboarding-page/onboarding-privacy.svelte
@@ -10,10 +10,15 @@
   import { t } from 'svelte-i18n';
   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
 
-  export let onDone: () => void;
-  export let onPrevious: () => void;
+  interface Props {
+    onDone: () => void;
+    onPrevious: () => void;
+  }
 
-  let config: SystemConfigDto | null = null;
+  let { onDone, onPrevious }: Props = $props();
+
+  let config: SystemConfigDto | null = $state(null);
+  let adminSettingsComponent = $state<ReturnType<typeof AdminSettings>>();
 
   onMount(async () => {
     config = await getConfig();
@@ -26,38 +31,42 @@
   </p>
 
   {#if config && $user}
-    <AdminSettings bind:config let:handleSave>
-      <SettingSwitch
-        title={$t('admin.map_settings')}
-        subtitle={$t('admin.map_implications')}
-        bind:checked={config.map.enabled}
-      />
-      <SettingSwitch
-        title={$t('admin.version_check_settings')}
-        subtitle={$t('admin.version_check_implications')}
-        bind:checked={config.newVersionCheck.enabled}
-      />
-      <div class="flex pt-4">
-        <div class="w-full flex place-content-start">
-          <Button class="flex gap-2 place-content-center" on:click={() => onPrevious()}>
-            <Icon path={mdiArrowLeft} size="18" />
-            <p>{$t('theme')}</p>
-          </Button>
-        </div>
-        <div class="flex w-full place-content-end">
-          <Button
-            on:click={() => {
-              handleSave({ map: config?.map, newVersionCheck: config?.newVersionCheck });
-              onDone();
-            }}
-          >
-            <span class="flex place-content-center place-items-center gap-2">
-              {$t('admin.storage_template_settings')}
-              <Icon path={mdiArrowRight} size="18" />
-            </span>
-          </Button>
-        </div>
-      </div>
+    <AdminSettings bind:config bind:this={adminSettingsComponent}>
+      {#snippet children()}
+        {#if config}
+          <SettingSwitch
+            title={$t('admin.map_settings')}
+            subtitle={$t('admin.map_implications')}
+            bind:checked={config.map.enabled}
+          />
+          <SettingSwitch
+            title={$t('admin.version_check_settings')}
+            subtitle={$t('admin.version_check_implications')}
+            bind:checked={config.newVersionCheck.enabled}
+          />
+          <div class="flex pt-4">
+            <div class="w-full flex place-content-start">
+              <Button class="flex gap-2 place-content-center" onclick={() => onPrevious()}>
+                <Icon path={mdiArrowLeft} size="18" />
+                <p>{$t('theme')}</p>
+              </Button>
+            </div>
+            <div class="flex w-full place-content-end">
+              <Button
+                onclick={() => {
+                  adminSettingsComponent?.handleSave({ map: config?.map, newVersionCheck: config?.newVersionCheck });
+                  onDone();
+                }}
+              >
+                <span class="flex place-content-center place-items-center gap-2">
+                  {$t('admin.storage_template_settings')}
+                  <Icon path={mdiArrowRight} size="18" />
+                </span>
+              </Button>
+            </div>
+          </div>
+        {/if}
+      {/snippet}
     </AdminSettings>
   {/if}
 </OnboardingCard>
diff --git a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte
index 69809dd39d..b692a6f2de 100644
--- a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte
+++ b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte
@@ -12,10 +12,15 @@
   import { t } from 'svelte-i18n';
   import FormatMessage from '$lib/components/i18n/format-message.svelte';
 
-  export let onDone: () => void;
-  export let onPrevious: () => void;
+  interface Props {
+    onDone: () => void;
+    onPrevious: () => void;
+  }
 
-  let config: SystemConfigDto | null = null;
+  let { onDone, onPrevious }: Props = $props();
+
+  let config: SystemConfigDto | undefined = $state();
+  let adminSettingsComponent = $state<ReturnType<typeof AdminSettings>>();
 
   onMount(async () => {
     config = await getConfig();
@@ -24,45 +29,51 @@
 
 <OnboardingCard title={$t('admin.storage_template_settings')} icon={mdiHarddisk}>
   <p>
-    <FormatMessage key="admin.storage_template_onboarding_description" let:message>
-      <a class="underline" href="https://immich.app/docs/administration/storage-template">{message}</a>
+    <FormatMessage key="admin.storage_template_onboarding_description">
+      {#snippet children({ message })}
+        <a class="underline" href="https://immich.app/docs/administration/storage-template">{message}</a>
+      {/snippet}
     </FormatMessage>
   </p>
 
   {#if config && $user}
-    <AdminSettings bind:config let:defaultConfig let:savedConfig let:handleSave let:handleReset>
-      <StorageTemplateSettings
-        minified
-        disabled={$featureFlags.configFile}
-        {config}
-        {defaultConfig}
-        {savedConfig}
-        onSave={(config) => handleSave(config)}
-        onReset={(options) => handleReset(options)}
-        duration={0}
-      >
-        <div class="flex pt-4">
-          <div class="w-full flex place-content-start">
-            <Button class="flex gap-2 place-content-center" on:click={() => onPrevious()}>
-              <Icon path={mdiArrowLeft} size="18" />
-              <p>{$t('theme')}</p>
-            </Button>
-          </div>
-          <div class="flex w-full place-content-end">
-            <Button
-              on:click={() => {
-                handleSave({ storageTemplate: config?.storageTemplate });
-                onDone();
-              }}
-            >
-              <span class="flex place-content-center place-items-center gap-2">
-                {$t('done')}
-                <Icon path={mdiCheck} size="18" />
-              </span>
-            </Button>
-          </div>
-        </div>
-      </StorageTemplateSettings>
+    <AdminSettings bind:config bind:this={adminSettingsComponent}>
+      {#snippet children({ defaultConfig, savedConfig })}
+        {#if config}
+          <StorageTemplateSettings
+            minified
+            disabled={$featureFlags.configFile}
+            {config}
+            {defaultConfig}
+            {savedConfig}
+            onSave={(config) => adminSettingsComponent?.handleSave(config)}
+            onReset={(options) => adminSettingsComponent?.handleReset(options)}
+            duration={0}
+          >
+            <div class="flex pt-4">
+              <div class="w-full flex place-content-start">
+                <Button class="flex gap-2 place-content-center" onclick={() => onPrevious()}>
+                  <Icon path={mdiArrowLeft} size="18" />
+                  <p>{$t('theme')}</p>
+                </Button>
+              </div>
+              <div class="flex w-full place-content-end">
+                <Button
+                  onclick={() => {
+                    adminSettingsComponent?.handleSave({ storageTemplate: config?.storageTemplate });
+                    onDone();
+                  }}
+                >
+                  <span class="flex place-content-center place-items-center gap-2">
+                    {$t('done')}
+                    <Icon path={mdiCheck} size="18" />
+                  </span>
+                </Button>
+              </div>
+            </div>
+          </StorageTemplateSettings>
+        {/if}
+      {/snippet}
     </AdminSettings>
   {/if}
 </OnboardingCard>
diff --git a/web/src/lib/components/onboarding-page/onboarding-theme.svelte b/web/src/lib/components/onboarding-page/onboarding-theme.svelte
index 975dbd1ec3..4229cf9f67 100644
--- a/web/src/lib/components/onboarding-page/onboarding-theme.svelte
+++ b/web/src/lib/components/onboarding-page/onboarding-theme.svelte
@@ -8,7 +8,11 @@
   import { Theme } from '$lib/constants';
   import { t } from 'svelte-i18n';
 
-  export let onDone: () => void;
+  interface Props {
+    onDone: () => void;
+  }
+
+  let { onDone }: Props = $props();
 </script>
 
 <OnboardingCard icon={mdiThemeLightDark} title={$t('color_theme')}>
@@ -20,7 +24,7 @@
     <button
       type="button"
       class="w-1/2 aspect-square bg-immich-bg rounded-3xl transition-all shadow-sm hover:shadow-xl border-[3px] border-immich-dark-primary/80 border-immich-primary dark:border dark:border-transparent"
-      on:click={() => ($colorTheme.value = Theme.LIGHT)}
+      onclick={() => ($colorTheme.value = Theme.LIGHT)}
     >
       <div
         class="flex flex-col place-items-center place-content-center justify-around h-full w-full text-immich-primary"
@@ -32,7 +36,7 @@
     <button
       type="button"
       class="w-1/2 aspect-square bg-immich-dark-bg rounded-3xl dark:border-[3px] dark:border-immich-dark-primary/80 dark:border-immich-dark-primary border border-transparent"
-      on:click={() => ($colorTheme.value = Theme.DARK)}
+      onclick={() => ($colorTheme.value = Theme.DARK)}
     >
       <div
         class="flex flex-col place-items-center place-content-center justify-around h-full w-full text-immich-dark-primary"
@@ -45,7 +49,7 @@
 
   <div class="flex">
     <div class="w-full flex place-content-end">
-      <Button class="flex gap-2 place-content-center" on:click={() => onDone()}>
+      <Button class="flex gap-2 place-content-center" onclick={() => onDone()}>
         <p>{$t('privacy')}</p>
         <Icon path={mdiArrowRight} size="18" />
       </Button>
diff --git a/web/src/lib/components/photos-page/actions/add-to-album.svelte b/web/src/lib/components/photos-page/actions/add-to-album.svelte
index 976f4bd9cf..10917a1d90 100644
--- a/web/src/lib/components/photos-page/actions/add-to-album.svelte
+++ b/web/src/lib/components/photos-page/actions/add-to-album.svelte
@@ -6,10 +6,16 @@
   import { getAssetControlContext } from '../asset-select-control-bar.svelte';
   import { mdiImageAlbum, mdiShareVariantOutline } from '@mdi/js';
   import { t } from 'svelte-i18n';
+  import type { OnAddToAlbum } from '$lib/utils/actions';
 
-  export let shared = false;
+  interface Props {
+    shared?: boolean;
+    onAddToAlbum?: OnAddToAlbum;
+  }
 
-  let showAlbumPicker = false;
+  let { shared = false, onAddToAlbum = () => {} }: Props = $props();
+
+  let showAlbumPicker = $state(false);
 
   const { getAssets } = getAssetControlContext();
 
@@ -21,13 +27,19 @@
     showAlbumPicker = false;
 
     const assetIds = [...getAssets()].map((asset) => asset.id);
-    await addAssetsToNewAlbum(albumName, assetIds);
+    const album = await addAssetsToNewAlbum(albumName, assetIds);
+    if (!album) {
+      return;
+    }
+
+    onAddToAlbum(assetIds, album.id);
   };
 
   const handleAddToAlbum = async (album: AlbumResponseDto) => {
     showAlbumPicker = false;
     const assetIds = [...getAssets()].map((asset) => asset.id);
     await addAssetsToAlbum(album.id, assetIds);
+    onAddToAlbum(assetIds, album.id);
   };
 </script>
 
@@ -40,8 +52,8 @@
 {#if showAlbumPicker}
   <AlbumSelectionModal
     {shared}
-    on:newAlbum={({ detail }) => handleAddToNewAlbum(detail)}
-    on:album={({ detail }) => handleAddToAlbum(detail)}
+    onNewAlbum={handleAddToNewAlbum}
+    onAlbumClick={handleAddToAlbum}
     onClose={handleHideAlbumPicker}
   />
 {/if}
diff --git a/web/src/lib/components/photos-page/actions/archive-action.svelte b/web/src/lib/components/photos-page/actions/archive-action.svelte
index 792b80b702..868a5ddd6d 100644
--- a/web/src/lib/components/photos-page/actions/archive-action.svelte
+++ b/web/src/lib/components/photos-page/actions/archive-action.svelte
@@ -7,15 +7,18 @@
   import { archiveAssets } from '$lib/utils/asset-utils';
   import { t } from 'svelte-i18n';
 
-  export let onArchive: OnArchive;
+  interface Props {
+    onArchive: OnArchive;
+    menuItem?: boolean;
+    unarchive?: boolean;
+  }
 
-  export let menuItem = false;
-  export let unarchive = false;
+  let { onArchive, menuItem = false, unarchive = false }: Props = $props();
 
-  $: text = unarchive ? $t('unarchive') : $t('to_archive');
-  $: icon = unarchive ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline;
+  let text = $derived(unarchive ? $t('unarchive') : $t('to_archive'));
+  let icon = $derived(unarchive ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline);
 
-  let loading = false;
+  let loading = $state(false);
 
   const { clearSelect, getOwnedAssets } = getAssetControlContext();
 
@@ -38,8 +41,8 @@
 
 {#if !menuItem}
   {#if loading}
-    <CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
+    <CircleIconButton title={$t('loading')} icon={mdiTimerSand} onclick={() => {}} />
   {:else}
-    <CircleIconButton title={text} {icon} on:click={handleArchive} />
+    <CircleIconButton title={text} {icon} onclick={handleArchive} />
   {/if}
 {/if}
diff --git a/web/src/lib/components/photos-page/actions/asset-job-actions.svelte b/web/src/lib/components/photos-page/actions/asset-job-actions.svelte
index ca61d54d43..b383729ecd 100644
--- a/web/src/lib/components/photos-page/actions/asset-job-actions.svelte
+++ b/web/src/lib/components/photos-page/actions/asset-job-actions.svelte
@@ -10,15 +10,16 @@
   import { getAssetControlContext } from '../asset-select-control-bar.svelte';
   import { t } from 'svelte-i18n';
 
-  export let jobs: AssetJobName[] = [
-    AssetJobName.RegenerateThumbnail,
-    AssetJobName.RefreshMetadata,
-    AssetJobName.TranscodeVideo,
-  ];
+  interface Props {
+    jobs?: AssetJobName[];
+  }
+
+  let { jobs = [AssetJobName.RegenerateThumbnail, AssetJobName.RefreshMetadata, AssetJobName.TranscodeVideo] }: Props =
+    $props();
 
   const { clearSelect, getOwnedAssets } = getAssetControlContext();
 
-  $: isAllVideos = [...getOwnedAssets()].every((asset) => asset.type === AssetTypeEnum.Video);
+  let isAllVideos = $derived([...getOwnedAssets()].every((asset) => asset.type === AssetTypeEnum.Video));
 
   const handleRunJob = async (name: AssetJobName) => {
     try {
diff --git a/web/src/lib/components/photos-page/actions/change-date-action.svelte b/web/src/lib/components/photos-page/actions/change-date-action.svelte
index 6ee775fa69..3232cbd2b4 100644
--- a/web/src/lib/components/photos-page/actions/change-date-action.svelte
+++ b/web/src/lib/components/photos-page/actions/change-date-action.svelte
@@ -9,10 +9,14 @@
   import { getAssetControlContext } from '../asset-select-control-bar.svelte';
   import { mdiCalendarEditOutline } from '@mdi/js';
   import { t } from 'svelte-i18n';
-  export let menuItem = false;
+  interface Props {
+    menuItem?: boolean;
+  }
+
+  let { menuItem = false }: Props = $props();
   const { clearSelect, getOwnedAssets } = getAssetControlContext();
 
-  let isShowChangeDate = false;
+  let isShowChangeDate = $state(false);
 
   const handleConfirm = async (dateTimeOriginal: string) => {
     isShowChangeDate = false;
@@ -31,9 +35,5 @@
   <MenuOption text={$t('change_date')} icon={mdiCalendarEditOutline} onClick={() => (isShowChangeDate = true)} />
 {/if}
 {#if isShowChangeDate}
-  <ChangeDate
-    initialDate={DateTime.now()}
-    on:confirm={({ detail: date }) => handleConfirm(date)}
-    on:cancel={() => (isShowChangeDate = false)}
-  />
+  <ChangeDate initialDate={DateTime.now()} onConfirm={handleConfirm} onCancel={() => (isShowChangeDate = false)} />
 {/if}
diff --git a/web/src/lib/components/photos-page/actions/change-location-action.svelte b/web/src/lib/components/photos-page/actions/change-location-action.svelte
index 0e19696a42..0ad93e5d81 100644
--- a/web/src/lib/components/photos-page/actions/change-location-action.svelte
+++ b/web/src/lib/components/photos-page/actions/change-location-action.svelte
@@ -9,10 +9,14 @@
   import { mdiMapMarkerMultipleOutline } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  export let menuItem = false;
+  interface Props {
+    menuItem?: boolean;
+  }
+
+  let { menuItem = false }: Props = $props();
   const { clearSelect, getOwnedAssets } = getAssetControlContext();
 
-  let isShowChangeLocation = false;
+  let isShowChangeLocation = $state(false);
 
   async function handleConfirm(point: { lng: number; lat: number }) {
     isShowChangeLocation = false;
@@ -35,8 +39,5 @@
   />
 {/if}
 {#if isShowChangeLocation}
-  <ChangeLocation
-    on:confirm={({ detail: point }) => handleConfirm(point)}
-    on:cancel={() => (isShowChangeLocation = false)}
-  />
+  <ChangeLocation onConfirm={handleConfirm} onCancel={() => (isShowChangeLocation = false)} />
 {/if}
diff --git a/web/src/lib/components/photos-page/actions/create-shared-link.svelte b/web/src/lib/components/photos-page/actions/create-shared-link.svelte
index 7436ff2177..1b99627ea9 100644
--- a/web/src/lib/components/photos-page/actions/create-shared-link.svelte
+++ b/web/src/lib/components/photos-page/actions/create-shared-link.svelte
@@ -5,11 +5,11 @@
   import { getAssetControlContext } from '../asset-select-control-bar.svelte';
   import { t } from 'svelte-i18n';
 
-  let showModal = false;
+  let showModal = $state(false);
   const { getAssets } = getAssetControlContext();
 </script>
 
-<CircleIconButton title={$t('share')} icon={mdiShareVariantOutline} on:click={() => (showModal = true)} />
+<CircleIconButton title={$t('share')} icon={mdiShareVariantOutline} onclick={() => (showModal = true)} />
 
 {#if showModal}
   <CreateSharedLinkModal assetIds={[...getAssets()].map(({ id }) => id)} onClose={() => (showModal = false)} />
diff --git a/web/src/lib/components/photos-page/actions/delete-assets.svelte b/web/src/lib/components/photos-page/actions/delete-assets.svelte
index 5c79e7b221..bdd442e50c 100644
--- a/web/src/lib/components/photos-page/actions/delete-assets.svelte
+++ b/web/src/lib/components/photos-page/actions/delete-assets.svelte
@@ -8,16 +8,20 @@
   import DeleteAssetDialog from '../delete-asset-dialog.svelte';
   import { t } from 'svelte-i18n';
 
-  export let onAssetDelete: OnDelete;
-  export let menuItem = false;
-  export let force = !$featureFlags.trash;
+  interface Props {
+    onAssetDelete: OnDelete;
+    menuItem?: boolean;
+    force?: boolean;
+  }
+
+  let { onAssetDelete, menuItem = false, force = !$featureFlags.trash }: Props = $props();
 
   const { clearSelect, getOwnedAssets } = getAssetControlContext();
 
-  let isShowConfirmation = false;
-  let loading = false;
+  let isShowConfirmation = $state(false);
+  let loading = $state(false);
 
-  $: label = force ? $t('permanently_delete') : $t('delete');
+  let label = $derived(force ? $t('permanently_delete') : $t('delete'));
 
   const handleTrash = async () => {
     if (force) {
@@ -41,15 +45,15 @@
 {#if menuItem}
   <MenuOption text={label} icon={mdiDeleteOutline} onClick={handleTrash} />
 {:else if loading}
-  <CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
+  <CircleIconButton title={$t('loading')} icon={mdiTimerSand} onclick={() => {}} />
 {:else}
-  <CircleIconButton title={label} icon={mdiDeleteForeverOutline} on:click={handleTrash} />
+  <CircleIconButton title={label} icon={mdiDeleteForeverOutline} onclick={handleTrash} />
 {/if}
 
 {#if isShowConfirmation}
   <DeleteAssetDialog
     size={getOwnedAssets().size}
-    on:confirm={handleDelete}
-    on:cancel={() => (isShowConfirmation = false)}
+    onConfirm={handleDelete}
+    onCancel={() => (isShowConfirmation = false)}
   />
 {/if}
diff --git a/web/src/lib/components/photos-page/actions/download-action.svelte b/web/src/lib/components/photos-page/actions/download-action.svelte
index 7716fbe36d..89eca9c6a8 100644
--- a/web/src/lib/components/photos-page/actions/download-action.svelte
+++ b/web/src/lib/components/photos-page/actions/download-action.svelte
@@ -7,8 +7,12 @@
   import { mdiCloudDownloadOutline, mdiFileDownloadOutline, mdiFolderDownloadOutline } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  export let filename = 'immich.zip';
-  export let menuItem = false;
+  interface Props {
+    filename?: string;
+    menuItem?: boolean;
+  }
+
+  let { filename = 'immich.zip', menuItem = false }: Props = $props();
 
   const { getAssets, clearSelect } = getAssetControlContext();
 
@@ -24,7 +28,7 @@
     await downloadArchive(filename, { assetIds: assets.map((asset) => asset.id) });
   };
 
-  $: menuItemIcon = getAssets().size === 1 ? mdiFileDownloadOutline : mdiFolderDownloadOutline;
+  let menuItemIcon = $derived(getAssets().size === 1 ? mdiFileDownloadOutline : mdiFolderDownloadOutline);
 </script>
 
 <svelte:window use:shortcut={{ shortcut: { key: 'd', shift: true }, onShortcut: handleDownloadFiles }} />
@@ -32,5 +36,5 @@
 {#if menuItem}
   <MenuOption text={$t('download')} icon={menuItemIcon} onClick={handleDownloadFiles} />
 {:else}
-  <CircleIconButton title={$t('download')} icon={mdiCloudDownloadOutline} on:click={handleDownloadFiles} />
+  <CircleIconButton title={$t('download')} icon={mdiCloudDownloadOutline} onclick={handleDownloadFiles} />
 {/if}
diff --git a/web/src/lib/components/photos-page/actions/favorite-action.svelte b/web/src/lib/components/photos-page/actions/favorite-action.svelte
index 1d723b1a9d..1bc6764157 100644
--- a/web/src/lib/components/photos-page/actions/favorite-action.svelte
+++ b/web/src/lib/components/photos-page/actions/favorite-action.svelte
@@ -12,15 +12,18 @@
   import { getAssetControlContext } from '../asset-select-control-bar.svelte';
   import { t } from 'svelte-i18n';
 
-  export let onFavorite: OnFavorite;
+  interface Props {
+    onFavorite: OnFavorite;
+    menuItem?: boolean;
+    removeFavorite: boolean;
+  }
 
-  export let menuItem = false;
-  export let removeFavorite: boolean;
+  let { onFavorite, menuItem = false, removeFavorite }: Props = $props();
 
-  $: text = removeFavorite ? $t('remove_from_favorites') : $t('to_favorite');
-  $: icon = removeFavorite ? mdiHeartMinusOutline : mdiHeartOutline;
+  let text = $derived(removeFavorite ? $t('remove_from_favorites') : $t('to_favorite'));
+  let icon = $derived(removeFavorite ? mdiHeartMinusOutline : mdiHeartOutline);
 
-  let loading = false;
+  let loading = $state(false);
 
   const { clearSelect, getOwnedAssets } = getAssetControlContext();
 
@@ -65,8 +68,8 @@
 
 {#if !menuItem}
   {#if loading}
-    <CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
+    <CircleIconButton title={$t('loading')} icon={mdiTimerSand} onclick={() => {}} />
   {:else}
-    <CircleIconButton title={text} {icon} on:click={handleFavorite} />
+    <CircleIconButton title={text} {icon} onclick={handleFavorite} />
   {/if}
 {/if}
diff --git a/web/src/lib/components/photos-page/actions/link-live-photo-action.svelte b/web/src/lib/components/photos-page/actions/link-live-photo-action.svelte
index 24107b9f88..27ac6cf042 100644
--- a/web/src/lib/components/photos-page/actions/link-live-photo-action.svelte
+++ b/web/src/lib/components/photos-page/actions/link-live-photo-action.svelte
@@ -8,15 +8,19 @@
   import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
   import { getAssetControlContext } from '../asset-select-control-bar.svelte';
 
-  export let onLink: OnLink;
-  export let onUnlink: OnUnlink;
-  export let menuItem = false;
-  export let unlink = false;
+  interface Props {
+    onLink: OnLink;
+    onUnlink: OnUnlink;
+    menuItem?: boolean;
+    unlink?: boolean;
+  }
 
-  let loading = false;
+  let { onLink, onUnlink, menuItem = false, unlink = false }: Props = $props();
 
-  $: text = unlink ? $t('unlink_motion_video') : $t('link_motion_video');
-  $: icon = unlink ? mdiLinkOff : mdiMotionPlayOutline;
+  let loading = $state(false);
+
+  let text = $derived(unlink ? $t('unlink_motion_video') : $t('link_motion_video'));
+  let icon = $derived(unlink ? mdiLinkOff : mdiMotionPlayOutline);
 
   const { clearSelect, getOwnedAssets } = getAssetControlContext();
 
@@ -68,8 +72,8 @@
 
 {#if !menuItem}
   {#if loading}
-    <CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
+    <CircleIconButton title={$t('loading')} icon={mdiTimerSand} onclick={() => {}} />
   {:else}
-    <CircleIconButton title={text} {icon} on:click={onClick} />
+    <CircleIconButton title={text} {icon} onclick={onClick} />
   {/if}
 {/if}
diff --git a/web/src/lib/components/photos-page/actions/remove-from-album.svelte b/web/src/lib/components/photos-page/actions/remove-from-album.svelte
index 2384f95d2e..19c1e54cfa 100644
--- a/web/src/lib/components/photos-page/actions/remove-from-album.svelte
+++ b/web/src/lib/components/photos-page/actions/remove-from-album.svelte
@@ -11,9 +11,13 @@
   import { dialogController } from '$lib/components/shared-components/dialog/dialog';
   import { t } from 'svelte-i18n';
 
-  export let album: AlbumResponseDto;
-  export let onRemove: ((assetIds: string[]) => void) | undefined;
-  export let menuItem = false;
+  interface Props {
+    album: AlbumResponseDto;
+    onRemove: ((assetIds: string[]) => void) | undefined;
+    menuItem?: boolean;
+  }
+
+  let { album = $bindable(), onRemove, menuItem = false }: Props = $props();
 
   const { getAssets, clearSelect } = getAssetControlContext();
 
@@ -57,5 +61,5 @@
 {#if menuItem}
   <MenuOption text={$t('remove_from_album')} icon={mdiImageRemoveOutline} onClick={removeFromAlbum} />
 {:else}
-  <CircleIconButton title={$t('remove_from_album')} icon={mdiDeleteOutline} on:click={removeFromAlbum} />
+  <CircleIconButton title={$t('remove_from_album')} icon={mdiDeleteOutline} onclick={removeFromAlbum} />
 {/if}
diff --git a/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte b/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte
index e838f0813d..e884a929a3 100644
--- a/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte
+++ b/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte
@@ -9,7 +9,11 @@
   import { dialogController } from '$lib/components/shared-components/dialog/dialog';
   import { t } from 'svelte-i18n';
 
-  export let sharedLink: SharedLinkResponseDto;
+  interface Props {
+    sharedLink: SharedLinkResponseDto;
+  }
+
+  let { sharedLink = $bindable() }: Props = $props();
 
   const { getAssets, clearSelect } = getAssetControlContext();
 
@@ -55,4 +59,4 @@
   };
 </script>
 
-<CircleIconButton title={$t('remove_from_shared_link')} on:click={handleRemove} icon={mdiDeleteOutline} />
+<CircleIconButton title={$t('remove_from_shared_link')} onclick={handleRemove} icon={mdiDeleteOutline} />
diff --git a/web/src/lib/components/photos-page/actions/restore-assets.svelte b/web/src/lib/components/photos-page/actions/restore-assets.svelte
index 19e1c206fd..037e3239ef 100644
--- a/web/src/lib/components/photos-page/actions/restore-assets.svelte
+++ b/web/src/lib/components/photos-page/actions/restore-assets.svelte
@@ -12,11 +12,15 @@
   import { getAssetControlContext } from '../asset-select-control-bar.svelte';
   import { t } from 'svelte-i18n';
 
-  export let onRestore: OnRestore | undefined;
+  interface Props {
+    onRestore: OnRestore | undefined;
+  }
+
+  let { onRestore }: Props = $props();
 
   const { getAssets, clearSelect } = getAssetControlContext();
 
-  let loading = false;
+  let loading = $state(false);
 
   const handleRestore = async () => {
     loading = true;
@@ -40,7 +44,7 @@
   };
 </script>
 
-<Button disabled={loading} size="sm" color="transparent-gray" shadow={false} rounded="lg" on:click={handleRestore}>
+<Button disabled={loading} size="sm" color="transparent-gray" shadow={false} rounded="lg" onclick={handleRestore}>
   <Icon path={mdiHistory} size="24" />
   <span class="ml-2">{$t('restore')}</span>
 </Button>
diff --git a/web/src/lib/components/photos-page/actions/select-all-assets.svelte b/web/src/lib/components/photos-page/actions/select-all-assets.svelte
index 98ee86ac63..9e7c2b9163 100644
--- a/web/src/lib/components/photos-page/actions/select-all-assets.svelte
+++ b/web/src/lib/components/photos-page/actions/select-all-assets.svelte
@@ -1,26 +1,29 @@
 <script lang="ts">
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
-  import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
   import { type AssetStore, isSelectingAllAssets } from '$lib/stores/assets.store';
   import { mdiSelectAll, mdiSelectRemove } from '@mdi/js';
-  import { selectAllAssets } from '$lib/utils/asset-utils';
+  import { selectAllAssets, cancelMultiselect } from '$lib/utils/asset-utils';
   import { t } from 'svelte-i18n';
+  import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 
-  export let assetStore: AssetStore;
-  export let assetInteractionStore: AssetInteractionStore;
+  interface Props {
+    assetStore: AssetStore;
+    assetInteraction: AssetInteraction;
+  }
+
+  let { assetStore, assetInteraction }: Props = $props();
 
   const handleSelectAll = async () => {
-    await selectAllAssets(assetStore, assetInteractionStore);
+    await selectAllAssets(assetStore, assetInteraction);
   };
 
   const handleCancel = () => {
-    $isSelectingAllAssets = false;
-    assetInteractionStore.clearMultiselect();
+    cancelMultiselect(assetInteraction);
   };
 </script>
 
 {#if $isSelectingAllAssets}
-  <CircleIconButton title={$t('unselect_all')} icon={mdiSelectRemove} on:click={handleCancel} />
+  <CircleIconButton title={$t('unselect_all')} icon={mdiSelectRemove} onclick={handleCancel} />
 {:else}
-  <CircleIconButton title={$t('select_all')} icon={mdiSelectAll} on:click={handleSelectAll} />
+  <CircleIconButton title={$t('select_all')} icon={mdiSelectAll} onclick={handleSelectAll} />
 {/if}
diff --git a/web/src/lib/components/photos-page/actions/stack-action.svelte b/web/src/lib/components/photos-page/actions/stack-action.svelte
index c1f2bf212f..fe4f066a0e 100644
--- a/web/src/lib/components/photos-page/actions/stack-action.svelte
+++ b/web/src/lib/components/photos-page/actions/stack-action.svelte
@@ -6,9 +6,13 @@
   import type { OnStack, OnUnstack } from '$lib/utils/actions';
   import { t } from 'svelte-i18n';
 
-  export let unstack = false;
-  export let onStack: OnStack | undefined;
-  export let onUnstack: OnUnstack | undefined;
+  interface Props {
+    unstack?: boolean;
+    onStack: OnStack | undefined;
+    onUnstack: OnUnstack | undefined;
+  }
+
+  let { unstack = false, onStack, onUnstack }: Props = $props();
 
   const { clearSelect, getOwnedAssets } = getAssetControlContext();
 
diff --git a/web/src/lib/components/photos-page/actions/tag-action.svelte b/web/src/lib/components/photos-page/actions/tag-action.svelte
index 77e91d7235..32cdaec16a 100644
--- a/web/src/lib/components/photos-page/actions/tag-action.svelte
+++ b/web/src/lib/components/photos-page/actions/tag-action.svelte
@@ -7,13 +7,17 @@
   import { getAssetControlContext } from '../asset-select-control-bar.svelte';
   import TagAssetForm from '$lib/components/forms/tag-asset-form.svelte';
 
-  export let menuItem = false;
+  interface Props {
+    menuItem?: boolean;
+  }
+
+  let { menuItem = false }: Props = $props();
 
   const text = $t('tag');
   const icon = mdiTagMultipleOutline;
 
-  let loading = false;
-  let isOpen = false;
+  let loading = $state(false);
+  let isOpen = $state(false);
 
   const { clearSelect, getOwnedAssets } = getAssetControlContext();
 
@@ -36,9 +40,9 @@
 
 {#if !menuItem}
   {#if loading}
-    <CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
+    <CircleIconButton title={$t('loading')} icon={mdiTimerSand} onclick={() => {}} />
   {:else}
-    <CircleIconButton title={text} {icon} on:click={handleOpen} />
+    <CircleIconButton title={text} {icon} onclick={handleOpen} />
   {/if}
 {/if}
 
diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte
index 240b6c2ba2..586491ef47 100644
--- a/web/src/lib/components/photos-page/asset-date-group.svelte
+++ b/web/src/lib/components/photos-page/asset-date-group.svelte
@@ -2,17 +2,17 @@
   import { intersectionObserver } from '$lib/actions/intersection-observer';
   import Icon from '$lib/components/elements/icon.svelte';
   import Skeleton from '$lib/components/photos-page/skeleton.svelte';
-  import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
   import { AssetBucket, type AssetStore, type Viewport } from '$lib/stores/assets.store';
   import { navigate } from '$lib/utils/navigation';
   import { findTotalOffset, type DateGroup, type ScrollTargetListener } from '$lib/utils/timeline-util';
   import type { AssetResponseDto } from '@immich/sdk';
   import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
-  import { createEventDispatcher, onDestroy } from 'svelte';
+  import { onDestroy } from 'svelte';
   import { fly } from 'svelte/transition';
   import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
   import { TUNABLES } from '$lib/utils/tunables';
   import { generateId } from '$lib/utils/generate-id';
+  import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 
   export let element: HTMLElement | undefined = undefined;
   export let isSelectionMode = false;
@@ -25,10 +25,13 @@
   export let renderThumbsAtTopMargin: string | undefined = undefined;
   export let assetStore: AssetStore;
   export let bucket: AssetBucket;
-  export let assetInteractionStore: AssetInteractionStore;
+  export let assetInteraction: AssetInteraction;
 
   export let onScrollTarget: ScrollTargetListener | undefined = undefined;
   export let onAssetInGrid: ((asset: AssetResponseDto) => void) | undefined = undefined;
+  export let onSelect: ({ title, assets }: { title: string; assets: AssetResponseDto[] }) => void;
+  export let onSelectAssets: (asset: AssetResponseDto) => void;
+  export let onSelectAssetCandidates: (asset: AssetResponseDto | null) => void;
 
   const componentId = generateId();
   $: bucketDate = bucket.bucketDate;
@@ -40,18 +43,11 @@
   /* TODO figure out a way to calculate this*/
   const TITLE_HEIGHT = 51;
 
-  const { selectedGroup, selectedAssets, assetSelectionCandidates, isMultiSelectState } = assetInteractionStore;
-  const dispatch = createEventDispatcher<{
-    select: { title: string; assets: AssetResponseDto[] };
-    selectAssets: AssetResponseDto;
-    selectAssetCandidates: AssetResponseDto | null;
-  }>();
-
   let isMouseOverGroup = false;
   let hoveredDateGroup = '';
 
   const onClick = (assets: AssetResponseDto[], groupTitle: string, asset: AssetResponseDto) => {
-    if (isSelectionMode || $isMultiSelectState) {
+    if (isSelectionMode || assetInteraction.selectionActive) {
       assetSelectHandler(asset, assets, groupTitle);
       return;
     }
@@ -65,19 +61,21 @@
     }
   };
 
-  const handleSelectGroup = (title: string, assets: AssetResponseDto[]) => dispatch('select', { title, assets });
+  const handleSelectGroup = (title: string, assets: AssetResponseDto[]) => onSelect({ title, assets });
 
   const assetSelectHandler = (asset: AssetResponseDto, assetsInDateGroup: AssetResponseDto[], groupTitle: string) => {
-    dispatch('selectAssets', asset);
+    onSelectAssets(asset);
 
     // Check if all assets are selected in a group to toggle the group selection's icon
-    let selectedAssetsInGroupCount = assetsInDateGroup.filter((asset) => $selectedAssets.has(asset)).length;
+    let selectedAssetsInGroupCount = assetsInDateGroup.filter((asset) =>
+      assetInteraction.selectedAssets.has(asset),
+    ).length;
 
     // if all assets are selected in a group, add the group to selected group
     if (selectedAssetsInGroupCount == assetsInDateGroup.length) {
-      assetInteractionStore.addGroupToMultiselectGroup(groupTitle);
+      assetInteraction.addGroupToMultiselectGroup(groupTitle);
     } else {
-      assetInteractionStore.removeGroupFromMultiselectGroup(groupTitle);
+      assetInteraction.removeGroupFromMultiselectGroup(groupTitle);
     }
   };
 
@@ -85,8 +83,8 @@
     // Show multi select icon on hover on date group
     hoveredDateGroup = groupTitle;
 
-    if ($isMultiSelectState) {
-      dispatch('selectAssetCandidates', asset);
+    if (assetInteraction.selectionActive) {
+      onSelectAssetCandidates(asset);
     }
   };
 
@@ -153,14 +151,14 @@
             class="flex z-[100] sticky top-[-1px] pt-[calc(1.75rem+1px)] pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm"
             style:width={dateGroup.geometry.containerWidth + 'px'}
           >
-            {#if !singleSelect && ((hoveredDateGroup == dateGroup.groupTitle && isMouseOverGroup) || $selectedGroup.has(dateGroup.groupTitle))}
+            {#if !singleSelect && ((hoveredDateGroup == dateGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dateGroup.groupTitle))}
               <div
                 transition:fly={{ x: -24, duration: 200, opacity: 0.5 }}
                 class="inline-block px-2 hover:cursor-pointer"
                 on:click={() => handleSelectGroup(dateGroup.groupTitle, dateGroup.assets)}
                 on:keydown={() => handleSelectGroup(dateGroup.groupTitle, dateGroup.assets)}
               >
-                {#if $selectedGroup.has(dateGroup.groupTitle)}
+                {#if assetInteraction.selectedGroup.has(dateGroup.groupTitle)}
                   <Icon path={mdiCheckCircle} size="24" color="#4250af" />
                 {:else}
                   <Icon path={mdiCircleOutline} size="24" color="#757575" />
@@ -214,8 +212,8 @@
                   onClick={(asset) => onClick(dateGroup.assets, dateGroup.groupTitle, asset)}
                   onSelect={(asset) => assetSelectHandler(asset, dateGroup.assets, dateGroup.groupTitle)}
                   onMouseEvent={() => assetMouseEventHandler(dateGroup.groupTitle, asset)}
-                  selected={$selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)}
-                  selectionCandidate={$assetSelectionCandidates.has(asset)}
+                  selected={assetInteraction.selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)}
+                  selectionCandidate={assetInteraction.assetSelectionCandidates.has(asset)}
                   disabled={$assetStore.albumAssets.has(asset.id)}
                   thumbnailWidth={box.width}
                   thumbnailHeight={box.height}
diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte
index f59911dbaf..55f935c8dd 100644
--- a/web/src/lib/components/photos-page/asset-grid.svelte
+++ b/web/src/lib/components/photos-page/asset-grid.svelte
@@ -3,21 +3,14 @@
   import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut';
   import type { Action } from '$lib/components/asset-viewer/actions/action';
   import { AppRoute, AssetAction } from '$lib/constants';
-  import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
-  import {
-    AssetBucket,
-    AssetStore,
-    isSelectingAllAssets,
-    type BucketListener,
-    type ViewportXY,
-  } from '$lib/stores/assets.store';
+  import { AssetBucket, AssetStore, type BucketListener, type ViewportXY } from '$lib/stores/assets.store';
   import { locale, showDeleteModal } from '$lib/stores/preferences.store';
   import { isSearchEnabled } from '$lib/stores/search.store';
   import { featureFlags } from '$lib/stores/server-config.store';
   import { handlePromiseError } from '$lib/utils';
   import { deleteAssets } from '$lib/utils/actions';
-  import { archiveAssets, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
+  import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
   import { navigate } from '$lib/utils/navigation';
   import {
     formatGroupTitle,
@@ -26,9 +19,9 @@
     type ScrollTargetListener,
   } from '$lib/utils/timeline-util';
   import { TUNABLES } from '$lib/utils/tunables';
-  import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk';
+  import type { AlbumResponseDto, AssetResponseDto, PersonResponseDto } from '@immich/sdk';
   import { throttle } from 'lodash-es';
-  import { createEventDispatcher, onDestroy, onMount } from 'svelte';
+  import { onDestroy, onMount, type Snippet } from 'svelte';
   import Portal from '../shared-components/portal/portal.svelte';
   import Scrubber from '../shared-components/scrubber/scrubber.svelte';
   import ShowShortcuts from '../shared-components/show-shortcuts.svelte';
@@ -42,79 +35,73 @@
   import { page } from '$app/stores';
   import type { UpdatePayload } from 'vite';
   import { generateId } from '$lib/utils/generate-id';
+  import { isTimelineScrolling } from '$lib/stores/timeline.store';
+  import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 
-  export let isSelectionMode = false;
-  export let singleSelect = false;
-
-  /** `true` if this asset grid is responds to navigation events; if `true`, then look at the
+  interface Props {
+    isSelectionMode?: boolean;
+    singleSelect?: boolean;
+    /** `true` if this asset grid is responds to navigation events; if `true`, then look at the
    `AssetViewingStore.gridScrollTarget` and load and scroll to the asset specified, and
    additionally, update the page location/url with the asset as the asset-grid is scrolled */
-  export let enableRouting: boolean;
+    enableRouting: boolean;
+    assetStore: AssetStore;
+    assetInteraction: AssetInteraction;
+    removeAction?: AssetAction.UNARCHIVE | AssetAction.ARCHIVE | AssetAction.FAVORITE | AssetAction.UNFAVORITE | null;
+    withStacked?: boolean;
+    showArchiveIcon?: boolean;
+    isShared?: boolean;
+    album?: AlbumResponseDto | null;
+    person?: PersonResponseDto | null;
+    isShowDeleteConfirmation?: boolean;
+    onSelect?: (asset: AssetResponseDto) => void;
+    onEscape?: () => void;
+    children?: Snippet;
+    empty?: Snippet;
+  }
 
-  export let assetStore: AssetStore;
-  export let assetInteractionStore: AssetInteractionStore;
-  export let removeAction:
-    | AssetAction.UNARCHIVE
-    | AssetAction.ARCHIVE
-    | AssetAction.FAVORITE
-    | AssetAction.UNFAVORITE
-    | null = null;
-  export let withStacked = false;
-  export let showArchiveIcon = false;
-  export let isShared = false;
-  export let album: AlbumResponseDto | null = null;
-  export let isShowDeleteConfirmation = false;
+  let {
+    isSelectionMode = false,
+    singleSelect = false,
+    enableRouting,
+    assetStore = $bindable(),
+    assetInteraction,
+    removeAction = null,
+    withStacked = false,
+    showArchiveIcon = false,
+    isShared = false,
+    album = null,
+    person = null,
+    isShowDeleteConfirmation = $bindable(false),
+    onSelect = () => {},
+    onEscape = () => {},
+    children,
+    empty,
+  }: Props = $props();
 
   let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets, gridScrollTarget } = assetViewingStore;
-  const { assetSelectionCandidates, assetSelectionStart, selectedGroup, selectedAssets, isMultiSelectState } =
-    assetInteractionStore;
 
-  const viewport: ViewportXY = { width: 0, height: 0, x: 0, y: 0 };
-  const safeViewport: ViewportXY = { width: 0, height: 0, x: 0, y: 0 };
+  const viewport: ViewportXY = $state({ width: 0, height: 0, x: 0, y: 0 });
+  const safeViewport: ViewportXY = $state({ width: 0, height: 0, x: 0, y: 0 });
 
   const componentId = generateId();
-  let element: HTMLElement;
-  let timelineElement: HTMLElement;
-  let showShortcuts = false;
-  let showSkeleton = true;
+  let element: HTMLElement | undefined = $state();
+  let timelineElement: HTMLElement | undefined = $state();
+  let showShortcuts = $state(false);
+  let showSkeleton = $state(true);
   let internalScroll = false;
   let navigating = false;
-  let preMeasure: AssetBucket[] = [];
+  let preMeasure: AssetBucket[] = $state([]);
   let lastIntersectedBucketDate: string | undefined;
-  let scrubBucketPercent = 0;
-  let scrubBucket: { bucketDate: string | undefined } | undefined;
-  let scrubOverallPercent: number = 0;
-  let topSectionHeight = 0;
-  let topSectionOffset = 0;
+  let scrubBucketPercent = $state(0);
+  let scrubBucket: { bucketDate: string | undefined } | undefined = $state();
+  let scrubOverallPercent: number = $state(0);
+  let topSectionHeight = $state(0);
+  let topSectionOffset = $state(0);
   // 60 is the bottom spacer element at 60px
   let bottomSectionHeight = 60;
-  let leadout = false;
+  let leadout = $state(false);
 
-  $: isTrashEnabled = $featureFlags.loaded && $featureFlags.trash;
-  $: isEmpty = $assetStore.initialized && $assetStore.buckets.length === 0;
-  $: idsSelectedAssets = [...$selectedAssets].map(({ id }) => id);
-  $: isAllArchived = [...$selectedAssets].every((asset) => asset.isArchived);
-  $: {
-    if (isEmpty) {
-      assetInteractionStore.clearMultiselect();
-    }
-  }
-  $: {
-    if (element && isViewportOrigin()) {
-      const rect = element.getBoundingClientRect();
-      viewport.height = rect.height;
-      viewport.width = rect.width;
-      viewport.x = rect.x;
-      viewport.y = rect.y;
-    }
-    if (!isViewportOrigin() && !isEqual(viewport, safeViewport)) {
-      safeViewport.height = viewport.height;
-      safeViewport.width = viewport.width;
-      safeViewport.x = viewport.x;
-      safeViewport.y = viewport.y;
-      updateViewport();
-    }
-  }
   const {
     ASSET_GRID: { NAVIGATE_ON_ASSET_IN_VIEW },
     BUCKET: {
@@ -127,8 +114,6 @@
     },
   } = TUNABLES;
 
-  const dispatch = createEventDispatcher<{ select: AssetResponseDto; escape: void }>();
-
   const isViewportOrigin = () => {
     return viewport.height === 0 && viewport.width === 0;
   };
@@ -146,11 +131,11 @@
 
     if ($gridScrollTarget?.at) {
       void $assetStore.scheduleScrollToAssetId($gridScrollTarget, () => {
-        element.scrollTo({ top: 0 });
+        element?.scrollTo({ top: 0 });
         showSkeleton = false;
       });
     } else {
-      element.scrollTo({ top: 0 });
+      element?.scrollTo({ top: 0 });
       showSkeleton = false;
     }
   };
@@ -190,7 +175,7 @@
                 { replaceState: true, forceNavigate: true },
               );
             } else {
-              element.scrollTo({ top: 0 });
+              element?.scrollTo({ top: 0 });
               showSkeleton = false;
             }
           }, 500);
@@ -281,14 +266,24 @@
     ($assetStore.timelineHeight + bottomSectionHeight + topSectionHeight - safeViewport.height) /
     ($assetStore.timelineHeight + bottomSectionHeight + topSectionHeight);
 
-  const getMaxScroll = () =>
-    topSectionHeight + bottomSectionHeight + (timelineElement.clientHeight - element.clientHeight);
+  const getMaxScroll = () => {
+    if (!element || !timelineElement) {
+      return 0;
+    }
+
+    return topSectionHeight + bottomSectionHeight + (timelineElement.clientHeight - element.clientHeight);
+  };
 
   const scrollToBucketAndOffset = (bucket: AssetBucket, bucketScrollPercent: number) => {
     const topOffset = getOffset(bucket.bucketDate) + topSectionHeight + topSectionOffset;
     const maxScrollPercent = getMaxScrollPercent();
     const delta = bucket.bucketHeight * bucketScrollPercent;
     const scrollTop = (topOffset + delta) * maxScrollPercent;
+
+    if (!element) {
+      return;
+    }
+
     element.scrollTop = scrollTop;
   };
 
@@ -302,6 +297,11 @@
 
       const maxScroll = getMaxScroll();
       const offset = maxScroll * scrollPercent;
+
+      if (!element) {
+        return;
+      }
+
       element.scrollTop = offset;
     } else {
       const bucket = assetStore.buckets.find((b) => b.bucketDate === bucketDate);
@@ -337,8 +337,23 @@
     }
   };
 
+  let scrollObserverTimer: NodeJS.Timeout;
+
   const _handleTimelineScroll = () => {
+    $isTimelineScrolling = true;
+    if (scrollObserverTimer) {
+      clearTimeout(scrollObserverTimer);
+    }
+    scrollObserverTimer = setTimeout(() => {
+      $isTimelineScrolling = false;
+    }, 1000);
+
     leadout = false;
+
+    if (!element) {
+      return;
+    }
+
     if ($assetStore.timelineHeight < safeViewport.height * 2) {
       // edge case - scroll limited due to size of content, must adjust -  use the overall percent instead
       const maxScroll = getMaxScroll();
@@ -404,7 +419,7 @@
     : () => void 0;
 
   const onScrollTarget: ScrollTargetListener = ({ bucket, offset }) => {
-    element.scrollTo({ top: offset });
+    element?.scrollTo({ top: offset });
     if (!bucket.measured) {
       preMeasure.push(bucket);
     }
@@ -422,11 +437,11 @@
       (assetIds) => $assetStore.removeAssets(assetIds),
       idsSelectedAssets,
     );
-    assetInteractionStore.clearMultiselect();
+    assetInteraction.clearMultiselect();
   };
 
   const onDelete = () => {
-    const hasTrashedAsset = Array.from($selectedAssets).some((asset) => asset.isTrashed);
+    const hasTrashedAsset = assetInteraction.selectedAssetsArray.some((asset) => asset.isTrashed);
 
     if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) {
       isShowDeleteConfirmation = true;
@@ -444,15 +459,15 @@
   };
 
   const onStackAssets = async () => {
-    const ids = await stackAssets(Array.from($selectedAssets));
+    const ids = await stackAssets(assetInteraction.selectedAssetsArray);
     if (ids) {
       $assetStore.removeAssets(ids);
-      dispatch('escape');
+      onEscape();
     }
   };
 
   const toggleArchive = async () => {
-    const ids = await archiveAssets(Array.from($selectedAssets), !isAllArchived);
+    const ids = await archiveAssets(assetInteraction.selectedAssetsArray, !assetInteraction.isAllArchived);
     if (ids) {
       $assetStore.removeAssets(ids);
       deselectAllAssets();
@@ -461,40 +476,13 @@
 
   const focusElement = () => {
     if (document.activeElement === document.body) {
-      element.focus();
+      element?.focus();
     }
   };
 
-  $: shortcutList = (() => {
-    if ($isSearchEnabled || $showAssetViewer) {
-      return [];
-    }
-
-    const shortcuts: ShortcutOptions[] = [
-      { shortcut: { key: 'Escape' }, onShortcut: () => dispatch('escape') },
-      { shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) },
-      { shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
-      { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets($assetStore, assetInteractionStore) },
-      { shortcut: { key: 'PageDown' }, preventDefault: false, onShortcut: focusElement },
-      { shortcut: { key: 'PageUp' }, preventDefault: false, onShortcut: focusElement },
-    ];
-
-    if ($isMultiSelectState) {
-      shortcuts.push(
-        { shortcut: { key: 'Delete' }, onShortcut: onDelete },
-        { shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete },
-        { shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
-        { shortcut: { key: 's' }, onShortcut: () => onStackAssets() },
-        { shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
-      );
-    }
-
-    return shortcuts;
-  })();
-
   const handleSelectAsset = (asset: AssetResponseDto) => {
     if (!$assetStore.albumAssets.has(asset.id)) {
-      assetInteractionStore.selectAsset(asset);
+      assetInteraction.selectAsset(asset);
     }
   };
 
@@ -539,7 +527,7 @@
     return !!nextAsset;
   };
 
-  const handleClose = async ({ detail: { asset } }: { detail: { asset: AssetResponseDto } }) => {
+  const handleClose = async ({ asset }: { asset: AssetResponseDto }) => {
     assetViewingStore.showAssetViewer(false);
     showSkeleton = true;
     $gridScrollTarget = { at: asset.id };
@@ -554,7 +542,7 @@
       case AssetAction.DELETE: {
         // find the next asset to show or close the viewer
         // eslint-disable-next-line @typescript-eslint/no-unused-expressions
-        (await handleNext()) || (await handlePrevious()) || (await handleClose({ detail: { asset: action.asset } }));
+        (await handleNext()) || (await handlePrevious()) || (await handleClose({ asset: action.asset }));
 
         // delete after find the next one
         assetStore.removeAssets([action.asset.id]);
@@ -580,17 +568,12 @@
     }
   };
 
-  let lastAssetMouseEvent: AssetResponseDto | null = null;
+  let lastAssetMouseEvent: AssetResponseDto | null = $state(null);
 
-  $: if (!lastAssetMouseEvent) {
-    assetInteractionStore.clearAssetSelectionCandidates();
-  }
-
-  let shiftKeyIsDown = false;
+  let shiftKeyIsDown = $state(false);
 
   const deselectAllAssets = () => {
-    $isSelectingAllAssets = false;
-    assetInteractionStore.clearMultiselect();
+    cancelMultiselect(assetInteraction);
   };
 
   const onKeyDown = (event: KeyboardEvent) => {
@@ -615,14 +598,6 @@
     }
   };
 
-  $: if (!shiftKeyIsDown) {
-    assetInteractionStore.clearAssetSelectionCandidates();
-  }
-
-  $: if (shiftKeyIsDown && lastAssetMouseEvent) {
-    selectAssetCandidates(lastAssetMouseEvent);
-  }
-
   const handleSelectAssetCandidates = (asset: AssetResponseDto | null) => {
     if (asset) {
       selectAssetCandidates(asset);
@@ -631,13 +606,13 @@
   };
 
   const handleGroupSelect = (group: string, assets: AssetResponseDto[]) => {
-    if ($selectedGroup.has(group)) {
-      assetInteractionStore.removeGroupFromMultiselectGroup(group);
+    if (assetInteraction.selectedGroup.has(group)) {
+      assetInteraction.removeGroupFromMultiselectGroup(group);
       for (const asset of assets) {
-        assetInteractionStore.removeAssetFromMultiselectGroup(asset);
+        assetInteraction.removeAssetFromMultiselectGroup(asset);
       }
     } else {
-      assetInteractionStore.addGroupToMultiselectGroup(group);
+      assetInteraction.addGroupToMultiselectGroup(group);
       for (const asset of assets) {
         handleSelectAsset(asset);
       }
@@ -649,33 +624,33 @@
       return;
     }
 
-    dispatch('select', asset);
+    onSelect(asset);
 
-    if (singleSelect) {
+    if (singleSelect && element) {
       element.scrollTop = 0;
       return;
     }
 
-    const rangeSelection = $assetSelectionCandidates.size > 0;
-    const deselect = $selectedAssets.has(asset);
+    const rangeSelection = assetInteraction.assetSelectionCandidates.size > 0;
+    const deselect = assetInteraction.selectedAssets.has(asset);
 
     // Select/deselect already loaded assets
     if (deselect) {
-      for (const candidate of $assetSelectionCandidates || []) {
-        assetInteractionStore.removeAssetFromMultiselectGroup(candidate);
+      for (const candidate of assetInteraction.assetSelectionCandidates) {
+        assetInteraction.removeAssetFromMultiselectGroup(candidate);
       }
-      assetInteractionStore.removeAssetFromMultiselectGroup(asset);
+      assetInteraction.removeAssetFromMultiselectGroup(asset);
     } else {
-      for (const candidate of $assetSelectionCandidates || []) {
+      for (const candidate of assetInteraction.assetSelectionCandidates) {
         handleSelectAsset(candidate);
       }
       handleSelectAsset(asset);
     }
 
-    assetInteractionStore.clearAssetSelectionCandidates();
+    assetInteraction.clearAssetSelectionCandidates();
 
-    if ($assetSelectionStart && rangeSelection) {
-      let startBucketIndex = $assetStore.getBucketIndexByAssetId($assetSelectionStart.id);
+    if (assetInteraction.assetSelectionStart && rangeSelection) {
+      let startBucketIndex = $assetStore.getBucketIndexByAssetId(assetInteraction.assetSelectionStart.id);
       let endBucketIndex = $assetStore.getBucketIndexByAssetId(asset.id);
 
       if (startBucketIndex === null || endBucketIndex === null) {
@@ -692,7 +667,7 @@
         await $assetStore.loadBucket(bucket.bucketDate);
         for (const asset of bucket.assets) {
           if (deselect) {
-            assetInteractionStore.removeAssetFromMultiselectGroup(asset);
+            assetInteraction.removeAssetFromMultiselectGroup(asset);
           } else {
             handleSelectAsset(asset);
           }
@@ -707,62 +682,135 @@
         const assetsGroupByDate = splitBucketIntoDateGroups(bucket, $locale);
         for (const dateGroup of assetsGroupByDate) {
           const dateGroupTitle = formatGroupTitle(dateGroup.date);
-          if (dateGroup.assets.every((a) => $selectedAssets.has(a))) {
-            assetInteractionStore.addGroupToMultiselectGroup(dateGroupTitle);
+          if (dateGroup.assets.every((a) => assetInteraction.selectedAssets.has(a))) {
+            assetInteraction.addGroupToMultiselectGroup(dateGroupTitle);
           } else {
-            assetInteractionStore.removeGroupFromMultiselectGroup(dateGroupTitle);
+            assetInteraction.removeGroupFromMultiselectGroup(dateGroupTitle);
           }
         }
       }
     }
 
-    assetInteractionStore.setAssetSelectionStart(deselect ? null : asset);
+    assetInteraction.setAssetSelectionStart(deselect ? null : asset);
   };
 
-  const selectAssetCandidates = (asset: AssetResponseDto) => {
+  const selectAssetCandidates = (endAsset: AssetResponseDto) => {
     if (!shiftKeyIsDown) {
       return;
     }
 
-    const rangeStart = $assetSelectionStart;
-    if (!rangeStart) {
+    const startAsset = assetInteraction.assetSelectionStart;
+    if (!startAsset) {
       return;
     }
 
-    let start = $assetStore.assets.indexOf(rangeStart);
-    let end = $assetStore.assets.indexOf(asset);
+    let start = $assetStore.assets.findIndex((a) => a.id === startAsset.id);
+    let end = $assetStore.assets.findIndex((a) => a.id === endAsset.id);
 
     if (start > end) {
       [start, end] = [end, start];
     }
 
-    assetInteractionStore.setAssetSelectionCandidates($assetStore.assets.slice(start, end + 1));
+    assetInteraction.setAssetSelectionCandidates($assetStore.assets.slice(start, end + 1));
   };
 
   const onSelectStart = (e: Event) => {
-    if ($isMultiSelectState && shiftKeyIsDown) {
+    if (assetInteraction.selectionActive && shiftKeyIsDown) {
       e.preventDefault();
     }
   };
   onDestroy(() => {
     assetStore.taskManager.removeAllTasksForComponent(componentId);
   });
+  let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
+  let isEmpty = $derived($assetStore.initialized && $assetStore.buckets.length === 0);
+  let idsSelectedAssets = $derived(assetInteraction.selectedAssetsArray.map(({ id }) => id));
+
+  $effect(() => {
+    if (isEmpty) {
+      assetInteraction.clearMultiselect();
+    }
+  });
+
+  $effect(() => {
+    if (element && isViewportOrigin()) {
+      const rect = element.getBoundingClientRect();
+      viewport.height = rect.height;
+      viewport.width = rect.width;
+      viewport.x = rect.x;
+      viewport.y = rect.y;
+    }
+    if (!isViewportOrigin() && !isEqual(viewport, safeViewport)) {
+      safeViewport.height = viewport.height;
+      safeViewport.width = viewport.width;
+      safeViewport.x = viewport.x;
+      safeViewport.y = viewport.y;
+      updateViewport();
+    }
+  });
+
+  let shortcutList = $derived(
+    (() => {
+      if ($isSearchEnabled || $showAssetViewer) {
+        return [];
+      }
+
+      const shortcuts: ShortcutOptions[] = [
+        { shortcut: { key: 'Escape' }, onShortcut: onEscape },
+        { shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) },
+        { shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
+        { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets($assetStore, assetInteraction) },
+        { shortcut: { key: 'PageDown' }, preventDefault: false, onShortcut: focusElement },
+        { shortcut: { key: 'PageUp' }, preventDefault: false, onShortcut: focusElement },
+      ];
+
+      if (assetInteraction.selectionActive) {
+        shortcuts.push(
+          { shortcut: { key: 'Delete' }, onShortcut: onDelete },
+          { shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete },
+          { shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
+          { shortcut: { key: 's' }, onShortcut: () => onStackAssets() },
+          { shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
+        );
+      }
+
+      return shortcuts;
+    })(),
+  );
+
+  $effect(() => {
+    if (!lastAssetMouseEvent) {
+      assetInteraction.clearAssetSelectionCandidates();
+    }
+  });
+
+  $effect(() => {
+    if (!shiftKeyIsDown) {
+      assetInteraction.clearAssetSelectionCandidates();
+    }
+  });
+
+  $effect(() => {
+    if (shiftKeyIsDown && lastAssetMouseEvent) {
+      selectAssetCandidates(lastAssetMouseEvent);
+    }
+  });
 </script>
 
-<svelte:window on:keydown={onKeyDown} on:keyup={onKeyUp} on:selectstart={onSelectStart} use:shortcuts={shortcutList} />
+<svelte:window onkeydown={onKeyDown} onkeyup={onKeyUp} onselectstart={onSelectStart} use:shortcuts={shortcutList} />
 
 {#if isShowDeleteConfirmation}
   <DeleteAssetDialog
     size={idsSelectedAssets.length}
-    on:cancel={() => (isShowDeleteConfirmation = false)}
-    on:confirm={() => handlePromiseError(trashOrDelete(true))}
+    onCancel={() => (isShowDeleteConfirmation = false)}
+    onConfirm={() => handlePromiseError(trashOrDelete(true))}
   />
 {/if}
 
 {#if showShortcuts}
-  <ShowShortcuts on:close={() => (showShortcuts = !showShortcuts)} />
+  <ShowShortcuts onClose={() => (showShortcuts = !showShortcuts)} />
 {/if}
-{#if assetStore.buckets.length > 0}
+{#if $assetStore.buckets.length > 0}
   <Scrubber
     invisible={showSkeleton}
     {assetStore}
@@ -785,16 +833,16 @@
   tabindex="-1"
   use:resizeObserver={({ height, width }) => ((viewport.width = width), (viewport.height = height))}
   bind:this={element}
-  on:scroll={() => ((assetStore.lastScrollTime = Date.now()), handleTimelineScroll())}
+  onscroll={() => ((assetStore.lastScrollTime = Date.now()), handleTimelineScroll())}
 >
   <section
     use:resizeObserver={({ target, height }) => ((topSectionHeight = height), (topSectionOffset = target.offsetTop))}
     class:invisible={showSkeleton}
   >
-    <slot />
+    {@render children?.()}
     {#if isEmpty}
       <!-- (optional) empty placeholder -->
-      <slot name="empty" />
+      {@render empty?.()}
     {/if}
   </section>
 
@@ -808,7 +856,7 @@
       {@const isPremeasure = preMeasure.includes(bucket)}
       {@const display = bucket.intersecting || bucket === $assetStore.pendingScrollBucket || isPremeasure}
       <div
-        id="bucket"
+        class="bucket"
         use:intersectionObserver={{
           key: bucket.viewId,
           onIntersect: () => handleIntersect(bucket),
@@ -840,16 +888,16 @@
             {withStacked}
             {showArchiveIcon}
             {assetStore}
-            {assetInteractionStore}
+            {assetInteraction}
             {isSelectionMode}
             {singleSelect}
             {onScrollTarget}
             {onAssetInGrid}
             {bucket}
             viewport={safeViewport}
-            on:select={({ detail: group }) => handleGroupSelect(group.title, group.assets)}
-            on:selectAssetCandidates={({ detail: asset }) => handleSelectAssetCandidates(asset)}
-            on:selectAssets={({ detail: asset }) => handleSelectAssets(asset)}
+            onSelect={({ title, assets }) => handleGroupSelect(title, assets)}
+            onSelectAssetCandidates={handleSelectAssetCandidates}
+            onSelectAssets={handleSelectAssets}
           />
         {/if}
       </div>
@@ -868,10 +916,11 @@
         preloadAssets={$preloadAssets}
         {isShared}
         {album}
+        {person}
         onAction={handleAction}
-        on:previous={handlePrevious}
-        on:next={handleNext}
-        on:close={handleClose}
+        onPrevious={handlePrevious}
+        onNext={handleNext}
+        onClose={handleClose}
       />
     {/await}
   {/if}
@@ -882,4 +931,9 @@
     contain: strict;
     scrollbar-width: none;
   }
+
+  .bucket {
+    contain: layout size;
+    transition: height 0.2s ease-out;
+  }
 </style>
diff --git a/web/src/lib/components/photos-page/asset-select-control-bar.svelte b/web/src/lib/components/photos-page/asset-select-control-bar.svelte
index c802c53454..2ab8f1e9c2 100644
--- a/web/src/lib/components/photos-page/asset-select-control-bar.svelte
+++ b/web/src/lib/components/photos-page/asset-select-control-bar.svelte
@@ -1,4 +1,4 @@
-<script lang="ts" context="module">
+<script lang="ts" module>
   import { createContext } from '$lib/utils/context';
   import { t } from 'svelte-i18n';
 
@@ -17,10 +17,16 @@
   import type { AssetResponseDto } from '@immich/sdk';
   import { mdiClose } from '@mdi/js';
   import ControlAppBar from '../shared-components/control-app-bar.svelte';
+  import type { Snippet } from 'svelte';
 
-  export let assets: Set<AssetResponseDto>;
-  export let clearSelect: () => void;
-  export let ownerId: string | undefined = undefined;
+  interface Props {
+    assets: Set<AssetResponseDto>;
+    clearSelect: () => void;
+    ownerId?: string | undefined;
+    children?: Snippet;
+  }
+
+  let { assets, clearSelect, ownerId = undefined, children }: Props = $props();
 
   setContext({
     getAssets: () => assets,
@@ -30,10 +36,14 @@
   });
 </script>
 
-<ControlAppBar on:close={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md">
-  <div class="font-medium text-immich-primary dark:text-immich-dark-primary" slot="leading">
-    <p class="block sm:hidden">{assets.size}</p>
-    <p class="hidden sm:block">{$t('selected_count', { values: { count: assets.size } })}</p>
-  </div>
-  <slot slot="trailing" />
+<ControlAppBar onClose={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md">
+  {#snippet leading()}
+    <div class="font-medium text-immich-primary dark:text-immich-dark-primary">
+      <p class="block sm:hidden">{assets.size}</p>
+      <p class="hidden sm:block">{$t('selected_count', { values: { count: assets.size } })}</p>
+    </div>
+  {/snippet}
+  {#snippet trailing()}
+    {@render children?.()}
+  {/snippet}
 </ControlAppBar>
diff --git a/web/src/lib/components/photos-page/delete-asset-dialog.svelte b/web/src/lib/components/photos-page/delete-asset-dialog.svelte
index 84782b2d7f..3053600a47 100644
--- a/web/src/lib/components/photos-page/delete-asset-dialog.svelte
+++ b/web/src/lib/components/photos-page/delete-asset-dialog.svelte
@@ -1,25 +1,25 @@
 <script lang="ts">
-  import { createEventDispatcher } from 'svelte';
   import ConfirmDialog from '../shared-components/dialog/confirm-dialog.svelte';
   import { showDeleteModal } from '$lib/stores/preferences.store';
   import Checkbox from '$lib/components/elements/checkbox.svelte';
   import { t } from 'svelte-i18n';
   import FormatMessage from '$lib/components/i18n/format-message.svelte';
 
-  export let size: number;
+  interface Props {
+    size: number;
+    onConfirm: () => void;
+    onCancel: () => void;
+  }
 
-  let checked = false;
+  let { size, onConfirm, onCancel }: Props = $props();
 
-  const dispatch = createEventDispatcher<{
-    confirm: void;
-    cancel: void;
-  }>();
+  let checked = $state(false);
 
   const handleConfirm = () => {
     if (checked) {
       $showDeleteModal = false;
     }
-    dispatch('confirm');
+    onConfirm();
   };
 </script>
 
@@ -27,12 +27,14 @@
   title={$t('permanently_delete_assets_count', { values: { count: size } })}
   confirmText={$t('delete')}
   onConfirm={handleConfirm}
-  onCancel={() => dispatch('cancel')}
+  {onCancel}
 >
-  <svelte:fragment slot="prompt">
+  {#snippet promptSnippet()}
     <p>
-      <FormatMessage key="permanently_delete_assets_prompt" values={{ count: size }} let:message>
-        <b>{message}</b>
+      <FormatMessage key="permanently_delete_assets_prompt" values={{ count: size }}>
+        {#snippet children({ message })}
+          <b>{message}</b>
+        {/snippet}
       </FormatMessage>
     </p>
     <p><b>{$t('cannot_undo_this_action')}</b></p>
@@ -40,5 +42,5 @@
     <div class="pt-4 flex justify-center items-center">
       <Checkbox id="confirm-deletion-input" label={$t('do_not_show_again')} bind:checked />
     </div>
-  </svelte:fragment>
+  {/snippet}
 </ConfirmDialog>
diff --git a/web/src/lib/components/photos-page/measure-date-group.svelte b/web/src/lib/components/photos-page/measure-date-group.svelte
index f458fe40dd..80ad7640fb 100644
--- a/web/src/lib/components/photos-page/measure-date-group.svelte
+++ b/web/src/lib/components/photos-page/measure-date-group.svelte
@@ -1,4 +1,4 @@
-<script lang="ts" context="module">
+<script lang="ts" module>
   const recentTimes: number[] = [];
   // TODO: track average time to measure, and use this to populate TUNABLES.ASSETS_STORE.CHECK_INTERVAL_MS
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -20,9 +20,13 @@
   import { resizeObserver } from '$lib/actions/resize-observer';
   import type { AssetBucket, AssetStore, BucketListener } from '$lib/stores/assets.store';
 
-  export let assetStore: AssetStore;
-  export let bucket: AssetBucket;
-  export let onMeasured: () => void;
+  interface Props {
+    assetStore: AssetStore;
+    bucket: AssetBucket;
+    onMeasured: () => void;
+  }
+
+  let { assetStore, bucket, onMeasured }: Props = $props();
 
   async function _measure(element: Element) {
     try {
diff --git a/web/src/lib/components/photos-page/memory-lane.svelte b/web/src/lib/components/photos-page/memory-lane.svelte
index 459c7a6118..3a6ac7e8cf 100644
--- a/web/src/lib/components/photos-page/memory-lane.svelte
+++ b/web/src/lib/components/photos-page/memory-lane.svelte
@@ -11,27 +11,29 @@
   import { fade } from 'svelte/transition';
   import { t } from 'svelte-i18n';
 
-  $: shouldRender = $memoryStore?.length > 0;
+  let shouldRender = $derived($memoryStore?.length > 0);
 
   onMount(async () => {
     const localTime = new Date();
     $memoryStore = await getMemoryLane({ month: localTime.getMonth() + 1, day: localTime.getDate() });
   });
 
-  let memoryLaneElement: HTMLElement;
-  let offsetWidth = 0;
-  let innerWidth = 0;
+  let memoryLaneElement: HTMLElement | undefined = $state();
+  let offsetWidth = $state(0);
+  let innerWidth = $state(0);
 
-  let scrollLeftPosition = 0;
+  let scrollLeftPosition = $state(0);
 
-  const onScroll = () => (scrollLeftPosition = memoryLaneElement?.scrollLeft);
+  const onScroll = () => {
+    scrollLeftPosition = memoryLaneElement?.scrollLeft ?? 0;
+  };
 
-  $: canScrollLeft = scrollLeftPosition > 0;
-  $: canScrollRight = Math.ceil(scrollLeftPosition) < innerWidth - offsetWidth;
+  let canScrollLeft = $derived(scrollLeftPosition > 0);
+  let canScrollRight = $derived(Math.ceil(scrollLeftPosition) < innerWidth - offsetWidth);
 
   const scrollBy = 400;
-  const scrollLeft = () => memoryLaneElement.scrollBy({ left: -scrollBy, behavior: 'smooth' });
-  const scrollRight = () => memoryLaneElement.scrollBy({ left: scrollBy, behavior: 'smooth' });
+  const scrollLeft = () => memoryLaneElement?.scrollBy({ left: -scrollBy, behavior: 'smooth' });
+  const scrollRight = () => memoryLaneElement?.scrollBy({ left: scrollBy, behavior: 'smooth' });
 </script>
 
 {#if shouldRender}
@@ -40,7 +42,7 @@
     bind:this={memoryLaneElement}
     class="relative mt-5 overflow-x-hidden whitespace-nowrap transition-all"
     use:resizeObserver={({ width }) => (offsetWidth = width)}
-    on:scroll={onScroll}
+    onscroll={onScroll}
   >
     {#if canScrollLeft || canScrollRight}
       <div class="sticky left-0 z-20">
@@ -49,7 +51,7 @@
             <button
               type="button"
               class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100"
-              on:click={scrollLeft}
+              onclick={scrollLeft}
             >
               <Icon path={mdiChevronLeft} size="36" /></button
             >
@@ -60,7 +62,7 @@
             <button
               type="button"
               class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100"
-              on:click={scrollRight}
+              onclick={scrollRight}
             >
               <Icon path={mdiChevronRight} size="36" /></button
             >
@@ -86,7 +88,7 @@
             </p>
             <div
               class="absolute left-0 top-0 z-0 h-full w-full rounded-xl bg-gradient-to-t from-black/40 via-transparent to-transparent transition-all hover:bg-black/20"
-            />
+            ></div>
           </a>
         {/if}
       {/each}
diff --git a/web/src/lib/components/photos-page/skeleton.svelte b/web/src/lib/components/photos-page/skeleton.svelte
index 07836eb4db..601a40cce2 100644
--- a/web/src/lib/components/photos-page/skeleton.svelte
+++ b/web/src/lib/components/photos-page/skeleton.svelte
@@ -1,6 +1,10 @@
 <script lang="ts">
-  export let title: string | null = null;
-  export let height: string | null = null;
+  interface Props {
+    title?: string | null;
+    height?: string | null;
+  }
+
+  let { title = null, height = null }: Props = $props();
 </script>
 
 <div class="overflow-clip" style={`height: ${height}`}>
diff --git a/web/src/lib/components/share-page/individual-shared-viewer.svelte b/web/src/lib/components/share-page/individual-shared-viewer.svelte
index af5c54c988..ebc4b49001 100644
--- a/web/src/lib/components/share-page/individual-shared-viewer.svelte
+++ b/web/src/lib/components/share-page/individual-shared-viewer.svelte
@@ -6,7 +6,7 @@
   import { downloadArchive } from '$lib/utils/asset-utils';
   import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
   import { handleError } from '$lib/utils/handle-error';
-  import { addSharedLinkAssets, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
+  import { addSharedLinkAssets, type SharedLinkResponseDto } from '@immich/sdk';
   import { mdiArrowLeft, mdiFileImagePlusOutline, mdiFolderDownloadOutline, mdiSelectAll } from '@mdi/js';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
   import DownloadAction from '../photos-page/actions/download-action.svelte';
@@ -14,20 +14,25 @@
   import AssetSelectControlBar from '../photos-page/asset-select-control-bar.svelte';
   import ControlAppBar from '../shared-components/control-app-bar.svelte';
   import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
+  import { cancelMultiselect } from '$lib/utils/asset-utils';
   import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
   import { NotificationType, notificationController } from '../shared-components/notification/notification';
   import type { Viewport } from '$lib/stores/assets.store';
   import { t } from 'svelte-i18n';
+  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 
-  export let sharedLink: SharedLinkResponseDto;
-  export let isOwned: boolean;
+  interface Props {
+    sharedLink: SharedLinkResponseDto;
+    isOwned: boolean;
+  }
 
-  const viewport: Viewport = { width: 0, height: 0 };
-  let selectedAssets: Set<AssetResponseDto> = new Set();
-  let innerWidth: number;
+  let { sharedLink = $bindable(), isOwned }: Props = $props();
 
-  $: assets = sharedLink.assets;
-  $: isMultiSelectionMode = selectedAssets.size > 0;
+  const viewport: Viewport = $state({ width: 0, height: 0 });
+  const assetInteraction = new AssetInteraction();
+  let innerWidth: number = $state(0);
+
+  let assets = $derived(sharedLink.assets);
 
   dragAndDropFilesStore.subscribe((value) => {
     if (value.isDragging && value.files.length > 0) {
@@ -66,16 +71,19 @@
   };
 
   const handleSelectAll = () => {
-    selectedAssets = new Set(assets);
+    assetInteraction.selectAssets(assets);
   };
 </script>
 
 <svelte:window bind:innerWidth />
 
 <section class="bg-immich-bg dark:bg-immich-dark-bg">
-  {#if isMultiSelectionMode}
-    <AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
-      <CircleIconButton title={$t('select_all')} icon={mdiSelectAll} on:click={handleSelectAll} />
+  {#if assetInteraction.selectionActive}
+    <AssetSelectControlBar
+      assets={assetInteraction.selectedAssets}
+      clearSelect={() => cancelMultiselect(assetInteraction)}
+    >
+      <CircleIconButton title={$t('select_all')} icon={mdiSelectAll} onclick={handleSelectAll} />
       {#if sharedLink?.allowDownload}
         <DownloadAction filename="immich-shared.zip" />
       {/if}
@@ -84,27 +92,27 @@
       {/if}
     </AssetSelectControlBar>
   {:else}
-    <ControlAppBar on:close={() => goto(AppRoute.PHOTOS)} backIcon={mdiArrowLeft} showBackButton={false}>
-      <svelte:fragment slot="leading">
+    <ControlAppBar onClose={() => goto(AppRoute.PHOTOS)} backIcon={mdiArrowLeft} showBackButton={false}>
+      {#snippet leading()}
         <ImmichLogoSmallLink width={innerWidth} />
-      </svelte:fragment>
+      {/snippet}
 
-      <svelte:fragment slot="trailing">
+      {#snippet trailing()}
         {#if sharedLink?.allowUpload}
           <CircleIconButton
             title={$t('add_photos')}
-            on:click={() => handleUploadAssets()}
+            onclick={() => handleUploadAssets()}
             icon={mdiFileImagePlusOutline}
           />
         {/if}
 
         {#if sharedLink?.allowDownload}
-          <CircleIconButton title={$t('download')} on:click={downloadAssets} icon={mdiFolderDownloadOutline} />
+          <CircleIconButton title={$t('download')} onclick={downloadAssets} icon={mdiFolderDownloadOutline} />
         {/if}
-      </svelte:fragment>
+      {/snippet}
     </ControlAppBar>
   {/if}
   <section class="my-[160px] mx-4" bind:clientHeight={viewport.height} bind:clientWidth={viewport.width}>
-    <GalleryViewer {assets} bind:selectedAssets {viewport} />
+    <GalleryViewer {assets} {assetInteraction} {viewport} />
   </section>
 </section>
diff --git a/web/src/lib/components/shared-components/__test__/combobox.spec.ts b/web/src/lib/components/shared-components/__test__/combobox.spec.ts
new file mode 100644
index 0000000000..e1518809b4
--- /dev/null
+++ b/web/src/lib/components/shared-components/__test__/combobox.spec.ts
@@ -0,0 +1,30 @@
+import { getIntersectionObserverMock } from '$lib/__mocks__/intersection-observer.mock';
+import { getVisualViewportMock } from '$lib/__mocks__/visual-viewport.mock';
+import Combobox from '$lib/components/shared-components/combobox.svelte';
+import { render, screen } from '@testing-library/svelte';
+
+describe('Combobox component', () => {
+  beforeAll(() => {
+    vi.stubGlobal('IntersectionObserver', getIntersectionObserverMock());
+    vi.stubGlobal('visualViewport', getVisualViewportMock());
+  });
+
+  it('shows selected option', () => {
+    render(Combobox, {
+      label: 'test',
+      selectedOption: { label: 'option-1', value: 'option-1' },
+    });
+
+    expect(screen.getByRole('combobox')).toHaveValue('option-1');
+  });
+
+  it('clears the selected option when set to undefined', async () => {
+    const { rerender } = render(Combobox, {
+      label: 'test',
+      selectedOption: { label: 'option-1', value: 'option-1' },
+    });
+
+    await rerender({ selectedOption: undefined });
+    expect(screen.getByRole('combobox')).toHaveValue('');
+  });
+});
diff --git a/web/src/lib/components/shared-components/__test__/number-range-input.spec.ts b/web/src/lib/components/shared-components/__test__/number-range-input.spec.ts
index d95b9114fd..be09d2a35c 100644
--- a/web/src/lib/components/shared-components/__test__/number-range-input.spec.ts
+++ b/web/src/lib/components/shared-components/__test__/number-range-input.spec.ts
@@ -1,5 +1,5 @@
 import NumberRangeInput from '$lib/components/shared-components/number-range-input.svelte';
-import { act, render, type RenderResult } from '@testing-library/svelte';
+import { render, type RenderResult } from '@testing-library/svelte';
 import userEvent from '@testing-library/user-event';
 
 describe('NumberRangeInput component', () => {
@@ -8,13 +8,18 @@ describe('NumberRangeInput component', () => {
   let input: HTMLInputElement;
 
   beforeEach(() => {
-    sut = render(NumberRangeInput, { id: '', min: -90, max: 90, onInput: () => {} });
+    sut = render(NumberRangeInput, {
+      id: '',
+      min: -90,
+      max: 90,
+      onInput: () => {},
+    });
     input = sut.getByRole('spinbutton') as HTMLInputElement;
   });
 
   it('updates value', async () => {
     expect(input.value).toBe('');
-    await act(() => sut.component.$set({ value: 10 }));
+    await sut.rerender({ value: 10 });
     expect(input.value).toBe('10');
   });
 
diff --git a/web/src/lib/components/shared-components/album-selection-modal.svelte b/web/src/lib/components/shared-components/album-selection-modal.svelte
index 0690374c01..3400864efd 100644
--- a/web/src/lib/components/shared-components/album-selection-modal.svelte
+++ b/web/src/lib/components/shared-components/album-selection-modal.svelte
@@ -2,7 +2,7 @@
   import Icon from '$lib/components/elements/icon.svelte';
   import { getAllAlbums, type AlbumResponseDto } from '@immich/sdk';
   import { mdiPlus } from '@mdi/js';
-  import { createEventDispatcher, onMount } from 'svelte';
+  import { onMount } from 'svelte';
   import AlbumListItem from '../asset-viewer/album-list-item.svelte';
   import { normalizeSearchString } from '$lib/utils/string-utils';
   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
@@ -11,19 +11,19 @@
   import { sortAlbums } from '$lib/utils/album-utils';
   import { albumViewSettings } from '$lib/stores/preferences.store';
 
-  let albums: AlbumResponseDto[] = [];
-  let recentAlbums: AlbumResponseDto[] = [];
-  let filteredAlbums: AlbumResponseDto[] = [];
-  let loading = true;
-  let search = '';
+  let albums: AlbumResponseDto[] = $state([]);
+  let recentAlbums: AlbumResponseDto[] = $state([]);
+  let loading = $state(true);
+  let search = $state('');
 
-  const dispatch = createEventDispatcher<{
-    newAlbum: string;
-    album: AlbumResponseDto;
-  }>();
+  interface Props {
+    onNewAlbum: (search: string) => void;
+    onAlbumClick: (album: AlbumResponseDto) => void;
+    shared: boolean;
+    onClose: () => void;
+  }
 
-  export let shared: boolean;
-  export let onClose: () => void;
+  let { onNewAlbum, onAlbumClick, shared, onClose }: Props = $props();
 
   onMount(async () => {
     albums = await getAllAlbums({ shared: shared || undefined });
@@ -31,23 +31,17 @@
     loading = false;
   });
 
-  $: filteredAlbums = sortAlbums(
-    search.length > 0 && albums.length > 0
-      ? albums.filter((album) => {
-          return normalizeSearchString(album.albumName).includes(normalizeSearchString(search));
-        })
-      : albums,
-    { sortBy: $albumViewSettings.sortBy, orderBy: $albumViewSettings.sortOrder },
+  let filteredAlbums = $derived(
+    sortAlbums(
+      search.length > 0 && albums.length > 0
+        ? albums.filter((album) => {
+            return normalizeSearchString(album.albumName).includes(normalizeSearchString(search));
+          })
+        : albums,
+      { sortBy: $albumViewSettings.sortBy, orderBy: $albumViewSettings.sortOrder },
+    ),
   );
 
-  const handleSelect = (album: AlbumResponseDto) => {
-    dispatch('album', album);
-  };
-
-  const handleNew = () => {
-    dispatch('newAlbum', search.length > 0 ? search : '');
-  };
-
   const getTitle = () => {
     if (shared) {
       return $t('add_to_shared_album');
@@ -61,12 +55,12 @@
     {#if loading}
       {#each { length: 3 } as _}
         <div class="flex animate-pulse gap-4 px-6 py-2">
-          <div class="h-12 w-12 rounded-xl bg-slate-200" />
+          <div class="h-12 w-12 rounded-xl bg-slate-200"></div>
           <div class="flex flex-col items-start justify-center gap-2">
-            <span class="h-4 w-36 animate-pulse bg-slate-200" />
+            <span class="h-4 w-36 animate-pulse bg-slate-200"></span>
             <div class="flex animate-pulse gap-1">
-              <span class="h-3 w-8 bg-slate-200" />
-              <span class="h-3 w-20 bg-slate-200" />
+              <span class="h-3 w-8 bg-slate-200"></span>
+              <span class="h-3 w-20 bg-slate-200"></span>
             </div>
           </div>
         </div>
@@ -81,7 +75,7 @@
       <div class="immich-scrollbar overflow-y-auto">
         <button
           type="button"
-          on:click={handleNew}
+          onclick={() => onNewAlbum(search)}
           class="flex w-full items-center gap-4 px-6 py-2 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"
         >
           <div class="flex h-12 w-12 items-center justify-center">
@@ -96,7 +90,7 @@
           {#if !shared && search.length === 0}
             <p class="px-5 py-3 text-xs">{$t('recent').toUpperCase()}</p>
             {#each recentAlbums as album (album.id)}
-              <AlbumListItem {album} on:album={() => handleSelect(album)} />
+              <AlbumListItem {album} onAlbumClick={() => onAlbumClick(album)} />
             {/each}
           {/if}
 
@@ -106,7 +100,7 @@
             </p>
           {/if}
           {#each filteredAlbums as album (album.id)}
-            <AlbumListItem {album} searchQuery={search} on:album={() => handleSelect(album)} />
+            <AlbumListItem {album} searchQuery={search} onAlbumClick={() => onAlbumClick(album)} />
           {/each}
         {:else if albums.length > 0}
           <p class="px-5 py-1 text-sm">{$t('no_albums_with_name_yet')}</p>
diff --git a/web/src/lib/components/shared-components/autogrow-textarea.svelte b/web/src/lib/components/shared-components/autogrow-textarea.svelte
index efbcf218e6..7a215e7e62 100644
--- a/web/src/lib/components/shared-components/autogrow-textarea.svelte
+++ b/web/src/lib/components/shared-components/autogrow-textarea.svelte
@@ -1,26 +1,21 @@
 <script lang="ts">
   import { autoGrowHeight } from '$lib/actions/autogrow';
   import { shortcut } from '$lib/actions/shortcut';
-  import { tick } from 'svelte';
 
-  export let content: string = '';
-  let className: string = '';
-  export { className as class };
-  export let onContentUpdate: (newContent: string) => void = () => null;
-  export let placeholder: string = '';
-
-  let textarea: HTMLTextAreaElement;
-  $: newContent = content;
-
-  $: {
-    // re-visit with svelte 5. runes will make this better.
-    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
-    newContent;
-    if (textarea && newContent.length > 0) {
-      void tick().then(() => autoGrowHeight(textarea));
-    }
+  interface Props {
+    content?: string;
+    class?: string;
+    onContentUpdate?: (newContent: string) => void;
+    placeholder?: string;
   }
 
+  let { content = '', class: className = '', onContentUpdate = () => null, placeholder = '' }: Props = $props();
+
+  let newContent = $state(content);
+  $effect(() => {
+    newContent = content;
+  });
+
   const updateContent = () => {
     if (content === newContent) {
       return;
@@ -30,14 +25,14 @@
 </script>
 
 <textarea
-  bind:this={textarea}
+  bind:value={newContent}
   class="resize-none {className}"
-  on:focusout={updateContent}
-  on:input={(e) => (newContent = e.currentTarget.value)}
+  onfocusout={updateContent}
   {placeholder}
   use:shortcut={{
     shortcut: { key: 'Enter', ctrl: true },
     onShortcut: (e) => e.currentTarget.blur(),
   }}
+  use:autoGrowHeight={{ value: newContent }}
   data-testid="autogrow-textarea">{content}</textarea
 >
diff --git a/web/src/lib/components/shared-components/change-date.spec.ts b/web/src/lib/components/shared-components/change-date.spec.ts
new file mode 100644
index 0000000000..38c72838e5
--- /dev/null
+++ b/web/src/lib/components/shared-components/change-date.spec.ts
@@ -0,0 +1,72 @@
+import { getIntersectionObserverMock } from '$lib/__mocks__/intersection-observer.mock';
+import { getVisualViewportMock } from '$lib/__mocks__/visual-viewport.mock';
+import { fireEvent, render, screen } from '@testing-library/svelte';
+import { DateTime } from 'luxon';
+import ChangeDate from './change-date.svelte';
+
+describe('ChangeDate component', () => {
+  const initialDate = DateTime.fromISO('2024-01-01');
+  const initialTimeZone = 'Europe/Berlin';
+  const onCancel = vi.fn();
+  const onConfirm = vi.fn();
+
+  const getDateInput = () => screen.getByLabelText('date_and_time') as HTMLInputElement;
+  const getTimeZoneInput = () => screen.getByLabelText('timezone') as HTMLInputElement;
+  const getCancelButton = () => screen.getByText('cancel');
+  const getConfirmButton = () => screen.getByText('confirm');
+
+  beforeEach(() => {
+    vi.stubGlobal('IntersectionObserver', getIntersectionObserverMock());
+    vi.stubGlobal('visualViewport', getVisualViewportMock());
+  });
+
+  afterEach(() => {
+    vi.resetAllMocks();
+  });
+
+  test('should render correct values', () => {
+    render(ChangeDate, { initialDate, initialTimeZone, onCancel, onConfirm });
+    expect(getDateInput().value).toBe('2024-01-01T00:00');
+    expect(getTimeZoneInput().value).toBe('Europe/Berlin (+01:00)');
+  });
+
+  test('calls onConfirm with correct date on confirm', async () => {
+    render(ChangeDate, {
+      props: { initialDate, initialTimeZone, onCancel, onConfirm },
+    });
+
+    await fireEvent.click(getConfirmButton());
+
+    expect(onConfirm).toHaveBeenCalledWith('2024-01-01T00:00:00.000+01:00');
+  });
+
+  test('calls onCancel on cancel', async () => {
+    render(ChangeDate, {
+      props: { initialDate, initialTimeZone, onCancel, onConfirm },
+    });
+
+    await fireEvent.click(getCancelButton());
+
+    expect(onCancel).toHaveBeenCalled();
+  });
+
+  describe('when date is in daylight saving time', () => {
+    const dstDate = DateTime.fromISO('2024-07-01');
+
+    test('should render correct timezone with offset', () => {
+      render(ChangeDate, { initialDate: dstDate, initialTimeZone, onCancel, onConfirm });
+
+      expect(getTimeZoneInput().value).toBe('Europe/Berlin (+02:00)');
+    });
+
+    test('calls onConfirm with correct date on confirm', async () => {
+      render(ChangeDate, {
+        props: { initialDate: dstDate, initialTimeZone, onCancel, onConfirm },
+      });
+
+      await fireEvent.click(getConfirmButton());
+
+      expect(onConfirm).toHaveBeenCalledWith('2024-07-01T00:00:00.000+02:00');
+    });
+  });
+});
diff --git a/web/src/lib/components/shared-components/change-date.svelte b/web/src/lib/components/shared-components/change-date.svelte
index 80eaa3d819..13b2752f0c 100644
--- a/web/src/lib/components/shared-components/change-date.svelte
+++ b/web/src/lib/components/shared-components/change-date.svelte
@@ -1,13 +1,18 @@
 <script lang="ts">
-  import { createEventDispatcher } from 'svelte';
   import { DateTime } from 'luxon';
   import ConfirmDialog from './dialog/confirm-dialog.svelte';
-  import Combobox from './combobox.svelte';
+  import Combobox, { type ComboBoxOption } from './combobox.svelte';
   import DateInput from '../elements/date-input.svelte';
   import { t } from 'svelte-i18n';
 
-  export let initialDate: DateTime = DateTime.now();
-  export let initialTimeZone: string = '';
+  interface Props {
+    initialDate?: DateTime;
+    initialTimeZone?: string;
+    onCancel: () => void;
+    onConfirm: (date: string) => void;
+  }
+
+  let { initialDate = DateTime.now(), initialTimeZone = '', onCancel, onConfirm }: Props = $props();
 
   type ZoneOption = {
     /**
@@ -48,21 +53,15 @@
 
   const knownTimezones = Intl.supportedValuesOf('timeZone');
 
-  let timezones: ZoneOption[];
-  $: timezones = knownTimezones
+  const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+  let selectedDate = $state(initialDate.toFormat("yyyy-MM-dd'T'HH:mm"));
+  let timezones: ZoneOption[] = knownTimezones
     .map((zone) => zoneOptionForDate(zone, selectedDate))
     .filter((zone) => zone.valid)
     .sort((zoneA, zoneB) => sortTwoZones(zoneA, zoneB));
-
-  const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
   // the offsets (and validity) for time zones may change if the date is changed, which is why we recompute the list
-  let selectedOption: ZoneOption | undefined;
-  $: selectedOption = getPreferredTimeZone(initialDate, userTimeZone, timezones, selectedOption);
-
-  let selectedDate = initialDate.toFormat("yyyy-MM-dd'T'HH:mm");
-
-  // when changing the time zone, assume the configured date/time is meant for that time zone (instead of updating it)
-  $: date = DateTime.fromISO(selectedDate, { zone: selectedOption?.value, setZone: true });
+  let selectedOption: ZoneOption | undefined = $state(getPreferredTimeZone(initialDate, userTimeZone, timezones));
 
   function zoneOptionForDate(zone: string, date: string) {
     const dateAtZone: DateTime = DateTime.fromISO(date, { zone });
@@ -118,19 +117,20 @@
     return zoneA.value.localeCompare(zoneB.value, undefined, { sensitivity: 'base' });
   }
 
-  const dispatch = createEventDispatcher<{
-    cancel: void;
-    confirm: string;
-  }>();
-
-  const handleCancel = () => dispatch('cancel');
-
   const handleConfirm = () => {
     const value = date.toISO();
     if (value) {
-      dispatch('confirm', value);
+      onConfirm(value);
     }
   };
+
+  const handleOnSelect = (option?: ComboBoxOption) => {
+    if (option) {
+      selectedOption = getPreferredTimeZone(initialDate, userTimeZone, timezones, option as ZoneOption);
+    }
+  };
+  // when changing the time zone, assume the configured date/time is meant for that time zone (instead of updating it)
+  let date = $derived(DateTime.fromISO(selectedDate, { zone: selectedOption?.value, setZone: true }));
 </script>
 
 <ConfirmDialog
@@ -139,15 +139,25 @@
   prompt="Please select a new date:"
   disabled={!date.isValid}
   onConfirm={handleConfirm}
-  onCancel={handleCancel}
+  {onCancel}
 >
-  <div class="flex flex-col text-left gap-2" slot="prompt">
-    <div class="flex flex-col">
-      <label for="datetime">{$t('date_and_time')}</label>
-      <DateInput class="immich-form-input" id="datetime" type="datetime-local" bind:value={selectedDate} />
+  <!-- @migration-task: migrate this slot by hand, `prompt` would shadow a prop on the parent component -->
+  <!-- @migration-task: migrate this slot by hand, `prompt` would shadow a prop on the parent component -->
+  {#snippet promptSnippet()}
+    <div class="flex flex-col text-left gap-2">
+      <div class="flex flex-col">
+        <label for="datetime">{$t('date_and_time')}</label>
+        <DateInput class="immich-form-input" id="datetime" type="datetime-local" bind:value={selectedDate} />
+      </div>
+      <div>
+        <Combobox
+          bind:selectedOption
+          label={$t('timezone')}
+          options={timezones}
+          placeholder={$t('search_timezone')}
+          onSelect={(option) => handleOnSelect(option)}
+        />
+      </div>
     </div>
-    <div>
-      <Combobox bind:selectedOption label={$t('timezone')} options={timezones} placeholder={$t('search_timezone')} />
-    </div>
-  </div>
+  {/snippet}
 </ConfirmDialog>
diff --git a/web/src/lib/components/shared-components/change-location.svelte b/web/src/lib/components/shared-components/change-location.svelte
index 3b0cb7bcc1..2b027596f4 100644
--- a/web/src/lib/components/shared-components/change-location.svelte
+++ b/web/src/lib/components/shared-components/change-location.svelte
@@ -1,5 +1,4 @@
 <script lang="ts">
-  import { createEventDispatcher } from 'svelte';
   import ConfirmDialog from './dialog/confirm-dialog.svelte';
   import { timeDebounceOnSearch } from '$lib/constants';
   import { handleError } from '$lib/utils/handle-error';
@@ -13,54 +12,50 @@
   import { listNavigation } from '$lib/actions/list-navigation';
   import { t } from 'svelte-i18n';
   import CoordinatesInput from '$lib/components/shared-components/coordinates-input.svelte';
-
-  export let asset: AssetResponseDto | undefined = undefined;
+  import Map from '$lib/components/shared-components/map/map.svelte';
 
   interface Point {
     lng: number;
     lat: number;
   }
 
-  let places: PlacesResponseDto[] = [];
-  let suggestedPlaces: PlacesResponseDto[] = [];
-  let searchWord: string;
+  interface Props {
+    asset?: AssetResponseDto | undefined;
+    onCancel: () => void;
+    onConfirm: (point: Point) => void;
+  }
+
+  let { asset = undefined, onCancel, onConfirm }: Props = $props();
+
+  let places: PlacesResponseDto[] = $state([]);
+  let suggestedPlaces: PlacesResponseDto[] = $state([]);
+  let searchWord: string = $state('');
   let latestSearchTimeout: number;
-  let showLoadingSpinner = false;
-  let suggestionContainer: HTMLDivElement;
-  let hideSuggestion = false;
-  let addClipMapMarker: (long: number, lat: number) => void;
+  let showLoadingSpinner = $state(false);
+  let suggestionContainer: HTMLDivElement | undefined = $state();
+  let hideSuggestion = $state(false);
+  let mapElement = $state<ReturnType<typeof Map>>();
 
-  const dispatch = createEventDispatcher<{
-    cancel: void;
-    confirm: Point;
-  }>();
+  let lat = $derived(asset?.exifInfo?.latitude ?? undefined);
+  let lng = $derived(asset?.exifInfo?.longitude ?? undefined);
+  let zoom = $derived(lat !== undefined && lng !== undefined ? 12.5 : 1);
 
-  $: lat = asset?.exifInfo?.latitude ?? undefined;
-  $: lng = asset?.exifInfo?.longitude ?? undefined;
-  $: zoom = lat !== undefined && lng !== undefined ? 12.5 : 1;
-
-  $: {
+  $effect(() => {
     if (places) {
       suggestedPlaces = places.slice(0, 5);
     }
     if (searchWord === '') {
       suggestedPlaces = [];
     }
-  }
+  });
 
-  let point: Point | null = null;
-
-  const handleCancel = () => dispatch('cancel');
-
-  const handleSelect = (selected: Point) => {
-    point = selected;
-  };
+  let point: Point | null = $state(null);
 
   const handleConfirm = () => {
     if (point) {
-      dispatch('confirm', point);
+      onConfirm(point);
     } else {
-      dispatch('cancel');
+      onCancel();
     }
   };
 
@@ -74,6 +69,7 @@
     }
     showLoadingSpinner = true;
 
+    // eslint-disable-next-line unicorn/prefer-global-this
     const searchTimeout = window.setTimeout(() => {
       if (searchWord === '') {
         places = [];
@@ -104,96 +100,95 @@
   const handleUseSuggested = (latitude: number, longitude: number) => {
     hideSuggestion = true;
     point = { lng: longitude, lat: latitude };
-    addClipMapMarker(longitude, latitude);
+    mapElement?.addClipMapMarker(longitude, latitude);
   };
 </script>
 
-<ConfirmDialog
-  confirmColor="primary"
-  title={$t('change_location')}
-  width="wide"
-  onConfirm={handleConfirm}
-  onCancel={handleCancel}
->
-  <div slot="prompt" class="flex flex-col w-full h-full gap-2">
-    <div
-      class="relative w-64 sm:w-96"
-      use:clickOutside={{ onOutclick: () => (hideSuggestion = true) }}
-      use:listNavigation={suggestionContainer}
-    >
-      <button type="button" class="w-full" on:click={() => (hideSuggestion = false)}>
-        <SearchBar
-          placeholder={$t('search_places')}
-          bind:name={searchWord}
-          {showLoadingSpinner}
-          on:reset={() => {
-            suggestedPlaces = [];
-          }}
-          on:search={handleSearchPlaces}
-          roundedBottom={suggestedPlaces.length === 0 || hideSuggestion}
-        />
-      </button>
-      <div class="absolute z-[99] w-full" id="suggestion" bind:this={suggestionContainer}>
-        {#if !hideSuggestion}
-          {#each suggestedPlaces as place, index}
-            <button
-              type="button"
-              class=" flex w-full border-t border-gray-400 dark:border-immich-dark-gray h-14 place-items-center bg-gray-200 p-2 dark:bg-gray-700 hover:bg-gray-300 hover:dark:bg-[#232932] focus:bg-gray-300 focus:dark:bg-[#232932] {index ===
-              suggestedPlaces.length - 1
-                ? 'rounded-b-lg border-b'
-                : ''}"
-              on:click={() => handleUseSuggested(place.latitude, place.longitude)}
-            >
-              <p class="ml-4 text-sm text-gray-700 dark:text-gray-100 truncate">
-                {getLocation(place.name, place.admin1name, place.admin2name)}
-              </p>
+<ConfirmDialog confirmColor="primary" title={$t('change_location')} width="wide" onConfirm={handleConfirm} {onCancel}>
+  {#snippet promptSnippet()}
+    <div class="flex flex-col w-full h-full gap-2">
+      <div class="relative w-64 sm:w-96">
+        {#if suggestionContainer}
+          <div
+            use:clickOutside={{ onOutclick: () => (hideSuggestion = true) }}
+            use:listNavigation={suggestionContainer}
+          >
+            <button type="button" class="w-full" onclick={() => (hideSuggestion = false)}>
+              <SearchBar
+                placeholder={$t('search_places')}
+                bind:name={searchWord}
+                {showLoadingSpinner}
+                onReset={() => (suggestedPlaces = [])}
+                onSearch={handleSearchPlaces}
+                roundedBottom={suggestedPlaces.length === 0 || hideSuggestion}
+              />
             </button>
-          {/each}
+          </div>
         {/if}
+
+        <div class="absolute z-[99] w-full" id="suggestion" bind:this={suggestionContainer}>
+          {#if !hideSuggestion}
+            {#each suggestedPlaces as place, index}
+              <button
+                type="button"
+                class=" flex w-full border-t border-gray-400 dark:border-immich-dark-gray h-14 place-items-center bg-gray-200 p-2 dark:bg-gray-700 hover:bg-gray-300 hover:dark:bg-[#232932] focus:bg-gray-300 focus:dark:bg-[#232932] {index ===
+                suggestedPlaces.length - 1
+                  ? 'rounded-b-lg border-b'
+                  : ''}"
+                onclick={() => handleUseSuggested(place.latitude, place.longitude)}
+              >
+                <p class="ml-4 text-sm text-gray-700 dark:text-gray-100 truncate">
+                  {getLocation(place.name, place.admin1name, place.admin2name)}
+                </p>
+              </button>
+            {/each}
+          {/if}
+        </div>
+      </div>
+
+      <span>{$t('pick_a_location')}</span>
+      <div class="h-[500px] min-h-[300px] w-full">
+        {#await import('../shared-components/map/map.svelte')}
+          {#await delay(timeToLoadTheMap) then}
+            <!-- show the loading spinner only if loading the map takes too much time -->
+            <div class="flex items-center justify-center h-full w-full">
+              <LoadingSpinner />
+            </div>
+          {/await}
+        {:then { default: Map }}
+          <Map
+            bind:this={mapElement}
+            mapMarkers={lat !== undefined && lng !== undefined && asset
+              ? [
+                  {
+                    id: asset.id,
+                    lat,
+                    lon: lng,
+                    city: asset.exifInfo?.city ?? null,
+                    state: asset.exifInfo?.state ?? null,
+                    country: asset.exifInfo?.country ?? null,
+                  },
+                ]
+              : []}
+            {zoom}
+            center={lat && lng ? { lat, lng } : undefined}
+            simplified={true}
+            clickable={true}
+            onClickPoint={(selected) => (point = selected)}
+          />
+        {/await}
+      </div>
+
+      <div class="grid sm:grid-cols-2 gap-4 text-sm text-left mt-4">
+        <CoordinatesInput
+          lat={point ? point.lat : lat}
+          lng={point ? point.lng : lng}
+          onUpdate={(lat, lng) => {
+            point = { lat, lng };
+            mapElement?.addClipMapMarker(lng, lat);
+          }}
+        />
       </div>
     </div>
-    <span>{$t('pick_a_location')}</span>
-    <div class="h-[500px] min-h-[300px] w-full">
-      {#await import('../shared-components/map/map.svelte')}
-        {#await delay(timeToLoadTheMap) then}
-          <!-- show the loading spinner only if loading the map takes too much time -->
-          <div class="flex items-center justify-center h-full w-full">
-            <LoadingSpinner />
-          </div>
-        {/await}
-      {:then { default: Map }}
-        <Map
-          mapMarkers={lat !== undefined && lng !== undefined && asset
-            ? [
-                {
-                  id: asset.id,
-                  lat,
-                  lon: lng,
-                  city: asset.exifInfo?.city ?? null,
-                  state: asset.exifInfo?.state ?? null,
-                  country: asset.exifInfo?.country ?? null,
-                },
-              ]
-            : []}
-          {zoom}
-          bind:addClipMapMarker
-          center={lat && lng ? { lat, lng } : undefined}
-          simplified={true}
-          clickable={true}
-          on:clickedPoint={({ detail: point }) => handleSelect(point)}
-        />
-      {/await}
-    </div>
-
-    <div class="grid sm:grid-cols-2 gap-4 text-sm text-left mt-4">
-      <CoordinatesInput
-        lat={point ? point.lat : lat}
-        lng={point ? point.lng : lng}
-        onUpdate={(lat, lng) => {
-          point = { lat, lng };
-          addClipMapMarker(lng, lat);
-        }}
-      />
-    </div>
-  </div>
+  {/snippet}
 </ConfirmDialog>
diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte
index d3e022a759..a6a1422eef 100644
--- a/web/src/lib/components/shared-components/combobox.svelte
+++ b/web/src/lib/components/shared-components/combobox.svelte
@@ -1,4 +1,4 @@
-<script lang="ts" context="module">
+<script lang="ts" module>
   export type ComboBoxOption = {
     id?: string;
     label: string;
@@ -21,7 +21,7 @@
   import { fly } from 'svelte/transition';
   import Icon from '$lib/components/elements/icon.svelte';
   import { mdiMagnify, mdiUnfoldMoreHorizontal, mdiClose } from '@mdi/js';
-  import { createEventDispatcher, tick } from 'svelte';
+  import { onMount, tick } from 'svelte';
   import type { FormEventHandler } from 'svelte/elements';
   import { shortcuts } from '$lib/actions/shortcut';
   import { focusOutside } from '$lib/actions/focus-outside';
@@ -30,11 +30,33 @@
   import { t } from 'svelte-i18n';
   import { get } from 'svelte/store';
 
-  export let label: string;
-  export let hideLabel = false;
-  export let options: ComboBoxOption[] = [];
-  export let selectedOption: ComboBoxOption | undefined = undefined;
-  export let placeholder = '';
+  interface Props {
+    label: string;
+    hideLabel?: boolean;
+    options?: ComboBoxOption[];
+    selectedOption?: ComboBoxOption | undefined;
+    placeholder?: string;
+    /**
+     * whether creating new items is allowed.
+     */
+    allowCreate?: boolean;
+    /**
+     * select first matching option on enter key.
+     */
+    defaultFirstOption?: boolean;
+    onSelect?: (option: ComboBoxOption | undefined) => void;
+  }
+
+  let {
+    label,
+    hideLabel = false,
+    options = [],
+    selectedOption = $bindable(),
+    placeholder = '',
+    allowCreate = false,
+    defaultFirstOption = false,
+    onSelect = () => {},
+  }: Props = $props();
 
   /**
    * Unique identifier for the combobox.
@@ -43,27 +65,54 @@
   /**
    * Indicates whether or not the dropdown autocomplete list should be visible.
    */
-  let isOpen = false;
+  let isOpen = $state(false);
   /**
    * Keeps track of whether the combobox is actively being used.
    */
-  let isActive = false;
-  let searchQuery = selectedOption?.label || '';
-  let selectedIndex: number | undefined;
-  let optionRefs: HTMLElement[] = [];
-  let input: HTMLInputElement;
+  let isActive = $state(false);
+  let searchQuery = $state(selectedOption?.label || '');
+  let selectedIndex: number | undefined = $state();
+  let optionRefs: HTMLElement[] = $state([]);
+  let input = $state<HTMLInputElement>();
+  let bounds: DOMRect | undefined = $state();
+
   const inputId = `combobox-${id}`;
   const listboxId = `listbox-${id}`;
+  /**
+   * Buffer distance between the dropdown and top/bottom of the viewport.
+   */
+  const dropdownOffset = 15;
+  /**
+   * Minimum space required for the dropdown to be displayed at the bottom of the input.
+   */
+  const bottomBreakpoint = 225;
+  const observer = new IntersectionObserver(
+    (entries) => {
+      const inputEntry = entries[0];
+      if (inputEntry.intersectionRatio < 1) {
+        isOpen = false;
+      }
+    },
+    { threshold: 0.5 },
+  );
 
-  $: filteredOptions = options.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase()));
+  onMount(() => {
+    if (!input) {
+      return;
+    }
+    observer.observe(input);
+    const scrollableAncestor = input?.closest('.overflow-y-auto, .overflow-y-scroll');
+    scrollableAncestor?.addEventListener('scroll', onPositionChange);
+    window.visualViewport?.addEventListener('resize', onPositionChange);
+    window.visualViewport?.addEventListener('scroll', onPositionChange);
 
-  $: {
-    searchQuery = selectedOption ? selectedOption.label : '';
-  }
-
-  const dispatch = createEventDispatcher<{
-    select: ComboBoxOption | undefined;
-  }>();
+    return () => {
+      observer.disconnect();
+      scrollableAncestor?.removeEventListener('scroll', onPositionChange);
+      window.visualViewport?.removeEventListener('resize', onPositionChange);
+      window.visualViewport?.removeEventListener('scroll', onPositionChange);
+    };
+  });
 
   const activate = () => {
     isActive = true;
@@ -79,6 +128,7 @@
 
   const openDropdown = () => {
     isOpen = true;
+    bounds = getInputPosition();
   };
 
   const closeDropdown = () => {
@@ -101,14 +151,14 @@
   const onInput: FormEventHandler<HTMLInputElement> = (event) => {
     openDropdown();
     searchQuery = event.currentTarget.value;
-    selectedIndex = undefined;
+    selectedIndex = defaultFirstOption ? 0 : undefined;
     optionRefs[0]?.scrollIntoView({ block: 'nearest' });
   };
 
-  let onSelect = (option: ComboBoxOption) => {
+  let handleSelect = (option: ComboBoxOption) => {
     selectedOption = option;
     searchQuery = option.label;
-    dispatch('select', option);
+    onSelect(option);
     closeDropdown();
   };
 
@@ -117,10 +167,84 @@
     selectedIndex = undefined;
     selectedOption = undefined;
     searchQuery = '';
-    dispatch('select', selectedOption);
+    onSelect(selectedOption);
   };
+
+  const calculatePosition = (boundary: DOMRect | undefined) => {
+    const visualViewport = window.visualViewport;
+
+    if (!boundary) {
+      return;
+    }
+
+    const left = boundary.left + (visualViewport?.offsetLeft || 0);
+    const offsetTop = visualViewport?.offsetTop || 0;
+
+    if (dropdownDirection === 'top') {
+      return {
+        bottom: `${window.innerHeight - boundary.top - offsetTop}px`,
+        left: `${left}px`,
+        width: `${boundary.width}px`,
+        maxHeight: maxHeight(boundary.top - dropdownOffset),
+      };
+    }
+
+    const viewportHeight = visualViewport?.height || 0;
+    const availableHeight = viewportHeight - boundary.bottom;
+    return {
+      top: `${boundary.bottom + offsetTop}px`,
+      left: `${left}px`,
+      width: `${boundary.width}px`,
+      maxHeight: maxHeight(availableHeight - dropdownOffset),
+    };
+  };
+
+  const maxHeight = (size: number) => `min(${size}px,18rem)`;
+
+  const onPositionChange = () => {
+    if (!isOpen) {
+      return;
+    }
+    bounds = getInputPosition();
+  };
+
+  const getComboboxDirection = (
+    boundary: DOMRect | undefined,
+    visualViewport: VisualViewport | null,
+  ): 'bottom' | 'top' => {
+    if (!boundary) {
+      return 'bottom';
+    }
+
+    const visualHeight = visualViewport?.height || 0;
+    const heightBelow = visualHeight - boundary.bottom;
+    const heightAbove = boundary.top;
+
+    const isViewportScaled = visualHeight && Math.floor(visualHeight) !== Math.floor(window.innerHeight);
+
+    return heightBelow <= bottomBreakpoint && heightAbove > heightBelow && !isViewportScaled ? 'top' : 'bottom';
+  };
+
+  const getInputPosition = () => input?.getBoundingClientRect();
+
+  $effect(() => {
+    searchQuery = selectedOption ? selectedOption.label : '';
+  });
+
+  let filteredOptions = $derived.by(() => {
+    const _options = options.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase()));
+
+    if (allowCreate && searchQuery !== '' && _options.filter((option) => option.label === searchQuery).length === 0) {
+      _options.unshift({ label: searchQuery, value: searchQuery });
+    }
+
+    return _options;
+  });
+  let position = $derived(calculatePosition(bounds));
+  let dropdownDirection: 'bottom' | 'top' = $derived(getComboboxDirection(bounds, visualViewport));
 </script>
 
+<svelte:window onresize={onPositionChange} />
 <label class="immich-form-label" class:sr-only={hideLabel} for={inputId}>{label}</label>
 <div
   class="relative w-full dark:text-gray-300 text-gray-700 text-base"
@@ -153,13 +277,14 @@
       autocomplete="off"
       bind:this={input}
       class:!pl-8={isActive}
-      class:!rounded-b-none={isOpen}
+      class:!rounded-b-none={isOpen && dropdownDirection === 'bottom'}
+      class:!rounded-t-none={isOpen && dropdownDirection === 'top'}
       class:cursor-pointer={!isActive}
       class="immich-form-input text-sm text-left w-full !pr-12 transition-all"
       id={inputId}
-      on:click={activate}
-      on:focus={activate}
-      on:input={onInput}
+      onclick={activate}
+      onfocus={activate}
+      oninput={onInput}
       role="combobox"
       type="text"
       value={searchQuery}
@@ -188,7 +313,7 @@
           shortcut: { key: 'Enter' },
           onShortcut: () => {
             if (selectedIndex !== undefined && filteredOptions.length > 0) {
-              onSelect(filteredOptions[selectedIndex]);
+              handleSelect(filteredOptions[selectedIndex]);
             }
             closeDropdown();
           },
@@ -209,7 +334,7 @@
       class:pointer-events-none={!selectedOption}
     >
       {#if selectedOption}
-        <CircleIconButton on:click={onClear} title={$t('clear_value')} icon={mdiClose} size="16" padding="2" />
+        <CircleIconButton onclick={onClear} title={$t('clear_value')} icon={mdiClose} size="16" padding="2" />
       {:else if !isOpen}
         <Icon path={mdiUnfoldMoreHorizontal} ariaHidden={true} />
       {/if}
@@ -220,32 +345,40 @@
     role="listbox"
     id={listboxId}
     transition:fly={{ duration: 250 }}
-    class="absolute text-left text-sm w-full max-h-64 overflow-y-auto bg-white dark:bg-gray-800 border-t-0 border-gray-300 dark:border-gray-900 rounded-b-xl z-10"
+    class="fixed text-left text-sm w-full overflow-y-auto bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-900 z-[10000]"
+    class:rounded-b-xl={dropdownDirection === 'bottom'}
+    class:rounded-t-xl={dropdownDirection === 'top'}
+    class:shadow={dropdownDirection === 'bottom'}
     class:border={isOpen}
+    style:top={position?.top}
+    style:bottom={position?.bottom}
+    style:left={position?.left}
+    style:width={position?.width}
+    style:max-height={position?.maxHeight}
     tabindex="-1"
   >
     {#if isOpen}
       {#if filteredOptions.length === 0}
-        <!-- svelte-ignore a11y-click-events-have-key-events -->
+        <!-- svelte-ignore a11y_click_events_have_key_events -->
         <li
           role="option"
           aria-selected={selectedIndex === 0}
           aria-disabled={true}
-          class="text-left w-full px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default aria-selected:bg-gray-100 aria-selected:dark:bg-gray-700"
+          class="text-left w-full px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default aria-selected:bg-gray-200 aria-selected:dark:bg-gray-700"
           id={`${listboxId}-${0}`}
-          on:click={() => closeDropdown()}
+          onclick={() => closeDropdown()}
         >
-          {$t('no_results')}
+          {allowCreate ? searchQuery : $t('no_results')}
         </li>
       {/if}
       {#each filteredOptions as option, index (option.id || option.label)}
-        <!-- svelte-ignore a11y-click-events-have-key-events -->
+        <!-- svelte-ignore a11y_click_events_have_key_events -->
         <li
           aria-selected={index === selectedIndex}
           bind:this={optionRefs[index]}
-          class="text-left w-full px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all cursor-pointer aria-selected:bg-gray-100 aria-selected:dark:bg-gray-700"
+          class="text-left w-full px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 transition-all cursor-pointer aria-selected:bg-gray-200 aria-selected:dark:bg-gray-700 break-words"
           id={`${listboxId}-${index}`}
-          on:click={() => onSelect(option)}
+          onclick={() => handleSelect(option)}
           role="option"
         >
           {option.label}
diff --git a/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte b/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte
index f1ee93cc50..46dc17b9ad 100644
--- a/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte
+++ b/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte
@@ -14,41 +14,52 @@
   import { optionClickCallbackStore, selectedIdStore } from '$lib/stores/context-menu.store';
   import { clickOutside } from '$lib/actions/click-outside';
   import { shortcuts } from '$lib/actions/shortcut';
+  import type { Snippet } from 'svelte';
 
-  export let icon: string;
-  export let title: string;
-  /**
-   * The alignment of the context menu relative to the button.
-   */
-  export let align: Align = 'top-left';
-  /**
-   * The direction in which the context menu should open.
-   */
-  export let direction: 'left' | 'right' = 'right';
-  export let color: Color = 'transparent';
-  export let size: string | undefined = undefined;
-  export let padding: Padding | undefined = undefined;
-  /**
-   * Additional classes to apply to the button.
-   */
-  export let buttonClass: string | undefined = undefined;
-  export let hideContent = false;
+  interface Props {
+    icon: string;
+    title: string;
+    /**
+     * The alignment of the context menu relative to the button.
+     */
+    align?: Align;
+    /**
+     * The direction in which the context menu should open.
+     */
+    direction?: 'left' | 'right';
+    color?: Color;
+    size?: string | undefined;
+    padding?: Padding | undefined;
+    /**
+     * Additional classes to apply to the button.
+     */
+    buttonClass?: string | undefined;
+    hideContent?: boolean;
+    children?: Snippet;
+  }
 
-  let isOpen = false;
-  let contextMenuPosition = { x: 0, y: 0 };
-  let menuContainer: HTMLUListElement;
-  let buttonContainer: HTMLDivElement;
+  let {
+    icon,
+    title,
+    align = 'top-left',
+    direction = 'right',
+    color = 'transparent',
+    size = undefined,
+    padding = undefined,
+    buttonClass = undefined,
+    hideContent = false,
+    children,
+  }: Props = $props();
+
+  let isOpen = $state(false);
+  let contextMenuPosition = $state({ x: 0, y: 0 });
+  let menuContainer: HTMLUListElement | undefined = $state();
+  let buttonContainer: HTMLDivElement | undefined = $state();
 
   const id = generateId();
   const buttonId = `context-menu-button-${id}`;
   const menuId = `context-menu-${id}`;
 
-  $: {
-    if (isOpen) {
-      $optionClickCallbackStore = handleOptionClick;
-    }
-  }
-
   const openDropdown = (event: KeyboardEvent | MouseEvent) => {
     contextMenuPosition = getContextMenuPositionFromEvent(event, align);
     isOpen = true;
@@ -72,9 +83,10 @@
   };
 
   const onResize = () => {
-    if (!isOpen) {
+    if (!isOpen || !buttonContainer) {
       return;
     }
+
     contextMenuPosition = getContextMenuPositionFromBoundingRect(buttonContainer.getBoundingClientRect(), align);
   };
 
@@ -92,12 +104,19 @@
   };
 
   const focusButton = () => {
-    const button: HTMLButtonElement | null = buttonContainer.querySelector(`#${buttonId}`);
+    const button = buttonContainer?.querySelector(`#${buttonId}`) as HTMLButtonElement | null;
     button?.focus();
   };
+
+  $effect(() => {
+    if (isOpen) {
+      $optionClickCallbackStore = handleOptionClick;
+    }
+  });
 </script>
 
-<svelte:window on:resize={onResize} />
+<svelte:window onresize={onResize} />
+
 <div
   use:contextMenuNavigation={{
     closeDropdown,
@@ -109,7 +128,7 @@
     selectionChanged: (id) => ($selectedIdStore = id),
   }}
   use:clickOutside={{ onOutclick: closeDropdown }}
-  on:resize={onResize}
+  onresize={onResize}
 >
   <div bind:this={buttonContainer}>
     <CircleIconButton
@@ -123,7 +142,7 @@
       aria-haspopup={true}
       class={buttonClass}
       id={buttonId}
-      on:click={handleClick}
+      onclick={handleClick}
     />
   </div>
   {#if isOpen || !hideContent}
@@ -150,7 +169,7 @@
         id={menuId}
         isVisible={isOpen}
       >
-        <slot />
+        {@render children?.()}
       </ContextMenu>
     </div>
   {/if}
diff --git a/web/src/lib/components/shared-components/context-menu/context-menu.svelte b/web/src/lib/components/shared-components/context-menu/context-menu.svelte
index 8f5ebfa2cf..aff583d1fc 100644
--- a/web/src/lib/components/shared-components/context-menu/context-menu.svelte
+++ b/web/src/lib/components/shared-components/context-menu/context-menu.svelte
@@ -2,27 +2,44 @@
   import { quintOut } from 'svelte/easing';
   import { slide } from 'svelte/transition';
   import { clickOutside } from '$lib/actions/click-outside';
+  import type { Snippet } from 'svelte';
 
-  export let isVisible: boolean = false;
-  export let direction: 'left' | 'right' = 'right';
-  export let x = 0;
-  export let y = 0;
-  export let id: string | undefined = undefined;
-  export let ariaLabel: string | undefined = undefined;
-  export let ariaLabelledBy: string | undefined = undefined;
-  export let ariaActiveDescendant: string | undefined = undefined;
+  interface Props {
+    isVisible?: boolean;
+    direction?: 'left' | 'right';
+    x?: number;
+    y?: number;
+    id?: string | undefined;
+    ariaLabel?: string | undefined;
+    ariaLabelledBy?: string | undefined;
+    ariaActiveDescendant?: string | undefined;
+    menuElement?: HTMLUListElement | undefined;
+    onClose?: (() => void) | undefined;
+    children?: Snippet;
+  }
 
-  export let menuElement: HTMLUListElement | undefined = undefined;
-  export let onClose: (() => void) | undefined = undefined;
+  let {
+    isVisible = false,
+    direction = 'right',
+    x = 0,
+    y = 0,
+    id = undefined,
+    ariaLabel = undefined,
+    ariaLabelledBy = undefined,
+    ariaActiveDescendant = undefined,
+    menuElement = $bindable(),
+    onClose = undefined,
+    children,
+  }: Props = $props();
 
-  let left: number;
-  let top: number;
+  let left: number = $state(0);
+  let top: number = $state(0);
 
   // We need to bind clientHeight since the bounding box may return a height
   // of zero when starting the 'slide' animation.
-  let height: number;
+  let height: number = $state(0);
 
-  $: {
+  $effect(() => {
     if (menuElement) {
       const rect = menuElement.getBoundingClientRect();
       const directionWidth = direction === 'left' ? rect.width : 0;
@@ -31,7 +48,7 @@
       left = Math.min(window.innerWidth - rect.width, x - directionWidth);
       top = Math.min(window.innerHeight - menuHeight, y);
     }
-  }
+  });
 </script>
 
 <div
@@ -54,6 +71,6 @@
     role="menu"
     tabindex="-1"
   >
-    <slot />
+    {@render children?.()}
   </ul>
 </div>
diff --git a/web/src/lib/components/shared-components/context-menu/menu-option.svelte b/web/src/lib/components/shared-components/context-menu/menu-option.svelte
index e7ff4c626e..5d3c29dc3c 100644
--- a/web/src/lib/components/shared-components/context-menu/menu-option.svelte
+++ b/web/src/lib/components/shared-components/context-menu/menu-option.svelte
@@ -3,16 +3,27 @@
   import { generateId } from '$lib/utils/generate-id';
   import { optionClickCallbackStore, selectedIdStore } from '$lib/stores/context-menu.store';
 
-  export let text: string;
-  export let subtitle = '';
-  export let icon = '';
-  export let activeColor = 'bg-slate-300';
-  export let textColor = 'text-immich-fg dark:text-immich-dark-bg';
-  export let onClick: () => void;
+  interface Props {
+    text: string;
+    subtitle?: string;
+    icon?: string;
+    activeColor?: string;
+    textColor?: string;
+    onClick: () => void;
+  }
+
+  let {
+    text,
+    subtitle = '',
+    icon = '',
+    activeColor = 'bg-slate-300',
+    textColor = 'text-immich-fg dark:text-immich-dark-bg',
+    onClick,
+  }: Props = $props();
 
   let id: string = generateId();
 
-  $: isActive = $selectedIdStore === id;
+  let isActive = $derived($selectedIdStore === id);
 
   const handleClick = () => {
     $optionClickCallbackStore?.();
@@ -20,13 +31,13 @@
   };
 </script>
 
-<!-- svelte-ignore a11y-click-events-have-key-events -->
-<!-- svelte-ignore a11y-mouse-events-have-key-events -->
+<!-- svelte-ignore a11y_click_events_have_key_events -->
+<!-- svelte-ignore a11y_mouse_events_have_key_events -->
 <li
   {id}
-  on:click={handleClick}
-  on:mouseover={() => ($selectedIdStore = id)}
-  on:mouseleave={() => ($selectedIdStore = undefined)}
+  onclick={handleClick}
+  onmouseover={() => ($selectedIdStore = id)}
+  onmouseleave={() => ($selectedIdStore = undefined)}
   class="w-full p-4 text-left text-sm font-medium {textColor} focus:outline-none focus:ring-2 focus:ring-inset cursor-pointer border-gray-200 flex gap-2 items-center {isActive
     ? activeColor
     : 'bg-slate-100'}"
diff --git a/web/src/lib/components/shared-components/context-menu/right-click-context-menu.svelte b/web/src/lib/components/shared-components/context-menu/right-click-context-menu.svelte
index f0b0408ff9..f7745893db 100644
--- a/web/src/lib/components/shared-components/context-menu/right-click-context-menu.svelte
+++ b/web/src/lib/components/shared-components/context-menu/right-click-context-menu.svelte
@@ -1,37 +1,35 @@
 <script lang="ts">
-  import { tick } from 'svelte';
+  import { tick, type Snippet } from 'svelte';
   import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
   import { shortcuts } from '$lib/actions/shortcut';
   import { generateId } from '$lib/utils/generate-id';
   import { contextMenuNavigation } from '$lib/actions/context-menu-navigation';
   import { optionClickCallbackStore, selectedIdStore } from '$lib/stores/context-menu.store';
 
-  export let title: string;
-  export let direction: 'left' | 'right' = 'right';
-  export let x = 0;
-  export let y = 0;
-  export let isOpen = false;
-  export let onClose: (() => unknown) | undefined;
+  interface Props {
+    title: string;
+    direction?: 'left' | 'right';
+    x?: number;
+    y?: number;
+    isOpen?: boolean;
+    onClose: (() => unknown) | undefined;
+    children?: Snippet;
+  }
 
-  let uniqueKey = {};
-  let menuContainer: HTMLUListElement;
-  let triggerElement: HTMLElement | undefined = undefined;
+  let { title, direction = 'right', x = 0, y = 0, isOpen = false, onClose, children }: Props = $props();
+
+  let uniqueKey = $state({});
+  let menuContainer: HTMLUListElement | undefined = $state();
+  let triggerElement: HTMLElement | undefined = $state(undefined);
 
   const id = generateId();
   const menuId = `context-menu-${id}`;
 
-  $: {
-    if (isOpen && menuContainer) {
-      triggerElement = document.activeElement as HTMLElement;
-      menuContainer.focus();
-      $optionClickCallbackStore = closeContextMenu;
-    }
-  }
-
   const reopenContextMenu = async (event: MouseEvent) => {
     const contextMenuEvent = new MouseEvent('contextmenu', {
       bubbles: true,
       cancelable: true,
+      // eslint-disable-next-line unicorn/prefer-global-this
       view: window,
       clientX: event.x,
       clientY: event.y,
@@ -39,7 +37,7 @@
 
     const elements = document.elementsFromPoint(event.x, event.y);
 
-    if (elements.includes(menuContainer)) {
+    if (menuContainer && elements.includes(menuContainer)) {
       // User right-clicked on the context menu itself, we keep the context
       // menu as is
       return;
@@ -58,6 +56,18 @@
     triggerElement?.focus();
     onClose?.();
   };
+  $effect(() => {
+    if (isOpen && menuContainer) {
+      triggerElement = document.activeElement as HTMLElement;
+      menuContainer.focus();
+      $optionClickCallbackStore = closeContextMenu;
+    }
+  });
+
+  const oncontextmenu = async (event: MouseEvent) => {
+    event.preventDefault();
+    await reopenContextMenu(event);
+  };
 </script>
 
 {#key uniqueKey}
@@ -81,11 +91,7 @@
         },
       ]}
     >
-      <section
-        class="fixed left-0 top-0 z-10 flex h-screen w-screen"
-        on:contextmenu|preventDefault={reopenContextMenu}
-        role="presentation"
-      >
+      <section class="fixed left-0 top-0 z-10 flex h-screen w-screen" {oncontextmenu} role="presentation">
         <ContextMenu
           {direction}
           {x}
@@ -97,7 +103,7 @@
           isVisible
           onClose={closeContextMenu}
         >
-          <slot />
+          {@render children?.()}
         </ContextMenu>
       </section>
     </div>
diff --git a/web/src/lib/components/shared-components/control-app-bar.svelte b/web/src/lib/components/shared-components/control-app-bar.svelte
index cf128104d1..c78edaa601 100644
--- a/web/src/lib/components/shared-components/control-app-bar.svelte
+++ b/web/src/lib/components/shared-components/control-app-bar.svelte
@@ -1,26 +1,39 @@
 <script lang="ts">
   import { browser } from '$app/environment';
 
-  import { createEventDispatcher, onDestroy, onMount } from 'svelte';
+  import { onDestroy, onMount, type Snippet } from 'svelte';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
   import { fly } from 'svelte/transition';
   import { mdiClose } from '@mdi/js';
   import { isSelectingAllAssets } from '$lib/stores/assets.store';
   import { t } from 'svelte-i18n';
 
-  export let showBackButton = true;
-  export let backIcon = mdiClose;
-  export let tailwindClasses = '';
-  export let forceDark = false;
+  interface Props {
+    showBackButton?: boolean;
+    backIcon?: string;
+    tailwindClasses?: string;
+    forceDark?: boolean;
+    onClose?: () => void;
+    leading?: Snippet;
+    children?: Snippet;
+    trailing?: Snippet;
+  }
 
-  let appBarBorder = 'bg-immich-bg border border-transparent';
+  let {
+    showBackButton = true,
+    backIcon = mdiClose,
+    tailwindClasses = '',
+    forceDark = false,
+    onClose = () => {},
+    leading,
+    children,
+    trailing,
+  }: Props = $props();
 
-  const dispatch = createEventDispatcher<{
-    close: void;
-  }>();
+  let appBarBorder = $state('bg-immich-bg border border-transparent');
 
   const onScroll = () => {
-    if (window.pageYOffset > 80) {
+    if (window.scrollY > 80) {
       appBarBorder = 'border border-gray-200 bg-gray-50 dark:border-gray-600';
 
       if (forceDark) {
@@ -33,7 +46,7 @@
 
   const handleClose = () => {
     $isSelectingAllAssets = false;
-    dispatch('close');
+    onClose();
   };
 
   onMount(() => {
@@ -48,7 +61,7 @@
     }
   });
 
-  $: buttonClass = forceDark ? 'hover:text-immich-dark-gray' : undefined;
+  let buttonClass = $derived(forceDark ? 'hover:text-immich-dark-gray' : undefined);
 </script>
 
 <div in:fly={{ y: 10, duration: 200 }} class="absolute top-0 w-full z-[100] bg-transparent">
@@ -60,17 +73,17 @@
   >
     <div class="flex place-items-center sm:gap-6 justify-self-start dark:text-immich-dark-fg">
       {#if showBackButton}
-        <CircleIconButton title={$t('close')} on:click={handleClose} icon={backIcon} size={'24'} class={buttonClass} />
+        <CircleIconButton title={$t('close')} onclick={handleClose} icon={backIcon} size={'24'} class={buttonClass} />
       {/if}
-      <slot name="leading" />
+      {@render leading?.()}
     </div>
 
     <div class="w-full">
-      <slot />
+      {@render children?.()}
     </div>
 
     <div class="mr-4 flex place-items-center gap-1 justify-self-end">
-      <slot name="trailing" />
+      {@render trailing?.()}
     </div>
   </div>
 </div>
diff --git a/web/src/lib/components/shared-components/coordinates-input.svelte b/web/src/lib/components/shared-components/coordinates-input.svelte
index f5ad120a7b..b3170e3ecf 100644
--- a/web/src/lib/components/shared-components/coordinates-input.svelte
+++ b/web/src/lib/components/shared-components/coordinates-input.svelte
@@ -3,9 +3,13 @@
   import { generateId } from '$lib/utils/generate-id';
   import { t } from 'svelte-i18n';
 
-  export let lat: number | null | undefined = undefined;
-  export let lng: number | null | undefined = undefined;
-  export let onUpdate: (lat: number, lng: number) => void;
+  interface Props {
+    lat?: number;
+    lng?: number;
+    onUpdate: (lat: number, lng: number) => void;
+  }
+
+  let { lat = $bindable(), lng = $bindable(), onUpdate }: Props = $props();
 
   const id = generateId();
 
@@ -31,6 +35,7 @@
 
     event.preventDefault();
     [lat, lng] = [latitude, longitude];
+    onInput();
   };
 </script>
 
diff --git a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte
index c50a07ad37..443e8f06b1 100644
--- a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte
+++ b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte
@@ -7,34 +7,41 @@
   import { handleError } from '$lib/utils/handle-error';
   import { SharedLinkType, createSharedLink, updateSharedLink, type SharedLinkResponseDto } from '@immich/sdk';
   import { mdiContentCopy, mdiLink } from '@mdi/js';
-  import { createEventDispatcher } from 'svelte';
   import { NotificationType, notificationController } from '../notification/notification';
-  import SettingInputField, { SettingInputFieldType } from '../settings/setting-input-field.svelte';
+  import SettingInputField from '../settings/setting-input-field.svelte';
   import SettingSwitch from '../settings/setting-switch.svelte';
   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
   import { t } from 'svelte-i18n';
   import { locale } from '$lib/stores/preferences.store';
   import { DateTime, Duration } from 'luxon';
   import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
+  import { SettingInputFieldType } from '$lib/constants';
 
-  export let onClose: () => void;
-  export let albumId: string | undefined = undefined;
-  export let assetIds: string[] = [];
-  export let editingLink: SharedLinkResponseDto | undefined = undefined;
+  interface Props {
+    onClose: () => void;
+    albumId?: string | undefined;
+    assetIds?: string[];
+    editingLink?: SharedLinkResponseDto | undefined;
+    onCreated?: () => void;
+  }
 
-  let sharedLink: string | null = null;
-  let description = '';
-  let allowDownload = true;
-  let allowUpload = false;
-  let showMetadata = true;
-  let expirationOption: number = 0;
-  let password = '';
-  let shouldChangeExpirationTime = false;
-  let enablePassword = false;
+  let {
+    onClose,
+    albumId = $bindable(undefined),
+    assetIds = $bindable([]),
+    editingLink = undefined,
+    onCreated = () => {},
+  }: Props = $props();
 
-  const dispatch = createEventDispatcher<{
-    created: void;
-  }>();
+  let sharedLink: string | null = $state(null);
+  let description = $state('');
+  let allowDownload = $state(true);
+  let allowUpload = $state(false);
+  let showMetadata = $state(true);
+  let expirationOption: number = $state(0);
+  let password = $state('');
+  let shouldChangeExpirationTime = $state(false);
+  let enablePassword = $state(false);
 
   const expirationOptions: [number, Intl.RelativeTimeFormatUnit][] = [
     [30, 'minutes'],
@@ -47,21 +54,23 @@
     [1, 'year'],
   ];
 
-  $: relativeTime = new Intl.RelativeTimeFormat($locale);
-  $: expiredDateOptions = [
+  let relativeTime = $derived(new Intl.RelativeTimeFormat($locale));
+  let expiredDateOptions = $derived([
     { text: $t('never'), value: 0 },
     ...expirationOptions.map(([value, unit]) => ({
       text: relativeTime.format(value, unit),
       value: Duration.fromObject({ [unit]: value }).toMillis(),
     })),
-  ];
+  ]);
 
-  $: shareType = albumId ? SharedLinkType.Album : SharedLinkType.Individual;
-  $: {
+  let shareType = $derived(albumId ? SharedLinkType.Album : SharedLinkType.Individual);
+
+  $effect(() => {
     if (!showMetadata) {
       allowDownload = false;
     }
-  }
+  });
+
   if (editingLink) {
     if (editingLink.description) {
       description = editingLink.description;
@@ -97,7 +106,7 @@
         },
       });
       sharedLink = makeSharedLinkUrl($serverConfig.externalDomain, data.key);
-      dispatch('created');
+      onCreated();
     } catch (error) {
       handleError(error, $t('errors.failed_to_create_shared_link'));
     }
@@ -226,22 +235,22 @@
     </div>
   </section>
 
-  <svelte:fragment slot="sticky-bottom">
+  {#snippet stickyBottom()}
     {#if !sharedLink}
       {#if editingLink}
-        <Button size="sm" fullwidth on:click={handleEditLink}>{$t('confirm')}</Button>
+        <Button size="sm" fullwidth onclick={handleEditLink}>{$t('confirm')}</Button>
       {:else}
-        <Button size="sm" fullwidth on:click={handleCreateSharedLink}>{$t('create_link')}</Button>
+        <Button size="sm" fullwidth onclick={handleCreateSharedLink}>{$t('create_link')}</Button>
       {/if}
     {:else}
       <div class="flex w-full gap-2">
         <input class="immich-form-input w-full" bind:value={sharedLink} disabled />
-        <LinkButton on:click={() => (sharedLink ? copyToClipboard(sharedLink) : '')}>
+        <LinkButton onclick={() => (sharedLink ? copyToClipboard(sharedLink) : '')}>
           <div class="flex place-items-center gap-2 text-sm">
             <Icon path={mdiContentCopy} ariaLabel={$t('copy_link_to_clipboard')} size="18" />
           </div>
         </LinkButton>
       </div>
     {/if}
-  </svelte:fragment>
+  {/snippet}
 </FullScreenModal>
diff --git a/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte b/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte
index 50d5fe56ce..3efc56dc41 100644
--- a/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte
+++ b/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte
@@ -3,18 +3,37 @@
   import Button from '../../elements/buttons/button.svelte';
   import type { Color } from '$lib/components/elements/buttons/button.svelte';
   import { t } from 'svelte-i18n';
+  import type { Snippet } from 'svelte';
 
-  export let title = $t('confirm');
-  export let prompt = $t('are_you_sure_to_do_this');
-  export let confirmText = $t('confirm');
-  export let confirmColor: Color = 'red';
-  export let cancelText = $t('cancel');
-  export let cancelColor: Color = 'secondary';
-  export let hideCancelButton = false;
-  export let disabled = false;
-  export let width: 'wide' | 'narrow' = 'narrow';
-  export let onCancel: () => void;
-  export let onConfirm: () => void;
+  interface Props {
+    title?: string;
+    prompt?: string;
+    confirmText?: string;
+    confirmColor?: Color;
+    cancelText?: string;
+    cancelColor?: Color;
+    hideCancelButton?: boolean;
+    disabled?: boolean;
+    width?: 'wide' | 'narrow';
+    onCancel: () => void;
+    onConfirm: () => void;
+    promptSnippet?: Snippet;
+  }
+
+  let {
+    title = $t('confirm'),
+    prompt = $t('are_you_sure_to_do_this'),
+    confirmText = $t('confirm'),
+    confirmColor = 'red',
+    cancelText = $t('cancel'),
+    cancelColor = 'secondary',
+    hideCancelButton = false,
+    disabled = false,
+    width = 'narrow',
+    onCancel,
+    onConfirm,
+    promptSnippet,
+  }: Props = $props();
 
   const handleConfirm = () => {
     onConfirm();
@@ -23,19 +42,19 @@
 
 <FullScreenModal {title} onClose={onCancel} {width}>
   <div class="text-md py-5 text-center">
-    <slot name="prompt">
+    {#if promptSnippet}{@render promptSnippet()}{:else}
       <p>{prompt}</p>
-    </slot>
+    {/if}
   </div>
 
-  <svelte:fragment slot="sticky-bottom">
+  {#snippet stickyBottom()}
     {#if !hideCancelButton}
-      <Button color={cancelColor} fullwidth on:click={onCancel}>
+      <Button color={cancelColor} fullwidth onclick={onCancel}>
         {cancelText}
       </Button>
     {/if}
-    <Button color={confirmColor} fullwidth on:click={handleConfirm} {disabled}>
+    <Button color={confirmColor} fullwidth onclick={handleConfirm} {disabled}>
       {confirmText}
     </Button>
-  </svelte:fragment>
+  {/snippet}
 </FullScreenModal>
diff --git a/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte b/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte
index 6f92d81886..911abdbcec 100644
--- a/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte
+++ b/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte
@@ -1,5 +1,5 @@
 <script lang="ts">
-  import { page } from '$app/stores';
+  import { page } from '$app/state';
   import { shouldIgnoreEvent } from '$lib/actions/shortcut';
   import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
   import { fileUploadHandler } from '$lib/utils/file-uploader';
@@ -8,10 +8,10 @@
   import { fade } from 'svelte/transition';
   import ImmichLogo from './immich-logo.svelte';
 
-  $: albumId = isAlbumsRoute($page.route?.id) ? $page.params.albumId : undefined;
-  $: isShare = isSharedLinkRoute($page.route?.id);
+  let albumId = $derived(isAlbumsRoute(page.route?.id) ? page.params.albumId : undefined);
+  let isShare = $derived(isSharedLinkRoute(page.route?.id));
 
-  let dragStartTarget: EventTarget | null = null;
+  let dragStartTarget: EventTarget | null = $state(null);
 
   const onDragEnter = (e: DragEvent) => {
     if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
@@ -96,13 +96,25 @@
     });
   };
 
+  const readEntriesAsync = (reader: FileSystemDirectoryReader) => {
+    return new Promise<FileSystemEntry[]>((resolve, reject) => {
+      reader.readEntries(resolve, reject);
+    });
+  };
+
   const getContentsFromFileSystemDirectoryEntry = async (
     fileSystemDirectoryEntry: FileSystemDirectoryEntry,
   ): Promise<FileSystemEntry[]> => {
-    return new Promise((resolve, reject) => {
-      const reader = fileSystemDirectoryEntry.createReader();
-      reader.readEntries(resolve, reject);
-    });
+    const reader = fileSystemDirectoryEntry.createReader();
+    const files: FileSystemEntry[] = [];
+    let entries: FileSystemEntry[];
+
+    do {
+      entries = await readEntriesAsync(reader);
+      files.push(...entries);
+    } while (entries.length > 0);
+
+    return files;
   };
 
   const handleFiles = async (files?: FileList | File[]) => {
@@ -117,26 +129,41 @@
       await fileUploadHandler(filesArray, albumId);
     }
   };
+
+  const ondragenter = (e: DragEvent) => {
+    e.preventDefault();
+    e.stopPropagation();
+    onDragEnter(e);
+  };
+
+  const ondragleave = (e: DragEvent) => {
+    e.preventDefault();
+    e.stopPropagation();
+    onDragLeave(e);
+  };
+
+  const ondrop = async (e: DragEvent) => {
+    e.preventDefault();
+    e.stopPropagation();
+    await onDrop(e);
+  };
+
+  const onDragOver = (e: DragEvent) => {
+    e.preventDefault();
+    e.stopPropagation();
+  };
 </script>
 
-<svelte:window on:paste={onPaste} />
+<svelte:window onpaste={onPaste} />
 
-<svelte:body
-  on:dragenter|stopPropagation|preventDefault={onDragEnter}
-  on:dragleave|stopPropagation|preventDefault={onDragLeave}
-  on:drop|stopPropagation|preventDefault={onDrop}
-/>
+<svelte:body {ondragenter} {ondragleave} {ondrop} />
 
 {#if dragStartTarget}
-  <!-- svelte-ignore a11y-no-static-element-interactions -->
+  <!-- svelte-ignore a11y_no_static_element_interactions -->
   <div
     class="fixed inset-0 z-[1000] flex h-full w-full flex-col items-center justify-center bg-gray-100/90 text-immich-dark-gray dark:bg-immich-dark-bg/90 dark:text-immich-gray"
     transition:fade={{ duration: 250 }}
-    on:dragover={(e) => {
-      // Prevent browser from opening the dropped file.
-      e.stopPropagation();
-      e.preventDefault();
-    }}
+    ondragover={onDragOver}
   >
     <ImmichLogo noText class="m-16 w-48 animate-bounce" />
     <div class="text-2xl">{$t('drop_files_to_upload')}</div>
diff --git a/web/src/lib/components/shared-components/empty-placeholder.svelte b/web/src/lib/components/shared-components/empty-placeholder.svelte
index 781f7821f1..922d7ad92f 100644
--- a/web/src/lib/components/shared-components/empty-placeholder.svelte
+++ b/web/src/lib/components/shared-components/empty-placeholder.svelte
@@ -1,22 +1,26 @@
 <script lang="ts">
   import empty1Url from '$lib/assets/empty-1.svg';
 
-  export let onClick: undefined | (() => unknown) = undefined;
-  export let text: string;
-  export let fullWidth = false;
-  export let src = empty1Url;
+  interface Props {
+    onClick?: undefined | (() => unknown);
+    text: string;
+    fullWidth?: boolean;
+    src?: string;
+  }
 
-  $: width = fullWidth ? 'w-full' : 'w-1/2';
+  let { onClick = undefined, text, fullWidth = false, src = empty1Url }: Props = $props();
+
+  let width = $derived(fullWidth ? 'w-full' : 'w-1/2');
 
   const hoverClasses = onClick
     ? `border dark:border-immich-dark-gray hover:bg-immich-primary/5 dark:hover:bg-immich-dark-primary/25`
     : '';
 </script>
 
-<!-- svelte-ignore a11y-no-static-element-interactions -->
+<!-- svelte-ignore a11y_no_static_element_interactions -->
 <svelte:element
   this={onClick ? 'button' : 'div'}
-  on:click={onClick}
+  onclick={onClick}
   class="{width} m-auto mt-10 flex flex-col place-content-center place-items-center rounded-3xl bg-gray-50 p-5 dark:bg-immich-dark-gray {hoverClasses}"
 >
   <img {src} alt="" width="500" draggable="false" />
diff --git a/web/src/lib/components/shared-components/full-screen-modal.svelte b/web/src/lib/components/shared-components/full-screen-modal.svelte
index b5b21f0c23..1263aed03b 100644
--- a/web/src/lib/components/shared-components/full-screen-modal.svelte
+++ b/web/src/lib/components/shared-components/full-screen-modal.svelte
@@ -4,36 +4,52 @@
   import { fade } from 'svelte/transition';
   import ModalHeader from '$lib/components/shared-components/modal-header.svelte';
   import { generateId } from '$lib/utils/generate-id';
+  import type { Snippet } from 'svelte';
 
-  export let onClose: () => void;
-  export let title: string;
-  /**
-   * If true, the logo will be displayed next to the modal title.
-   */
-  export let showLogo = false;
-  /**
-   * Optional icon to display next to the modal title, if `showLogo` is false.
-   */
-  export let icon: string | undefined = undefined;
-  /**
-   * Sets the width of the modal.
-   *
-   * - `wide`: 48rem
-   * - `narrow`: 28rem
-   * - `auto`: fits the width of the modal content, up to a maximum of 32rem
-   */
-  export let width: 'extra-wide' | 'wide' | 'narrow' | 'auto' = 'narrow';
+  interface Props {
+    onClose: () => void;
+    title: string;
+    /**
+     * If true, the logo will be displayed next to the modal title.
+     */
+    showLogo?: boolean;
+    /**
+     * Optional icon to display next to the modal title, if `showLogo` is false.
+     */
+    icon?: string | undefined;
+    /**
+     * Sets the width of the modal.
+     *
+     * - `wide`: 48rem
+     * - `narrow`: 28rem
+     * - `auto`: fits the width of the modal content, up to a maximum of 32rem
+     */
+    width?: 'extra-wide' | 'wide' | 'narrow' | 'auto';
+    stickyBottom?: Snippet;
+    children?: Snippet;
+  }
+
+  let {
+    onClose,
+    title,
+    showLogo = false,
+    icon = undefined,
+    width = 'narrow',
+    stickyBottom,
+    children,
+  }: Props = $props();
 
   /**
    * Unique identifier for the modal.
    */
   let id: string = generateId();
 
-  $: titleId = `${id}-title`;
-  $: isStickyBottom = !!$$slots['sticky-bottom'];
+  let titleId = $derived(`${id}-title`);
+  let isStickyBottom = $derived(!!stickyBottom);
 
-  let modalWidth: string;
-  $: {
+  let modalWidth = $state<string>();
+
+  $effect(() => {
     switch (width) {
       case 'extra-wide': {
         modalWidth = 'w-[56rem]';
@@ -54,7 +70,7 @@
         modalWidth = 'sm:max-w-4xl';
       }
     }
-  }
+  });
 </script>
 
 <section
@@ -62,34 +78,30 @@
   in:fade={{ duration: 100 }}
   out:fade={{ duration: 100 }}
   class="fixed left-0 top-0 z-[9999] flex h-dvh w-screen place-content-center place-items-center bg-black/40"
-  on:keydown={(event) => {
+  onkeydown={(event) => {
     event.stopPropagation();
   }}
   use:focusTrap
 >
   <div
-    class="z-[9999] max-w-[95vw] {modalWidth} overflow-hidden rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg pt-3 pb-4"
+    class="flex flex-col max-h-[min(95dvh,60rem)] z-[9999] max-w-[95vw] {modalWidth} overflow-hidden rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg pt-3 pb-4"
     use:clickOutside={{ onOutclick: onClose, onEscape: onClose }}
     tabindex="-1"
     aria-modal="true"
     aria-labelledby={titleId}
   >
-    <div
-      class="immich-scrollbar overflow-y-auto max-h-[min(92dvh,64rem)] py-1"
-      class:scroll-pb-40={isStickyBottom}
-      class:sm:scroll-p-24={isStickyBottom}
-    >
+    <div class="immich-scrollbar overflow-y-auto pt-1" class:pb-4={isStickyBottom}>
       <ModalHeader id={titleId} {title} {showLogo} {icon} {onClose} />
-      <div class="px-5 pt-0">
-        <slot />
+      <div class="px-5 pt-0 mb-5">
+        {@render children?.()}
       </div>
-      {#if isStickyBottom}
-        <div
-          class="flex flex-col sm:flex-row justify-end w-full gap-2 sm:gap-4 sticky -bottom-[4px] py-2 px-5 bg-immich-bg dark:bg-immich-dark-gray border-t border-gray-200 dark:border-gray-500 shadow z-[9999]"
-        >
-          <slot name="sticky-bottom" />
-        </div>
-      {/if}
     </div>
+    {#if isStickyBottom}
+      <div
+        class="flex flex-col sm:flex-row justify-end w-full gap-2 sm:gap-4 sticky pt-4 px-5 bg-immich-bg dark:bg-immich-dark-gray border-t border-gray-200 dark:border-gray-500"
+      >
+        {@render stickyBottom?.()}
+      </div>
+    {/if}
   </div>
 </section>
diff --git a/web/src/lib/components/shared-components/fullscreen-container.svelte b/web/src/lib/components/shared-components/fullscreen-container.svelte
index 6d577f60bd..64ee41a225 100644
--- a/web/src/lib/components/shared-components/fullscreen-container.svelte
+++ b/web/src/lib/components/shared-components/fullscreen-container.svelte
@@ -1,8 +1,15 @@
 <script lang="ts">
+  import type { Snippet } from 'svelte';
   import ImmichLogo from './immich-logo.svelte';
 
-  export let title: string;
-  export let showMessage = $$slots.message;
+  interface Props {
+    title: string;
+    message?: Snippet;
+    showMessage?: boolean;
+    children?: Snippet;
+  }
+
+  let { title, message, showMessage = message != undefined, children }: Props = $props();
 </script>
 
 <section class="min-w-screen flex min-h-screen place-content-center place-items-center p-4">
@@ -20,10 +27,10 @@
       <div
         class="w-full rounded-xl border-2 border-immich-primary bg-immich-primary/5 p-4 text-sm font-medium text-immich-primary dark:border-immich-dark-bg dark:text-immich-dark-primary"
       >
-        <slot name="message" />
+        {@render message?.()}
       </div>
     {/if}
 
-    <slot />
+    {@render children?.()}
   </div>
 </section>
diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte
index d64d784177..8f8a067a90 100644
--- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte
+++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte
@@ -1,36 +1,60 @@
 <script lang="ts">
+  import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut';
   import { goto } from '$app/navigation';
   import type { Action } from '$lib/components/asset-viewer/actions/action';
   import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
   import { AppRoute, AssetAction } from '$lib/constants';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
   import type { Viewport } from '$lib/stores/assets.store';
-  import { getAssetRatio } from '$lib/utils/asset-utils';
+  import { showDeleteModal } from '$lib/stores/preferences.store';
+  import { deleteAssets } from '$lib/utils/actions';
+  import { archiveAssets, cancelMultiselect, getAssetRatio } from '$lib/utils/asset-utils';
+  import { featureFlags } from '$lib/stores/server-config.store';
   import { handleError } from '$lib/utils/handle-error';
   import { navigate } from '$lib/utils/navigation';
   import { calculateWidth } from '$lib/utils/timeline-util';
   import { type AssetResponseDto } from '@immich/sdk';
   import justifiedLayout from 'justified-layout';
-  import { onDestroy } from 'svelte';
   import { t } from 'svelte-i18n';
   import AssetViewer from '../../asset-viewer/asset-viewer.svelte';
+  import ShowShortcuts from '../show-shortcuts.svelte';
   import Portal from '../portal/portal.svelte';
   import { handlePromiseError } from '$lib/utils';
+  import DeleteAssetDialog from '../../photos-page/delete-asset-dialog.svelte';
+  import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 
-  export let assets: AssetResponseDto[];
-  export let selectedAssets: Set<AssetResponseDto> = new Set();
-  export let disableAssetSelect = false;
-  export let showArchiveIcon = false;
-  export let viewport: Viewport;
-  export let onIntersected: (() => void) | undefined = undefined;
-  export let showAssetName = false;
-  export let onPrevious: (() => Promise<AssetResponseDto | undefined>) | undefined = undefined;
-  export let onNext: (() => Promise<AssetResponseDto | undefined>) | undefined = undefined;
+  interface Props {
+    assets: AssetResponseDto[];
+    assetInteraction: AssetInteraction;
+    disableAssetSelect?: boolean;
+    showArchiveIcon?: boolean;
+    viewport: Viewport;
+    onIntersected?: (() => void) | undefined;
+    showAssetName?: boolean;
+    isShowDeleteConfirmation?: boolean;
+    onPrevious?: (() => Promise<AssetResponseDto | undefined>) | undefined;
+    onNext?: (() => Promise<AssetResponseDto | undefined>) | undefined;
+  }
+
+  let {
+    assets = $bindable(),
+    assetInteraction,
+    disableAssetSelect = false,
+    showArchiveIcon = false,
+    viewport,
+    onIntersected = undefined,
+    showAssetName = false,
+    isShowDeleteConfirmation = $bindable(false),
+    onPrevious = undefined,
+    onNext = undefined,
+  }: Props = $props();
 
   let { isViewing: isViewerOpen, asset: viewingAsset, setAsset } = assetViewingStore;
 
+  let showShortcuts = $state(false);
   let currentViewAssetIndex = 0;
-  $: isMultiSelectionMode = selectedAssets.size > 0;
+  let shiftKeyIsDown = $state(false);
+  let lastAssetMouseEvent: AssetResponseDto | null = $state(null);
 
   const viewAssetHandler = async (asset: AssetResponseDto) => {
     currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id);
@@ -38,25 +62,157 @@
     await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
   };
 
-  const selectAssetHandler = (asset: AssetResponseDto) => {
-    let temporary = new Set(selectedAssets);
+  const selectAllAssets = () => {
+    assetInteraction.selectAssets(assets);
+  };
 
-    if (selectedAssets.has(asset)) {
-      temporary.delete(asset);
+  const deselectAllAssets = () => {
+    cancelMultiselect(assetInteraction);
+  };
+
+  const onKeyDown = (event: KeyboardEvent) => {
+    if (event.key === 'Shift') {
+      event.preventDefault();
+      shiftKeyIsDown = true;
+    }
+  };
+
+  const onKeyUp = (event: KeyboardEvent) => {
+    if (event.key === 'Shift') {
+      event.preventDefault();
+      shiftKeyIsDown = false;
+    }
+  };
+
+  const handleSelectAssets = (asset: AssetResponseDto) => {
+    if (!asset) {
+      return;
+    }
+    const deselect = assetInteraction.selectedAssets.has(asset);
+
+    // Select/deselect already loaded assets
+    if (deselect) {
+      for (const candidate of assetInteraction.assetSelectionCandidates) {
+        assetInteraction.removeAssetFromMultiselectGroup(candidate);
+      }
+      assetInteraction.removeAssetFromMultiselectGroup(asset);
     } else {
-      temporary.add(asset);
+      for (const candidate of assetInteraction.assetSelectionCandidates) {
+        assetInteraction.selectAsset(candidate);
+      }
+      assetInteraction.selectAsset(asset);
     }
 
-    selectedAssets = temporary;
+    assetInteraction.clearAssetSelectionCandidates();
+    assetInteraction.setAssetSelectionStart(deselect ? null : asset);
   };
 
+  const handleSelectAssetCandidates = (asset: AssetResponseDto | null) => {
+    if (asset) {
+      selectAssetCandidates(asset);
+    }
+    lastAssetMouseEvent = asset;
+  };
+
+  const selectAssetCandidates = (endAsset: AssetResponseDto) => {
+    if (!shiftKeyIsDown) {
+      return;
+    }
+
+    const startAsset = assetInteraction.assetSelectionStart;
+    if (!startAsset) {
+      return;
+    }
+
+    let start = assets.findIndex((a) => a.id === startAsset.id);
+    let end = assets.findIndex((a) => a.id === endAsset.id);
+
+    if (start > end) {
+      [start, end] = [end, start];
+    }
+
+    assetInteraction.setAssetSelectionCandidates(assets.slice(start, end + 1));
+  };
+
+  const onSelectStart = (e: Event) => {
+    if (assetInteraction.selectionActive && shiftKeyIsDown) {
+      e.preventDefault();
+    }
+  };
+
+  const onDelete = () => {
+    const hasTrashedAsset = assetInteraction.selectedAssetsArray.some((asset) => asset.isTrashed);
+
+    if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) {
+      isShowDeleteConfirmation = true;
+      return;
+    }
+    handlePromiseError(trashOrDelete(hasTrashedAsset));
+  };
+
+  const onForceDelete = () => {
+    if ($showDeleteModal) {
+      isShowDeleteConfirmation = true;
+      return;
+    }
+    handlePromiseError(trashOrDelete(true));
+  };
+
+  const trashOrDelete = async (force: boolean = false) => {
+    isShowDeleteConfirmation = false;
+    await deleteAssets(
+      !(isTrashEnabled && !force),
+      (assetIds) => (assets = assets.filter((asset) => !assetIds.includes(asset.id))),
+      idsSelectedAssets,
+    );
+    assetInteraction.clearMultiselect();
+  };
+
+  const toggleArchive = async () => {
+    const ids = await archiveAssets(assetInteraction.selectedAssetsArray, !assetInteraction.isAllArchived);
+    if (ids) {
+      assets.filter((asset) => !ids.includes(asset.id));
+      deselectAllAssets();
+    }
+  };
+
+  let shortcutList = $derived(
+    (() => {
+      if ($isViewerOpen) {
+        return [];
+      }
+
+      const shortcuts: ShortcutOptions[] = [
+        { shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) },
+        { shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
+        { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets() },
+      ];
+
+      if (assetInteraction.selectionActive) {
+        shortcuts.push(
+          { shortcut: { key: 'Escape' }, onShortcut: deselectAllAssets },
+          { shortcut: { key: 'Delete' }, onShortcut: onDelete },
+          { shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete },
+          { shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
+          { shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
+        );
+      }
+
+      return shortcuts;
+    })(),
+  );
+
   const handleNext = async () => {
     try {
-      const asset = onNext ? await onNext() : assets[++currentViewAssetIndex];
-      if (asset) {
-        setAsset(asset);
-        await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
+      let asset: AssetResponseDto | undefined;
+      if (onNext) {
+        asset = await onNext();
+      } else {
+        currentViewAssetIndex = Math.min(currentViewAssetIndex + 1, assets.length - 1);
+        asset = assets[currentViewAssetIndex];
       }
+
+      await navigateToAsset(asset);
     } catch (error) {
       handleError(error, $t('errors.cannot_navigate_next_asset'));
     }
@@ -64,16 +220,27 @@
 
   const handlePrevious = async () => {
     try {
-      const asset = onPrevious ? await onPrevious() : assets[--currentViewAssetIndex];
-      if (asset) {
-        setAsset(asset);
-        await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
+      let asset: AssetResponseDto | undefined;
+      if (onPrevious) {
+        asset = await onPrevious();
+      } else {
+        currentViewAssetIndex = Math.max(currentViewAssetIndex - 1, 0);
+        asset = assets[currentViewAssetIndex];
       }
+
+      await navigateToAsset(asset);
     } catch (error) {
       handleError(error, $t('errors.cannot_navigate_previous_asset'));
     }
   };
 
+  const navigateToAsset = async (asset?: AssetResponseDto) => {
+    if (asset && asset.id !== $viewingAsset.id) {
+      setAsset(asset);
+      await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
+    }
+  };
+
   const handleAction = async (action: Action) => {
     switch (action.type) {
       case AssetAction.ARCHIVE:
@@ -83,7 +250,6 @@
           assets.findIndex((a) => a.id === action.asset.id),
           1,
         );
-        assets = assets;
         if (assets.length === 0) {
           await goto(AppRoute.PHOTOS);
         } else if (currentViewAssetIndex === assets.length) {
@@ -96,29 +262,68 @@
     }
   };
 
-  onDestroy(() => {
-    $isViewerOpen = false;
+  const assetMouseEventHandler = (asset: AssetResponseDto | null) => {
+    if (assetInteraction.selectionActive) {
+      handleSelectAssetCandidates(asset);
+    }
+  };
+
+  let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
+  let idsSelectedAssets = $derived(assetInteraction.selectedAssetsArray.map(({ id }) => id));
+
+  let geometry = $derived(
+    (() => {
+      const justifiedLayoutResult = justifiedLayout(
+        assets.map((asset) => getAssetRatio(asset)),
+        {
+          boxSpacing: 2,
+          containerWidth: Math.floor(viewport.width),
+          containerPadding: 0,
+          targetRowHeightTolerance: 0.15,
+          targetRowHeight: 235,
+        },
+      );
+
+      return {
+        ...justifiedLayoutResult,
+        containerWidth: calculateWidth(justifiedLayoutResult.boxes),
+      };
+    })(),
+  );
+
+  $effect(() => {
+    if (!lastAssetMouseEvent) {
+      assetInteraction.clearAssetSelectionCandidates();
+    }
   });
 
-  $: geometry = (() => {
-    const justifiedLayoutResult = justifiedLayout(
-      assets.map((asset) => getAssetRatio(asset)),
-      {
-        boxSpacing: 2,
-        containerWidth: Math.floor(viewport.width),
-        containerPadding: 0,
-        targetRowHeightTolerance: 0.15,
-        targetRowHeight: 235,
-      },
-    );
+  $effect(() => {
+    if (!shiftKeyIsDown) {
+      assetInteraction.clearAssetSelectionCandidates();
+    }
+  });
 
-    return {
-      ...justifiedLayoutResult,
-      containerWidth: calculateWidth(justifiedLayoutResult.boxes),
-    };
-  })();
+  $effect(() => {
+    if (shiftKeyIsDown && lastAssetMouseEvent) {
+      selectAssetCandidates(lastAssetMouseEvent);
+    }
+  });
 </script>
 
+<svelte:window onkeydown={onKeyDown} onkeyup={onKeyUp} onselectstart={onSelectStart} use:shortcuts={shortcutList} />
+
+{#if isShowDeleteConfirmation}
+  <DeleteAssetDialog
+    size={assetInteraction.selectedAssets.size}
+    onCancel={() => (isShowDeleteConfirmation = false)}
+    onConfirm={() => handlePromiseError(trashOrDelete(true))}
+  />
+{/if}
+
+{#if showShortcuts}
+  <ShowShortcuts onClose={() => (showShortcuts = !showShortcuts)} />
+{/if}
+
 {#if assets.length > 0}
   <div class="relative" style="height: {geometry.containerHeight}px;width: {geometry.containerWidth}px ">
     {#each assets as asset, i (i)}
@@ -129,19 +334,21 @@
         title={showAssetName ? asset.originalFileName : ''}
       >
         <Thumbnail
-          {asset}
           readonly={disableAssetSelect}
           onClick={(asset) => {
-            if (isMultiSelectionMode) {
-              selectAssetHandler(asset);
+            if (assetInteraction.selectionActive) {
+              handleSelectAssets(asset);
               return;
             }
             void viewAssetHandler(asset);
           }}
-          onSelect={(asset) => selectAssetHandler(asset)}
+          onSelect={(asset) => handleSelectAssets(asset)}
+          onMouseEvent={() => assetMouseEventHandler(asset)}
           onIntersected={() => (i === Math.max(1, assets.length - 7) ? onIntersected?.() : void 0)}
-          selected={selectedAssets.has(asset)}
           {showArchiveIcon}
+          {asset}
+          selected={assetInteraction.selectedAssets.has(asset)}
+          selectionCandidate={assetInteraction.assetSelectionCandidates.has(asset)}
           thumbnailWidth={geometry.boxes[i].width}
           thumbnailHeight={geometry.boxes[i].height}
         />
@@ -163,9 +370,9 @@
     <AssetViewer
       asset={$viewingAsset}
       onAction={handleAction}
-      on:previous={handlePrevious}
-      on:next={handleNext}
-      on:close={() => {
+      onPrevious={handlePrevious}
+      onNext={handleNext}
+      onClose={() => {
         assetViewingStore.showAssetViewer(false);
         handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
       }}
diff --git a/web/src/lib/components/shared-components/help-and-feedback-modal.svelte b/web/src/lib/components/shared-components/help-and-feedback-modal.svelte
new file mode 100644
index 0000000000..c122e0f23e
--- /dev/null
+++ b/web/src/lib/components/shared-components/help-and-feedback-modal.svelte
@@ -0,0 +1,134 @@
+<script lang="ts">
+  import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
+  import Portal from '$lib/components/shared-components/portal/portal.svelte';
+  import { type ServerAboutResponseDto } from '@immich/sdk';
+  import { t } from 'svelte-i18n';
+  import Icon from '$lib/components/elements/icon.svelte';
+  import { mdiBugOutline, mdiFaceAgent, mdiGit, mdiGithub, mdiInformationOutline } from '@mdi/js';
+  import { discordPath } from '$lib/assets/svg-paths';
+
+  interface Props {
+    onClose: () => void;
+    info: ServerAboutResponseDto;
+  }
+
+  let { onClose, info }: Props = $props();
+</script>
+
+<Portal>
+  <FullScreenModal title={$t('support_and_feedback')} {onClose}>
+    <p>{$t('official_immich_resources')}</p>
+    <div class="flex flex-col sm:grid sm:grid-cols-2 gap-2 mt-5">
+      <div>
+        <a href="https://{info.version}.archive.immich.app/docs/overview/introduction" target="_blank" rel="noreferrer">
+          <Icon path={mdiInformationOutline} size="1.5em" class="inline-block" />
+          <p
+            class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm underline inline-block"
+            id="documentation-label"
+          >
+            {$t('documentation')}
+          </p>
+        </a>
+      </div>
+
+      <div>
+        <a href="https://github.com/immich-app/immich/" target="_blank" rel="noreferrer">
+          <Icon path={mdiGithub} size="1.5em" class="inline-block" />
+          <p
+            class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm underline inline-block"
+            id="github-label"
+          >
+            {$t('source')}
+          </p>
+        </a>
+      </div>
+
+      <div>
+        <a href="https://discord.immich.app" target="_blank" rel="noreferrer">
+          <Icon path={discordPath} class="inline-block" size="1.5em" />
+          <p
+            class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm underline inline-block"
+            id="github-label"
+          >
+            {$t('discord')}
+          </p>
+        </a>
+      </div>
+
+      <div>
+        <a href="https://github.com/immich-app/immich/issues/new/choose" target="_blank" rel="noreferrer">
+          <Icon path={mdiBugOutline} size="1.5em" class="inline-block" />
+          <p
+            class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm underline inline-block"
+            id="github-label"
+          >
+            {$t('bugs_and_feature_requests')}
+          </p>
+        </a>
+      </div>
+    </div>
+    {#if info.thirdPartyBugFeatureUrl || info.thirdPartySourceUrl || info.thirdPartyDocumentationUrl || info.thirdPartySupportUrl}
+      <p class="mt-5">{$t('third_party_resources')}</p>
+      <p class="text-sm mt-1">
+        {$t('support_third_party_description')}
+      </p>
+      <div class="flex flex-col sm:grid sm:grid-cols-2 gap-2 mt-5">
+        {#if info.thirdPartyDocumentationUrl}
+          <div>
+            <a href={info.thirdPartyDocumentationUrl} target="_blank" rel="noreferrer">
+              <Icon path={mdiInformationOutline} size="1.5em" class="inline-block" />
+              <p
+                class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm underline inline-block"
+                id="documentation-label"
+              >
+                {$t('documentation')}
+              </p>
+            </a>
+          </div>
+        {/if}
+
+        {#if info.thirdPartySourceUrl}
+          <div>
+            <a href={info.thirdPartySourceUrl} target="_blank" rel="noreferrer">
+              <Icon path={mdiGit} size="1.5em" class="inline-block" />
+              <p
+                class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm underline inline-block"
+                id="github-label"
+              >
+                {$t('source')}
+              </p>
+            </a>
+          </div>
+        {/if}
+
+        {#if info.thirdPartySupportUrl}
+          <div>
+            <a href={info.thirdPartySupportUrl} target="_blank" rel="noreferrer">
+              <Icon path={mdiFaceAgent} class="inline-block" size="1.5em" />
+              <p
+                class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm underline inline-block"
+                id="github-label"
+              >
+                {$t('support')}
+              </p>
+            </a>
+          </div>
+        {/if}
+
+        {#if info.thirdPartyBugFeatureUrl}
+          <div>
+            <a href={info.thirdPartyBugFeatureUrl} target="_blank" rel="noreferrer">
+              <Icon path={mdiBugOutline} size="1.5em" class="inline-block" />
+              <p
+                class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm underline inline-block"
+                id="github-label"
+              >
+                {$t('bugs_and_feature_requests')}
+              </p>
+            </a>
+          </div>
+        {/if}
+      </div>
+    {/if}
+  </FullScreenModal>
+</Portal>
diff --git a/web/src/lib/components/shared-components/immich-logo-small-link.svelte b/web/src/lib/components/shared-components/immich-logo-small-link.svelte
index 9f1dd9714e..cd3149e6de 100644
--- a/web/src/lib/components/shared-components/immich-logo-small-link.svelte
+++ b/web/src/lib/components/shared-components/immich-logo-small-link.svelte
@@ -1,7 +1,11 @@
 <script lang="ts">
   import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
 
-  export let width: number;
+  interface Props {
+    width: number;
+  }
+
+  let { width }: Props = $props();
 </script>
 
 <a data-sveltekit-preload-data="hover" class="ml-4" href="/">
diff --git a/web/src/lib/components/shared-components/immich-logo.svelte b/web/src/lib/components/shared-components/immich-logo.svelte
index 952960ef3f..7046ea689e 100644
--- a/web/src/lib/components/shared-components/immich-logo.svelte
+++ b/web/src/lib/components/shared-components/immich-logo.svelte
@@ -9,14 +9,12 @@
   import type { HTMLImgAttributes } from 'svelte/elements';
   import { t } from 'svelte-i18n';
 
-  // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  interface $$Props extends HTMLImgAttributes {
+  interface Props extends HTMLImgAttributes {
     noText?: boolean;
     draggable?: boolean;
   }
 
-  export let noText = false;
-  export let draggable = false;
+  let { noText = false, draggable = false, ...rest }: Props = $props();
 
   const today = DateTime.now().toLocal();
 </script>
@@ -28,6 +26,6 @@
     src={noText ? logoNoText : $colorTheme.value == Theme.LIGHT ? logoLightUrl : logoDarkUrl}
     alt={$t('immich_logo')}
     {draggable}
-    {...$$restProps}
+    {...rest}
   />
 {/if}
diff --git a/web/src/lib/components/shared-components/loading-spinner.svelte b/web/src/lib/components/shared-components/loading-spinner.svelte
index 48626a50f4..e81d2225b7 100644
--- a/web/src/lib/components/shared-components/loading-spinner.svelte
+++ b/web/src/lib/components/shared-components/loading-spinner.svelte
@@ -1,5 +1,9 @@
 <script lang="ts">
-  export let size: string = '24';
+  interface Props {
+    size?: string;
+  }
+
+  let { size = '24' }: Props = $props();
 </script>
 
 <div>
diff --git a/web/src/lib/components/shared-components/map/map.svelte b/web/src/lib/components/shared-components/map/map.svelte
index d769d8a559..7644064d9d 100644
--- a/web/src/lib/components/shared-components/map/map.svelte
+++ b/web/src/lib/components/shared-components/map/map.svelte
@@ -1,4 +1,4 @@
-<script lang="ts" context="module">
+<script lang="ts" module>
   void maplibregl.setRTLTextPlugin(mapboxRtlUrl, true);
 </script>
 
@@ -6,14 +6,14 @@
   import Icon from '$lib/components/elements/icon.svelte';
   import { Theme } from '$lib/constants';
   import { colorTheme, mapSettings } from '$lib/stores/preferences.store';
-  import { getAssetThumbnailUrl, getKey, handlePromiseError } from '$lib/utils';
-  import { getMapStyle, MapTheme, type MapMarkerResponseDto } from '@immich/sdk';
+  import { serverConfig } from '$lib/stores/server-config.store';
+  import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
+  import { type MapMarkerResponseDto } from '@immich/sdk';
   import mapboxRtlUrl from '@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js?url';
   import { mdiCog, mdiMap, mdiMapMarker } from '@mdi/js';
   import type { Feature, GeoJsonProperties, Geometry, Point } from 'geojson';
-  import type { GeoJSONSource, LngLatLike, StyleSpecification } from 'maplibre-gl';
+  import type { GeoJSONSource, LngLatLike } from 'maplibre-gl';
   import maplibregl from 'maplibre-gl';
-  import { createEventDispatcher } from 'svelte';
   import { t } from 'svelte-i18n';
   import {
     AttributionControl,
@@ -31,14 +31,43 @@
     type Map,
   } from 'svelte-maplibre';
 
-  export let mapMarkers: MapMarkerResponseDto[];
-  export let showSettingsModal: boolean | undefined = undefined;
-  export let zoom: number | undefined = undefined;
-  export let center: LngLatLike | undefined = undefined;
-  export let hash = false;
-  export let simplified = false;
-  export let clickable = false;
-  export let useLocationPin = false;
+  interface Props {
+    mapMarkers: MapMarkerResponseDto[];
+    showSettingsModal?: boolean | undefined;
+    zoom?: number | undefined;
+    center?: LngLatLike | undefined;
+    hash?: boolean;
+    simplified?: boolean;
+    clickable?: boolean;
+    useLocationPin?: boolean;
+    onOpenInMapView?: (() => Promise<void> | void) | undefined;
+    onSelect?: (assetIds: string[]) => void;
+    onClickPoint?: ({ lat, lng }: { lat: number; lng: number }) => void;
+    popup?: import('svelte').Snippet<[{ marker: MapMarkerResponseDto }]>;
+  }
+
+  let {
+    mapMarkers = $bindable(),
+    showSettingsModal = $bindable(undefined),
+    zoom = undefined,
+    center = $bindable(undefined),
+    hash = false,
+    simplified = false,
+    clickable = false,
+    useLocationPin = false,
+    onOpenInMapView = undefined,
+    onSelect = () => {},
+    onClickPoint = () => {},
+    popup,
+  }: Props = $props();
+
+  let map: maplibregl.Map | undefined = $state();
+  let marker: maplibregl.Marker | null = null;
+
+  const theme = $derived($mapSettings.allowDarkMode ? $colorTheme.value : Theme.LIGHT);
+  const styleUrl = $derived(theme === Theme.DARK ? $serverConfig.mapDarkStyleUrl : $serverConfig.mapLightStyleUrl);
+  const style = $derived(fetch(styleUrl).then((response) => response.json()));
+
   export function addClipMapMarker(lng: number, lat: number) {
     if (map) {
       if (marker) {
@@ -47,31 +76,14 @@
 
       center = { lng, lat };
       marker = new maplibregl.Marker().setLngLat([lng, lat]).addTo(map);
-      map.setZoom(15);
     }
   }
 
-  export let onOpenInMapView: (() => Promise<void> | void) | undefined = undefined;
-
-  let map: maplibregl.Map;
-  let marker: maplibregl.Marker | null = null;
-
-  $: style = (() =>
-    getMapStyle({
-      theme: ($mapSettings.allowDarkMode ? $colorTheme.value : Theme.LIGHT) as unknown as MapTheme,
-      key: getKey(),
-    }) as Promise<StyleSpecification>)();
-
-  const dispatch = createEventDispatcher<{
-    selected: string[];
-    clickedPoint: { lat: number; lng: number };
-  }>();
-
   function handleAssetClick(assetId: string, map: Map | null) {
     if (!map) {
       return;
     }
-    dispatch('selected', [assetId]);
+    onSelect([assetId]);
   }
 
   async function handleClusterClick(clusterId: number, map: Map | null) {
@@ -82,19 +94,21 @@
     const mapSource = map?.getSource('geojson') as GeoJSONSource;
     const leaves = await mapSource.getClusterLeaves(clusterId, 10_000, 0);
     const ids = leaves.map((leaf) => leaf.properties?.id);
-    dispatch('selected', ids);
+    onSelect(ids);
   }
 
   function handleMapClick(event: maplibregl.MapMouseEvent) {
     if (clickable) {
       const { lng, lat } = event.lngLat;
-      dispatch('clickedPoint', { lng, lat });
+      onClickPoint({ lng, lat });
 
       if (marker) {
         marker.remove();
       }
 
-      marker = new maplibregl.Marker().setLngLat([lng, lat]).addTo(map);
+      if (map) {
+        marker = new maplibregl.Marker().setLngLat([lng, lat]).addTo(map);
+      }
     }
   }
 
@@ -136,92 +150,96 @@
     {zoom}
     attributionControl={false}
     diffStyleUpdates={true}
-    let:map
     on:load={(event) => event.detail.setMaxZoom(18)}
     on:load={(event) => event.detail.on('click', handleMapClick)}
     bind:map
   >
-    <NavigationControl position="top-left" showCompass={!simplified} />
+    {#snippet children({ map }: { map: maplibregl.Map })}
+      <NavigationControl position="top-left" showCompass={!simplified} />
 
-    {#if !simplified}
-      <GeolocateControl position="top-left" />
-      <FullscreenControl position="top-left" />
-      <ScaleControl />
-      <AttributionControl compact={false} />
-    {/if}
+      {#if !simplified}
+        <GeolocateControl position="top-left" />
+        <FullscreenControl position="top-left" />
+        <ScaleControl />
+        <AttributionControl compact={false} />
+      {/if}
 
-    {#if showSettingsModal !== undefined}
-      <Control>
-        <ControlGroup>
-          <ControlButton on:click={() => (showSettingsModal = true)}><Icon path={mdiCog} size="100%" /></ControlButton>
-        </ControlGroup>
-      </Control>
-    {/if}
+      {#if showSettingsModal !== undefined}
+        <Control>
+          <ControlGroup>
+            <ControlButton on:click={() => (showSettingsModal = true)}><Icon path={mdiCog} size="100%" /></ControlButton
+            >
+          </ControlGroup>
+        </Control>
+      {/if}
 
-    {#if onOpenInMapView}
-      <Control position="top-right">
-        <ControlGroup>
-          <ControlButton on:click={() => onOpenInMapView()}>
-            <Icon title={$t('open_in_map_view')} path={mdiMap} size="100%" />
-          </ControlButton>
-        </ControlGroup>
-      </Control>
-    {/if}
+      {#if onOpenInMapView}
+        <Control position="top-right">
+          <ControlGroup>
+            <ControlButton on:click={() => onOpenInMapView()}>
+              <Icon title={$t('open_in_map_view')} path={mdiMap} size="100%" />
+            </ControlButton>
+          </ControlGroup>
+        </Control>
+      {/if}
 
-    <GeoJSON
-      data={{
-        type: 'FeatureCollection',
-        features: mapMarkers.map((marker) => asFeature(marker)),
-      }}
-      id="geojson"
-      cluster={{ radius: 500, maxZoom: 24 }}
-    >
-      <MarkerLayer
-        applyToClusters
-        asButton
-        let:feature
-        on:click={(event) => handlePromiseError(handleClusterClick(event.detail.feature.properties?.cluster_id, map))}
-      >
-        <div
-          class="rounded-full w-[40px] h-[40px] bg-immich-primary text-immich-gray flex justify-center items-center font-mono font-bold shadow-lg hover:bg-immich-dark-primary transition-all duration-200 hover:text-immich-dark-bg opacity-90"
-        >
-          {feature.properties?.point_count}
-        </div>
-      </MarkerLayer>
-      <MarkerLayer
-        applyToClusters={false}
-        asButton
-        let:feature
-        on:click={(event) => {
-          if (!$$slots.popup) {
-            handleAssetClick(event.detail.feature.properties?.id, map);
-          }
+      <GeoJSON
+        data={{
+          type: 'FeatureCollection',
+          features: mapMarkers.map((marker) => asFeature(marker)),
         }}
+        id="geojson"
+        cluster={{ radius: 500, maxZoom: 24 }}
       >
-        {#if useLocationPin}
-          <Icon
-            path={mdiMapMarker}
-            size="50px"
-            class="location-pin dark:text-immich-dark-primary text-immich-primary"
-          />
-        {:else}
-          <img
-            src={getAssetThumbnailUrl(feature.properties?.id)}
-            class="rounded-full w-[60px] h-[60px] border-2 border-immich-primary shadow-lg hover:border-immich-dark-primary transition-all duration-200 hover:scale-150 object-cover bg-immich-primary"
-            alt={feature.properties?.city && feature.properties.country
-              ? $t('map_marker_for_images', {
-                  values: { city: feature.properties.city, country: feature.properties.country },
-                })
-              : $t('map_marker_with_image')}
-          />
-        {/if}
-        {#if $$slots.popup}
-          <Popup offset={[0, -30]} openOn="click" closeOnClickOutside>
-            <slot name="popup" marker={asMarker(feature)} />
-          </Popup>
-        {/if}
-      </MarkerLayer>
-    </GeoJSON>
+        <MarkerLayer
+          applyToClusters
+          asButton
+          on:click={(event) => handlePromiseError(handleClusterClick(event.detail.feature.properties?.cluster_id, map))}
+        >
+          {#snippet children({ feature }: { feature: maplibregl.Feature })}
+            <div
+              class="rounded-full w-[40px] h-[40px] bg-immich-primary text-immich-gray flex justify-center items-center font-mono font-bold shadow-lg hover:bg-immich-dark-primary transition-all duration-200 hover:text-immich-dark-bg opacity-90"
+            >
+              {feature.properties?.point_count}
+            </div>
+          {/snippet}
+        </MarkerLayer>
+        <MarkerLayer
+          applyToClusters={false}
+          asButton
+          on:click={(event) => {
+            if (!popup) {
+              handleAssetClick(event.detail.feature.properties?.id, map);
+            }
+          }}
+        >
+          {#snippet children({ feature }: { feature: Feature<Geometry, GeoJsonProperties> })}
+            {#if useLocationPin}
+              <Icon
+                path={mdiMapMarker}
+                size="50px"
+                class="location-pin dark:text-immich-dark-primary text-immich-primary"
+              />
+            {:else}
+              <img
+                src={getAssetThumbnailUrl(feature.properties?.id)}
+                class="rounded-full w-[60px] h-[60px] border-2 border-immich-primary shadow-lg hover:border-immich-dark-primary transition-all duration-200 hover:scale-150 object-cover bg-immich-primary"
+                alt={feature.properties?.city && feature.properties.country
+                  ? $t('map_marker_for_images', {
+                      values: { city: feature.properties.city, country: feature.properties.country },
+                    })
+                  : $t('map_marker_with_image')}
+              />
+            {/if}
+            {#if popup}
+              <Popup offset={[0, -30]} openOn="click" closeOnClickOutside>
+                {@render popup?.({ marker: asMarker(feature) })}
+              </Popup>
+            {/if}
+          {/snippet}
+        </MarkerLayer>
+      </GeoJSON>
+    {/snippet}
   </MapLibre>
   <style>
     .location-pin {
diff --git a/web/src/lib/components/shared-components/modal-header.svelte b/web/src/lib/components/shared-components/modal-header.svelte
index efd87b476c..53f3fbdabb 100644
--- a/web/src/lib/components/shared-components/modal-header.svelte
+++ b/web/src/lib/components/shared-components/modal-header.svelte
@@ -5,20 +5,24 @@
   import { mdiClose } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  /**
-   * Unique identifier for the header text.
-   */
-  export let id: string;
-  export let title: string;
-  export let onClose: () => void;
-  /**
-   * If true, the logo will be displayed next to the modal title.
-   */
-  export let showLogo = false;
-  /**
-   * Optional icon to display next to the modal title, if `showLogo` is false.
-   */
-  export let icon: string | undefined = undefined;
+  interface Props {
+    /**
+     * Unique identifier for the header text.
+     */
+    id: string;
+    title: string;
+    onClose: () => void;
+    /**
+     * If true, the logo will be displayed next to the modal title.
+     */
+    showLogo?: boolean;
+    /**
+     * Optional icon to display next to the modal title, if `showLogo` is false.
+     */
+    icon?: string;
+  }
+
+  let { id, title, onClose, showLogo = false, icon = undefined }: Props = $props();
 </script>
 
 <div class="flex place-items-center justify-between px-5 pb-3">
@@ -33,5 +37,5 @@
     </h1>
   </div>
 
-  <CircleIconButton on:click={onClose} icon={mdiClose} size={'20'} title={$t('close')} />
+  <CircleIconButton onclick={onClose} icon={mdiClose} size={'20'} title={$t('close')} />
 </div>
diff --git a/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte b/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte
index ef103a9e03..5bf3f0a621 100644
--- a/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte
+++ b/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte
@@ -1,4 +1,5 @@
 <script lang="ts">
+  import { page } from '$app/state';
   import { focusTrap } from '$lib/actions/focus-trap';
   import Button from '$lib/components/elements/buttons/button.svelte';
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
@@ -8,20 +9,20 @@
   import { handleError } from '$lib/utils/handle-error';
   import { deleteProfileImage, updateMyPreferences, type UserAvatarColor } from '@immich/sdk';
   import { mdiCog, mdiLogout, mdiPencil, mdiWrench } from '@mdi/js';
-  import { createEventDispatcher } from 'svelte';
+  import { t } from 'svelte-i18n';
   import { fade } from 'svelte/transition';
   import { NotificationType, notificationController } from '../notification/notification';
   import UserAvatar from '../user-avatar.svelte';
   import AvatarSelector from './avatar-selector.svelte';
-  import { t } from 'svelte-i18n';
-  import { page } from '$app/stores';
 
-  let isShowSelectAvatar = false;
+  interface Props {
+    onLogout: () => void;
+    onClose?: () => void;
+  }
 
-  const dispatch = createEventDispatcher<{
-    logout: void;
-    close: void;
-  }>();
+  let { onLogout, onClose = () => {} }: Props = $props();
+
+  let isShowSelectAvatar = $state(false);
 
   const handleSaveProfile = async (color: UserAvatarColor) => {
     try {
@@ -63,7 +64,7 @@
           class="border"
           size="12"
           padding="2"
-          on:click={() => (isShowSelectAvatar = true)}
+          onclick={() => (isShowSelectAvatar = true)}
         />
       </div>
     </div>
@@ -75,14 +76,7 @@
     </div>
 
     <div class="flex flex-col gap-1">
-      <Button
-        href={AppRoute.USER_SETTINGS}
-        on:click={() => dispatch('close')}
-        color="dark-gray"
-        size="sm"
-        shadow={false}
-        border
-      >
+      <Button href={AppRoute.USER_SETTINGS} onclick={onClose} color="dark-gray" size="sm" shadow={false} border>
         <div class="flex place-content-center place-items-center text-center gap-2 px-2">
           <Icon path={mdiCog} size="18" ariaHidden />
           {$t('account_settings')}
@@ -91,12 +85,12 @@
       {#if $user.isAdmin}
         <Button
           href={AppRoute.ADMIN_USER_MANAGEMENT}
-          on:click={() => dispatch('close')}
+          onclick={onClose}
           color="dark-gray"
           size="sm"
           shadow={false}
           border
-          aria-current={$page.url.pathname.includes('/admin') ? 'page' : undefined}
+          aria-current={page.url.pathname.includes('/admin') ? 'page' : undefined}
         >
           <div class="flex place-content-center place-items-center text-center gap-2 px-2">
             <Icon path={mdiWrench} size="18" ariaHidden />
@@ -111,7 +105,7 @@
     <button
       type="button"
       class="flex w-full place-content-center place-items-center gap-2 py-3 font-medium text-gray-500 hover:bg-immich-primary/10 dark:text-gray-300"
-      on:click={() => dispatch('logout')}
+      onclick={onLogout}
     >
       <Icon path={mdiLogout} size={24} />
       {$t('sign_out')}</button
@@ -120,9 +114,5 @@
 </div>
 
 {#if isShowSelectAvatar}
-  <AvatarSelector
-    user={$user}
-    on:close={() => (isShowSelectAvatar = false)}
-    on:choose={({ detail: color }) => handleSaveProfile(color)}
-  />
+  <AvatarSelector user={$user} onClose={() => (isShowSelectAvatar = false)} onChoose={handleSaveProfile} />
 {/if}
diff --git a/web/src/lib/components/shared-components/navigation-bar/avatar-selector.svelte b/web/src/lib/components/shared-components/navigation-bar/avatar-selector.svelte
index bbe2c4f142..d762c7ba88 100644
--- a/web/src/lib/components/shared-components/navigation-bar/avatar-selector.svelte
+++ b/web/src/lib/components/shared-components/navigation-bar/avatar-selector.svelte
@@ -1,24 +1,25 @@
 <script lang="ts">
   import { UserAvatarColor, type UserResponseDto } from '@immich/sdk';
-  import { createEventDispatcher } from 'svelte';
+  import { t } from 'svelte-i18n';
   import FullScreenModal from '../full-screen-modal.svelte';
   import UserAvatar from '../user-avatar.svelte';
-  import { t } from 'svelte-i18n';
 
-  export let user: UserResponseDto;
+  interface Props {
+    user: UserResponseDto;
+    onClose: () => void;
+    onChoose: (color: UserAvatarColor) => void;
+  }
+
+  let { user, onClose, onChoose }: Props = $props();
 
-  const dispatch = createEventDispatcher<{
-    close: void;
-    choose: UserAvatarColor;
-  }>();
   const colors: UserAvatarColor[] = Object.values(UserAvatarColor);
 </script>
 
-<FullScreenModal title={$t('select_avatar_color')} width="auto" onClose={() => dispatch('close')}>
+<FullScreenModal title={$t('select_avatar_color')} width="auto" {onClose}>
   <div class="flex items-center justify-center mt-4">
     <div class="grid grid-cols-2 md:grid-cols-5 gap-4">
       {#each colors as color}
-        <button type="button" on:click={() => dispatch('choose', color)}>
+        <button type="button" onclick={() => onChoose(color)}>
           <UserAvatar label={color} {user} {color} size="xl" showProfileImage={false} />
         </button>
       {/each}
diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte
index 58a4c23d74..ae63a249b5 100644
--- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte
+++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte
@@ -1,5 +1,5 @@
 <script lang="ts">
-  import { page } from '$app/stores';
+  import { page } from '$app/state';
   import { clickOutside } from '$lib/actions/click-outside';
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
@@ -7,36 +7,51 @@
   import Icon from '$lib/components/elements/icon.svelte';
   import { featureFlags } from '$lib/stores/server-config.store';
   import { user } from '$lib/stores/user.store';
+  import { userInteraction } from '$lib/stores/user.svelte';
   import { handleLogout } from '$lib/utils/auth';
-  import { logout } from '@immich/sdk';
-  import { mdiMagnify, mdiTrayArrowUp } from '@mdi/js';
-  import { createEventDispatcher } from 'svelte';
+  import { getAboutInfo, logout, type ServerAboutResponseDto } from '@immich/sdk';
+  import { mdiHelpCircleOutline, mdiMagnify, mdiTrayArrowUp } from '@mdi/js';
   import { t } from 'svelte-i18n';
   import { fade } from 'svelte/transition';
-  import { AppRoute } from '../../../constants';
-  import ImmichLogo from '../immich-logo.svelte';
-  import SearchBar from '../search-bar/search-bar.svelte';
+  import { AppRoute } from '$lib/constants';
+  import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
+  import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
   import ThemeButton from '../theme-button.svelte';
   import UserAvatar from '../user-avatar.svelte';
   import AccountInfoPanel from './account-info-panel.svelte';
+  import HelpAndFeedbackModal from '$lib/components/shared-components/help-and-feedback-modal.svelte';
+  import { onMount } from 'svelte';
 
-  export let showUploadButton = true;
+  interface Props {
+    showUploadButton?: boolean;
+    onUploadClick: () => void;
+  }
 
-  let shouldShowAccountInfo = false;
-  let shouldShowAccountInfoPanel = false;
-  let innerWidth: number;
-  const dispatch = createEventDispatcher<{
-    uploadClicked: void;
-  }>();
+  let { showUploadButton = true, onUploadClick }: Props = $props();
+
+  let shouldShowAccountInfo = $state(false);
+  let shouldShowAccountInfoPanel = $state(false);
+  let shouldShowHelpPanel = $state(false);
+  let innerWidth: number = $state(0);
 
   const onLogout = async () => {
     const { redirectUri } = await logout();
     await handleLogout(redirectUri);
   };
+
+  let info: ServerAboutResponseDto | undefined = $state();
+
+  onMount(async () => {
+    info = userInteraction.aboutInfo ?? (await getAboutInfo());
+  });
 </script>
 
 <svelte:window bind:innerWidth />
 
+{#if shouldShowHelpPanel && info}
+  <HelpAndFeedbackModal onClose={() => (shouldShowHelpPanel = false)} {info} />
+{/if}
+
 <section id="dashboard-navbar" class="fixed z-[900] h-[var(--navbar-height)] w-screen text-sm">
   <SkipLink text={$t('skip_to_content')} />
   <div
@@ -52,7 +67,7 @@
         {/if}
       </div>
 
-      <section class="flex place-items-center justify-end gap-2 md:gap-4 w-full sm:w-auto">
+      <section class="flex place-items-center justify-end gap-1 md:gap-2 w-full sm:w-auto">
         {#if $featureFlags.search}
           <CircleIconButton
             href={AppRoute.SEARCH}
@@ -61,20 +76,35 @@
             title={$t('go_to_search')}
             icon={mdiMagnify}
             padding="2"
+            onclick={() => {}}
           />
         {/if}
 
         <ThemeButton padding="2" />
 
-        {#if !$page.url.pathname.includes('/admin') && showUploadButton}
-          <LinkButton on:click={() => dispatch('uploadClicked')} class="hidden lg:block">
+        <div
+          use:clickOutside={{
+            onEscape: () => (shouldShowHelpPanel = false),
+          }}
+        >
+          <CircleIconButton
+            id="support-feedback-button"
+            title={$t('support_and_feedback')}
+            icon={mdiHelpCircleOutline}
+            onclick={() => (shouldShowHelpPanel = !shouldShowHelpPanel)}
+            padding="1"
+          />
+        </div>
+
+        {#if !page.url.pathname.includes('/admin') && showUploadButton}
+          <LinkButton onclick={onUploadClick} class="hidden lg:block">
             <div class="flex gap-2">
               <Icon path={mdiTrayArrowUp} size="1.5em" />
               <span>{$t('upload')}</span>
             </div>
           </LinkButton>
           <CircleIconButton
-            on:click={() => dispatch('uploadClicked')}
+            onclick={onUploadClick}
             title={$t('upload')}
             icon={mdiTrayArrowUp}
             class="lg:hidden"
@@ -90,12 +120,12 @@
         >
           <button
             type="button"
-            class="flex"
-            on:mouseover={() => (shouldShowAccountInfo = true)}
-            on:focus={() => (shouldShowAccountInfo = true)}
-            on:blur={() => (shouldShowAccountInfo = false)}
-            on:mouseleave={() => (shouldShowAccountInfo = false)}
-            on:click={() => (shouldShowAccountInfoPanel = !shouldShowAccountInfoPanel)}
+            class="flex pl-2"
+            onmouseover={() => (shouldShowAccountInfo = true)}
+            onfocus={() => (shouldShowAccountInfo = true)}
+            onblur={() => (shouldShowAccountInfo = false)}
+            onmouseleave={() => (shouldShowAccountInfo = false)}
+            onclick={() => (shouldShowAccountInfoPanel = !shouldShowAccountInfoPanel)}
           >
             {#key $user}
               <UserAvatar user={$user} size="md" showTitle={false} interactive />
@@ -114,7 +144,7 @@
           {/if}
 
           {#if shouldShowAccountInfoPanel}
-            <AccountInfoPanel on:logout={onLogout} />
+            <AccountInfoPanel {onLogout} />
           {/if}
         </div>
       </section>
diff --git a/web/src/lib/components/shared-components/navigation-loading-bar.svelte b/web/src/lib/components/shared-components/navigation-loading-bar.svelte
index bed9b8d7ad..f5879cf0a1 100644
--- a/web/src/lib/components/shared-components/navigation-loading-bar.svelte
+++ b/web/src/lib/components/shared-components/navigation-loading-bar.svelte
@@ -3,7 +3,7 @@
   import { cubicOut } from 'svelte/easing';
   import { tweened } from 'svelte/motion';
 
-  let showing = false;
+  let showing = $state(false);
 
   // delay showing any progress for a little bit so very fast loads
   // do not cause flicker
@@ -27,6 +27,6 @@
 
 {#if showing}
   <div class="absolute left-0 top-0 z-[999999999] h-[3px] w-screen bg-white">
-    <span class="absolute h-[3px] bg-immich-primary" style:width={`${$progress}%`} />
+    <span class="absolute h-[3px] bg-immich-primary" style:width={`${$progress}%`}></span>
   </div>
 {/if}
diff --git a/web/src/lib/components/shared-components/notification/__tests__/notification-card.spec.ts b/web/src/lib/components/shared-components/notification/__tests__/notification-card.spec.ts
index 2d92e77377..afc582e21f 100644
--- a/web/src/lib/components/shared-components/notification/__tests__/notification-card.spec.ts
+++ b/web/src/lib/components/shared-components/notification/__tests__/notification-card.spec.ts
@@ -5,10 +5,10 @@ import { NotificationType } from '../notification';
 import NotificationCard from '../notification-card.svelte';
 
 describe('NotificationCard component', () => {
-  let sut: RenderResult<NotificationCard>;
+  let sut: RenderResult<typeof NotificationCard>;
 
   it('disposes timeout if already removed from the DOM', () => {
-    vi.spyOn(window, 'clearTimeout');
+    vi.spyOn(globalThis, 'clearTimeout');
 
     sut = render(NotificationCard, {
       notification: {
@@ -21,7 +21,7 @@ describe('NotificationCard component', () => {
     });
 
     cleanup();
-    expect(window.clearTimeout).toHaveBeenCalledTimes(1);
+    expect(globalThis.clearTimeout).toHaveBeenCalledTimes(1);
   });
 
   it('shows message and title', () => {
@@ -79,6 +79,8 @@ describe('NotificationCard component', () => {
     });
 
     expect(sut.getByTestId('title')).toHaveTextContent('info');
-    expect(sut.getByTestId('message').innerHTML).toEqual('Notification <b>message</b> with <a href="link">link</a>');
+    expect(sut.getByTestId('message').innerHTML.replaceAll('<!---->', '')).toEqual(
+      'Notification <b>message</b> with <a href="link">link</a>',
+    );
   });
 });
diff --git a/web/src/lib/components/shared-components/notification/__tests__/notification-component-test.svelte b/web/src/lib/components/shared-components/notification/__tests__/notification-component-test.svelte
index dfa305a19d..4dea370952 100644
--- a/web/src/lib/components/shared-components/notification/__tests__/notification-component-test.svelte
+++ b/web/src/lib/components/shared-components/notification/__tests__/notification-component-test.svelte
@@ -1,5 +1,9 @@
 <script lang="ts">
-  export let href: string;
+  interface Props {
+    href: string;
+  }
+
+  let { href }: Props = $props();
 </script>
 
 Notification <b>message</b> with <a {href}>link</a>
diff --git a/web/src/lib/components/shared-components/notification/__tests__/notification-list.spec.ts b/web/src/lib/components/shared-components/notification/__tests__/notification-list.spec.ts
index 669b7d75bd..9c0d1ec005 100644
--- a/web/src/lib/components/shared-components/notification/__tests__/notification-list.spec.ts
+++ b/web/src/lib/components/shared-components/notification/__tests__/notification-list.spec.ts
@@ -1,3 +1,4 @@
+import { getAnimateMock } from '$lib/__mocks__/animate.mock';
 import '@testing-library/jest-dom';
 import { render, waitFor, type RenderResult } from '@testing-library/svelte';
 import { get } from 'svelte/store';
@@ -10,10 +11,7 @@ function _getNotificationListElement(sut: RenderResult<NotificationList>): HTMLA
 
 describe('NotificationList component', () => {
   beforeAll(() => {
-    // https://testing-library.com/docs/svelte-testing-library/faq#why-arent-transition-events-running
-    vi.stubGlobal('requestAnimationFrame', (fn: FrameRequestCallback) => {
-      setTimeout(() => fn(Date.now()), 16);
-    });
+    Element.prototype.animate = getAnimateMock();
   });
 
   afterAll(() => {
@@ -21,7 +19,7 @@ describe('NotificationList component', () => {
   });
 
   it('shows a notification when added and closes it automatically after the delay timeout', async () => {
-    const sut: RenderResult<NotificationList> = render(NotificationList);
+    const sut: RenderResult<NotificationList> = render(NotificationList, { intro: false });
     const status = await sut.findAllByRole('status');
 
     expect(status).toHaveLength(1);
diff --git a/web/src/lib/components/shared-components/notification/notification-card.svelte b/web/src/lib/components/shared-components/notification/notification-card.svelte
index 61e710a170..5054c18695 100644
--- a/web/src/lib/components/shared-components/notification/notification-card.svelte
+++ b/web/src/lib/components/shared-components/notification/notification-card.svelte
@@ -13,10 +13,14 @@
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import { t } from 'svelte-i18n';
 
-  export let notification: Notification | ComponentNotification;
+  interface Props {
+    notification: Notification | ComponentNotification;
+  }
 
-  $: icon = notification.type === NotificationType.Error ? mdiCloseCircleOutline : mdiInformationOutline;
-  $: hoverStyle = notification.action.type === 'discard' ? 'hover:cursor-pointer' : '';
+  let { notification }: Props = $props();
+
+  let icon = $derived(notification.type === NotificationType.Error ? mdiCloseCircleOutline : mdiInformationOutline);
+  let hoverStyle = $derived(notification.action.type === 'discard' ? 'hover:cursor-pointer' : '');
 
   const backgroundColor: Record<NotificationType, string> = {
     [NotificationType.Info]: '#E0E2F0',
@@ -66,14 +70,14 @@
   };
 </script>
 
-<!-- svelte-ignore a11y-no-static-element-interactions -->
+<!-- svelte-ignore a11y_no_static_element_interactions -->
 <div
   transition:fade={{ duration: 250 }}
   style:background-color={backgroundColor[notification.type]}
   style:border-color={borderColor[notification.type]}
   class="border z-[999999] mb-4 min-h-[80px] w-[300px] rounded-2xl p-4 shadow-md {hoverStyle}"
-  on:click={handleClick}
-  on:keydown={handleClick}
+  onclick={handleClick}
+  onkeydown={handleClick}
 >
   <div class="flex justify-between">
     <div class="flex place-items-center gap-2">
@@ -90,15 +94,15 @@
       class="dark:text-immich-dark-gray"
       size="20"
       padding="2"
-      on:click={discard}
-      aria-hidden="true"
+      onclick={discard}
+      aria-hidden={true}
       tabindex={-1}
     />
   </div>
 
   <p class="whitespace-pre-wrap pl-[28px] pr-[16px] text-sm" data-testid="message">
     {#if isComponentNotification(notification)}
-      <svelte:component this={notification.component.type} {...notification.component.props} />
+      <notification.component.type {...notification.component.props} />
     {:else}
       {notification.message}
     {/if}
@@ -109,7 +113,7 @@
       <button
         type="button"
         class="{buttonStyle[notification.type]} rounded px-3 pt-1.5 pb-1 transition-all duration-200"
-        on:click={handleButtonClick}
+        onclick={handleButtonClick}
         aria-hidden="true"
         tabindex={-1}
       >
diff --git a/web/src/lib/components/shared-components/notification/notification.ts b/web/src/lib/components/shared-components/notification/notification.ts
index 9cafcd9eaf..fd54768c04 100644
--- a/web/src/lib/components/shared-components/notification/notification.ts
+++ b/web/src/lib/components/shared-components/notification/notification.ts
@@ -1,4 +1,4 @@
-import type { ComponentProps, ComponentType, SvelteComponent } from 'svelte';
+import type { Component as ComponentType } from 'svelte';
 import { writable } from 'svelte/store';
 
 export enum NotificationType {
@@ -28,27 +28,26 @@ type NoopAction = { type: 'noop' };
 
 export type NotificationAction = DiscardAction | NoopAction;
 
-type Component<T extends ComponentType> = {
-  type: T;
-  props: ComponentProps<InstanceType<T>>;
+type Props = Record<string, unknown>;
+type Component<T extends Props> = {
+  type: ComponentType<T>;
+  props: T;
 };
 
 type BaseNotificationOptions<T, R extends keyof T> = Partial<Omit<T, 'id'>> & Pick<T, R>;
 
 export type NotificationOptions = BaseNotificationOptions<Notification, 'message'>;
-export type ComponentNotificationOptions<T extends ComponentType> = BaseNotificationOptions<
+export type ComponentNotificationOptions<T extends Props> = BaseNotificationOptions<
   ComponentNotification<T>,
   'component'
 >;
 
-export type ComponentNotification<T extends ComponentType = ComponentType<SvelteComponent>> = Omit<
-  Notification,
-  'message'
-> & {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export type ComponentNotification<T extends Props = any> = Omit<Notification, 'message'> & {
   component: Component<T>;
 };
 
-export const isComponentNotification = <T extends ComponentType>(
+export const isComponentNotification = <T extends Props>(
   notification: Notification | ComponentNotification<T>,
 ): notification is ComponentNotification<T> => {
   return 'component' in notification;
@@ -58,7 +57,7 @@ function createNotificationList() {
   const notificationList = writable<(Notification | ComponentNotification)[]>([]);
   let count = 1;
 
-  const show = <T>(options: T extends ComponentType ? ComponentNotificationOptions<T> : NotificationOptions) => {
+  const show = <T>(options: T extends Props ? ComponentNotificationOptions<T> : NotificationOptions) => {
     notificationList.update((currentList) => {
       currentList.push({
         id: count++,
diff --git a/web/src/lib/components/shared-components/number-range-input.svelte b/web/src/lib/components/shared-components/number-range-input.svelte
index 2e7dca8781..6ee993cf88 100644
--- a/web/src/lib/components/shared-components/number-range-input.svelte
+++ b/web/src/lib/components/shared-components/number-range-input.svelte
@@ -2,14 +2,38 @@
   import { clamp } from 'lodash-es';
   import type { ClipboardEventHandler } from 'svelte/elements';
 
-  export let id: string;
-  export let min: number;
-  export let max: number;
-  export let step: number | string = 'any';
-  export let required = true;
-  export let value: number | null = null;
-  export let onInput: (value: number | null) => void;
-  export let onPaste: ClipboardEventHandler<HTMLInputElement> | undefined = undefined;
+  interface Props {
+    id: string;
+    min: number;
+    max: number;
+    step?: number | string;
+    required?: boolean;
+    value?: number;
+    onInput: (value: number | null) => void;
+    onPaste?: ClipboardEventHandler<HTMLInputElement>;
+  }
+
+  let {
+    id,
+    min,
+    max,
+    step = 'any',
+    required = true,
+    value = $bindable(),
+    onInput,
+    onPaste = undefined,
+  }: Props = $props();
+
+  const oninput = () => {
+    if (!value) {
+      return;
+    }
+
+    if (value !== null && (value < min || value > max)) {
+      value = clamp(value, min, max);
+    }
+    onInput(value);
+  };
 </script>
 
 <input
@@ -21,11 +45,6 @@
   {step}
   {required}
   bind:value
-  on:input={() => {
-    if (value !== null && (value < min || value > max)) {
-      value = clamp(value, min, max);
-    }
-    onInput(value);
-  }}
-  on:paste={onPaste}
+  {oninput}
+  onpaste={onPaste}
 />
diff --git a/web/src/lib/components/shared-components/password-field.svelte b/web/src/lib/components/shared-components/password-field.svelte
index d69ea98845..8519f84134 100644
--- a/web/src/lib/components/shared-components/password-field.svelte
+++ b/web/src/lib/components/shared-components/password-field.svelte
@@ -4,28 +4,26 @@
   import Icon from '../elements/icon.svelte';
   import { t } from 'svelte-i18n';
 
-  interface $$Props extends HTMLInputAttributes {
+  interface Props extends HTMLInputAttributes {
     password: string;
-    autocomplete: string;
+    autocomplete: AutoFill;
     required?: boolean;
     onInput?: (value: string) => void;
   }
 
-  export let password: $$Props['password'];
-  export let required = true;
-  export let onInput: $$Props['onInput'] = undefined;
+  let { password = $bindable(), required = true, onInput = undefined, ...rest }: Props = $props();
 
-  let showPassword = false;
+  let showPassword = $state(false);
 </script>
 
 <div class="relative w-full">
   <input
-    {...$$restProps}
+    {...rest}
     class="immich-form-input w-full !pr-12"
     type={showPassword ? 'text' : 'password'}
     {required}
     value={password}
-    on:input={(e) => {
+    oninput={(e) => {
       password = e.currentTarget.value;
       onInput?.(password);
     }}
@@ -36,7 +34,7 @@
       type="button"
       tabindex="-1"
       class="absolute inset-y-0 end-0 px-4 text-gray-700 dark:text-gray-200"
-      on:click={() => (showPassword = !showPassword)}
+      onclick={() => (showPassword = !showPassword)}
       title={showPassword ? $t('hide_password') : $t('show_password')}
     >
       <Icon path={showPassword ? mdiEyeOffOutline : mdiEyeOutline} size="1.25em" />
diff --git a/web/src/lib/components/shared-components/portal/portal.svelte b/web/src/lib/components/shared-components/portal/portal.svelte
index 7a9e577083..60ccc993af 100644
--- a/web/src/lib/components/shared-components/portal/portal.svelte
+++ b/web/src/lib/components/shared-components/portal/portal.svelte
@@ -1,6 +1,6 @@
-<script context="module" lang="ts">
+<script module lang="ts">
   import { handlePromiseError } from '$lib/utils';
-  import { tick } from 'svelte';
+  import { tick, type Snippet } from 'svelte';
 
   /**
    * Usage: <div use:portal={'css selector'}> or <div use:portal={document.body}>
@@ -64,12 +64,17 @@ Used for every occurrence of an HTML tag in a message
 ```
 -->
 <script lang="ts">
-  /**
-   * DOM Element or CSS Selector
-   */
-  export let target: HTMLElement | string = 'body';
+  interface Props {
+    /**
+     * DOM Element or CSS Selector
+     */
+    target?: HTMLElement | string;
+    children?: Snippet;
+  }
+
+  let { target = 'body', children }: Props = $props();
 </script>
 
 <div use:portal={target} hidden>
-  <slot />
+  {@render children?.()}
 </div>
diff --git a/web/src/lib/components/shared-components/profile-image-cropper.svelte b/web/src/lib/components/shared-components/profile-image-cropper.svelte
index 0c3f79895e..b8ac866761 100644
--- a/web/src/lib/components/shared-components/profile-image-cropper.svelte
+++ b/web/src/lib/components/shared-components/profile-image-cropper.svelte
@@ -10,12 +10,20 @@
   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
   import { t } from 'svelte-i18n';
 
-  export let asset: AssetResponseDto;
-  export let onClose: () => void;
+  interface Props {
+    asset: AssetResponseDto;
+    onClose: () => void;
+  }
 
-  let imgElement: HTMLDivElement;
+  let { asset, onClose }: Props = $props();
+
+  let imgElement: HTMLDivElement | undefined = $state();
 
   onMount(() => {
+    if (!imgElement) {
+      return;
+    }
+
     imgElement.style.width = '100%';
   });
 
@@ -45,6 +53,10 @@
   };
 
   const handleSetProfilePicture = async () => {
+    if (!imgElement) {
+      return;
+    }
+
     try {
       const blob = await domtoimage.toBlob(imgElement);
       if (await hasTransparentPixels(blob)) {
@@ -56,13 +68,14 @@
         return;
       }
       const file = new File([blob], 'profile-picture.png', { type: 'image/png' });
-      const { profileImagePath } = await createProfileImage({ createProfileImageDto: { file } });
+      const { profileImagePath, profileChangedAt } = await createProfileImage({ createProfileImageDto: { file } });
       notificationController.show({
         type: NotificationType.Info,
         message: $t('profile_picture_set'),
         timeout: 3000,
       });
       $user.profileImagePath = profileImagePath;
+      $user.profileChangedAt = profileChangedAt;
     } catch (error) {
       handleError(error, $t('errors.unable_to_set_profile_picture'));
     }
@@ -78,7 +91,8 @@
       <PhotoViewer bind:element={imgElement} {asset} />
     </div>
   </div>
-  <svelte:fragment slot="sticky-bottom">
-    <Button fullwidth on:click={handleSetProfilePicture}>{$t('set_as_profile_picture')}</Button>
-  </svelte:fragment>
+
+  {#snippet stickyBottom()}
+    <Button fullwidth onclick={handleSetProfilePicture}>{$t('set_as_profile_picture')}</Button>
+  {/snippet}
 </FullScreenModal>
diff --git a/web/src/lib/components/shared-components/progress-bar/progress-bar.svelte b/web/src/lib/components/shared-components/progress-bar/progress-bar.svelte
index 817cccac38..5b663bd1a9 100644
--- a/web/src/lib/components/shared-components/progress-bar/progress-bar.svelte
+++ b/web/src/lib/components/shared-components/progress-bar/progress-bar.svelte
@@ -1,53 +1,53 @@
-<script context="module" lang="ts">
-  export enum ProgressBarStatus {
-    Playing = 'playing',
-    Paused = 'paused',
-  }
-</script>
-
 <script lang="ts">
+  import { ProgressBarStatus } from '$lib/constants';
   import { handlePromiseError } from '$lib/utils';
 
-  import { createEventDispatcher, onMount } from 'svelte';
+  import { onMount } from 'svelte';
   import { tweened } from 'svelte/motion';
 
-  /**
-   * Autoplay on mount
-   * @default false
-   */
-  export let autoplay = false;
+  interface Props {
+    /**
+     * Autoplay on mount
+     * @default false
+     */
+    autoplay?: boolean;
+    /**
+     * Progress bar status
+     */
+    status?: ProgressBarStatus;
+    hidden?: boolean;
+    duration?: number;
+    onDone: () => void;
+    onPlaying?: () => void;
+    onPaused?: () => void;
+  }
 
-  /**
-   * Progress bar status
-   */
-  export let status: ProgressBarStatus = ProgressBarStatus.Paused;
+  let {
+    autoplay = false,
+    status = $bindable(),
+    hidden = false,
+    duration = 5,
+    onDone,
+    onPlaying = () => {},
+    onPaused = () => {},
+  }: Props = $props();
 
-  export let hidden = false;
-
-  export let duration = 5;
-
-  const onChange = async () => {
-    progress = setDuration(duration);
+  const onChange = async (progressDuration: number) => {
+    progress = setDuration(progressDuration);
     await play();
   };
 
   let progress = setDuration(duration);
 
-  // svelte 5, again....
-  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
-  $: duration, handlePromiseError(onChange());
+  $effect(() => {
+    handlePromiseError(onChange(duration));
+  });
 
-  $: {
+  $effect(() => {
     if ($progress === 1) {
-      dispatch('done');
+      onDone();
     }
-  }
-
-  const dispatch = createEventDispatcher<{
-    done: void;
-    playing: void;
-    paused: void;
-  }>();
+  });
 
   onMount(async () => {
     if (autoplay) {
@@ -57,13 +57,13 @@
 
   export const play = async () => {
     status = ProgressBarStatus.Playing;
-    dispatch('playing');
+    onPlaying();
     await progress.set(1);
   };
 
   export const pause = async () => {
     status = ProgressBarStatus.Paused;
-    dispatch('paused');
+    onPaused();
     await progress.set($progress);
   };
 
@@ -88,5 +88,5 @@
 </script>
 
 {#if !hidden}
-  <span class="absolute left-0 h-[3px] bg-immich-primary shadow-2xl" style:width={`${$progress * 100}%`} />
+  <span class="absolute left-0 h-[3px] bg-immich-primary shadow-2xl" style:width={`${$progress * 100}%`}></span>
 {/if}
diff --git a/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte b/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte
index 2b8c678543..00800ab489 100644
--- a/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte
+++ b/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte
@@ -7,7 +7,11 @@
   import { preferences } from '$lib/stores/user.store';
   import { setSupportBadgeVisibility } from '$lib/utils/purchase-utils';
 
-  export let onDone: () => void;
+  interface Props {
+    onDone: () => void;
+  }
+
+  let { onDone }: Props = $props();
 </script>
 
 <div class="m-auto w-3/4 text-center flex flex-col place-content-center place-items-center dark:text-white my-6">
@@ -20,11 +24,11 @@
       title={$t('show_supporter_badge')}
       subtitle={$t('show_supporter_badge_description')}
       bind:checked={$preferences.purchase.showSupportBadge}
-      on:toggle={({ detail }) => setSupportBadgeVisibility(detail)}
+      onToggle={setSupportBadgeVisibility}
     />
   </div>
 
   <div class="mt-6 w-full">
-    <Button fullwidth on:click={onDone}>{$t('ok')}</Button>
+    <Button fullwidth onclick={onDone}>{$t('ok')}</Button>
   </div>
 </div>
diff --git a/web/src/lib/components/shared-components/purchasing/purchase-content.svelte b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte
index 8a01834409..6a4e7f1a4b 100644
--- a/web/src/lib/components/shared-components/purchasing/purchase-content.svelte
+++ b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte
@@ -8,12 +8,15 @@
   import { purchaseStore } from '$lib/stores/purchase.store';
   import { t } from 'svelte-i18n';
 
-  export let onActivate: () => void;
+  interface Props {
+    onActivate: () => void;
+    showTitle?: boolean;
+    showMessage?: boolean;
+  }
 
-  export let showTitle = true;
-  export let showMessage = true;
-  let productKey = '';
-  let isLoading = false;
+  let { onActivate, showTitle = true, showMessage = true }: Props = $props();
+  let productKey = $state('');
+  let isLoading = $state(false);
 
   const activate = async () => {
     try {
@@ -50,7 +53,7 @@
         <p>
           {$t('purchase_panel_info_2')}
         </p>
-        <div />
+        <div></div>
       </div>
     {/if}
 
@@ -61,7 +64,7 @@
 
     <div class="mt-6">
       <p class="dark:text-immich-gray">{$t('purchase_input_suggestion')}</p>
-      <form class="mt-2 flex gap-2" on:submit={activate}>
+      <form class="mt-2 flex gap-2" onsubmit={activate}>
         <input
           class="immich-form-input w-full"
           id="purchaseKey"
diff --git a/web/src/lib/components/shared-components/purchasing/purchase-modal.svelte b/web/src/lib/components/shared-components/purchasing/purchase-modal.svelte
index 52757bc32a..0334fb9e99 100644
--- a/web/src/lib/components/shared-components/purchasing/purchase-modal.svelte
+++ b/web/src/lib/components/shared-components/purchasing/purchase-modal.svelte
@@ -5,9 +5,13 @@
 
   import Portal from '$lib/components/shared-components/portal/portal.svelte';
 
-  export let onClose: () => void;
+  interface Props {
+    onClose: () => void;
+  }
 
-  let showProductActivated = false;
+  let { onClose }: Props = $props();
+
+  let showProductActivated = $state(false);
 </script>
 
 <Portal>
diff --git a/web/src/lib/components/shared-components/scrubber/scrubber.svelte b/web/src/lib/components/shared-components/scrubber/scrubber.svelte
index e2cc638650..bdcca509bb 100644
--- a/web/src/lib/components/shared-components/scrubber/scrubber.svelte
+++ b/web/src/lib/components/shared-components/scrubber/scrubber.svelte
@@ -1,32 +1,51 @@
 <script lang="ts">
   import type { AssetStore, AssetBucket, BucketListener } from '$lib/stores/assets.store';
-  import type { DateTime } from 'luxon';
+  import { DateTime } from 'luxon';
   import { fromLocalDateTime, type ScrubberListener } from '$lib/utils/timeline-util';
   import { clamp } from 'lodash-es';
   import { onMount } from 'svelte';
+  import { isTimelineScrolling } from '$lib/stores/timeline.store';
+  import { fade, fly } from 'svelte/transition';
 
-  export let timelineTopOffset = 0;
-  export let timelineBottomOffset = 0;
-  export let height = 0;
-  export let assetStore: AssetStore;
-  export let invisible = false;
-  export let scrubOverallPercent: number = 0;
-  export let scrubBucketPercent: number = 0;
-  export let scrubBucket: { bucketDate: string | undefined } | undefined = undefined;
-  export let leadout: boolean = false;
-  export let onScrub: ScrubberListener | undefined = undefined;
-  export let startScrub: ScrubberListener | undefined = undefined;
-  export let stopScrub: ScrubberListener | undefined = undefined;
+  interface Props {
+    timelineTopOffset?: number;
+    timelineBottomOffset?: number;
+    height?: number;
+    assetStore: AssetStore;
+    invisible?: boolean;
+    scrubOverallPercent?: number;
+    scrubBucketPercent?: number;
+    scrubBucket?: { bucketDate: string | undefined } | undefined;
+    leadout?: boolean;
+    onScrub?: ScrubberListener | undefined;
+    startScrub?: ScrubberListener | undefined;
+    stopScrub?: ScrubberListener | undefined;
+  }
 
-  let isHover = false;
-  let isDragging = false;
-  let hoverLabel: string | undefined;
+  let {
+    timelineTopOffset = 0,
+    timelineBottomOffset = 0,
+    height = 0,
+    assetStore,
+    invisible = false,
+    scrubOverallPercent = 0,
+    scrubBucketPercent = 0,
+    scrubBucket = undefined,
+    leadout = false,
+    onScrub = undefined,
+    startScrub = undefined,
+    stopScrub = undefined,
+  }: Props = $props();
+
+  let isHover = $state(false);
+  let isDragging = $state(false);
+  let hoverLabel: string | undefined = $state();
   let bucketDate: string | undefined;
-  let hoverY = 0;
+  let hoverY = $state(0);
   let clientY = 0;
-  let windowHeight = 0;
-  let scrollBar: HTMLElement | undefined;
-  let segments: Segment[] = [];
+  let windowHeight = $state(0);
+  let scrollBar: HTMLElement | undefined = $state();
+  let segments: Segment[] = $state([]);
 
   const toScrollY = (percent: number) => percent * (height - HOVER_DATE_HEIGHT * 2);
   const toTimelineY = (scrollY: number) => scrollY / (height - HOVER_DATE_HEIGHT * 2);
@@ -68,10 +87,14 @@
       return scrubOverallPercent * (height - HOVER_DATE_HEIGHT * 2) - 2;
     }
   };
-  $: scrollY = toScrollFromBucketPercentage(scrubBucket, scrubBucketPercent, scrubOverallPercent);
-  $: timelineFullHeight = $assetStore.timelineHeight + timelineTopOffset + timelineBottomOffset;
-  $: relativeTopOffset = toScrollY(timelineTopOffset / timelineFullHeight);
-  $: relativeBottomOffset = toScrollY(timelineBottomOffset / timelineFullHeight);
+  let scrollY = $state(0);
+  $effect(() => {
+    scrollY = toScrollFromBucketPercentage(scrubBucket, scrubBucketPercent, scrubOverallPercent);
+  });
+
+  let timelineFullHeight = $derived($assetStore.timelineHeight + timelineTopOffset + timelineBottomOffset);
+  let relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
+  let relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight));
 
   const listener: BucketListener = (event) => {
     const { type } = event;
@@ -202,16 +225,17 @@
 
 <svelte:window
   bind:innerHeight={windowHeight}
-  on:mousemove={({ clientY }) => (isDragging || isHover) && handleMouseEvent({ clientY })}
-  on:mousedown={({ clientY }) => isHover && handleMouseEvent({ clientY, isDragging: true })}
-  on:mouseup={({ clientY }) => handleMouseEvent({ clientY, isDragging: false })}
+  onmousemove={({ clientY }) => (isDragging || isHover) && handleMouseEvent({ clientY })}
+  onmousedown={({ clientY }) => isHover && handleMouseEvent({ clientY, isDragging: true })}
+  onmouseup={({ clientY }) => handleMouseEvent({ clientY, isDragging: false })}
 />
 
-<!-- svelte-ignore a11y-no-static-element-interactions -->
+<!-- svelte-ignore a11y_no_static_element_interactions -->
 
 <div
+  transition:fly={{ x: 50, duration: 250 }}
   id="immich-scrubbable-scrollbar"
-  class={`absolute right-0 z-[1] select-none bg-immich-bg hover:cursor-row-resize`}
+  class="absolute right-0 z-[1] select-none bg-immich-bg hover:cursor-row-resize"
   style:padding-top={HOVER_DATE_HEIGHT + 'px'}
   style:padding-bottom={HOVER_DATE_HEIGHT + 'px'}
   class:invisible
@@ -220,8 +244,8 @@
   style:background-color={isDragging ? 'transparent' : 'transparent'}
   draggable="false"
   bind:this={scrollBar}
-  on:mouseenter={() => (isHover = true)}
-  on:mouseleave={() => (isHover = false)}
+  onmouseenter={() => (isHover = true)}
+  onmouseleave={() => (isHover = false)}
 >
   {#if hoverLabel && (isHover || isDragging)}
     <div
@@ -237,11 +261,20 @@
     <div
       class="absolute right-0 h-[2px] w-10 bg-immich-primary dark:bg-immich-dark-primary"
       style:top="{scrollY + HOVER_DATE_HEIGHT}px"
-    />
+    >
+      {#if $isTimelineScrolling && scrubBucket?.bucketDate}
+        <p
+          transition:fade={{ duration: 200 }}
+          class="truncate pointer-events-none absolute right-0 bottom-0 z-[100] min-w-20 max-w-64 w-fit rounded-tl-md border-b-2 border-immich-primary bg-immich-bg/80 py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:bg-immich-dark-gray/80 dark:text-immich-dark-fg"
+        >
+          {assetStore.getBucketByDate(scrubBucket.bucketDate)?.bucketDateFormattted}
+        </p>
+      {/if}
+    </div>
   {/if}
   <div id="lead-in" class="relative" style:height={relativeTopOffset + 'px'} data-label={segments.at(0)?.dateFormatted}>
     {#if relativeTopOffset > 6}
-      <div class="absolute right-[0.75rem] h-[4px] w-[4px] rounded-full bg-gray-300" />
+      <div class="absolute right-[0.75rem] h-[4px] w-[4px] rounded-full bg-gray-300"></div>
     {/if}
   </div>
   <!-- Time Segment -->
@@ -266,7 +299,7 @@
         <div
           aria-label={segment.dateFormatted + ' ' + segment.count}
           class="absolute right-[0.75rem] bottom-0 h-[4px] w-[4px] rounded-full bg-gray-300"
-        />
+        ></div>
       {/if}
     </div>
   {/each}
diff --git a/web/src/lib/components/shared-components/search-bar/search-bar.svelte b/web/src/lib/components/shared-components/search-bar/search-bar.svelte
index b0bbdbe71f..d92bd1806c 100644
--- a/web/src/lib/components/shared-components/search-bar/search-bar.svelte
+++ b/web/src/lib/components/shared-components/search-bar/search-bar.svelte
@@ -15,35 +15,38 @@
   import { generateId } from '$lib/utils/generate-id';
   import { tick } from 'svelte';
 
-  export let value = '';
-  export let grayTheme: boolean;
-  export let searchQuery: MetadataSearchDto | SmartSearchDto = {};
+  interface Props {
+    value?: string;
+    grayTheme: boolean;
+    searchQuery?: MetadataSearchDto | SmartSearchDto;
+    onSearch?: () => void;
+  }
 
-  $: showClearIcon = value.length > 0;
+  let { value = $bindable(''), grayTheme, searchQuery = {}, onSearch }: Props = $props();
 
-  let input: HTMLInputElement;
+  let showClearIcon = $derived(value.length > 0);
 
-  let showSuggestions = false;
-  let showFilter = false;
-  let isSearchSuggestions = false;
-  let selectedId: string | undefined;
-  let moveSelection: (direction: 1 | -1) => void;
-  let clearSelection: () => void;
-  let selectActiveOption: () => void;
+  let input = $state<HTMLInputElement>();
+  let searchHistoryBox = $state<ReturnType<typeof SearchHistoryBox>>();
+  let showSuggestions = $state(false);
+  let showFilter = $state(false);
+  let isSearchSuggestions = $state(false);
+  let selectedId: string | undefined = $state();
 
   const listboxId = generateId();
 
-  const onSearch = async (payload: SmartSearchDto | MetadataSearchDto) => {
+  const handleSearch = async (payload: SmartSearchDto | MetadataSearchDto) => {
     const params = getMetadataSearchQuery(payload);
 
     closeDropdown();
     showFilter = false;
     $isSearchEnabled = false;
     await goto(`${AppRoute.SEARCH}?${params}`);
+    onSearch?.();
   };
 
   const clearSearchTerm = (searchTerm: string) => {
-    input.focus();
+    input?.focus();
     $savedSearchTerms = $savedSearchTerms.filter((item) => item !== searchTerm);
   };
 
@@ -57,7 +60,7 @@
   };
 
   const clearAllSearchTerms = () => {
-    input.focus();
+    input?.focus();
     $savedSearchTerms = [];
   };
 
@@ -66,19 +69,23 @@
   };
 
   const onFocusOut = () => {
-    if ($isSearchEnabled) {
-      $preventRaceConditionSearchBar = true;
-    }
+    const focusOutTimer = setTimeout(() => {
+      if ($isSearchEnabled) {
+        $preventRaceConditionSearchBar = true;
+      }
 
-    closeDropdown();
-    $isSearchEnabled = false;
-    showFilter = false;
+      closeDropdown();
+      $isSearchEnabled = false;
+      showFilter = false;
+    }, 100);
+
+    clearTimeout(focusOutTimer);
   };
 
   const onHistoryTermClick = async (searchTerm: string) => {
     value = searchTerm;
     const searchPayload = { query: searchTerm };
-    await onSearch(searchPayload);
+    await handleSearch(searchPayload);
   };
 
   const onFilterClick = () => {
@@ -91,13 +98,13 @@
   };
 
   const onSubmit = () => {
-    handlePromiseError(onSearch({ query: value }));
+    handlePromiseError(handleSearch({ query: value }));
     saveSearchTerm(value);
   };
 
   const onClear = () => {
     value = '';
-    input.focus();
+    input?.focus();
   };
 
   const onEscape = () => {
@@ -108,19 +115,19 @@
   const onArrow = async (direction: 1 | -1) => {
     openDropdown();
     await tick();
-    moveSelection(direction);
+    searchHistoryBox?.moveSelection(direction);
   };
 
   const onEnter = (event: KeyboardEvent) => {
     if (selectedId) {
       event.preventDefault();
-      selectActiveOption();
+      searchHistoryBox?.selectActiveOption();
     }
   };
 
   const onInput = () => {
     openDropdown();
-    clearSelection();
+    searchHistoryBox?.clearSelection();
   };
 
   const openDropdown = () => {
@@ -129,14 +136,19 @@
 
   const closeDropdown = () => {
     showSuggestions = false;
-    clearSelection();
+    searchHistoryBox?.clearSelection();
+  };
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+    onSubmit();
   };
 </script>
 
 <svelte:window
   use:shortcuts={[
     { shortcut: { key: 'Escape' }, onShortcut: onEscape },
-    { shortcut: { ctrl: true, key: 'k' }, onShortcut: () => input.select() },
+    { shortcut: { ctrl: true, key: 'k' }, onShortcut: () => input?.select() },
     { shortcut: { ctrl: true, shift: true, key: 'k' }, onShortcut: onFilterClick },
   ]}
 />
@@ -147,9 +159,9 @@
     autocomplete="off"
     class="select-text text-sm"
     action={AppRoute.SEARCH}
-    on:reset={() => (value = '')}
-    on:submit|preventDefault={onSubmit}
-    on:focusin={onFocusIn}
+    onreset={() => (value = '')}
+    {onsubmit}
+    onfocusin={onFocusIn}
     role="search"
   >
     <div use:focusOutside={{ onFocusOut: closeDropdown }} tabindex="-1">
@@ -167,8 +179,8 @@
         pattern="^(?!m:$).*$"
         bind:value
         bind:this={input}
-        on:focus={openDropdown}
-        on:input={onInput}
+        onfocus={openDropdown}
+        oninput={onInput}
         disabled={showFilter}
         role="combobox"
         aria-controls={listboxId}
@@ -187,13 +199,11 @@
 
       <!-- SEARCH HISTORY BOX -->
       <SearchHistoryBox
+        bind:this={searchHistoryBox}
+        bind:isSearchSuggestions
         id={listboxId}
         searchQuery={value}
         isOpen={showSuggestions}
-        bind:isSearchSuggestions
-        bind:moveSelection
-        bind:clearSelection
-        bind:selectActiveOption
         onClearAllSearchTerms={clearAllSearchTerms}
         onClearSearchTerm={(searchTerm) => clearSearchTerm(searchTerm)}
         onSelectSearchTerm={(searchTerm) => handlePromiseError(onHistoryTermClick(searchTerm))}
@@ -202,19 +212,30 @@
     </div>
 
     <div class="absolute inset-y-0 {showClearIcon ? 'right-14' : 'right-2'} flex items-center pl-6 transition-all">
-      <CircleIconButton title={$t('show_search_options')} icon={mdiTune} on:click={onFilterClick} size="20" />
+      <CircleIconButton title={$t('show_search_options')} icon={mdiTune} onclick={onFilterClick} size="20" />
     </div>
     {#if showClearIcon}
       <div class="absolute inset-y-0 right-0 flex items-center pr-2">
-        <CircleIconButton on:click={onClear} icon={mdiClose} title={$t('clear')} size="20" />
+        <CircleIconButton onclick={onClear} icon={mdiClose} title={$t('clear')} size="20" />
       </div>
     {/if}
     <div class="absolute inset-y-0 left-0 flex items-center pl-2">
-      <CircleIconButton type="submit" disabled={showFilter} title={$t('search')} icon={mdiMagnify} size="20" />
+      <CircleIconButton
+        type="submit"
+        disabled={showFilter}
+        title={$t('search')}
+        icon={mdiMagnify}
+        size="20"
+        onclick={() => {}}
+      />
     </div>
   </form>
 
   {#if showFilter}
-    <SearchFilterModal {searchQuery} onSearch={(payload) => onSearch(payload)} onClose={() => (showFilter = false)} />
+    <SearchFilterModal
+      {searchQuery}
+      onSearch={(payload) => handleSearch(payload)}
+      onClose={() => (showFilter = false)}
+    />
   {/if}
 </div>
diff --git a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte
index f1cd0c8596..08ed57d70e 100644
--- a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte
+++ b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte
@@ -1,4 +1,4 @@
-<script lang="ts" context="module">
+<script lang="ts" module>
   export interface SearchCameraFilter {
     make?: string;
     model?: string;
@@ -6,20 +6,21 @@
 </script>
 
 <script lang="ts">
+  import { run } from 'svelte/legacy';
+
   import Combobox, { asComboboxOptions, asSelectedOption } from '$lib/components/shared-components/combobox.svelte';
   import { handlePromiseError } from '$lib/utils';
   import { SearchSuggestionType, getSearchSuggestions } from '@immich/sdk';
   import { t } from 'svelte-i18n';
 
-  export let filters: SearchCameraFilter;
+  interface Props {
+    filters: SearchCameraFilter;
+  }
 
-  let makes: string[] = [];
-  let models: string[] = [];
+  let { filters = $bindable() }: Props = $props();
 
-  $: makeFilter = filters.make;
-  $: modelFilter = filters.model;
-  $: handlePromiseError(updateMakes());
-  $: handlePromiseError(updateModels(makeFilter));
+  let makes: string[] = $state([]);
+  let models: string[] = $state([]);
 
   async function updateMakes() {
     const results: Array<string | null> = await getSearchSuggestions({
@@ -47,6 +48,14 @@
       filters.model = undefined;
     }
   }
+  let makeFilter = $derived(filters.make);
+  let modelFilter = $derived(filters.model);
+  run(() => {
+    handlePromiseError(updateMakes());
+  });
+  run(() => {
+    handlePromiseError(updateModels(makeFilter));
+  });
 </script>
 
 <div id="camera-selection">
@@ -56,7 +65,7 @@
     <div class="w-full">
       <Combobox
         label={$t('make')}
-        on:select={({ detail }) => (filters.make = detail?.value)}
+        onSelect={(option) => (filters.make = option?.value)}
         options={asComboboxOptions(makes)}
         placeholder={$t('search_camera_make')}
         selectedOption={asSelectedOption(makeFilter)}
@@ -66,7 +75,7 @@
     <div class="w-full">
       <Combobox
         label={$t('model')}
-        on:select={({ detail }) => (filters.model = detail?.value)}
+        onSelect={(option) => (filters.model = option?.value)}
         options={asComboboxOptions(models)}
         placeholder={$t('search_camera_model')}
         selectedOption={asSelectedOption(modelFilter)}
diff --git a/web/src/lib/components/shared-components/search-bar/search-date-section.svelte b/web/src/lib/components/shared-components/search-bar/search-date-section.svelte
index 6b661b6c03..ea27142074 100644
--- a/web/src/lib/components/shared-components/search-bar/search-date-section.svelte
+++ b/web/src/lib/components/shared-components/search-bar/search-date-section.svelte
@@ -1,4 +1,4 @@
-<script lang="ts" context="module">
+<script lang="ts" module>
   export interface SearchDateFilter {
     takenBefore?: string;
     takenAfter?: string;
@@ -9,7 +9,11 @@
   import DateInput from '$lib/components/elements/date-input.svelte';
   import { t } from 'svelte-i18n';
 
-  export let filters: SearchDateFilter;
+  interface Props {
+    filters: SearchDateFilter;
+  }
+
+  let { filters = $bindable() }: Props = $props();
 </script>
 
 <div id="date-range-selection" class="grid grid-auto-fit-40 gap-5">
diff --git a/web/src/lib/components/shared-components/search-bar/search-display-section.svelte b/web/src/lib/components/shared-components/search-bar/search-display-section.svelte
index 00a5403068..06fa3c5bdf 100644
--- a/web/src/lib/components/shared-components/search-bar/search-display-section.svelte
+++ b/web/src/lib/components/shared-components/search-bar/search-display-section.svelte
@@ -1,4 +1,4 @@
-<script lang="ts" context="module">
+<script lang="ts" module>
   export interface SearchDisplayFilters {
     isNotInAlbum?: boolean;
     isArchive?: boolean;
@@ -10,7 +10,11 @@
   import Checkbox from '$lib/components/elements/checkbox.svelte';
   import { t } from 'svelte-i18n';
 
-  export let filters: SearchDisplayFilters;
+  interface Props {
+    filters: SearchDisplayFilters;
+  }
+
+  let { filters = $bindable() }: Props = $props();
 </script>
 
 <div id="display-options-selection">
diff --git a/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte b/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte
index 3ec539ad97..de34092658 100644
--- a/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte
+++ b/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte
@@ -1,18 +1,13 @@
-<script lang="ts" context="module">
+<script lang="ts" module>
   import type { SearchLocationFilter } from './search-location-section.svelte';
   import type { SearchDisplayFilters } from './search-display-section.svelte';
   import type { SearchDateFilter } from './search-date-section.svelte';
-
-  export enum MediaType {
-    All = 'all',
-    Image = 'image',
-    Video = 'video',
-  }
+  import { MediaType } from '$lib/constants';
 
   export type SearchFilter = {
     query: string;
     queryType: 'smart' | 'metadata';
-    personIds: Set<string>;
+    personIds: SvelteSet<string>;
     location: SearchLocationFilter;
     camera: SearchCameraFilter;
     date: SearchDateFilter;
@@ -36,10 +31,15 @@
   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
   import { mdiTune } from '@mdi/js';
   import { generateId } from '$lib/utils/generate-id';
+  import { SvelteSet } from 'svelte/reactivity';
 
-  export let searchQuery: MetadataSearchDto | SmartSearchDto;
-  export let onClose: () => void;
-  export let onSearch: (search: SmartSearchDto | MetadataSearchDto) => void;
+  interface Props {
+    searchQuery: MetadataSearchDto | SmartSearchDto;
+    onClose: () => void;
+    onSearch: (search: SmartSearchDto | MetadataSearchDto) => void;
+  }
+
+  let { searchQuery, onClose, onSearch }: Props = $props();
 
   const parseOptionalDate = (dateString?: string) => (dateString ? parseUtcDate(dateString) : undefined);
   const toStartOfDayDate = (dateString: string) => parseUtcDate(dateString)?.startOf('day').toISODate() || undefined;
@@ -50,10 +50,10 @@
     return value === null ? undefined : value;
   }
 
-  let filter: SearchFilter = {
+  let filter: SearchFilter = $state({
     query: 'query' in searchQuery ? searchQuery.query : searchQuery.originalFileName || '',
     queryType: 'query' in searchQuery ? 'smart' : 'metadata',
-    personIds: new Set('personIds' in searchQuery ? searchQuery.personIds : []),
+    personIds: new SvelteSet('personIds' in searchQuery ? searchQuery.personIds : []),
     location: {
       country: withNullAsUndefined(searchQuery.country),
       state: withNullAsUndefined(searchQuery.state),
@@ -78,13 +78,13 @@
         : searchQuery.type === AssetTypeEnum.Video
           ? MediaType.Video
           : MediaType.All,
-  };
+  });
 
   const resetForm = () => {
     filter = {
       query: '',
       queryType: 'smart',
-      personIds: new Set(),
+      personIds: new SvelteSet(),
       location: {},
       camera: {},
       date: {},
@@ -122,10 +122,20 @@
 
     onSearch(payload);
   };
+
+  const onreset = (event: Event) => {
+    event.preventDefault();
+    resetForm();
+  };
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+    search();
+  };
 </script>
 
 <FullScreenModal icon={mdiTune} width="extra-wide" title={$t('search_options')} {onClose}>
-  <form id={formId} autocomplete="off" on:submit|preventDefault={search} on:reset|preventDefault={resetForm}>
+  <form id={formId} autocomplete="off" {onsubmit} {onreset}>
     <div class="space-y-10 pb-10" tabindex="-1">
       <!-- PEOPLE -->
       <SearchPeopleSection bind:selectedPeople={filter.personIds} />
@@ -152,8 +162,8 @@
     </div>
   </form>
 
-  <svelte:fragment slot="sticky-bottom">
+  {#snippet stickyBottom()}
     <Button type="reset" color="gray" fullwidth form={formId}>{$t('clear_all')}</Button>
     <Button type="submit" fullwidth form={formId}>{$t('search')}</Button>
-  </svelte:fragment>
+  {/snippet}
 </FullScreenModal>
diff --git a/web/src/lib/components/shared-components/search-bar/search-history-box.svelte b/web/src/lib/components/shared-components/search-bar/search-history-box.svelte
index ca25ef5691..92a2f8847e 100644
--- a/web/src/lib/components/shared-components/search-bar/search-history-box.svelte
+++ b/web/src/lib/components/shared-components/search-bar/search-history-box.svelte
@@ -6,22 +6,41 @@
   import { t } from 'svelte-i18n';
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 
-  export let id: string;
-  export let searchQuery: string = '';
-  export let isSearchSuggestions: boolean = false;
-  export let isOpen: boolean = false;
-  export let onSelectSearchTerm: (searchTerm: string) => void;
-  export let onClearSearchTerm: (searchTerm: string) => void;
-  export let onClearAllSearchTerms: () => void;
-  export let onActiveSelectionChange: (selectedId: string | undefined) => void;
+  interface Props {
+    id: string;
+    searchQuery?: string;
+    isSearchSuggestions?: boolean;
+    isOpen?: boolean;
+    onSelectSearchTerm: (searchTerm: string) => void;
+    onClearSearchTerm: (searchTerm: string) => void;
+    onClearAllSearchTerms: () => void;
+    onActiveSelectionChange: (selectedId: string | undefined) => void;
+  }
 
-  $: filteredSearchTerms = $savedSearchTerms.filter((term) => term.toLowerCase().includes(searchQuery.toLowerCase()));
-  $: isSearchSuggestions = filteredSearchTerms.length > 0;
-  $: showClearAll = searchQuery === '';
-  $: suggestionCount = showClearAll ? filteredSearchTerms.length + 1 : filteredSearchTerms.length;
+  let {
+    id,
+    searchQuery = '',
+    isSearchSuggestions = $bindable(false),
+    isOpen = false,
+    onSelectSearchTerm,
+    onClearSearchTerm,
+    onClearAllSearchTerms,
+    onActiveSelectionChange,
+  }: Props = $props();
 
-  let selectedIndex: number | undefined = undefined;
-  let element: HTMLDivElement;
+  let filteredSearchTerms = $derived(
+    $savedSearchTerms.filter((term) => term.toLowerCase().includes(searchQuery.toLowerCase())),
+  );
+
+  $effect(() => {
+    isSearchSuggestions = filteredSearchTerms.length > 0;
+  });
+
+  let showClearAll = $derived(searchQuery === '');
+  let suggestionCount = $derived(showClearAll ? filteredSearchTerms.length + 1 : filteredSearchTerms.length);
+
+  let selectedIndex: number | undefined = $state(undefined);
+  let element = $state<HTMLDivElement>();
 
   export function moveSelection(increment: 1 | -1) {
     if (!isSearchSuggestions) {
@@ -45,7 +64,7 @@
     if (selectedIndex === undefined) {
       return;
     }
-    const selectedElement = element.querySelector(`#${getId(selectedIndex)}`) as HTMLElement;
+    const selectedElement = element?.querySelector(`#${getId(selectedIndex)}`) as HTMLElement;
     selectedElement?.click();
   }
 
@@ -86,7 +105,7 @@
             type="button"
             class="rounded-lg p-2 font-semibold text-immich-primary aria-selected:bg-immich-primary/25 hover:bg-immich-primary/25 dark:text-immich-dark-primary"
             role="option"
-            on:click={() => handleClearAll()}
+            onclick={() => handleClearAll()}
             tabindex="-1"
             aria-selected={selectedIndex === 0}
             aria-label={$t('clear_all_recent_searches')}
@@ -100,11 +119,11 @@
         {@const index = showClearAll ? i + 1 : i}
         <div class="flex w-full items-center justify-between text-sm text-black dark:text-gray-300">
           <div class="relative w-full items-center">
-            <!-- svelte-ignore a11y-click-events-have-key-events -->
+            <!-- svelte-ignore a11y_click_events_have_key_events -->
             <div
               id={getId(index)}
               class="relative flex w-full cursor-pointer gap-3 py-3 pl-5 hover:bg-gray-100 aria-selected:bg-gray-100 dark:aria-selected:bg-gray-500/30 dark:hover:bg-gray-500/30"
-              on:click={() => handleSelect(savedSearchTerm)}
+              onclick={() => handleSelect(savedSearchTerm)}
               role="option"
               tabindex="-1"
               aria-selected={selectedIndex === index}
@@ -120,7 +139,7 @@
                 size="18"
                 padding="1"
                 tabindex={-1}
-                on:click={() => handleClearSingle(savedSearchTerm)}
+                onclick={() => handleClearSingle(savedSearchTerm)}
               />
             </div>
           </div>
diff --git a/web/src/lib/components/shared-components/search-bar/search-location-section.svelte b/web/src/lib/components/shared-components/search-bar/search-location-section.svelte
index ce265d0030..d68578276c 100644
--- a/web/src/lib/components/shared-components/search-bar/search-location-section.svelte
+++ b/web/src/lib/components/shared-components/search-bar/search-location-section.svelte
@@ -1,4 +1,4 @@
-<script lang="ts" context="module">
+<script lang="ts" module>
   export interface SearchLocationFilter {
     country?: string;
     state?: string;
@@ -7,22 +7,22 @@
 </script>
 
 <script lang="ts">
+  import { run } from 'svelte/legacy';
+
   import Combobox, { asComboboxOptions, asSelectedOption } from '$lib/components/shared-components/combobox.svelte';
   import { handlePromiseError } from '$lib/utils';
   import { getSearchSuggestions, SearchSuggestionType } from '@immich/sdk';
   import { t } from 'svelte-i18n';
 
-  export let filters: SearchLocationFilter;
+  interface Props {
+    filters: SearchLocationFilter;
+  }
 
-  let countries: string[] = [];
-  let states: string[] = [];
-  let cities: string[] = [];
+  let { filters = $bindable() }: Props = $props();
 
-  $: countryFilter = filters.country;
-  $: stateFilter = filters.state;
-  $: handlePromiseError(updateCountries());
-  $: handlePromiseError(updateStates(countryFilter));
-  $: handlePromiseError(updateCities(countryFilter, stateFilter));
+  let countries: string[] = $state([]);
+  let states: string[] = $state([]);
+  let cities: string[] = $state([]);
 
   async function updateCountries() {
     const results: Array<string | null> = await getSearchSuggestions({
@@ -64,6 +64,17 @@
       filters.city = undefined;
     }
   }
+  let countryFilter = $derived(filters.country);
+  let stateFilter = $derived(filters.state);
+  run(() => {
+    handlePromiseError(updateCountries());
+  });
+  run(() => {
+    handlePromiseError(updateStates(countryFilter));
+  });
+  run(() => {
+    handlePromiseError(updateCities(countryFilter, stateFilter));
+  });
 </script>
 
 <div id="location-selection">
@@ -73,7 +84,7 @@
     <div class="w-full">
       <Combobox
         label={$t('country')}
-        on:select={({ detail }) => (filters.country = detail?.value)}
+        onSelect={(option) => (filters.country = option?.value)}
         options={asComboboxOptions(countries)}
         placeholder={$t('search_country')}
         selectedOption={asSelectedOption(filters.country)}
@@ -83,7 +94,7 @@
     <div class="w-full">
       <Combobox
         label={$t('state')}
-        on:select={({ detail }) => (filters.state = detail?.value)}
+        onSelect={(option) => (filters.state = option?.value)}
         options={asComboboxOptions(states)}
         placeholder={$t('search_state')}
         selectedOption={asSelectedOption(filters.state)}
@@ -93,7 +104,7 @@
     <div class="w-full">
       <Combobox
         label={$t('city')}
-        on:select={({ detail }) => (filters.city = detail?.value)}
+        onSelect={(option) => (filters.city = option?.value)}
         options={asComboboxOptions(cities)}
         placeholder={$t('search_city')}
         selectedOption={asSelectedOption(filters.city)}
diff --git a/web/src/lib/components/shared-components/search-bar/search-media-section.svelte b/web/src/lib/components/shared-components/search-bar/search-media-section.svelte
index b78868d614..658944055a 100644
--- a/web/src/lib/components/shared-components/search-bar/search-media-section.svelte
+++ b/web/src/lib/components/shared-components/search-bar/search-media-section.svelte
@@ -1,9 +1,13 @@
 <script lang="ts">
   import RadioButton from '$lib/components/elements/radio-button.svelte';
-  import { MediaType } from './search-filter-modal.svelte';
+  import { MediaType } from '$lib/constants';
   import { t } from 'svelte-i18n';
 
-  export let filteredMedia: MediaType;
+  interface Props {
+    filteredMedia: MediaType;
+  }
+
+  let { filteredMedia = $bindable() }: Props = $props();
 </script>
 
 <div id="media-type-selection">
diff --git a/web/src/lib/components/shared-components/search-bar/search-people-section.svelte b/web/src/lib/components/shared-components/search-bar/search-people-section.svelte
index 0c8d32a1ae..d06c4dc5c0 100644
--- a/web/src/lib/components/shared-components/search-bar/search-people-section.svelte
+++ b/web/src/lib/components/shared-components/search-bar/search-people-section.svelte
@@ -9,13 +9,18 @@
   import { handleError } from '$lib/utils/handle-error';
   import { t } from 'svelte-i18n';
   import SingleGridRow from '$lib/components/shared-components/single-grid-row.svelte';
+  import type { SvelteSet } from 'svelte/reactivity';
 
-  export let selectedPeople: Set<string>;
+  interface Props {
+    selectedPeople: SvelteSet<string>;
+  }
+
+  let { selectedPeople = $bindable() }: Props = $props();
 
   let peoplePromise = getPeople();
-  let showAllPeople = false;
-  let name = '';
-  let numberOfPeople = 1;
+  let showAllPeople = $state(false);
+  let name = $state('');
+  let numberOfPeople = $state(1);
 
   function orderBySelectedPeopleFirst(people: PersonResponseDto[]) {
     return [
@@ -39,7 +44,6 @@
     } else {
       selectedPeople.add(id);
     }
-    selectedPeople = selectedPeople;
   }
 
   const filterPeople = (list: PersonResponseDto[], name: string) => {
@@ -72,7 +76,7 @@
             )
               ? 'dark:border-slate-500 border-slate-400 bg-slate-200 dark:bg-slate-800 dark:text-white'
               : 'border-transparent'}"
-            on:click={() => togglePersonSelection(person.id)}
+            onclick={() => togglePersonSelection(person.id)}
           >
             <ImageThumbnail circle shadow url={getPeopleThumbnailUrl(person)} altText={person.name} widthStyle="100%" />
             <p class="mt-2 line-clamp-2 text-sm font-medium dark:text-white">{person.name}</p>
@@ -86,7 +90,7 @@
             shadow={false}
             color="text-primary"
             class="flex gap-2 place-items-center"
-            on:click={() => (showAllPeople = !showAllPeople)}
+            onclick={() => (showAllPeople = !showAllPeople)}
           >
             {#if showAllPeople}
               <span><Icon path={mdiClose} ariaHidden /></span>
diff --git a/web/src/lib/components/shared-components/search-bar/search-text-section.svelte b/web/src/lib/components/shared-components/search-bar/search-text-section.svelte
index c3145b2f0c..2f118e6567 100644
--- a/web/src/lib/components/shared-components/search-bar/search-text-section.svelte
+++ b/web/src/lib/components/shared-components/search-bar/search-text-section.svelte
@@ -2,8 +2,12 @@
   import RadioButton from '$lib/components/elements/radio-button.svelte';
   import { t } from 'svelte-i18n';
 
-  export let query: string | undefined;
-  export let queryType: 'smart' | 'metadata' = 'smart';
+  interface Props {
+    query: string | undefined;
+    queryType?: 'smart' | 'metadata';
+  }
+
+  let { query = $bindable(), queryType = $bindable('smart') }: Props = $props();
 </script>
 
 <fieldset>
diff --git a/web/src/lib/components/shared-components/server-about-modal.svelte b/web/src/lib/components/shared-components/server-about-modal.svelte
index d347170033..cf935cd314 100644
--- a/web/src/lib/components/shared-components/server-about-modal.svelte
+++ b/web/src/lib/components/shared-components/server-about-modal.svelte
@@ -1,19 +1,24 @@
 <script lang="ts">
   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
   import Portal from '$lib/components/shared-components/portal/portal.svelte';
-  import { type ServerAboutResponseDto } from '@immich/sdk';
+  import { type ServerAboutResponseDto, type ServerVersionHistoryResponseDto } from '@immich/sdk';
+  import { DateTime } from 'luxon';
   import { t } from 'svelte-i18n';
+  import { mdiAlert } from '@mdi/js';
+  import Icon from '$lib/components/elements/icon.svelte';
 
-  export let onClose: () => void;
+  interface Props {
+    onClose: () => void;
+    info: ServerAboutResponseDto;
+    versions: ServerVersionHistoryResponseDto[];
+  }
 
-  export let info: ServerAboutResponseDto;
+  let { onClose, info, versions }: Props = $props();
 </script>
 
 <Portal>
   <FullScreenModal title={$t('about')} {onClose}>
-    <div
-      class="immich-scrollbar max-h-[500px] overflow-y-auto flex flex-col sm:grid sm:grid-cols-2 gap-1 text-immich-primary dark:text-immich-dark-primary"
-    >
+    <div class="flex flex-col sm:grid sm:grid-cols-2 gap-1 text-immich-primary dark:text-immich-dark-primary">
       <div>
         <label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="version-desc"
           >Immich</label
@@ -151,6 +156,44 @@
           </div>
         </div>
       {/if}
+
+      {#if info.sourceRef === 'main' && info.repository === 'immich-app/immich'}
+        <div class="col-span-full p-4 flex gap-1">
+          <Icon path={mdiAlert} size="2em" color="#ffcc4d" />
+          <p class="immich-form-label text-sm" id="main-warning">
+            {$t('main_branch_warning')}
+          </p>
+        </div>
+      {/if}
+
+      <div class="col-span-full">
+        <label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="version-history"
+          >{$t('version_history')}</label
+        >
+        <ul id="version-history" class="list-none">
+          {#each versions.slice(0, 5) as item (item.id)}
+            {@const createdAt = DateTime.fromISO(item.createdAt)}
+            <li>
+              <span
+                class="immich-form-label pb-2 text-xs"
+                id="version-history"
+                title={createdAt.toLocaleString(DateTime.DATETIME_SHORT_WITH_SECONDS)}
+              >
+                {$t('version_history_item', {
+                  values: {
+                    version: item.version,
+                    date: createdAt.toLocaleString({
+                      month: 'short',
+                      day: 'numeric',
+                      year: 'numeric',
+                    }),
+                  },
+                })}
+              </span>
+            </li>
+          {/each}
+        </ul>
+      </div>
     </div>
   </FullScreenModal>
 </Portal>
diff --git a/web/src/lib/components/shared-components/settings/setting-accordion-state.svelte b/web/src/lib/components/shared-components/settings/setting-accordion-state.svelte
index a6257fce29..6b3ae81685 100644
--- a/web/src/lib/components/shared-components/settings/setting-accordion-state.svelte
+++ b/web/src/lib/components/shared-components/settings/setting-accordion-state.svelte
@@ -1,4 +1,4 @@
-<script lang="ts" context="module">
+<script lang="ts" module>
   export type AccordionState = Set<string>;
 
   const { get: getAccordionState, set: setAccordionState } = createContext<Writable<AccordionState>>();
@@ -8,20 +8,27 @@
 <script lang="ts">
   import { writable, type Writable } from 'svelte/store';
   import { createContext } from '$lib/utils/context';
-  import { page } from '$app/stores';
-  import { handlePromiseError } from '$lib/utils';
+  import { page } from '$app/state';
   import { goto } from '$app/navigation';
+  import type { Snippet } from 'svelte';
+  import { handlePromiseError } from '$lib/utils';
 
   const getParamValues = (param: string) => {
-    return new Set(($page.url.searchParams.get(param) || '').split(' ').filter((x) => x !== ''));
+    return new Set((page.url.searchParams.get(param) || '').split(' ').filter((x) => x !== ''));
   };
 
-  export let queryParam: string;
-  export let state: Writable<AccordionState> = writable(getParamValues(queryParam));
+  interface Props {
+    queryParam: string;
+    state?: Writable<AccordionState>;
+    children?: Snippet;
+  }
+
+  let { queryParam, state = writable(getParamValues(queryParam)), children }: Props = $props();
   setAccordionState(state);
 
-  $: if (queryParam && $state) {
-    const searchParams = new URLSearchParams($page.url.searchParams);
+  const searchParams = new URLSearchParams(page.url.searchParams);
+
+  $effect(() => {
     if ($state.size > 0) {
       searchParams.set(queryParam, [...$state].join(' '));
     } else {
@@ -29,7 +36,7 @@
     }
 
     handlePromiseError(goto(`?${searchParams.toString()}`, { replaceState: true, noScroll: true, keepFocus: true }));
-  }
+  });
 </script>
 
-<slot />
+{@render children?.()}
diff --git a/web/src/lib/components/shared-components/settings/setting-accordion.svelte b/web/src/lib/components/shared-components/settings/setting-accordion.svelte
index eec4fae9c2..0fe1c9dc14 100755
--- a/web/src/lib/components/shared-components/settings/setting-accordion.svelte
+++ b/web/src/lib/components/shared-components/settings/setting-accordion.svelte
@@ -1,19 +1,34 @@
 <script lang="ts">
   import { slide } from 'svelte/transition';
   import { getAccordionState } from './setting-accordion-state.svelte';
-  import { onDestroy } from 'svelte';
+  import { onDestroy, onMount, type Snippet } from 'svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
 
   const accordionState = getAccordionState();
 
-  export let title: string;
-  export let subtitle = '';
-  export let key: string;
-  export let isOpen = $accordionState.has(key);
-  export let autoScrollTo = false;
+  interface Props {
+    title: string;
+    subtitle?: string;
+    key: string;
+    isOpen?: boolean;
+    autoScrollTo?: boolean;
+    icon?: string;
+    subtitleSnippet?: Snippet;
+    children?: Snippet;
+  }
 
-  let accordionElement: HTMLDivElement;
+  let {
+    title,
+    subtitle = '',
+    key,
+    isOpen = $bindable($accordionState.has(key)),
+    autoScrollTo = false,
+    icon = '',
+    subtitleSnippet,
+    children,
+  }: Props = $props();
 
-  $: setIsOpen(isOpen);
+  let accordionElement: HTMLDivElement | undefined = $state();
 
   const setIsOpen = (isOpen: boolean) => {
     if (isOpen) {
@@ -21,7 +36,7 @@
 
       if (autoScrollTo) {
         setTimeout(() => {
-          accordionElement.scrollIntoView({
+          accordionElement?.scrollIntoView({
             behavior: 'smooth',
             block: 'start',
           });
@@ -36,23 +51,42 @@
   onDestroy(() => {
     setIsOpen(false);
   });
+
+  const onclick = () => {
+    isOpen = !isOpen;
+    setIsOpen(isOpen);
+  };
+
+  onMount(() => {
+    setIsOpen(isOpen);
+  });
 </script>
 
-<div class="border-b-[1px] border-gray-200 py-4 dark:border-gray-700" bind:this={accordionElement}>
+<div
+  class="border rounded-2xl my-4 px-6 py-4 transition-all {isOpen
+    ? 'border-immich-primary/40 dark:border-immich-dark-primary/50 shadow-md'
+    : 'dark:border-gray-800'}"
+  bind:this={accordionElement}
+>
   <button
     type="button"
     aria-expanded={isOpen}
-    on:click={() => (isOpen = !isOpen)}
+    {onclick}
     class="flex w-full place-items-center justify-between text-left"
   >
     <div>
-      <h2 class="font-medium text-immich-primary dark:text-immich-dark-primary">
-        {title}
-      </h2>
+      <div class="flex gap-2 place-items-center">
+        {#if icon}
+          <Icon path={icon} class="text-immich-primary dark:text-immich-dark-primary" size="24" ariaHidden />
+        {/if}
+        <h2 class="font-medium text-immich-primary dark:text-immich-dark-primary">
+          {title}
+        </h2>
+      </div>
 
-      <slot name="subtitle">
-        <p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
-      </slot>
+      {#if subtitleSnippet}{@render subtitleSnippet()}{:else}
+        <p class="text-sm dark:text-immich-dark-fg mt-1">{subtitle}</p>
+      {/if}
     </div>
 
     <div
@@ -76,7 +110,7 @@
 
   {#if isOpen}
     <ul transition:slide={{ duration: 150 }} class="mb-2 ml-4">
-      <slot />
+      {@render children?.()}
     </ul>
   {/if}
 </div>
diff --git a/web/src/lib/components/shared-components/settings/setting-buttons-row.svelte b/web/src/lib/components/shared-components/settings/setting-buttons-row.svelte
index 97bcb1d499..95edac6dfb 100644
--- a/web/src/lib/components/shared-components/settings/setting-buttons-row.svelte
+++ b/web/src/lib/components/shared-components/settings/setting-buttons-row.svelte
@@ -3,10 +3,14 @@
   import type { ResetOptions } from '$lib/utils/dipatch';
   import { t } from 'svelte-i18n';
 
-  export let showResetToDefault = true;
-  export let disabled = false;
-  export let onReset: (options: ResetOptions) => void;
-  export let onSave: () => void;
+  interface Props {
+    showResetToDefault?: boolean;
+    disabled?: boolean;
+    onReset: (options: ResetOptions) => void;
+    onSave: () => void;
+  }
+
+  let { showResetToDefault = true, disabled = false, onReset, onSave }: Props = $props();
 </script>
 
 <div class="mt-8 flex justify-between gap-2">
@@ -14,7 +18,7 @@
     {#if showResetToDefault}
       <button
         type="button"
-        on:click={() => onReset({ default: true })}
+        onclick={() => onReset({ default: true })}
         class="bg-none text-sm font-medium text-immich-primary hover:text-immich-primary/75 dark:text-immich-dark-primary hover:dark:text-immich-dark-primary/75"
       >
         {$t('reset_to_default')}
@@ -23,7 +27,7 @@
   </div>
 
   <div class="right">
-    <Button {disabled} size="sm" color="gray" on:click={() => onReset({ default: false })}>{$t('reset')}</Button>
-    <Button type="submit" {disabled} size="sm" on:click={() => onSave()}>{$t('save')}</Button>
+    <Button {disabled} size="sm" color="gray" onclick={() => onReset({ default: false })}>{$t('reset')}</Button>
+    <Button type="submit" {disabled} size="sm" onclick={() => onSave()}>{$t('save')}</Button>
   </div>
 </div>
diff --git a/web/src/lib/components/shared-components/settings/setting-checkboxes.svelte b/web/src/lib/components/shared-components/settings/setting-checkboxes.svelte
index 3def0ce08d..09f0ea438b 100644
--- a/web/src/lib/components/shared-components/settings/setting-checkboxes.svelte
+++ b/web/src/lib/components/shared-components/settings/setting-checkboxes.svelte
@@ -4,13 +4,25 @@
   import { fly } from 'svelte/transition';
   import { t } from 'svelte-i18n';
 
-  export let value: string[];
-  export let options: { value: string; text: string }[];
-  export let label = '';
-  export let desc = '';
-  export let name = '';
-  export let isEdited = false;
-  export let disabled = false;
+  interface Props {
+    value: string[];
+    options: { value: string; text: string }[];
+    label?: string;
+    desc?: string;
+    name?: string;
+    isEdited?: boolean;
+    disabled?: boolean;
+  }
+
+  let {
+    value = $bindable(),
+    options,
+    label = '',
+    desc = '',
+    name = '',
+    isEdited = false,
+    disabled = false,
+  }: Props = $props();
 
   function handleCheckboxChange(option: string) {
     value = value.includes(option) ? value.filter((item) => item !== option) : [...value, option];
@@ -46,7 +58,7 @@
         checked={value.includes(option.value)}
         {disabled}
         labelClass="text-gray-500 dark:text-gray-300"
-        on:change={() => handleCheckboxChange(option.value)}
+        onchange={() => handleCheckboxChange(option.value)}
       />
     {/each}
   </div>
diff --git a/web/src/lib/components/shared-components/settings/setting-combobox.svelte b/web/src/lib/components/shared-components/settings/setting-combobox.svelte
index 502cd94cce..5314ad7193 100644
--- a/web/src/lib/components/shared-components/settings/setting-combobox.svelte
+++ b/web/src/lib/components/shared-components/settings/setting-combobox.svelte
@@ -3,14 +3,29 @@
   import { fly } from 'svelte/transition';
   import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte';
   import { t } from 'svelte-i18n';
+  import type { Snippet } from 'svelte';
 
-  export let title: string;
-  export let comboboxPlaceholder: string;
-  export let subtitle = '';
-  export let isEdited = false;
-  export let options: ComboBoxOption[];
-  export let selectedOption: ComboBoxOption;
-  export let onSelect: (combobox: ComboBoxOption | undefined) => void;
+  interface Props {
+    title: string;
+    comboboxPlaceholder: string;
+    subtitle?: string;
+    isEdited?: boolean;
+    options: ComboBoxOption[];
+    selectedOption: ComboBoxOption;
+    onSelect: (combobox: ComboBoxOption | undefined) => void;
+    children?: Snippet;
+  }
+
+  let {
+    title,
+    comboboxPlaceholder,
+    subtitle = '',
+    isEdited = false,
+    options,
+    selectedOption,
+    onSelect,
+    children,
+  }: Props = $props();
 </script>
 
 <div class="grid grid-cols-2">
@@ -32,14 +47,7 @@
     <p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
   </div>
   <div class="flex items-center">
-    <Combobox
-      label={title}
-      hideLabel={true}
-      {selectedOption}
-      {options}
-      placeholder={comboboxPlaceholder}
-      on:select={({ detail }) => onSelect(detail)}
-    />
-    <slot />
+    <Combobox label={title} hideLabel={true} {selectedOption} {options} placeholder={comboboxPlaceholder} {onSelect} />
+    {@render children?.()}
   </div>
 </div>
diff --git a/web/src/lib/components/shared-components/settings/setting-dropdown.svelte b/web/src/lib/components/shared-components/settings/setting-dropdown.svelte
index 5243a14931..57e78e6c6f 100644
--- a/web/src/lib/components/shared-components/settings/setting-dropdown.svelte
+++ b/web/src/lib/components/shared-components/settings/setting-dropdown.svelte
@@ -3,14 +3,27 @@
   import { fly } from 'svelte/transition';
   import Dropdown, { type RenderedOption } from '$lib/components/elements/dropdown.svelte';
   import { t } from 'svelte-i18n';
+  import type { Snippet } from 'svelte';
 
-  export let title: string;
-  export let subtitle = '';
-  export let options: RenderedOption[];
-  export let selectedOption: RenderedOption;
-  export let isEdited = false;
+  interface Props {
+    title: string;
+    subtitle?: string;
+    options: RenderedOption[];
+    selectedOption: RenderedOption;
+    isEdited?: boolean;
+    onToggle: (option: RenderedOption) => void;
+    children?: Snippet;
+  }
 
-  export let onToggle: (option: RenderedOption) => void;
+  let {
+    title,
+    subtitle = '',
+    options,
+    selectedOption = $bindable(),
+    isEdited = false,
+    onToggle,
+    children,
+  }: Props = $props();
 </script>
 
 <div class="flex place-items-center justify-between">
@@ -30,7 +43,7 @@
     </div>
 
     <p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
-    <slot />
+    {@render children?.()}
   </div>
   <div class="w-fit">
     <Dropdown
@@ -43,7 +56,7 @@
           icon: option.icon,
         };
       }}
-      on:select={({ detail }) => onToggle(detail)}
+      onSelect={onToggle}
     />
   </div>
 </div>
diff --git a/web/src/lib/components/shared-components/settings/setting-input-field.spec.ts b/web/src/lib/components/shared-components/settings/setting-input-field.spec.ts
index 642492dda5..80cb920074 100644
--- a/web/src/lib/components/shared-components/settings/setting-input-field.spec.ts
+++ b/web/src/lib/components/shared-components/settings/setting-input-field.spec.ts
@@ -1,7 +1,7 @@
+import { SettingInputFieldType } from '$lib/constants';
 import { render } from '@testing-library/svelte';
 import userEvent from '@testing-library/user-event';
-// @ts-expect-error the import works but tsc check errors
-import SettingInputField, { SettingInputFieldType } from './setting-input-field.svelte';
+import SettingInputField from './setting-input-field.svelte';
 
 describe('SettingInputField component', () => {
   it('validates number input on blur', async () => {
diff --git a/web/src/lib/components/shared-components/settings/setting-input-field.svelte b/web/src/lib/components/shared-components/settings/setting-input-field.svelte
index 0767ecf12f..a04f521773 100644
--- a/web/src/lib/components/shared-components/settings/setting-input-field.svelte
+++ b/web/src/lib/components/shared-components/settings/setting-input-field.svelte
@@ -1,36 +1,49 @@
-<script lang="ts" context="module">
-  export enum SettingInputFieldType {
-    EMAIL = 'email',
-    TEXT = 'text',
-    NUMBER = 'number',
-    PASSWORD = 'password',
-    COLOR = 'color',
-  }
-</script>
-
 <script lang="ts">
   import { quintOut } from 'svelte/easing';
   import type { FormEventHandler } from 'svelte/elements';
   import { fly } from 'svelte/transition';
   import PasswordField from '../password-field.svelte';
   import { t } from 'svelte-i18n';
-  import { onMount, tick } from 'svelte';
+  import { onMount, tick, type Snippet } from 'svelte';
+  import { SettingInputFieldType } from '$lib/constants';
 
-  export let inputType: SettingInputFieldType;
-  export let value: string | number;
-  export let min = Number.MIN_SAFE_INTEGER;
-  export let max = Number.MAX_SAFE_INTEGER;
-  export let step = '1';
-  export let label = '';
-  export let desc = '';
-  export let title = '';
-  export let required = false;
-  export let disabled = false;
-  export let isEdited = false;
-  export let autofocus = false;
-  export let passwordAutocomplete: string = 'current-password';
+  interface Props {
+    inputType: SettingInputFieldType;
+    value: string | number;
+    min?: number;
+    max?: number;
+    step?: string;
+    label?: string;
+    description?: string;
+    title?: string;
+    required?: boolean;
+    disabled?: boolean;
+    isEdited?: boolean;
+    autofocus?: boolean;
+    passwordAutocomplete?: AutoFill;
+    descriptionSnippet?: Snippet;
+    trailingSnippet?: Snippet;
+  }
 
-  let input: HTMLInputElement;
+  let {
+    inputType,
+    value = $bindable(),
+    min = Number.MIN_SAFE_INTEGER,
+    max = Number.MAX_SAFE_INTEGER,
+    step = '1',
+    label = '',
+    description = '',
+    title = '',
+    required = false,
+    disabled = false,
+    isEdited = false,
+    autofocus = false,
+    passwordAutocomplete = 'current-password',
+    descriptionSnippet,
+    trailingSnippet,
+  }: Props = $props();
+
+  let input: HTMLInputElement | undefined = $state();
 
   const handleChange: FormEventHandler<HTMLInputElement> = (e) => {
     value = e.currentTarget.value;
@@ -57,7 +70,7 @@
 </script>
 
 <div class="mb-4 w-full">
-  <div class={`flex h-[26px] place-items-center gap-1`}>
+  <div class={`flex place-items-center gap-1`}>
     <label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for={label}>{label}</label>
     {#if required}
       <div class="text-red-400">*</div>
@@ -73,12 +86,12 @@
     {/if}
   </div>
 
-  {#if desc}
+  {#if description}
     <p class="immich-form-label pb-2 text-sm" id="{label}-desc">
-      {desc}
+      {description}
     </p>
   {:else}
-    <slot name="desc" />
+    {@render descriptionSnippet?.()}
   {/if}
 
   {#if inputType !== SettingInputFieldType.PASSWORD}
@@ -87,7 +100,7 @@
         <input
           bind:this={input}
           class="immich-form-input w-full pb-2 rounded-none mr-1"
-          aria-describedby={desc ? `${label}-desc` : undefined}
+          aria-describedby={description ? `${label}-desc` : undefined}
           aria-labelledby="{label}-label"
           id={label}
           name={label}
@@ -97,7 +110,7 @@
           {step}
           {required}
           {value}
-          on:change={handleChange}
+          onchange={handleChange}
           {disabled}
           {title}
         />
@@ -107,7 +120,7 @@
         bind:this={input}
         class="immich-form-input w-full pb-2"
         class:color-picker={inputType === SettingInputFieldType.COLOR}
-        aria-describedby={desc ? `${label}-desc` : undefined}
+        aria-describedby={description ? `${label}-desc` : undefined}
         aria-labelledby="{label}-label"
         id={label}
         name={label}
@@ -117,14 +130,16 @@
         {step}
         {required}
         {value}
-        on:change={handleChange}
+        onchange={handleChange}
         {disabled}
         {title}
       />
+
+      {@render trailingSnippet?.()}
     </div>
   {:else}
     <PasswordField
-      aria-describedby={desc ? `${label}-desc` : undefined}
+      aria-describedby={description ? `${label}-desc` : undefined}
       aria-labelledby="{label}-label"
       id={label}
       name={label}
diff --git a/web/src/lib/components/shared-components/settings/setting-select.svelte b/web/src/lib/components/shared-components/settings/setting-select.svelte
index c5b9e2c02e..44f03075da 100644
--- a/web/src/lib/components/shared-components/settings/setting-select.svelte
+++ b/web/src/lib/components/shared-components/settings/setting-select.svelte
@@ -1,28 +1,40 @@
 <script lang="ts">
   import { quintOut } from 'svelte/easing';
   import { fly } from 'svelte/transition';
-  import { createEventDispatcher } from 'svelte';
   import { t } from 'svelte-i18n';
   import Icon from '$lib/components/elements/icon.svelte';
   import { mdiChevronDown } from '@mdi/js';
 
-  export let value: string | number;
-  export let options: { value: string | number; text: string }[];
-  export let label = '';
-  export let desc = '';
-  export let name = '';
-  export let isEdited = false;
-  export let number = false;
-  export let disabled = false;
+  interface Props {
+    value: string | number;
+    options: { value: string | number; text: string }[];
+    label?: string;
+    desc?: string;
+    name?: string;
+    isEdited?: boolean;
+    number?: boolean;
+    disabled?: boolean;
+    onSelect?: (setting: string | number) => void;
+  }
 
-  const dispatch = createEventDispatcher<{ select: string | number }>();
+  let {
+    value = $bindable(),
+    options,
+    label = '',
+    desc = '',
+    name = '',
+    isEdited = false,
+    number = false,
+    disabled = false,
+    onSelect = () => {},
+  }: Props = $props();
 
   const handleChange = (e: Event) => {
     value = (e.target as HTMLInputElement).value;
     if (number) {
       value = Number.parseInt(value);
     }
-    dispatch('select', value);
+    onSelect(value);
   };
 </script>
 
@@ -64,7 +76,7 @@
       {name}
       id="{name}-select"
       bind:value
-      on:change={handleChange}
+      onchange={handleChange}
     >
       {#each options as option}
         <option value={option.value}>{option.text}</option>
diff --git a/web/src/lib/components/shared-components/settings/setting-switch.svelte b/web/src/lib/components/shared-components/settings/setting-switch.svelte
index d933b27ab5..29c1f213d3 100644
--- a/web/src/lib/components/shared-components/settings/setting-switch.svelte
+++ b/web/src/lib/components/shared-components/settings/setting-switch.svelte
@@ -1,24 +1,35 @@
 <script lang="ts">
   import { quintOut } from 'svelte/easing';
   import { fly } from 'svelte/transition';
-  import { createEventDispatcher } from 'svelte';
   import Slider from '$lib/components/elements/slider.svelte';
   import { generateId } from '$lib/utils/generate-id';
   import { t } from 'svelte-i18n';
+  import type { Snippet } from 'svelte';
 
-  export let title: string;
-  export let subtitle = '';
-  export let checked = false;
-  export let disabled = false;
-  export let isEdited = false;
+  interface Props {
+    title: string;
+    subtitle?: string;
+    checked?: boolean;
+    disabled?: boolean;
+    isEdited?: boolean;
+    onToggle?: (isChecked: boolean) => void;
+    children?: Snippet;
+  }
+
+  let {
+    title,
+    subtitle = '',
+    checked = $bindable(false),
+    disabled = false,
+    isEdited = false,
+    onToggle = () => {},
+    children,
+  }: Props = $props();
 
   let id: string = generateId();
 
-  $: sliderId = `${id}-slider`;
-  $: subtitleId = subtitle ? `${id}-subtitle` : undefined;
-
-  const dispatch = createEventDispatcher<{ toggle: boolean }>();
-  const onToggle = (isChecked: boolean) => dispatch('toggle', isChecked);
+  let sliderId = $derived(`${id}-slider`);
+  let subtitleId = $derived(subtitle ? `${id}-subtitle` : undefined);
 </script>
 
 <div class="flex place-items-center justify-between">
@@ -40,14 +51,8 @@
     {#if subtitle}
       <p id={subtitleId} class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
     {/if}
-    <slot />
+    {@render children?.()}
   </div>
 
-  <Slider
-    id={sliderId}
-    bind:checked
-    {disabled}
-    on:toggle={({ detail }) => onToggle(detail)}
-    ariaDescribedBy={subtitleId}
-  />
+  <Slider id={sliderId} bind:checked {disabled} {onToggle} ariaDescribedBy={subtitleId} />
 </div>
diff --git a/web/src/lib/components/shared-components/settings/setting-textarea.svelte b/web/src/lib/components/shared-components/settings/setting-textarea.svelte
index 2f579e9db4..9f9f885263 100644
--- a/web/src/lib/components/shared-components/settings/setting-textarea.svelte
+++ b/web/src/lib/components/shared-components/settings/setting-textarea.svelte
@@ -2,13 +2,27 @@
   import { quintOut } from 'svelte/easing';
   import { fly } from 'svelte/transition';
   import { t } from 'svelte-i18n';
+  import type { Snippet } from 'svelte';
 
-  export let value: string;
-  export let label = '';
-  export let desc = '';
-  export let required = false;
-  export let disabled = false;
-  export let isEdited = false;
+  interface Props {
+    value: string;
+    label?: string;
+    description?: string;
+    required?: boolean;
+    disabled?: boolean;
+    isEdited?: boolean;
+    descriptionSnippet?: Snippet;
+  }
+
+  let {
+    value = $bindable(),
+    label = '',
+    description = '',
+    required = false,
+    disabled = false,
+    isEdited = false,
+    descriptionSnippet,
+  }: Props = $props();
 
   const handleInput = (e: Event) => {
     value = (e.target as HTMLInputElement).value;
@@ -32,23 +46,23 @@
     {/if}
   </div>
 
-  {#if desc}
+  {#if description}
     <p class="immich-form-label pb-2 text-sm" id="{label}-desc">
-      {desc}
+      {description}
     </p>
   {:else}
-    <slot name="desc" />
+    {@render descriptionSnippet?.()}
   {/if}
 
   <textarea
     class="immich-form-input w-full pb-2"
-    aria-describedby={desc ? `${label}-desc` : undefined}
+    aria-describedby={description ? `${label}-desc` : undefined}
     aria-labelledby="{label}-label"
     id={label}
     name={label}
     {required}
     {value}
-    on:input={handleInput}
+    oninput={handleInput}
     {disabled}
-  />
+  ></textarea>
 </div>
diff --git a/web/src/lib/components/shared-components/show-shortcuts.svelte b/web/src/lib/components/shared-components/show-shortcuts.svelte
index ebc0dd688c..a3cfd83ad5 100644
--- a/web/src/lib/components/shared-components/show-shortcuts.svelte
+++ b/web/src/lib/components/shared-components/show-shortcuts.svelte
@@ -1,9 +1,8 @@
 <script lang="ts">
-  import { createEventDispatcher } from 'svelte';
-  import FullScreenModal from './full-screen-modal.svelte';
   import { mdiInformationOutline } from '@mdi/js';
-  import Icon from '../elements/icon.svelte';
   import { t } from 'svelte-i18n';
+  import Icon from '../elements/icon.svelte';
+  import FullScreenModal from './full-screen-modal.svelte';
 
   interface Shortcuts {
     general: ExplainedShortcut[];
@@ -16,29 +15,34 @@
     info?: string;
   }
 
-  export let shortcuts: Shortcuts = {
-    general: [
-      { key: ['←', '→'], action: $t('previous_or_next_photo') },
-      { key: ['Esc'], action: $t('back_close_deselect') },
-      { key: ['Ctrl', 'k'], action: $t('search_your_photos') },
-      { key: ['Ctrl', '⇧', 'k'], action: $t('open_the_search_filters') },
-    ],
-    actions: [
-      { key: ['f'], action: $t('favorite_or_unfavorite_photo') },
-      { key: ['i'], action: $t('show_or_hide_info') },
-      { key: ['s'], action: $t('stack_selected_photos') },
-      { key: ['⇧', 'a'], action: $t('archive_or_unarchive_photo') },
-      { key: ['⇧', 'd'], action: $t('download') },
-      { key: ['Space'], action: $t('play_or_pause_video') },
-      { key: ['Del'], action: $t('trash_delete_asset'), info: $t('shift_to_permanent_delete') },
-    ],
-  };
-  const dispatch = createEventDispatcher<{
-    close: void;
-  }>();
+  interface Props {
+    onClose: () => void;
+    shortcuts?: Shortcuts;
+  }
+
+  let {
+    onClose,
+    shortcuts = {
+      general: [
+        { key: ['←', '→'], action: $t('previous_or_next_photo') },
+        { key: ['Esc'], action: $t('back_close_deselect') },
+        { key: ['Ctrl', 'k'], action: $t('search_your_photos') },
+        { key: ['Ctrl', '⇧', 'k'], action: $t('open_the_search_filters') },
+      ],
+      actions: [
+        { key: ['f'], action: $t('favorite_or_unfavorite_photo') },
+        { key: ['i'], action: $t('show_or_hide_info') },
+        { key: ['s'], action: $t('stack_selected_photos') },
+        { key: ['⇧', 'a'], action: $t('archive_or_unarchive_photo') },
+        { key: ['⇧', 'd'], action: $t('download') },
+        { key: ['Space'], action: $t('play_or_pause_video') },
+        { key: ['Del'], action: $t('trash_delete_asset'), info: $t('shift_to_permanent_delete') },
+      ],
+    },
+  }: Props = $props();
 </script>
 
-<FullScreenModal title={$t('keyboard_shortcuts')} width="auto" onClose={() => dispatch('close')}>
+<FullScreenModal title={$t('keyboard_shortcuts')} width="auto" {onClose}>
   <div class="grid grid-cols-1 gap-4 px-4 pb-4 md:grid-cols-2">
     {#if shortcuts.general.length > 0}
       <div class="p-4">
diff --git a/web/src/lib/components/shared-components/side-bar/more-information-albums.svelte b/web/src/lib/components/shared-components/side-bar/more-information-albums.svelte
deleted file mode 100644
index 68c58ab155..0000000000
--- a/web/src/lib/components/shared-components/side-bar/more-information-albums.svelte
+++ /dev/null
@@ -1,23 +0,0 @@
-<script lang="ts">
-  import { type AlbumStatisticsResponseDto, getAlbumStatistics } from '@immich/sdk';
-  import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
-  import { t } from 'svelte-i18n';
-
-  export let albumType: keyof AlbumStatisticsResponseDto;
-
-  const handleAlbumCount = async () => {
-    try {
-      return await getAlbumStatistics();
-    } catch {
-      return { owned: 0, shared: 0, notShared: 0 };
-    }
-  };
-</script>
-
-{#await handleAlbumCount()}
-  <LoadingSpinner />
-{:then data}
-  <div>
-    <p>{$t('albums_count', { values: { count: data[albumType] } })}</p>
-  </div>
-{/await}
diff --git a/web/src/lib/components/shared-components/side-bar/more-information-assets.svelte b/web/src/lib/components/shared-components/side-bar/more-information-assets.svelte
deleted file mode 100644
index 1da245390b..0000000000
--- a/web/src/lib/components/shared-components/side-bar/more-information-assets.svelte
+++ /dev/null
@@ -1,16 +0,0 @@
-<script lang="ts">
-  import { getAssetStatistics } from '@immich/sdk';
-  import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
-  import { t } from 'svelte-i18n';
-
-  export let assetStats: NonNullable<Parameters<typeof getAssetStatistics>[0]>;
-</script>
-
-{#await getAssetStatistics(assetStats)}
-  <LoadingSpinner />
-{:then data}
-  <div>
-    <p>{$t('videos_count', { values: { count: data.videos } })}</p>
-    <p>{$t('photos_count', { values: { count: data.images } })}</p>
-  </div>
-{/await}
diff --git a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte
index a284c7efc1..2c4ab8818c 100644
--- a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte
+++ b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte
@@ -18,12 +18,12 @@
   import { getButtonVisibility } from '$lib/utils/purchase-utils';
   import SupporterBadge from '$lib/components/shared-components/side-bar/supporter-badge.svelte';
 
-  let showMessage = false;
-  let isOpen = false;
-  let hoverMessage = false;
-  let hoverButton = false;
+  let showMessage = $state(false);
+  let isOpen = $state(false);
+  let hoverMessage = $state(false);
+  let hoverButton = $state(false);
 
-  let showBuyButton = getButtonVisibility();
+  let showBuyButton = $state(getButtonVisibility());
 
   const { isPurchased } = purchaseStore;
 
@@ -63,13 +63,15 @@
     }
   };
 
-  $: if (showMessage && !hoverMessage && !hoverButton) {
-    setTimeout(() => {
-      if (!hoverMessage && !hoverButton) {
-        showMessage = false;
-      }
-    }, 300);
-  }
+  $effect(() => {
+    if (showMessage && !hoverMessage && !hoverButton) {
+      setTimeout(() => {
+        if (!hoverMessage && !hoverButton) {
+          showMessage = false;
+        }
+      }, 300);
+    }
+  });
 </script>
 
 {#if isOpen}
@@ -79,7 +81,7 @@
 <div class="hidden md:block license-status pl-4 text-sm">
   {#if $isPurchased && $preferences.purchase.showSupportBadge}
     <button
-      on:click={() => goto(`${AppRoute.USER_SETTINGS}?isOpen=user-purchase-settings`)}
+      onclick={() => goto(`${AppRoute.USER_SETTINGS}?isOpen=user-purchase-settings`)}
       class="w-full"
       type="button"
     >
@@ -88,11 +90,11 @@
   {:else if !$isPurchased && showBuyButton && getAccountAge() > 14}
     <button
       type="button"
-      on:click={openPurchaseModal}
-      on:mouseover={onButtonHover}
-      on:mouseleave={() => (hoverButton = false)}
-      on:focus={onButtonHover}
-      on:blur={() => (hoverButton = false)}
+      onclick={openPurchaseModal}
+      onmouseover={onButtonHover}
+      onmouseleave={() => (hoverButton = false)}
+      onfocus={onButtonHover}
+      onblur={() => (hoverButton = false)}
       class="p-2 flex justify-between place-items-center place-content-center border border-immich-primary/20 dark:border-immich-dark-primary/10 mt-2 rounded-lg shadow-md dark:bg-immich-dark-primary/10 w-full"
     >
       <div class="flex justify-between w-full place-items-center place-content-center">
@@ -122,10 +124,10 @@
     <div
       class="w-[500px] absolute bottom-[75px] left-[255px] bg-gray-50 dark:border-gray-800 border border-gray-200 dark:bg-immich-dark-gray dark:text-white text-black rounded-3xl z-10 shadow-2xl px-8 py-6"
       transition:fade={{ duration: 150 }}
-      on:mouseover={() => (hoverMessage = true)}
-      on:mouseleave={() => (hoverMessage = false)}
-      on:focus={() => (hoverMessage = true)}
-      on:blur={() => (hoverMessage = false)}
+      onmouseover={() => (hoverMessage = true)}
+      onmouseleave={() => (hoverMessage = false)}
+      onfocus={() => (hoverMessage = true)}
+      onblur={() => (hoverMessage = false)}
       role="dialog"
     >
       <div class="flex justify-between place-items-center">
@@ -134,7 +136,7 @@
         </div>
         <CircleIconButton
           icon={mdiClose}
-          on:click={() => {
+          onclick={() => {
             showMessage = false;
           }}
           title={$t('close')}
@@ -157,12 +159,12 @@
         </p>
       </div>
 
-      <Button class="mt-2" fullwidth on:click={openPurchaseModal}>{$t('purchase_button_buy_immich')}</Button>
+      <Button class="mt-2" fullwidth onclick={openPurchaseModal}>{$t('purchase_button_buy_immich')}</Button>
       <div class="mt-3 flex gap-4">
-        <Button size="sm" fullwidth shadow={false} color="transparent-gray" on:click={() => hideButton(true)}>
+        <Button size="sm" fullwidth shadow={false} color="transparent-gray" onclick={() => hideButton(true)}>
           {$t('purchase_button_never_show_again')}
         </Button>
-        <Button size="sm" fullwidth shadow={false} color="transparent-gray" on:click={() => hideButton(false)}>
+        <Button size="sm" fullwidth shadow={false} color="transparent-gray" onclick={() => hideButton(false)}>
           {$t('purchase_button_reminder')}
         </Button>
       </div>
diff --git a/web/src/lib/components/shared-components/side-bar/recent-albums.spec.ts b/web/src/lib/components/shared-components/side-bar/recent-albums.spec.ts
new file mode 100644
index 0000000000..d5c197c003
--- /dev/null
+++ b/web/src/lib/components/shared-components/side-bar/recent-albums.spec.ts
@@ -0,0 +1,28 @@
+import { sdkMock } from '$lib/__mocks__/sdk.mock';
+import RecentAlbums from '$lib/components/shared-components/side-bar/recent-albums.svelte';
+import { albumFactory } from '@test-data/factories/album-factory';
+import { render, screen } from '@testing-library/svelte';
+import { tick } from 'svelte';
+
+describe('RecentAlbums component', () => {
+  it('sorts albums by most recently updated', async () => {
+    const albums = [
+      albumFactory.build({ updatedAt: '2024-01-01T00:00:00Z' }),
+      albumFactory.build({ updatedAt: '2024-01-09T00:00:01Z' }),
+      albumFactory.build({ updatedAt: '2024-01-10T00:00:00Z' }),
+      albumFactory.build({ updatedAt: '2024-01-09T00:00:00Z' }),
+    ];
+
+    sdkMock.getAllAlbums.mockResolvedValueOnce([...albums]);
+    render(RecentAlbums);
+
+    expect(sdkMock.getAllAlbums).toBeCalledTimes(1);
+    await tick();
+
+    const links = screen.getAllByRole('link');
+    expect(links).toHaveLength(3);
+    expect(links[0]).toHaveAttribute('href', `/albums/${albums[2].id}`);
+    expect(links[1]).toHaveAttribute('href', `/albums/${albums[1].id}`);
+    expect(links[2]).toHaveAttribute('href', `/albums/${albums[3].id}`);
+  });
+});
diff --git a/web/src/lib/components/shared-components/side-bar/recent-albums.svelte b/web/src/lib/components/shared-components/side-bar/recent-albums.svelte
new file mode 100644
index 0000000000..b11935d643
--- /dev/null
+++ b/web/src/lib/components/shared-components/side-bar/recent-albums.svelte
@@ -0,0 +1,44 @@
+<script lang="ts">
+  import { onMount } from 'svelte';
+  import { getAssetThumbnailUrl } from '$lib/utils';
+  import { getAllAlbums, type AlbumResponseDto } from '@immich/sdk';
+  import { handleError } from '$lib/utils/handle-error';
+  import { t } from 'svelte-i18n';
+  import { userInteraction } from '$lib/stores/user.svelte';
+
+  let albums: AlbumResponseDto[] = $state([]);
+
+  onMount(async () => {
+    if (userInteraction.recentAlbums) {
+      albums = userInteraction.recentAlbums;
+      return;
+    }
+    try {
+      const allAlbums = await getAllAlbums({});
+      albums = allAlbums.sort((a, b) => (a.updatedAt > b.updatedAt ? -1 : 1)).slice(0, 3);
+      userInteraction.recentAlbums = albums;
+    } catch (error) {
+      handleError(error, $t('failed_to_load_assets'));
+    }
+  });
+</script>
+
+{#each albums as album}
+  <a
+    href={'/albums/' + album.id}
+    title={album.albumName}
+    class="flex w-full place-items-center justify-between gap-4 rounded-r-full py-3 transition-[padding] delay-100 duration-100 hover:cursor-pointer hover:bg-immich-gray hover:text-immich-primary dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray dark:hover:text-immich-dark-primary pl-10 group-hover:sm:px-10 md:px-10"
+  >
+    <div>
+      <div
+        class="h-6 w-6 bg-cover rounded bg-gray-200 dark:bg-gray-600"
+        style={album.albumThumbnailAssetId
+          ? `background-image:url('${getAssetThumbnailUrl({ id: album.albumThumbnailAssetId })}')`
+          : ''}
+      ></div>
+    </div>
+    <div class="grow text-sm font-medium truncate">
+      {album.albumName}
+    </div>
+  </a>
+{/each}
diff --git a/web/src/lib/components/shared-components/side-bar/server-status.svelte b/web/src/lib/components/shared-components/side-bar/server-status.svelte
index 83ed98584a..e1d7340c46 100644
--- a/web/src/lib/components/shared-components/side-bar/server-status.svelte
+++ b/web/src/lib/components/shared-components/side-bar/server-status.svelte
@@ -4,24 +4,41 @@
   import { requestServerInfo } from '$lib/utils/auth';
   import { onMount } from 'svelte';
   import { t } from 'svelte-i18n';
-  import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk';
+  import {
+    getAboutInfo,
+    getVersionHistory,
+    type ServerAboutResponseDto,
+    type ServerVersionHistoryResponseDto,
+  } from '@immich/sdk';
+  import Icon from '$lib/components/elements/icon.svelte';
+  import { mdiAlert } from '@mdi/js';
+  import { userInteraction } from '$lib/stores/user.svelte';
 
   const { serverVersion, connected } = websocketStore;
 
-  let isOpen = false;
-
-  $: version = $serverVersion ? `v${$serverVersion.major}.${$serverVersion.minor}.${$serverVersion.patch}` : null;
-
-  let aboutInfo: ServerAboutResponseDto;
+  let isOpen = $state(false);
+  let info: ServerAboutResponseDto | undefined = $state();
+  let versions: ServerVersionHistoryResponseDto[] = $state([]);
 
   onMount(async () => {
+    if (userInteraction.aboutInfo && userInteraction.versions && $serverVersion) {
+      info = userInteraction.aboutInfo;
+      versions = userInteraction.versions;
+      return;
+    }
     await requestServerInfo();
-    aboutInfo = await getAboutInfo();
+    [info, versions] = await Promise.all([getAboutInfo(), getVersionHistory()]);
+    userInteraction.aboutInfo = info;
+    userInteraction.versions = versions;
   });
+  let isMain = $derived(info?.sourceRef === 'main' && info.repository === 'immich-app/immich');
+  let version = $derived(
+    $serverVersion ? `v${$serverVersion.major}.${$serverVersion.minor}.${$serverVersion.patch}` : null,
+  );
 </script>
 
-{#if isOpen}
-  <ServerAboutModal onClose={() => (isOpen = false)} info={aboutInfo} />
+{#if isOpen && info}
+  <ServerAboutModal onClose={() => (isOpen = false)} {info} {versions} />
 {/if}
 
 <div
@@ -29,19 +46,25 @@
 >
   {#if $connected}
     <div class="flex gap-2 place-items-center place-content-center">
-      <div class="w-[7px] h-[7px] bg-green-500 rounded-full" />
+      <div class="w-[7px] h-[7px] bg-green-500 rounded-full"></div>
       <p class="dark:text-immich-gray">{$t('server_online')}</p>
     </div>
   {:else}
     <div class="flex gap-2 place-items-center place-content-center">
-      <div class="w-[7px] h-[7px] bg-red-500 rounded-full" />
+      <div class="w-[7px] h-[7px] bg-red-500 rounded-full"></div>
       <p class="text-red-500">{$t('server_offline')}</p>
     </div>
   {/if}
 
   <div class="flex justify-between justify-items-center">
     {#if $connected && version}
-      <button type="button" on:click={() => (isOpen = true)} class="dark:text-immich-gray">{version}</button>
+      <button type="button" onclick={() => (isOpen = true)} class="dark:text-immich-gray flex gap-1">
+        {#if isMain}
+          <Icon path={mdiAlert} size="1.5em" color="#ffcc4d" /> {info?.sourceRef}
+        {:else}
+          {version}
+        {/if}
+      </button>
     {:else}
       <p class="text-red-500">{$t('unknown')}</p>
     {/if}
diff --git a/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte b/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte
index 4590b12255..c7f8cd4fae 100644
--- a/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte
+++ b/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte
@@ -1,65 +1,80 @@
 <script lang="ts">
-  import { fade } from 'svelte/transition';
   import Icon from '$lib/components/elements/icon.svelte';
-  import { mdiInformationOutline } from '@mdi/js';
+  import { mdiChevronDown, mdiChevronLeft } from '@mdi/js';
   import { resolveRoute } from '$app/paths';
-  import { page } from '$app/stores';
+  import { page } from '$app/state';
+  import type { Snippet } from 'svelte';
+  import { t } from 'svelte-i18n';
 
-  export let title: string;
-  export let routeId: string;
-  export let icon: string;
-  export let flippedLogo = false;
-  export let isSelected = false;
-  export let preloadData = true;
+  interface Props {
+    title: string;
+    routeId: string;
+    icon: string;
+    flippedLogo?: boolean;
+    isSelected?: boolean;
+    preloadData?: boolean;
+    moreInformation?: Snippet;
+    dropDownContent?: Snippet;
+    dropdownOpen?: boolean;
+  }
 
-  let showMoreInformation = false;
-  $: routePath = resolveRoute(routeId, {});
-  $: isSelected = ($page.route.id?.match(/^\/(admin|\(user\))\/[^/]*/) || [])[0] === routeId;
+  let {
+    title,
+    routeId,
+    icon,
+    flippedLogo = false,
+    isSelected = $bindable(false),
+    preloadData = true,
+    dropDownContent: hasDropdown,
+    dropdownOpen = $bindable(false),
+  }: Props = $props();
+
+  let routePath = $derived(resolveRoute(routeId, {}));
+
+  $effect(() => {
+    isSelected = (page.route.id?.match(/^\/(admin|\(user\))\/[^/]*/) || [])[0] === routeId;
+  });
 </script>
 
-<a
-  href={routePath}
-  data-sveltekit-preload-data={preloadData ? 'hover' : 'off'}
-  draggable="false"
-  aria-current={isSelected ? 'page' : undefined}
-  class="flex w-full place-items-center justify-between gap-4 rounded-r-full py-3 transition-[padding] delay-100 duration-100 hover:cursor-pointer hover:bg-immich-gray hover:text-immich-primary dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray dark:hover:text-immich-dark-primary
+<div class="relative">
+  {#if hasDropdown}
+    <span class="hidden md:block absolute left-1 z-50 h-full">
+      <button
+        type="button"
+        aria-label={$t('recent-albums')}
+        class="relative flex cursor-default pt-4 pb-4 select-none justify-center hover:cursor-pointer hover:bg-immich-gray hover:fill-gray hover:text-immich-primary dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray dark:hover:text-immich-dark-primary rounded h-fill"
+        onclick={() => (dropdownOpen = !dropdownOpen)}
+      >
+        <Icon
+          path={dropdownOpen ? mdiChevronDown : mdiChevronLeft}
+          size="1em"
+          class="shrink-0 delay-100 duration-100 "
+          flipped={flippedLogo}
+          ariaHidden
+        />
+      </button>
+    </span>
+  {/if}
+  <a
+    href={routePath}
+    data-sveltekit-preload-data={preloadData ? 'hover' : 'off'}
+    draggable="false"
+    aria-current={isSelected ? 'page' : undefined}
+    class="flex w-full place-items-center gap-4 rounded-r-full py-3 transition-[padding] delay-100 duration-100 hover:cursor-pointer hover:bg-immich-gray hover:text-immich-primary dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray dark:hover:text-immich-dark-primary
     {isSelected
-    ? 'bg-immich-primary/10 text-immich-primary hover:bg-immich-primary/10 dark:bg-immich-dark-primary/10 dark:text-immich-dark-primary'
-    : ''}
+      ? 'bg-immich-primary/10 text-immich-primary hover:bg-immich-primary/10 dark:bg-immich-dark-primary/10 dark:text-immich-dark-primary'
+      : ''}
 		pl-5 group-hover:sm:px-5 md:px-5
   "
->
-  <div class="flex w-full place-items-center gap-4 overflow-hidden truncate">
-    <Icon path={icon} size="1.5em" class="shrink-0" flipped={flippedLogo} ariaHidden />
-    <span class="text-sm font-medium">{title}</span>
-  </div>
-
-  <div
-    class="h-0 overflow-hidden transition-[height] delay-1000 duration-100 sm:group-hover:h-auto group-hover:sm:overflow-visible md:h-auto md:overflow-visible"
   >
-    {#if $$slots.moreInformation}
-      <!-- svelte-ignore a11y-no-static-element-interactions -->
-      <div
-        class="relative flex cursor-default select-none justify-center"
-        on:mouseenter={() => (showMoreInformation = true)}
-        on:mouseleave={() => (showMoreInformation = false)}
-      >
-        <div class="p-1 text-gray-600 hover:cursor-help dark:text-gray-400">
-          <Icon path={mdiInformationOutline} />
-        </div>
+    <div class="flex w-full place-items-center gap-4 overflow-hidden truncate">
+      <Icon path={icon} size="1.5em" class="shrink-0" flipped={flippedLogo} ariaHidden />
+      <span class="text-sm font-medium">{title}</span>
+    </div>
+    <div></div>
+  </a>
+</div>
 
-        {#if showMoreInformation}
-          <div class="absolute right-6 top-0">
-            <div
-              class="flex place-content-center place-items-center whitespace-nowrap rounded-3xl border bg-immich-bg px-6 py-3 text-xs text-immich-fg shadow-lg dark:border-immich-dark-gray dark:bg-gray-600 dark:text-immich-dark-fg"
-              class:hidden={!showMoreInformation}
-              transition:fade={{ duration: 200 }}
-            >
-              <slot name="moreInformation" />
-            </div>
-          </div>
-        {/if}
-      </div>
-    {/if}
-  </div>
-</a>
+{#if hasDropdown && dropdownOpen}
+  {@render hasDropdown?.()}
+{/if}
diff --git a/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte b/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte
index 233010153f..37867da7af 100644
--- a/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte
+++ b/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte
@@ -1,4 +1,11 @@
 <script lang="ts">
+  import type { Snippet } from 'svelte';
+
+  interface Props {
+    children?: Snippet;
+  }
+
+  let { children }: Props = $props();
 </script>
 
 <section
@@ -6,5 +13,5 @@
   tabindex="-1"
   class="immich-scrollbar group relative z-10 flex w-18 flex-col gap-1 overflow-y-auto bg-immich-bg pt-8 transition-all duration-200 dark:bg-immich-dark-bg hover:sm:w-64 hover:sm:border-r hover:sm:pr-6 hover:sm:shadow-2xl hover:sm:dark:border-r-immich-dark-gray md:w-64 md:pr-6 hover:md:border-none hover:md:shadow-none"
 >
-  <slot />
+  {@render children?.()}
 </section>
diff --git a/web/src/lib/components/shared-components/side-bar/side-bar.svelte b/web/src/lib/components/shared-components/side-bar/side-bar.svelte
index fab7c6ed6d..9c49b971ba 100644
--- a/web/src/lib/components/shared-components/side-bar/side-bar.svelte
+++ b/web/src/lib/components/shared-components/side-bar/side-bar.svelte
@@ -24,20 +24,21 @@
   } from '@mdi/js';
   import SideBarSection from './side-bar-section.svelte';
   import SideBarLink from './side-bar-link.svelte';
-  import MoreInformationAssets from '$lib/components/shared-components/side-bar/more-information-assets.svelte';
-  import MoreInformationAlbums from '$lib/components/shared-components/side-bar/more-information-albums.svelte';
   import { t } from 'svelte-i18n';
   import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
   import { preferences } from '$lib/stores/user.store';
+  import { recentAlbumsDropdown } from '$lib/stores/preferences.store';
+  import RecentAlbums from '$lib/components/shared-components/side-bar/recent-albums.svelte';
+  import { fly } from 'svelte/transition';
 
-  let isArchiveSelected: boolean;
-  let isFavoritesSelected: boolean;
-  let isMapSelected: boolean;
-  let isPeopleSelected: boolean;
-  let isPhotosSelected: boolean;
-  let isSharingSelected: boolean;
-  let isTrashSelected: boolean;
-  let isUtilitiesSelected: boolean;
+  let isArchiveSelected: boolean = $state(false);
+  let isFavoritesSelected: boolean = $state(false);
+  let isMapSelected: boolean = $state(false);
+  let isPeopleSelected: boolean = $state(false);
+  let isPhotosSelected: boolean = $state(false);
+  let isSharingSelected: boolean = $state(false);
+  let isTrashSelected: boolean = $state(false);
+  let isUtilitiesSelected: boolean = $state(false);
 </script>
 
 <SideBarSection>
@@ -47,11 +48,7 @@
       routeId="/(user)/photos"
       bind:isSelected={isPhotosSelected}
       icon={isPhotosSelected ? mdiImageMultiple : mdiImageMultipleOutline}
-    >
-      <svelte:fragment slot="moreInformation">
-        <MoreInformationAssets assetStats={{ isArchived: false }} />
-      </svelte:fragment>
-    </SideBarLink>
+    ></SideBarLink>
 
     {#if $featureFlags.search}
       <SideBarLink title={$t('explore')} routeId="/(user)/explore" icon={mdiMagnify} />
@@ -80,11 +77,7 @@
       routeId="/(user)/sharing"
       icon={isSharingSelected ? mdiAccountMultiple : mdiAccountMultipleOutline}
       bind:isSelected={isSharingSelected}
-    >
-      <svelte:fragment slot="moreInformation">
-        <MoreInformationAlbums albumType="shared" />
-      </svelte:fragment>
-    </SideBarLink>
+    ></SideBarLink>
 
     <div class="text-xs transition-all duration-200 dark:text-immich-dark-fg">
       <p class="hidden p-6 group-hover:sm:block md:block">{$t('library').toUpperCase()}</p>
@@ -96,16 +89,20 @@
       routeId="/(user)/favorites"
       icon={isFavoritesSelected ? mdiHeart : mdiHeartOutline}
       bind:isSelected={isFavoritesSelected}
-    >
-      <svelte:fragment slot="moreInformation">
-        <MoreInformationAssets assetStats={{ isFavorite: true }} />
-      </svelte:fragment>
-    </SideBarLink>
+    ></SideBarLink>
 
-    <SideBarLink title={$t('albums')} routeId="/(user)/albums" icon={mdiImageAlbum} flippedLogo>
-      <svelte:fragment slot="moreInformation">
-        <MoreInformationAlbums albumType="owned" />
-      </svelte:fragment>
+    <SideBarLink
+      title={$t('albums')}
+      routeId="/(user)/albums"
+      icon={mdiImageAlbum}
+      flippedLogo
+      bind:dropdownOpen={$recentAlbumsDropdown}
+    >
+      {#snippet dropDownContent()}
+        <span in:fly={{ y: -20 }} class="hidden md:block">
+          <RecentAlbums />
+        </span>
+      {/snippet}
     </SideBarLink>
 
     {#if $preferences.tags.enabled && $preferences.tags.sidebarWeb}
@@ -128,11 +125,7 @@
       routeId="/(user)/archive"
       bind:isSelected={isArchiveSelected}
       icon={isArchiveSelected ? mdiArchiveArrowDown : mdiArchiveArrowDownOutline}
-    >
-      <svelte:fragment slot="moreInformation">
-        <MoreInformationAssets assetStats={{ isArchived: true }} />
-      </svelte:fragment>
-    </SideBarLink>
+    ></SideBarLink>
 
     {#if $featureFlags.trash}
       <SideBarLink
@@ -140,11 +133,7 @@
         routeId="/(user)/trash"
         bind:isSelected={isTrashSelected}
         icon={isTrashSelected ? mdiTrashCan : mdiTrashCanOutline}
-      >
-        <svelte:fragment slot="moreInformation">
-          <MoreInformationAssets assetStats={{ isTrashed: true }} />
-        </svelte:fragment>
-      </SideBarLink>
+      ></SideBarLink>
     {/if}
   </nav>
 
diff --git a/web/src/lib/components/shared-components/side-bar/storage-space.svelte b/web/src/lib/components/shared-components/side-bar/storage-space.svelte
index 415ce307a2..9472397565 100644
--- a/web/src/lib/components/shared-components/side-bar/storage-space.svelte
+++ b/web/src/lib/components/shared-components/side-bar/storage-space.svelte
@@ -1,24 +1,19 @@
 <script lang="ts">
-  import ServerAboutModal from '$lib/components/shared-components/server-about-modal.svelte';
   import { locale } from '$lib/stores/preferences.store';
-  import { serverInfo } from '$lib/stores/server-info.store';
   import { user } from '$lib/stores/user.store';
   import { requestServerInfo } from '$lib/utils/auth';
   import { onMount } from 'svelte';
   import { t } from 'svelte-i18n';
-  import { getByteUnitString } from '../../../utils/byte-units';
+  import { getByteUnitString } from '$lib/utils/byte-units';
   import LoadingSpinner from '../loading-spinner.svelte';
-  import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk';
+  import { userInteraction } from '$lib/stores/user.svelte';
 
-  let usageClasses = '';
-  let isOpen = false;
+  let usageClasses = $state('');
 
-  $: hasQuota = $user?.quotaSizeInBytes !== null;
-  $: availableBytes = (hasQuota ? $user?.quotaSizeInBytes : $serverInfo?.diskSizeRaw) || 0;
-  $: usedBytes = (hasQuota ? $user?.quotaUsageInBytes : $serverInfo?.diskUseRaw) || 0;
-  $: usedPercentage = Math.min(Math.round((usedBytes / availableBytes) * 100), 100);
-
-  let aboutInfo: ServerAboutResponseDto;
+  let hasQuota = $derived($user?.quotaSizeInBytes !== null);
+  let availableBytes = $derived((hasQuota ? $user?.quotaSizeInBytes : userInteraction.serverInfo?.diskSizeRaw) || 0);
+  let usedBytes = $derived((hasQuota ? $user?.quotaUsageInBytes : userInteraction.serverInfo?.diskUseRaw) || 0);
+  let usedPercentage = $derived(Math.min(Math.round((usedBytes / availableBytes) * 100), 100));
 
   const onUpdate = () => {
     usageClasses = getUsageClass();
@@ -36,20 +31,20 @@
     return 'bg-immich-primary dark:bg-immich-dark-primary';
   };
 
-  $: if ($user) {
-    onUpdate();
-  }
+  $effect(() => {
+    if ($user) {
+      onUpdate();
+    }
+  });
 
   onMount(async () => {
+    if (userInteraction.serverInfo && $user) {
+      return;
+    }
     await requestServerInfo();
-    aboutInfo = await getAboutInfo();
   });
 </script>
 
-{#if isOpen}
-  <ServerAboutModal onClose={() => (isOpen = false)} info={aboutInfo} />
-{/if}
-
 <div
   class="hidden md:block storage-status p-4 bg-gray-100 dark:bg-immich-dark-primary/10 ml-4 rounded-lg text-sm"
   title={$t('storage_usage', {
@@ -62,7 +57,7 @@
   <div class="hidden group-hover:sm:block md:block">
     <p class="font-medium text-immich-dark-gray dark:text-white mb-2">{$t('storage')}</p>
 
-    {#if $serverInfo}
+    {#if userInteraction.serverInfo}
       <p class="text-gray-500 dark:text-gray-300">
         {$t('storage_usage', {
           values: {
@@ -73,7 +68,7 @@
       </p>
 
       <div class="mt-4 h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
-        <div class="h-[7px] rounded-full {usageClasses}" style="width: {usedPercentage}%" />
+        <div class="h-[7px] rounded-full {usageClasses}" style="width: {usedPercentage}%"></div>
       </div>
     {:else}
       <div class="mt-2">
diff --git a/web/src/lib/components/shared-components/side-bar/supporter-badge.svelte b/web/src/lib/components/shared-components/side-bar/supporter-badge.svelte
index f2cb326c39..3d5e815996 100644
--- a/web/src/lib/components/shared-components/side-bar/supporter-badge.svelte
+++ b/web/src/lib/components/shared-components/side-bar/supporter-badge.svelte
@@ -2,8 +2,12 @@
   import { t } from 'svelte-i18n';
   import ImmichLogo from '../immich-logo.svelte';
 
-  export let centered = false;
-  export let logoSize: 'sm' | 'lg' = 'sm';
+  interface Props {
+    centered?: boolean;
+    logoSize?: 'sm' | 'lg';
+  }
+
+  let { centered = false, logoSize = 'sm' }: Props = $props();
 </script>
 
 <div
diff --git a/web/src/lib/components/shared-components/single-grid-row.svelte b/web/src/lib/components/shared-components/single-grid-row.svelte
index 90020f2922..7764b9eb17 100644
--- a/web/src/lib/components/shared-components/single-grid-row.svelte
+++ b/web/src/lib/components/shared-components/single-grid-row.svelte
@@ -1,10 +1,14 @@
 <script lang="ts">
-  let className = '';
-  export { className as class };
-  export let itemCount = 1;
+  interface Props {
+    class?: string;
+    itemCount?: number;
+    children?: import('svelte').Snippet<[{ itemCount: number }]>;
+  }
 
-  let container: HTMLElement | undefined;
-  let contentRect: DOMRectReadOnly | undefined;
+  let { class: className = '', itemCount = $bindable(1), children }: Props = $props();
+
+  let container: HTMLElement | undefined = $state();
+  let contentRect: DOMRectReadOnly | undefined = $state();
 
   const getGridGap = (element: Element) => {
     const style = getComputedStyle(element);
@@ -28,11 +32,13 @@
     return Math.floor((containerWidth + columnGap) / (childWidth + columnGap)) || 1;
   };
 
-  $: if (container && contentRect) {
-    itemCount = getItemCount(container, contentRect.width);
-  }
+  $effect(() => {
+    if (container && contentRect) {
+      itemCount = getItemCount(container, contentRect.width);
+    }
+  });
 </script>
 
 <div class={className} bind:this={container} bind:contentRect>
-  <slot {itemCount} />
+  {@render children?.({ itemCount })}
 </div>
diff --git a/web/src/lib/components/shared-components/star-rating.svelte b/web/src/lib/components/shared-components/star-rating.svelte
index ee1b2b7433..333248c227 100644
--- a/web/src/lib/components/shared-components/star-rating.svelte
+++ b/web/src/lib/components/shared-components/star-rating.svelte
@@ -5,17 +5,23 @@
   import { generateId } from '$lib/utils/generate-id';
   import { t } from 'svelte-i18n';
 
-  export let count = 5;
-  export let rating: number;
-  export let readOnly = false;
-  export let onRating: (rating: number) => void | undefined;
+  interface Props {
+    count?: number;
+    rating: number;
+    readOnly?: boolean;
+    onRating: (rating: number) => void | undefined;
+  }
 
-  let ratingSelection = 0;
-  let hoverRating = 0;
-  let focusRating = 0;
+  let { count = 5, rating, readOnly = false, onRating }: Props = $props();
+
+  let ratingSelection = $state(rating);
+  let hoverRating = $state(0);
+  let focusRating = $state(0);
   let timeoutId: ReturnType<typeof setTimeout> | undefined;
 
-  $: ratingSelection = rating;
+  $effect(() => {
+    ratingSelection = rating;
+  });
 
   const starIcon =
     'M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.007 5.404.433c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.433 2.082-5.006z';
@@ -53,10 +59,10 @@
   };
 </script>
 
-<!-- svelte-ignore a11y-mouse-events-have-key-events -->
+<!-- svelte-ignore a11y_mouse_events_have_key_events -->
 <fieldset
   class="text-immich-primary dark:text-immich-dark-primary w-fit cursor-default"
-  on:mouseleave={() => setHoverRating(0)}
+  onmouseleave={() => setHoverRating(0)}
   use:focusOutside={{ onFocusOut: reset }}
   use:shortcuts={[
     { shortcut: { key: 'ArrowLeft' }, preventDefault: false, onShortcut: (event) => event.stopPropagation() },
@@ -69,13 +75,13 @@
       {@const value = index + 1}
       {@const filled = hoverRating >= value || (hoverRating === 0 && ratingSelection >= value)}
       {@const starId = `${id}-${value}`}
-      <!-- svelte-ignore a11y-mouse-events-have-key-events -->
-      <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
+      <!-- svelte-ignore a11y_mouse_events_have_key_events -->
+      <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
       <label
         for={starId}
         class:cursor-pointer={!readOnly}
         class:ring-2={focusRating === value}
-        on:mouseover={() => setHoverRating(value)}
+        onmouseover={() => setHoverRating(value)}
         tabindex={-1}
         data-testid="star"
       >
@@ -96,10 +102,10 @@
         id={starId}
         bind:group={ratingSelection}
         disabled={readOnly}
-        on:focus={() => {
+        onfocus={() => {
           focusRating = value;
         }}
-        on:change={() => handleSelectDebounced(value)}
+        onchange={() => handleSelectDebounced(value)}
         class="sr-only"
       />
     {/each}
@@ -108,7 +114,7 @@
 {#if ratingSelection > 0 && !readOnly}
   <button
     type="button"
-    on:click={() => {
+    onclick={() => {
       ratingSelection = 0;
       handleSelect(ratingSelection);
     }}
diff --git a/web/src/lib/components/shared-components/theme-button.svelte b/web/src/lib/components/shared-components/theme-button.svelte
index 5a78a7005e..446668256f 100644
--- a/web/src/lib/components/shared-components/theme-button.svelte
+++ b/web/src/lib/components/shared-components/theme-button.svelte
@@ -5,11 +5,15 @@
   import { colorTheme, handleToggleTheme } from '$lib/stores/preferences.store';
   import { t } from 'svelte-i18n';
 
-  $: icon = $colorTheme.value === Theme.LIGHT ? moonPath : sunPath;
-  $: viewBox = $colorTheme.value === Theme.LIGHT ? moonViewBox : sunViewBox;
-  $: isDark = $colorTheme.value === Theme.DARK;
+  let icon = $derived($colorTheme.value === Theme.LIGHT ? moonPath : sunPath);
+  let viewBox = $derived($colorTheme.value === Theme.LIGHT ? moonViewBox : sunViewBox);
+  let isDark = $derived($colorTheme.value === Theme.DARK);
 
-  export let padding: Padding = '3';
+  interface Props {
+    padding?: Padding;
+  }
+
+  let { padding = '3' }: Props = $props();
 </script>
 
 {#if !$colorTheme.system}
@@ -19,7 +23,7 @@
     {viewBox}
     role="switch"
     aria-checked={isDark ? 'true' : 'false'}
-    on:click={handleToggleTheme}
+    onclick={handleToggleTheme}
     {padding}
   />
 {/if}
diff --git a/web/src/lib/components/shared-components/tree/breadcrumbs.svelte b/web/src/lib/components/shared-components/tree/breadcrumbs.svelte
index a3c49a1430..1d841339bc 100644
--- a/web/src/lib/components/shared-components/tree/breadcrumbs.svelte
+++ b/web/src/lib/components/shared-components/tree/breadcrumbs.svelte
@@ -4,12 +4,16 @@
   import { mdiArrowUpLeft, mdiChevronRight } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  export let pathSegments: string[] = [];
-  export let getLink: (path: string) => string;
-  export let title: string;
-  export let icon: string;
+  interface Props {
+    pathSegments?: string[];
+    getLink: (path: string) => string;
+    title: string;
+    icon: string;
+  }
 
-  $: isRoot = pathSegments.length === 0;
+  let { pathSegments = [], getLink, title, icon }: Props = $props();
+
+  let isRoot = $derived(pathSegments.length === 0);
 </script>
 
 <nav class="flex items-center py-2">
@@ -21,6 +25,7 @@
         href={getLink(pathSegments.slice(0, -1).join('/'))}
         class="mr-2"
         padding="2"
+        onclick={() => {}}
       />
     </div>
   {/if}
@@ -37,6 +42,7 @@
           size="1.25em"
           padding="2"
           aria-current={isRoot ? 'page' : undefined}
+          onclick={() => {}}
         />
       </li>
       {#each pathSegments as segment, index}
diff --git a/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte b/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte
index 759a3e5e65..1b4d30d050 100644
--- a/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte
+++ b/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte
@@ -1,9 +1,13 @@
 <script lang="ts">
   import Icon from '$lib/components/elements/icon.svelte';
 
-  export let items: string[] = [];
-  export let icon: string;
-  export let onClick: (path: string) => void;
+  interface Props {
+    items?: string[];
+    icon: string;
+    onClick: (path: string) => void;
+  }
+
+  let { items = [], icon, onClick }: Props = $props();
 </script>
 
 {#if items.length > 0}
@@ -13,7 +17,7 @@
     {#each items as item}
       <button
         class="flex flex-col place-items-center gap-2 py-2 px-4 hover:bg-immich-primary/10 dark:hover:bg-immich-primary/40 rounded-xl"
-        on:click={() => onClick(item)}
+        onclick={() => onClick(item)}
         title={item}
         type="button"
       >
diff --git a/web/src/lib/components/shared-components/tree/tree-items.svelte b/web/src/lib/components/shared-components/tree/tree-items.svelte
index 4bdc95db9f..c6db9fec8d 100644
--- a/web/src/lib/components/shared-components/tree/tree-items.svelte
+++ b/web/src/lib/components/shared-components/tree/tree-items.svelte
@@ -2,12 +2,16 @@
   import Tree from '$lib/components/shared-components/tree/tree.svelte';
   import { normalizeTreePath, type RecursiveObject } from '$lib/utils/tree-utils';
 
-  export let items: RecursiveObject;
-  export let parent = '';
-  export let active = '';
-  export let icons: { default: string; active: string };
-  export let getLink: (path: string) => string;
-  export let getColor: (path: string) => string | undefined = () => undefined;
+  interface Props {
+    items: RecursiveObject;
+    parent?: string;
+    active?: string;
+    icons: { default: string; active: string };
+    getLink: (path: string) => string;
+    getColor?: (path: string) => string | undefined;
+  }
+
+  let { items, parent = '', active = '', icons, getLink, getColor = () => undefined }: Props = $props();
 </script>
 
 <ul class="list-none ml-2">
diff --git a/web/src/lib/components/shared-components/tree/tree.svelte b/web/src/lib/components/shared-components/tree/tree.svelte
index 5c4b367a54..c6a13ec197 100644
--- a/web/src/lib/components/shared-components/tree/tree.svelte
+++ b/web/src/lib/components/shared-components/tree/tree.svelte
@@ -4,19 +4,31 @@
   import { normalizeTreePath, type RecursiveObject } from '$lib/utils/tree-utils';
   import { mdiChevronDown, mdiChevronRight } from '@mdi/js';
 
-  export let tree: RecursiveObject;
-  export let parent: string;
-  export let value: string;
-  export let active = '';
-  export let icons: { default: string; active: string };
-  export let getLink: (path: string) => string;
-  export let getColor: (path: string) => string | undefined;
+  interface Props {
+    tree: RecursiveObject;
+    parent: string;
+    value: string;
+    active?: string;
+    icons: { default: string; active: string };
+    getLink: (path: string) => string;
+    getColor: (path: string) => string | undefined;
+  }
 
-  $: path = normalizeTreePath(`${parent}/${value}`);
-  $: isActive = active === path || active.startsWith(`${path}/`);
-  $: isOpen = isActive;
-  $: isTarget = active === path;
-  $: color = getColor(path);
+  let { tree, parent, value, active = '', icons, getLink, getColor }: Props = $props();
+
+  let path = $derived(normalizeTreePath(`${parent}/${value}`));
+  let isActive = $derived(active === path || active.startsWith(`${path}/`));
+  let isOpen = $state(false);
+  $effect(() => {
+    isOpen = isActive;
+  });
+  let isTarget = $derived(active === path);
+  let color = $derived(getColor(path));
+
+  const onclick = (event: MouseEvent) => {
+    event.preventDefault();
+    isOpen = !isOpen;
+  };
 </script>
 
 <a
@@ -24,11 +36,7 @@
   title={value}
   class={`flex flex-grow place-items-center pl-2 py-1 text-sm rounded-lg hover:bg-slate-200 dark:hover:bg-slate-800 hover:font-semibold ${isTarget ? 'bg-slate-100 dark:bg-slate-700 font-semibold text-immich-primary dark:text-immich-dark-primary' : 'dark:text-gray-200'}`}
 >
-  <button
-    type="button"
-    on:click|preventDefault={() => (isOpen = !isOpen)}
-    class={Object.values(tree).length === 0 ? 'invisible' : ''}
-  >
+  <button type="button" {onclick} class={Object.values(tree).length === 0 ? 'invisible' : ''}>
     <Icon path={isOpen ? mdiChevronDown : mdiChevronRight} class="text-gray-400" size={20} />
   </button>
   <div>
diff --git a/web/src/lib/components/shared-components/upload-asset-preview.svelte b/web/src/lib/components/shared-components/upload-asset-preview.svelte
index d0abf12ab5..bd3b7856d1 100644
--- a/web/src/lib/components/shared-components/upload-asset-preview.svelte
+++ b/web/src/lib/components/shared-components/upload-asset-preview.svelte
@@ -20,7 +20,11 @@
   import { t } from 'svelte-i18n';
   import { fade } from 'svelte/transition';
 
-  export let uploadAsset: UploadAsset;
+  interface Props {
+    uploadAsset: UploadAsset;
+  }
+
+  let { uploadAsset }: Props = $props();
 
   const handleDismiss = (uploadAsset: UploadAsset) => {
     uploadAssetsStore.removeItem(uploadAsset.id);
@@ -74,16 +78,16 @@
         >
           <Icon path={mdiOpenInNew} size="20" />
         </a>
-        <button type="button" on:click={() => handleDismiss(uploadAsset)} class="" aria-hidden="true" tabindex={-1}>
+        <button type="button" onclick={() => handleDismiss(uploadAsset)} class="" aria-hidden="true" tabindex={-1}>
           <Icon path={mdiClose} size="20" />
         </button>
       </div>
     {:else if uploadAsset.state === UploadState.ERROR}
       <div class="flex items-center justify-between gap-1">
-        <button type="button" on:click={() => handleRetry(uploadAsset)} class="" aria-hidden="true" tabindex={-1}>
+        <button type="button" onclick={() => handleRetry(uploadAsset)} class="" aria-hidden="true" tabindex={-1}>
           <Icon path={mdiRestart} size="20" />
         </button>
-        <button type="button" on:click={() => handleDismiss(uploadAsset)} class="" aria-hidden="true" tabindex={-1}>
+        <button type="button" onclick={() => handleDismiss(uploadAsset)} class="" aria-hidden="true" tabindex={-1}>
           <Icon path={mdiClose} size="20" />
         </button>
       </div>
@@ -92,7 +96,7 @@
 
   {#if uploadAsset.state === UploadState.STARTED}
     <div class="text-black relative mt-[5px] h-[15px] w-full rounded-md bg-gray-300 dark:bg-immich-dark-gray">
-      <div class="h-[15px] rounded-md bg-immich-primary transition-all" style={`width: ${uploadAsset.progress}%`} />
+      <div class="h-[15px] rounded-md bg-immich-primary transition-all" style={`width: ${uploadAsset.progress}%`}></div>
       <p class="absolute top-0 h-full w-full text-center text-[10px]">
         {#if uploadAsset.message}
           {uploadAsset.message}
diff --git a/web/src/lib/components/shared-components/upload-panel.svelte b/web/src/lib/components/shared-components/upload-panel.svelte
index d536053286..0eb7d1655c 100644
--- a/web/src/lib/components/shared-components/upload-panel.svelte
+++ b/web/src/lib/components/shared-components/upload-panel.svelte
@@ -11,32 +11,24 @@
   import { t } from 'svelte-i18n';
   import { locale } from '$lib/stores/preferences.store';
 
-  let showDetail = false;
-  let showOptions = false;
-  let concurrency = uploadExecutionQueue.concurrency;
+  let showDetail = $state(false);
+  let showOptions = $state(false);
+  let concurrency = $state(uploadExecutionQueue.concurrency);
 
   let { stats, isDismissible, isUploading, remainingUploads } = uploadAssetsStore;
 
-  const autoHide = () => {
-    if (!$isUploading && showDetail) {
-      showDetail = false;
-    }
-
-    if ($isUploading && !showDetail) {
+  $effect(() => {
+    if ($isUploading) {
       showDetail = true;
     }
-  };
-
-  $: if ($isUploading) {
-    autoHide();
-  }
+  });
 </script>
 
 {#if $isUploading}
   <div
     in:fade={{ duration: 250 }}
     out:fade={{ duration: 250 }}
-    on:outroend={() => {
+    onoutroend={() => {
       if ($stats.errors > 0) {
         notificationController.show({
           message: $t('upload_errors', { values: { count: $stats.errors } }),
@@ -56,7 +48,7 @@
       }
       uploadAssetsStore.reset();
     }}
-    class="fixed bottom-6 right-6 z-[10000]"
+    class="fixed bottom-6 right-16 z-[10000]"
   >
     {#if showDetail}
       <div
@@ -92,14 +84,14 @@
                 icon={mdiCog}
                 size="14"
                 padding="1"
-                on:click={() => (showOptions = !showOptions)}
+                onclick={() => (showOptions = !showOptions)}
               />
               <CircleIconButton
                 title={$t('minimize')}
                 icon={mdiWindowMinimize}
                 size="14"
                 padding="1"
-                on:click={() => (showDetail = false)}
+                onclick={() => (showDetail = false)}
               />
             </div>
             {#if $isDismissible}
@@ -108,7 +100,7 @@
                 icon={mdiCancel}
                 size="14"
                 padding="1"
-                on:click={() => uploadAssetsStore.dismissErrors()}
+                onclick={() => uploadAssetsStore.dismissErrors()}
               />
             {/if}
           </div>
@@ -128,7 +120,7 @@
               max="50"
               step="1"
               bind:value={concurrency}
-              on:change={() => (uploadExecutionQueue.concurrency = concurrency)}
+              onchange={() => (uploadExecutionQueue.concurrency = concurrency)}
             />
           </div>
         {/if}
@@ -143,7 +135,7 @@
         <button
           type="button"
           in:scale={{ duration: 250, easing: quartInOut }}
-          on:click={() => (showDetail = true)}
+          onclick={() => (showDetail = true)}
           class="absolute -left-4 -top-4 flex h-10 w-10 place-content-center place-items-center rounded-full bg-immich-primary p-5 text-xs text-gray-200"
         >
           {$remainingUploads.toLocaleString($locale)}
@@ -152,7 +144,7 @@
           <button
             type="button"
             in:scale={{ duration: 250, easing: quartInOut }}
-            on:click={() => (showDetail = true)}
+            onclick={() => (showDetail = true)}
             class="absolute -right-4 -top-4 flex h-10 w-10 place-content-center place-items-center rounded-full bg-immich-error p-5 text-xs text-gray-200"
           >
             {$stats.errors.toLocaleString($locale)}
@@ -161,7 +153,7 @@
         <button
           type="button"
           in:scale={{ duration: 250, easing: quartInOut }}
-          on:click={() => (showDetail = true)}
+          onclick={() => (showDetail = true)}
           class="flex h-16 w-16 place-content-center place-items-center rounded-full bg-gray-200 p-5 text-sm text-immich-primary shadow-lg dark:bg-gray-600 dark:text-immich-gray"
         >
           <div class="animate-pulse">
diff --git a/web/src/lib/components/shared-components/user-avatar.svelte b/web/src/lib/components/shared-components/user-avatar.svelte
index bb59af9aab..938f569508 100644
--- a/web/src/lib/components/shared-components/user-avatar.svelte
+++ b/web/src/lib/components/shared-components/user-avatar.svelte
@@ -1,4 +1,4 @@
-<script lang="ts" context="module">
+<script lang="ts" module>
   export type Size = 'full' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl';
 </script>
 
@@ -13,27 +13,37 @@
     email: string;
     profileImagePath: string;
     avatarColor: UserAvatarColor;
+    profileChangedAt: string;
   }
 
-  export let user: User;
-  export let color: UserAvatarColor | undefined = undefined;
-  export let size: Size = 'full';
-  export let rounded = true;
-  export let interactive = false;
-  export let showTitle = true;
-  export let showProfileImage = true;
-  export let label: string | undefined = undefined;
+  interface Props {
+    user: User;
+    color?: UserAvatarColor | undefined;
+    size?: Size;
+    rounded?: boolean;
+    interactive?: boolean;
+    showTitle?: boolean;
+    showProfileImage?: boolean;
+    label?: string | undefined;
+  }
 
-  let img: HTMLImageElement;
-  let showFallback = true;
+  let {
+    user,
+    color = undefined,
+    size = 'full',
+    rounded = true,
+    interactive = false,
+    showTitle = true,
+    showProfileImage = true,
+    label = undefined,
+  }: Props = $props();
 
-  // sveeeeeeelteeeeee fiveeeeee
-  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
-  $: img, user, void tryLoadImage();
+  let img: HTMLImageElement | undefined = $state();
+  let showFallback = $state(true);
 
   const tryLoadImage = async () => {
     try {
-      await img.decode();
+      await img?.decode();
       showFallback = false;
     } catch {
       showFallback = true;
@@ -63,12 +73,20 @@
     xxxl: 'w-28 h-28',
   };
 
-  $: colorClass = colorClasses[color || user.avatarColor];
-  $: sizeClass = sizeClasses[size];
-  $: title = label ?? `${user.name} (${user.email})`;
-  $: interactiveClass = interactive
-    ? 'border-2 border-immich-primary hover:border-immich-dark-primary dark:hover:border-immich-primary dark:border-immich-dark-primary transition-colors'
-    : '';
+  $effect(() => {
+    if (img && user) {
+      tryLoadImage().catch(console.error);
+    }
+  });
+
+  let colorClass = $derived(colorClasses[color || user.avatarColor]);
+  let sizeClass = $derived(sizeClasses[size]);
+  let title = $derived(label ?? `${user.name} (${user.email})`);
+  let interactiveClass = $derived(
+    interactive
+      ? 'border-2 border-immich-primary hover:border-immich-dark-primary dark:hover:border-immich-primary dark:border-immich-dark-primary transition-colors'
+      : '',
+  );
 </script>
 
 <figure
@@ -79,7 +97,7 @@
   {#if showProfileImage && user.profileImagePath}
     <img
       bind:this={img}
-      src={getProfileImageUrl(user.id)}
+      src={getProfileImageUrl(user)}
       alt={$t('profile_image_of_user', { values: { user: title } })}
       class="h-full w-full object-cover"
       class:hidden={showFallback}
diff --git a/web/src/lib/components/shared-components/version-announcement-box.svelte b/web/src/lib/components/shared-components/version-announcement-box.svelte
index fb5466e7ae..62e9baf779 100644
--- a/web/src/lib/components/shared-components/version-announcement-box.svelte
+++ b/web/src/lib/components/shared-components/version-announcement-box.svelte
@@ -6,18 +6,12 @@
   import { t } from 'svelte-i18n';
   import FormatMessage from '$lib/components/i18n/format-message.svelte';
 
-  let showModal = false;
+  let showModal = $state(false);
 
   const { release } = websocketStore;
 
   const semverToName = ({ major, minor, patch }: ServerVersionResponseDto) => `v${major}.${minor}.${patch}`;
 
-  $: releaseVersion = $release && semverToName($release.releaseVersion);
-  $: serverVersion = $release && semverToName($release.serverVersion);
-  $: if ($release?.isAvailable) {
-    handleRelease();
-  }
-
   const onAcknowledge = () => {
     localStorage.setItem('appVersion', releaseVersion);
     showModal = false;
@@ -34,21 +28,30 @@
       console.error('Error [VersionAnnouncementBox]:', error);
     }
   };
+  let releaseVersion = $derived($release && semverToName($release.releaseVersion));
+  let serverVersion = $derived($release && semverToName($release.serverVersion));
+  $effect(() => {
+    if ($release?.isAvailable) {
+      handleRelease();
+    }
+  });
 </script>
 
 {#if showModal}
   <FullScreenModal title="🎉 {$t('new_version_available')}" onClose={() => (showModal = false)}>
     <div>
-      <FormatMessage key="version_announcement_message" let:tag let:message>
-        {#if tag === 'link'}
-          <span class="font-medium underline">
-            <a href="https://github.com/immich-app/immich/releases/latest" target="_blank" rel="noopener noreferrer">
-              {message}
-            </a>
-          </span>
-        {:else if tag === 'code'}
-          <code>{message}</code>
-        {/if}
+      <FormatMessage key="version_announcement_message">
+        {#snippet children({ tag, message })}
+          {#if tag === 'link'}
+            <span class="font-medium underline">
+              <a href="https://github.com/immich-app/immich/releases/latest" target="_blank" rel="noopener noreferrer">
+                {message}
+              </a>
+            </span>
+          {:else if tag === 'code'}
+            <code>{message}</code>
+          {/if}
+        {/snippet}
       </FormatMessage>
     </div>
 
@@ -60,8 +63,8 @@
       <code>{$t('latest_version')}: {releaseVersion}</code>
     </div>
 
-    <svelte:fragment slot="sticky-bottom">
-      <Button fullwidth on:click={onAcknowledge}>{$t('acknowledge')}</Button>
-    </svelte:fragment>
+    {#snippet stickyBottom()}
+      <Button fullwidth onclick={onAcknowledge}>{$t('acknowledge')}</Button>
+    {/snippet}
   </FullScreenModal>
 {/if}
diff --git a/web/src/lib/components/sharedlinks-page/actions/shared-link-copy.svelte b/web/src/lib/components/sharedlinks-page/actions/shared-link-copy.svelte
index f955d8479a..9ec9fc76ce 100644
--- a/web/src/lib/components/sharedlinks-page/actions/shared-link-copy.svelte
+++ b/web/src/lib/components/sharedlinks-page/actions/shared-link-copy.svelte
@@ -7,8 +7,12 @@
   import { mdiContentCopy } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  export let link: SharedLinkResponseDto;
-  export let menuItem = false;
+  interface Props {
+    link: SharedLinkResponseDto;
+    menuItem?: boolean;
+  }
+
+  let { link, menuItem = false }: Props = $props();
 
   const handleCopy = async () => {
     await copyToClipboard(makeSharedLinkUrl($serverConfig.externalDomain, link.key));
@@ -18,5 +22,5 @@
 {#if menuItem}
   <MenuOption text={$t('copy_link')} icon={mdiContentCopy} onClick={handleCopy} />
 {:else}
-  <CircleIconButton title={$t('copy_link')} icon={mdiContentCopy} on:click={handleCopy} />
+  <CircleIconButton title={$t('copy_link')} icon={mdiContentCopy} onclick={handleCopy} />
 {/if}
diff --git a/web/src/lib/components/sharedlinks-page/actions/shared-link-delete.svelte b/web/src/lib/components/sharedlinks-page/actions/shared-link-delete.svelte
index d458d5d77a..8c81e736bb 100644
--- a/web/src/lib/components/sharedlinks-page/actions/shared-link-delete.svelte
+++ b/web/src/lib/components/sharedlinks-page/actions/shared-link-delete.svelte
@@ -4,12 +4,16 @@
   import { mdiDelete } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  export let menuItem = false;
-  export let onDelete: () => void;
+  interface Props {
+    menuItem?: boolean;
+    onDelete: () => void;
+  }
+
+  let { menuItem = false, onDelete }: Props = $props();
 </script>
 
 {#if menuItem}
   <MenuOption text={$t('delete_link')} icon={mdiDelete} onClick={onDelete} />
 {:else}
-  <CircleIconButton title={$t('delete_link')} icon={mdiDelete} on:click={onDelete} />
+  <CircleIconButton title={$t('delete_link')} icon={mdiDelete} onclick={onDelete} />
 {/if}
diff --git a/web/src/lib/components/sharedlinks-page/actions/shared-link-edit.svelte b/web/src/lib/components/sharedlinks-page/actions/shared-link-edit.svelte
index 49c6105632..0a0c5a4736 100644
--- a/web/src/lib/components/sharedlinks-page/actions/shared-link-edit.svelte
+++ b/web/src/lib/components/sharedlinks-page/actions/shared-link-edit.svelte
@@ -4,12 +4,16 @@
   import { mdiCircleEditOutline } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  export let menuItem = false;
-  export let onEdit: () => void;
+  interface Props {
+    menuItem?: boolean;
+    onEdit: () => void;
+  }
+
+  let { menuItem = false, onEdit }: Props = $props();
 </script>
 
 {#if menuItem}
   <MenuOption text={$t('edit_link')} icon={mdiCircleEditOutline} onClick={onEdit} />
 {:else}
-  <CircleIconButton title={$t('edit_link')} icon={mdiCircleEditOutline} on:click={onEdit} />
+  <CircleIconButton title={$t('edit_link')} icon={mdiCircleEditOutline} onclick={onEdit} />
 {/if}
diff --git a/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte b/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte
index bf5031e39d..76822cc786 100644
--- a/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte
+++ b/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte
@@ -1,13 +1,16 @@
 <script lang="ts">
   import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
 
-  export let alt;
-  export let preload = false;
-  export let src: string;
-  let className = '';
-  export { className as class };
+  interface Props {
+    alt?: string;
+    preload?: boolean;
+    src: string;
+    class?: string;
+  }
 
-  let isBroken = false;
+  let { alt, preload = false, src, class: className = '' }: Props = $props();
+
+  let isBroken = $state(false);
 </script>
 
 {#if isBroken}
@@ -15,7 +18,7 @@
 {:else}
   <img
     {alt}
-    on:error={() => (isBroken = true)}
+    onerror={() => (isBroken = true)}
     class="size-full rounded-xl object-cover aspect-square {className}"
     data-testid="album-image"
     draggable="false"
diff --git a/web/src/lib/components/sharedlinks-page/covers/no-cover.svelte b/web/src/lib/components/sharedlinks-page/covers/no-cover.svelte
index 087204d6a5..1e09c6bcfa 100644
--- a/web/src/lib/components/sharedlinks-page/covers/no-cover.svelte
+++ b/web/src/lib/components/sharedlinks-page/covers/no-cover.svelte
@@ -1,8 +1,11 @@
 <script lang="ts">
-  export let alt = '';
-  export let preload = false;
-  let className = '';
-  export { className as class };
+  interface Props {
+    alt?: string;
+    preload?: boolean;
+    class?: string;
+  }
+
+  let { alt = '', preload = false, class: className = '' }: Props = $props();
 </script>
 
 <enhanced:img
diff --git a/web/src/lib/components/sharedlinks-page/covers/share-cover.svelte b/web/src/lib/components/sharedlinks-page/covers/share-cover.svelte
index 09f32d7dac..6f15cca45f 100644
--- a/web/src/lib/components/sharedlinks-page/covers/share-cover.svelte
+++ b/web/src/lib/components/sharedlinks-page/covers/share-cover.svelte
@@ -6,10 +6,13 @@
   import { getAssetThumbnailUrl } from '$lib/utils';
   import { t } from 'svelte-i18n';
 
-  export let link: SharedLinkResponseDto;
-  export let preload = false;
-  let className = '';
-  export { className as class };
+  interface Props {
+    link: SharedLinkResponseDto;
+    preload?: boolean;
+    class?: string;
+  }
+
+  let { link, preload = false, class: className = '' }: Props = $props();
 </script>
 
 <div class="relative shrink-0 size-24">
diff --git a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte
index 13beec0ec0..70f6247533 100644
--- a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte
+++ b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte
@@ -12,13 +12,17 @@
   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
   import { mdiDotsVertical } from '@mdi/js';
 
-  export let link: SharedLinkResponseDto;
-  export let onDelete: () => void;
-  export let onEdit: () => void;
+  interface Props {
+    link: SharedLinkResponseDto;
+    onDelete: () => void;
+    onEdit: () => void;
+  }
+
+  let { link, onDelete, onEdit }: Props = $props();
 
   let now = DateTime.now();
-  $: expiresAt = link.expiresAt ? DateTime.fromISO(link.expiresAt) : undefined;
-  $: isExpired = expiresAt ? now > expiresAt : false;
+  let expiresAt = $derived(link.expiresAt ? DateTime.fromISO(link.expiresAt) : undefined);
+  let isExpired = $derived(expiresAt ? now > expiresAt : false);
 
   const getCountDownExpirationDate = (expiresAtDate: DateTime, now: DateTime) => {
     const relativeUnits: ToRelativeUnit[] = ['days', 'hours', 'minutes', 'seconds'];
diff --git a/web/src/lib/components/slideshow-settings.svelte b/web/src/lib/components/slideshow-settings.svelte
index e2bf6a4b2c..723960d914 100644
--- a/web/src/lib/components/slideshow-settings.svelte
+++ b/web/src/lib/components/slideshow-settings.svelte
@@ -1,8 +1,6 @@
 <script lang="ts">
   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
-  import SettingInputField, {
-    SettingInputFieldType,
-  } from '$lib/components/shared-components/settings/setting-input-field.svelte';
+  import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
   import {
     mdiArrowDownThin,
@@ -17,10 +15,15 @@
   import type { RenderedOption } from './elements/dropdown.svelte';
   import SettingDropdown from './shared-components/settings/setting-dropdown.svelte';
   import { t } from 'svelte-i18n';
+  import { SettingInputFieldType } from '$lib/constants';
 
-  const { slideshowDelay, showProgressBar, slideshowNavigation, slideshowLook } = slideshowStore;
+  const { slideshowDelay, showProgressBar, slideshowNavigation, slideshowLook, slideshowTransition } = slideshowStore;
 
-  export let onClose = () => {};
+  interface Props {
+    onClose?: () => void;
+  }
+
+  let { onClose = () => {} }: Props = $props();
 
   const navigationOptions: Record<SlideshowNavigation, RenderedOption> = {
     [SlideshowNavigation.Shuffle]: { icon: mdiShuffle, title: $t('shuffle') },
@@ -46,7 +49,7 @@
   };
 </script>
 
-<FullScreenModal title={$t('slideshow_settings')} {onClose}>
+<FullScreenModal title={$t('slideshow_settings')} onClose={() => onClose()}>
   <div class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary">
     <SettingDropdown
       title={$t('direction')}
@@ -65,15 +68,17 @@
       }}
     />
     <SettingSwitch title={$t('show_progress_bar')} bind:checked={$showProgressBar} />
+    <SettingSwitch title={$t('show_slideshow_transition')} bind:checked={$slideshowTransition} />
     <SettingInputField
       inputType={SettingInputFieldType.NUMBER}
       label={$t('duration')}
-      desc={$t('admin.slideshow_duration_description')}
+      description={$t('admin.slideshow_duration_description')}
       min={1}
       bind:value={$slideshowDelay}
     />
   </div>
-  <svelte:fragment slot="sticky-bottom">
-    <Button fullwidth color="primary" on:click={onClose}>{$t('done')}</Button>
-  </svelte:fragment>
+
+  {#snippet stickyBottom()}
+    <Button fullwidth color="primary" onclick={(_) => onClose()}>{$t('done')}</Button>
+  {/snippet}
 </FullScreenModal>
diff --git a/web/src/lib/components/user-settings-page/app-settings.svelte b/web/src/lib/components/user-settings-page/app-settings.svelte
index de4bbafdd9..63209ca289 100644
--- a/web/src/lib/components/user-settings-page/app-settings.svelte
+++ b/web/src/lib/components/user-settings-page/app-settings.svelte
@@ -19,25 +19,7 @@
   import { fade } from 'svelte/transition';
   import { invalidateAll } from '$app/navigation';
 
-  let time = new Date();
-
-  $: formattedDate = time.toLocaleString(editedLocale, {
-    year: 'numeric',
-    month: '2-digit',
-    day: '2-digit',
-  });
-  $: timePortion = time.toLocaleString(editedLocale, {
-    hour: '2-digit',
-    minute: '2-digit',
-    second: '2-digit',
-  });
-  $: selectedDate = `${formattedDate} ${timePortion}`;
-  $: editedLocale = findLocale($locale).code;
-  $: selectedOption = {
-    value: findLocale(editedLocale).code || fallbackLocale.code,
-    label: findLocale(editedLocale).name || fallbackLocale.name,
-  };
-  $: closestLanguage = getClosestAvailableLocale([$lang], langCodes);
+  let time = $state(new Date());
 
   onMount(() => {
     const interval = setInterval(() => {
@@ -89,6 +71,27 @@
       $locale = newLocale;
     }
   };
+  let editedLocale = $derived(findLocale($locale).code);
+  let formattedDate = $derived(
+    time.toLocaleString(editedLocale, {
+      year: 'numeric',
+      month: '2-digit',
+      day: '2-digit',
+    }),
+  );
+  let timePortion = $derived(
+    time.toLocaleString(editedLocale, {
+      hour: '2-digit',
+      minute: '2-digit',
+      second: '2-digit',
+    }),
+  );
+  let selectedDate = $derived(`${formattedDate} ${timePortion}`);
+  let selectedOption = $derived({
+    value: findLocale(editedLocale).code || fallbackLocale.code,
+    label: findLocale(editedLocale).name || fallbackLocale.name,
+  });
+  let closestLanguage = $derived(getClosestAvailableLocale([$lang], langCodes));
 </script>
 
 <section class="my-4">
@@ -99,7 +102,7 @@
           title={$t('theme_selection')}
           subtitle={$t('theme_selection_description')}
           bind:checked={$colorTheme.system}
-          on:toggle={handleToggleColorTheme}
+          onToggle={handleToggleColorTheme}
         />
       </div>
 
@@ -119,7 +122,7 @@
           title={$t('default_locale')}
           subtitle={$t('default_locale_description')}
           checked={$locale == undefined}
-          on:toggle={handleToggleLocaleBrowser}
+          onToggle={handleToggleLocaleBrowser}
         >
           <p class="mt-2 dark:text-gray-400">{selectedDate}</p>
         </SettingSwitch>
@@ -142,7 +145,7 @@
           title={$t('display_original_photos')}
           subtitle={$t('display_original_photos_setting_description')}
           bind:checked={$alwaysLoadOriginalFile}
-          on:toggle={() => ($alwaysLoadOriginalFile = !$alwaysLoadOriginalFile)}
+          onToggle={() => ($alwaysLoadOriginalFile = !$alwaysLoadOriginalFile)}
         />
       </div>
       <div class="ml-4">
@@ -150,7 +153,7 @@
           title={$t('video_hover_setting')}
           subtitle={$t('video_hover_setting_description')}
           bind:checked={$playVideoThumbnailOnHover}
-          on:toggle={() => ($playVideoThumbnailOnHover = !$playVideoThumbnailOnHover)}
+          onToggle={() => ($playVideoThumbnailOnHover = !$playVideoThumbnailOnHover)}
         />
       </div>
       <div class="ml-4">
@@ -158,7 +161,7 @@
           title={$t('loop_videos')}
           subtitle={$t('loop_videos_description')}
           bind:checked={$loopVideo}
-          on:toggle={() => ($loopVideo = !$loopVideo)}
+          onToggle={() => ($loopVideo = !$loopVideo)}
         />
       </div>
 
diff --git a/web/src/lib/components/user-settings-page/change-password-settings.svelte b/web/src/lib/components/user-settings-page/change-password-settings.svelte
index 39fd78e037..b31cbac34f 100644
--- a/web/src/lib/components/user-settings-page/change-password-settings.svelte
+++ b/web/src/lib/components/user-settings-page/change-password-settings.svelte
@@ -8,14 +8,13 @@
 
   import Button from '$lib/components/elements/buttons/button.svelte';
   import type { HttpError } from '@sveltejs/kit';
-  import SettingInputField, {
-    SettingInputFieldType,
-  } from '$lib/components/shared-components/settings/setting-input-field.svelte';
+  import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
   import { t } from 'svelte-i18n';
+  import { SettingInputFieldType } from '$lib/constants';
 
-  let password = '';
-  let newPassword = '';
-  let confirmPassword = '';
+  let password = $state('');
+  let newPassword = $state('');
+  let confirmPassword = $state('');
 
   const handleChangePassword = async () => {
     try {
@@ -37,11 +36,15 @@
       });
     }
   };
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+  };
 </script>
 
 <section class="my-4">
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault>
+    <form autocomplete="off" {onsubmit}>
       <div class="ml-4 mt-4 flex flex-col gap-4">
         <SettingInputField
           inputType={SettingInputFieldType.PASSWORD}
@@ -72,7 +75,7 @@
             type="submit"
             size="sm"
             disabled={!(password && newPassword && newPassword === confirmPassword)}
-            on:click={() => handleChangePassword()}>{$t('save')}</Button
+            onclick={() => handleChangePassword()}>{$t('save')}</Button
           >
         </div>
       </div>
diff --git a/web/src/lib/components/user-settings-page/device-card.svelte b/web/src/lib/components/user-settings-page/device-card.svelte
index 676e984364..5248a6d119 100644
--- a/web/src/lib/components/user-settings-page/device-card.svelte
+++ b/web/src/lib/components/user-settings-page/device-card.svelte
@@ -15,14 +15,14 @@
     mdiUbuntu,
   } from '@mdi/js';
   import { DateTime, type ToRelativeCalendarOptions } from 'luxon';
-  import { createEventDispatcher } from 'svelte';
   import { t } from 'svelte-i18n';
 
-  export let device: SessionResponseDto;
+  interface Props {
+    device: SessionResponseDto;
+    onDelete?: (() => void) | undefined;
+  }
 
-  const dispatcher = createEventDispatcher<{
-    delete: void;
-  }>();
+  let { device, onDelete = undefined }: Props = $props();
 
   const options: ToRelativeCalendarOptions = {
     unit: 'days',
@@ -68,14 +68,14 @@
         </span>
       </div>
     </div>
-    {#if !device.current}
+    {#if !device.current && onDelete}
       <div>
         <CircleIconButton
           color="primary"
           icon={mdiTrashCanOutline}
           title={$t('log_out')}
           size="16"
-          on:click={() => dispatcher('delete')}
+          onclick={onDelete}
         />
       </div>
     {/if}
diff --git a/web/src/lib/components/user-settings-page/device-list.svelte b/web/src/lib/components/user-settings-page/device-list.svelte
index 57299bb46f..bb202b3606 100644
--- a/web/src/lib/components/user-settings-page/device-list.svelte
+++ b/web/src/lib/components/user-settings-page/device-list.svelte
@@ -7,12 +7,16 @@
   import { dialogController } from '$lib/components/shared-components/dialog/dialog';
   import { t } from 'svelte-i18n';
 
-  export let devices: SessionResponseDto[];
+  interface Props {
+    devices: SessionResponseDto[];
+  }
+
+  let { devices = $bindable() }: Props = $props();
 
   const refresh = () => getSessions().then((_devices) => (devices = _devices));
 
-  $: currentDevice = devices.find((device) => device.current);
-  $: otherDevices = devices.filter((device) => !device.current);
+  let currentDevice = $derived(devices.find((device) => device.current));
+  let otherDevices = $derived(devices.filter((device) => !device.current));
 
   const handleDelete = async (device: SessionResponseDto) => {
     const isConfirmed = await dialogController.show({
@@ -68,7 +72,7 @@
         {$t('other_devices').toUpperCase()}
       </h3>
       {#each otherDevices as device, index}
-        <DeviceCard {device} on:delete={() => handleDelete(device)} />
+        <DeviceCard {device} onDelete={() => handleDelete(device)} />
         {#if index !== otherDevices.length - 1}
           <hr class="my-3" />
         {/if}
@@ -78,7 +82,7 @@
       {$t('log_out_all_devices').toUpperCase()}
     </h3>
     <div class="flex justify-end">
-      <Button color="red" size="sm" on:click={handleDeleteAll}>{$t('log_out_all_devices')}</Button>
+      <Button color="red" size="sm" onclick={handleDeleteAll}>{$t('log_out_all_devices')}</Button>
     </div>
   {/if}
 </section>
diff --git a/web/src/lib/components/user-settings-page/download-settings.svelte b/web/src/lib/components/user-settings-page/download-settings.svelte
index f5b94ebee8..97da347fb7 100644
--- a/web/src/lib/components/user-settings-page/download-settings.svelte
+++ b/web/src/lib/components/user-settings-page/download-settings.svelte
@@ -10,14 +10,13 @@
   import { preferences } from '$lib/stores/user.store';
   import Button from '../elements/buttons/button.svelte';
   import { t } from 'svelte-i18n';
-  import SettingInputField, {
-    SettingInputFieldType,
-  } from '$lib/components/shared-components/settings/setting-input-field.svelte';
+  import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
   import { ByteUnit, convertFromBytes, convertToBytes } from '$lib/utils/byte-units';
   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
+  import { SettingInputFieldType } from '$lib/constants';
 
-  let archiveSize = convertFromBytes($preferences?.download?.archiveSize || 4, ByteUnit.GiB);
-  let includeEmbeddedVideos = $preferences?.download?.includeEmbeddedVideos || false;
+  let archiveSize = $state(convertFromBytes($preferences?.download?.archiveSize || 4, ByteUnit.GiB));
+  let includeEmbeddedVideos = $state($preferences?.download?.includeEmbeddedVideos || false);
 
   const handleSave = async () => {
     try {
@@ -36,16 +35,20 @@
       handleError(error, $t('errors.unable_to_update_settings'));
     }
   };
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+  };
 </script>
 
 <section class="my-4">
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault>
+    <form autocomplete="off" {onsubmit}>
       <div class="ml-4 mt-4 flex flex-col gap-4">
         <SettingInputField
           inputType={SettingInputFieldType.NUMBER}
           label={$t('archive_size')}
-          desc={$t('archive_size_description')}
+          description={$t('archive_size_description')}
           bind:value={archiveSize}
         />
         <SettingSwitch
@@ -54,7 +57,7 @@
           bind:checked={includeEmbeddedVideos}
         ></SettingSwitch>
         <div class="flex justify-end">
-          <Button type="submit" size="sm" on:click={() => handleSave()}>{$t('save')}</Button>
+          <Button type="submit" size="sm" onclick={() => handleSave()}>{$t('save')}</Button>
         </div>
       </div>
     </form>
diff --git a/web/src/lib/components/user-settings-page/feature-settings.svelte b/web/src/lib/components/user-settings-page/feature-settings.svelte
index dc11dab15e..9a60f83647 100644
--- a/web/src/lib/components/user-settings-page/feature-settings.svelte
+++ b/web/src/lib/components/user-settings-page/feature-settings.svelte
@@ -14,22 +14,22 @@
   import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
 
   // Folders
-  let foldersEnabled = $preferences?.folders?.enabled ?? false;
-  let foldersSidebar = $preferences?.folders?.sidebarWeb ?? false;
+  let foldersEnabled = $state($preferences?.folders?.enabled ?? false);
+  let foldersSidebar = $state($preferences?.folders?.sidebarWeb ?? false);
 
   // Memories
-  let memoriesEnabled = $preferences?.memories?.enabled ?? true;
+  let memoriesEnabled = $state($preferences?.memories?.enabled ?? true);
 
   // People
-  let peopleEnabled = $preferences?.people?.enabled ?? false;
-  let peopleSidebar = $preferences?.people?.sidebarWeb ?? false;
+  let peopleEnabled = $state($preferences?.people?.enabled ?? false);
+  let peopleSidebar = $state($preferences?.people?.sidebarWeb ?? false);
 
   // Ratings
-  let ratingsEnabled = $preferences?.ratings?.enabled ?? false;
+  let ratingsEnabled = $state($preferences?.ratings?.enabled ?? false);
 
   // Tags
-  let tagsEnabled = $preferences?.tags?.enabled ?? false;
-  let tagsSidebar = $preferences?.tags?.sidebarWeb ?? false;
+  let tagsEnabled = $state($preferences?.tags?.enabled ?? false);
+  let tagsSidebar = $state($preferences?.tags?.sidebarWeb ?? false);
 
   const handleSave = async () => {
     try {
@@ -50,12 +50,16 @@
       handleError(error, $t('errors.unable_to_update_settings'));
     }
   };
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+  };
 </script>
 
 <section class="my-4">
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault>
-      <div class="ml-4 mt-4 flex flex-col gap-4">
+    <form autocomplete="off" {onsubmit}>
+      <div class="ml-4 mt-4 flex flex-col">
         <SettingAccordion key="folders" title={$t('folders')} subtitle={$t('folders_feature_description')}>
           <div class="ml-4 mt-6">
             <SettingSwitch title={$t('enable')} bind:checked={foldersEnabled} />
@@ -116,7 +120,7 @@
         </SettingAccordion>
 
         <div class="flex justify-end">
-          <Button type="submit" size="sm" on:click={() => handleSave()}>{$t('save')}</Button>
+          <Button type="submit" size="sm" onclick={() => handleSave()}>{$t('save')}</Button>
         </div>
       </div>
     </form>
diff --git a/web/src/lib/components/user-settings-page/notifications-settings.svelte b/web/src/lib/components/user-settings-page/notifications-settings.svelte
index 275f628f0a..bec0633964 100644
--- a/web/src/lib/components/user-settings-page/notifications-settings.svelte
+++ b/web/src/lib/components/user-settings-page/notifications-settings.svelte
@@ -12,9 +12,9 @@
   import Button from '../elements/buttons/button.svelte';
   import { t } from 'svelte-i18n';
 
-  let emailNotificationsEnabled = $preferences?.emailNotifications?.enabled ?? true;
-  let albumInviteNotificationEnabled = $preferences?.emailNotifications?.albumInvite ?? true;
-  let albumUpdateNotificationEnabled = $preferences?.emailNotifications?.albumUpdate ?? true;
+  let emailNotificationsEnabled = $state($preferences?.emailNotifications?.enabled ?? true);
+  let albumInviteNotificationEnabled = $state($preferences?.emailNotifications?.albumInvite ?? true);
+  let albumUpdateNotificationEnabled = $state($preferences?.emailNotifications?.albumUpdate ?? true);
 
   const handleSave = async () => {
     try {
@@ -37,11 +37,15 @@
       handleError(error, $t('errors.unable_to_update_settings'));
     }
   };
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+  };
 </script>
 
 <section class="my-4">
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault>
+    <form autocomplete="off" {onsubmit}>
       <div class="ml-4 mt-4 flex flex-col gap-4">
         <div class="ml-4">
           <SettingSwitch
@@ -67,7 +71,7 @@
         </div>
 
         <div class="flex justify-end">
-          <Button type="submit" size="sm" on:click={() => handleSave()}>{$t('save')}</Button>
+          <Button type="submit" size="sm" onclick={() => handleSave()}>{$t('save')}</Button>
         </div>
       </div>
     </form>
diff --git a/web/src/lib/components/user-settings-page/oauth-settings.svelte b/web/src/lib/components/user-settings-page/oauth-settings.svelte
index 10e71e64eb..40ea82fd8c 100644
--- a/web/src/lib/components/user-settings-page/oauth-settings.svelte
+++ b/web/src/lib/components/user-settings-page/oauth-settings.svelte
@@ -11,16 +11,20 @@
   import { notificationController, NotificationType } from '../shared-components/notification/notification';
   import { t } from 'svelte-i18n';
 
-  export let user: UserAdminResponseDto;
+  interface Props {
+    user: UserAdminResponseDto;
+  }
 
-  let loading = true;
+  let { user = $bindable() }: Props = $props();
+
+  let loading = $state(true);
 
   onMount(async () => {
-    if (oauth.isCallback(window.location)) {
+    if (oauth.isCallback(globalThis.location)) {
       try {
         loading = true;
 
-        user = await oauth.link(window.location);
+        user = await oauth.link(globalThis.location);
 
         notificationController.show({
           message: $t('linked_oauth_account'),
@@ -58,9 +62,9 @@
         </div>
       {:else if $featureFlags.oauth}
         {#if user.oauthId}
-          <Button size="sm" on:click={() => handleUnlink()}>{$t('unlink_oauth')}</Button>
+          <Button size="sm" onclick={() => handleUnlink()}>{$t('unlink_oauth')}</Button>
         {:else}
-          <Button size="sm" on:click={() => oauth.authorize(window.location)}>{$t('link_to_oauth')}</Button>
+          <Button size="sm" onclick={() => oauth.authorize(globalThis.location)}>{$t('link_to_oauth')}</Button>
         {/if}
       {/if}
     </div>
diff --git a/web/src/lib/components/user-settings-page/partner-selection-modal.svelte b/web/src/lib/components/user-settings-page/partner-selection-modal.svelte
index 3cff1cd1de..070246b612 100644
--- a/web/src/lib/components/user-settings-page/partner-selection-modal.svelte
+++ b/web/src/lib/components/user-settings-page/partner-selection-modal.svelte
@@ -1,18 +1,21 @@
 <script lang="ts">
-  import { searchUsers, getPartners, type UserResponseDto, PartnerDirection } from '@immich/sdk';
-  import { createEventDispatcher, onMount } from 'svelte';
+  import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
+  import { getPartners, PartnerDirection, searchUsers, type UserResponseDto } from '@immich/sdk';
+  import { onMount } from 'svelte';
+  import { t } from 'svelte-i18n';
   import Button from '../elements/buttons/button.svelte';
   import UserAvatar from '../shared-components/user-avatar.svelte';
-  import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
-  import { t } from 'svelte-i18n';
 
-  export let user: UserResponseDto;
-  export let onClose: () => void;
+  interface Props {
+    user: UserResponseDto;
+    onClose: () => void;
+    onAddUsers: (users: UserResponseDto[]) => void;
+  }
 
-  let availableUsers: UserResponseDto[] = [];
-  let selectedUsers: UserResponseDto[] = [];
+  let { user, onClose, onAddUsers }: Props = $props();
 
-  const dispatch = createEventDispatcher<{ 'add-users': UserResponseDto[] }>();
+  let availableUsers: UserResponseDto[] = $state([]);
+  let selectedUsers: UserResponseDto[] = $state([]);
 
   onMount(async () => {
     let users = await searchUsers();
@@ -39,7 +42,7 @@
       {#each availableUsers as user}
         <button
           type="button"
-          on:click={() => selectUser(user)}
+          onclick={() => selectUser(user)}
           class="flex w-full place-items-center gap-4 px-5 py-4 transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"
         >
           {#if selectedUsers.includes(user)}
@@ -69,7 +72,7 @@
 
     {#if selectedUsers.length > 0}
       <div class="pt-5">
-        <Button size="sm" fullwidth on:click={() => dispatch('add-users', selectedUsers)}>{$t('add')}</Button>
+        <Button size="sm" fullwidth onclick={() => onAddUsers(selectedUsers)}>{$t('add')}</Button>
       </div>
     {/if}
   </div>
diff --git a/web/src/lib/components/user-settings-page/partner-settings.svelte b/web/src/lib/components/user-settings-page/partner-settings.svelte
index 0cd110dc57..a71266a76b 100644
--- a/web/src/lib/components/user-settings-page/partner-settings.svelte
+++ b/web/src/lib/components/user-settings-page/partner-settings.svelte
@@ -27,11 +27,15 @@
     inTimeline: boolean;
   }
 
-  export let user: UserResponseDto;
+  interface Props {
+    user: UserResponseDto;
+  }
 
-  let createPartnerFlag = false;
+  let { user }: Props = $props();
+
+  let createPartnerFlag = $state(false);
   // let removePartnerDto: PartnerResponseDto | null = null;
-  let partners: Array<PartnerSharing> = [];
+  let partners: Array<PartnerSharing> = $state([]);
 
   onMount(async () => {
     await refreshPartners();
@@ -60,10 +64,7 @@
     for (const candidate of sharedWith) {
       const existIndex = partners.findIndex((p) => candidate.id === p.user.id);
 
-      if (existIndex >= 0) {
-        partners[existIndex].sharedWithMe = true;
-        partners[existIndex].inTimeline = candidate.inTimeline ?? false;
-      } else {
+      if (existIndex === -1) {
         partners = [
           ...partners,
           {
@@ -73,6 +74,9 @@
             inTimeline: candidate.inTimeline ?? false,
           },
         ];
+      } else {
+        partners[existIndex].sharedWithMe = true;
+        partners[existIndex].inTimeline = candidate.inTimeline ?? false;
       }
     }
   };
@@ -113,7 +117,6 @@
       await updatePartner({ id: partner.user.id, updatePartnerDto: { inTimeline } });
 
       partner.inTimeline = inTimeline;
-      partners = partners;
     } catch (error) {
       handleError(error, $t('errors.unable_to_update_timeline_display_status'));
     }
@@ -139,7 +142,7 @@
 
           {#if partner.sharedByMe}
             <CircleIconButton
-              on:click={() => handleRemovePartner(partner.user)}
+              onclick={() => handleRemovePartner(partner.user)}
               icon={mdiClose}
               size={'16'}
               title={$t('stop_sharing_photos_with_user')}
@@ -177,7 +180,7 @@
               title={$t('show_in_timeline')}
               subtitle={$t('show_in_timeline_setting_description')}
               bind:checked={partner.inTimeline}
-              on:toggle={({ detail }) => handleShowOnTimelineChanged(partner, detail)}
+              onToggle={(isChecked) => handleShowOnTimelineChanged(partner, isChecked)}
             />
           {/if}
         </div>
@@ -186,14 +189,10 @@
   {/if}
 
   <div class="flex justify-end mt-5">
-    <Button size="sm" on:click={() => (createPartnerFlag = true)}>{$t('add_partner')}</Button>
+    <Button size="sm" onclick={() => (createPartnerFlag = true)}>{$t('add_partner')}</Button>
   </div>
 </section>
 
 {#if createPartnerFlag}
-  <PartnerSelectionModal
-    {user}
-    onClose={() => (createPartnerFlag = false)}
-    on:add-users={(event) => handleCreatePartners(event.detail)}
-  />
+  <PartnerSelectionModal {user} onClose={() => (createPartnerFlag = false)} onAddUsers={handleCreatePartners} />
 {/if}
diff --git a/web/src/lib/components/user-settings-page/user-api-key-list.svelte b/web/src/lib/components/user-settings-page/user-api-key-list.svelte
index 13ec440082..c5c6bae9e5 100644
--- a/web/src/lib/components/user-settings-page/user-api-key-list.svelte
+++ b/web/src/lib/components/user-settings-page/user-api-key-list.svelte
@@ -19,11 +19,15 @@
   import { dialogController } from '$lib/components/shared-components/dialog/dialog';
   import { t } from 'svelte-i18n';
 
-  export let keys: ApiKeyResponseDto[];
+  interface Props {
+    keys: ApiKeyResponseDto[];
+  }
 
-  let newKey: { name: string } | null = null;
-  let editKey: ApiKeyResponseDto | null = null;
-  let secret = '';
+  let { keys = $bindable() }: Props = $props();
+
+  let newKey: { name: string } | null = $state(null);
+  let editKey: ApiKeyResponseDto | null = $state(null);
+  let secret = $state('');
 
   const format: Intl.DateTimeFormatOptions = {
     month: 'short',
@@ -102,7 +106,7 @@
 {/if}
 
 {#if secret}
-  <APIKeySecret {secret} on:done={() => (secret = '')} />
+  <APIKeySecret {secret} onDone={() => (secret = '')} />
 {/if}
 
 {#if editKey}
@@ -118,7 +122,7 @@
 <section class="my-4">
   <div class="flex flex-col gap-2" in:fade={{ duration: 500 }}>
     <div class="mb-2 flex justify-end">
-      <Button size="sm" on:click={() => (newKey = { name: $t('api_key') })}>{$t('new_api_key')}</Button>
+      <Button size="sm" onclick={() => (newKey = { name: $t('api_key') })}>{$t('new_api_key')}</Button>
     </div>
 
     {#if keys.length > 0}
@@ -152,14 +156,14 @@
                     icon={mdiPencilOutline}
                     title={$t('edit_key')}
                     size="16"
-                    on:click={() => (editKey = key)}
+                    onclick={() => (editKey = key)}
                   />
                   <CircleIconButton
                     color="primary"
                     icon={mdiTrashCanOutline}
                     title={$t('delete_key')}
                     size="16"
-                    on:click={() => handleDelete(key)}
+                    onclick={() => handleDelete(key)}
                   />
                 </td>
               </tr>
diff --git a/web/src/lib/components/user-settings-page/user-profile-settings.svelte b/web/src/lib/components/user-settings-page/user-profile-settings.svelte
index 1f3b59bfdd..a49eabcf13 100644
--- a/web/src/lib/components/user-settings-page/user-profile-settings.svelte
+++ b/web/src/lib/components/user-settings-page/user-profile-settings.svelte
@@ -1,11 +1,12 @@
 <script lang="ts">
+  import { createBubbler, preventDefault } from 'svelte/legacy';
+
+  const bubble = createBubbler();
   import {
     notificationController,
     NotificationType,
   } from '$lib/components/shared-components/notification/notification';
-  import SettingInputField, {
-    SettingInputFieldType,
-  } from '$lib/components/shared-components/settings/setting-input-field.svelte';
+  import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
   import { user } from '$lib/stores/user.store';
   import { updateMyUser } from '@immich/sdk';
   import { cloneDeep } from 'lodash-es';
@@ -13,8 +14,9 @@
   import { handleError } from '../../utils/handle-error';
   import Button from '../elements/buttons/button.svelte';
   import { t } from 'svelte-i18n';
+  import { SettingInputFieldType } from '$lib/constants';
 
-  let editedUser = cloneDeep($user);
+  let editedUser = $state(cloneDeep($user));
 
   const handleSaveProfile = async () => {
     try {
@@ -40,7 +42,7 @@
 
 <section class="my-4">
   <div in:fade={{ duration: 500 }}>
-    <form autocomplete="off" on:submit|preventDefault>
+    <form autocomplete="off" onsubmit={preventDefault(bubble('submit'))}>
       <div class="ml-4 mt-4 flex flex-col gap-4">
         <SettingInputField
           inputType={SettingInputFieldType.TEXT}
@@ -67,7 +69,7 @@
         />
 
         <div class="flex justify-end">
-          <Button type="submit" size="sm" on:click={() => handleSaveProfile()}>{$t('save')}</Button>
+          <Button type="submit" size="sm" onclick={() => handleSaveProfile()}>{$t('save')}</Button>
         </div>
       </div>
     </form>
diff --git a/web/src/lib/components/user-settings-page/user-purchase-settings.svelte b/web/src/lib/components/user-settings-page/user-purchase-settings.svelte
index bf0fd3c874..2408651234 100644
--- a/web/src/lib/components/user-settings-page/user-purchase-settings.svelte
+++ b/web/src/lib/components/user-settings-page/user-purchase-settings.svelte
@@ -24,8 +24,8 @@
   import { setSupportBadgeVisibility } from '$lib/utils/purchase-utils';
   const { isPurchased } = purchaseStore;
 
-  let isServerProduct = false;
-  let serverPurchaseInfo: LicenseResponseDto | null = null;
+  let isServerProduct = $state(false);
+  let serverPurchaseInfo: LicenseResponseDto | null = $state(null);
 
   const checkPurchaseInfo = async () => {
     const serverInfo = await getAboutInfo();
@@ -115,7 +115,7 @@
           title={$t('show_supporter_badge')}
           subtitle={$t('show_supporter_badge_description')}
           bind:checked={$preferences.purchase.showSupportBadge}
-          on:toggle={({ detail }) => setSupportBadgeVisibility(detail)}
+          onToggle={setSupportBadgeVisibility}
         />
       </div>
 
@@ -145,7 +145,7 @@
 
         {#if $user.isAdmin}
           <div class="text-right mt-4">
-            <Button size="sm" color="red" on:click={removeServerProductKey}>{$t('purchase_button_remove_key')}</Button>
+            <Button size="sm" color="red" onclick={removeServerProductKey}>{$t('purchase_button_remove_key')}</Button>
           </div>
         {/if}
       {:else}
@@ -169,8 +169,7 @@
         </div>
 
         <div class="text-right mt-4">
-          <Button size="sm" color="red" on:click={removeIndividualProductKey}>{$t('purchase_button_remove_key')}</Button
-          >
+          <Button size="sm" color="red" onclick={removeIndividualProductKey}>{$t('purchase_button_remove_key')}</Button>
         </div>
       {/if}
     {:else}
diff --git a/web/src/lib/components/user-settings-page/user-settings-list.svelte b/web/src/lib/components/user-settings-page/user-settings-list.svelte
index 596efaedef..934fa5708f 100644
--- a/web/src/lib/components/user-settings-page/user-settings-list.svelte
+++ b/web/src/lib/components/user-settings-page/user-settings-list.svelte
@@ -19,33 +19,72 @@
   import DownloadSettings from '$lib/components/user-settings-page/download-settings.svelte';
   import UserPurchaseSettings from '$lib/components/user-settings-page/user-purchase-settings.svelte';
   import FeatureSettings from '$lib/components/user-settings-page/feature-settings.svelte';
+  import {
+    mdiAccountGroupOutline,
+    mdiAccountOutline,
+    mdiApi,
+    mdiBellOutline,
+    mdiCogOutline,
+    mdiDevices,
+    mdiDownload,
+    mdiFeatureSearchOutline,
+    mdiKeyOutline,
+    mdiOnepassword,
+    mdiServerOutline,
+    mdiTwoFactorAuthentication,
+  } from '@mdi/js';
+  import UserUsageStatistic from '$lib/components/user-settings-page/user-usage-statistic.svelte';
 
-  export let keys: ApiKeyResponseDto[] = [];
-  export let sessions: SessionResponseDto[] = [];
+  interface Props {
+    keys?: ApiKeyResponseDto[];
+    sessions?: SessionResponseDto[];
+  }
+
+  let { keys = $bindable([]), sessions = $bindable([]) }: Props = $props();
 
   let oauthOpen =
-    oauth.isCallback(window.location) ||
+    oauth.isCallback(globalThis.location) ||
     $page.url.searchParams.get(QueryParameter.OPEN_SETTING) === OpenSettingQueryParameterValue.OAUTH;
 </script>
 
 <SettingAccordionState queryParam={QueryParameter.IS_OPEN}>
-  <SettingAccordion key="app-settings" title={$t('app_settings')} subtitle={$t('manage_the_app_settings')}>
+  <SettingAccordion
+    icon={mdiCogOutline}
+    key="app-settings"
+    title={$t('app_settings')}
+    subtitle={$t('manage_the_app_settings')}
+  >
     <AppSettings />
   </SettingAccordion>
 
-  <SettingAccordion key="account" title={$t('account')} subtitle={$t('manage_your_account')}>
+  <SettingAccordion icon={mdiAccountOutline} key="account" title={$t('account')} subtitle={$t('manage_your_account')}>
     <UserProfileSettings />
   </SettingAccordion>
 
-  <SettingAccordion key="api-keys" title={$t('api_keys')} subtitle={$t('manage_your_api_keys')}>
+  <SettingAccordion
+    icon={mdiServerOutline}
+    key="user-usage-info"
+    title={$t('user_usage_stats')}
+    subtitle={$t('user_usage_stats_description')}
+  >
+    <UserUsageStatistic />
+  </SettingAccordion>
+
+  <SettingAccordion icon={mdiApi} key="api-keys" title={$t('api_keys')} subtitle={$t('manage_your_api_keys')}>
     <UserAPIKeyList bind:keys />
   </SettingAccordion>
 
-  <SettingAccordion key="authorized-devices" title={$t('authorized_devices')} subtitle={$t('manage_your_devices')}>
+  <SettingAccordion
+    icon={mdiDevices}
+    key="authorized-devices"
+    title={$t('authorized_devices')}
+    subtitle={$t('manage_your_devices')}
+  >
     <DeviceList bind:devices={sessions} />
   </SettingAccordion>
 
   <SettingAccordion
+    icon={mdiDownload}
     key="download-settings"
     title={$t('download_settings')}
     subtitle={$t('download_settings_description')}
@@ -53,16 +92,27 @@
     <DownloadSettings />
   </SettingAccordion>
 
-  <SettingAccordion key="feature" title={$t('features')} subtitle={$t('features_setting_description')}>
+  <SettingAccordion
+    icon={mdiFeatureSearchOutline}
+    key="feature"
+    title={$t('features')}
+    subtitle={$t('features_setting_description')}
+  >
     <FeatureSettings />
   </SettingAccordion>
 
-  <SettingAccordion key="notifications" title={$t('notifications')} subtitle={$t('notifications_setting_description')}>
+  <SettingAccordion
+    icon={mdiBellOutline}
+    key="notifications"
+    title={$t('notifications')}
+    subtitle={$t('notifications_setting_description')}
+  >
     <NotificationsSettings />
   </SettingAccordion>
 
   {#if $featureFlags.loaded && $featureFlags.oauth}
     <SettingAccordion
+      icon={mdiTwoFactorAuthentication}
       key="oauth"
       title={$t('oauth')}
       subtitle={$t('manage_your_oauth_connection')}
@@ -72,15 +122,21 @@
     </SettingAccordion>
   {/if}
 
-  <SettingAccordion key="password" title={$t('password')} subtitle={$t('change_your_password')}>
+  <SettingAccordion icon={mdiOnepassword} key="password" title={$t('password')} subtitle={$t('change_your_password')}>
     <ChangePasswordSettings />
   </SettingAccordion>
 
-  <SettingAccordion key="partner-sharing" title={$t('partner_sharing')} subtitle={$t('manage_sharing_with_partners')}>
+  <SettingAccordion
+    icon={mdiAccountGroupOutline}
+    key="partner-sharing"
+    title={$t('partner_sharing')}
+    subtitle={$t('manage_sharing_with_partners')}
+  >
     <PartnerSettings user={$user} />
   </SettingAccordion>
 
   <SettingAccordion
+    icon={mdiKeyOutline}
     key="user-purchase-settings"
     title={$t('user_purchase_settings')}
     subtitle={$t('user_purchase_settings_description')}
diff --git a/web/src/lib/components/user-settings-page/user-usage-statistic.svelte b/web/src/lib/components/user-settings-page/user-usage-statistic.svelte
new file mode 100644
index 0000000000..f7de1d8f64
--- /dev/null
+++ b/web/src/lib/components/user-settings-page/user-usage-statistic.svelte
@@ -0,0 +1,114 @@
+<script lang="ts">
+  import { locale } from '$lib/stores/preferences.store';
+  import {
+    getAlbumStatistics,
+    getAssetStatistics,
+    type AlbumStatisticsResponseDto,
+    type AssetStatsResponseDto,
+  } from '@immich/sdk';
+  import { onMount } from 'svelte';
+  import { t } from 'svelte-i18n';
+
+  let timelineStats: AssetStatsResponseDto = $state({
+    videos: 0,
+    images: 0,
+    total: 0,
+  });
+
+  let favoriteStats: AssetStatsResponseDto = $state({
+    videos: 0,
+    images: 0,
+    total: 0,
+  });
+
+  let archiveStats: AssetStatsResponseDto = $state({
+    videos: 0,
+    images: 0,
+    total: 0,
+  });
+
+  let trashStats: AssetStatsResponseDto = $state({
+    videos: 0,
+    images: 0,
+    total: 0,
+  });
+
+  let albumStats: AlbumStatisticsResponseDto = $state({
+    owned: 0,
+    shared: 0,
+    notShared: 0,
+  });
+
+  const getUsage = async () => {
+    [timelineStats, favoriteStats, archiveStats, trashStats, albumStats] = await Promise.all([
+      getAssetStatistics({ isArchived: false }),
+      getAssetStatistics({ isFavorite: true }),
+      getAssetStatistics({ isArchived: true }),
+      getAssetStatistics({ isTrashed: true }),
+      getAlbumStatistics(),
+    ]);
+  };
+
+  onMount(async () => {
+    await getUsage();
+  });
+</script>
+
+{#snippet row(viewName: string, stats: AssetStatsResponseDto)}
+  <tr
+    class="flex h-14 w-full place-items-center text-center dark:text-immich-dark-fg odd:bg-immich-bg even:bg-immich-gray odd:dark:bg-immich-dark-gray/50 even:dark:bg-immich-dark-gray/75"
+  >
+    <td class="w-1/4 px-4 text-sm">{viewName}</td>
+    <td class="w-1/4 px-4 text-sm">{stats.images.toLocaleString($locale)}</td>
+    <td class="w-1/4 px-4 text-sm">{stats.videos.toLocaleString($locale)}</td>
+    <td class="w-1/4 px-4">{stats.total.toLocaleString($locale)}</td>
+  </tr>
+{/snippet}
+
+<section class="my-6">
+  <p class="text-xs dark:text-white uppercase">{$t('photos_and_videos')}</p>
+  <div class="overflow-x-auto">
+    <table class="w-full text-left mt-4">
+      <thead
+        class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
+      >
+        <tr class="flex w-full place-items-center text-sm font-medium text-center">
+          <th class="w-1/4">{$t('view_name')}</th>
+          <th class="w-1/4">{$t('photos')}</th>
+          <th class="w-1/4">{$t('videos')}</th>
+          <th class="w-1/4">{$t('total')}</th>
+        </tr>
+      </thead>
+      <tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
+        {@render row($t('timeline'), timelineStats)}
+        {@render row($t('favorites'), favoriteStats)}
+        {@render row($t('archive'), archiveStats)}
+        {@render row($t('trash'), trashStats)}
+      </tbody>
+    </table>
+  </div>
+
+  <div class="mt-6">
+    <p class="text-xs dark:text-white uppercase">{$t('albums')}</p>
+  </div>
+  <div class="overflow-x-auto">
+    <table class="w-full text-left mt-4">
+      <thead
+        class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
+      >
+        <tr class="flex w-full place-items-center text-sm font-medium text-center">
+          <th class="w-1/2">{$t('owned')}</th>
+          <th class="w-1/2">{$t('shared')}</th>
+        </tr>
+      </thead>
+      <tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
+        <tr
+          class="flex h-14 w-full place-items-center text-center dark:text-immich-dark-fg bg-immich-bg dark:bg-immich-dark-gray/50"
+        >
+          <td class="w-1/2 px-4 text-sm">{albumStats.owned.toLocaleString($locale)}</td>
+          <td class="w-1/2 px-4 text-sm">{albumStats.shared.toLocaleString($locale)}</td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+</section>
diff --git a/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte b/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte
index 2103250b54..19190745d1 100644
--- a/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte
+++ b/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte
@@ -7,13 +7,17 @@
   import { mdiHeart, mdiMagnifyPlus, mdiImageMultipleOutline } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
-  export let asset: AssetResponseDto;
-  export let isSelected: boolean;
-  export let onSelectAsset: (asset: AssetResponseDto) => void;
-  export let onViewAsset: (asset: AssetResponseDto) => void;
+  interface Props {
+    asset: AssetResponseDto;
+    isSelected: boolean;
+    onSelectAsset: (asset: AssetResponseDto) => void;
+    onViewAsset: (asset: AssetResponseDto) => void;
+  }
 
-  $: isFromExternalLibrary = !!asset.libraryId;
-  $: assetData = JSON.stringify(asset, null, 2);
+  let { asset, isSelected, onSelectAsset, onViewAsset }: Props = $props();
+
+  let isFromExternalLibrary = $derived(!!asset.libraryId);
+  let assetData = $derived(JSON.stringify(asset, null, 2));
 </script>
 
 <div
@@ -24,7 +28,7 @@
   <div class="relative w-full">
     <button
       type="button"
-      on:click={() => onSelectAsset(asset)}
+      onclick={() => onSelectAsset(asset)}
       class="block relative w-full"
       aria-pressed={isSelected}
       aria-label={$t('keep')}
@@ -74,7 +78,7 @@
 
     <button
       type="button"
-      on:click={() => onViewAsset(asset)}
+      onclick={() => onViewAsset(asset)}
       class="absolute rounded-full top-1 left-1 text-gray-200 p-2 hover:text-white bg-black/35 hover:bg-black/50"
       title={$t('view')}
     >
diff --git a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte
index 2f1efc487c..2afeffb6e4 100644
--- a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte
+++ b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte
@@ -11,26 +11,30 @@
   import { mdiCheck, mdiTrashCanOutline, mdiImageMultipleOutline } from '@mdi/js';
   import { onDestroy, onMount } from 'svelte';
   import { t } from 'svelte-i18n';
+  import { SvelteSet } from 'svelte/reactivity';
 
-  export let assets: AssetResponseDto[];
-  export let onResolve: (duplicateAssetIds: string[], trashIds: string[]) => void;
-  export let onStack: (assets: AssetResponseDto[]) => void;
+  interface Props {
+    assets: AssetResponseDto[];
+    onResolve: (duplicateAssetIds: string[], trashIds: string[]) => void;
+    onStack: (assets: AssetResponseDto[]) => void;
+  }
+
+  let { assets, onResolve, onStack }: Props = $props();
   const { isViewing: showAssetViewer, asset: viewingAsset, setAsset } = assetViewingStore;
   const getAssetIndex = (id: string) => assets.findIndex((asset) => asset.id === id);
 
-  let selectedAssetIds = new Set<string>();
-  $: trashCount = assets.length - selectedAssetIds.size;
+  let selectedAssetIds = $state(new SvelteSet<string>());
+  let trashCount = $derived(assets.length - selectedAssetIds.size);
 
   onMount(() => {
     const suggestedAsset = suggestDuplicateByFileSize(assets);
 
     if (!suggestedAsset) {
-      selectedAssetIds = new Set(assets[0].id);
+      selectedAssetIds = new SvelteSet(assets[0].id);
       return;
     }
 
     selectedAssetIds.add(suggestedAsset.id);
-    selectedAssetIds = selectedAssetIds;
   });
 
   onDestroy(() => {
@@ -43,17 +47,14 @@
     } else {
       selectedAssetIds.add(asset.id);
     }
-
-    selectedAssetIds = selectedAssetIds;
   };
 
   const onSelectNone = () => {
     selectedAssetIds.clear();
-    selectedAssetIds = selectedAssetIds;
   };
 
   const onSelectAll = () => {
-    selectedAssetIds = new Set(assets.map((asset) => asset.id));
+    selectedAssetIds = new SvelteSet(assets.map((asset) => asset.id));
   };
 
   const handleResolve = () => {
@@ -100,12 +101,12 @@
       <button
         type="button"
         class="px-4 py-3 flex place-items-center gap-2 rounded-tl-full rounded-bl-full dark:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/90 bg-immich-primary/25 hover:bg-immich-primary/50"
-        on:click={onSelectAll}><Icon path={mdiCheck} size="20" />{$t('select_keep_all')}</button
+        onclick={onSelectAll}><Icon path={mdiCheck} size="20" />{$t('select_keep_all')}</button
       >
       <button
         type="button"
         class="px-4 py-3 flex place-items-center gap-2 rounded-tr-full rounded-br-full dark:bg-immich-dark-primary/50 hover:dark:bg-immich-dark-primary/70 bg-immich-primary hover:bg-immich-primary/80 text-white"
-        on:click={onSelectNone}><Icon path={mdiTrashCanOutline} size="20" />{$t('select_trash_all')}</button
+        onclick={onSelectNone}><Icon path={mdiTrashCanOutline} size="20" />{$t('select_trash_all')}</button
       >
     </div>
 
@@ -116,7 +117,7 @@
           size="sm"
           color="primary"
           class="flex place-items-center rounded-tl-full rounded-bl-full gap-2"
-          on:click={handleResolve}
+          onclick={handleResolve}
         >
           <Icon path={mdiCheck} size="20" />{$t('keep_all')}
         </Button>
@@ -125,7 +126,7 @@
           size="sm"
           color="red"
           class="flex place-items-center rounded-tl-full rounded-bl-full gap-2 py-3"
-          on:click={handleResolve}
+          onclick={handleResolve}
         >
           <Icon path={mdiTrashCanOutline} size="20" />{trashCount === assets.length
             ? $t('trash_all')
@@ -136,7 +137,7 @@
         size="sm"
         color="primary"
         class="flex place-items-center rounded-tr-full rounded-br-full  gap-2"
-        on:click={handleStack}
+        onclick={handleStack}
         disabled={selectedAssetIds.size !== 1}
       >
         <Icon path={mdiImageMultipleOutline} size="20" />{$t('stack')}
@@ -151,15 +152,15 @@
       <AssetViewer
         asset={$viewingAsset}
         showNavigation={assets.length > 1}
-        on:next={() => {
+        onNext={() => {
           const index = getAssetIndex($viewingAsset.id) + 1;
           setAsset(assets[index % assets.length]);
         }}
-        on:previous={() => {
+        onPrevious={() => {
           const index = getAssetIndex($viewingAsset.id) - 1 + assets.length;
           setAsset(assets[index % assets.length]);
         }}
-        on:close={() => {
+        onClose={() => {
           assetViewingStore.showAssetViewer(false);
           handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
         }}
diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts
index c37ecf08c9..0a5a4e1c2a 100644
--- a/web/src/lib/constants.ts
+++ b/web/src/lib/constants.ts
@@ -9,6 +9,7 @@ export enum AssetAction {
   ADD = 'add',
   ADD_TO_ALBUM = 'add-to-album',
   UNSTACK = 'unstack',
+  KEEP_THIS_DELETE_OTHERS = 'keep-this-delete-others',
 }
 
 export enum AppRoute {
@@ -84,6 +85,11 @@ export enum QueryParameter {
   PATH = 'path',
 }
 
+export enum SessionStorageKey {
+  INFINITE_SCROLL_PAGE = 'infiniteScrollPage',
+  SCROLL_POSITION = 'scrollPosition',
+}
+
 export enum OpenSettingQueryParameterValue {
   OAUTH = 'oauth',
   JOB = 'job',
@@ -251,74 +257,83 @@ export const locales = [
   { code: 'zu-ZA', name: 'Zulu (South Africa)' },
 ];
 
-export const defaultLang = { name: 'English', code: 'en', loader: () => import('$lib/i18n/en.json') };
+export const defaultLang = { name: 'English', code: 'en', loader: () => import('$i18n/en.json') };
 
 export const langs = [
-  { name: 'Afrikaans', code: 'af', loader: () => import('$lib/i18n/af.json') },
-  { name: 'Arabic', code: 'ar', loader: () => import('$lib/i18n/ar.json') },
-  { name: 'Azerbaijani', code: 'az', loader: () => import('$lib/i18n/az.json') },
-  { name: 'Belarusian', code: 'be', loader: () => import('$lib/i18n/be.json') },
-  { name: 'Bulgarian', code: 'bg', loader: () => import('$lib/i18n/bg.json') },
-  { name: 'Bislama', code: 'bi', loader: () => import('$lib/i18n/bi.json') },
-  { name: 'Catalan', code: 'ca', loader: () => import('$lib/i18n/ca.json') },
-  { name: 'Czech', code: 'cs', loader: () => import('$lib/i18n/cs.json') },
-  { name: 'Danish', code: 'da', loader: () => import('$lib/i18n/da.json') },
-  { name: 'German', code: 'de', loader: () => import('$lib/i18n/de.json') },
+  { name: 'Afrikaans', code: 'af', loader: () => import('$i18n/af.json') },
+  { name: 'Arabic', code: 'ar', loader: () => import('$i18n/ar.json') },
+  { name: 'Azerbaijani', code: 'az', loader: () => import('$i18n/az.json') },
+  { name: 'Belarusian', code: 'be', loader: () => import('$i18n/be.json') },
+  { name: 'Bulgarian', code: 'bg', loader: () => import('$i18n/bg.json') },
+  { name: 'Bislama', code: 'bi', loader: () => import('$i18n/bi.json') },
+  { name: 'Bengali', code: 'bn', loader: () => import('$i18n/bn.json') },
+  { name: 'Catalan', code: 'ca', loader: () => import('$i18n/ca.json') },
+  { name: 'Czech', code: 'cs', loader: () => import('$i18n/cs.json') },
+  { name: 'Chuvash', code: 'cv', loader: () => import('$i18n/cv.json') },
+  { name: 'Danish', code: 'da', loader: () => import('$i18n/da.json') },
+  { name: 'German', code: 'de', loader: () => import('$i18n/de.json') },
   defaultLang,
-  { name: 'Greek', code: 'el', loader: () => import('$lib/i18n/el.json') },
-  { name: 'Spanish', code: 'es', loader: () => import('$lib/i18n/es.json') },
-  { name: 'Estonian', code: 'et', loader: () => import('$lib/i18n/et.json') },
-  { name: 'Persian', code: 'fa', loader: () => import('$lib/i18n/fa.json') },
-  { name: 'Finnish', code: 'fi', loader: () => import('$lib/i18n/fi.json') },
-  { name: 'French', code: 'fr', loader: () => import('$lib/i18n/fr.json') },
-  { name: 'Hebrew', code: 'he', loader: () => import('$lib/i18n/he.json') },
-  { name: 'Hindi', code: 'hi', loader: () => import('$lib/i18n/hi.json') },
-  { name: 'Croatian', code: 'hr', loader: () => import('$lib/i18n/hr.json') },
-  { name: 'Hungarian', code: 'hu', loader: () => import('$lib/i18n/hu.json') },
-  { name: 'Armenian', code: 'hy', loader: () => import('$lib/i18n/hy.json') },
-  { name: 'Indonesian', code: 'id', loader: () => import('$lib/i18n/id.json') },
-  { name: 'Italian', code: 'it', loader: () => import('$lib/i18n/it.json') },
-  { name: 'Japanese', code: 'ja', loader: () => import('$lib/i18n/ja.json') },
-  { name: 'Kurdish (Northern)', code: 'kmr', loader: () => import('$lib/i18n/kmr.json') },
-  { name: 'Korean', code: 'ko', loader: () => import('$lib/i18n/ko.json') },
-  { name: 'Lithuanian', code: 'lt', loader: () => import('$lib/i18n/lt.json') },
-  { name: 'Latvian', code: 'lv', loader: () => import('$lib/i18n/lv.json') },
-  { name: 'Mongolian', code: 'mn', loader: () => import('$lib/i18n/mn.json') },
-  { name: 'Malay', code: 'ms', loader: () => import('$lib/i18n/ms.json') },
-  { name: 'Norwegian Bokmål', code: 'nb-NO', weblateCode: 'nb_NO', loader: () => import('$lib/i18n/nb_NO.json') },
-  { name: 'Dutch', code: 'nl', loader: () => import('$lib/i18n/nl.json') },
-  { name: 'Polish', code: 'pl', loader: () => import('$lib/i18n/pl.json') },
-  { name: 'Portuguese', code: 'pt', loader: () => import('$lib/i18n/pt.json') },
-  { name: 'Portuguese (Brazil) ', code: 'pt-BR', weblateCode: 'pt_BR', loader: () => import('$lib/i18n/pt_BR.json') },
-  { name: 'Romanian', code: 'ro', loader: () => import('$lib/i18n/ro.json') },
-  { name: 'Russian', code: 'ru', loader: () => import('$lib/i18n/ru.json') },
-  { name: 'Slovak', code: 'sk', loader: () => import('$lib/i18n/sk.json') },
-  { name: 'Slovenian', code: 'sl', loader: () => import('$lib/i18n/sl.json') },
+  { name: 'Greek', code: 'el', loader: () => import('$i18n/el.json') },
+  { name: 'Spanish', code: 'es', loader: () => import('$i18n/es.json') },
+  { name: 'Estonian', code: 'et', loader: () => import('$i18n/et.json') },
+  { name: 'Persian', code: 'fa', loader: () => import('$i18n/fa.json') },
+  { name: 'Finnish', code: 'fi', loader: () => import('$i18n/fi.json') },
+  { name: 'Filipino', code: 'fil', loader: () => import('$i18n/fil.json') },
+  { name: 'French', code: 'fr', loader: () => import('$i18n/fr.json') },
+  { name: 'Hebrew', code: 'he', loader: () => import('$i18n/he.json') },
+  { name: 'Hindi', code: 'hi', loader: () => import('$i18n/hi.json') },
+  { name: 'Croatian', code: 'hr', loader: () => import('$i18n/hr.json') },
+  { name: 'Hungarian', code: 'hu', loader: () => import('$i18n/hu.json') },
+  { name: 'Armenian', code: 'hy', loader: () => import('$i18n/hy.json') },
+  { name: 'Indonesian', code: 'id', loader: () => import('$i18n/id.json') },
+  { name: 'Italian', code: 'it', loader: () => import('$i18n/it.json') },
+  { name: 'Japanese', code: 'ja', loader: () => import('$i18n/ja.json') },
+  { name: 'Kurdish (Northern)', code: 'kmr', loader: () => import('$i18n/kmr.json') },
+  { name: 'Korean', code: 'ko', loader: () => import('$i18n/ko.json') },
+  { name: 'Luxembourgish', code: 'lb', loader: () => import('$i18n/lb.json') },
+  { name: 'Lithuanian', code: 'lt', loader: () => import('$i18n/lt.json') },
+  { name: 'Latvian', code: 'lv', loader: () => import('$i18n/lv.json') },
+  { name: 'Malay (Pattani)', code: 'mfa', loader: () => import('$i18n/mfa.json') },
+  { name: 'Macedonian', code: 'mk', loader: () => import('$i18n/mk.json') },
+  { name: 'Mongolian', code: 'mn', loader: () => import('$i18n/mn.json') },
+  { name: 'Marathi', code: 'mr', loader: () => import('$i18n/mr.json') },
+  { name: 'Malay', code: 'ms', loader: () => import('$i18n/ms.json') },
+  { name: 'Norwegian Bokmål', code: 'nb-NO', weblateCode: 'nb_NO', loader: () => import('$i18n/nb_NO.json') },
+  { name: 'Dutch', code: 'nl', loader: () => import('$i18n/nl.json') },
+  { name: 'Norwegian Nynorsk', code: 'nn', loader: () => import('$i18n/nn.json') },
+  { name: 'Polish', code: 'pl', loader: () => import('$i18n/pl.json') },
+  { name: 'Portuguese', code: 'pt', loader: () => import('$i18n/pt.json') },
+  { name: 'Portuguese (Brazil) ', code: 'pt-BR', weblateCode: 'pt_BR', loader: () => import('$i18n/pt_BR.json') },
+  { name: 'Romanian', code: 'ro', loader: () => import('$i18n/ro.json') },
+  { name: 'Russian', code: 'ru', loader: () => import('$i18n/ru.json') },
+  { name: 'Slovak', code: 'sk', loader: () => import('$i18n/sk.json') },
+  { name: 'Slovenian', code: 'sl', loader: () => import('$i18n/sl.json') },
   {
     name: 'Serbian (Cyrillic)',
     code: 'sr-Cyrl',
     weblateCode: 'sr_Cyrl',
-    loader: () => import('$lib/i18n/sr_Cyrl.json'),
+    loader: () => import('$i18n/sr_Cyrl.json'),
   },
-  { name: 'Serbian (Latin)', code: 'sr-Latn', weblateCode: 'sr_Latn', loader: () => import('$lib/i18n/sr_Latn.json') },
-  { name: 'Swedish', code: 'sv', loader: () => import('$lib/i18n/sv.json') },
-  { name: 'Tamil', code: 'ta', loader: () => import('$lib/i18n/ta.json') },
-  { name: 'Telugu', code: 'te', loader: () => import('$lib/i18n/te.json') },
-  { name: 'Thai', code: 'th', loader: () => import('$lib/i18n/th.json') },
-  { name: 'Turkish', code: 'tr', loader: () => import('$lib/i18n/tr.json') },
-  { name: 'Ukrainian', code: 'uk', loader: () => import('$lib/i18n/uk.json') },
-  { name: 'Vietnamese', code: 'vi', loader: () => import('$lib/i18n/vi.json') },
+  { name: 'Serbian (Latin)', code: 'sr-Latn', weblateCode: 'sr_Latn', loader: () => import('$i18n/sr_Latn.json') },
+  { name: 'Swedish', code: 'sv', loader: () => import('$i18n/sv.json') },
+  { name: 'Tamil', code: 'ta', loader: () => import('$i18n/ta.json') },
+  { name: 'Telugu', code: 'te', loader: () => import('$i18n/te.json') },
+  { name: 'Thai', code: 'th', loader: () => import('$i18n/th.json') },
+  { name: 'Turkish', code: 'tr', loader: () => import('$i18n/tr.json') },
+  { name: 'Ukrainian', code: 'uk', loader: () => import('$i18n/uk.json') },
+  { name: 'Urdu', code: 'ur', loader: () => import('$i18n/ur.json') },
+  { name: 'Vietnamese', code: 'vi', loader: () => import('$i18n/vi.json') },
   {
     name: 'Chinese (Traditional)',
-    code: 'zh-Hant',
+    code: 'zh-TW',
     weblateCode: 'zh_Hant',
-    loader: () => import('$lib/i18n/zh_Hant.json'),
+    loader: () => import('$i18n/zh_Hant.json'),
   },
   {
     name: 'Chinese (Simplified)',
-    code: 'zh-Hans',
+    code: 'zh-CN',
     weblateCode: 'zh_SIMPLIFIED',
-    loader: () => import('$lib/i18n/zh_SIMPLIFIED.json'),
+    loader: () => import('$i18n/zh_SIMPLIFIED.json'),
   },
   { name: 'Development (keys only)', code: 'dev', loader: () => Promise.resolve({}) },
 ];
@@ -327,3 +342,47 @@ export enum ImmichProduct {
   Client = 'immich-client',
   Server = 'immich-server',
 }
+
+export enum SettingInputFieldType {
+  EMAIL = 'email',
+  TEXT = 'text',
+  NUMBER = 'number',
+  PASSWORD = 'password',
+  COLOR = 'color',
+}
+
+export enum AlbumPageViewMode {
+  LINK_SHARING = 'link-sharing',
+  SELECT_USERS = 'select-users',
+  SELECT_THUMBNAIL = 'select-thumbnail',
+  SELECT_ASSETS = 'select-assets',
+  VIEW_USERS = 'view-users',
+  VIEW = 'view',
+  OPTIONS = 'options',
+}
+
+export enum PersonPageViewMode {
+  VIEW_ASSETS = 'view-assets',
+  SELECT_PERSON = 'select-person',
+  MERGE_PEOPLE = 'merge-people',
+  SUGGEST_MERGE = 'suggest-merge',
+  BIRTH_DATE = 'birth-date',
+  UNASSIGN_ASSETS = 'unassign-faces',
+}
+
+export enum MediaType {
+  All = 'all',
+  Image = 'image',
+  Video = 'video',
+}
+
+export enum ProgressBarStatus {
+  Playing = 'playing',
+  Paused = 'paused',
+}
+
+export enum ToggleVisibility {
+  HIDE_ALL = 'hide-all',
+  HIDE_UNNANEMD = 'hide-unnamed',
+  SHOW_ALL = 'show-all',
+}
diff --git a/web/src/lib/i18n.spec.ts b/web/src/lib/i18n.spec.ts
index 13d926e647..63aae0419c 100644
--- a/web/src/lib/i18n.spec.ts
+++ b/web/src/lib/i18n.spec.ts
@@ -4,7 +4,7 @@ import { readFileSync, readdirSync } from 'node:fs';
 
 describe('i18n', () => {
   describe('loaders', () => {
-    const languageFiles = readdirSync('src/lib/i18n').sort();
+    const languageFiles = readdirSync('../i18n').sort();
     for (const filename of languageFiles) {
       test(`${filename} should have a loader`, async () => {
         const code = filename.replaceAll('.json', '');
@@ -17,7 +17,7 @@ describe('i18n', () => {
         // verify it loads the right file
         const module: { default?: unknown } = await item.loader();
         const translations = JSON.stringify(module.default, null, 2).trim();
-        const content = readFileSync(`src/lib/i18n/${filename}`).toString().trim();
+        const content = readFileSync(`../i18n/${filename}`).toString().trim();
         expect(translations === content, `${item.name} did not load ${filename}`).toEqual(true);
       });
     }
diff --git a/web/src/lib/i18n/el.json b/web/src/lib/i18n/el.json
deleted file mode 100644
index 5f88772ea7..0000000000
--- a/web/src/lib/i18n/el.json
+++ /dev/null
@@ -1,574 +0,0 @@
-{
-  "about": "Σχετικά",
-  "account": "Λογαριασμός",
-  "account_settings": "Ρυθμίσεις Λογαριασμού",
-  "acknowledge": "Έλαβα γνώση",
-  "action": "Ενέργεια",
-  "actions": "Ενέργειες",
-  "active": "Ενεργά",
-  "activity": "Δραστηριότητα",
-  "add": "Προσθήκη",
-  "add_a_description": "Προσθήκη περιγραφής",
-  "add_a_location": "Προσθήκη μιας τοποθεσίας",
-  "add_a_name": "Προσθήκη Ονόματος",
-  "add_a_title": "Προσθήκη τίτλου",
-  "add_exclusion_pattern": "Προσθήκη προτύπου αποκλεισμού",
-  "add_import_path": "Προσθήκη διαδρομής εισαγωγής",
-  "add_location": "Προσθήκη τοποθεσίας",
-  "add_more_users": "Προσθήκη επιπλέον χρηστών",
-  "add_partner": "Προσθήκη συνεργάτη",
-  "add_path": "Προσθήκη διαδρομής",
-  "add_photos": "Προσθήκη φωτογραφιών",
-  "add_to": "Προσθήκη σε...",
-  "add_to_album": "Προσθήκη σε άλμπουμ",
-  "add_to_shared_album": "Προσθήκη σε κοινόχρηστο άλμπουμ",
-  "added_to_archive": "Αρχειοθέτηση",
-  "added_to_favorites": "Προστέθηκε στα αγαπημένα",
-  "added_to_favorites_count": "Προστέθηκαν {count, number} στα αγαπημένα",
-  "admin": {
-    "add_exclusion_pattern_description": "Προσθέστε πρότυπα αποκλεισμού. Υποστηρίζεται η επιλογή πολλών με *, **, και ?. Για να αγνοηθούν όλα τα αρχεία σε έναν φάκελο με το όνομα \"Raw\", χρησιμοποιήστε \"**/Raw/**\". Για να αγνοηθούν όλα τα αρχεία με κατάληξη \".tif\", χρησιμοποιήστε \"**/*.tif\". Για να αγνοηθεί μία απόλυτη διαδρομή, χρησιμοποιήστε \"/path/to/ignore/**\".",
-    "authentication_settings": "Ρυθμίσεις ελέγχου ταυτότητας",
-    "authentication_settings_description": "Διαχείριση κωδικού πρόσβασης, OAuth και άλλες ρυθμίσεις ελέγχου ταυτότητας",
-    "authentication_settings_disable_all": "Είστε βέβαιοι ότι θέλετε να απενεργοποιήσετε όλες τις μεθόδους σύνδεσης; Η σύνδεση θα απενεργοποιηθεί πλήρως.",
-    "authentication_settings_reenable": "Για να επαναενεργοποιηθεί, χρησιμοποιήστε μία <link>Server Command</link>.",
-    "background_task_job": "Εργασίες Παρασκηνίου",
-    "check_all": "Έλεγχος Όλων",
-    "cleared_jobs": "Εκκαθάριση εργασιών για: {job}",
-    "config_set_by_file": "Η διαμόρφωση γίνεται προς το παρόν από ένα αρχείο config",
-    "confirm_delete_library": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τη βιβλιοθήκη {library};",
-    "confirm_delete_library_assets": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτή τη βιβλιοθήκη; Αυτό θα διαγράψει τα {count, plural, one {# contained asset} other {all # contained assets}} από το Immich και δεν μπορεί να αναιρεθεί. Τα αρχεία θα παραμείνουν στον δίσκο.",
-    "confirm_email_below": "Για επιβεβαίωση, πληκτρολογήστε \"{email}\" παρακάτω",
-    "confirm_reprocess_all_faces": "Είστε βέβαιοι ότι θέλετε να επεξεργαστείτε ξανά όλα τα πρόσωπα; Αυτό θα διαγράψει επίσης άτομα με όνομα.",
-    "confirm_user_password_reset": "Είστε βέβαιοι ότι θέλετε να επαναφέρετε τον κωδικό πρόσβασης του χρήστη {user};",
-    "disable_login": "Απενεργοποίηση σύνδεσης κατά την είσοδο",
-    "duplicate_detection_job_description": "Εκτελέστε τη εκμάθηση μηχανής σε στοιχεία για να εντοπίσετε παρόμοιες εικόνες. Βασίζεται στην Έξυπνη Αναζήτηση",
-    "exclusion_pattern_description": "Τα πρότυπα αποκλεισμού σας επιτρέπουν να αγνοείται αρχεία κκαι φακέλους όσο σαρώνεται η βιβλιοθήκη. Αυτό είναι χρήσιμο εάν εχετε φακέλους που περιέχουν αρχεία που δεν θέλετε να εισαγάγετε, όπως αρχεία RAW.",
-    "external_library_created_at": "Εξωτερική βιβλιοθήκη (δημιουργήθηκε {date})",
-    "external_library_management": "Διαχείριση Εξωτερικών Βιβλιοθηκών",
-    "face_detection": "Αναγνώριση προσώπου",
-    "face_detection_description": "Εντοπίστε τα πρόσωπα σε στοιχεία χρησιμοποιώντας μηχανική εκμάθηση. Για βίντεο, λαμβάνεται υπόψη μόνο η μικρογραφία. Η επιλογή \"Όλα\" επεξεργάζεται εκ νέου όλα τα στοιχεία. Η επιλογή \"Όσα Λείπουν\" προσθέτει στην ουρά στοιχεία που δεν έχουν υποστεί ακόμη επεξεργασία. Τα πρόσωπα που έχουν εντοπιστεί θα μπουν στην ουρά για την Αναγνώριση Προσώπου μετά την ολοκλήρωση της Ανίχνευσης Προσώπου, ομαδοποιώντας τα σε υπάρχοντα ή νέα άτομα.",
-    "facial_recognition_job_description": "Ομαδοποιήστε εντοπισμένα πρόσωπα σε άτομα. Αυτό το βήμα εκτελείται αφού ολοκληρωθεί η Ανίχνευση προσώπου. Η επιλογή \"Όλα\" ομαδοποιεί εκ νέου όλα τα πρόσωπα. Η επιλογή \"Όσα Λείπουν\" ομαδοποιεί πρόσωπα που δεν έχουν αντιστοιχηθεί σε κάποιο άτομο.",
-    "failed_job_command": "Η Εντολή {command} απέτυχε για την εργασία: {job}",
-    "force_delete_user_warning": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Αυτό θα αφαιρέσει άμεσα το χρήστη και όλα τα στοιχεία. Αυτό δεν μπορεί να αναιρεθεί και τα αρχεία δεν μπορούν να ανακτηθούν.",
-    "forcing_refresh_library_files": "Επιβολή ανανέωσης όλων των αρχείων της βιβλιοθήκης",
-    "image_format_description": "Η μορφή WebP παράγει μικρότερα αρχεία από τη μορφή JPEG, αλλά είναι πιο αργή στην κωδικοποίηση.",
-    "image_prefer_embedded_preview": "Προτίμηση ενσωματωμένης προεπισκόπησης",
-    "image_prefer_embedded_preview_setting_description": "Χρησιμοποιήστε ενσωματωμένες προεπισκοπίσεις για εικόνες RAW ως εισαγωγή στην επεξεργασία εικόνας όταν είναι διαθέσιμο. Αυτό μπορεί να δημιουργήσει πιο ακριβή χρωματα για κάποιες εικόνες, αλλά η ποιότητα των προεπισκοπίσεων εξαρτάται από την κάμερα και ενδέχεται να υπάρχουν περισσότερα μπιμπίκια λόγω συμπίεσης.",
-    "image_prefer_wide_gamut": "Προτίμηση ευρείας γκάμας",
-    "image_prefer_wide_gamut_setting_description": "Χρησιμοποιήστε Display P3 για τις μικρογραφίες. Αυτό διατηρεί την ζωντάνια των χρωμάτων σε εικόνες μεγάλου χρωματικού εύρους, αλλά ενδέχεται να εμφανίζονται αλλιώς σε παλαιότερες συσκευές με παλαιότερες εκδόσεις περιηγητών. Οι εικόνες sRGB μένουν ως έχουν για να αποφευχθούν χρωματικές αλλαγές.",
-    "image_preview_format": "Μορφή προεπισκόπησης",
-    "image_preview_resolution": "Ανάλυση προεπισκόπησης",
-    "image_preview_resolution_description": "Χρησιμοποιείται κατά την προβολή μιας φωτογραφίας και για μηχανική εκμάθηση. Οι υψηλότερες αναλύσεις μπορούν να διατηρήσουν περισσότερες λεπτομέρειες, αλλά χρειάζονται περισσότερο χρόνο για την κωδικοποίηση, έχουν μεγαλύτερα μεγέθη αρχείων και μπορούν να μειώσουν την απόκριση της εφαρμογής.",
-    "image_quality": "Ποιότητα",
-    "image_quality_description": "Ποιότητα εικόνας από 1-100. Μεγαλύτερη τιμή σημαίνει καλύτερη ποιότητα, αλλά παράγει μεγαλύτερα αρχεία. Αυτή η επιλογή επηρεάζει τις εικόνες προεπισκόπησης και μικρογραφιών.",
-    "image_settings": "Ρυθμίσεις Εικόνας",
-    "image_settings_description": "Διαχείριση της ποιότητας και της ανάλυσης των εικόνων που δημιουργούνται",
-    "image_thumbnail_format": "Μορφή μικρογραφίας",
-    "image_thumbnail_resolution": "Ανάλυση μικρογραφίας",
-    "image_thumbnail_resolution_description": "Χρησιμοποιείται κατά την προβολή ομάδων φωτογραφιών (κύριο χρονολόγιο, προβολή άλμπουμ κλπ.). Υψηλότερες αναλύσεις μπορούν να διατηρήσουν περισσότερες λεπτομέρειες, αλλά χρειάζονται περισσότερο χρόνο για την κωδικοποίηση, έχουν μεγαλύτερα μεγέθη αρχείων και μπορούν να μειώσουν την απόκριση της εφαρμογής.",
-    "job_concurrency": "{job} συγχρονισμός",
-    "job_not_concurrency_safe": "Αυτή η εργασία δεν είναι ασφαλής για ταυτόχρονη εκτέλεση.",
-    "job_settings": "Ρυθμίσεις Εργασιών",
-    "job_settings_description": "Διαχείρηση ταυτόχρονων εργασιών",
-    "job_status": "Κατάσταση Εργασιών",
-    "jobs_delayed": "{jobCount, plural, other {# delayed}}",
-    "jobs_failed": "{jobCount, plural, other {# failed}}",
-    "library_created": "Δημιουργήθηκε η βιβλιοθήκη: {library}",
-    "library_cron_expression": "Εκφράσεις Cron",
-    "library_cron_expression_description": "Ορισμός των διαστημάτων μεταξύ των σαρώσεων με χρήση cron μορφής. Για περισσότερες πληροφορίες παρακαλώ επισκεφθείτε το π.χ. <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Προκαθορισμένες εκφράσεις Cron",
-    "library_deleted": "Η βιβλιοθήκη διαγράφηκε",
-    "library_import_path_description": "Καθορίστε έναν φάκελο για εισαγωγή. Αυτός ο φάκελος, συμπεριλαμβανομένων των υποφακέλων του, θα σαρωθεί για εικόνες και βίντεο.",
-    "library_scanning": "Περιοδική Σάρωση",
-    "library_scanning_description": "Διαμόρφωση περιοδικής σάρωσης βιβλιοθήκης",
-    "library_scanning_enable_description": "Ενεργοποίηση περιοδικής σάρωσης βιβλιοθήκης",
-    "library_settings": "Εξωτερική Βιβλιοθήκη",
-    "library_settings_description": "Διαχείριση ρυθμίσεων εξωτερικής βιβλιοθήκης",
-    "library_tasks_description": "Εκτέλεση εργασιών βιβλιοθήκης",
-    "library_watching_enable_description": "Παρακολούθηση εξωτερικών βιβλιοθηκών για τροποποιήσεις αρχείων",
-    "library_watching_settings": "Παρακολούθηση βιβλιοθήκης (ΠΕΙΡΑΜΑΤΙΚΟ)",
-    "library_watching_settings_description": "Αυτόματη παρακολούθηση για τροποποιημένα αρχεία",
-    "logging_enable_description": "Ενεργοποίηση καταγραφής",
-    "logging_level_description": "Όταν είναι ενεργοποιημένο, τι επίπεδο καταγραφής να εφαρμοστεί.",
-    "logging_settings": "Καταγραφή",
-    "machine_learning_clip_model": "Μοντέλο CLIP",
-    "machine_learning_duplicate_detection": "Εντοπισμός Διπλότυπων",
-    "machine_learning_duplicate_detection_enabled": "Ενεργοποίηση εντοπισμού διπλότυπων",
-    "machine_learning_enabled": "Ενεργοποίηση μηχανικής εκμάθησης",
-    "machine_learning_enabled_description": "Εάν απενεργοποιηθεί, όλες οι λειτουργίες μηχανικής εκμάθησης θα απενεργοποιηθούν, ανεξάρτητα από τις παρακάτω ρυθμίσεις.",
-    "machine_learning_facial_recognition": "Αναγνώριση προσώπου",
-    "machine_learning_facial_recognition_description": "Εντοπισμός, αναγνώριση και ομαδοποίηση προσώπων σε εικόνες",
-    "machine_learning_facial_recognition_model": "Μοντέλο αναγνώρισης προσώπου",
-    "machine_learning_facial_recognition_model_description": "Τα μοντέλα παρατίθενται με φθίνουσα σειρά μεγέθους. Τα μεγαλύτερα μοντέλα είναι πιο αργά και χρησιμοποιούν περισσότερη μνήμη, αλλά παράγουν καλύτερα αποτελέσματα. Σημειώστε ότι πρέπει να εκτελέσετε ξανά την εργασία Ανίχνευση προσώπου για όλες τις εικόνες κατά την αλλαγή ενός μοντέλου.",
-    "machine_learning_facial_recognition_setting": "Ενεργοποίηση αναγνώρισης προσώπου",
-    "machine_learning_facial_recognition_setting_description": "Αν απενεργοποιηθεί, οι εικόνες δεν θα κωδικοποιούνται για αναγνώριση προσώπου και δεν θα συμπληρώνουν την ενότητα Άτομα στη σελίδα Εξερεύνηση.",
-    "machine_learning_max_detection_distance": "Μέγιστη απόσταση ανίχνευσης",
-    "machine_learning_max_detection_distance_description": "Η μέγιστη απόσταση μεταξύ δύο εικόνων για να θεωρηθούν διπλότυπες, που κυμαίνεται από 0,001-0,1. Οι υψηλότερες τιμές θα εντοπίσουν περισσότερες διπλότυπες, αλλά μπορεί να οδηγήσουν σε ψευδώς θετικά αποτελέσματα.",
-    "machine_learning_max_recognition_distance": "Μέγιστη απόσταση αναγνώρισης",
-    "machine_learning_max_recognition_distance_description": "Η μέγιστη απόσταση μεταξύ δύο προσώπων για να θεωρείται το ίδιο άτομο, που κυμαίνεται από 0-2. Η μείωση αυτή μπορεί να αποτρέψει την επισήμανση δύο ατόμων ως το ίδιο άτομο, ενώ η αύξηση της μπορεί να αποτρέψει την επισήμανση του ίδιου ατόμου ως δύο διαφορετικών ατόμων. Λάβετε υπόψη ότι είναι πιο εύκολο να συγχωνεύσετε δύο άτομα παρά να χωρίσετε ένα άτομο στα δύο, οπότε προτιμήστε ένα χαμηλότερο όριο, όταν αυτό είναι δυνατό.",
-    "machine_learning_min_detection_score": "Ελάχιστο σκορ ανίχνευσης",
-    "machine_learning_min_detection_score_description": "Ελάχιστο σκορ εμπιστοσύνης για ανίχνευση προσώπου από 0-1. Οι χαμηλότερες τιμές θα εντοπίσουν περισσότερα πρόσωπα, αλλά μπορεί να οδηγήσουν σε ψευδώς θετικά αποτελέσματα.",
-    "machine_learning_min_recognized_faces": "Ελάχιστα αναγνωρισμένα πρόσωπα",
-    "machine_learning_min_recognized_faces_description": "Ο ελάχιστος αριθμός αναγνωρισμένων προσώπων για ένα άτομο που θα δημιουργηθεί. Η αύξηση αυτή καθιστά την Αναγνώριση Προσώπου πιο ακριβή με το κόστος να αυξηθεί η πιθανότητα να μην εκχωρηθεί ένα πρόσωπο σε ένα άτομο.",
-    "machine_learning_settings": "Ρυθμίσεις Μηχανικής Εκμάθησης",
-    "machine_learning_settings_description": "Διαχειριστείτε τις λειτουργίες και τις ρυθμίσεις μηχανικής εκμάθησης",
-    "machine_learning_smart_search": "Έξυπνη Αναζήτηση",
-    "machine_learning_smart_search_description": "Αναζητήστε εικόνες σημασιολογικά χρησιμοποιώντας ενσωματώσεις CLIP",
-    "machine_learning_smart_search_enabled": "Ενεργοποίηση έξυπνης αναζήτησης",
-    "machine_learning_smart_search_enabled_description": "Αν απενεργοποιηθεί, οι εικόνες δεν θα κωδικοποιούνται για έξυπνη αναζήτηση.",
-    "machine_learning_url_description": "URL του διακομιστή μηχανικής εκμάθησης",
-    "manage_log_settings": "Διαχείριση ρυθμίσεων αρχείου καταγραφής",
-    "map_dark_style": "Σκούρο Θέμα",
-    "map_enable_description": "Ενεργοποίηση λειτουργιών χάρτη",
-    "map_gps_settings": "Ρυθμίσεις Χάρτη & GPS",
-    "map_gps_settings_description": "Διαχείριση Ρυθμίσεων Χάρτη & GPS (Αντίστροφη γεωκωδικοποίηση)",
-    "map_light_style": "Φωτεινό Θέμα",
-    "note_unlimited_quota": "Σημείωση: Εισαγάγετε 0 για απεριόριστο όριο",
-    "notification_email_from_address": "Διεύθυνση αποστολέα"
-  },
-  "assets_restore_confirmation": "Είστε βέβαιοι ότι θέλετε να επαναφέρετε όλα τα στοιχεία που βρίσκονται στον κάδο απορριμμάτων; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί!",
-  "assets_restored_count": "Έγινε επαναφορά {count, plural, one {# στοιχείου} other {# στοιχείων}}",
-  "assets_trashed_count": "Μετακιν. στον κάδο απορριμάτων {count, plural, one {# στοιχείο} other {# στοιχεία}}",
-  "assets_were_part_of_album_count": "{count, plural, one {Το στοιχείο ανήκει} other {Τα στοιχεία ανήκουν}} ήδη στο άλμπουμ",
-  "authorized_devices": "Εξουσιοδοτημένες Συσκευές",
-  "back": "Πίσω",
-  "backward": "Προς τα πίσω",
-  "birthdate_saved": "Η ημερομηνία γέννησης αποθηκεύτηκε επιτυχώς",
-  "birthdate_set_description": "Η ημερομηνία γέννησης χρησιμοποιείται για τον υπολογισμό της ηλικίας αυτού του ατόμου, τη χρονική στιγμή μιας φωτογραφίας.",
-  "blurred_background": "Θολό φόντο",
-  "dismiss_error": "Παράβλεψη σφάλματος",
-  "display_options": "Επιλογές εμφάνισης",
-  "display_original_photos": "Εμφάνιση πρωτότυπων φωτογραφιών",
-  "do_not_show_again": "Να μην εμφανιστεί ξανά αυτό το μήνυμα",
-  "done": "Έγινε",
-  "download": "Λήψη",
-  "download_settings": "Λήψη",
-  "duplicates": "Διπλότυπα",
-  "duplicates_description": "Επιλύστε κάθε ομάδα υποδεικνύοντας ποιες είναι διπλότυπες, εάν υπάρχουν",
-  "duration": "Διάρκεια",
-  "edit": "Επεξεργασία",
-  "edit_album": "Επεξεργασία άλμπουμ",
-  "edit_avatar": "Επεξεργασία άβαταρ",
-  "edit_date": "Επεξεργασία ημερομηνίας",
-  "edit_date_and_time": "Επεξεργασία ημερομηνίας και ώρας",
-  "edit_faces": "Επεξεργασία προσώπων",
-  "edit_import_path": "Επεξεργασία διαδρομής εισαγωγής",
-  "edit_import_paths": "Επεξεργασία Διαδρομών Εισαγωγής",
-  "edit_link": "Επεξεργασία συνδέσμου",
-  "edit_location": "Επεξεργασία τοποθεσίας",
-  "edit_name": "Επεξεργασία ονόματος",
-  "edit_people": "Επεξεργασία ατόμων",
-  "edit_title": "Επεξεργασία Τίτλου",
-  "edit_user": "Επεξεργασία χρήστη",
-  "email": "Email",
-  "empty_trash": "Άδειασμα κάδου απορριμμάτων",
-  "enable": "Ενεργοποίηση",
-  "enabled": "Ενεργοποιημένο",
-  "error": "Σφάλμα",
-  "error_loading_image": "Σφάλμα κατά τη φόρτωση της εικόνας",
-  "error_title": "Σφάλμα - Κάτι πήγε στραβά",
-  "errors": {
-    "cannot_navigate_next_asset": "Δεν είναι δυνατή η πλοήγηση στο επόμενο στοιχείο",
-    "cannot_navigate_previous_asset": "Δεν είναι δυνατή η πλοήγηση στο προηγούμενο στοιχείο",
-    "cant_apply_changes": "Δεν είναι δυνατή η εφαρμογή αλλαγών"
-  },
-  "jobs": "Εργασίες",
-  "keep": "Διατήρηση",
-  "keep_all": "Διατήρηση Όλων",
-  "keyboard_shortcuts": "Συντομεύσεις πληκτρολογίου",
-  "language": "Γλώσσα",
-  "language_setting_description": "Επιλέξτε τη γλώσσα που προτιμάτε",
-  "latest_version": "Τελευταία Έκδοση",
-  "latitude": "Γεωγραφικό πλάτος",
-  "level": "Επίπεδο",
-  "library": "Βιβλιοθήκη",
-  "library_options": "Επιλογές βιβλιοθήκης",
-  "link_options": "Επιλογές συνδέσμου",
-  "list": "Λίστα",
-  "loading": "Φόρτωση",
-  "loading_search_results_failed": "Η φόρτωση αποτελεσμάτων αναζήτησης απέτυχε",
-  "log_out": "Αποσύνδεση",
-  "log_out_all_devices": "Αποσύνδεση από Όλες τις Συσκευές",
-  "logged_out_all_devices": "Όλες οι συσκευές αποσυνδέθηκαν",
-  "logged_out_device": "Αποσυνδεδεμένη συσκευή",
-  "login": "Είσοδος",
-  "login_has_been_disabled": "Η σύνδεση έχει απενεργοποιηθεί.",
-  "logout_all_device_confirmation": "Είστε βέβαιοι ότι θέλετε να αποσυνδεθείτε από όλες τις συσκευές;",
-  "logout_this_device_confirmation": "Είστε βέβαιοι ότι θέλετε να αποσυνδεθείτε από αυτήν τη συσκευή;",
-  "longitude": "Γεωγραφικό μήκος",
-  "look": "Εμφάνιση",
-  "loop_videos": "Επανάληψη βίντεο",
-  "loop_videos_description": "Ενεργοποιήστε την αυτόματη επανάληψη ενός βίντεο στο πρόγραμμα προβολής λεπτομερειών.",
-  "make": "Κατασκευαστής",
-  "manage_shared_links": "Διαχείριση κοινόχρηστων συνδέσμων",
-  "manage_sharing_with_partners": "Διαχειριστείτε την κοινή χρήση με συνεργάτες",
-  "manage_the_app_settings": "Διαχειριστείτε τις ρυθμίσεις της εφαρμογής",
-  "manage_your_account": "Διαχειριστείτε τον λογαριασμό σας",
-  "manage_your_api_keys": "Διαχειριστείτε τα κλειδιά API",
-  "manage_your_devices": "Διαχειριστείτε τις συνδεδεμένες συσκευές σας",
-  "manage_your_oauth_connection": "Διαχειριστείτε τη σύνδεσή σας OAuth",
-  "map": "Χάρτης",
-  "map_marker_for_images": "Δείκτης χάρτη για εικόνες που τραβήχτηκαν σε {city}, {country}",
-  "map_marker_with_image": "Χάρτης δείκτη με εικόνα",
-  "map_settings": "Ρυθμίσεις χάρτη",
-  "matches": "Αντιστοιχίες",
-  "media_type": "Τύπος πολυμέσου",
-  "memories": "Αναμνήσεις",
-  "memories_setting_description": "Διαχειριστείτε τι θα εμφανίζεται στις αναμνήσεις σας",
-  "memory": "Ανάμνηση",
-  "menu": "Μενού",
-  "merge": "Συγχώνευση",
-  "merge_people": "Συγχώνευση ατόμων",
-  "merge_people_limit": "Μπορείτε να συγχωνεύσετε μόνο έως και 5 πρόσωπα τη φορά",
-  "merge_people_prompt": "Θέλετε να συγχωνεύσετε αυτά τα άτομα; Αυτή η ενέργεια είναι μη αναστρέψιμη.",
-  "merge_people_successfully": "Τα άτομα συγχωνεύθηκαν με επιτυχία",
-  "merged_people_count": "Έγινε συγχώνευση {count, plural, one {# ατόμου} other {# ατόμων}}",
-  "minimize": "Ελαχιστοποίηση",
-  "minute": "Λεπτό",
-  "missing": "Όσα Λείπουν",
-  "model": "Μοντέλο",
-  "month": "Μήνας",
-  "more": "Περισσότερα",
-  "moved_to_trash": "Μετακινήθηκε στον κάδο απορριμμάτων",
-  "my_albums": "Τα άλμπουμ μου",
-  "name": "Όνομα",
-  "name_or_nickname": "Όνομα ή ψευδώνυμο",
-  "never": "Ποτέ",
-  "new_album": "Νέο Άλμπουμ",
-  "new_api_key": "Νέο API Key",
-  "new_password": "Νέος κωδικός πρόσβασης",
-  "new_person": "Νέο άτομο",
-  "new_user_created": "Ο νέος χρήστης δημιουργήθηκε",
-  "new_version_available": "ΔΙΑΘΕΣΙΜΗ ΝΕΑ ΕΚΔΟΣΗ",
-  "newest_first": "Τα νεότερα πρώτα",
-  "next": "Επόμενο",
-  "next_memory": "Επόμενη ανάμνηση",
-  "no": "Όχι",
-  "no_albums_message": "Δημιουργήστε ένα άλμπουμ για να οργανώσετε τις φωτογραφίες και τα βίντεό σας",
-  "no_albums_with_name_yet": "Φαίνεται ότι δεν έχετε κανένα άλμπουμ με αυτό το όνομα ακόμα.",
-  "no_albums_yet": "Φαίνεται ότι δεν έχετε κανένα άλμπουμ ακόμα.",
-  "no_archived_assets_message": "Αρχειοθετήστε φωτογραφίες και βίντεο για να τα αποκρύψετε από την Προβολή Φωτογραφιών",
-  "no_assets_message": "ΚΑΝΤΕ ΚΛΙΚ ΓΙΑ ΝΑ ΑΝΕΒΑΣΕΤΕ ΤΗΝ ΠΡΩΤΗ ΣΑΣ ΦΩΤΟΓΡΑΦΙΑ",
-  "no_duplicates_found": "Δεν βρέθηκαν διπλότυπα.",
-  "no_exif_info_available": "Καμία πληροφορία exif διαθέσιμη",
-  "no_explore_results_message": "Ανεβάστε περισσότερες φωτογραφίες για να εξερευνήσετε τη συλλογή σας.",
-  "no_favorites_message": "Προσθέστε αγαπημένα για να βρείτε γρήγορα τις καλύτερες φωτογραφίες και τα βίντεό σας",
-  "no_libraries_message": "Δημιουργήστε μια εξωτερική βιβλιοθήκη για να προβάλετε τις φωτογραφίες και τα βίντεό σας",
-  "no_name": "Χωρίς Όνομα",
-  "no_results": "Κανένα αποτέλεσμα",
-  "no_results_description": "Δοκιμάστε ένα συνώνυμο ή πιο γενική λέξη-κλειδί",
-  "no_shared_albums_message": "Δημιουργήστε ένα άλμπουμ για να μοιράζεστε φωτογραφίες και βίντεο με άτομα στο δίκτυό σας",
-  "not_in_any_album": "Σε κανένα άλμπουμ",
-  "note_apply_storage_label_to_previously_uploaded assets": "Σημείωση: Για να εφαρμόσετε την Ετικέτα Αποθήκευσης σε στοιχεία που έχουν μεταφορτωθεί προηγουμένως, εκτελέστε το",
-  "note_unlimited_quota": "Σημείωση: Εισαγάγετε 0 για απεριόριστο όριο",
-  "notes": "Σημειώσεις",
-  "notification_toggle_setting_description": "Ενεργοποίηση ειδοποιήσεων μέσω email",
-  "notifications": "Ειδοποιήσεις",
-  "notifications_setting_description": "Διαχείριση ειδοποιήσεων",
-  "oauth": "OAuth",
-  "offline": "Εκτός σύνδεσης",
-  "offline_paths": "Διαδρομές εκτός σύνδεσης",
-  "offline_paths_description": "Αυτά τα αποτελέσματα μπορεί να οφείλονται στη μη αυτόματη διαγραφή αρχείων που δεν αποτελούν μέρος μιας εξωτερικής βιβλιοθήκης.",
-  "ok": "Έγινε",
-  "oldest_first": "Τα παλαιότερα πρώτα",
-  "onboarding_theme_description": "Επιλέξτε ένα θέμα χρώματος για το προφίλ σας. Μπορείτε να το αλλάξετε αργότερα στις ρυθμίσεις σας.",
-  "onboarding_welcome_description": "Ας ρυθμίσουμε το προφίλ σας με ορισμένες κοινές ρυθμίσεις.",
-  "onboarding_welcome_user": "Καλωσόρισες, {user}",
-  "online": "Σε σύνδεση",
-  "only_favorites": "Μόνο αγαπημένα",
-  "only_refreshes_modified_files": "Ανανεώνει μόνο τροποποιημένα αρχεία",
-  "open_in_map_view": "Άνοιγμα σε προβολή χάρτη",
-  "open_in_openstreetmap": "Άνοιγμα στο OpenStreetMap",
-  "open_the_search_filters": "Ανοίξτε τα φίλτρα αναζήτησης",
-  "options": "Επιλογές",
-  "or": "ή",
-  "organize_your_library": "Οργανώστε τη βιβλιοθήκη σας",
-  "original": "πρωτότυπο",
-  "other": "Άλλες",
-  "other_devices": "Άλλες συσκευές",
-  "other_variables": "Άλλες μεταβλητές",
-  "owned": "Δικά μου",
-  "owner": "Κάτοχος",
-  "partner": "Συνεργάτης",
-  "partner_can_access": "Ο χρήστης {partner} έχει πρόσβαση",
-  "partner_can_access_assets": "Όλες οι φωτογραφίες και τα βίντεό σας εκτός από αυτά που βρίσκονται στο Αρχείο και τα Διαγραμμένα",
-  "partner_can_access_location": "Η τοποθεσία όπου τραβήχτηκαν οι φωτογραφίες σας",
-  "partner_sharing": "Κοινή Χρήση Συνεργατών",
-  "partners": "Συνεργάτες",
-  "password": "Κωδικός Πρόσβασης",
-  "password_does_not_match": "Ο κωδικός πρόσβασης δεν ταιριάζει",
-  "password_required": "Απαιτείται Κωδικός Πρόσβασης",
-  "password_reset_success": "Επιτυχής επαναφορά κωδικού πρόσβασης",
-  "path": "Διαδρομή",
-  "pattern": "Μοτίβο",
-  "pause": "Πάυση",
-  "pause_memories": "Παύση αναμνήσεων",
-  "paused": "Σε Πάυση",
-  "pending": "Εκκρεμεί",
-  "people": "Άτομα",
-  "people_edits_count": "Έγινε επεξεργασία {count, plural, one {# ατόμου} other {# ατόμων}}",
-  "people_sidebar_description": "Εμφάνιση Ατόμων στην πλαϊνή γραμμή",
-  "permanent_deletion_warning": "Προειδοποίηση οριστικής διαγραφής",
-  "permanent_deletion_warning_setting_description": "Εμφάνιση προειδοποίησης κατά την οριστική διαγραφή στοιχείων",
-  "permanently_delete": "Οριστική διαγραφή",
-  "permanently_delete_assets_count": "Οριστική διαγραφή {count, plural, one {στοιχείου} other {στοιχείων}}",
-  "permanently_delete_assets_prompt": "Είστε βέβαιοι ότι θέλετε να διαγράψετε οριστικά {count, plural, one {αυτό το στοιχείο;} other {αυτά τα <b>#</b> στοιχεία;}} Αυτό θα {count, plural, one {το} other {τα}} αφαιρέσει επίσης από τα άλμπουμ στα οποία {count, plural, one {ανήκει} other {ανήκουν}} .",
-  "permanently_deleted_asset": "Οριστικά διαγραμμένο στοιχείο",
-  "permanently_deleted_assets_count": "Οριστική διαγραφή {count, plural, one {# στοιχείου} other {# στοιχείων}}",
-  "person": "Άτομο",
-  "photo_shared_all_users": "Φαίνεται ότι μοιραστήκατε τις φωτογραφίες σας με όλους τους χρήστες ή δεν έχετε κανέναν χρήστη για κοινή χρήση.",
-  "photos": "Φωτογραφίες",
-  "photos_and_videos": "Φωτογραφίες & Βίντεο",
-  "photos_count": "{count, plural, one {{count, number} Φωτογραφία} other {{count, number} Φωτογραφίες}}",
-  "photos_from_previous_years": "Φωτογραφίες προηγούμενων ετών",
-  "pick_a_location": "Επιλέξτε μια τοποθεσία",
-  "place": "Τοποθεσία",
-  "places": "Τοποθεσίες",
-  "play": "Αναπαραγωγή",
-  "play_memories": "Αναπαραγωγή αναμνήσεων",
-  "play_motion_photo": "Αναπαραγωγή Κινούμενης Φωτογραφίας",
-  "play_or_pause_video": "Αναπαραγωγή ή παύση βίντεο",
-  "preview": "Προεπισκόπηση",
-  "previous": "Προηγούμενο",
-  "previous_memory": "Προηγούμενη ανάμνηση",
-  "previous_or_next_photo": "Προηγούμενη ή επόμενη φωτογραφία",
-  "profile_image_of_user": "Εικόνα προφίλ του χρήστη {user}",
-  "profile_picture_set": "Ορισμός εικόνας προφίλ.",
-  "public_album": "Δημόσιο άλμπουμ",
-  "public_share": "Δημόσια Κοινή Χρήση",
-  "purchase_account_info": "Υποστηρικτής",
-  "purchase_activated_subtitle": "Σας ευχαριστούμε για την υποστήριξη του Immich και λογισμικών ανοιχτού κώδικα",
-  "purchase_activated_time": "Ενεργοποιήθηκε στις {date, date}",
-  "purchase_activated_title": "Το κλειδί σας ενεργοποιήθηκε με επιτυχία",
-  "purchase_button_activate": "Ενεργοποίηση",
-  "purchase_button_buy": "Αγορά",
-  "purchase_button_buy_immich": "Αγορά Immich",
-  "purchase_button_never_show_again": "Να μην εμφανιστεί ποτέ ξανά",
-  "purchase_button_reminder": "Υπενθύμιση σε 30 μέρες",
-  "purchase_button_remove_key": "Αφαίρεση κλειδιού",
-  "purchase_button_select": "Επιλέξτε",
-  "purchase_failed_activation": "Η ενεργοποίηση απέτυχε! Ελέγξτε το email σας για το σωστό κλειδί προϊόντος!",
-  "purchase_individual_description_1": "Για ένα άτομο",
-  "purchase_individual_description_2": "Κατάσταση υποστηρικτή",
-  "purchase_individual_title": "Ατομο",
-  "purchase_input_suggestion": "Έχετε ένα κλειδί προϊόντος; Εισαγάγετε το κλειδί παρακάτω",
-  "purchase_license_subtitle": "Αγοράστε το Immich για να υποστηρίξετε τη συνεχή ανάπτυξη της υπηρεσίας",
-  "purchase_lifetime_description": "Αγορά εφ' όρου ζωής",
-  "purchase_option_title": "ΕΠΙΛΟΓΕΣ ΑΓΟΡΑΣ",
-  "purchase_panel_info_1": "Η ανάπτυξη του Immich απαιτεί πολύ χρόνο και προσπάθεια, και έχουμε μηχανικούς πλήρους απασχόλησης που εργάζονται σε αυτό για να το κάνουμε όσο το δυνατόν καλύτερο. Η αποστολή μας είναι το λογισμικό ανοιχτού κώδικα και οι ηθικές επιχειρηματικές πρακτικές να γίνουν βιώσιμη πηγή εισοδήματος για προγραμματιστές και να δημιουργήσουμε ένα οικοσύστημα που σέβεται το απόρρητο, με πραγματικές εναλλακτικές λύσεις στις υπηρεσίες cloud που παρουσιάζουν συμπεριφορές εκμετάλλευσης.",
-  "purchase_panel_info_2": "Καθώς δεσμευόμαστε να μην προσθέσουμε φραγμούς με σκοπό το κέρδος, αυτή η αγορά δεν θα σας προσφέρει πρόσθετες δυνατότητες στο Immich. Βασιζόμαστε σε χρήστες όπως εσείς για την υποστήριξη της συνεχούς ανάπτυξης του Immich.",
-  "purchase_panel_title": "Υποστηρίξτε το πρότζεκτ",
-  "purchase_per_server": "Ανά διακομιστή",
-  "purchase_per_user": "Ανά χρήστη",
-  "purchase_remove_product_key": "Κατάργηση κλειδιού προϊόντος",
-  "purchase_remove_product_key_prompt": "Είστε βέβαιοι ότι θέλετε να αφαιρέσετε τον αριθμό-κλειδί προϊόντος;",
-  "purchase_remove_server_product_key": "Κατάργηση κλειδιού προϊόντος διακομιστή",
-  "purchase_remove_server_product_key_prompt": "Είστε βέβαιοι ότι θέλετε να καταργήσετε το κλειδί προϊόντος διακομιστή;",
-  "purchase_server_description_1": "Για ολόκληρο τον διακομιστή",
-  "purchase_server_description_2": "Κατάσταση υποστηρικτή",
-  "purchase_server_title": "Διακομιστής",
-  "purchase_settings_server_activated": "Η διαχείριση του κλειδιού προϊόντος του διακομιστή γίνεται από τον διαχειριστή",
-  "reaction_options": "Επιλογές αντίδρασης",
-  "read_changelog": "Διαβάστε το Αρχείο Καταγραφής Αλλαγών",
-  "restore_user": "Επαναφορά χρήστη",
-  "retry_upload": "Επανάληψη ανεβάσματος",
-  "review_duplicates": "Προβολή διπλότυπων",
-  "save": "Αποθήκευση",
-  "saved_profile": "Αποθηκευμένο προφίλ",
-  "saved_settings": "Αποθηκευμένες ρυθμίσεις",
-  "say_something": "Πείτε κάτι",
-  "scan_all_libraries": "Σάρωση Όλων των Βιβλιοθηκών",
-  "scan_new_library_files": "Σάρωση Νέων Αρχείων Βιβλιοθήκης",
-  "scan_settings": "Ρυθμίσεις Σάρωσης",
-  "scanning_for_album": "Σάρωση για άλμπουμ...",
-  "search": "Αναζήτηση",
-  "search_albums": "Αναζήτηση άλμπουμ",
-  "search_by_filename": "Αναζήτηση βάσει ονόματος αρχείου ή επέκτασης αρχείου",
-  "search_by_filename_example": "π.χ. IMG_1234.JPG ή PNG",
-  "search_camera_make": "Αναζήτηση κατασκευαστή κάμερας...",
-  "search_camera_model": "Αναζήτηση μοντέλου κάμερας...",
-  "search_city": "Αναζήτηση πόλης...",
-  "search_country": "Αναζήτηση χώρας...",
-  "search_for_existing_person": "Αναζήτηση υπάρχοντος ατόμου",
-  "search_no_people": "Κανένα άτομο",
-  "search_no_people_named": "Κανένα άτομο με όνομα \"{name}\"",
-  "search_people": "Αναζήτηση ατόμων",
-  "search_places": "Αναζήτηση τοποθεσιών",
-  "search_state": "Αναζήτηση νομού...",
-  "search_timezone": "Αναζήτηση ζώνης ώρας...",
-  "search_type": "Τύπος αναζήτησης",
-  "search_your_photos": "Αναζήτηση φωτογραφιών",
-  "second": "Δευτερόλεπτο",
-  "see_all_people": "Προβολή όλων των ατόμων",
-  "select_album_cover": "Επιλέξτε εξώφυλλο άλμπουμ",
-  "select_all": "Επιλογή όλων",
-  "select_all_duplicates": "Επιλογή όλων των διπλότυπων",
-  "select_avatar_color": "Επιλέξτε χρώμα avatar",
-  "select_face": "Επιλογή προσώπου",
-  "select_from_computer": "Επιλέξτε από υπολογιστή",
-  "select_keep_all": "Επιλέξτε διατήρηση όλων",
-  "select_library_owner": "Επιλέξτε κάτοχο βιβλιοθήκης",
-  "select_new_face": "Επιλέξτε νέο πρόσωπο",
-  "select_photos": "Επιλέξτε φωτογραφίες",
-  "select_trash_all": "Επιλέξτε διαγραφή όλων",
-  "selected": "Επιλεγμένοι",
-  "selected_count": "{count, plural, other {# επιλεγμένοι}}",
-  "send_message": "Αποστολή μηνύματος",
-  "send_welcome_email": "Αποστολή email καλωσορίσματος",
-  "server_offline": "Διακομιστής Εκτός Σύνδεσης",
-  "server_online": "Διακομιστής Σε Σύνδεση",
-  "server_stats": "Στατιστικά Διακομιστή",
-  "server_version": "Έκδοση Διακομιστή",
-  "set": "Ορισμός",
-  "set_as_album_cover": "Ορισμός ως εξώφυλλο άλμπουμ",
-  "set_as_profile_picture": "Ορισμός ως εικόνα προφίλ",
-  "set_date_of_birth": "Ορισμός ημερομηνίας γέννησης",
-  "set_profile_picture": "Ορισμός εικόνας προφίλ",
-  "settings": "Ρυθμίσεις",
-  "settings_saved": "Οι ρυθμίσεις αποθηκεύτηκαν",
-  "share": "Κοινοποίηση",
-  "shared": "Σε κοινή χρήση",
-  "shared_by": "Σε κοινή χρήση από",
-  "shared_by_user": "Σε κοινή χρήση από {user}",
-  "shared_by_you": "Σε κοινή χρήση από εσάς",
-  "shared_from_partner": "Φωτογραφίες από {partner}",
-  "shared_links": "Κοινόχρηστοι σύνδεσμοι",
-  "shared_photos_and_videos_count": "{assetCount, plural, other {# κοινόχρηστες φωτογραφίες & βίντεο.}}",
-  "shared_with_partner": "Σε κοινή χρήση με {partner}",
-  "sharing": "Κοινοποίηση",
-  "sharing_enter_password": "Εισαγάγετε τον κωδικό πρόσβασης για να δείτε αυτήν τη σελίδα.",
-  "sharing_sidebar_description": "Εμφανίστε έναν σύνδεσμο για Κοινή χρήση στην πλαϊνή γραμμή",
-  "shift_to_permanent_delete": "πατήστε ⇧ για οριστική διαγραφή στοιχείου",
-  "show_album_options": "Εμφάνιση επιλογών άλμπουμ",
-  "show_all_people": "Προβολή όλων των ατόμων",
-  "show_and_hide_people": "Εμφάνιση & απόκρυψη ατόμων",
-  "show_file_location": "Εμφάνιση θέσης αρχείου",
-  "show_gallery": "Εμφάνιση γκαλερί",
-  "show_hidden_people": "Εμφάνιση κρυμμένων ατόμων",
-  "show_in_timeline": "Εμφάνιση στο χρονολόγιο",
-  "show_in_timeline_setting_description": "Εμφάνιση φωτογραφιών και βίντεο από αυτόν τον χρήστη στο χρονολόγιό σας",
-  "show_keyboard_shortcuts": "Εμφάνιση συντομεύσεων πληκτρολογίου",
-  "show_metadata": "Εμφάνιση μεταδεδομένων",
-  "show_or_hide_info": "Εμφάνιση ή απόκρυψη πληροφοριών",
-  "show_password": "Εμφάνιση κωδικού",
-  "show_person_options": "Εμφάνιση επιλογών ατόμου",
-  "show_progress_bar": "Εμφάνιση γραμμής προόδου",
-  "show_search_options": "Εμφάνιση επιλογών αναζήτησης",
-  "show_supporter_badge": "Σήμα υποστηρικτή",
-  "show_supporter_badge_description": "Εμφάνιση σήματος υποστηρικτή",
-  "shuffle": "Ανάμειξη",
-  "sign_out": "Αποσύνδεση",
-  "sign_up": "Εγγραφή",
-  "size": "Μέγεθος",
-  "skip_to_content": "Μετάβαση στο περιεχόμενο",
-  "slideshow": "Παρουσίαση",
-  "slideshow_settings": "Ρυθμίσεις παρουσίασης",
-  "sort_albums_by": "Ταξινόμηση άλμπουμ κατά...",
-  "sort_created": "Ημερομηνία Δημιουργίας",
-  "sort_items": "Αριθμός αντικειμένων",
-  "sort_modified": "Ημερομηνία τροποποίησης",
-  "sort_oldest": "Η πιο παλιά φωτογραφία",
-  "sort_recent": "Η πιο πρόσφατη φωτογραφία",
-  "sort_title": "Τίτλος",
-  "source": "Πηγή",
-  "start_date": "Από",
-  "state": "Νομός",
-  "status": "Κατάσταση",
-  "stop_photo_sharing": "Διακοπή κοινής χρήσης των φωτογραφιών σας;",
-  "stop_photo_sharing_description": "Ο χρήστης {partner} δεν θα έχει πλέον πρόσβαση στις φωτογραφίες σας.",
-  "stop_sharing_photos_with_user": "Διακοπή κοινής χρήσης των φωτογραφιών σας με αυτό το χρήστη",
-  "storage": "Χώρος αποθήκευσης",
-  "storage_label": "Ετικέτα αποθήκευσης",
-  "storage_usage": "{used} από {available} σε χρήση",
-  "submit": "Υποβολή",
-  "suggestions": "Προτάσεις",
-  "sunrise_on_the_beach": "Ηλιοβασίλεμα στην παραλία",
-  "swap_merge_direction": "Εναλλαγή κατεύθυνσης συγχώνευσης",
-  "sync": "Συγχρονισμός",
-  "template": "Πρότυπο",
-  "theme": "Θέμα",
-  "theme_selection": "Επιλογή θέματος",
-  "theme_selection_description": "Ρυθμίστε αυτόματα το θέμα σε ανοιχτό ή σκούρο με βάση τις προτιμήσεις συστήματος του προγράμματος περιήγησής σας",
-  "they_will_be_merged_together": "Θα συγχωνευθούν μαζί",
-  "time_based_memories": "Μνήμες βασισμένες στο χρόνο",
-  "timezone": "Ζώνη ώρας",
-  "to_archive": "Αρχειοθέτηση",
-  "to_change_password": "Αλλαγή κωδικού πρόσβασης",
-  "to_favorite": "Αγαπημένο",
-  "to_login": "Είσοδος",
-  "to_trash": "Κάδος απορριμμάτων",
-  "toggle_settings": "Εναλλαγή ρυθμίσεων",
-  "toggle_theme": "Εναλλαγή θέματος",
-  "total_usage": "Συνολική χρήση",
-  "trash": "Κάδος απορριμμάτων",
-  "trash_all": "Διαγραφή Όλων",
-  "trash_count": "Διαγραφή {count, number}",
-  "trash_delete_asset": "Διαγραφή/Οριστ. Διαγραφή Αντικειμένου",
-  "trash_no_results_message": "Οι φωτογραφίες και τα βίντεο που βρίσκονται στον κάδο απορριμμάτων θα εμφανίζονται εδώ.",
-  "trashed_items_will_be_permanently_deleted_after": "Τα στοιχεία που βρίσκονται στον κάδο απορριμμάτων θα διαγραφούν οριστικά μετά από {days, plural, one {# ημέρα} other {# ημέρες}}.",
-  "unarchive": "Αναίρεση αρχειοθέτησης",
-  "unarchived_count": "{count, plural, other {Αρχειοθετήσεις αναιρέθηκαν #}}",
-  "unfavorite": "Αφαίρεση από τα αγαπημένα",
-  "unhide_person": "Αναίρεση απόκρυψης ατόμου",
-  "unknown": "Άγνωστο",
-  "unknown_year": "Άγνωστο Έτος",
-  "unlimited": "Απεριόριστο",
-  "unlink_oauth": "Αποσύνδεση OAuth",
-  "unlinked_oauth_account": "Ο λογαριασμός OAuth αποσυνδέθηκε",
-  "unnamed_album": "Ανώνυμο Άλμπουμ",
-  "unnamed_share": "Ανώνυμη Κοινή Χρήση",
-  "unsaved_change": "Μη αποθηκευμένη αλλαγή",
-  "unselect_all": "Αποεπιλογή όλων",
-  "unselect_all_duplicates": "Αποεπιλογή όλων των διπλότυπων",
-  "untracked_files": "Μη παρακολουθούμενα αρχεία",
-  "untracked_files_decription": "Αυτά τα αρχεία δεν παρακολουθούνται από την εφαρμογή. Μπορεί να είναι αποτελέσματα αποτυχημένων μετακινήσεων, αποτυχημένες μεταφορτώσεις ή εναπομείναντα λόγω σφάλματος",
-  "updated_password": "Ο κωδικός πρόσβασης ενημερώθηκε",
-  "upload": "Μεταφόρτωση",
-  "upload_errors": "Η μεταφόρτωση ολοκληρώθηκε με {count, plural, one {# σφάλμα} other {# σφάλματα}}, ανανεώστε τη σελίδα για να δείτε νέα στοιχεία μεταφόρτωσης.",
-  "upload_progress": "Απομένουν {remaining, number} - Ολοκληρώθηκαν {processed, number}/{total, number}",
-  "upload_skipped_duplicates": "Παραλείφθηκαν {count, plural, one {# διπλότυπο στοιχείο} other {# διπλότυπα στοιχεία}}",
-  "upload_status_duplicates": "Διπλότυπα",
-  "upload_status_errors": "Σφάλματα",
-  "upload_status_uploaded": "Μεταφορτώθηκαν",
-  "upload_success": "Η μεταφόρτωση ολοκληρώθηκε, ανανεώστε τη σελίδα για να δείτε τα νέα αντικείμενα.",
-  "url": "URL",
-  "usage": "Χρήση",
-  "use_custom_date_range": "Χρήση προσαρμοσμένου εύρους ημερομηνιών",
-  "user": "Χρήστης",
-  "user_id": "ID Χρήστη",
-  "user_liked": "Στο χρήστη {user} αρέσει {type, select, photo {αυτή η φωτογραφία} video {αυτό το βίντεο} asset {αυτό το αντικείμενο} other {it}}",
-  "user_purchase_settings": "Αγορά",
-  "user_purchase_settings_description": "Διαχείριση Αγοράς",
-  "user_role_set": "Ορισμός {user} ως {role}",
-  "username": "Όνομα Χρήστη",
-  "users": "Χρήστες",
-  "utilities": "Βοηθητικά προγράμματα",
-  "validate": "Επικύρωση",
-  "variables": "Μεταβλητές",
-  "version": "Έκδοση",
-  "version_announcement_closing": "Ο φίλος σου, Alex",
-  "version_announcement_message": "Γεια σου φίλε, υπάρχει μια νέα έκδοση της εφαρμογής, αφιέρωσε λίγο χρόνο για να επισκεφθείς την τοποθεσία <link>release notes</link> και να βεβαιωθείς ότι τα <code>docker-compose.yml</code>, και <code>.env</code> είναι ενημερωμένα για την αποτροπή τυχόν εσφαλμένων διαμορφώσεων, ειδικά εάν χρησιμοποιείτε το WatchTower ή οποιονδήποτε μηχανισμό που χειρίζεται την αυτόματη ενημέρωση της εφαρμογής σας.",
-  "video": "Βίντεο",
-  "video_hover_setting": "Προεπισκόπηση βίντεο με το δείκτη του ποντικιού",
-  "video_hover_setting_description": "Προεπισκόπηση βίντεο όταν το ποντίκι βρίσκεται πάνω από το στοιχείο. Ακόμη και όταν είναι απενεργοποιημένη, η αναπαραγωγή μπορεί να ξεκινήσει τοποθετώντας το δείκτη του ποντικιού πάνω από το εικονίδιο αναπαραγωγής.",
-  "videos": "Βίντεο",
-  "videos_count": "{count, plural, one {# Βίντεο} other {# Βίντεο}}",
-  "view": "Προβολή",
-  "view_album": "Προβολή Άλμπουμ",
-  "view_all": "Προβολή Όλων",
-  "view_all_users": "Προβολή όλων των χρηστών",
-  "view_links": "Προβολή συνδέσμων",
-  "view_next_asset": "Προβολή επόμενου στοιχείου",
-  "view_previous_asset": "Προβολή προηγούμενου στοιχείου",
-  "visibility_changed": "Η ορατότητα άλλαξε για {count, plural, one {# άτομο} other {# άτομα}}",
-  "waiting": "Σε αναμονή",
-  "warning": "Προειδοποίηση",
-  "week": "Εβδομάδα",
-  "welcome": "Καλωσορίσατε",
-  "welcome_to_immich": "Καλωσορίσατε στο immich",
-  "year": "Έτος",
-  "years_ago": "πριν από {years, plural, one {# χρόνο} other {# χρόνια}}",
-  "yes": "Ναι",
-  "you_dont_have_any_shared_links": "Δεν έχετε κοινόχρηστους συνδέσμους",
-  "zoom_image": "Ζουμ Εικόνας"
-}
diff --git a/web/src/lib/i18n/hr.json b/web/src/lib/i18n/hr.json
deleted file mode 100644
index 291abaa690..0000000000
--- a/web/src/lib/i18n/hr.json
+++ /dev/null
@@ -1,932 +0,0 @@
-{
-  "about": "O",
-  "account": "Račun",
-  "account_settings": "Postavke računa",
-  "acknowledge": "Potvrdi",
-  "action": "Akcija",
-  "actions": "Akcije",
-  "active": "Aktivno",
-  "activity": "Aktivnost",
-  "activity_changed": "Aktivnost je {enabled, select, true {omogućena} other {onemogućena}}",
-  "add": "Dodaj",
-  "add_a_description": "Dodaj opis",
-  "add_a_location": "Dodaj lokaciju",
-  "add_a_name": "Dodaj ime",
-  "add_a_title": "Dodaj naslov",
-  "add_exclusion_pattern": "Dodaj uzorak izuzimanja",
-  "add_import_path": "Dodaj import folder",
-  "add_location": "Dodaj lokaciju",
-  "add_more_users": "Dodaj još korisnika",
-  "add_partner": "Dodaj partnera",
-  "add_path": "Dodaj putanju",
-  "add_photos": "Dodaj slike",
-  "add_to": "Dodaj u...",
-  "add_to_album": "Dodaj u album",
-  "add_to_shared_album": "Dodaj u dijeljeni album",
-  "added_to_archive": "Dodano u arhivu",
-  "added_to_favorites": "Dodano u omiljeno",
-  "added_to_favorites_count": "Dodano {count, number} u omiljeno",
-  "admin": {
-    "add_exclusion_pattern_description": "",
-    "authentication_settings": "Postavke autentikacije",
-    "authentication_settings_description": "Uredi lozinku, OAuth, i druge postavke autentikacije",
-    "authentication_settings_disable_all": "Jeste li sigurni da želite onemogućenit sve načine prijave? Prijava će biti potpuno onemogućena.",
-    "background_task_job": "Pozadinski zadaci",
-    "check_all": "Provjeri sve",
-    "cleared_jobs": "Izbrisani poslovi za: {job}",
-    "config_set_by_file": "Konfiguracija je trenutno postavljena konfiguracijskom datotekom",
-    "confirm_delete_library": "Jeste li sigurni da želite izbrisati biblioteku {library}?",
-    "confirm_delete_library_assets": "Jeste li sigurni da želite izbrisati ovu biblioteku? Time će se izbrisati sva {count} sadržana sredstva iz Immicha i ne može se poništiti. Datoteke će ostati na disku.",
-    "confirm_email_below": "Za potvrdu upišite \"{email}\" ispod",
-    "confirm_reprocess_all_faces": "Jeste li sigurni da želite ponovno obraditi sva lica? Ovo će također obrisati imenovane osobe.",
-    "confirm_user_password_reset": "Jeste li sigurni da želite poništiti lozinku korisnika {user}?",
-    "crontab_guru": "Crontab Guru",
-    "disable_login": "Onemogući prijavu",
-    "duplicate_detection_job_description": "Pokrenite strojno učenje na materijalima kako biste otkrili slične slike. Oslanja se na Pametno Pretraživanje",
-    "exclusion_pattern_description": "Uzorci izuzimanja omogućuju vam da zanemarite datoteke i mape prilikom skeniranja svoje biblioteke. Ovo je korisno ako imate mape koje sadrže datoteke koje ne želite uvesti, kao što su RAW datoteke.",
-    "external_library_created_at": "Vanjska biblioteka (stvorena: {date})",
-    "external_library_management": "Upravljanje vanjskom knjižnicom",
-    "face_detection": "Detekcija lica",
-    "face_detection_description": "Prepoznajte lica u sredstvima pomoću strojnog učenja. Za videozapise u obzir se uzima samo minijaturni prikaz. \"Sve\" (ponovno) obrađuje svu imovinu. \"Nedostaje\" stavlja u red čekanja sredstva koja još nisu obrađena. Otkrivena lica bit će stavljena u red čekanja za prepoznavanje lica nakon dovršetka prepoznavanja lica, grupirajući ih u postojeće ili nove osobe.",
-    "facial_recognition_job_description": "Grupirajte otkrivena lica u osobe. Ovaj se korak pokreće nakon dovršetka prepoznavanja lica. \"Sve\" (ponovno) grupira sva lica. \"Nedostajuća\" lica u redovima kojima nije dodijeljena osoba.",
-    "failed_job_command": "Naredba {command} nije uspjela za posao: {job}",
-    "force_delete_user_warning": "UPOZORENJE: Ovo će odmah ukloniti korisnika i sve pripadajuće podatke. Ovo se ne može poništiti i datoteke se ne mogu vratiti.",
-    "forcing_refresh_library_files": "Prisilno osvježavanje svih datoteka knjižnice",
-    "image_format_description": "WebP proizvodi manje datoteke od JPEG-a, ali se sporije kodira.",
-    "image_prefer_embedded_preview": "Preferiraj ugrađeni pregled",
-    "image_prefer_embedded_preview_setting_description": "Koristite ugrađene preglede u RAW fotografije kao ulaz za obradu slike kada su dostupni. To može proizvesti preciznije boje za neke slike, ali kvaliteta pregleda ovisi o kameri i slika može imati više artefakata kompresije.",
-    "image_prefer_wide_gamut": "Preferirajte široku gamu",
-    "image_prefer_wide_gamut_setting_description": "Koristite Display P3 za sličice. Ovo bolje čuva živost slika sa širokim prostorima boja, ali slike mogu izgledati drugačije na starim uređajima sa starom verzijom preglednika. sRGB slike čuvaju se kao sRGB kako bi se izbjegle promjene boja.",
-    "image_preview_format": "Format pregleda",
-    "image_preview_resolution": "Razlučivost pregleda",
-    "image_preview_resolution_description": "Koristi se pri gledanju jedne fotografije i za strojno učenje. Veće razlučivosti mogu sačuvati više detalja, ali trebaju dulje za kodiranje, imaju veće veličine datoteka i mogu smanjiti odaziv aplikacije.",
-    "image_quality": "Kvaliteta",
-    "image_quality_description": "Kvaliteta slike od 1-100. Više je bolji za kvalitetu, ali daje veće datoteke, ova opcija utječe na Pretpregled i sličice.",
-    "image_settings": "Postavke slike",
-    "image_settings_description": "Upravljajte kvalitetom i rezolucijom generiranih slika",
-    "image_thumbnail_format": "Format sličica",
-    "image_thumbnail_resolution": "Razlučivost sličica",
-    "image_thumbnail_resolution_description": "Koristi se prilikom pregledavanja grupa fotografija (glavna vremenska traka, prikaz albuma itd.). Veće razlučivosti mogu sačuvati više detalja, ali trebaju dulje za kodiranje, imaju veće veličine datoteka i mogu smanjiti odaziv aplikacije.",
-    "job_concurrency": "{job} istovremenost",
-    "job_not_concurrency_safe": "Ovaj posao nije siguran za istovremenost.",
-    "job_settings": "Postavke posla",
-    "job_settings_description": "Upravljajte istovremenošću poslova",
-    "job_status": "Status posla",
-    "jobs_delayed": "",
-    "jobs_failed": "",
-    "library_created": "Stvorena biblioteka: {library}",
-    "library_cron_expression": "Cron izraz",
-    "library_cron_expression_description": "Postavite interval skeniranja koristeći cron format. Za više informacija pogledajte npr. <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Unaprijed postavljene cron izraze",
-    "library_deleted": "Biblioteka izbrisana",
-    "library_import_path_description": "Navedite mapu za uvoz. Ova će se mapa, uključujući podmape, skenirati u potrazi za slikama i videozapisima.",
-    "library_scanning": "Periodično Skeniranje",
-    "library_scanning_description": "Konfigurirajte periodično skeniranje biblioteke",
-    "library_scanning_enable_description": "Omogući periodično skeniranje biblioteke",
-    "library_settings": "Externa biblioteka",
-    "library_settings_description": "Upravljajte postavkama vanjske biblioteke",
-    "library_tasks_description": "Obavljati bibliotekne zadatke",
-    "library_watching_enable_description": "Pratite vanjske biblioteke za promjena datoteke",
-    "library_watching_settings": "Gledanje biblioteke (EKSPERIMENTALNO)",
-    "library_watching_settings_description": "Automatsko praćenje promijenjenih datoteke",
-    "logging_enable_description": "Omogući zapisivanje",
-    "logging_level_description": "Kada je omogućeno, koju razinu zapisavanje koristiti.",
-    "logging_settings": "Zapisavanje",
-    "machine_learning_clip_model": "CLIP model",
-    "machine_learning_clip_model_description": "Naziv CLIP modela navedenog <link>ovdje</link>. Imajte na umu da morate ponovno pokrenuti posao 'Pametno Pretraživanje' za sve slike nakon promjene modela.",
-    "machine_learning_duplicate_detection": "Detekcija Duplikata",
-    "machine_learning_duplicate_detection_enabled": "Omogući detekciju duplikata",
-    "machine_learning_duplicate_detection_enabled_description": "",
-    "machine_learning_duplicate_detection_setting_description": "",
-    "machine_learning_enabled": "Uključi strojsko učenje",
-    "machine_learning_enabled_description": "Ukoliko je ovo isključeno, sve funkcije strojnoga učenja biti će isključene bez obzira na postavke ispod.",
-    "machine_learning_facial_recognition": "Detekcija lica",
-    "machine_learning_facial_recognition_description": "Detektiraj, prepoznaj i grupiraj lica u fotografijama",
-    "machine_learning_facial_recognition_model": "Model prepoznavanja lica",
-    "machine_learning_facial_recognition_model_description": "Modeli su navedeni silaznim redoslijedom veličine. Veći modeli su sporiji i koriste više memorije, ali daju bolje rezultate. Imajte na umu da morate ponovno pokrenuti posao detekcije lica za sve slike nakon promjene modela.",
-    "machine_learning_facial_recognition_setting": "Omogući prepoznavanje lica",
-    "machine_learning_facial_recognition_setting_description": "Ako je onemogućeno, slike neće biti kodirane za prepoznavanje lica i neće popuniti odjeljak Ljudi na stranici Istraživanje.",
-    "machine_learning_max_detection_distance": "Maksimalna udaljenost za detektiranje",
-    "machine_learning_max_detection_distance_description": "Maksimalna udaljenost između dvije slike da bi se smatrale duplikatima, u rasponu od 0,001-0,1. Više vrijednosti otkrit će više duplikata, ali mogu rezultirati netočnim rezultatima.",
-    "machine_learning_max_recognition_distance": "Maksimalna udaljenost za detekciju",
-    "machine_learning_max_recognition_distance_description": "Maksimalna udaljenost između dva lica koja se smatraju istom osobom, u rasponu od 0-2. Snižavanje može spriječiti označavanje dvije osobe kao iste osobe, dok podizanje može spriječiti označavanje iste osobe kao dvije različite osobe. Imajte na umu da je lakše spojiti dvije osobe nego jednu osobu podijeliti na dvije, stoga koristite niži prag kada je to moguće.",
-    "machine_learning_min_detection_score": "Minimalni rezultat otkrivanja",
-    "machine_learning_min_detection_score_description": "Minimalni rezultat pouzdanosti za detektirano lice od 0-1. Niže vrijednosti otkrit će više lica, ali mogu dovesti do lažno pozitivnih rezultata.",
-    "machine_learning_min_recognized_faces": "Minimum prepoznatih lica",
-    "machine_learning_min_recognized_faces_description": "Najmanji broj prepoznatih lica za osobu koja se stvara. Povećanje toga čini prepoznavanje lica preciznijim po cijenu povećanja šanse da lice nije dodijeljeno osobi.",
-    "machine_learning_settings": "Postavke strojnog učenja",
-    "machine_learning_settings_description": "Upravljajte značajkama i postavkama strojnog učenja",
-    "machine_learning_smart_search": "Pametna pretraga",
-    "machine_learning_smart_search_description": "Pretražujte slike semantički koristeći CLIP ugradnje",
-    "machine_learning_smart_search_enabled": "Omogući pametno pretraživanje",
-    "machine_learning_smart_search_enabled_description": "Ako je onemogućeno, slike neće biti kodirane za pametno pretraživanje.",
-    "machine_learning_url_description": "URL poslužitelja strojnog učenja",
-    "manage_concurrency": "Upravljanje Istovremenošću",
-    "manage_log_settings": "Upravljanje postavkama zapisivanje",
-    "map_dark_style": "Tamni stil",
-    "map_enable_description": "Omogući značajke karte",
-    "map_gps_settings": "Postavke Karte i GPS-a",
-    "map_gps_settings_description": "Upravljajte Postavkama Karte i GPS-a (Obrnuto Geokodiranje)",
-    "map_implications": "Značajka karte se oslanja na vanjsku uslugu pločica (tiles.immich.cloud)",
-    "map_light_style": "Svijetli stil",
-    "map_manage_reverse_geocoding_settings": "Upravljajte postavkama <link>Obrnutog Geokodiranja</link>",
-    "map_reverse_geocoding": "Obrnuto Geokodiranje",
-    "map_reverse_geocoding_enable_description": "Omogući obrnuto geokodiranje",
-    "map_reverse_geocoding_settings": "Postavke Obrnuto Geokodiranje",
-    "map_settings": "Karta",
-    "map_settings_description": "Upravljanje postavkama karte",
-    "map_style_description": "URL na style.json temu karte",
-    "metadata_extraction_job": "Izdvoj metapodatke",
-    "metadata_extraction_job_description": "Izdvojite podatke o metapodacima iz svakog sredstva, kao što su GPS i rezolucija",
-    "migration_job": "Migracija",
-    "migration_job_description": "Premjestite minijature za sredstva i lica u najnoviju strukturu mapa",
-    "no_paths_added": "Nema dodanih putanja",
-    "no_pattern_added": "Nije dodan uzorak",
-    "note_apply_storage_label_previous_assets": "Napomena: da biste primijenili Oznaku Pohrane na prethodno prenesena sredstva, pokrenite",
-    "note_cannot_be_changed_later": "NAPOMENA: Ovo se ne može promijeniti kasnije!",
-    "note_unlimited_quota": "Napomena: Unesite 0 za neograničenu kvotu",
-    "notification_email_from_address": "Od adrese",
-    "notification_email_from_address_description": "E-mail adresa pošiljatelja, na primjer: \"Immich Photo Server <noreply@immich.app>\"",
-    "notification_email_host_description": "Poslužitelja e-pošte (npr. smtp.immich.app)",
-    "notification_email_ignore_certificate_errors": "Ignoriraj pogreške certifikata",
-    "notification_email_ignore_certificate_errors_description": "Ignoriraj pogreške provjere valjanosti TLS certifikata (nije preporučeno)",
-    "notification_email_password_description": "Lozinka za korištenje pri autentifikaciji s poslužiteljem e-pošte",
-    "notification_email_port_description": "Port poslužitelja e-pošte (npr. 25, 465, ili 587)",
-    "notification_email_sent_test_email_button": "Pošaljite probni e-mail i spremi",
-    "notification_email_setting_description": "Postavke za slanje e-mail obavijeste",
-    "notification_email_test_email": "Pošalji probni e-mail",
-    "notification_email_test_email_failed": "Slanje testne e-pošte nije uspjelo, provjerite svoje postavke",
-    "notification_email_test_email_sent": "Testna e-poruka poslana je na {email}. Provjerite svoju pristiglu poštu.",
-    "notification_email_username_description": "Korisničko ime koje se koristi pri autentifikaciji s poslužiteljem e-pošte",
-    "notification_enable_email_notifications": "Omogući obavijesti putem e-pošte",
-    "notification_settings": "Postavke Obavijesti",
-    "notification_settings_description": "Upravljanje postavkama obavijesti, uključujući e-poštu",
-    "oauth_auto_launch": "Automatsko pokretanje",
-    "oauth_auto_launch_description": "Automatski pokrenite OAuth prijavu nakon navigacije na stranicu za prijavu",
-    "oauth_auto_register": "Automatska registracija",
-    "oauth_auto_register_description": "Automatski registrirajte nove korisnike nakon prijave s OAuth",
-    "oauth_button_text": "Tekst gumba",
-    "oauth_client_id": "ID Klijenta",
-    "oauth_client_secret": "Tajna Klijenta",
-    "oauth_enable_description": "Prijavite se putem OAutha",
-    "oauth_issuer_url": "URL Izdavatelja",
-    "oauth_mobile_redirect_uri": "Mobilnog Preusmjeravanja URI",
-    "oauth_mobile_redirect_uri_override": "",
-    "oauth_mobile_redirect_uri_override_description": "",
-    "oauth_scope": "",
-    "oauth_settings": "OAuth",
-    "oauth_settings_description": "Upravljanje postavkama za prijavu kroz OAuth",
-    "oauth_settings_more_details": "Za više pojedinosti o ovoj značajci pogledajte <link>uputstva</link>.",
-    "oauth_signing_algorithm": "",
-    "oauth_storage_label_claim": "",
-    "oauth_storage_label_claim_description": "",
-    "oauth_storage_quota_claim": "",
-    "oauth_storage_quota_claim_description": "",
-    "oauth_storage_quota_default": "",
-    "oauth_storage_quota_default_description": "Kvota u GiB koja će se koristiti kada nema zahtjeva (unesite 0 za neograničenu kvotu).",
-    "offline_paths": "Izvanmrežne putanje",
-    "offline_paths_description": "Ovi rezultati mogu biti posljedica ručnog brisanja datoteka koje nisu dio vanjske biblioteke.",
-    "password_enable_description": "Prijava s email adresom i zaporkom",
-    "password_settings": "Prijava zaporkom",
-    "password_settings_description": "Upravljanje postavkama za prijavu zaporkom",
-    "paths_validated_successfully": "Sve su putanje uspješno potvrđene",
-    "quota_size_gib": "Veličina kvote (GiB)",
-    "refreshing_all_libraries": "Osvježavanje svih biblioteka",
-    "registration": "Registracija administratora",
-    "registration_description": "Budući da ste prvi korisnik na sustavu, bit ćete dodijeljeni administratorsku ulogu i odgovorni ste za administrativne poslove, a dodatne korisnike kreirat ćete sami.",
-    "removing_offline_files": "Uklanjanje izvanmrežnih datoteka",
-    "repair_all": "Popravi sve",
-    "repair_matched_items": "",
-    "repaired_items": "",
-    "require_password_change_on_login": "Zahtijevajte od korisnika promjenu lozinke pri prvoj prijavi",
-    "reset_settings_to_default": "Vrati postavke na zadane",
-    "reset_settings_to_recent_saved": "Resetirajte postavke na nedavno spremljene postavke",
-    "scanning_library_for_changed_files": "Skeniranje biblioteke za promijenjene datoteke",
-    "scanning_library_for_new_files": "Skeniranje biblioteke za nove datoteke",
-    "send_welcome_email": "Pošaljite email dobrodošlice",
-    "server_external_domain_settings": "Vanjska domena",
-    "server_external_domain_settings_description": "Domena za javno dijeljene linkove, uključujući http(s)://",
-    "server_settings": "Postavke servera",
-    "server_settings_description": "Upravljanje postavkama servera",
-    "server_welcome_message": "Poruka dobrodošlice",
-    "server_welcome_message_description": "Poruka koja je prikazana na prijavi.",
-    "sidecar_job": "",
-    "sidecar_job_description": "",
-    "slideshow_duration_description": "",
-    "smart_search_job_description": "",
-    "storage_template_enable_description": "",
-    "storage_template_hash_verification_enabled": "",
-    "storage_template_hash_verification_enabled_description": "",
-    "storage_template_migration": "",
-    "storage_template_migration_job": "",
-    "storage_template_settings": "",
-    "storage_template_settings_description": "",
-    "system_settings": "",
-    "theme_custom_css_settings": "Prilagođeni CSS",
-    "theme_custom_css_settings_description": "Kaskadni listovi stilova (CSS) omogućuju prilagođavanje dizajna Immicha.",
-    "theme_settings": "Postavke tema",
-    "theme_settings_description": "Upravljajte prilagodbom Immich web sučelja",
-    "these_files_matched_by_checksum": "",
-    "thumbnail_generation_job": "Generirajte sličice",
-    "thumbnail_generation_job_description": "",
-    "transcoding_acceleration_api": "API ubrzanja",
-    "transcoding_acceleration_api_description": "API koji će komunicirati s vašim uređajem radi ubrzanja transkodiranja. Ova postavka je 'najveći trud': vratit će se na softversko transkodiranje u slučaju kvara. VP9 može ili ne mora raditi ovisno o vašem hardveru.",
-    "transcoding_acceleration_nvenc": "NVENC (zahtjeva NVIDIA GPU)",
-    "transcoding_acceleration_qsv": "Quick Sync (zahtjeva Intel CPU sedme ili veće generacije)",
-    "transcoding_acceleration_rkmpp": "RKMPP (samo na Rockchip SOCima)",
-    "transcoding_acceleration_vaapi": "VAAPI",
-    "transcoding_accepted_audio_codecs": "Prihvačeni audio kodeci",
-    "transcoding_accepted_audio_codecs_description": "Odaberite koji audio kodeci ne trebaju biti transkodirani. Samo korišteno za neka pravila za transkodiranje.",
-    "transcoding_accepted_containers": "Prihvaćeni kontenjeri",
-    "transcoding_accepted_containers_description": "Odaberite koji formati spremnika ne moraju biti remulksirani u MP4. Koristi se samo za određena pravila transkodiranja.",
-    "transcoding_accepted_video_codecs": "Prihvaćeni video kodeci",
-    "transcoding_accepted_video_codecs_description": "",
-    "transcoding_advanced_options_description": "Postavke većina korisnika ne treba mjenjati",
-    "transcoding_audio_codec": "Audio kodek",
-    "transcoding_audio_codec_description": "Opus je opcija s najvećom kvalitetom, no ima manju podršku s starim uređajima i softverima.",
-    "transcoding_bitrate_description": "Videozapisi veći od maksimalne brzine prijenosa ili nisu u prihvatljivom formatu",
-    "transcoding_constant_quality_mode": "",
-    "transcoding_constant_quality_mode_description": "",
-    "transcoding_constant_rate_factor": "",
-    "transcoding_constant_rate_factor_description": "",
-    "transcoding_disabled_description": "",
-    "transcoding_hardware_acceleration": "",
-    "transcoding_hardware_acceleration_description": "",
-    "transcoding_hardware_decoding": "",
-    "transcoding_hardware_decoding_setting_description": "",
-    "transcoding_hevc_codec": "HEVC kodek",
-    "transcoding_max_b_frames": "",
-    "transcoding_max_b_frames_description": "",
-    "transcoding_max_bitrate": "Maksimalne brzina prijenosa (bitrate)",
-    "transcoding_max_bitrate_description": "",
-    "transcoding_max_keyframe_interval": "",
-    "transcoding_max_keyframe_interval_description": "",
-    "transcoding_optimal_description": "",
-    "transcoding_preferred_hardware_device": "",
-    "transcoding_preferred_hardware_device_description": "",
-    "transcoding_preset_preset": "",
-    "transcoding_preset_preset_description": "",
-    "transcoding_reference_frames": "",
-    "transcoding_reference_frames_description": "",
-    "transcoding_required_description": "",
-    "transcoding_settings": "",
-    "transcoding_settings_description": "",
-    "transcoding_target_resolution": "",
-    "transcoding_target_resolution_description": "",
-    "transcoding_temporal_aq": "",
-    "transcoding_temporal_aq_description": "",
-    "transcoding_threads": "",
-    "transcoding_threads_description": "",
-    "transcoding_tone_mapping": "",
-    "transcoding_tone_mapping_description": "",
-    "transcoding_tone_mapping_npl": "",
-    "transcoding_tone_mapping_npl_description": "",
-    "transcoding_transcode_policy": "",
-    "transcoding_transcode_policy_description": "",
-    "transcoding_two_pass_encoding": "",
-    "transcoding_two_pass_encoding_setting_description": "",
-    "transcoding_video_codec": "",
-    "transcoding_video_codec_description": "",
-    "trash_enabled_description": "",
-    "trash_number_of_days": "",
-    "trash_number_of_days_description": "",
-    "trash_settings": "",
-    "trash_settings_description": "",
-    "untracked_files": "",
-    "untracked_files_description": "",
-    "user_delete_delay_settings": "",
-    "user_delete_delay_settings_description": "",
-    "user_management": "",
-    "user_password_has_been_reset": "",
-    "user_password_reset_description": "",
-    "user_settings": "",
-    "user_settings_description": "",
-    "user_successfully_removed": "",
-    "version_check_enabled_description": "",
-    "version_check_settings": "",
-    "version_check_settings_description": "",
-    "video_conversion_job": "",
-    "video_conversion_job_description": ""
-  },
-  "admin_email": "",
-  "admin_password": "",
-  "administration": "",
-  "advanced": "",
-  "album_added": "",
-  "album_added_notification_setting_description": "",
-  "album_cover_updated": "",
-  "album_info_updated": "",
-  "album_name": "",
-  "album_options": "",
-  "album_updated": "",
-  "album_updated_setting_description": "",
-  "albums": "",
-  "albums_count": "",
-  "all": "",
-  "all_people": "",
-  "allow_dark_mode": "",
-  "allow_edits": "",
-  "api_key": "",
-  "api_keys": "",
-  "app_settings": "",
-  "appears_in": "",
-  "archive": "",
-  "archive_or_unarchive_photo": "",
-  "archived": "",
-  "asset_offline": "",
-  "assets": "",
-  "authorized_devices": "",
-  "back": "",
-  "backward": "",
-  "blurred_background": "",
-  "camera": "",
-  "camera_brand": "",
-  "camera_model": "",
-  "cancel": "",
-  "cancel_search": "",
-  "cannot_merge_people": "",
-  "cannot_update_the_description": "",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
-  "change_date": "",
-  "change_expiration_time": "",
-  "change_location": "",
-  "change_name": "",
-  "change_name_successfully": "",
-  "change_password": "",
-  "change_your_password": "",
-  "changed_visibility_successfully": "",
-  "check_all": "",
-  "check_logs": "",
-  "choose_matching_people_to_merge": "",
-  "city": "",
-  "clear": "",
-  "clear_all": "",
-  "clear_message": "",
-  "clear_value": "",
-  "close": "",
-  "collapse_all": "",
-  "color_theme": "",
-  "comment_options": "",
-  "comments_are_disabled": "",
-  "confirm": "",
-  "confirm_admin_password": "",
-  "confirm_delete_shared_link": "",
-  "confirm_password": "",
-  "contain": "",
-  "context": "",
-  "continue": "",
-  "copied_image_to_clipboard": "",
-  "copied_to_clipboard": "",
-  "copy_error": "",
-  "copy_file_path": "",
-  "copy_image": "",
-  "copy_link": "",
-  "copy_link_to_clipboard": "",
-  "copy_password": "",
-  "copy_to_clipboard": "",
-  "country": "",
-  "cover": "",
-  "covers": "",
-  "create": "",
-  "create_album": "",
-  "create_library": "",
-  "create_link": "",
-  "create_link_to_share": "",
-  "create_new_person": "",
-  "create_new_user": "",
-  "create_user": "",
-  "created": "",
-  "current_device": "",
-  "custom_locale": "",
-  "custom_locale_description": "",
-  "dark": "",
-  "date_after": "",
-  "date_and_time": "",
-  "date_before": "",
-  "date_range": "",
-  "day": "",
-  "default_locale": "",
-  "default_locale_description": "",
-  "delete": "",
-  "delete_album": "",
-  "delete_api_key_prompt": "",
-  "delete_key": "",
-  "delete_library": "",
-  "delete_link": "",
-  "delete_shared_link": "",
-  "delete_user": "",
-  "deleted_shared_link": "",
-  "description": "",
-  "details": "",
-  "direction": "",
-  "disabled": "",
-  "disallow_edits": "",
-  "discover": "",
-  "dismiss_all_errors": "",
-  "dismiss_error": "",
-  "display_options": "",
-  "display_order": "",
-  "display_original_photos": "",
-  "display_original_photos_setting_description": "",
-  "done": "",
-  "download": "",
-  "downloading": "",
-  "duration": "",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
-  "edit_album": "",
-  "edit_avatar": "",
-  "edit_date": "",
-  "edit_date_and_time": "",
-  "edit_exclusion_pattern": "",
-  "edit_faces": "",
-  "edit_import_path": "",
-  "edit_import_paths": "",
-  "edit_key": "",
-  "edit_link": "",
-  "edit_location": "",
-  "edit_name": "",
-  "edit_people": "",
-  "edit_title": "",
-  "edit_user": "",
-  "edited": "",
-  "editor": "",
-  "email": "",
-  "empty_album": "",
-  "empty_trash": "",
-  "enable": "",
-  "enabled": "",
-  "end_date": "",
-  "error": "",
-  "error_loading_image": "",
-  "errors": {
-    "cleared_jobs": "",
-    "exclusion_pattern_already_exists": "",
-    "failed_job_command": "",
-    "import_path_already_exists": "",
-    "paths_validation_failed": "",
-    "quota_higher_than_disk_size": "",
-    "repair_unable_to_check_items": "",
-    "unable_to_add_album_users": "",
-    "unable_to_add_comment": "",
-    "unable_to_add_exclusion_pattern": "",
-    "unable_to_add_import_path": "",
-    "unable_to_add_partners": "",
-    "unable_to_change_album_user_role": "",
-    "unable_to_change_date": "",
-    "unable_to_change_location": "",
-    "unable_to_change_password": "",
-    "unable_to_copy_to_clipboard": "",
-    "unable_to_create_api_key": "",
-    "unable_to_create_library": "",
-    "unable_to_create_user": "",
-    "unable_to_delete_album": "",
-    "unable_to_delete_asset": "",
-    "unable_to_delete_exclusion_pattern": "",
-    "unable_to_delete_import_path": "",
-    "unable_to_delete_shared_link": "",
-    "unable_to_delete_user": "",
-    "unable_to_edit_exclusion_pattern": "",
-    "unable_to_edit_import_path": "",
-    "unable_to_empty_trash": "",
-    "unable_to_enter_fullscreen": "",
-    "unable_to_exit_fullscreen": "",
-    "unable_to_hide_person": "",
-    "unable_to_link_oauth_account": "",
-    "unable_to_load_album": "",
-    "unable_to_load_asset_activity": "",
-    "unable_to_load_items": "",
-    "unable_to_load_liked_status": "",
-    "unable_to_play_video": "",
-    "unable_to_refresh_user": "",
-    "unable_to_remove_album_users": "",
-    "unable_to_remove_api_key": "",
-    "unable_to_remove_library": "",
-    "unable_to_remove_offline_files": "",
-    "unable_to_remove_partner": "",
-    "unable_to_remove_reaction": "",
-    "unable_to_repair_items": "",
-    "unable_to_reset_password": "",
-    "unable_to_resolve_duplicate": "",
-    "unable_to_restore_assets": "",
-    "unable_to_restore_trash": "",
-    "unable_to_restore_user": "",
-    "unable_to_save_album": "",
-    "unable_to_save_api_key": "",
-    "unable_to_save_name": "",
-    "unable_to_save_profile": "",
-    "unable_to_save_settings": "",
-    "unable_to_scan_libraries": "",
-    "unable_to_scan_library": "",
-    "unable_to_set_profile_picture": "",
-    "unable_to_submit_job": "",
-    "unable_to_trash_asset": "",
-    "unable_to_unlink_account": "",
-    "unable_to_update_library": "",
-    "unable_to_update_location": "",
-    "unable_to_update_settings": "",
-    "unable_to_update_timeline_display_status": "",
-    "unable_to_update_user": ""
-  },
-  "exit_slideshow": "",
-  "expand_all": "",
-  "expire_after": "",
-  "expired": "",
-  "explore": "",
-  "export": "",
-  "export_as_json": "",
-  "extension": "",
-  "external": "",
-  "external_libraries": "",
-  "failed_to_get_people": "",
-  "favorite": "",
-  "favorite_or_unfavorite_photo": "",
-  "favorites": "",
-  "feature_photo_updated": "",
-  "file_name": "",
-  "file_name_or_extension": "",
-  "filename": "",
-  "filetype": "",
-  "filter_people": "",
-  "find_them_fast": "",
-  "fix_incorrect_match": "",
-  "force_re-scan_library_files": "",
-  "forward": "",
-  "general": "",
-  "get_help": "",
-  "getting_started": "",
-  "go_back": "",
-  "go_to_search": "",
-  "go_to_share_page": "",
-  "group_albums_by": "",
-  "has_quota": "",
-  "hide_gallery": "",
-  "hide_password": "",
-  "hide_person": "",
-  "host": "",
-  "hour": "",
-  "image": "",
-  "immich_logo": "",
-  "import_from_json": "",
-  "import_path": "",
-  "in_archive": "",
-  "include_archived": "",
-  "include_shared_albums": "",
-  "include_shared_partner_assets": "",
-  "individual_share": "",
-  "info": "",
-  "interval": {
-    "day_at_onepm": "",
-    "hours": "",
-    "night_at_midnight": "",
-    "night_at_twoam": ""
-  },
-  "invite_people": "",
-  "invite_to_album": "",
-  "jobs": "",
-  "keep": "",
-  "keyboard_shortcuts": "",
-  "language": "",
-  "language_setting_description": "",
-  "last_seen": "",
-  "leave": "",
-  "let_others_respond": "",
-  "level": "",
-  "library": "",
-  "library_options": "",
-  "light": "",
-  "link_options": "",
-  "link_to_oauth": "",
-  "linked_oauth_account": "",
-  "list": "",
-  "loading": "",
-  "loading_search_results_failed": "",
-  "log_out": "",
-  "log_out_all_devices": "",
-  "login_has_been_disabled": "",
-  "look": "",
-  "loop_videos": "",
-  "loop_videos_description": "",
-  "make": "",
-  "manage_shared_links": "",
-  "manage_sharing_with_partners": "",
-  "manage_the_app_settings": "",
-  "manage_your_account": "",
-  "manage_your_api_keys": "",
-  "manage_your_devices": "",
-  "manage_your_oauth_connection": "",
-  "map": "",
-  "map_marker_with_image": "",
-  "map_settings": "",
-  "matches": "",
-  "media_type": "",
-  "memories": "",
-  "memories_setting_description": "",
-  "menu": "",
-  "merge": "",
-  "merge_people": "",
-  "merge_people_successfully": "",
-  "minimize": "",
-  "minute": "",
-  "missing": "",
-  "model": "",
-  "month": "",
-  "more": "",
-  "moved_to_trash": "",
-  "my_albums": "",
-  "name": "",
-  "name_or_nickname": "",
-  "never": "",
-  "new_api_key": "",
-  "new_password": "",
-  "new_person": "",
-  "new_user_created": "",
-  "newest_first": "",
-  "next": "",
-  "next_memory": "",
-  "no": "",
-  "no_albums_message": "",
-  "no_archived_assets_message": "",
-  "no_assets_message": "",
-  "no_duplicates_found": "",
-  "no_exif_info_available": "",
-  "no_explore_results_message": "",
-  "no_favorites_message": "",
-  "no_libraries_message": "",
-  "no_name": "",
-  "no_places": "",
-  "no_results": "",
-  "no_shared_albums_message": "",
-  "not_in_any_album": "",
-  "note_apply_storage_label_to_previously_uploaded assets": "",
-  "note_unlimited_quota": "",
-  "notes": "",
-  "notification_toggle_setting_description": "",
-  "notifications": "",
-  "notifications_setting_description": "",
-  "oauth": "",
-  "offline": "",
-  "offline_paths": "",
-  "offline_paths_description": "",
-  "ok": "",
-  "oldest_first": "",
-  "online": "",
-  "only_favorites": "",
-  "only_refreshes_modified_files": "",
-  "open_the_search_filters": "",
-  "options": "",
-  "organize_your_library": "",
-  "other": "",
-  "other_devices": "",
-  "other_variables": "",
-  "owned": "",
-  "owner": "",
-  "partner_can_access": "",
-  "partner_can_access_assets": "",
-  "partner_can_access_location": "",
-  "partner_sharing": "",
-  "partners": "",
-  "password": "",
-  "password_does_not_match": "",
-  "password_required": "",
-  "password_reset_success": "",
-  "past_durations": {
-    "days": "",
-    "hours": "",
-    "years": ""
-  },
-  "path": "",
-  "pattern": "",
-  "pause": "",
-  "pause_memories": "",
-  "paused": "",
-  "pending": "",
-  "people": "",
-  "people_sidebar_description": "",
-  "permanent_deletion_warning": "",
-  "permanent_deletion_warning_setting_description": "",
-  "permanently_delete": "",
-  "permanently_deleted_asset": "",
-  "photos": "",
-  "photos_count": "",
-  "photos_from_previous_years": "",
-  "pick_a_location": "",
-  "place": "",
-  "places": "",
-  "play": "",
-  "play_memories": "",
-  "play_motion_photo": "",
-  "play_or_pause_video": "",
-  "port": "",
-  "preset": "",
-  "preview": "",
-  "previous": "",
-  "previous_memory": "",
-  "previous_or_next_photo": "",
-  "primary": "",
-  "profile_picture_set": "",
-  "public_share": "",
-  "reaction_options": "",
-  "read_changelog": "",
-  "recent": "",
-  "recent_searches": "",
-  "refresh": "",
-  "refreshed": "",
-  "refreshes_every_file": "",
-  "remove": "",
-  "remove_from_album": "",
-  "remove_from_favorites": "",
-  "remove_from_shared_link": "",
-  "remove_offline_files": "",
-  "removed_api_key": "",
-  "rename": "",
-  "repair": "",
-  "repair_no_results_message": "",
-  "replace_with_upload": "",
-  "require_password": "",
-  "require_user_to_change_password_on_first_login": "",
-  "reset": "",
-  "reset_password": "",
-  "reset_people_visibility": "",
-  "restore": "",
-  "restore_all": "",
-  "restore_user": "",
-  "resume": "",
-  "retry_upload": "",
-  "review_duplicates": "",
-  "role": "",
-  "save": "",
-  "saved_api_key": "",
-  "saved_profile": "",
-  "saved_settings": "",
-  "say_something": "",
-  "scan_all_libraries": "",
-  "scan_all_library_files": "",
-  "scan_new_library_files": "",
-  "scan_settings": "",
-  "search": "",
-  "search_albums": "",
-  "search_by_context": "",
-  "search_camera_make": "",
-  "search_camera_model": "",
-  "search_city": "",
-  "search_country": "",
-  "search_for_existing_person": "",
-  "search_people": "",
-  "search_places": "",
-  "search_state": "",
-  "search_timezone": "",
-  "search_type": "",
-  "search_your_photos": "",
-  "searching_locales": "",
-  "second": "",
-  "select_album_cover": "",
-  "select_all": "",
-  "select_avatar_color": "",
-  "select_face": "",
-  "select_featured_photo": "",
-  "select_keep_all": "",
-  "select_library_owner": "",
-  "select_new_face": "",
-  "select_photos": "",
-  "select_trash_all": "",
-  "selected": "",
-  "send_message": "",
-  "send_welcome_email": "",
-  "server": "",
-  "server_stats": "",
-  "set": "",
-  "set_as_album_cover": "",
-  "set_as_profile_picture": "",
-  "set_date_of_birth": "",
-  "set_profile_picture": "",
-  "set_slideshow_to_fullscreen": "",
-  "settings": "",
-  "settings_saved": "",
-  "share": "",
-  "shared": "",
-  "shared_by": "",
-  "shared_by_you": "",
-  "shared_from_partner": "",
-  "shared_links": "",
-  "shared_with_partner": "",
-  "sharing": "",
-  "sharing_sidebar_description": "",
-  "show_album_options": "",
-  "show_and_hide_people": "",
-  "show_file_location": "",
-  "show_gallery": "",
-  "show_hidden_people": "",
-  "show_in_timeline": "",
-  "show_in_timeline_setting_description": "",
-  "show_keyboard_shortcuts": "",
-  "show_metadata": "",
-  "show_or_hide_info": "",
-  "show_password": "",
-  "show_person_options": "",
-  "show_progress_bar": "",
-  "show_search_options": "",
-  "shuffle": "",
-  "sign_out": "",
-  "sign_up": "",
-  "size": "",
-  "skip_to_content": "",
-  "slideshow": "",
-  "slideshow_settings": "",
-  "sort_albums_by": "",
-  "stack": "",
-  "stack_selected_photos": "",
-  "stacktrace": "",
-  "start": "",
-  "start_date": "",
-  "state": "",
-  "status": "",
-  "stop_motion_photo": "",
-  "stop_photo_sharing": "",
-  "stop_photo_sharing_description": "",
-  "stop_sharing_photos_with_user": "",
-  "storage": "",
-  "storage_label": "",
-  "storage_usage": "",
-  "submit": "",
-  "suggestions": "Prijedlozi",
-  "sunrise_on_the_beach": "Sunrise on the beach",
-  "swap_merge_direction": "",
-  "sync": "Sink.",
-  "template": "",
-  "theme": "Tema",
-  "theme_selection": "Izbor teme",
-  "theme_selection_description": "Automatski postavite temu na svijetlu ili tamnu ovisno o postavkama sustava vašeg preglednika",
-  "they_will_be_merged_together": "Oni ću biti spojeni zajedno",
-  "time_based_memories": "Uspomene temeljene na vremenu",
-  "timezone": "Vremenska zona",
-  "to_archive": "Arhivaj",
-  "to_change_password": "Promjeni lozinku",
-  "to_favorite": "Omiljeni",
-  "to_login": "Prijava",
-  "to_trash": "Smeće",
-  "toggle_settings": "Uključi/isključi postavke",
-  "toggle_theme": "Promjeni temu",
-  "toggle_visibility": "",
-  "total_usage": "Ukupna upotreba",
-  "trash": "Smeće",
-  "trash_all": "Stavi sve u smeće",
-  "trash_no_results_message": "Ovdje će se prikazati bačene fotografije i videozapisi.",
-  "trashed_items_will_be_permanently_deleted_after": "Stavke bačene u smeće trajno će se izbrisati nakon {days, plural, one {# day} other {# days}}.",
-  "type": "Vrsta",
-  "unarchive": "",
-  "unarchived": "",
-  "unfavorite": "",
-  "unhide_person": "",
-  "unknown": "",
-  "unknown_album": "",
-  "unknown_year": "",
-  "unlimited": "",
-  "unlink_oauth": "",
-  "unlinked_oauth_account": "",
-  "unselect_all": "",
-  "unstack": "",
-  "untracked_files": "",
-  "untracked_files_decription": "",
-  "up_next": "",
-  "updated_password": "",
-  "upload": "",
-  "upload_concurrency": "",
-  "url": "",
-  "usage": "",
-  "user": "",
-  "user_id": "",
-  "user_usage_detail": "",
-  "username": "",
-  "users": "",
-  "utilities": "",
-  "validate": "",
-  "variables": "",
-  "version": "",
-  "video": "",
-  "video_hover_setting": "",
-  "video_hover_setting_description": "",
-  "videos": "",
-  "videos_count": "",
-  "view_all": "",
-  "view_all_users": "",
-  "view_links": "",
-  "view_next_asset": "",
-  "view_previous_asset": "",
-  "viewer": "",
-  "waiting": "",
-  "week": "",
-  "welcome_to_immich": "",
-  "year": "",
-  "yes": "",
-  "you_dont_have_any_shared_links": "",
-  "zoom_image": ""
-}
diff --git a/web/src/lib/i18n/hu.json b/web/src/lib/i18n/hu.json
deleted file mode 100644
index 753839c384..0000000000
--- a/web/src/lib/i18n/hu.json
+++ /dev/null
@@ -1,1292 +0,0 @@
-{
-  "about": "Az Immich-ről",
-  "account": "Fiók",
-  "account_settings": "Fiók Beállítások",
-  "acknowledge": "Rendben, láttam",
-  "action": "Művelet",
-  "actions": "Műveletek",
-  "active": "Feldolgozás alatt",
-  "activity": "Aktivitás",
-  "activity_changed": "A tevékenység {enabled, select, true {enabled} other {disabled}}",
-  "add": "Hozzáadás",
-  "add_a_description": "Leírás hozzáadása",
-  "add_a_location": "Helyszín hozzáadása",
-  "add_a_name": "Név megadása",
-  "add_a_title": "Címadás",
-  "add_exclusion_pattern": "Kizárási minta hozzáadása",
-  "add_import_path": "Importálási útvonal hozzáadása",
-  "add_location": "Helyszín megadása",
-  "add_more_users": "További felhasználók hozzáadása",
-  "add_partner": "Társ hozzáadás",
-  "add_path": "Elérési útvonal megadása",
-  "add_photos": "Fotók hozzáadása",
-  "add_to": "Hozzáadás ide...",
-  "add_to_album": "Felvétel albumba",
-  "add_to_shared_album": "Felvétel megosztott albumba",
-  "added_to_archive": "Hozzáadva az archívumhoz",
-  "added_to_favorites": "Hozzáadva a kedvencekhez",
-  "added_to_favorites_count": "{count, number} hozzáadva a kedvencekhez",
-  "admin": {
-    "add_exclusion_pattern_description": "Kizáró minta megadása. Támogatja *, ** és ? dzsókerek használatát. Pl. a \"Raw\" könyvtárban tárolt összes fájl figyelmen kívül hagyásához használható a \"**/Raw/**\". Minden \".tif\" fájl figyelmen kívül hagyásához használható a \"**/*.tif\". Abszolut elérési útvonal figyelmen kívül hagyásához használható a \"/path/to/ignore/**\".",
-    "authentication_settings": "Hitelesítési beállítások",
-    "authentication_settings_description": "Jelszó, OAuth és egyéb hitelesítési beállítások szerkesztése",
-    "authentication_settings_disable_all": "Biztosan letiltja az összes bejelentkezési módot? A bejelentkezés teljesen le lesz tiltva.",
-    "authentication_settings_reenable": "Az újbóli engedélyezéshez használjon egy<link>Szerver Parancsot</link>.",
-    "background_task_job": "Háttérfolyamatok",
-    "check_all": "Összes Kipiálása",
-    "cleared_jobs": "{job} munkák kitörölve",
-    "config_set_by_file": "A konfigurációt jelenleg egy konfigurációs fájl állítja be",
-    "confirm_delete_library": "Biztosan ki szeretné törölni a {library} képtárat?",
-    "confirm_delete_library_assets": "Biztosan kitörli ezt a képtárat? Ez kitöröl {count, plural, one {#} other {#}} benne lévő fájlt az Immichből és nem visszavonható. A fájlok a lemezen maradnak.",
-    "confirm_email_below": "A megerősítéshez írja \"{email}\"-t alább",
-    "confirm_reprocess_all_faces": "Biztos benne, hogy újra szeretné feldolgozni az összes arcot? Ez a megnevezett személyeket is törli.",
-    "confirm_user_password_reset": "Biztosan vissza szeretné állítani {user} jelszavát?",
-    "crontab_guru": "Crontab Guru",
-    "disable_login": "Belépés letiltása",
-    "disabled": "Letiltva",
-    "duplicate_detection_job_description": "Gépi tanulás futtatása a hasonló képek megtalálása céljából. Az Okos Keresés feladattól függ",
-    "exclusion_pattern_description": "Kizáró minták használata lehetőséget ad arra, hogy bizonyos fájlok vagy könyvtárak át legyenek ugorva a képtárak átfésülésekor. Akkor hasznos, ha a könyvtárakban vannak olyan fájlok, amelyeket nem kell importálni, pl. nyers (RAW) fájlok.",
-    "external_library_created_at": "Külső képtár (készült: {date})",
-    "external_library_management": "Külső Képtárak Menedzselése",
-    "face_detection": "Arckeresés",
-    "face_detection_description": "Gépi tanulás segítségével felismeri, hol találhatóak arcok a képeken. Videok esetében csak a bélyegképen keres. \"Mind\" (újra) feldolgozza az összes képet. \"Hiányzók\" sorba állítja azokat, amelyek eddig még nem lettek feldolgozva. A megtalált arcok ezután az Arcfelismeréshez lesznek sorba állítva, amely ezután az arcokat csoportosítja és meglevő vagy új személyekhez rendeli azokat.",
-    "facial_recognition_job_description": "A felismert arcokat csoportosítva személyekhez rendeli. Ez a lépés azután következik, amikor az arckeresés lefutott. \"Mind\" (újra)csoportosítja az össze arcot. \"Hiányzók\" csak azokkal az arcokkal foglalkozik, amelyekhez még nincsen személy rendelve.",
-    "failed_job_command": "A(z) {command} parancs nem sikerült a következő feladathoz: {job}",
-    "force_delete_user_warning": "FIGYELEM: Ez azonnal eltávolítja a felhasználót és az összes hozzá tartozó fájlt. A művelet nem visszavonható, és a fájlokat sem lehet később visszanyerni.",
-    "forcing_refresh_library_files": "A képtár összes fájlának frissítése",
-    "image_format_description": "WebP a JPEG-nél kisebb fájlokat készít, viszont lassaban dolgozik.",
-    "image_prefer_embedded_preview": "Beágyazott előnézeti kép előnyben részesítése",
-    "image_prefer_embedded_preview_setting_description": "Nyers (RAW) fotók esetén használja a beépített előnézeti képet (ha van) a képek feldogozásához. Ez néhány kép esetében pontosabb színeket eredményezhet, de az előnézeti kép minősége erősen fényképezőgép függő, és a képen előfordulhatnak tömörítési hibák.",
-    "image_prefer_wide_gamut": "Inkább a széles skálát preferálja",
-    "image_prefer_wide_gamut_setting_description": "A bélyegképekhez használjon P3-at. Ez a széles spektrumú színképek esetében jobban átadja az élénkebb színeket, de régebbi eszközökön, régebbi böngészők esetében a képek másképpen jelenhetnek meg. Az sRGB képek ebben a színtartományban maradnak a színeltolódások megelőzése érdekében.",
-    "image_preview_format": "Előnézet formátuma",
-    "image_preview_resolution": "Előnézet felbontása",
-    "image_preview_resolution_description": "Fotó egyedüli nézetéhez használatos beállítás, valamint a gépi tanulás is ezt használja. Nagyobb felbontás több részletet megőriz, de tovább tart a folyamat, nagyobb fájl méretet eredményez, és befolyásolhatja az alkalmazás reakcióidejét.",
-    "image_quality": "Minőség",
-    "image_quality_description": "Képminőség 1 és 100 között. A nagyobb érték jobb minőséget, de nagyobb fájlt eredményez.",
-    "image_settings": "Kép beállítások",
-    "image_settings_description": "A generált képek minőség és felbontás beállításainak módosítása",
-    "image_thumbnail_format": "Bélyegkép formátum",
-    "image_thumbnail_resolution": "Bélyegkép felbontás",
-    "image_thumbnail_resolution_description": "Képek csoportosított nézetekor használatos (idővonal, album nézet stb). Nagyobb felbontás esetén a kép részletgazdagabb marad, de tovább tart elkészíteni, nagyobb fájl méretet eredményes, és ronthatja az alkalmazás reagálását.",
-    "job_concurrency": "{job} párhuzamosság",
-    "job_not_concurrency_safe": "Ez a feladat nem párhuzamosság-biztos.",
-    "job_settings": "Feladat beállítások",
-    "job_settings_description": "Feladatok párhuzamosságának beállítása",
-    "job_status": "Feladat állapota",
-    "jobs_delayed": "{jobCount, plural, other {# késik}}",
-    "jobs_failed": "{jobCount, plural, other {# sikertelen}}",
-    "library_created": "A(z) {library} képtár elkészült",
-    "library_cron_expression": "Cron kifejezés",
-    "library_cron_expression_description": "Átfésülések közötti intervallum beállítása cron formátumban. Több információt találhat például itt: <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "Cron kifejezés sablonok",
-    "library_deleted": "Képtár törölve",
-    "library_import_path_description": "A betöltendő könyvtár. A rendszer ezt a könyvtárat (alkönyvtárait is beleértve) fogja átfésülni képekért és videokért.",
-    "library_scanning": "Időszakos Átfésülés",
-    "library_scanning_description": "A képtár időszakos átfésülésének beállítása",
-    "library_scanning_enable_description": "Képtár időszakos átfésülésének engedélyezése",
-    "library_settings": "Külső képtár",
-    "library_settings_description": "Külső képtár beállításai",
-    "library_tasks_description": "Könytár tevékenységek elvégzése",
-    "library_watching_enable_description": "Külső képtár változásainak figyelemmel kísérése",
-    "library_watching_settings": "Képtár figyelése (KÍSÉRLETI)",
-    "library_watching_settings_description": "Megváltozott fájlok automatikus észlelése",
-    "logging_enable_description": "Naplózás engedélyezése",
-    "logging_level_description": "Ha be van kapcsolva, milyen mélységű legyen a naplózás.",
-    "logging_settings": "Naplózás",
-    "machine_learning_clip_model": "CLIP modell",
-    "machine_learning_clip_model_description": "Egy CLIP modell neve az <link>itt</link> felsoroltak közül. A modell megváltoztatása után újra kell futtatni az 'Okos Keresés' munkát minden képre.",
-    "machine_learning_duplicate_detection": "Másolatok Észlelése",
-    "machine_learning_duplicate_detection_enabled": "Másolatkeresés engedélyezése",
-    "machine_learning_duplicate_detection_enabled_description": "Ha ki van kapcsolva, a pontosan azonos fájlok akkor sem lesznek duplikálva.",
-    "machine_learning_duplicate_detection_setting_description": "CLIP beágyazások használata a valószínű másolatok kereséséhez",
-    "machine_learning_enabled": "Gépi tanulás engedélyezése",
-    "machine_learning_enabled_description": "Ha ki van kapcsolva, a gépi tanulási képességek az alábbi beállításoktól függetlenül ki lesznek kapcsolva.",
-    "machine_learning_facial_recognition": "Arcfelismerés",
-    "machine_learning_facial_recognition_description": "A képekben szereplő arcok megtalálása, felismerése és csoportosítása",
-    "machine_learning_facial_recognition_model": "Arcfelismerési modell",
-    "machine_learning_facial_recognition_model_description": "A modellek méret szerint csökkenő sorrendben vannak felsorolva. A nagyobb modellek lassabbak és több memóriát használnak, de jobb eredményt produkálnak. Modellváltás után az összes képen újra le kell futtatni az arcfelismerési feladatot.",
-    "machine_learning_facial_recognition_setting": "Arckeresés engedélyezése",
-    "machine_learning_facial_recognition_setting_description": "Ha ki van kapcsolva, a képek nem lesznek az arcfelismerésen lefuttatva és a Felfedezés oldalon az Személyek szekcióban nem fog szerepelni senki.",
-    "machine_learning_max_detection_distance": "Maximum észlelési távolság",
-    "machine_learning_max_detection_distance_description": "Két kép közötti maximális távolság, amely esetében még másolatnak tekintjük őket (0.001 és 0.1 közötti érték). Magasabb értékek több másolatot találnak meg, de a hamis találatok esélye is nagyobb.",
-    "machine_learning_max_recognition_distance": "Maximum felismerési távolság",
-    "machine_learning_max_recognition_distance_description": "Két arc közötti maximális távolság, amely alapján ugyanazon személynek tekinthetők, 0 és 2 között. Ennek csökkentése megakadályozhatja, hogy két különböző személyt ugyanannak a személynek jelöljünk, míg a növelése megakadályozhatja, hogy ugyanazt a személyt két különböző személyként jelöljük. Figyelembe kell venni, hogy könnyebb két személyt összevonni, mint egy személyt kettéosztani, ezért lehetőség szerint inkább alacsonyabb küszöbértéket válasszunk.",
-    "machine_learning_min_detection_score": "Minimum felismerési pontszám",
-    "machine_learning_min_detection_score_description": "Az arcok észleléséhez szükséges minimális megbízhatósági pontszám 0 és 1 között. Alacsonyabb értékek több arcot észlelnek, de hamis pozitív eredményekhez vezethetnek.",
-    "machine_learning_min_recognized_faces": "Minimum felismert arc",
-    "machine_learning_min_recognized_faces_description": "Egy személy létrehozásához szükséges minimálisan felismert arcok száma. Ennek növelésével a arcfelismerés pontosabbá válik, azonban növeli annak az esélyét, hogy egy arc nem rendelődik hozzá egy személyhez.",
-    "machine_learning_settings": "Gépi Tanulási Beállítások",
-    "machine_learning_settings_description": "Gépi tanulási funkciók és beállítások kezelése",
-    "machine_learning_smart_search": "Okos Keresés",
-    "machine_learning_smart_search_description": "Képek szemantikai keresése CLIP beágyazások segítségével",
-    "machine_learning_smart_search_enabled": "Okos keresés engedélyezése",
-    "machine_learning_smart_search_enabled_description": "Ha ki van kapcsolva, a képek nem lesznek kódolva okos kereséshez.",
-    "machine_learning_url_description": "Gépi tanulás szerver URL-je",
-    "manage_concurrency": "Párhuzamos feladatok beállítása",
-    "manage_log_settings": "Naplózási beállítások kezelése",
-    "map_dark_style": "Sötét stílus",
-    "map_enable_description": "Térkép funkciók engedélyezése",
-    "map_gps_settings": "Térkép és GPS beállítások",
-    "map_gps_settings_description": "A térkép és a GPS (fordított geokódolás) beállításainak kezelése",
-    "map_implications": "A térkép szolgáltatás egy külső csempeszolgáltatót használ (tiles.immich.cloud)",
-    "map_light_style": "Világos stílus",
-    "map_manage_reverse_geocoding_settings": "A <link>fordított geokódolás</link> beállításainak kezelése",
-    "map_reverse_geocoding": "Fordított Geokódolás",
-    "map_reverse_geocoding_enable_description": "Fordított geokódolás engedélyezése",
-    "map_reverse_geocoding_settings": "Fordított Geokódolási Beállítások",
-    "map_settings": "Térkép",
-    "map_settings_description": "Térkép beállítások kezelése",
-    "map_style_description": "Egy style.json térképstílusra mutató URL",
-    "metadata_extraction_job": "Metaadatok feldolgozása",
-    "metadata_extraction_job_description": "Metaadat-információk kinyerése minden tartalomból, például GPS, arcok és felbontás",
-    "metadata_settings": "Metaadat beállítások",
-    "migration_job": "Migráció",
-    "migration_job_description": "Az képi vagyon és arcok bélyegképeinek migrálása a legújabb mappastruktúrába",
-    "no_paths_added": "Nincs megadva elérési útvonal",
-    "no_pattern_added": "Nincs megadva illesztési minta (pattern)",
-    "note_apply_storage_label_previous_assets": "Megjegyzés: Tárolási Cimkék már korábban feltöltött képi vagyonra ragasztásához futtasd a következőt -",
-    "note_cannot_be_changed_later": "FIGYELEM: ezt később nem lehet megváltoztatni!",
-    "note_unlimited_quota": "Megjegyzés: 0 - korlátlan kvóta",
-    "notification_email_from_address": "Feladó cím",
-    "notification_email_from_address_description": "Küldő email címe, például: \"Immich Fotószerver <noreply@immich.app>\"",
-    "notification_email_host_description": "Email szerver kiszolgálója (pl. smtp.immich.app)",
-    "notification_email_ignore_certificate_errors": "Tanúsítvány hibák figyelmen kívül hagyása",
-    "notification_email_ignore_certificate_errors_description": "TLS tanúsítvány érvényességi hibák figyelmen kívül hagyása (nem ajánlott)",
-    "notification_email_password_description": "Az email szerverrel való hitelesítéshez használt jelszó",
-    "notification_email_port_description": "Email szerver portja (pl. 25, 465 vagy 587)",
-    "notification_email_sent_test_email_button": "Teszt email küldése és mentés",
-    "notification_email_setting_description": "Email értesítés küldés beállításai",
-    "notification_email_test_email": "Küldj teszt e-mailt",
-    "notification_email_test_email_failed": "Nem sikerült elküldeni a teszt emailt, ellenőrizze az értékeit",
-    "notification_email_test_email_sent": "Egy teszt emailt elküldtünk a(z) {email} címre. Kérem, ellenőrizze a beérkező üzeneteket.",
-    "notification_email_username_description": "Az email szerverrel való hitelesítéshez használt felhasználónév",
-    "notification_enable_email_notifications": "Email értesítések engedélyezése",
-    "notification_settings": "Értesítés Beállítások",
-    "notification_settings_description": "Értesítési beállítások kezelése, beleértve az emailt",
-    "oauth_auto_launch": "Automatikus indítás",
-    "oauth_auto_launch_description": "Indítsa el automatikusan az OAuth bejelentkezési folyamatot, amikor a bejelentkezési oldalra navigál",
-    "oauth_auto_register": "Automatikus regisztráció",
-    "oauth_auto_register_description": "Új felhasználók automatikus regisztrálása az OAuth használatával történő bejelentkezés után",
-    "oauth_button_text": "Gomb szövege",
-    "oauth_client_id": "Kliens ID",
-    "oauth_client_secret": "Kliens Titok",
-    "oauth_enable_description": "Bejelentkezés OAuth-hal",
-    "oauth_issuer_url": "Kibocsátó URL",
-    "oauth_mobile_redirect_uri": "Mobil átirányítási URI",
-    "oauth_mobile_redirect_uri_override": "Mobil átirányítási URI felülírás",
-    "oauth_mobile_redirect_uri_override_description": "Engedélyezze, ha az 'app.immich:/' érvénytelen átirányítási URI.",
-    "oauth_profile_signing_algorithm": "Profil aláíró algoritmus",
-    "oauth_profile_signing_algorithm_description": "A felhasználói profil aláírásához használt algoritmus.",
-    "oauth_scope": "Hatókör",
-    "oauth_settings": "OAuth",
-    "oauth_settings_description": "OAuth bejelentkezési beállítások kezelése",
-    "oauth_settings_more_details": "Több információ erről a funkcióról elérhető a <link>dokumentációban</link>.",
-    "oauth_signing_algorithm": "Aláírás algoritmusa",
-    "oauth_storage_label_claim": "Tárolási címke igény",
-    "oauth_storage_label_claim_description": "A felhasználó tárolási címkéjének automatikus beállítása a követelés értékére.",
-    "oauth_storage_quota_claim": "Tárhelykvóta igénylés",
-    "oauth_storage_quota_claim_description": "A felhasználó tárhelykvótájának automatikus beállítása ennek a követelésnek az értékére.",
-    "oauth_storage_quota_default": "Alapértelmezett tárhelykvóta (GiB)",
-    "oauth_storage_quota_default_description": "Kvóta GiB-ben, amelyet akkor kell használni, ha nem nyújtanak be követelést (adjon meg 0-t a korlátlan kvótához).",
-    "offline_paths": "Offilne Útvonalak",
-    "offline_paths_description": "Ezek az eredmények olyan fájlok kézi törlésének tudhatók be, amelyek nem részei külső képtárnak.",
-    "password_enable_description": "Bejelentkezés emaillel és jelszóval",
-    "password_settings": "Jelszavas Bejelentkezés",
-    "password_settings_description": "Jelszavas bejelentkezés beállítások kezelése",
-    "paths_validated_successfully": "Összes útvonal sikeresen érvényesítve",
-    "quota_size_gib": "Kvóta Mérete (GiB)",
-    "refreshing_all_libraries": "Összes képtár újratöltése",
-    "registration": "Admin Regisztráció",
-    "registration_description": "Mivel ez az első felhasználó a rendszerben, ez a felhasználó lesz az Admin és lesz felelős adminisztratív teendőkért, illetve további felhasználókat ő tud létrehozni.",
-    "removing_offline_files": "Offline Fájlok eltávolítása",
-    "repair_all": "Összes Javítása",
-    "repair_matched_items": "{count, plural, one {# egyezés} other {# egyezés}}",
-    "repaired_items": "Javítva {count, plural, one {# fájl} other {# fájl}}",
-    "require_password_change_on_login": "Legyen kötelező a felhasználóknak az első bejelentkezéskor jelszót változtatni",
-    "reset_settings_to_default": "Beállítások visszaállítása az alapértelmezettre",
-    "reset_settings_to_recent_saved": "Beállítások visszaállítása a legutóbb mentettre",
-    "scanning_library_for_changed_files": "Képtár átfésülése megváltozott fájlok után",
-    "scanning_library_for_new_files": "Képtár átfésülése új fájlok után",
-    "send_welcome_email": "Üdvözlő email küldése",
-    "server_external_domain_settings": "Külső domain",
-    "server_external_domain_settings_description": "Nyilvánosan megosztott linkek domainje (http(s)://-sel)",
-    "server_settings": "Szerver Beállítások",
-    "server_settings_description": "Szerver beállítások kezelése",
-    "server_welcome_message": "Üdvözlő üzenet",
-    "server_welcome_message_description": "A bejelentkezőoldalon megjelenő üzenet.",
-    "sidecar_job": "Oldalkocsi fájl metaadatok",
-    "sidecar_job_description": "Fedezze fel vagy szinkronizálja az oldalkocsi fájlokban tárolt metaadatokat a fájlrendszerből",
-    "slideshow_duration_description": "Az egyes képek megjelenítésének ideje másodpercben",
-    "smart_search_job_description": "Futtasson gépi tanulást a képi vagyonon az intelligens keresés támogatása érdekében",
-    "storage_template_date_time_description": "A fájl készítési időpontja lesz felhasználva az időpont információhoz",
-    "storage_template_date_time_sample": "Példa időpont {date}",
-    "storage_template_enable_description": "Tárolási sablon motor engedélyezése",
-    "storage_template_hash_verification_enabled": "Hash ellenőrzés engedélyezve",
-    "storage_template_hash_verification_enabled_description": "Engedélyezi a hash-ellenőrzést - ne kapcsolja ki, csak ha tisztában van a következményekkel",
-    "storage_template_migration": "Tárolási sablon migrálása",
-    "storage_template_migration_description": "A jelenlegi <link>{template}</link> alkalmazása az ezelőtt feltöltött fájlokra",
-    "storage_template_migration_info": "A megváltozott sablon csak az újonnan feltöltött fájlokra lesz alkalmazva. A fájlok visszamenőleges megváltoztatásához futtatni kell a megfelelő munkát: <link>{job}</link>.",
-    "storage_template_migration_job": "Tárhely Sablon Migrációja",
-    "storage_template_more_details": "További információért erről a szolgáltatásról lásd <template-link>Tárolási Sablont</template-link> és az <implications-link>implikációkat</implications-link>",
-    "storage_template_onboarding_description": "Ez a funkció, ha be van kapcsolva, automatikusan rendszerezi a fájlokat egy felhasználó által megadott sablon alapján. Stabilitási problémák miatt a funkció alapértelmezés szerint ki van kapcsolva. További információkért tekintse meg a <link>dokumentációt</link>.",
-    "storage_template_path_length": "Út hozzávetőleges maximális hossza: <b>{length, number}</b>{limit, number}",
-    "storage_template_settings": "Tárolási sablon",
-    "storage_template_settings_description": "Kezelje a feltöltött képi vagyontárgyak mappaszerkezetét és fájlnevét",
-    "storage_template_user_label": "A felhasználó Tároló Címkéje <code>{label}</code>",
-    "system_settings": "Rendszerbeállítások",
-    "theme_custom_css_settings": "Egyedi CSS",
-    "theme_custom_css_settings_description": "CSS Stíluslapokkal az Immich stílusa megváltoztatható.",
-    "theme_settings": "Stílus Beállítások",
-    "theme_settings_description": "Kezelje az Immich webes felület testreszabását",
-    "these_files_matched_by_checksum": "Ezek a fájlok egyeznek az ellenőrző összegük alapján",
-    "thumbnail_generation_job": "Bélyegképek Generálása",
-    "thumbnail_generation_job_description": "Hozzon létre nagy, kicsi és elmosódott bélyegképeket minden egyes elemhez, valamint bélyegképeket minden egyes személyhez",
-    "transcode_policy_description": "",
-    "transcoding_acceleration_api": "Gyorsító API",
-    "transcoding_acceleration_api_description": "Az API, amely interakcióba lép az eszközzel az átkódolás felgyorsítása érdekében. Ez a beállítás a „legtöbb, amit megtehetünk” alapon működik: hiba esetén visszaáll a szoftveres átkódolásra. A VP9 a hardvertől függően vagy működik, vagy nem.",
-    "transcoding_acceleration_nvenc": "NVENC (NVIDIA GPU-t igényel)",
-    "transcoding_acceleration_qsv": "Gyors Szinkronizálás (7. generációs vagy újabb Intel CPU-t igényel)",
-    "transcoding_acceleration_rkmpp": "RKMPP (csak Rockchip SOC-kon)",
-    "transcoding_acceleration_vaapi": "VAAPI",
-    "transcoding_accepted_audio_codecs": "Elfogadott audio kodekek",
-    "transcoding_accepted_audio_codecs_description": "Válassza ki, mely audió kodekeket nem kell átkódolni. Csak bizonyos átkódolási szabályzatokhoz használatos.",
-    "transcoding_accepted_containers": "Elfogadott tárolók",
-    "transcoding_accepted_containers_description": "Válassza ki, hogy melyik tároló formátumokat nem szükséges átkódolni MP4 formátumba. Csak bizonyos átkódoló szabályok használják.",
-    "transcoding_accepted_video_codecs": "Elfogadott videó kodekek",
-    "transcoding_accepted_video_codecs_description": "Válassza ki, mely videó kodexeket nem kell átkódolni. Csak bizonyos átkódolási szabályzatokhoz használatos.",
-    "transcoding_advanced_options_description": "Ezeket az opciókat a legtöbb felhasználónak nem kell módosítania",
-    "transcoding_audio_codec": "Audio kodek",
-    "transcoding_audio_codec_description": "Az Opus a legjobb minőségű opció (jobb minőség ugyanannyi helyet foglalva), de kevésbé kompatibilis a régi eszközökkel vagy szoftverekkel.",
-    "transcoding_bitrate_description": "A maximum bitrátát meghaladó vagy nem megfelelő formátumú videókat",
-    "transcoding_codecs_learn_more": "Hogy többet tudjon meg az itt felhasznált kifejezésekről, látogassa meg az FFmpeg dokumentációt a <h264-link>H.264 kodekhez</h264-link>, a <hevc-link>HEVC kodekhez</hevc-link> és a <vp9-link>VP9 kodekhez</vp9-link>.",
-    "transcoding_constant_quality_mode": "Állandó minőségi mód",
-    "transcoding_constant_quality_mode_description": "Az ICQ jobb, mint a CQP, viszont nem minden hardver támogatja. A rendszer az itt beállított módszert részesíti előnyben. A NVENC ignorálja a beállítást, mivel nem támogatja az ICQ-t.",
-    "transcoding_constant_rate_factor": "Állandó ráta tényező (-crf)",
-    "transcoding_constant_rate_factor_description": "Videó minőségi szint. Jellemző értékek kodekenként: H.264: 23, HEVC: 28, VP9: 31, AV1: 35. Minél alacsonyabb, annál jobb minőséget eredményez, viszont nagyobb fájlmérettel is jár.",
-    "transcoding_disabled_description": "Ne transzkódoljon videót. Nem lejátszható videókhoz vezethet néhány kliensen",
-    "transcoding_hardware_acceleration": "Hardveres Gyorsítás",
-    "transcoding_hardware_acceleration_description": "Kísérleti funkció. Sokkal gyorsabb, viszont azonos bitrátán is alacsonyabb minőséghez vezet",
-    "transcoding_hardware_decoding": "Hardveres dekódolás",
-    "transcoding_hardware_decoding_setting_description": "Csak NVENC, QSV és RKMPP használja. Lehetővé teszi az egész folyamat gyorsítását ahelyett, hogy csak az átkódolást gyorsítsa. Nem biztos, hogy minden videó esetén működik.",
-    "transcoding_hevc_codec": "HEVC kodek",
-    "transcoding_max_b_frames": "B-képkockák maximum száma",
-    "transcoding_max_b_frames_description": "Nagyobb értékek megnővelik a tömörítési hatékonyságot, de lelassítják a kódolást. Nem minden hardvereszköz támogatja. 0 esetén kikapcsolja a B-képkockákat, -1 esetén a szoftver magának beállítja az értéket.",
-    "transcoding_max_bitrate": "Maximum bitráta",
-    "transcoding_max_bitrate_description": "Maximum bitráta beállítása konzisztensebb fájlméreteket eredményez alacsonyegy kevés minőségi romlás árán. 720p esetén jellemző érték lehet 2600k a VP9 vagy HEVC kódoláshoz, 4500k a H.264 kódoláshoz. 0 érték esetén nincs maximum bitráta.",
-    "transcoding_max_keyframe_interval": "Maximum kulcskocka intervallum",
-    "transcoding_max_keyframe_interval_description": "Beállítja a kulcskockák közötti legnagyobb lehetséges távolságot. Alacsony érték esetén csökken a tömörítési hatékonyság, de lejátszás közben az előre- és hátratekerés gyorsabb, valamint javíthatja gyors mozgás esetén a képminőséget. 0 esetén a szoftver magának beállítja az értéket.",
-    "transcoding_optimal_description": "A célfelbontást meghaladó vagy el nem fogadott formátumú videókat",
-    "transcoding_preferred_hardware_device": "Átkódoláshoz preferált hardver eszköz",
-    "transcoding_preferred_hardware_device_description": "Csak VAAPI vagy QSV esetén. Beállítja a hardveres transzkódoláshoz használt DRI node-ot.",
-    "transcoding_preset_preset": "Beállítás (-preset)",
-    "transcoding_preset_preset_description": "Tömörítési gyorsaság. Lassabb beállítások esetén kisebb fájlokat generál, valamint növeli a minőséget megcélzott bitráta esetén. A VP9 kódolás figyelmen kívül hagyja a 'faster (gyorsabb)'-nál gyorsabb beállításokat.",
-    "transcoding_reference_frames": "Referencia képkockák",
-    "transcoding_reference_frames_description": "Ennyi képkockára hivatkozzon egy képkocka tömörítéséhez. Magasabb értékek növelik a tömörítési hatékonyságot, de lelassítják a kódolási folyamatot. 0 esetén a szoftver magának beállítja az értéket.",
-    "transcoding_required_description": "Csak az el nem fogadott formátumú videókat",
-    "transcoding_settings": "Videó Transzkódolási Beállítások",
-    "transcoding_settings_description": "Videófájlok felbontásának és kódólásának kezelése",
-    "transcoding_target_resolution": "Célfelbontás",
-    "transcoding_target_resolution_description": "Magasabb felbontás jobb minőségben őrzi meg a részleteket, de tovább tart kódolni, nagyobb fájlmérethez vezet, és csökkentheti az alkalmazás teljesítményét.",
-    "transcoding_temporal_aq": "Időbeli (Temporal) AQ",
-    "transcoding_temporal_aq_description": "Csak NVENC esetén. Növeli a nagyon részletes, keveset mozgó videóanyag minőségét. Nem minden régi hardver támogatja.",
-    "transcoding_threads": "Folyamatok száma",
-    "transcoding_threads_description": "Magas értékek esetén gyorsabban kódol, viszont kevesebb erőforrást hagy a szerver többi funkciójának ellátására. Ez az érték nem kéne hogy meghaladja a CPU magjainak számát. 0 érték esetén maximalizálja a processzor kihasználását.",
-    "transcoding_tone_mapping": "Tónusleképezés (tone-mapping)",
-    "transcoding_tone_mapping_description": "Megpróbálja megőrizni a HDR videók kinézetét SDR-re való konvertálás során. Minden algoritmus különböző módon tesz kompromisszumot a színek, részletek, és világosság megőrzésében. A Hable inkább a részletek őrzi meg, a Mobius a színeket, a Reinhard pedig a világosságot.",
-    "transcoding_tone_mapping_npl": "Tónusleképezés NPL",
-    "transcoding_tone_mapping_npl_description": "A színek úgy lesznek beállítva, hogy ezen a fényerőn levő kijelzőn nézzenek ki jól. Alacsonyabb értékek esetén világosabb videót készít, és magasabb értékek esetén sötétebbet, mivel a kijelző fényerejéhez kompenzál. 0 esetén a szoftver magának beállítja az értéket.",
-    "transcoding_transcode_policy": "Transzkódolási szabályzat",
-    "transcoding_transcode_policy_description": "Mely videókat transzkódolja. HDR videók mindig transzkódolásra kerülnek (kivéve, ha a transzkódolás ki van kapcsolva).",
-    "transcoding_two_pass_encoding": "Enkódolás két menetben",
-    "transcoding_two_pass_encoding_setting_description": "Ha két menetben lettek transzkódolva, az elkészült videok jobbak. Ha engedélyezve van a bitráta maximalizálása (amely egyébként szükséges a H.264 és a HEVC használatakor), ez a funkció figyelmen kívül hagyja a CRF-et és a maximális bitráta alapján választja ki a megfelelő bitráta sávot. VP9 használata esetén CRF használható, ha a bitráta nincs maxmalizáva (ki van kapcsolva).",
-    "transcoding_video_codec": "Videó Kodek",
-    "transcoding_video_codec_description": "VP9 hatékonyabb és kompatibilisebb webre, de tovább tart a transzkódolás. HEVC hasonló teljesítményű, de több web kompatibilitási problémát okozhat. H.264 széles körben kompatibilis és gyors a transzkódolása, de sokkal nagyobb fájlokat készít. AV1 a leghatékonyabb kodek, de régebbi eszközök nem támogatják.",
-    "trash_enabled_description": "Lomtár engedélyezése",
-    "trash_number_of_days": "Napok száma",
-    "trash_number_of_days_description": "Hány napig legyenek a lomtárban tárolva a törölt képek, videok, mielőtt véglegesen kiürítődnek",
-    "trash_settings": "Lomtár Beállítások",
-    "trash_settings_description": "Lomtár beállítások kezelése",
-    "untracked_files": "Nem kezelt fájlok",
-    "untracked_files_description": "Ezekkel a fájlokkal semmit nem csinál az alkalmazás. Ez lehetséges pl. meghiúsult mozgatás, megszakított feltöltés miatt, vagy valamilyen alkalmazáshiba következtében",
-    "user_delete_delay": "<b>{user}</b> felhasználói fiókja és képi vagyona véglegesen törölve lesz {delay, plural, one {# nap} other {# nap}} múlva.",
-    "user_delete_delay_settings": "Törlési késleltetés",
-    "user_delete_delay_settings_description": "Ennyi nap teljen el az eltávolítás után a felhasználói fiók és ahhoz tartozó elemek végleges törlése között. A törlésért felelős folyamat éjfélkor indul, és megnézi van-e törlésre kész felhasználó. A beállítás változtatása a következő végrehajtás során lép életbe.",
-    "user_delete_immediately": "<b>{user}</b> felhasználója és fájljai sorbaállításra kerülnek végleges törléshez <b>azonnal</b>.",
-    "user_delete_immediately_checkbox": "Felhasználó és fájlok azonnali törlésre való sorbaállítása",
-    "user_management": "Felhasználók kezelése",
-    "user_password_has_been_reset": "A felhasználó jelszava megváltoztatásra került:",
-    "user_password_reset_description": "Juttassa el a felhasználóhoz az átmeneti jelszót, és tájékoztassa, hogy a következő belépésnél azt meg kell majd változtatnia.",
-    "user_restore_description": "<b>{user}</b> felhasználója vissza lesz állítva.",
-    "user_restore_scheduled_removal": "Felhasználó visszaállítása - törlésre fog kerülni: {date, date, long}",
-    "user_settings": "Felhasználó Beállítások",
-    "user_settings_description": "Felhasználó beállítások kezelése",
-    "user_successfully_removed": "{email} felhasználó sikeresen törlésre került.",
-    "version_check_enabled_description": "Új verziók elérhetőségének ellenőrzése",
-    "version_check_implications": "Az új verziók ellenőrzése szolgáltatás időszakos kommunikációt igényel a github.com oldallal",
-    "version_check_settings": "Verzió Ellenőrzés",
-    "version_check_settings_description": "Az új verzióról való értesítés be- és kikapcsolása",
-    "video_conversion_job": "Videók Átkódolása",
-    "video_conversion_job_description": "Videók transzkódolása böngészőkkel és eszközökkel való széleskörű kompatibilitás érdekében"
-  },
-  "admin_email": "Admin Email",
-  "admin_password": "Admin Jelszó",
-  "administration": "Adminisztráció",
-  "advanced": "Haladó",
-  "age_months": "Kor {months, plural, one {# month} other {# months}}",
-  "age_year_months": "Kor 1 év, {months, plural, one {# month} other {# months}}",
-  "age_years": "{years, plural, other {# éves}}",
-  "album_added": "Albumhoz hozzáadva",
-  "album_added_notification_setting_description": "Küldjön emailes értesítőt, amikor hozzáadnak egy megosztott albumhoz",
-  "album_cover_updated": "Album borító frissítve",
-  "album_delete_confirmation": "Biztos, hogy ki szeretné törölni a {album} albumot?",
-  "album_delete_confirmation_description": "Amennyiben ez egy megosztott album, a többi felhasználó sem fog tudni hozzáférni.",
-  "album_info_updated": "Album infó frissítve",
-  "album_leave": "Elhagyja az albumot?",
-  "album_leave_confirmation": "Biztos, hogy el szeretné hagyni a {album} albumot?",
-  "album_name": "Album Név",
-  "album_options": "Album beállítások",
-  "album_remove_user": "Felhasználó eltávolítása?",
-  "album_remove_user_confirmation": "Biztos, hogy el szeretné távolítani {user} felhasználót?",
-  "album_share_no_users": "Úgy tűnik, hogy minden felhasználóval megosztotta ezt az albumot, vagy nincs, akivel meg tudná osztani.",
-  "album_updated": "Album frissült",
-  "album_updated_setting_description": "Küldjön emailes értesítőt, amikor egy megosztott albumhoz új elemet adnak hozzá",
-  "album_user_left": "Elhagyta a {album} albumot",
-  "album_user_removed": "{user} eltávolítva",
-  "album_with_link_access": "Engedélyezze, hogy a link birtokában bárki láthatja a fotókat és a személyeket ebben az albumban.",
-  "albums": "Albumok",
-  "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Album}}",
-  "all": "Összes",
-  "all_albums": "Összes album",
-  "all_people": "Minden személy",
-  "all_videos": "Összes videó",
-  "allow_dark_mode": "Sötét stílus engedélyezése",
-  "allow_edits": "Szerkesztések engedélyezése",
-  "allow_public_user_to_download": "Engedélyezze publikus felhasználónak, hogy letöltse",
-  "allow_public_user_to_upload": "Engedélyezze a feltöltést publikus felhasználónak",
-  "anti_clockwise": "Óramutató járásával ellentétes irány",
-  "api_key": "API kulcs",
-  "api_key_description": "Ez az érték csak egyszer jelenik meg. Az ablak bezárása előtt feltétlenül másolja át.",
-  "api_key_empty": "A te API Kulcs neved nem kéne üres legyen",
-  "api_keys": "API Kulcsok",
-  "app_settings": "Alkalmazás Beállítások",
-  "appears_in": "Megjelenik itt",
-  "archive": "Archívum",
-  "archive_or_unarchive_photo": "Fotó archiválása vagy archiválásának visszavonása",
-  "archive_size": "Archívum mérete",
-  "archive_size_description": "Beállítja letöltésnél az archívum méretét (GiB)",
-  "archived": "Archíválva",
-  "archived_count": "{count, plural, other {Archived #}}",
-  "are_these_the_same_person": "Ugyanaz a személy?",
-  "are_you_sure_to_do_this": "Biztosan ezt akarod csinálni?",
-  "asset_added_to_album": "Hozzáadva az albumhoz",
-  "asset_adding_to_album": "Hozzáadás az albumhoz...",
-  "asset_description_updated": "A leírás frissült",
-  "asset_filename_is_offline": "A(z) {filename} elem offline állapotban van",
-  "asset_has_unassigned_faces": "Az elemnek hozzá nem rendelt arcai vannak",
-  "asset_hashing": "Hash számítása...",
-  "asset_offline": "Elem offline",
-  "asset_offline_description": "Ez az elem nem elérhető. Immich nem képes elérni a file helyét. Győződjön meg az elem elérhetőségéről és szkennelje újra a könyvtárat.",
-  "asset_skipped": "Kihagyva",
-  "asset_uploaded": "Feltöltve",
-  "asset_uploading": "Feltöltés...",
-  "assets": "elemek",
-  "assets_added_count": "{count, plural, other {# elem}} hozzáadva",
-  "assets_added_to_album_count": "{count, plural, other {# elem}} hozzáadva az albumhoz",
-  "assets_added_to_name_count": "{count, plural, other {# elem}} hozzáadva a(z) {hasName, select, true {<b>{name}</b>} other {új}} albumba",
-  "assets_count": "{count, plural, other {# elem}}",
-  "assets_moved_to_trash": "{count, plural, one {# fájl} other {# fájl}} a lomtárba mozgatva",
-  "assets_moved_to_trash_count": "{count, plural, other {# elem}} szemétbe mozgatva",
-  "assets_permanently_deleted_count": "{count, plural, other {# elem}} örökre törölve",
-  "assets_removed_count": "{count, plural, other {# elem}} eltávolítva",
-  "assets_restore_confirmation": "Biztosan visszaállítja a lomtárbeli elemeket? Ez a művelet nem visszavonható!",
-  "assets_restored_count": "{count, plural, other {# elem}} visszaállítva",
-  "assets_trashed_count": "{count, plural, other {# elem}} kidobva",
-  "assets_were_part_of_album_count": "{count, plural, other {# elem}} már az album része volt",
-  "authorized_devices": "Engedélyezett készülékek",
-  "back": "Vissza",
-  "back_close_deselect": "Vissza, bezárás, vagy kijelölés törlése",
-  "backward": "Visszafele",
-  "birthdate_saved": "Születésnap elmentve",
-  "birthdate_set_description": "A születés napját a rendszer annak kijelzésére használja, hogy a fénykép készítésének idejében az illető hány éves volt.",
-  "blurred_background": "Homályos háttér",
-  "build": "Építés",
-  "build_image": "Kép építése",
-  "bulk_delete_duplicates_confirmation": "Biztosan kitöröl {count, plural, one {# duplikált fájlt} other {# duplikált fájlt}}? A művelet során minden hasonló fájlcsoportból a legnagyobb méretű fájlt megtartja, minden másik duplikált fájlt kitörli. Ez a művelet nem visszavonható!",
-  "bulk_keep_duplicates_confirmation": "Biztosan meg szeretne tartani {count, plural, other {# egyező elemet}}? Ez felold minden duplikátum csoportot elemek törlése nélkül.",
-  "bulk_trash_duplicates_confirmation": "Biztosan kitöröl {count, plural, one {# duplikált fájlt} other {# duplikált fájlt}}? Ez a művelet megtartja minden fájlcsoportból a legnagyobb méretű elemet, és kitörli minden másik duplikáltat.",
-  "buy": "Immich megvásárlása",
-  "camera": "Fényképezőgép",
-  "camera_brand": "Fényképezőgép márka",
-  "camera_model": "Fényképezőgép modell",
-  "cancel": "Mégsem",
-  "cancel_search": "Keresés visszavonása",
-  "cannot_merge_people": "Személyek összevonása nem lehetséges",
-  "cannot_undo_this_action": "Ez a művelet nem visszavonható!",
-  "cannot_update_the_description": "A leírás megváltoztatása nem lehetséges",
-  "cant_apply_changes": "A változtatások nem alkalmazhatóak",
-  "cant_get_faces": "Az arcok nem elérhetőek",
-  "cant_search_people": "",
-  "cant_search_places": "A helyek nem kereshetőek",
-  "change_date": "Dátum változtatása",
-  "change_expiration_time": "Lejárati idő megváltoztatása",
-  "change_location": "Helyszín változtatása",
-  "change_name": "Név változtatása",
-  "change_name_successfully": "A név megváltoztatása sikeres",
-  "change_password": "Jelszócsere",
-  "change_password_description": "Most jelentkezik be a rendszerbe első alkalommal, vagy valaki jelszóváltoztatást kezdeményezett. Kérem, írjon be új jelszót.",
-  "change_your_password": "Jelszó megváltoztatása",
-  "changed_visibility_successfully": "Láthatóság sikeresen megváltoztatva",
-  "check_all": "Jelenleg nincs használatban (v1.106.4)",
-  "check_logs": "Hibajegyzék",
-  "choose_matching_people_to_merge": "Válassza ki a megegyező személyeket összevonásra",
-  "city": "Város",
-  "clear": "Kitöröl",
-  "clear_all": "Alaphelyzet",
-  "clear_all_recent_searches": "Legutóbbi keresések törlése",
-  "clear_message": "Üzenet törlése",
-  "clear_value": "Érték törlése",
-  "clockwise": "Óramutató járásával megegyező irány",
-  "close": "Bezárás",
-  "collapse": "Összecsuk",
-  "collapse_all": "Mindet összecsuk",
-  "color": "Szín",
-  "color_theme": "Szín stílus",
-  "comment_deleted": "Megjegyzés törölve",
-  "comment_options": "Megjegyzés beállítások",
-  "comments_and_likes": "Megjegyzések és reakciók",
-  "comments_are_disabled": "A megjegyzések le vannak tiltva",
-  "confirm": "Jóváhagy",
-  "confirm_admin_password": "Admin jelszó újból",
-  "confirm_delete_shared_link": "Biztosan törölni szeretné ezt a megosztott linket?",
-  "confirm_password": "Jelszó megerősítése",
-  "contain": "Belül",
-  "context": "Környezet",
-  "continue": "Folytatás",
-  "copied_image_to_clipboard": "Kép másolva a vágólapra.",
-  "copied_to_clipboard": "Vágólapra másolva!",
-  "copy_error": "Másolási hiba",
-  "copy_file_path": "Fájlútvonal másolása",
-  "copy_image": "Kép másolása",
-  "copy_link": "Link másolása",
-  "copy_link_to_clipboard": "Link másolása a vágólapra",
-  "copy_password": "Jelszó másolása",
-  "copy_to_clipboard": "Másolás a vágólapra",
-  "country": "Ország",
-  "cover": "Borító",
-  "covers": "Borítók",
-  "create": "Létrehoz",
-  "create_album": "Album készítése",
-  "create_library": "Képtár Létrehozása",
-  "create_link": "Link készítése",
-  "create_link_to_share": "Megosztási link létrehozása",
-  "create_link_to_share_description": "A kiválasztott fotókat mindenki láthassa, aki a linket használja",
-  "create_new_person": "Új személy létrehozása",
-  "create_new_person_hint": "A kiválasztott képekhez új személyt rendel hozzá",
-  "create_new_user": "Új felhasználó létrehozása",
-  "create_user": "Felhasználó létrehozása",
-  "created": "Készült",
-  "current_device": "Ez az eszköz",
-  "custom_locale": "Egyéni területi beállítás",
-  "custom_locale_description": "Dátumok és számok formázása a nyelv és terület szerint",
-  "dark": "Sötét",
-  "date_after": "Dátum utána",
-  "date_and_time": "Dátum és Idő",
-  "date_before": "Dátum előtte",
-  "date_of_birth_saved": "Születésnap elmentve",
-  "date_range": "Dátum intervallum",
-  "day": "Nap",
-  "deduplicate_all": "Az összes deduplikálása",
-  "default_locale": "Alapértelmezett területi beállítás",
-  "default_locale_description": "Dátumok és számok formázása a bőngésző területi beállítása alapján",
-  "delete": "Törlés",
-  "delete_album": "Album törlése",
-  "delete_api_key_prompt": "Biztosan törölni szeretné ezt az API kulcsot?",
-  "delete_duplicates_confirmation": "Biztosan véglegesen törölni szeretné ezeket a másolatokat?",
-  "delete_key": "Kulcs törlése",
-  "delete_library": "Képtár törlése",
-  "delete_link": "Link törlése",
-  "delete_shared_link": "Megosztott link törlése",
-  "delete_user": "Felhasználó törlése",
-  "deleted_shared_link": "Törölt megosztott link",
-  "description": "Leírás",
-  "details": "Részletek",
-  "direction": "Irány",
-  "disabled": "Letiltott",
-  "disallow_edits": "Módosítások letiltása",
-  "discover": "Felfedezés",
-  "dismiss_all_errors": "Minden hiba elvetése",
-  "dismiss_error": "Hiba elvetése",
-  "display_options": "Megjelenítési beállítások",
-  "display_order": "Megjelenítési sorrend",
-  "display_original_photos": "Eredeti fotók megjelenítése",
-  "display_original_photos_setting_description": "Egy kép nézegetése közben jelenítse meg inkább az eredeti képet a bélyegkép helyett, amennyiben az is web-kompatibilis. Ez lelassíthatja a fotók megjelenítését.",
-  "do_not_show_again": "Ne mutassa többet ezt az üzenetet",
-  "done": "Kész",
-  "download": "Letöltés",
-  "download_include_embedded_motion_videos": "Beágyazott videók",
-  "download_include_embedded_motion_videos_description": "Mozgó képekbe beágyazott videók mutatása külön fájlként",
-  "download_settings": "Letöltés",
-  "download_settings_description": "Képi vagyontárgyak letöltésére vonatkozó beállítások",
-  "downloading": "Letöltés",
-  "downloading_asset_filename": "Fájl letöltése {filename}",
-  "drop_files_to_upload": "Húzza a fájlokat bárhova a feltöltéshez",
-  "duplicates": "Duplikátumok",
-  "duplicates_description": "Oldja fel a csoportokat a (ha léteznek) duplukátumok megjelölésével",
-  "duration": "Időtartam",
-  "durations": {
-    "days": "{days, plural, one {nap} other {{days, number} nap}}",
-    "hours": "{hours, plural, one {óra} other {{hours, number} óra}}",
-    "minutes": "{minutes, plural, one {perc} other {{minutes, number} perc}}",
-    "months": "{months, plural, one {hónap} other {{months, number} hónap}}",
-    "years": "{years, plural, one {év} other {{years, number} év}}"
-  },
-  "edit": "Szerkesztés",
-  "edit_album": "Album szerkesztése",
-  "edit_avatar": "Avatar szerkesztése",
-  "edit_date": "Dátum szerkesztése",
-  "edit_date_and_time": "Dátum és idő szerkesztése",
-  "edit_exclusion_pattern": "Kizárási minta szerkesztése",
-  "edit_faces": "Arcok szerkesztése",
-  "edit_import_path": "Importálási útvonal szerkesztése",
-  "edit_import_paths": "Importálási útvonalak szerkesztése",
-  "edit_key": "Kulcs szerkesztése",
-  "edit_link": "Link módosítása",
-  "edit_location": "Hely módosítása",
-  "edit_name": "Név módosítása",
-  "edit_people": "Személyek módosítása",
-  "edit_title": "Cím Módosítása",
-  "edit_user": "Felhasználó módosítása",
-  "edited": "Módosítva",
-  "editor": "Szerkesztő",
-  "editor_close_without_save_prompt": "A változtatások nem lesznek mentve",
-  "editor_close_without_save_title": "Szerkesztő bezárása?",
-  "editor_crop_tool_h2_aspect_ratios": "Oldalarányok",
-  "editor_crop_tool_h2_rotation": "Forgatás",
-  "email": "Email",
-  "empty": "",
-  "empty_album": "Üres Album",
-  "empty_trash": "Lomtár Ürítése",
-  "empty_trash_confirmation": "Biztosan kiüríti a lomtárat? Ezzel minden lomtárbeli fájlt véglegesen letöröl az Immich szolgáltatásból.\nEz a művelet nem visszavonható!",
-  "enable": "Engedélyezés",
-  "enabled": "Engedélyezve",
-  "end_date": "Vég dátum",
-  "error": "Hiba",
-  "error_loading_image": "Hiba a kép betöltése közben",
-  "error_title": "Hiba - valami félresikerült",
-  "errors": {
-    "cannot_navigate_next_asset": "Nem lehet a következő elemhez navigálni",
-    "cannot_navigate_previous_asset": "Nem lehet az előző elemhez navigálni",
-    "cant_apply_changes": "Nem lehet alkalmazni a változtatásokat",
-    "cant_change_activity": "Nem lehet {enabled, select, true {engedélyezni} other {kikapcsolni}} tevékenységet",
-    "cant_change_asset_favorite": "Nem lehet a kedvenc állapotot megváltoztatni ehhez az elemhez",
-    "cant_change_metadata_assets_count": "Nem lehet {count, plural, other {# elem}} metaadatát megváltoztatni",
-    "cant_get_faces": "Arcok lekérdezése sikertelen",
-    "cant_get_number_of_comments": "Hozzászólások számának lekérdezése sikertelen",
-    "cant_search_people": "Emberek keresése sikertelen",
-    "cant_search_places": "Helyek keresése sikertelen",
-    "cleared_jobs": "A {job} munkák törölve",
-    "error_adding_assets_to_album": "Hiba történt az elemek albumhoz való hozzáadása során",
-    "error_adding_users_to_album": "Hiba történt a felhasználók albumhoz való hozzáadása során",
-    "error_deleting_shared_user": "Hiba történt megosztott felhasználó törlése során",
-    "error_downloading": "{filename} letöltése sikertelen",
-    "error_hiding_buy_button": "Hiba történt a megvásárlás gomb elrejtése során",
-    "error_removing_assets_from_album": "Hiba történt az elemek albumból való eltávolítása során, további információért ellenőrizze a logokat",
-    "error_selecting_all_assets": "Minden elem kijelölése közben hiba lépett fel",
-    "exclusion_pattern_already_exists": "Ez a kizárási minta már létezik.",
-    "failed_job_command": "Parancs {command} hibával zárult a {job} munkában",
-    "failed_to_create_album": "Album készítése sikertelen",
-    "failed_to_create_shared_link": "Megosztott link készítése sikertelen",
-    "failed_to_edit_shared_link": "Megosztott link szerkesztése sikertelen",
-    "failed_to_get_people": "Emberek lekérdezése sikertelen",
-    "failed_to_load_asset": "Elem betöltése sikertelen",
-    "failed_to_load_assets": "Elemek betöltése sikertelen",
-    "failed_to_load_people": "Emberek betöltése sikertelen",
-    "failed_to_remove_product_key": "Termékkulcs eltávolítása sikertelen",
-    "failed_to_stack_assets": "Elemek csoportosítása sikertelen",
-    "failed_to_unstack_assets": "Elemek szétszedése sikertelen",
-    "import_path_already_exists": "Ez az importálási útvonal már létezik.",
-    "incorrect_email_or_password": "Helytelen e-mail vagy jelszó",
-    "paths_validation_failed": "Sikertelen érvényesítés {paths, plural, one {# elérési útvonalon} other {# elérési útvonalon}}",
-    "profile_picture_transparent_pixels": "Profilképek nem tartalmazhatnak átlátszó pixeleket. Közelítsen rá és/vagy mozgassa a képet.",
-    "quota_higher_than_disk_size": "Az elérhető háttértárnál nagyobb kvótát állított be",
-    "repair_unable_to_check_items": "Nem sikerült {count, select, one {element} other {elemeket}} ellenőrizni",
-    "unable_to_add_album_users": "Felhasználók hozzáadása albumhoz sikertelen",
-    "unable_to_add_assets_to_shared_link": "Felhasználók hozzáadása megosztott linkhez sikertelen",
-    "unable_to_add_comment": "Hozzászólás sikertelen",
-    "unable_to_add_exclusion_pattern": "Kivétel minta hozzáadása sikertelen",
-    "unable_to_add_import_path": "Importálási útvonal hozzáadása sikertelen",
-    "unable_to_add_partners": "Partnerek hozzáadása sikertelen",
-    "unable_to_add_remove_archive": "Elem {archived, select, true {eltávolítása archívumból} other {hozzáadása archívumba}} sikertelen",
-    "unable_to_add_remove_favorites": "Elem {favorite, select, true {eltávolítása kedvencekből} other {hozzáadása kedvencekhez}} sikertelen",
-    "unable_to_archive_unarchive": "Elem {archived, select, true {archiválása} other {kivétele archívumból}} sikertelen",
-    "unable_to_change_album_user_role": "Album tagjának szerepének megváltoztatása sikertelen",
-    "unable_to_change_date": "Dátum megváltoztatása sikertelen",
-    "unable_to_change_favorite": "Kedvenc állapot megváltoztatása sikertelen",
-    "unable_to_change_location": "Hely megváltoztatása sikertelen",
-    "unable_to_change_password": "Jelszó megváltoztatása sikertelen",
-    "unable_to_change_visibility": "{count, plural, other {# ember}} láthatóságának a megváltoztatása sikertelen",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
-    "unable_to_complete_oauth_login": "OAuth bejelentkezés sikertelen",
-    "unable_to_connect": "Csatlakozás sikertelen",
-    "unable_to_connect_to_server": "Szerverhez való csatlakozás sikertelen",
-    "unable_to_copy_to_clipboard": "Vágólapra másolás sikertelen. Ellenőrizze, hogy a kapcsolat https-en keresztül történik",
-    "unable_to_create_admin_account": "Admin felhasználó létrehozása sikertelen",
-    "unable_to_create_api_key": "Új API kulcs létrehozása sikertelen",
-    "unable_to_create_library": "Könyvtár létrehozása sikertelen",
-    "unable_to_create_user": "Felhasználó létrehozása sikertelen",
-    "unable_to_delete_album": "Album törlése sikertelen",
-    "unable_to_delete_asset": "Elem törlése sikertelen",
-    "unable_to_delete_assets": "Hiba történt az elemek törlésekor",
-    "unable_to_delete_exclusion_pattern": "Kizárási minta törlése sikertelen",
-    "unable_to_delete_import_path": "Import útvonal törlése sikertelen",
-    "unable_to_delete_shared_link": "Megosztott link törlése sikertelen",
-    "unable_to_delete_user": "Nem sikerült törölni a felhasználót",
-    "unable_to_download_files": "Fájlok letöltése sikertelen",
-    "unable_to_edit_exclusion_pattern": "Kizárási minta szerkesztése sikertelen",
-    "unable_to_edit_import_path": "Import útvonal szerkesztése sikertelen",
-    "unable_to_empty_trash": "Nem sikerült a lomtár ürítése",
-    "unable_to_enter_fullscreen": "Nem lehet belépni a teljes képernyőre",
-    "unable_to_exit_fullscreen": "Nem lehet kilépni a teljes képernyőről",
-    "unable_to_get_comments_number": "Hozzászólások számának lekérdezése sikertelen",
-    "unable_to_get_shared_link": "Megosztott link lekérdezése sikertelen",
-    "unable_to_hide_person": "Személy elrejtése sikertelen",
-    "unable_to_link_oauth_account": "OAuth felhasználó csatlakoztatása sikertelen",
-    "unable_to_load_album": "Album betöltése sikertelen",
-    "unable_to_load_asset_activity": "Elem aktivitásának betöltése sikertelen",
-    "unable_to_load_items": "Elemek betöltése sikertelen",
-    "unable_to_load_liked_status": "Tetszik állapot betöltése sikertelen",
-    "unable_to_log_out_all_devices": "Minden eszközből való kijelentkeztetés sikertelen",
-    "unable_to_log_out_device": "Sikertelen kijelentkezés",
-    "unable_to_login_with_oauth": "Sikertelen bejelentkezés OAuth-tal",
-    "unable_to_play_video": "Videó lejátszása sikertelen",
-    "unable_to_reassign_assets_existing_person": "Nem sikerült az elemeket áthelyezni {name, select, null {egy létező személyhez} other {hozzá: {name}}}",
-    "unable_to_reassign_assets_new_person": "Elemek áthelyezése új személyhez sikertelen",
-    "unable_to_refresh_user": "Felhasználó újratöltése sikertelen",
-    "unable_to_remove_album_users": "Felhasználó albumból való eltávolítása sikertelen",
-    "unable_to_remove_api_key": "API kulcs eltávolítása sikertelen",
-    "unable_to_remove_assets_from_shared_link": "Elemek eltávolítása megosztott linkből sikertelen",
-    "unable_to_remove_comment": "",
-    "unable_to_remove_library": "Könyvtár törlése sikertelen",
-    "unable_to_remove_offline_files": "Offline fájlok törlése sikertelen",
-    "unable_to_remove_partner": "Partner eltávolítása sikertelen",
-    "unable_to_remove_reaction": "Reakció eltávolítása sikertelen",
-    "unable_to_remove_user": "",
-    "unable_to_repair_items": "Elemek javítása sikertelen",
-    "unable_to_reset_password": "Jelszó visszaállítása sikertelen",
-    "unable_to_resolve_duplicate": "Duplikátum feloldása sikertelen",
-    "unable_to_restore_assets": "Elemek szemeteskosárból való visszaállítása sikertelen",
-    "unable_to_restore_trash": "Nem sikerült a lomtár visszaállítása",
-    "unable_to_restore_user": "Felhasználó visszaállítása sikertelen",
-    "unable_to_save_album": "Album mentése sikertelen",
-    "unable_to_save_api_key": "API kulcs mentése sikertelen",
-    "unable_to_save_date_of_birth": "Születési időpont mentése sikertelen",
-    "unable_to_save_name": "Név mentése sikertelen",
-    "unable_to_save_profile": "Profil mentése sikertelen",
-    "unable_to_save_settings": "Beállítások mentése sikertelen",
-    "unable_to_scan_libraries": "Könyvtárak ellenőrzése sikertelen",
-    "unable_to_scan_library": "Könyvtár ellenőrzése sikertelen",
-    "unable_to_set_feature_photo": "Kijelölt fénykép beállítása sikertelen",
-    "unable_to_set_profile_picture": "Profilkép beállítása sikertelen",
-    "unable_to_submit_job": "Nem sikerült a profilt elmenteni",
-    "unable_to_trash_asset": "Nem sikerült a fájl lomtárba mozgatása",
-    "unable_to_unlink_account": "Nem sikerült a fiók lekapcsolása",
-    "unable_to_update_album_cover": "Albumborító beállítása sikertelen",
-    "unable_to_update_album_info": "Album információ frissítése sikertelen",
-    "unable_to_update_library": "Nem sikerült a képtár módosítása",
-    "unable_to_update_location": "Nem sikerült az elérés módosítása",
-    "unable_to_update_settings": "Nem sikerült a beállítások módosítása",
-    "unable_to_update_timeline_display_status": "Nem sikerült az idővonal kijelzési státuszának módosítása",
-    "unable_to_update_user": "Nem sikerült a felhasználó módosítása",
-    "unable_to_upload_file": "Fájlfeltöltés sikertelen"
-  },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
-  "exif": "Exif",
-  "exit_slideshow": "Kilépés a diavetítésből",
-  "expand_all": "Minden kinyitása",
-  "expire_after": "Lejárati idő",
-  "expired": "Lejárt",
-  "expires_date": "Lejár {date}",
-  "explore": "Felfedezés",
-  "export": "Exportálás",
-  "export_as_json": "Exportálás JSON formátumban",
-  "extension": "Kiterjesztés",
-  "external": "Külső",
-  "external_libraries": "Külső Képtárak",
-  "face_unassigned": "Nincs hozzárendelve",
-  "failed_to_get_people": "Személyek lekérése sikertelen",
-  "favorite": "Kedvenc",
-  "favorite_or_unfavorite_photo": "Fotó kedvencnek jelölése vagy annak visszavonása",
-  "favorites": "Kedvencek",
-  "feature": "",
-  "feature_photo_updated": "Címlapkép frissítve",
-  "featurecollection": "",
-  "file_name": "Fájlnév",
-  "file_name_or_extension": "Fájlnév vagy kiterjesztés",
-  "filename": "Fájlnév",
-  "files": "",
-  "filetype": "Fájltípus",
-  "filter_people": "Személyek szűrése",
-  "find_them_fast": "Kereséssel gyorsan megtalálhatóak név alapján",
-  "fix_incorrect_match": "Hibás találat korrigálása",
-  "force_re-scan_library_files": "Az összes Képtár fájl újbóli átfésülésének indítása",
-  "forward": "Előre",
-  "general": "Általános",
-  "get_help": "Segítségkérés",
-  "getting_started": "Kezdő Lépések",
-  "go_back": "Visszalépés",
-  "go_to_search": "Ugrás a kereséshez",
-  "go_to_share_page": "Ugrás a megosztás oldalhoz",
-  "group_albums_by": "Albumok csoportosítása...",
-  "group_no": "Nincs csoportosítás",
-  "group_owner": "Csoportosítás tulajdonosonként",
-  "group_year": "Csoportosítás évenként",
-  "has_quota": "Van kvótája",
-  "hi_user": "Helló {name} ({email})",
-  "hide_all_people": "Minden személy elrejtése",
-  "hide_gallery": "Galéria elrejtése",
-  "hide_named_person": "Személy {name} elrejtése",
-  "hide_password": "Jelszó elrejtése",
-  "hide_person": "Személy elrejtése",
-  "hide_unnamed_people": "Megnevezetlen emberek elrejtése",
-  "host": "",
-  "hour": "Óra",
-  "image": "Kép",
-  "image_alt_text_date": "{isVideo, select, true {Videó} other {Kép}} készítési dátuma {date}",
-  "image_alt_text_date_1_person": "{isVideo, select, true {Videó} other {Kép}} vele: {person1} készítve {date}",
-  "image_alt_text_date_2_people": "{isVideo, select, true {Videó} other {Kép}} velük: {person1} és {person2}, ekkor: {date}",
-  "image_alt_text_date_3_people": "{isVideo, select, true {Videó} other {Kép}} velük: {person1}, {person2} és {person3} ekkor: {date}",
-  "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Videó} other {Kép}} velük: {person1}, {person2} és {additionalCount, number} más ekkor: {date}",
-  "image_alt_text_date_place": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city}, ekkor: {date}",
-  "image_alt_text_date_place_1_person": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city}, vele: {person1}, ekkor: {date}",
-  "image_alt_text_date_place_2_people": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city}, velük: {person1} és {person2}, ekkor: {date}",
-  "image_alt_text_date_place_3_people": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city}, velük: {person1}, {person2} és {person3}, ekkor: {date}",
-  "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city}, velük: {person1}, {person2} és {additionalCount, number} más, ekkor: {date}",
-  "img": "",
-  "immich_logo": "Immich Logó",
-  "immich_web_interface": "Immich web felület",
-  "import_from_json": "Importálás JSON formátumból",
-  "import_path": "Importálási útvonal",
-  "in_albums": "{count, plural, one {# albumban} other {# albumban}}",
-  "in_archive": "Archívumban",
-  "include_archived": "Archívokkal Együtt",
-  "include_shared_albums": "Megosztott albumokkal együtt",
-  "include_shared_partner_assets": "Partner által megosztott képekkel együtt",
-  "individual_share": "Egyéni megosztás",
-  "info": "Infó",
-  "interval": {
-    "day_at_onepm": "Minden nap 13 órakor",
-    "hours": "{hours, plural, one {óránként} other {{hours, number} óránként}}",
-    "night_at_midnight": "Minden éjjel éjfélkor",
-    "night_at_twoam": "Minden éjjel 2 órakor"
-  },
-  "invite_people": "Személyek Meghívása",
-  "invite_to_album": "Meghívás az albumba",
-  "items_count": "{count, plural, other {# elem}}",
-  "job_settings_description": "",
-  "jobs": "Feladatok",
-  "keep": "Megtartás",
-  "keep_all": "Összes megtartása",
-  "keyboard_shortcuts": "Billentyűparancsok",
-  "language": "Nyelv",
-  "language_setting_description": "Válassza ki preferált nyelvét",
-  "last_seen": "Utoljára látva",
-  "latest_version": "Legfrissebb verzió",
-  "latitude": "Szélesség",
-  "leave": "Elhagyás",
-  "let_others_respond": "Engedd, hogy mások reagáljanak",
-  "level": "Szint",
-  "library": "Képtár",
-  "library_options": "Képtár beállítások",
-  "light": "Világos",
-  "like_deleted": "Tetszik törölve",
-  "link_options": "Link beállítások",
-  "link_to_oauth": "Csatlakoztatás OAuth-hoz",
-  "linked_oauth_account": "Csatlakoztatott OAuth felhasználó",
-  "list": "Lista",
-  "loading": "Betöltés",
-  "loading_search_results_failed": "Keresési eredmények betöltése sikertelen",
-  "log_out": "Kijelentkezés",
-  "log_out_all_devices": "Összes Eszköz Kijelentkeztetése",
-  "logged_out_all_devices": "Az összes eszköz kijelentkeztetve",
-  "logged_out_device": "Eszköz kijelentkeztetve",
-  "login": "Bejelentkezés",
-  "login_has_been_disabled": "Bejelentkezés le van tiltva.",
-  "logout_all_device_confirmation": "Biztos, hogy minden eszközből szeretne kijelentkezni?",
-  "logout_this_device_confirmation": "Biztos, hogy szeretne kijelentkezni ebből az eszközből?",
-  "longitude": "Hosszúság",
-  "look": "Kinézet",
-  "loop_videos": "Videók ismétlése",
-  "loop_videos_description": "Engedélyezi a videók folyamatosan ismételt lejátszását az elem megjelenítőben.",
-  "make": "Gyártó",
-  "manage_shared_links": "Megosztási linkek kezelése",
-  "manage_sharing_with_partners": "Társakkal való megosztás kezelése",
-  "manage_the_app_settings": "Alkalmazás beállításainak kezelése",
-  "manage_your_account": "Saját fiók kezelése",
-  "manage_your_api_keys": "Saját API kulcsok kezelése",
-  "manage_your_devices": "Engedélyezett készülékek kezelése",
-  "manage_your_oauth_connection": "OAuth kapcsolat kezelése",
-  "map": "Térkép",
-  "map_marker_for_images": "Térképjelölő a képekhez itt készült: {country}, {city}",
-  "map_marker_with_image": "Térképjelölő képpel",
-  "map_settings": "Térkép beállítások",
-  "matches": "Megegyezések",
-  "media_type": "Médiatípus",
-  "memories": "Emlékek",
-  "memories_setting_description": "Emlékek tartalmának kezelése",
-  "memory": "Emlék",
-  "memory_lane_title": "Emlékek {title}",
-  "menu": "Menü",
-  "merge": "Összevonás",
-  "merge_people": "Személyek összevonása",
-  "merge_people_limit": "Egyszerre legfeljebb 5 arcot vonhatsz össze",
-  "merge_people_prompt": "Biztosan összevonod ezeket a személyeket? Ez a művelet nem visszavonható.",
-  "merge_people_successfully": "Személyek sikeresen egyesítve",
-  "merged_people_count": "{count, plural, other {# személy}} egyesítve",
-  "minimize": "Lekicsinítés",
-  "minute": "Perc",
-  "missing": "Hiányzó",
-  "model": "Modell",
-  "month": "Hónap",
-  "more": "Több",
-  "moved_to_trash": "Lomtárba mozgatva",
-  "my_albums": "Albumaim",
-  "name": "Név",
-  "name_or_nickname": "Név vagy becenév",
-  "never": "Soha",
-  "new_album": "Új album",
-  "new_api_key": "Új API Kulcs",
-  "new_password": "Új jelszó",
-  "new_person": "Új személy",
-  "new_user_created": "Új felhasználó létrehozva",
-  "new_version_available": "ÚJ VERZIÓ ELÉRHETŐ",
-  "newest_first": "Legújabb először",
-  "next": "Következő",
-  "next_memory": "Következő emlék",
-  "no": "Nem",
-  "no_albums_message": "Hozzon létre új albumot a fotói és videói rendszerezéséhez",
-  "no_albums_with_name_yet": "Úgy tűnik, hogy nincs még ilyen névvel album.",
-  "no_albums_yet": "Úgy tűnik, hogy még nem lett album létrehozva.",
-  "no_archived_assets_message": "Archiváljon fényképeket és videókat, hogy elrejtse azokat a Fényképek nézetből",
-  "no_assets_message": "KATTINTSON AZ ELSŐ FÉNYKÉPE FELTÖLTÉSÉHEZ",
-  "no_duplicates_found": "Duplikátumok nem találhatók.",
-  "no_exif_info_available": "Exif információ nem elérhető",
-  "no_explore_results_message": "Töltsön fel több fényképet, hogy felfedezze a gyűjteményét.",
-  "no_favorites_message": "Jelöljön meg kedvenceket, hogy gyorsan megtalálhassa legjobb fényképeit és videóit",
-  "no_libraries_message": "Hozzon létre külső képtárat a fényképei és videói megtekintéséhez",
-  "no_name": "Nincs Név",
-  "no_places": "Nincsenek helyek",
-  "no_results": "Nincsenek eredmények",
-  "no_results_description": "Próbáljon egy szinonimát, vagy fogalmazzon általánosabban",
-  "no_shared_albums_message": "Hozzon létre egy új albumot, hogy megoszthassa fényképeit és videóit másokkal",
-  "not_in_any_album": "Nincs albumban",
-  "note_apply_storage_label_to_previously_uploaded assets": "Megjegyzés: hogy a Tárhelycímkézést végrehajtódjon a korábban feltöltött elemeken, futtassa a",
-  "note_unlimited_quota": "Megjegyzés: Írjon 0-t végtelen kvótához",
-  "notes": "Jegyzetek",
-  "notification_toggle_setting_description": "Emailes értesítések engedélyezése",
-  "notifications": "Értesítések",
-  "notifications_setting_description": "Értesítések kezelése",
-  "oauth": "OAuth",
-  "offline": "Offline",
-  "offline_paths": "Offline útvonalak",
-  "offline_paths_description": "Ezeket az eredményeket okozhatja a külső könyvtárhoz nem tartozó fájlok manuális törlése.",
-  "ok": "Rendben",
-  "oldest_first": "Legrégebbi először",
-  "onboarding": "Első lépések",
-  "onboarding_privacy_description": "Az alábbi (nem kötelező) szolgáltatások külső szolgáltatásokon alapulnak, és bármikor kikapcsolhatóak az adminisztrációs beállításokban.",
-  "onboarding_theme_description": "Válasszon egy színt az alkalmazásnak. Ezt bármikor megváltoztathatja a beállításokban.",
-  "onboarding_welcome_description": "Állítsunk be néhány gyakori beállítást.",
-  "onboarding_welcome_user": "Üdvözlöm, {user}",
-  "online": "Online",
-  "only_favorites": "Csak kedvencek",
-  "only_refreshes_modified_files": "Csak a megváltoztatott fájlokat frissíti",
-  "open_in_map_view": "Megnyitás térkép nézetben",
-  "open_in_openstreetmap": "Megnyitás OpenStreetMap-ben",
-  "open_the_search_filters": "Keresési szűrők megnyitása",
-  "options": "Beállítások",
-  "or": "vagy",
-  "organize_your_library": "Rendszerezze képtárát",
-  "original": "eredeti",
-  "other": "Egyéb",
-  "other_devices": "Egyéb eszközök",
-  "other_variables": "Egyéb változók",
-  "owned": "Tulajdonos",
-  "owner": "Tulajdonos",
-  "partner": "Partner",
-  "partner_can_access": "{partner} hozzáférhet",
-  "partner_can_access_assets": "Minden fényképe és videója, kivéve amik archiválásra vagy törlésre kerültek",
-  "partner_can_access_location": "A fényképei készítési helye",
-  "partner_sharing": "Társmegosztás",
-  "partners": "Társak",
-  "password": "Jelszó",
-  "password_does_not_match": "Jelszavak nem egyeznek",
-  "password_required": "Jelszó szükséges",
-  "password_reset_success": "Jelszóvisszaállítás sikeres",
-  "past_durations": {
-    "days": "{days, plural, one {Tegnap} other {Elmúlt # nap}}",
-    "hours": "{hours, plural, one {Előző óra} other {Elmúlt # óra}}",
-    "years": "{years, plural, one {Tavaly} other {Elmúlt # év}}"
-  },
-  "path": "Útvonal",
-  "pattern": "Minta",
-  "pause": "Szüneteltetés",
-  "pause_memories": "Emlékek szüneteltetése",
-  "paused": "Szüneteltetve",
-  "pending": "Folyamatban lévő",
-  "people": "Személyek",
-  "people_edits_count": "{count, plural, other {# személy}} szerkesztve",
-  "people_sidebar_description": "Jelenítsen meg linket a Személyek fülhöz oldalt",
-  "perform_library_tasks": "",
-  "permanent_deletion_warning": "Figyelmeztetés végleges törlésről",
-  "permanent_deletion_warning_setting_description": "Figyelmeztessen fájlok végleges törlése előtt",
-  "permanently_delete": "Végleges törlés",
-  "permanently_delete_assets_count": "{count, plural, one {Elem} other {Elemek}} végleges törlése",
-  "permanently_delete_assets_prompt": "Biztos, hogy véglegesen törölni szeretné ezt {count, plural, one {az elemet?} other {a(z) <b>#</b> elemet?}} Ez el fogja távolítani az albumokból, amikben {count, plural, one {szerepel} other {szerepelnek}}.",
-  "permanently_deleted_asset": "Elem véglegesen törölve",
-  "permanently_deleted_assets_count": "{count, plural, other {# elem}} véglegesen törölve",
-  "person": "Személy",
-  "person_hidden": "{name}{hidden, select, true { (rejtett)} other {}}",
-  "photo_shared_all_users": "Mindenkivel megosztotta a fényképeit, vagy nincs senki, akivel meg tudná osztani.",
-  "photos": "Képek",
-  "photos_and_videos": "Fényképek és videók",
-  "photos_count": "{count, plural, one {{count, number} Fotó} other {{count, number} Fotó}}",
-  "photos_from_previous_years": "Képek előző évekből",
-  "pick_a_location": "Válasszon egy helyet",
-  "place": "Hely",
-  "places": "Helyek",
-  "play": "Lejátszás",
-  "play_memories": "Emlékek lejátszása",
-  "play_motion_photo": "Mozgókép lejátszása",
-  "play_or_pause_video": "Videó elindítása vagy megállítása",
-  "point": "",
-  "port": "Port",
-  "preset": "Sablon",
-  "preview": "Előnézet",
-  "previous": "Előző",
-  "previous_memory": "Előző emlék",
-  "previous_or_next_photo": "Előző vagy következő fotó",
-  "primary": "Elsődleges",
-  "privacy": "Magánszféra",
-  "profile_image_of_user": "{user} profilképe",
-  "profile_picture_set": "Profilkép beállítva.",
-  "public_album": "Publikus album",
-  "public_share": "Nyilvános Megosztás",
-  "purchase_account_info": "Támogató",
-  "purchase_activated_subtitle": "Köszönjük, hogy támogatja az Immich-et és a nyílt forráskódú programokat",
-  "purchase_activated_time": "Aktiválva ekkor: {date, date}",
-  "purchase_activated_title": "Kulcs sikeresen aktiválva",
-  "purchase_button_activate": "Aktiválás",
-  "purchase_button_buy": "Vásárlás",
-  "purchase_button_buy_immich": "Vásárolja meg az Immich-et",
-  "purchase_button_never_show_again": "Soha többé ne mutassa",
-  "purchase_button_reminder": "Emlékeztessen 30 nap múlva",
-  "purchase_button_remove_key": "Kulcs eltávolítása",
-  "purchase_button_select": "Kiválasztás",
-  "purchase_failed_activation": "Aktiválás sikertelen! Ellenőrizze az e-mailjét a helyes termékkulcsért!",
-  "purchase_individual_description_1": "Magánszemélynek",
-  "purchase_individual_description_2": "Támogató állapot",
-  "purchase_individual_title": "Magánszemély",
-  "purchase_input_suggestion": "Van termékkulcsa? Adja meg a kulcsot alább",
-  "purchase_license_subtitle": "Vásárolja meg az Immich-et, hogy támogassa a szolgáltatás fejlesztését a jövőben is",
-  "purchase_lifetime_description": "Élethosszú vásárlás",
-  "purchase_option_title": "VÁSÁRLÁSI LEHETŐSÉGEK",
-  "purchase_panel_info_1": "Az Immich készítése sok időt és erőfeszítést igényel, és teljes munkaidőben foglalkoztatunk szoftvermérnököket hogy olyan jóvá tegyük, amennyire csak lehet. Küldetésünk, hogy a nyílt forráskódú szoftver és etikus üzleti gyakorlat fenntartható bevételi forrás legyen a fejlesztőinknek, és egy magánszférát tiszteletben tartó ökoszisztéma készítése, amely valódi alternatívát nyújt a felhasználókat kihasználó felhőszolgáltatásoknak.",
-  "purchase_panel_info_2": "Mivel elkötelezettek vagyunk, hogy nem zárunk fizetés mögé szolgáltatásokat, ez a vásárlás az Immich semmilyen új részét nem oldja fel. Olyan felhasználóktól, mint Öntől, függünk, hogy az Immich-et tudjuk fejleszteni.",
-  "purchase_panel_title": "Támogassa a projektet",
-  "purchase_per_server": "Szerverenként",
-  "purchase_per_user": "Felhasználónként",
-  "purchase_remove_product_key": "Termékkulcs eltávolítása",
-  "purchase_remove_product_key_prompt": "Biztosan el szeretné távolítani a termékkulcsot?",
-  "purchase_remove_server_product_key": "Szerver termékkulcs eltávolítása",
-  "purchase_remove_server_product_key_prompt": "Biztosan el szeretné távolítani a szerver termékkulcsot?",
-  "purchase_server_description_1": "Az egész szerverre",
-  "purchase_server_description_2": "Támogító állapot",
-  "purchase_server_title": "Szerver",
-  "purchase_settings_server_activated": "A szerver termékkulcsot az admin menedzseli",
-  "range": "",
-  "rating": "Értékelés csillagokkal",
-  "rating_description": "Exif értékelés megjelenítése az infópanelben",
-  "raw": "",
-  "reaction_options": "Reakció lehetőségek",
-  "read_changelog": "Változtatások olvasása",
-  "reassign": "Áthelyezés",
-  "reassigned_assets_to_existing_person": "{count, plural, other {# elem}} áthelyezve {name, select, null {egy létező személyhez} other {{name}}}",
-  "reassigned_assets_to_new_person": "{count, plural, other {# elem}} áthelyezve egy új személyhez",
-  "reassing_hint": "Kijelölt média hozzáadása létező emberhez",
-  "recent": "Friss",
-  "recent_searches": "Friss keresések",
-  "refresh": "Frissítés",
-  "refresh_encoded_videos": "Elkódolt videók frissítése",
-  "refresh_metadata": "Metaadatok frissítése",
-  "refresh_thumbnails": "Előnézetek frissítése",
-  "refreshed": "Frissítve",
-  "refreshes_every_file": "Minden fájl frissítése",
-  "refreshing_encoded_video": "Elkódolt videók frissítése",
-  "refreshing_metadata": "Metaadatok frissítése",
-  "regenerating_thumbnails": "Előnézetek újragenerálása",
-  "remove": "Eltávolítás",
-  "remove_assets_album_confirmation": "Biztosan szeretne eltávolítani {count, plural, one {# elemet} other {# elemet}} az albumból?",
-  "remove_assets_shared_link_confirmation": "Biztosan szeretne eltávolítani {count, plural, one {# elemet} other {# elemet}} ebből a megosztott linkből?",
-  "remove_assets_title": "Elemek eltávolítása?",
-  "remove_custom_date_range": "Szabadon megadott időintervallum eltávolítása",
-  "remove_from_album": "Eltávolítás az albumból",
-  "remove_from_favorites": "Eltávolítás a kedvencekből",
-  "remove_from_shared_link": "Eltávolítás a megosztott linkből",
-  "remove_offline_files": "Offline Fájlok Eltávolítása",
-  "remove_user": "Felhasználó eltávolítása",
-  "removed_api_key": "API Kulcs eltávolítva: {name}",
-  "removed_from_archive": "Archívumból eltávolítva",
-  "removed_from_favorites": "Kedvencekből eltávolítva",
-  "removed_from_favorites_count": "A kedvencekből el lett távolítva {count, plural, other {# elem}}",
-  "rename": "Átnevezés",
-  "repair": "Javítás",
-  "repair_no_results_message": "Nem megfigyelt és hiányzó fájlok itt jelennek meg",
-  "replace_with_upload": "Csere feltöltéssel",
-  "repository": "Adattár",
-  "require_password": "Jelszó szükségessé tétele",
-  "require_user_to_change_password_on_first_login": "Felhasználó első bejelentkezéskor való jelszóváltoztatásának szükségessé tétele",
-  "reset": "Visszaállítás",
-  "reset_password": "Jelszó visszaállítása",
-  "reset_people_visibility": "Emberek láthatóságának visszaállítása",
-  "reset_settings_to_default": "",
-  "reset_to_default": "Visszaállítás alapállapotba",
-  "resolve_duplicates": "Duplikátumok feloldása",
-  "resolved_all_duplicates": "Minden duplikátum feloldása",
-  "restore": "Visszaállít",
-  "restore_all": "Minden visszaállítása",
-  "restore_user": "Felhasználó visszaállítása",
-  "restored_asset": "Elem visszaállítása",
-  "resume": "Folytatás",
-  "retry_upload": "Feltöltés újrapróbálása",
-  "review_duplicates": "Megegyező elemek átnézése",
-  "role": "Szerep",
-  "role_editor": "Szerkesztő",
-  "role_viewer": "Néző",
-  "save": "Mentés",
-  "saved_api_key": "API Kulcs elmentve",
-  "saved_profile": "Profil elmentve",
-  "saved_settings": "Beállítások elmentve",
-  "say_something": "Szólj hozzá",
-  "scan_all_libraries": "Minden könyvtár átnézése",
-  "scan_all_library_files": "Minden könyvtárbeli elem újraellenőrzése",
-  "scan_new_library_files": "Ellenőrzés új könyvtárbeli elemekért",
-  "scan_settings": "Felfedezési beállítások",
-  "search": "Keresés",
-  "search_albums": "Albumok keresése",
-  "search_by_context": "Keresés kontextus alapján",
-  "search_by_filename": "Keresés fájlnév vagy kiterjesztés alapján",
-  "search_by_filename_example": "például IMG_1234.JPG vagy PNG",
-  "search_camera_make": "Kameragyártó keresése...",
-  "search_camera_model": "Kameramodell keresése...",
-  "search_city": "Város keresése...",
-  "search_country": "Ország keresése...",
-  "search_for_existing_person": "Már meglévő személy keresése",
-  "search_no_people": "Nincs személy",
-  "search_no_people_named": "Nincs személy \"{name}\" néven",
-  "search_people": "Személyek keresése",
-  "search_places": "Helyek keresése",
-  "search_state": "Régió keresése...",
-  "search_timezone": "Időzóna keresése...",
-  "search_type": "Típus keresése",
-  "search_your_photos": "Fotók keresése",
-  "searching_locales": "",
-  "second": "Másodperc",
-  "see_all_people": "Minden személy megtekintése",
-  "select_album_cover": "Albumborító kiválasztása",
-  "select_all": "Összes kijelölése",
-  "select_all_duplicates": "Minden duplikátum kiválasztása",
-  "select_avatar_color": "Avatár színének választása",
-  "select_face": "Arc kiválasztása",
-  "select_featured_photo": "Kijelölt fénykép kiválasztása",
-  "select_from_computer": "Kiválasztás számítógépről",
-  "select_keep_all": "Minden megtartása",
-  "select_library_owner": "Könyvtártulajdonos kijelölése",
-  "select_new_face": "Új arc kiválasztása",
-  "select_photos": "Fotók választása",
-  "select_trash_all": "Minden szemétbe helyezése",
-  "selected": "Kijelölt",
-  "selected_count": "{count, plural, other {# kiválasztva}}",
-  "send_message": "Üzenet küldése",
-  "send_welcome_email": "Üdvözlő üzenet küldése",
-  "server": "Szerver",
-  "server_offline": "Szerver Nem Elérhető",
-  "server_online": "Szerver Elérhető",
-  "server_stats": "Szerver Statisztikák",
-  "server_version": "Szerver Verzió",
-  "set": "Beállítás",
-  "set_as_album_cover": "Beállítás albumborítóként",
-  "set_as_profile_picture": "Beállítás profilképként",
-  "set_date_of_birth": "Születési dátum beállítása",
-  "set_profile_picture": "Profilkép beállítása",
-  "set_slideshow_to_fullscreen": "Diavetítés teljes képernyőre állítása",
-  "settings": "Beállítások",
-  "settings_saved": "Beállítások mentve",
-  "share": "Megosztás",
-  "shared": "Megosztva",
-  "shared_by": "Megosztva általa:",
-  "shared_by_user": "Megosztva {user} által",
-  "shared_by_you": "Megosztva Ön által",
-  "shared_from_partner": "Fényképek {partner}-tól/től",
-  "shared_link_options": "Megosztott link beállítások",
-  "shared_links": "Megosztott Linkek",
-  "shared_photos_and_videos_count": "{assetCount, plural, other {# megosztott kép és videó.}}",
-  "shared_with_partner": "Megosztva vele: {partner}",
-  "sharing": "Megosztás",
-  "sharing_enter_password": "Jelszó megadása szükséges az oldal megtekintéséhez.",
-  "sharing_sidebar_description": "Jelenítsen meg linket a Megosztás fülhöz oldalt",
-  "shift_to_permanent_delete": "nyomja meg a ⇧-t hogy véglegesen törölje az elemet",
-  "show_album_options": "Albummegjelenítési beállítások",
-  "show_albums": "Albumok megtekintése",
-  "show_all_people": "Minden személy megjelenítése",
-  "show_and_hide_people": "Személyek megjelenítése és elrejtése",
-  "show_file_location": "Fájl helyének megjelenítése",
-  "show_gallery": "Galéria megjelenítése",
-  "show_hidden_people": "Rejtett személyek megjelenítése",
-  "show_in_timeline": "Megjelenítés az idővonalon",
-  "show_in_timeline_setting_description": "Ettől a felhasználótól származó képek és videók megjelenítése az Ön idővonalán",
-  "show_keyboard_shortcuts": "Billentyűparancsok megjelenítése",
-  "show_metadata": "Metaadatok mutatása",
-  "show_or_hide_info": "Info mutatása vagy elrejtése",
-  "show_password": "Jelszó mutatása",
-  "show_person_options": "Személy opciók mutatása",
-  "show_progress_bar": "Haladás megjelenítése",
-  "show_search_options": "Keresési opciók mutatása",
-  "show_supporter_badge": "Támogató jelvény",
-  "show_supporter_badge_description": "Támogató jelvény megjelenítése",
-  "shuffle": "Keverés",
-  "sign_out": "Kilépés",
-  "sign_up": "Feliratkozás",
-  "size": "Méret",
-  "skip_to_content": "Ugrás a tartalomhoz",
-  "slideshow": "Diavetítés",
-  "slideshow_settings": "Diavetítés beállításai",
-  "sort_albums_by": "Albumok rendezése...",
-  "sort_created": "Létrehozva",
-  "sort_items": "Elemek száma",
-  "sort_modified": "Módosítva",
-  "sort_oldest": "Legrégebbi fénykép",
-  "sort_recent": "Legújabb fénykép",
-  "sort_title": "Cím",
-  "source": "Forrás",
-  "stack": "Fotók csoportosítása",
-  "stack_duplicates": "Duplikátumok csoportosítása",
-  "stack_select_one_photo": "Fő fénykép kiválasztása",
-  "stack_selected_photos": "Kiválasztott fényképek csoportosítása",
-  "stacked_assets_count": "{count, plural, other {# elem}} csoportba helyezve",
-  "stacktrace": "Stacktrace",
-  "start": "Kezdet",
-  "start_date": "Kezdet",
-  "state": "Állam",
-  "status": "Állapot",
-  "stop_motion_photo": "Mozgókép megállítása",
-  "stop_photo_sharing": "Fotók megosztásának megszűntetése?",
-  "stop_photo_sharing_description": "{partner} mostantól nem fog tudni hozzáférni az Ön fényképeihez.",
-  "stop_sharing_photos_with_user": "Fényképek megosztásának abbahagyása ezzel a felhasználóval",
-  "storage": "Tárhely",
-  "storage_label": "Tárolási címke",
-  "storage_usage": "{used}/{available} használatban",
-  "submit": "Beadás",
-  "suggestions": "Javaslatok",
-  "sunrise_on_the_beach": "Napkelte a tengerparton",
-  "swap_merge_direction": "Egyesítés irányának megfordítása",
-  "sync": "Szinkronizálás",
-  "template": "Minta",
-  "theme": "Téma",
-  "theme_selection": "Témaválasztás",
-  "theme_selection_description": "A böngésző beállításának megfelelően automatikusan használjon világos vagy sötét témát",
-  "they_will_be_merged_together": "Egyesítve lesznek",
-  "time_based_memories": "Emlékek idő alapján",
-  "timezone": "Időzóna",
-  "to_archive": "Archívum",
-  "to_change_password": "Jelszó megváltoztatása",
-  "to_favorite": "Kedvenc",
-  "to_login": "Bejelentkezés",
-  "to_trash": "Szemétbe helyezés",
-  "toggle_settings": "Beállítások változtatása",
-  "toggle_theme": "Témaváltás",
-  "toggle_visibility": "Láthatóság változtatása",
-  "total_usage": "Összesen használatban",
-  "trash": "Lomtár",
-  "trash_all": "Mindet lomtárba",
-  "trash_count": "{count, number} elem szemétbe helyezése",
-  "trash_delete_asset": "Elem szemétbe helyezése / törlése",
-  "trash_no_results_message": "Itt lesznek láthatóak a lomtárba tett képek és videok.",
-  "trashed_items_will_be_permanently_deleted_after": "A szemeteskosárban lévő elemek véglegesen törlésre kerülnek {days, plural, other {# nap}} múlva.",
-  "type": "Típus",
-  "unarchive": "Archívumból kivétel",
-  "unarchived": "Archívumból kivett",
-  "unarchived_count": "{count, plural, other {# elem kivéve az archívumból}}",
-  "unfavorite": "Nem Kedvenc",
-  "unhide_person": "Nem rejtett személy",
-  "unknown": "Ismeretlen",
-  "unknown_album": "Ismeretlen Album",
-  "unknown_year": "Ismeretlen év",
-  "unlimited": "Korlátlan",
-  "unlink_oauth": "OAuth leválasztása",
-  "unlinked_oauth_account": "Leválasztott OAuth felhasználó",
-  "unnamed_album": "Névtelen Album",
-  "unnamed_share": "Névtelen Megosztás",
-  "unsaved_change": "Mentés nélküli változtatás",
-  "unselect_all": "Összes kiválasztás törlése",
-  "unselect_all_duplicates": "Duplikátumok kijelölésének megszüntetése",
-  "unstack": "Csoport Megszűntetése",
-  "unstacked_assets_count": "{count, plural, other {# elemből}} álló csoport szétszedve",
-  "untracked_files": "Nem megfigyelt fájlok",
-  "untracked_files_decription": "Ezek a fájlok nincsenek az alkalmazás által megfigyelve. Létrehozódhattak sikertelen mozgatástól, félbeszakított feltöltéstől, vagy hátrahagyva hiba miatt",
-  "up_next": "Következik",
-  "updated_password": "Jelszó megváltoztatva",
-  "upload": "Feltöltés",
-  "upload_concurrency": "",
-  "upload_errors": "Feltöltés befejezve {count, plural, other {# hibával}}, frissítse az oldalt az újonnan feltöltött elemek megtekintéséhez.",
-  "upload_progress": "Hátra van {remaining, number} - Feldolgozva {processed, number}/{total, number}",
-  "upload_skipped_duplicates": "{count, plural, other {# megegyező elem}} kihagyva",
-  "upload_status_duplicates": "Duplikátumok",
-  "upload_status_errors": "Hibák",
-  "upload_status_uploaded": "Feltöltve",
-  "upload_success": "Feltöltés sikeres, frissítse az oldalt az újonnan feltöltött elemek megtekintéséhez.",
-  "url": "URL",
-  "usage": "Felhasználás",
-  "use_custom_date_range": "Szabadon megadott időintervallum használata",
-  "user": "Felhasználó",
-  "user_id": "Felhasználó azonosítója",
-  "user_liked": "{type, select, photo {ez a fénykép} video {ez a videó} other {ez}} tetszik neki: {user}",
-  "user_purchase_settings": "Megvásárlás",
-  "user_purchase_settings_description": "Vásárlás kezelése",
-  "user_role_set": "{user} beállítása {role} szerepbe",
-  "user_usage_detail": "Felhasználó használati adatai",
-  "username": "Felhasználónév",
-  "users": "Felhasználók",
-  "utilities": "Eszközök",
-  "validate": "Ellenőrzés",
-  "variables": "Változók",
-  "version": "Verzió",
-  "video": "Videó",
-  "video_hover_setting": "Bélyegkép felett lebegésnél videó indítás",
-  "video_hover_setting_description": "Ha az egér a bélyegkép felett időzik, a bélyegkép videó lejátszása induljon el. A lejátszás az indítás ikon feletti időzéssel akkor is elindul, ha ez az opció ki van kapcsolva.",
-  "videos": "Videók",
-  "videos_count": "{count, plural, one {# Videó} other {# Videó}}",
-  "view": "Nézet",
-  "view_album": "Album megtekintése",
-  "view_all": "Összes mutatása",
-  "view_all_users": "Minden felhasználó megtekintése",
-  "view_links": "Linkek megtekintése",
-  "view_next_asset": "Következő elem megtekintése",
-  "view_previous_asset": "Előző elem megtekintése",
-  "view_stack": "Csoport megtekintése",
-  "viewer": "",
-  "visibility_changed": "Láthatóság megváltozott {count, plural, other {# személy}} számára",
-  "waiting": "Várakozás",
-  "warning": "Figyelmeztetés",
-  "week": "Hét",
-  "welcome": "Üdv",
-  "welcome_to_immich": "Üdvözöljük az Immich-ben",
-  "year": "Év",
-  "years_ago": "{years, plural, one {# évvel} other {# évvel}} ezelőtt",
-  "yes": "Igen",
-  "you_dont_have_any_shared_links": "Nincsenek megosztási linkek",
-  "zoom_image": "Kép nagyítása"
-}
diff --git a/web/src/lib/i18n/pt.json b/web/src/lib/i18n/pt.json
deleted file mode 100644
index 24c13ef03d..0000000000
--- a/web/src/lib/i18n/pt.json
+++ /dev/null
@@ -1,1300 +0,0 @@
-{
-  "about": "Sobre",
-  "account": "Conta",
-  "account_settings": "Configurações da Conta",
-  "acknowledge": "Confirmar",
-  "action": "Ação",
-  "actions": "Ações",
-  "active": "Ativo",
-  "activity": "Atividade",
-  "activity_changed": "A actividade está {enabled, select, true {ativada} other {desativada}}",
-  "add": "Adicionar",
-  "add_a_description": "Adicionar uma descrição",
-  "add_a_location": "Adicionar localização",
-  "add_a_name": "Adicionar um nome",
-  "add_a_title": "Adicionar um título",
-  "add_exclusion_pattern": "Adicionar um padrão de exclusão",
-  "add_import_path": "Adicionar um caminho de importação",
-  "add_location": "Adicionar localização",
-  "add_more_users": "Adicionar mais utilizadores",
-  "add_partner": "Adicionar parceiro",
-  "add_path": "Adicionar caminho",
-  "add_photos": "Adicionar fotos",
-  "add_to": "Adicionar a...",
-  "add_to_album": "Adicionar ao álbum",
-  "add_to_shared_album": "Adicionar ao álbum compartilhado",
-  "added_to_archive": "Adicionado ao arquivo",
-  "added_to_favorites": "Adicionado aos favoritos",
-  "added_to_favorites_count": "{count, plural, one {{count, number} adicionado aos favoritos} other {{count, number} adicionados aos favoritos}}",
-  "admin": {
-    "add_exclusion_pattern_description": "Adicione padrões de exclusão. Utilizar *, ** ou ? são suportados. Para ignorar todos os arquivos em qualquer diretório chamado \"Raw\", use \"**/Raw/**'. Para ignorar todos os arquivos que finalizam em \".tif\", use \"**/*.tif\". Para ignorar um caminho absoluto, use \"/caminho/para/ignorar/**\".",
-    "authentication_settings": "Configurações de Autenticação",
-    "authentication_settings_description": "Gerenciar senhas, OAuth, e outras configurações de autenticação",
-    "authentication_settings_disable_all": "Tem a certeza que deseja desativar todos os métodos de entrada? Entrar será completamente desativado.",
-    "authentication_settings_reenable": "Para reativar, use um <link>Comando de servidor</link>.",
-    "background_task_job": "Tarefas em segundo plano",
-    "check_all": "Selecionar Tudo",
-    "cleared_jobs": "Eliminadas as tarefas de: {job}",
-    "config_set_by_file": "A configuração está atualmente definida por um arquivo de configuração",
-    "confirm_delete_library": "Você tem certeza que deseja excluir a biblioteca {library} ?",
-    "confirm_delete_library_assets": "Você tem certeza que deseja eliminar esta biblioteca? Isto eliminará {count, plural, one {# arquivo incluído} other {todos os # arquivos incluídos}} do Immich e esta ação não pode ser revertida. Os ficheiros permanecerão no disco.",
-    "confirm_email_below": "Para confirmar, digite o {email} abaixo",
-    "confirm_reprocess_all_faces": "Tem certeza de que deseja reprocessar todos as faces? Isso também limpará as pessoas nomeadas.",
-    "confirm_user_password_reset": "Tem certeza de que deseja redefinir a senha de {user}?",
-    "crontab_guru": "Guru do Crontab",
-    "disable_login": "Desabilitar login",
-    "disabled": "",
-    "duplicate_detection_job_description": "Execute o aprendizado de máquina em arquivos para detectar imagens semelhantes. Depende da pesquisa inteligente",
-    "exclusion_pattern_description": "Os padrões de exclusão permitem ignorar arquivos e pastas ao escanear sua biblioteca. Isso é útil se você tiver pastas que contenham arquivos que não deseja importar, como arquivos RAW.",
-    "external_library_created_at": "Biblioteca externa (criada em {date})",
-    "external_library_management": "Gerenciamento de bibliotecas externas",
-    "face_detection": "Detecção de faces",
-    "face_detection_description": "Deteta rostos em arquivos com aprendizagem automática. Para vídeos, apenas a miniatura é considerada. \"Todos\" (re)processa todos os arquivos. \"Ausente\" enfileira arquivos que ainda não foram processados. Os rostos detetados serão enfileirados para reconhecimento facial após a conclusão da deteção de rostos, agrupando-os em pessoas novas ou já existentes.",
-    "facial_recognition_job_description": "Agrupa rostos detectados em pessoas. Esta etapa é executada após a conclusão da deteção de faces. \"Todos\" (re)agrupa todos os rostos. \"Ausentes\" enfileira rostos que ainda não têm uma pessoa atribuída.",
-    "failed_job_command": "Comando {command} falhou para a tarefa: {job}",
-    "force_delete_user_warning": "AVISO: Isso removerá imediatamente o utilizador e todos os arquivos. Isso não pode ser desfeito e os ficheiros não poderão ser recuperados.",
-    "forcing_refresh_library_files": "Forçando a atualização de todos os arquivos da biblioteca",
-    "image_format_description": "WebP produz arquivos menores que JPEG, mas é mais lento para codificar.",
-    "image_prefer_embedded_preview": "Prefira visualização incorporada",
-    "image_prefer_embedded_preview_setting_description": "Use visualizações incorporadas em fotos RAW como entrada para processamento de imagem, quando disponível. Isso pode produzir cores mais precisas para algumas imagens, mas a qualidade da visualização depende da câmera e a imagem pode ter mais artefatos de compactação.",
-    "image_prefer_wide_gamut": "Prefira ampla gama",
-    "image_prefer_wide_gamut_setting_description": "Use o Display P3 para miniaturas. Isso preserva melhor a vibração das imagens com espaços de cores amplos, mas as imagens podem aparecer de maneira diferente em dispositivos antigos com uma versão antiga do navegador. As imagens sRGB são mantidas como sRGB para evitar mudanças de cores.",
-    "image_preview_format": "Formato de visualização",
-    "image_preview_resolution": "Resolução de visualização",
-    "image_preview_resolution_description": "Usado ao visualizar uma única foto e para aprendizado de máquina. Resoluções mais altas podem preservar mais detalhes, mas demoram mais para codificar, têm tamanhos de arquivo maiores e podem reduzir a capacidade de resposta do aplicativo.",
-    "image_quality": "Qualidade",
-    "image_quality_description": "Qualidade de imagem de 1 a 100. Quanto maior, melhor para a qualidade, mas produz arquivos maiores. Esta opção afeta as imagens de visualização e miniatura.",
-    "image_settings": "Configurações de imagem",
-    "image_settings_description": "Gerenciar a qualidade e resolução das imagens geradas",
-    "image_thumbnail_format": "Formato de miniatura",
-    "image_thumbnail_resolution": "Resolução de miniatura",
-    "image_thumbnail_resolution_description": "Usado ao visualizar grupos de fotos (linha do tempo principal, visualização de álbum, etc.). Resoluções mais altas podem preservar mais detalhes, mas demoram mais para codificar, têm tamanhos de arquivo maiores e podem reduzir a capacidade de resposta do aplicativo.",
-    "job_concurrency": "{job} simultâneo",
-    "job_not_concurrency_safe": "Este trabalho não é compatível com simultaneidade.",
-    "job_settings": "Configurações de trabalho",
-    "job_settings_description": "Gerenciar simultaneidade dos trabalhos",
-    "job_status": "Status do trabalho",
-    "jobs_delayed": "{jobCount, plural, one {# adiado} other {# adiados}}",
-    "jobs_failed": "{jobCount, plural, one {# falhou} other {# falharam}}",
-    "library_created": "Criado biblioteca: {library}",
-    "library_cron_expression": "Expressão Cron",
-    "library_cron_expression_description": "Defina o intervalo de procura utilizando o formato cron. Para mais informações consulte <link>Guru Crontab</link>",
-    "library_cron_expression_presets": "Predefinições de expressão Cron",
-    "library_deleted": "Biblioteca excluída",
-    "library_import_path_description": "Especifique uma pasta para importar. Esta pasta, incluindo subpastas, será escaneada em busca de imagens e vídeos.",
-    "library_scanning": "Escanear periódicamente",
-    "library_scanning_description": "Configurar o escaneamento periódico da biblioteca",
-    "library_scanning_enable_description": "Habilitar escaneamento periódico da biblioteca",
-    "library_settings": "Biblioteca Externa",
-    "library_settings_description": "Gerenciar configurações de biblioteca externa",
-    "library_tasks_description": "Execute tarefas de biblioteca",
-    "library_watching_enable_description": "Observe bibliotecas externas para alterações de arquivos",
-    "library_watching_settings": "Observação de biblioteca (EXPERIMENTAL)",
-    "library_watching_settings_description": "Observe automaticamente os arquivos alterados",
-    "logging_enable_description": "Habilitar registro",
-    "logging_level_description": "Quando ativado, qual nível de log usar.",
-    "logging_settings": "Registros",
-    "machine_learning_clip_model": "Modelo CLIP",
-    "machine_learning_clip_model_description": "O nome do modelo CLIP definido <link>aqui</link>. Note que é necessário voltar a executar a \"Pesquisa Inteligente\" para todas as imagens depois de alterar um modelo.",
-    "machine_learning_duplicate_detection": "Detecção de duplicidade",
-    "machine_learning_duplicate_detection_enabled": "Habilitar detecção de duplicidade",
-    "machine_learning_duplicate_detection_enabled_description": "Se desativado, arquivos exatamente idênticos ainda serão desduplicados.",
-    "machine_learning_duplicate_detection_setting_description": "Use embeddings CLIP para encontrar prováveis duplicidades",
-    "machine_learning_enabled": "Habilitar o aprendizado da máquina",
-    "machine_learning_enabled_description": "Se desativado, todos os recursos de ML serão desativados, independentemente das configurações abaixo.",
-    "machine_learning_facial_recognition": "Reconhecimento Facial",
-    "machine_learning_facial_recognition_description": "Deteta, reconhece e agrupa rostos em imagens",
-    "machine_learning_facial_recognition_model": "Modelo de reconhecimento facial",
-    "machine_learning_facial_recognition_model_description": "Os modelos estão listados em ordem decrescente de tamanho. Modelos maiores são mais lentos e utilizam mais memória, mas produzem melhores resultados. Observe que ao alterar um modelo, você deve executar novamente o trabalho de Detecção de faces para todas as imagens.",
-    "machine_learning_facial_recognition_setting": "Ativar reconhecimento facial",
-    "machine_learning_facial_recognition_setting_description": "Se desativado, as imagens não serão codificadas para reconhecimento facial e não preencherão a seção Pessoas na página Explorar.",
-    "machine_learning_max_detection_distance": "Distância máxima de detecção",
-    "machine_learning_max_detection_distance_description": "Distância máxima entre duas imagens para considerá-las duplicadas, variando de 0,001 a 0,1. Valores mais altos detectarão mais duplicidades, mas poderão resultar em falsos positivos.",
-    "machine_learning_max_recognition_distance": "Distância máxima de reconhecimento",
-    "machine_learning_max_recognition_distance_description": "Distância máxima entre duas faces para ser considerada a mesma pessoa, variando de 0 a 2. Valores menores evitam rotular duas faces como a mesma pessoa, enquanto valores maiores evitam rotular a mesma face como duas pessoas diferentes. Observe que é mais fácil mesclar duas pessoas do que dividir uma pessoa em duas, portanto tenha preferência por valores mais baixos quando possível.",
-    "machine_learning_min_detection_score": "Pontuação mínima de detecção",
-    "machine_learning_min_detection_score_description": "Pontuação mínima de confiança para uma face ser detectada, de 0 a 1. Valores mais baixos detectam mais rostos, mas poderão resultar em falsos positivos.",
-    "machine_learning_min_recognized_faces": "Mínimo de faces reconhecidas",
-    "machine_learning_min_recognized_faces_description": "O número mínimo de faces reconhecidas para uma pessoa ser criada na lista. Aumentar isso torna o Reconhecimento Facial mais preciso, ao custo de aumentar a chance de um rosto não ser atribuído a uma pessoa.",
-    "machine_learning_settings": "Configurações de aprendizado de máquina (Machine Learning)",
-    "machine_learning_settings_description": "Gerenciar recursos e configurações de aprendizado de máquina",
-    "machine_learning_smart_search": "Busca inteligente",
-    "machine_learning_smart_search_description": "Pesquise imagens semanticamente usando embeddings CLIP",
-    "machine_learning_smart_search_enabled": "Habilite a pesquisa inteligente",
-    "machine_learning_smart_search_enabled_description": "Se desativado, as imagens não serão codificadas para pesquisa inteligente.",
-    "machine_learning_url_description": "URL do servidor de aprendizado de máquina",
-    "manage_concurrency": "Gerenciar simultaneidade",
-    "manage_log_settings": "Gerenciar configurações de registro",
-    "map_dark_style": "Tema Escuro",
-    "map_enable_description": "Ativar recursos do mapa",
-    "map_gps_settings": "Mapas e Definições de GPS",
-    "map_gps_settings_description": "Configurações de mapas e GPS (Geocoding inverso)",
-    "map_implications": "A funcionalidade do mapa necessita um servico externo (tiles.immich.cloud)",
-    "map_light_style": "Tema Claro",
-    "map_manage_reverse_geocoding_settings": "Gerir definições de <link>Geocoding inverso</link>",
-    "map_reverse_geocoding": "Geocodificação reversa",
-    "map_reverse_geocoding_enable_description": "Ativar geocodificação reversa",
-    "map_reverse_geocoding_settings": "Configurações de geocodificação reversa",
-    "map_settings": "Mapa",
-    "map_settings_description": "Gerenciar configurações do mapa",
-    "map_style_description": "URL para um tema de mapa style.json",
-    "metadata_extraction_job": "Extrair metadados",
-    "metadata_extraction_job_description": "Extrai informações de metadados de cada ativo, como GPS e resolução",
-    "metadata_faces_import_setting": "Ativar a importação facial",
-    "migration_job": "Migração",
-    "migration_job_description": "Migre miniaturas de arquivos e rostos para a estrutura de pastas mais recente",
-    "no_paths_added": "Nenhum caminho adicionado",
-    "no_pattern_added": "Nenhum padrão adicionado",
-    "note_apply_storage_label_previous_assets": "Observação: Para aplicar o rótulo de armazenamento a arquivos carregados anteriormente, execute o",
-    "note_cannot_be_changed_later": "NOTA: Isto não pode ser alterado posteriormente!",
-    "note_unlimited_quota": "Observação: insira 0 para cota ilimitada",
-    "notification_email_from_address": "A partir do endereço",
-    "notification_email_from_address_description": "Endereço de e-mail do remetente, por exemplo: \"Immich Photo Server <noreply@immich.app>\"",
-    "notification_email_host_description": "Host do servidor de e-mail (por exemplo, smtp.immich.app)",
-    "notification_email_ignore_certificate_errors": "Ignorar erros de certificado",
-    "notification_email_ignore_certificate_errors_description": "Ignorar erros de validação de certificado TLS (não recomendado)",
-    "notification_email_password_description": "Senha a ser usada ao autenticar no servidor de e-mail",
-    "notification_email_port_description": "Porta do servidor de e-mail (por exemplo, 25, 465 ou 587)",
-    "notification_email_sent_test_email_button": "Envie e-mail de teste e salve",
-    "notification_email_setting_description": "Configurações para envio de notificações por e-mail",
-    "notification_email_test_email": "Enviar e-mail de teste",
-    "notification_email_test_email_failed": "Falha ao enviar e-mail de teste. Verifique seus valores",
-    "notification_email_test_email_sent": "Um email de teste foi enviado para {email}. Por favor, verifique sua caixa de entrada.",
-    "notification_email_username_description": "Nome de utilizador a ser usado ao autenticar com o servidor de e-mail",
-    "notification_enable_email_notifications": "Habilitar notificações por e-mail",
-    "notification_settings": "Configurações de notificação",
-    "notification_settings_description": "Gerenciar configurações de notificação, incluindo e-mail",
-    "oauth_auto_launch": "Inicialização automática",
-    "oauth_auto_launch_description": "Inicie o fluxo de login do OAuth automaticamente ao navegar até a página de login",
-    "oauth_auto_register": "Registro automático",
-    "oauth_auto_register_description": "Registre automaticamente novos utilizadores após fazer login com OAuth",
-    "oauth_button_text": "Botão de texto",
-    "oauth_client_id": "ID do Cliente",
-    "oauth_client_secret": "Segredo do cliente",
-    "oauth_enable_description": "Faça login com OAuth",
-    "oauth_issuer_url": "URL do emissor",
-    "oauth_mobile_redirect_uri": "URI de redirecionamento móvel",
-    "oauth_mobile_redirect_uri_override": "Substituição de URI de redirecionamento móvel",
-    "oauth_mobile_redirect_uri_override_description": "Ative quando 'app.immich:/' for um URI de redirecionamento inválido.",
-    "oauth_profile_signing_algorithm": "Algoritmo de assinatura de perfis",
-    "oauth_profile_signing_algorithm_description": "Algoritmo utilizado para assinar o perfil de utilizador.",
-    "oauth_scope": "Escopo",
-    "oauth_settings": "OAuth",
-    "oauth_settings_description": "Gerenciar configurações de login do OAuth",
-    "oauth_settings_more_details": "Para mais informações sobre esta funcionalidade, veja a <link>documentação</link>.",
-    "oauth_signing_algorithm": "Algoritmo de assinatura",
-    "oauth_storage_label_claim": "Reivindicação de rótulo de armazenamento",
-    "oauth_storage_label_claim_description": "Defina automaticamente o rótulo de armazenamento do utilizador para o valor desta declaração.",
-    "oauth_storage_quota_claim": "Reivindicação de cota de armazenamento",
-    "oauth_storage_quota_claim_description": "Defina automaticamente a cota de armazenamento do utilizador para o valor desta declaração.",
-    "oauth_storage_quota_default": "Cota de armazenamento padrão (GiB)",
-    "oauth_storage_quota_default_description": "Cota em GiB a ser usada quando nenhuma reivindicação for fornecida (insira 0 para cota ilimitada).",
-    "offline_paths": "Caminhos off-line",
-    "offline_paths_description": "Esses resultados podem ser devidos à exclusão manual de arquivos que não fazem parte de uma biblioteca externa.",
-    "password_enable_description": "Login com e-mail e senha",
-    "password_settings": "Senha de acesso",
-    "password_settings_description": "Gerenciar configurações de login e senha",
-    "paths_validated_successfully": "Todos os caminhos validados com sucesso",
-    "quota_size_gib": "Tamanho da cota (GiB)",
-    "refreshing_all_libraries": "Atualizando todas as bibliotecas",
-    "registration": "Registo de Admin",
-    "registration_description": "Como é o primeiro utilizador no sistema, será marcado como administrador, e será responsável pelas tarefas administrativas, sendo que utilizadores adicionais serão criados por si.",
-    "removing_offline_files": "Removendo arquivos offline",
-    "repair_all": "Reparar tudo",
-    "repair_matched_items": "Encontrado {count, plural, one {# item} other {# itens}}",
-    "repaired_items": "Reparado {count, plural, one {# item} other {# itens}}",
-    "require_password_change_on_login": "Exigir que o utilizador altere a senha no primeiro início de sessão",
-    "reset_settings_to_default": "Redefinir as configurações para o padrão",
-    "reset_settings_to_recent_saved": "Redefinir as configurações para as configurações salvas recentemente",
-    "scanning_library_for_changed_files": "Escaneando a biblioteca em busca de arquivos alterados",
-    "scanning_library_for_new_files": "Escaneando a biblioteca em busca de novos arquivos",
-    "send_welcome_email": "Enviar e-mail de boas-vindas",
-    "server_external_domain_settings": "Domínio externo",
-    "server_external_domain_settings_description": "Domínio para links públicos compartilhados, incluindo http(s)://",
-    "server_settings": "Configurações do servidor",
-    "server_settings_description": "Gerenciar configurações do servidor",
-    "server_welcome_message": "Mensagem de boas-vindas",
-    "server_welcome_message_description": "Uma mensagem exibida na página de login.",
-    "sidecar_job": "Metadados secundários",
-    "sidecar_job_description": "Descubra ou sincronize metadados secundários do sistema de arquivos",
-    "slideshow_duration_description": "Tempo em segundos para exibir cada imagem",
-    "smart_search_job_description": "Execute a aprendizagem automática em arquivos para oferecer suporte à pesquisa inteligente",
-    "storage_template_date_time_description": "O registro de data e hora da criação é usado para fornecer essas informações",
-    "storage_template_date_time_sample": "Exemplo de tempo {date}",
-    "storage_template_enable_description": "Habilitar mecanismo de modelo de armazenamento",
-    "storage_template_hash_verification_enabled": "Verificação de hash ativada",
-    "storage_template_hash_verification_enabled_description": "Ativa a verificação de hash, não desative esta opção a menos que tenha certeza das implicações",
-    "storage_template_migration": "Migração de modelo de armazenamento",
-    "storage_template_migration_description": "Aplicar o <link>{template}</link> atual para arquivos previamente carregados",
-    "storage_template_migration_info": "As mudanças do modelo apenas se aplicarão a novos arquivos. Para aplicar o modelo retroativamente para os arquivos carregados anteriormente, execute o <link>{job}</link>.",
-    "storage_template_migration_job": "Trabalho de migração do modelo de armazenamento",
-    "storage_template_more_details": "Para mais informações sobre esta funcionalidade, dirija-se a <template-link>Modelo de Armazenamento</template-link> e as suas <implications-link>implicações</implications-link>",
-    "storage_template_onboarding_description": "Quando ativada, esta funcionalidade irá organizar os ficheiros automaticamente baseando-se num modelo definido pelo utilizador. Devido a problemas de estabilidade esta funcionalidade está desativada por defeito. Para mais informações, por favor leia a <link>documentação</link>.",
-    "storage_template_path_length": "Limite aproximado do tamanho do caminho: <b>{length, number}</b>{limit, number}",
-    "storage_template_settings": "Modelo de armazenamento",
-    "storage_template_settings_description": "Gerenciar a estrutura de pastas e o nome do arquivo dos ativos carregados",
-    "storage_template_user_label": "<code>{label}</code> é o Rótulo do Armazenamento do utilizador",
-    "system_settings": "Configurações de Sistema",
-    "theme_custom_css_settings": "CSS customizado",
-    "theme_custom_css_settings_description": "Folhas de estilo em cascata permitem que o design do Immich seja personalizado.",
-    "theme_settings": "Configurações de tema",
-    "theme_settings_description": "Gerencie a personalização da interface web do Immich",
-    "these_files_matched_by_checksum": "Esses arquivos são correspondidos por seus checksum",
-    "thumbnail_generation_job": "Gerar miniaturas",
-    "thumbnail_generation_job_description": "Gere miniaturas grandes, pequenas e desfocadas para cada ativo, bem como miniaturas para cada pessoa",
-    "transcode_policy_description": "",
-    "transcoding_acceleration_api": "API de aceleração",
-    "transcoding_acceleration_api_description": "A API que irá interagir com o seu dispositivo para acelerar a transcodificação. Esta configuração é a 'melhor opção': ela retornará à transcodificação de software em caso de falha. O VP9 pode não funcionar dependendo do seu hardware.",
-    "transcoding_acceleration_nvenc": "NVENC (requer GPU NVIDIA)",
-    "transcoding_acceleration_qsv": "Quick Sync (requer CPU Intel de 7ª geração ou posterior)",
-    "transcoding_acceleration_rkmpp": "RKMPP (apenas em SOCs Rockchip)",
-    "transcoding_acceleration_vaapi": "VAAPI",
-    "transcoding_accepted_audio_codecs": "Codecs de áudio aceitos",
-    "transcoding_accepted_audio_codecs_description": "Selecione quais codecs de áudio não precisam ser transcodificados. Usado apenas para determinadas políticas de transcodificação.",
-    "transcoding_accepted_containers": "Contentores aceites",
-    "transcoding_accepted_containers_description": "Selecione os formatos de contentores que não precisam de ser remuxed para MP4. Apenas usados para algumas políticas de transcodificação.",
-    "transcoding_accepted_video_codecs": "Codecs de vídeo aceitos",
-    "transcoding_accepted_video_codecs_description": "Selecione quais codecs de vídeo não precisam ser transcodificados. Usado apenas para determinadas políticas de transcodificação.",
-    "transcoding_advanced_options_description": "Opções que a maioria dos utilizadores não deveria precisar alterar",
-    "transcoding_audio_codec": "Codec de áudio",
-    "transcoding_audio_codec_description": "Opus é a opção de mais alta qualidade, mas tem menor compatibilidade com dispositivos ou softwares antigos.",
-    "transcoding_bitrate_description": "Vídeos com taxa de bits superior à máxima ou que não estão em um formato aceito",
-    "transcoding_codecs_learn_more": "Para aprender mais sobre as terminologias utilizadas aqui, consulte a documentação do FFmpeg para o <h264-link>codec H.264</h264-link>, <hevc-link>codec HEVC</hevc-link> e <vp9-link>codec VP9</vp9-link>.",
-    "transcoding_constant_quality_mode": "Modo de qualidade constante",
-    "transcoding_constant_quality_mode_description": "ICQ é melhor que CQP, mas alguns dispositivos de aceleração de hardware não suportam este modo. Definir esta opção dará preferência ao modo especificado ao usar codificação baseada em qualidade. Ignorado pelo NVENC porque não suporta ICQ.",
-    "transcoding_constant_rate_factor": "Fator de taxa constante (-crf)",
-    "transcoding_constant_rate_factor_description": "Nível de qualidade do vídeo. Os valores típicos são 23 para H.264, 28 para HEVC, 31 para VP9 e 35 para AV1. Menor é melhor, mas produz arquivos maiores.",
-    "transcoding_disabled_description": "Não transcodifique nenhum vídeo, pois pode interromper a reprodução em alguns clientes",
-    "transcoding_hardware_acceleration": "Aceleração de hardware",
-    "transcoding_hardware_acceleration_description": "Experimental; muito mais rápido, mas terá qualidade inferior com a mesma taxa de bits",
-    "transcoding_hardware_decoding": "Decodificação de hardware",
-    "transcoding_hardware_decoding_setting_description": "Aplica-se apenas a NVENC, QSV e RKMPP. Permite aceleração ponta a ponta em vez de apenas acelerar a codificação. Pode não funcionar em todos os vídeos.",
-    "transcoding_hevc_codec": "Codec HEVC",
-    "transcoding_max_b_frames": "Máximo de quadros B",
-    "transcoding_max_b_frames_description": "Valores mais altos melhoram a eficiência da compactação, mas retardam a codificação. Pode não ser compatível com aceleração de hardware em dispositivos mais antigos. 0 desativa os quadros B, enquanto -1 define esse valor automaticamente.",
-    "transcoding_max_bitrate": "Taxa de bits máxima",
-    "transcoding_max_bitrate_description": "Definir uma taxa de bits máxima pode tornar os tamanhos dos arquivos mais previsíveis com um custo menor de qualidade. Em 720p, os valores típicos são 2.600k para VP9 ou HEVC, ou 4.500k para H.264. Desativado se definido como 0.",
-    "transcoding_max_keyframe_interval": "Intervalo máximo de quadro-chave",
-    "transcoding_max_keyframe_interval_description": "Define a distância máxima do quadro entre os quadros-chave. Valores mais baixos pioram a eficiência da compressão, mas melhoram os tempos de busca e podem melhorar a qualidade em cenas com movimento rápido. 0 define esse valor automaticamente.",
-    "transcoding_optimal_description": "Vídeos com resolução superior à desejada ou em formato não aceito",
-    "transcoding_preferred_hardware_device": "Dispositivo de hardware preferido",
-    "transcoding_preferred_hardware_device_description": "Aplica-se apenas a VAAPI e QSV. Define o nó dri usado para transcodificação de hardware.",
-    "transcoding_preset_preset": "Predefinido (-preset)",
-    "transcoding_preset_preset_description": "Velocidade de compressão. Predefinições mais lentas produzem arquivos menores e aumentam a qualidade ao atingir uma determinada taxa de bits. VP9 ignora velocidades acima de \"mais rápidas\".",
-    "transcoding_reference_frames": "Quadros de referência",
-    "transcoding_reference_frames_description": "O número de quadros a serem referenciados ao compactar um determinado quadro. Valores mais altos melhoram a eficiência da compactação, mas retardam a codificação. 0 define esse valor automaticamente.",
-    "transcoding_required_description": "Somente vídeos que não estejam em um formato aceito",
-    "transcoding_settings": "Configurações de transcodificação de vídeo",
-    "transcoding_settings_description": "Gerencie as informações de resolução e codificação dos arquivos de vídeo",
-    "transcoding_target_resolution": "Resolução desejada",
-    "transcoding_target_resolution_description": "Resoluções mais altas podem preservar mais detalhes, mas demoram mais para codificar, têm tamanhos de arquivo maiores e podem reduzir a capacidade de resposta do aplicativo.",
-    "transcoding_temporal_aq": "QA temporal",
-    "transcoding_temporal_aq_description": "Aplica-se apenas ao NVENC. Aumenta a qualidade de cenas com alto detalhe e pouco movimento. Pode não ser compatível com dispositivos mais antigos.",
-    "transcoding_threads": "Threads",
-    "transcoding_threads_description": "Valores mais altos levam a uma codificação mais rápida, mas deixam menos espaço para o servidor processar outras tarefas enquanto estiver ativo. Este valor não deve ser superior ao número de núcleos da CPU. Maximiza a utilização se definido como 0.",
-    "transcoding_tone_mapping": "Mapeamento de tons",
-    "transcoding_tone_mapping_description": "Tenta preservar a aparência dos vídeos HDR quando convertidos para SDR. Cada algoritmo faz compensações diferentes em termos de cor, detalhes e brilho. Hable preserva os detalhes, Mobius preserva as cores e Reinhard preserva o brilho.",
-    "transcoding_tone_mapping_npl": "NPL de mapeamento de tons",
-    "transcoding_tone_mapping_npl_description": "As cores serão ajustadas para parecerem normais para uma exibição com esse brilho. Contra-intuitivamente, valores mais baixos aumentam o brilho do vídeo e vice-versa, uma vez que compensam o brilho da tela. 0 define esse valor automaticamente.",
-    "transcoding_transcode_policy": "Política de transcodificação",
-    "transcoding_transcode_policy_description": "Política para quando um vídeo deve ser transcodificado. Os vídeos HDR sempre serão transcodificados (exceto se a transcodificação estiver desativada).",
-    "transcoding_two_pass_encoding": "Codificação de duas passagens",
-    "transcoding_two_pass_encoding_setting_description": "Transcodifique em duas passagens para produzir vídeos melhor codificados. Quando a taxa de bits máxima está habilitada (necessária para funcionar com H.264 e HEVC), este modo usa um intervalo de taxa de bits baseado na taxa de bits máxima e ignora o CRF. Para VP9, o CRF pode ser usado se a taxa de bits máxima estiver desabilitada.",
-    "transcoding_video_codec": "Codec de vídeo",
-    "transcoding_video_codec_description": "O VP9 tem alta eficiência e compatibilidade com a web, mas leva mais tempo para transcodificar. HEVC tem desempenho semelhante, mas tem menor compatibilidade com a web. H.264 é amplamente compatível e rápido de transcodificar, mas produz arquivos muito maiores. AV1 é o codec mais eficiente, mas não possui suporte em dispositivos mais antigos.",
-    "trash_enabled_description": "Ativar recursos da Lixeira",
-    "trash_number_of_days": "Número de dias",
-    "trash_number_of_days_description": "Número de dias para manter os arquivos na lixeira antes de eliminar permanentemente",
-    "trash_settings": "Configurações da Lixeira",
-    "trash_settings_description": "Gerenciar configurações da lixeira",
-    "untracked_files": "Arquivos não rastreados",
-    "untracked_files_description": "Esses arquivos não são rastreados pelo aplicativo. Eles podem ser o resultado de movimentos malsucedidos, carregamentos interrompidos ou deixados para trás devido a um bug",
-    "user_delete_delay": "A conta e os arquivos de <b>{user}</b> serão agendados para eliminação permanente em {delay, plural, one {# dia} other {# dias}}.",
-    "user_delete_delay_settings": "Excluir atraso",
-    "user_delete_delay_settings_description": "Número de dias após a remoção para excluir permanentemente a conta e os arquivos de um utilizador. O trabalho de exclusão de utilizadores é executado à meia-noite para verificar utilizadores que estão prontos para exclusão. As alterações nesta configuração serão avaliadas na próxima execução.",
-    "user_delete_immediately": "A conta e os arquivos de <b>{user}</b> serão enfileirados para exclusão permanente <b>imediatamente</b>.",
-    "user_delete_immediately_checkbox": "Adicionar utilizador e arquivos à fila para eliminação imediata",
-    "user_management": "Gerenciamento de utilizadores",
-    "user_password_has_been_reset": "A senha do utilizador foi redefinida:",
-    "user_password_reset_description": "Forneça a senha temporária ao utilizador e informe que ele precisará alterar a senha no próximo início de sessão.",
-    "user_restore_description": "A conta de <b>{user}</b> será restaurada.",
-    "user_restore_scheduled_removal": "Restaurar usuário - planejar remoção em  {date, date, long}",
-    "user_settings": "Configurações do Utilizador",
-    "user_settings_description": "Gerenciar configurações do utilizador",
-    "user_successfully_removed": "O utilizador {email} foi removido com sucesso.",
-    "version_check_enabled_description": "Ativa verificação de novas versões",
-    "version_check_implications": "A funcionalidade de verificação da versão necessita comunicação periodica com github.com",
-    "version_check_settings": "Verificação de versão",
-    "version_check_settings_description": "Ativar/desativar a notificação de nova versão",
-    "video_conversion_job": "Transcodificar vídeos",
-    "video_conversion_job_description": "Transcodifique vídeos para maior compatibilidade com navegadores e dispositivos"
-  },
-  "admin_email": "E-mail do administrador",
-  "admin_password": "Senha do administrador",
-  "administration": "Administração",
-  "advanced": "Avançado",
-  "age_months": "Idade {months, plural, one {# mês} other {# meses}}",
-  "age_year_months": "Idade 1 ano, {months, plural, one {# mês} other {# meses}}",
-  "age_years": "Idade {years, plural, one{# ano} other {# anos}}",
-  "album_added": "Álbum adicionado",
-  "album_added_notification_setting_description": "Receba uma notificação por e-mail quando você for adicionado a um álbum compartilhado",
-  "album_cover_updated": "Capa do álbum atualizada",
-  "album_delete_confirmation": "Tem a certeza que quer apagar o álbum {album}? Se o álbum for partilhado, os outros utilizadores não poderão aceder-lhe novamente.",
-  "album_delete_confirmation_description": "Se este álbum for partilhado, os outros utilizadores deixam de poder aceder.",
-  "album_info_updated": "Informações do álbum atualizadas",
-  "album_leave": "Sair do álbum?",
-  "album_leave_confirmation": "Tem a certeza que quer sair de {album}?",
-  "album_name": "Nome do álbum",
-  "album_options": "Opções de álbum",
-  "album_remove_user": "Remover utilizador?",
-  "album_remove_user_confirmation": "Tem a certeza que quer remover {user}?",
-  "album_share_no_users": "Parece que tem este álbum partilhado com todos os utilizadores ou que não existem utilizadores para o partilhar.",
-  "album_updated": "Álbum atualizado",
-  "album_updated_setting_description": "Receba uma notificação por e-mail quando um álbum compartilhado tiver novos arquivos",
-  "album_user_left": "Saída {album}",
-  "album_user_removed": "Utilizador {user} removido",
-  "album_with_link_access": "Permite acesso a fotos e pessoas deste album por qualquer pessoa com o link.",
-  "albums": "Álbuns",
-  "albums_count": "{count, plural, one {{count, number} Álbum} other {{count, number} Álbuns}}",
-  "all": "Todos",
-  "all_albums": "Todos os álbuns",
-  "all_people": "Todas as pessoas",
-  "all_videos": "Todos os vídeos",
-  "allow_dark_mode": "Permitir modo escuro",
-  "allow_edits": "Permitir edições",
-  "allow_public_user_to_download": "Permit acesso de download ao user publico",
-  "allow_public_user_to_upload": "Permite acesso de upload ao user publico",
-  "anti_clockwise": "Sentido anti-horário",
-  "api_key": "Chave de API",
-  "api_key_description": "Este valor será apresentado apenas uma única vez. Por favor, certifique-se que o copiou antes de fechar a janela.",
-  "api_key_empty": "O nome da API Key não pode ser vazio",
-  "api_keys": "Chaves de API",
-  "app_settings": "Configurações do Aplicativo",
-  "appears_in": "Aparece em",
-  "archive": "Arquivo",
-  "archive_or_unarchive_photo": "Arquivar ou desarquivar foto",
-  "archive_size": "Tamanho do arquivo",
-  "archive_size_description": "Configure o tamanho do arquivo para downloads (em GiB)",
-  "archived": "Arquivado",
-  "archived_count": "{count, plural, other {Arquivado #}}",
-  "are_these_the_same_person": "São a mesma pessoa?",
-  "are_you_sure_to_do_this": "Tem a certeza que quer fazer isto?",
-  "asset_added_to_album": "Adicionado ao álbum",
-  "asset_adding_to_album": "A adicionar ao álbum...",
-  "asset_description_updated": "A descrição do arquivo foi atualizada",
-  "asset_filename_is_offline": "O arquivo {filename} está offline",
-  "asset_has_unassigned_faces": "O arquivo tem rostos sem atribuição",
-  "asset_hashing": "Hashing...",
-  "asset_offline": "Ativo off-line",
-  "asset_offline_description": "Este arquivo está offline. Immich não consegue acessar o local do arquivo. Certifique-se de que o arquivo esteja disponível e, em seguida, escaneie a biblioteca novamente.",
-  "asset_skipped": "Ignorado",
-  "asset_uploaded": "Enviado",
-  "asset_uploading": "Em upload...",
-  "assets": "Arquivos",
-  "assets_added_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}}",
-  "assets_added_to_album_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}} ao álbum",
-  "assets_added_to_name_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}} a {hasName, select, true {<b>{name}</b>} other {novo álbum}}",
-  "assets_count": "{count, plural, one {# arquivo} other {# arquivos}}",
-  "assets_moved_to_trash": "{count, plural, one {# ativo enviado} other {# ativos enviados}} para a lixeira",
-  "assets_moved_to_trash_count": "{count, plural, one {# arquivo movido} other {# arquivos movidos}} para a lixeira",
-  "assets_permanently_deleted_count": "{count, plural, one {# arquivo} other {# arquivos}} excluídos permanentemente",
-  "assets_removed_count": "{count, plural, one {# arquivo excluído} other {# arquivos excluídos}}",
-  "assets_restore_confirmation": "Tem a certeza que quer recuperar todos os artigos apagados? Não é possivel voltar atrás nesta acção!",
-  "assets_restored_count": "{count, plural, one {# arquivo restaurado} other {# arquivos restaurados}}",
-  "assets_trashed_count": "{count, plural, one {# arquivo enviado} other {# arquivos enviados}} para a lixeira",
-  "assets_were_part_of_album_count": "{count, plural, one {Arquivo já era} other {Os arquivos já eram}} parte do álbum",
-  "authorized_devices": "Dispositivos Autorizados",
-  "back": "Voltar",
-  "back_close_deselect": "Voltar, fechar ou desmarcar",
-  "backward": "Para trás",
-  "birthdate_saved": "Data de nascimento guardada com sucesso",
-  "birthdate_set_description": "A data de nascimento é usada para calcular a idade desta pessoa no momento em que uma fotografia foi tirada.",
-  "blurred_background": "Fundo desfocado",
-  "build": "Construir",
-  "build_image": "Construir Imagem",
-  "bulk_delete_duplicates_confirmation": "Tem a certeza de que deseja excluir {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Esta ação mantém o maior arquivo de cada grupo e exclui permanentemente todas as outras duplicidades. Você não pode desfazer esta ação!",
-  "bulk_keep_duplicates_confirmation": "Tem certeza de que deseja manter {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Isso resolverá todos os grupos duplicados sem excluir nada.",
-  "bulk_trash_duplicates_confirmation": "Tem a certeza de que deseja mover para a lixeira {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Isso manterá o maior arquivo de cada grupo e moverá para a lixeira todas as outras duplicidades.",
-  "buy": "Comprar Immich",
-  "camera": "Câmera",
-  "camera_brand": "Marca da câmera",
-  "camera_model": "Modelo da câmera",
-  "cancel": "Cancelar",
-  "cancel_search": "Cancelar pesquisa",
-  "cannot_merge_people": "Não é possível mesclar pessoas",
-  "cannot_undo_this_action": "Não pode voltar atrás nesta ação!",
-  "cannot_update_the_description": "Não é possível atualizar a descrição",
-  "cant_apply_changes": "Não é possível aplicar alterações",
-  "cant_get_faces": "Não foi possível obter faces",
-  "cant_search_people": "Não foi possível pesquisar pessoas",
-  "cant_search_places": "Não foi possível pesquisar lugares",
-  "change_date": "Alterar data",
-  "change_expiration_time": "Alterar o prazo de validade",
-  "change_location": "Alterar localização",
-  "change_name": "Alterar nome",
-  "change_name_successfully": "Nome alterado com sucesso",
-  "change_password": "Mudar a senha",
-  "change_password_description": "Esta é a primeira vez que você está entrando no sistema ou uma solicitação foi feita para alterar sua senha. Insira a nova senha abaixo.",
-  "change_your_password": "Alterar sua senha",
-  "changed_visibility_successfully": "Visibilidade alterada com sucesso",
-  "check_all": "Verificar tudo",
-  "check_logs": "Verificar registros",
-  "choose_matching_people_to_merge": "Escolha pessoas correspondentes para mesclar",
-  "city": "Cidade",
-  "clear": "Limpar",
-  "clear_all": "Limpar tudo",
-  "clear_all_recent_searches": "Limpar todas as pesquisas recentes",
-  "clear_message": "Limpar mensagem",
-  "clear_value": "Limpar valor",
-  "clockwise": "Sentido horário",
-  "close": "Fechar",
-  "collapse": "Colapsar",
-  "collapse_all": "Colapsar tudo",
-  "color_theme": "Tema de cores",
-  "comment_deleted": "Comentário eliminado",
-  "comment_options": "Opções de comentário",
-  "comments_and_likes": "Comentários e gostos",
-  "comments_are_disabled": "Comentários estão desativados",
-  "confirm": "Confirmar",
-  "confirm_admin_password": "Confirmar senha de administrador",
-  "confirm_delete_shared_link": "Tem certeza de que deseja excluir este link compartilhado?",
-  "confirm_password": "Confirme a senha",
-  "contain": "Caber",
-  "context": "Contexto",
-  "continue": "Continuar",
-  "copied_image_to_clipboard": "Imagem copiada para a área de transferência.",
-  "copied_to_clipboard": "Copiado para a área de transferência!",
-  "copy_error": "Copiar erro",
-  "copy_file_path": "Copiar caminho do arquivo",
-  "copy_image": "Copiar Imagem",
-  "copy_link": "Copiar link",
-  "copy_link_to_clipboard": "Copiar link para a área de transferência",
-  "copy_password": "Copiar senha",
-  "copy_to_clipboard": "Copiar para a área de transferência",
-  "country": "País",
-  "cover": "Preencher",
-  "covers": "Capas",
-  "create": "Criar",
-  "create_album": "Criar álbum",
-  "create_library": "Criar biblioteca",
-  "create_link": "Criar link",
-  "create_link_to_share": "Criar link para partilhar",
-  "create_link_to_share_description": "Permiter a visualização desta imagem(s) a qualquer pessoa com este link",
-  "create_new_person": "Criar nova pessoa",
-  "create_new_person_hint": "Associe os arquivos para uma nova pessoa",
-  "create_new_user": "Criar novo utilizador",
-  "create_user": "Criar utilizador",
-  "created": "Criado",
-  "current_device": "Dispositivo atual",
-  "custom_locale": "Localização Customizada",
-  "custom_locale_description": "Formatar datas e números baseados na linguagem e região",
-  "dark": "Escuro",
-  "date_after": "Data após",
-  "date_and_time": "Data e Hora",
-  "date_before": "Data antes",
-  "date_of_birth_saved": "Data de nascimento guardada com sucesso",
-  "date_range": "Intervalo de datas",
-  "day": "Dia",
-  "deduplicate_all": "Limpar todas Duplicidades",
-  "default_locale": "Localização Padrão",
-  "default_locale_description": "Formatar datas e números baseados na linguagem do seu navegador",
-  "delete": "Excluir",
-  "delete_album": "Excluir álbum",
-  "delete_api_key_prompt": "Tem certeza de que deseja excluir esta chave de API?",
-  "delete_duplicates_confirmation": "Tem certeza de que deseja excluir permanentemente estas duplicidades?",
-  "delete_key": "Excluir chave",
-  "delete_library": "Excluir biblioteca",
-  "delete_link": "Excluir link",
-  "delete_shared_link": "Excluir link de compartilhamento",
-  "delete_user": "Excluir utilizador",
-  "deleted_shared_link": "Link de compartilhamento excluído",
-  "description": "Descrição",
-  "details": "Detalhes",
-  "direction": "Direção",
-  "disabled": "Desativado",
-  "disallow_edits": "Não permitir edições",
-  "discover": "Descobrir",
-  "dismiss_all_errors": "Dispensar todos os erros",
-  "dismiss_error": "Dispensar erro",
-  "display_options": "Opções de exibição",
-  "display_order": "Ordem de exibição",
-  "display_original_photos": "Exibir fotos originais",
-  "display_original_photos_setting_description": "Prefira exibir a foto original ao visualizar um ativo em vez de miniaturas quando o ativo original é compatível com a web. Isso pode diminuir a velocidade de exibição das fotos.",
-  "do_not_show_again": "Não mostrar esta mensagem novamente",
-  "done": "Feito",
-  "download": "Transferir",
-  "download_include_embedded_motion_videos": "Vídeos incorporados",
-  "download_include_embedded_motion_videos_description": "Incluir vídeos incorporados em fotos em movimento como um arquivo separado",
-  "download_settings": "Transferir",
-  "download_settings_description": "Gerenciar configurações relacionadas a transferir ativos",
-  "downloading": "Baixando",
-  "downloading_asset_filename": "A transferir o arquivo {filename}",
-  "drop_files_to_upload": "Coloque os ficheiros em qualquer lugar para fazer o upload",
-  "duplicates": "Duplicados",
-  "duplicates_description": "Marque cada grupo indicando quais arquivos, se algum, são duplicados",
-  "duration": "Duração",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
-  "edit": "Editar",
-  "edit_album": "Editar álbum",
-  "edit_avatar": "Editar foto de perfil",
-  "edit_date": "Editar data",
-  "edit_date_and_time": "Editar data e hora",
-  "edit_exclusion_pattern": "Editar o padrão de exclusão",
-  "edit_faces": "Editar faces",
-  "edit_import_path": "Editar caminho de importação",
-  "edit_import_paths": "Editar caminhos de importação",
-  "edit_key": "Editar chave",
-  "edit_link": "Editar link",
-  "edit_location": "Editar Localização",
-  "edit_name": "Editar nome",
-  "edit_people": "Editar pessoas",
-  "edit_title": "Editar Título",
-  "edit_user": "Editar utilizador",
-  "edited": "Editado",
-  "editor": "Editar",
-  "editor_close_without_save_prompt": "As alterações não serão salvas",
-  "editor_close_without_save_title": "Fechar editor?",
-  "editor_crop_tool_h2_aspect_ratios": "Proporções de aspecto",
-  "editor_crop_tool_h2_rotation": "Rotação",
-  "email": "E-mail",
-  "empty": "",
-  "empty_album": "",
-  "empty_trash": "Esvaziar lixo",
-  "empty_trash_confirmation": "Tem certeza de que deseja esvaziar a lixeira? Isso removerá todos os arquivos da lixeira do Immich permanentemente.\nVocê não pode desfazer esta ação!",
-  "enable": "Ativar",
-  "enabled": "Ativado",
-  "end_date": "Data final",
-  "error": "Erro",
-  "error_loading_image": "Erro ao carregar a página",
-  "error_title": "Erro - Algo correu mal",
-  "errors": {
-    "cannot_navigate_next_asset": "Não pode navegar para o proximo artigo",
-    "cannot_navigate_previous_asset": "Não pode navegar para o artigo anterior",
-    "cant_apply_changes": "Não foi possível aplicar as alterações",
-    "cant_change_activity": "Não é possível {enabled, select, true {desativar} other {ativar}} atividade",
-    "cant_change_asset_favorite": "Não pode alterar o favorito deste artigo",
-    "cant_change_metadata_assets_count": "Não foi possível alterar os metadados de {count, plural, one {# arquivo} other {# arquivos}}",
-    "cant_get_faces": "Não foi possível obter os rostos",
-    "cant_get_number_of_comments": "Não foi possível obter o número de comentários",
-    "cant_search_people": "Não foi possível pesquisar pessoas",
-    "cant_search_places": "Não foi possível pesquisar locais",
-    "cleared_jobs": "Trabalhos eliminados para: {job}",
-    "error_adding_assets_to_album": "Erro ao adicionar arquivos ao álbum",
-    "error_adding_users_to_album": "Erro a adicionar utilizador ao album",
-    "error_deleting_shared_user": "Error a apagar o utilizador partilhado",
-    "error_downloading": "Erro a transferir {filename}",
-    "error_hiding_buy_button": "Erro ao esconder botão de compra",
-    "error_removing_assets_from_album": "Erro a eliminar artigos do album, verifique a consola para mais detalhes",
-    "error_selecting_all_assets": "Erro ao selecionar todos os arquivos",
-    "exclusion_pattern_already_exists": "Este padrão de exclusão já existe.",
-    "failed_job_command": "Comando {command} falhou para o trabalho: {job}",
-    "failed_to_create_album": "Falha ao criar álbum",
-    "failed_to_create_shared_link": "Falhou a criar um link partilhado",
-    "failed_to_edit_shared_link": "Falhou a editar o link partilhado",
-    "failed_to_get_people": "Falha na obtenção de pessoas",
-    "failed_to_load_asset": "Falha ao carregar arquivo",
-    "failed_to_load_assets": "Falha ao carregar arquivos",
-    "failed_to_load_people": "Falha ao carregar pessoas",
-    "failed_to_remove_product_key": "Falha ao remover chave de produto",
-    "failed_to_stack_assets": "Falha ao empilhar os arquivos",
-    "failed_to_unstack_assets": "Falha ao desempilhar arquivos",
-    "import_path_already_exists": "Este caminho de importação já existe.",
-    "incorrect_email_or_password": "Email ou password incorretos",
-    "paths_validation_failed": "a validação de {paths, plural, one {# caminho falhou} other {# caminhos falharam}}",
-    "profile_picture_transparent_pixels": "Imagem de perfil não pode ter pixels transparentes. Por favor faça zoom in e/ou mova a imagem.",
-    "quota_higher_than_disk_size": "Você definiu uma cota maior do que o tamanho do disco",
-    "repair_unable_to_check_items": "Não foi possível verificar {count, select, one {um item} other {alguns itens}}",
-    "unable_to_add_album_users": "Não foi possível adicionar utilizadores ao álbum",
-    "unable_to_add_assets_to_shared_link": "Não foi possivel adicionar os artigos ao link partilhado",
-    "unable_to_add_comment": "Não foi possível adicionar o comentário",
-    "unable_to_add_exclusion_pattern": "Não foi possível adicionar o padrão de exclusão",
-    "unable_to_add_import_path": "Não foi possível adicionar o caminho de importação",
-    "unable_to_add_partners": "Não foi possível adicionar parceiros",
-    "unable_to_add_remove_archive": "Não é possível {archived, select, true {remover o arquivo de} other {adicionar o arquivo}}",
-    "unable_to_add_remove_favorites": "Não foi possível {favorite, select, true {adicionar arquivo aos} other {remover arquivo dos}} favoritos",
-    "unable_to_archive_unarchive": "Não é possível {archived, select, true {arquivar} other {desarquivar}}",
-    "unable_to_change_album_user_role": "Não foi possível alterar a permissão do utilizador no álbum",
-    "unable_to_change_date": "Não foi possível alterar a data",
-    "unable_to_change_favorite": "Não foi possivel mudar o favorito do artigo",
-    "unable_to_change_location": "Não foi possível alterar a localização",
-    "unable_to_change_password": "Não foi possível alterar a senha",
-    "unable_to_change_visibility": "Não é possível alterar a visibilidade de {count, plural, one {# pessoa} other {# pessoas}}",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
-    "unable_to_complete_oauth_login": "Não foi possível completar início de sessão com OAuth",
-    "unable_to_connect": "Não é possível conectar",
-    "unable_to_connect_to_server": "Não foi possível ligar ao servidor",
-    "unable_to_copy_to_clipboard": "Não é possível copiar para a área de transferência, certifique-se que está acessando a pagina através de https",
-    "unable_to_create_admin_account": "Não foi possível criar conta de administrador",
-    "unable_to_create_api_key": "Não foi possível criar uma nova Chave de API",
-    "unable_to_create_library": "Não foi possível criar a biblioteca",
-    "unable_to_create_user": "Não foi possível criar o utilizador",
-    "unable_to_delete_album": "Não foi possível deletar o álbum",
-    "unable_to_delete_asset": "Não foi possível deletar o ativo",
-    "unable_to_delete_assets": "Erro ao eliminar arquivos",
-    "unable_to_delete_exclusion_pattern": "Não foi possível deletar o padrão de exclusão",
-    "unable_to_delete_import_path": "Não foi possível deletar o caminho de importação",
-    "unable_to_delete_shared_link": "Não foi possível deletar o link compartilhado",
-    "unable_to_delete_user": "Não foi possível deletar o utilizador",
-    "unable_to_download_files": "Não foi possível transferir ficheiros",
-    "unable_to_edit_exclusion_pattern": "Não foi possível editar o padrão de exclusão",
-    "unable_to_edit_import_path": "Não foi possível editar o caminho de importação",
-    "unable_to_empty_trash": "Não foi possível esvaziar a lixeira",
-    "unable_to_enter_fullscreen": "Não foi possível entrar em modo de tela cheia",
-    "unable_to_exit_fullscreen": "Não foi possível sair do modo de tela cheia",
-    "unable_to_get_comments_number": "Não foi possível obter número de comentários",
-    "unable_to_get_shared_link": "Falha ao obter link compartilhado",
-    "unable_to_hide_person": "Não foi possível esconder a pessoa",
-    "unable_to_link_oauth_account": "Não foi possível associar a conta OAuth",
-    "unable_to_load_album": "Não foi possível carregar o álbum",
-    "unable_to_load_asset_activity": "Não foi possível carregar as atividades do ativo",
-    "unable_to_load_items": "Não foi possível carregar os items",
-    "unable_to_load_liked_status": "Não foi possível carregar os status de gostei",
-    "unable_to_log_out_all_devices": "Não foi possível terminar a sessão em todos os dispositivos",
-    "unable_to_log_out_device": "Não foi possível terminar a sessão no dispositivo",
-    "unable_to_login_with_oauth": "Não foi possível iniciar sessão com OAuth",
-    "unable_to_play_video": "Não foi possível reproduzir o vídeo",
-    "unable_to_reassign_assets_existing_person": "Não é possível reatribuir arquivos para {name, select, null {uma pessoa existente} other {{name}}}",
-    "unable_to_reassign_assets_new_person": "Não é possível reatribuir os arquivos a uma nova pessoa",
-    "unable_to_refresh_user": "Não foi possível atualizar o utilizador",
-    "unable_to_remove_album_users": "Não foi possível remover utilizador do álbum",
-    "unable_to_remove_api_key": "Não foi possível a Chave de API",
-    "unable_to_remove_assets_from_shared_link": "Não é possível remover os arquivos do link compartilhado",
-    "unable_to_remove_comment": "",
-    "unable_to_remove_library": "Não foi possível remover a biblioteca",
-    "unable_to_remove_offline_files": "Não foi possível remover arquivos offline",
-    "unable_to_remove_partner": "Não foi possível remover parceiro",
-    "unable_to_remove_reaction": "Não foi possível remover a reação",
-    "unable_to_remove_user": "",
-    "unable_to_repair_items": "Não foi possível reparar os itens",
-    "unable_to_reset_password": "Não foi possível resetar a senha",
-    "unable_to_resolve_duplicate": "Não foi possível resolver a duplicidade",
-    "unable_to_restore_assets": "Não foi possível restaurar arquivos",
-    "unable_to_restore_trash": "Não foi possível restaurar itens da lixeira",
-    "unable_to_restore_user": "Não foi possível restaurar utilizador",
-    "unable_to_save_album": "Não foi possível salvar o álbum",
-    "unable_to_save_api_key": "Não foi possível salvar a Chave de API",
-    "unable_to_save_date_of_birth": "Não foi possível guardar a data de nascimento",
-    "unable_to_save_name": "Não foi possível salvar o nome",
-    "unable_to_save_profile": "Não foi possível salvar o perfil",
-    "unable_to_save_settings": "Não foi possível salvar as configurações",
-    "unable_to_scan_libraries": "Não foi possível escanear as bibliotecas",
-    "unable_to_scan_library": "Não foi possível escanear a biblioteca",
-    "unable_to_set_feature_photo": "Não é possível definir a foto do recurso",
-    "unable_to_set_profile_picture": "Não foi possível definir a foto de perfil",
-    "unable_to_submit_job": "Não foi possível enviar o trabalho",
-    "unable_to_trash_asset": "Não foi possível enviar o ativo para a lixeira",
-    "unable_to_unlink_account": "Não foi possível desvincular conta",
-    "unable_to_update_album_cover": "Não foi possível atualizar a capa do álbum",
-    "unable_to_update_album_info": "Não foi possível atualizar informações do álbum",
-    "unable_to_update_library": "Não foi possível atualizar a biblioteca",
-    "unable_to_update_location": "Não foi possível atualizar a localização",
-    "unable_to_update_settings": "Não foi possível atualizar as configurações",
-    "unable_to_update_timeline_display_status": "Não foi possível atualizar o modo de visualização da linha do tempo",
-    "unable_to_update_user": "Não foi possível atualizar o usuário",
-    "unable_to_upload_file": "Não foi possível carregar o ficheiro"
-  },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
-  "exif": "Exif",
-  "exit_slideshow": "Sair da apresentação",
-  "expand_all": "Expandir tudo",
-  "expire_after": "Expira depois",
-  "expired": "Expirou",
-  "expires_date": "Expira em {date}",
-  "explore": "Explorar",
-  "explorer": "Explorador",
-  "export": "Exportar",
-  "export_as_json": "Exportar como JSON",
-  "extension": "Extensão",
-  "external": "Externo",
-  "external_libraries": "Bibliotecas externas",
-  "face_unassigned": "Sem atribuição",
-  "failed_to_get_people": "Falha ao carregar as pessoas",
-  "favorite": "Favorito",
-  "favorite_or_unfavorite_photo": "Marque ou desmarque a foto como favorita",
-  "favorites": "Favoritos",
-  "feature": "",
-  "feature_photo_updated": "Foto principal atualizada",
-  "featurecollection": "",
-  "file_name": "Nome do arquivo",
-  "file_name_or_extension": "Nome do arquivo ou extensão",
-  "filename": "Nome do arquivo",
-  "files": "",
-  "filetype": "Tipo de arquivo",
-  "filter_people": "Filtrar pessoas",
-  "find_them_fast": "Encontre pelo nome em uma pesquisa",
-  "fix_incorrect_match": "Corrigir correspondência incorreta",
-  "folders": "Pastas",
-  "force_re-scan_library_files": "Força escanear novamente todos os arquivos da biblioteca",
-  "forward": "Para frente",
-  "general": "Geral",
-  "get_help": "Obter Ajuda",
-  "getting_started": "Primeiros passos",
-  "go_back": "Voltar",
-  "go_to_search": "Ir para a pesquisa",
-  "go_to_share_page": "Ir para a página de compartilhamento",
-  "group_albums_by": "Agrupar álbuns por...",
-  "group_no": "Sem agrupamento",
-  "group_owner": "Agrupar por dono",
-  "group_year": "Agrupar por ano",
-  "has_quota": "Há cota",
-  "hi_user": "Olá {name} ({email})",
-  "hide_all_people": "Ocultar todas as pessoas",
-  "hide_gallery": "Ocultar galeria",
-  "hide_named_person": "Ocultar pessoa {name}",
-  "hide_password": "Ocultar senha",
-  "hide_person": "Ocultar pessoa",
-  "hide_unnamed_people": "Ocultar pessoas sem nome",
-  "host": "Host",
-  "hour": "Hora",
-  "image": "Imagem",
-  "image_alt_text_date": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {date}",
-  "image_alt_text_date_1_person": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1} em {date}",
-  "image_alt_text_date_2_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1} e {person2} em {date}",
-  "image_alt_text_date_3_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1}, {person2}, e {person3} em {date}",
-  "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1}, {person2}, e outras {additionalCount, number} pessoas em {date}",
-  "image_alt_text_date_place": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} em {date}",
-  "image_alt_text_date_place_1_person": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1} em {date}",
-  "image_alt_text_date_place_2_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1} e {person2} em {date}",
-  "image_alt_text_date_place_3_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1}, {person2}, e {person3} em {date}",
-  "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1}, {person2}, e outras {additionalCount, number} pessoas em {date}",
-  "img": "",
-  "immich_logo": "Logo do Immich",
-  "immich_web_interface": "Interface web do Immich",
-  "import_from_json": "Importar do JSON",
-  "import_path": "Caminho de importação",
-  "in_albums": "Em {count, plural, one {# álbum} other {# álbuns}}",
-  "in_archive": "Arquivado",
-  "include_archived": "Incluir arquivados",
-  "include_shared_albums": "Incluir álbuns compartilhados",
-  "include_shared_partner_assets": "Incluir arquivos compartilhados por parceiros",
-  "individual_share": "Compartilhamento único",
-  "info": "Informações",
-  "interval": {
-    "day_at_onepm": "Todo dia, 1pm",
-    "hours": "A cada {hours, plural, one {hora} other {{hours, number} horas}}",
-    "night_at_midnight": "Toda noite, meia noite",
-    "night_at_twoam": "Toda noite, 2am"
-  },
-  "invite_people": "Convidar Pessoas",
-  "invite_to_album": "Convidar para o álbum",
-  "items_count": "{count, plural, one {item #} other {itens #}}",
-  "job_settings_description": "",
-  "jobs": "Trabalhos",
-  "keep": "Manter",
-  "keep_all": "Manter Todos",
-  "keyboard_shortcuts": "Atalhos do teclado",
-  "language": "Idioma",
-  "language_setting_description": "Selecione seu Idioma preferido",
-  "last_seen": "Visto pela ultima vez",
-  "latest_version": "Versão mais recente",
-  "latitude": "Latitude",
-  "leave": "Sair",
-  "let_others_respond": "Permitir respostas",
-  "level": "Nível",
-  "library": "Biblioteca",
-  "library_options": "Opções da biblioteca",
-  "light": "Claro",
-  "like_deleted": "Curtida removida",
-  "link_options": "Opções do Link",
-  "link_to_oauth": "Link do OAuth",
-  "linked_oauth_account": "Conta OAuth Vinculada",
-  "list": "Lista",
-  "loading": "Carregando",
-  "loading_search_results_failed": "Falha ao carregar os resultados da pesquisa",
-  "log_out": "Sair",
-  "log_out_all_devices": "Sair de todos dispositivos",
-  "logged_out_all_devices": "Sessão terminada em todos os dispositivos",
-  "logged_out_device": "Sessão terminada no dispositivo",
-  "login": "Iniciar sessão",
-  "login_has_been_disabled": "Login foi desativado.",
-  "logout_all_device_confirmation": "Tem certeza de que deseja desconectar todos os dispositivos?",
-  "logout_this_device_confirmation": "Tem certeza de que deseja sair deste dispositivo?",
-  "longitude": "Longitude",
-  "look": "Estilo",
-  "loop_videos": "Repetir vídeos",
-  "loop_videos_description": "Ative para repetir os vídeos automaticamente durante a exibição.",
-  "make": "Marca",
-  "manage_shared_links": "Gerir links partilhados",
-  "manage_sharing_with_partners": "Gerenciar compartilhamento com parceiros",
-  "manage_the_app_settings": "Gerenciar configurações do app",
-  "manage_your_account": "Gerenciar sua conta",
-  "manage_your_api_keys": "Gerenciar suas Chaves de API",
-  "manage_your_devices": "Gerenciar seus dispositivos logados",
-  "manage_your_oauth_connection": "Gerenciar sua conexão OAuth",
-  "map": "Mapa",
-  "map_marker_for_images": "Marcador no mapa para fotos tiradas em {city}, {country}",
-  "map_marker_with_image": "Marcador de mapa com imagem",
-  "map_settings": "Definições do mapa",
-  "matches": "Correspondências",
-  "media_type": "Tipo de mídia",
-  "memories": "Memórias",
-  "memories_setting_description": "Gerencie o que vê em suas memórias",
-  "memory": "Memória",
-  "memory_lane_title": "Memórias {title}",
-  "menu": "Menu",
-  "merge": "Mesclar",
-  "merge_people": "Mesclar pessoas",
-  "merge_people_limit": "Só é possível mesclar até 5 faces de uma só vez",
-  "merge_people_prompt": "Tem certeza que deseja mesclar estas pessoas? Esta ação é irreversível.",
-  "merge_people_successfully": "Pessoas mescladas com sucesso",
-  "merged_people_count": "Mesclada {count, plural, one {1 pessoa} other {# pessoas}}",
-  "minimize": "Minimizar",
-  "minute": "Minuto",
-  "missing": "Faltando",
-  "model": "Modelo",
-  "month": "Mês",
-  "more": "Mais",
-  "moved_to_trash": "Enviado para a lixeira",
-  "my_albums": "Meus Álbuns",
-  "name": "Nome",
-  "name_or_nickname": "Nome ou apelido",
-  "never": "Nunca",
-  "new_album": "Novo Álbum",
-  "new_api_key": "Nova Chave de API",
-  "new_password": "Nova senha",
-  "new_person": "Nova Pessoa",
-  "new_user_created": "Novo utilizador criado",
-  "new_version_available": "NOVA VERSÃO DISPONÍVEL",
-  "newest_first": "Mais recente primeiro",
-  "next": "Avançar",
-  "next_memory": "Próxima memória",
-  "no": "Não",
-  "no_albums_message": "Crie um álbum para organizar suas fotos e vídeos",
-  "no_albums_with_name_yet": "Parece que você ainda não tem nenhum álbum com este nome.",
-  "no_albums_yet": "Parece que você ainda não tem nenhum álbum.",
-  "no_archived_assets_message": "Arquive fotos e vídeos para os ocultar da sua visualização de fotos",
-  "no_assets_message": "CLIQUE PARA CARREGAR SUA PRIMEIRA FOTO",
-  "no_duplicates_found": "Nenhuma duplicidade foi encontrada.",
-  "no_exif_info_available": "Sem informações exif disponíveis",
-  "no_explore_results_message": "Carregue mais fotos para explorar sua coleção.",
-  "no_favorites_message": "Adicione aos favoritos para encontrar suas melhores fotos e vídeos rapidamente",
-  "no_libraries_message": "Crie uma biblioteca externa para ver suas fotos e vídeos",
-  "no_name": "Sem nome",
-  "no_places": "Sem lugares",
-  "no_results": "Sem resultados",
-  "no_results_description": "Tente um sinônimo ou uma palavra-chave mais comum",
-  "no_shared_albums_message": "Crie um álbum para compartilhar fotos e vídeos com pessoas em sua rede",
-  "not_in_any_album": "Fora de álbum",
-  "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar o rótulo de armazenamento a arquivos carregados anteriormente, execute o",
-  "note_unlimited_quota": "Nota: Digite 0 para cota ilimitada",
-  "notes": "Notas",
-  "notification_toggle_setting_description": "Habilitar notificações por e-mail",
-  "notifications": "Notificações",
-  "notifications_setting_description": "Gerenciar notificações",
-  "oauth": "OAuth",
-  "offline": "Offline",
-  "offline_paths": "Caminhos offline",
-  "offline_paths_description": "Estes resultados podem ser devidos a arquivos deletados manualmente e que não são parte de uma biblioteca externa.",
-  "ok": "Ok",
-  "oldest_first": "Mais antigo primeiro",
-  "onboarding": "Integração",
-  "onboarding_privacy_description": "Os seguintes recursos (opcionais) dependem de serviços externos e podem ser desabilitados a qualquer momento nas configurações de administração.",
-  "onboarding_theme_description": "Escolha um tema de cor para sua instância. Você pode alterar isso mais tarde em suas configurações.",
-  "onboarding_welcome_description": "Vamos configurar sua instância com algumas configurações comuns.",
-  "onboarding_welcome_user": "Bem-vindo(a), {user}",
-  "online": "Online",
-  "only_favorites": "Somente favoritos",
-  "only_refreshes_modified_files": "Somente atualize arquivos modificados",
-  "open_in_map_view": "Abrir na visualização do mapa",
-  "open_in_openstreetmap": "Abrir no OpenStreetMap",
-  "open_the_search_filters": "Abre os filtros de pesquisa",
-  "options": "Opções",
-  "or": "ou",
-  "organize_your_library": "Organize sua biblioteca",
-  "original": "original",
-  "other": "Outro",
-  "other_devices": "Outros dispositivos",
-  "other_variables": "Outras variáveis",
-  "owned": "Seu",
-  "owner": "Dono",
-  "partner": "Parceiro",
-  "partner_can_access": "{partner} pode acessar",
-  "partner_can_access_assets": "Todas as suas fotos e vídeos, exceto os Arquivados ou Excluídos",
-  "partner_can_access_location": "A localização onde as fotos foram tiradas",
-  "partner_sharing": "Compartilhamento com Parceiro",
-  "partners": "Parceiros",
-  "password": "Senha",
-  "password_does_not_match": "As senhas não são iguais",
-  "password_required": "A senha é obrigatório",
-  "password_reset_success": "Senha resetada com sucesso",
-  "past_durations": {
-    "days": "{days, plural, one {Último dia} other {# últimos dias}}",
-    "hours": "Últimas {hours, plural, one {horas} other {# horas}}",
-    "years": "{years, plural, one {Último ano} other {Últimos # anos}}"
-  },
-  "path": "Caminho",
-  "pattern": "Padrão",
-  "pause": "Interromper",
-  "pause_memories": "Interromper memórias",
-  "paused": "Interrompido",
-  "pending": "Pendente",
-  "people": "Pessoas",
-  "people_edits_count": "{count, plural, one {# pessoa editada} other {# pessoas editadas}}",
-  "people_sidebar_description": "Exibe o link Pessoas na barra lateral",
-  "perform_library_tasks": "",
-  "permanent_deletion_warning": "Aviso para deletar permanentemente",
-  "permanent_deletion_warning_setting_description": "Exibe um aviso ao excluir arquivos de forma permanente",
-  "permanently_delete": "Deletar permanentemente",
-  "permanently_delete_assets_count": "Excluir permanentemente {count, plural, one {arquivo} other {arquivos}}",
-  "permanently_delete_assets_prompt": "Tem certeza que deseja excluir permanentemente {count, plural, one {esse arquivo?} other {estes <b>#</b> arquivos?}} Essa ação também removerá {count, plural, one {isto do} other {isto dos}} álbum(s).",
-  "permanently_deleted_asset": "Ativo deletado permanentemente",
-  "permanently_deleted_assets": "{count, plural, one {# ativo deletado} other {# ativos deletados}} permanentemente",
-  "permanently_deleted_assets_count": "{count, plural, one {# arquivo excluído} other {# arquivos excluídos}} permanentemente",
-  "person": "Pessoa",
-  "person_hidden": "{name}{hidden, select, true { (oculto)} other {}}",
-  "photo_shared_all_users": "Parece que você compartilhou suas fotos com todos os usuários ou não tem nenhum usuário para compartilhar.",
-  "photos": "Fotos",
-  "photos_and_videos": "Fotos & Vídeos",
-  "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}",
-  "photos_from_previous_years": "Fotos de anos anteriores",
-  "pick_a_location": "Selecione uma localização",
-  "place": "Lugar",
-  "places": "Lugares",
-  "play": "Reproduzir",
-  "play_memories": "Reproduzir memórias",
-  "play_motion_photo": "Reproduzir foto em movimento",
-  "play_or_pause_video": "Reproduzir ou Pausar vídeo",
-  "point": "",
-  "port": "Porta",
-  "preset": "Predefinição",
-  "preview": "Pré-visualizar",
-  "previous": "Anterior",
-  "previous_memory": "Memória anterior",
-  "previous_or_next_photo": "Foto anterior ou próxima",
-  "primary": "Primário",
-  "privacy": "Privacidade",
-  "profile_image_of_user": "Imagem de perfil de {user}",
-  "profile_picture_set": "Foto de perfil definida.",
-  "public_album": "Álbum público",
-  "public_share": "Compartilhar Publicamente",
-  "purchase_account_info": "Apoiador",
-  "purchase_activated_subtitle": "Agradecemos por apoiar o Immich e software de código aberto",
-  "purchase_activated_time": "Ativado em {date, date}",
-  "purchase_activated_title": "Sua chave foi ativada com sucesso",
-  "purchase_button_activate": "Ativar",
-  "purchase_button_buy": "Comprar",
-  "purchase_button_buy_immich": "Comprar Immich",
-  "purchase_button_never_show_again": "Nunca mostrar novamente",
-  "purchase_button_reminder": "Relembrar-me daqui a 30 dias",
-  "purchase_button_remove_key": "Remover chave",
-  "purchase_button_select": "Selecionar",
-  "purchase_failed_activation": "Falha ao ativar! Verifique seu e-mail para obter a chave de produto correta!",
-  "purchase_individual_description_1": "Para uma pessoa",
-  "purchase_individual_description_2": "Status de apoiador",
-  "purchase_individual_title": "Particular",
-  "purchase_input_suggestion": "Tem uma chave de produto? Insira a chave abaixo",
-  "purchase_license_subtitle": "Compre Immich para apoiar o desenvolvimento contínuo do serviço",
-  "purchase_lifetime_description": "Compra vitalícia",
-  "purchase_option_title": "OPÇÕES DE COMPRA",
-  "purchase_panel_info_1": "O desenvolvimento do Immich requer muito tempo e esforço, e temos engenheiros a tempo inteiro a trabalhar nele para melhorá-lo quanto possível. A nossa missão é para que o software de código aberto e práticas de negócio éticas se tornem numa fonte de rendimento sustentável para os desenvolvedores e criar um ecossistema que respeite a privacidade dos utilizadores e que ofereça alternativas reais a serviços cloud explorativos.",
-  "purchase_panel_info_2": "Como estamos comprometidos em não adicionar acesso pago, esta compra não lhe dará nenhum recurso adicional no Immich. Contamos com usuários como você para dar suporte ao desenvolvimento contínuo do Immich.",
-  "purchase_panel_title": "Apoie o projeto",
-  "purchase_per_server": "Por servidor",
-  "purchase_per_user": "Por utilizador",
-  "purchase_remove_product_key": "Remover chave de produto",
-  "purchase_remove_product_key_prompt": "Tem certeza de que deseja remover a chave do produto?",
-  "purchase_remove_server_product_key": "Remover chave do produto do servidor",
-  "purchase_remove_server_product_key_prompt": "Tem certeza de que deseja remover a chave do produto do servidor?",
-  "purchase_server_description_1": "Para o servidor inteiro",
-  "purchase_server_description_2": "Status de apoiador",
-  "purchase_server_title": "Servidor",
-  "purchase_settings_server_activated": "A chave de produto para servidor é gerida pelo administrador",
-  "range": "",
-  "rating": "Classificação por estrelas",
-  "rating_clear": "Limpar classificação",
-  "rating_count": "{contar, plural, um {# estrela} outro {# estrelas}}",
-  "rating_description": "Exibir a classificação exif no painel de informações",
-  "raw": "",
-  "reaction_options": "Opções de reação",
-  "read_changelog": "Ler Novidades",
-  "reassign": "Reatribuir",
-  "reassigned_assets_to_existing_person": "Reatribuir {count, plural, one {# arquivo} other {# arquivos}} PARA {name, select, null {uma pessoa existente} other {{name}}}",
-  "reassigned_assets_to_new_person": "Reatribuir {count, plural, one {# arquivo} other {# arquivos}} a uma nova pessoa",
-  "reassing_hint": "Atribuir ativos selecionados a uma pessoa existente",
-  "recent": "Recente",
-  "recent_searches": "Pesquisas recentes",
-  "refresh": "Atualizar",
-  "refresh_encoded_videos": "Atualizar vídeos codificados",
-  "refresh_metadata": "Atualizar metadados",
-  "refresh_thumbnails": "Atualizar miniaturas",
-  "refreshed": "Atualizado",
-  "refreshes_every_file": "Atualiza todos arquivos",
-  "refreshing_encoded_video": "Atualizando vídeo codificado",
-  "refreshing_metadata": "A atualizar metadados",
-  "regenerating_thumbnails": "A atualizar miniaturas",
-  "remove": "Remover",
-  "remove_assets_album_confirmation": "Tem certeza que deseja remover {count, plural, one {# arquivo} other {# arquivos}} do álbum?",
-  "remove_assets_shared_link_confirmation": "Tem certeza que deseja remover {count, plural, one {# arquivo} other {# arquivos}} desse link compartilhado?",
-  "remove_assets_title": "Remover arquivos?",
-  "remove_custom_date_range": "Remover intervalo de datas personalizado",
-  "remove_from_album": "Remover do álbum",
-  "remove_from_favorites": "Remover dos favoritos",
-  "remove_from_shared_link": "Remover do link compartilhado",
-  "remove_offline_files": "Remover arquivos offline",
-  "remove_user": "Remover utilizador",
-  "removed_api_key": "Removido a Chave de API: {name}",
-  "removed_from_archive": "Removido do arquivo",
-  "removed_from_favorites": "Removido dos favoritos",
-  "removed_from_favorites_count": "{count, plural, other {Removido #}} dos favoritos",
-  "rename": "Renomear",
-  "repair": "Reparar",
-  "repair_no_results_message": "Arquivos perdidos ou não rastreados aparecem aqui",
-  "replace_with_upload": "Substituir",
-  "repository": "Repositório",
-  "require_password": "Proteger com senha",
-  "require_user_to_change_password_on_first_login": "Obrigar utilizador a alterar a senha após primeiro início de sessão",
-  "reset": "Resetar",
-  "reset_password": "Resetar senha",
-  "reset_people_visibility": "Resetar pessoas ocultas",
-  "reset_settings_to_default": "",
-  "reset_to_default": "Repor predefinições",
-  "resolve_duplicates": "Resolver itens duplicados",
-  "resolved_all_duplicates": "Todas duplicidades resolvidas",
-  "restore": "Restaurar",
-  "restore_all": "Restaurar tudo",
-  "restore_user": "Restaurar utilizador",
-  "restored_asset": "Arquivo restaurado",
-  "resume": "Continuar",
-  "retry_upload": "Tentar carregar novamente",
-  "review_duplicates": "Revisar duplicidade",
-  "role": "Função",
-  "role_editor": "Editor",
-  "role_viewer": "Visualizador",
-  "save": "Guardar",
-  "saved_api_key": "Chave de API salva",
-  "saved_profile": "Perfil Salvo",
-  "saved_settings": "Configurações salvas",
-  "say_something": "Diga algo",
-  "scan_all_libraries": "Escanear Todas Bibliotecas",
-  "scan_all_library_files": "Re-escanear todos arquivos da biblioteca",
-  "scan_new_library_files": "Escanear novos arquivos na biblioteca",
-  "scan_settings": "Opções de escanear",
-  "scanning_for_album": "Escaneando por álbum...",
-  "search": "Pesquisar",
-  "search_albums": "Pesquisar álbuns",
-  "search_by_context": "Pesquisar por contexto",
-  "search_by_filename": "Pesquisar por nome de ficheiro ou extensão",
-  "search_by_filename_example": "por exemplo, IMG_1234.JPG ou PNG",
-  "search_camera_make": "Pesquisar câmeras da marca...",
-  "search_camera_model": "Pesquisar câmera do modelo...",
-  "search_city": "Pesquisar cidade...",
-  "search_country": "Pesquisar país...",
-  "search_for_existing_person": "Pesquisar por pessoas",
-  "search_no_people": "Nenhuma pessoa",
-  "search_no_people_named": "Nenhuma pessoa chamada \"{name}\"",
-  "search_people": "Pesquisar pessoas",
-  "search_places": "Pesquisar lugares",
-  "search_state": "Pesquisar estado...",
-  "search_timezone": "Pesquisar fuso horário...",
-  "search_type": "Pesquisar tipo",
-  "search_your_photos": "Pesquisar fotos",
-  "searching_locales": "Pesquisar Lugares....",
-  "second": "Segundo",
-  "see_all_people": "Ver todas as pessoas",
-  "select_album_cover": "Escolher capa do álbum",
-  "select_all": "Selecionar todos",
-  "select_all_duplicates": "Selecionar todos os itens duplicados",
-  "select_avatar_color": "Selecionar cor do avatar",
-  "select_face": "Selecionar face",
-  "select_featured_photo": "Selecionar foto principal",
-  "select_from_computer": "Selecionar do computador",
-  "select_keep_all": "Marcar manter em todos",
-  "select_library_owner": "Selecione o dono da biblioteca",
-  "select_new_face": "Selecionar nova face",
-  "select_photos": "Selecionar fotos",
-  "select_trash_all": "Marcar lixo em todos",
-  "selected": "Selecionados",
-  "selected_count": "{count, plural, other {# selecionado}}",
-  "send_message": "Enviar mensagem",
-  "send_welcome_email": "Enviar E-mail de boas vindas",
-  "server": "Servidor",
-  "server_offline": "Servidor Offline",
-  "server_online": "Servidor Online",
-  "server_stats": "Status do servidor",
-  "server_version": "Versão do servidor",
-  "set": "Definir",
-  "set_as_album_cover": "Definir como capa do álbum",
-  "set_as_profile_picture": "Definir como foto de perfil",
-  "set_date_of_birth": "Definir data de nascimento",
-  "set_profile_picture": "Definir foto de perfil",
-  "set_slideshow_to_fullscreen": "Apresentação em tela cheia",
-  "settings": "Configurações",
-  "settings_saved": "Configurações salvas",
-  "share": "Compartilhar",
-  "shared": "Compartilhado",
-  "shared_by": "Compartilhado por",
-  "shared_by_user": "Partilhado por {user}",
-  "shared_by_you": "Compartilhado por você",
-  "shared_from_partner": "Fotos de {partner}",
-  "shared_link_options": "Opções de link compartilhado",
-  "shared_links": "Links compartilhados",
-  "shared_photos_and_videos_count": "{assetCount, plural, other {# Fotos & videos compartilhados.}}",
-  "shared_with_partner": "Compartilhado com {partner}",
-  "sharing": "Compartilhar",
-  "sharing_enter_password": "Por favor, digite a senha para visualizar esta página.",
-  "sharing_sidebar_description": "Exibe o link Compartilhar na barra lateral",
-  "shift_to_permanent_delete": "Pressione ⇧ para excluir o arquivo permanentemente",
-  "show_album_options": "Exibir opções do álbum",
-  "show_albums": "Mostrar álbuns",
-  "show_all_people": "Mostrar todas as pessoas",
-  "show_and_hide_people": "Mostrar & ocultar pessoas",
-  "show_file_location": "Exibir local do arquivo",
-  "show_gallery": "Exibir galeria",
-  "show_hidden_people": "Exibir pessoas ocultadas",
-  "show_in_timeline": "Exibir na linha do tempo",
-  "show_in_timeline_setting_description": "Exibe fotos e vídeos deste utilizador na sua linha do tempo",
-  "show_keyboard_shortcuts": "Exibir atalhos do teclado",
-  "show_metadata": "Mostrar metadados",
-  "show_or_hide_info": "Exibir ou ocultar informações",
-  "show_password": "Exibir senha",
-  "show_person_options": "Exibir opções da pessoa",
-  "show_progress_bar": "Exibir barra de progresso",
-  "show_search_options": "Exibir opções de pesquisa",
-  "show_supporter_badge": "Emblema de apoiador",
-  "show_supporter_badge_description": "Mostrar um emblema de apoiador",
-  "shuffle": "Aleatório",
-  "sign_out": "Sair",
-  "sign_up": "Registrar",
-  "size": "Tamanho",
-  "skip_to_content": "Pular para o conteúdo",
-  "slideshow": "Apresentação",
-  "slideshow_settings": "Opções de apresentação",
-  "sort_albums_by": "Ordenar álbuns por...",
-  "sort_created": "Data de criação",
-  "sort_items": "Número de itens",
-  "sort_modified": "Data de modificação",
-  "sort_oldest": "Foto mais antiga",
-  "sort_recent": "Foto mais recente",
-  "sort_title": "Título",
-  "source": "Fonte",
-  "stack": "Empilhar",
-  "stack_duplicates": "Empilhar duplicados",
-  "stack_select_one_photo": "Selecione uma foto principal para a pilha",
-  "stack_selected_photos": "Empilhar fotos selecionadas",
-  "stacked_assets_count": "Empilhado {count, plural, one {# arquivo} other {# arquivos}}",
-  "stacktrace": "Stacktrace",
-  "start": "Início",
-  "start_date": "Data inicial",
-  "state": "Estado",
-  "status": "Status",
-  "stop_motion_photo": "Parar foto em movimento",
-  "stop_photo_sharing": "Parar de partilhar as suas fotos?",
-  "stop_photo_sharing_description": "{partner} não terá mais acesso às suas fotos.",
-  "stop_sharing_photos_with_user": "Parar de compartilhar as fotos com este utilizador",
-  "storage": "Espaço de armazenamento",
-  "storage_label": "Rótulo de armazenamento",
-  "storage_usage": "utilizado {used} de {available}",
-  "submit": "Enviar",
-  "suggestions": "Sugestões",
-  "sunrise_on_the_beach": "Nascer do sol na praia",
-  "swap_merge_direction": "Alternar direção da mesclagem",
-  "sync": "Sincronizar",
-  "template": "Modelo",
-  "theme": "Tema",
-  "theme_selection": "Selecionar tema",
-  "theme_selection_description": "Defina automaticamente o tema como claro ou escuro com base na preferência do sistema do seu navegador",
-  "they_will_be_merged_together": "Eles serão mesclados",
-  "time_based_memories": "Memórias baseada no tempo",
-  "timezone": "Fuso horário",
-  "to_archive": "Arquivar",
-  "to_change_password": "Alterar senha",
-  "to_favorite": "Favorito",
-  "to_login": "Iniciar sessão",
-  "to_trash": "Lixo",
-  "toggle_settings": "Alternar configurações",
-  "toggle_theme": "Alternar tema",
-  "toggle_visibility": "Alternar visibilidade",
-  "total_usage": "Total utilizado",
-  "trash": "Lixeira",
-  "trash_all": "Todos para o lixo",
-  "trash_count": "Lixeira {count, number}",
-  "trash_delete_asset": "Excluir arquivo",
-  "trash_no_results_message": "Fotos e vídeos enviados para o lixo aparecem aqui.",
-  "trashed_items_will_be_permanently_deleted_after": "Os itens da lixeira são deletados permanentemente após {days, plural, one {# dia} other {# dias}}.",
-  "type": "Tipo",
-  "unarchive": "Desarquivar",
-  "unarchived": "Restaurado do arquivo",
-  "unarchived_count": "{count, plural, other {Não arquivado #}}",
-  "unfavorite": "Remover favorito",
-  "unhide_person": "Exibir pessoa",
-  "unknown": "Desconhecido",
-  "unknown_album": "",
-  "unknown_year": "Ano desconhecido",
-  "unlimited": "Ilimitado",
-  "unlink_oauth": "Desvincular OAuth",
-  "unlinked_oauth_account": "Conta OAuth desvinculada",
-  "unnamed_album": "Álbum sem nome",
-  "unnamed_album_delete_confirmation": "Tem a certeza que pretende remover este album?",
-  "unnamed_share": "Compartilhamento sem nome",
-  "unsaved_change": "Alteração não guardada",
-  "unselect_all": "Limpar seleção",
-  "unselect_all_duplicates": "Remover seleção de todos os duplicados",
-  "unstack": "Desempilhar",
-  "unstacked_assets_count": "Desempilhar {count, plural, one {# arquivo} other {# arquivos}}",
-  "untracked_files": "Arquivos não monitorados",
-  "untracked_files_decription": "Estes arquivos não são monitorados pela aplicação. Podem ser resultados de falhas em uma movimentação, carregamentos interrompidos, ou deixados para trás por causa de um problema",
-  "up_next": "A seguir",
-  "updated_password": "Senha atualizada",
-  "upload": "Carregar",
-  "upload_concurrency": "Carregar simultâneo",
-  "upload_errors": "Envio completo com {count, plural, one {# erro} other {# erros}}, atualize a página para ver novos arquivos enviados.",
-  "upload_progress": "Restante(s) {remaining, number} - Processado(s) {processed, number}/{total, number}",
-  "upload_skipped_duplicates": "Ignorado {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}",
-  "upload_status_duplicates": "Duplicados",
-  "upload_status_errors": "Erros",
-  "upload_status_uploaded": "Enviado",
-  "upload_success": "Upload realizado com sucesso, atualize a página para ver os novos ativos de upload.",
-  "url": "URL",
-  "usage": "Uso",
-  "use_custom_date_range": "Usar um intervalo de datas personalizado",
-  "user": "Utilizador",
-  "user_id": "ID do utilizador",
-  "user_liked": "{user} gostou {type, select, photo {dessa foto} video {deste video} asset {deste arquivo} other {disto}}",
-  "user_purchase_settings": "Compra",
-  "user_purchase_settings_description": "Gerencie sua compra",
-  "user_role_set": "Definir {user} como {role}",
-  "user_usage_detail": "Detalhes de uso do utilizador",
-  "username": "Nome do utilizador",
-  "users": "Utilizadores",
-  "utilities": "Utilitários",
-  "validate": "Validar",
-  "variables": "Variáveis",
-  "version": "Versão",
-  "version_announcement_closing": "Seu amigo, Alex",
-  "version_announcement_message": "Olá amigo, há uma nova versão do aplicativo. Reserve um tempo para visitar as <link>histórico de mudanças</link> e garantir que suas configurações <code>docker-compose.yml</code> e <code>.env</code> estejam atualizadas para evitar qualquer configuração incorreta, especialmente se você usar o WatchTower ou qualquer mecanismo que lide com a atualização do seu aplicativo automaticamente.",
-  "video": "Vídeo",
-  "video_hover_setting": "Reproduzir vídeo em miniatura quando passar por cima",
-  "video_hover_setting_description": "Reproduzir vídeo em miniatura quando o mouse está sobre o item. Mesmo quando desativado, a reprodução ainda pode ser iniciada passando sobre o ícone.",
-  "videos": "Vídeos",
-  "videos_count": "{count, plural, one {# Vídeo} other {# Vídeos}}",
-  "view": "Ver",
-  "view_album": "Ver Álbum",
-  "view_all": "Ver tudo",
-  "view_all_users": "Ver todos os utilizadores",
-  "view_links": "Ver links",
-  "view_next_asset": "Ver próximo ativo",
-  "view_previous_asset": "Ver ativo anterior",
-  "view_stack": "Visualizar pilha",
-  "viewer": "Visualizar",
-  "visibility_changed": "Visibilidade alterada para {count, plural, one {# pessoa} other {# pessoas}}",
-  "waiting": "Aguardando",
-  "warning": "Aviso",
-  "week": "Semana",
-  "welcome": "Bem-vindo",
-  "welcome_to_immich": "Bem-vindo ao Immich",
-  "year": "Ano",
-  "years_ago": "Há {years, plural, one {# ano} other {# anos}}",
-  "yes": "Sim",
-  "you_dont_have_any_shared_links": "Não há links compartilhados",
-  "zoom_image": "Ampliar imagem"
-}
diff --git a/web/src/lib/i18n/ro.json b/web/src/lib/i18n/ro.json
deleted file mode 100644
index 534794b6d3..0000000000
--- a/web/src/lib/i18n/ro.json
+++ /dev/null
@@ -1,971 +0,0 @@
-{
-  "about": "Despre",
-  "account": "Cont",
-  "account_settings": "Setări Cont",
-  "acknowledge": "Văzut",
-  "action": "Acţiune",
-  "actions": "Acţiuni",
-  "active": "Activ",
-  "activity": "Activitate",
-  "activity_changed": "Activitatea este {enabled, select, true {activată} other {dezactivată}}",
-  "add": "Adaugă",
-  "add_a_description": "Adaugă o descriere",
-  "add_a_location": "Adaugă locație",
-  "add_a_name": "Adaugă un nume",
-  "add_a_title": "Adaugă un titlu",
-  "add_exclusion_pattern": "Adăugă un model de excludere",
-  "add_import_path": "Adaugă o cale de import",
-  "add_location": "Adaugă o locație",
-  "add_more_users": "Adaugă mai mulți utilizatori",
-  "add_partner": "Adaugă partener",
-  "add_path": "Adaugă o cale",
-  "add_photos": "Adaugă fotografii",
-  "add_to": "Adaugă la...",
-  "add_to_album": "Adaugă în album",
-  "add_to_shared_album": "Adaugă la album partajat",
-  "added_to_archive": "Adăugat la arhivă",
-  "added_to_favorites": "Adaugă la favorite",
-  "added_to_favorites_count": "Adăugat {count, number} la favorite",
-  "admin": {
-    "add_exclusion_pattern_description": "Adăugați modele de excludere. Globing folosind *, ** și ? este suportat. Pentru a ignora toate fișierele din orice director numit „Raw”, utilizați „**/Raw/**”. Pentru a ignora toate fișierele care se termină în „.tif”, utilizați „**/*.tif”. Pentru a ignora o cale absolută, utilizați „/path/to/ignore/**”.",
-    "authentication_settings": "Setări de autentificare",
-    "authentication_settings_description": "Gestionează parola, OAuth și alte setări de autentificare",
-    "authentication_settings_disable_all": "Ești sigur că vrei sa dezactivezi toate metodele de autentificare? Autentificarea va fi complet dezactivată.",
-    "authentication_settings_reenable": "Pentru a reactiva, folosește <link>Comandă Server</link>.",
-    "background_task_job": "Activități de fundal",
-    "check_all": "Bifează toate",
-    "cleared_jobs": "Activități eliminate pentru: {job}",
-    "config_set_by_file": "Configurația este setată în prezent de un fișier de configurare",
-    "confirm_delete_library": "Sigur doriți să ștergeți biblioteca {library}?",
-    "confirm_delete_library_assets": "Sigur doriți să ștergeți această bibliotecă? Aceasta va șterge {count, plural, one {# contained asset} other {all # contained assets}} din Immich și nu poate fi anulată. Fișierele vor rămâne pe disc.",
-    "confirm_email_below": "Pentru a confirma, tastați „{email}” mai jos",
-    "confirm_reprocess_all_faces": "Sigur doriți să reprocesați toate fețele? Acest lucru va șterge și persoanele cu nume.",
-    "confirm_user_password_reset": "Sigur doriți să resetați parola utilizatorului {user}?",
-    "crontab_guru": "",
-    "disable_login": "Dezactivați autentificarea",
-    "disabled": "",
-    "duplicate_detection_job_description": "Rulați învățarea automată pe materiale pentru a detecta imagini similare. Se bazează pe Căutare Inteligentă",
-    "exclusion_pattern_description": "Modelele de excludere vă permit să ignorați fișierele și folderele atunci când vă scanați biblioteca. Acest lucru este util dacă aveți foldere care conțin fișiere pe care nu doriți să le importați, cum ar fi fișierele RAW.",
-    "external_library_created_at": "Bibliotecă externă (creată pe {date})",
-    "external_library_management": "Managementul Bibliotecii Externe",
-    "face_detection": "Detecție facială",
-    "face_detection_description": "Detectează fețele din active folosind învățarea automată. Pentru videoclipuri, este luată în considerare doar miniatura. „Toate” (re)procesează toate activele. „Lipsă” adaugă în coadă active care nu au fost încă procesate. Fețele detectate vor fi puse în coadă pentru recunoașterea facială după finalizarea detectării feței, grupându-le în persoane existente sau noi.",
-    "facial_recognition_job_description": "Grupați fețele detectate în persoane. Acest pas rulează după ce Detectarea Feței este finalizată. „Toate” (re)grupează toate fețele. „Lipsă” adaugă în coadă fețe care nu au o persoană desemnată.",
-    "failed_job_command": "Comanda {command} a eșuat pentru jobul: {job}",
-    "force_delete_user_warning": "AVERTISMENT: Acest lucru va elimina imediat utilizatorul și toate activele sale. Acest lucru nu poate fi anulat și fișierele nu pot fi recuperate.",
-    "forcing_refresh_library_files": "Forțarea reîmprospătării tuturor fișierelor din bibliotecă",
-    "image_format_description": "WebP produce fișiere mai mici decât JPEG, dar este mai lent de codat.",
-    "image_prefer_embedded_preview": "Preferați previzualizarea încorporată",
-    "image_prefer_embedded_preview_setting_description": "Folosiți previzualizările încorporate în fotografiile RAW ca intrare pentru procesarea imaginii, atunci când sunt disponibile. Acest lucru poate produce culori mai precise pentru unele imagini, dar calitatea previzualizării depinde de cameră și imaginea poate avea mai multe artefacte de compresie.",
-    "image_prefer_wide_gamut": "Preferă o gamă largă",
-    "image_prefer_wide_gamut_setting_description": "Utilizați Display P3 pentru miniaturi. Acest lucru păstrează mai bine vibrația imaginilor cu spații de culoare largi, dar imaginile pot apărea diferit pe dispozitivele cu o versiune mai veche de browser. Imaginile sRGB sunt păstrate ca sRGB pentru a evita schimbările de culoare.",
-    "image_preview_format": "Format de previzualizare",
-    "image_preview_resolution": "Previzualizare rezoluție",
-    "image_preview_resolution_description": "Folosit la vizualizarea unei singure fotografii și pentru învățarea automată. Rezoluțiile mai mari pot păstra mai multe detalii, dar codarea durează mai mult, au dimensiuni mai mari ale fișierelor și pot reduce capacitatea de răspuns a aplicației.",
-    "image_quality": "Calitate",
-    "image_quality_description": "Calitatea imaginii de la 1 la 100. Număr mai mare este mai bun pentru calitate dar produce fișiere mai mari, această opțiune afectează imaginile Preview și Thumbnail.",
-    "image_settings": "Setările imaginii",
-    "image_settings_description": "Gestionează calitatea și rezoluția imaginilor generate",
-    "image_thumbnail_format": "Format imagini miniatură",
-    "image_thumbnail_resolution": "Rezoluție imagini miniatură",
-    "image_thumbnail_resolution_description": "Folosit la vizualizarea unor grupuri de fotografii (cronologie principală, vizualizare album etc.). Rezoluțiile mai mari pot păstra mai multe detalii, dar codarea durează mai mult, au dimensiuni mai mari ale fișierelor și pot reduce capacitatea de răspuns a aplicației.",
-    "job_concurrency": "concurență {job}",
-    "job_not_concurrency_safe": "Acest job nu este sigur pentru a rula în concurență.",
-    "job_settings": "Setări sarcină",
-    "job_settings_description": "Administrează concurența sarcinilor",
-    "job_status": "Starea sarcinii",
-    "jobs_delayed": "{jobCount, plural, other {# delayed}}",
-    "jobs_failed": "{jobCount, plural, other {# eșuat}}",
-    "library_created": "Librărie creată:{library}",
-    "library_cron_expression": "Expresie Cron",
-    "library_cron_expression_description": "Setează intervalul de scanare folosind formatul cron. Pentru mai multe informații, vă rugăm referiți-vă la pentru exemplu: <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "presetări expresie cron",
-    "library_deleted": "Bibliotecă ștearsă",
-    "library_import_path_description": "Specificați un folder pentru a îl importa. Acest folder, inclusiv sub-folderele, vor fi scanate pentru imagini și videoclipuri.",
-    "library_scanning": "Scanare Periodică",
-    "library_scanning_description": "Configurează scanarea periodică pentru bibliotecă",
-    "library_scanning_enable_description": "Activează scanarea periodică pentru bibliotecă",
-    "library_settings": "Bibliotecă Externă",
-    "library_settings_description": "Administrează setările pentru biblioteci externe",
-    "library_tasks_description": "Efectuează sarcini asupra bibliotecii",
-    "library_watching_enable_description": "Urmărește bibliotecile externe pentru schimbări ale fișierelor",
-    "library_watching_settings": "Urmărirea bibliotecii (EXPERIMENTAL)",
-    "library_watching_settings_description": "Urmărește automat fișierele schimbate",
-    "logging_enable_description": "Activează înregistrarea log-urilor",
-    "logging_level_description": "Dacă setarea este activată, înregistrează evenimentele cu nivelul.",
-    "logging_settings": "Înregistrare",
-    "machine_learning_clip_model": "Model CLIP",
-    "machine_learning_clip_model_description": "Numele unui model CLIP listat <link>aici</link>. Rețineți că trebuie să rulați din nou funcția „Smart Search” pentru toate imaginile la schimbarea unui model.",
-    "machine_learning_duplicate_detection": "Detectarea duplicatelor",
-    "machine_learning_duplicate_detection_enabled": "Activează detectarea duplicatelor",
-    "machine_learning_duplicate_detection_enabled_description": "Dacă este dezactivată, activele identice vor fi în continuare de-duplicate.",
-    "machine_learning_duplicate_detection_setting_description": "Utilizați încorporările CLIP pentru a găsi dubluri probabile",
-    "machine_learning_enabled": "Activează algoritmii de învățare automată",
-    "machine_learning_enabled_description": "Dacă este dezactivat, toate funcțiile ML vor fi dezactivate indiferent de setările de mai jos.",
-    "machine_learning_facial_recognition": "Recunoaștere Facială",
-    "machine_learning_facial_recognition_description": "Detectează, recunoaște și grupează fețe din imagini",
-    "machine_learning_facial_recognition_model": "Model de recunoaștere facială",
-    "machine_learning_facial_recognition_model_description": "Modelele sunt aranjate descrescător după mărime. Modelele mai mari sunt lente și folosesc multă memorie, dar produc rezultate mai bune. Rețineți că va trebui să rulați din nou Recunoașterea Facială pentru toate imaginile dacă schimbați modelul.",
-    "machine_learning_facial_recognition_setting": "Activează Recunoașterea Facială",
-    "machine_learning_facial_recognition_setting_description": "Dacă este dezactivată, imaginile nu vor fi codificate pentru recunoașterea facială și nu vor popula secțiunea Persoane din pagina Explorare.",
-    "machine_learning_max_detection_distance": "Distanța maximă pentru recunoaștere",
-    "machine_learning_max_detection_distance_description": "Distanța maximă dintre două imagini pentru a le considera duplicate, variind între 0,001-0,1. Valorile mai mari vor detecta mai multe duplicate, dar pot duce la rezultate fals pozitive.",
-    "machine_learning_max_recognition_distance": "Distanța maximă de recunoaștere",
-    "machine_learning_max_recognition_distance_description": "Distanța maximă dintre două fețe pentru a fi considerate aceeași persoană, variind între 0-2. Reducerea acestui prag poate împiedica etichetarea a două persoane ca fiind aceeași persoană, în timp ce creșterea lui poate împiedica etichetarea aceleiași persoane ca fiind două persoane diferite. Rețineți că este mai ușor să unificați două persoane decât să împărțiți o persoană în două, deci, dacă este posibil, alegeți un prag mai mic.",
-    "machine_learning_min_detection_score": "Scor minim de detecție",
-    "machine_learning_min_detection_score_description": "Scorul minim de încredere pentru ca o față să fie detectată de la 0 la 1. Valorile mai mici vor detecta mai multe fețe, dar pot duce la fals pozitive.",
-    "machine_learning_min_recognized_faces": "Fețe minime recunoscute",
-    "machine_learning_min_recognized_faces_description": "Numărul minim de fețe recunoscute pentru ca o persoană să fie creată. Creșterea acestui număr face ca recunoașterea facială să fie mai precisă, cu prețul creșterii șanselor ca o față să nu fie atribuită unei persoane.",
-    "machine_learning_settings": "Setări machine learning",
-    "machine_learning_settings_description": "Gestionați caracteristicile și setările de învățare automată",
-    "machine_learning_smart_search": "Căutare inteligentă",
-    "machine_learning_smart_search_description": "Căutarea semantică a imaginilor utilizând încorporările CLIP",
-    "machine_learning_smart_search_enabled": "Activați căutarea inteligentă",
-    "machine_learning_smart_search_enabled_description": "Dacă este dezactivată, imaginile nu vor fi codificate pentru căutarea inteligentă.",
-    "machine_learning_url_description": "Adresa URL a serverului de învățare automată",
-    "manage_concurrency": "Gestionarea simultaneității",
-    "manage_log_settings": "Administrați setările jurnalului",
-    "map_dark_style": "Mod întunecat",
-    "map_enable_description": "Activare hartă",
-    "map_gps_settings": "Setări Hartă & GPS",
-    "map_gps_settings_description": "Gestionare setări Hartă & GPS (localizare inversă)",
-    "map_implications": "Caracteristica hărții se bazează pe un serviciu extern de planșe (tiles.immich.cloud)",
-    "map_light_style": "Mod deschis",
-    "map_manage_reverse_geocoding_settings": "Gestionare setări <link>Localizare Inversă</link>",
-    "map_reverse_geocoding": "Localizare Inversă",
-    "map_reverse_geocoding_enable_description": "Activați geocodarea inversă",
-    "map_reverse_geocoding_settings": "Setări geocodare inversă",
-    "map_settings": "Hartă",
-    "map_settings_description": "Gestionare setări hartă",
-    "map_style_description": "URL-ul style.json către o temă pentru hartă",
-    "metadata_extraction_job": "Extragere metadata",
-    "metadata_extraction_job_description": "Extragere informații metadata din fiecare fișier cum ar fi localizare GPS, fețe și rezoluție,",
-    "metadata_faces_import_setting": "Activare import fețe",
-    "metadata_faces_import_setting_description": "Importă fețe din datele EXIF ale imaginii și din fișiere tip \"sidecar\"",
-    "metadata_settings": "Setări Metadata",
-    "metadata_settings_description": "Gestionează setările metadata",
-    "migration_job": "Migrare",
-    "migration_job_description": "Migrați miniaturile pentru elemente și fețe la cea mai recentă structură de foldere",
-    "no_paths_added": "Nicio cale adăugată",
-    "no_pattern_added": "Niciun tipar adăugat",
-    "note_apply_storage_label_previous_assets": "Notă: Pentru a aplica Eticheta de Stocare la elementele încărcate anterior, executați",
-    "note_cannot_be_changed_later": "NOTĂ: Nu se va mai putea modifica ulterior!",
-    "note_unlimited_quota": "Notă: Introduceți 0 pentru cotă nelimitată",
-    "notification_email_from_address": "De la adresa",
-    "notification_email_from_address_description": "Adresa expeditorului, spre exemplu: „Immich Photo Server <noreply@immich.app>”",
-    "notification_email_host_description": "Adresa serverului de email (e.g. smtp.immich.app)",
-    "notification_email_ignore_certificate_errors": "Ingnoră erorile de certificat",
-    "notification_email_ignore_certificate_errors_description": "Ignoră erorile de validare a certificatului TLS (nerecomandat)",
-    "notification_email_password_description": "Parola utilizată pentru autentificarea în serverul de email",
-    "notification_email_port_description": "Portul utilizat de serverul de email (ex. 25, 465 sau 587)",
-    "notification_email_sent_test_email_button": "Trimite un email de test și salvează configurația",
-    "notification_email_setting_description": "Setări pentru trimiterea de notificări pe email",
-    "notification_email_test_email": "Trimitere email de test",
-    "notification_email_test_email_failed": "Eroare la trimiterea emailului de test, verificați setările",
-    "notification_email_test_email_sent": "Un email de test a fost trimis la adresa {email}. Vă rugăm să verificați.",
-    "notification_email_username_description": "Numele de utilizator pentru autentificarea pe serverul de email",
-    "notification_enable_email_notifications": "Activare notificări pe email",
-    "notification_settings": "Setări Notificare",
-    "notification_settings_description": "Gestionează setările pentru notificări, inclusiv adresa de email",
-    "oauth_auto_launch": "Pornire automată",
-    "oauth_auto_launch_description": "Lansează automat autorizarea OAuth la accesarea paginii de login",
-    "oauth_auto_register": "Auto înregistrare",
-    "oauth_auto_register_description": "Înregistrează automat utilizatori noi după autentificarea cu OAuth",
-    "oauth_button_text": "Text buton",
-    "oauth_client_id": "ID Client",
-    "oauth_client_secret": "Secret Client",
-    "oauth_enable_description": "Autentifică-te cu OAuth",
-    "oauth_issuer_url": "Emitentul URL",
-    "oauth_mobile_redirect_uri": "URI de redirecționare mobilă",
-    "oauth_mobile_redirect_uri_override": "Înlocuire URI de redirecționare mobilă",
-    "oauth_mobile_redirect_uri_override_description": "Activați atunci când furnizorul OAuth nu permite un URI mobil, precum '{callback}'",
-    "oauth_profile_signing_algorithm": "Algoritm de semnare a profilului",
-    "oauth_profile_signing_algorithm_description": "Algoritm folosit pentru a semna profilul utilizatorului.",
-    "oauth_scope": "Domeniul de aplicare",
-    "oauth_settings": "OAuth",
-    "oauth_settings_description": "Gestionați setările de conectare OAuth",
-    "oauth_settings_more_details": "Pentru mai multe detalii despre aceastǎ funcționalitate, verificǎ <link>documentația</link>.",
-    "oauth_signing_algorithm": "Algoritm de semnare",
-    "oauth_storage_label_claim": "Revendicare eticheta de stocare",
-    "oauth_storage_label_claim_description": "Setați automat eticheta de stocare a utilizatorului la valoarea acestei revendicări.",
-    "oauth_storage_quota_claim": "Revendicare cotă de stocare",
-    "oauth_storage_quota_claim_description": "Setează automat cota de stocare a utilizatorului la valoarea acestei cereri.",
-    "oauth_storage_quota_default": "Cota implicită de stocare (GiB)",
-    "oauth_storage_quota_default_description": "Cota în GiB ce urmează a fi utilizată atunci când nu este furnizată nicio solicitare (introduceți 0 pentru o cotă nelimitată).",
-    "offline_paths": "Cǎi invalide",
-    "offline_paths_description": "Acestea pot fi rezultate în urma ștergerii manuale a fișierelor ce nu fac parte dintr-o bibliotecǎ externǎ.",
-    "password_enable_description": "Autentificare cu email și parolǎ",
-    "password_settings": "Autentificare cu parolǎ",
-    "password_settings_description": "Gestioneazǎ setǎrile de autentificare cu parola",
-    "paths_validated_successfully": "Toate cǎile au fost validate cu succes",
-    "quota_size_gib": "Spațiu de stocare alocat (GiB)",
-    "refreshing_all_libraries": "Bibliotecile sunt în curs de reîmprospǎtare",
-    "registration": "Înregistrare administratori",
-    "registration_description": "Deoarece sunteți primul utilizator de pe sistem, veți fi desemnat ca administrator și sunteți responsabil pentru sarcinile administrative, iar utilizatorii suplimentari vor fi creați de dumneavoastra.",
-    "removing_offline_files": "Eliminarea fișierelor offline",
-    "repair_all": "Reparǎ toate",
-    "repair_matched_items": "{count, plural, one {Potrivit # obiect} other {Potrivite # obiecte}}",
-    "repaired_items": "{count, plural, one {Reparat # obiect} other {Reparate # obiecte}}",
-    "require_password_change_on_login": "Obligǎ utilizatorul sǎ își schimbe parola la prima autentificare",
-    "reset_settings_to_default": "Reseteazǎ setǎrile la valorile implicite",
-    "reset_settings_to_recent_saved": "Reseteazǎ setǎrile la valorile salvate recent",
-    "scanning_library_for_changed_files": "Se scaneazǎ biblioteca pentru fișiere modificate",
-    "scanning_library_for_new_files": "Se scaneazǎ biblioteca pentru fișiere noi",
-    "send_welcome_email": "Trimite email de bun-venit",
-    "server_external_domain_settings": "Domeniu extern",
-    "server_external_domain_settings_description": "Domeniu pentru distribuire publicǎ a scurtǎturilor, incluzând http(s)://",
-    "server_settings": "Setǎri server",
-    "server_settings_description": "Gestioneazǎ setǎrile serverului",
-    "server_welcome_message": "Mesaj de bun-venit",
-    "server_welcome_message_description": "Un mesaj ce este afișat pe pagina de autentificare.",
-    "sidecar_job": "Metadate Sidecar",
-    "sidecar_job_description": "Descoperirea sau sincronizarea metadatelor sidecar din sistemul de fișiere",
-    "slideshow_duration_description": "Numǎrul de secunde pentru afișarea fiecǎrei imagini",
-    "smart_search_job_description": "Rulați machine learning pe active pentru a sprijini căutarea inteligentă",
-    "storage_template_date_time_description": "Momentul creării activului este utilizat pentru informațiile privind data și ora",
-    "storage_template_date_time_sample": "Eșantion de timp {date}",
-    "storage_template_enable_description": "Activați motorul de șabloane de stocare",
-    "storage_template_hash_verification_enabled": "Verificarea hash este activată",
-    "storage_template_hash_verification_enabled_description": "Activează verificarea hash, nu o dezactivați decât dacă sunteți sigur de implicații",
-    "storage_template_migration": "Migrarea șablonului de stocare",
-    "storage_template_migration_description": "Aplicați <link>{template}</link> actual la elementele încărcate anterior",
-    "storage_template_migration_info": "Modificările de șablon se vor aplica numai materialelor noi. Pentru a aplica retroactiv șablonul la materialele încărcate anterior, rulați <link>{job}</link>.",
-    "storage_template_migration_job": "Activitate migrare template stocare",
-    "storage_template_more_details": "Pentru mai multe detalii despre aceasta caracteristică, accesați <template-link>Șablon stocare</template-link> si <implications-link>implicațiile</implications-link>",
-    "storage_template_onboarding_description": "Atunci când este activată, această caracteristică va organiza automat fișierele pe baza unui șablon definit de utilizator. Din cauza unor probleme de stabilitate, aceasta caracteristică este dezactivată implicit. Pentru mai multe informații, te rog sa consulți <link>documentația</link>.",
-    "storage_template_path_length": "Limita de lungime pentru calea aproximativă: <b>{length, number}</b>/{limit, number}",
-    "storage_template_settings": "Șablon stocare",
-    "storage_template_settings_description": "",
-    "system_settings": "Setǎri de sistem",
-    "theme_custom_css_settings": "CSS personalizat",
-    "theme_custom_css_settings_description": "",
-    "theme_settings": "",
-    "theme_settings_description": "",
-    "thumbnail_generation_job_description": "",
-    "transcode_policy_description": "",
-    "transcoding_acceleration_api": "",
-    "transcoding_acceleration_api_description": "",
-    "transcoding_acceleration_nvenc": "NVENC (necesitǎ GPU NVIDIA)",
-    "transcoding_acceleration_qsv": "Quick Sync (necesitǎ CPU Intel de generația a 7-a sau mai mare)",
-    "transcoding_acceleration_rkmpp": "RKMPP (doar pe SOC-uri Rockchip)",
-    "transcoding_acceleration_vaapi": "VAAPI",
-    "transcoding_accepted_audio_codecs": "Codec-uri audio acceptate",
-    "transcoding_accepted_audio_codecs_description": "",
-    "transcoding_accepted_video_codecs": "Codec-uri video acceptate",
-    "transcoding_accepted_video_codecs_description": "",
-    "transcoding_advanced_options_description": "",
-    "transcoding_audio_codec": "Codec audio",
-    "transcoding_audio_codec_description": "",
-    "transcoding_bitrate_description": "",
-    "transcoding_constant_quality_mode": "",
-    "transcoding_constant_quality_mode_description": "",
-    "transcoding_constant_rate_factor": "",
-    "transcoding_constant_rate_factor_description": "",
-    "transcoding_disabled_description": "",
-    "transcoding_hardware_acceleration": "",
-    "transcoding_hardware_acceleration_description": "",
-    "transcoding_hardware_decoding": "",
-    "transcoding_hardware_decoding_setting_description": "",
-    "transcoding_hevc_codec": "",
-    "transcoding_max_b_frames": "",
-    "transcoding_max_b_frames_description": "",
-    "transcoding_max_bitrate": "",
-    "transcoding_max_bitrate_description": "",
-    "transcoding_max_keyframe_interval": "",
-    "transcoding_max_keyframe_interval_description": "",
-    "transcoding_optimal_description": "",
-    "transcoding_preferred_hardware_device": "",
-    "transcoding_preferred_hardware_device_description": "",
-    "transcoding_preset_preset": "",
-    "transcoding_preset_preset_description": "",
-    "transcoding_reference_frames": "",
-    "transcoding_reference_frames_description": "",
-    "transcoding_required_description": "",
-    "transcoding_settings": "",
-    "transcoding_settings_description": "",
-    "transcoding_target_resolution": "",
-    "transcoding_target_resolution_description": "",
-    "transcoding_temporal_aq": "",
-    "transcoding_temporal_aq_description": "",
-    "transcoding_threads": "",
-    "transcoding_threads_description": "",
-    "transcoding_tone_mapping": "",
-    "transcoding_tone_mapping_description": "",
-    "transcoding_tone_mapping_npl": "",
-    "transcoding_tone_mapping_npl_description": "",
-    "transcoding_transcode_policy": "",
-    "transcoding_two_pass_encoding": "",
-    "transcoding_two_pass_encoding_setting_description": "",
-    "transcoding_video_codec": "Codec video",
-    "transcoding_video_codec_description": "VP9 are eficiențǎ mare și compatibilitate web, însǎ transcodarea este de duratǎ mai mare. HEVC se comportǎ asemǎnǎtor, însǎ are compatibilitate web mai micǎ. H.264 este foarte compatibil și rapid în transcodare, însǎ genereazǎ fișiere mult mai mari. AV1 este cel mai eficient codec dar nu este compatibil cu dispozitivele mai vechi.",
-    "trash_enabled_description": "",
-    "trash_number_of_days": "Numǎr de zile",
-    "trash_number_of_days_description": "Numǎr de zile pentru pǎstrarea fișierelor în coșul de gunoi pânǎ la ștergerea permanentǎ",
-    "trash_settings": "Setǎri coș de gunoi",
-    "trash_settings_description": "Gestioneazǎ setǎrile coșului de gunoi",
-    "user_delete_delay_settings": "",
-    "user_delete_delay_settings_description": "",
-    "user_settings": "Setǎri utilizator",
-    "user_settings_description": "Gestioneazǎ setǎrile utilizatorului",
-    "version_check_enabled_description": "Activeazǎ verificarea periodicǎ pe GitHub pentru versiuni noi",
-    "version_check_settings": "Verificare versiune",
-    "version_check_settings_description": "Activeazǎ/dezactiveazǎ notificarea unei noi versiuni",
-    "video_conversion_job_description": "Transcodeazǎ videoclipurile pentru compatibilitate cu browsere și dispozitive"
-  },
-  "admin_email": "E-mailul administratorului",
-  "admin_password": "Parola administratorului",
-  "administration": "Administrare",
-  "advanced": "Avansat",
-  "album_added": "Album adăugat",
-  "album_added_notification_setting_description": "Primiți o notificare prin e-mail când sunteți adăugat la un album partajat",
-  "album_cover_updated": "Coperta albumului a fost actualizată",
-  "album_info_updated": "Informațiile albumului au fost actualizate",
-  "album_name": "Nume de album",
-  "album_options": "Opțiuni de album",
-  "album_updated": "Album actualizat",
-  "album_updated_setting_description": "Primiți o notificare prin e-mail când un album partajat are elemente noi",
-  "albums": "Albume",
-  "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albume}}",
-  "all": "Toate",
-  "all_albums": "Toate albumele",
-  "all_people": "Toți oamenii",
-  "all_videos": "Toate videoclipurile",
-  "allow_dark_mode": "Permite mod întunecat",
-  "allow_edits": "Permite editări",
-  "allow_public_user_to_download": "Permite utilizatorului public să descarce",
-  "allow_public_user_to_upload": "Permite utilizatorului public să încarce",
-  "api_key": "Cheie API",
-  "api_key_description": "Această valoare va fi afișată o singură dată. Vă rugăm să vă asigurați că o copiați înainte de a închide fereastra.",
-  "api_key_empty": "Numele cheii API nu trebuie să fie gol",
-  "api_keys": "Chei API",
-  "app_settings": "Setări în aplicație",
-  "appears_in": "Apare în",
-  "archive": "Arhivă",
-  "archive_or_unarchive_photo": "Arhiveazǎ sau dezarhiveazǎ fotografia",
-  "archived": "",
-  "archived_count": "{count, plural, one {S-a arhivat #}, other {S-au arhivat #}}",
-  "are_you_sure_to_do_this": "Sunteți sigur că doriți să faceți acest lucru?",
-  "asset_added_to_album": "Adăugat la album",
-  "asset_adding_to_album": "Se adauga la album...",
-  "asset_description_updated": "Descrierea activelor a fost actualizată",
-  "asset_filename_is_offline": "Activul {filename} este offline",
-  "asset_has_unassigned_faces": "Activul are fețe neatribuite",
-  "asset_hashing": "Hasurare...",
-  "asset_offline": "Resursă offline",
-  "asset_offline_description": "Acest activ este offline. Immich nu poate accesa locația fișierului său. Vă rugăm să vă asigurați că activul este disponibil și apoi să efectuați o nouă scanare a bibliotecii.",
-  "asset_skipped": "Sărit",
-  "asset_uploaded": "Încărcat",
-  "asset_uploading": "Se incărca...",
-  "assets": "Resurse",
-  "authorized_devices": "Dispozitive autorizate",
-  "back": "Înapoi",
-  "back_close_deselect": "Înapoi, închidere sau deselectare",
-  "backward": "Invers",
-  "birthdate_saved": "Data nașterii salvată cu succes",
-  "birthdate_set_description": "Data nașterii este utilizată pentru a calcula vârsta acestei persoane la momentul realizării fotografiei.",
-  "blurred_background": "Fundal neclar",
-  "build": "Construiți",
-  "build_image": "Construiți o imagine",
-  "bulk_delete_duplicates_confirmation": "Sunteți sigur că doriți să ștergeți în masă {count, plural, one {# duplicate asset} other {# duplicate assets}}? Acest lucru va păstra cel mai mare activ din fiecare grup și va șterge definitiv toate celelalte duplicate. Nu puteți anula această acțiune!",
-  "buy": "Cumpără Immich",
-  "camera": "Camerǎ",
-  "camera_brand": "Marcǎ cameră",
-  "camera_model": "Model cameră",
-  "cancel": "Anulează",
-  "cancel_search": "Anulează căutarea",
-  "cannot_merge_people": "Nu se pot îmbina oamenii",
-  "cannot_undo_this_action": "Nu puteți anula această acțiune!",
-  "cannot_update_the_description": "Nu se poate actualiza descrierea",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
-  "change_date": "Schimbă dată",
-  "change_expiration_time": "Shimbă data expirării",
-  "change_location": "Schimbă locația",
-  "change_name": "Schimbă numele",
-  "change_name_successfully": "Schimbă numele cu succes",
-  "change_password": "Schimbă parola",
-  "change_password_description": "Aceasta este fie prima dată când vă conectați la sistem, fie vi s-a solicitat să vă schimbați parola. Vă rugăm să introduceți noua parolă mai jos.",
-  "change_your_password": "Schimbă-ți parola",
-  "changed_visibility_successfully": "Schimbă visibilitate cu succes",
-  "check_logs": "Verificarea logurilor",
-  "choose_matching_people_to_merge": "Alegeți persoanele potrivite pentru fuzionare",
-  "city": "Oraș",
-  "clear": "ȘTERGE",
-  "clear_all": "Șterge tot",
-  "clear_message": "Șterge mesajul",
-  "clear_value": "Valoare clară",
-  "close": "Închide",
-  "collapse": "Colaps",
-  "collapse_all": "Închideți pe toate",
-  "color_theme": "Tema de culoare",
-  "comment_deleted": "Comentariu șters",
-  "comment_options": "Opțiuni de comentariu",
-  "comments_and_likes": "Comentarii și aprecieri",
-  "comments_are_disabled": "Comentariile sunt dezactivate",
-  "confirm": "Confirmați",
-  "confirm_admin_password": "Confirmați parola de administrator",
-  "confirm_delete_shared_link": "Sunteți sigur că doriți să ștergeți acest link partajat?",
-  "confirm_password": "Confirmați parola",
-  "contain": "Conține",
-  "context": "Context",
-  "continue": "Continuați",
-  "copied_image_to_clipboard": "Copiat imaginea în clipboard.",
-  "copied_to_clipboard": "Copiat în clipboard!",
-  "copy_error": "Eroare de copiere",
-  "copy_file_path": "Copiați calea fișierului",
-  "copy_image": "Copiere imagine",
-  "copy_link": "Copiere link",
-  "copy_link_to_clipboard": "Copiați link-ul în clipboard",
-  "copy_password": "Copiați parola",
-  "copy_to_clipboard": "Copiere în Clipboard",
-  "country": "Țara",
-  "cover": "Acoperire",
-  "covers": "Acoperiri",
-  "create": "Creează",
-  "create_album": "Creează album",
-  "create_library": "Crearea bibliotecii",
-  "create_link": "Creează link",
-  "create_link_to_share": "Creează link pentru a distribui",
-  "create_link_to_share_description": "Permiteți oricui are link-ul să vadă fotografia (fotografiile) selectată(e)",
-  "create_new_person": "Creați o persoană nouă",
-  "create_new_person_hint": "Atribuiți activele selectate unei persoane noi",
-  "create_new_user": "Crearea unui nou utilizator",
-  "create_user": "Creează utilizator",
-  "created": "Creat",
-  "current_device": "Dispozitiv curent",
-  "custom_locale": "Local personalizat",
-  "custom_locale_description": "Formatați datele și numerele în funcție de limbă și regiune",
-  "dark": "Întuneric",
-  "date_after": "Data după",
-  "date_and_time": "Dată și Oră",
-  "date_before": "Data anterioară",
-  "date_of_birth_saved": "Data nașterii salvată cu succes",
-  "date_range": "Interval de date",
-  "day": "Ziua",
-  "deduplicate_all": "Deduplicați toate",
-  "default_locale": "Local implicit",
-  "default_locale_description": "Formatați datele și numerele în funcție de locația browserului dvs.",
-  "delete": "Șterge",
-  "delete_album": "Șterge album",
-  "delete_api_key_prompt": "Sunteți sigur că doriți să ștergeți această cheie API?",
-  "delete_duplicates_confirmation": "Sunteți sigur că doriți să ștergeți permanent aceste duplicate?",
-  "delete_key": "Tasta de ștergere",
-  "delete_library": "Ștergeți biblioteca",
-  "delete_link": "Ștergeți linkul",
-  "delete_shared_link": "Ștergeți link-ul partajat",
-  "delete_user": "Ștergeți utilizatorul",
-  "deleted_shared_link": "Link partajat șters",
-  "description": "Descriere",
-  "details": "DETALII",
-  "direction": "Direcție",
-  "disabled": "Dezactivat",
-  "disallow_edits": "Interziceți editările",
-  "discover": "Descoperiți",
-  "dismiss_all_errors": "Eliminați toate erorile",
-  "dismiss_error": "Anulați eroarea",
-  "display_options": "Opțiuni de afișare",
-  "display_order": "Ordine de afișare",
-  "display_original_photos": "Afișați fotografiile originale",
-  "display_original_photos_setting_description": "Preferați să afișați fotografia originală atunci când vizualizați un bun în loc de miniaturi atunci când bunul original este compatibil cu web. Acest lucru poate duce la o viteză mai mică de afișare a fotografiilor.",
-  "do_not_show_again": "Nu mai afișați acest mesaj",
-  "done": "Gata",
-  "download": "Descarcă",
-  "download_settings": "Descarcă",
-  "download_settings_description": "Gestionați setările legate de descărcarea activelor",
-  "downloading": "Descărcare",
-  "downloading_asset_filename": "Descărcarea activului {filename}",
-  "drop_files_to_upload": "Aruncați fișiere oriunde pentru a le încărca",
-  "duplicates": "Duplicate",
-  "duplicates_description": "Rezolvați fiecare grup indicând care, dacă există, sunt duplicate",
-  "duration": "Durată",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
-  "edit": "Editare",
-  "edit_album": "Editare album",
-  "edit_avatar": "Editare avatar",
-  "edit_date": "Editează data",
-  "edit_date_and_time": "Editarea datei și orei",
-  "edit_exclusion_pattern": "Editarea modelului de excludere",
-  "edit_faces": "Editează fețele",
-  "edit_import_path": "Editarea căii de import",
-  "edit_import_paths": "Editarea căilor de import",
-  "edit_key": "Tastă de editare",
-  "edit_link": "Modifică link",
-  "edit_location": "Editează locație",
-  "edit_name": "Editează nume",
-  "edit_people": "Editează persoane",
-  "edit_title": "Editează Titlul",
-  "edit_user": "",
-  "edited": "Editat",
-  "editor": "",
-  "email": "Email",
-  "empty": "",
-  "empty_album": "",
-  "empty_trash": "Golește coșul",
-  "empty_trash_confirmation": "Sunteți sigur că doriți să goliți coșul de gunoi? Acest lucru va elimina definitiv din Immich toate bunurile din coșul de gunoi.\nNu puteți anula această acțiune!",
-  "enable": "Activează",
-  "enabled": "Activat",
-  "end_date": "Data de încheiere",
-  "error": "Eroare",
-  "error_loading_image": "Eroare la incarcarea fotografiei",
-  "error_title": "Eroare - Ceva nu a mers",
-  "errors": {
-    "cannot_navigate_next_asset": "Nu se poate naviga către următorul activ",
-    "cannot_navigate_previous_asset": "Nu se poate naviga la activul anterior",
-    "cant_apply_changes": "Nu se pot aplica schimbări",
-    "cant_change_asset_favorite": "Nu pot schimba favoritul pentru activ",
-    "cant_get_faces": "Nu pot obține fețe",
-    "cant_get_number_of_comments": "Nu pot obține numărul de comentarii",
-    "cant_search_people": "Nu pot căuta oameni",
-    "cant_search_places": "Nu se pot căuta locații",
-    "cleared_jobs": "Joburi terminate pentru: {job}",
-    "error_adding_assets_to_album": "Eroare la adăugarea activelor la album",
-    "error_adding_users_to_album": "Eroare la adăugarea utilizatorilor la album",
-    "error_deleting_shared_user": "Eroare la ștergerea utilizatorului partajat",
-    "error_downloading": "Eroare la descărcarea {filename}",
-    "error_hiding_buy_button": "Eroare la ascunderea butonului de cumpărare",
-    "error_removing_assets_from_album": "Eroare la eliminarea activelor din album, verificați consola pentru mai multe detalii",
-    "error_selecting_all_assets": "Eroare la selectarea tuturor activelor",
-    "exclusion_pattern_already_exists": "Acest model de excludere există deja.",
-    "failed_job_command": "Comanda {command} a eșuat pentru job: {job}",
-    "failed_to_create_album": "A eșuat crearea albumului",
-    "failed_to_create_shared_link": "A eșuat crearea legăturii partajate",
-    "failed_to_edit_shared_link": "A eșuat editarea legăturii partajate",
-    "unable_to_add_album_users": "",
-    "unable_to_add_comment": "",
-    "unable_to_add_partners": "",
-    "unable_to_change_album_user_role": "",
-    "unable_to_change_date": "",
-    "unable_to_change_location": "",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
-    "unable_to_create_admin_account": "",
-    "unable_to_create_library": "",
-    "unable_to_create_user": "",
-    "unable_to_delete_album": "",
-    "unable_to_delete_asset": "",
-    "unable_to_delete_user": "",
-    "unable_to_empty_trash": "",
-    "unable_to_enter_fullscreen": "",
-    "unable_to_exit_fullscreen": "",
-    "unable_to_hide_person": "",
-    "unable_to_load_album": "",
-    "unable_to_load_asset_activity": "",
-    "unable_to_load_items": "",
-    "unable_to_load_liked_status": "",
-    "unable_to_play_video": "",
-    "unable_to_refresh_user": "",
-    "unable_to_remove_album_users": "",
-    "unable_to_remove_comment": "",
-    "unable_to_remove_library": "",
-    "unable_to_remove_partner": "",
-    "unable_to_remove_reaction": "",
-    "unable_to_remove_user": "",
-    "unable_to_repair_items": "",
-    "unable_to_reset_password": "",
-    "unable_to_resolve_duplicate": "",
-    "unable_to_restore_assets": "",
-    "unable_to_restore_trash": "",
-    "unable_to_restore_user": "",
-    "unable_to_save_album": "",
-    "unable_to_save_name": "",
-    "unable_to_save_profile": "",
-    "unable_to_save_settings": "",
-    "unable_to_scan_libraries": "",
-    "unable_to_scan_library": "",
-    "unable_to_set_profile_picture": "",
-    "unable_to_submit_job": "",
-    "unable_to_trash_asset": "",
-    "unable_to_unlink_account": "",
-    "unable_to_update_library": "",
-    "unable_to_update_location": "",
-    "unable_to_update_settings": "",
-    "unable_to_update_user": ""
-  },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
-  "exit_slideshow": "",
-  "expand_all": "",
-  "expire_after": "Expiră după",
-  "expired": "Expirat",
-  "explore": "Exploreazǎ",
-  "extension": "Extensie",
-  "external": "Extern",
-  "external_libraries": "",
-  "failed_to_get_people": "",
-  "favorite": "",
-  "favorite_or_unfavorite_photo": "",
-  "favorites": "Favorite",
-  "feature": "",
-  "feature_photo_updated": "",
-  "featurecollection": "",
-  "file_name": "",
-  "file_name_or_extension": "",
-  "filename": "",
-  "files": "",
-  "filetype": "",
-  "filter_people": "",
-  "fix_incorrect_match": "",
-  "force_re-scan_library_files": "",
-  "forward": "",
-  "general": "",
-  "get_help": "",
-  "getting_started": "",
-  "go_back": "",
-  "go_to_search": "",
-  "go_to_share_page": "",
-  "group_albums_by": "",
-  "has_quota": "",
-  "hide_gallery": "",
-  "hide_password": "",
-  "hide_person": "",
-  "host": "",
-  "hour": "",
-  "image": "",
-  "img": "",
-  "immich_logo": "",
-  "import_path": "",
-  "in_archive": "",
-  "include_archived": "Include resursele arhivate",
-  "include_shared_albums": "",
-  "include_shared_partner_assets": "",
-  "individual_share": "",
-  "info": "",
-  "interval": {
-    "day_at_onepm": "",
-    "hours": "",
-    "night_at_midnight": "",
-    "night_at_twoam": ""
-  },
-  "invite_people": "",
-  "invite_to_album": "Invită în album",
-  "job_settings_description": "",
-  "jobs": "",
-  "keep": "",
-  "keyboard_shortcuts": "",
-  "language": "",
-  "language_setting_description": "",
-  "last_seen": "",
-  "leave": "",
-  "let_others_respond": "Permite altora să răspundă",
-  "level": "",
-  "library": "Librărie",
-  "library_options": "",
-  "light": "",
-  "link_options": "",
-  "link_to_oauth": "",
-  "linked_oauth_account": "",
-  "list": "",
-  "loading": "",
-  "loading_search_results_failed": "",
-  "log_out": "Deconectare",
-  "log_out_all_devices": "",
-  "login_has_been_disabled": "",
-  "look": "",
-  "loop_videos": "",
-  "loop_videos_description": "",
-  "make": "",
-  "manage_shared_links": "Administrează link-urile distribuite",
-  "manage_sharing_with_partners": "",
-  "manage_the_app_settings": "",
-  "manage_your_account": "",
-  "manage_your_api_keys": "",
-  "manage_your_devices": "",
-  "manage_your_oauth_connection": "",
-  "map": "",
-  "map_marker_with_image": "",
-  "map_settings": "Setările hărții",
-  "media_type": "Tip fișier",
-  "memories": "Amintiri",
-  "memories_setting_description": "Administreazǎ ce vezi în amintiri",
-  "memory": "Amintire",
-  "menu": "Meniu",
-  "merge": "",
-  "merge_people": "",
-  "merge_people_successfully": "",
-  "minimize": "",
-  "minute": "",
-  "missing": "Absente",
-  "model": "Model",
-  "month": "Lună",
-  "more": "Mai multe",
-  "moved_to_trash": "",
-  "my_albums": "Albumele mele",
-  "name": "Nume",
-  "name_or_nickname": "Nume sau poreclǎ",
-  "never": "Niciodată",
-  "new_api_key": "Cheie API nouǎ",
-  "new_password": "Parolă nouă",
-  "new_person": "Persoanǎ nouǎ",
-  "new_user_created": "Utilizator nou creat",
-  "newest_first": "",
-  "next": "Următorul",
-  "next_memory": "",
-  "no": "",
-  "no_albums_message": "",
-  "no_archived_assets_message": "",
-  "no_assets_message": "",
-  "no_exif_info_available": "",
-  "no_explore_results_message": "",
-  "no_favorites_message": "",
-  "no_libraries_message": "",
-  "no_name": "",
-  "no_places": "",
-  "no_results": "",
-  "no_shared_albums_message": "",
-  "not_in_any_album": "",
-  "notes": "",
-  "notification_toggle_setting_description": "",
-  "notifications": "Notificări",
-  "notifications_setting_description": "",
-  "oauth": "",
-  "offline": "",
-  "ok": "",
-  "oldest_first": "",
-  "online": "",
-  "only_favorites": "",
-  "only_refreshes_modified_files": "",
-  "open_the_search_filters": "",
-  "options": "Opțiuni",
-  "organize_your_library": "",
-  "other": "",
-  "other_devices": "",
-  "other_variables": "",
-  "owned": "Deținut",
-  "owner": "Admin",
-  "partner": "Partener",
-  "partner_sharing": "",
-  "partners": "",
-  "password": "Parolă",
-  "password_does_not_match": "",
-  "password_required": "",
-  "password_reset_success": "",
-  "past_durations": {
-    "days": "",
-    "hours": "",
-    "years": ""
-  },
-  "path": "",
-  "pattern": "",
-  "pause": "",
-  "pause_memories": "",
-  "paused": "",
-  "pending": "",
-  "people": "Persoane",
-  "people_sidebar_description": "",
-  "perform_library_tasks": "",
-  "permanent_deletion_warning": "",
-  "permanent_deletion_warning_setting_description": "",
-  "permanently_delete": "",
-  "permanently_deleted_asset": "",
-  "person": "Persoanǎ",
-  "photos": "Fotografii",
-  "photos_from_previous_years": "",
-  "pick_a_location": "",
-  "place": "",
-  "places": "Locații",
-  "play": "",
-  "play_memories": "",
-  "play_motion_photo": "",
-  "play_or_pause_video": "",
-  "point": "",
-  "port": "",
-  "preset": "",
-  "preview": "",
-  "previous": "",
-  "previous_memory": "",
-  "previous_or_next_photo": "",
-  "primary": "",
-  "profile_picture_set": "",
-  "public_share": "",
-  "range": "",
-  "raw": "",
-  "reaction_options": "",
-  "read_changelog": "",
-  "recent": "",
-  "recent_searches": "",
-  "refresh": "",
-  "refreshed": "",
-  "refreshes_every_file": "",
-  "remove": "",
-  "remove_from_album": "Șterge din album",
-  "remove_from_favorites": "",
-  "remove_from_shared_link": "",
-  "remove_offline_files": "",
-  "repair": "",
-  "repair_no_results_message": "",
-  "replace_with_upload": "",
-  "require_password": "",
-  "reset": "",
-  "reset_password": "",
-  "reset_people_visibility": "",
-  "reset_settings_to_default": "",
-  "restore": "Restaurează",
-  "restore_user": "",
-  "retry_upload": "",
-  "review_duplicates": "",
-  "role": "",
-  "save": "Salvează",
-  "saved_profile": "",
-  "saved_settings": "",
-  "say_something": "Spune ceva",
-  "scan_all_libraries": "",
-  "scan_all_library_files": "",
-  "scan_new_library_files": "",
-  "scan_settings": "",
-  "search": "Caută",
-  "search_albums": "",
-  "search_by_context": "",
-  "search_camera_make": "",
-  "search_camera_model": "",
-  "search_city": "",
-  "search_country": "",
-  "search_for_existing_person": "",
-  "search_people": "",
-  "search_places": "",
-  "search_state": "",
-  "search_timezone": "",
-  "search_type": "Tip cǎutare",
-  "search_your_photos": "Căutare fotografii",
-  "searching_locales": "",
-  "second": "Secundǎ",
-  "select_album_cover": "",
-  "select_all": "",
-  "select_all_duplicates": "Selecteazǎ toate duplicatele",
-  "select_avatar_color": "",
-  "select_face": "Selecteazǎ fațǎ",
-  "select_featured_photo": "",
-  "select_from_computer": "Selecteazǎ din calculator",
-  "select_keep_all": "Selecteazǎ tot pentru salvare",
-  "select_library_owner": "Selecteazǎ proprietarul bibliotecii",
-  "select_new_face": "Selecteazǎ o nouǎ fațǎ",
-  "select_photos": "Selectează fotografii",
-  "select_trash_all": "Selecteazǎ tot pentru ștergere",
-  "selected": "Selectați",
-  "send_message": "",
-  "server": "",
-  "server_stats": "",
-  "set": "",
-  "set_as_album_cover": "",
-  "set_as_profile_picture": "",
-  "set_date_of_birth": "",
-  "set_profile_picture": "",
-  "set_slideshow_to_fullscreen": "",
-  "settings": "Setări",
-  "settings_saved": "",
-  "share": "Distribuie",
-  "shared": "Distribuit",
-  "shared_by": "",
-  "shared_by_you": "",
-  "shared_links": "Link-uri distribuite",
-  "sharing": "Distribuire",
-  "sharing_sidebar_description": "",
-  "show_album_options": "",
-  "show_file_location": "",
-  "show_gallery": "",
-  "show_hidden_people": "",
-  "show_in_timeline": "",
-  "show_in_timeline_setting_description": "",
-  "show_keyboard_shortcuts": "",
-  "show_metadata": "Arată metadata",
-  "show_or_hide_info": "",
-  "show_password": "",
-  "show_person_options": "",
-  "show_progress_bar": "",
-  "show_search_options": "",
-  "shuffle": "",
-  "sign_up": "",
-  "size": "",
-  "skip_to_content": "",
-  "slideshow": "",
-  "slideshow_settings": "",
-  "sort_albums_by": "",
-  "stack": "Grup",
-  "stack_selected_photos": "",
-  "stacktrace": "",
-  "start_date": "",
-  "state": "",
-  "status": "",
-  "stop_motion_photo": "",
-  "stop_photo_sharing": "Încetezi distribuirea fotografiilor?",
-  "storage": "Spațiu de stocare",
-  "storage_label": "",
-  "storage_usage": "{used} din {available} utilizați",
-  "submit": "",
-  "suggestions": "Sugestii",
-  "sunrise_on_the_beach": "Rǎsǎrit pe plajǎ",
-  "swap_merge_direction": "",
-  "sync": "Sincronizare",
-  "template": "",
-  "theme": "Temă",
-  "theme_selection": "",
-  "theme_selection_description": "",
-  "time_based_memories": "",
-  "timezone": "Fus orar",
-  "to_favorite": "Favorit",
-  "toggle_settings": "",
-  "toggle_theme": "",
-  "toggle_visibility": "",
-  "total_usage": "",
-  "trash": "Coș",
-  "trash_all": "Șterge tot",
-  "trash_count": "Șterge {count, number}",
-  "trash_no_results_message": "",
-  "type": "",
-  "unarchive": "Șterge din arhivă",
-  "unarchived": "",
-  "unfavorite": "Șterge din favorite",
-  "unhide_person": "",
-  "unknown": "",
-  "unknown_album": "",
-  "unknown_year": "",
-  "unlink_oauth": "",
-  "unlinked_oauth_account": "",
-  "unselect_all": "",
-  "unstack": "Anulează grup",
-  "up_next": "",
-  "updated_password": "",
-  "upload": "Încarcă",
-  "upload_concurrency": "",
-  "url": "",
-  "usage": "",
-  "user": "",
-  "user_id": "",
-  "user_usage_detail": "",
-  "username": "",
-  "users": "Utilizatori",
-  "utilities": "Utilitǎți",
-  "validate": "Valideazǎ",
-  "variables": "Variabile",
-  "version": "Versiune",
-  "version_announcement_closing": "Prietenul tǎu, Alex",
-  "video": "Videoclip",
-  "video_hover_setting_description": "",
-  "videos": "Videoclipuri",
-  "view_album": "Vezi album",
-  "view_all": "Vezi toate",
-  "view_all_users": "Vezi toți utilizatorii",
-  "view_links": "Vezi scurtǎturi",
-  "view_next_asset": "",
-  "view_previous_asset": "",
-  "viewer": "",
-  "waiting": "În așteptare",
-  "warning": "Avertisment",
-  "week": "Sǎptǎmânǎ",
-  "welcome": "Salutare",
-  "welcome_to_immich": "Bun venit în Immich",
-  "year": "An",
-  "years_ago": "acum {years, plural, one {# an} other {# ani}}",
-  "yes": "Da",
-  "you_dont_have_any_shared_links": "Nu aveți niciun link partajat",
-  "zoom_image": "Mărește imaginea"
-}
diff --git a/web/src/lib/i18n/sk.json b/web/src/lib/i18n/sk.json
deleted file mode 100644
index d6c066a4cc..0000000000
--- a/web/src/lib/i18n/sk.json
+++ /dev/null
@@ -1,817 +0,0 @@
-{
-  "about": "O aplikácií",
-  "account": "Účet",
-  "account_settings": "Nastavenia účtu",
-  "acknowledge": "Potvrdiť",
-  "action": "Akcia",
-  "actions": "Akcie",
-  "active": "Aktívny",
-  "activity": "Aktivita",
-  "activity_changed": "Aktivita je {enabled, select, true{povolená} other {zakázaná}}",
-  "add": "Pridať",
-  "add_a_description": "Pridať popis",
-  "add_a_location": "Pridať polohu",
-  "add_a_name": "Pridať meno",
-  "add_a_title": "Pridať názov",
-  "add_exclusion_pattern": "Pridať vzor vylúčenia",
-  "add_import_path": "Pridať cestu importu",
-  "add_location": "Pridať lokáciu",
-  "add_more_users": "Pridať viac používateľov",
-  "add_partner": "Pridať partnera",
-  "add_path": "Pridať cestu",
-  "add_photos": "Pridať fotografie",
-  "add_to": "Pridať do...",
-  "add_to_album": "Pridať do albumu",
-  "add_to_shared_album": "Pridať do zdieľaného albumu",
-  "added_to_archive": "Pridané do archívu",
-  "added_to_favorites": "Pridané do obľúbených",
-  "added_to_favorites_count": "Pridané {count} do obľúbených",
-  "admin": {
-    "authentication_settings": "Nastavenia overenia",
-    "authentication_settings_description": "Spravovať heslo, protokol OAuth a ďalšie nastavenia overenia",
-    "authentication_settings_disable_all": "Naozaj chcete zakázať všetky spôsoby prihlásenia? Prihlásenie bude úplne zakázané.",
-    "authentication_settings_reenable": "Pre povolenie použite <link>Serverový príkaz</link>.",
-    "background_task_job": "Úlohy na pozadí",
-    "check_all": "Skontrolovať všetko",
-    "config_set_by_file": "Konfigurácia je v súčasnosti nastavená konfiguračným súborom",
-    "confirm_delete_library": "Naozaj chcete vymazať knižnicu {library}?",
-    "confirm_email_below": "Pre potvrdenie zadajte nižšie \"{email}\"",
-    "confirm_user_password_reset": "Určite chcete resetovať heslo pre {user}?",
-    "crontab_guru": "",
-    "disable_login": "Zakázať prihlásenie",
-    "disabled": "",
-    "duplicate_detection_job_description": "Spustiť strojové učenie na položkách pre detekciu podobných obrázkov. Spolieha sa na inteligentné vyhľadávanie",
-    "external_library_created_at": "Externá knižnica (vytvorená {date})",
-    "external_library_management": "Správa Externej Knižnice",
-    "face_detection": "Detekcia tváre",
-    "force_delete_user_warning": "VAROVANIE: Toto okamžite zmaže užívateľa a všetky súbory. Táto akcia sa nedá zvrátiť a súbory sa už nebudú dať vrátiť späť.",
-    "image_format_description": "WebP vytvára menšie súbory ako JPEG, ale kódovanie je pomalšie.",
-    "image_prefer_embedded_preview": "Uprednostňovať vstavaný náhľad",
-    "image_prefer_embedded_preview_setting_description": "Použiť vložené náhľady vo fotografiách RAW ako vstup pre spracovanie obrazu, ak sú k dispozícii. To môže vytvoriť presnejšie farby pre niektoré obrázky, ale kvalita náhľadu závisí od fotoaparátu a obrázok môže mať viac kompresných artefaktov.",
-    "image_prefer_wide_gamut": "Uprednostňovať široký rozsah",
-    "image_prefer_wide_gamut_setting_description": "",
-    "image_preview_format": "Formát ukážky",
-    "image_preview_resolution": "Rozlíšenie náhľadu",
-    "image_preview_resolution_description": "Používa sa pri prezeraní jednej fotografie a pre strojové učenie. Vyššie rozlíšenie zachová viac detailov, ale kódovanie trvá dlhšie, súbory sú väčšie, a môže znížiť rýchlosť aplikácie.",
-    "image_quality": "Kvalita",
-    "image_quality_description": "",
-    "image_settings": "",
-    "image_settings_description": "",
-    "image_thumbnail_format": "Formát náhľadu",
-    "image_thumbnail_resolution": "",
-    "image_thumbnail_resolution_description": "",
-    "job_settings": "",
-    "job_settings_description": "",
-    "library_created": "Vytvorená knižnica: {library}",
-    "library_cron_expression": "",
-    "library_cron_expression_description": "Nastaviť skenovací interval pomocou formátu cron. Viac informácií nájdete napr. na <link>Crontab Guru</link>",
-    "library_cron_expression_presets": "",
-    "library_deleted": "Knižnica vymazaná",
-    "library_scanning": "Periodické skenovanie",
-    "library_scanning_description": "Nastaviť pravidelné skenovanie knižnice",
-    "library_scanning_enable_description": "Zapnúť pravidelné skenovanie knižnice",
-    "library_settings": "Externá knižnica",
-    "library_settings_description": "Spravovať nastavenia externej knižnice",
-    "library_tasks_description": "",
-    "library_watching_enable_description": "Sledovať zmeny súborov v externých knižniciach",
-    "library_watching_settings": "",
-    "library_watching_settings_description": "Automaticky sledovať zmenené súbory",
-    "logging_enable_description": "",
-    "logging_level_description": "",
-    "logging_settings": "",
-    "machine_learning_clip_model": "Model CLIP",
-    "machine_learning_duplicate_detection": "Detekcia duplikátov",
-    "machine_learning_duplicate_detection_enabled": "Zapnúť detekciu duplikátov",
-    "machine_learning_duplicate_detection_enabled_description": "",
-    "machine_learning_duplicate_detection_setting_description": "",
-    "machine_learning_enabled": "Zapnúť strojové učenie",
-    "machine_learning_enabled_description": "",
-    "machine_learning_facial_recognition": "Rozpoznávanie tváre",
-    "machine_learning_facial_recognition_description": "",
-    "machine_learning_facial_recognition_model": "",
-    "machine_learning_facial_recognition_model_description": "",
-    "machine_learning_facial_recognition_setting_description": "",
-    "machine_learning_max_detection_distance": "",
-    "machine_learning_max_detection_distance_description": "",
-    "machine_learning_max_recognition_distance": "",
-    "machine_learning_max_recognition_distance_description": "",
-    "machine_learning_min_detection_score": "",
-    "machine_learning_min_detection_score_description": "",
-    "machine_learning_min_recognized_faces": "",
-    "machine_learning_min_recognized_faces_description": "",
-    "machine_learning_settings": "",
-    "machine_learning_settings_description": "",
-    "machine_learning_smart_search": "",
-    "machine_learning_smart_search_description": "",
-    "machine_learning_smart_search_enabled_description": "",
-    "machine_learning_url_description": "",
-    "manage_log_settings": "",
-    "map_dark_style": "",
-    "map_enable_description": "",
-    "map_light_style": "",
-    "map_reverse_geocoding": "",
-    "map_reverse_geocoding_enable_description": "",
-    "map_reverse_geocoding_settings": "",
-    "map_settings": "",
-    "map_settings_description": "",
-    "map_style_description": "",
-    "metadata_extraction_job_description": "",
-    "migration_job_description": "",
-    "notification_email_from_address": "",
-    "notification_email_from_address_description": "",
-    "notification_email_host_description": "",
-    "notification_email_ignore_certificate_errors": "",
-    "notification_email_ignore_certificate_errors_description": "",
-    "notification_email_password_description": "",
-    "notification_email_port_description": "",
-    "notification_email_sent_test_email_button": "",
-    "notification_email_setting_description": "",
-    "notification_email_test_email_failed": "",
-    "notification_email_test_email_sent": "",
-    "notification_email_username_description": "",
-    "notification_enable_email_notifications": "",
-    "notification_settings": "",
-    "notification_settings_description": "",
-    "oauth_auto_launch": "",
-    "oauth_auto_launch_description": "",
-    "oauth_auto_register": "",
-    "oauth_auto_register_description": "",
-    "oauth_button_text": "",
-    "oauth_client_id": "",
-    "oauth_client_secret": "",
-    "oauth_enable_description": "",
-    "oauth_issuer_url": "",
-    "oauth_mobile_redirect_uri": "",
-    "oauth_mobile_redirect_uri_override": "",
-    "oauth_mobile_redirect_uri_override_description": "",
-    "oauth_scope": "",
-    "oauth_settings": "",
-    "oauth_settings_description": "",
-    "oauth_signing_algorithm": "",
-    "oauth_storage_label_claim": "",
-    "oauth_storage_label_claim_description": "",
-    "oauth_storage_quota_claim": "",
-    "oauth_storage_quota_claim_description": "",
-    "oauth_storage_quota_default": "",
-    "oauth_storage_quota_default_description": "",
-    "password_enable_description": "",
-    "password_settings": "",
-    "password_settings_description": "",
-    "server_external_domain_settings": "",
-    "server_external_domain_settings_description": "",
-    "server_settings": "",
-    "server_settings_description": "",
-    "server_welcome_message": "",
-    "server_welcome_message_description": "",
-    "sidecar_job_description": "",
-    "slideshow_duration_description": "",
-    "smart_search_job_description": "",
-    "storage_template_enable_description": "",
-    "storage_template_hash_verification_enabled": "",
-    "storage_template_hash_verification_enabled_description": "",
-    "storage_template_migration_job": "",
-    "storage_template_settings": "",
-    "storage_template_settings_description": "",
-    "theme_custom_css_settings": "",
-    "theme_custom_css_settings_description": "",
-    "theme_settings": "",
-    "theme_settings_description": "",
-    "thumbnail_generation_job_description": "",
-    "transcode_policy_description": "",
-    "transcoding_acceleration_api": "",
-    "transcoding_acceleration_api_description": "",
-    "transcoding_acceleration_nvenc": "",
-    "transcoding_acceleration_qsv": "",
-    "transcoding_acceleration_rkmpp": "",
-    "transcoding_acceleration_vaapi": "",
-    "transcoding_accepted_audio_codecs": "",
-    "transcoding_accepted_audio_codecs_description": "",
-    "transcoding_accepted_video_codecs": "",
-    "transcoding_accepted_video_codecs_description": "",
-    "transcoding_advanced_options_description": "",
-    "transcoding_audio_codec": "",
-    "transcoding_audio_codec_description": "",
-    "transcoding_bitrate_description": "",
-    "transcoding_constant_quality_mode": "",
-    "transcoding_constant_quality_mode_description": "",
-    "transcoding_constant_rate_factor": "",
-    "transcoding_constant_rate_factor_description": "",
-    "transcoding_disabled_description": "",
-    "transcoding_hardware_acceleration": "",
-    "transcoding_hardware_acceleration_description": "",
-    "transcoding_hardware_decoding": "",
-    "transcoding_hardware_decoding_setting_description": "",
-    "transcoding_hevc_codec": "",
-    "transcoding_max_b_frames": "",
-    "transcoding_max_b_frames_description": "",
-    "transcoding_max_bitrate": "",
-    "transcoding_max_bitrate_description": "",
-    "transcoding_max_keyframe_interval": "",
-    "transcoding_max_keyframe_interval_description": "",
-    "transcoding_optimal_description": "",
-    "transcoding_preferred_hardware_device": "",
-    "transcoding_preferred_hardware_device_description": "",
-    "transcoding_preset_preset": "",
-    "transcoding_preset_preset_description": "",
-    "transcoding_reference_frames": "",
-    "transcoding_reference_frames_description": "",
-    "transcoding_required_description": "",
-    "transcoding_settings": "",
-    "transcoding_settings_description": "",
-    "transcoding_target_resolution": "",
-    "transcoding_target_resolution_description": "",
-    "transcoding_temporal_aq": "",
-    "transcoding_temporal_aq_description": "",
-    "transcoding_threads": "",
-    "transcoding_threads_description": "",
-    "transcoding_tone_mapping": "",
-    "transcoding_tone_mapping_description": "",
-    "transcoding_tone_mapping_npl": "",
-    "transcoding_tone_mapping_npl_description": "",
-    "transcoding_transcode_policy": "",
-    "transcoding_two_pass_encoding": "",
-    "transcoding_two_pass_encoding_setting_description": "",
-    "transcoding_video_codec": "",
-    "transcoding_video_codec_description": "",
-    "trash_enabled_description": "",
-    "trash_number_of_days": "",
-    "trash_number_of_days_description": "",
-    "trash_settings": "",
-    "trash_settings_description": "",
-    "user_delete_delay_settings": "",
-    "user_delete_delay_settings_description": "",
-    "user_settings": "",
-    "user_settings_description": "",
-    "version_check_enabled_description": "",
-    "version_check_settings": "",
-    "version_check_settings_description": "",
-    "video_conversion_job_description": ""
-  },
-  "admin_email": "",
-  "admin_password": "",
-  "administration": "",
-  "advanced": "Pokročilé",
-  "album_added": "",
-  "album_added_notification_setting_description": "",
-  "album_cover_updated": "",
-  "album_info_updated": "",
-  "album_name": "",
-  "album_options": "",
-  "album_updated": "",
-  "album_updated_setting_description": "",
-  "albums": "Albumy",
-  "all": "Všetko",
-  "all_people": "",
-  "allow_dark_mode": "",
-  "allow_edits": "",
-  "api_key": "",
-  "api_keys": "",
-  "app_settings": "",
-  "appears_in": "",
-  "archive": "Archivovať",
-  "archive_or_unarchive_photo": "",
-  "archived": "",
-  "asset_offline": "",
-  "assets": "položiek",
-  "authorized_devices": "",
-  "back": "Späť",
-  "backward": "",
-  "blurred_background": "",
-  "camera": "",
-  "camera_brand": "",
-  "camera_model": "",
-  "cancel": "Zrušiť",
-  "cancel_search": "",
-  "cannot_merge_people": "",
-  "cannot_update_the_description": "",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
-  "change_date": "",
-  "change_expiration_time": "Zmeniť čas vypršania",
-  "change_location": "",
-  "change_name": "",
-  "change_name_successfully": "",
-  "change_password": "Zmeniť heslo",
-  "change_your_password": "",
-  "changed_visibility_successfully": "",
-  "check_logs": "",
-  "city": "",
-  "clear": "VYMAZAŤ",
-  "clear_all": "",
-  "clear_message": "",
-  "clear_value": "",
-  "close": "",
-  "collapse_all": "",
-  "color_theme": "",
-  "comment_options": "",
-  "comments_are_disabled": "",
-  "confirm": "",
-  "confirm_admin_password": "",
-  "confirm_password": "Potvrďte heslo",
-  "contain": "",
-  "context": "",
-  "continue": "",
-  "copied_image_to_clipboard": "",
-  "copy_error": "",
-  "copy_file_path": "",
-  "copy_image": "",
-  "copy_link": "",
-  "copy_link_to_clipboard": "",
-  "copy_password": "",
-  "copy_to_clipboard": "",
-  "country": "",
-  "cover": "",
-  "covers": "",
-  "create": "Vytvoriť",
-  "create_album": "Vytvoriť album",
-  "create_library": "",
-  "create_link": "Vytvoriť odkaz",
-  "create_link_to_share": "Vytvoriť odkaz na zdieľanie",
-  "create_new_person": "",
-  "create_new_user": "",
-  "create_user": "",
-  "created": "",
-  "current_device": "",
-  "custom_locale": "",
-  "custom_locale_description": "",
-  "dark": "",
-  "date_after": "",
-  "date_and_time": "Dátum a čas",
-  "date_before": "",
-  "date_range": "Rozsah dátumu",
-  "day": "",
-  "default_locale": "",
-  "default_locale_description": "",
-  "delete": "Vymazať",
-  "delete_album": "Odstrániť album",
-  "delete_key": "",
-  "delete_library": "",
-  "delete_link": "",
-  "delete_shared_link": "Odstrániť zdieľaný odkaz",
-  "delete_user": "",
-  "deleted_shared_link": "",
-  "description": "Popis",
-  "details": "PODROBNOSTI",
-  "direction": "",
-  "disallow_edits": "",
-  "discover": "",
-  "dismiss_all_errors": "",
-  "dismiss_error": "",
-  "display_options": "",
-  "display_order": "",
-  "display_original_photos": "",
-  "display_original_photos_setting_description": "",
-  "done": "Hotovo",
-  "download": "Stiahnuť",
-  "downloading": "",
-  "duration": "",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
-  "edit_album": "",
-  "edit_avatar": "",
-  "edit_date": "",
-  "edit_date_and_time": "",
-  "edit_exclusion_pattern": "",
-  "edit_faces": "",
-  "edit_import_path": "",
-  "edit_import_paths": "",
-  "edit_key": "",
-  "edit_link": "Upraviť odkaz",
-  "edit_location": "Upraviť polohu",
-  "edit_name": "Upraviť meno",
-  "edit_people": "",
-  "edit_title": "",
-  "edit_user": "",
-  "edited": "",
-  "editor": "",
-  "email": "E-mail",
-  "empty": "",
-  "empty_album": "",
-  "empty_trash": "Vyprázdniť kôš",
-  "enable": "",
-  "enabled": "",
-  "end_date": "",
-  "error": "",
-  "error_loading_image": "",
-  "errors": {
-    "unable_to_add_album_users": "",
-    "unable_to_add_comment": "",
-    "unable_to_add_partners": "",
-    "unable_to_change_album_user_role": "",
-    "unable_to_change_date": "",
-    "unable_to_change_location": "",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
-    "unable_to_create_admin_account": "",
-    "unable_to_create_library": "",
-    "unable_to_create_user": "",
-    "unable_to_delete_album": "",
-    "unable_to_delete_asset": "",
-    "unable_to_delete_user": "",
-    "unable_to_empty_trash": "",
-    "unable_to_enter_fullscreen": "",
-    "unable_to_exit_fullscreen": "",
-    "unable_to_hide_person": "",
-    "unable_to_load_album": "",
-    "unable_to_load_asset_activity": "",
-    "unable_to_load_items": "",
-    "unable_to_load_liked_status": "",
-    "unable_to_play_video": "",
-    "unable_to_refresh_user": "",
-    "unable_to_remove_album_users": "",
-    "unable_to_remove_comment": "",
-    "unable_to_remove_library": "",
-    "unable_to_remove_partner": "",
-    "unable_to_remove_reaction": "",
-    "unable_to_remove_user": "",
-    "unable_to_repair_items": "",
-    "unable_to_reset_password": "",
-    "unable_to_resolve_duplicate": "",
-    "unable_to_restore_assets": "",
-    "unable_to_restore_trash": "",
-    "unable_to_restore_user": "",
-    "unable_to_save_album": "",
-    "unable_to_save_name": "",
-    "unable_to_save_profile": "",
-    "unable_to_save_settings": "",
-    "unable_to_scan_libraries": "",
-    "unable_to_scan_library": "",
-    "unable_to_set_profile_picture": "",
-    "unable_to_submit_job": "",
-    "unable_to_trash_asset": "",
-    "unable_to_unlink_account": "",
-    "unable_to_update_library": "",
-    "unable_to_update_location": "",
-    "unable_to_update_settings": "",
-    "unable_to_update_user": ""
-  },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
-  "exit_slideshow": "",
-  "expand_all": "",
-  "expire_after": "Expiruje po",
-  "expired": "Vypršalo",
-  "explore": "",
-  "extension": "",
-  "external_libraries": "",
-  "failed_to_get_people": "",
-  "favorite": "Obľúbené",
-  "favorite_or_unfavorite_photo": "",
-  "favorites": "Obľúbené",
-  "feature": "",
-  "feature_photo_updated": "",
-  "featurecollection": "",
-  "file_name": "",
-  "file_name_or_extension": "",
-  "filename": "",
-  "files": "",
-  "filetype": "",
-  "filter_people": "",
-  "fix_incorrect_match": "",
-  "force_re-scan_library_files": "",
-  "forward": "",
-  "general": "",
-  "get_help": "",
-  "getting_started": "",
-  "go_back": "",
-  "go_to_search": "",
-  "go_to_share_page": "",
-  "group_albums_by": "",
-  "has_quota": "",
-  "hide_gallery": "",
-  "hide_password": "",
-  "hide_person": "",
-  "host": "",
-  "hour": "",
-  "image": "",
-  "img": "",
-  "immich_logo": "",
-  "import_path": "",
-  "in_archive": "",
-  "include_archived": "Zahrnúť archivované",
-  "include_shared_albums": "",
-  "include_shared_partner_assets": "",
-  "individual_share": "",
-  "info": "",
-  "interval": {
-    "day_at_onepm": "",
-    "hours": "",
-    "night_at_midnight": "",
-    "night_at_twoam": ""
-  },
-  "invite_people": "",
-  "invite_to_album": "Pozvať do albumu",
-  "job_settings_description": "",
-  "jobs": "",
-  "keep": "",
-  "keyboard_shortcuts": "",
-  "language": "",
-  "language_setting_description": "",
-  "last_seen": "",
-  "leave": "",
-  "let_others_respond": "Nechajte ostatných reagovať",
-  "level": "",
-  "library": "Knižnica",
-  "library_options": "",
-  "light": "",
-  "link_options": "",
-  "link_to_oauth": "",
-  "linked_oauth_account": "",
-  "list": "",
-  "loading": "",
-  "loading_search_results_failed": "",
-  "log_out": "Odhlásiť sa",
-  "log_out_all_devices": "",
-  "login_has_been_disabled": "",
-  "look": "",
-  "loop_videos": "",
-  "loop_videos_description": "",
-  "make": "",
-  "manage_shared_links": "Spravovať zdieľané odkazy",
-  "manage_sharing_with_partners": "",
-  "manage_the_app_settings": "",
-  "manage_your_account": "",
-  "manage_your_api_keys": "",
-  "manage_your_devices": "",
-  "manage_your_oauth_connection": "",
-  "map": "",
-  "map_marker_with_image": "",
-  "map_settings": "Nastavenia máp",
-  "media_type": "",
-  "memories": "",
-  "memories_setting_description": "",
-  "menu": "",
-  "merge": "",
-  "merge_people": "",
-  "merge_people_successfully": "",
-  "minimize": "",
-  "minute": "",
-  "missing": "",
-  "model": "",
-  "month": "Mesiac",
-  "more": "",
-  "moved_to_trash": "",
-  "my_albums": "",
-  "name": "Meno",
-  "name_or_nickname": "",
-  "never": "nikdy",
-  "new_api_key": "",
-  "new_password": "Nové heslo",
-  "new_person": "",
-  "new_user_created": "",
-  "newest_first": "",
-  "next": "Ďalej",
-  "next_memory": "",
-  "no": "",
-  "no_albums_message": "",
-  "no_archived_assets_message": "",
-  "no_assets_message": "",
-  "no_exif_info_available": "",
-  "no_explore_results_message": "",
-  "no_favorites_message": "",
-  "no_libraries_message": "",
-  "no_name": "",
-  "no_places": "",
-  "no_results": "",
-  "no_shared_albums_message": "",
-  "not_in_any_album": "",
-  "notes": "",
-  "notification_toggle_setting_description": "",
-  "notifications": "Oznámenia",
-  "notifications_setting_description": "",
-  "oauth": "",
-  "offline": "",
-  "ok": "",
-  "oldest_first": "",
-  "online": "",
-  "only_favorites": "",
-  "only_refreshes_modified_files": "",
-  "open_the_search_filters": "",
-  "options": "Nastavenia",
-  "organize_your_library": "",
-  "other": "",
-  "other_devices": "",
-  "other_variables": "",
-  "owned": "Vlastnené",
-  "owner": "Vlastník",
-  "partner_sharing": "",
-  "partners": "",
-  "password": "Heslo",
-  "password_does_not_match": "",
-  "password_required": "",
-  "password_reset_success": "",
-  "past_durations": {
-    "days": "",
-    "hours": "",
-    "years": ""
-  },
-  "path": "",
-  "pattern": "",
-  "pause": "",
-  "pause_memories": "",
-  "paused": "",
-  "pending": "",
-  "people": "Ľudia",
-  "people_sidebar_description": "",
-  "perform_library_tasks": "",
-  "permanent_deletion_warning": "",
-  "permanent_deletion_warning_setting_description": "",
-  "permanently_delete": "",
-  "permanently_deleted_asset": "",
-  "photos": "Fotografie",
-  "photos_from_previous_years": "",
-  "pick_a_location": "",
-  "place": "",
-  "places": "Miesta",
-  "play": "",
-  "play_memories": "",
-  "play_motion_photo": "",
-  "play_or_pause_video": "",
-  "point": "",
-  "port": "",
-  "preset": "",
-  "preview": "",
-  "previous": "",
-  "previous_memory": "",
-  "previous_or_next_photo": "",
-  "primary": "",
-  "profile_picture_set": "",
-  "public_share": "",
-  "range": "",
-  "raw": "",
-  "reaction_options": "",
-  "read_changelog": "",
-  "recent": "",
-  "recent_searches": "",
-  "refresh": "",
-  "refreshed": "",
-  "refreshes_every_file": "",
-  "remove": "",
-  "remove_from_album": "Odstrániť z albumu",
-  "remove_from_favorites": "",
-  "remove_from_shared_link": "",
-  "remove_offline_files": "",
-  "repair": "",
-  "repair_no_results_message": "",
-  "replace_with_upload": "",
-  "require_password": "",
-  "reset": "",
-  "reset_password": "",
-  "reset_people_visibility": "",
-  "reset_settings_to_default": "",
-  "restore": "Obnoviť",
-  "restore_user": "",
-  "retry_upload": "",
-  "review_duplicates": "",
-  "role": "",
-  "save": "Uložiť",
-  "saved_profile": "",
-  "saved_settings": "",
-  "say_something": "Napíšte niečo",
-  "scan_all_libraries": "",
-  "scan_all_library_files": "",
-  "scan_new_library_files": "",
-  "scan_settings": "",
-  "search": "Vyhľadávanie",
-  "search_albums": "",
-  "search_by_context": "",
-  "search_camera_make": "",
-  "search_camera_model": "",
-  "search_city": "",
-  "search_country": "",
-  "search_for_existing_person": "",
-  "search_people": "",
-  "search_places": "",
-  "search_state": "",
-  "search_timezone": "",
-  "search_type": "",
-  "search_your_photos": "Prehľadajte svoje obrázky",
-  "searching_locales": "",
-  "second": "",
-  "select_album_cover": "",
-  "select_all": "",
-  "select_avatar_color": "",
-  "select_face": "",
-  "select_featured_photo": "",
-  "select_library_owner": "",
-  "select_new_face": "",
-  "select_photos": "Vybrať fotografie",
-  "selected": "",
-  "send_message": "",
-  "server": "",
-  "server_stats": "",
-  "set": "",
-  "set_as_album_cover": "",
-  "set_as_profile_picture": "",
-  "set_date_of_birth": "",
-  "set_profile_picture": "",
-  "set_slideshow_to_fullscreen": "",
-  "settings": "Nastavenia",
-  "settings_saved": "",
-  "share": "Zdieľať",
-  "shared": "Zdieľané",
-  "shared_by": "",
-  "shared_by_you": "",
-  "shared_links": "Zdieľané odkazy",
-  "sharing": "Zdieľanie",
-  "sharing_sidebar_description": "",
-  "show_album_options": "",
-  "show_file_location": "",
-  "show_gallery": "",
-  "show_hidden_people": "",
-  "show_in_timeline": "",
-  "show_in_timeline_setting_description": "",
-  "show_keyboard_shortcuts": "",
-  "show_metadata": "Zobraziť metadáta",
-  "show_or_hide_info": "",
-  "show_password": "",
-  "show_person_options": "",
-  "show_progress_bar": "",
-  "show_search_options": "",
-  "shuffle": "",
-  "sign_up": "",
-  "size": "",
-  "skip_to_content": "",
-  "slideshow": "",
-  "slideshow_settings": "",
-  "sort_albums_by": "",
-  "stack": "Zoskupenie",
-  "stack_selected_photos": "",
-  "stacktrace": "",
-  "start_date": "",
-  "state": "",
-  "status": "",
-  "stop_motion_photo": "",
-  "stop_photo_sharing": "Zastaviť zdieľanie vašich fotiek?",
-  "storage": "",
-  "storage_label": "",
-  "submit": "",
-  "suggestions": "Návrhy",
-  "sunrise_on_the_beach": "",
-  "swap_merge_direction": "",
-  "sync": "",
-  "template": "",
-  "theme": "Téma",
-  "theme_selection": "",
-  "theme_selection_description": "",
-  "time_based_memories": "",
-  "timezone": "Časové pásmo",
-  "toggle_settings": "",
-  "toggle_theme": "",
-  "toggle_visibility": "",
-  "total_usage": "",
-  "trash": "Kôš",
-  "trash_all": "",
-  "trash_no_results_message": "",
-  "type": "",
-  "unarchive": "Odarchivovať",
-  "unarchived": "",
-  "unfavorite": "Odznačiť ako obľúbené",
-  "unhide_person": "",
-  "unknown": "",
-  "unknown_album": "",
-  "unknown_year": "",
-  "unlink_oauth": "",
-  "unlinked_oauth_account": "",
-  "unselect_all": "",
-  "unstack": "Odskupiť",
-  "up_next": "",
-  "updated_password": "",
-  "upload": "Nahrať",
-  "upload_concurrency": "",
-  "url": "",
-  "usage": "",
-  "user": "",
-  "user_id": "",
-  "user_usage_detail": "",
-  "username": "",
-  "users": "",
-  "utilities": "",
-  "validate": "",
-  "variables": "",
-  "version": "",
-  "video": "",
-  "video_hover_setting_description": "",
-  "videos": "Videá",
-  "view_all": "Zobraziť všetky",
-  "view_all_users": "",
-  "view_links": "",
-  "view_next_asset": "",
-  "view_previous_asset": "",
-  "viewer": "",
-  "waiting": "",
-  "week": "",
-  "welcome_to_immich": "",
-  "year": "",
-  "yes": "Áno",
-  "zoom_image": ""
-}
diff --git a/web/src/lib/i18n/sl.json b/web/src/lib/i18n/sl.json
deleted file mode 100644
index ccd488174b..0000000000
--- a/web/src/lib/i18n/sl.json
+++ /dev/null
@@ -1,833 +0,0 @@
-{
-  "about": "O programu",
-  "account": "Račun",
-  "account_settings": "Nastavitve računa",
-  "acknowledge": "Potrdi",
-  "action": "Dejanje",
-  "actions": "Dejanja",
-  "active": "Aktivno",
-  "activity": "Aktivnost",
-  "activity_changed": "Aktivnost {enabled, select, true {omogočena} other {onemogočena}}",
-  "add": "Dodaj",
-  "add_a_description": "Dodaj opis",
-  "add_a_location": "Dodaj lokacijo",
-  "add_a_name": "Dodaj ime",
-  "add_a_title": "Dodaj naslov",
-  "add_exclusion_pattern": "Dodaj vzorec izključitve",
-  "add_import_path": "Dodaj pot uvoza",
-  "add_location": "Dodaj lokacijo",
-  "add_more_users": "Dodaj še več uporabnikov",
-  "add_partner": "Dodaj partnerja",
-  "add_path": "Dodaj pot",
-  "add_photos": "Dodaj fotografije",
-  "add_to": "Dodaj k...",
-  "add_to_album": "Dodaj v album",
-  "add_to_shared_album": "Dodaj k deljenemu albumu",
-  "added_to_archive": "Dodano v arhiv",
-  "added_to_favorites": "Dodano med priljubljene",
-  "added_to_favorites_count": "{count, number} dodanih med priljubljene",
-  "admin": {
-    "add_exclusion_pattern_description": "Dodajte vzorec izključitev. Globiranje z uporabo *, ** in ? je podprto. Če želite prezreti vse datoteke v katerem koli imeniku z imenom \"Raw\", uporabite \"**/Raw/**\". Če želite prezreti vse datoteke, ki se končajo na \".tif\", uporabite \"**/*.tif\". Če želite prezreti absolutno pot, uporabite \"/pot/za/ignoriranje/**\".",
-    "authentication_settings": "Nastavitve preverjanja pristnosti",
-    "authentication_settings_description": "Upravljanje gesel, OAuth in drugih nastavitev preverjanja pristnosti",
-    "authentication_settings_disable_all": "Ali zares želite onemogočiti vse prijavne metode? Prijava bo popolnoma onemogočena.",
-    "authentication_settings_reenable": "Ponovno omogoči z uporabo <link>Server Command</link>.",
-    "background_task_job": "Opravila v ozadju",
-    "check_all": "Označi vse",
-    "cleared_jobs": "Razčiščeno delo za: {job}",
-    "config_set_by_file": "Konfiguracija je trenutno nastavljena s konfiguracijsko datoteko",
-    "confirm_delete_library": "Ali ste prepričani, da želite izbrisati knjižnico {library}?",
-    "confirm_delete_library_assets": "Ali ste prepričani, da želite izbrisati to knjižnico? S tem boste iz Immicha izbrisali vsa {count} vsebovana sredstva in tega dejanja ni mogoče razveljaviti. Datoteke bodo ostale na disku.",
-    "confirm_email_below": "Za potrditev vnesite \"{email}\" spodaj",
-    "confirm_reprocess_all_faces": "Ali ste prepričani, da želite znova obdelati vse obraze? S tem boste počistili tudi že imenovane osebe.",
-    "confirm_user_password_reset": "Ali ste prepričani, da želite ponastaviti geslo uporabnika {user}?",
-    "crontab_guru": "Crontab guru",
-    "disable_login": "Onemogoči prijavo",
-    "disabled": "",
-    "duplicate_detection_job_description": "Zaženite strojno učenje na sredstvih, da zaznate podobne slike. Zanaša se na Pametno Iskanje",
-    "exclusion_pattern_description": "Vzorci izključitev vam omogočajo, da prezrete datoteke in mape pri skeniranju knjižnice. To je uporabno, če imate mape z datotekami, ki jih ne želite uvoziti, na primer datoteke RAW.",
-    "external_library_created_at": "Zunanja knjižnica (ustvarjena dne {date})",
-    "external_library_management": "Upravljanje zunanje knjižnice",
-    "face_detection": "Zaznavanje obrazov",
-    "face_detection_description": "Zaznajte obraze v sredstvih s pomočjo strojnega učenja. Pri videoposnetkih se upošteva samo sličica. \"Vse\" (ponovno) obdela vsa sredstva. \"Manjkajoče\" postavi v čakalno vrsto sredstva, ki še niso bila obdelana. Zaznani obrazi bodo postavljeni v čakalno vrsto za prepoznavanje obrazov, ko bo zaznavanje obrazov končano, in jih bodo združili v obstoječe ali nove osebe.",
-    "facial_recognition_job_description": "Združi zaznane obraze v osebe. Ta korak se izvede po končanem zaznavanju obrazov. \"Vse\" (ponovno) združuje vse obraze. \"Manjkajoče\", doda v čakalno vrsto obraze, ki nimajo dodeljene osebe.",
-    "failed_job_command": "Za opravilo {job} ukaz {command} ni uspel",
-    "force_delete_user_warning": "OPOZORILO: S tem boste takoj odstranili uporabnika in vsa sredstva. Tega ni mogoče razveljaviti in datotek ni mogoče obnoviti.",
-    "forcing_refresh_library_files": "Vsiljena osvežitev vseh datotek knjižnice",
-    "image_format_description": "WebP ustvari manjše datoteke kot JPEG, vendar je počasnejši za kodiranje.",
-    "image_prefer_embedded_preview": "Uporabi raje vdelan predogled",
-    "image_prefer_embedded_preview_setting_description": "Uporabite vdelane predoglede v fotografije RAW kot vhod za obdelavo slik, ko so na voljo. To lahko ustvari natančnejše barve za nekatere slike, vendar je kakovost predogleda odvisna od kamere in slika ima lahko več artefaktov stiskanja.",
-    "image_prefer_wide_gamut": "Uporabi raje širok razpon",
-    "image_prefer_wide_gamut_setting_description": "Uporabite P3 Display za sličice. To bolje ohranja živahnost slik s širokimi barvnimi prostori, vendar so lahko slike videti drugače na starih napravah s staro različico brskalnika. Slike sRGB se ohranijo kot sRGB, da se izognejo barvnim zamikom.",
-    "image_preview_format": "Oblika predogleda",
-    "image_preview_resolution": "Ločljivost predogleda",
-    "image_preview_resolution_description": "Uporablja se pri ogledu ene fotografije in za strojno učenje. Višje ločljivosti lahko ohranijo več podrobnosti, vendar kodiranje traja dlje, imajo večje velikosti datotek in lahko zmanjšajo odzivnost aplikacije.",
-    "image_quality": "Kvaliteta",
-    "image_quality_description": "Kakovost slike od 1-100. Višja je boljša za kakovost, vendar ustvarja večje datoteke in ta možnost lahko vpliva na predogled in sličice.",
-    "image_settings": "Nastavitve slike",
-    "image_settings_description": "Upravljajte kakovost in ločljivost ustvarjenih slik",
-    "image_thumbnail_format": "Oblika sličic",
-    "image_thumbnail_resolution": "Ločljivost sličic",
-    "image_thumbnail_resolution_description": "Uporablja se pri ogledovanju skupin fotografij (glavna časovnica, pogled albuma itd.). Višje ločljivosti lahko ohranijo več podrobnosti, vendar kodiranje traja dlje, imajo večje velikosti datotek in lahko zmanjšajo odzivnost aplikacije.",
-    "job_concurrency": "{job} sočasnost",
-    "job_not_concurrency_safe": "To opravilo ni sočasno-varno.",
-    "job_settings": "Nastavitve opravil",
-    "job_settings_description": "Upravljaj sočasnost opravil",
-    "job_status": "Status opravila",
-    "jobs_delayed": "{jobCount} zadržan",
-    "jobs_failed": "{jobCount} neuspešen",
-    "library_created": "Ustvarjena knjižnica: {library}",
-    "library_cron_expression": "",
-    "library_cron_expression_presets": "",
-    "library_deleted": "Knjižnica izbrisana",
-    "library_import_path_description": "Določi mapo za uvoz. Ta mapa in njene podmape bodo pregledane za slike in video posnetke.",
-    "library_scanning": "Periodični pregledi",
-    "library_scanning_description": "Nastavi periodični pregled knjižnic",
-    "library_scanning_enable_description": "Omogoči periodični pregled knjižnic",
-    "library_settings": "Zunanja knjižnica",
-    "library_settings_description": "Uredi nastavitve zunanje knjižnice",
-    "library_tasks_description": "Izvedi nalogo knjižnice",
-    "library_watching_enable_description": "Opazuj spremembe datotek v zunanji knjižnici",
-    "library_watching_settings": "Opazovanje knjižnice (EKSPERIMENTALNO)",
-    "library_watching_settings_description": "Samodejno opazuj spremembo datotek",
-    "logging_enable_description": "Omogoči dnevnik",
-    "logging_level_description": "Nivo dnevnika, ko je le-ta omogočen.",
-    "logging_settings": "Dnevnik",
-    "machine_learning_clip_model": "model CLIP",
-    "machine_learning_clip_model_description": "Ime CLIP modela iz seznama <link>tukaj</link>. Vedite, da boste morali po menjavi modela ponovno zagnati delo za 'Pametno iskanje' za vse slike.",
-    "machine_learning_duplicate_detection": "Zaznavanje dvojnikov",
-    "machine_learning_duplicate_detection_enabled": "Omogoči zaznavanje dvojnikov",
-    "machine_learning_duplicate_detection_enabled_description": "Če je onemogočeno, bodo popolnoma enaki posnetki še vedno obravnavani.",
-    "machine_learning_duplicate_detection_setting_description": "Za iskanje verjetnih dvojnikov uporabite vdelave CLIP",
-    "machine_learning_enabled": "Omogoči strojno učenje",
-    "machine_learning_enabled_description": "Če je onemogočeno, bodo vse funkcije strojnega učenja onemogočene ne glede na spodnje nastavitve.",
-    "machine_learning_facial_recognition": "Zaznavanje obrazov",
-    "machine_learning_facial_recognition_description": "",
-    "machine_learning_facial_recognition_model": "",
-    "machine_learning_facial_recognition_model_description": "",
-    "machine_learning_facial_recognition_setting_description": "",
-    "machine_learning_max_detection_distance": "",
-    "machine_learning_max_detection_distance_description": "",
-    "machine_learning_max_recognition_distance": "",
-    "machine_learning_max_recognition_distance_description": "",
-    "machine_learning_min_detection_score": "",
-    "machine_learning_min_detection_score_description": "",
-    "machine_learning_min_recognized_faces": "",
-    "machine_learning_min_recognized_faces_description": "",
-    "machine_learning_settings": "",
-    "machine_learning_settings_description": "",
-    "machine_learning_smart_search": "",
-    "machine_learning_smart_search_description": "",
-    "machine_learning_smart_search_enabled_description": "",
-    "machine_learning_url_description": "",
-    "manage_log_settings": "",
-    "map_dark_style": "",
-    "map_enable_description": "",
-    "map_light_style": "",
-    "map_reverse_geocoding": "",
-    "map_reverse_geocoding_enable_description": "",
-    "map_reverse_geocoding_settings": "",
-    "map_settings": "",
-    "map_settings_description": "",
-    "map_style_description": "",
-    "metadata_extraction_job_description": "",
-    "migration_job_description": "",
-    "notification_email_from_address": "",
-    "notification_email_from_address_description": "",
-    "notification_email_host_description": "",
-    "notification_email_ignore_certificate_errors": "",
-    "notification_email_ignore_certificate_errors_description": "",
-    "notification_email_password_description": "",
-    "notification_email_port_description": "",
-    "notification_email_sent_test_email_button": "",
-    "notification_email_setting_description": "",
-    "notification_email_test_email_failed": "",
-    "notification_email_test_email_sent": "",
-    "notification_email_username_description": "",
-    "notification_enable_email_notifications": "",
-    "notification_settings": "",
-    "notification_settings_description": "",
-    "oauth_auto_launch": "",
-    "oauth_auto_launch_description": "",
-    "oauth_auto_register": "",
-    "oauth_auto_register_description": "",
-    "oauth_button_text": "",
-    "oauth_client_id": "",
-    "oauth_client_secret": "",
-    "oauth_enable_description": "",
-    "oauth_issuer_url": "",
-    "oauth_mobile_redirect_uri": "",
-    "oauth_mobile_redirect_uri_override": "",
-    "oauth_mobile_redirect_uri_override_description": "",
-    "oauth_scope": "",
-    "oauth_settings": "",
-    "oauth_settings_description": "",
-    "oauth_signing_algorithm": "",
-    "oauth_storage_label_claim": "",
-    "oauth_storage_label_claim_description": "",
-    "oauth_storage_quota_claim": "",
-    "oauth_storage_quota_claim_description": "",
-    "oauth_storage_quota_default": "",
-    "oauth_storage_quota_default_description": "",
-    "password_enable_description": "",
-    "password_settings": "",
-    "password_settings_description": "",
-    "server_external_domain_settings": "",
-    "server_external_domain_settings_description": "",
-    "server_settings": "",
-    "server_settings_description": "",
-    "server_welcome_message": "",
-    "server_welcome_message_description": "",
-    "sidecar_job_description": "",
-    "slideshow_duration_description": "",
-    "smart_search_job_description": "",
-    "storage_template_enable_description": "",
-    "storage_template_hash_verification_enabled": "",
-    "storage_template_hash_verification_enabled_description": "",
-    "storage_template_migration_job": "",
-    "storage_template_settings": "",
-    "storage_template_settings_description": "",
-    "theme_custom_css_settings": "",
-    "theme_custom_css_settings_description": "",
-    "theme_settings": "",
-    "theme_settings_description": "",
-    "thumbnail_generation_job_description": "",
-    "transcode_policy_description": "",
-    "transcoding_acceleration_api": "",
-    "transcoding_acceleration_api_description": "",
-    "transcoding_acceleration_nvenc": "",
-    "transcoding_acceleration_qsv": "",
-    "transcoding_acceleration_rkmpp": "",
-    "transcoding_acceleration_vaapi": "",
-    "transcoding_accepted_audio_codecs": "",
-    "transcoding_accepted_audio_codecs_description": "",
-    "transcoding_accepted_video_codecs": "",
-    "transcoding_accepted_video_codecs_description": "",
-    "transcoding_advanced_options_description": "",
-    "transcoding_audio_codec": "",
-    "transcoding_audio_codec_description": "",
-    "transcoding_bitrate_description": "",
-    "transcoding_constant_quality_mode": "",
-    "transcoding_constant_quality_mode_description": "",
-    "transcoding_constant_rate_factor": "",
-    "transcoding_constant_rate_factor_description": "",
-    "transcoding_disabled_description": "",
-    "transcoding_hardware_acceleration": "",
-    "transcoding_hardware_acceleration_description": "",
-    "transcoding_hardware_decoding": "",
-    "transcoding_hardware_decoding_setting_description": "",
-    "transcoding_hevc_codec": "",
-    "transcoding_max_b_frames": "",
-    "transcoding_max_b_frames_description": "",
-    "transcoding_max_bitrate": "",
-    "transcoding_max_bitrate_description": "",
-    "transcoding_max_keyframe_interval": "",
-    "transcoding_max_keyframe_interval_description": "",
-    "transcoding_optimal_description": "",
-    "transcoding_preferred_hardware_device": "",
-    "transcoding_preferred_hardware_device_description": "",
-    "transcoding_preset_preset": "",
-    "transcoding_preset_preset_description": "",
-    "transcoding_reference_frames": "",
-    "transcoding_reference_frames_description": "",
-    "transcoding_required_description": "",
-    "transcoding_settings": "",
-    "transcoding_settings_description": "",
-    "transcoding_target_resolution": "",
-    "transcoding_target_resolution_description": "",
-    "transcoding_temporal_aq": "",
-    "transcoding_temporal_aq_description": "",
-    "transcoding_threads": "",
-    "transcoding_threads_description": "",
-    "transcoding_tone_mapping": "",
-    "transcoding_tone_mapping_description": "",
-    "transcoding_tone_mapping_npl": "",
-    "transcoding_tone_mapping_npl_description": "",
-    "transcoding_transcode_policy": "",
-    "transcoding_two_pass_encoding": "",
-    "transcoding_two_pass_encoding_setting_description": "",
-    "transcoding_video_codec": "",
-    "transcoding_video_codec_description": "",
-    "trash_enabled_description": "",
-    "trash_number_of_days": "",
-    "trash_number_of_days_description": "",
-    "trash_settings": "",
-    "trash_settings_description": "",
-    "user_delete_delay_settings": "",
-    "user_delete_delay_settings_description": "",
-    "user_settings": "",
-    "user_settings_description": "",
-    "version_check_enabled_description": "",
-    "version_check_settings": "",
-    "version_check_settings_description": "",
-    "video_conversion_job_description": ""
-  },
-  "admin_email": "",
-  "admin_password": "",
-  "administration": "",
-  "advanced": "Napredno",
-  "album_added": "",
-  "album_added_notification_setting_description": "",
-  "album_cover_updated": "",
-  "album_info_updated": "",
-  "album_name": "",
-  "album_options": "",
-  "album_updated": "",
-  "album_updated_setting_description": "",
-  "albums": "Albumi",
-  "all": "Vse",
-  "all_people": "",
-  "allow_dark_mode": "",
-  "allow_edits": "",
-  "api_key": "",
-  "api_keys": "",
-  "app_settings": "",
-  "appears_in": "",
-  "archive": "Arhiv",
-  "archive_or_unarchive_photo": "",
-  "archived": "",
-  "asset_offline": "",
-  "assets": "sredstva",
-  "authorized_devices": "",
-  "back": "Nazaj",
-  "backward": "",
-  "blurred_background": "",
-  "camera": "",
-  "camera_brand": "",
-  "camera_model": "",
-  "cancel": "Prekliči",
-  "cancel_search": "",
-  "cannot_merge_people": "",
-  "cannot_update_the_description": "",
-  "cant_apply_changes": "",
-  "cant_get_faces": "",
-  "cant_search_people": "",
-  "cant_search_places": "",
-  "change_date": "",
-  "change_expiration_time": "Spremeni čas poteka",
-  "change_location": "",
-  "change_name": "",
-  "change_name_successfully": "",
-  "change_password": "Zamenjaj geslo",
-  "change_your_password": "",
-  "changed_visibility_successfully": "",
-  "check_logs": "",
-  "city": "Mesto",
-  "clear": "Počisti",
-  "clear_all": "",
-  "clear_message": "",
-  "clear_value": "",
-  "close": "",
-  "collapse_all": "",
-  "color_theme": "",
-  "comment_options": "",
-  "comments_are_disabled": "",
-  "confirm": "Potrdi",
-  "confirm_admin_password": "",
-  "confirm_password": "Potrdi geslo",
-  "contain": "",
-  "context": "",
-  "continue": "",
-  "copied_image_to_clipboard": "",
-  "copy_error": "",
-  "copy_file_path": "",
-  "copy_image": "",
-  "copy_link": "",
-  "copy_link_to_clipboard": "",
-  "copy_password": "",
-  "copy_to_clipboard": "",
-  "country": "Država",
-  "cover": "",
-  "covers": "",
-  "create": "Ustvari",
-  "create_album": "Ustvari album",
-  "create_library": "",
-  "create_link": "Ustvari povezavo",
-  "create_link_to_share": "Ustvari povezavo za skupno rabo",
-  "create_new_person": "",
-  "create_new_user": "",
-  "create_user": "",
-  "created": "",
-  "current_device": "",
-  "custom_locale": "",
-  "custom_locale_description": "",
-  "dark": "",
-  "date_after": "",
-  "date_and_time": "Datum in ura",
-  "date_before": "",
-  "date_range": "Časovno obdobje",
-  "day": "",
-  "default_locale": "",
-  "default_locale_description": "",
-  "delete": "Izbriši",
-  "delete_album": "Izbriši album",
-  "delete_key": "",
-  "delete_library": "",
-  "delete_link": "",
-  "delete_shared_link": "Izbriši povezavo skupne rabe",
-  "delete_user": "",
-  "deleted_shared_link": "",
-  "description": "Opis",
-  "details": "PODROBNOSTI",
-  "direction": "",
-  "disallow_edits": "",
-  "discover": "",
-  "dismiss_all_errors": "",
-  "dismiss_error": "",
-  "display_options": "",
-  "display_order": "",
-  "display_original_photos": "",
-  "display_original_photos_setting_description": "",
-  "done": "Končano",
-  "download": "Prenesi",
-  "downloading": "",
-  "duration": "",
-  "durations": {
-    "days": "",
-    "hours": "",
-    "minutes": "",
-    "months": "",
-    "years": ""
-  },
-  "edit_album": "",
-  "edit_avatar": "",
-  "edit_date": "",
-  "edit_date_and_time": "",
-  "edit_exclusion_pattern": "",
-  "edit_faces": "",
-  "edit_import_path": "",
-  "edit_import_paths": "",
-  "edit_key": "",
-  "edit_link": "Uredi povezavo",
-  "edit_location": "Uredi lokacijo",
-  "edit_name": "Uredi ime",
-  "edit_people": "",
-  "edit_title": "",
-  "edit_user": "",
-  "edited": "",
-  "editor": "",
-  "email": "E-pošta",
-  "empty": "",
-  "empty_album": "",
-  "empty_trash": "Izprazni smeti",
-  "enable": "",
-  "enabled": "",
-  "end_date": "",
-  "error": "",
-  "error_loading_image": "",
-  "errors": {
-    "unable_to_add_album_users": "",
-    "unable_to_add_comment": "",
-    "unable_to_add_partners": "",
-    "unable_to_change_album_user_role": "",
-    "unable_to_change_date": "",
-    "unable_to_change_location": "",
-    "unable_to_check_item": "",
-    "unable_to_check_items": "",
-    "unable_to_create_admin_account": "",
-    "unable_to_create_library": "",
-    "unable_to_create_user": "",
-    "unable_to_delete_album": "",
-    "unable_to_delete_asset": "",
-    "unable_to_delete_user": "",
-    "unable_to_empty_trash": "",
-    "unable_to_enter_fullscreen": "",
-    "unable_to_exit_fullscreen": "",
-    "unable_to_hide_person": "",
-    "unable_to_load_album": "",
-    "unable_to_load_asset_activity": "",
-    "unable_to_load_items": "",
-    "unable_to_load_liked_status": "",
-    "unable_to_play_video": "",
-    "unable_to_refresh_user": "",
-    "unable_to_remove_album_users": "",
-    "unable_to_remove_comment": "",
-    "unable_to_remove_library": "",
-    "unable_to_remove_partner": "",
-    "unable_to_remove_reaction": "",
-    "unable_to_remove_user": "",
-    "unable_to_repair_items": "",
-    "unable_to_reset_password": "",
-    "unable_to_resolve_duplicate": "",
-    "unable_to_restore_assets": "",
-    "unable_to_restore_trash": "",
-    "unable_to_restore_user": "",
-    "unable_to_save_album": "",
-    "unable_to_save_name": "",
-    "unable_to_save_profile": "",
-    "unable_to_save_settings": "",
-    "unable_to_scan_libraries": "",
-    "unable_to_scan_library": "",
-    "unable_to_set_profile_picture": "",
-    "unable_to_submit_job": "",
-    "unable_to_trash_asset": "",
-    "unable_to_unlink_account": "",
-    "unable_to_update_library": "",
-    "unable_to_update_location": "",
-    "unable_to_update_settings": "",
-    "unable_to_update_user": ""
-  },
-  "every_day_at_onepm": "",
-  "every_night_at_midnight": "",
-  "every_night_at_twoam": "",
-  "every_six_hours": "",
-  "exit_slideshow": "",
-  "expand_all": "",
-  "expire_after": "Poteče čez",
-  "expired": "Poteklo",
-  "explore": "",
-  "extension": "",
-  "external_libraries": "",
-  "failed_to_get_people": "",
-  "favorite": "Priljubljen",
-  "favorite_or_unfavorite_photo": "",
-  "favorites": "Priljubljene",
-  "feature": "",
-  "feature_photo_updated": "",
-  "featurecollection": "",
-  "file_name": "",
-  "file_name_or_extension": "",
-  "filename": "",
-  "files": "",
-  "filetype": "",
-  "filter_people": "",
-  "fix_incorrect_match": "",
-  "force_re-scan_library_files": "",
-  "forward": "",
-  "general": "",
-  "get_help": "",
-  "getting_started": "",
-  "go_back": "",
-  "go_to_search": "",
-  "go_to_share_page": "",
-  "group_albums_by": "",
-  "has_quota": "",
-  "hide_gallery": "",
-  "hide_password": "",
-  "hide_person": "",
-  "host": "",
-  "hour": "",
-  "image": "Slika",
-  "img": "",
-  "immich_logo": "",
-  "import_path": "",
-  "in_archive": "",
-  "include_archived": "Vključi arhivirane",
-  "include_shared_albums": "",
-  "include_shared_partner_assets": "",
-  "individual_share": "",
-  "info": "",
-  "interval": {
-    "day_at_onepm": "",
-    "hours": "",
-    "night_at_midnight": "",
-    "night_at_twoam": ""
-  },
-  "invite_people": "",
-  "invite_to_album": "Povabi v album",
-  "job_settings_description": "",
-  "jobs": "",
-  "keep": "",
-  "keyboard_shortcuts": "",
-  "language": "",
-  "language_setting_description": "",
-  "last_seen": "",
-  "leave": "",
-  "let_others_respond": "Naj drugi odgovorijo",
-  "level": "",
-  "library": "Knjižnica",
-  "library_options": "",
-  "light": "",
-  "link_options": "",
-  "link_to_oauth": "",
-  "linked_oauth_account": "",
-  "list": "",
-  "loading": "",
-  "loading_search_results_failed": "",
-  "log_out": "Odjava",
-  "log_out_all_devices": "",
-  "login_has_been_disabled": "",
-  "look": "",
-  "loop_videos": "",
-  "loop_videos_description": "Omogočite samodejno ponavljanje videoposnetka v pregledovalniku podrobnosti.",
-  "make": "Izdelava",
-  "manage_shared_links": "Upravljanje povezav v skupni rabi",
-  "manage_sharing_with_partners": "",
-  "manage_the_app_settings": "",
-  "manage_your_account": "",
-  "manage_your_api_keys": "",
-  "manage_your_devices": "",
-  "manage_your_oauth_connection": "",
-  "map": "",
-  "map_marker_with_image": "",
-  "map_settings": "Nastavitve zemljevida",
-  "media_type": "",
-  "memories": "",
-  "memories_setting_description": "",
-  "menu": "",
-  "merge": "",
-  "merge_people": "",
-  "merge_people_successfully": "",
-  "minimize": "",
-  "minute": "",
-  "missing": "",
-  "model": "",
-  "month": "Mesec",
-  "more": "",
-  "moved_to_trash": "",
-  "my_albums": "",
-  "name": "Ime",
-  "name_or_nickname": "",
-  "never": "nikoli",
-  "new_api_key": "",
-  "new_password": "Novo geslo",
-  "new_person": "",
-  "new_user_created": "",
-  "newest_first": "",
-  "next": "Naslednji",
-  "next_memory": "",
-  "no": "",
-  "no_albums_message": "",
-  "no_archived_assets_message": "",
-  "no_assets_message": "",
-  "no_exif_info_available": "",
-  "no_explore_results_message": "",
-  "no_favorites_message": "",
-  "no_libraries_message": "",
-  "no_name": "",
-  "no_places": "",
-  "no_results": "",
-  "no_shared_albums_message": "",
-  "not_in_any_album": "",
-  "notes": "",
-  "notification_toggle_setting_description": "",
-  "notifications": "Obvestila",
-  "notifications_setting_description": "",
-  "oauth": "",
-  "offline": "",
-  "ok": "V redu",
-  "oldest_first": "",
-  "online": "",
-  "only_favorites": "",
-  "only_refreshes_modified_files": "",
-  "open_the_search_filters": "",
-  "options": "Možnosti",
-  "organize_your_library": "",
-  "other": "",
-  "other_devices": "",
-  "other_variables": "",
-  "owned": "V lasti",
-  "owner": "Lastnik",
-  "partner_sharing": "",
-  "partners": "",
-  "password": "Geslo",
-  "password_does_not_match": "",
-  "password_required": "",
-  "password_reset_success": "",
-  "past_durations": {
-    "days": "",
-    "hours": "",
-    "years": ""
-  },
-  "path": "",
-  "pattern": "",
-  "pause": "",
-  "pause_memories": "",
-  "paused": "",
-  "pending": "",
-  "people": "Ljudje",
-  "people_sidebar_description": "",
-  "perform_library_tasks": "",
-  "permanent_deletion_warning": "",
-  "permanent_deletion_warning_setting_description": "",
-  "permanently_delete": "",
-  "permanently_deleted_asset": "",
-  "photos": "Slike",
-  "photos_from_previous_years": "",
-  "pick_a_location": "",
-  "place": "",
-  "places": "Lokacije",
-  "play": "",
-  "play_memories": "",
-  "play_motion_photo": "",
-  "play_or_pause_video": "",
-  "point": "",
-  "port": "",
-  "preset": "",
-  "preview": "",
-  "previous": "",
-  "previous_memory": "",
-  "previous_or_next_photo": "",
-  "primary": "",
-  "profile_picture_set": "",
-  "public_share": "",
-  "range": "",
-  "raw": "",
-  "reaction_options": "",
-  "read_changelog": "",
-  "recent": "",
-  "recent_searches": "",
-  "refresh": "",
-  "refreshed": "",
-  "refreshes_every_file": "",
-  "remove": "",
-  "remove_from_album": "Odstrani iz albuma",
-  "remove_from_favorites": "",
-  "remove_from_shared_link": "",
-  "remove_offline_files": "",
-  "repair": "",
-  "repair_no_results_message": "",
-  "replace_with_upload": "",
-  "require_password": "",
-  "reset": "",
-  "reset_password": "",
-  "reset_people_visibility": "",
-  "reset_settings_to_default": "",
-  "restore": "Obnovi",
-  "restore_user": "",
-  "retry_upload": "",
-  "review_duplicates": "",
-  "role": "",
-  "save": "Shrani",
-  "saved_profile": "",
-  "saved_settings": "",
-  "say_something": "Reci kaj",
-  "scan_all_libraries": "",
-  "scan_all_library_files": "",
-  "scan_new_library_files": "",
-  "scan_settings": "",
-  "search": "Iskanje",
-  "search_albums": "",
-  "search_by_context": "",
-  "search_camera_make": "",
-  "search_camera_model": "",
-  "search_city": "",
-  "search_country": "",
-  "search_for_existing_person": "",
-  "search_people": "",
-  "search_places": "",
-  "search_state": "",
-  "search_timezone": "",
-  "search_type": "",
-  "search_your_photos": "Poišči svoje fotografije",
-  "searching_locales": "",
-  "second": "",
-  "select_album_cover": "",
-  "select_all": "",
-  "select_avatar_color": "",
-  "select_face": "",
-  "select_featured_photo": "",
-  "select_library_owner": "",
-  "select_new_face": "",
-  "select_photos": "Izberi fotografije",
-  "selected": "",
-  "send_message": "",
-  "server": "",
-  "server_stats": "",
-  "set": "",
-  "set_as_album_cover": "",
-  "set_as_profile_picture": "",
-  "set_date_of_birth": "",
-  "set_profile_picture": "",
-  "set_slideshow_to_fullscreen": "",
-  "settings": "Nastavitve",
-  "settings_saved": "",
-  "share": "Deli",
-  "shared": "V skupni rabi",
-  "shared_by": "",
-  "shared_by_you": "",
-  "shared_links": "Povezave v skupni rabi",
-  "sharing": "Skupna raba",
-  "sharing_sidebar_description": "",
-  "show_album_options": "",
-  "show_file_location": "",
-  "show_gallery": "",
-  "show_hidden_people": "",
-  "show_in_timeline": "",
-  "show_in_timeline_setting_description": "",
-  "show_keyboard_shortcuts": "",
-  "show_metadata": "Pokaži metapodatke",
-  "show_or_hide_info": "",
-  "show_password": "",
-  "show_person_options": "",
-  "show_progress_bar": "",
-  "show_search_options": "",
-  "shuffle": "",
-  "sign_up": "",
-  "size": "",
-  "skip_to_content": "",
-  "slideshow": "",
-  "slideshow_settings": "",
-  "sort_albums_by": "",
-  "stack": "Sklad",
-  "stack_selected_photos": "",
-  "stacktrace": "",
-  "start_date": "",
-  "state": "Dežela",
-  "status": "",
-  "stop_motion_photo": "",
-  "stop_photo_sharing": "Želite prenehati deliti svoje fotografije?",
-  "storage": "",
-  "storage_label": "",
-  "submit": "",
-  "suggestions": "Predlogi",
-  "sunrise_on_the_beach": "",
-  "swap_merge_direction": "",
-  "sync": "",
-  "template": "",
-  "theme": "Tema",
-  "theme_selection": "",
-  "theme_selection_description": "",
-  "time_based_memories": "",
-  "timezone": "Časovni pas",
-  "toggle_settings": "",
-  "toggle_theme": "",
-  "toggle_visibility": "",
-  "total_usage": "",
-  "trash": "Smetnjak",
-  "trash_all": "",
-  "trash_no_results_message": "",
-  "type": "",
-  "unarchive": "Odstrani iz arhiva",
-  "unarchived": "",
-  "unfavorite": "Odstrani iz priljubljeno",
-  "unhide_person": "",
-  "unknown": "",
-  "unknown_album": "",
-  "unknown_year": "",
-  "unlink_oauth": "",
-  "unlinked_oauth_account": "",
-  "unselect_all": "",
-  "unstack": "Razkladi",
-  "up_next": "",
-  "updated_password": "",
-  "upload": "Naloži",
-  "upload_concurrency": "",
-  "url": "",
-  "usage": "",
-  "user": "",
-  "user_id": "",
-  "user_usage_detail": "",
-  "username": "",
-  "users": "",
-  "utilities": "",
-  "validate": "",
-  "variables": "",
-  "version": "",
-  "video": "",
-  "video_hover_setting_description": "",
-  "videos": "Videoposnetki",
-  "view_all": "Poglej vse",
-  "view_all_users": "",
-  "view_links": "",
-  "view_next_asset": "",
-  "view_previous_asset": "",
-  "viewer": "",
-  "waiting": "",
-  "week": "",
-  "welcome": "Dobrodošli",
-  "welcome_to_immich": "",
-  "year": "Leto",
-  "yes": "Da",
-  "zoom_image": "Povečava slike"
-}
diff --git a/web/src/lib/stores/asset-interaction.store.ts b/web/src/lib/stores/asset-interaction.store.ts
deleted file mode 100644
index f7db5382b0..0000000000
--- a/web/src/lib/stores/asset-interaction.store.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import type { AssetResponseDto } from '@immich/sdk';
-import { derived, readonly, writable } from 'svelte/store';
-
-export type AssetInteractionStore = ReturnType<typeof createAssetInteractionStore>;
-
-export function createAssetInteractionStore() {
-  const selectedAssets = writable(new Set<AssetResponseDto>());
-  const selectedGroup = writable(new Set<string>());
-  const isMultiSelectStoreState = derived(selectedAssets, ($selectedAssets) => $selectedAssets.size > 0);
-
-  // Candidates for the range selection. This set includes only loaded assets, so it improves highlight
-  // performance. From the user's perspective, range is highlighted almost immediately
-  const assetSelectionCandidates = writable(new Set<AssetResponseDto>());
-  // The beginning of the selection range
-  const assetSelectionStart = writable<AssetResponseDto | null>(null);
-
-  const selectAsset = (asset: AssetResponseDto) => {
-    selectedAssets.update(($selectedAssets) => $selectedAssets.add(asset));
-  };
-
-  const selectAssets = (assets: AssetResponseDto[]) => {
-    selectedAssets.update(($selectedAssets) => {
-      for (const asset of assets) {
-        $selectedAssets.add(asset);
-      }
-      return $selectedAssets;
-    });
-  };
-
-  const removeAssetFromMultiselectGroup = (asset: AssetResponseDto) => {
-    selectedAssets.update(($selectedAssets) => {
-      $selectedAssets.delete(asset);
-      return $selectedAssets;
-    });
-  };
-
-  const addGroupToMultiselectGroup = (group: string) => {
-    selectedGroup.update(($selectedGroup) => $selectedGroup.add(group));
-  };
-
-  const removeGroupFromMultiselectGroup = (group: string) => {
-    selectedGroup.update(($selectedGroup) => {
-      $selectedGroup.delete(group);
-      return $selectedGroup;
-    });
-  };
-
-  const setAssetSelectionStart = (asset: AssetResponseDto | null) => {
-    assetSelectionStart.set(asset);
-  };
-
-  const setAssetSelectionCandidates = (assets: AssetResponseDto[]) => {
-    assetSelectionCandidates.set(new Set(assets));
-  };
-
-  const clearAssetSelectionCandidates = () => {
-    assetSelectionCandidates.set(new Set());
-  };
-
-  const clearMultiselect = () => {
-    // Multi-selection
-    selectedAssets.set(new Set());
-    selectedGroup.set(new Set());
-
-    // Range selection
-    assetSelectionCandidates.set(new Set());
-    assetSelectionStart.set(null);
-  };
-
-  return {
-    selectAsset,
-    selectAssets,
-    removeAssetFromMultiselectGroup,
-    addGroupToMultiselectGroup,
-    removeGroupFromMultiselectGroup,
-    setAssetSelectionCandidates,
-    clearAssetSelectionCandidates,
-    setAssetSelectionStart,
-    clearMultiselect,
-    isMultiSelectState: readonly(isMultiSelectStoreState),
-    selectedAssets: readonly(selectedAssets),
-    selectedGroup: readonly(selectedGroup),
-    assetSelectionCandidates: readonly(assetSelectionCandidates),
-    assetSelectionStart: readonly(assetSelectionStart),
-  };
-}
diff --git a/web/src/lib/stores/asset-interaction.svelte.spec.ts b/web/src/lib/stores/asset-interaction.svelte.spec.ts
new file mode 100644
index 0000000000..5d3043b37c
--- /dev/null
+++ b/web/src/lib/stores/asset-interaction.svelte.spec.ts
@@ -0,0 +1,40 @@
+import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
+import { resetSavedUser, user } from '$lib/stores/user.store';
+import { assetFactory } from '@test-data/factories/asset-factory';
+import { userAdminFactory } from '@test-data/factories/user-factory';
+
+describe('AssetInteraction', () => {
+  let assetInteraction: AssetInteraction;
+
+  beforeEach(() => {
+    assetInteraction = new AssetInteraction();
+  });
+
+  it('calculates derived values from selection', () => {
+    assetInteraction.selectAsset(assetFactory.build({ isFavorite: true, isArchived: true, isTrashed: true }));
+    assetInteraction.selectAsset(assetFactory.build({ isFavorite: true, isArchived: false, isTrashed: false }));
+
+    expect(assetInteraction.selectionActive).toBe(true);
+    expect(assetInteraction.isAllTrashed).toBe(false);
+    expect(assetInteraction.isAllArchived).toBe(false);
+    expect(assetInteraction.isAllFavorite).toBe(true);
+  });
+
+  it('updates isAllUserOwned when the active user changes', () => {
+    const [user1, user2] = userAdminFactory.buildList(2);
+    assetInteraction.selectAsset(assetFactory.build({ ownerId: user1.id }));
+
+    const cleanup = $effect.root(() => {
+      expect(assetInteraction.isAllUserOwned).toBe(false);
+
+      user.set(user1);
+      expect(assetInteraction.isAllUserOwned).toBe(true);
+
+      user.set(user2);
+      expect(assetInteraction.isAllUserOwned).toBe(false);
+    });
+
+    cleanup();
+    resetSavedUser();
+  });
+});
diff --git a/web/src/lib/stores/asset-interaction.svelte.ts b/web/src/lib/stores/asset-interaction.svelte.ts
new file mode 100644
index 0000000000..4397c7f71f
--- /dev/null
+++ b/web/src/lib/stores/asset-interaction.svelte.ts
@@ -0,0 +1,66 @@
+import { user } from '$lib/stores/user.store';
+import type { AssetResponseDto, UserAdminResponseDto } from '@immich/sdk';
+import { SvelteSet } from 'svelte/reactivity';
+import { fromStore } from 'svelte/store';
+
+export class AssetInteraction {
+  readonly selectedAssets = new SvelteSet<AssetResponseDto>();
+  readonly selectedGroup = new SvelteSet<string>();
+  assetSelectionCandidates = $state(new SvelteSet<AssetResponseDto>());
+  assetSelectionStart = $state<AssetResponseDto | null>(null);
+
+  selectionActive = $derived(this.selectedAssets.size > 0);
+  selectedAssetsArray = $derived([...this.selectedAssets]);
+
+  private user = fromStore<UserAdminResponseDto | undefined>(user);
+  private userId = $derived(this.user.current?.id);
+
+  isAllTrashed = $derived(this.selectedAssetsArray.every((asset) => asset.isTrashed));
+  isAllArchived = $derived(this.selectedAssetsArray.every((asset) => asset.isArchived));
+  isAllFavorite = $derived(this.selectedAssetsArray.every((asset) => asset.isFavorite));
+  isAllUserOwned = $derived(this.selectedAssetsArray.every((asset) => asset.ownerId === this.userId));
+
+  selectAsset(asset: AssetResponseDto) {
+    this.selectedAssets.add(asset);
+  }
+
+  selectAssets(assets: AssetResponseDto[]) {
+    for (const asset of assets) {
+      this.selectedAssets.add(asset);
+    }
+  }
+
+  removeAssetFromMultiselectGroup(asset: AssetResponseDto) {
+    this.selectedAssets.delete(asset);
+  }
+
+  addGroupToMultiselectGroup(group: string) {
+    this.selectedGroup.add(group);
+  }
+
+  removeGroupFromMultiselectGroup(group: string) {
+    this.selectedGroup.delete(group);
+  }
+
+  setAssetSelectionStart(asset: AssetResponseDto | null) {
+    this.assetSelectionStart = asset;
+  }
+
+  setAssetSelectionCandidates(assets: AssetResponseDto[]) {
+    this.assetSelectionCandidates = new SvelteSet(assets);
+  }
+
+  clearAssetSelectionCandidates() {
+    this.assetSelectionCandidates.clear();
+  }
+
+  clearMultiselect() {
+    // Multi-selection
+    this.selectedAssets.clear();
+    this.selectedGroup.clear();
+
+    // Range selection
+    this.assetSelectionCandidates.clear();
+    this.assetSelectionStart = null;
+  }
+}
diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts
index 3892fc09cc..215707543c 100644
--- a/web/src/lib/stores/assets.store.ts
+++ b/web/src/lib/stores/assets.store.ts
@@ -398,7 +398,9 @@ export class AssetStore {
   }
 
   async updateOptions(options: AssetStoreOptions) {
-    if (!this.initialized) {
+    // Make sure to re-initialize if the personId changes
+    const needsReinitializing = this.options.personId !== options.personId;
+    if (!this.initialized && !needsReinitializing) {
       this.setOptions(options);
       return;
     }
@@ -638,9 +640,7 @@ export class AssetStore {
         this.options.userId ||
         this.options.personId ||
         this.options.albumId ||
-        isMismatched(this.options.isArchived, asset.isArchived) ||
-        isMismatched(this.options.isFavorite, asset.isFavorite) ||
-        isMismatched(this.options.isTrashed, asset.isTrashed)
+        this.isExcluded(asset)
       ) {
         // If asset is already in the bucket we don't need to recalculate
         // asset store containers
@@ -699,26 +699,22 @@ export class AssetStore {
 
   async findAndLoadBucketAsPending(id: string) {
     const bucketInfo = this.assetToBucket[id];
-    if (bucketInfo) {
-      const bucket = bucketInfo.bucket;
+    let bucket: AssetBucket | null = bucketInfo?.bucket ?? null;
+    if (!bucket) {
+      const asset = await getAssetInfo({ id });
+      if (!asset || this.isExcluded(asset)) {
+        return;
+      }
+
+      bucket = await this.loadBucketAtTime(asset.localDateTime, { preventCancel: true, pending: true });
+    }
+
+    if (bucket && bucket.assets.some((a) => a.id === id)) {
       this.pendingScrollBucket = bucket;
       this.pendingScrollAssetId = id;
       this.emit(false);
       return bucket;
     }
-    const asset = await getAssetInfo({ id });
-    if (asset) {
-      if (this.options.isArchived !== asset.isArchived) {
-        return;
-      }
-      const bucket = await this.loadBucketAtTime(asset.localDateTime, { preventCancel: true, pending: true });
-      if (bucket) {
-        this.pendingScrollBucket = bucket;
-        this.pendingScrollAssetId = asset.id;
-        this.emit(false);
-      }
-      return bucket;
-    }
   }
 
   /* Must be paired with matching clearPendingScroll() call */
@@ -905,6 +901,14 @@ export class AssetStore {
     }
     this.store$.set(this);
   }
+
+  private isExcluded(asset: AssetResponseDto) {
+    return (
+      isMismatched(this.options.isArchived ?? false, asset.isArchived) ||
+      isMismatched(this.options.isFavorite, asset.isFavorite) ||
+      isMismatched(this.options.isTrashed ?? false, asset.isTrashed)
+    );
+  }
 }
 
 export const isSelectingAllAssets = writable(false);
diff --git a/web/src/lib/stores/folders.store.ts b/web/src/lib/stores/folders.store.ts
deleted file mode 100644
index 2e491374e2..0000000000
--- a/web/src/lib/stores/folders.store.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import {
-  getAssetsByOriginalPath,
-  getUniqueOriginalPaths,
-  /**
-   * TODO: Incorrect type
-   */
-  type AssetResponseDto,
-} from '@immich/sdk';
-import { get, writable } from 'svelte/store';
-
-type AssetCache = {
-  [path: string]: AssetResponseDto[];
-};
-
-type FoldersStore = {
-  uniquePaths: string[] | null;
-  assets: AssetCache;
-};
-
-function createFoldersStore() {
-  const initialState: FoldersStore = {
-    uniquePaths: null,
-    assets: {},
-  };
-
-  const { subscribe, set, update } = writable(initialState);
-
-  async function fetchUniquePaths() {
-    const state = get(foldersStore);
-
-    if (state.uniquePaths !== null) {
-      return;
-    }
-
-    const uniquePaths = await getUniqueOriginalPaths();
-    if (uniquePaths) {
-      update((state) => ({ ...state, uniquePaths }));
-    }
-  }
-
-  async function fetchAssetsByPath(path: string) {
-    const state = get(foldersStore);
-
-    if (state.assets[path]) {
-      return;
-    }
-
-    const assets = await getAssetsByOriginalPath({ path });
-    if (assets) {
-      update((state) => ({
-        ...state,
-        assets: { ...state.assets, [path]: assets },
-      }));
-    }
-  }
-
-  function clearCache() {
-    set(initialState);
-  }
-
-  return {
-    subscribe,
-    fetchUniquePaths,
-    fetchAssetsByPath,
-    clearCache,
-  };
-}
-
-export const foldersStore = createFoldersStore();
diff --git a/web/src/lib/stores/folders.svelte.ts b/web/src/lib/stores/folders.svelte.ts
new file mode 100644
index 0000000000..cff7aea455
--- /dev/null
+++ b/web/src/lib/stores/folders.svelte.ts
@@ -0,0 +1,45 @@
+import {
+  getAssetsByOriginalPath,
+  getUniqueOriginalPaths,
+  /**
+   * TODO: Incorrect type
+   */
+  type AssetResponseDto,
+} from '@immich/sdk';
+
+type AssetCache = {
+  [path: string]: AssetResponseDto[];
+};
+
+class FoldersStore {
+  private initialized = false;
+  uniquePaths = $state<string[]>([]);
+  assets = $state<AssetCache>({});
+
+  async fetchUniquePaths() {
+    if (this.initialized) {
+      return;
+    }
+    this.initialized = true;
+
+    const uniquePaths = await getUniqueOriginalPaths();
+    this.uniquePaths.push(...uniquePaths);
+    this.uniquePaths.sort();
+  }
+
+  async fetchAssetsByPath(path: string) {
+    if (this.assets[path]) {
+      return;
+    }
+
+    this.assets[path] = await getAssetsByOriginalPath({ path });
+  }
+
+  clearCache() {
+    this.initialized = false;
+    this.uniquePaths = [];
+    this.assets = {};
+  }
+}
+
+export const foldersStore = new FoldersStore();
diff --git a/web/src/lib/stores/preferences.store.ts b/web/src/lib/stores/preferences.store.ts
index de80702b95..e162d26fc9 100644
--- a/web/src/lib/stores/preferences.store.ts
+++ b/web/src/lib/stores/preferences.store.ts
@@ -16,7 +16,7 @@ export const handleToggleTheme = () => {
 };
 
 const initTheme = (): ThemeSetting => {
-  if (browser && !window.matchMedia('(prefers-color-scheme: dark)').matches) {
+  if (browser && globalThis.matchMedia && !globalThis.matchMedia('(prefers-color-scheme: dark)').matches) {
     return { value: Theme.LIGHT, system: false };
   }
   return { value: Theme.DARK, system: false };
@@ -144,3 +144,5 @@ export const alwaysLoadOriginalFile = persisted<boolean>('always-load-original-f
 export const playVideoThumbnailOnHover = persisted<boolean>('play-video-thumbnail-on-hover', true, {});
 
 export const loopVideo = persisted<boolean>('loop-video', true, {});
+
+export const recentAlbumsDropdown = persisted<boolean>('recent-albums-open', true, {});
diff --git a/web/src/lib/stores/server-config.store.ts b/web/src/lib/stores/server-config.store.ts
index 14d1e4e66e..254db71946 100644
--- a/web/src/lib/stores/server-config.store.ts
+++ b/web/src/lib/stores/server-config.store.ts
@@ -32,6 +32,9 @@ export const serverConfig = writable<ServerConfig>({
   isInitialized: false,
   isOnboarded: false,
   externalDomain: '',
+  mapDarkStyleUrl: '',
+  mapLightStyleUrl: '',
+  publicUsers: true,
 });
 
 export const retrieveServerConfig = async () => {
diff --git a/web/src/lib/stores/server-info.store.ts b/web/src/lib/stores/server-info.store.ts
deleted file mode 100644
index 360b2c5630..0000000000
--- a/web/src/lib/stores/server-info.store.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import type { ServerStorageResponseDto } from '@immich/sdk';
-import { writable } from 'svelte/store';
-
-export const serverInfo = writable<ServerStorageResponseDto>();
diff --git a/web/src/lib/stores/slideshow.store.ts b/web/src/lib/stores/slideshow.store.ts
index daa4e98791..3be96fda3e 100644
--- a/web/src/lib/stores/slideshow.store.ts
+++ b/web/src/lib/stores/slideshow.store.ts
@@ -38,6 +38,7 @@ function createSlideshowStore() {
 
   const showProgressBar = persisted<boolean>('slideshow-show-progressbar', true);
   const slideshowDelay = persisted<number>('slideshow-delay', 5, {});
+  const slideshowTransition = persisted<boolean>('slideshow-transition', true);
 
   return {
     restartProgress: {
@@ -67,6 +68,7 @@ function createSlideshowStore() {
     slideshowState,
     slideshowDelay,
     showProgressBar,
+    slideshowTransition,
   };
 }
 
diff --git a/web/src/lib/stores/timeline.store.ts b/web/src/lib/stores/timeline.store.ts
new file mode 100644
index 0000000000..b86a171765
--- /dev/null
+++ b/web/src/lib/stores/timeline.store.ts
@@ -0,0 +1,3 @@
+import { writable } from 'svelte/store';
+
+export const isTimelineScrolling = writable(false);
diff --git a/web/src/lib/stores/user.svelte.ts b/web/src/lib/stores/user.svelte.ts
new file mode 100644
index 0000000000..71b2cdd847
--- /dev/null
+++ b/web/src/lib/stores/user.svelte.ts
@@ -0,0 +1,26 @@
+import type {
+  AlbumResponseDto,
+  ServerAboutResponseDto,
+  ServerStorageResponseDto,
+  ServerVersionHistoryResponseDto,
+} from '@immich/sdk';
+
+interface UserInteractions {
+  recentAlbums?: AlbumResponseDto[];
+  versions?: ServerVersionHistoryResponseDto[];
+  aboutInfo?: ServerAboutResponseDto;
+  serverInfo?: ServerStorageResponseDto;
+}
+
+const defaultUserInteraction: UserInteractions = {
+  recentAlbums: undefined,
+  versions: undefined,
+  aboutInfo: undefined,
+  serverInfo: undefined,
+};
+
+export const resetUserInteraction = () => {
+  Object.assign(userInteraction, defaultUserInteraction);
+};
+
+export const userInteraction = $state<UserInteractions>(defaultUserInteraction);
diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts
index 395d9796f4..349ae1c479 100644
--- a/web/src/lib/utils.ts
+++ b/web/src/lib/utils.ts
@@ -19,8 +19,9 @@ import {
   type AssetResponseDto,
   type PersonResponseDto,
   type SharedLinkResponseDto,
+  type UserResponseDto,
 } from '@immich/sdk';
-import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiImageRefreshOutline } from '@mdi/js';
+import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiHeadSyncOutline, mdiImageRefreshOutline } from '@mdi/js';
 import { sortBy } from 'lodash-es';
 import { init, register, t } from 'svelte-i18n';
 import { derived, get } from 'svelte/store';
@@ -147,6 +148,7 @@ export const getJobName = derived(t, ($t) => {
       [JobName.Search]: $t('search'),
       [JobName.Library]: $t('library'),
       [JobName.Notifications]: $t('notifications'),
+      [JobName.BackupDatabase]: $t('admin.backup_database'),
     };
 
     return names[jobName];
@@ -204,7 +206,8 @@ export const getAssetPlaybackUrl = (options: string | { id: string; checksum?: s
   return createUrl(getAssetPlaybackPath(id), { key: getKey(), c: checksum });
 };
 
-export const getProfileImageUrl = (userId: string) => createUrl(getUserProfileImagePath(userId));
+export const getProfileImageUrl = (user: UserResponseDto) =>
+  createUrl(getUserProfileImagePath(user.id), { updatedAt: user.profileChangedAt });
 
 export const getPeopleThumbnailUrl = (person: PersonResponseDto, updatedAt?: string) =>
   createUrl(getPeopleThumbnailPath(person.id), { updatedAt: updatedAt ?? person.updatedAt });
@@ -212,6 +215,7 @@ export const getPeopleThumbnailUrl = (person: PersonResponseDto, updatedAt?: str
 export const getAssetJobName = derived(t, ($t) => {
   return (job: AssetJobName) => {
     const names: Record<AssetJobName, string> = {
+      [AssetJobName.RefreshFaces]: $t('refresh_faces'),
       [AssetJobName.RefreshMetadata]: $t('refresh_metadata'),
       [AssetJobName.RegenerateThumbnail]: $t('refresh_thumbnails'),
       [AssetJobName.TranscodeVideo]: $t('refresh_encoded_videos'),
@@ -224,6 +228,7 @@ export const getAssetJobName = derived(t, ($t) => {
 export const getAssetJobMessage = derived(t, ($t) => {
   return (job: AssetJobName) => {
     const messages: Record<AssetJobName, string> = {
+      [AssetJobName.RefreshFaces]: $t('refreshing_faces'),
       [AssetJobName.RefreshMetadata]: $t('refreshing_metadata'),
       [AssetJobName.RegenerateThumbnail]: $t('regenerating_thumbnails'),
       [AssetJobName.TranscodeVideo]: $t('refreshing_encoded_video'),
@@ -235,6 +240,7 @@ export const getAssetJobMessage = derived(t, ($t) => {
 
 export const getAssetJobIcon = (job: AssetJobName) => {
   const names: Record<AssetJobName, string> = {
+    [AssetJobName.RefreshFaces]: mdiHeadSyncOutline,
     [AssetJobName.RefreshMetadata]: mdiDatabaseRefreshOutline,
     [AssetJobName.RegenerateThumbnail]: mdiImageRefreshOutline,
     [AssetJobName.TranscodeVideo]: mdiCogRefreshOutline,
@@ -255,11 +261,7 @@ export const copyToClipboard = async (secret: string) => {
 };
 
 export const makeSharedLinkUrl = (externalDomain: string, key: string) => {
-  let url = externalDomain || window.location.origin;
-  if (!url.endsWith('/')) {
-    url += '/';
-  }
-  return `${url}share/${key}`;
+  return new URL(`share/${key}`, externalDomain || globalThis.location.origin).href;
 };
 
 export const oauth = {
@@ -281,7 +283,7 @@ export const oauth = {
     try {
       const redirectUri = location.href.split('?')[0];
       const { url } = await startOAuth({ oAuthConfigDto: { redirectUri } });
-      window.location.href = url;
+      globalThis.location.href = url;
       return true;
     } catch (error) {
       handleError(error, $t('errors.unable_to_login_with_oauth'));
diff --git a/web/src/lib/utils/actions.ts b/web/src/lib/utils/actions.ts
index 291d3926ee..d4715db729 100644
--- a/web/src/lib/utils/actions.ts
+++ b/web/src/lib/utils/actions.ts
@@ -8,6 +8,7 @@ export type OnDelete = (assetIds: string[]) => void;
 export type OnRestore = (ids: string[]) => void;
 export type OnLink = (assets: { still: AssetResponseDto; motion: AssetResponseDto }) => void;
 export type OnUnlink = (assets: { still: AssetResponseDto; motion: AssetResponseDto }) => void;
+export type OnAddToAlbum = (ids: string[], albumId: string) => void;
 export type OnArchive = (ids: string[], isArchived: boolean) => void;
 export type OnFavorite = (ids: string[], favorite: boolean) => void;
 export type OnStack = (ids: string[]) => void;
diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts
index 84a896452f..70f5c5f8f2 100644
--- a/web/src/lib/utils/asset-utils.ts
+++ b/web/src/lib/utils/asset-utils.ts
@@ -1,8 +1,9 @@
 import { goto } from '$app/navigation';
 import FormatBoldMessage from '$lib/components/i18n/format-bold-message.svelte';
+import type { InterpolationValues } from '$lib/components/i18n/format-message';
 import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification';
 import { AppRoute } from '$lib/constants';
-import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
+import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 import { assetViewingStore } from '$lib/stores/asset-viewing.store';
 import { isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store';
 import { downloadManager } from '$lib/stores/download';
@@ -14,6 +15,7 @@ import { getFormatter } from '$lib/utils/i18n';
 import {
   addAssetsToAlbum as addAssets,
   createStack,
+  deleteAssets,
   deleteStacks,
   getAssetInfo,
   getBaseUrl,
@@ -27,11 +29,12 @@ import {
   type AssetResponseDto,
   type AssetTypeEnum,
   type DownloadInfoDto,
+  type StackResponseDto,
   type UserPreferencesResponseDto,
   type UserResponseDto,
 } from '@immich/sdk';
 import { DateTime } from 'luxon';
-import { t } from 'svelte-i18n';
+import { t, type Translations } from 'svelte-i18n';
 import { get } from 'svelte/store';
 import { handleError } from './handle-error';
 
@@ -118,7 +121,8 @@ export const addAssetsToNewAlbum = async (albumName: string, assetIds: string[])
     return;
   }
   const $t = get(t);
-  notificationController.show({
+  // for reasons beyond me <ComponentProps<typeof FormatBoldMessage>> doesn't work, even though it's (afaik) exactly this object
+  notificationController.show<{ key: Translations; values: InterpolationValues }>({
     type: NotificationType.Info,
     timeout: 5000,
     component: {
@@ -211,13 +215,6 @@ export const downloadArchive = async (fileName: string, options: Omit<DownloadIn
 
 export const downloadFile = async (asset: AssetResponseDto) => {
   const $t = get(t);
-  if (asset.isOffline) {
-    notificationController.show({
-      type: NotificationType.Info,
-      message: $t('asset_filename_is_offline', { values: { filename: asset.originalFileName } }),
-    });
-    return;
-  }
   const assets = [
     {
       filename: asset.originalFileName,
@@ -445,7 +442,27 @@ export const deleteStack = async (stackIds: string[]) => {
   }
 };
 
-export const selectAllAssets = async (assetStore: AssetStore, assetInteractionStore: AssetInteractionStore) => {
+export const keepThisDeleteOthers = async (keepAsset: AssetResponseDto, stack: StackResponseDto) => {
+  const $t = get(t);
+
+  try {
+    const assetsToDeleteIds = stack.assets.filter((asset) => asset.id !== keepAsset.id).map((asset) => asset.id);
+    await deleteAssets({ assetBulkDeleteDto: { ids: assetsToDeleteIds } });
+    await deleteStacks({ bulkIdsDto: { ids: [stack.id] } });
+
+    notificationController.show({
+      type: NotificationType.Info,
+      message: $t('kept_this_deleted_others', { values: { count: assetsToDeleteIds.length } }),
+    });
+
+    keepAsset.stack = null;
+    return keepAsset;
+  } catch (error) {
+    handleError(error, $t('errors.failed_to_keep_this_delete_others'));
+  }
+};
+
+export const selectAllAssets = async (assetStore: AssetStore, assetInteraction: AssetInteraction) => {
   if (get(isSelectingAllAssets)) {
     // Selection is already ongoing
     return;
@@ -459,7 +476,7 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteractionSt
       if (!get(isSelectingAllAssets)) {
         break; // Cancelled
       }
-      assetInteractionStore.selectAssets(bucket.assets);
+      assetInteraction.selectAssets(bucket.assets);
 
       // We use setTimeout to allow the UI to update. Otherwise, this may
       // cause a long delay between the start of 'select all' and the
@@ -474,6 +491,11 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteractionSt
   }
 };
 
+export const cancelMultiselect = (assetInteraction: AssetInteraction) => {
+  isSelectingAllAssets.set(false);
+  assetInteraction.clearMultiselect();
+};
+
 export const toggleArchive = async (asset: AssetResponseDto) => {
   const $t = get(t);
   try {
@@ -529,7 +551,7 @@ export const delay = async (ms: number) => {
 };
 
 export const canCopyImageToClipboard = (): boolean => {
-  return !!(navigator.clipboard && window.ClipboardItem);
+  return !!(navigator.clipboard && globalThis.ClipboardItem);
 };
 
 const imgToBlob = async (imageElement: HTMLImageElement) => {
diff --git a/web/src/lib/utils/auth.ts b/web/src/lib/utils/auth.ts
index 0ac1658948..f14de693f3 100644
--- a/web/src/lib/utils/auth.ts
+++ b/web/src/lib/utils/auth.ts
@@ -1,9 +1,9 @@
 import { browser } from '$app/environment';
 import { goto } from '$app/navigation';
-import { foldersStore } from '$lib/stores/folders.store';
+import { foldersStore } from '$lib/stores/folders.svelte';
 import { purchaseStore } from '$lib/stores/purchase.store';
-import { serverInfo } from '$lib/stores/server-info.store';
 import { preferences as preferences$, resetSavedUser, user as user$ } from '$lib/stores/user.store';
+import { resetUserInteraction, userInteraction } from '$lib/stores/user.svelte';
 import { getAboutInfo, getMyPreferences, getMyUser, getStorage } from '@immich/sdk';
 import { redirect } from '@sveltejs/kit';
 import { DateTime } from 'luxon';
@@ -72,7 +72,7 @@ export const authenticate = async (options?: AuthOptions) => {
 export const requestServerInfo = async () => {
   if (get(user$)) {
     const data = await getStorage();
-    serverInfo.set(data);
+    userInteraction.serverInfo = data;
   }
 };
 
@@ -95,10 +95,11 @@ export const handleLogout = async (redirectUri: string) => {
     if (redirectUri.startsWith('/')) {
       await goto(redirectUri);
     } else {
-      window.location.href = redirectUri;
+      globalThis.location.href = redirectUri;
     }
   } finally {
     resetSavedUser();
+    resetUserInteraction();
     foldersStore.clearCache();
   }
 };
diff --git a/web/src/lib/utils/handle-error.ts b/web/src/lib/utils/handle-error.ts
index 9ca5bc8773..a7e9a4340c 100644
--- a/web/src/lib/utils/handle-error.ts
+++ b/web/src/lib/utils/handle-error.ts
@@ -2,9 +2,21 @@ import { isHttpError } from '@immich/sdk';
 import { notificationController, NotificationType } from '../components/shared-components/notification/notification';
 
 export function getServerErrorMessage(error: unknown) {
-  if (isHttpError(error)) {
-    return error.data?.message || error.message;
+  if (!isHttpError(error)) {
+    return;
   }
+
+  // errors for endpoints without return types aren't parsed as json
+  let data = error.data;
+  if (typeof data === 'string') {
+    try {
+      data = JSON.parse(data);
+    } catch {
+      // Not a JSON string
+    }
+  }
+
+  return data?.message || error.message;
 }
 
 export function handleError(error: unknown, message: string) {
diff --git a/web/src/lib/utils/idle-callback-support.ts b/web/src/lib/utils/idle-callback-support.ts
index 0f7f060084..6e79640f3c 100644
--- a/web/src/lib/utils/idle-callback-support.ts
+++ b/web/src/lib/utils/idle-callback-support.ts
@@ -16,5 +16,5 @@ function fake_cancelIdleCallback(id: number) {
   return clearTimeout(id);
 }
 
-export const idleCB = window.requestIdleCallback || fake_requestIdleCallback;
-export const cancelIdleCB = window.cancelIdleCallback || fake_cancelIdleCallback;
+export const idleCB = globalThis.requestIdleCallback || fake_requestIdleCallback;
+export const cancelIdleCB = globalThis.cancelIdleCallback || fake_cancelIdleCallback;
diff --git a/web/src/lib/utils/keyed-priority-queue.ts b/web/src/lib/utils/keyed-priority-queue.ts
index 2483b22c6d..b78ecdf353 100644
--- a/web/src/lib/utils/keyed-priority-queue.ts
+++ b/web/src/lib/utils/keyed-priority-queue.ts
@@ -11,7 +11,7 @@ export class KeyedPriorityQueue<K, T> {
     const removed = this.set.delete(key);
     if (removed) {
       const idx = this.items.findIndex((i) => i.key === key);
-      if (idx >= 0) {
+      if (idx !== -1) {
         this.items.splice(idx, 1);
       }
     }
diff --git a/web/src/lib/utils/license-utils.ts b/web/src/lib/utils/license-utils.ts
index 6b429a0115..b592dbff48 100644
--- a/web/src/lib/utils/license-utils.ts
+++ b/web/src/lib/utils/license-utils.ts
@@ -24,6 +24,6 @@ export const getActivationKey = async (licenseKey: string): Promise<string> => {
 export const getLicenseLink = (license: ImmichProduct) => {
   const url = new URL('/', PUBLIC_IMMICH_BUY_HOST);
   url.searchParams.append('productId', license);
-  url.searchParams.append('instanceUrl', get(serverConfig).externalDomain || window.origin);
+  url.searchParams.append('instanceUrl', get(serverConfig).externalDomain || globalThis.origin);
   return url.href;
 };
diff --git a/web/src/lib/utils/navigation.ts b/web/src/lib/utils/navigation.ts
index 304376b347..41eb5ee73b 100644
--- a/web/src/lib/utils/navigation.ts
+++ b/web/src/lib/utils/navigation.ts
@@ -9,7 +9,7 @@ export type AssetGridRouteSearchParams = {
   at: string | null | undefined;
 };
 export const isExternalUrl = (url: string): boolean => {
-  return new URL(url, window.location.href).origin !== window.location.origin;
+  return new URL(url, globalThis.location.href).origin !== globalThis.location.origin;
 };
 
 export const isPhotosRoute = (route?: string | null) => !!route?.startsWith('/(user)/photos/[[assetId=id]]');
diff --git a/web/src/lib/utils/thumbnail-util.spec.ts b/web/src/lib/utils/thumbnail-util.spec.ts
index b4dbec8752..7fc53f662b 100644
--- a/web/src/lib/utils/thumbnail-util.spec.ts
+++ b/web/src/lib/utils/thumbnail-util.spec.ts
@@ -10,7 +10,7 @@ const fourPeople = [{ name: 'person1' }, { name: 'person2' }, { name: 'person3'
 describe('getAltText', () => {
   beforeAll(async () => {
     await init({ fallbackLocale: 'en-US' });
-    register('en-US', () => import('$lib/i18n/en.json'));
+    register('en-US', () => import('$i18n/en.json'));
     await waitLocale('en-US');
   });
 
diff --git a/web/src/lib/utils/timeline-util.spec.ts b/web/src/lib/utils/timeline-util.spec.ts
index 4643ae1218..f842b948e0 100644
--- a/web/src/lib/utils/timeline-util.spec.ts
+++ b/web/src/lib/utils/timeline-util.spec.ts
@@ -5,11 +5,13 @@ import { DateTime } from 'luxon';
 describe('formatGroupTitle', () => {
   beforeAll(() => {
     vi.useFakeTimers();
+    process.env.TZ = 'UTC';
     vi.setSystemTime(new Date('2024-07-27T12:00:00Z'));
   });
 
   afterAll(() => {
     vi.useRealTimers();
+    delete process.env.TZ;
   });
 
   it('formats today', () => {
diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts
index 541ebea7f5..7e65dcdb99 100644
--- a/web/src/lib/utils/timeline-util.ts
+++ b/web/src/lib/utils/timeline-util.ts
@@ -36,6 +36,9 @@ export type ScrollTargetListener = ({
 export const fromLocalDateTime = (localDateTime: string) =>
   DateTime.fromISO(localDateTime, { zone: 'UTC', locale: get(locale) });
 
+export const fromDateTimeOriginal = (dateTimeOriginal: string, timeZone: string) =>
+  DateTime.fromISO(dateTimeOriginal, { zone: timeZone });
+
 export const groupDateFormat: Intl.DateTimeFormatOptions = {
   weekday: 'short',
   month: 'short',
diff --git a/web/src/lib/utils/tree-utils.ts b/web/src/lib/utils/tree-utils.ts
index 13fb6c1605..5a6e917079 100644
--- a/web/src/lib/utils/tree-utils.ts
+++ b/web/src/lib/utils/tree-utils.ts
@@ -7,8 +7,6 @@ export const normalizeTreePath = (path: string) => path.replace(/^\//, '').repla
 export function buildTree(paths: string[]) {
   const root: RecursiveObject = {};
 
-  paths.sort();
-
   for (const path of paths) {
     const parts = path.split('/');
     let current = root;
diff --git a/web/src/routes/(user)/+layout.svelte b/web/src/routes/(user)/+layout.svelte
index bf24d0e7e4..feda36fa01 100644
--- a/web/src/routes/(user)/+layout.svelte
+++ b/web/src/routes/(user)/+layout.svelte
@@ -1,13 +1,21 @@
 <script lang="ts">
+  import { run } from 'svelte/legacy';
+
   import UploadCover from '$lib/components/shared-components/drag-and-drop-upload-overlay.svelte';
   import { page } from '$app/stores';
 
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
+  import type { Snippet } from 'svelte';
+  interface Props {
+    children?: Snippet;
+  }
+
+  let { children }: Props = $props();
   let { isViewing: showAssetViewer, setAsset, gridScrollTarget } = assetViewingStore;
 
   // $page.data.asset is loaded by route specific +page.ts loaders if that
   // route contains the assetId path.
-  $: {
+  run(() => {
     if ($page.data.asset) {
       setAsset($page.data.asset);
     } else {
@@ -15,11 +23,11 @@
     }
     const asset = $page.url.searchParams.get('at');
     $gridScrollTarget = { at: asset };
-  }
+  });
 </script>
 
 <div class:display-none={$showAssetViewer}>
-  <slot />
+  {@render children?.()}
 </div>
 <UploadCover />
 
diff --git a/web/src/routes/(user)/albums/+page.svelte b/web/src/routes/(user)/albums/+page.svelte
index 35402ce331..239c6cc38a 100644
--- a/web/src/routes/(user)/albums/+page.svelte
+++ b/web/src/routes/(user)/albums/+page.svelte
@@ -1,5 +1,6 @@
 <script lang="ts">
   import type { PageData } from './$types';
+  import { scrollMemory } from '$lib/actions/scroll-memory';
   import { AlbumFilter, albumViewSettings } from '$lib/stores/preferences.store';
   import { createAlbumAndRedirect } from '$lib/utils/album-utils';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
@@ -8,18 +9,25 @@
   import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
   import GroupTab from '$lib/components/elements/group-tab.svelte';
   import SearchBar from '$lib/components/elements/search-bar.svelte';
+  import { AppRoute } from '$lib/constants';
   import { t } from 'svelte-i18n';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
 
-  let searchQuery = '';
-  let albumGroups: string[] = [];
+  let { data }: Props = $props();
+
+  let searchQuery = $state('');
+  let albumGroups: string[] = $state([]);
 </script>
 
-<UserPageLayout title={data.meta.title}>
-  <div class="flex place-items-center gap-2" slot="buttons">
-    <AlbumsControls {albumGroups} bind:searchQuery />
-  </div>
+<UserPageLayout title={data.meta.title} use={[[scrollMemory, { routeStartsWith: AppRoute.ALBUMS }]]}>
+  {#snippet buttons()}
+    <div class="flex place-items-center gap-2">
+      <AlbumsControls {albumGroups} bind:searchQuery />
+    </div>
+  {/snippet}
 
   <div class="xl:hidden">
     <div class="w-fit h-14 dark:text-immich-dark-fg py-2">
@@ -43,6 +51,8 @@
     {searchQuery}
     bind:albumGroupIds={albumGroups}
   >
-    <EmptyPlaceholder slot="empty" text={$t('no_albums_message')} onClick={() => createAlbumAndRedirect()} />
+    {#snippet empty()}
+      <EmptyPlaceholder text={$t('no_albums_message')} onClick={() => createAlbumAndRedirect()} />
+    {/snippet}
   </Albums>
 </UserPageLayout>
diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 6e75273f3b..0f6c62a5fa 100644
--- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -1,5 +1,6 @@
 <script lang="ts">
   import { afterNavigate, goto, onNavigate } from '$app/navigation';
+  import { scrollMemoryClearer } from '$lib/actions/scroll-memory';
   import AlbumDescription from '$lib/components/album-page/album-description.svelte';
   import AlbumOptions from '$lib/components/album-page/album-options.svelte';
   import AlbumSummary from '$lib/components/album-page/album-summary.svelte';
@@ -32,15 +33,14 @@
     notificationController,
   } from '$lib/components/shared-components/notification/notification';
   import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
-  import { AppRoute } from '$lib/constants';
+  import { AppRoute, AlbumPageViewMode } from '$lib/constants';
   import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
-  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
   import { AssetStore } from '$lib/stores/assets.store';
   import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
   import { preferences, user } from '$lib/stores/user.store';
   import { handlePromiseError } from '$lib/utils';
-  import { downloadAlbum } from '$lib/utils/asset-utils';
+  import { downloadAlbum, cancelMultiselect } from '$lib/utils/asset-utils';
   import { openFileUploadDialog } from '$lib/utils/file-uploader';
   import { handleError } from '$lib/utils/handle-error';
   import {
@@ -86,67 +86,29 @@
   import { onDestroy } from 'svelte';
   import { confirmAlbumDelete } from '$lib/utils/album-utils';
   import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
+  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
+
+  let { data = $bindable() }: Props = $props();
 
   let { isViewing: showAssetViewer, setAsset, gridScrollTarget } = assetViewingStore;
   let { slideshowState, slideshowNavigation } = slideshowStore;
 
-  let oldAt: AssetGridRouteSearchParams | null | undefined;
+  let oldAt: AssetGridRouteSearchParams | null | undefined = $state();
 
-  $: album = data.album;
-  $: albumId = album.id;
-  $: albumKey = `${albumId}_${albumOrder}`;
+  let backUrl: string = $state(AppRoute.ALBUMS);
+  let viewMode = $state(AlbumPageViewMode.VIEW);
+  let isCreatingSharedAlbum = $state(false);
+  let isShowActivity = $state(false);
+  let isLiked: ActivityResponseDto | null = $state(null);
+  let reactions: ActivityResponseDto[] = $state([]);
+  let albumOrder: AssetOrder | undefined = $state(data.album.order);
 
-  $: {
-    if (!album.isActivityEnabled && $numberOfComments === 0) {
-      isShowActivity = false;
-    }
-  }
-
-  enum ViewMode {
-    LINK_SHARING = 'link-sharing',
-    SELECT_USERS = 'select-users',
-    SELECT_THUMBNAIL = 'select-thumbnail',
-    SELECT_ASSETS = 'select-assets',
-    VIEW_USERS = 'view-users',
-    VIEW = 'view',
-    OPTIONS = 'options',
-  }
-
-  let backUrl: string = AppRoute.ALBUMS;
-  let viewMode = ViewMode.VIEW;
-  let isCreatingSharedAlbum = false;
-  let isShowActivity = false;
-  let isLiked: ActivityResponseDto | null = null;
-  let reactions: ActivityResponseDto[] = [];
-  let globalWidth: number;
-  let assetGridWidth: number;
-  let albumOrder: AssetOrder | undefined = data.album.order;
-
-  $: assetStore = new AssetStore({ albumId, order: albumOrder });
-  const assetInteractionStore = createAssetInteractionStore();
-  const { isMultiSelectState, selectedAssets } = assetInteractionStore;
-
-  $: timelineStore = new AssetStore({ isArchived: false, withPartners: true }, albumId);
-  const timelineInteractionStore = createAssetInteractionStore();
-  const { selectedAssets: timelineSelected } = timelineInteractionStore;
-
-  $: isOwned = $user.id == album.ownerId;
-  $: isAllUserOwned = [...$selectedAssets].every((asset) => asset.ownerId === $user.id);
-  $: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite);
-  $: isAllArchived = [...$selectedAssets].every((asset) => asset.isArchived);
-  $: {
-    assetGridWidth = isShowActivity ? globalWidth - (globalWidth < 768 ? 360 : 460) : globalWidth;
-  }
-  $: showActivityStatus =
-    album.albumUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0);
-
-  $: isEditor =
-    album.albumUsers.find(({ user: { id } }) => id === $user.id)?.role === AlbumUserRole.Editor ||
-    album.ownerId === $user.id;
-
-  $: albumHasViewers = album.albumUsers.some(({ role }) => role === AlbumUserRole.Viewer);
+  const assetInteraction = new AssetInteraction();
+  const timelineInteraction = new AssetInteraction();
 
   afterNavigate(({ from }) => {
     let url: string | undefined = from?.url?.pathname;
@@ -164,17 +126,23 @@
 
     if (backUrl === AppRoute.SHARING && album.albumUsers.length === 0 && !album.hasSharedLink) {
       isCreatingSharedAlbum = true;
+    } else if (backUrl === AppRoute.SHARED_LINKS) {
+      backUrl = history.state?.backUrl || AppRoute.ALBUMS;
     }
   });
 
   const handleToggleEnableActivity = async () => {
     try {
-      album = await updateAlbumInfo({
+      const updateAlbum = await updateAlbumInfo({
         id: album.id,
         updateAlbumDto: {
           isActivityEnabled: !album.isActivityEnabled,
         },
       });
+
+      album = { ...album, isActivityEnabled: updateAlbum.isActivityEnabled };
+
+      await refreshAlbum();
       notificationController.show({
         type: NotificationType.Info,
         message: $t('activity_changed', { values: { enabled: album.isActivityEnabled } }),
@@ -233,11 +201,6 @@
     isShowActivity = !isShowActivity;
   };
 
-  $: if (album.albumUsers.length > 0) {
-    handlePromiseError(getFavorite());
-    handlePromiseError(getNumberOfComments());
-  }
-
   const handleStartSlideshow = async () => {
     const asset =
       $slideshowNavigation === SlideshowNavigation.Shuffle ? await assetStore.getRandomAsset() : assetStore.assets[0];
@@ -248,28 +211,28 @@
   };
 
   const handleEscape = async () => {
-    if (viewMode === ViewMode.SELECT_USERS) {
-      viewMode = ViewMode.VIEW;
+    if (viewMode === AlbumPageViewMode.SELECT_USERS) {
+      viewMode = AlbumPageViewMode.VIEW;
       return;
     }
 
-    if (viewMode === ViewMode.SELECT_ASSETS) {
+    if (viewMode === AlbumPageViewMode.SELECT_ASSETS) {
       await handleCloseSelectAssets();
       return;
     }
-    if (viewMode === ViewMode.LINK_SHARING) {
-      viewMode = ViewMode.VIEW;
+    if (viewMode === AlbumPageViewMode.LINK_SHARING) {
+      viewMode = AlbumPageViewMode.VIEW;
       return;
     }
-    if (viewMode === ViewMode.OPTIONS) {
-      viewMode = ViewMode.VIEW;
+    if (viewMode === AlbumPageViewMode.OPTIONS) {
+      viewMode = AlbumPageViewMode.VIEW;
       return;
     }
     if ($showAssetViewer) {
       return;
     }
-    if ($isMultiSelectState) {
-      assetInteractionStore.clearMultiselect();
+    if (assetInteraction.selectionActive) {
+      cancelMultiselect(assetInteraction);
       return;
     }
     await goto(backUrl);
@@ -279,9 +242,8 @@
   const refreshAlbum = async () => {
     album = await getAlbumInfo({ id: album.id, withoutAssets: true });
   };
-
   const handleAddAssets = async () => {
-    const assetIds = [...$timelineSelected].map((asset) => asset.id);
+    const assetIds = timelineInteraction.selectedAssetsArray.map((asset) => asset.id);
 
     try {
       const results = await addAssetsToAlbum({
@@ -297,7 +259,7 @@
 
       await refreshAlbum();
 
-      timelineInteractionStore.clearMultiselect();
+      timelineInteraction.clearMultiselect();
       await setModeToView();
     } catch (error) {
       handleError(error, $t('errors.error_adding_assets_to_album'));
@@ -305,7 +267,7 @@
   };
 
   const setModeToView = async () => {
-    viewMode = ViewMode.VIEW;
+    viewMode = AlbumPageViewMode.VIEW;
     assetStore.destroy();
     assetStore = new AssetStore({ albumId, order: albumOrder });
     timelineStore.destroy();
@@ -318,32 +280,33 @@
   };
 
   const handleCloseSelectAssets = async () => {
-    timelineInteractionStore.clearMultiselect();
+    timelineInteraction.clearMultiselect();
     await setModeToView();
   };
 
   const handleSelectFromComputer = async () => {
     await openFileUploadDialog({ albumId: album.id });
-    timelineInteractionStore.clearMultiselect();
+    timelineInteraction.clearMultiselect();
     await setModeToView();
   };
 
   const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => {
     try {
-      album = await addUsersToAlbum({
+      await addUsersToAlbum({
         id: album.id,
         addUsersDto: {
           albumUsers,
         },
       });
+      await refreshAlbum();
 
-      viewMode = ViewMode.VIEW;
+      viewMode = AlbumPageViewMode.VIEW;
     } catch (error) {
       handleError(error, $t('errors.error_adding_users_to_album'));
     }
   };
 
-  const handleRemoveUser = async (userId: string) => {
+  const handleRemoveUser = async (userId: string, nextViewMode: AlbumPageViewMode) => {
     if (userId == 'me' || userId === $user.id) {
       await goto(backUrl);
       return;
@@ -351,7 +314,9 @@
 
     try {
       await refreshAlbum();
-      viewMode = album.albumUsers.length > 0 ? ViewMode.VIEW_USERS : ViewMode.VIEW;
+
+      // Dynamically set the view mode based on the passed argument
+      viewMode = album.albumUsers.length > 0 ? nextViewMode : AlbumPageViewMode.VIEW;
     } catch (error) {
       handleError(error, $t('errors.error_deleting_shared_user'));
     }
@@ -365,7 +330,7 @@
     const isConfirmed = await confirmAlbumDelete(album);
 
     if (!isConfirmed) {
-      viewMode = ViewMode.VIEW;
+      viewMode = AlbumPageViewMode.VIEW;
       return;
     }
 
@@ -375,7 +340,7 @@
     } catch (error) {
       handleError(error, $t('errors.unable_to_delete_album'));
     } finally {
-      viewMode = ViewMode.VIEW;
+      viewMode = AlbumPageViewMode.VIEW;
     }
   };
 
@@ -385,21 +350,21 @@
   };
 
   const handleUpdateThumbnail = async (assetId: string) => {
-    if (viewMode !== ViewMode.SELECT_THUMBNAIL) {
+    if (viewMode !== AlbumPageViewMode.SELECT_THUMBNAIL) {
       return;
     }
 
-    viewMode = ViewMode.VIEW;
-    assetInteractionStore.clearMultiselect();
+    viewMode = AlbumPageViewMode.VIEW;
+    assetInteraction.clearMultiselect();
 
     await updateThumbnail(assetId);
   };
 
   const updateThumbnailUsingCurrentSelection = async () => {
-    if ($selectedAssets.size === 1) {
-      const assetId = [...$selectedAssets][0].id;
-      assetInteractionStore.clearMultiselect();
-      await updateThumbnail(assetId);
+    if (assetInteraction.selectedAssets.size === 1) {
+      const [firstAsset] = assetInteraction.selectedAssets;
+      assetInteraction.clearMultiselect();
+      await updateThumbnail(firstAsset.id);
     }
   };
 
@@ -426,57 +391,98 @@
     assetStore.destroy();
     timelineStore.destroy();
   });
+
+  let album = $state(data.album);
+  let albumId = $derived(album.id);
+  let albumKey = $derived(`${albumId}_${albumOrder}`);
+
+  $effect(() => {
+    if (!album.isActivityEnabled && $numberOfComments === 0) {
+      isShowActivity = false;
+    }
+  });
+
+  let assetStore = $derived(new AssetStore({ albumId, order: albumOrder }));
+  let timelineStore = $derived(new AssetStore({ isArchived: false, withPartners: true }, albumId));
+
+  let isOwned = $derived($user.id == album.ownerId);
+
+  let showActivityStatus = $derived(
+    album.albumUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0),
+  );
+  let isEditor = $derived(
+    album.albumUsers.find(({ user: { id } }) => id === $user.id)?.role === AlbumUserRole.Editor ||
+      album.ownerId === $user.id,
+  );
+
+  let albumHasViewers = $derived(album.albumUsers.some(({ role }) => role === AlbumUserRole.Viewer));
+  $effect(() => {
+    if (album.albumUsers.length > 0) {
+      handlePromiseError(getFavorite());
+      handlePromiseError(getNumberOfComments());
+    }
+  });
 </script>
 
-<div class="flex overflow-hidden" bind:clientWidth={globalWidth}>
+<div class="flex overflow-hidden" use:scrollMemoryClearer={{ routeStartsWith: AppRoute.ALBUMS }}>
   <div class="relative w-full shrink">
-    {#if $isMultiSelectState}
-      <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
+    {#if assetInteraction.selectionActive}
+      <AssetSelectControlBar
+        assets={assetInteraction.selectedAssets}
+        clearSelect={() => assetInteraction.clearMultiselect()}
+      >
         <CreateSharedLink />
-        <SelectAllAssets {assetStore} {assetInteractionStore} />
+        <SelectAllAssets {assetStore} {assetInteraction} />
         <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
           <AddToAlbum />
           <AddToAlbum shared />
         </ButtonContextMenu>
-        {#if isAllUserOwned}
-          <FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
+        {#if assetInteraction.isAllUserOwned}
+          <FavoriteAction
+            removeFavorite={assetInteraction.isAllFavorite}
+            onFavorite={() => assetStore.triggerUpdate()}
+          />
         {/if}
         <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
           <DownloadAction menuItem filename="{album.albumName}.zip" />
-          {#if isAllUserOwned}
+          {#if assetInteraction.isAllUserOwned}
             <ChangeDate menuItem />
             <ChangeLocation menuItem />
-            {#if $selectedAssets.size === 1}
+            {#if assetInteraction.selectedAssets.size === 1}
               <MenuOption
                 text={$t('set_as_album_cover')}
                 icon={mdiImageOutline}
                 onClick={() => updateThumbnailUsingCurrentSelection()}
               />
             {/if}
-            <ArchiveAction menuItem unarchive={isAllArchived} onArchive={() => assetStore.triggerUpdate()} />
+            <ArchiveAction
+              menuItem
+              unarchive={assetInteraction.isAllArchived}
+              onArchive={() => assetStore.triggerUpdate()}
+            />
           {/if}
 
-          {#if $preferences.tags.enabled && isAllUserOwned}
+          {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
             <TagAction menuItem />
           {/if}
 
-          {#if isOwned || isAllUserOwned}
+          {#if isOwned || assetInteraction.isAllUserOwned}
             <RemoveFromAlbum menuItem bind:album onRemove={handleRemoveAssets} />
           {/if}
-          {#if isAllUserOwned}
+          {#if assetInteraction.isAllUserOwned}
             <DeleteAssets menuItem onAssetDelete={handleRemoveAssets} />
           {/if}
         </ButtonContextMenu>
       </AssetSelectControlBar>
     {:else}
-      {#if viewMode === ViewMode.VIEW}
-        <ControlAppBar showBackButton backIcon={mdiArrowLeft} on:close={() => goto(backUrl)}>
-          <svelte:fragment slot="trailing">
+      {#if viewMode === AlbumPageViewMode.VIEW}
+        <ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(backUrl)}>
+          {#snippet trailing()}
             {#if isEditor}
               <CircleIconButton
                 title={$t('add_photos')}
-                on:click={async () => {
-                  viewMode = ViewMode.SELECT_ASSETS;
+                onclick={async () => {
+                  viewMode = AlbumPageViewMode.SELECT_ASSETS;
                   oldAt = { at: $gridScrollTarget?.at };
                   await navigate(
                     { targetRoute: 'current', assetId: null, assetGridRouteSearchParams: { at: null } },
@@ -490,26 +496,33 @@
             {#if isOwned}
               <CircleIconButton
                 title={$t('share')}
-                on:click={() => (viewMode = ViewMode.SELECT_USERS)}
+                onclick={() => (viewMode = AlbumPageViewMode.SELECT_USERS)}
                 icon={mdiShareVariantOutline}
               />
             {/if}
 
             {#if album.assetCount > 0}
-              <CircleIconButton title={$t('slideshow')} on:click={handleStartSlideshow} icon={mdiPresentationPlay} />
-              <CircleIconButton title={$t('download')} on:click={handleDownloadAlbum} icon={mdiFolderDownloadOutline} />
+              <CircleIconButton title={$t('slideshow')} onclick={handleStartSlideshow} icon={mdiPresentationPlay} />
+              <CircleIconButton title={$t('download')} onclick={handleDownloadAlbum} icon={mdiFolderDownloadOutline} />
+            {/if}
 
-              {#if isOwned}
-                <ButtonContextMenu icon={mdiDotsVertical} title={$t('album_options')}>
+            {#if isOwned}
+              <ButtonContextMenu icon={mdiDotsVertical} title={$t('album_options')}>
+                {#if album.assetCount > 0}
                   <MenuOption
                     icon={mdiImageOutline}
                     text={$t('select_album_cover')}
-                    onClick={() => (viewMode = ViewMode.SELECT_THUMBNAIL)}
+                    onClick={() => (viewMode = AlbumPageViewMode.SELECT_THUMBNAIL)}
                   />
-                  <MenuOption icon={mdiCogOutline} text={$t('options')} onClick={() => (viewMode = ViewMode.OPTIONS)} />
-                  <MenuOption icon={mdiDeleteOutline} text={$t('delete_album')} onClick={() => handleRemoveAlbum()} />
-                </ButtonContextMenu>
-              {/if}
+                  <MenuOption
+                    icon={mdiCogOutline}
+                    text={$t('options')}
+                    onClick={() => (viewMode = AlbumPageViewMode.OPTIONS)}
+                  />
+                {/if}
+
+                <MenuOption icon={mdiDeleteOutline} text={$t('delete_album')} onClick={() => handleRemoveAlbum()} />
+              </ButtonContextMenu>
             {/if}
 
             {#if isCreatingSharedAlbum && album.albumUsers.length === 0}
@@ -517,60 +530,59 @@
                 size="sm"
                 rounded="lg"
                 disabled={album.assetCount === 0}
-                on:click={() => (viewMode = ViewMode.SELECT_USERS)}
+                onclick={() => (viewMode = AlbumPageViewMode.SELECT_USERS)}
               >
                 {$t('share')}
               </Button>
             {/if}
-          </svelte:fragment>
+          {/snippet}
         </ControlAppBar>
       {/if}
 
-      {#if viewMode === ViewMode.SELECT_ASSETS}
-        <ControlAppBar on:close={handleCloseSelectAssets}>
-          <svelte:fragment slot="leading">
+      {#if viewMode === AlbumPageViewMode.SELECT_ASSETS}
+        <ControlAppBar onClose={handleCloseSelectAssets}>
+          {#snippet leading()}
             <p class="text-lg dark:text-immich-dark-fg">
-              {#if $timelineSelected.size === 0}
+              {#if !timelineInteraction.selectionActive}
                 {$t('add_to_album')}
               {:else}
-                {$t('selected_count', { values: { count: $timelineSelected.size } })}
+                {$t('selected_count', { values: { count: timelineInteraction.selectedAssets.size } })}
               {/if}
             </p>
-          </svelte:fragment>
+          {/snippet}
 
-          <svelte:fragment slot="trailing">
+          {#snippet trailing()}
             <button
               type="button"
-              on:click={handleSelectFromComputer}
+              onclick={handleSelectFromComputer}
               class="rounded-lg px-6 py-2 text-sm font-medium text-immich-primary transition-all hover:bg-immich-primary/10 dark:text-immich-dark-primary dark:hover:bg-immich-dark-primary/25"
             >
               {$t('select_from_computer')}
             </button>
-            <Button size="sm" rounded="lg" disabled={$timelineSelected.size === 0} on:click={handleAddAssets}
+            <Button size="sm" rounded="lg" disabled={!timelineInteraction.selectionActive} onclick={handleAddAssets}
               >{$t('done')}</Button
             >
-          </svelte:fragment>
+          {/snippet}
         </ControlAppBar>
       {/if}
 
-      {#if viewMode === ViewMode.SELECT_THUMBNAIL}
-        <ControlAppBar on:close={() => (viewMode = ViewMode.VIEW)}>
-          <svelte:fragment slot="leading">{$t('select_album_cover')}</svelte:fragment>
+      {#if viewMode === AlbumPageViewMode.SELECT_THUMBNAIL}
+        <ControlAppBar onClose={() => (viewMode = AlbumPageViewMode.VIEW)}>
+          {#snippet leading()}
+            {$t('select_album_cover')}
+          {/snippet}
         </ControlAppBar>
       {/if}
     {/if}
 
-    <main
-      class="relative h-screen overflow-hidden bg-immich-bg px-6 pt-[var(--navbar-height)] dark:bg-immich-dark-bg"
-      style={`width:${assetGridWidth}px`}
-    >
+    <main class="relative h-screen overflow-hidden bg-immich-bg px-6 pt-[var(--navbar-height)] dark:bg-immich-dark-bg">
       <!-- Use key because AssetGrid can't deal with changing stores -->
       {#key albumKey}
-        {#if viewMode === ViewMode.SELECT_ASSETS}
+        {#if viewMode === AlbumPageViewMode.SELECT_ASSETS}
           <AssetGrid
             enableRouting={false}
             assetStore={timelineStore}
-            assetInteractionStore={timelineInteractionStore}
+            assetInteraction={timelineInteraction}
             isSelectionMode={true}
           />
         {:else}
@@ -578,18 +590,23 @@
             enableRouting={true}
             {album}
             {assetStore}
-            {assetInteractionStore}
+            {assetInteraction}
             isShared={album.albumUsers.length > 0}
-            isSelectionMode={viewMode === ViewMode.SELECT_THUMBNAIL}
-            singleSelect={viewMode === ViewMode.SELECT_THUMBNAIL}
+            isSelectionMode={viewMode === AlbumPageViewMode.SELECT_THUMBNAIL}
+            singleSelect={viewMode === AlbumPageViewMode.SELECT_THUMBNAIL}
             showArchiveIcon
-            on:select={({ detail: asset }) => handleUpdateThumbnail(asset.id)}
-            on:escape={handleEscape}
+            onSelect={({ id }) => handleUpdateThumbnail(id)}
+            onEscape={handleEscape}
           >
-            {#if viewMode !== ViewMode.SELECT_THUMBNAIL}
+            {#if viewMode !== AlbumPageViewMode.SELECT_THUMBNAIL}
               <!-- ALBUM TITLE -->
               <section class="pt-8 md:pt-24">
-                <AlbumTitle id={album.id} bind:albumName={album.albumName} {isOwned} />
+                <AlbumTitle
+                  id={album.id}
+                  albumName={album.albumName}
+                  {isOwned}
+                  onUpdate={(albumName) => (album.albumName = albumName)}
+                />
 
                 {#if album.assetCount > 0}
                   <AlbumSummary {album} />
@@ -605,18 +622,18 @@
                         color="gray"
                         size="20"
                         icon={mdiLink}
-                        on:click={() => (viewMode = ViewMode.LINK_SHARING)}
+                        onclick={() => (viewMode = AlbumPageViewMode.LINK_SHARING)}
                       />
                     {/if}
 
                     <!-- owner -->
-                    <button type="button" on:click={() => (viewMode = ViewMode.VIEW_USERS)}>
+                    <button type="button" onclick={() => (viewMode = AlbumPageViewMode.VIEW_USERS)}>
                       <UserAvatar user={album.owner} size="md" />
                     </button>
 
                     <!-- users with write access (collaborators) -->
                     {#each album.albumUsers.filter(({ role }) => role === AlbumUserRole.Editor) as { user } (user.id)}
-                      <button type="button" on:click={() => (viewMode = ViewMode.VIEW_USERS)}>
+                      <button type="button" onclick={() => (viewMode = AlbumPageViewMode.VIEW_USERS)}>
                         <UserAvatar {user} size="md" />
                       </button>
                     {/each}
@@ -628,7 +645,7 @@
                         color="gray"
                         size="20"
                         icon={mdiDotsVertical}
-                        on:click={() => (viewMode = ViewMode.VIEW_USERS)}
+                        onclick={() => (viewMode = AlbumPageViewMode.VIEW_USERS)}
                       />
                     {/if}
 
@@ -637,7 +654,7 @@
                         color="gray"
                         size="20"
                         icon={mdiPlus}
-                        on:click={() => (viewMode = ViewMode.SELECT_USERS)}
+                        onclick={() => (viewMode = AlbumPageViewMode.SELECT_USERS)}
                         title={$t('add_more_users')}
                       />
                     {/if}
@@ -654,7 +671,7 @@
                   <p class="text-xs dark:text-immich-dark-fg">{$t('add_photos').toUpperCase()}</p>
                   <button
                     type="button"
-                    on:click={() => (viewMode = ViewMode.SELECT_ASSETS)}
+                    onclick={() => (viewMode = AlbumPageViewMode.SELECT_ASSETS)}
                     class="mt-5 flex w-full place-items-center gap-6 rounded-md border bg-immich-bg px-8 py-8 text-immich-fg transition-all hover:bg-gray-100 hover:text-immich-primary dark:border-none dark:bg-immich-dark-gray dark:text-immich-dark-fg dark:hover:text-immich-dark-primary"
                   >
                     <span class="text-text-immich-primary dark:text-immich-dark-primary"
@@ -674,8 +691,8 @@
               disabled={!album.isActivityEnabled}
               {isLiked}
               numberOfComments={$numberOfComments}
-              on:favorite={handleFavorite}
-              on:openActivityTab={handleOpenAndCloseActivityTab}
+              onFavorite={handleFavorite}
+              onOpenActivityTab={handleOpenAndCloseActivityTab}
             />
           </div>
         {/if}
@@ -697,38 +714,38 @@
           albumId={album.id}
           {isLiked}
           bind:reactions
-          on:addComment={() => updateNumberOfComments(1)}
-          on:deleteComment={() => updateNumberOfComments(-1)}
-          on:deleteLike={() => (isLiked = null)}
-          on:close={handleOpenAndCloseActivityTab}
+          onAddComment={() => updateNumberOfComments(1)}
+          onDeleteComment={() => updateNumberOfComments(-1)}
+          onDeleteLike={() => (isLiked = null)}
+          onClose={handleOpenAndCloseActivityTab}
         />
       </div>
     </div>
   {/if}
 </div>
-{#if viewMode === ViewMode.SELECT_USERS}
+{#if viewMode === AlbumPageViewMode.SELECT_USERS}
   <UserSelectionModal
     {album}
-    on:select={({ detail: users }) => handleAddUsers(users)}
-    on:share={() => (viewMode = ViewMode.LINK_SHARING)}
-    onClose={() => (viewMode = ViewMode.VIEW)}
+    onSelect={handleAddUsers}
+    onShare={() => (viewMode = AlbumPageViewMode.LINK_SHARING)}
+    onClose={() => (viewMode = AlbumPageViewMode.VIEW)}
   />
 {/if}
 
-{#if viewMode === ViewMode.LINK_SHARING}
-  <CreateSharedLinkModal albumId={album.id} onClose={() => (viewMode = ViewMode.VIEW)} />
+{#if viewMode === AlbumPageViewMode.LINK_SHARING}
+  <CreateSharedLinkModal albumId={album.id} onClose={() => (viewMode = AlbumPageViewMode.VIEW)} />
 {/if}
 
-{#if viewMode === ViewMode.VIEW_USERS}
+{#if viewMode === AlbumPageViewMode.VIEW_USERS}
   <ShareInfoModal
-    onClose={() => (viewMode = ViewMode.VIEW)}
+    onClose={() => (viewMode = AlbumPageViewMode.VIEW)}
     {album}
-    on:remove={({ detail: userId }) => handleRemoveUser(userId)}
-    on:refreshAlbum={refreshAlbum}
+    onRemove={(userId) => handleRemoveUser(userId, AlbumPageViewMode.VIEW_USERS)}
+    onRefreshAlbum={refreshAlbum}
   />
 {/if}
 
-{#if viewMode === ViewMode.OPTIONS && $user}
+{#if viewMode === AlbumPageViewMode.OPTIONS && $user}
   <AlbumOptions
     {album}
     order={albumOrder}
@@ -737,9 +754,11 @@
       albumOrder = order;
       await setModeToView();
     }}
-    on:close={() => (viewMode = ViewMode.VIEW)}
-    on:toggleEnableActivity={handleToggleEnableActivity}
-    on:showSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)}
+    onRemove={(userId) => handleRemoveUser(userId, AlbumPageViewMode.OPTIONS)}
+    onRefreshAlbum={refreshAlbum}
+    onClose={() => (viewMode = AlbumPageViewMode.VIEW)}
+    onToggleEnabledActivity={handleToggleEnableActivity}
+    onShowSelectSharedUser={() => (viewMode = AlbumPageViewMode.SELECT_USERS)}
   />
 {/if}
 
diff --git a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 2ce1309351..5301364ccb 100644
--- a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -12,36 +12,40 @@
   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
   import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
   import { AssetAction } from '$lib/constants';
-  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
   import { AssetStore } from '$lib/stores/assets.store';
   import type { PageData } from './$types';
   import { mdiPlus, mdiDotsVertical } from '@mdi/js';
   import { t } from 'svelte-i18n';
   import { onDestroy } from 'svelte';
+  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
+
+  let { data }: Props = $props();
 
   const assetStore = new AssetStore({ isArchived: true });
-  const assetInteractionStore = createAssetInteractionStore();
-  const { isMultiSelectState, selectedAssets } = assetInteractionStore;
-
-  $: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite);
+  const assetInteraction = new AssetInteraction();
 
   onDestroy(() => {
     assetStore.destroy();
   });
 </script>
 
-{#if $isMultiSelectState}
-  <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
+{#if assetInteraction.selectionActive}
+  <AssetSelectControlBar
+    assets={assetInteraction.selectedAssets}
+    clearSelect={() => assetInteraction.clearMultiselect()}
+  >
     <ArchiveAction unarchive onArchive={(assetIds) => assetStore.removeAssets(assetIds)} />
     <CreateSharedLink />
-    <SelectAllAssets {assetStore} {assetInteractionStore} />
+    <SelectAllAssets {assetStore} {assetInteraction} />
     <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
       <AddToAlbum />
       <AddToAlbum shared />
     </ButtonContextMenu>
-    <FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
+    <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
     <ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}>
       <DownloadAction menuItem />
       <DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
@@ -49,8 +53,10 @@
   </AssetSelectControlBar>
 {/if}
 
-<UserPageLayout hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={false}>
-  <AssetGrid enableRouting={true} {assetStore} {assetInteractionStore} removeAction={AssetAction.UNARCHIVE}>
-    <EmptyPlaceholder text={$t('no_archived_assets_message')} slot="empty" />
+<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
+  <AssetGrid enableRouting={true} {assetStore} {assetInteraction} removeAction={AssetAction.UNARCHIVE}>
+    {#snippet empty()}
+      <EmptyPlaceholder text={$t('no_archived_assets_message')} />
+    {/snippet}
   </AssetGrid>
 </UserPageLayout>
diff --git a/web/src/routes/(user)/buy/+page.svelte b/web/src/routes/(user)/buy/+page.svelte
index 1f71269c11..eb0194c447 100644
--- a/web/src/routes/(user)/buy/+page.svelte
+++ b/web/src/routes/(user)/buy/+page.svelte
@@ -11,8 +11,12 @@
   import { purchaseStore } from '$lib/stores/purchase.store';
   import SupporterBadge from '$lib/components/shared-components/side-bar/supporter-badge.svelte';
 
-  export let data: PageData;
-  let showLicenseActivated = false;
+  interface Props {
+    data: PageData;
+  }
+
+  let { data }: Props = $props();
+  let showLicenseActivated = $state(false);
   const { isPurchased } = purchaseStore;
 </script>
 
diff --git a/web/src/routes/(user)/explore/+page.svelte b/web/src/routes/(user)/explore/+page.svelte
index 18c9dffdf3..fef6a29b85 100644
--- a/web/src/routes/(user)/explore/+page.svelte
+++ b/web/src/routes/(user)/explore/+page.svelte
@@ -12,22 +12,21 @@
   import { websocketEvents } from '$lib/stores/websocket';
   import SingleGridRow from '$lib/components/shared-components/single-grid-row.svelte';
 
-  export let data: PageData;
-
-  enum Field {
-    CITY = 'exifInfo.city',
-    TAGS = 'smartInfo.tags',
-    OBJECTS = 'smartInfo.objects',
+  interface Props {
+    data: PageData;
   }
 
-  const getFieldItems = (items: SearchExploreResponseDto[], field: Field) => {
+  let { data }: Props = $props();
+
+  const getFieldItems = (items: SearchExploreResponseDto[], field: string) => {
     const targetField = items.find((item) => item.fieldName === field);
     return targetField?.items || [];
   };
 
-  $: places = getFieldItems(data.items, Field.CITY);
-  $: people = data.response.people;
-  $: hasPeople = data.response.total > 0;
+  let places = $derived(getFieldItems(data.items, 'exifInfo.city'));
+  let people = $state(data.response.people);
+
+  let hasPeople = $derived(data.response.total > 0);
 
   onMount(() => {
     return websocketEvents.on('on_person_thumbnail', (personId: string) => {
@@ -36,8 +35,6 @@
           person.updatedAt = Date.now().toString();
         }
       });
-      // trigger reactivity
-      people = people;
     });
   });
 </script>
@@ -53,13 +50,21 @@
           draggable="false">{$t('view_all')}</a
         >
       </div>
-      <SingleGridRow class="grid md:grid-auto-fill-28 grid-auto-fill-20 gap-x-4" let:itemCount>
-        {#each people.slice(0, itemCount) as person (person.id)}
-          <a href="{AppRoute.PEOPLE}/{person.id}" class="text-center">
-            <ImageThumbnail circle shadow url={getPeopleThumbnailUrl(person)} altText={person.name} widthStyle="100%" />
-            <p class="mt-2 text-ellipsis text-sm font-medium dark:text-white">{person.name}</p>
-          </a>
-        {/each}
+      <SingleGridRow class="grid md:grid-auto-fill-28 grid-auto-fill-20 gap-x-4">
+        {#snippet children({ itemCount })}
+          {#each people.slice(0, itemCount) as person (person.id)}
+            <a href="{AppRoute.PEOPLE}/{person.id}" class="text-center">
+              <ImageThumbnail
+                circle
+                shadow
+                url={getPeopleThumbnailUrl(person)}
+                altText={person.name}
+                widthStyle="100%"
+              />
+              <p class="mt-2 text-ellipsis text-sm font-medium dark:text-white">{person.name}</p>
+            </a>
+          {/each}
+        {/snippet}
       </SingleGridRow>
     </div>
   {/if}
@@ -74,23 +79,29 @@
           draggable="false">{$t('view_all')}</a
         >
       </div>
-      <SingleGridRow class="grid md:grid-auto-fill-36 grid-auto-fill-28 gap-x-4" let:itemCount>
-        {#each places.slice(0, itemCount) as item (item.data.id)}
-          <a class="relative" href="{AppRoute.SEARCH}?{getMetadataSearchQuery({ city: item.value })}" draggable="false">
-            <div class="flex justify-center overflow-hidden rounded-xl brightness-75 filter">
-              <img
-                src={getAssetThumbnailUrl({ id: item.data.id, size: AssetMediaSize.Thumbnail })}
-                alt={item.value}
-                class="object-cover aspect-square w-full"
-              />
-            </div>
-            <span
-              class="w-100 absolute bottom-2 w-full text-ellipsis px-1 text-center text-sm font-medium capitalize text-white backdrop-blur-[1px] hover:cursor-pointer"
+      <SingleGridRow class="grid md:grid-auto-fill-36 grid-auto-fill-28 gap-x-4">
+        {#snippet children({ itemCount })}
+          {#each places.slice(0, itemCount) as item (item.data.id)}
+            <a
+              class="relative"
+              href="{AppRoute.SEARCH}?{getMetadataSearchQuery({ city: item.value })}"
+              draggable="false"
             >
-              {item.value}
-            </span>
-          </a>
-        {/each}
+              <div class="flex justify-center overflow-hidden rounded-xl brightness-75 filter">
+                <img
+                  src={getAssetThumbnailUrl({ id: item.data.id, size: AssetMediaSize.Thumbnail })}
+                  alt={item.value}
+                  class="object-cover aspect-square w-full"
+                />
+              </div>
+              <span
+                class="w-100 absolute bottom-2 w-full text-ellipsis px-1 text-center text-sm font-medium capitalize text-white backdrop-blur-[1px] hover:cursor-pointer"
+              >
+                {item.value}
+              </span>
+            </a>
+          {/each}
+        {/snippet}
       </SingleGridRow>
     </div>
   {/if}
diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 13e70c9161..33a03292cd 100644
--- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -14,20 +14,23 @@
   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
   import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
   import { AssetAction } from '$lib/constants';
-  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
   import { AssetStore } from '$lib/stores/assets.store';
   import type { PageData } from './$types';
   import { mdiDotsVertical, mdiPlus } from '@mdi/js';
   import { t } from 'svelte-i18n';
   import { onDestroy } from 'svelte';
+  import { preferences } from '$lib/stores/user.store';
+  import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
+  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
+
+  let { data }: Props = $props();
 
   const assetStore = new AssetStore({ isFavorite: true });
-  const assetInteractionStore = createAssetInteractionStore();
-  const { isMultiSelectState, selectedAssets } = assetInteractionStore;
-
-  $: isAllArchive = [...$selectedAssets].every((asset) => asset.isArchived);
+  const assetInteraction = new AssetInteraction();
 
   onDestroy(() => {
     assetStore.destroy();
@@ -35,11 +38,14 @@
 </script>
 
 <!-- Multiselection mode app bar -->
-{#if $isMultiSelectState}
-  <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
+{#if assetInteraction.selectionActive}
+  <AssetSelectControlBar
+    assets={assetInteraction.selectedAssets}
+    clearSelect={() => assetInteraction.clearMultiselect()}
+  >
     <FavoriteAction removeFavorite onFavorite={(assetIds) => assetStore.removeAssets(assetIds)} />
     <CreateSharedLink />
-    <SelectAllAssets {assetStore} {assetInteractionStore} />
+    <SelectAllAssets {assetStore} {assetInteraction} />
     <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
       <AddToAlbum />
       <AddToAlbum shared />
@@ -48,14 +54,23 @@
       <DownloadAction menuItem />
       <ChangeDate menuItem />
       <ChangeLocation menuItem />
-      <ArchiveAction menuItem unarchive={isAllArchive} onArchive={(assetIds) => assetStore.removeAssets(assetIds)} />
+      <ArchiveAction
+        menuItem
+        unarchive={assetInteraction.isAllArchived}
+        onArchive={(assetIds) => assetStore.removeAssets(assetIds)}
+      />
+      {#if $preferences.tags.enabled}
+        <TagAction menuItem />
+      {/if}
       <DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
     </ButtonContextMenu>
   </AssetSelectControlBar>
 {/if}
 
-<UserPageLayout hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={false}>
-  <AssetGrid enableRouting={true} {assetStore} {assetInteractionStore} removeAction={AssetAction.UNFAVORITE}>
-    <EmptyPlaceholder text={$t('no_favorites_message')} slot="empty" />
+<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
+  <AssetGrid enableRouting={true} {assetStore} {assetInteraction} removeAction={AssetAction.UNFAVORITE}>
+    {#snippet empty()}
+      <EmptyPlaceholder text={$t('no_favorites_message')} />
+    {/snippet}
   </AssetGrid>
 </UserPageLayout>
diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 2cd3d8c9f3..1c3118755d 100644
--- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -8,25 +8,30 @@
   import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
   import { AppRoute, QueryParameter } from '$lib/constants';
   import type { Viewport } from '$lib/stores/assets.store';
-  import { foldersStore } from '$lib/stores/folders.store';
+  import { foldersStore } from '$lib/stores/folders.svelte';
   import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
-  import { type AssetResponseDto } from '@immich/sdk';
   import { mdiFolder, mdiFolderHome, mdiFolderOutline } from '@mdi/js';
   import { onMount } from 'svelte';
   import { t } from 'svelte-i18n';
   import type { PageData } from './$types';
   import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte';
   import SkipLink from '$lib/components/elements/buttons/skip-link.svelte';
+  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
 
-  let selectedAssets: Set<AssetResponseDto> = new Set();
-  const viewport: Viewport = { width: 0, height: 0 };
+  let { data }: Props = $props();
 
-  $: pathSegments = data.path ? data.path.split('/') : [];
-  $: tree = buildTree($foldersStore?.uniquePaths || []);
-  $: currentPath = $page.url.searchParams.get(QueryParameter.PATH) || '';
-  $: currentTreeItems = currentPath ? data.currentFolders : Object.keys(tree);
+  const viewport: Viewport = $state({ width: 0, height: 0 });
+
+  let pathSegments = $derived(data.path ? data.path.split('/') : []);
+  let tree = $derived(buildTree(foldersStore.uniquePaths));
+  let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || '');
+  let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree));
+
+  const assetInteraction = new AssetInteraction();
 
   onMount(async () => {
     await foldersStore.fetchUniquePaths();
@@ -37,7 +42,7 @@
   };
 
   const getLink = (path: string) => {
-    const url = new URL(AppRoute.FOLDERS, window.location.href);
+    const url = new URL(AppRoute.FOLDERS, globalThis.location.href);
     if (path) {
       url.searchParams.set(QueryParameter.PATH, path);
     }
@@ -48,20 +53,22 @@
 </script>
 
 <UserPageLayout title={data.meta.title}>
-  <SideBarSection slot="sidebar">
-    <SkipLink target={`#${headerId}`} text={$t('skip_to_folders')} />
-    <section>
-      <div class="text-xs pl-4 mb-2 dark:text-white">{$t('explorer').toUpperCase()}</div>
-      <div class="h-full">
-        <TreeItems
-          icons={{ default: mdiFolderOutline, active: mdiFolder }}
-          items={tree}
-          active={currentPath}
-          {getLink}
-        />
-      </div>
-    </section>
-  </SideBarSection>
+  {#snippet sidebar()}
+    <SideBarSection>
+      <SkipLink target={`#${headerId}`} text={$t('skip_to_folders')} />
+      <section>
+        <div class="text-xs pl-4 mb-2 dark:text-white">{$t('explorer').toUpperCase()}</div>
+        <div class="h-full">
+          <TreeItems
+            icons={{ default: mdiFolderOutline, active: mdiFolder }}
+            items={tree}
+            active={currentPath}
+            {getLink}
+          />
+        </div>
+      </section>
+    </SideBarSection>
+  {/snippet}
 
   <Breadcrumbs {pathSegments} icon={mdiFolderHome} title={$t('folders')} {getLink} />
 
@@ -73,7 +80,7 @@
       <div bind:clientHeight={viewport.height} bind:clientWidth={viewport.width} class="mt-2">
         <GalleryViewer
           assets={data.pathAssets}
-          bind:selectedAssets
+          {assetInteraction}
           {viewport}
           disableAssetSelect={true}
           showAssetName={true}
diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts
index 41800c1a7d..d6fc683c08 100644
--- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts
+++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts
@@ -1,10 +1,9 @@
 import { QueryParameter } from '$lib/constants';
-import { foldersStore } from '$lib/stores/folders.store';
+import { foldersStore } from '$lib/stores/folders.svelte';
 import { authenticate } from '$lib/utils/auth';
 import { getFormatter } from '$lib/utils/i18n';
 import { getAssetInfoFromParam } from '$lib/utils/navigation';
 import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
-import { get } from 'svelte/store';
 import type { PageLoad } from './$types';
 
 export const load = (async ({ params, url }) => {
@@ -13,18 +12,16 @@ export const load = (async ({ params, url }) => {
   const $t = await getFormatter();
 
   await foldersStore.fetchUniquePaths();
-  const { uniquePaths } = get(foldersStore);
 
   let pathAssets = null;
 
   const path = url.searchParams.get(QueryParameter.PATH);
   if (path) {
     await foldersStore.fetchAssetsByPath(path);
-    const { assets } = get(foldersStore);
-    pathAssets = assets[path] || null;
+    pathAssets = foldersStore.assets[path] || null;
   }
 
-  let tree = buildTree(uniquePaths || []);
+  let tree = buildTree(foldersStore.uniquePaths);
   const parts = normalizeTreePath(path || '').split('/');
   for (const part of parts) {
     tree = tree?.[part];
diff --git a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 0ea0ed18bb..613ae4d66b 100644
--- a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -1,4 +1,6 @@
 <script lang="ts">
+  import { run } from 'svelte/legacy';
+
   import { goto } from '$app/navigation';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
   import MapSettingsModal from '$lib/components/map-page/map-settings-modal.svelte';
@@ -17,15 +19,19 @@
   import { handlePromiseError } from '$lib/utils';
   import { navigate } from '$lib/utils/navigation';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
+
+  let { data }: Props = $props();
 
   let { isViewing: showAssetViewer, asset: viewingAsset, setAssetId } = assetViewingStore;
 
   let abortController: AbortController;
-  let mapMarkers: MapMarkerResponseDto[] = [];
-  let viewingAssets: string[] = [];
+  let mapMarkers: MapMarkerResponseDto[] = $state([]);
+  let viewingAssets: string[] = $state([]);
   let viewingAssetCursor = 0;
-  let showSettingsModal = false;
+  let showSettingsModal = $state(false);
 
   onMount(async () => {
     mapMarkers = await loadMapMarkers();
@@ -36,9 +42,11 @@
     assetViewingStore.showAssetViewer(false);
   });
 
-  $: if (!$featureFlags.map) {
-    handlePromiseError(goto(AppRoute.PHOTOS));
-  }
+  run(() => {
+    if (!$featureFlags.map) {
+      handlePromiseError(goto(AppRoute.PHOTOS));
+    }
+  });
   const omit = (obj: MapSettings, key: string) => {
     return Object.fromEntries(Object.entries(obj).filter(([k]) => k !== key));
   };
@@ -113,7 +121,7 @@
 {#if $featureFlags.loaded && $featureFlags.map}
   <UserPageLayout title={data.meta.title}>
     <div class="isolate h-full w-full">
-      <Map hash bind:mapMarkers bind:showSettingsModal on:selected={(event) => onViewAssets(event.detail)} />
+      <Map hash bind:mapMarkers bind:showSettingsModal onSelect={onViewAssets} />
     </div>
   </UserPageLayout>
   <Portal target="body">
@@ -122,9 +130,9 @@
         <AssetViewer
           asset={$viewingAsset}
           showNavigation={viewingAssets.length > 1}
-          on:next={navigateNext}
-          on:previous={navigatePrevious}
-          on:close={() => {
+          onNext={navigateNext}
+          onPrevious={navigatePrevious}
+          onClose={() => {
             assetViewingStore.showAssetViewer(false);
             handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
           }}
@@ -137,11 +145,11 @@
   {#if showSettingsModal}
     <MapSettingsModal
       settings={{ ...$mapSettings }}
-      on:close={() => (showSettingsModal = false)}
-      on:save={async ({ detail }) => {
-        const shouldUpdate = !isEqual(omit(detail, 'allowDarkMode'), omit($mapSettings, 'allowDarkMode'));
+      onClose={() => (showSettingsModal = false)}
+      onSave={async (settings) => {
+        const shouldUpdate = !isEqual(omit(settings, 'allowDarkMode'), omit($mapSettings, 'allowDarkMode'));
         showSettingsModal = false;
-        $mapSettings = detail;
+        $mapSettings = settings;
 
         if (shouldUpdate) {
           mapMarkers = await loadMapMarkers();
diff --git a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index b580c4faa5..92699f916c 100644
--- a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -8,28 +8,33 @@
   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
   import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
   import { AppRoute } from '$lib/constants';
-  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
   import { AssetStore } from '$lib/stores/assets.store';
   import { onDestroy } from 'svelte';
   import type { PageData } from './$types';
   import { mdiPlus, mdiArrowLeft } from '@mdi/js';
   import { t } from 'svelte-i18n';
+  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
+
+  let { data }: Props = $props();
 
   const assetStore = new AssetStore({ userId: data.partner.id, isArchived: false, withStacked: true });
-  const assetInteractionStore = createAssetInteractionStore();
-  const { isMultiSelectState, selectedAssets, clearMultiselect } = assetInteractionStore;
+  const assetInteraction = new AssetInteraction();
 
   onDestroy(() => {
-    assetInteractionStore.clearMultiselect();
     assetStore.destroy();
   });
 </script>
 
 <main class="grid h-screen bg-immich-bg pt-18 dark:bg-immich-dark-bg">
-  {#if $isMultiSelectState}
-    <AssetSelectControlBar assets={$selectedAssets} clearSelect={clearMultiselect}>
+  {#if assetInteraction.selectionActive}
+    <AssetSelectControlBar
+      assets={assetInteraction.selectedAssets}
+      clearSelect={() => assetInteraction.clearMultiselect()}
+    >
       <CreateSharedLink />
       <ButtonContextMenu icon={mdiPlus} title={$t('add')}>
         <AddToAlbum />
@@ -38,13 +43,13 @@
       <DownloadAction />
     </AssetSelectControlBar>
   {:else}
-    <ControlAppBar showBackButton backIcon={mdiArrowLeft} on:close={() => goto(AppRoute.SHARING)}>
-      <svelte:fragment slot="leading">
+    <ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(AppRoute.SHARING)}>
+      {#snippet leading()}
         <p class="whitespace-nowrap text-immich-fg dark:text-immich-dark-fg">
           {data.partner.name}'s photos
         </p>
-      </svelte:fragment>
+      {/snippet}
     </ControlAppBar>
   {/if}
-  <AssetGrid enableRouting={true} {assetStore} {assetInteractionStore} />
+  <AssetGrid enableRouting={true} {assetStore} {assetInteraction} />
 </main>
diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte
index f1a2674e24..0b51a7e240 100644
--- a/web/src/routes/(user)/people/+page.svelte
+++ b/web/src/routes/(user)/people/+page.svelte
@@ -2,6 +2,7 @@
   import { goto } from '$app/navigation';
   import { page } from '$app/stores';
   import { focusTrap } from '$lib/actions/focus-trap';
+  import { scrollMemory } from '$lib/actions/scroll-memory';
   import Button from '$lib/components/elements/buttons/button.svelte';
   import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
   import Icon from '$lib/components/elements/icon.svelte';
@@ -17,7 +18,7 @@
     notificationController,
     NotificationType,
   } from '$lib/components/shared-components/notification/notification';
-  import { ActionQueryParameterValue, AppRoute, QueryParameter } from '$lib/constants';
+  import { ActionQueryParameterValue, AppRoute, QueryParameter, SessionStorageKey } from '$lib/constants';
   import { locale } from '$lib/stores/preferences.store';
   import { websocketEvents } from '$lib/stores/websocket';
   import { handlePromiseError } from '$lib/utils';
@@ -38,47 +39,77 @@
   import { fly } from 'svelte/transition';
   import type { PageData } from './$types';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
 
-  $: people = data.people.people;
-  $: visiblePeople = people.filter((people) => !people.isHidden);
-  $: countVisiblePeople = searchName ? searchedPeopleLocal.length : data.people.total - data.people.hidden;
-  $: showPeople = searchName ? searchedPeopleLocal : visiblePeople;
-
-  let selectHidden = false;
-  let searchName = '';
-  let showChangeNameModal = false;
-  let showSetBirthDateModal = false;
-  let showMergeModal = false;
-  let personName = '';
-  let nextPage = data.people.hasNextPage ? 2 : null;
-  let personMerge1: PersonResponseDto;
-  let personMerge2: PersonResponseDto;
-  let potentialMergePeople: PersonResponseDto[] = [];
-  let edittingPerson: PersonResponseDto | null = null;
-  let searchedPeopleLocal: PersonResponseDto[] = [];
-  let handleSearchPeople: (force?: boolean, name?: string) => Promise<void>;
-  let changeNameInputEl: HTMLInputElement | null;
-  let innerHeight: number;
+  let { data }: Props = $props();
 
+  let selectHidden = $state(false);
+  let searchName = $state('');
+  let showChangeNameModal = $state(false);
+  let showSetBirthDateModal = $state(false);
+  let showMergeModal = $state(false);
+  let personName = $state('');
+  let currentPage = $state(1);
+  let nextPage = $state(data.people.hasNextPage ? 2 : null);
+  let personMerge1 = $state<PersonResponseDto>();
+  let personMerge2 = $state<PersonResponseDto>();
+  let potentialMergePeople: PersonResponseDto[] = $state([]);
+  let edittingPerson: PersonResponseDto | null = $state(null);
+  let searchedPeopleLocal: PersonResponseDto[] = $state([]);
+  // let handleSearchPeople: (force?: boolean, name?: string) => Promise<void> = $state();
+  let changeNameInputEl = $state<HTMLInputElement>();
+  let innerHeight = $state(0);
+  let searchPeopleElement = $state<ReturnType<typeof SearchPeople>>();
   onMount(() => {
     const getSearchedPeople = $page.url.searchParams.get(QueryParameter.SEARCHED_PEOPLE);
     if (getSearchedPeople) {
       searchName = getSearchedPeople;
-      handlePromiseError(handleSearchPeople(true, searchName));
+      if (searchPeopleElement) {
+        handlePromiseError(searchPeopleElement.searchPeople(true, searchName));
+      }
     }
+
     return websocketEvents.on('on_person_thumbnail', (personId: string) => {
       for (const person of people) {
         if (person.id === personId) {
           person.updatedAt = new Date().toISOString();
         }
       }
-
-      // trigger reactivity
-      people = people;
     });
   });
 
+  const loadInitialScroll = () =>
+    new Promise<void>((resolve) => {
+      // Load up to previously loaded page when returning.
+      let newNextPage = sessionStorage.getItem(SessionStorageKey.INFINITE_SCROLL_PAGE);
+      if (newNextPage && nextPage) {
+        let startingPage = nextPage,
+          pagesToLoad = Number.parseInt(newNextPage) - nextPage;
+
+        if (pagesToLoad) {
+          handlePromiseError(
+            Promise.all(
+              Array.from({ length: pagesToLoad }).map((_, i) => {
+                return getAllPeople({ withHidden: true, page: startingPage + i });
+              }),
+            ).then((pages) => {
+              for (const page of pages) {
+                people = people.concat(page.people);
+              }
+              currentPage = startingPage + pagesToLoad - 1;
+              nextPage = pages.at(-1)?.hasNextPage ? startingPage + pagesToLoad : null;
+              resolve(); // wait until extra pages are loaded
+            }),
+          );
+        } else {
+          resolve();
+        }
+        sessionStorage.removeItem(SessionStorageKey.INFINITE_SCROLL_PAGE);
+      }
+    });
+
   const loadNextPage = async () => {
     if (!nextPage) {
       return;
@@ -87,6 +118,9 @@
     try {
       const { people: newPeople, hasNextPage } = await getAllPeople({ withHidden: true, page: nextPage });
       people = people.concat(newPeople);
+      if (nextPage !== null) {
+        currentPage = nextPage;
+      }
       nextPage = hasNextPage ? nextPage + 1 : null;
     } catch (error) {
       handleError(error, $t('errors.failed_to_load_people'));
@@ -145,9 +179,6 @@
           message: $t('change_name_successfully'),
           type: NotificationType.Info,
         });
-
-        // trigger reactivity
-        people = people;
       } catch (error) {
         handleError(error, $t('errors.unable_to_save_name'));
       }
@@ -198,7 +229,9 @@
     );
   };
 
-  const submitNameChange = async () => {
+  const submitNameChange = async (event: Event) => {
+    event.preventDefault();
+
     potentialMergePeople = [];
     showChangeNameModal = false;
     if (!edittingPerson || personName === edittingPerson.name) {
@@ -225,9 +258,9 @@
       potentialMergePeople = people
         .filter(
           (person: PersonResponseDto) =>
-            personMerge2.name.toLowerCase() === person.name.toLowerCase() &&
+            personMerge2?.name.toLowerCase() === person.name.toLowerCase() &&
             person.id !== personMerge2.id &&
-            person.id !== personMerge1.id &&
+            person.id !== personMerge1?.id &&
             !person.isHidden,
         )
         .slice(0, 3);
@@ -293,42 +326,74 @@
   const onResetSearchBar = async () => {
     await clearQueryParam(QueryParameter.SEARCHED_PEOPLE, $page.url);
   };
+
+  let people = $state(data.people.people);
+  $effect(() => {
+    people = data.people.people;
+  });
+  let visiblePeople = $derived(people.filter((people) => !people.isHidden));
+  let countVisiblePeople = $derived(searchName ? searchedPeopleLocal.length : data.people.total - data.people.hidden);
+  let showPeople = $derived(searchName ? searchedPeopleLocal : visiblePeople);
+
+  // const submitNameChange = (event: Event) => {
+  //   event.preventDefault();
+  //   if (searchPeopleElement) {
+  //     handlePromiseError(searchPeopleElement.searchPeople(true, searchName));
+  //   }
+  // };
 </script>
 
 <svelte:window bind:innerHeight />
 
-{#if showMergeModal}
+{#if showMergeModal && personMerge1 && personMerge2}
   <MergeSuggestionModal
     {personMerge1}
     {personMerge2}
     {potentialMergePeople}
-    on:close={() => (showMergeModal = false)}
-    on:reject={() => changeName()}
-    on:confirm={(event) => handleMergeSamePerson(event.detail)}
+    onClose={() => (showMergeModal = false)}
+    onReject={changeName}
+    onConfirm={handleMergeSamePerson}
   />
 {/if}
 
 <UserPageLayout
   title={$t('people')}
   description={countVisiblePeople === 0 && !searchName ? undefined : `(${countVisiblePeople.toLocaleString($locale)})`}
+  use={[
+    [
+      scrollMemory,
+      {
+        routeStartsWith: AppRoute.PEOPLE,
+        beforeSave: () => {
+          if (currentPage) {
+            sessionStorage.setItem(SessionStorageKey.INFINITE_SCROLL_PAGE, currentPage.toString());
+          }
+        },
+        beforeClear: () => {
+          sessionStorage.removeItem(SessionStorageKey.INFINITE_SCROLL_PAGE);
+        },
+        beforeLoad: loadInitialScroll,
+      },
+    ],
+  ]}
 >
-  <svelte:fragment slot="buttons">
+  {#snippet buttons()}
     {#if people.length > 0}
       <div class="flex gap-2 items-center justify-center">
         <div class="hidden sm:block">
           <div class="w-40 lg:w-80 h-10">
             <SearchPeople
+              bind:this={searchPeopleElement}
               type="searchBar"
               placeholder={$t('search_people')}
               onReset={onResetSearchBar}
               onSearch={handleSearch}
               bind:searchName
               bind:searchedPeopleLocal
-              bind:handleSearch={handleSearchPeople}
             />
           </div>
         </div>
-        <LinkButton on:click={() => (selectHidden = !selectHidden)}>
+        <LinkButton onclick={() => (selectHidden = !selectHidden)}>
           <div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm">
             <Icon path={mdiEyeOutline} size="18" />
             <p class="ml-2">{$t('show_and_hide_people')}</p>
@@ -336,24 +401,20 @@
         </LinkButton>
       </div>
     {/if}
-  </svelte:fragment>
+  {/snippet}
 
   {#if countVisiblePeople > 0 && (!searchName || searchedPeopleLocal.length > 0)}
-    <PeopleInfiniteScroll
-      people={showPeople}
-      hasNextPage={!!nextPage && !searchName}
-      {loadNextPage}
-      let:person
-      let:index
-    >
-      <PeopleCard
-        {person}
-        preload={index < 20}
-        on:change-name={() => handleChangeName(person)}
-        on:set-birth-date={() => handleSetBirthDate(person)}
-        on:merge-people={() => handleMergePeople(person)}
-        on:hide-person={() => handleHidePerson(person)}
-      />
+    <PeopleInfiniteScroll people={showPeople} hasNextPage={!!nextPage && !searchName} {loadNextPage}>
+      {#snippet children({ person, index })}
+        <PeopleCard
+          {person}
+          preload={index < 20}
+          onChangeName={() => handleChangeName(person)}
+          onSetBirthDate={() => handleSetBirthDate(person)}
+          onMergePeople={() => handleMergePeople(person)}
+          onHidePerson={() => handleHidePerson(person)}
+        />
+      {/snippet}
     </PeopleInfiniteScroll>
   {:else}
     <div class="flex min-h-[calc(66vh_-_11rem)] w-full place-content-center items-center dark:text-white">
@@ -368,7 +429,7 @@
 
   {#if showChangeNameModal}
     <FullScreenModal title={$t('change_name')} onClose={() => (showChangeNameModal = false)}>
-      <form on:submit|preventDefault={submitNameChange} autocomplete="off" id="change-name-form">
+      <form onsubmit={submitNameChange} autocomplete="off" id="change-name-form">
         <div class="flex flex-col gap-2">
           <label class="immich-form-label" for="name">{$t('name')}</label>
           <input
@@ -381,24 +442,25 @@
           />
         </div>
       </form>
-      <svelte:fragment slot="sticky-bottom">
+
+      {#snippet stickyBottom()}
         <Button
           color="gray"
           fullwidth
-          on:click={() => {
+          onclick={() => {
             showChangeNameModal = false;
           }}>{$t('cancel')}</Button
         >
         <Button type="submit" fullwidth form="change-name-form">{$t('ok')}</Button>
-      </svelte:fragment>
+      {/snippet}
     </FullScreenModal>
   {/if}
 
   {#if showSetBirthDateModal}
     <SetBirthDateModal
       birthDate={edittingPerson?.birthDate ?? ''}
-      on:close={() => (showSetBirthDateModal = false)}
-      on:updated={(event) => submitBirthDateChange(event.detail)}
+      onClose={() => (showSetBirthDateModal = false)}
+      onUpdate={submitBirthDateChange}
     />
   {/if}
 </UserPageLayout>
diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index daa5821e85..79760b192c 100644
--- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -1,6 +1,7 @@
 <script lang="ts">
   import { afterNavigate, goto } from '$app/navigation';
   import { page } from '$app/stores';
+  import { scrollMemoryClearer } from '$lib/actions/scroll-memory';
   import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
   import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte';
   import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte';
@@ -25,8 +26,7 @@
     NotificationType,
     notificationController,
   } from '$lib/components/shared-components/notification/notification';
-  import { AppRoute, QueryParameter } from '$lib/constants';
-  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
+  import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
   import { AssetStore } from '$lib/stores/assets.store';
   import { websocketEvents } from '$lib/stores/websocket';
@@ -57,48 +57,44 @@
   import { listNavigation } from '$lib/actions/list-navigation';
   import { t } from 'svelte-i18n';
   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
+  import { preferences } from '$lib/stores/user.store';
+  import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
+  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
 
-  let numberOfAssets = data.statistics.assets;
+  let { data }: Props = $props();
+
+  let numberOfAssets = $state(data.statistics.assets);
   let { isViewing: showAssetViewer } = assetViewingStore;
 
-  enum ViewMode {
-    VIEW_ASSETS = 'view-assets',
-    SELECT_PERSON = 'select-person',
-    MERGE_PEOPLE = 'merge-people',
-    SUGGEST_MERGE = 'suggest-merge',
-    BIRTH_DATE = 'birth-date',
-    UNASSIGN_ASSETS = 'unassign-faces',
-  }
+  const assetStoreOptions = { isArchived: false, personId: data.person.id };
+  const assetStore = new AssetStore(assetStoreOptions);
 
-  let assetStore = new AssetStore({
-    isArchived: false,
-    personId: data.person.id,
+  $effect(() => {
+    // Check to trigger rebuild the timeline when navigating between people from the info panel
+    const change = assetStoreOptions.personId !== data.person.id;
+    assetStoreOptions.personId = data.person.id;
+    handlePromiseError(assetStore.updateOptions(assetStoreOptions));
+    if (change) {
+      assetStore.triggerUpdate();
+    }
   });
 
-  $: person = data.person;
-  $: thumbnailData = getPeopleThumbnailUrl(person);
-  $: if (person) {
-    handlePromiseError(updateAssetCount());
-    handlePromiseError(assetStore.updateOptions({ personId: person.id }));
-  }
+  const assetInteraction = new AssetInteraction();
 
-  const assetInteractionStore = createAssetInteractionStore();
-  const { selectedAssets, isMultiSelectState } = assetInteractionStore;
-
-  let viewMode: ViewMode = ViewMode.VIEW_ASSETS;
-  let isEditingName = false;
-  let previousRoute: string = AppRoute.EXPLORE;
+  let viewMode: PersonPageViewMode = $state(PersonPageViewMode.VIEW_ASSETS);
+  let isEditingName = $state(false);
+  let previousRoute: string = $state(AppRoute.EXPLORE);
   let people: PersonResponseDto[] = [];
-  let personMerge1: PersonResponseDto;
-  let personMerge2: PersonResponseDto;
-  let potentialMergePeople: PersonResponseDto[] = [];
-
-  let refreshAssetGrid = false;
+  let personMerge1: PersonResponseDto | undefined = $state();
+  let personMerge2: PersonResponseDto | undefined = $state();
+  let potentialMergePeople: PersonResponseDto[] = $state([]);
 
   let personName = '';
-  let suggestedPeople: PersonResponseDto[] = [];
+  let suggestedPeople: PersonResponseDto[] = $state([]);
 
   /**
    * Save the word used to search people name: for example,
@@ -107,11 +103,8 @@
    * However, it needs to make a new api request if searching 'r' returns 20 names (arbitrary value, the limit sent back by the server).
    * or if the new search word starts with another word / letter
    **/
-  let isSearchingPeople = false;
-  let suggestionContainer: HTMLDivElement;
-
-  $: isAllArchive = [...$selectedAssets].every((asset) => asset.isArchived);
-  $: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite);
+  let isSearchingPeople = $state(false);
+  let suggestionContainer: HTMLElement | undefined = $state();
 
   onMount(() => {
     const action = $page.url.searchParams.get(QueryParameter.ACTION);
@@ -120,7 +113,7 @@
       previousRoute = getPreviousRoute;
     }
     if (action == 'merge') {
-      viewMode = ViewMode.MERGE_PEOPLE;
+      viewMode = PersonPageViewMode.MERGE_PEOPLE;
     }
 
     return websocketEvents.on('on_person_thumbnail', (personId: string) => {
@@ -131,11 +124,11 @@
   });
 
   const handleEscape = async () => {
-    if ($showAssetViewer || viewMode === ViewMode.SUGGEST_MERGE) {
+    if ($showAssetViewer || viewMode === PersonPageViewMode.SUGGEST_MERGE) {
       return;
     }
-    if ($isMultiSelectState) {
-      assetInteractionStore.clearMultiselect();
+    if (assetInteraction.selectionActive) {
+      assetInteraction.clearMultiselect();
       return;
     } else {
       await goto(previousRoute);
@@ -160,13 +153,13 @@
   });
 
   const handleUnmerge = () => {
-    $assetStore.removeAssets([...$selectedAssets].map((a) => a.id));
-    assetInteractionStore.clearMultiselect();
-    viewMode = ViewMode.VIEW_ASSETS;
+    $assetStore.removeAssets(assetInteraction.selectedAssetsArray.map((a) => a.id));
+    assetInteraction.clearMultiselect();
+    viewMode = PersonPageViewMode.VIEW_ASSETS;
   };
 
   const handleReassignAssets = () => {
-    viewMode = ViewMode.UNASSIGN_ASSETS;
+    viewMode = PersonPageViewMode.UNASSIGN_ASSETS;
   };
 
   const toggleHidePerson = async () => {
@@ -181,7 +174,7 @@
         type: NotificationType.Info,
       });
 
-      await goto(previousRoute, { replaceState: true });
+      await goto(previousRoute);
     } catch (error) {
       handleError(error, $t('errors.unable_to_hide_person'));
     }
@@ -191,30 +184,28 @@
     await updateAssetCount();
     await handleGoBack();
 
-    data.person = person;
-
-    refreshAssetGrid = !refreshAssetGrid;
+    data = { ...data, person };
   };
 
   const handleSelectFeaturePhoto = async (asset: AssetResponseDto) => {
-    if (viewMode !== ViewMode.SELECT_PERSON) {
+    if (viewMode !== PersonPageViewMode.SELECT_PERSON) {
       return;
     }
     try {
-      await updatePerson({ id: person.id, personUpdateDto: { featureFaceAssetId: asset.id } });
+      person = await updatePerson({ id: person.id, personUpdateDto: { featureFaceAssetId: asset.id } });
       notificationController.show({ message: $t('feature_photo_updated'), type: NotificationType.Info });
     } catch (error) {
       handleError(error, $t('errors.unable_to_set_feature_photo'));
     }
 
-    assetInteractionStore.clearMultiselect();
+    assetInteraction.clearMultiselect();
 
-    viewMode = ViewMode.VIEW_ASSETS;
+    viewMode = PersonPageViewMode.VIEW_ASSETS;
   };
 
   const handleMergeSamePerson = async (response: [PersonResponseDto, PersonResponseDto]) => {
     const [personToMerge, personToBeMergedIn] = response;
-    viewMode = ViewMode.VIEW_ASSETS;
+    viewMode = PersonPageViewMode.VIEW_ASSETS;
     isEditingName = false;
     try {
       await mergePerson({
@@ -228,7 +219,6 @@
       people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id);
       if (personToBeMergedIn.name != personName && person.id === personToBeMergedIn.id) {
         await updateAssetCount();
-        refreshAssetGrid = !refreshAssetGrid;
         return;
       }
       await goto(`${AppRoute.PEOPLE}/${personToBeMergedIn.id}`, { replaceState: true });
@@ -243,16 +233,16 @@
     personName = person.name;
     personMerge1 = person;
     personMerge2 = person2;
-    viewMode = ViewMode.SUGGEST_MERGE;
+    viewMode = PersonPageViewMode.SUGGEST_MERGE;
   };
 
   const changeName = async () => {
-    viewMode = ViewMode.VIEW_ASSETS;
+    viewMode = PersonPageViewMode.VIEW_ASSETS;
     person.name = personName;
     try {
       isEditingName = false;
 
-      await updatePerson({ id: person.id, personUpdateDto: { name: personName } });
+      person = await updatePerson({ id: person.id, personUpdateDto: { name: personName } });
 
       notificationController.show({
         message: $t('change_name_successfully'),
@@ -264,7 +254,7 @@
   };
 
   const handleCancelEditName = () => {
-    if (viewMode === ViewMode.SUGGEST_MERGE) {
+    if (viewMode === PersonPageViewMode.SUGGEST_MERGE) {
       return;
     }
     isSearchingPeople = false;
@@ -295,13 +285,13 @@
       potentialMergePeople = result
         .filter(
           (person: PersonResponseDto) =>
-            personMerge2.name.toLowerCase() === person.name.toLowerCase() &&
+            personMerge2?.name.toLowerCase() === person.name.toLowerCase() &&
             person.id !== personMerge2.id &&
-            person.id !== personMerge1.id &&
+            person.id !== personMerge1?.id &&
             !person.isHidden,
         )
         .slice(0, 3);
-      viewMode = ViewMode.SUGGEST_MERGE;
+      viewMode = PersonPageViewMode.SUGGEST_MERGE;
       return;
     }
     await changeName();
@@ -309,7 +299,7 @@
 
   const handleSetBirthDate = async (birthDate: string) => {
     try {
-      viewMode = ViewMode.VIEW_ASSETS;
+      viewMode = PersonPageViewMode.VIEW_ASSETS;
       person.birthDate = birthDate;
 
       const updatedPerson = await updatePerson({
@@ -331,7 +321,7 @@
   };
 
   const handleGoBack = async () => {
-    viewMode = ViewMode.VIEW_ASSETS;
+    viewMode = PersonPageViewMode.VIEW_ASSETS;
     if ($page.url.searchParams.has(QueryParameter.ACTION)) {
       $page.url.searchParams.delete(QueryParameter.ACTION);
       await goto($page.url);
@@ -341,50 +331,62 @@
   onDestroy(() => {
     assetStore.destroy();
   });
+  let person = $derived(data.person);
+
+  let thumbnailData = $derived(getPeopleThumbnailUrl(person));
+
+  $effect(() => {
+    if (person) {
+      handlePromiseError(updateAssetCount());
+    }
+  });
 </script>
 
-{#if viewMode === ViewMode.UNASSIGN_ASSETS}
+{#if viewMode === PersonPageViewMode.UNASSIGN_ASSETS}
   <UnMergeFaceSelector
-    assetIds={[...$selectedAssets].map((a) => a.id)}
+    assetIds={assetInteraction.selectedAssetsArray.map((a) => a.id)}
     personAssets={person}
-    on:close={() => (viewMode = ViewMode.VIEW_ASSETS)}
-    on:confirm={handleUnmerge}
+    onClose={() => (viewMode = PersonPageViewMode.VIEW_ASSETS)}
+    onConfirm={handleUnmerge}
   />
 {/if}
 
-{#if viewMode === ViewMode.SUGGEST_MERGE}
+{#if viewMode === PersonPageViewMode.SUGGEST_MERGE && personMerge1 && personMerge2}
   <MergeSuggestionModal
     {personMerge1}
     {personMerge2}
     {potentialMergePeople}
-    on:close={() => (viewMode = ViewMode.VIEW_ASSETS)}
-    on:reject={() => changeName()}
-    on:confirm={(event) => handleMergeSamePerson(event.detail)}
+    onClose={() => (viewMode = PersonPageViewMode.VIEW_ASSETS)}
+    onReject={changeName}
+    onConfirm={handleMergeSamePerson}
   />
 {/if}
 
-{#if viewMode === ViewMode.BIRTH_DATE}
+{#if viewMode === PersonPageViewMode.BIRTH_DATE}
   <SetBirthDateModal
     birthDate={person.birthDate ?? ''}
-    on:close={() => (viewMode = ViewMode.VIEW_ASSETS)}
-    on:updated={(event) => handleSetBirthDate(event.detail)}
+    onClose={() => (viewMode = PersonPageViewMode.VIEW_ASSETS)}
+    onUpdate={handleSetBirthDate}
   />
 {/if}
 
-{#if viewMode === ViewMode.MERGE_PEOPLE}
-  <MergeFaceSelector {person} on:back={handleGoBack} on:merge={({ detail }) => handleMerge(detail)} />
+{#if viewMode === PersonPageViewMode.MERGE_PEOPLE}
+  <MergeFaceSelector {person} onBack={handleGoBack} onMerge={handleMerge} />
 {/if}
 
 <header>
-  {#if $isMultiSelectState}
-    <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
+  {#if assetInteraction.selectionActive}
+    <AssetSelectControlBar
+      assets={assetInteraction.selectedAssets}
+      clearSelect={() => assetInteraction.clearMultiselect()}
+    >
       <CreateSharedLink />
-      <SelectAllAssets {assetStore} {assetInteractionStore} />
+      <SelectAllAssets {assetStore} {assetInteraction} />
       <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
         <AddToAlbum />
         <AddToAlbum shared />
       </ButtonContextMenu>
-      <FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
+      <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
       <ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}>
         <DownloadAction menuItem filename="{person.name || 'immich'}.zip" />
         <MenuOption
@@ -394,19 +396,26 @@
         />
         <ChangeDate menuItem />
         <ChangeLocation menuItem />
-        <ArchiveAction menuItem unarchive={isAllArchive} onArchive={(assetIds) => $assetStore.removeAssets(assetIds)} />
+        <ArchiveAction
+          menuItem
+          unarchive={assetInteraction.isAllArchived}
+          onArchive={(assetIds) => $assetStore.removeAssets(assetIds)}
+        />
+        {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
+          <TagAction menuItem />
+        {/if}
         <DeleteAssets menuItem onAssetDelete={(assetIds) => $assetStore.removeAssets(assetIds)} />
       </ButtonContextMenu>
     </AssetSelectControlBar>
   {:else}
-    {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE || viewMode === ViewMode.BIRTH_DATE}
-      <ControlAppBar showBackButton backIcon={mdiArrowLeft} on:close={() => goto(previousRoute)}>
-        <svelte:fragment slot="trailing">
+    {#if viewMode === PersonPageViewMode.VIEW_ASSETS || viewMode === PersonPageViewMode.SUGGEST_MERGE || viewMode === PersonPageViewMode.BIRTH_DATE}
+      <ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(previousRoute)}>
+        {#snippet trailing()}
           <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
             <MenuOption
               text={$t('select_featured_photo')}
               icon={mdiAccountBoxOutline}
-              onClick={() => (viewMode = ViewMode.SELECT_PERSON)}
+              onClick={() => (viewMode = PersonPageViewMode.SELECT_PERSON)}
             />
             <MenuOption
               text={person.isHidden ? $t('unhide_person') : $t('hide_person')}
@@ -416,38 +425,49 @@
             <MenuOption
               text={$t('set_date_of_birth')}
               icon={mdiCalendarEditOutline}
-              onClick={() => (viewMode = ViewMode.BIRTH_DATE)}
+              onClick={() => (viewMode = PersonPageViewMode.BIRTH_DATE)}
             />
             <MenuOption
               text={$t('merge_people')}
               icon={mdiAccountMultipleCheckOutline}
-              onClick={() => (viewMode = ViewMode.MERGE_PEOPLE)}
+              onClick={() => (viewMode = PersonPageViewMode.MERGE_PEOPLE)}
             />
           </ButtonContextMenu>
-        </svelte:fragment>
+        {/snippet}
       </ControlAppBar>
     {/if}
 
-    {#if viewMode === ViewMode.SELECT_PERSON}
-      <ControlAppBar on:close={() => (viewMode = ViewMode.VIEW_ASSETS)}>
-        <svelte:fragment slot="leading">{$t('select_featured_photo')}</svelte:fragment>
+    {#if viewMode === PersonPageViewMode.SELECT_PERSON}
+      <ControlAppBar onClose={() => (viewMode = PersonPageViewMode.VIEW_ASSETS)}>
+        {#snippet leading()}
+          {$t('select_featured_photo')}
+        {/snippet}
       </ControlAppBar>
     {/if}
   {/if}
 </header>
 
-<main class="relative h-screen overflow-hidden bg-immich-bg tall:ml-4 pt-[var(--navbar-height)] dark:bg-immich-dark-bg">
+<main
+  class="relative h-screen overflow-hidden bg-immich-bg tall:ml-4 pt-[var(--navbar-height)] dark:bg-immich-dark-bg"
+  use:scrollMemoryClearer={{
+    routeStartsWith: AppRoute.PEOPLE,
+    beforeClear: () => {
+      sessionStorage.removeItem(SessionStorageKey.INFINITE_SCROLL_PAGE);
+    },
+  }}
+>
   {#key person.id}
     <AssetGrid
       enableRouting={true}
+      {person}
       {assetStore}
-      {assetInteractionStore}
-      isSelectionMode={viewMode === ViewMode.SELECT_PERSON}
-      singleSelect={viewMode === ViewMode.SELECT_PERSON}
-      on:select={({ detail: asset }) => handleSelectFeaturePhoto(asset)}
-      on:escape={handleEscape}
+      {assetInteraction}
+      isSelectionMode={viewMode === PersonPageViewMode.SELECT_PERSON}
+      singleSelect={viewMode === PersonPageViewMode.SELECT_PERSON}
+      onSelect={handleSelectFeaturePhoto}
+      onEscape={handleEscape}
     >
-      {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE || viewMode === ViewMode.BIRTH_DATE}
+      {#if viewMode === PersonPageViewMode.VIEW_ASSETS || viewMode === PersonPageViewMode.SUGGEST_MERGE || viewMode === PersonPageViewMode.BIRTH_DATE}
         <!-- Person information block -->
         <div
           class="relative w-fit p-4 sm:px-6"
@@ -464,7 +484,7 @@
                 bind:suggestedPeople
                 name={person.name}
                 bind:isSearchingPeople
-                on:change={(event) => handleNameChange(event.detail)}
+                onChange={handleNameChange}
                 {thumbnailData}
               />
             {:else}
@@ -473,7 +493,7 @@
                   type="button"
                   class="flex items-center justify-center"
                   title={$t('edit_name')}
-                  on:click={() => (isEditingName = true)}
+                  onclick={() => (isEditingName = true)}
                 >
                   <ImageThumbnail
                     circle
@@ -486,17 +506,10 @@
                   <div
                     class="flex flex-col justify-center text-left px-4 h-14 text-immich-primary dark:text-immich-dark-primary"
                   >
-                    {#if person.name}
-                      <p class="w-40 sm:w-72 font-medium truncate">{person.name}</p>
-                      <p class="absolute w-fit text-sm text-gray-500 dark:text-immich-gray bottom-0">
-                        {$t('assets_count', { values: { count: numberOfAssets } })}
-                      </p>
-                    {:else}
-                      <p class="font-medium">{$t('add_a_name')}</p>
-                      <p class="text-sm text-gray-500 dark:text-immich-gray">
-                        {$t('find_them_fast')}
-                      </p>
-                    {/if}
+                    <p class="w-40 sm:w-72 font-medium truncate">{person.name || $t('add_a_name')}</p>
+                    <p class="absolute w-fit text-sm text-gray-500 dark:text-immich-gray bottom-0">
+                      {$t('assets_count', { values: { count: numberOfAssets } })}
+                    </p>
                   </div>
                 </button>
               </div>
@@ -517,11 +530,11 @@
                   {#each suggestedPeople as person, index (person.id)}
                     <button
                       type="button"
-                      class="flex w-full border-t border-gray-400 dark:border-immich-dark-gray h-14 place-items-center bg-gray-200 p-2 dark:bg-gray-700 hover:bg-gray-300 hover:dark:bg-[#232932] focus:bg-gray-300 focus:dark:bg-[#232932] {index ===
+                      class="flex w-full border border-gray-200 dark:border-immich-dark-gray h-14 place-items-center bg-gray-100 p-2 dark:bg-gray-700 hover:bg-gray-300 hover:dark:bg-[#232932] focus:bg-gray-300 focus:dark:bg-[#232932] {index ===
                       suggestedPeople.length - 1
                         ? 'rounded-b-lg border-b'
                         : ''}"
-                      on:click={() => handleSuggestPeople(person)}
+                      onclick={() => handleSuggestPeople(person)}
                     >
                       <ImageThumbnail
                         circle
diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte
index 4649da8205..b485dd532b 100644
--- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte
@@ -19,7 +19,7 @@
   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
   import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
   import { AssetAction } from '$lib/constants';
-  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
+  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
   import { AssetStore } from '$lib/stores/assets.store';
   import { preferences, user } from '$lib/stores/user.store';
@@ -32,33 +32,25 @@
 
   let { isViewing: showAssetViewer } = assetViewingStore;
   const assetStore = new AssetStore({ isArchived: false, withStacked: true, withPartners: true });
-  const assetInteractionStore = createAssetInteractionStore();
-  const { isMultiSelectState, selectedAssets } = assetInteractionStore;
+  const assetInteraction = new AssetInteraction();
 
-  let isAllFavorite: boolean;
-  let isAllOwned: boolean;
-  let isAssetStackSelected: boolean;
-  let isLinkActionAvailable: boolean;
-
-  $: {
-    const selection = [...$selectedAssets];
-    isAllOwned = selection.every((asset) => asset.ownerId === $user.id);
-    isAllFavorite = selection.every((asset) => asset.isFavorite);
-    isAssetStackSelected = selection.length === 1 && !!selection[0].stack;
-    const isLivePhoto = selection.length === 1 && !!selection[0].livePhotoVideoId;
+  let selectedAssets = $derived(assetInteraction.selectedAssetsArray);
+  let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack);
+  let isLinkActionAvailable = $derived.by(() => {
+    const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId;
     const isLivePhotoCandidate =
-      selection.length === 2 &&
-      selection.some((asset) => asset.type === AssetTypeEnum.Image) &&
-      selection.some((asset) => asset.type === AssetTypeEnum.Image);
-    isLinkActionAvailable = isAllOwned && (isLivePhoto || isLivePhotoCandidate);
-  }
+      selectedAssets.length === 2 &&
+      selectedAssets.some((asset) => asset.type === AssetTypeEnum.Image) &&
+      selectedAssets.some((asset) => asset.type === AssetTypeEnum.Video);
 
+    return assetInteraction.isAllUserOwned && (isLivePhoto || isLivePhotoCandidate);
+  });
   const handleEscape = () => {
     if ($showAssetViewer) {
       return;
     }
-    if ($isMultiSelectState) {
-      assetInteractionStore.clearMultiselect();
+    if (assetInteraction.selectionActive) {
+      assetInteraction.clearMultiselect();
       return;
     }
   };
@@ -78,22 +70,22 @@
   });
 </script>
 
-{#if $isMultiSelectState}
+{#if assetInteraction.selectionActive}
   <AssetSelectControlBar
     ownerId={$user.id}
-    assets={$selectedAssets}
-    clearSelect={() => assetInteractionStore.clearMultiselect()}
+    assets={assetInteraction.selectedAssets}
+    clearSelect={() => assetInteraction.clearMultiselect()}
   >
     <CreateSharedLink />
-    <SelectAllAssets {assetStore} {assetInteractionStore} />
+    <SelectAllAssets {assetStore} {assetInteraction} />
     <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
       <AddToAlbum />
       <AddToAlbum shared />
     </ButtonContextMenu>
-    <FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
+    <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
     <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
       <DownloadAction menuItem />
-      {#if $selectedAssets.size > 1 || isAssetStackSelected}
+      {#if assetInteraction.selectedAssets.size > 1 || isAssetStackSelected}
         <StackAction
           unstack={isAssetStackSelected}
           onStack={(assetIds) => assetStore.removeAssets(assetIds)}
@@ -103,7 +95,7 @@
       {#if isLinkActionAvailable}
         <LinkLivePhotoAction
           menuItem
-          unlink={[...$selectedAssets].length === 1}
+          unlink={assetInteraction.selectedAssets.size === 1}
           onLink={handleLink}
           onUnlink={handleUnlink}
         />
@@ -121,18 +113,20 @@
   </AssetSelectControlBar>
 {/if}
 
-<UserPageLayout hideNavbar={$isMultiSelectState} showUploadButton scrollbar={false}>
+<UserPageLayout hideNavbar={assetInteraction.selectionActive} showUploadButton scrollbar={false}>
   <AssetGrid
     enableRouting={true}
     {assetStore}
-    {assetInteractionStore}
+    {assetInteraction}
     removeAction={AssetAction.ARCHIVE}
-    on:escape={handleEscape}
+    onEscape={handleEscape}
     withStacked
   >
     {#if $preferences.memories.enabled}
       <MemoryLane />
     {/if}
-    <EmptyPlaceholder text={$t('no_assets_message')} onClick={() => openFileUploadDialog()} slot="empty" />
+    {#snippet empty()}
+      <EmptyPlaceholder text={$t('no_assets_message')} onClick={() => openFileUploadDialog()} />
+    {/snippet}
   </AssetGrid>
 </UserPageLayout>
diff --git a/web/src/routes/(user)/places/+page.svelte b/web/src/routes/(user)/places/+page.svelte
index 28c8e95cb1..1808755482 100644
--- a/web/src/routes/(user)/places/+page.svelte
+++ b/web/src/routes/(user)/places/+page.svelte
@@ -9,7 +9,11 @@
   import { t } from 'svelte-i18n';
   import { getAssetThumbnailUrl } from '$lib/utils';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
+
+  let { data }: Props = $props();
 
   type AssetWithCity = AssetResponseDto & {
     exifInfo: {
@@ -17,10 +21,10 @@
     };
   };
 
-  $: places = data.items.filter((item): item is AssetWithCity => !!item.exifInfo?.city);
-  $: hasPlaces = places.length > 0;
+  let places = $derived(data.items.filter((item): item is AssetWithCity => !!item.exifInfo?.city));
+  let hasPlaces = $derived(places.length > 0);
 
-  let innerHeight: number;
+  let innerHeight: number = $state(0);
 </script>
 
 <svelte:window bind:innerHeight />
diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte
index da85eb49c8..fe4a7a6612 100644
--- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -15,6 +15,7 @@
   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
   import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
   import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
+  import { cancelMultiselect } from '$lib/utils/asset-utils';
   import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
   import { AppRoute, QueryParameter } from '$lib/constants';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
@@ -22,8 +23,8 @@
   import { shortcut } from '$lib/actions/shortcut';
   import {
     type AssetResponseDto,
+    searchAssets,
     searchSmart,
-    searchMetadata,
     getPerson,
     type SmartSearchDto,
     type MetadataSearchDto,
@@ -40,31 +41,48 @@
   import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte';
   import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation';
   import { t } from 'svelte-i18n';
-  import { afterUpdate, tick } from 'svelte';
+  import { onMount, tick } from 'svelte';
+  import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
+  import { preferences } from '$lib/stores/user.store';
+  import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
+  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 
   const MAX_ASSET_COUNT = 5000;
   let { isViewing: showAssetViewer } = assetViewingStore;
-  const viewport: Viewport = { width: 0, height: 0 };
+  const viewport: Viewport = $state({ width: 0, height: 0 });
 
   // The GalleryViewer pushes it's own history state, which causes weird
   // behavior for history.back(). To prevent that we store the previous page
   // manually and navigate back to that.
-  let previousRoute = AppRoute.EXPLORE as string;
+  let previousRoute = $state(AppRoute.EXPLORE as string);
 
   let nextPage: number | null = 1;
-  let searchResultAlbums: AlbumResponseDto[] = [];
-  let searchResultAssets: AssetResponseDto[] = [];
-  let isLoading = true;
-  let scrollY = 0;
+  let searchResultAlbums: AlbumResponseDto[] = $state([]);
+  let searchResultAssets: AssetResponseDto[] = $state([]);
+  let isLoading = $state(true);
+  let scrollY = $state(0);
   let scrollYHistory = 0;
 
+  const assetInteraction = new AssetInteraction();
+
+  type SearchTerms = MetadataSearchDto & Pick<SmartSearchDto, 'query'>;
+  let searchQuery = $derived($page.url.searchParams.get(QueryParameter.QUERY));
+
+  onMount(() => {
+    if (terms && $featureFlags.loaded) {
+      handlePromiseError(onSearchQueryUpdate());
+    }
+  });
+
+  let terms = $derived(searchQuery ? JSON.parse(searchQuery) : {});
+
   const onEscape = () => {
     if ($showAssetViewer) {
       return;
     }
 
-    if (isMultiSelectionMode) {
-      selectedAssets = new Set();
+    if (assetInteraction.selectionActive) {
+      assetInteraction.selectedAssets.clear();
       return;
     }
     if (!$preventRaceConditionSearchBar) {
@@ -73,8 +91,7 @@
     $preventRaceConditionSearchBar = false;
   };
 
-  // save and restore scroll position
-  afterUpdate(() => {
+  $effect(() => {
     if (scrollY) {
       scrollYHistory = scrollY;
     }
@@ -104,29 +121,14 @@
       });
   });
 
-  let selectedAssets: Set<AssetResponseDto> = new Set();
-  $: isMultiSelectionMode = selectedAssets.size > 0;
-  $: isAllArchived = [...selectedAssets].every((asset) => asset.isArchived);
-  $: isAllFavorite = [...selectedAssets].every((asset) => asset.isFavorite);
-
   const onAssetDelete = (assetIds: string[]) => {
     const assetIdSet = new Set(assetIds);
     searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id));
   };
   const handleSelectAll = () => {
-    selectedAssets = new Set(searchResultAssets);
+    assetInteraction.selectAssets(searchResultAssets);
   };
 
-  type SearchTerms = MetadataSearchDto & Pick<SmartSearchDto, 'query'>;
-
-  $: searchQuery = $page.url.searchParams.get(QueryParameter.QUERY);
-  let terms: SearchTerms;
-  $: terms = searchQuery ? JSON.parse(searchQuery) : {};
-
-  $: if (terms && $featureFlags.loaded) {
-    handlePromiseError(onSearchQueryUpdate());
-  }
-
   async function onSearchQueryUpdate() {
     nextPage = 1;
     searchResultAssets = [];
@@ -151,12 +153,10 @@
       const { albums, assets } =
         'query' in searchDto && $featureFlags.smartSearch
           ? await searchSmart({ smartSearchDto: searchDto })
-          : await searchMetadata({ metadataSearchDto: searchDto });
+          : await searchAssets({ metadataSearchDto: searchDto });
 
       searchResultAlbums.push(...albums.items);
       searchResultAssets.push(...assets.items);
-      searchResultAlbums = searchResultAlbums;
-      searchResultAssets = searchResultAssets;
 
       nextPage = assets.nextPage ? Number(assets.nextPage) : null;
     } catch (error) {
@@ -216,6 +216,13 @@
 
   const triggerAssetUpdate = () => (searchResultAssets = searchResultAssets);
 
+  const onAddToAlbum = (assetIds: string[]) => {
+    if (terms.isNotInAlbum.toString() == 'true') {
+      const assetIdSet = new Set(assetIds);
+      searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id));
+    }
+  };
+
   function getObjectKeys<T extends object>(obj: T): (keyof T)[] {
     return Object.keys(obj) as (keyof T)[];
   }
@@ -224,69 +231,84 @@
 <svelte:window use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onEscape }} bind:scrollY />
 
 <section>
-  {#if isMultiSelectionMode}
+  {#if assetInteraction.selectionActive}
     <div class="fixed z-[100] top-0 left-0 w-full">
-      <AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
+      <AssetSelectControlBar
+        assets={assetInteraction.selectedAssets}
+        clearSelect={() => cancelMultiselect(assetInteraction)}
+      >
         <CreateSharedLink />
-        <CircleIconButton title={$t('select_all')} icon={mdiSelectAll} on:click={handleSelectAll} />
+        <CircleIconButton title={$t('select_all')} icon={mdiSelectAll} onclick={handleSelectAll} />
         <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
-          <AddToAlbum />
-          <AddToAlbum shared />
+          <AddToAlbum {onAddToAlbum} />
+          <AddToAlbum shared {onAddToAlbum} />
         </ButtonContextMenu>
-        <FavoriteAction removeFavorite={isAllFavorite} onFavorite={triggerAssetUpdate} />
+        <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={triggerAssetUpdate} />
 
         <ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}>
           <DownloadAction menuItem />
           <ChangeDate menuItem />
           <ChangeLocation menuItem />
-          <ArchiveAction menuItem unarchive={isAllArchived} onArchive={triggerAssetUpdate} />
+          <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} onArchive={triggerAssetUpdate} />
+          {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
+            <TagAction menuItem />
+          {/if}
           <DeleteAssets menuItem {onAssetDelete} />
+          <hr />
+          <AssetJobActions />
         </ButtonContextMenu>
       </AssetSelectControlBar>
     </div>
   {:else}
     <div class="fixed z-[100] top-0 left-0 w-full">
-      <ControlAppBar on:close={() => goto(previousRoute)} backIcon={mdiArrowLeft}>
+      <ControlAppBar onClose={() => goto(previousRoute)} backIcon={mdiArrowLeft}>
         <div class="w-full flex-1 pl-4">
-          <SearchBar grayTheme={false} value={terms.query ?? ''} searchQuery={terms} />
+          <SearchBar
+            grayTheme={false}
+            value={terms?.query ?? ''}
+            searchQuery={terms}
+            onSearch={() => handlePromiseError(onSearchQueryUpdate())}
+          />
         </div>
       </ControlAppBar>
     </div>
   {/if}
 </section>
 
-<section
-  id="search-chips"
-  class="mt-24 text-center w-full flex gap-5 place-content-center place-items-center flex-wrap px-24"
->
-  {#each getObjectKeys(terms) as key (key)}
-    {@const value = terms[key]}
-    <div class="flex place-content-center place-items-center text-xs">
-      <div
-        class="bg-immich-primary py-2 px-4 text-white dark:text-black dark:bg-immich-dark-primary
+{#if terms}
+  <section
+    id="search-chips"
+    class="mt-24 text-center w-full flex gap-5 place-content-center place-items-center flex-wrap px-24"
+  >
+    {#each getObjectKeys(terms) as key (key)}
+      {@const value = terms[key]}
+      <div class="flex place-content-center place-items-center text-xs">
+        <div
+          class="bg-immich-primary py-2 px-4 text-white dark:text-black dark:bg-immich-dark-primary
           {value === true ? 'rounded-full' : 'rounded-tl-full rounded-bl-full'}"
-      >
-        {getHumanReadableSearchKey(key)}
-      </div>
-
-      {#if value !== true}
-        <div class="bg-gray-300 py-2 px-4 dark:bg-gray-800 dark:text-white rounded-tr-full rounded-br-full">
-          {#if (key === 'takenAfter' || key === 'takenBefore') && typeof value === 'string'}
-            {getHumanReadableDate(value)}
-          {:else if key === 'personIds' && Array.isArray(value)}
-            {#await getPersonName(value) then personName}
-              {personName}
-            {/await}
-          {:else if value === null || value === ''}
-            {$t('unknown')}
-          {:else}
-            {value}
-          {/if}
+        >
+          {getHumanReadableSearchKey(key as keyof SearchTerms)}
         </div>
-      {/if}
-    </div>
-  {/each}
-</section>
+
+        {#if value !== true}
+          <div class="bg-gray-300 py-2 px-4 dark:bg-gray-800 dark:text-white rounded-tr-full rounded-br-full">
+            {#if (key === 'takenAfter' || key === 'takenBefore') && typeof value === 'string'}
+              {getHumanReadableDate(value)}
+            {:else if key === 'personIds' && Array.isArray(value)}
+              {#await getPersonName(value) then personName}
+                {personName}
+              {/await}
+            {:else if value === null || value === ''}
+              {$t('unknown')}
+            {:else}
+              {value}
+            {/if}
+          </div>
+        {/if}
+      </div>
+    {/each}
+  </section>
+{/if}
 
 <section
   class="relative mb-12 bg-immich-bg dark:bg-immich-dark-bg m-4"
@@ -308,7 +330,7 @@
       {#if searchResultAssets.length > 0}
         <GalleryViewer
           assets={searchResultAssets}
-          bind:selectedAssets
+          {assetInteraction}
           onIntersected={loadNextPage}
           showArchiveIcon={true}
           {viewport}
diff --git a/web/src/routes/(user)/share/[key]/+error.svelte b/web/src/routes/(user)/share/[key]/+error.svelte
index 4065d86586..3e1a156571 100644
--- a/web/src/routes/(user)/share/[key]/+error.svelte
+++ b/web/src/routes/(user)/share/[key]/+error.svelte
@@ -1,5 +1,5 @@
 <script lang="ts">
-  import { page } from '$app/stores';
+  import { page } from '$app/state';
 </script>
 
 <svelte:head>
@@ -8,7 +8,7 @@
 
 <section class="flex flex-col px-4 h-screen w-screen place-content-center place-items-center">
   <h1 class="py-10 text-4xl text-immich-primary dark:text-immich-dark-primary">Page not found :/</h1>
-  {#if $page.error?.message}
-    <h2 class="text-xl text-immich-fg dark:text-immich-dark-fg">{$page.error.message}</h2>
+  {#if page.error?.message}
+    <h2 class="text-xl text-immich-fg dark:text-immich-dark-fg">{page.error.message}</h2>
   {/if}
 </section>
diff --git a/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index f4fac282ba..dfe465f94d 100644
--- a/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -15,21 +15,24 @@
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
   import { tick } from 'svelte';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
+
+  let { data }: Props = $props();
 
   let { gridScrollTarget } = assetViewingStore;
-  let { sharedLink, passwordRequired, sharedLinkKey: key, meta } = data;
-  let { title, description } = meta;
-  let isOwned = $user ? $user.id === sharedLink?.userId : false;
-  let password = '';
-  let innerWidth: number;
+  let { sharedLink, passwordRequired, sharedLinkKey: key, meta } = $state(data);
+  let { title, description } = $state(meta);
+  let isOwned = $derived($user ? $user.id === sharedLink?.userId : false);
+  let password = $state('');
+  let innerWidth: number = $state(0);
 
   const handlePasswordSubmit = async () => {
     try {
       sharedLink = await getMySharedLink({ password, key });
       setSharedLink(sharedLink);
       passwordRequired = false;
-      isOwned = $user ? $user.id === sharedLink.userId : false;
       title = (sharedLink.album ? sharedLink.album.albumName : $t('public_share')) + ' - Immich';
       description =
         sharedLink.description ||
@@ -43,6 +46,11 @@
       handleError(error, $t('errors.unable_to_get_shared_link'));
     }
   };
+
+  const onsubmit = async (event: Event) => {
+    event.preventDefault();
+    await handlePasswordSubmit();
+  };
 </script>
 
 <svelte:window bind:innerWidth />
@@ -54,13 +62,13 @@
 {#if passwordRequired}
   <header>
     <ControlAppBar showBackButton={false}>
-      <svelte:fragment slot="leading">
+      {#snippet leading()}
         <ImmichLogoSmallLink width={innerWidth} />
-      </svelte:fragment>
+      {/snippet}
 
-      <svelte:fragment slot="trailing">
+      {#snippet trailing()}
         <ThemeButton />
-      </svelte:fragment>
+      {/snippet}
     </ControlAppBar>
   </header>
   <main
@@ -72,7 +80,7 @@
         {$t('sharing_enter_password')}
       </div>
       <div class="mt-4">
-        <form novalidate autocomplete="off" on:submit|preventDefault={handlePasswordSubmit}>
+        <form novalidate autocomplete="off" {onsubmit}>
           <input type="password" class="immich-form-input mr-2" placeholder={$t('password')} bind:value={password} />
           <Button type="submit">{$t('submit')}</Button>
         </form>
diff --git a/web/src/routes/(user)/sharing/+page.svelte b/web/src/routes/(user)/sharing/+page.svelte
index 35279a02db..1e59a2720d 100644
--- a/web/src/routes/(user)/sharing/+page.svelte
+++ b/web/src/routes/(user)/sharing/+page.svelte
@@ -20,7 +20,11 @@
   import Albums from '$lib/components/album-page/albums-list.svelte';
   import { t } from 'svelte-i18n';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
+
+  let { data }: Props = $props();
 
   const settings: AlbumViewSettings = {
     view: AlbumViewMode.Cover,
@@ -34,21 +38,23 @@
 </script>
 
 <UserPageLayout title={data.meta.title}>
-  <div class="flex" slot="buttons">
-    <LinkButton on:click={() => createAlbumAndRedirect()}>
-      <div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm">
-        <Icon path={mdiPlusBoxOutline} size="18" class="shrink-0" />
-        <span class="leading-none max-sm:text-xs">{$t('create_album')}</span>
-      </div>
-    </LinkButton>
+  {#snippet buttons()}
+    <div class="flex">
+      <LinkButton onclick={() => createAlbumAndRedirect()}>
+        <div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm">
+          <Icon path={mdiPlusBoxOutline} size="18" class="shrink-0" />
+          <span class="leading-none max-sm:text-xs">{$t('create_album')}</span>
+        </div>
+      </LinkButton>
 
-    <LinkButton href={AppRoute.SHARED_LINKS}>
-      <div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm">
-        <Icon path={mdiLink} size="18" class="shrink-0" />
-        <span class="leading-none max-sm:text-xs">{$t('shared_links')}</span>
-      </div>
-    </LinkButton>
-  </div>
+      <LinkButton href={AppRoute.SHARED_LINKS}>
+        <div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm">
+          <Icon path={mdiLink} size="18" class="shrink-0" />
+          <span class="leading-none max-sm:text-xs">{$t('shared_links')}</span>
+        </div>
+      </LinkButton>
+    </div>
+  {/snippet}
 
   <div class="flex flex-col">
     {#if data.partners.length > 0}
@@ -89,7 +95,9 @@
         <!-- Shared Album List -->
         <Albums sharedAlbums={data.sharedAlbums} userSettings={settings} showOwner>
           <!-- Empty List -->
-          <EmptyPlaceholder slot="empty" text={$t('no_shared_albums_message')} src={empty2Url} />
+          {#snippet empty()}
+            <EmptyPlaceholder text={$t('no_shared_albums_message')} src={empty2Url} />
+          {/snippet}
         </Albums>
       </div>
     </div>
diff --git a/web/src/routes/(user)/sharing/sharedlinks/+page.svelte b/web/src/routes/(user)/sharing/sharedlinks/+page.svelte
index 5e934143df..b7d4da2941 100644
--- a/web/src/routes/(user)/sharing/sharedlinks/+page.svelte
+++ b/web/src/routes/(user)/sharing/sharedlinks/+page.svelte
@@ -1,5 +1,5 @@
 <script lang="ts">
-  import { goto } from '$app/navigation';
+  import { goto, afterNavigate } from '$app/navigation';
   import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
   import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
   import {
@@ -15,8 +15,8 @@
   import { dialogController } from '$lib/components/shared-components/dialog/dialog';
   import { t } from 'svelte-i18n';
 
-  let sharedLinks: SharedLinkResponseDto[] = [];
-  let editSharedLink: SharedLinkResponseDto | null = null;
+  let sharedLinks: SharedLinkResponseDto[] = $state([]);
+  let editSharedLink: SharedLinkResponseDto | null = $state(null);
 
   const refresh = async () => {
     sharedLinks = await getAllSharedLinks();
@@ -50,10 +50,19 @@
     await refresh();
     editSharedLink = null;
   };
+
+  let backUrl: string = AppRoute.SHARING;
+
+  afterNavigate(({ from }) => {
+    let url: string | undefined = from?.url?.pathname;
+    backUrl = url || AppRoute.SHARING;
+  });
 </script>
 
-<ControlAppBar backIcon={mdiArrowLeft} on:close={() => goto(AppRoute.SHARING)}>
-  <svelte:fragment slot="leading">{$t('shared_links')}</svelte:fragment>
+<ControlAppBar backIcon={mdiArrowLeft} onClose={() => goto(backUrl)}>
+  {#snippet leading()}
+    {$t('shared_links')}
+  {/snippet}
 </ControlAppBar>
 
 <section class="mt-[120px] flex flex-col pb-[120px] container max-w-screen-lg mx-auto px-3">
diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte
index ce91abb451..d66eab9870 100644
--- a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -11,14 +11,11 @@
     notificationController,
     NotificationType,
   } from '$lib/components/shared-components/notification/notification';
-  import SettingInputField, {
-    SettingInputFieldType,
-  } from '$lib/components/shared-components/settings/setting-input-field.svelte';
+  import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
   import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte';
   import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte';
   import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
-  import { AppRoute, AssetAction, QueryParameter } from '$lib/constants';
-  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
+  import { AppRoute, AssetAction, QueryParameter, SettingInputFieldType } from '$lib/constants';
   import { AssetStore } from '$lib/stores/assets.store';
   import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
   import { deleteTag, getAllTags, updateTag, upsertTags, type TagResponseDto } from '@immich/sdk';
@@ -28,13 +25,18 @@
   import { dialogController } from '$lib/components/shared-components/dialog/dialog';
   import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte';
   import SkipLink from '$lib/components/elements/buttons/skip-link.svelte';
+  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
 
-  $: pathSegments = data.path ? data.path.split('/') : [];
-  $: currentPath = $page.url.searchParams.get(QueryParameter.PATH) || '';
+  let { data }: Props = $props();
 
-  const assetInteractionStore = createAssetInteractionStore();
+  let pathSegments = $derived(data.path ? data.path.split('/') : []);
+  let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || '');
+
+  const assetInteraction = new AssetInteraction();
 
   const buildMap = (tags: TagResponseDto[]) => {
     return Object.fromEntries(tags.map((tag) => [tag.value, tag]));
@@ -42,21 +44,26 @@
 
   const assetStore = new AssetStore({});
 
-  $: tags = data.tags;
-  $: tagsMap = buildMap(tags);
-  $: tag = currentPath ? tagsMap[currentPath] : null;
-  $: tagId = tag?.id;
-  $: tree = buildTree(tags.map((tag) => tag.value));
-  $: {
+  let tags = $state<TagResponseDto[]>([]);
+  $effect(() => {
+    tags = data.tags;
+  });
+
+  let tagsMap = $derived(buildMap(tags));
+  let tag = $derived(currentPath ? tagsMap[currentPath] : null);
+  let tagId = $derived(tag?.id);
+  let tree = $derived(buildTree(tags.map((tag) => tag.value)));
+
+  $effect.pre(() => {
     void assetStore.updateOptions({ tagId });
-  }
+  });
 
   const handleNavigation = async (tag: string) => {
     await navigateToView(normalizeTreePath(`${data.path || ''}/${tag}`));
   };
 
   const getLink = (path: string) => {
-    const url = new URL(AppRoute.TAGS, window.location.href);
+    const url = new URL(AppRoute.TAGS, globalThis.location.href);
     if (path) {
       url.searchParams.set(QueryParameter.PATH, path);
     }
@@ -67,15 +74,15 @@
 
   const navigateToView = (path: string) => goto(getLink(path));
 
-  let isNewOpen = false;
-  let newTagValue = '';
+  let isNewOpen = $state(false);
+  let newTagValue = $state('');
   const handleCreate = () => {
     newTagValue = tag ? tag.value + '/' : '';
     isNewOpen = true;
   };
 
-  let isEditOpen = false;
-  let newTagColor = '';
+  let isEditOpen = $state(false);
+  let newTagColor = $state('');
   const handleEdit = () => {
     newTagColor = tag?.color ?? '';
     isEditOpen = true;
@@ -135,49 +142,66 @@
     const parentPath = pathSegments.slice(0, -1).join('/');
     await navigateToView(parentPath);
   };
+
+  const onsubmit = async (event: Event) => {
+    event.preventDefault();
+    await handleSubmit();
+  };
 </script>
 
 <UserPageLayout title={data.meta.title} scrollbar={false}>
-  <SideBarSection slot="sidebar">
-    <SkipLink target={`#${headerId}`} text={$t('skip_to_tags')} />
+  {#snippet sidebar()}
+    <SideBarSection>
+      <SkipLink target={`#${headerId}`} text={$t('skip_to_tags')} />
+      <section>
+        <div class="text-xs pl-4 mb-2 dark:text-white">{$t('explorer').toUpperCase()}</div>
+        <div class="h-full">
+          <TreeItems
+            icons={{ default: mdiTag, active: mdiTag }}
+            items={tree}
+            active={currentPath}
+            {getLink}
+            {getColor}
+          />
+        </div>
+      </section>
+    </SideBarSection>
+  {/snippet}
+
+  {#snippet buttons()}
     <section>
-      <div class="text-xs pl-4 mb-2 dark:text-white">{$t('explorer').toUpperCase()}</div>
-      <div class="h-full">
-        <TreeItems icons={{ default: mdiTag, active: mdiTag }} items={tree} active={currentPath} {getLink} {getColor} />
-      </div>
+      <LinkButton onclick={handleCreate}>
+        <div class="flex place-items-center gap-2 text-sm">
+          <Icon path={mdiPlus} size="18" />
+          <p class="hidden md:block">{$t('create_tag')}</p>
+        </div>
+      </LinkButton>
+
+      {#if pathSegments.length > 0 && tag}
+        <LinkButton onclick={handleEdit}>
+          <div class="flex place-items-center gap-2 text-sm">
+            <Icon path={mdiPencil} size="18" />
+            <p class="hidden md:block">{$t('edit_tag')}</p>
+          </div>
+        </LinkButton>
+        <LinkButton onclick={handleDelete}>
+          <div class="flex place-items-center gap-2 text-sm">
+            <Icon path={mdiTrashCanOutline} size="18" />
+            <p class="hidden md:block">{$t('delete_tag')}</p>
+          </div>
+        </LinkButton>
+      {/if}
     </section>
-  </SideBarSection>
-
-  <section slot="buttons">
-    <LinkButton on:click={handleCreate}>
-      <div class="flex place-items-center gap-2 text-sm">
-        <Icon path={mdiPlus} size="18" />
-        <p class="hidden md:block">{$t('create_tag')}</p>
-      </div>
-    </LinkButton>
-
-    {#if pathSegments.length > 0 && tag}
-      <LinkButton on:click={handleEdit}>
-        <div class="flex place-items-center gap-2 text-sm">
-          <Icon path={mdiPencil} size="18" />
-          <p class="hidden md:block">{$t('edit_tag')}</p>
-        </div>
-      </LinkButton>
-      <LinkButton on:click={handleDelete}>
-        <div class="flex place-items-center gap-2 text-sm">
-          <Icon path={mdiTrashCanOutline} size="18" />
-          <p class="hidden md:block">{$t('delete_tag')}</p>
-        </div>
-      </LinkButton>
-    {/if}
-  </section>
+  {/snippet}
 
   <Breadcrumbs {pathSegments} icon={mdiTagMultiple} title={$t('tags')} {getLink} />
 
   <section class="mt-2 h-full">
     {#if tag}
-      <AssetGrid enableRouting={true} {assetStore} {assetInteractionStore} removeAction={AssetAction.UNARCHIVE}>
-        <TreeItemThumbnails items={data.children} icon={mdiTag} onClick={handleNavigation} slot="empty" />
+      <AssetGrid enableRouting={true} {assetStore} {assetInteraction} removeAction={AssetAction.UNARCHIVE}>
+        {#snippet empty()}
+          <TreeItemThumbnails items={data.children} icon={mdiTag} onClick={handleNavigation} />
+        {/snippet}
       </AssetGrid>
     {:else}
       <TreeItemThumbnails items={Object.keys(tree)} icon={mdiTag} onClick={handleNavigation} />
@@ -193,7 +217,7 @@
       </p>
     </div>
 
-    <form on:submit|preventDefault={handleSubmit} autocomplete="off" id="create-tag-form">
+    <form {onsubmit} autocomplete="off" id="create-tag-form">
       <div class="my-4 flex flex-col gap-2">
         <SettingInputField
           inputType={SettingInputFieldType.TEXT}
@@ -204,16 +228,17 @@
         />
       </div>
     </form>
-    <svelte:fragment slot="sticky-bottom">
-      <Button color="gray" fullwidth on:click={() => handleCancel()}>{$t('cancel')}</Button>
+
+    {#snippet stickyBottom()}
+      <Button color="gray" fullwidth onclick={() => handleCancel()}>{$t('cancel')}</Button>
       <Button type="submit" fullwidth form="create-tag-form">{$t('create')}</Button>
-    </svelte:fragment>
+    {/snippet}
   </FullScreenModal>
 {/if}
 
 {#if isEditOpen}
   <FullScreenModal title={$t('edit_tag')} icon={mdiTag} onClose={handleCancel}>
-    <form on:submit|preventDefault={handleSubmit} autocomplete="off" id="edit-tag-form">
+    <form {onsubmit} autocomplete="off" id="edit-tag-form">
       <div class="my-4 flex flex-col gap-2">
         <SettingInputField
           inputType={SettingInputFieldType.COLOR}
@@ -222,9 +247,10 @@
         />
       </div>
     </form>
-    <svelte:fragment slot="sticky-bottom">
-      <Button color="gray" fullwidth on:click={() => handleCancel()}>{$t('cancel')}</Button>
+
+    {#snippet stickyBottom()}
+      <Button color="gray" fullwidth onclick={() => handleCancel()}>{$t('cancel')}</Button>
       <Button type="submit" fullwidth form="edit-tag-form">{$t('save')}</Button>
-    </svelte:fragment>
+    {/snippet}
   </FullScreenModal>
 {/if}
diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 27ad5bb3f0..7f97d3772b 100644
--- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -15,7 +15,6 @@
     notificationController,
   } from '$lib/components/shared-components/notification/notification';
   import { AppRoute } from '$lib/constants';
-  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
   import { AssetStore } from '$lib/stores/assets.store';
   import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
   import { handleError } from '$lib/utils/handle-error';
@@ -26,16 +25,21 @@
   import { dialogController } from '$lib/components/shared-components/dialog/dialog';
   import { t } from 'svelte-i18n';
   import { onDestroy } from 'svelte';
+  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
+
+  let { data }: Props = $props();
 
   if (!$featureFlags.trash) {
     handlePromiseError(goto(AppRoute.PHOTOS));
   }
 
-  const assetStore = new AssetStore({ isTrashed: true });
-  const assetInteractionStore = createAssetInteractionStore();
-  const { isMultiSelectState, selectedAssets } = assetInteractionStore;
+  const options = { isTrashed: true };
+  const assetStore = new AssetStore(options);
+  const assetInteraction = new AssetInteraction();
 
   const handleEmptyTrash = async () => {
     const isConfirmed = await dialogController.show({
@@ -47,16 +51,15 @@
     }
 
     try {
-      await emptyTrash();
-
-      const deletedAssetIds = assetStore.assets.map((a) => a.id);
-      const numberOfAssets = deletedAssetIds.length;
-      assetStore.removeAssets(deletedAssetIds);
+      const { count } = await emptyTrash();
 
       notificationController.show({
-        message: $t('assets_permanently_deleted_count', { values: { count: numberOfAssets } }),
+        message: $t('assets_permanently_deleted_count', { values: { count } }),
         type: NotificationType.Info,
       });
+
+      // reset asset grid (TODO fix in asset store that it should reset when it is empty)
+      await assetStore.updateOptions(options);
     } catch (error) {
       handleError(error, $t('errors.unable_to_empty_trash'));
     }
@@ -71,16 +74,14 @@
       return;
     }
     try {
-      await restoreTrash();
-
-      const restoredAssetIds = assetStore.assets.map((a) => a.id);
-      const numberOfAssets = restoredAssetIds.length;
-      assetStore.removeAssets(restoredAssetIds);
-
+      const { count } = await restoreTrash();
       notificationController.show({
-        message: $t('assets_restored_count', { values: { count: numberOfAssets } }),
+        message: $t('assets_restored_count', { values: { count } }),
         type: NotificationType.Info,
       });
+
+      // reset asset grid (TODO fix in asset store that it should reset when it is empty)
+      await assetStore.updateOptions(options);
     } catch (error) {
       handleError(error, $t('errors.unable_to_restore_trash'));
     }
@@ -91,36 +92,43 @@
   });
 </script>
 
-{#if $isMultiSelectState}
-  <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
-    <SelectAllAssets {assetStore} {assetInteractionStore} />
+{#if assetInteraction.selectionActive}
+  <AssetSelectControlBar
+    assets={assetInteraction.selectedAssets}
+    clearSelect={() => assetInteraction.clearMultiselect()}
+  >
+    <SelectAllAssets {assetStore} {assetInteraction} />
     <DeleteAssets force onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
     <RestoreAssets onRestore={(assetIds) => assetStore.removeAssets(assetIds)} />
   </AssetSelectControlBar>
 {/if}
 
 {#if $featureFlags.loaded && $featureFlags.trash}
-  <UserPageLayout hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={false}>
-    <div class="flex place-items-center gap-2" slot="buttons">
-      <LinkButton on:click={handleRestoreTrash} disabled={$isMultiSelectState}>
-        <div class="flex place-items-center gap-2 text-sm">
-          <Icon path={mdiHistory} size="18" />
-          {$t('restore_all')}
-        </div>
-      </LinkButton>
-      <LinkButton on:click={() => handleEmptyTrash()} disabled={$isMultiSelectState}>
-        <div class="flex place-items-center gap-2 text-sm">
-          <Icon path={mdiDeleteForeverOutline} size="18" />
-          {$t('empty_trash')}
-        </div>
-      </LinkButton>
-    </div>
+  <UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
+    {#snippet buttons()}
+      <div class="flex place-items-center gap-2">
+        <LinkButton onclick={handleRestoreTrash} disabled={assetInteraction.selectionActive}>
+          <div class="flex place-items-center gap-2 text-sm">
+            <Icon path={mdiHistory} size="18" />
+            {$t('restore_all')}
+          </div>
+        </LinkButton>
+        <LinkButton onclick={() => handleEmptyTrash()} disabled={assetInteraction.selectionActive}>
+          <div class="flex place-items-center gap-2 text-sm">
+            <Icon path={mdiDeleteForeverOutline} size="18" />
+            {$t('empty_trash')}
+          </div>
+        </LinkButton>
+      </div>
+    {/snippet}
 
-    <AssetGrid enableRouting={true} {assetStore} {assetInteractionStore}>
+    <AssetGrid enableRouting={true} {assetStore} {assetInteraction}>
       <p class="font-medium text-gray-500/60 dark:text-gray-300/60 p-4">
         {$t('trashed_items_will_be_permanently_deleted_after', { values: { days: $serverConfig.trashDays } })}
       </p>
-      <EmptyPlaceholder text={$t('trash_no_results_message')} src={empty3Url} slot="empty" />
+      {#snippet empty()}
+        <EmptyPlaceholder text={$t('trash_no_results_message')} src={empty3Url} />
+      {/snippet}
     </AssetGrid>
   </UserPageLayout>
 {/if}
diff --git a/web/src/routes/(user)/user-settings/+page.svelte b/web/src/routes/(user)/user-settings/+page.svelte
index ea302454b2..53cc661a30 100644
--- a/web/src/routes/(user)/user-settings/+page.svelte
+++ b/web/src/routes/(user)/user-settings/+page.svelte
@@ -7,18 +7,22 @@
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import { t } from 'svelte-i18n';
 
-  export let data: PageData;
-  export let isShowKeyboardShortcut = false;
+  interface Props {
+    data: PageData;
+    isShowKeyboardShortcut?: boolean;
+  }
+
+  let { data, isShowKeyboardShortcut = $bindable(false) }: Props = $props();
 </script>
 
 <UserPageLayout title={data.meta.title}>
-  <svelte:fragment slot="buttons">
+  {#snippet buttons()}
     <CircleIconButton
       icon={mdiKeyboard}
       title={$t('show_keyboard_shortcuts')}
-      on:click={() => (isShowKeyboardShortcut = !isShowKeyboardShortcut)}
+      onclick={() => (isShowKeyboardShortcut = !isShowKeyboardShortcut)}
     />
-  </svelte:fragment>
+  {/snippet}
   <section class="mx-4 flex place-content-center">
     <div class="w-full max-w-3xl">
       <UserSettingsList keys={data.keys} sessions={data.sessions} />
@@ -27,5 +31,5 @@
 </UserPageLayout>
 
 {#if isShowKeyboardShortcut}
-  <ShowShortcuts on:close={() => (isShowKeyboardShortcut = false)} />
+  <ShowShortcuts onClose={() => (isShowKeyboardShortcut = false)} />
 {/if}
diff --git a/web/src/routes/(user)/utilities/+page.svelte b/web/src/routes/(user)/utilities/+page.svelte
index bf18b99436..6713fe4a4b 100644
--- a/web/src/routes/(user)/utilities/+page.svelte
+++ b/web/src/routes/(user)/utilities/+page.svelte
@@ -3,7 +3,11 @@
   import type { PageData } from './$types';
   import UtilitiesMenu from '$lib/components/utilities-page/utilities-menu.svelte';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
+
+  let { data }: Props = $props();
 </script>
 
 <UserPageLayout title={data.meta.title}>
diff --git a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 34889261d5..fd2bcb438c 100644
--- a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -22,8 +22,12 @@
   import Icon from '$lib/components/elements/icon.svelte';
   import { locale } from '$lib/stores/preferences.store';
 
-  export let data: PageData;
-  export let isShowKeyboardShortcut = false;
+  interface Props {
+    data: PageData;
+    isShowKeyboardShortcut?: boolean;
+  }
+
+  let { data = $bindable(), isShowKeyboardShortcut = $bindable(false) }: Props = $props();
 
   interface Shortcuts {
     general: ExplainedShortcut[];
@@ -42,12 +46,12 @@
       { key: ['s'], action: $t('view') },
       { key: ['d'], action: $t('unselect_all_duplicates') },
       { key: ['⇧', 'c'], action: $t('resolve_duplicates') },
-      { key: ['⇧', 'c'], action: $t('stack_duplicates') },
+      { key: ['⇧', 's'], action: $t('stack_duplicates') },
     ],
   };
 
-  $: hasDuplicates = data.duplicates.length > 0;
-
+  let duplicates = $state(data.duplicates);
+  let hasDuplicates = $derived(duplicates.length > 0);
   const withConfirmation = async (callback: () => Promise<void>, prompt?: string, confirmText?: string) => {
     if (prompt && confirmText) {
       const isConfirmed = await dialogController.show({ prompt, confirmText });
@@ -82,7 +86,7 @@
         await deleteAssets({ assetBulkDeleteDto: { ids: trashIds, force: !$featureFlags.trash } });
         await updateAssets({ assetBulkUpdateDto: { ids: duplicateAssetIds, duplicateId: null } });
 
-        data.duplicates = data.duplicates.filter((duplicate) => duplicate.duplicateId !== duplicateId);
+        duplicates = duplicates.filter((duplicate) => duplicate.duplicateId !== duplicateId);
 
         deletedNotification(trashIds.length);
       },
@@ -95,14 +99,12 @@
     await stackAssets(assets, false);
     const duplicateAssetIds = assets.map((asset) => asset.id);
     await updateAssets({ assetBulkUpdateDto: { ids: duplicateAssetIds, duplicateId: null } });
-    data.duplicates = data.duplicates.filter((duplicate) => duplicate.duplicateId !== duplicateId);
+    duplicates = duplicates.filter((duplicate) => duplicate.duplicateId !== duplicateId);
   };
 
   const handleDeduplicateAll = async () => {
-    const idsToKeep = data.duplicates
-      .map((group) => suggestDuplicateByFileSize(group.assets))
-      .map((asset) => asset?.id);
-    const idsToDelete = data.duplicates.flatMap((group, i) =>
+    const idsToKeep = duplicates.map((group) => suggestDuplicateByFileSize(group.assets)).map((asset) => asset?.id);
+    const idsToDelete = duplicates.flatMap((group, i) =>
       group.assets.map((asset) => asset.id).filter((asset) => asset !== idsToKeep[i]),
     );
 
@@ -125,7 +127,7 @@
           },
         });
 
-        data.duplicates = [];
+        duplicates = [];
 
         deletedNotification(idsToDelete.length);
       },
@@ -135,12 +137,12 @@
   };
 
   const handleKeepAll = async () => {
-    const ids = data.duplicates.flatMap((group) => group.assets.map((asset) => asset.id));
+    const ids = duplicates.flatMap((group) => group.assets.map((asset) => asset.id));
     return withConfirmation(
       async () => {
         await updateAssets({ assetBulkUpdateDto: { ids, duplicateId: null } });
 
-        data.duplicates = [];
+        duplicates = [];
 
         notificationController.show({
           message: $t('resolved_all_duplicates'),
@@ -153,38 +155,40 @@
   };
 </script>
 
-<UserPageLayout title={data.meta.title + ` (${data.duplicates.length.toLocaleString($locale)})`} scrollbar={true}>
-  <div class="flex place-items-center gap-2" slot="buttons">
-    <LinkButton on:click={() => handleDeduplicateAll()} disabled={!hasDuplicates}>
-      <div class="flex place-items-center gap-2 text-sm">
-        <Icon path={mdiTrashCanOutline} size="18" />
-        {$t('deduplicate_all')}
-      </div>
-    </LinkButton>
-    <LinkButton on:click={() => handleKeepAll()} disabled={!hasDuplicates}>
-      <div class="flex place-items-center gap-2 text-sm">
-        <Icon path={mdiCheckOutline} size="18" />
-        {$t('keep_all')}
-      </div>
-    </LinkButton>
-    <CircleIconButton
-      icon={mdiKeyboard}
-      title={$t('show_keyboard_shortcuts')}
-      on:click={() => (isShowKeyboardShortcut = !isShowKeyboardShortcut)}
-    />
-  </div>
+<UserPageLayout title={data.meta.title + ` (${duplicates.length.toLocaleString($locale)})`} scrollbar={true}>
+  {#snippet buttons()}
+    <div class="flex place-items-center gap-2">
+      <LinkButton onclick={() => handleDeduplicateAll()} disabled={!hasDuplicates}>
+        <div class="flex place-items-center gap-2 text-sm">
+          <Icon path={mdiTrashCanOutline} size="18" />
+          {$t('deduplicate_all')}
+        </div>
+      </LinkButton>
+      <LinkButton onclick={() => handleKeepAll()} disabled={!hasDuplicates}>
+        <div class="flex place-items-center gap-2 text-sm">
+          <Icon path={mdiCheckOutline} size="18" />
+          {$t('keep_all')}
+        </div>
+      </LinkButton>
+      <CircleIconButton
+        icon={mdiKeyboard}
+        title={$t('show_keyboard_shortcuts')}
+        onclick={() => (isShowKeyboardShortcut = !isShowKeyboardShortcut)}
+      />
+    </div>
+  {/snippet}
 
   <div class="mt-4">
-    {#if data.duplicates && data.duplicates.length > 0}
+    {#if duplicates && duplicates.length > 0}
       <div class="mb-4 text-sm dark:text-white">
         <p>{$t('duplicates_description')}</p>
       </div>
-      {#key data.duplicates[0].duplicateId}
+      {#key duplicates[0].duplicateId}
         <DuplicatesCompareControl
-          assets={data.duplicates[0].assets}
+          assets={duplicates[0].assets}
           onResolve={(duplicateAssetIds, trashIds) =>
-            handleResolve(data.duplicates[0].duplicateId, duplicateAssetIds, trashIds)}
-          onStack={(assets) => handleStack(data.duplicates[0].duplicateId, assets)}
+            handleResolve(duplicates[0].duplicateId, duplicateAssetIds, trashIds)}
+          onStack={(assets) => handleStack(duplicates[0].duplicateId, assets)}
         />
       {/key}
     {:else}
@@ -196,5 +200,5 @@
 </UserPageLayout>
 
 {#if isShowKeyboardShortcut}
-  <ShowShortcuts shortcuts={duplicateShortcuts} on:close={() => (isShowKeyboardShortcut = false)} />
+  <ShowShortcuts shortcuts={duplicateShortcuts} onClose={() => (isShowKeyboardShortcut = false)} />
 {/if}
diff --git a/web/src/routes/+error.svelte b/web/src/routes/+error.svelte
index 23e8fd3ff1..95f28e1539 100644
--- a/web/src/routes/+error.svelte
+++ b/web/src/routes/+error.svelte
@@ -1,6 +1,6 @@
 <script>
-  import { page } from '$app/stores';
+  import { page } from '$app/state';
   import Error from '$lib/components/error.svelte';
 </script>
 
-<Error error={$page.error}></Error>
+<Error error={page.error}></Error>
diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte
index 8f7f372efd..9fdf1b5ec0 100644
--- a/web/src/routes/+layout.svelte
+++ b/web/src/routes/+layout.svelte
@@ -1,6 +1,8 @@
 <script lang="ts">
+  import { run } from 'svelte/legacy';
+
   import { afterNavigate, beforeNavigate } from '$app/navigation';
-  import { page } from '$app/stores';
+  import { page } from '$app/state';
   import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
   import AppleHeader from '$lib/components/shared-components/apple-header.svelte';
   import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
@@ -16,26 +18,24 @@
   import { user } from '$lib/stores/user.store';
   import { closeWebsocketConnection, openWebsocketConnection } from '$lib/stores/websocket';
   import { copyToClipboard, setKey } from '$lib/utils';
-  import { onDestroy, onMount } from 'svelte';
+  import { onDestroy, onMount, type Snippet } from 'svelte';
   import '../app.css';
   import { isAssetViewerRoute, isSharedLinkRoute } from '$lib/utils/navigation';
   import DialogWrapper from '$lib/components/shared-components/dialog/dialog-wrapper.svelte';
   import { t } from 'svelte-i18n';
   import Error from '$lib/components/error.svelte';
   import { shortcut } from '$lib/actions/shortcut';
-
-  let showNavigationLoadingBar = false;
-  $: changeTheme($colorTheme);
-
-  $: if ($user) {
-    openWebsocketConnection();
-  } else {
-    closeWebsocketConnection();
+  interface Props {
+    children?: Snippet;
   }
 
+  let { children }: Props = $props();
+
+  let showNavigationLoadingBar = $state(false);
+
   const changeTheme = (theme: ThemeSetting) => {
     if (theme.system) {
-      theme.value = window.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.DARK : Theme.LIGHT;
+      theme.value = globalThis.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.DARK : Theme.LIGHT;
     }
 
     if (theme.value === Theme.LIGHT) {
@@ -52,22 +52,22 @@
   };
 
   const getMyImmichLink = () => {
-    return new URL($page.url.pathname + $page.url.search, 'https://my.immich.app');
+    return new URL(page.url.pathname + page.url.search, 'https://my.immich.app');
   };
 
   onMount(() => {
     const element = document.querySelector('#stencil');
     element?.remove();
     // if the browser theme changes, changes the Immich theme too
-    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', handleChangeTheme);
+    globalThis.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', handleChangeTheme);
   });
 
   onDestroy(() => {
     document.removeEventListener('change', handleChangeTheme);
   });
 
-  if (isSharedLinkRoute($page.route?.id)) {
-    setKey($page.params.key);
+  if (isSharedLinkRoute(page.route?.id)) {
+    setKey(page.params.key);
   }
 
   beforeNavigate(({ from, to }) => {
@@ -82,36 +82,46 @@
   afterNavigate(() => {
     showNavigationLoadingBar = false;
   });
+  run(() => {
+    changeTheme($colorTheme);
+  });
+  run(() => {
+    if ($user) {
+      openWebsocketConnection();
+    } else {
+      closeWebsocketConnection();
+    }
+  });
 </script>
 
 <svelte:head>
-  <title>{$page.data.meta?.title || 'Web'} - Immich</title>
-  <link rel="manifest" href="/manifest.json" />
+  <title>{page.data.meta?.title || 'Web'} - Immich</title>
+  <link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
   <meta name="theme-color" content="currentColor" />
   <AppleHeader />
 
-  {#if $page.data.meta}
-    <meta name="description" content={$page.data.meta.description} />
+  {#if page.data.meta}
+    <meta name="description" content={page.data.meta.description} />
 
     <!-- Facebook Meta Tags -->
     <meta property="og:type" content="website" />
-    <meta property="og:title" content={$page.data.meta.title} />
-    <meta property="og:description" content={$page.data.meta.description} />
-    {#if $page.data.meta.imageUrl}
+    <meta property="og:title" content={page.data.meta.title} />
+    <meta property="og:description" content={page.data.meta.description} />
+    {#if page.data.meta.imageUrl}
       <meta
         property="og:image"
-        content={new URL($page.data.meta.imageUrl, $serverConfig.externalDomain || window.location.origin).href}
+        content={new URL(page.data.meta.imageUrl, $serverConfig.externalDomain || globalThis.location.origin).href}
       />
     {/if}
 
     <!-- Twitter Meta Tags -->
     <meta name="twitter:card" content="summary_large_image" />
-    <meta name="twitter:title" content={$page.data.meta.title} />
-    <meta name="twitter:description" content={$page.data.meta.description} />
-    {#if $page.data.meta.imageUrl}
+    <meta name="twitter:title" content={page.data.meta.title} />
+    <meta name="twitter:description" content={page.data.meta.description} />
+    {#if page.data.meta.imageUrl}
       <meta
         name="twitter:image"
-        content={new URL($page.data.meta.imageUrl, $serverConfig.externalDomain || window.location.origin).href}
+        content={new URL(page.data.meta.imageUrl, $serverConfig.externalDomain || globalThis.location.origin).href}
       />
     {/if}
   {/if}
@@ -132,10 +142,10 @@
   }}
 />
 
-{#if $page.data.error}
-  <Error error={$page.data.error}></Error>
+{#if page.data.error}
+  <Error error={page.data.error}></Error>
 {:else}
-  <slot />
+  {@render children?.()}
 {/if}
 
 {#if showNavigationLoadingBar}
diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte
index dcd6630a01..b323a136aa 100644
--- a/web/src/routes/admin/jobs-status/+page.svelte
+++ b/web/src/routes/admin/jobs-status/+page.svelte
@@ -3,19 +3,32 @@
   import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
   import Icon from '$lib/components/elements/icon.svelte';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
+  import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte';
+  import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
+  import {
+    notificationController,
+    NotificationType,
+  } from '$lib/components/shared-components/notification/notification';
   import { AppRoute } from '$lib/constants';
   import { asyncTimeout } from '$lib/utils';
-  import { getAllJobsStatus, type AllJobStatusResponseDto } from '@immich/sdk';
-  import { mdiCog } from '@mdi/js';
+  import { handleError } from '$lib/utils/handle-error';
+  import { createJob, getAllJobsStatus, ManualJobName, type AllJobStatusResponseDto } from '@immich/sdk';
+  import { mdiCog, mdiPlus } from '@mdi/js';
   import { onDestroy, onMount } from 'svelte';
   import { t } from 'svelte-i18n';
   import type { PageData } from './$types';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
 
-  let jobs: AllJobStatusResponseDto;
+  let { data }: Props = $props();
+
+  let jobs: AllJobStatusResponseDto | undefined = $state();
 
   let running = true;
+  let isOpen = $state(false);
+  let selectedJob: ComboBoxOption | undefined = $state(undefined);
 
   onMount(async () => {
     while (running) {
@@ -27,17 +40,52 @@
   onDestroy(() => {
     running = false;
   });
+
+  const options = [
+    { title: $t('admin.person_cleanup_job'), value: ManualJobName.PersonCleanup },
+    { title: $t('admin.tag_cleanup_job'), value: ManualJobName.TagCleanup },
+    { title: $t('admin.user_cleanup_job'), value: ManualJobName.UserCleanup },
+  ].map(({ value, title }) => ({ id: value, label: title, value }));
+
+  const handleCancel = () => (isOpen = false);
+
+  const handleCreate = async () => {
+    if (!selectedJob) {
+      return;
+    }
+
+    try {
+      await createJob({ jobCreateDto: { name: selectedJob.value as ManualJobName } });
+      notificationController.show({ message: $t('admin.job_created'), type: NotificationType.Info });
+      handleCancel();
+    } catch (error) {
+      handleError(error, $t('errors.unable_to_submit_job'));
+    }
+  };
+
+  const onsubmit = async (event: Event) => {
+    event.preventDefault();
+    await handleCreate();
+  };
 </script>
 
 <UserPageLayout title={data.meta.title} admin>
-  <div class="flex justify-end" slot="buttons">
-    <LinkButton href="{AppRoute.ADMIN_SETTINGS}?isOpen=job">
-      <div class="flex place-items-center gap-2 text-sm">
-        <Icon path={mdiCog} size="18" />
-        {$t('admin.manage_concurrency')}
-      </div>
-    </LinkButton>
-  </div>
+  {#snippet buttons()}
+    <div class="flex justify-end">
+      <LinkButton onclick={() => (isOpen = true)}>
+        <div class="flex place-items-center gap-2 text-sm">
+          <Icon path={mdiPlus} size="18" />
+          {$t('admin.create_job')}
+        </div>
+      </LinkButton>
+      <LinkButton href="{AppRoute.ADMIN_SETTINGS}?isOpen=job">
+        <div class="flex place-items-center gap-2 text-sm">
+          <Icon path={mdiCog} size="18" />
+          {$t('admin.manage_concurrency')}
+        </div>
+      </LinkButton>
+    </div>
+  {/snippet}
   <section id="setting-content" class="flex place-content-center sm:mx-4">
     <section class="w-full pb-28 sm:w-5/6 md:w-[850px]">
       {#if jobs}
@@ -46,3 +94,26 @@
     </section>
   </section>
 </UserPageLayout>
+
+{#if isOpen}
+  <ConfirmDialog
+    confirmColor="primary"
+    title={$t('admin.create_job')}
+    disabled={!selectedJob}
+    onConfirm={handleCreate}
+    onCancel={handleCancel}
+  >
+    {#snippet promptSnippet()}
+      <form {onsubmit} autocomplete="off" id="create-tag-form" class="w-full">
+        <div class="flex flex-col gap-1 text-left">
+          <Combobox
+            bind:selectedOption={selectedJob}
+            label={$t('jobs')}
+            {options}
+            placeholder={$t('admin.search_jobs')}
+          />
+        </div>
+      </form>
+    {/snippet}
+  </ConfirmDialog>
+{/if}
diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte
index 74db5628ba..b89e81ebf6 100644
--- a/web/src/routes/admin/library-management/+page.svelte
+++ b/web/src/routes/admin/library-management/+page.svelte
@@ -20,7 +20,6 @@
     getAllLibraries,
     getLibraryStatistics,
     getUserAdmin,
-    removeOfflineFiles,
     scanLibrary,
     updateLibrary,
     type LibraryResponseDto,
@@ -37,32 +36,36 @@
   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
   import { locale } from '$lib/stores/preferences.store';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
 
-  let libraries: LibraryResponseDto[] = [];
+  let { data }: Props = $props();
+
+  let libraries: LibraryResponseDto[] = $state([]);
 
   let stats: LibraryStatsResponseDto[] = [];
-  let owner: UserResponseDto[] = [];
+  let owner: UserResponseDto[] = $state([]);
   let photos: number[] = [];
   let videos: number[] = [];
-  let totalCount: number[] = [];
-  let diskUsage: number[] = [];
-  let diskUsageUnit: ByteUnit[] = [];
-  let editImportPaths: number | null;
-  let editScanSettings: number | null;
-  let renameLibrary: number | null;
+  let totalCount: number[] = $state([]);
+  let diskUsage: number[] = $state([]);
+  let diskUsageUnit: ByteUnit[] = $state([]);
+  let editImportPaths: number | undefined = $state();
+  let editScanSettings: number | undefined = $state();
+  let renameLibrary: number | undefined = $state();
   let updateLibraryIndex: number | null;
   let dropdownOpen: boolean[] = [];
-  let toCreateLibrary = false;
+  let toCreateLibrary = $state(false);
 
   onMount(async () => {
     await readLibraryList();
   });
 
   const closeAll = () => {
-    editImportPaths = null;
-    editScanSettings = null;
-    renameLibrary = null;
+    editImportPaths = undefined;
+    editScanSettings = undefined;
+    renameLibrary = undefined;
     updateLibraryIndex = null;
 
     for (let index = 0; index < dropdownOpen.length; index++) {
@@ -122,7 +125,7 @@
   const handleScanAll = async () => {
     try {
       for (const library of libraries) {
-        await scanLibrary({ id: library.id, scanLibraryDto: {} });
+        await scanLibrary({ id: library.id });
       }
       notificationController.show({
         message: $t('admin.refreshing_all_libraries'),
@@ -135,9 +138,9 @@
 
   const handleScan = async (libraryId: string) => {
     try {
-      await scanLibrary({ id: libraryId, scanLibraryDto: {} });
+      await scanLibrary({ id: libraryId });
       notificationController.show({
-        message: $t('admin.scanning_library_for_new_files'),
+        message: $t('admin.scanning_library'),
         type: NotificationType.Info,
       });
     } catch (error) {
@@ -145,42 +148,6 @@
     }
   };
 
-  const handleScanChanges = async (libraryId: string) => {
-    try {
-      await scanLibrary({ id: libraryId, scanLibraryDto: { refreshModifiedFiles: true } });
-      notificationController.show({
-        message: $t('admin.scanning_library_for_changed_files'),
-        type: NotificationType.Info,
-      });
-    } catch (error) {
-      handleError(error, $t('errors.unable_to_scan_library'));
-    }
-  };
-
-  const handleForceScan = async (libraryId: string) => {
-    try {
-      await scanLibrary({ id: libraryId, scanLibraryDto: { refreshAllFiles: true } });
-      notificationController.show({
-        message: $t('admin.forcing_refresh_library_files'),
-        type: NotificationType.Info,
-      });
-    } catch (error) {
-      handleError(error, $t('errors.unable_to_scan_library'));
-    }
-  };
-
-  const handleRemoveOffline = async (libraryId: string) => {
-    try {
-      await removeOfflineFiles({ id: libraryId });
-      notificationController.show({
-        message: $t('admin.removing_offline_files'),
-        type: NotificationType.Info,
-      });
-    } catch (error) {
-      handleError(error, $t('errors.unable_to_remove_offline_files'));
-    }
-  };
-
   const onRenameClicked = (index: number) => {
     closeAll();
     renameLibrary = index;
@@ -193,7 +160,7 @@
     updateLibraryIndex = index;
   };
 
-  const onScanNewLibraryClicked = async (library: LibraryResponseDto) => {
+  const onScanClicked = async (library: LibraryResponseDto) => {
     closeAll();
 
     if (library) {
@@ -207,27 +174,6 @@
     updateLibraryIndex = index;
   };
 
-  const onScanAllLibraryFilesClicked = async (library: LibraryResponseDto) => {
-    closeAll();
-    if (library) {
-      await handleScanChanges(library.id);
-    }
-  };
-
-  const onForceScanAllLibraryFilesClicked = async (library: LibraryResponseDto) => {
-    closeAll();
-    if (library) {
-      await handleForceScan(library.id);
-    }
-  };
-
-  const onRemoveOfflineFilesClicked = async (library: LibraryResponseDto) => {
-    closeAll();
-    if (library) {
-      await handleRemoveOffline(library.id);
-    }
-  };
-
   const handleDelete = async (library: LibraryResponseDto, index: number) => {
     closeAll();
 
@@ -267,29 +213,28 @@
 </script>
 
 {#if toCreateLibrary}
-  <LibraryUserPickerForm
-    on:submit={({ detail }) => handleCreate(detail.ownerId)}
-    on:cancel={() => (toCreateLibrary = false)}
-  />
+  <LibraryUserPickerForm onSubmit={handleCreate} onCancel={() => (toCreateLibrary = false)} />
 {/if}
 
 <UserPageLayout title={data.meta.title} admin>
-  <div class="flex justify-end gap-2" slot="buttons">
-    {#if libraries.length > 0}
-      <LinkButton on:click={() => handleScanAll()}>
+  {#snippet buttons()}
+    <div class="flex justify-end gap-2">
+      {#if libraries.length > 0}
+        <LinkButton onclick={() => handleScanAll()}>
+          <div class="flex gap-1 text-sm">
+            <Icon path={mdiSync} size="18" />
+            <span>{$t('scan_all_libraries')}</span>
+          </div>
+        </LinkButton>
+      {/if}
+      <LinkButton onclick={() => (toCreateLibrary = true)}>
         <div class="flex gap-1 text-sm">
-          <Icon path={mdiSync} size="18" />
-          <span>{$t('scan_all_libraries')}</span>
+          <Icon path={mdiPlusBoxOutline} size="18" />
+          <span>{$t('create_library')}</span>
         </div>
       </LinkButton>
-    {/if}
-    <LinkButton on:click={() => (toCreateLibrary = true)}>
-      <div class="flex gap-1 text-sm">
-        <Icon path={mdiPlusBoxOutline} size="18" />
-        <span>{$t('create_library')}</span>
-      </div>
-    </LinkButton>
-  </div>
+    </div>
+  {/snippet}
   <section class="my-4">
     <div class="flex flex-col gap-2" in:fade={{ duration: 500 }}>
       {#if libraries.length > 0}
@@ -303,7 +248,7 @@
               <th class="text-center text-sm font-medium">{$t('owner')}</th>
               <th class="text-center text-sm font-medium">{$t('assets')}</th>
               <th class="text-center text-sm font-medium">{$t('size')}</th>
-              <th class="text-center text-sm font-medium" />
+              <th class="text-center text-sm font-medium"></th>
             </tr>
           </thead>
           <tbody class="block overflow-y-auto rounded-md border dark:border-immich-dark-gray">
@@ -354,59 +299,44 @@
                     icon={mdiDotsVertical}
                     title={$t('library_options')}
                   >
+                    <MenuOption onClick={() => onScanClicked(library)} text={$t('scan_library')} />
+                    <hr />
                     <MenuOption onClick={() => onRenameClicked(index)} text={$t('rename')} />
                     <MenuOption onClick={() => onEditImportPathClicked(index)} text={$t('edit_import_paths')} />
                     <MenuOption onClick={() => onScanSettingClicked(index)} text={$t('scan_settings')} />
                     <hr />
-                    <MenuOption onClick={() => onScanNewLibraryClicked(library)} text={$t('scan_new_library_files')} />
                     <MenuOption
-                      onClick={() => onScanAllLibraryFilesClicked(library)}
-                      text={$t('scan_all_library_files')}
-                      subtitle={$t('only_refreshes_modified_files')}
-                    />
-                    <MenuOption
-                      onClick={() => onForceScanAllLibraryFilesClicked(library)}
-                      text={$t('force_re-scan_library_files')}
-                      subtitle={$t('refreshes_every_file')}
-                    />
-                    <hr />
-                    <MenuOption
-                      onClick={() => onRemoveOfflineFilesClicked(library)}
-                      text={$t('remove_offline_files')}
-                    />
-                    <MenuOption
-                      text={$t('delete_library')}
+                      onClick={() => handleDelete(library, index)}
                       activeColor="bg-red-200"
                       textColor="text-red-600"
-                      onClick={() => handleDelete(library, index)}
+                      text={$t('delete_library')}
                     />
                   </ButtonContextMenu>
                 </td>
               </tr>
               {#if renameLibrary === index}
+                <!-- svelte-ignore node_invalid_placement_ssr -->
                 <div transition:slide={{ duration: 250 }}>
-                  <LibraryRenameForm
-                    {library}
-                    on:submit={({ detail }) => handleUpdate(detail)}
-                    on:cancel={() => (renameLibrary = null)}
-                  />
+                  <LibraryRenameForm {library} onSubmit={handleUpdate} onCancel={() => (renameLibrary = undefined)} />
                 </div>
               {/if}
               {#if editImportPaths === index}
+                <!-- svelte-ignore node_invalid_placement_ssr -->
                 <div transition:slide={{ duration: 250 }}>
                   <LibraryImportPathsForm
                     {library}
-                    on:submit={({ detail }) => handleUpdate(detail)}
-                    on:cancel={() => (editImportPaths = null)}
+                    onSubmit={handleUpdate}
+                    onCancel={() => (editImportPaths = undefined)}
                   />
                 </div>
               {/if}
               {#if editScanSettings === index}
+                <!-- svelte-ignore node_invalid_placement_ssr -->
                 <div transition:slide={{ duration: 250 }} class="mb-4 ml-4 mr-4">
                   <LibraryScanSettingsForm
                     {library}
-                    on:submit={({ detail: library }) => handleUpdate(library)}
-                    on:cancel={() => (editScanSettings = null)}
+                    onSubmit={handleUpdate}
+                    onCancel={() => (editScanSettings = undefined)}
                   />
                 </div>
               {/if}
diff --git a/web/src/routes/admin/repair/+page.svelte b/web/src/routes/admin/repair/+page.svelte
index e8cb0649c2..9f19fddd03 100644
--- a/web/src/routes/admin/repair/+page.svelte
+++ b/web/src/routes/admin/repair/+page.svelte
@@ -19,7 +19,11 @@
   import { t } from 'svelte-i18n';
   import { locale } from '$lib/stores/preferences.store';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
+
+  let { data }: Props = $props();
 
   interface UntrackedFile {
     filename: string;
@@ -33,12 +37,12 @@
 
   const normalize = (filenames: string[]) => filenames.map((filename) => ({ filename, checksum: null }));
 
-  let checking = false;
-  let repairing = false;
+  let checking = $state(false);
+  let repairing = $state(false);
 
-  let orphans: FileReportItemDto[] = data.orphans;
-  let extras: UntrackedFile[] = normalize(data.extras);
-  let matches: Match[] = [];
+  let orphans: FileReportItemDto[] = $state(data.orphans);
+  let extras: UntrackedFile[] = $state(normalize(data.extras));
+  let matches: Match[] = $state([]);
 
   const handleDownload = () => {
     if (extras.length > 0) {
@@ -180,33 +184,34 @@
 </script>
 
 <UserPageLayout title={data.meta.title} admin>
-  <svelte:fragment slot="sidebar" />
-  <div class="flex justify-end gap-2" slot="buttons">
-    <LinkButton on:click={() => handleRepair()} disabled={matches.length === 0 || repairing}>
-      <div class="flex place-items-center gap-2 text-sm">
-        <Icon path={mdiWrench} size="18" />
-        {$t('admin.repair_all')}
-      </div>
-    </LinkButton>
-    <LinkButton on:click={() => handleCheckAll()} disabled={extras.length === 0 || checking}>
-      <div class="flex place-items-center gap-2 text-sm">
-        <Icon path={mdiCheckAll} size="18" />
-        {$t('admin.check_all')}
-      </div>
-    </LinkButton>
-    <LinkButton on:click={() => handleDownload()} disabled={extras.length + orphans.length === 0}>
-      <div class="flex place-items-center gap-2 text-sm">
-        <Icon path={mdiDownload} size="18" />
-        {$t('export')}
-      </div>
-    </LinkButton>
-    <LinkButton on:click={() => handleRefresh()}>
-      <div class="flex place-items-center gap-2 text-sm">
-        <Icon path={mdiRefresh} size="18" />
-        {$t('refresh')}
-      </div>
-    </LinkButton>
-  </div>
+  {#snippet buttons()}
+    <div class="flex justify-end gap-2">
+      <LinkButton onclick={() => handleRepair()} disabled={matches.length === 0 || repairing}>
+        <div class="flex place-items-center gap-2 text-sm">
+          <Icon path={mdiWrench} size="18" />
+          {$t('admin.repair_all')}
+        </div>
+      </LinkButton>
+      <LinkButton onclick={() => handleCheckAll()} disabled={extras.length === 0 || checking}>
+        <div class="flex place-items-center gap-2 text-sm">
+          <Icon path={mdiCheckAll} size="18" />
+          {$t('admin.check_all')}
+        </div>
+      </LinkButton>
+      <LinkButton onclick={() => handleDownload()} disabled={extras.length + orphans.length === 0}>
+        <div class="flex place-items-center gap-2 text-sm">
+          <Icon path={mdiDownload} size="18" />
+          {$t('export')}
+        </div>
+      </LinkButton>
+      <LinkButton onclick={() => handleRefresh()}>
+        <div class="flex place-items-center gap-2 text-sm">
+          <Icon path={mdiRefresh} size="18" />
+          {$t('refresh')}
+        </div>
+      </LinkButton>
+    </div>
+  {/snippet}
   <section id="setting-content" class="flex place-content-center sm:mx-4">
     <section class="w-full pb-28 sm:w-5/6 md:w-[850px]">
       {#if matches.length + extras.length + orphans.length === 0}
@@ -238,7 +243,7 @@
                 <tr
                   class="w-full h-[75px] place-items-center border-[3px] border-transparent p-2 odd:bg-immich-gray even:bg-immich-bg hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5 flex justify-between"
                   tabindex="0"
-                  on:click={() => handleSplit(match)}
+                  onclick={() => handleSplit(match)}
                 >
                   <td class="text-sm text-ellipsis flex flex-col gap-1 font-mono">
                     <span>{match.orphan.pathValue} =></span>
@@ -279,8 +284,8 @@
                   tabindex="0"
                   title={orphan.pathValue}
                 >
-                  <td on:click={() => copyToClipboard(orphan.pathValue)}>
-                    <CircleIconButton title={$t('copy_file_path')} icon={mdiContentCopy} size="18" />
+                  <td onclick={() => copyToClipboard(orphan.pathValue)}>
+                    <CircleIconButton title={$t('copy_file_path')} icon={mdiContentCopy} size="18" onclick={() => {}} />
                   </td>
                   <td class="truncate text-sm font-mono text-left" title={orphan.pathValue}>
                     {orphan.pathValue}
@@ -318,11 +323,11 @@
                 <tr
                   class="flex h-[50px] w-full place-items-center border-[3px] border-transparent p-1 odd:bg-immich-gray even:bg-immich-bg hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5 justify-between"
                   tabindex="0"
-                  on:click={() => handleCheckOne(extra.filename)}
+                  onclick={() => handleCheckOne(extra.filename)}
                   title={extra.filename}
                 >
-                  <td on:click={() => copyToClipboard(extra.filename)}>
-                    <CircleIconButton title={$t('copy_file_path')} icon={mdiContentCopy} size="18" />
+                  <td onclick={() => copyToClipboard(extra.filename)}>
+                    <CircleIconButton title={$t('copy_file_path')} icon={mdiContentCopy} size="18" onclick={() => {}} />
                   </td>
                   <td class="w-full text-md text-ellipsis flex justify-between pr-5">
                     <span class="text-ellipsis grow truncate font-mono text-sm pr-5" title={extra.filename}
diff --git a/web/src/routes/admin/server-status/+page.svelte b/web/src/routes/admin/server-status/+page.svelte
index 54f62b3adb..0aa4c3dd69 100644
--- a/web/src/routes/admin/server-status/+page.svelte
+++ b/web/src/routes/admin/server-status/+page.svelte
@@ -6,7 +6,11 @@
   import type { PageData } from './$types';
   import { asyncTimeout } from '$lib/utils';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
+
+  let { data = $bindable() }: Props = $props();
 
   let running = true;
 
diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte
index d03865cb39..6a71245051 100644
--- a/web/src/routes/admin/system-settings/+page.svelte
+++ b/web/src/routes/admin/system-settings/+page.svelte
@@ -1,6 +1,7 @@
 <script lang="ts">
   import AdminSettings from '$lib/components/admin-page/settings/admin-settings.svelte';
   import AuthSettings from '$lib/components/admin-page/settings/auth/auth-settings.svelte';
+  import BackupSettings from '$lib/components/admin-page/settings/backup-settings/backup-settings.svelte';
   import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte';
   import ImageSettings from '$lib/components/admin-page/settings/image/image-settings.svelte';
   import JobSettings from '$lib/components/admin-page/settings/job-settings/job-settings.svelte';
@@ -26,22 +27,60 @@
   import { featureFlags } from '$lib/stores/server-config.store';
   import { copyToClipboard } from '$lib/utils';
   import { downloadBlob } from '$lib/utils/asset-utils';
-  import type { SystemConfigDto } from '@immich/sdk';
-  import { mdiAlert, mdiContentCopy, mdiDownload, mdiUpload } from '@mdi/js';
+  import {
+    mdiAccountOutline,
+    mdiAlert,
+    mdiBackupRestore,
+    mdiBellOutline,
+    mdiBookshelf,
+    mdiContentCopy,
+    mdiDatabaseOutline,
+    mdiDownload,
+    mdiFileDocumentOutline,
+    mdiFolderOutline,
+    mdiImageOutline,
+    mdiLockOutline,
+    mdiMapMarkerOutline,
+    mdiPaletteOutline,
+    mdiRobotOutline,
+    mdiServerOutline,
+    mdiSync,
+    mdiTrashCanOutline,
+    mdiUpdate,
+    mdiUpload,
+    mdiVideoOutline,
+  } from '@mdi/js';
   import type { PageData } from './$types';
   import { t } from 'svelte-i18n';
-  import type { ComponentType, SvelteComponent } from 'svelte';
+  import type { Component } from 'svelte';
   import type { SettingsComponentProps } from '$lib/components/admin-page/settings/admin-settings';
+  import SearchBar from '$lib/components/elements/search-bar.svelte';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
 
-  let config = data.configs;
-  let handleSave: (update: Partial<SystemConfigDto>) => Promise<void>;
+  let { data }: Props = $props();
 
-  type SettingsComponent = ComponentType<SvelteComponent<SettingsComponentProps>>;
+  let config = $state(data.configs);
+  let adminSettingElement = $state<ReturnType<typeof AdminSettings>>();
+
+  type SettingsComponent = Component<SettingsComponentProps>;
+
+  // https://stackoverflow.com/questions/16167581/sort-object-properties-and-json-stringify/43636793#43636793
+  const jsonReplacer = (key: string, value: unknown) =>
+    value instanceof Object && !Array.isArray(value)
+      ? Object.keys(value)
+          .sort()
+          // eslint-disable-next-line unicorn/no-array-reduce
+          .reduce((sorted: { [key: string]: unknown }, key) => {
+            sorted[key] = (value as { [key: string]: unknown })[key];
+            return sorted;
+          }, {})
+      : value;
 
   const downloadConfig = () => {
-    const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
+    const blob = new Blob([JSON.stringify(config, jsonReplacer, 2)], { type: 'application/json' });
     const downloadKey = 'immich-config.json';
     downloadManager.add(downloadKey, blob.size);
     downloadManager.update(downloadKey, blob.size);
@@ -49,7 +88,8 @@
     setTimeout(() => downloadManager.clear(downloadKey), 5000);
   };
 
-  let inputElement: HTMLInputElement;
+  let inputElement: HTMLInputElement | undefined = $state();
+
   const uploadConfig = (e: Event) => {
     const file = (e.target as HTMLInputElement).files?.[0];
     if (!file) {
@@ -58,7 +98,7 @@
     const reader = async () => {
       const text = await file.text();
       const newConfig = JSON.parse(text);
-      await handleSave(newConfig);
+      await adminSettingElement?.handleSave(newConfig);
     };
     reader().catch((error) => console.error('Error handling JSON config upload', error));
   };
@@ -68,107 +108,140 @@
     title: string;
     subtitle: string;
     key: string;
+    icon: string;
   }> = [
     {
       component: AuthSettings,
       title: $t('admin.authentication_settings'),
       subtitle: $t('admin.authentication_settings_description'),
-      key: 'image',
+      key: 'authentication',
+      icon: mdiLockOutline,
+    },
+    {
+      component: BackupSettings,
+      title: $t('admin.backup_settings'),
+      subtitle: $t('admin.backup_settings_description'),
+      key: 'backup',
+      icon: mdiBackupRestore,
     },
     {
       component: ImageSettings,
       title: $t('admin.image_settings'),
       subtitle: $t('admin.image_settings_description'),
       key: 'image',
+      icon: mdiImageOutline,
     },
     {
       component: JobSettings,
       title: $t('admin.job_settings'),
       subtitle: $t('admin.job_settings_description'),
       key: 'job',
+      icon: mdiSync,
     },
     {
       component: MetadataSettings,
       title: $t('admin.metadata_settings'),
       subtitle: $t('admin.metadata_settings_description'),
       key: 'metadata',
+      icon: mdiDatabaseOutline,
     },
     {
       component: LibrarySettings,
       title: $t('admin.library_settings'),
       subtitle: $t('admin.library_settings_description'),
       key: 'external-library',
+      icon: mdiBookshelf,
     },
     {
       component: LoggingSettings,
       title: $t('admin.logging_settings'),
       subtitle: $t('admin.manage_log_settings'),
       key: 'logging',
+      icon: mdiFileDocumentOutline,
     },
     {
       component: MachineLearningSettings,
       title: $t('admin.machine_learning_settings'),
       subtitle: $t('admin.machine_learning_settings_description'),
       key: 'machine-learning',
+      icon: mdiRobotOutline,
     },
     {
       component: MapSettings,
       title: $t('admin.map_gps_settings'),
       subtitle: $t('admin.map_gps_settings_description'),
       key: 'location',
+      icon: mdiMapMarkerOutline,
     },
     {
       component: NotificationSettings,
       title: $t('admin.notification_settings'),
       subtitle: $t('admin.notification_settings_description'),
       key: 'notifications',
+      icon: mdiBellOutline,
     },
     {
       component: ServerSettings,
       title: $t('admin.server_settings'),
       subtitle: $t('admin.server_settings_description'),
       key: 'server',
+      icon: mdiServerOutline,
     },
     {
       component: StorageTemplateSettings,
       title: $t('admin.storage_template_settings'),
       subtitle: $t('admin.storage_template_settings_description'),
       key: 'storage-template',
+      icon: mdiFolderOutline,
     },
     {
       component: ThemeSettings,
       title: $t('admin.theme_settings'),
       subtitle: $t('admin.theme_settings_description'),
       key: 'theme',
+      icon: mdiPaletteOutline,
     },
     {
       component: TrashSettings,
       title: $t('admin.trash_settings'),
       subtitle: $t('admin.trash_settings_description'),
       key: 'trash',
+      icon: mdiTrashCanOutline,
     },
     {
       component: UserSettings,
       title: $t('admin.user_settings'),
       subtitle: $t('admin.user_settings_description'),
       key: 'user-settings',
+      icon: mdiAccountOutline,
     },
     {
       component: NewVersionCheckSettings,
       title: $t('admin.version_check_settings'),
       subtitle: $t('admin.version_check_settings_description'),
       key: 'version-check',
+      icon: mdiUpdate,
     },
     {
       component: FFmpegSettings,
       title: $t('admin.transcoding_settings'),
       subtitle: $t('admin.transcoding_settings_description'),
       key: 'video-transcoding',
+      icon: mdiVideoOutline,
     },
   ];
+
+  let searchQuery = $state('');
+
+  let filteredSettings = $derived(
+    settings.filter(({ title, subtitle }) => {
+      const query = searchQuery.toLowerCase();
+      return title.toLowerCase().includes(query) || subtitle.toLowerCase().includes(query);
+    }),
+  );
 </script>
 
-<input bind:this={inputElement} type="file" accept=".json" style="display: none" on:change={uploadConfig} />
+<input bind:this={inputElement} type="file" accept=".json" style="display: none" onchange={uploadConfig} />
 
 <div class="h-svh flex flex-col overflow-hidden">
   {#if $featureFlags.configFile}
@@ -181,48 +254,58 @@
   {/if}
 
   <UserPageLayout title={data.meta.title} admin>
-    <div class="flex justify-end gap-2" slot="buttons">
-      <LinkButton on:click={() => copyToClipboard(JSON.stringify(config, null, 2))}>
-        <div class="flex place-items-center gap-2 text-sm">
-          <Icon path={mdiContentCopy} size="18" />
-          {$t('copy_to_clipboard')}
+    {#snippet buttons()}
+      <div class="flex justify-end gap-2">
+        <div class="hidden lg:block">
+          <SearchBar placeholder={$t('search_settings')} bind:name={searchQuery} showLoadingSpinner={false} />
         </div>
-      </LinkButton>
-      <LinkButton on:click={() => downloadConfig()}>
-        <div class="flex place-items-center gap-2 text-sm">
-          <Icon path={mdiDownload} size="18" />
-          {$t('export_as_json')}
-        </div>
-      </LinkButton>
-      {#if !$featureFlags.configFile}
-        <LinkButton on:click={() => inputElement?.click()}>
+        <LinkButton onclick={() => copyToClipboard(JSON.stringify(config, jsonReplacer, 2))}>
           <div class="flex place-items-center gap-2 text-sm">
-            <Icon path={mdiUpload} size="18" />
-            {$t('import_from_json')}
+            <Icon path={mdiContentCopy} size="18" />
+            {$t('copy_to_clipboard')}
           </div>
         </LinkButton>
-      {/if}
-    </div>
+        <LinkButton onclick={() => downloadConfig()}>
+          <div class="flex place-items-center gap-2 text-sm">
+            <Icon path={mdiDownload} size="18" />
+            {$t('export_as_json')}
+          </div>
+        </LinkButton>
+        {#if !$featureFlags.configFile}
+          <LinkButton onclick={() => inputElement?.click()}>
+            <div class="flex place-items-center gap-2 text-sm">
+              <Icon path={mdiUpload} size="18" />
+              {$t('import_from_json')}
+            </div>
+          </LinkButton>
+        {/if}
+      </div>
+    {/snippet}
 
-    <AdminSettings bind:config let:handleReset bind:handleSave let:savedConfig let:defaultConfig>
-      <section id="setting-content" class="flex place-content-center sm:mx-4">
-        <section class="w-full pb-28 sm:w-5/6 md:w-[850px]">
-          <SettingAccordionState queryParam={QueryParameter.IS_OPEN}>
-            {#each settings as { component: Component, title, subtitle, key }}
-              <SettingAccordion {title} {subtitle} {key}>
-                <Component
-                  onSave={(config) => handleSave(config)}
-                  onReset={(options) => handleReset(options)}
-                  disabled={$featureFlags.configFile}
-                  {defaultConfig}
-                  {config}
-                  {savedConfig}
-                />
-              </SettingAccordion>
-            {/each}
-          </SettingAccordionState>
+    <AdminSettings bind:config bind:this={adminSettingElement}>
+      {#snippet children({ savedConfig, defaultConfig })}
+        <section id="setting-content" class="flex place-content-center sm:mx-4">
+          <section class="w-full pb-28 sm:w-5/6 md:w-[896px]">
+            <div class="block lg:hidden">
+              <SearchBar placeholder={$t('search_settings')} bind:name={searchQuery} showLoadingSpinner={false} />
+            </div>
+            <SettingAccordionState queryParam={QueryParameter.IS_OPEN}>
+              {#each filteredSettings as { component: Component, title, subtitle, key, icon } (key)}
+                <SettingAccordion {title} {subtitle} {key} {icon}>
+                  <Component
+                    onSave={(config) => adminSettingElement?.handleSave(config)}
+                    onReset={(options) => adminSettingElement?.handleReset(options)}
+                    disabled={$featureFlags.configFile}
+                    bind:config
+                    {defaultConfig}
+                    {savedConfig}
+                  />
+                </SettingAccordion>
+              {/each}
+            </SettingAccordionState>
+          </section>
         </section>
-      </section>
+      {/snippet}
     </AdminSettings>
   </UserPageLayout>
 </div>
diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte
index 6028a7dae8..d93a8a5731 100644
--- a/web/src/routes/admin/user-management/+page.svelte
+++ b/web/src/routes/admin/user-management/+page.svelte
@@ -1,6 +1,5 @@
 <script lang="ts">
   import { page } from '$app/stores';
-  import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
   import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialogue.svelte';
   import RestoreDialogue from '$lib/components/admin-page/restore-dialogue.svelte';
   import Button from '$lib/components/elements/buttons/button.svelte';
@@ -10,33 +9,38 @@
   import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
   import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
+  import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
   import {
     NotificationType,
     notificationController,
   } from '$lib/components/shared-components/notification/notification';
   import { locale } from '$lib/stores/preferences.store';
-  import { serverConfig } from '$lib/stores/server-config.store';
+  import { serverConfig, featureFlags } from '$lib/stores/server-config.store';
   import { user } from '$lib/stores/user.store';
   import { websocketEvents } from '$lib/stores/websocket';
   import { copyToClipboard } from '$lib/utils';
   import { getByteUnitString } from '$lib/utils/byte-units';
   import { UserStatus, searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk';
-  import { mdiInfinity, mdiContentCopy, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
+  import { mdiContentCopy, mdiDeleteRestore, mdiInfinity, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
   import { DateTime } from 'luxon';
   import { onMount } from 'svelte';
-  import type { PageData } from './$types';
   import { t } from 'svelte-i18n';
+  import type { PageData } from './$types';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
 
-  let allUsers: UserAdminResponseDto[] = [];
-  let shouldShowEditUserForm = false;
-  let shouldShowCreateUserForm = false;
-  let shouldShowPasswordResetSuccess = false;
-  let shouldShowDeleteConfirmDialog = false;
-  let shouldShowRestoreDialog = false;
-  let selectedUser: UserAdminResponseDto;
-  let newPassword: string;
+  let { data }: Props = $props();
+
+  let allUsers: UserAdminResponseDto[] = $state([]);
+  let shouldShowEditUserForm = $state(false);
+  let shouldShowCreateUserForm = $state(false);
+  let shouldShowPasswordResetSuccess = $state(false);
+  let shouldShowDeleteConfirmDialog = $state(false);
+  let shouldShowRestoreDialog = $state(false);
+  let selectedUser = $state<UserAdminResponseDto>();
+  let newPassword = $state('');
 
   const refresh = async () => {
     allUsers = await searchUsersAdmin({ withDeleted: true });
@@ -110,38 +114,39 @@
     <section class="w-full pb-28 lg:w-[850px]">
       {#if shouldShowCreateUserForm}
         <CreateUserForm
-          on:submit={onUserCreated}
-          on:cancel={() => (shouldShowCreateUserForm = false)}
+          onSubmit={onUserCreated}
+          onCancel={() => (shouldShowCreateUserForm = false)}
           onClose={() => (shouldShowCreateUserForm = false)}
+          oauthEnabled={$featureFlags.oauth}
         />
       {/if}
 
-      {#if shouldShowEditUserForm}
+      {#if shouldShowEditUserForm && selectedUser}
         <EditUserForm
           user={selectedUser}
           bind:newPassword
           canResetPassword={selectedUser?.id !== $user.id}
-          on:editSuccess={onEditUserSuccess}
-          on:resetPasswordSuccess={onEditPasswordSuccess}
+          onEditSuccess={onEditUserSuccess}
+          onResetPasswordSuccess={onEditPasswordSuccess}
           onClose={() => (shouldShowEditUserForm = false)}
         />
       {/if}
 
-      {#if shouldShowDeleteConfirmDialog}
+      {#if shouldShowDeleteConfirmDialog && selectedUser}
         <DeleteConfirmDialog
           user={selectedUser}
-          on:success={onUserDelete}
-          on:fail={onUserDelete}
-          on:cancel={() => (shouldShowDeleteConfirmDialog = false)}
+          onSuccess={onUserDelete}
+          onFail={onUserDelete}
+          onCancel={() => (shouldShowDeleteConfirmDialog = false)}
         />
       {/if}
 
-      {#if shouldShowRestoreDialog}
+      {#if shouldShowRestoreDialog && selectedUser}
         <RestoreDialogue
           user={selectedUser}
-          on:success={onUserRestore}
-          on:fail={onUserRestore}
-          on:cancel={() => (shouldShowRestoreDialog = false)}
+          onSuccess={onUserRestore}
+          onFail={onUserRestore}
+          onCancel={() => (shouldShowRestoreDialog = false)}
         />
       {/if}
 
@@ -154,7 +159,7 @@
           hideCancelButton={true}
           confirmColor="green"
         >
-          <svelte:fragment slot="prompt">
+          {#snippet promptSnippet()}
             <div class="flex flex-col gap-4">
               <p>{$t('admin.user_password_has_been_reset')}</p>
 
@@ -164,7 +169,7 @@
                 >
                   {newPassword}
                 </code>
-                <LinkButton on:click={() => copyToClipboard(newPassword)} title={$t('copy_password')}>
+                <LinkButton onclick={() => copyToClipboard(newPassword)} title={$t('copy_password')}>
                   <div class="flex place-items-center gap-2 text-sm">
                     <Icon path={mdiContentCopy} size="18" />
                   </div>
@@ -173,7 +178,7 @@
 
               <p>{$t('admin.user_password_reset_description')}</p>
             </div>
-          </svelte:fragment>
+          {/snippet}
         </ConfirmDialog>
       {/if}
 
@@ -222,7 +227,7 @@
                       title={$t('edit_user')}
                       color="primary"
                       size="16"
-                      on:click={() => editUserHandler(immichUser)}
+                      onclick={() => editUserHandler(immichUser)}
                     />
                     {#if immichUser.id !== $user.id}
                       <CircleIconButton
@@ -230,7 +235,7 @@
                         title={$t('delete_user')}
                         color="primary"
                         size="16"
-                        on:click={() => deleteUserHandler(immichUser)}
+                        onclick={() => deleteUserHandler(immichUser)}
                       />
                     {/if}
                   {/if}
@@ -242,7 +247,7 @@
                       })}
                       color="primary"
                       size="16"
-                      on:click={() => restoreUserHandler(immichUser)}
+                      onclick={() => restoreUserHandler(immichUser)}
                     />
                   {/if}
                 </td>
@@ -252,7 +257,7 @@
         </tbody>
       </table>
 
-      <Button size="sm" on:click={() => (shouldShowCreateUserForm = true)}>{$t('create_user')}</Button>
+      <Button size="sm" onclick={() => (shouldShowCreateUserForm = true)}>{$t('create_user')}</Button>
     </section>
   </section>
 </UserPageLayout>
diff --git a/web/src/routes/auth/change-password/+page.svelte b/web/src/routes/auth/change-password/+page.svelte
index aa23e4e7d2..21226b9387 100644
--- a/web/src/routes/auth/change-password/+page.svelte
+++ b/web/src/routes/auth/change-password/+page.svelte
@@ -8,7 +8,11 @@
   import type { PageData } from './$types';
   import { t } from 'svelte-i18n';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
+
+  let { data }: Props = $props();
 
   const onSuccess = async () => {
     await goto(AppRoute.AUTH_LOGIN);
@@ -18,12 +22,14 @@
 </script>
 
 <FullscreenContainer title={data.meta.title}>
-  <p slot="message">
-    {$t('hi_user', { values: { name: $user.name, email: $user.email } })}
-    <br />
-    <br />
-    {$t('change_password_description')}
-  </p>
+  {#snippet message()}
+    <p>
+      {$t('hi_user', { values: { name: $user.name, email: $user.email } })}
+      <br />
+      <br />
+      {$t('change_password_description')}
+    </p>
+  {/snippet}
 
-  <ChangePasswordForm on:success={onSuccess} />
+  <ChangePasswordForm {onSuccess} />
 </FullscreenContainer>
diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte
index dd0f64c5a8..0ab506f5e3 100644
--- a/web/src/routes/auth/login/+page.svelte
+++ b/web/src/routes/auth/login/+page.svelte
@@ -6,15 +6,21 @@
   import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
   import type { PageData } from './$types';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
+
+  let { data }: Props = $props();
 </script>
 
 {#if $featureFlags.loaded}
   <FullscreenContainer title={data.meta.title} showMessage={!!$serverConfig.loginPageMessage}>
-    <p slot="message">
-      <!-- eslint-disable-next-line svelte/no-at-html-tags -->
-      {@html $serverConfig.loginPageMessage}
-    </p>
+    {#snippet message()}
+      <p>
+        <!-- eslint-disable-next-line svelte/no-at-html-tags -->
+        {@html $serverConfig.loginPageMessage}
+      </p>
+    {/snippet}
 
     <LoginForm
       onSuccess={async () => await goto(AppRoute.PHOTOS, { invalidateAll: true })}
diff --git a/web/src/routes/auth/onboarding/+page.svelte b/web/src/routes/auth/onboarding/+page.svelte
index ddb30d1b45..3f31dd587a 100644
--- a/web/src/routes/auth/onboarding/+page.svelte
+++ b/web/src/routes/auth/onboarding/+page.svelte
@@ -1,4 +1,6 @@
 <script lang="ts">
+  import { run } from 'svelte/legacy';
+
   import { goto } from '$app/navigation';
   import { page } from '$app/stores';
   import OnboardingHello from '$lib/components/onboarding-page/onboarding-hello.svelte';
@@ -9,7 +11,7 @@
   import { retrieveServerConfig } from '$lib/stores/server-config.store';
   import { updateAdminOnboarding } from '@immich/sdk';
 
-  let index = 0;
+  let index = $state(0);
 
   interface OnboardingStep {
     name: string;
@@ -27,11 +29,11 @@
     { name: 'storage', component: OnboadingStorageTemplate },
   ];
 
-  $: {
+  run(() => {
     const stepState = $page.url.searchParams.get('step');
     const temporaryIndex = onboardingSteps.findIndex((step) => step.name === stepState);
-    index = temporaryIndex >= 0 ? temporaryIndex : 0;
-  }
+    index = temporaryIndex === -1 ? 0 : temporaryIndex;
+  });
 
   const handleDoneClicked = async () => {
     if (index >= onboardingSteps.length - 1) {
@@ -50,6 +52,8 @@
       await goto(`${AppRoute.AUTH_ONBOARDING}?${QueryParameter.ONBOARDING_STEP}=${onboardingSteps[index].name}`);
     }
   };
+
+  const SvelteComponent = $derived(onboardingSteps[index].component);
 </script>
 
 <section id="onboarding-page" class="min-w-screen flex min-h-screen p-4">
@@ -61,11 +65,7 @@
       ></div>
     </div>
     <div class="w-full min-w-screen py-8 flex h-full place-content-center place-items-center">
-      <svelte:component
-        this={onboardingSteps[index].component}
-        onDone={handleDoneClicked}
-        onPrevious={handlePrevious}
-      />
+      <SvelteComponent onDone={handleDoneClicked} onPrevious={handlePrevious} />
     </div>
   </div>
 </section>
diff --git a/web/src/routes/auth/register/+page.svelte b/web/src/routes/auth/register/+page.svelte
index 9c1f0ca6c4..2e55ba7435 100644
--- a/web/src/routes/auth/register/+page.svelte
+++ b/web/src/routes/auth/register/+page.svelte
@@ -4,13 +4,19 @@
   import type { PageData } from './$types';
   import { t } from 'svelte-i18n';
 
-  export let data: PageData;
+  interface Props {
+    data: PageData;
+  }
+
+  let { data }: Props = $props();
 </script>
 
 <FullscreenContainer title={data.meta.title}>
-  <p slot="message">
-    {$t('admin.registration_description')}
-  </p>
+  {#snippet message()}
+    <p>
+      {$t('admin.registration_description')}
+    </p>
+  {/snippet}
 
   <AdminRegistrationForm />
 </FullscreenContainer>
diff --git a/web/src/test-data/factories/asset-factory.ts b/web/src/test-data/factories/asset-factory.ts
index 700b98c180..656c4143a7 100644
--- a/web/src/test-data/factories/asset-factory.ts
+++ b/web/src/test-data/factories/asset-factory.ts
@@ -18,8 +18,8 @@ export const assetFactory = Sync.makeFactory<AssetResponseDto>({
   localDateTime: Sync.each(() => faker.date.past().toISOString()),
   updatedAt: Sync.each(() => faker.date.past().toISOString()),
   isFavorite: Sync.each(() => faker.datatype.boolean()),
-  isArchived: Sync.each(() => faker.datatype.boolean()),
-  isTrashed: Sync.each(() => faker.datatype.boolean()),
+  isArchived: false,
+  isTrashed: false,
   duration: '0:00:00.00000',
   checksum: Sync.each(() => faker.string.alphanumeric(28)),
   isOffline: Sync.each(() => faker.datatype.boolean()),
diff --git a/web/src/test-data/factories/user-factory.ts b/web/src/test-data/factories/user-factory.ts
index c89dccb2bb..92d1510d40 100644
--- a/web/src/test-data/factories/user-factory.ts
+++ b/web/src/test-data/factories/user-factory.ts
@@ -8,6 +8,7 @@ export const userFactory = Sync.makeFactory<UserResponseDto>({
   name: Sync.each(() => faker.person.fullName()),
   profileImagePath: '',
   avatarColor: UserAvatarColor.Primary,
+  profileChangedAt: Sync.each(() => faker.date.recent().toISOString()),
 });
 
 export const userAdminFactory = Sync.makeFactory<UserAdminResponseDto>({
@@ -31,4 +32,5 @@ export const userAdminFactory = Sync.makeFactory<UserAdminResponseDto>({
     activationKey: 'activation-key',
     activatedAt: new Date().toISOString(),
   },
+  profileChangedAt: Sync.each(() => faker.date.recent().toISOString()),
 });
diff --git a/web/svelte.config.js b/web/svelte.config.js
index a66f798f31..c5c93e5330 100644
--- a/web/svelte.config.js
+++ b/web/svelte.config.js
@@ -19,6 +19,7 @@ const config = {
       $lib: 'src/lib',
       '$lib/*': 'src/lib/*',
       '@test-data': 'src/test-data',
+      $i18n: '../i18n',
     },
   },
 };
diff --git a/web/tsconfig.json b/web/tsconfig.json
index 63e16e7976..31aef23e31 100644
--- a/web/tsconfig.json
+++ b/web/tsconfig.json
@@ -10,7 +10,7 @@
     "skipLibCheck": true,
     "sourceMap": true,
     "strict": true,
-    "target": "es2020",
+    "target": "es2022",
     "types": ["vitest/globals"]
   },
   "extends": "./.svelte-kit/tsconfig.json"
diff --git a/web/vite.config.js b/web/vite.config.js
index 7d15832de4..266312e137 100644
--- a/web/vite.config.js
+++ b/web/vite.config.js
@@ -1,11 +1,12 @@
 import { enhancedImages } from '@sveltejs/enhanced-img';
 import { sveltekit } from '@sveltejs/kit/vite';
+import { svelteTesting } from '@testing-library/svelte/vite';
 import path from 'node:path';
 import { visualizer } from 'rollup-plugin-visualizer';
 import { defineConfig } from 'vite';
 
 const upstream = {
-  target: process.env.IMMICH_SERVER_URL || 'http://immich-server:3001/',
+  target: process.env.IMMICH_SERVER_URL || 'http://immich-server:2283/',
   secure: true,
   changeOrigin: true,
   logLevel: 'info',
@@ -37,6 +38,7 @@ export default defineConfig({
         })
       : undefined,
     enhancedImages(),
+    svelteTesting(),
   ],
   optimizeDeps: {
     entries: ['src/**/*.{svelte,ts,html}'],
@@ -49,6 +51,5 @@ export default defineConfig({
     sequence: {
       hooks: 'list',
     },
-    alias: [{ find: /^svelte$/, replacement: 'svelte/internal' }],
   },
 });